chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,472 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const exportDir = resolve(rootDir, '.tmp/rea-css-recovered')
|
||||
const runtimePath = resolve(rootDir, '.tmp/rea-runtime-scripts.json')
|
||||
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/question-source.css')
|
||||
|
||||
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const runtimeFile = 'src-0FGg1seX.js'
|
||||
const runtimeSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
||||
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
const inlineResetFile = 'inline-90c5e4f0726a5658.css'
|
||||
const inlineResetSha256 = '90c5e4f0726a56583f83dcffecc1fe87b5ad54ec6d0d593008ac033c8fe931c9'
|
||||
const optionRuntimeClassOffset = 1043927
|
||||
|
||||
const sourceSpecs = [
|
||||
{
|
||||
file: 'src-CBaV_A3S.css',
|
||||
sha256: '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c'
|
||||
},
|
||||
{
|
||||
file: 'cb-bridge-jsj-8pWS.css',
|
||||
sha256: '71007d54555800c62d65d6188c743d71481cd9034e0609d3c63bb2626f9d5446'
|
||||
}
|
||||
]
|
||||
|
||||
const classAliases = {
|
||||
questionFloating: 'question-floating',
|
||||
questionFloatingSlideUp: null,
|
||||
header: 'question-floating__header',
|
||||
headerLeft: 'question-floating__heading',
|
||||
title: 'question-floating__title',
|
||||
titleHelp: 'question-floating__hint',
|
||||
headerRight: 'question-floating__header-actions',
|
||||
pagination: 'question-floating__pagination',
|
||||
pageBtn: 'question-floating__page-button',
|
||||
pageInfo: 'question-floating__pagination-info',
|
||||
skipAllBtn: 'question-floating__dismiss',
|
||||
content: 'question-floating__content',
|
||||
options: 'question-floating__options',
|
||||
optionItem: 'question-floating__option',
|
||||
optionArrow: 'question-floating__option-arrow',
|
||||
optionNumber: 'question-floating__option-number',
|
||||
selected: 'question-floating__option--selected',
|
||||
checkbox: 'question-floating__checkbox',
|
||||
disabled: 'question-floating__option--disabled',
|
||||
optionText: 'question-floating__option-copy',
|
||||
optionLabel: 'question-floating__option-label',
|
||||
optionDesc: 'question-floating__option-description',
|
||||
optionHelp: 'question-floating__option-help',
|
||||
customInputRow: 'question-floating__custom',
|
||||
customInputIcon: 'question-floating__custom-icon',
|
||||
focused: 'question-floating__custom--focused',
|
||||
customInput: 'question-floating__custom-input',
|
||||
footer: 'question-floating__footer',
|
||||
footerSingle: 'question-floating__footer--single',
|
||||
selectedCount: 'question-floating__selected-count',
|
||||
footerActions: 'question-floating__footer-actions',
|
||||
skipBtn: 'question-floating__skip',
|
||||
iconBtn: 'question-floating__advance',
|
||||
iconBtnDisabled: 'question-floating__advance--disabled',
|
||||
iconBtnActive: 'question-floating__advance--active',
|
||||
iconBtnSend: 'question-floating__advance--send',
|
||||
confirmVariant: 'question-floating--confirm'
|
||||
}
|
||||
|
||||
function sha256(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function sourceComment({ file, hash, offset, selectors, evidenceId, note }) {
|
||||
return postcss.comment({
|
||||
text: ` REA evidence=${evidenceId}; source=${file}; sha256=${hash}; offset=${offset}; original-selectors=${JSON.stringify(selectors)}${note ? `; ${note}` : ''} `
|
||||
})
|
||||
}
|
||||
|
||||
function isGlobalButtonOrUniversalSelector(selector) {
|
||||
const normalized = selector
|
||||
.replace(/:where\(\s*(button|input|select|textarea)\s*\)/gi, '$1')
|
||||
.replace(/:is\(\s*(button|input|select|textarea)\s*\)/gi, '$1')
|
||||
return normalized === '*' || /^(?:(?:html|body|:root)\s+)*(?:button|input|select|textarea)(?=$|[:[])/i.test(normalized)
|
||||
}
|
||||
|
||||
function getClassMap(source) {
|
||||
const footerOffset = source.indexOf('QuestionFloatingFooter=')
|
||||
if (footerOffset < 0) throw new Error(`Runtime source is missing QuestionFloatingFooter: ${runtimeFile}`)
|
||||
|
||||
const candidates = [...source.matchAll(/cH=\{([^{}]+)\}/g)]
|
||||
.filter(match => match.index < footerOffset && footerOffset - match.index < 12000)
|
||||
const match = candidates.at(-1)
|
||||
if (!match) throw new Error(`Runtime source is missing the QuestionFloating CSS module map: ${runtimeFile}`)
|
||||
const componentOffset = source.indexOf('xH=(0,yH.forwardRef', match.index)
|
||||
if (componentOffset < match.index) {
|
||||
throw new Error(`Runtime source is missing the QuestionFloating component after its CSS module map: ${runtimeFile}`)
|
||||
}
|
||||
|
||||
const bindings = Object.fromEntries(
|
||||
[...match[1].matchAll(/([A-Za-z_$][\w$]*):([A-Za-z_$][\w$]*)/g)]
|
||||
.map(item => [item[1], item[2]])
|
||||
)
|
||||
const classNames = {}
|
||||
for (const [key, variable] of Object.entries(bindings)) {
|
||||
const assignment = variable + '=`'
|
||||
const position = source.lastIndexOf(assignment, match.index)
|
||||
if (position < 0 || /[\w$]/.test(source[position - 1] || '')) {
|
||||
throw new Error(`Runtime source is missing the CSS module binding ${variable} for ${key}`)
|
||||
}
|
||||
const start = position + assignment.length
|
||||
const end = source.indexOf('`', start)
|
||||
if (end < 0) throw new Error(`Runtime source has an unterminated CSS module binding for ${key}`)
|
||||
classNames[key] = source.slice(start, end)
|
||||
}
|
||||
|
||||
const missingAliases = Object.keys(bindings).filter(key => !(key in classAliases))
|
||||
const extraAliases = Object.keys(classAliases).filter(key => !(key in bindings))
|
||||
if (missingAliases.length || extraAliases.length) {
|
||||
throw new Error(`QuestionFloating module map changed: missing aliases [${missingAliases.join(', ')}], stale aliases [${extraAliases.join(', ')}]`)
|
||||
}
|
||||
|
||||
const rootClass = classNames.questionFloating
|
||||
const moduleId = /_([A-Za-z0-9]+)_\d+$/.exec(rootClass)?.[1]
|
||||
if (!moduleId || !rootClass.startsWith('_questionFloating_')) {
|
||||
throw new Error(`Unexpected QuestionFloating module class: ${rootClass}`)
|
||||
}
|
||||
for (const [key, className] of Object.entries(classNames)) {
|
||||
if (!className.includes(`_${moduleId}_`)) {
|
||||
throw new Error(`QuestionFloating class ${key} is outside CSS module ${moduleId}: ${className}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { componentOffset, footerOffset, moduleMapOffset: match.index, moduleId, classNames }
|
||||
}
|
||||
|
||||
function classPattern(className) {
|
||||
return new RegExp(`\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`)
|
||||
}
|
||||
|
||||
function rewriteSelector(selector, classNames) {
|
||||
let rewritten = selector
|
||||
for (const [key, sourceClass] of Object.entries(classNames)) {
|
||||
const alias = classAliases[key]
|
||||
if (!alias) continue
|
||||
rewritten = rewritten.replace(
|
||||
new RegExp(`\\.${sourceClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`, 'g'),
|
||||
`.${alias}`
|
||||
)
|
||||
}
|
||||
return `.chat-pane.cr-theme ${rewritten}`
|
||||
}
|
||||
|
||||
function projectModuleNode(node, source, classNames, selectedRules) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = postcss.list.comma(node.selector).map(selector => selector.trim())
|
||||
const selected = selectors.filter(selector => Object.entries(classNames).some(([key, className]) => (
|
||||
classAliases[key] && classPattern(className).test(selector)
|
||||
)))
|
||||
if (selected.length === 0) return []
|
||||
|
||||
const projected = selected.map(selector => rewriteSelector(selector, classNames))
|
||||
selectedRules.push({ file: source.file, originalSelectors: selected, offset: node.source?.start?.offset ?? 0 })
|
||||
return [
|
||||
sourceComment({
|
||||
file: source.file,
|
||||
hash: source.sha256,
|
||||
offset: node.source?.start?.offset ?? 0,
|
||||
selectors: selected,
|
||||
evidenceId: cssEvidenceId,
|
||||
note: `runtime stylesheet ASAR comparison=${source.asarComparison}`
|
||||
}),
|
||||
node.clone({ selector: projected.join(',') })
|
||||
]
|
||||
}
|
||||
|
||||
if (node.type !== 'atrule' || !node.nodes) return []
|
||||
const children = node.nodes.flatMap(child => projectModuleNode(child, source, classNames, selectedRules))
|
||||
if (children.length === 0) return []
|
||||
const projected = node.clone()
|
||||
projected.removeAll()
|
||||
projected.append(children)
|
||||
return [projected]
|
||||
}
|
||||
|
||||
function projectThemeTokens(root, source, output, records) {
|
||||
root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
const isDark = selectors.some(selector => /(?:^|[.:])(?:dark|cb-dark)(?:$|[.: ])/.test(selector))
|
||||
const isLight = selectors.some(selector => /(?:^|[.:])(?:light|cb-light)(?:$|[.: ])/.test(selector))
|
||||
const theme = isDark ? 'dark' : isLight ? 'light' : null
|
||||
if (!theme) return
|
||||
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--cb-question-floating-'))
|
||||
if (declarations.length === 0) return
|
||||
const selector = `.chat-pane.cr-theme[data-cr-theme=${theme}]`
|
||||
const offset = rule.source?.start?.offset ?? 0
|
||||
records.push({ file: source.file, originalSelectors: selectors, offset, projectedSelector: selector })
|
||||
output.append(sourceComment({ file: source.file, hash: source.sha256, offset, selectors, evidenceId: cssEvidenceId, note: `theme token projection=${theme}` }))
|
||||
output.append(postcss.rule({ selector, nodes: declarations.map(node => node.clone()) }))
|
||||
})
|
||||
}
|
||||
|
||||
function projectBridgeTokens(root, source, output, records) {
|
||||
root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--cb-question-floating-'))
|
||||
if (declarations.length === 0) return
|
||||
|
||||
let projectedSelector
|
||||
if (selectors.includes(':root') || selectors.some(selector => selector.startsWith(':root.'))) {
|
||||
projectedSelector = '.chat-pane.cr-theme'
|
||||
} else if (selectors.some(selector => selector.includes('[class*=questionFloating]'))) {
|
||||
projectedSelector = '.chat-pane.cr-theme .question-floating'
|
||||
} else if (selectors.some(selector => selector.includes('[class*=skipBtn]'))) {
|
||||
projectedSelector = '.chat-pane.cr-theme .question-floating__skip'
|
||||
}
|
||||
if (!projectedSelector) return
|
||||
|
||||
const offset = rule.source?.start?.offset ?? 0
|
||||
records.push({ file: source.file, originalSelectors: selectors, offset, projectedSelector })
|
||||
output.append(sourceComment({ file: source.file, hash: source.sha256, offset, selectors, evidenceId: cssEvidenceId, note: `theme bridge projection=${projectedSelector}` }))
|
||||
output.append(postcss.rule({ selector: projectedSelector, nodes: declarations.map(node => node.clone()) }))
|
||||
})
|
||||
}
|
||||
|
||||
function formatContainer(container, depth) {
|
||||
if (!container.nodes) return
|
||||
container.raws.semicolon = true
|
||||
for (const child of container.nodes) {
|
||||
child.raws.before = `\n${' '.repeat(Math.max(depth + 1, 0))}`
|
||||
if (child.type === 'decl') {
|
||||
child.raws.between = ': '
|
||||
continue
|
||||
}
|
||||
if (child.nodes) {
|
||||
child.raws.between = ' '
|
||||
formatContainer(child, depth + 1)
|
||||
}
|
||||
}
|
||||
container.raws.after = `\n${' '.repeat(Math.max(depth, 0))}`
|
||||
}
|
||||
|
||||
const runtimeBytes = await readFile(runtimePath)
|
||||
const runtimeExport = JSON.parse(runtimeBytes.toString('utf8'))
|
||||
if (runtimeExport.evidence_id !== runtimeEvidenceId) {
|
||||
throw new Error(`REA runtime Evidence ID changed: expected ${runtimeEvidenceId}, found ${runtimeExport.evidence_id ?? 'none'}`)
|
||||
}
|
||||
const runtimeItem = runtimeExport.items.find(item => item.file_path?.replaceAll('\\', '/').endsWith(`/assets/${runtimeFile}`))
|
||||
if (!runtimeItem?.source?.artifact?.text) throw new Error(`REA runtime script is missing: ${runtimeFile}`)
|
||||
const runtimeSource = runtimeItem.source.artifact.text
|
||||
const actualRuntimeSha = sha256(Buffer.from(runtimeSource, 'utf8'))
|
||||
if (runtimeItem.source.artifact.sha256 !== runtimeSha256 || actualRuntimeSha !== runtimeSha256) {
|
||||
throw new Error(`REA runtime script digest mismatch for ${runtimeFile}: expected ${runtimeSha256}, export ${runtimeItem.source.artifact.sha256 ?? 'none'}, actual ${actualRuntimeSha}`)
|
||||
}
|
||||
|
||||
const { componentOffset, footerOffset, moduleMapOffset, moduleId, classNames } = getClassMap(runtimeSource)
|
||||
const cssManifest = JSON.parse(await readFile(resolve(exportDir, 'manifest.json'), 'utf8'))
|
||||
if (cssManifest.sourceEvidenceId !== cssEvidenceId) {
|
||||
throw new Error(`REA CSS Evidence ID changed: expected ${cssEvidenceId}, found ${cssManifest.sourceEvidenceId ?? 'none'}`)
|
||||
}
|
||||
|
||||
const globalResetRules = []
|
||||
const globalButtonTextAlignRules = []
|
||||
for (const record of cssManifest.files ?? []) {
|
||||
const bytes = await readFile(resolve(exportDir, record.file))
|
||||
const actualHash = sha256(bytes)
|
||||
if (!record.sha256 || actualHash !== record.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch during global reset scan for ${record.file}: manifest ${record.sha256 ?? 'none'}, actual ${actualHash}`)
|
||||
}
|
||||
const root = postcss.parse(bytes.toString('utf8'), { from: record.file })
|
||||
root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
const relevantSelectors = selectors.filter(isGlobalButtonOrUniversalSelector)
|
||||
if (relevantSelectors.length === 0) return
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl')
|
||||
const textAlign = declarations.filter(node => node.prop.toLowerCase() === 'text-align')
|
||||
if (relevantSelectors.includes('*')) {
|
||||
globalResetRules.push({
|
||||
file: record.file,
|
||||
sha256: actualHash,
|
||||
originalSelectors: selectors,
|
||||
offset: rule.source?.start?.offset ?? 0,
|
||||
declarations: declarations.map(node => node.clone())
|
||||
})
|
||||
}
|
||||
if (textAlign.length) {
|
||||
globalButtonTextAlignRules.push({
|
||||
file: record.file,
|
||||
sha256: actualHash,
|
||||
originalSelectors: selectors,
|
||||
relevantSelectors,
|
||||
offset: rule.source?.start?.offset ?? 0,
|
||||
declarations: textAlign.map(node => ({ property: node.prop, value: node.value }))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sources = new Map()
|
||||
for (const spec of sourceSpecs) {
|
||||
const bytes = await readFile(resolve(exportDir, spec.file))
|
||||
const manifestRecord = cssManifest.files?.find(record => record.file === spec.file)
|
||||
const actualHash = sha256(bytes)
|
||||
if (!manifestRecord?.sha256 || manifestRecord.sha256 !== spec.sha256 || actualHash !== spec.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch for ${spec.file}: expected ${spec.sha256}, manifest ${manifestRecord?.sha256 ?? 'none'}, actual ${actualHash}`)
|
||||
}
|
||||
sources.set(spec.file, {
|
||||
file: spec.file,
|
||||
sha256: actualHash,
|
||||
asarComparison: manifestRecord.asarComparison?.status ?? 'not-compared',
|
||||
root: postcss.parse(bytes.toString('utf8'), { from: spec.file })
|
||||
})
|
||||
}
|
||||
|
||||
const componentCss = sources.get('src-CBaV_A3S.css')
|
||||
const bridgeCss = sources.get('cb-bridge-jsj-8pWS.css')
|
||||
const optionClassName = classNames.optionItem
|
||||
const optionCssRules = []
|
||||
componentCss.root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
if (selectors.includes(`.${optionClassName}`)) {
|
||||
optionCssRules.push({
|
||||
originalSelectors: selectors,
|
||||
offset: rule.source?.start?.offset ?? 0,
|
||||
textAlign: rule.nodes.filter(node => node.type === 'decl' && node.prop.toLowerCase() === 'text-align').map(node => node.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
if (optionCssRules.length !== 1 || optionCssRules[0].offset !== 650985 || optionCssRules[0].textAlign.length > 0) {
|
||||
throw new Error(`QuestionFloating option CSS evidence changed: ${JSON.stringify(optionCssRules)}`)
|
||||
}
|
||||
|
||||
const inlineResetRule = globalResetRules.find(record => record.file === inlineResetFile && record.originalSelectors.includes('*'))
|
||||
if (!inlineResetRule || inlineResetRule.sha256 !== inlineResetSha256 || inlineResetRule.offset !== 5) {
|
||||
throw new Error(`REA global reset evidence changed: ${JSON.stringify(inlineResetRule ?? null)}`)
|
||||
}
|
||||
if (!inlineResetRule.declarations.some(node => node.prop === 'box-sizing' && node.value === 'border-box')) {
|
||||
throw new Error('REA global universal reset no longer declares box-sizing: border-box')
|
||||
}
|
||||
|
||||
const optionClassOffset = runtimeSource.indexOf('cH.optionItem')
|
||||
if (optionClassOffset !== optionRuntimeClassOffset) {
|
||||
throw new Error(`QuestionFloating option JSX offset changed: expected ${optionRuntimeClassOffset}, found ${optionClassOffset}`)
|
||||
}
|
||||
const sourceOptionStart = runtimeSource.lastIndexOf('(0,bH.jsxs)(`div`,{', optionClassOffset)
|
||||
const sourceOptionSnippet = runtimeSource.slice(sourceOptionStart, optionClassOffset + 700)
|
||||
if (sourceOptionStart < 0 || !sourceOptionSnippet.includes('role:`button`') || !sourceOptionSnippet.includes('tabIndex:O?-1:0') || !sourceOptionSnippet.includes('"aria-disabled":O||void 0') || !sourceOptionSnippet.includes('onKeyDown:')) {
|
||||
throw new Error('REA QuestionFloating option no longer uses the verified div/button keyboard structure')
|
||||
}
|
||||
|
||||
const output = postcss.root()
|
||||
const optionUniversalResetRules = globalResetRules.filter(record => record.originalSelectors.includes('*'))
|
||||
for (const reset of optionUniversalResetRules) {
|
||||
output.append(sourceComment({
|
||||
file: reset.file,
|
||||
hash: reset.sha256,
|
||||
offset: reset.offset,
|
||||
selectors: reset.originalSelectors,
|
||||
evidenceId: cssEvidenceId,
|
||||
note: 'global universal reset projection=.chat-pane.cr-theme .question-floating__option'
|
||||
}))
|
||||
output.append(postcss.rule({
|
||||
selector: '.chat-pane.cr-theme .question-floating__option',
|
||||
nodes: reset.declarations.map(node => node.clone())
|
||||
}))
|
||||
}
|
||||
const moduleRules = []
|
||||
for (const node of componentCss.root.nodes) {
|
||||
output.append(...projectModuleNode(node, componentCss, classNames, moduleRules))
|
||||
}
|
||||
|
||||
const sourceModuleClasses = Object.entries(classNames)
|
||||
.filter(([key]) => classAliases[key])
|
||||
.map(([, className]) => className)
|
||||
const missingRules = sourceModuleClasses.filter(className => !moduleRules.some(record => record.originalSelectors.some(selector => classPattern(className).test(selector))))
|
||||
if (missingRules.length) throw new Error(`REA CSS is missing QuestionFloating module rules for: ${missingRules.join(', ')}`)
|
||||
|
||||
const animations = []
|
||||
output.walkDecls(/^animation(?:-name)?$/, declaration => {
|
||||
animations.push(...declaration.value.split(/[\s,]+/))
|
||||
})
|
||||
let projectedAnimationRuleCount = 0
|
||||
componentCss.root.walkAtRules(/keyframes$/, atRule => {
|
||||
if (!animations.includes(atRule.params)) return
|
||||
const offset = atRule.source?.start?.offset ?? 0
|
||||
output.append(sourceComment({
|
||||
file: componentCss.file,
|
||||
hash: componentCss.sha256,
|
||||
offset,
|
||||
selectors: [`@${atRule.name} ${atRule.params}`],
|
||||
evidenceId: cssEvidenceId,
|
||||
note: `runtime stylesheet ASAR comparison=${componentCss.asarComparison}`
|
||||
}))
|
||||
output.append(atRule.clone())
|
||||
projectedAnimationRuleCount += 1
|
||||
})
|
||||
|
||||
const themeRules = []
|
||||
projectThemeTokens(componentCss.root, componentCss, output, themeRules)
|
||||
const bridgeRules = []
|
||||
projectBridgeTokens(bridgeCss.root, bridgeCss, output, bridgeRules)
|
||||
|
||||
const header = postcss.comment({
|
||||
text: ` Generated by scripts/rea/recover-question-styles.mjs. Runtime component evidence=${runtimeEvidenceId}; source=${runtimeFile}; sha256=${runtimeSha256}; QuestionFloating offset=${componentOffset}; CSS module map offset=${moduleMapOffset}; footer offset=${footerOffset}; module=${moduleId}. CSS evidence=${cssEvidenceId}; ${componentCss.file} sha256=${componentCss.sha256}; ASAR comparison=${componentCss.asarComparison}; ${bridgeCss.file} sha256=${bridgeCss.sha256}; global reset=${inlineResetFile}@${inlineResetRule.offset}; global button text-align matches=${globalButtonTextAlignRules.length}. Selectors are scoped to .chat-pane.cr-theme and translated from the runtime CSS module map. `
|
||||
})
|
||||
output.prepend(header)
|
||||
formatContainer(output, -1)
|
||||
|
||||
await writeFile(outputPath, output.toString() + '\n')
|
||||
const localComponent = await readFile(resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionFloating.tsx'), 'utf8')
|
||||
const localOptionClassOffset = localComponent.indexOf("'question-floating__option'")
|
||||
const localOptionStart = localComponent.lastIndexOf('<div', localOptionClassOffset)
|
||||
const localOptionSnippet = localComponent.slice(localOptionStart, localOptionClassOffset + 900)
|
||||
if (localOptionStart < 0 || !localOptionSnippet.includes('role="button"') || !localOptionSnippet.includes('tabIndex=') || !localOptionSnippet.includes('aria-disabled=') || !localOptionSnippet.includes('onKeyDown=')) {
|
||||
throw new Error('Local QuestionFloating option must preserve the recovered div/button keyboard structure')
|
||||
}
|
||||
const localClasses = [...new Set([...localComponent.matchAll(/['"](question-floating(?:__[\w-]+|--[\w-]+)?)['"]/g)].map(match => match[1]))]
|
||||
const mappedClasses = new Set(Object.values(classAliases).filter(Boolean))
|
||||
const unmappedLocalClasses = localClasses.filter(className => !mappedClasses.has(className))
|
||||
const projectedRuleCount = moduleRules.length + themeRules.length + bridgeRules.length + projectedAnimationRuleCount + optionUniversalResetRules.length
|
||||
console.log(JSON.stringify({
|
||||
runtimeEvidenceId,
|
||||
runtimeFile,
|
||||
runtimeSha256,
|
||||
componentOffset,
|
||||
moduleMapOffset,
|
||||
footerOffset,
|
||||
cssEvidenceId,
|
||||
moduleId,
|
||||
projectedRuleCount,
|
||||
projectedModuleRuleCount: moduleRules.length,
|
||||
projectedThemeTokenRuleCount: themeRules.length,
|
||||
projectedBridgeTokenRuleCount: bridgeRules.length,
|
||||
projectedAnimationRuleCount,
|
||||
projectedUniversalResetRuleCount: optionUniversalResetRules.length,
|
||||
globalButtonTextAlignRules,
|
||||
globalResetSource: {
|
||||
evidenceId: cssEvidenceId,
|
||||
file: inlineResetFile,
|
||||
sha256: inlineResetSha256,
|
||||
originalSelectors: inlineResetRule.originalSelectors,
|
||||
offset: inlineResetRule.offset,
|
||||
declarations: inlineResetRule.declarations.map(node => ({ property: node.prop, value: node.value }))
|
||||
},
|
||||
optionSource: {
|
||||
runtimeEvidenceId,
|
||||
file: runtimeFile,
|
||||
sha256: runtimeSha256,
|
||||
jsxClassOffset: optionClassOffset,
|
||||
element: 'div',
|
||||
role: 'button',
|
||||
cssEvidenceId,
|
||||
cssFile: componentCss.file,
|
||||
cssSha256: componentCss.sha256,
|
||||
cssOriginalSelectors: optionCssRules[0].originalSelectors,
|
||||
cssOffset: optionCssRules[0].offset,
|
||||
cssTextAlignDeclarations: optionCssRules[0].textAlign,
|
||||
localElement: 'div',
|
||||
localRole: 'button',
|
||||
localClassOffset: localOptionClassOffset
|
||||
},
|
||||
globalCssFilesScanned: cssManifest.files?.length ?? 0,
|
||||
sources: sourceSpecs.map(spec => ({
|
||||
file: spec.file,
|
||||
sha256: spec.sha256,
|
||||
asarComparison: cssManifest.files.find(record => record.file === spec.file)?.asarComparison?.status ?? 'not-compared'
|
||||
})),
|
||||
unmappedLocalClasses,
|
||||
outputPath
|
||||
}, null, 2))
|
||||
Reference in new issue
Block a user