Files
ycode/scripts/rea/export-css-reference.mjs
T

65 lines
5.4 KiB
JavaScript

// Materialize only CSS text already returned in real REA Evidence. No target access.
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import postcss from 'postcss'
const input = path.resolve(process.argv[2] || '.tmp/rea-workbuddy-css-evidence.json')
const output = path.resolve(process.argv[3] || '.tmp/rea-css-recovered')
const evidence = JSON.parse(await fs.readFile(input, 'utf8'))
if (evidence.operation !== 'inspect_electron_page' || evidence.parameters?.include_style_sources !== true || !evidence.normalized_result?.styles) throw new Error('Expected actual REA CSS capture Evidence')
const selection = JSON.parse(await fs.readFile('.tmp/rea-style-selection.json', 'utf8'))
const nodes = new Map(selection.snapshot.nodes.map(node => [node.artifact_id, node]))
const inventory = new Map(selection.snapshot.occurrences.map(occurrence => [occurrence.logical_path, { occurrence, node: nodes.get(occurrence.artifact_id) }]))
const files = []
const usedNames = new Map()
const families = ['cr-tool-call', 'question-floating', 'cb-plan-task-wait-icon', 'cb-plan-tool-container', 'cb-task-tool-renderer', 'cr-reasoning', 'artifact-content', 'markdown-editor-container']
const coverage = Object.fromEntries(families.map(family => [family, []]))
await fs.mkdir(output, { recursive: true })
for (const style of evidence.normalized_result.styles.items) {
if (!style.source.included) continue
const artifact = style.source.artifact
const bytes = Buffer.from(artifact.text, 'utf8')
const sha256 = crypto.createHash('sha256').update(bytes).digest('hex')
if (sha256 !== artifact.sha256 || bytes.length !== artifact.bytes || artifact.media_type !== 'text/css') throw new Error('REA CSS artifact digest/size/type mismatch')
const logicalPath = style.file_path?.replace(/\\/g, '/').split('/app.asar/')[1]
let filename = style.is_inline || !logicalPath ? `inline-${sha256.slice(0, 16)}.css` : path.posix.basename(logicalPath)
if (!filename.endsWith('.css') || !/^[a-zA-Z0-9._-]+$/.test(filename)) throw new Error('Invalid CSS output filename')
if (usedNames.has(filename) && usedNames.get(filename) !== sha256) filename = filename.replace(/\.css$/, `-${sha256.slice(0, 16)}.css`)
usedNames.set(filename, sha256)
const known = logicalPath && !style.is_inline ? inventory.get(logicalPath) : undefined
const asarComparison = known ? {
status: known.node?.sha256 === sha256 && known.node?.size === bytes.length ? 'match' : 'mismatch',
evidenceId: selection.parentEvidenceId, artifactId: known.occurrence.artifact_id,
rootSha256: selection.snapshot.manifest.root_sha256,
expectedSha256: known.node?.sha256, expectedBytes: known.node?.size,
} : undefined
const root = postcss.parse(artifact.text, { from: filename })
let rules = 0
const counts = Object.fromEntries(families.map(family => [family, 0]))
root.walkRules(rule => {
rules++
for (const family of families) if (rule.selector.includes(family)) counts[family]++
})
const destination = path.join(output, filename)
await fs.writeFile(destination, bytes)
if (crypto.createHash('sha256').update(await fs.readFile(destination)).digest('hex') !== sha256) throw new Error('Written CSS digest mismatch')
for (const family of families) if (counts[family]) coverage[family].push({ file: filename, rules: counts[family] })
files.push({ file: filename, filePath: style.file_path, frameId: style.frame_id, styleSheetKey: style.style_sheet_key,
isInline: style.is_inline, sha256, bytes: bytes.length, rules,
...(asarComparison ? { asarComparison } : {}) })
}
files.sort((a, b) => a.file.localeCompare(b.file))
const capturedPaths = new Set(files.filter(file => file.asarComparison).map(file => file.filePath.replace(/\\/g, '/').split('/app.asar/')[1]))
const manifest = { sourceEvidenceId: evidence.evidence_id, sourceEvidencePath: input, provider: evidence.provider,
target: evidence.normalized_result.target, captureWindow: evidence.normalized_result.capture_window,
runtimeStyles: { total: evidence.normalized_result.styles.total, excluded: evidence.normalized_result.styles.excluded,
sourceFailures: evidence.normalized_result.styles.source_failures, truncated: evidence.normalized_result.styles.truncated },
files, bytes: files.reduce((total, file) => total + file.bytes, 0), matchedAsarArtifacts: files.filter(file => file.asarComparison?.status === 'match').length,
mismatchedAsarArtifacts: files.filter(file => file.asarComparison?.status === 'mismatch').map(file => file.file),
selectedInventoryNotObserved: [...inventory.keys()].filter(key => !capturedPaths.has(key)), selectorCoverage: coverage,
limitations: ['Scope is styles loaded in the selected Electron capture window, not every packaged CSS file.', 'Inline/constructed styles are runtime text and do not establish original file identity.', 'Only the prior selected ASAR inventory has been compared; other CSS remains runtime-attributed.', 'A mismatch is preserved as runtime CSS and does not establish original ASAR byte identity; its cause has not been determined.'] }
await fs.writeFile(path.join(output, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
console.log(JSON.stringify({ evidenceId: manifest.sourceEvidenceId, output, files: files.length, bytes: manifest.bytes,
matchedAsarArtifacts: manifest.matchedAsarArtifacts, mismatchedAsarArtifacts: manifest.mismatchedAsarArtifacts,
selectedInventoryNotObserved: manifest.selectedInventoryNotObserved, selectorCoverage: coverage }, null, 2))