Files
ycode/scripts/rea/recover-task-styles.mjs

128 lines
5.8 KiB
JavaScript

// Recover only the TaskToolRenderer rules from already-exported REA Evidence.
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import postcss from 'postcss'
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const runtimeEvidencePath = resolve(rootDir, '.tmp/rea-runtime-scripts.json')
const cssDirectory = resolve(rootDir, '.tmp/rea-css-recovered')
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/task-source.css')
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
const runtimeScriptPath = 'renderer/assets/src-0FGg1seX.js'
const runtimeScriptSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
const cssFile = 'src-CBaV_A3S.css'
const cssSha256 = '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c'
const sourceSelector = '.cb-task-tool-renderer'
function digest(bytes) {
return createHash('sha256').update(bytes).digest('hex')
}
function verifyArtifact(label, bytes, expectedSha256) {
const actual = digest(bytes)
if (actual !== expectedSha256) throw new Error(`${label} SHA-256 mismatch: expected ${expectedSha256}, got ${actual}`)
}
const runtimeEvidence = JSON.parse(await readFile(runtimeEvidencePath, 'utf8'))
if (runtimeEvidence.evidence_id !== runtimeEvidenceId) {
throw new Error(`Runtime Evidence changed: expected ${runtimeEvidenceId}, got ${runtimeEvidence.evidence_id ?? 'none'}`)
}
const runtimeMatches = runtimeEvidence.items.filter((item) => item.file_path?.replace(/\\/g, '/').endsWith(runtimeScriptPath))
if (runtimeMatches.length !== 1) throw new Error(`Expected one REA runtime script at ${runtimeScriptPath}, found ${runtimeMatches.length}`)
const runtimeSource = runtimeMatches[0].source
const runtimeArtifact = runtimeSource?.artifact
if (!runtimeSource?.included || runtimeArtifact?.media_type !== 'text/javascript') throw new Error('REA TaskToolRenderer source is not included as JavaScript')
const runtimeBytes = Buffer.from(runtimeArtifact.text, 'utf8')
verifyArtifact(runtimeScriptPath, runtimeBytes, runtimeScriptSha256)
if (runtimeArtifact.sha256 !== runtimeScriptSha256 || runtimeArtifact.bytes !== runtimeBytes.length) {
throw new Error(`REA runtime artifact metadata mismatch for ${runtimeScriptPath}`)
}
for (const marker of ['TaskToolRenderer=', 'cb-task-result-item-title', 'cb-task-result-item-border', 'cb-task-tool-info-text', 'cb-task-hidden']) {
if (!runtimeArtifact.text.includes(marker)) throw new Error(`REA TaskToolRenderer source is missing ${marker}`)
}
const cssManifest = JSON.parse(await readFile(resolve(cssDirectory, 'manifest.json'), 'utf8'))
if (cssManifest.sourceEvidenceId !== cssEvidenceId) {
throw new Error(`CSS Evidence changed: expected ${cssEvidenceId}, got ${cssManifest.sourceEvidenceId ?? 'none'}`)
}
const cssMetadata = cssManifest.files?.find((file) => file.file === cssFile)
if (!cssMetadata || cssMetadata.sha256 !== cssSha256) throw new Error(`REA CSS manifest has no expected digest for ${cssFile}`)
const cssBytes = await readFile(resolve(cssDirectory, cssFile))
verifyArtifact(cssFile, cssBytes, cssSha256)
if (cssMetadata.bytes !== cssBytes.length) throw new Error(`REA CSS byte count mismatch for ${cssFile}`)
const root = postcss.parse(cssBytes, { from: cssFile })
const rootSelector = `[data-source-evidence-id="${runtimeEvidenceId}"][data-css-evidence-id="${cssEvidenceId}"].cb-task-tool-renderer`
const outputRoot = postcss.root()
let ruleCount = 0
function project(node) {
if (node.type === 'rule') {
const selectors = (node.selectors ?? []).filter((selector) => selector.includes(sourceSelector))
if (selectors.length === 0) return null
const projected = selectors.map((selector) => {
if (!selector.startsWith(sourceSelector)) throw new Error(`Unexpected TaskToolRenderer selector root: ${selector}`)
return selector.replace(sourceSelector, rootSelector)
})
ruleCount += 1
return node.clone({ selector: projected.join(',') })
}
if (node.type === 'atrule' && /keyframes$/.test(node.name)) {
return node.params === 'cb-task-pulse' ? node.clone() : null
}
if (node.type === 'atrule' && node.nodes) {
const children = node.nodes.map(project).filter(Boolean)
return children.length ? node.clone({ nodes: children }) : null
}
return null
}
for (const node of root.nodes) {
const projected = project(node)
if (projected) outputRoot.append(projected)
}
if (ruleCount !== 40) throw new Error(`Expected 40 TaskToolRenderer CSS rules, found ${ruleCount}`)
const header = `/* Generated by scripts/rea/recover-task-styles.mjs from fixed REA Evidence.
* Runtime JSX: ${runtimeEvidenceId}; ${runtimeScriptPath} sha256=${runtimeScriptSha256}.
* Source CSS: ${cssEvidenceId}; ${cssFile} sha256=${cssSha256}.
* Original selected rule bodies are preserved and scoped to the matching DOM evidence markers.
* Native button selectors below adapt the original clickable div and SVG controls for keyboard access.
*/\n`
const buttonAdapters = `
${rootSelector} .cb-task-result-item-title {
box-sizing: border-box;
width: 100%;
border: 0;
color: inherit;
font-family: inherit;
font-size: inherit;
text-align: left;
}
${rootSelector} .cb-task-status-icon {
border: 0;
padding: 0;
background: transparent;
font: inherit;
}
`
const output = `${header}${outputRoot.toString()}${buttonAdapters}`
postcss.parse(output, { from: relative(rootDir, outputPath) })
await writeFile(outputPath, output, 'utf8')
console.log(JSON.stringify({
runtimeEvidenceId,
runtimeScriptPath,
runtimeScriptSha256,
cssEvidenceId,
cssFile,
cssSha256,
selectedRules: ruleCount,
destination: relative(rootDir, outputPath)
}, null, 2))