// 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))