Files
ycode/scripts/rea/ui-designer-recover.mjs

44 lines
5.5 KiB
JavaScript

/** Recover exact selected REA bytes only; no original package filesystem reads. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import { spawnSync } from 'node:child_process'
const pin = 'c9c83df190f0f0f871706d52b7e372f20b7ad63fde739f59cf9181ace1d08afa'
const inventoryPath = '.tmp/rea-ui-designer-inventory-20261016.json', inventory = JSON.parse(fs.readFileSync(inventoryPath, 'utf8'))
if (inventory.normalized_result.root_sha256 !== pin) throw Error('Pinned inventory changed')
const input = inventory.normalized_result.root_identity.path, output = path.resolve('.tmp/rea-ui-designer-recovered-20261016')
const members = inventory.normalized_result.members.filter(item => item.kind === 'file'), markers = ['.downloaded_at', '.created-by-session', '.pending-preview-session']
const skipped = members.filter(item => markers.includes(path.posix.basename(item.path))), wanted = members.filter(item => !markers.includes(path.posix.basename(item.path)))
const mode = process.argv[2] ?? '--all', hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
if (!['--all', '--text', '--text-supported', '--binary', '--verify'].includes(mode)) throw Error('Use --all/--text/--text-supported/--binary/--verify')
fs.mkdirSync(output, { recursive: true })
const manifestPath = path.join(output, 'evidence-manifest.json'), previous = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf8')) : { rootSha256: pin, files: [] }
if (previous.rootSha256 !== pin) throw Error('Recovery manifest has another root identity')
const recovered = new Map(previous.files.map(item => [item.path, item]))
for (const file of previous.files) { const expected = wanted.find(item => item.path === file.path); const bytes = fs.readFileSync(path.join(output, file.path)); if (!expected || hash(bytes) !== expected.sha256 || bytes.length !== expected.size_bytes || file.sha256 !== expected.sha256) throw Error('Existing recovered byte identity mismatch ' + file.path) }
const groups = []
if (['--all', '--text', '--text-supported'].includes(mode)) groups.push({ binary: false, members: wanted.filter(item => (mode === '--text-supported' ? /\.(js|css|json|md|txt|yaml|yml|py)$/i : /\.(js|css|json|md|txt|yaml|yml|py|html|LICENSE)$/i).test(item.path) && !recovered.has(item.path)) })
if (['--all', '--binary'].includes(mode)) groups.push({ binary: true, members: wanted.filter(item => /\.(png|ttf)$/i.test(item.path) && !recovered.has(item.path)) })
for (const group of groups) for (let offset = 0; offset < group.members.length; offset += 16) {
const batch = group.members.slice(offset, offset + 16), selection = group.binary ? batch.map(item => ({ path: item.path, sha256: item.sha256 })) : batch.map(item => item.path)
const result = spawnSync(process.execPath, ['.tmp/rea-css-runtime/scripts/rea.mjs', group.binary ? 'export-selected-directory-binary' : 'export-selected-directory-text', input, pin, JSON.stringify(selection)], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
if (result.status !== 0) throw Error(result.stderr || result.stdout || `REA exit ${result.status}`)
const evidence = JSON.parse(result.stdout)
if (evidence.normalized_result.root_sha256 !== pin || evidence.normalized_result.files.length !== batch.length) throw Error('REA selected scope mismatch')
const retainedExport = `.tmp/rea-ui-designer-${group.binary ? 'binary' : 'text'}-${evidence.evidence_id}.json`
fs.writeFileSync(retainedExport, JSON.stringify(evidence, null, 2))
for (const file of evidence.normalized_result.files) {
const expected = batch.find(item => item.path === file.path), bytes = group.binary ? Buffer.from(file.data_base64, 'base64') : Buffer.from(file.text, 'utf8')
if (!expected || file.sha256 !== expected.sha256 || hash(bytes) !== expected.sha256 || file.size_bytes !== expected.size_bytes || bytes.length !== expected.size_bytes) throw Error('Selected source SHA/byte mismatch ' + file.path)
const destination = path.resolve(output, file.path), relative = path.relative(output, destination)
if (path.isAbsolute(relative) || relative === '..' || relative.startsWith('..' + path.sep)) throw Error('Recovered output path escaped')
fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.writeFileSync(destination, bytes)
recovered.set(file.path, { path: file.path, bytes: bytes.length, sha256: file.sha256, evidenceId: evidence.evidence_id, retainedExport, ...(group.binary ? { encoding: 'base64', mediaType: file.media_type } : { encoding: 'utf8' }) })
}
save()
console.log(JSON.stringify({ batch: group.binary ? 'binary' : 'text', evidenceId: evidence.evidence_id, files: batch.length, recovered: recovered.size }))
}
function save() { fs.writeFileSync(manifestPath, JSON.stringify({ rootSha256: pin, inputRoot: input, inventoryEvidenceId: inventory.evidence_id, retainedInventory: inventoryPath, inventoryCoverage: inventory.normalized_result.coverage, excluded: skipped.map(item => ({ ...item, reason: 'WorkBuddy host download/session marker, not functional package content' })), files: [...recovered.values()].sort((a, b) => a.path.localeCompare(b.path)), remaining: wanted.filter(item => !recovered.has(item.path)).map(item => ({ path: item.path, bytes: item.size_bytes, sha256: item.sha256 })) }, null, 2) + '\n') }
save()
console.log(JSON.stringify({ output, manifest: manifestPath, rootSha256: pin, recoveredFiles: recovered.size, requiredFiles: wanted.length, recoveredBytes: [...recovered.values()].reduce((sum, file) => sum + file.bytes, 0), remaining: wanted.filter(item => !recovered.has(item.path)).length }))