Files
ycode/scripts/rea/recover-artifact-styles.mjs
T

339 lines
17 KiB
JavaScript

// Materialize the focused artifact-preview CSS projection from captured REA evidence.
import crypto from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import postcss from 'postcss'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const cssDir = path.join(rootDir, '.tmp/rea-css-recovered')
const cssManifestPath = path.join(cssDir, 'manifest.json')
const runtimePath = path.join(rootDir, '.tmp/rea-runtime-scripts.json')
const outputPath = path.join(rootDir, 'src/renderer/src/workbuddy/artifacts/artifact-source.css')
const projectionPath = path.join(cssDir, 'artifact-projection.json')
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
const asarEvidenceId = 'ev_97959691fb7cabf7c41f948b08332196924fec1fa8cb71e34dbafd65d912e637'
const asarRootSha256 = '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
const cssSpecs = [
{ file: 'default-dark-CB7rPan2.css', sha256: '6f1a5b770eceb591ca2e922d075335b170b59ae4e85b2828c176d9a1a5b479fe', runtimeOnly: true },
{ file: 'src-BPUOynUo.css', sha256: '02a85e652dd1be508ab2bcf1b67880c3673d4c47e50b2caea24ac205bfe8f879' },
{ file: 'src-CyXHA0IC.css', sha256: 'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe' },
{ file: 'media-preview-Dvr0Ekj9.css', sha256: '36758fadd61577145a3b46dd12bcd966c662ea0cfaa4e62dc3e31ba4bf7f5fb8' },
{ file: 'image-preview-component-6U_ZZMG_.css', sha256: '17387c5eefa224be5ebde7400f6985aca3da39899db60adecb800de7868ca11c' },
{ file: 'media-BjV-0OHV.css', sha256: '2d7d011d57d6c4266f6a7d4465f7d6d0b9894892fe291bd52c3dfe2e9f4b18ab' },
{ file: 'tokens-Df-i-zsO.css', sha256: '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696' }
]
const runtimeSpecs = [
{ file: 'src-Cng7axVa.js', sha256: '5ce8649e3bfac10aec1e63afee726ab45abaafc5aa7cc372892e20f3eba18c8b' },
{ file: 'media-preview-DMz4TW4q.js', sha256: 'e8c19693ac7d8d06b45b531de405cc23c071fa19ebd5543b483a43dbdfce8111' },
{ file: 'image-preview-component-BgOcW3Xe.js', sha256: '6720317ba018215f96a87d966b08c0d0aa74c26d2298d5fd95f71e8ff2e04e59' },
{ file: 'media.module-DC8Y6boy.js', sha256: '2ad55767cb3f3f0302d053022346ecd40921199a16c2b31aff071dc81acbad51' }
]
const moduleSelectors = {
'media-preview-DMz4TW4q.js': {
object: 'Lo',
css: 'media-preview-Dvr0Ekj9.css',
keys: {
mediaPreview: '.artifact-media-preview',
mediaPreviewHeader: '.artifact-media-preview__header',
mediaPreviewTitle: '.artifact-media-preview__title',
mediaPreviewActions: '.artifact-media-preview__actions',
mediaPreviewActionsButton: '.artifact-media-preview__button',
mediaPreviewActionsButtonActive: '.artifact-media-preview__button--active',
mediaPreviewContent: '.artifact-media-preview__content',
mediaPreviewContentFullBleed: '.artifact-media-preview__content--full-bleed',
mediaPreviewContentSheet: '.artifact-media-preview__content--sheet'
}
},
'media-preview-DMz4TW4q.js#browser': {
object: 'Ur',
css: 'media-preview-Dvr0Ekj9.css',
keys: {
browserFrameContainer: '.artifact-html-frame-container',
browserFrame: '.artifact-html-frame',
browserFrameEmpty: '.artifact-html-frame-empty',
browserFrameError: '.artifact-html-frame-error'
}
},
'image-preview-component-BgOcW3Xe.js': {
object: 'Y',
css: 'image-preview-component-6U_ZZMG_.css',
keys: {
previewOverlay: '.wb-image-preview',
previewOverlayClosing: '.wb-image-preview--closing',
previewStage: '.wb-image-preview__stage',
previewStageClosing: '.wb-image-preview__stage--closing',
previewImage: '.wb-image-preview__image',
previewCloseActionArea: '.wb-image-preview__close-area',
previewCloseButton: '.wb-image-preview__close'
}
},
'media.module-DC8Y6boy.js': {
object: 'A',
css: 'media-BjV-0OHV.css',
keys: {
bottomCenterActionArea: '.wb-image-preview__toolbar',
actionAreaContent: '.wb-image-preview__toolbar-content',
actionButtonsArea: '.wb-image-preview__toolbar-buttons',
topRightActionArea: '.wb-image-preview__close-area',
overlayButton: '.wb-image-preview__control',
overlayIcon: '.wb-image-preview__control-icon',
scalePercentage: '.wb-image-preview__scale',
pageControl: '.wb-image-preview__count',
previewContainer: '.wb-image-preview__stage',
previewInstance: '.wb-image-preview__image'
}
}
}
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
const cssManifest = JSON.parse(await fs.readFile(cssManifestPath, 'utf8'))
const runtimeManifest = JSON.parse(await fs.readFile(runtimePath, 'utf8'))
if (cssManifest.sourceEvidenceId !== cssEvidenceId) throw new Error(`CSS Evidence changed: expected ${cssEvidenceId}, found ${cssManifest.sourceEvidenceId ?? 'none'}`)
if (runtimeManifest.evidence_id !== runtimeEvidenceId) throw new Error(`Runtime Evidence changed: expected ${runtimeEvidenceId}, found ${runtimeManifest.evidence_id ?? 'none'}`)
const cssSources = new Map()
for (const spec of cssSpecs) {
const record = cssManifest.files?.find(item => item.file === spec.file)
if (!record || record.sha256 !== spec.sha256) throw new Error(`REA manifest SHA mismatch for ${spec.file}`)
if (!spec.runtimeOnly && (record.asarComparison?.status !== 'match' || record.asarComparison.evidenceId !== asarEvidenceId || record.asarComparison.rootSha256 !== asarRootSha256)) {
throw new Error(`Static REA comparison is not the locked matching artifact for ${spec.file}`)
}
const bytes = await fs.readFile(path.join(cssDir, spec.file))
const actual = digest(bytes)
if (actual !== spec.sha256) throw new Error(`REA CSS digest mismatch for ${spec.file}: ${actual}`)
cssSources.set(spec.file, { ...spec, root: postcss.parse(bytes.toString('utf8'), { from: spec.file }) })
}
const runtimeSources = new Map()
for (const spec of runtimeSpecs) {
const item = runtimeManifest.items?.find(candidate => candidate.file_path?.replace(/\\/g, '/').endsWith(`/assets/${spec.file}`))
const artifact = item?.source?.artifact
if (!item || item.frame_id !== cssManifest.target?.target_id || artifact?.media_type !== 'text/javascript' || artifact.sha256 !== spec.sha256 || digest(Buffer.from(artifact.text, 'utf8')) !== spec.sha256) {
throw new Error(`REA runtime script digest mismatch or missing source: ${spec.file}`)
}
runtimeSources.set(spec.file, artifact.text)
}
const artifactSource = runtimeSources.get('src-Cng7axVa.js')
for (const selector of ['artifact-content', 'artifact-body', 'plan-content', 'overview-content', 'markdown-content', 'diff-code-content']) {
if (!artifactSource.includes('className:`' + selector + '`')) throw new Error(`Recovered renderer DOM class is missing: ${selector}`)
}
function moduleMap(script, objectName) {
const generated = new Map()
for (const match of script.matchAll(/\b([A-Za-z_$][\w$]*)=`([^`]+)`/g)) generated.set(match[1], match[2])
const match = new RegExp(`\\b${objectName}\\s*=\\{([^{}]*)\\}`).exec(script)
if (!match) throw new Error(`CSS module object ${objectName} was not found`)
const result = new Map()
const entryPattern = /(?:([A-Za-z_$][\w$]*)|["']([^"']+)["']):([A-Za-z_$][\w$]*|`[^`]*`)/g
for (const entry of match[1].matchAll(entryPattern)) {
const key = entry[1] ?? entry[2]
const value = entry[3]
result.set(key, value.startsWith('`') ? value.slice(1, -1) : generated.get(value))
}
return result
}
const projectedModules = new Map()
const classProjection = new Map()
for (const [moduleId, spec] of Object.entries(moduleSelectors)) {
const [scriptName, role] = moduleId.split('#')
const script = runtimeSources.get(scriptName)
const objectName = role === 'browser' ? spec.object : spec.object
const mapping = moduleMap(script, objectName)
const projected = new Map()
for (const [key, localSelector] of Object.entries(spec.keys)) {
const generatedClass = mapping.get(key)
if (!generatedClass) throw new Error(`CSS module key ${scriptName}:${key} is missing`)
const selector = `.${generatedClass}`
const source = cssSources.get(spec.css)
let found = false
source.root.walkRules(rule => {
if (postcss.list.comma(rule.selector).some(candidate => candidate.includes(selector))) found = true
})
if (!found) throw new Error(`CSS module selector ${selector} is missing from ${spec.css}`)
projected.set(selector, localSelector)
classProjection.set(`${scriptName}:${key}`, { sourceSelector: selector, localSelector, css: spec.css })
}
projectedModules.set(moduleId, projected)
}
const output = postcss.root()
const selected = new Map()
function appendProjectedRule(rule, projectSelector, source) {
const parts = postcss.list.comma(rule.selector)
const selectors = parts.flatMap(selector => {
const projected = projectSelector(selector)
return Array.isArray(projected) ? projected : projected ? [projected] : []
})
if (!selectors.length) return
let clone = rule.clone({ selector: [...new Set(selectors)].join(',') })
for (let parent = rule.parent; parent && parent.type !== 'root'; parent = parent.parent) {
if (parent.type === 'atrule' && parent.nodes) clone = parent.clone({ nodes: [clone] })
}
clone.raws.before = '\n'
output.append(clone)
selected.set(source, (selected.get(source) ?? 0) + 1)
}
function rewriteMappedSelector(selector, mappings) {
let rewritten = selector
let matched = false
const moduleTokens = selector.match(/\._[A-Za-z][\w-]*_[A-Za-z0-9]+_\d+/g) ?? []
if (moduleTokens.some(token => !mappings.has(token))) return undefined
for (const [sourceSelector, localSelector] of mappings) {
if (!rewritten.includes(sourceSelector)) continue
rewritten = rewritten.replaceAll(sourceSelector, localSelector)
matched = true
}
if (!matched) return undefined
const selectors = [rewritten]
if (/(^|\s)(?::root\.(?:dark|cb-dark)|:root\s+\.(?:dark|cb-dark))\s/.test(selector)) {
const hostDark = rewritten.replace(/:root\.(?:dark|cb-dark)|:root\s+\.(?:dark|cb-dark)/, ':root[data-theme=dark]')
if (hostDark !== rewritten) selectors.push(hostDark)
}
return selectors
}
const artifactFamilies = /\.(?:artifact-content|artifact-body|plan-content|overview-content|custom-artifact-content|markdown-content|markdown-viewer|code-content|diff-code-content|diff-loading|diff-error)(?=$|[\s.#:[>+~])/
const directImages = new Set(['.cr-image-block', '.cr-image-block-fallback'])
for (const file of ['src-BPUOynUo.css', 'src-CyXHA0IC.css']) {
const source = cssSources.get(file)
source.root.walkRules(rule => {
const project = selector => {
if (file === 'src-BPUOynUo.css' && artifactFamilies.test(selector)) return selector.replace(/:global\(([^)]+)\)/g, '$1').replace(/\.markdown-content(?=$|[^\w-])/g, '.artifact-markdown-content')
if (file === 'src-CyXHA0IC.css' && directImages.has(selector)) return selector
return undefined
}
appendProjectedRule(rule, project, file)
})
}
for (const [moduleId, mappings] of projectedModules) {
const cssFile = moduleSelectors[moduleId].css
const source = cssSources.get(cssFile)
source.root.walkRules(rule => {
appendProjectedRule(rule, selector => rewriteMappedSelector(selector, mappings), `${cssFile}:${moduleId}`)
})
}
const workbuddyTokens = new Map([['light', new Map()], ['dark', new Map()]])
// Runtime CSS Evidence locks this theme variable; this file has no separate ASAR comparison.
cssSources.get('default-dark-CB7rPan2.css').root.walkRules(rule => {
const theme = rule.selector.includes('IDE\\ Night') ? 'dark' : rule.selector.includes('IDE\\ Light') ? 'light' : undefined
const background = rule.nodes.find(node => node.type === 'decl' && node.prop === '--vscode-diffCodeContent-background')
if (!theme || !background) return
output.append(postcss.rule({ selector: theme === 'dark' ? ':root[data-theme=dark] .artifact-content' : ':root .artifact-content', nodes: [background.clone()] }))
selected.set(`default-dark-CB7rPan2.css:${theme}`, 1)
})
cssSources.get('tokens-Df-i-zsO.css').root.walkRules(rule => {
const selectors = postcss.list.comma(rule.selector)
const dark = selectors.some(selector => /\[data-theme=dark\]|\.dark|cb-dark|vscode-dark|IDE\\ Night/.test(selector))
const light = selectors.some(selector => selector.includes(':root') || /IDE\\ Light/.test(selector))
const variant = dark ? 'dark' : light ? 'light' : undefined
if (!variant) return
for (const declaration of rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--wb-'))) {
workbuddyTokens.get(variant).set(declaration.prop, declaration)
}
})
const requiredTokens = new Set()
output.walkDecls(declaration => {
for (const match of declaration.value.matchAll(/var\((--wb-[\w-]+)/g)) requiredTokens.add(match[1])
})
let discoveredToken = true
while (discoveredToken) {
discoveredToken = false
for (const variant of ['light', 'dark']) {
const tokens = workbuddyTokens.get(variant)
for (const token of [...requiredTokens]) {
const declaration = tokens.get(token) ?? workbuddyTokens.get('light').get(token)
if (!declaration) continue
for (const match of declaration.value.matchAll(/var\((--wb-[\w-]+)/g)) {
if (!requiredTokens.has(match[1])) {
requiredTokens.add(match[1])
discoveredToken = true
}
}
}
}
}
for (const [variant, selector] of [
['light', ':root .artifact-content, :root .artifact-media-preview, :root .wb-image-preview'],
['dark', ':root[data-theme=dark] .artifact-content, :root[data-theme=dark] .artifact-media-preview, :root[data-theme=dark] .wb-image-preview']
]) {
const declarations = [...requiredTokens]
.map(name => workbuddyTokens.get(variant).get(name))
.filter(Boolean)
.map(declaration => declaration.clone())
if (!declarations.length) continue
output.append(postcss.rule({ selector, nodes: declarations }))
selected.set(`tokens-Df-i-zsO.css:${variant}`, declarations.length)
}
output.append(postcss.rule({
selector: '.artifact-content, .artifact-media-preview, .wb-image-preview',
nodes: [
['--cb-bg-secondary', 'var(--yc-bg-secondary)'],
['--cb-hover-bg', 'var(--yc-bg-hover)'],
['--cb-panel-bg-primary', 'var(--yc-bg-primary)'],
['--cr-md-bg-code-block', 'var(--yc-bg-secondary)'],
['--cr-md-text-tertiary', 'var(--yc-text-tertiary)'],
['--vscode-font-family', 'var(--yc-font-sans)']
].map(([prop, value]) => postcss.decl({ prop, value }))
}))
const usedKeyframes = new Set()
output.walkDecls(declaration => {
if (!/^animation(?:-name)?$/.test(declaration.prop)) return
const sourceNames = declaration.value.match(/_[A-Za-z][\w-]*_[A-Za-z0-9]+_\d+/g) ?? []
sourceNames.forEach(name => usedKeyframes.add(name))
})
for (const { file } of cssSpecs) {
const source = cssSources.get(file)
if (!source) continue
source.root.walkAtRules(/^keyframes$/, rule => {
if (!usedKeyframes.has(rule.params)) return
const clone = rule.clone()
clone.raws.before = '\n'
output.append(clone)
selected.set(`${file}:keyframes`, (selected.get(`${file}:keyframes`) ?? 0) + 1)
})
}
if ((selected.get('src-BPUOynUo.css') ?? 0) < 20) throw new Error('Artifact CSS projection did not recover the expected global preview family')
if ((selected.get('src-CyXHA0IC.css') ?? 0) < 2) throw new Error('Markdown image styles were not recovered')
const cssHeader = `/* Generated by scripts/rea/recover-artifact-styles.mjs from captured REA Evidence.\n * CSS Evidence ${cssEvidenceId}; runtime Evidence ${runtimeEvidenceId}.\n * Original selected declarations and at-rule/theme conditions are preserved.\n * CSS module classes are projected from locked runtime mappings; source selectors are listed in .tmp/rea-css-recovered/artifact-projection.json.\n */\n`
const cssText = `${cssHeader}${output.toString()}\n`
postcss.parse(cssText, { from: path.relative(rootDir, outputPath) })
await fs.writeFile(outputPath, cssText, 'utf8')
const projection = {
cssEvidenceId,
asarEvidenceId,
asarRootSha256,
runtimeEvidenceId,
target: cssManifest.target,
selected: [...selected].map(([source, rules]) => ({ source, rules })),
hostAdapters: {
darkTheme: 'Retained source theme selectors and added :root[data-theme=dark] projections for the WorkBuddy color theme.',
moduleClasses: 'Locked CSS module selectors are projected to YCode component classes; unneeded dynamic module selectors are omitted.',
selector: ':global(.monaco-editor) is projected to .monaco-editor for a native browser stylesheet.',
markdownClass: 'Original artifact-only .markdown-content becomes .artifact-markdown-content to avoid colliding with YCode chat Markdown.',
hostTokens: ['--cb-bg-secondary', '--cb-hover-bg', '--cb-panel-bg-primary', '--cr-md-bg-code-block', '--cr-md-text-tertiary', '--vscode-font-family']
},
css: cssSpecs.map(spec => ({
...spec,
artifactId: cssManifest.files.find(item => item.file === spec.file).asarComparison?.artifactId,
observationScope: spec.runtimeOnly ? 'REA runtime CSS only; no independent ASAR comparison' : 'REA runtime CSS and matching ASAR artifact'
})),
runtime: runtimeSpecs,
moduleProjection: Object.fromEntries([...classProjection].map(([key, value]) => [key, value]))
}
await fs.writeFile(projectionPath, `${JSON.stringify(projection, null, 2)}\n`, 'utf8')
console.log(JSON.stringify({ output: path.relative(rootDir, outputPath), cssEvidenceId, runtimeEvidenceId,
selected: projection.selected, projectedModuleClasses: classProjection.size }, null, 2))