chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"activate-current-algorithm-expert","selector":"[role=dialog] button[class*='_headerSummonBtn_']","frames":0},{"kind":"wait","step_id":"algorithm-package-ready","duration_ms":1800,"frames":1}]}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Read only retained REA observations. No original filesystem access. */
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
const partial=JSON.parse(fs.readFileSync('.tmp/rea-cpp-plugins-analysis-20261016.json','utf8')).details.partial_observation
|
||||
const modules=partial.normalized_result.graph.nodes.flatMap(n=>n.observations??[]).filter(o=>o.evidence?.extractor?.operation==='inventory-relevant-file')
|
||||
const metadata=modules.filter(o=>/^marketplaces\/experts\/plugins\/[^/]+\/\.(?:codebuddy|workbuddy)-plugin\/plugin\.json$/.test(o.properties.path)).map(o=>({path:o.properties.path,sha256:o.evidence.artifact.sha256,bytes:o.properties.bytes}))
|
||||
const exported=JSON.parse(fs.readFileSync('.tmp/rea-cpp-text-ev_5cc1639489033f42be3eef3c4eea6df9f41c54201a6c86f36680473cd278f8d2.json','utf8'))
|
||||
for(const f of exported.normalized_result.files)if(crypto.createHash('sha256').update(f.text).digest('hex')!==f.sha256)throw Error('Retained export SHA mismatch')
|
||||
const catalog=JSON.parse(exported.normalized_result.files.find(f=>f.path==='manifest.json').text),map=JSON.parse(exported.normalized_result.files.find(f=>f.path==='expert-bundle-map.json').text)
|
||||
const matches=catalog.experts.filter(e=>/高级算法|小算|algorithm engineer|algorithm-engineer/i.test(JSON.stringify([e.id,e.name,e.displayName,e.profession,e.plugin,e.agentName])))
|
||||
const otherCandidates=modules.filter(o=>!o.properties.path.includes('node_modules/')&&/algorithm|小算|算法/i.test(o.properties.path)).map(o=>({path:o.properties.path,sha256:o.evidence.artifact.sha256,bytes:o.properties.bytes}))
|
||||
const report={partialEvidence:partial.evidence_id,partialScope:partial.normalized_result.graph.coverage,cacheEvidence:exported.evidence_id,metadata,catalogMatches:matches,bundleMap:map,otherCandidates}
|
||||
fs.writeFileSync('.tmp/rea-algorithm-locate.json',JSON.stringify(report,null,2))
|
||||
console.log(JSON.stringify(report,null,2))
|
||||
@@ -0,0 +1,19 @@
|
||||
/** Current exact known metadata directories via formal REA CLI, never original FS directly. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
import crypto from 'node:crypto'
|
||||
const retained=JSON.parse(fs.readFileSync('.tmp/rea-algorithm-locate.json','utf8'))
|
||||
const results=[]
|
||||
function cli(command,args){const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:4*1024*1024});if(r.status!==0)throw Error(r.stderr||r.stdout);return JSON.parse(r.stdout)}
|
||||
for(const old of retained.metadata){
|
||||
const root='C:/Users/Administrator/.workbuddy/plugins/'+path.posix.dirname(old.path)
|
||||
try{
|
||||
const inventory=cli('inspect-selected-directory-text-root',[root]);fs.writeFileSync(`.tmp/rea-algorithm-metadata-inventory-${inventory.evidence_id}.json`,JSON.stringify(inventory))
|
||||
const evidence=cli('export-selected-directory-text',[root,inventory.normalized_result.root_sha256,JSON.stringify(['plugin.json'])])
|
||||
const file=evidence.normalized_result.files[0];if(crypto.createHash('sha256').update(file.text).digest('hex')!==file.sha256)throw Error('SHA mismatch')
|
||||
fs.writeFileSync(`.tmp/rea-algorithm-metadata-${evidence.evidence_id}.json`,JSON.stringify(evidence))
|
||||
const p=JSON.parse(file.text);results.push({root,evidenceId:evidence.evidence_id,rootSha256:inventory.normalized_result.root_sha256,sha256:file.sha256,oldInventoryShaMatches:file.sha256===old.sha256,name:p.name,profession:p.profession,displayName:p.displayName,agentName:p.agentName,isTarget:/高级算法|小算|algorithm engineer|algorithm-engineer/i.test(JSON.stringify([p.name,p.profession,p.displayName,p.agentName]))})
|
||||
}catch(error){results.push({root,error:String(error),status:'unknown'})}
|
||||
}
|
||||
fs.writeFileSync('.tmp/rea-algorithm-current-metadata.json',JSON.stringify({results},null,2));console.log(JSON.stringify({results},null,2))
|
||||
@@ -0,0 +1,27 @@
|
||||
/** Reproduce target recovery only through public formal REA directory CLI. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
const input=process.argv[2]
|
||||
if(!input)throw Error('Provide retained narrow package analysis or formal directory inventory')
|
||||
const analysis=JSON.parse(fs.readFileSync(input,'utf8')),source=analysis.normalized_result
|
||||
const root=source?.input_path??source?.root_identity?.path,pin=source?.root_artifact_sha256??source?.root_sha256,recovered='.tmp/rea-algorithm-recovered-20261016'
|
||||
if(!root||!pin)throw Error('Pinned narrow REA analysis or formal inventory required')
|
||||
function cli(command,args){const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:40*1024*1024});if(r.status!==0)throw Error(r.stderr||r.stdout);return JSON.parse(r.stdout)}
|
||||
const inventory=cli('inspect-selected-directory-text-root',[root]);if(inventory.normalized_result.root_sha256!==pin)throw Error('Package changed since narrow REA analysis')
|
||||
fs.writeFileSync(`.tmp/rea-algorithm-inventory-${inventory.evidence_id}.json`,JSON.stringify(inventory))
|
||||
if(inventory.normalized_result.coverage.status!=='complete')throw Error('Target package inventory is incomplete')
|
||||
const members=inventory.normalized_result.members.filter(m=>m.kind==='file'),meta=members.find(m=>/^\.(codebuddy|workbuddy)-plugin\/plugin\.json$/.test(m.path))
|
||||
if(!meta)throw Error('No exact source plugin manifest')
|
||||
function exportBatch(list,binary=false){return cli(binary?'export-selected-directory-binary':'export-selected-directory-text',[root,pin,JSON.stringify(binary?list.map(m=>({path:m.path,sha256:m.sha256})):list.map(m=>m.path))])}
|
||||
const identityEvidence=exportBatch([meta]),p=JSON.parse(identityEvidence.normalized_result.files[0].text)
|
||||
if(!/高级算法|小算|algorithm.?engineer/i.test(JSON.stringify([p.name,p.displayName,p.profession,p.agentName])))throw Error('Source metadata does not establish the requested algorithm expert')
|
||||
const manifest={sourceExpert:'高级算法工程师 | 小算',sourceRoot:root,rootSha256:pin,inputEvidenceId:analysis.evidence_id,inputEvidenceKind:source.input_path?'javascript-application-analysis':'selected-directory-inventory',...(source.input_path?{analysisEvidenceId:analysis.evidence_id}:{}),inventoryEvidenceId:inventory.evidence_id,inventoryCoverage:inventory.normalized_result.coverage,identity:{name:p.name,agentName:p.agentName,expertType:p.expertType,displayName:p.displayName,profession:p.profession,version:p.version,agents:p.agents,skills:p.skills,dependencies:p.dependencies,mcpServers:p.mcpServers},files:[],omittedMarkers:[],unknown:[]}
|
||||
const text=[],binary=[]
|
||||
for(const m of members){if(/^\.(downloaded_at|created-by-session|pending-preview-session)$/.test(m.path)){manifest.omittedMarkers.push(m.path);continue}if(/\.png$/i.test(m.path))binary.push(m);else if(/\.(md|txt|json|yaml|yml|py|js|css|html|mjs|cjs|ps1)$/i.test(m.path)||/(^|\/)(LICENSE|NOTICE)$/i.test(m.path))text.push(m);else manifest.unknown.push({path:m.path,reason:'Required extension not yet supported by this recovery script'})}
|
||||
function restore(evidence){if(evidence.normalized_result.root_sha256!==pin)throw Error('Root mismatch');const exportPath=`.tmp/rea-algorithm-export-${evidence.evidence_id}.json`;fs.writeFileSync(exportPath,JSON.stringify(evidence));for(const f of evidence.normalized_result.files){const original=members.find(m=>m.path===f.path),bytes=f.encoding==='base64'?Buffer.from(f.data_base64,'base64'):Buffer.from(f.text,'utf8'),sha=crypto.createHash('sha256').update(bytes).digest('hex');if(sha!==f.sha256||sha!==original.sha256||bytes.length!==f.size_bytes||bytes.length!==original.size_bytes)throw Error('Source byte/SHA mismatch');if(f.path.split('/').some(p=>!p||p==='.'||p==='..')||f.path.includes('\\'))throw Error('Noncanonical relative path');const destination=path.join(recovered,...f.path.split('/'));fs.mkdirSync(path.dirname(destination),{recursive:true});fs.writeFileSync(destination,bytes);if(crypto.createHash('sha256').update(fs.readFileSync(destination)).digest('hex')!==sha)throw Error('Recovery disk SHA mismatch');manifest.files.push({path:f.path,bytes:f.size_bytes,sha256:sha,evidenceId:evidence.evidence_id,retainedExport:exportPath,...(f.encoding?{encoding:f.encoding,mediaType:f.media_type}:{})})}}
|
||||
restore(identityEvidence)
|
||||
for(const [list,isBinary]of [[text.filter(m=>m.path!==meta.path),false],[binary,true]]){let batch=[],bytes=0;for(const m of list){if(batch.length&&(batch.length>=12||bytes+m.size_bytes>8*1024*1024)){restore(exportBatch(batch,isBinary));batch=[];bytes=0}batch.push(m);bytes+=m.size_bytes}if(batch.length)restore(exportBatch(batch,isBinary))}
|
||||
fs.writeFileSync(path.join(recovered,'evidence-manifest.json'),JSON.stringify(manifest,null,2)+'\n')
|
||||
console.log(JSON.stringify({result:manifest.unknown.length?'PARTIAL':'PASS',recovered,identity:manifest.identity,files:manifest.files.length,bytes:manifest.files.reduce((n,f)=>n+f.bytes,0),unknown:manifest.unknown,omittedMarkers:manifest.omittedMarkers},null,2))
|
||||
@@ -0,0 +1,50 @@
|
||||
/** Verify retained REA exports and recovery only; never read original paths. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import assert from 'node:assert/strict'
|
||||
import {parse} from 'yaml'
|
||||
const root='.tmp/rea-algorithm-recovered-20261016'
|
||||
const manifest=JSON.parse(fs.readFileSync(path.join(root,'evidence-manifest.json'),'utf8'))
|
||||
const inventory=JSON.parse(fs.readFileSync('.tmp/rea-algorithm-package-inventory.json','utf8'))
|
||||
const members=inventory.normalized_result.members.filter(x=>x.kind==='file')
|
||||
assert.equal(inventory.normalized_result.root_sha256,manifest.rootSha256)
|
||||
assert.equal(inventory.normalized_result.coverage.status,'complete')
|
||||
assert.deepEqual(manifest.unknown,[])
|
||||
const checked=[]
|
||||
for(const file of manifest.files){
|
||||
assert.ok(!path.isAbsolute(file.path)&&!file.path.split('/').includes('..'))
|
||||
const bytes=fs.readFileSync(path.join(root,...file.path.split('/')))
|
||||
const sha=crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
const entry=members.find(x=>x.path===file.path)
|
||||
assert.equal(sha,file.sha256);assert.equal(sha,entry.sha256)
|
||||
assert.equal(bytes.length,file.bytes);assert.equal(bytes.length,entry.size_bytes)
|
||||
const evidence=JSON.parse(fs.readFileSync(file.retainedExport,'utf8'))
|
||||
assert.equal(evidence.evidence_id,file.evidenceId)
|
||||
assert.equal(evidence.normalized_result.root_sha256,manifest.rootSha256)
|
||||
const exported=evidence.normalized_result.files.find(x=>x.path===file.path)
|
||||
const exportedBytes=exported.encoding==='base64'?Buffer.from(exported.data_base64,'base64'):Buffer.from(exported.text,'utf8')
|
||||
assert.deepEqual(bytes,exportedBytes)
|
||||
checked.push(file.path)
|
||||
}
|
||||
assert.deepEqual(members.map(x=>x.path).sort(),[...checked,...manifest.omittedMarkers].sort())
|
||||
assert.deepEqual(manifest.omittedMarkers,['.downloaded_at'])
|
||||
const read=rel=>fs.readFileSync(path.join(root,...rel.split('/')),'utf8')
|
||||
const plugin=JSON.parse(read('.codebuddy-plugin/plugin.json'))
|
||||
const frontmatter=rel=>parse(read(rel).match(/^---\r?\n([\s\S]*?)\r?\n---/)[1])
|
||||
const agent=frontmatter(plugin.agents[0].replace(/^\.\//,''))
|
||||
const skillRoot=plugin.skills[0].replace(/^\.\//,''),skill=frontmatter(`${skillRoot}/SKILL.md`)
|
||||
assert.equal(agent.name,plugin.agentName)
|
||||
assert.ok(agent.skills.includes(skill.name))
|
||||
assert.equal(agent.maxTurns,100)
|
||||
assert.ok(!Object.hasOwn(agent,'tools'))
|
||||
assert.ok(!Object.hasOwn(plugin,'dependencies')&&!Object.hasOwn(plugin,'mcpServers'))
|
||||
const references=[...read(`${skillRoot}/SKILL.md`).matchAll(/@((?:references|templates)\/[\w./-]+\.md)/g)].map(m=>m[1])
|
||||
for(const rel of references)assert.ok(checked.includes(`${skillRoot}/${rel}`))
|
||||
assert.ok(checked.includes(`${skillRoot}/scripts/verify_shortest_paths.py`))
|
||||
const avatar=fs.readFileSync(path.join(root,plugin.avatar))
|
||||
assert.equal(avatar.subarray(0,8).toString('hex'),'89504e470d0a1a0a')
|
||||
assert.equal(avatar.readUInt32BE(16),512);assert.equal(avatar.readUInt32BE(20),512)
|
||||
const report={result:'PASS',scope:'Retained REA bytes/SHA, complete inventory coverage, JSON/YAML identity and internal skill references; no original script execution or model behavior tested',rootSha256:manifest.rootSha256,files:checked.length,bytes:manifest.files.reduce((n,x)=>n+x.bytes,0),internalMarkdownReferences:references.length,avatar:{width:512,height:512,bytes:avatar.length},omittedMarkers:manifest.omittedMarkers}
|
||||
fs.writeFileSync('.tmp/rea-algorithm-verification.json',JSON.stringify(report,null,2)+'\n')
|
||||
console.log(JSON.stringify(report,null,2))
|
||||
@@ -0,0 +1,74 @@
|
||||
// Invoke the public repaired REA CLI. This wrapper has no target protocol access.
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const project = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
process.chdir(project)
|
||||
const includeVisuals = process.argv.includes('--visuals')
|
||||
const args = process.argv.slice(2).filter(arg => arg !== '--visuals')
|
||||
function option(name) { const index=args.indexOf(name);if(index<0)return undefined;const value=args[index+1];if(!value||value.startsWith('--'))throw new Error(`Missing ${name} value`);args.splice(index,2);return value }
|
||||
const probeFile=option('--probe'),outputDirectory=option('--output'),selectedTargetId=option('--target-id')
|
||||
const probe=probeFile?JSON.parse(await fs.readFile(probeFile,'utf8')):undefined
|
||||
if(probe&&!includeVisuals)throw new Error('--probe requires --visuals')
|
||||
const endpoint = args[0] || 'http://127.0.0.1:9223'
|
||||
const expectedPage = args[1] || 'C:/Users/Administrator/AppData/Local/Programs/WorkBuddy/resources/app.asar/renderer/index.html'
|
||||
const launcher = path.resolve('.tmp/rea-css-runtime/scripts/rea.mjs')
|
||||
await fs.access(launcher).catch(() => { throw new Error('Run npm run rea:css:prepare first') })
|
||||
async function run(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [launcher, ...args, '--json'], { cwd: project, stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true })
|
||||
const chunks = []
|
||||
child.stdout.on('data', chunk => chunks.push(chunk))
|
||||
child.once('error', reject)
|
||||
child.once('close', async code => {
|
||||
if (code === 0) return resolve(Buffer.concat(chunks))
|
||||
await fs.writeFile('.tmp/rea-capture-failure.json', Buffer.concat(chunks))
|
||||
reject(new Error(`REA exited with ${code}; diagnostic saved to .tmp/rea-capture-failure.json`))
|
||||
})
|
||||
})
|
||||
}
|
||||
const normalize = value => value.replace(/\\/g, '/').toLowerCase()
|
||||
const discoveryBytes = await run(['list-electron-targets', endpoint])
|
||||
const discovery = JSON.parse(discoveryBytes.toString('utf8'))
|
||||
if (discovery.operation !== 'list_electron_targets') throw new Error('REA did not return target discovery Evidence')
|
||||
const matches = discovery.normalized_result.targets.filter(target => normalize(target.file_path) === normalize(expectedPage) && (!selectedTargetId || target.target_id === selectedTargetId))
|
||||
if (matches.length !== 1) throw new Error(`Expected exactly one authorized WorkBuddy page; found ${matches.length}`)
|
||||
const bytes = await run(['inspect-electron-page', endpoint, matches[0].target_id, '--include-style-sources', ...(includeVisuals ? ['--include-visuals'] : []), ...(probe?['--visual-probe',JSON.stringify(probe)]:[]), '--observation-ms', '250'])
|
||||
const evidence = JSON.parse(bytes.toString('utf8'))
|
||||
if (evidence.operation !== 'inspect_electron_page' || evidence.parameters?.include_style_sources !== true || !evidence.normalized_result?.styles) throw new Error('REA did not return CSS Evidence')
|
||||
await fs.writeFile('.tmp/rea-css-targets.json', discoveryBytes)
|
||||
await fs.writeFile('.tmp/rea-workbuddy-css-evidence.json', bytes)
|
||||
await fs.mkdir('.tmp/rea-css-evidence', { recursive: true })
|
||||
if (!/^ev_[a-f0-9]{64}$/.test(evidence.evidence_id) || !/^ev_[a-f0-9]{64}$/.test(discovery.evidence_id)) throw new Error('Invalid REA Evidence identity')
|
||||
await fs.writeFile(path.join('.tmp/rea-css-evidence', `${discovery.evidence_id}.json`), discoveryBytes)
|
||||
const archive = path.join('.tmp/rea-css-evidence', `${evidence.evidence_id}.json`)
|
||||
await fs.writeFile(archive, bytes)
|
||||
if (includeVisuals) {
|
||||
const visuals = evidence.normalized_result.visuals
|
||||
if (!visuals) throw new Error('REA did not retain visual capture')
|
||||
const visualDirectory=outputDirectory?path.resolve(outputDirectory):path.resolve('.tmp/rea-visual-reference')
|
||||
await fs.mkdir(visualDirectory, { recursive: true })
|
||||
const { createHash } = await import('node:crypto')
|
||||
const image = Buffer.from(visuals.artifact.data_base64, 'base64')
|
||||
if (createHash('sha256').update(image).digest('hex') !== visuals.artifact.sha256 || image.length !== visuals.artifact.bytes) throw new Error('REA screenshot digest mismatch')
|
||||
await fs.writeFile(path.join(visualDirectory,'workbuddy.png'), image)
|
||||
await fs.writeFile(path.join(visualDirectory,'layout.json'), JSON.stringify({ evidenceId: evidence.evidence_id, archive: path.resolve(archive), ...visuals, artifact: { ...visuals.artifact, data_base64: undefined },probe:visuals.probe?{...visuals.probe,frames:visuals.probe.frames.map(f=>({...f,artifact:{...f.artifact,data_base64:undefined}}))}:undefined }, null, 2))
|
||||
for(let index=0;index<(visuals.probe?.frames.length??0);index++){
|
||||
const frame=visuals.probe.frames[index],png=Buffer.from(frame.artifact.data_base64,'base64');
|
||||
if(createHash('sha256').update(png).digest('hex')!==frame.artifact.sha256)throw new Error('REA scenario frame digest mismatch');
|
||||
await fs.writeFile(path.join(visualDirectory,`frame-${String(index).padStart(3,'0')}.png`),png)
|
||||
}
|
||||
if(visuals.font_samples){
|
||||
const os=await import('node:os');
|
||||
const report={evidenceId:evidence.evidence_id,archive:path.resolve(archive),evidenceSha256:createHash('sha256').update(bytes).digest('hex'),version:evidence.normalized_result.browser.user_agent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1]??'unobserved',environment:{platform:os.platform(),osRelease:os.release(),browser:evidence.normalized_result.browser},source:{kind:'rea-workbuddy'},samples:visuals.font_samples,missing:visuals.font_missing??[]};
|
||||
await fs.writeFile(path.join(visualDirectory,'font-platform-report.json'),JSON.stringify(report,null,2))
|
||||
await fs.writeFile('.tmp/rea-font-platform-report.json',JSON.stringify(report,null,2))
|
||||
}
|
||||
}
|
||||
const styles = evidence.normalized_result.styles
|
||||
console.log(JSON.stringify({ evidenceId: evidence.evidence_id, targetId: matches[0].target_id, total: styles.total,
|
||||
included: styles.items.filter(item => item.source.included).length, excluded: styles.excluded,
|
||||
sourceFailures: styles.source_failures, truncated: styles.truncated,
|
||||
output: path.resolve('.tmp/rea-workbuddy-css-evidence.json'), archive: path.resolve(archive) }, null, 2))
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import { captureYcodePlatformFonts } from './verify-font-platform-parity.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const outputPath = path.resolve(projectRoot, process.argv[2] ?? '.tmp/ycode-font-platform-selftest-reference.json')
|
||||
const samples = [
|
||||
{ category: 'assistant-name', text: 'YCode' },
|
||||
{ category: 'chinese-body', text: '中文字体字形探针:你好,世界!这是用于复现的正文样本。' },
|
||||
{ category: 'english-body', text: 'Typography fixture: The quick brown fox jumps over 13 lazy dogs.' },
|
||||
{ category: 'user-bubble', text: '请对照这条用户消息的 glyph count,其中包含 English。' },
|
||||
{ category: 'code', text: 'const greeting = "你好, YCode";\nconsole.log(greeting);' }
|
||||
]
|
||||
|
||||
const captured = await captureYcodePlatformFonts(samples)
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
source: { kind: 'ycode-electron-self-test', description: 'Local YCode Electron fixture; this is not WorkBuddy evidence.' },
|
||||
evidenceId: `local-ycode-font-fixture-${Date.now()}`,
|
||||
version: `YCode ${require('../../package.json').version}`,
|
||||
capturedAt: new Date().toISOString(),
|
||||
environment: captured.environment,
|
||||
samples: captured.samples,
|
||||
missing: []
|
||||
}
|
||||
await mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({
|
||||
kind: report.source.kind,
|
||||
evidenceId: report.evidenceId,
|
||||
version: report.version,
|
||||
samples: report.samples.map((sample) => ({ category: sample.category, fontFamily: sample.fontFamily, platformFonts: sample.platformFonts })),
|
||||
outputPath: path.relative(projectRoot, outputPath).replaceAll('\\', '/')
|
||||
}, null, 2))
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,590 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const electronExecutable = require('electron')
|
||||
const runtime = path.resolve('.tmp/rea-css-runtime/dist')
|
||||
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
|
||||
const evidenceIdPattern = /^ev_[a-f0-9]{64}$/
|
||||
|
||||
const electronHelperSource = String.raw`
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { app, nativeImage } = require('electron')
|
||||
|
||||
const jobPath = process.env.YCODE_PIXEL_JOB
|
||||
const resultPath = process.env.YCODE_PIXEL_RESULT
|
||||
const finish = (value, code = 0) => {
|
||||
try { fs.writeFileSync(resultPath, JSON.stringify(value)) } catch {}
|
||||
app.exit(code)
|
||||
}
|
||||
const fail = error => finish({ ok: false, error: String(error && (error.stack || error.message) || error) }, 1)
|
||||
|
||||
try {
|
||||
const job = JSON.parse(fs.readFileSync(jobPath, 'utf8'))
|
||||
const isolated = path.join(path.dirname(jobPath), 'electron-data')
|
||||
for (const name of ['userData', 'sessionData', 'logs']) {
|
||||
try { app.setPath(name, path.join(isolated, name)) } catch {}
|
||||
}
|
||||
app.disableHardwareAcceleration()
|
||||
app.whenReady().then(() => {
|
||||
try {
|
||||
if (job.mode === 'fixtures') {
|
||||
const createPng = (file, width, height, changed) => {
|
||||
const bitmap = Buffer.alloc(width * height * 4)
|
||||
for (let offset = 0; offset < bitmap.length; offset += 4) bitmap.set([20, 40, 60, 255], offset)
|
||||
if (changed) bitmap[0] += 1
|
||||
const image = nativeImage.createFromBitmap(bitmap, { width, height, scaleFactor: 1 })
|
||||
if (image.isEmpty()) throw new Error('nativeImage could not encode self-test fixture')
|
||||
fs.writeFileSync(file, image.toPNG())
|
||||
}
|
||||
createPng(job.files.reference, 2, 2, false)
|
||||
createPng(job.files.changed, 2, 2, true)
|
||||
createPng(job.files.wide, 3, 2, false)
|
||||
finish({ ok: true })
|
||||
return
|
||||
}
|
||||
if (job.mode !== 'compare') throw new Error('Unknown nativeImage helper mode')
|
||||
const output = []
|
||||
for (const pair of job.pairs) {
|
||||
const reference = nativeImage.createFromBuffer(fs.readFileSync(pair.reference))
|
||||
const candidate = nativeImage.createFromBuffer(fs.readFileSync(pair.candidate))
|
||||
if (reference.isEmpty() || candidate.isEmpty()) throw new Error('A screenshot could not be decoded by nativeImage')
|
||||
const beforeSize = reference.getSize(1)
|
||||
const afterSize = candidate.getSize(1)
|
||||
const base = {
|
||||
name: pair.name,
|
||||
before: beforeSize,
|
||||
after: afterSize,
|
||||
channel_tolerance: job.channelTolerance,
|
||||
}
|
||||
if (beforeSize.width !== afterSize.width || beforeSize.height !== afterSize.height) {
|
||||
output.push({ ...base, status: 'dimension_mismatch', compared_pixels: 0, changed_pixels: null, changed_ratio: null, mean_absolute_channel_delta: null, maximum_channel_delta: null, diff_width: null, diff_height: null })
|
||||
continue
|
||||
}
|
||||
const region = pair.region || { x: 0, y: 0, width: beforeSize.width, height: beforeSize.height }
|
||||
if (!Number.isInteger(region.x) || !Number.isInteger(region.y) || !Number.isInteger(region.width) || !Number.isInteger(region.height) || region.x < 0 || region.y < 0 || region.width < 1 || region.height < 1 || region.x + region.width > beforeSize.width || region.y + region.height > beforeSize.height) {
|
||||
throw new Error('Comparison region must be a positive integer rectangle inside both screenshots')
|
||||
}
|
||||
const beforeBitmap = reference.toBitmap({ scaleFactor: 1 })
|
||||
const afterBitmap = candidate.toBitmap({ scaleFactor: 1 })
|
||||
const expectedBeforeBytes = beforeSize.width * beforeSize.height * 4
|
||||
const expectedAfterBytes = afterSize.width * afterSize.height * 4
|
||||
if (beforeBitmap.length !== expectedBeforeBytes || afterBitmap.length !== expectedAfterBytes) throw new Error('nativeImage returned an unexpected bitmap byte count')
|
||||
const diffBitmap = Buffer.alloc(region.width * region.height * 4)
|
||||
const comparedPixels = region.width * region.height
|
||||
let changedPixels = 0
|
||||
let maximumChannelDelta = 0
|
||||
let absoluteChannelDelta = 0
|
||||
for (let y = 0; y < region.height; y += 1) {
|
||||
for (let x = 0; x < region.width; x += 1) {
|
||||
const beforeOffset = ((region.y + y) * beforeSize.width + region.x + x) * 4
|
||||
const afterOffset = ((region.y + y) * afterSize.width + region.x + x) * 4
|
||||
const diffOffset = (y * region.width + x) * 4
|
||||
let pixelMaximum = 0
|
||||
for (let channel = 0; channel < 4; channel += 1) {
|
||||
const delta = Math.abs(beforeBitmap[beforeOffset + channel] - afterBitmap[afterOffset + channel])
|
||||
absoluteChannelDelta += delta
|
||||
pixelMaximum = Math.max(pixelMaximum, delta)
|
||||
maximumChannelDelta = Math.max(maximumChannelDelta, delta)
|
||||
}
|
||||
if (pixelMaximum > job.channelTolerance) {
|
||||
changedPixels += 1
|
||||
diffBitmap.fill(255, diffOffset, diffOffset + 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
const diffImage = nativeImage.createFromBitmap(diffBitmap, { width: region.width, height: region.height, scaleFactor: 1 })
|
||||
if (diffImage.isEmpty()) throw new Error('nativeImage could not encode the diff bitmap')
|
||||
fs.writeFileSync(pair.diffPath, diffImage.toPNG())
|
||||
output.push({
|
||||
...base,
|
||||
region,
|
||||
status: changedPixels === 0 ? 'identical' : 'different',
|
||||
compared_pixels: comparedPixels,
|
||||
changed_pixels: changedPixels,
|
||||
changed_ratio: changedPixels / comparedPixels,
|
||||
mean_absolute_channel_delta: absoluteChannelDelta / (comparedPixels * 4),
|
||||
maximum_channel_delta: maximumChannelDelta,
|
||||
diff_width: region.width,
|
||||
diff_height: region.height,
|
||||
})
|
||||
}
|
||||
finish({ ok: true, pairs: output })
|
||||
} catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
}).catch(fail)
|
||||
} catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
`
|
||||
|
||||
function parseManifest(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Manifest must be a JSON object')
|
||||
const source = value.source ?? { kind: 'local-fixture' }
|
||||
if (!source || typeof source !== 'object' || !['rea-workbuddy', 'local-fixture'].includes(source.kind)) throw new Error('source.kind must be rea-workbuddy or local-fixture')
|
||||
if (!Array.isArray(value.requiredStates) || value.requiredStates.length === 0 || value.requiredStates.some(state => typeof state !== 'string' || !state.trim()) || new Set(value.requiredStates).size !== value.requiredStates.length) throw new Error('requiredStates must be a non-empty array of unique non-empty names')
|
||||
if (!Array.isArray(value.pairs)) throw new Error('pairs must be an array')
|
||||
const names = new Set()
|
||||
const pairs = value.pairs.map((pair, index) => {
|
||||
if (!pair || typeof pair !== 'object' || Array.isArray(pair)) throw new Error(`pairs[${index}] must be an object`)
|
||||
if (typeof pair.name !== 'string' || !pair.name.trim() || names.has(pair.name)) throw new Error(`pairs[${index}].name must be unique and non-empty`)
|
||||
names.add(pair.name)
|
||||
if (typeof pair.reference !== 'string' || !pair.reference || typeof pair.candidate !== 'string' || !pair.candidate) throw new Error(`pairs[${index}] requires reference and candidate PNG paths`)
|
||||
if (pair.region !== undefined) {
|
||||
const { x, y, width, height } = pair.region ?? {}
|
||||
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width < 1 || height < 1) throw new Error(`pairs[${index}].region must be a positive integer rectangle`)
|
||||
}
|
||||
if (source.kind === 'rea-workbuddy') {
|
||||
const evidence = pair.referenceEvidence
|
||||
if (!evidence || typeof evidence !== 'object' || !evidenceIdPattern.test(evidence.id) || typeof evidence.archive !== 'string' || !evidence.archive) throw new Error(`pairs[${index}] requires referenceEvidence with a valid Evidence id and archive`)
|
||||
}
|
||||
return { ...pair }
|
||||
})
|
||||
const channelTolerance = value.channelTolerance ?? 0
|
||||
if (!Number.isInteger(channelTolerance) || channelTolerance < 0 || channelTolerance > 255) throw new Error('channelTolerance must be an integer from 0 to 255')
|
||||
return { source, requiredStates: [...value.requiredStates], pairs, channelTolerance }
|
||||
}
|
||||
|
||||
async function authenticateEvidence(evidencePath) {
|
||||
const modulePath = pathToFileURL(path.join(runtime, 'domain/evidence.js'))
|
||||
let parseEvidence
|
||||
try {
|
||||
({ parseEvidence } = await import(modulePath))
|
||||
} catch (error) {
|
||||
throw new Error(`Prepared REA runtime is required to authenticate Evidence: ${error.message}`)
|
||||
}
|
||||
return parseEvidence(evidencePath)
|
||||
}
|
||||
|
||||
function screenshotArtifacts(evidence) {
|
||||
const visuals = evidence.normalized_result?.visuals
|
||||
const artifacts = []
|
||||
if (visuals?.artifact) artifacts.push({ location: 'visuals.artifact', artifact: visuals.artifact })
|
||||
for (let index = 0; index < (visuals?.probe?.frames?.length ?? 0); index += 1) {
|
||||
const artifact = visuals.probe.frames[index]?.artifact
|
||||
if (artifact) artifacts.push({ location: `visuals.probe.frames[${index}].artifact`, artifact })
|
||||
}
|
||||
return artifacts
|
||||
}
|
||||
|
||||
function checkedArtifactHash(artifact) {
|
||||
if (!artifact || artifact.media_type !== 'image/png' || typeof artifact.data_base64 !== 'string' || typeof artifact.sha256 !== 'string') return undefined
|
||||
const bytes = Buffer.from(artifact.data_base64, 'base64')
|
||||
if (!bytes.length || bytes.toString('base64') !== artifact.data_base64 || (artifact.bytes !== undefined && artifact.bytes !== bytes.length)) return undefined
|
||||
const actualHash = hash(bytes)
|
||||
return actualHash === artifact.sha256 ? actualHash : undefined
|
||||
}
|
||||
|
||||
async function verifyReferenceEvidence(pair, manifestDirectory, referenceHash) {
|
||||
const archive = path.resolve(manifestDirectory, pair.referenceEvidence.archive)
|
||||
let raw
|
||||
try {
|
||||
raw = JSON.parse(await fs.readFile(archive, 'utf8'))
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot read reference Evidence archive for ${pair.name}: ${error.message}`)
|
||||
}
|
||||
if (raw.evidence_id !== pair.referenceEvidence.id) throw new Error(`Evidence id does not match its archive for ${pair.name}`)
|
||||
let evidence
|
||||
try {
|
||||
evidence = await authenticateEvidence(raw)
|
||||
} catch (error) {
|
||||
throw new Error(`Reference Evidence archive failed authentication for ${pair.name}: ${error.message}`)
|
||||
}
|
||||
if (evidence.operation !== 'inspect_electron_page') throw new Error(`Reference Evidence is not an Electron page inspection for ${pair.name}`)
|
||||
if (evidence.provider?.id !== 'rea-cdp-electron') throw new Error(`Reference Evidence provider is not rea-cdp-electron for ${pair.name}`)
|
||||
const targetPath = evidence.normalized_result?.target?.file_path
|
||||
if (typeof targetPath !== 'string' || !/WorkBuddy[\\/]resources[\\/]app\.asar[\\/]renderer[\\/]index\.html$/i.test(targetPath)) {
|
||||
throw new Error(`Reference Evidence target is not the WorkBuddy renderer for ${pair.name}`)
|
||||
}
|
||||
const visualProbe = evidence.parameters?.visual_probe
|
||||
if (visualProbe !== undefined && (!visualProbe || typeof visualProbe !== 'object' || Array.isArray(visualProbe) || !Array.isArray(visualProbe.actions))) {
|
||||
throw new Error(`Reference Evidence visual_probe.actions is malformed for ${pair.name}`)
|
||||
}
|
||||
const visualProbeActions = visualProbe?.actions ?? []
|
||||
const hasVisualProbeActions = visualProbeActions.length > 0
|
||||
const expectedAuthority = hasVisualProbeActions ? 'controlled-replay' : 'external-service'
|
||||
if (evidence.authority !== expectedAuthority) throw new Error(`Reference Evidence authority does not match visual_probe.actions for ${pair.name}`)
|
||||
const userAgent = evidence.normalized_result?.browser?.user_agent
|
||||
const version = typeof userAgent === 'string' ? userAgent.match(/WorkBuddy[ /]([\d.]+)/i)?.[1] ?? 'unobserved' : 'unobserved'
|
||||
const matching = screenshotArtifacts(evidence).filter(({ artifact }) => checkedArtifactHash(artifact) === referenceHash)
|
||||
if (!matching.length) throw new Error(`Reference PNG SHA is absent from authenticated REA baseline/probe artifacts for ${pair.name}`)
|
||||
return {
|
||||
id: evidence.evidence_id,
|
||||
archive,
|
||||
artifact_locations: matching.map(item => item.location),
|
||||
sha256: referenceHash,
|
||||
target_file_path: targetPath,
|
||||
authority: evidence.authority,
|
||||
version,
|
||||
version_observed: version !== 'unobserved',
|
||||
}
|
||||
}
|
||||
|
||||
function safeDiffName(name) {
|
||||
const stem = name.normalize('NFKD').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'state'
|
||||
return `${stem}-${hash(Buffer.from(name, 'utf8')).slice(0, 12)}-diff.png`
|
||||
}
|
||||
|
||||
async function runElectronHelper(job) {
|
||||
const helperDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-visual-pixel-helper-'))
|
||||
const helperPath = path.join(helperDirectory, 'helper.cjs')
|
||||
const packagePath = path.join(helperDirectory, 'package.json')
|
||||
const jobPath = path.join(helperDirectory, 'job.json')
|
||||
const resultPath = path.join(helperDirectory, 'result.json')
|
||||
await fs.writeFile(helperPath, electronHelperSource, 'utf8')
|
||||
await fs.writeFile(packagePath, JSON.stringify({ name: 'ycode-visual-pixel-helper', version: '1.0.0', main: 'helper.cjs' }), 'utf8')
|
||||
await fs.writeFile(jobPath, JSON.stringify(job), 'utf8')
|
||||
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'ELECTRON_RUN_AS_NODE'))
|
||||
env.YCODE_PIXEL_JOB = jobPath
|
||||
env.YCODE_PIXEL_RESULT = resultPath
|
||||
try {
|
||||
const completion = await new Promise((resolve, reject) => {
|
||||
const child = spawn(electronExecutable, ['--no-sandbox', '--disable-gpu', '--disable-extensions', helperDirectory], {
|
||||
cwd: helperDirectory,
|
||||
env,
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
})
|
||||
let stderr = ''
|
||||
let settled = false
|
||||
const timer = setTimeout(() => {
|
||||
child.kill()
|
||||
if (!settled) {
|
||||
settled = true
|
||||
reject(new Error('Hidden Electron nativeImage helper timed out'))
|
||||
}
|
||||
}, 45000)
|
||||
timer.unref()
|
||||
child.stderr.on('data', chunk => { stderr += chunk.toString() })
|
||||
child.once('error', error => {
|
||||
clearTimeout(timer)
|
||||
if (!settled) { settled = true; reject(error) }
|
||||
})
|
||||
child.once('close', (code, signal) => {
|
||||
clearTimeout(timer)
|
||||
if (settled) return
|
||||
settled = true
|
||||
resolve({ code, signal, stderr })
|
||||
})
|
||||
})
|
||||
let result
|
||||
try { result = JSON.parse(await fs.readFile(resultPath, 'utf8')) } catch {
|
||||
throw new Error(`Hidden Electron nativeImage helper exited without a result (code ${completion.code}${completion.stderr ? `): ${completion.stderr.trim()}` : ')'}`)
|
||||
}
|
||||
if (!result.ok || completion.code !== 0) throw new Error(result.error || `Hidden Electron nativeImage helper failed (code ${completion.code})`)
|
||||
return result
|
||||
} finally {
|
||||
await fs.rm(helperDirectory, { recursive: true, force: true, maxRetries: 8, retryDelay: 200 })
|
||||
}
|
||||
}
|
||||
|
||||
async function compareManifest(manifest, manifestDirectory, outputDirectory) {
|
||||
const parsed = parseManifest(manifest)
|
||||
const prepared = []
|
||||
for (const pair of parsed.pairs) {
|
||||
const referencePath = path.resolve(manifestDirectory, pair.reference)
|
||||
const candidatePath = path.resolve(manifestDirectory, pair.candidate)
|
||||
let referenceBytes, candidateBytes
|
||||
try {
|
||||
[referenceBytes, candidateBytes] = await Promise.all([fs.readFile(referencePath), fs.readFile(candidatePath)])
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot read screenshot pair ${pair.name}: ${error.message}`)
|
||||
}
|
||||
const referenceSha = hash(referenceBytes)
|
||||
const candidateSha = hash(candidateBytes)
|
||||
const referenceEvidence = parsed.source.kind === 'rea-workbuddy'
|
||||
? await verifyReferenceEvidence(pair, manifestDirectory, referenceSha)
|
||||
: undefined
|
||||
prepared.push({
|
||||
name: pair.name,
|
||||
reference: referencePath,
|
||||
candidate: candidatePath,
|
||||
reference_sha256: referenceSha,
|
||||
candidate_sha256: candidateSha,
|
||||
region: pair.region,
|
||||
reference_evidence: referenceEvidence,
|
||||
})
|
||||
}
|
||||
await fs.mkdir(outputDirectory, { recursive: true })
|
||||
const helperPairs = prepared.map(pair => ({
|
||||
name: pair.name,
|
||||
reference: pair.reference,
|
||||
candidate: pair.candidate,
|
||||
region: pair.region,
|
||||
diffPath: path.join(outputDirectory, safeDiffName(pair.name)),
|
||||
}))
|
||||
const nativeResult = await runElectronHelper({ mode: 'compare', channelTolerance: parsed.channelTolerance, pairs: helperPairs })
|
||||
const nativeByName = new Map(nativeResult.pairs.map(pair => [pair.name, pair]))
|
||||
const results = prepared.map(pair => {
|
||||
const compared = nativeByName.get(pair.name)
|
||||
if (!compared) throw new Error(`nativeImage helper omitted comparison ${pair.name}`)
|
||||
const dimensions = { width: compared.before.width, height: compared.before.height }
|
||||
const afterDimensions = { width: compared.after.width, height: compared.after.height }
|
||||
const diffPath = compared.diff_width === null ? undefined : helperPairs.find(item => item.name === pair.name).diffPath
|
||||
return {
|
||||
name: pair.name,
|
||||
status: compared.status,
|
||||
reference: { path: pair.reference, sha256: pair.reference_sha256, dimensions },
|
||||
candidate: { path: pair.candidate, sha256: pair.candidate_sha256, dimensions: afterDimensions },
|
||||
...(compared.region ? { region: compared.region } : {}),
|
||||
channel_tolerance: compared.channel_tolerance,
|
||||
compared_pixels: compared.compared_pixels,
|
||||
changed_pixels: compared.changed_pixels,
|
||||
changed_ratio: compared.changed_ratio,
|
||||
mean_absolute_channel_delta: compared.mean_absolute_channel_delta,
|
||||
maximum_channel_delta: compared.maximum_channel_delta,
|
||||
diff_png: diffPath ? { path: diffPath, sha256: undefined, dimensions: { width: compared.diff_width, height: compared.diff_height } } : null,
|
||||
...(pair.reference_evidence ? { reference_evidence: pair.reference_evidence } : {}),
|
||||
}
|
||||
})
|
||||
for (const pair of results) {
|
||||
if (pair.diff_png) pair.diff_png.sha256 = hash(await fs.readFile(pair.diff_png.path))
|
||||
}
|
||||
const missingRequiredStates = parsed.requiredStates.filter(name => !parsed.pairs.some(pair => pair.name === name))
|
||||
const status = missingRequiredStates.length
|
||||
? 'incomplete'
|
||||
: results.some(pair => pair.status !== 'identical') ? 'different' : 'identical'
|
||||
const observedVersions = results.flatMap(pair => pair.reference_evidence ? [{
|
||||
state: pair.name,
|
||||
evidence_id: pair.reference_evidence.id,
|
||||
version: pair.reference_evidence.version,
|
||||
observed: pair.reference_evidence.version_observed,
|
||||
}] : [])
|
||||
let originalParity
|
||||
if (parsed.source.kind !== 'rea-workbuddy') {
|
||||
originalParity = { status: 'not_assessed', exactOriginalParity: false, versions: [], reasons: ['Source is local fixture data, not authenticated WorkBuddy Evidence.'] }
|
||||
} else {
|
||||
const reasons = []
|
||||
if (missingRequiredStates.length) reasons.push('One or more required states are missing.')
|
||||
if (observedVersions.length !== results.length || observedVersions.some(item => !item.observed)) reasons.push('A WorkBuddy version was not observed in browser.user_agent.')
|
||||
if (reasons.length) originalParity = { status: 'incomplete', exactOriginalParity: false, versions: observedVersions, reasons }
|
||||
else if (results.some(pair => pair.status !== 'identical')) originalParity = { status: 'different', exactOriginalParity: false, versions: observedVersions, reasons: ['At least one declared state differs beyond channel tolerance.'] }
|
||||
else if (parsed.channelTolerance !== 0) originalParity = { status: 'not_exact', exactOriginalParity: false, versions: observedVersions, reasons: ['Non-zero channel tolerance does not establish exact pixel parity.'] }
|
||||
else originalParity = { status: 'exact_for_declared_states', exactOriginalParity: true, versions: observedVersions, reasons: [] }
|
||||
}
|
||||
const reportStatus = parsed.source.kind === 'rea-workbuddy' && originalParity.status === 'incomplete'
|
||||
? 'incomplete'
|
||||
: parsed.source.kind === 'rea-workbuddy' && originalParity.status === 'not_exact'
|
||||
? 'approximate'
|
||||
: status
|
||||
return {
|
||||
status: reportStatus,
|
||||
source: parsed.source,
|
||||
originalParity,
|
||||
scope: {
|
||||
assessment: 'declared_state_set_only',
|
||||
required_states: parsed.requiredStates,
|
||||
full_1to1: 'not_assessed',
|
||||
},
|
||||
missing_required_states: missingRequiredStates,
|
||||
channel_tolerance: parsed.channelTolerance,
|
||||
metric_definition: 'Changed pixels have at least one of four decoded bitmap components above tolerance. Mean absolute channel delta is averaged across all four components in the compared region.',
|
||||
limitations: [
|
||||
'Only the manifest state set is assessed; this output is not a complete 1:1 product acceptance.',
|
||||
'Images are decoded to nativeImage 8-bit bitmaps; their platform-specific byte order is compared consistently within this run. No registration, OCR, or semantic layout analysis is performed.',
|
||||
'Diff PNG marks changed pixels white and unchanged pixels transparent.',
|
||||
],
|
||||
pairs: results,
|
||||
}
|
||||
}
|
||||
|
||||
async function runSelfTest() {
|
||||
const fixture = await fs.mkdtemp(path.join(os.tmpdir(), 'ycode-visual-pixels-self-test-'))
|
||||
try {
|
||||
assert.throws(() => parseManifest({ source: { kind: 'local-fixture' }, requiredStates: [], pairs: [] }), /non-empty array/)
|
||||
const files = {
|
||||
reference: path.join(fixture, 'reference.png'),
|
||||
changed: path.join(fixture, 'changed.png'),
|
||||
wide: path.join(fixture, 'wide.png'),
|
||||
}
|
||||
await runElectronHelper({ mode: 'fixtures', files })
|
||||
const manifest = {
|
||||
source: { kind: 'local-fixture' },
|
||||
requiredStates: ['same', 'one-pixel', 'dimension-mismatch'],
|
||||
pairs: [
|
||||
{ name: 'same', reference: files.reference, candidate: files.reference },
|
||||
{ name: 'one-pixel', reference: files.reference, candidate: files.changed, region: { x: 0, y: 0, width: 1, height: 1 } },
|
||||
{ name: 'dimension-mismatch', reference: files.reference, candidate: files.wide },
|
||||
],
|
||||
}
|
||||
const report = await compareManifest(manifest, fixture, path.join(fixture, 'diffs'))
|
||||
assert.equal(report.status, 'different')
|
||||
assert.equal(report.scope.full_1to1, 'not_assessed')
|
||||
assert.equal(report.originalParity.status, 'not_assessed')
|
||||
assert.equal(report.originalParity.exactOriginalParity, false)
|
||||
const same = report.pairs.find(pair => pair.name === 'same')
|
||||
assert.equal(same.status, 'identical')
|
||||
assert.equal(same.compared_pixels, 4)
|
||||
assert.equal(same.changed_pixels, 0)
|
||||
assert.equal(same.changed_ratio, 0)
|
||||
assert.equal(same.maximum_channel_delta, 0)
|
||||
assert.equal(same.mean_absolute_channel_delta, 0)
|
||||
const changed = report.pairs.find(pair => pair.name === 'one-pixel')
|
||||
assert.equal(changed.status, 'different')
|
||||
assert.equal(changed.changed_pixels, 1)
|
||||
assert.equal(changed.changed_ratio, 1)
|
||||
assert.equal(changed.maximum_channel_delta, 1)
|
||||
assert.equal(changed.mean_absolute_channel_delta, 0.25)
|
||||
assert.deepEqual(changed.diff_png.dimensions, { width: 1, height: 1 })
|
||||
assert.match(changed.diff_png.sha256, /^[a-f0-9]{64}$/)
|
||||
assert.ok((await fs.stat(changed.diff_png.path)).size > 0)
|
||||
const mismatch = report.pairs.find(pair => pair.name === 'dimension-mismatch')
|
||||
assert.equal(mismatch.status, 'dimension_mismatch')
|
||||
assert.deepEqual(mismatch.reference.dimensions, { width: 2, height: 2 })
|
||||
assert.deepEqual(mismatch.candidate.dimensions, { width: 3, height: 2 })
|
||||
assert.equal(mismatch.changed_pixels, null)
|
||||
assert.equal(mismatch.diff_png, null)
|
||||
|
||||
const incomplete = await compareManifest({
|
||||
source: { kind: 'local-fixture' },
|
||||
requiredStates: ['present', 'missing'],
|
||||
pairs: [{ name: 'present', reference: files.reference, candidate: files.reference }],
|
||||
}, fixture, path.join(fixture, 'incomplete-diffs'))
|
||||
assert.equal(incomplete.status, 'incomplete')
|
||||
assert.deepEqual(incomplete.missing_required_states, ['missing'])
|
||||
|
||||
const { createEvidence } = await import(pathToFileURL(path.join(runtime, 'domain/evidence.js')))
|
||||
const referenceBytes = await fs.readFile(files.reference)
|
||||
const changedBytes = await fs.readFile(files.changed)
|
||||
const asArtifact = bytes => ({ sha256: hash(bytes), bytes: bytes.length, media_type: 'image/png', data_base64: bytes.toString('base64') })
|
||||
const baselineArtifact = asArtifact(referenceBytes)
|
||||
const probeArtifact = asArtifact(changedBytes)
|
||||
const workbuddyTargetPath = 'C:\\Program Files\\WorkBuddy\\resources\\app.asar\\renderer\\index.html'
|
||||
const syntheticEvidence = ({ visuals, targetFilePath = workbuddyTargetPath, userAgent = 'WorkBuddy/5.7.0 Electron/37.10.3', actions = [], authority = actions.length ? 'controlled-replay' : 'external-service' }) => createEvidence(undefined, {
|
||||
id: 'rea-cdp-electron',
|
||||
name: 'Synthetic WorkBuddy REA fixture provider',
|
||||
version: 'self-test',
|
||||
}, {
|
||||
predicateType: 'rea.electron-page-inspection',
|
||||
operation: 'inspect_electron_page',
|
||||
parameters: actions.length ? { visual_probe: { actions } } : {},
|
||||
result: { browser: { user_agent: userAgent }, target: { file_path: targetFilePath }, visuals },
|
||||
confidence: 'observed',
|
||||
authority,
|
||||
limitations: ['Synthetic self-test fixture only; not captured from WorkBuddy.'],
|
||||
})
|
||||
const authenticatedEvidence = syntheticEvidence({
|
||||
visuals: { artifact: baselineArtifact, probe: { frames: [{ step_id: 'probe-step', at_ms: 0, artifact: probeArtifact }] } },
|
||||
actions: [{ kind: 'click', step_id: 'synthetic-probe-step' }],
|
||||
})
|
||||
const archivePath = path.join(fixture, 'synthetic-evidence.json')
|
||||
await fs.writeFile(archivePath, JSON.stringify(authenticatedEvidence), 'utf8')
|
||||
const evidenced = await compareManifest({
|
||||
source: { kind: 'rea-workbuddy' },
|
||||
requiredStates: ['baseline', 'probe'],
|
||||
pairs: [
|
||||
{ name: 'baseline', reference: files.reference, candidate: files.reference, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: archivePath } },
|
||||
{ name: 'probe', reference: files.changed, candidate: files.changed, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: archivePath } },
|
||||
],
|
||||
}, fixture, path.join(fixture, 'evidenced-diffs'))
|
||||
assert.equal(evidenced.status, 'identical')
|
||||
assert.equal(evidenced.originalParity.status, 'exact_for_declared_states')
|
||||
assert.equal(evidenced.originalParity.exactOriginalParity, true)
|
||||
assert.equal(evidenced.originalParity.versions[0].version, '5.7.0')
|
||||
assert.equal(evidenced.pairs[0].reference_evidence.authority, 'controlled-replay')
|
||||
assert.equal(evidenced.scope.assessment, 'declared_state_set_only')
|
||||
assert.equal(evidenced.scope.full_1to1, 'not_assessed')
|
||||
assert.deepEqual(evidenced.pairs[0].reference_evidence.artifact_locations, ['visuals.artifact'])
|
||||
assert.deepEqual(evidenced.pairs[1].reference_evidence.artifact_locations, ['visuals.probe.frames[0].artifact'])
|
||||
|
||||
const localFixtureEvidence = syntheticEvidence({
|
||||
visuals: { artifact: baselineArtifact },
|
||||
targetFilePath: path.join(fixture, 'renderer', 'index.html'),
|
||||
})
|
||||
await fs.writeFile(archivePath, JSON.stringify(localFixtureEvidence), 'utf8')
|
||||
const localMasqueradeManifest = {
|
||||
source: { kind: 'rea-workbuddy' },
|
||||
requiredStates: ['local-fixture'],
|
||||
pairs: [{ name: 'local-fixture', reference: files.reference, candidate: files.reference, referenceEvidence: { id: localFixtureEvidence.evidence_id, archive: archivePath } }],
|
||||
}
|
||||
await assert.rejects(compareManifest(localMasqueradeManifest, fixture, path.join(fixture, 'local-masquerade-diffs')), /target is not the WorkBuddy renderer/)
|
||||
|
||||
const falseBindingEvidence = syntheticEvidence({ visuals: { artifact: probeArtifact } })
|
||||
const workbuddyManifest = {
|
||||
source: { kind: 'rea-workbuddy' },
|
||||
requiredStates: ['forged'],
|
||||
pairs: [{ name: 'forged', reference: files.reference, candidate: files.reference, referenceEvidence: { id: falseBindingEvidence.evidence_id, archive: archivePath } }],
|
||||
}
|
||||
const forgedEvidenceId = `ev_${'0'.repeat(64)}`
|
||||
await fs.writeFile(archivePath, JSON.stringify({ ...falseBindingEvidence, evidence_id: forgedEvidenceId }), 'utf8')
|
||||
const forgedIdManifest = {
|
||||
...workbuddyManifest,
|
||||
pairs: [{ ...workbuddyManifest.pairs[0], referenceEvidence: { id: forgedEvidenceId, archive: archivePath } }],
|
||||
}
|
||||
await assert.rejects(compareManifest(forgedIdManifest, fixture, path.join(fixture, 'forged-id-diffs')), /authentication|identifier|digest|Evidence/i)
|
||||
await fs.writeFile(archivePath, JSON.stringify(falseBindingEvidence), 'utf8')
|
||||
await assert.rejects(compareManifest(workbuddyManifest, fixture, path.join(fixture, 'unclaimed-reference-diffs')), /SHA is absent/)
|
||||
|
||||
const unversionedEvidence = syntheticEvidence({ visuals: { artifact: baselineArtifact }, userAgent: 'Chromium/123.0' })
|
||||
await fs.writeFile(archivePath, JSON.stringify(unversionedEvidence), 'utf8')
|
||||
const unversioned = await compareManifest({
|
||||
source: { kind: 'rea-workbuddy' },
|
||||
requiredStates: ['unversioned'],
|
||||
pairs: [{ name: 'unversioned', reference: files.reference, candidate: files.reference, referenceEvidence: { id: unversionedEvidence.evidence_id, archive: archivePath } }],
|
||||
}, fixture, path.join(fixture, 'unversioned-diffs'))
|
||||
assert.equal(unversioned.status, 'incomplete')
|
||||
assert.equal(unversioned.originalParity.status, 'incomplete')
|
||||
assert.equal(unversioned.originalParity.exactOriginalParity, false)
|
||||
assert.equal(unversioned.originalParity.versions[0].version, 'unobserved')
|
||||
|
||||
await fs.writeFile(archivePath, JSON.stringify(authenticatedEvidence), 'utf8')
|
||||
const approximate = await compareManifest({
|
||||
source: { kind: 'rea-workbuddy' },
|
||||
requiredStates: ['tolerant'],
|
||||
channelTolerance: 1,
|
||||
pairs: [{ name: 'tolerant', reference: files.reference, candidate: files.reference, referenceEvidence: { id: authenticatedEvidence.evidence_id, archive: path.join(fixture, 'synthetic-evidence.json') } }],
|
||||
}, fixture, path.join(fixture, 'tolerant-diffs'))
|
||||
assert.equal(approximate.status, 'approximate')
|
||||
assert.equal(approximate.originalParity.status, 'not_exact')
|
||||
assert.equal(approximate.originalParity.exactOriginalParity, false)
|
||||
|
||||
const mismatchedAuthority = syntheticEvidence({
|
||||
visuals: { artifact: baselineArtifact },
|
||||
actions: [{ kind: 'click', step_id: 'synthetic-action' }],
|
||||
authority: 'external-service',
|
||||
})
|
||||
await fs.writeFile(archivePath, JSON.stringify(mismatchedAuthority), 'utf8')
|
||||
await assert.rejects(compareManifest({
|
||||
source: { kind: 'rea-workbuddy' },
|
||||
requiredStates: ['authority'],
|
||||
pairs: [{ name: 'authority', reference: files.reference, candidate: files.reference, referenceEvidence: { id: mismatchedAuthority.evidence_id, archive: archivePath } }],
|
||||
}, fixture, path.join(fixture, 'authority-diffs')), /authority does not match visual_probe.actions/)
|
||||
console.log('Visual pixel self-test passed: pixel comparison, incomplete required states, authenticated synthetic baseline/probe binding, local-target masquerade rejection, observed-version and authority checks, nonzero-tolerance exactness rejection, and forged Evidence rejection')
|
||||
} finally {
|
||||
await fs.rm(fixture, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length === 1 && args[0] === '--self-test') {
|
||||
await runSelfTest()
|
||||
return
|
||||
}
|
||||
const manifestPath = args[0]
|
||||
if (!manifestPath || manifestPath.startsWith('--')) throw new Error('Usage: node scripts/rea/compare-visual-pixels.mjs <manifest.json> [--output <directory>] | --self-test')
|
||||
let outputDirectory
|
||||
for (let index = 1; index < args.length; index += 1) {
|
||||
if (args[index] !== '--output' || !args[index + 1]) throw new Error('Expected --output followed by a directory')
|
||||
outputDirectory = path.resolve(args[++index])
|
||||
}
|
||||
const absoluteManifest = path.resolve(manifestPath)
|
||||
const raw = JSON.parse(await fs.readFile(absoluteManifest, 'utf8'))
|
||||
const parsed = parseManifest(raw)
|
||||
const report = await compareManifest(parsed, path.dirname(absoluteManifest), outputDirectory ?? path.join(path.dirname(absoluteManifest), 'visual-pixel-diffs'))
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
||||
if (report.status !== 'identical') process.exitCode = 1
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
process.stderr.write(`${error.stack || error.message || error}\n`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import crypto from 'node:crypto'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
const root=await fs.mkdtemp(path.resolve('.tmp/rea-cpp-binary-fixture-')),target=path.join(root,'target'),checks=[]
|
||||
await fs.mkdir(path.join(target,'avatars'),{recursive:true})
|
||||
const png=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a6hoAAAAASUVORK5CYII=','base64'),hash=b=>crypto.createHash('sha256').update(b).digest('hex')
|
||||
await fs.writeFile(path.join(target,'avatars/expert.png'),png)
|
||||
function cli(command,args){return spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:32*1024*1024})}
|
||||
function inspect(){const r=cli('inspect-selected-directory-text-root',[target]);assert.equal(r.status,0,r.stderr);return JSON.parse(r.stdout).normalized_result}
|
||||
function run(pin,selection){return cli('export-selected-directory-binary',[target,pin,JSON.stringify(selection)])}
|
||||
function reject(label,r,pattern){assert.notEqual(r.status,0,label);assert.match(r.stderr,pattern,label);checks.push({label,pass:true})}
|
||||
let inventory=inspect(),pin=inventory.root_sha256,selected=[{path:'avatars/expert.png',sha256:hash(png)}],r=run(pin,selected)
|
||||
assert.equal(r.status,0,r.stderr);const evidence=JSON.parse(r.stdout),f=evidence.normalized_result.files[0]
|
||||
assert.equal(evidence.operation,'export_selected_directory_binary');assert.equal(f.media_type,'image/png');assert.equal(f.encoding,'base64');assert.equal(f.sha256,hash(png));assert.deepEqual(Buffer.from(f.data_base64,'base64'),png);assert.equal(f.size_bytes,png.length);assert.equal('text' in f,false);checks.push({label:'actual own PNG public CLI export is exact bytes/SHA/type and base64 Evidence',pass:true})
|
||||
reject('wrong root SHA',run('0'.repeat(64),selected),/root SHA mismatch/)
|
||||
reject('wrong selected PNG SHA',run(pin,[{...selected[0],sha256:'0'.repeat(64)}]),/selected PNG SHA mismatch/)
|
||||
reject('path traversal',run(pin,[{path:'../expert.png',sha256:hash(png)}]),/canonical/)
|
||||
reject('unsupported binary extension',run(pin,[{path:'avatars/expert.jpg',sha256:hash(png)}]),/canonical/)
|
||||
reject('missing selected member',run(pin,[{path:'avatars/missing.png',sha256:hash(png)}]),/absent/)
|
||||
reject('duplicate paths',run(pin,[...selected,...selected]),/Duplicate/)
|
||||
reject('required explicit image SHA',run(pin,[{path:'avatars/expert.png'}]),/SHA required/)
|
||||
await fs.writeFile(path.join(target,'avatars/fake.png'),'not an image');inventory=inspect();reject('PNG signature validated',run(inventory.root_sha256,[{path:'avatars/fake.png',sha256:hash('not an image')}]),/signature/)
|
||||
const large=Buffer.alloc(8*1024*1024+1,97);png.subarray(0,8).copy(large);await fs.writeFile(path.join(target,'avatars/large.png'),large);inventory=inspect();reject('binary per-file budget',run(inventory.root_sha256,[{path:'avatars/large.png',sha256:hash(large)}]),/byte budget/)
|
||||
await fs.rm(path.join(target,'avatars/large.png'));const exact=large.subarray(0,8*1024*1024);for(const name of['one','two'])await fs.writeFile(path.join(target,'avatars/'+name+'.png'),exact);inventory=inspect();reject('binary aggregate budget',run(inventory.root_sha256,[{path:'avatars/one.png',sha256:hash(exact)},{path:'avatars/two.png',sha256:hash(exact)},...selected]),/byte budget/)
|
||||
await fs.writeFile(path.join(root,'report.json'),JSON.stringify({result:'PASS',checks,evidenceId:evidence.evidence_id},null,2));await fs.writeFile(path.join(root,'evidence.json'),JSON.stringify(evidence,null,2));console.log(JSON.stringify({result:'PASS',root,checks:checks.length,evidenceId:evidence.evidence_id}))
|
||||
@@ -0,0 +1,73 @@
|
||||
/** Formal local REA directory adapter. Uses REA inventory and stable non-following reader. */
|
||||
import path from 'node:path'
|
||||
import {lstat,realpath} from 'node:fs/promises'
|
||||
import {createHash} from 'node:crypto'
|
||||
import {DirectoryArtifactReader} from './DirectoryArtifactReader.js'
|
||||
import {ArtifactResourceScope} from './ArtifactResourceScope.js'
|
||||
import {scanCanonicalArtifactInventory} from './inventory/scanCanonical.js'
|
||||
import {createEvidence} from '../domain/evidence.js'
|
||||
const FILE=8*1024*1024,TOTAL=16*1024*1024,SCAN=64*1024*1024,ENTRIES=4096
|
||||
const sha=s=>createHash('sha256').update(s).digest('hex')
|
||||
const textPath=p=>/\.(js|cjs|mjs|ts|css|json|md|txt|yaml|yml|py|html|cs|csproj|slnx|xml|xsd|sh|ps1|LICENSE)$/i.test(p)||['LICENSE','.gitignore'].includes(path.posix.basename(p))
|
||||
function selection(paths,binary=false){if(!Array.isArray(paths)||!paths.length||paths.length>16)throw Error('Select 1–16 exact paths');const seen=new Set;for(const p of paths){if(typeof p!=='string'||!p||p.includes('\\')||p.includes('\0')||path.posix.normalize(p)!==p||p.startsWith('/')||/^[a-z]:/i.test(p)||p.split('/').some(x=>!x||x==='.'||x==='..')||!(binary?/\.(png|ttf)$/i.test(p):textPath(p)))throw Error('Invalid canonical supported artifact path');if(seen.has(p))throw Error('Duplicate selected path');seen.add(p)}return paths}
|
||||
async function rootIdentity(input){if(typeof input!=='string'||!path.isAbsolute(input))throw Error('Directory root must be explicitly absolute');const resolved=path.resolve(input);let current=path.parse(resolved).root;for(const part of path.relative(current,resolved).split(path.sep)){current=path.join(current,part);if((await lstat(current)).isSymbolicLink())throw Error('Directory root cannot contain symlink ancestors')}const canonical=await realpath(resolved),meta=await lstat(canonical,{bigint:true});if(!meta.isDirectory()||meta.isSymbolicLink())throw Error('Root is not an ordinary directory');return{path:canonical,dev:String(meta.dev),ino:String(meta.ino)}}
|
||||
class BoundedReader extends DirectoryArtifactReader{async *entries(signal,filter){let bytes=0,count=0;for await(const entry of super.entries(signal,filter)){if(++count>ENTRIES)throw Error('Directory inventory entry budget exceeded');if(entry.kind==='file'){bytes+=entry.declaredSize;if(!Number.isSafeInteger(entry.declaredSize)||entry.declaredSize<0||bytes>SCAN)throw Error('Directory inventory byte budget exceeded')}yield entry}}}
|
||||
async function inventory(input){const scope=new ArtifactResourceScope;try{return await scanCanonicalArtifactInventory(input,{resourceScope:scope,integrity:{mode:'fail'}},(root,format)=>{if(format!=='directory')throw Error('Directory format required');return new BoundedReader(root)})}finally{await scope.close()}}
|
||||
function members(snapshot){const nodes=new Map(snapshot.nodes.map(n=>[n.artifact_id,n]));return snapshot.occurrences.filter(o=>o.logical_path!=='.').map(o=>({path:o.logical_path,kind:o.entry_kind,size_bytes:o.size_bytes??o.declared_size??null,sha256:nodes.get(o.artifact_id)?.sha256??null})).sort((a,b)=>a.path.localeCompare(b.path))}
|
||||
export async function inspectSelectedDirectory(input){const identity=await rootIdentity(input),snapshot=await inventory(identity.path);return createEvidence({path:identity.path,sha256:snapshot.manifest.root_sha256,format:'directory'},{id:'rea-selected-directory-text',name:'REA bounded directory text adapter',version:'1+local'}, {predicateType:'rea.artifact.directory-inventory',operation:'inspect_selected_directory_text_root',parameters:{max_inventory_bytes:SCAN,max_inventory_entries:ENTRIES},result:{root_sha256:snapshot.manifest.root_sha256,root_identity:identity,inventory_graph_sha256:snapshot.manifest.graph_sha256,coverage:snapshot.coverage,members:members(snapshot)},confidence:'observed',authority:'shipped-artifact',limitations:['Directory content root digest uses the official REA canonical inventory. All inventoried member streams are hashed within independent inventory budgets; source text is not exported by this operation. Symlink targets are never followed.'],locations:[{kind:'artifact-path',path:identity.path}]})}
|
||||
export async function exportSelectedDirectoryText(input,expectedRoot,paths){return exportSelectedDirectoryArtifact(input,expectedRoot,paths,false,{})}
|
||||
export async function exportSelectedDirectoryBinary(input,expectedRoot,selected){
|
||||
if(!Array.isArray(selected))throw Error('Select exact PNG/TTF paths and SHA values')
|
||||
const hashes=Object.create(null)
|
||||
for(const item of selected){if(!item||typeof item.path!=='string'||!/^[a-f0-9]{64}$/.test(item.sha256))throw Error('Expected selected binary SHA required');hashes[item.path]=item.sha256}
|
||||
return exportSelectedDirectoryArtifact(input,expectedRoot,selected.map(item=>item.path),true,hashes)
|
||||
}
|
||||
async function exportSelectedDirectoryArtifact(input,expectedRoot,paths,binary,expectedHashes){
|
||||
selection(paths,binary)
|
||||
if(!/^[a-f0-9]{64}$/.test(expectedRoot))throw Error('Expected directory SHA must be64 lowercase hex')
|
||||
const identity=await rootIdentity(input),snapshot=await inventory(identity.path)
|
||||
if(snapshot.manifest.root_sha256!==expectedRoot)throw Error('Directory root SHA mismatch before selected export')
|
||||
const listed=members(snapshot),reader=new BoundedReader(identity.path),entries=new Map,wanted=new Set(paths)
|
||||
let total=0
|
||||
try{
|
||||
for await(const e of reader.entries(undefined,{descendInto:p=>paths.some(s=>s.startsWith(p+'/'))})){if(wanted.has(e.path)){if(entries.has(e.path))throw Error('Ambiguous selected member');entries.set(e.path,e)}}
|
||||
const files=[]
|
||||
for(const p of paths){
|
||||
const entry=entries.get(p),known=listed.filter(e=>e.path===p)
|
||||
if(!entry||known.length!==1||entry.kind!=='file')throw Error('Selected ordinary member absent: '+p)
|
||||
if(binary&&known[0].sha256!==expectedHashes[p])throw Error('Expected selected PNG SHA mismatch')
|
||||
if(entry.declaredSize>FILE||total+entry.declaredSize>TOTAL)throw Error('Selected text byte budget exceeded')
|
||||
const chunks=[];let size=0
|
||||
for await(const b of await reader.open(entry)){size+=b.length;if(size>FILE||size>entry.declaredSize||total+size>TOTAL)throw Error('Selected text byte budget exceeded');chunks.push(b)}
|
||||
const bytes=Buffer.concat(chunks),digest=sha(bytes)
|
||||
if(size!==entry.declaredSize||digest!==known[0].sha256||(binary&&digest!==expectedHashes[p]))throw Error('Selected member changed or SHA mismatch')
|
||||
if(binary){
|
||||
let mediaType
|
||||
if(/\.png$/i.test(p)){
|
||||
if(!bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))throw Error('Selected PNG signature mismatch')
|
||||
mediaType='image/png'
|
||||
}else{
|
||||
if(bytes.length<12||![0x00010000,0x74727565].includes(bytes.readUInt32BE(0)))throw Error('Selected TTF signature mismatch')
|
||||
const tables=bytes.readUInt16BE(4),directoryEnd=12+16*tables
|
||||
if(!tables||tables>4096||directoryEnd>bytes.length)throw Error('Selected TTF table directory invalid')
|
||||
for(let i=0;i<tables;i++){const start=12+16*i,offset=bytes.readUInt32BE(start+8),length=bytes.readUInt32BE(start+12);if(offset<directoryEnd||offset+length>bytes.length)throw Error('Selected TTF table range invalid')}
|
||||
mediaType='font/ttf'
|
||||
}
|
||||
files.push({path:p,size_bytes:size,sha256:digest,encoding:'base64',media_type:mediaType,data_base64:bytes.toString('base64')})
|
||||
}else{
|
||||
let text;try{text=new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes)}catch{throw Error('Selected member is not strict UTF-8')}
|
||||
files.push({path:p,size_bytes:size,sha256:digest,text})
|
||||
}
|
||||
total+=size
|
||||
}
|
||||
const after=await rootIdentity(input)
|
||||
if(JSON.stringify(after)!==JSON.stringify(identity))throw Error('Directory physical root identity changed')
|
||||
const verified=await inventory(identity.path)
|
||||
if(verified.manifest.root_sha256!==expectedRoot)throw Error('Directory root SHA mismatch after selected export')
|
||||
return createEvidence({path:identity.path,sha256:expectedRoot,format:'directory'},{id:binary?'rea-selected-directory-binary':'rea-selected-directory-text',name:'REA bounded directory artifact adapter',version:'1+binary-local'},
|
||||
{predicateType:binary?'rea.artifact.selected-directory-binary':'rea.artifact.selected-directory-text',operation:binary?'export_selected_directory_binary':'export_selected_directory_text',
|
||||
parameters:{expected_root_sha256:expectedRoot,selected_paths:paths,...(binary?{expected_file_sha256:expectedHashes}:{}),max_file_bytes:FILE,max_total_bytes:TOTAL,max_inventory_bytes_per_pass:SCAN,max_inventory_entries:ENTRIES},
|
||||
result:{root_sha256:expectedRoot,root_identity:identity,inventory_graph_sha256:snapshot.manifest.graph_sha256,scope:{kind:'selected-files-only-export',selected_paths:paths,root_inventory:'Full bounded root inventory hashed before/after selected export; unselected content not exported.'},files},
|
||||
confidence:'observed',authority:'shipped-artifact',limitations:['Only selected artifacts are returned. Full directory inventory bytes are hashed to validate the same REA root content digest before and after export.','No target code is executed, no HTTP resource is requested, no symlink target is followed. Mutable directories can fail pin validation; do not silently replace the expected pin.'],locations:paths.map(p=>({kind:'artifact-path',path:p}))})
|
||||
}finally{await reader.close()}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import crypto from 'node:crypto'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
const root=await fs.mkdtemp(path.resolve('.tmp/rea-cpp-directory-fixture-')),target=path.join(root,'target'),checks=[]
|
||||
await fs.mkdir(path.join(target,'agents'),{recursive:true})
|
||||
const files={'plugin.json':'{"name":"cpp-fixture","agents":["agents/cpp.md"]}\n','agents/cpp.md':'---\nname: cpp\n---\n# 测试专家\n','schema.yml':'name: cpp\n','tool.py':'# Never execute retained text\n'}
|
||||
for(const[p,text]of Object.entries(files))await fs.writeFile(path.join(target,p),text)
|
||||
function cli(command,args){return spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',command,...args],{encoding:'utf8',maxBuffer:32*1024*1024})}
|
||||
function inspect(dir=target){const r=cli('inspect-selected-directory-text-root',[dir]);assert.equal(r.status,0,r.stderr);return JSON.parse(r.stdout)}
|
||||
function exportText(pin,paths,dir=target){return cli('export-selected-directory-text',[dir,pin,JSON.stringify(paths)])}
|
||||
function reject(label,r,pattern){assert.notEqual(r.status,0,label);assert.match(r.stderr,pattern,label);checks.push({label,pass:true})}
|
||||
let inventory=inspect(),pin=inventory.normalized_result.root_sha256
|
||||
const analysis=cli('analyze-javascript-application',[target,'--source-path','plugin.json','--json']);assert.equal(analysis.status,0,analysis.stderr);assert.equal(JSON.parse(analysis.stdout).normalized_result.root_artifact_sha256,pin);checks.push({label:'new provider pin equals public REA analyzer canonical directory SHA',pass:true})
|
||||
let r=exportText(pin,Object.keys(files));assert.equal(r.status,0,r.stderr);const evidence=JSON.parse(r.stdout)
|
||||
assert.deepEqual(evidence.normalized_result.files.map(f=>f.path),Object.keys(files));for(const f of evidence.normalized_result.files){assert.equal(f.text,files[f.path]);assert.equal(f.sha256,crypto.createHash('sha256').update(files[f.path]).digest('hex'))}checks.push({label:'exact selected bytes/SHA and UTF-8 source text exported through public CLI',pass:true})
|
||||
reject('incorrect root pin',exportText('0'.repeat(64),['plugin.json']),/root SHA mismatch/)
|
||||
reject('traversal selection',exportText(pin,['../plugin.json']),/canonical/)
|
||||
reject('unsupported selection',exportText(pin,['private.db']),/canonical/)
|
||||
reject('duplicate selection',exportText(pin,['plugin.json','plugin.json']),/Duplicate/)
|
||||
reject('absent member',exportText(pin,['agents/absent.md']),/absent/)
|
||||
await fs.writeFile(path.join(target,'bad.md'),Buffer.from([0xc3,0x28]));const bad=inspect().normalized_result.root_sha256;reject('strict UTF-8 rejects invalid selected bytes',exportText(bad,['bad.md']),/strict UTF-8/)
|
||||
reject('mutable content invalidates old root pin',exportText(pin,['plugin.json']),/root SHA mismatch/)
|
||||
await fs.writeFile(path.join(target,'large.md'),Buffer.alloc(8*1024*1024+1,97));const large=inspect().normalized_result.root_sha256;reject('per-file text budget',exportText(large,['large.md']),/text byte budget/)
|
||||
await fs.rm(path.join(target,'large.md'));for(const p of['one.md','two.md'])await fs.writeFile(path.join(target,p),Buffer.alloc(8*1024*1024,97));await fs.writeFile(path.join(target,'extra.txt'),'x');const aggregate=inspect().normalized_result.root_sha256;reject('aggregate text budget',exportText(aggregate,['one.md','two.md','extra.txt']),/text byte budget/)
|
||||
const outside=path.join(root,'outside');await fs.mkdir(outside);await fs.writeFile(path.join(outside,'secret.md'),'outside-source-not-exported')
|
||||
await fs.symlink(outside,path.join(root,'root-link'),'junction');reject('symlink root rejected',cli('inspect-selected-directory-text-root',[path.join(root,'root-link')]),/symlink ancestors/)
|
||||
await fs.symlink(outside,path.join(target,'link'),'junction');const linked=inspect().normalized_result.root_sha256;reject('symlink descendant never followed',exportText(linked,['link/secret.md']),/absent/)
|
||||
await fs.writeFile(path.join(root,'report.json'),JSON.stringify({result:'PASS',checks,evidenceId:evidence.evidence_id,pin},null,2));await fs.writeFile(path.join(root,'export-evidence.json'),JSON.stringify(evidence,null,2));console.log(JSON.stringify({result:'PASS',root,checks:checks.length,evidenceId:evidence.evidence_id}))
|
||||
@@ -0,0 +1,18 @@
|
||||
/** Exact avatar recovery from formal public REA binary export; no original FS access here. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
const analysis=JSON.parse(fs.readFileSync('.tmp/rea-cpp-expert-package-analysis-20261016.json','utf8'))
|
||||
const inventory=JSON.parse(fs.readFileSync('.tmp/rea-cpp-inventory-ev_d8bab73f2bffe8f99a6af2e4d50f9a820ec3ec8a4ea8cff3a4ef4bc9063a4a83.json','utf8')).normalized_result
|
||||
const member=inventory.members.find(m=>m.path==='avatars/expert.png'),pin='b90023e9622bd74c55d583051bee3625eb491b552baeea17ae10ac792a352498'
|
||||
if(inventory.root_sha256!==pin||analysis.normalized_result.root_artifact_sha256!==pin||member?.size_bytes!==420773)throw Error('Pinned inventory mismatch')
|
||||
const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs','export-selected-directory-binary',analysis.normalized_result.input_path,pin,JSON.stringify([{path:member.path,sha256:member.sha256}])],{encoding:'utf8',maxBuffer:8*1024*1024})
|
||||
if(r.status!==0)throw Error(r.stderr||r.stdout)
|
||||
const evidence=JSON.parse(r.stdout),file=evidence.normalized_result.files[0],bytes=Buffer.from(file.data_base64,'base64'),hash=b=>crypto.createHash('sha256').update(b).digest('hex')
|
||||
if(evidence.normalized_result.root_sha256!==pin||file.sha256!==member.sha256||hash(bytes)!==member.sha256||bytes.length!==420773)throw Error('REA binary SHA/size mismatch')
|
||||
const exportPath=`.tmp/rea-cpp-binary-${evidence.evidence_id}.json`;fs.writeFileSync(exportPath,JSON.stringify(evidence))
|
||||
const recovered='.tmp/rea-cpp-expert-recovered-20261016',output=path.join(recovered,member.path);fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,bytes)
|
||||
if(hash(fs.readFileSync(output))!==member.sha256)throw Error('Recovered binary SHA mismatch')
|
||||
const manifestPath=path.join(recovered,'evidence-manifest.json'),manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));manifest.files=manifest.files.filter(f=>f.path!==member.path);manifest.files.push({path:member.path,bytes:bytes.length,sha256:file.sha256,evidenceId:evidence.evidence_id,encoding:'base64',mediaType:'image/png',retainedExport:exportPath});fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+'\n')
|
||||
console.log(JSON.stringify({output,evidenceId:evidence.evidence_id,sha256:file.sha256,bytes:bytes.length,exportPath}))
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Original access goes exclusively through formal public REA directory CLI. */
|
||||
import fs from 'node:fs'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
import crypto from 'node:crypto'
|
||||
const saved=JSON.parse(fs.readFileSync(process.argv[2],'utf8')),analysis=saved.normalized_result?saved:saved.details.partial_observation
|
||||
const inventory=process.argv[3]==='--inventory'
|
||||
const r=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',...(inventory?['inspect-selected-directory-text-root',analysis.normalized_result.input_path]:['export-selected-directory-text',analysis.normalized_result.input_path,analysis.normalized_result.root_artifact_sha256,JSON.stringify(process.argv.slice(3))])],{encoding:'utf8',maxBuffer:32*1024*1024})
|
||||
if(r.status!==0)throw Error(r.stderr||r.stdout)
|
||||
const evidence=JSON.parse(r.stdout)
|
||||
if(evidence.normalized_result.root_sha256!==analysis.normalized_result.root_artifact_sha256)throw Error('Inventory root pin mismatch')
|
||||
for(const f of evidence.normalized_result.files??[])if(crypto.createHash('sha256').update(f.text).digest('hex')!==f.sha256)throw Error('Export SHA mismatch')
|
||||
const output=`.tmp/rea-cpp-${inventory?'inventory':'text'}-${evidence.evidence_id}.json`;fs.writeFileSync(output,JSON.stringify(evidence))
|
||||
console.log(JSON.stringify({output,evidenceId:evidence.evidence_id,root:evidence.normalized_result.root_sha256,files:inventory?evidence.normalized_result.members:evidence.normalized_result.files.map(({text,...m})=>m)},null,2))
|
||||
@@ -0,0 +1,7 @@
|
||||
/** Review copies only from retained REA text, never from original filesystem. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
const root='.tmp/rea-cpp-expert-recovered-20261016',pin='b90023e9622bd74c55d583051bee3625eb491b552baeea17ae10ac792a352498',manifest={rootSha256:pin,files:[]}
|
||||
for(const input of process.argv.slice(2)){const evidence=JSON.parse(fs.readFileSync(input,'utf8'));if(evidence.normalized_result.root_sha256!==pin)throw Error('Root mismatch');for(const f of evidence.normalized_result.files){const sha=crypto.createHash('sha256').update(f.text).digest('hex');if(sha!==f.sha256||Buffer.byteLength(f.text)!==f.size_bytes)throw Error('Text byte mismatch');if(f.path.split('/').some(p=>!p||p==='.'||p==='..')||f.path.includes('\\'))throw Error('Canonical path required');const output=path.join(root,...f.path.split('/'));fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,f.text);if(crypto.createHash('sha256').update(fs.readFileSync(output)).digest('hex')!==sha)throw Error('Recovered file SHA mismatch');manifest.files.push({path:f.path,bytes:f.size_bytes,sha256:f.sha256,evidenceId:evidence.evidence_id,retainedExport:input})}}
|
||||
fs.writeFileSync(path.join(root,'evidence-manifest.json'),JSON.stringify(manifest,null,2)+'\n');console.log(JSON.stringify({root,files:manifest.files.length,bytes:manifest.files.reduce((n,f)=>n+f.bytes,0)}))
|
||||
@@ -0,0 +1,35 @@
|
||||
/** Install formal provider/CLI in isolated REA runtime only; never reads original target. */
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
const root=path.resolve('.tmp/rea-css-runtime'),hash=s=>crypto.createHash('sha256').update(s).digest('hex')
|
||||
const source=await fs.readFile('scripts/rea/cpp-expert-directory-provider.mjs','utf8')
|
||||
await fs.writeFile(path.join(root,'dist/artifacts/SelectedDirectoryText.js'),source)
|
||||
let cli=await fs.readFile(path.join(root,'scripts/rea.mjs'),'utf8')
|
||||
if(!cli.includes('inspect-selected-directory-text-root')){
|
||||
const anchor='} else if (args[0] === "export-selected-artifact-text") {'
|
||||
if(cli.split(anchor).length!==2)throw Error('REA CLI anchor mismatch')
|
||||
cli=cli.replace(anchor,`} else if (args[0] === "inspect-selected-directory-text-root") {
|
||||
const { inspectSelectedDirectory } = await import("../dist/artifacts/SelectedDirectoryText.js");
|
||||
process.stdout.write(JSON.stringify(await inspectSelectedDirectory(args[1])) + "\\n");
|
||||
} else if (args[0] === "export-selected-directory-text") {
|
||||
const { exportSelectedDirectoryText } = await import("../dist/artifacts/SelectedDirectoryText.js");
|
||||
process.stdout.write(JSON.stringify(await exportSelectedDirectoryText(args[1],args[2],JSON.parse(args[3]))) + "\\n");
|
||||
${anchor}`)
|
||||
await fs.writeFile(path.join(root,'scripts/rea.mjs'),cli)
|
||||
}
|
||||
if(!cli.includes('export-selected-directory-binary')){
|
||||
const anchor='} else if (args[0] === "export-selected-artifact-text") {'
|
||||
if(cli.split(anchor).length!==2)throw Error('REA CLI binary anchor mismatch')
|
||||
cli=cli.replace(anchor,`} else if (args[0] === "export-selected-directory-binary") {
|
||||
const { exportSelectedDirectoryBinary } = await import("../dist/artifacts/SelectedDirectoryText.js");
|
||||
process.stdout.write(JSON.stringify(await exportSelectedDirectoryBinary(args[1],args[2],JSON.parse(args[3]))) + "\\n");
|
||||
${anchor}`)
|
||||
await fs.writeFile(path.join(root,'scripts/rea.mjs'),cli)
|
||||
}
|
||||
const manifestPath=path.join(root,'css-repair-manifest.json'),manifest=JSON.parse(await fs.readFile(manifestPath,'utf8'))
|
||||
manifest.files.find(f=>f.path==='scripts/rea.mjs').modified_sha256=hash(cli)
|
||||
manifest.files=manifest.files.filter(f=>f.path!=='dist/artifacts/SelectedDirectoryText.js')
|
||||
manifest.files.push({path:'dist/artifacts/SelectedDirectoryText.js',original_sha256:null,modified_sha256:hash(source)})
|
||||
await fs.writeFile(manifestPath,JSON.stringify(manifest,null,2)+'\n')
|
||||
console.log(JSON.stringify({root,providerSha256:hash(source),cliSha256:hash(cli),files:manifest.files.length}))
|
||||
@@ -0,0 +1,183 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const projectRoot = resolve(fileURLToPath(new URL('../..', import.meta.url)))
|
||||
const runtimeModule = join(projectRoot, '.tmp/rea-css-runtime/dist/browser/CdpElectronStyles.js')
|
||||
const { captureElectronStyles, ingestElectronStyleEvent } = await import(pathToFileURL(runtimeModule))
|
||||
const authorizedFrameIds = new Set(['frame-main'])
|
||||
const noCompletenessWrites = { exclude() { assert.fail('CSS capture must not write into the old completeness sections') } }
|
||||
const limitations = []
|
||||
const styles = new Map()
|
||||
const addStyle = (styleSheetId, header) => ingestElectronStyleEvent({ method: 'CSS.styleSheetAdded', params: { header: { styleSheetId, ...header } } }, styles)
|
||||
const removeStyle = styleSheetId => ingestElectronStyleEvent({ method: 'CSS.styleSheetRemoved', params: { styleSheetId } }, styles)
|
||||
|
||||
const fixtureRoot = await mkdtemp(join(tmpdir(), 'rea-css-fixtures-'))
|
||||
const cssFile = join(fixtureRoot, 'authorized.css')
|
||||
await writeFile(cssFile, 'body { color: red; }\n')
|
||||
const cssUrl = pathToFileURL(cssFile).href
|
||||
addStyle('file-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
|
||||
addStyle('inline-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
||||
addStyle('constructed-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
|
||||
addStyle('remote-style', { frameId: 'frame-main', sourceURL: 'https://example.invalid/site.css', origin: 'regular' })
|
||||
addStyle('other-frame-style', { frameId: 'frame-other', sourceURL: cssUrl, origin: 'regular' })
|
||||
addStyle('user-agent-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'user-agent' })
|
||||
addStyle('injected-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'injected' })
|
||||
addStyle('inspector-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'inspector' })
|
||||
addStyle('unknown-origin-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'unknown' })
|
||||
addStyle('unattributed-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular' })
|
||||
addStyle('removed-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
|
||||
removeStyle('removed-style')
|
||||
assert.equal(styles.has('removed-style'), false)
|
||||
removeStyle('missing-style')
|
||||
|
||||
const bodyTexts = new Map([
|
||||
['file-style', 'body { color: red; }\n'],
|
||||
['inline-style', '.inline { display: block; }'],
|
||||
['constructed-style', '.constructed { display: grid; }']
|
||||
])
|
||||
const calls = []
|
||||
const connection = {
|
||||
async send(method, params, sessionId, signal) {
|
||||
calls.push({ method, params, sessionId, signal })
|
||||
if (method !== 'CSS.getStyleSheetText') throw new Error(`Unexpected command: ${method}`)
|
||||
return { text: bodyTexts.get(params.styleSheetId) }
|
||||
}
|
||||
}
|
||||
const included = await captureElectronStyles({
|
||||
connection,
|
||||
sessionId: 'private-session-1',
|
||||
signal: undefined,
|
||||
styles,
|
||||
frameIds: authorizedFrameIds,
|
||||
request: { include_style_sources: true },
|
||||
completeness: noCompletenessWrites,
|
||||
limitations
|
||||
})
|
||||
assert.equal(included.total, 10)
|
||||
assert.equal(included.excluded, 7)
|
||||
assert.equal(included.source_failures, 0)
|
||||
assert.equal(included.truncated, false)
|
||||
assert.equal(included.items.length, 3)
|
||||
assert.equal(calls.length, 3)
|
||||
assert.ok(calls.every(call => call.method === 'CSS.getStyleSheetText' && call.sessionId === 'private-session-1'))
|
||||
assert.deepEqual(included.limits, { max_sheets: 1024, max_sheet_bytes: 8 * 1024 * 1024, max_total_bytes: 64 * 1024 * 1024 })
|
||||
const fileItem = included.items.find(item => item.style_sheet_id === 'file-style')
|
||||
assert.equal(fileItem.file_path, await realpath(cssFile))
|
||||
assert.equal(fileItem.source.included, true)
|
||||
assert.equal(fileItem.source.artifact.media_type, 'text/css')
|
||||
assert.equal(fileItem.source.artifact.text, bodyTexts.get('file-style'))
|
||||
assert.equal(fileItem.source.artifact.bytes, Buffer.byteLength(bodyTexts.get('file-style'), 'utf8'))
|
||||
assert.equal(fileItem.source.artifact.sha256, createHash('sha256').update(bodyTexts.get('file-style')).digest('hex'))
|
||||
assert.match(fileItem.style_sheet_key, /^electron_stylesheet_[a-f0-9]{64}$/)
|
||||
assert.equal(included.items.find(item => item.style_sheet_id === 'inline-style').file_path, null)
|
||||
assert.equal(included.items.find(item => item.style_sheet_id === 'inline-style').is_inline, true)
|
||||
assert.equal(included.items.find(item => item.style_sheet_id === 'constructed-style').is_constructed, true)
|
||||
assert.doesNotMatch(JSON.stringify(included), /example\.invalid/)
|
||||
|
||||
const unselectedCalls = []
|
||||
const unselected = await captureElectronStyles({
|
||||
connection: { async send(...args) { unselectedCalls.push(args); throw new Error('CSS body must not be requested') } },
|
||||
sessionId: 'private-session-1',
|
||||
styles,
|
||||
frameIds: authorizedFrameIds,
|
||||
request: { include_style_sources: false },
|
||||
completeness: noCompletenessWrites,
|
||||
limitations: []
|
||||
})
|
||||
assert.equal(unselected.total, 10)
|
||||
assert.equal(unselected.items.length, 0)
|
||||
assert.equal(unselected.excluded, 7)
|
||||
assert.equal(unselectedCalls.length, 0)
|
||||
|
||||
const failingStyles = new Map()
|
||||
addStyleTo(failingStyles, 'throwing-style', { frameId: 'frame-main', sourceURL: cssUrl, origin: 'regular' })
|
||||
addStyleTo(failingStyles, 'invalid-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
||||
addStyleTo(failingStyles, 'surviving-style', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
|
||||
const failures = await captureElectronStyles({
|
||||
connection: { async send(_method, params) {
|
||||
if (params.styleSheetId === 'throwing-style') throw new Error('stylesheet was removed')
|
||||
if (params.styleSheetId === 'invalid-style') return { text: 3 }
|
||||
return { text: '.still-captured{}' }
|
||||
} },
|
||||
sessionId: 'private-session-2',
|
||||
styles: failingStyles,
|
||||
frameIds: authorizedFrameIds,
|
||||
request: { include_style_sources: true },
|
||||
completeness: noCompletenessWrites,
|
||||
limitations: []
|
||||
})
|
||||
assert.equal(failures.items.length, 3)
|
||||
assert.equal(failures.source_failures, 2)
|
||||
assert.equal(failures.truncated, false)
|
||||
assert.equal(failures.items.find(item => item.style_sheet_id === 'throwing-style').source.reason, 'CSS.getStyleSheetText failed')
|
||||
assert.equal(failures.items.find(item => item.style_sheet_id === 'invalid-style').source.reason, 'CSS.getStyleSheetText returned invalid text')
|
||||
assert.equal(failures.items.find(item => item.style_sheet_id === 'surviving-style').source.included, true)
|
||||
|
||||
const oversizedStyles = new Map()
|
||||
addStyleTo(oversizedStyles, 'oversized', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
||||
const oversized = await captureElectronStyles({
|
||||
connection: { async send() { return { text: 'x'.repeat(8 * 1024 * 1024 + 1) } } },
|
||||
sessionId: 'private-session-3',
|
||||
styles: oversizedStyles,
|
||||
frameIds: authorizedFrameIds,
|
||||
request: { include_style_sources: true },
|
||||
completeness: noCompletenessWrites,
|
||||
limitations: []
|
||||
})
|
||||
assert.equal(oversized.source_failures, 1)
|
||||
assert.equal(oversized.truncated, true)
|
||||
assert.equal(oversized.items[0].source.included, false)
|
||||
assert.match(oversized.items[0].source.reason, /per-stylesheet byte limit/)
|
||||
|
||||
const aggregateStyles = new Map()
|
||||
const aggregateText = 'a'.repeat(8 * 1024 * 1024)
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
addStyleTo(aggregateStyles, `aggregate-${String(index).padStart(2, '0')}`, { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
||||
}
|
||||
addStyleTo(aggregateStyles, 'aggregate-overflow', { frameId: 'frame-main', sourceURL: '', origin: 'regular', isInline: true })
|
||||
const aggregate = await captureElectronStyles({
|
||||
connection: { async send(_method, params) { return { text: params.styleSheetId === 'aggregate-overflow' ? 'overflow' : aggregateText } } },
|
||||
sessionId: 'private-session-4',
|
||||
styles: aggregateStyles,
|
||||
frameIds: authorizedFrameIds,
|
||||
request: { include_style_sources: true },
|
||||
completeness: noCompletenessWrites,
|
||||
limitations: []
|
||||
})
|
||||
assert.equal(aggregate.source_failures, 1)
|
||||
assert.equal(aggregate.truncated, true)
|
||||
assert.equal(aggregate.items.filter(item => item.source.included).length, 8)
|
||||
assert.equal(aggregate.items.find(item => item.style_sheet_id === 'aggregate-overflow').source.included, false)
|
||||
assert.match(aggregate.items.find(item => item.style_sheet_id === 'aggregate-overflow').source.reason, /aggregate stylesheet source byte limit/)
|
||||
|
||||
const countLimitedStyles = new Map()
|
||||
for (let index = 0; index < 1025; index += 1) {
|
||||
addStyleTo(countLimitedStyles, `sheet-${String(index).padStart(4, '0')}`, { frameId: 'frame-main', sourceURL: '', origin: 'regular', isConstructed: true })
|
||||
}
|
||||
let countRequests = 0
|
||||
const countLimited = await captureElectronStyles({
|
||||
connection: { async send() { countRequests += 1; return { text: '' } } },
|
||||
sessionId: 'private-session-5',
|
||||
styles: countLimitedStyles,
|
||||
frameIds: authorizedFrameIds,
|
||||
request: { include_style_sources: true },
|
||||
completeness: noCompletenessWrites,
|
||||
limitations: []
|
||||
})
|
||||
assert.equal(countLimited.total, 1025)
|
||||
assert.equal(countLimited.items.length, 1025)
|
||||
assert.equal(countLimited.source_failures, 1)
|
||||
assert.equal(countLimited.truncated, true)
|
||||
assert.equal(countRequests, 1024)
|
||||
assert.equal(countLimited.items.filter(item => item.source.included).length, 1024)
|
||||
|
||||
await rm(fixtureRoot, { recursive: true, force: true })
|
||||
console.log('REA Electron CSS capture regression checks passed')
|
||||
|
||||
function addStyleTo(styleMap, styleSheetId, header) {
|
||||
ingestElectronStyleEvent({ method: 'CSS.styleSheetAdded', params: { header: { styleSheetId, ...header } } }, styleMap)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Test the public local REA MCP entry point without inspecting WorkBuddy.
|
||||
import assert from 'node:assert/strict'
|
||||
import path from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const runtime = path.resolve('.tmp/rea-css-runtime')
|
||||
const require = createRequire(path.join(runtime, 'package.json'))
|
||||
const { Client } = await import(pathToFileURL(require.resolve('@modelcontextprotocol/client')).href)
|
||||
const { StdioClientTransport } = await import(pathToFileURL(require.resolve('@modelcontextprotocol/client/stdio')).href)
|
||||
const transport = new StdioClientTransport({ command: process.execPath, args: [path.join(runtime, 'scripts/rea.mjs'), 'mcp'], cwd: process.cwd(), stderr: 'pipe' })
|
||||
const client = new Client({ name: 'ycode-local-rea-css-regression', version: '1' })
|
||||
try {
|
||||
await client.connect(transport)
|
||||
assert.equal(client.getServerVersion()?.version, '6.5.0-visual.3')
|
||||
const listing = await client.listTools()
|
||||
const tool = listing.tools.find(tool => tool.name === 'inspect_electron_page')
|
||||
assert.ok(tool, 'Electron inspection tool is advertised')
|
||||
assert.ok(JSON.stringify(tool.inputSchema).includes('include_style_sources'), 'MCP input retains CSS option')
|
||||
assert.ok(JSON.stringify(tool.inputSchema).includes('include_visuals'), 'MCP input retains viewport option')
|
||||
assert.ok(JSON.stringify(tool.inputSchema).includes('visual_probe'), 'MCP input retains native visual probe option')
|
||||
console.log('REA CSS public MCP passed: local version, tool listing, CSS input option')
|
||||
} finally {
|
||||
await client.close()
|
||||
await transport.close()
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const runtime = path.resolve('.tmp/rea-css-runtime')
|
||||
const moduleAt = relative => import(pathToFileURL(path.join(runtime, 'dist', relative)).href)
|
||||
const { inspectElectronPageInputSchema, electronPageInspectionSchema } = await moduleAt('domain/javascript/electronObservation.js')
|
||||
const { inspectCdpElectronPage } = await moduleAt('browser/CdpElectronInspection.js')
|
||||
const { createElectronEvidence } = await moduleAt('application/javascript/ElectronEvidence.js')
|
||||
const fixture = await fs.mkdtemp(path.join(tmpdir(), 'rea-css-contract-'))
|
||||
try {
|
||||
const html = path.join(fixture, 'index.html'), css = path.join(fixture, 'app.css')
|
||||
await fs.writeFile(html, '<!doctype html><title>REA fixture</title>')
|
||||
await fs.writeFile(css, '.fixture { color: blue; }')
|
||||
const pageUrl = pathToFileURL(html).href, cssUrl = pathToFileURL(css).href
|
||||
const frame = { id: 'frame-main', url: pageUrl }
|
||||
const input = inspectElectronPageInputSchema.parse({ cdp_endpoint: 'http://127.0.0.1:9223', target_id: 'test', observation_ms: 0, include_style_sources: true })
|
||||
assert.equal(input.include_style_sources, true)
|
||||
assert.equal(input.include_script_sources, false)
|
||||
assert.equal(inspectElectronPageInputSchema.safeParse({ ...input, visual_probe: { include_fonts: true } }).success, false, 'a probe cannot be silently ignored without include_visuals')
|
||||
assert.equal(inspectElectronPageInputSchema.parse({ cdp_endpoint: 'http://127.0.0.1:9223', target_id: 'test' }).include_style_sources, false)
|
||||
assert.equal(inspectElectronPageInputSchema.safeParse({ ...input, cdp_endpoint: 'https://example.com' }).success, false)
|
||||
let listener
|
||||
const calls = []
|
||||
const connection = {
|
||||
onEvent(callback) { listener = callback; return () => { listener = undefined } },
|
||||
async send(method, _params, sessionId) {
|
||||
calls.push(method)
|
||||
if (method === 'Page.getFrameTree') return { frameTree: { frame } }
|
||||
if (method === 'Page.getResourceTree') return { frameTree: { frame, resources: [{ url: cssUrl, type: 'Stylesheet', mimeType: 'text/css' }] } }
|
||||
if (method === 'DOMSnapshot.captureSnapshot') return { strings: [], documents: [] }
|
||||
if (method === 'Target.getTargets') return { targetInfos: [] }
|
||||
if (method === 'CSS.enable') {
|
||||
listener({ sessionId, method: 'CSS.styleSheetAdded', params: { header: { styleSheetId: 'css-1', frameId: frame.id, sourceURL: cssUrl, origin: 'regular', isInline: false } } })
|
||||
listener({ sessionId: 'another-session', method: 'CSS.styleSheetAdded', params: { header: { styleSheetId: 'foreign-session', frameId: frame.id, sourceURL: cssUrl, origin: 'regular' } } })
|
||||
}
|
||||
if (method === 'CSS.getStyleSheetText') return { text: '.fixture { color: blue; }' }
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
connection, sessionId: 'session-main', input,
|
||||
target: { id: 'test', type: 'page', title: 'REA fixture', attached: null },
|
||||
discovery: { version: { product: 'Fixture/1', protocol_version: '1.3', revision: 'fixture', user_agent: 'fixture', js_version: 'fixture' } }
|
||||
}
|
||||
const result = electronPageInspectionSchema.parse(await inspectCdpElectronPage(context))
|
||||
assert.equal(result.styles.total, 1, 'foreign session events cannot be attributed to this target')
|
||||
assert.equal(result.styles.items[0].source.artifact.text, '.fixture { color: blue; }')
|
||||
assert.equal(result.styles.items[0].file_path, await fs.realpath(css))
|
||||
assert.equal(result.styles.source_failures, 0)
|
||||
assert.equal(result.styles.truncated, false)
|
||||
assert.ok(calls.indexOf('DOM.enable') < calls.indexOf('CSS.enable'))
|
||||
assert.equal(listener, undefined, 'capture listener is removed')
|
||||
const evidence = createElectronEvidence('inspect_electron_page', input, result, { id: 'rea-cdp-electron', name: 'REA Electron file-page CDP observation provider', version: '1+css-local.1' })
|
||||
assert.equal(evidence.parameters.include_style_sources, true)
|
||||
assert.equal(evidence.normalized_result.styles.items.length, 1)
|
||||
assert.match(evidence.evidence_id, /^ev_[a-f0-9]{64}$/)
|
||||
calls.length = 0
|
||||
const optedOut = electronPageInspectionSchema.parse(await inspectCdpElectronPage({ ...context, input: { ...input, include_style_sources: false } }))
|
||||
assert.equal(optedOut.styles.items.length, 0)
|
||||
assert.equal(calls.some(method => method.startsWith('CSS.') || method === 'DOM.enable'), false)
|
||||
assert.equal(calls.some(method => method === 'Runtime.evaluate'), false)
|
||||
console.log('REA CSS runtime contracts passed: opt-in, session attribution, schema retention, Evidence, and existing opt-out behavior')
|
||||
} finally {
|
||||
await fs.rm(fixture, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const runtime = path.resolve('.tmp/rea-css-runtime')
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(runtime, 'css-repair-manifest.json'), 'utf8'))
|
||||
const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
for (const file of manifest.files) {
|
||||
assert.equal(sha(await fs.readFile(path.join(runtime, file.path))), file.modified_sha256, `Local repaired module changed: ${file.path}`)
|
||||
if (file.original_sha256 !== null) {
|
||||
assert.equal(sha(await fs.readFile(path.join(manifest.sourceRoot, file.path))), file.original_sha256, `Global REA module changed: ${file.path}`)
|
||||
assert.equal(sha(await fs.readFile(path.join(runtime, 'css-repair-baseline', file.path))), file.original_sha256)
|
||||
}
|
||||
}
|
||||
assert.equal(await fs.realpath(path.join(runtime, 'node_modules')), await fs.realpath(path.join(manifest.sourceRoot, 'node_modules')))
|
||||
console.log(`REA local repair integrity passed: ${manifest.files.length} module digests; corresponding global files unchanged`)
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
const output = await fs.mkdtemp(path.resolve('.tmp/rea-extra-resources-')), root = path.join(output, 'fixture'), checks = []
|
||||
await fs.mkdir(root)
|
||||
const hash = b => createHash('sha256').update(b).digest('hex')
|
||||
const names = ['Tool.cs', 'Tool.csproj', 'Tool.slnx', 'schema.xml', 'schema.xsd', 'setup.sh', 'setup.ps1', 'viewer.html', 'font.LICENSE', 'LICENSE', '.gitignore']
|
||||
for (const name of names) await fs.writeFile(path.join(root, name), 'OWN_FIXTURE_UTF8_中文_' + name)
|
||||
const font = Buffer.alloc(32); font.writeUInt32BE(0x00010000); font.writeUInt16BE(1, 4); font.write('test', 12); font.writeUInt32BE(28, 20); font.writeUInt32BE(4, 24)
|
||||
await fs.writeFile(path.join(root, 'valid.ttf'), font)
|
||||
await fs.writeFile(path.join(root, 'invalid.ttf'), 'not an SFNT font')
|
||||
const badRange = Buffer.from(font); badRange.writeUInt32BE(1000, 20); await fs.writeFile(path.join(root, 'range.ttf'), badRange)
|
||||
const cli = (command, ...args) => spawnSync(process.execPath, ['.tmp/rea-css-runtime/scripts/rea.mjs', command, root, ...args], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
|
||||
const inventory = cli('inspect-selected-directory-text-root'); assert.equal(inventory.status, 0, inventory.stderr)
|
||||
const pin = JSON.parse(inventory.stdout).normalized_result.root_sha256
|
||||
const text = cli('export-selected-directory-text', pin, JSON.stringify(names)); assert.equal(text.status, 0, text.stderr)
|
||||
const files = JSON.parse(text.stdout).normalized_result.files
|
||||
for (const name of names) { const file = files.find(f => f.path === name); assert.equal(file.text, 'OWN_FIXTURE_UTF8_中文_' + name); assert.equal(file.sha256, hash(Buffer.from(file.text))); checks.push('exact UTF-8 export: ' + name) }
|
||||
const binary = (name, bytes, sha = hash(bytes)) => cli('export-selected-directory-binary', pin, JSON.stringify([{ path: name, sha256: sha }]))
|
||||
const valid = binary('valid.ttf', font); assert.equal(valid.status, 0, valid.stderr); const result = JSON.parse(valid.stdout).normalized_result.files[0]
|
||||
assert.equal(result.media_type, 'font/ttf'); assert.deepEqual(Buffer.from(result.data_base64, 'base64'), font); checks.push('SFNT font exact bytes/SHA/media')
|
||||
for (const [label, response, pattern] of [['invalid TTF signature', binary('invalid.ttf', Buffer.from('not an SFNT font')), /TTF signature/], ['TTF table bounds', binary('range.ttf', badRange), /table range/], ['selected SHA pin', binary('valid.ttf', font, '0'.repeat(64)), /SHA mismatch/], ['path traversal', cli('export-selected-directory-text', pin, JSON.stringify(['../Tool.cs'])), /canonical/]]) { assert.notEqual(response.status, 0, label); assert.match(response.stderr, pattern, label); checks.push(label) }
|
||||
await fs.writeFile(path.join(output, 'report.json'), JSON.stringify({ result: 'PASS', checks, ownFixtureOnly: true }, null, 2))
|
||||
console.log(JSON.stringify({ result: 'PASS', checks: checks.length, directory: output }))
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Bounded excerpts from SHA-verified REA exports, never target filesystem. */
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
const input = process.argv[2]
|
||||
const pattern = process.argv[3] || 'expertScanCustomExperts|expertActivatePlugin|expertInstallFromPath|clearSessionMarker|createdBySession|expert-manager'
|
||||
const radius = Number(process.argv[4] || 550)
|
||||
const d = JSON.parse(fs.readFileSync(input,'utf8'))
|
||||
for (const f of d.normalized_result.files) {
|
||||
if (crypto.createHash('sha256').update(f.text).digest('hex') !== f.sha256) throw new Error('SHA mismatch')
|
||||
const matches = [...f.text.matchAll(new RegExp(pattern,'g'))].map(m=>({offset:m.index,text:f.text.slice(Math.max(0,m.index-radius),m.index+m[0].length+radius)}))
|
||||
if(matches.length) console.log(JSON.stringify({path:f.path,sha256:f.sha256,evidenceId:d.evidence_id,matches:matches.slice(0,32),omitted:Math.max(0,matches.length-32)},null,2))
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/** AST anchors and finite reviewable definitions from retained REA text. */
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import {parse} from '@babel/parser'
|
||||
const inputs=process.argv.slice(2)
|
||||
const digest = s=>crypto.createHash('sha256').update(s).digest('hex')
|
||||
const output={offsetUnit:'UTF-16 code units, end exclusive',modules:[]}
|
||||
for(const input of inputs){
|
||||
const d=JSON.parse(fs.readFileSync(input,'utf8'))
|
||||
for(const f of d.normalized_result.files.filter(f=>f.path.endsWith('.js'))){
|
||||
if(digest(f.text)!==f.sha256)throw new Error('SHA mismatch')
|
||||
const ast=parse(f.text,{sourceType:'unambiguous'}), defs=[]
|
||||
function visit(n,owner=''){
|
||||
if(!n||typeof n!=='object')return
|
||||
if(n.type==='ClassExpression'||n.type==='ClassDeclaration')owner=n.id?.name||owner
|
||||
if(n.type==='VariableDeclarator'&&n.init?.type==='ClassExpression')owner=n.id.name
|
||||
const name=n.type==='FunctionDeclaration'?n.id?.name:n.type==='ClassMethod'||n.type==='ObjectMethod'?n.key.name:undefined
|
||||
if(name&&(/Expert|expert|SessionMarker|WorkbuddyMeta|resolveArgPath/.test(name)||/Expert/.test(owner)))defs.push({name,owner,start:n.start,end:n.end,sha256:digest(f.text.slice(n.start,n.end)),text:f.text.slice(n.start,n.end)})
|
||||
for(const[k,v]of Object.entries(n)){if(['loc','comments','tokens'].includes(k))continue;if(Array.isArray(v))v.forEach(c=>visit(c,owner));else if(v&&typeof v==='object')visit(v,owner)}
|
||||
}visit(ast.program)
|
||||
output.modules.push({input,evidenceId:d.evidence_id,path:f.path,sha256:f.sha256,definitions:defs})
|
||||
}
|
||||
}
|
||||
fs.writeFileSync('.tmp/rea-expert-package-contracts.json',JSON.stringify(output,null,2))
|
||||
console.log(JSON.stringify({output:'.tmp/rea-expert-package-contracts.json',modules:output.modules.map(({definitions,...m})=>({...m,count:definitions.length}))},null,2))
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Exact public REA export. Does not read ASAR or installed files itself. */
|
||||
import fs from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import crypto from 'node:crypto'
|
||||
const inventory = JSON.parse(fs.readFileSync('.tmp/rea-expert-package-inventory.json', 'utf8'))
|
||||
const paths = process.argv.slice(2)
|
||||
if (!paths.length) throw new Error('Select exact paths')
|
||||
const r = spawnSync(process.execPath, ['.tmp/rea-css-runtime/scripts/rea.mjs', 'export-selected-artifact-text',
|
||||
inventory.inputPath, inventory.rootSha256, JSON.stringify(paths)], {encoding:'utf8', maxBuffer:32*1024*1024})
|
||||
if (r.status !== 0) throw new Error(r.stderr || r.stdout || `REA exited ${r.status}`)
|
||||
const evidence = JSON.parse(r.stdout)
|
||||
if (evidence.normalized_result.root_sha256 !== inventory.rootSha256) throw new Error('REA root mismatch')
|
||||
for (const file of evidence.normalized_result.files) {
|
||||
const sha = crypto.createHash('sha256').update(file.text).digest('hex')
|
||||
if (sha !== file.sha256) throw new Error(`Text SHA mismatch: ${file.path}`)
|
||||
const expected = inventory.files.find(item => item.path === file.path)
|
||||
if (expected && expected.sha256 !== sha) throw new Error(`Inventory SHA mismatch: ${file.path}`)
|
||||
}
|
||||
const output = `.tmp/rea-expert-package-${evidence.evidence_id}.json`
|
||||
fs.writeFileSync(output,JSON.stringify(evidence))
|
||||
console.log(JSON.stringify({output,evidenceId:evidence.evidence_id,files:evidence.normalized_result.files.map(({text,...metadata})=>metadata)},null,2))
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Read only retained REA inventory; never opens the installed target. */
|
||||
import fs from 'node:fs'
|
||||
const input = '.tmp/rea-workbuddy-takeover-2026-10-15.json'
|
||||
const pin = '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
|
||||
const retained = JSON.parse(fs.readFileSync(input, 'utf8'))
|
||||
const record = retained.records.find(item => item.provider.id === 'rea-javascript-application')
|
||||
if (record.normalized_result.root_artifact_sha256 !== pin) throw new Error('Root pin mismatch')
|
||||
const files = new Map()
|
||||
for (const node of record.normalized_result.graph.nodes) for (const observation of node.observations) {
|
||||
const p = observation.properties
|
||||
if (observation.evidence.extractor.operation !== 'inventory-relevant-file') continue
|
||||
if ((!p.path.startsWith('main/') && !/(?:expert|daemon|plugin|agent|schema)/i.test(p.path)) || p.path.startsWith('node_modules/')) continue
|
||||
files.set(p.path, { path: p.path, sha256: observation.evidence.artifact.sha256, bytes: p.bytes,
|
||||
fileKind: p.file_kind, unpacked: p.unpacked, textStatus: p.text_status })
|
||||
}
|
||||
const output = '.tmp/rea-expert-package-inventory.json'
|
||||
fs.writeFileSync(output, JSON.stringify({ input, evidenceId: record.evidence_id, rootSha256: pin,
|
||||
inventoryGraphSha256: record.normalized_result.inventory_graph_sha256,
|
||||
inputPath: record.normalized_result.input_path, scope: 'matching paths in retained relevant-file inventory; not all archive entries',
|
||||
files: [...files.values()].sort((a,b) => a.path.localeCompare(b.path)) }, null, 2))
|
||||
console.log(JSON.stringify({ output, evidenceId: record.evidence_id, count: files.size,
|
||||
candidates: [...files.values()].filter(file => /^main\/(?:daemon|runtime-handlers|module\.app-server|server)\./.test(file.path) || /expert-manager|marketplace\.json/.test(file.path)) },null,2))
|
||||
@@ -0,0 +1,31 @@
|
||||
/** Restore exact SHA-pinned REA skill exports; no original file access/execution. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
const prefix='resources/plugins/workbuddy-builtin/skills/expert-manager/'
|
||||
const target=path.resolve('src/main/capabilities/assets/expert-manager')
|
||||
const pin='035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
|
||||
const inputs=process.argv.slice(2)
|
||||
if(!inputs.length)throw new Error('Select retained REA exports')
|
||||
const manifest={targetVersion:'WorkBuddy 5.8.2',rootSha256:pin,scope:'exact retained expert-manager files; no original initialization or Python execution',files:[]}
|
||||
const bundle={files:{}}
|
||||
for(const input of inputs){
|
||||
const d=JSON.parse(fs.readFileSync(input,'utf8'))
|
||||
if(d.normalized_result.root_sha256!==pin)throw new Error('Root mismatch')
|
||||
for(const f of d.normalized_result.files){
|
||||
if(!f.path.startsWith(prefix))continue
|
||||
const relative=f.path.slice(prefix.length)
|
||||
if(relative.split('/').some(p=>!p||p==='.'||p==='..'))throw new Error('Noncanonical member')
|
||||
const sha=crypto.createHash('sha256').update(f.text).digest('hex')
|
||||
if(sha!==f.sha256||Buffer.byteLength(f.text)!==f.size_bytes)throw new Error('REA bytes mismatch')
|
||||
const destination=path.join(target,...relative.split('/'))
|
||||
fs.mkdirSync(path.dirname(destination),{recursive:true})
|
||||
fs.writeFileSync(destination,f.text)
|
||||
if(crypto.createHash('sha256').update(fs.readFileSync(destination)).digest('hex')!==sha)throw new Error('Restored SHA mismatch')
|
||||
manifest.files.push({file:relative,originalMember:f.path,sha256:sha,bytes:f.size_bytes,evidenceId:d.evidence_id,retainedExport:input})
|
||||
bundle.files[relative]=f.text
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(target,'rea-source-manifest.json'),JSON.stringify(manifest,null,2)+'\n')
|
||||
fs.writeFileSync(path.join(path.dirname(target),'expert-manager-source.json'),JSON.stringify(bundle,null,2)+'\n')
|
||||
console.log(JSON.stringify({target,files:manifest.files.length,bytes:manifest.files.reduce((n,f)=>n+f.bytes,0)}))
|
||||
@@ -0,0 +1,41 @@
|
||||
/** Own ASAR fixture exercises the official local REA provider/public CLI only. */
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { createRequire } from 'node:module'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
const root = await fs.mkdtemp(path.resolve('.tmp/rea-expert-package-fixture-'))
|
||||
const requireRuntime = createRequire(path.resolve('.tmp/rea-css-runtime/package.json'))
|
||||
const {createPackageWithOptions} = await import(pathToFileURL(requireRuntime.resolve('@electron/asar')).href)
|
||||
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
const source = path.join(root,'source'), archive = path.join(root,'fixture.asar')
|
||||
const files = {'expert/SKILL.md':'---\nname: local-test\n---\n# 本地专家\n', 'expert/schema.yaml':'name: local-test\n',
|
||||
'expert/schema.yml':'version: 1\n','expert/README.txt':'strict UTF-8\n', 'expert/register.py':'# retained, never executed\nprint("中文")\n', 'expert/bad.md':Buffer.from([0xc3,0x28]),
|
||||
'expert/large.txt':Buffer.alloc(8*1024*1024+1,97),'expert/one.md':Buffer.alloc(8*1024*1024,97),
|
||||
'expert/two.md':Buffer.alloc(8*1024*1024,98),'expert/extra.txt':'x','expert/unselected.md':'not selected'}
|
||||
for(const [p,content] of Object.entries(files)){await fs.mkdir(path.dirname(path.join(source,p)),{recursive:true});await fs.writeFile(path.join(source,p),content)}
|
||||
await createPackageWithOptions(source,archive,{unpackDir:'expert'})
|
||||
await fs.rm(path.join(archive+'.unpacked','expert/unselected.md'))
|
||||
const pin=digest(await fs.readFile(archive)), checks=[]
|
||||
function cli(paths,expected=pin){return spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs','export-selected-artifact-text',archive,expected,JSON.stringify(paths)],{encoding:'utf8',maxBuffer:32*1024*1024})}
|
||||
function reject(label,paths,pattern,expected=pin){const r=cli(paths,expected);assert.notEqual(r.status,0,label);assert.match(r.stderr,pattern,label);checks.push({label,pass:true})}
|
||||
const selected=Object.keys(files).slice(0,5), r=cli(selected)
|
||||
assert.equal(r.status,0,r.stderr)
|
||||
const evidence=JSON.parse(r.stdout)
|
||||
assert.equal(evidence.normalized_result.root_sha256,pin)
|
||||
assert.deepEqual(evidence.normalized_result.files.map(f=>f.path),selected)
|
||||
for(const f of evidence.normalized_result.files){assert.equal(f.text,files[f.path]);assert.equal(f.sha256,digest(files[f.path]));assert.equal(f.size_bytes,Buffer.byteLength(files[f.path]))}
|
||||
checks.push({label:'five new suffixes, exact bytes/SHA/root and missing unselected unpacked ignored; Python not executed',pass:true})
|
||||
reject('root mismatch',selected,/changed before interpretation/,'0'.repeat(64))
|
||||
reject('canonical selection',['../expert/SKILL.md'],/not canonical/)
|
||||
reject('unknown suffix',['expert/private.db'],/supported UTF-8/)
|
||||
reject('strict UTF-8',['expert/bad.md'],/strict UTF-8/)
|
||||
reject('per-file budget',['expert/large.txt'],/per-file budget/)
|
||||
reject('aggregate budget',['expert/one.md','expert/two.md','expert/extra.txt'],/aggregate budget/)
|
||||
reject('absent selected member',['expert/absent.md'],/absent from/)
|
||||
reject('duplicate selected member',['expert/SKILL.md','expert/SKILL.md'],/Duplicate selected path/)
|
||||
await fs.writeFile(path.join(root,'evidence.json'),JSON.stringify(evidence,null,2))
|
||||
await fs.writeFile(path.join(root,'report.json'),JSON.stringify({result:'PASS',rootPin:pin,evidenceId:evidence.evidence_id,checks},null,2))
|
||||
console.log(JSON.stringify({result:'PASS',root,checks:checks.length,evidenceId:evidence.evidence_id}))
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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))
|
||||
@@ -0,0 +1,102 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const runtime=path.resolve('.tmp/rea-css-runtime/dist')
|
||||
const load=relative=>import(pathToFileURL(path.join(runtime,relative)))
|
||||
const {captureElectronMediaAssets}=await load('browser/CdpElectronMediaAssets.js')
|
||||
const {electronVisualProbeInputSchema,electronMediaAssetsSchema}=await load('domain/javascript/ElectronVisualProbeSchema.js')
|
||||
const {inspectElectronPageInputSchema,electronPageInspectionSchema}=await load('domain/javascript/electronObservation.js')
|
||||
const {inspectCdpElectronPage}=await load('browser/CdpElectronInspection.js')
|
||||
const {createElectronEvidence}=await load('application/javascript/ElectronEvidence.js')
|
||||
const temporary=await fs.mkdtemp(path.join(tmpdir(),'rea-img-resource-'))
|
||||
try {
|
||||
const html=path.join(temporary,'index.html');await fs.writeFile(html,'<!doctype html><img class="selected">')
|
||||
const page=pathToFileURL(html).href,resourceUrl=pathToFileURL(path.join(temporary,'actual.png')).href
|
||||
const main={frame_id:'main',file_path:await fs.realpath(html)},frame={id:'main',url:page}
|
||||
const png='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jKXcAAAAASUVORK5CYII='
|
||||
const bytes=Buffer.from(png,'base64'),digest=crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
const makeSnapshot=()=>({strings:[page,'main','HTML','IMG','class','selected','src','fallback.png',resourceUrl,'contenteditable','true','14px'],documents:[{
|
||||
documentURL:0,frameId:1,scrollOffsetX:0,scrollOffsetY:0,
|
||||
nodes:{nodeName:[2,3],parentIndex:[-1,0],backendNodeId:[10,20],attributes:[[],[4,5,6,7]],currentSourceURL:{index:[1],value:[8]}},
|
||||
layout:{nodeIndex:[0,1],bounds:[[0,0,100,80],[10,10,40,50]],styles:[]}
|
||||
}]})
|
||||
let mode='ok',snapshot=makeSnapshot(),calls=[],listener,selectedResourceUrl=resourceUrl
|
||||
const send=async(method,params={})=>{
|
||||
calls.push({method,params})
|
||||
assert.ok(!['Runtime.evaluate','Runtime.callFunctionOn','Runtime.getProperties','Network.getCookies','Network.loadNetworkResource','Page.navigate'].includes(method))
|
||||
if(method==='Page.getFrameTree')return {frameTree:{frame:mode==='navigation-after-content'&&calls.some(c=>c.method==='Page.getResourceContent')?{...frame,id:'changed'}:frame}}
|
||||
if(method==='DOM.getDocument')return {root:{nodeId:1}}
|
||||
if(method==='DOM.querySelectorAll')return {nodeIds:mode==='absent'?[]:mode==='ambiguous'?[2,3]:[2]}
|
||||
if(method==='DOM.describeNode')return {node:{nodeName:mode==='not-img'?'DIV':'IMG',backendNodeId:mode==='foreign-backend'?999:20}}
|
||||
if(method==='Page.getLayoutMetrics')return {cssLayoutViewport:{clientWidth:100,clientHeight:80,pageX:0,pageY:0}}
|
||||
if(method==='DOM.getNodeForLocation')return {backendNodeId:mode==='covered'?999:mode==='partly-covered'&¶ms.y>20?999:20}
|
||||
if(method==='Page.getResourceTree')return {frameTree:{frame,resources:mode==='not-loaded'?[]:[{url:selectedResourceUrl,type:'Image',mimeType:mode==='svg'?'image/svg+xml':'image/png',...(mode==='declared-oversize'?{contentSize:2097153}:{})}],childFrames:[{frame:{id:'foreign',url:'https://foreign.invalid'},resources:[{url:resourceUrl,type:'Image',mimeType:'image/png'}]}]}}
|
||||
if(method==='Page.getResourceContent'){
|
||||
if(mode==='content-unavailable')throw Error('private-url-or-cookie-must-not-leak')
|
||||
return {base64Encoded:mode!=='not-binary',content:mode==='invalid-base64'?'not-base64':mode==='signature'?'YWJj':mode==='oversize'?Buffer.alloc(2097153).toString('base64'):png}
|
||||
}
|
||||
if(method==='DOMSnapshot.captureSnapshot'){const copy=structuredClone(snapshot);for(const doc of copy.documents)doc.layout.styles=doc.layout.nodeIndex.map(()=>params.computedStyles.map(()=>11));return copy}
|
||||
if(method==='Page.captureScreenshot')return {data:png}
|
||||
if(method==='Target.getTargets')return {targetInfos:[]}
|
||||
return {}
|
||||
}
|
||||
assert.deepEqual(electronVisualProbeInputSchema.parse({}).media_selectors,[])
|
||||
assert.equal(electronVisualProbeInputSchema.safeParse({media_selectors:Array.from({length:5},(_,i)=>'#img'+i)}).success,false)
|
||||
assert.equal(electronVisualProbeInputSchema.safeParse({media_selectors:['#same','#same']}).success,false)
|
||||
assert.equal(await captureElectronMediaAssets(snapshot,[main],[],send),undefined)
|
||||
assert.equal(calls.length,0,'default off must perform no resource reads')
|
||||
const capture=async()=>{calls=[];return electronMediaAssetsSchema.parse(await captureElectronMediaAssets(snapshot,[main],['.selected'],send))}
|
||||
let result=await capture()
|
||||
assert.equal(result.items[0].status,'exported')
|
||||
assert.equal(result.items[0].source.selection,'current_source_url')
|
||||
assert.equal(result.items[0].artifact.sha256,digest)
|
||||
assert.equal(result.items[0].artifact.data_base64,png)
|
||||
assert.equal(result.total_bytes,bytes.length)
|
||||
assert.equal(calls.find(c=>c.method==='Page.getResourceContent').params.url,resourceUrl,'currentSrc wins over the fallback src attribute')
|
||||
assert.equal(result.items[0].backend_node_id,20)
|
||||
mode='covered';result=await capture();assert.equal(result.items[0].status,'exported');assert.equal(result.items[0].visibility.status,'covered');assert.equal(result.items[0].selected_node.current_source_url,resourceUrl);
|
||||
mode='partly-covered';assert.equal((await capture()).items[0].status,'exported','partial occlusion may still export actual loaded full image bytes')
|
||||
for(const [test,reason] of [['absent','selector_absent'],['ambiguous','selector_ambiguous'],['not-img','selected_node_not_img'],['foreign-backend','snapshot_identity_unavailable'],['not-loaded','selected_source_not_loaded_in_main_frame'],['svg','unsupported_image_type'],['declared-oversize','asset_byte_budget_exceeded'],['oversize','asset_byte_budget_exceeded'],['signature','image_signature_mismatch'],['invalid-base64','invalid_base64'],['not-binary','binary_content_unavailable'],['content-unavailable','protocol_resource_unavailable'],['navigation-after-content','frame_changed']]){
|
||||
mode=test;result=await capture();assert.equal(result.items[0].status,'unknown',test);assert.equal(result.items[0].reason,reason,test);assert.equal(result.total_bytes,0,test)
|
||||
assert.equal(result.items[0].artifact,undefined,test)
|
||||
if(['not-loaded','svg','declared-oversize'].includes(test))assert.equal(calls.some(c=>c.method==='Page.getResourceContent'),false,test+' must reject before the read')
|
||||
assert.equal(JSON.stringify(result).includes('private-url-or-cookie'),false)
|
||||
}
|
||||
mode='ok';snapshot=makeSnapshot();snapshot.documents[0].nodes.attributes[0]=[9,10]
|
||||
assert.equal((await capture()).items[0].reason,'editable_subtree_excluded')
|
||||
snapshot=makeSnapshot();snapshot.documents[0].layout.bounds[1]=[200,200,40,50]
|
||||
assert.equal((await capture()).items[0].visibility.reason,'outside_visible_viewport')
|
||||
snapshot=makeSnapshot();snapshot.documents[0].layout.nodeIndex=[0]
|
||||
assert.equal((await capture()).items[0].visibility.reason,'no_visible_layout')
|
||||
snapshot=makeSnapshot();delete snapshot.documents[0].nodes.currentSourceURL;snapshot.strings[7]=resourceUrl
|
||||
assert.equal((await capture()).items[0].source.selection,'src')
|
||||
snapshot=makeSnapshot();selectedResourceUrl='https://account:private-password@assets.invalid/img.png?token=private-query'
|
||||
snapshot.strings[8]=selectedResourceUrl+'#fragment'
|
||||
result=await capture()
|
||||
assert.equal(result.items[0].status,'exported')
|
||||
assert.equal(result.items[0].source.url,'https://assets.invalid/img.png')
|
||||
assert.equal(result.items[0].selected_node.current_source_url,'https://assets.invalid/img.png')
|
||||
assert.equal(result.items[0].source.url_sha256,crypto.createHash('sha256').update(snapshot.strings[8]).digest('hex'))
|
||||
assert.equal(JSON.stringify(result).includes('private-'),false,'source metadata omits URL credentials and queries')
|
||||
selectedResourceUrl=resourceUrl
|
||||
snapshot=makeSnapshot()
|
||||
// Exercise the public inspection normalization and Evidence path, not only the helper.
|
||||
const input=inspectElectronPageInputSchema.parse({cdp_endpoint:'http://127.0.0.1:9223',target_id:'fixture',observation_ms:0,include_visuals:true,visual_probe:{media_selectors:['.selected']}})
|
||||
const context={connection:{send,onEvent(fn){listener=fn;return()=>{listener=undefined}}},sessionId:'selected-session',input,target:{id:'fixture',type:'page',title:'Own fixture',attached:false},discovery:{version:{product:'Fixture/1',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'}}}
|
||||
const inspection=electronPageInspectionSchema.parse(await inspectCdpElectronPage(context))
|
||||
assert.equal(inspection.visuals.media_assets.items[0].artifact.sha256,digest)
|
||||
assert.deepEqual(inspection.visuals.layout.documents[0].nodes[1].attributes,{class:'selected'})
|
||||
const evidence=createElectronEvidence('inspect_electron_page',input,inspection,{id:'rea-cdp-electron',name:'Local selected resource fixture',version:'1+visual-local.3'})
|
||||
assert.deepEqual(evidence.parameters.visual_probe.media_selectors,['.selected'])
|
||||
assert.equal(evidence.normalized_result.visuals.media_assets.items[0].artifact.data_base64,png)
|
||||
assert.match(evidence.evidence_id,/^ev_[a-f0-9]{64}$/)
|
||||
const optedOut=electronPageInspectionSchema.parse(await inspectCdpElectronPage({...context,input:{...input,visual_probe:undefined}}))
|
||||
assert.equal(optedOut.visuals.media_assets,undefined)
|
||||
assert.equal(JSON.stringify(optedOut.visuals).includes('fallback.png'),false)
|
||||
assert.equal(listener,undefined)
|
||||
console.log('REA selected IMG resource regression passed: opt-in/schema/Evidence, snapshot currentSrc/backend attribution, covered bytes with separate visibility, scope, binary budgets/signatures, URL credential redaction, unknowns and default URL omission')
|
||||
}finally{await fs.rm(temporary,{recursive:true,force:true})}
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
const output = path.resolve('.tmp/modern-conversation-labels-regression.mjs')
|
||||
await build({
|
||||
entryPoints: ['src/renderer/src/workbuddy/conversation/source-labels.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
outfile: output,
|
||||
logLevel: 'silent'
|
||||
})
|
||||
const labels = await import(`${pathToFileURL(output).href}?run=${Date.now()}`)
|
||||
const label = labels.sourceConversationLabel
|
||||
|
||||
assert.equal(labels.SOURCE_CONVERSATION_DEFAULT_LOCALE, 'zh-CN')
|
||||
assert.equal(label('tool.command.confirm'), '确认执行命令?')
|
||||
assert.equal(label('tool.command.run'), '运行')
|
||||
assert.equal(label('tool.command.skip'), '跳过')
|
||||
assert.equal(label('tool.command.reject'), '拒绝')
|
||||
assert.equal(label('tool.writeFile.editing'), '修改中')
|
||||
assert.equal(label('tool.writeFile.create'), '创建')
|
||||
assert.equal(label('tool.writeFile.accept'), '接受')
|
||||
assert.equal(label('fold.completedWithDuration', { duration: '2.4 秒' }), '已完成 2.4 秒')
|
||||
assert.equal(label('chatInput.modelSelector.thinkingMode'), '思考模式')
|
||||
|
||||
assert.equal(label('tool.command.confirm', undefined, 'en-US'), 'Run this command?')
|
||||
assert.equal(label('tool.command.run', undefined, 'en-US'), 'Run')
|
||||
assert.equal(label('tool.command.skip', undefined, 'en-US'), 'Skip')
|
||||
assert.equal(label('tool.command.reject', undefined, 'en-US'), 'Reject')
|
||||
assert.equal(label('tool.writeFile.editing', undefined, 'en-US'), 'Editing')
|
||||
assert.equal(label('tool.writeFile.create', undefined, 'en-US'), 'Create')
|
||||
assert.equal(label('tool.writeFile.accept', undefined, 'en-US'), 'Accept')
|
||||
assert.equal(label('fold.completedWithDuration', { duration: '2.4 sec' }, 'en-US'), 'Completed 2.4 sec')
|
||||
assert.equal(label('chatInput.modelSelector.thinkingMode', undefined, 'en-US'), 'Thinking mode')
|
||||
|
||||
assert.equal(label('fold.completedWithDuration'), '已完成 {duration}')
|
||||
assert.equal(label('fold.completedWithDuration', { duration: 0 }), '已完成 0')
|
||||
assert.equal(label('not.a.source.label'), 'not.a.source.label')
|
||||
console.log('Modern conversation label regression passed (zh-CN/en-US, source anchors, interpolation, fallback).')
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { authorizedElectronFile } from './ElectronFileScope.js';
|
||||
import { decodeCanonicalBase64 } from '../domain/webScreenshot.js';
|
||||
|
||||
const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024;
|
||||
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
||||
const types=new Set(['image/png','image/jpeg','image/gif','image/webp']);
|
||||
const mime=value=>value==='image/jpg'?'image/jpeg':value;
|
||||
function sniff(bytes){
|
||||
if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png';
|
||||
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
|
||||
if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif';
|
||||
if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp';
|
||||
}
|
||||
function safeSourceUrl(value){
|
||||
try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;}
|
||||
}
|
||||
|
||||
/** Only a caller-selected IMG and its already loaded main-frame resource are eligible. */
|
||||
export async function captureElectronMediaAssets(snapshot,frames,selectors,send){
|
||||
if(!selectors?.length)return undefined;
|
||||
const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[
|
||||
'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.',
|
||||
'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.',
|
||||
'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
|
||||
'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.'
|
||||
]};
|
||||
const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
|
||||
const document=snapshot.documents?.find(value=>str(value.frameId)===main.frame_id);
|
||||
const stable=async()=>{const frame=(await send('Page.getFrameTree',{})).frameTree?.frame;if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw Error('frame_changed');};
|
||||
for(const selector of selectors.slice(0,MAX_ASSETS)){
|
||||
const item={selector,frame_id:main.frame_id,status:'unknown',reason:'unavailable'};
|
||||
result.items.push(item);
|
||||
try{
|
||||
await stable();
|
||||
if(!document||await authorizedElectronFile(str(document.documentURL))!==main.file_path)throw Error('snapshot_frame_unavailable');
|
||||
const root=(await send('DOM.getDocument',{depth:0,pierce:false})).root;
|
||||
const ids=(await send('DOM.querySelectorAll',{nodeId:root.nodeId,selector})).nodeIds??[];
|
||||
if(ids.length!==1)throw Error(ids.length?'selector_ambiguous':'selector_absent');
|
||||
const node=(await send('DOM.describeNode',{nodeId:ids[0],depth:0,pierce:false})).node;
|
||||
if(node?.nodeName!=='IMG')throw Error('selected_node_not_img');
|
||||
const raw=document.nodes??{},index=raw.backendNodeId?.indexOf(node.backendNodeId)??-1;
|
||||
if(index<0||str(raw.nodeName?.[index])!=='IMG')throw Error('snapshot_identity_unavailable');
|
||||
item.node_index=index;item.backend_node_id=node.backendNodeId;
|
||||
let excluded=false;
|
||||
for(let n=index;n>=0;n=raw.parentIndex?.[n]??-1){const tag=str(raw.nodeName?.[n]),attrs=raw.attributes?.[n]??[];if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag))excluded=true;for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='contenteditable'&&str(attrs[a+1])!=='false')excluded=true;}
|
||||
if(excluded)throw Error('editable_subtree_excluded');
|
||||
const layoutIndex=document.layout?.nodeIndex?.indexOf(index)??-1,bounds=document.layout?.bounds?.[layoutIndex];
|
||||
item.visibility={status:'unknown',reason:'no_visible_layout'};
|
||||
if(layoutIndex>=0&&bounds&&bounds[2]>0&&bounds[3]>0)try{
|
||||
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
|
||||
const x=bounds[0]-(viewport.pageX??document.scrollOffsetX??0),y=bounds[1]-(viewport.pageY??document.scrollOffsetY??0);
|
||||
const left=Math.max(0,x),top=Math.max(0,y),right=Math.min(viewport.clientWidth,x+bounds[2]),bottom=Math.min(viewport.clientHeight,y+bounds[3]);
|
||||
if(right<=left||bottom<=top)item.visibility={status:'unknown',reason:'outside_visible_viewport'};
|
||||
else{
|
||||
item.visibility={status:'covered',reason:'sampled_points_hit_other_layers'};item.visibility_samples=[];
|
||||
for(const [fx,fy] of [[.5,.05],[.5,.5],[.5,.95],[.05,.05],[.95,.05],[.05,.5],[.95,.5],[.05,.95],[.95,.95]]){
|
||||
const point={x:Math.floor(left+(right-left)*fx),y:Math.floor(top+(bottom-top)*fy)};
|
||||
const hit=await send('DOM.getNodeForLocation',{...point,includeUserAgentShadowDOM:false,ignorePointerEventsNone:true});
|
||||
const hitIndex=raw.backendNodeId?.indexOf(hit.backendNodeId)??-1,hitAttrs=raw.attributes?.[hitIndex]??[];
|
||||
let hitClass='';for(let a=0;a<hitAttrs.length;a+=2)if(str(hitAttrs[a])==='class')hitClass=str(hitAttrs[a+1]).slice(0,500);
|
||||
item.visibility_samples.push({...point,...(hit.backendNodeId?{hit_backend_node_id:hit.backendNodeId}:{}),...(hitIndex>=0?{hit_node_index:hitIndex,hit_node_name:str(raw.nodeName?.[hitIndex]),hit_class:hitClass}:{})});
|
||||
if(hit.backendNodeId===node.backendNodeId){item.visibility={status:'observed'};break;}
|
||||
}
|
||||
}
|
||||
}catch{item.visibility={status:'unknown',reason:'hit_test_unavailable'};}
|
||||
const current=raw.currentSourceURL,at=current?.index?.indexOf(index)??-1;
|
||||
let source=at>=0?str(current.value?.[at]):'',selection='current_source_url';
|
||||
if(!source){selection='src';const attrs=raw.attributes?.[index]??[];for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='src')source=str(attrs[a+1]);}
|
||||
if(!source)throw Error('source_url_unavailable');
|
||||
const selectedUrl=new URL(source,str(document.documentURL)),url=new URL(selectedUrl);url.hash='';
|
||||
const selectedAttributes={},nodeAttrs=raw.attributes?.[index]??[];
|
||||
for(let a=0;a<nodeAttrs.length;a+=2){const name=str(nodeAttrs[a]),value=str(nodeAttrs[a+1]);if(['class','id','width','height','loading','decoding','alt'].includes(name))selectedAttributes[name]=value.slice(0,500);else if(name==='src'){try{selectedAttributes.src=safeSourceUrl(new URL(value,str(document.documentURL)).href)??'[URL omitted]';}catch{selectedAttributes.src='[URL unavailable]';}}}
|
||||
item.selected_node={node_name:'IMG',attributes:selectedAttributes,current_source_url:at>=0?safeSourceUrl(str(current.value?.[at])):null};
|
||||
const tree=(await send('Page.getResourceTree',{})).frameTree;
|
||||
if(!tree?.frame||tree.frame.id!==main.frame_id||await authorizedElectronFile(tree.frame.url)!==main.file_path)throw Error('frame_changed');
|
||||
const resource=tree.resources?.find(value=>value.url===url.href&&value.type==='Image'&&!value.failed&&!value.canceled);
|
||||
if(!resource)throw Error('selected_source_not_loaded_in_main_frame');
|
||||
const contentType=mime(resource.mimeType?.toLowerCase());
|
||||
item.source={url:safeSourceUrl(selectedUrl.href),url_sha256:sha(selectedUrl.href),selection,mime_type:contentType??'unknown'};
|
||||
if(!types.has(contentType))throw Error('unsupported_image_type');
|
||||
if(resource.contentSize>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
|
||||
const content=await send('Page.getResourceContent',{frameId:main.frame_id,url:resource.url});
|
||||
if(typeof content.content!=='string'||!content.base64Encoded)throw Error('binary_content_unavailable');
|
||||
if(content.content.length>4*Math.ceil(MAX_ASSET_BYTES/3))throw Error('asset_byte_budget_exceeded');
|
||||
const bytes=decodeCanonicalBase64(content.content);
|
||||
if(!bytes?.length)throw Error('invalid_base64');
|
||||
if(bytes.length>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
|
||||
if(result.total_bytes+bytes.length>MAX_TOTAL_BYTES)throw Error('total_byte_budget_exceeded');
|
||||
if(sniff(bytes)!==contentType)throw Error('image_signature_mismatch');
|
||||
await stable();
|
||||
item.status='exported';delete item.reason;
|
||||
item.artifact={mime_type:contentType,bytes:bytes.length,sha256:sha(bytes),data_base64:bytes.toString('base64')};
|
||||
result.total_bytes+=bytes.length;
|
||||
}catch(error){const allowed=new Set(['frame_changed','snapshot_frame_unavailable','selector_ambiguous','selector_absent','selected_node_not_img','snapshot_identity_unavailable','editable_subtree_excluded','no_visible_layout','outside_visible_viewport','visibility_unconfirmed_or_covered','source_url_unavailable','selected_source_not_loaded_in_main_frame','unsupported_image_type','asset_byte_budget_exceeded','binary_content_unavailable','invalid_base64','total_byte_budget_exceeded','image_signature_mismatch']);item.reason=allowed.has(error.message)?error.message:'protocol_resource_unavailable';}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Read actual fonts used for bounded, visible message/name/code samples. No renderer evaluation.
|
||||
export async function captureElectronPlatformFonts(snapshot,frames,send) {
|
||||
const categories=['assistant-name','chinese-body','english-body','user-bubble','code'];
|
||||
const samples=[],missing=[];
|
||||
const document=snapshot.documents?.find(d=>snapshot.strings[d.frameId]===frames[0].frame_id);
|
||||
if(!document)return {samples,missing:categories.map(category=>({category,reason:'Authorized main document absent'}))};
|
||||
const strings=snapshot.strings,nodes=document.nodes,layout=document.layout;
|
||||
const value=index=>strings[index]??'';
|
||||
const className=index=>{const a=nodes.attributes[index]??[];for(let i=0;i<a.length;i+=2)if(value(a[i])==='class')return value(a[i+1]);return ''};
|
||||
const hasClass=(index,name)=>className(index).split(' ').includes(name);
|
||||
const under=(index,test)=>{for(let n=index;n>=0;n=nodes.parentIndex[n])if(test(n))return true;return false};
|
||||
const code=index=>under(index,n=>/^(CODE|PRE)$/.test(value(nodes.nodeName[n])));
|
||||
const text=index=>{let out='';for(let n=0;n<nodes.nodeName.length;n++)if(value(nodes.nodeName[n])==='#text'&&under(n,i=>i===index))out+=value(nodes.nodeValue[n]);return out};
|
||||
const viewport=snapshot.reaViewport;
|
||||
const positionOf=new Map(layout.nodeIndex.map((n,p)=>[n,p]));
|
||||
const visible=new Set(layout.nodeIndex.filter((_,position)=>{
|
||||
if(!viewport)return true;
|
||||
const [x,y,w,h]=layout.bounds[position]??[];
|
||||
let left=viewport.page_x,top=viewport.page_y,right=left+viewport.width,bottom=top+viewport.height;
|
||||
for(let n=nodes.parentIndex[layout.nodeIndex[position]];n>=0;n=nodes.parentIndex[n]){
|
||||
const parentPosition=positionOf.get(n);if(parentPosition===undefined)continue;
|
||||
const overflowIndex=snapshot.reaLayoutProperties.indexOf('overflow');
|
||||
const overflow=value((layout.styles[parentPosition]??[])[overflowIndex]);
|
||||
if(!/auto|scroll|hidden|clip/.test(overflow))continue;
|
||||
const [px,py,pw,ph]=layout.bounds[parentPosition];
|
||||
left=Math.max(left,px);top=Math.max(top,py);right=Math.min(right,px+pw);bottom=Math.min(bottom,py+ph);
|
||||
}
|
||||
return w>0&&h>0&&x>=left&&y>=top&&x+w<=right&&y+h<=bottom;
|
||||
}));
|
||||
const candidates=[];
|
||||
for(let n=0;n<nodes.nodeName.length;n++)if(value(nodes.nodeName[n])==='#text'){
|
||||
const parent=nodes.parentIndex[n];if(parent>=0&&visible.has(parent)&&!candidates.includes(parent))candidates.push(parent);
|
||||
}
|
||||
await send('DOM.getDocument',{depth:0});
|
||||
for(const category of categories){
|
||||
let found=false,reason='No short visible sample of this category in the current document';
|
||||
const selected=category==='assistant-name'?layout.nodeIndex.filter(n=>visible.has(n)&&hasClass(n,'cr-agent__name')):candidates;
|
||||
for(const index of selected){
|
||||
if(under(index,n=>/^(INPUT|TEXTAREA|SELECT)$/.test(value(nodes.nodeName[n]))||(nodes.attributes[n]??[]).some((a,i,attrs)=>i%2===0&&value(a)==='contenteditable'&&value(attrs[i+1])!=='false')))continue;
|
||||
const sample=text(index);if(!sample.trim()||sample.length>256)continue;
|
||||
const body=under(index,n=>hasClass(n,'cr-agent__content'))&&under(index,n=>hasClass(n,'cr-text-block'))&&!under(index,n=>hasClass(n,'cr-code-like-box'));
|
||||
// Plain leaf prose avoids treating a composite bold/inline subtree as one default-body font sample.
|
||||
const proseLeaf=!nodes.parentIndex.some((parent,n)=>parent===index&&value(nodes.nodeName[n])!=='#text'&&value(nodes.nodeName[n])!=='BR');
|
||||
if((category==='chinese-body'||category==='english-body')&&!proseLeaf)continue;
|
||||
const weightProperty=snapshot.reaLayoutProperties.indexOf('font-weight');
|
||||
if((category==='chinese-body'||category==='english-body')&&weightProperty>=0&&value((layout.styles[positionOf.get(index)]??[])[weightProperty])!=='400')continue;
|
||||
if(category==='chinese-body'&&(!body||code(index)||!/[\u3400-\u9fff]/.test(sample)))continue;
|
||||
if(category==='english-body'&&(!body||code(index)||/[\u3400-\u9fff]/.test(sample)||!/[A-Za-z]{3}/.test(sample)))continue;
|
||||
if(category==='user-bubble'&&!under(index,n=>hasClass(n,'cr-self-bubble')))continue;
|
||||
if(category==='code'&&!code(index))continue;
|
||||
const backend=nodes.backendNodeId?.[index];if(!backend){reason='Backend node identity absent';continue;}
|
||||
try{
|
||||
const pushed=await send('DOM.pushNodesByBackendIdsToFrontend',{backendNodeIds:[backend]});const nodeId=pushed.nodeIds?.[0];if(!nodeId)continue;
|
||||
const fonts=(await send('CSS.getPlatformFontsForNode',{nodeId})).fonts;
|
||||
if(!Array.isArray(fonts)||!fonts.some(f=>f.glyphCount>0)){reason='No rendered glyph fonts reported';continue;}
|
||||
const position=layout.nodeIndex.indexOf(index),styleIndexes=layout.styles[position]??[];
|
||||
// The order is supplied by the visuals module, not inferred from font names.
|
||||
const properties=snapshot.reaLayoutProperties;
|
||||
const styles=Object.fromEntries(properties.map((p,i)=>[p,value(styleIndexes[i])]));
|
||||
samples.push({category,text:sample,fontFamily:styles['font-family']??'',...(category==='code'?{codeContext:under(index,n=>value(nodes.nodeName[n])==='PRE')?'block':'inline'}:{}),platformFonts:fonts.map(f=>({familyName:f.familyName,postScriptName:f.postScriptName??'',isCustomFont:f.isCustomFont,glyphCount:f.glyphCount})),bounds:layout.bounds[position],styles});
|
||||
found=true;break;
|
||||
}catch{reason='Platform font query failed for the visible candidate';}
|
||||
}
|
||||
if(!found)missing.push({category,reason});
|
||||
}
|
||||
return {samples,missing};
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
const MAX_SHEETS = 1024;
|
||||
const MAX_SHEET_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
/** Retain only stylesheet lifecycle metadata from the selected target session. */
|
||||
export const ingestElectronStyleEvent = (event, stylesMap) => {
|
||||
if (event?.method === "CSS.styleSheetRemoved") {
|
||||
const styleSheetId = stringValue(recordValue(event.params)?.styleSheetId);
|
||||
if (styleSheetId !== undefined)
|
||||
stylesMap.delete(styleSheetId);
|
||||
return;
|
||||
}
|
||||
if (event?.method !== "CSS.styleSheetAdded")
|
||||
return;
|
||||
const header = recordValue(recordValue(event.params)?.header);
|
||||
const styleSheetId = stringValue(header?.styleSheetId);
|
||||
if (styleSheetId === undefined || styleSheetId === "")
|
||||
return;
|
||||
stylesMap.set(styleSheetId, {
|
||||
styleSheetId,
|
||||
frameId: stringValue(header?.frameId) ?? null,
|
||||
sourceURL: stringValue(header?.sourceURL) ?? null,
|
||||
origin: stringValue(header?.origin) ?? null,
|
||||
isInline: header?.isInline === true,
|
||||
isConstructed: header?.isConstructed === true,
|
||||
});
|
||||
};
|
||||
|
||||
/** Capture CSS text only for authorized file frames and explicit source opt-in. */
|
||||
export const captureElectronStyles = async (input) => {
|
||||
const styles = [...input.styles.values()];
|
||||
const items = [];
|
||||
let excluded = 0;
|
||||
let sourceFailures = 0;
|
||||
let truncated = false;
|
||||
let includedBytes = 0;
|
||||
let sourceRequests = 0;
|
||||
const limitationNotes = new Set();
|
||||
void input.completeness;
|
||||
|
||||
limitationNotes.add("Stylesheet inventory includes only CSS.styleSheetAdded events still present at the capture snapshot.");
|
||||
|
||||
for (const style of styles) {
|
||||
const scoped = await authorizedStyle(style, input.frameIds);
|
||||
if (scoped === undefined) {
|
||||
excluded += 1;
|
||||
continue;
|
||||
}
|
||||
if (input.request?.include_style_sources !== true)
|
||||
continue;
|
||||
|
||||
const identity = {
|
||||
style_sheet_id: style.styleSheetId,
|
||||
frame_id: scoped.frameId,
|
||||
file_path: scoped.filePath,
|
||||
origin: style.origin,
|
||||
is_inline: style.isInline,
|
||||
is_constructed: style.isConstructed,
|
||||
};
|
||||
const item = {
|
||||
style_sheet_key: `electron_stylesheet_${digestCanonicalValue(identity, "CDP capture")}`,
|
||||
style_sheet_id: style.styleSheetId,
|
||||
frame_id: scoped.frameId,
|
||||
file_path: scoped.filePath,
|
||||
origin: style.origin,
|
||||
is_inline: style.isInline,
|
||||
is_constructed: style.isConstructed,
|
||||
source: { included: false, reason: "CSS source was not requested" },
|
||||
};
|
||||
|
||||
if (sourceRequests >= MAX_SHEETS) {
|
||||
sourceFailures += 1;
|
||||
truncated = true;
|
||||
item.source = { included: false, reason: "stylesheet source count limit reached" };
|
||||
limitationNotes.add(`CSS source capture reached the ${String(MAX_SHEETS)} stylesheet limit.`);
|
||||
items.push(item);
|
||||
continue;
|
||||
}
|
||||
sourceRequests += 1;
|
||||
|
||||
let sourceText;
|
||||
try {
|
||||
const result = await input.connection.send(
|
||||
"CSS.getStyleSheetText",
|
||||
{ styleSheetId: style.styleSheetId },
|
||||
input.sessionId,
|
||||
input.signal
|
||||
);
|
||||
sourceText = stringValue(recordValue(result)?.text);
|
||||
}
|
||||
catch (cause) {
|
||||
if (input.signal?.aborted === true)
|
||||
throw cause;
|
||||
sourceFailures += 1;
|
||||
item.source = { included: false, reason: "CSS.getStyleSheetText failed" };
|
||||
limitationNotes.add("CSS text could not be retrieved for one or more authorized stylesheets.");
|
||||
items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sourceText === undefined) {
|
||||
sourceFailures += 1;
|
||||
item.source = { included: false, reason: "CSS.getStyleSheetText returned invalid text" };
|
||||
limitationNotes.add("CSS text was invalid or absent for one or more authorized stylesheets.");
|
||||
items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bytes = Buffer.byteLength(sourceText, "utf8");
|
||||
if (bytes > MAX_SHEET_BYTES) {
|
||||
sourceFailures += 1;
|
||||
truncated = true;
|
||||
item.source = { included: false, reason: "stylesheet source exceeds the per-stylesheet byte limit" };
|
||||
limitationNotes.add(`CSS source capture omitted text larger than ${String(MAX_SHEET_BYTES)} bytes per stylesheet.`);
|
||||
items.push(item);
|
||||
continue;
|
||||
}
|
||||
if (includedBytes + bytes > MAX_TOTAL_BYTES) {
|
||||
sourceFailures += 1;
|
||||
truncated = true;
|
||||
item.source = { included: false, reason: "aggregate stylesheet source byte limit reached" };
|
||||
limitationNotes.add(`CSS source capture omitted text after reaching the ${String(MAX_TOTAL_BYTES)} byte aggregate limit.`);
|
||||
items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
includedBytes += bytes;
|
||||
item.source = { included: true, artifact: createWebTextArtifact(sourceText, "text/css") };
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
if (input.request?.include_style_sources !== true)
|
||||
limitationNotes.add("Stylesheet source capture was not selected; no CSS.getStyleSheetText requests were sent.");
|
||||
if (excluded > 0)
|
||||
limitationNotes.add(`${String(excluded)} stylesheet(s) were excluded because their origin or frame was outside the authorized local scope.`);
|
||||
if (sourceFailures > 0)
|
||||
limitationNotes.add(`${String(sourceFailures)} stylesheet source(s) failed retrieval or exceeded a source budget.`);
|
||||
input.limitations.push(...limitationNotes);
|
||||
|
||||
return {
|
||||
total: styles.length,
|
||||
items: items.sort((left, right) => compareUnicodeCodePoints(left.style_sheet_key, right.style_sheet_key)),
|
||||
excluded,
|
||||
source_failures: sourceFailures,
|
||||
truncated,
|
||||
limits: {
|
||||
max_sheets: MAX_SHEETS,
|
||||
max_sheet_bytes: MAX_SHEET_BYTES,
|
||||
max_total_bytes: MAX_TOTAL_BYTES,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const authorizedStyle = async (style, frameIds) => {
|
||||
if (style.frameId === null || !frameIds.has(style.frameId) || style.origin !== "regular")
|
||||
return undefined;
|
||||
const sourceURL = style.sourceURL ?? "";
|
||||
if (sourceURL === "") {
|
||||
if (!style.isInline && !style.isConstructed)
|
||||
return undefined;
|
||||
return { frameId: style.frameId, filePath: null };
|
||||
}
|
||||
const filePath = await authorizedElectronFile(sourceURL);
|
||||
return filePath === undefined ? undefined : { frameId: style.frameId, filePath };
|
||||
};
|
||||
|
||||
const recordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value
|
||||
: undefined;
|
||||
const stringValue = (value) => typeof value === "string" ? value : undefined;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { createWebScreenshotArtifact, decodeCanonicalBase64 } from '../domain/webScreenshot.js';
|
||||
import { readPngHeaderDimensions } from '../domain/pngHeader.js';
|
||||
import { authorizedElectronFile } from './ElectronFileScope.js';
|
||||
|
||||
export async function runElectronVisualProbe(context,frames,send,makeCleanupSend) {
|
||||
const probe=context.input.visual_probe;
|
||||
if(!probe||probe.actions.length===0)return undefined;
|
||||
const result={status:'completed',steps:[],frames:[],restoration:[],limitations:[
|
||||
'Native debugging input only; no renderer JavaScript evaluation or model request is performed by this provider.',
|
||||
'Frames are sequential PNG samples with measured timestamps, not a guaranteed video frame rate.',
|
||||
'Viewport and media cleanup clear the overrides; any overrides that existed before this request were not observable.',
|
||||
'Click restoration is suitable for caller-selected toggles only; other application effects are not rolled back.',
|
||||
'Scroll restoration applies the inverse wheel input; clipping, scroll inertia and layout changes can prevent exact position restoration.',
|
||||
'Keyboard focus and other keyboard effects are not restored. Pointer cleanup moves to the viewport origin, not the prior pointer position.'
|
||||
,'Text replacement is limited to visible, enabled single-line text/search inputs; it does not submit the input or restore its prior value.'
|
||||
]};
|
||||
const start=performance.now(),undo=[];let viewport=false,media=false,hover=false,totalBytes=0;
|
||||
const main=frames[0];
|
||||
const stable=async()=>{
|
||||
const tree=await send('Page.getFrameTree',{});const frame=tree.frameTree?.frame;
|
||||
if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw new Error('Selected Electron frame changed');
|
||||
};
|
||||
const select=async(selector,index)=>{
|
||||
const document=await send('DOM.getDocument',{depth:0});
|
||||
const selected=await send('DOM.querySelectorAll',{nodeId:document.root.nodeId,selector});
|
||||
if(index===undefined&&selected.nodeIds.length!==1)throw new Error('Selector must match exactly one node or supply an explicit index');
|
||||
const nodeId=selected.nodeIds[index??0];if(!nodeId)throw new Error('Selected node is absent');
|
||||
return nodeId;
|
||||
};
|
||||
const point=async(nodeId)=>{
|
||||
const box=(await send('DOM.getBoxModel',{nodeId})).model?.content;
|
||||
if(!box||box.length!==8)throw new Error('Selected node has no visible box');
|
||||
const x=(box[0]+box[2]+box[4]+box[6])/4,y=(box[1]+box[3]+box[5]+box[7])/4;
|
||||
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
|
||||
if(x<0||y<0||x>=viewport.clientWidth||y>=viewport.clientHeight)throw new Error('Selected node is outside the visible viewport');
|
||||
const hit=await send('DOM.getNodeForLocation',{x:Math.floor(x),y:Math.floor(y),includeUserAgentShadowDOM:false});
|
||||
const target=(await send('DOM.describeNode',{nodeId,depth:-1,pierce:false})).node;
|
||||
let visited=0;const contains=node=>{
|
||||
if(!node||++visited>5000)return false;
|
||||
return node.backendNodeId===hit.backendNodeId||(node.children??[]).some(contains);
|
||||
};
|
||||
if(!hit.backendNodeId||!contains(target))throw new Error('Selected node center is clipped or covered by another element');
|
||||
return {x,y};
|
||||
};
|
||||
const click=async(action)=>{
|
||||
const at=await point(await select(action.selector,action.index));
|
||||
await send('Input.dispatchMouseEvent',{type:'mouseMoved',...at});
|
||||
await send('Input.dispatchMouseEvent',{type:'mousePressed',...at,button:'left',clickCount:1});
|
||||
await send('Input.dispatchMouseEvent',{type:'mouseReleased',...at,button:'left',clickCount:1});
|
||||
hover=true;
|
||||
};
|
||||
const scroll=async(action)=>{
|
||||
const at=await point(await select(action.selector,action.index));
|
||||
await send('Input.dispatchMouseEvent',{type:'mouseWheel',...at,deltaX:0,deltaY:action.delta_y});
|
||||
};
|
||||
const sample=async(step_id)=>{
|
||||
await stable();
|
||||
const captured=await send('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false});
|
||||
const bytes=decodeCanonicalBase64(captured.data);
|
||||
if(!bytes||!readPngHeaderDimensions(bytes))throw new Error('Invalid scenario PNG');
|
||||
totalBytes+=bytes.length;if(totalBytes>32*1024*1024)throw new Error('Scenario PNG budget exceeded');
|
||||
const regions=[];
|
||||
for(const selector of probe.track_selectors){
|
||||
let bounds=null;
|
||||
try{const b=(await send('DOM.getBoxModel',{nodeId:await select(selector)})).model.border;const xs=[b[0],b[2],b[4],b[6]],ys=[b[1],b[3],b[5],b[7]];bounds=[Math.min(...xs),Math.min(...ys),Math.max(...xs)-Math.min(...xs),Math.max(...ys)-Math.min(...ys)];}catch{}
|
||||
regions.push({selector,bounds});
|
||||
}
|
||||
result.frames.push({step_id,at_ms:performance.now()-start,artifact:createWebScreenshotArtifact(bytes),regions});
|
||||
};
|
||||
const pause=milliseconds=>new Promise((resolve,reject)=>{const timer=setTimeout(done,milliseconds);function done(){context.signal?.removeEventListener('abort',abort);resolve()}function abort(){clearTimeout(timer);context.signal?.removeEventListener('abort',abort);reject(new Error('Visual probe cancelled'))}context.signal?.addEventListener('abort',abort,{once:true});if(context.signal?.aborted)abort()});
|
||||
try{
|
||||
for(const action of probe.actions){
|
||||
await stable();
|
||||
try{
|
||||
if(action.kind==='click'){await click(action);if(action.restore)undo.push({kind:'click',action});}
|
||||
else if(action.kind==='hover'){await send('Input.dispatchMouseEvent',{type:'mouseMoved',...await point(await select(action.selector,action.index))});hover=true;}
|
||||
else if(action.kind==='scroll'){await scroll(action);if(action.restore)undo.push({kind:'scroll',action:{...action,delta_y:-action.delta_y}});}
|
||||
else if(action.kind==='viewport'){await send('Emulation.setDeviceMetricsOverride',{width:action.width,height:action.height,deviceScaleFactor:0,mobile:false});viewport=true;}
|
||||
else if(action.kind==='media'){await send('Emulation.setEmulatedMedia',{features:[{name:'prefers-reduced-motion',value:action.reduced_motion}]});media=true;}
|
||||
else if(action.kind==='wait')await pause(action.duration_ms);
|
||||
else if(action.kind==='text'){
|
||||
const nodeId=await select(action.selector,action.index);
|
||||
await point(nodeId);
|
||||
const node=(await send('DOM.describeNode',{nodeId})).node,raw=node.attributes??[];
|
||||
const attrs=Object.fromEntries(Array.from({length:raw.length/2},(_,index)=>[raw[index*2].toLowerCase(),raw[index*2+1]]));
|
||||
if(node.nodeName!=='INPUT'||!['text','search'].includes((attrs.type??'text').toLowerCase())||'readonly'in attrs||'disabled'in attrs)throw new Error('Text replacement requires an enabled text/search INPUT');
|
||||
await send('DOM.focus',{nodeId});
|
||||
const selectAll={key:'a',code:'KeyA',windowsVirtualKeyCode:65,nativeVirtualKeyCode:65,modifiers:2};
|
||||
await send('Input.dispatchKeyEvent',{type:'keyDown',...selectAll});
|
||||
await send('Input.dispatchKeyEvent',{type:'keyUp',...selectAll});
|
||||
const remove={key:'Backspace',code:'Backspace',windowsVirtualKeyCode:8,nativeVirtualKeyCode:8,modifiers:0};
|
||||
await send('Input.dispatchKeyEvent',{type:'keyDown',...remove});
|
||||
await send('Input.dispatchKeyEvent',{type:'keyUp',...remove});
|
||||
if(action.text)await send('Input.insertText',{text:action.text});
|
||||
}
|
||||
else if(action.kind==='key'){
|
||||
if(action.selector){const nodeId=await select(action.selector,action.index);const node=(await send('DOM.describeNode',{nodeId})).node;const attrs=node.attributes??[];
|
||||
if(['Enter',' '].includes(action.key)&&(/^(INPUT|TEXTAREA)$/i.test(node.nodeName)||attrs.includes('contenteditable')))throw new Error('Activation key on editable content is excluded');
|
||||
await send('DOM.focus',{nodeId});
|
||||
}else if(['Enter',' '].includes(action.key))throw new Error('Activation keys require an explicit noneditable target');
|
||||
const codes={Escape:27,Enter:13,Tab:9,ArrowDown:40,ArrowUp:38,ArrowLeft:37,ArrowRight:39,Home:36,End:35,' ':32,'+':187,'-':189,'0':48};
|
||||
const packet={key:action.key,windowsVirtualKeyCode:codes[action.key],nativeVirtualKeyCode:codes[action.key],modifiers:action.modifiers};
|
||||
await send('Input.dispatchKeyEvent',{type:'keyDown',...packet,...(action.key==='Enter'?{text:'\r'}:action.key===' '?{text:' '}:{})});
|
||||
await send('Input.dispatchKeyEvent',{type:'keyUp',...packet});
|
||||
}
|
||||
const actionCompleted=performance.now()-start;
|
||||
for(let n=0;n<action.frames;n++){if(n)await pause(action.interval_ms);await sample(action.step_id);}
|
||||
result.steps.push({step_id:action.step_id,kind:action.kind,status:'completed',error:null,action_completed_at_ms:actionCompleted});
|
||||
}catch(error){result.status='failed';result.steps.push({step_id:action.step_id,kind:action.kind,status:'failed',error:String(error.message??error)});break;}
|
||||
}
|
||||
}finally{
|
||||
if(makeCleanupSend)send=makeCleanupSend();
|
||||
if(viewport){try{await send('Emulation.clearDeviceMetricsOverride',{});result.restoration.push({kind:'viewport',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'viewport',status:'failed',error:String(error.message??error)});}}
|
||||
for(const restoration of undo.reverse()){
|
||||
try{await stable();if(restoration.kind==='click')await click(restoration.action);else await scroll(restoration.action);result.restoration.push({kind:restoration.kind,status:'completed',error:null});}
|
||||
catch(error){result.status='failed';result.restoration.push({kind:restoration.kind,status:'failed',error:String(error.message??error)});}
|
||||
}
|
||||
if(media){try{await send('Emulation.setEmulatedMedia',{features:[]});result.restoration.push({kind:'media',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'media',status:'failed',error:String(error.message??error)});}}
|
||||
if(hover){try{await send('Input.dispatchMouseEvent',{type:'mouseMoved',x:0,y:0});result.restoration.push({kind:'neutral-pointer',status:'completed',error:null});}catch(error){result.status='failed';result.restoration.push({kind:'neutral-pointer',status:'failed',error:String(error.message??error)});}}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createWebScreenshotArtifact, decodeCanonicalBase64 } from '../domain/webScreenshot.js';
|
||||
import { readPngHeaderDimensions } from '../domain/pngHeader.js';
|
||||
import { BrowserObservationError } from '../domain/browserObservationError.js';
|
||||
import { authorizedElectronFile } from './ElectronFileScope.js';
|
||||
import { runElectronVisualProbe } from './CdpElectronVisualProbe.js';
|
||||
import { captureElectronPlatformFonts } from './CdpElectronPlatformFonts.js';
|
||||
import { captureElectronMediaAssets } from './CdpElectronMediaAssets.js';
|
||||
|
||||
export const ELECTRON_LAYOUT_PROPERTIES = ['display', 'position', 'font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing', 'color', 'background-color', 'border-radius', 'border-top-width', 'border-top-color', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'gap', 'align-items', 'justify-content', 'box-shadow', 'overflow', 'max-width', 'min-height','transition-property','transition-duration','transition-timing-function','opacity','visibility','transform','text-rendering','-webkit-font-smoothing','font-kerning','font-synthesis','font-optical-sizing','font-feature-settings','text-shadow','zoom','-webkit-text-size-adjust'];
|
||||
|
||||
// Optional bounded message DOM text for reproducing identical fixture content.
|
||||
// Attribute values outside the style identity allowlist and all editable/input subtrees are omitted.
|
||||
export function captureElectronMessageContent(snapshot,frames) {
|
||||
const strings=snapshot.strings??[],value=i=>strings[i]??'';
|
||||
const document=snapshot.documents?.find(d=>value(d.frameId)===frames[0].frame_id);
|
||||
if(!document)return {nodes:[],text_characters:0,truncated:false};
|
||||
const raw=document.nodes,selected=[],roots=new Set(),excluded=new Set();let characters=0,truncated=false;
|
||||
const siblingCounts=new Map(),ordinals=new Map();
|
||||
for(let n=0;n<(raw.nodeName?.length??0);n++){const tag=value(raw.nodeName[n]);if(tag.startsWith('#'))continue;const key=raw.parentIndex[n]+'|'+tag;const count=(siblingCounts.get(key)??0)+1;siblingCounts.set(key,count);ordinals.set(n,count)}
|
||||
const classOf=n=>{const attrs=raw.attributes?.[n]??[];for(let a=0;a<attrs.length;a+=2)if(value(attrs[a])==='class')return value(attrs[a+1]).split(/\s+/);return []};
|
||||
const messageLists=raw.nodeName.map((_,n)=>n).filter(n=>classOf(n).includes('cr-message-list'));
|
||||
const selectorFor=index=>{const parts=[];for(let n=index;n>=0;n=raw.parentIndex[n]){if(messageLists.length===1&&n===messageLists[0]){parts.unshift('.cr-message-list');break}const tag=value(raw.nodeName[n]);if(!tag.startsWith('#'))parts.unshift(tag.toLowerCase()+':nth-of-type('+ordinals.get(n)+')')}return parts.join(' > ')};
|
||||
for(let n=0;n<(raw.nodeName?.length??0);n++){
|
||||
const attrs=raw.attributes?.[n]??[],attributes={};
|
||||
for(let a=0;a<attrs.length;a+=2)if(['class','id','role','lang','data-language'].includes(value(attrs[a])))attributes[value(attrs[a])]=value(attrs[a+1]);
|
||||
const names=(attributes.class??'').split(/\s+/),tag=value(raw.nodeName[n]),parent=raw.parentIndex[n];
|
||||
if(names.includes('cr-self-message')||names.includes('cr-agent'))roots.add(n);
|
||||
if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag)||excluded.has(parent)||attrs.some((a,i)=>i%2===0&&value(a)==='contenteditable'&&value(attrs[i+1])!=='false'))excluded.add(n);
|
||||
let inside=false;for(let p=n;p>=0;p=raw.parentIndex[p])if(roots.has(p)){inside=true;break}
|
||||
if(!inside||excluded.has(n))continue;
|
||||
if(selected.length>=5000){truncated=true;break}
|
||||
const text=tag==='#text'?value(raw.nodeValue[n]):'';
|
||||
if(characters+text.length>65536){truncated=true;break}
|
||||
characters+=text.length;
|
||||
selected.push({node_index:n,parent_index:parent,node_name:tag,attributes,...(tag==='#text'?{text}:{}),...(names.some(name=>['cr-agent','cr-self-message','cr-collapse'].includes(name))?{selector:selectorFor(n)}:{})});
|
||||
}
|
||||
return {nodes:selected,text_characters:characters,truncated};
|
||||
}
|
||||
|
||||
// Called only after inspectCdpElectronPage has authorized the selected file page.
|
||||
export async function captureElectronVisuals(context, frames) {
|
||||
if (!context.input.include_visuals) return undefined;
|
||||
const signal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000);
|
||||
const send = async (method, params) => {
|
||||
if (process.env.REA_VISUAL_TRACE === '1') process.stderr.write(`[REA visuals] ${method}\n`);
|
||||
return context.connection.send(method, params, context.sessionId, signal);
|
||||
};
|
||||
const metrics = await send('Page.getLayoutMetrics', {});
|
||||
let environmentGpu;
|
||||
if (context.input.visual_probe?.include_fonts) {
|
||||
try {
|
||||
// Browser metadata only; no renderer evaluation or GPU benchmark.
|
||||
const {gpu} = await context.connection.send('SystemInfo.getInfo', {}, undefined, signal);
|
||||
const allowed = ['inProcessGpu','skiaBackendType','glImplementationParts','subpixelFontRendering','sandboxed','passthroughCmdDecoder','targetCpuBits','driverVendor','driverVersion','displayType'];
|
||||
const aux = Object.fromEntries(allowed.filter(key => ['string','number','boolean'].includes(typeof gpu.auxAttributes?.[key]) || gpu.auxAttributes?.[key] === null).map(key => [key,gpu.auxAttributes[key]]));
|
||||
environmentGpu = { status:'observed', devices:(gpu.devices??[]).slice(0,8).map(device=>({vendor_id:device.vendorId,device_id:device.deviceId,...(typeof device.deviceString==='string'?{device_string:device.deviceString.slice(0,256)}:{}),...(typeof device.active==='boolean'?{active:device.active}:{})})),aux_attributes:aux,feature_status:Object.fromEntries(Object.entries(gpu.featureStatus??{}).filter(([key,value])=>key.length<100&&typeof value==='string'&&value.length<100)) };
|
||||
} catch { environmentGpu = { status:'unavailable' }; }
|
||||
}
|
||||
if (context.input.visual_probe) { await send('DOM.enable', {}); await send('CSS.enable', {}); }
|
||||
let environmentMedia;
|
||||
if(context.input.visual_probe){
|
||||
try{const media=await send('CSS.getMediaQueries',{});const reduced=(media.medias??[]).find(m=>/^\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)$/i.test(m.text??''));environmentMedia={reduced_motion:reduced?.mediaList?.[0]?.active??null};}
|
||||
catch{environmentMedia={reduced_motion:null}}
|
||||
}
|
||||
const snapshot = await send('DOMSnapshot.captureSnapshot', { computedStyles: ELECTRON_LAYOUT_PROPERTIES, includePaintOrder: false, includeDOMRects: true });
|
||||
const strings = snapshot.strings ?? [];
|
||||
const str = index => typeof strings[index] === 'string' ? strings[index] : '';
|
||||
const frameIds = new Set(frames.map(frame => frame.frame_id));
|
||||
const documents = [];
|
||||
for (const document of snapshot.documents ?? []) {
|
||||
if (!frameIds.has(str(document.frameId)) || await authorizedElectronFile(str(document.documentURL)) === undefined) continue;
|
||||
const nodes = document.nodes ?? {};
|
||||
const layout = document.layout ?? {};
|
||||
const items = [];
|
||||
const svgNodes = [];
|
||||
const svgTags = new Set(['svg', 'path', 'g', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse', 'defs', 'clippath', 'mask']);
|
||||
const svgAttributes = new Set(['class', 'id', 'viewBox', 'width', 'height', 'd', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'fill', 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'transform', 'clip-path', 'opacity']);
|
||||
for (let n = 0; n < (nodes.nodeName?.length ?? 0); n++) {
|
||||
const tag = str(nodes.nodeName[n]);
|
||||
if (!svgTags.has(tag.toLowerCase())) continue;
|
||||
const attributes = {};
|
||||
const attrs = nodes.attributes?.[n] ?? [];
|
||||
for (let a = 0; a < attrs.length; a += 2) if (svgAttributes.has(str(attrs[a]))) attributes[str(attrs[a])] = str(attrs[a + 1]);
|
||||
svgNodes.push({ node_index: n, parent_index: nodes.parentIndex?.[n] ?? -1, node_name: tag, attributes });
|
||||
}
|
||||
for (let i = 0; i < (layout.nodeIndex?.length ?? 0); i++) {
|
||||
const node = layout.nodeIndex[i];
|
||||
const attrs = nodes.attributes?.[node] ?? [];
|
||||
const attributes = {};
|
||||
for (let a = 0; a < attrs.length; a += 2) {
|
||||
const key = str(attrs[a]);
|
||||
// Retain style identity only; never input values, URL attributes or hidden payloads.
|
||||
if (['class', 'id', 'role'].includes(key)) attributes[key] = str(attrs[a + 1]);
|
||||
}
|
||||
const styles = {};
|
||||
for (let p = 0; p < ELECTRON_LAYOUT_PROPERTIES.length; p++) styles[ELECTRON_LAYOUT_PROPERTIES[p]] = str(layout.styles?.[i]?.[p]);
|
||||
const bounds = layout.bounds?.[i];
|
||||
if (!Array.isArray(bounds) || bounds.length !== 4 || !bounds.every(Number.isFinite)) continue;
|
||||
items.push({ node_index: node, parent_index: nodes.parentIndex?.[node] ?? -1, node_name: str(nodes.nodeName?.[node]), attributes, bounds, styles });
|
||||
}
|
||||
documents.push({ frame_id: str(document.frameId), file_path: await authorizedElectronFile(str(document.documentURL)), scroll_x: document.scrollOffsetX ?? 0, scroll_y: document.scrollOffsetY ?? 0, nodes: items, svg_nodes: svgNodes });
|
||||
}
|
||||
const captured = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false });
|
||||
if (typeof captured.data !== 'string' || captured.data.length > 24 * 1024 * 1024) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
|
||||
const bytes = decodeCanonicalBase64(captured.data);
|
||||
const viewport = bytes && readPngHeaderDimensions(bytes);
|
||||
if (!viewport) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
|
||||
const css = metrics.cssLayoutViewport ?? metrics.layoutViewport;
|
||||
if (!css || !Number.isFinite(css.clientWidth) || !Number.isFinite(css.clientHeight)) throw new BrowserObservationError('inspect_electron_page', 'protocol_error');
|
||||
const fonts = context.input.visual_probe?.include_fonts ? await captureElectronPlatformFonts({...snapshot,reaLayoutProperties:ELECTRON_LAYOUT_PROPERTIES,reaViewport:{width:css.clientWidth,height:css.clientHeight,page_x:css.pageX??0,page_y:css.pageY??0}},frames,send) : undefined;
|
||||
const mediaAssets = await captureElectronMediaAssets(snapshot,frames,context.input.visual_probe?.media_selectors,send);
|
||||
// Baseline DOM/fonts/media acquisition must not consume the scenario's input budget.
|
||||
// Both stages remain bounded and still inherit the caller's cancellation.
|
||||
const probeSignal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000);
|
||||
const probeSend = (method,params)=>context.connection.send(method,params,context.sessionId,probeSignal);
|
||||
const probe = await runElectronVisualProbe({...context,signal:probeSignal},frames,probeSend,()=>{const cleanupSignal=AbortSignal.timeout(5000);return (method,params)=>context.connection.send(method,params,context.sessionId,cleanupSignal)});
|
||||
const environment = { ...(environmentMedia?{environment_media:environmentMedia}:{}), ...(environmentGpu?{environment_gpu:environmentGpu}:{}), ...(mediaAssets?{media_assets:mediaAssets}:{}) };
|
||||
return { captured_at: new Date().toISOString(), viewport, css_viewport: { width: css.clientWidth, height: css.clientHeight, page_x: css.pageX ?? 0, page_y: css.pageY ?? 0 }, ...environment,artifact: createWebScreenshotArtifact(bytes), layout: { properties: ELECTRON_LAYOUT_PROPERTIES, documents }, ...(fonts ? {font_samples:fonts.samples,font_missing:fonts.missing} : {}), ...(context.input.visual_probe?.include_message_content?{message_content:captureElectronMessageContent(snapshot,frames)}:{}), ...(probe ? {probe} : {}), limitations: ['Baseline captures the visible viewport; optional native visual_probe actions are recorded separately. No renderer JavaScript is evaluated.', 'DOM layout and screenshot are sequential observations, not an atomic rendering snapshot.', 'Layout omits text and input values; include_fonts retains bounded visible samples; optional include_message_content retains bounded message DOM text including collapsed message descendants, excluding editable/input subtrees.'] };
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Native metadata diagnostic owned by the local REA provider. No target injection or termination.
|
||||
import { spawn } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { createEvidence } from '../domain/evidence.js';
|
||||
export async function inspectDesktopElectronProcesses(processName) {
|
||||
if (process.platform !== 'win32') throw new Error('Windows process metadata provider requires Windows');
|
||||
if (!/^[A-Za-z0-9_.-]{1,80}\.exe$/i.test(processName)) throw new Error('Select one literal executable basename');
|
||||
const script = `$targets=Get-CimInstance Win32_Process -Filter "Name='${processName}'"; $rows=foreach($p in $targets){$debugPort=$null;$processType='main';if($p.CommandLine -match '--remote-debugging-port[= ]+(\\d+)'){$debugPort=[int]$Matches[1]};if($p.CommandLine -match '--type[= ]+([^\\s]+)'){$processType=$Matches[1]};[pscustomobject]@{pid=$p.ProcessId;parentPid=$p.ParentProcessId;executable=$p.ExecutablePath;type=$processType;debugPort=$debugPort}};$listeners=Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue|Where-Object{$_.OwningProcess -in $targets.ProcessId}|Select-Object LocalAddress,LocalPort,OwningProcess;[pscustomobject]@{processes=@($rows);listeners=@($listeners)}|ConvertTo-Json -Depth 5 -Compress`;
|
||||
const result = await new Promise((resolve,reject)=>{
|
||||
const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']});
|
||||
const chunks=[];let size=0;const timer=setTimeout(()=>{child.kill();reject(new Error('Windows metadata query timed out'))},15000);
|
||||
child.stdout.on('data',chunk=>{size+=chunk.length;if(size>1024*1024){child.kill();reject(new Error('Windows metadata budget exceeded'))}else chunks.push(chunk)});
|
||||
child.stderr.on('data',()=>{});
|
||||
child.once('error',error=>{clearTimeout(timer);reject(error)});
|
||||
child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Windows metadata query failed'));try{resolve(JSON.parse(Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/,'')))}catch(error){reject(error)}});
|
||||
});
|
||||
return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron metadata provider',version:'1+state-local.1'},{
|
||||
predicateType:'rea.desktop-electron-processes',operation:'inspect_desktop_electron_processes',parameters:{process_name:processName},result,
|
||||
confidence:'observed',authority:'external-service',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'},
|
||||
limitations:['Native process metadata and listening sockets only; no target injection, file extraction, restart or termination.','Command lines are reduced to process type and declared remote-debugging port; other argument values are omitted.']
|
||||
});
|
||||
}
|
||||
|
||||
// Explicit lifecycle operation. The caller must already have authorization to interrupt WorkBuddy.
|
||||
export async function restartDesktopElectronDebug(executable,port,authorized) {
|
||||
if(process.platform!=='win32'||authorized!==true)throw new Error('An authorized Windows restart is required');
|
||||
if(!Number.isInteger(port)||port<1024||port>65535)throw new Error('Select one debugging port from 1024 to 65535');
|
||||
const expected=path.join(process.env.LOCALAPPDATA,'Programs','WorkBuddy','WorkBuddy.exe');
|
||||
const actual=await fs.realpath(executable),allowed=await fs.realpath(expected);
|
||||
if(actual.toLowerCase()!==allowed.toLowerCase())throw new Error('Restart is limited to the installed WorkBuddy executable');
|
||||
const quoted=actual.replaceAll("'","''");
|
||||
const script=`$ErrorActionPreference='Stop';$exe='${quoted}';$before=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});$close=@();foreach($p in $before){$native=Get-Process -Id $p.ProcessId -ErrorAction SilentlyContinue;if($native -and $native.MainWindowHandle -ne 0){$close+= [pscustomobject]@{pid=$p.ProcessId;requested=$native.CloseMainWindow()}}};$deadline=[datetime]::UtcNow.AddSeconds(8);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 200}while([datetime]::UtcNow -lt $deadline);$forced=@();foreach($p in $before){$current=Get-CimInstance Win32_Process -Filter ("ProcessId="+$p.ProcessId);if($current -and $current.ExecutablePath -eq $exe -and $current.CreationDate -eq $p.CreationDate){Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue;$forced+=$p.ProcessId}};$deadline=[datetime]::UtcNow.AddSeconds(4);do{$remaining=@(Get-CimInstance Win32_Process -Filter "Name='WorkBuddy.exe'"|Where-Object{$_.ExecutablePath -eq $exe});if($remaining.Count -eq 0){break};Start-Sleep -Milliseconds 100}while([datetime]::UtcNow -lt $deadline);if($remaining.Count -ne 0){throw 'Selected WorkBuddy processes did not exit; no replacement was launched'};[pscustomobject]@{beforePids=@($before|ForEach-Object{$_.ProcessId});closeRequests=@($close);forcedPids=@($forced);remaining=0}|ConvertTo-Json -Depth 4 -Compress`;
|
||||
const stopped=await new Promise((resolve,reject)=>{
|
||||
const child=spawn('C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],{windowsHide:true,stdio:['ignore','pipe','pipe']});
|
||||
let output='',errors='';const timer=setTimeout(()=>{child.kill();reject(new Error('Authorized restart shutdown timed out'))},20000);
|
||||
child.stdout.on('data',b=>{output+=b;if(output.length>65536){child.kill();reject(new Error('Restart result budget exceeded'))}});child.stderr.on('data',b=>errors+=b);
|
||||
child.once('error',e=>{clearTimeout(timer);reject(e)});child.once('close',code=>{clearTimeout(timer);if(code!==0)return reject(new Error('Restart shutdown failed: '+errors.slice(0,1000)));try{resolve(JSON.parse(output.replace(/^\uFEFF/,'')))}catch(e){reject(e)}});
|
||||
});
|
||||
const env={...process.env};for(const key of Object.keys(env))if(key.toUpperCase()==='ELECTRON_RUN_AS_NODE')delete env[key];
|
||||
const args=[`--remote-debugging-port=${port}`,'--remote-debugging-address=127.0.0.1'];
|
||||
const launched=spawn(actual,args,{detached:true,windowsHide:true,stdio:'ignore',env});
|
||||
await new Promise((resolve,reject)=>{launched.once('spawn',resolve);launched.once('error',reject)});launched.unref();
|
||||
return createEvidence(undefined,{id:'rea-windows-electron-processes',name:'Local REA Windows Electron lifecycle provider',version:'1+state-local.2'},{
|
||||
predicateType:'rea.desktop-electron-restart',operation:'restart_desktop_electron_debug',parameters:{executable:actual,port,restart_authorized:true},result:{...stopped,launchedPid:launched.pid,arguments:args},confidence:'observed',authority:'controlled-replay',environment:{id:`${os.platform()}-${os.release()}`,platform:os.platform(),architecture:os.arch(),isolation:'none'},
|
||||
limitations:['Caller-authorized restart can interrupt in-flight tasks.','Graceful close is attempted first; surviving launch identities belonging to the exact selected executable are terminated.','Launch evidence confirms process creation only; inspect the debugging endpoint separately.','No original file contents or application data are extracted by this lifecycle operation.']
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
import { webScreenshotArtifactSchema } from '../webScreenshot.js';
|
||||
const sampling = { step_id:z.string().min(1).max(80), frames:z.number().int().min(0).max(24).default(1), interval_ms:z.number().int().min(16).max(500).default(50) };
|
||||
const target = {selector:z.string().min(1).max(500),index:z.number().int().min(0).max(100).optional()};
|
||||
export const electronVisualProbeInputSchema = z.strictObject({
|
||||
include_fonts:z.boolean().default(false),
|
||||
include_message_content:z.boolean().default(false),
|
||||
media_selectors:z.array(z.string().trim().min(1).max(500)).max(4).default([]),
|
||||
track_selectors:z.array(z.string().min(1).max(500)).max(12).default([]),
|
||||
actions:z.array(z.discriminatedUnion('kind',[
|
||||
z.strictObject({...sampling,...target,kind:z.literal('hover')}),
|
||||
z.strictObject({...sampling,...target,kind:z.literal('click'),restore:z.boolean().default(false)}),
|
||||
z.strictObject({...sampling,...target,kind:z.literal('text'),text:z.string().max(2048).refine(value=>!/[\u0000-\u001f\u007f]/u.test(value),'Single-line text only')}),
|
||||
z.strictObject({...sampling,...target,kind:z.literal('scroll'),delta_y:z.number().min(-3000).max(3000),restore:z.boolean().default(false)}),
|
||||
z.strictObject({...sampling,kind:z.literal('key'),selector:z.string().min(1).max(500).optional(),index:z.number().int().min(0).max(100).optional(),key:z.enum(['Escape','Enter','Tab','ArrowDown','ArrowUp','ArrowLeft','ArrowRight','Home','End',' ','+','-','0']),modifiers:z.number().int().min(0).max(15).default(0)}),
|
||||
z.strictObject({...sampling,kind:z.literal('wait'),duration_ms:z.number().int().min(0).max(2000)}),
|
||||
z.strictObject({...sampling,kind:z.literal('viewport'),width:z.number().int().min(160).max(3840),height:z.number().int().min(160).max(2160)})
|
||||
,z.strictObject({...sampling,kind:z.literal('media'),reduced_motion:z.enum(['reduce','no-preference'])})
|
||||
])).max(12).default([])
|
||||
}).superRefine((value,ctx)=>{
|
||||
if(value.actions.reduce((n,a)=>n+a.frames,0)>64)ctx.addIssue({code:'custom',message:'At most 64 scenario frames'});
|
||||
if(value.actions.reduce((n,a)=>n+a.frames*a.interval_ms+(a.duration_ms??0),0)>15000)ctx.addIssue({code:'custom',message:'Declared sampling and waits must fit 15 seconds'});
|
||||
if(new Set(value.actions.map(a=>a.step_id)).size!==value.actions.length)ctx.addIssue({code:'custom',message:'Step IDs must be unique'});
|
||||
if(new Set(value.media_selectors).size!==value.media_selectors.length)ctx.addIssue({code:'custom',message:'Media selectors must be unique'});
|
||||
});
|
||||
const mediaIdentity = {
|
||||
selector:z.string(),frame_id:z.string(),node_index:z.number().int().optional(),backend_node_id:z.number().int().optional(),
|
||||
selected_node:z.object({node_name:z.literal('IMG'),attributes:z.record(z.string(),z.string()),current_source_url:z.string().nullable()}).optional(),
|
||||
visibility:z.object({status:z.enum(['observed','covered','unknown']),reason:z.string().optional()}).optional(),
|
||||
visibility_samples:z.array(z.object({x:z.number(),y:z.number(),hit_backend_node_id:z.number().int().optional(),hit_node_index:z.number().int().optional(),hit_node_name:z.string().optional(),hit_class:z.string().optional()})).max(9).optional(),
|
||||
source:z.object({url:z.string().nullable(),url_sha256:z.string().regex(/^[a-f0-9]{64}$/),selection:z.enum(['current_source_url','src']),mime_type:z.string()}).optional()
|
||||
};
|
||||
export const electronMediaAssetsSchema = z.object({
|
||||
items:z.array(z.discriminatedUnion('status',[
|
||||
z.object({...mediaIdentity,status:z.literal('exported'),artifact:z.object({mime_type:z.enum(['image/png','image/jpeg','image/gif','image/webp']),bytes:z.number().int().positive().max(2097152),sha256:z.string().regex(/^[a-f0-9]{64}$/),data_base64:z.string().max(2796204)})}),
|
||||
z.object({...mediaIdentity,status:z.literal('unknown'),reason:z.string()})
|
||||
])).max(4),
|
||||
total_bytes:z.number().int().min(0).max(8388608),
|
||||
limits:z.object({max_assets:z.literal(4),max_asset_bytes:z.literal(2097152),max_total_bytes:z.literal(8388608)}),
|
||||
limitations:z.array(z.string())
|
||||
});
|
||||
export const platformFontSampleSchema = z.object({category:z.string(),text:z.string().max(256),fontFamily:z.string(),codeContext:z.enum(['inline','block']).optional(),platformFonts:z.array(z.object({familyName:z.string(),postScriptName:z.string(),isCustomFont:z.boolean(),glyphCount:z.number().int().min(0)}))});
|
||||
export const visualProbeOutputSchema = z.object({
|
||||
status:z.enum(['completed','failed']),
|
||||
steps:z.array(z.object({step_id:z.string(),kind:z.string(),status:z.enum(['completed','failed']),error:z.string().nullable(),action_completed_at_ms:z.number().optional()})),
|
||||
frames:z.array(z.object({step_id:z.string(),at_ms:z.number(),artifact:webScreenshotArtifactSchema,regions:z.array(z.object({selector:z.string(),bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]).nullable()}))})),
|
||||
restoration:z.array(z.object({kind:z.string(),status:z.enum(['completed','failed']),error:z.string().nullable()})),
|
||||
limitations:z.array(z.string())
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const MAX_SELECTED_FILES = 16
|
||||
const MAX_FILE_BYTES = 8 * 1024 * 1024
|
||||
const MAX_TOTAL_BYTES = 16 * 1024 * 1024
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/u
|
||||
|
||||
function assertCanonicalSelectedPath(value, binary = false) {
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
throw new TypeError('Selected paths must be non-empty relative strings')
|
||||
}
|
||||
if (
|
||||
value.startsWith('/') ||
|
||||
value.endsWith('/') ||
|
||||
value.includes('\\') ||
|
||||
value.includes('\0') ||
|
||||
/^[a-zA-Z]:/u.test(value) ||
|
||||
value.split('/').some((part) => part === '' || part === '.' || part === '..') ||
|
||||
path.posix.normalize(value) !== value
|
||||
) {
|
||||
throw new TypeError(`Selected path is not canonical: ${value}`)
|
||||
}
|
||||
if (binary ? !/\.(?:png|jpe?g|gif|webp|svg|woff2?|ttf)$/u.test(value) : !/\.(?:js|css|json|md|txt|yaml|yml|py)$/u.test(value)) {
|
||||
if (binary) throw new TypeError(`Selected binary path must name a supported image or font: ${value}`)
|
||||
throw new TypeError(`Selected path must name a supported UTF-8 text file (.js/.css/.json/.md/.txt/.yaml/.yml/.py): ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateSelection(expectedRootSha256, selectedPaths, binary = false) {
|
||||
if (typeof expectedRootSha256 !== 'string' || !SHA256_PATTERN.test(expectedRootSha256)) {
|
||||
throw new TypeError('Expected ASAR root SHA-256 must be 64 lowercase hexadecimal characters')
|
||||
}
|
||||
if (!Array.isArray(selectedPaths) || selectedPaths.length === 0 || selectedPaths.length > MAX_SELECTED_FILES) {
|
||||
throw new RangeError(`Select between 1 and ${MAX_SELECTED_FILES} exact files`)
|
||||
}
|
||||
|
||||
const paths = [...selectedPaths]
|
||||
const seen = new Set()
|
||||
for (const selectedPath of paths) {
|
||||
assertCanonicalSelectedPath(selectedPath, binary)
|
||||
if (seen.has(selectedPath)) throw new TypeError(`Duplicate selected path: ${selectedPath}`)
|
||||
seen.add(selectedPath)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
async function loadReaRuntime() {
|
||||
const runtimeRoot = path.resolve(process.env.REA_RUNTIME_ROOT || '.tmp/rea-css-runtime')
|
||||
const runtimeUrl = (relativePath) => pathToFileURL(path.join(runtimeRoot, relativePath)).href
|
||||
const [readerModule, evidenceModule] = await Promise.all([
|
||||
import(runtimeUrl('dist/artifacts/AsarArtifactReader.js')),
|
||||
import(runtimeUrl('dist/domain/evidence.js')),
|
||||
])
|
||||
const { createRequire } = await import('node:module')
|
||||
const requireFromRuntime = createRequire(path.join(runtimeRoot, 'package.json'))
|
||||
const packageMetadata = requireFromRuntime('./package.json')
|
||||
return {
|
||||
AsarArtifactReader: readerModule.AsarArtifactReader,
|
||||
createEvidence: evidenceModule.createEvidence,
|
||||
version: packageMetadata.version,
|
||||
}
|
||||
}
|
||||
|
||||
async function readSelectedEntry(reader, entry, totalBytes, binary = false) {
|
||||
if (entry.kind !== 'file') throw new TypeError(`Selected path is not a regular file: ${entry.path}`)
|
||||
if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) {
|
||||
throw new Error(`Selected file has no valid declared size: ${entry.path}`)
|
||||
}
|
||||
if (entry.declaredSize > MAX_FILE_BYTES) {
|
||||
throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${entry.path}`)
|
||||
}
|
||||
if (typeof entry.declaredSha256 !== 'string' || !SHA256_PATTERN.test(entry.declaredSha256)) {
|
||||
throw new Error(`Selected file has no valid declared SHA-256: ${entry.path}`)
|
||||
}
|
||||
|
||||
const chunks = []
|
||||
const digest = createHash('sha256')
|
||||
let bytes = 0
|
||||
const stream = await reader.open(entry)
|
||||
for await (const value of stream) {
|
||||
if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
||||
throw new TypeError(`Selected file stream returned non-byte data: ${entry.path}`)
|
||||
}
|
||||
const chunk = Buffer.from(value)
|
||||
bytes += chunk.length
|
||||
if (bytes > MAX_FILE_BYTES || bytes > entry.declaredSize || totalBytes + bytes > MAX_TOTAL_BYTES) {
|
||||
throw new RangeError(`Selected text exceeds its declared size or aggregate budget: ${entry.path}`)
|
||||
}
|
||||
digest.update(chunk)
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
if (bytes !== entry.declaredSize) {
|
||||
throw new Error(`Selected file size mismatch for ${entry.path}: declared ${entry.declaredSize}, read ${bytes}`)
|
||||
}
|
||||
const actualSha256 = digest.digest('hex')
|
||||
if (actualSha256 !== entry.declaredSha256) {
|
||||
throw new Error(`Selected file SHA-256 mismatch for ${entry.path}`)
|
||||
}
|
||||
|
||||
if (binary) return { path: entry.path, size_bytes: bytes, sha256: actualSha256, unpacked: entry.unpacked,
|
||||
encoding: 'base64', data_base64: Buffer.concat(chunks, bytes).toString('base64') }
|
||||
let text
|
||||
try {
|
||||
text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks, bytes))
|
||||
} catch (cause) {
|
||||
throw new Error(`Selected file is not strict UTF-8: ${entry.path}`, { cause })
|
||||
}
|
||||
|
||||
return {
|
||||
path: entry.path,
|
||||
size_bytes: bytes,
|
||||
sha256: actualSha256,
|
||||
unpacked: entry.unpacked,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
/** Export bounded UTF-8 text from exact selected files in a root-pinned ASAR. */
|
||||
export async function exportSelectedArtifactText(inputPath, expectedRootSha256, selectedPaths, binary = false) {
|
||||
if (typeof inputPath !== 'string' || inputPath.length === 0) {
|
||||
throw new TypeError('ASAR input path must be a non-empty string')
|
||||
}
|
||||
const paths = validateSelection(expectedRootSha256, selectedPaths, binary)
|
||||
const { AsarArtifactReader, createEvidence, version } = await loadReaRuntime()
|
||||
const reader = new AsarArtifactReader(inputPath)
|
||||
let primaryFailure
|
||||
let primaryFailed = false
|
||||
let evidence
|
||||
|
||||
try {
|
||||
await reader.prepareContainer(expectedRootSha256)
|
||||
|
||||
const wanted = new Set(paths)
|
||||
const entries = new Map()
|
||||
for await (const entry of reader.entries()) {
|
||||
if (!wanted.has(entry.path)) continue
|
||||
if (entries.has(entry.path)) throw new Error(`ASAR inventory contains a duplicate path: ${entry.path}`)
|
||||
entries.set(entry.path, entry)
|
||||
}
|
||||
|
||||
for (const selectedPath of paths) {
|
||||
if (!entries.has(selectedPath)) throw new Error(`Selected path is absent from the ASAR inventory: ${selectedPath}`)
|
||||
}
|
||||
|
||||
let declaredTotalBytes = 0
|
||||
for (const selectedPath of paths) {
|
||||
const entry = entries.get(selectedPath)
|
||||
if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) {
|
||||
throw new Error(`Selected file has no valid declared size: ${selectedPath}`)
|
||||
}
|
||||
if (entry.declaredSize > MAX_FILE_BYTES) {
|
||||
throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${selectedPath}`)
|
||||
}
|
||||
declaredTotalBytes += entry.declaredSize
|
||||
if (declaredTotalBytes > MAX_TOTAL_BYTES) {
|
||||
throw new RangeError(`Selected files exceed the ${MAX_TOTAL_BYTES}-byte aggregate budget`)
|
||||
}
|
||||
}
|
||||
|
||||
const files = []
|
||||
let actualTotalBytes = 0
|
||||
for (const selectedPath of paths) {
|
||||
const file = await readSelectedEntry(reader, entries.get(selectedPath), actualTotalBytes, binary)
|
||||
actualTotalBytes += file.size_bytes
|
||||
files.push(file)
|
||||
}
|
||||
|
||||
const unselectedScope = 'Not read; unpacked payload content and availability are unknown.'
|
||||
evidence = createEvidence(
|
||||
{ path: inputPath, sha256: expectedRootSha256, format: 'asar' },
|
||||
{
|
||||
id: binary ? 'rea-selected-asar-binary' : 'rea-selected-asar-text',
|
||||
name: binary ? 'REA selected ASAR binary adapter' : 'REA selected ASAR text adapter',
|
||||
version,
|
||||
},
|
||||
{
|
||||
predicateType: binary ? 'rea.artifact.selected-binary' : 'rea.artifact.selected-text',
|
||||
operation: binary ? 'export_selected_asar_artifact_binary' : 'export_selected_asar_artifact_text',
|
||||
parameters: {
|
||||
expected_root_sha256: expectedRootSha256,
|
||||
selected_paths: paths,
|
||||
max_file_bytes: MAX_FILE_BYTES,
|
||||
max_total_bytes: MAX_TOTAL_BYTES,
|
||||
},
|
||||
result: {
|
||||
root_sha256: expectedRootSha256,
|
||||
scope: {
|
||||
kind: 'selected-files-only',
|
||||
selected_paths: paths,
|
||||
unselected_members: unselectedScope,
|
||||
},
|
||||
files,
|
||||
},
|
||||
confidence: 'observed',
|
||||
authority: 'shipped-artifact',
|
||||
limitations: [
|
||||
'Only selected file streams were read and validated.',
|
||||
`Unselected ASAR members were not opened; ${unselectedScope}`,
|
||||
],
|
||||
locations: paths.map((selectedPath) => ({ kind: 'artifact-path', path: selectedPath })),
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
primaryFailure = error
|
||||
primaryFailed = true
|
||||
}
|
||||
|
||||
try {
|
||||
await reader.close()
|
||||
} catch (cleanupFailure) {
|
||||
if (primaryFailed) {
|
||||
throw new AggregateError(
|
||||
[primaryFailure, cleanupFailure],
|
||||
'Selected ASAR text export failed and reader cleanup also failed',
|
||||
{ cause: primaryFailure },
|
||||
)
|
||||
}
|
||||
throw cleanupFailure
|
||||
}
|
||||
if (primaryFailed) throw primaryFailure
|
||||
return evidence
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import { DirectoryArtifactReader } from './DirectoryArtifactReader.js'
|
||||
import { createEvidence } from '../domain/evidence.js'
|
||||
|
||||
const MAX_BYTES = 32 * 1024 * 1024
|
||||
const MAX_ENTRIES = 20000
|
||||
export async function exportSelectedConversationText(projectsRoot, sessionId, includeImages = false) {
|
||||
if (!path.isAbsolute(projectsRoot) || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(sessionId)) {
|
||||
throw new TypeError('An absolute projects root and exact UUID session ID are required')
|
||||
}
|
||||
const reader = new DirectoryArtifactReader(projectsRoot)
|
||||
const matches = []
|
||||
let examined = 0
|
||||
try {
|
||||
for await (const entry of reader.entries()) {
|
||||
if (++examined > MAX_ENTRIES) throw new RangeError('Conversation inventory entry budget exceeded')
|
||||
if (entry.path.split('/').at(-1) === `${sessionId}.jsonl`) matches.push(entry)
|
||||
}
|
||||
if (matches.length !== 1) throw new Error(`Expected one exact session transcript, found ${matches.length}`)
|
||||
const entry = matches[0]
|
||||
if (entry.kind !== 'file' || !Number.isSafeInteger(entry.declaredSize) || entry.declaredSize > MAX_BYTES) {
|
||||
throw new RangeError('Selected transcript is not a bounded regular file')
|
||||
}
|
||||
const chunks = []
|
||||
let bytes = 0
|
||||
for await (const chunk of await reader.open(entry)) {
|
||||
const data = Buffer.from(chunk)
|
||||
bytes += data.length
|
||||
if (bytes > MAX_BYTES) throw new RangeError('Transcript grew beyond the byte budget')
|
||||
chunks.push(data)
|
||||
}
|
||||
const data = Buffer.concat(chunks)
|
||||
if (data.length !== entry.declaredSize) throw new Error('Transcript size changed during read')
|
||||
const text = new TextDecoder('utf-8', { fatal: true }).decode(data)
|
||||
const sha256 = createHash('sha256').update(data).digest('hex')
|
||||
let records = 0
|
||||
const imageRefs = new Map()
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
if (!line.trim()) continue
|
||||
const record = JSON.parse(line)
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)) throw new Error('JSONL record is not an object')
|
||||
if (record.sessionId && record.sessionId !== sessionId) throw new Error('Transcript contains another session ID')
|
||||
if (includeImages && record.type === 'message' && Array.isArray(record.content)) {
|
||||
for (const block of record.content) {
|
||||
const image = block?.type === 'input_image' ? block.image : undefined
|
||||
if (image?.type !== 'image_blob_ref') continue
|
||||
if (!/^[a-f0-9]{64}$/.test(image.blob_id) || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(image.mime)) {
|
||||
throw new Error('Unsupported or invalid transcript image reference')
|
||||
}
|
||||
imageRefs.set(image.blob_id, image)
|
||||
}
|
||||
}
|
||||
records++
|
||||
}
|
||||
const attachments = includeImages ? await readImages(projectsRoot, [...imageRefs.values()]) : []
|
||||
return createEvidence({ path: path.join(projectsRoot, entry.path), sha256, format: 'file' },
|
||||
{ id: 'rea-selected-conversation-text', name: 'REA selected local conversation transcript', version: '1+local.1' }, {
|
||||
predicateType: 'rea.conversation.selected-text', operation: 'export_selected_conversation_text',
|
||||
parameters: { projects_root: projectsRoot, session_id: sessionId, include_images: includeImages, max_bytes: MAX_BYTES, max_entries: MAX_ENTRIES },
|
||||
result: { session_id: sessionId, file: { path: entry.path, sha256, bytes, records, text }, attachments,
|
||||
scope: 'One exact session JSONL and optionally its referenced image blobs; other file contents are not read' },
|
||||
confidence: 'observed', authority: 'historical-reference',
|
||||
limitations: ['Stable directory-reader identity checks do not establish an atomic snapshot of an actively written conversation.',
|
||||
'Exports stored history only; non-image external files and live kernel state are not copied.'],
|
||||
locations: [{ kind: 'artifact-path', path: entry.path }]
|
||||
})
|
||||
} finally { await reader.close() }
|
||||
}
|
||||
|
||||
async function readImages(projectsRoot, references) {
|
||||
if (!references.length) return []
|
||||
if (references.length > 32) throw new RangeError('Too many image references')
|
||||
const root = path.resolve(projectsRoot, '../blobs')
|
||||
const selected = new Map()
|
||||
for (const ref of references) {
|
||||
const target = path.resolve(ref.blob_path)
|
||||
const relative = path.relative(root, target).split(path.sep).join('/')
|
||||
if (!new RegExp(`^${ref.blob_id.slice(0, 2)}/${ref.blob_id}\\.(png|jpg|jpeg|webp|gif)$`).test(relative)) {
|
||||
throw new Error('Image reference is outside the selected content-addressed blob root')
|
||||
}
|
||||
selected.set(relative, ref)
|
||||
}
|
||||
const reader = new DirectoryArtifactReader(root)
|
||||
const files = []
|
||||
let total = 0
|
||||
try {
|
||||
for await (const entry of reader.entries(undefined, { descendInto: directory => [...selected.keys()].some(file => file.startsWith(directory + '/')) })) {
|
||||
const ref = selected.get(entry.path)
|
||||
if (!ref) continue
|
||||
if (entry.kind !== 'file' || !Number.isSafeInteger(entry.declaredSize) || entry.declaredSize > 8 * 1024 * 1024) {
|
||||
throw new Error('Referenced image is not a bounded regular file')
|
||||
}
|
||||
const chunks = []
|
||||
for await (const chunk of await reader.open(entry)) {
|
||||
total += chunk.length
|
||||
if (total > 16 * 1024 * 1024) throw new RangeError('Image export byte budget exceeded')
|
||||
chunks.push(Buffer.from(chunk))
|
||||
}
|
||||
const data = Buffer.concat(chunks)
|
||||
const sha256 = createHash('sha256').update(data).digest('hex')
|
||||
if (sha256 !== ref.blob_id || data.length !== ref.size || data.length !== entry.declaredSize) {
|
||||
throw new Error('Image blob digest/size does not match the transcript reference')
|
||||
}
|
||||
files.push({ blob_id: ref.blob_id, mime: ref.mime, bytes: data.length, sha256, data_base64: data.toString('base64') })
|
||||
}
|
||||
if (files.length !== references.length) throw new Error('A referenced image blob is absent')
|
||||
return files
|
||||
} finally { await reader.close() }
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Prepare a reviewable local REA 6.5.0 repair. Never reads WorkBuddy or changes global registration.
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const sourceRoot = path.resolve(process.argv[2] || 'C:/Users/Administrator/scoop/apps/nvm/current/nodejs/nodejs/node_modules/rea-agents')
|
||||
const output = path.resolve('.tmp/rea-css-runtime')
|
||||
const patchRoot = path.resolve('scripts/rea/patches')
|
||||
const sourcePackage = JSON.parse(await fs.readFile(path.join(sourceRoot, 'package.json'), 'utf8'))
|
||||
if (sourcePackage.name !== 'rea-agents' || sourcePackage.version !== '6.5.0') throw new Error('Repair requires installed rea-agents 6.5.0')
|
||||
if (output === sourceRoot || !output.startsWith(path.resolve('.tmp') + path.sep)) throw new Error('Invalid local repair destination')
|
||||
await fs.mkdir(output, { recursive: true })
|
||||
const marker = path.join(output, 'css-repair-manifest.json')
|
||||
try {
|
||||
const previous = JSON.parse(await fs.readFile(marker, 'utf8'))
|
||||
if (previous.sourceRoot !== sourceRoot) throw new Error('Local repair belongs to a different source installation')
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error
|
||||
for (const entry of await fs.readdir(sourceRoot, { withFileTypes: true })) {
|
||||
if (entry.name === 'node_modules') continue
|
||||
await fs.cp(path.join(sourceRoot, entry.name), path.join(output, entry.name), { recursive: true, force: false, errorOnExist: true, verbatimSymlinks: true })
|
||||
}
|
||||
await fs.symlink(path.join(sourceRoot, 'node_modules'), path.join(output, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
|
||||
}
|
||||
const records = []
|
||||
const digest = text => crypto.createHash('sha256').update(text).digest('hex')
|
||||
async function patch(relative, transform) {
|
||||
const original = await fs.readFile(path.join(sourceRoot, relative), 'utf8')
|
||||
const modified = transform(original)
|
||||
if (original === modified) throw new Error(`No change for ${relative}`)
|
||||
const baseline = path.join(output, 'css-repair-baseline', relative)
|
||||
await fs.mkdir(path.dirname(baseline), { recursive: true })
|
||||
await fs.writeFile(baseline, original)
|
||||
await fs.writeFile(path.join(output, relative), modified)
|
||||
records.push({ path: relative, original_sha256: digest(original), modified_sha256: digest(modified) })
|
||||
}
|
||||
function once(text, before, after) {
|
||||
if (text.split(before).length !== 2) throw new Error(`Expected exactly one patch anchor: ${before.slice(0, 70)}`)
|
||||
return text.replace(before, after)
|
||||
}
|
||||
|
||||
// Use the package's shipped unbundled build, so CLI and MCP load the repaired modules.
|
||||
await patch('scripts/rea.mjs', text => once(text.replaceAll('dist/mcp/main.js', 'dist/main.js')
|
||||
.replaceAll('dist/cli-runtime/cli.js', 'dist/cli.js')
|
||||
.replaceAll('dist/cli-runtime/cliOutput.js', 'dist/cliOutput.js')
|
||||
.replaceAll('dist/cli-runtime/cliResultOutput.js', 'dist/cli/cliResultOutput.js'), 'if (!(await compiledRuntimeExists(runtimeFiles))) {', `if (args[0] === "inspect-desktop-electron-processes") {
|
||||
const { inspectDesktopElectronProcesses } = await import("../dist/browser/DesktopElectronProcesses.js");
|
||||
process.stdout.write(JSON.stringify(await inspectDesktopElectronProcesses(args[1] || "WorkBuddy.exe")) + "\\n");
|
||||
} else if (args[0] === "restart-desktop-electron-debug") {
|
||||
const { restartDesktopElectronDebug } = await import("../dist/browser/DesktopElectronProcesses.js");
|
||||
process.stdout.write(JSON.stringify(await restartDesktopElectronDebug(args[1], Number(args[2]), args.includes("--restart-authorized"))) + "\\n");
|
||||
} else if (args[0] === "export-selected-artifact-text") {
|
||||
const { exportSelectedArtifactText } = await import("../dist/artifacts/SelectedArtifactText.js");
|
||||
process.stdout.write(JSON.stringify(await exportSelectedArtifactText(args[1], args[2], JSON.parse(args[3]))) + "\\n");
|
||||
} else if (args[0] === "export-selected-artifact-binary") {
|
||||
const { exportSelectedArtifactText } = await import("../dist/artifacts/SelectedArtifactText.js");
|
||||
process.stdout.write(JSON.stringify(await exportSelectedArtifactText(args[1], args[2], JSON.parse(args[3]), true)) + "\\n");
|
||||
} else if (args[0] === "export-selected-conversation-text") {
|
||||
const { exportSelectedConversationText } = await import("../dist/artifacts/SelectedConversationText.js");
|
||||
process.stdout.write(JSON.stringify(await exportSelectedConversationText(args[1], args[2], args.includes("--include-images"))) + "\\n");
|
||||
} else if (!(await compiledRuntimeExists(runtimeFiles))) {`))
|
||||
await patch('package.json', text => {
|
||||
const pkg = JSON.parse(text)
|
||||
pkg.version = '6.5.0-visual.3'
|
||||
return JSON.stringify(pkg, null, 2) + '\n'
|
||||
})
|
||||
await patch('dist/generatedPackageMetadata.js', text => once(text, 'version: "6.5.0",', 'version: "6.5.0-visual.3",'))
|
||||
await patch('dist/domain/javascript/electronObservation.js', text => {
|
||||
text = 'import { electronVisualProbeInputSchema, electronMediaAssetsSchema, platformFontSampleSchema, visualProbeOutputSchema } from "./ElectronVisualProbeSchema.js";\nimport { webScreenshotArtifactSchema } from "../webScreenshot.js";\n' + text
|
||||
text = once(text, ' target_id: z.string().trim().min(1),', ' target_id: z.string().trim().min(1),\n include_style_sources: z.boolean().default(false).describe("Include CSS text from stylesheets attributed to authorized Electron frames"),\n include_visuals: z.boolean().default(false).describe("Include visible viewport PNG and attributed DOM layout styles"),')
|
||||
text = text.replace(' include_visuals: z.boolean()', ' visual_probe: electronVisualProbeInputSchema.optional(),\n include_visuals: z.boolean()')
|
||||
text = once(text, ']);\nconst electronTargetSchema', `]).superRefine((value, ctx) => {
|
||||
if (value.visual_probe && !value.include_visuals) ctx.addIssue({code: "custom", message: "visual_probe requires include_visuals"});
|
||||
});
|
||||
const electronTargetSchema`)
|
||||
text = once(text, ' workers: z', ` visuals: z.object({
|
||||
captured_at: z.iso.datetime(),
|
||||
viewport: z.object({width:z.number().int().positive(),height:z.number().int().positive()}),
|
||||
css_viewport: z.object({width:z.number().positive(),height:z.number().positive(),page_x:z.number(),page_y:z.number()}),
|
||||
environment_media:z.object({reduced_motion:z.boolean().nullable()}).optional(),
|
||||
environment_gpu:z.object({status:z.enum(['observed','unavailable']),devices:z.array(z.object({vendor_id:z.number(),device_id:z.number(),device_string:z.string().optional(),active:z.boolean().optional()})).optional(),aux_attributes:z.record(z.string(),z.union([z.string(),z.number(),z.boolean(),z.null()])).optional(),feature_status:z.record(z.string(),z.string()).optional()}).optional(),
|
||||
artifact: webScreenshotArtifactSchema,
|
||||
layout: z.object({properties:z.array(z.string()),documents:z.array(z.object({frame_id:z.string(),file_path:z.string(),scroll_x:z.number(),scroll_y:z.number(),nodes:z.array(z.object({node_index:z.number().int(),parent_index:z.number().int(),node_name:z.string(),attributes:z.record(z.string(),z.string()),bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]),styles:z.record(z.string(),z.string())})),svg_nodes:z.array(z.object({node_index:z.number().int(),parent_index:z.number().int(),node_name:z.string(),attributes:z.record(z.string(),z.string())}))}))}),
|
||||
limitations:z.array(z.string()),
|
||||
font_samples:z.array(platformFontSampleSchema.extend({bounds:z.tuple([z.number(),z.number(),z.number(),z.number()]).optional(),styles:z.record(z.string(),z.string()).optional()})).optional(),
|
||||
font_missing:z.array(z.object({category:z.string(),reason:z.string()})).optional(),
|
||||
message_content:z.object({nodes:z.array(z.object({node_index:z.number().int(),parent_index:z.number().int(),node_name:z.string(),attributes:z.record(z.string(),z.string()),text:z.string().optional(),selector:z.string().optional()})),text_characters:z.number().int().min(0),truncated:z.boolean()}).optional(),
|
||||
probe:visualProbeOutputSchema.optional(),
|
||||
media_assets:electronMediaAssetsSchema.optional(),
|
||||
}).optional(),
|
||||
workers: z`)
|
||||
return once(text, ' workers: z', ` styles: z.object({
|
||||
total: z.number().int().min(0),
|
||||
items: z.array(z.object({
|
||||
style_sheet_key: prefixedDigestSchema("electron_stylesheet"),
|
||||
style_sheet_id: z.string(), frame_id: z.string().nullable(), file_path: z.string().nullable(),
|
||||
origin: z.string(), is_inline: z.boolean(), is_constructed: z.boolean(), source: electronSourceSchema,
|
||||
})),
|
||||
excluded: z.number().int().min(0), source_failures: z.number().int().min(0), truncated: z.boolean(),
|
||||
limits: z.object({ max_sheets: z.number().int().positive(), max_sheet_bytes: z.number().int().positive(), max_total_bytes: z.number().int().positive() }),
|
||||
}).optional(),
|
||||
workers: z`)
|
||||
})
|
||||
await patch('dist/browser/CdpElectronInspection.js', text => {
|
||||
text = 'import { captureElectronVisuals } from "./CdpElectronVisuals.js";\nimport { ingestElectronStyleEvent, captureElectronStyles } from "./CdpElectronStyles.js";\n' + text
|
||||
text = once(text,'"REA does not evaluate renderer JavaScript, invoke Electron APIs, navigate, click, or close the page."','"REA does not evaluate renderer JavaScript or invoke Electron APIs; declared visual_probe native inputs are the only optional UI actions."')
|
||||
text = once(text, ' scripts: [],', ' scripts: [],\n styles: new Map(),')
|
||||
text = once(text, ' ingestElectronScriptEvent({', ' if (context.input.include_style_sources) ingestElectronStyleEvent(event, state.styles);\n ingestElectronScriptEvent({')
|
||||
text = once(text, ' await enableElectronCapture(context);', ` await enableElectronCapture(context);
|
||||
if (context.input.include_style_sources) {
|
||||
await context.connection.send("DOM.enable", {}, context.sessionId, context.signal);
|
||||
await context.connection.send("CSS.enable", {}, context.sessionId, context.signal);
|
||||
state.limitations.push("CSS capture is a local REA 6.5.0-css.1 extension: only currently loaded styles attributed to authorized file-page frames are eligible. No renderer JavaScript is evaluated. Styles have independent coverage and budgets; inspect the styles section.");
|
||||
}`)
|
||||
return once(text, ' return { dom, scripts, resources, workers };', ` const styles = await captureElectronStyles({
|
||||
connection: context.connection, sessionId: context.sessionId, signal: context.signal,
|
||||
styles: state.styles, frameIds, request: context.input,
|
||||
completeness: state.completeness, limitations: state.limitations,
|
||||
});
|
||||
const visuals = await captureElectronVisuals(context, frames);
|
||||
return { dom, scripts, resources, workers, styles, ...(visuals ? {visuals} : {}) };`)
|
||||
})
|
||||
await patch('dist/browser/CdpElectronProvider.js', text => {
|
||||
text = once(text, ' return CDP_ELECTRON_PROVIDER_IDENTITY;', ' return { ...CDP_ELECTRON_PROVIDER_IDENTITY, version: "1+visual-local.3" };')
|
||||
return once(text, '["Debugger", "Runtime", "Page"]', '["CSS", "DOM", "Debugger", "Runtime", "Page"]')
|
||||
})
|
||||
await patch('dist/application/javascript/ElectronEvidence.js', text => once(once(text, 'authority: "external-service",','authority: input.visual_probe?.actions?.length ? "controlled-replay" : "external-service",'),
|
||||
' include_script_sources: input.include_script_sources,',
|
||||
' include_script_sources: input.include_script_sources,\n include_style_sources: input.include_style_sources,\n include_visuals: input.include_visuals,\n ...(input.visual_probe ? { visual_probe: input.visual_probe } : {}),'))
|
||||
await patch('dist/cliObservationOptions.js', text => once(text,
|
||||
'export const electronPageInspectionOptions = z.object({',
|
||||
'export const electronPageInspectionOptions = z.object({\n visualProbe: z.string().optional().describe("JSON for declared native UI actions, sampled PNG frames, font usage and opt-in media_selectors for loaded IMG bytes"),\n includeStyleSources: z.boolean().default(false).describe("Include attributed local or inline stylesheet CSS text"),\n includeVisuals: z.boolean().default(false).describe("Include current viewport PNG and layout styles"),'))
|
||||
await patch('dist/cli/electronCommands.js', text => once(text,
|
||||
' include_script_sources: options.includeScriptSources,',
|
||||
' include_script_sources: options.includeScriptSources,\n include_style_sources: options.includeStyleSources,\n include_visuals: options.includeVisuals,\n ...(options.visualProbe ? {visual_probe: JSON.parse(options.visualProbe)} : {}),'))
|
||||
await patch('dist/contracts/javascript/electronToolContracts.js', text => once(text,
|
||||
'Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs.',
|
||||
'Returns frames, DOM structure, resource paths, scripts and optional attributed CSS sources without evaluating renderer JavaScript or invoking Electron APIs. Default inspection is passive. include_style_sources selects CSS text; include_visuals selects viewport PNG and layout. Optional visual_probe declares native hover/click/scroll/key/viewport actions, sequential PNG samples, and actual platform fonts; UI actions can change application state and restoration is limited. visual_probe.media_selectors explicitly selects at most four unique main-frame IMG nodes and their already loaded PNG/JPEG/GIF/WebP resources, with byte budgets, SHA/base64 and visibility reported separately; no HTTP fetch is performed. These are local REA extensions; inspect coverage, budgets, restoration and limitations.'))
|
||||
|
||||
const styleModule = await fs.readFile(path.join(patchRoot, 'CdpElectronStyles.js'), 'utf8')
|
||||
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronStyles.js'), styleModule)
|
||||
records.push({ path: 'dist/browser/CdpElectronStyles.js', original_sha256: null, modified_sha256: digest(styleModule) })
|
||||
const visualModule = await fs.readFile(path.join(patchRoot, 'CdpElectronVisuals.js'), 'utf8')
|
||||
await fs.writeFile(path.join(output, 'dist/browser/CdpElectronVisuals.js'), visualModule)
|
||||
records.push({ path: 'dist/browser/CdpElectronVisuals.js', original_sha256: null, modified_sha256: digest(visualModule) })
|
||||
const processModule = await fs.readFile(path.join(patchRoot, 'DesktopElectronProcesses.js'), 'utf8')
|
||||
await fs.writeFile(path.join(output, 'dist/browser/DesktopElectronProcesses.js'), processModule)
|
||||
records.push({ path: 'dist/browser/DesktopElectronProcesses.js', original_sha256: null, modified_sha256: digest(processModule) })
|
||||
for(const [source,destination] of [['ElectronVisualProbeSchema.js','domain/javascript/ElectronVisualProbeSchema.js'],['CdpElectronVisualProbe.js','browser/CdpElectronVisualProbe.js'],['CdpElectronPlatformFonts.js','browser/CdpElectronPlatformFonts.js'],['CdpElectronMediaAssets.js','browser/CdpElectronMediaAssets.js']]) {
|
||||
const module=await fs.readFile(path.join(patchRoot,source),'utf8')
|
||||
await fs.writeFile(path.join(output,'dist',destination),module)
|
||||
records.push({path:'dist/'+destination,original_sha256:null,modified_sha256:digest(module)})
|
||||
}
|
||||
const selectedArtifactModule=await fs.readFile(path.join(patchRoot,'SelectedArtifactText.js'),'utf8')
|
||||
await fs.writeFile(path.join(output,'dist/artifacts/SelectedArtifactText.js'),selectedArtifactModule)
|
||||
records.push({path:'dist/artifacts/SelectedArtifactText.js',original_sha256:null,modified_sha256:digest(selectedArtifactModule)})
|
||||
const conversationModule=await fs.readFile(path.join(patchRoot,'SelectedConversationText.js'),'utf8')
|
||||
await fs.writeFile(path.join(output,'dist/artifacts/SelectedConversationText.js'),conversationModule)
|
||||
records.push({path:'dist/artifacts/SelectedConversationText.js',original_sha256:null,modified_sha256:digest(conversationModule)})
|
||||
const manifest = { sourceRoot, upstreamVersion: sourcePackage.version, localVersion: '6.5.0-visual.3', output, files: records,
|
||||
notes: ['Local REA repair, not an upstream release.', 'Global installation and Codex registration unchanged.', 'Public scripts/rea.mjs entry point loads the shipped unbundled build.', 'Dependencies reused read-only through node_modules link.'] }
|
||||
await fs.writeFile(marker, JSON.stringify(manifest, null, 2) + '\n')
|
||||
console.log(JSON.stringify({ output, localVersion: manifest.localVersion, files: records.length }))
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"back-to-skills-market","selector":".sm-installed-topbar-nav","frames":0},{"kind":"wait","step_id":"market-ready","duration_ms":200,"frames":0},{"kind":"click","step_id":"open-add-skill-options","selector":".sm-topbar-action-btn[aria-label='添加技能']","frames":0},{"kind":"wait","step_id":"add-skill-options-ready","duration_ms":300,"frames":1},{"kind":"key","step_id":"dismiss-add-skill-options","key":"Escape","frames":0}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"restore-home","selector":".conversation-list-tabs button[role=tab]","index":0,"frames":0}]}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": true,
|
||||
"track_selectors": [".workbuddy-topbar", ".workbuddy-topbar-title-area", ".workbuddy-topbar-actions"],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "select-existing-imported-conversation", "selector": ".conversation-item", "index": 4, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "conversation-header-stable", "duration_ms": 700, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": true,
|
||||
"track_selectors": [".workbuddy-topbar", ".workbuddy-topbar-actions", ".cb-chat-search", ".workbuddy-prompt-list"],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "collapse-real-workbench", "selector": ".sidebar-next-collapse-toggle", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "collapsed-header", "duration_ms": 300, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-conversation-search", "selector": ".workbuddy-topbar-actions .wb-button", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "conversation-search-open", "duration_ms": 200, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": true,
|
||||
"track_selectors": [".workbuddy-topbar", ".workbuddy-topbar-title-area", ".workbuddy-topbar-actions"],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "restore-existing-home-view", "selector": ".conversation-list-tab-button", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "home-restored", "duration_ms": 250, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": true,
|
||||
"track_selectors": [".workbuddy-topbar", ".workbuddy-topbar-title-area", ".workbuddy-topbar-title-input", ".cb-chat-search"],
|
||||
"actions": [
|
||||
{ "kind": "key", "step_id": "close-search-with-escape", "selector": ".cb-chat-search__input", "key": "Escape", "frames": 0 },
|
||||
{ "kind": "hover", "step_id": "hover-title", "selector": ".workbuddy-topbar-title-area", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "title-edit-button-visible", "duration_ms": 180, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-title-editor-without-changing-title", "selector": ".workbuddy-topbar-edit-button", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "title-editor-open", "duration_ms": 120, "frames": 1 },
|
||||
{ "kind": "key", "step_id": "cancel-title-editor", "selector": ".workbuddy-topbar-title-input", "key": "Escape", "frames": 0 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"open-qq-mail-detail","selector":".connector-card-name","index":2,"frames":0},{"kind":"wait","step_id":"connector-detail-ready","duration_ms":500,"frames":1},{"kind":"key","step_id":"close-detail","key":"Escape","frames":0},{"kind":"wait","step_id":"detail-close-result","duration_ms":150,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"close-qq-mail-detail","selector":".connector-detail-close","frames":0},{"kind":"click","step_id":"open-custom-connectors","selector":".connector-topbar-action-btn","frames":0},{"kind":"wait","step_id":"custom-connectors-ready","duration_ms":500,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"track_selectors":[".conversation-list-tabs"],"actions":[{"kind":"click","step_id":"open-expert-entry","selector":".conversation-list-tabs button[role=tab]","index":2,"frames":0},{"kind":"wait","step_id":"expert-entry-ready","duration_ms":500,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"close-mcp-manager","selector":".mcp-modal-close","frames":0},{"kind":"click","step_id":"skills-tab","selector":".um-header__tabs [role=tab]","index":1,"frames":0},{"kind":"wait","step_id":"skills-tab-ready","duration_ms":350,"frames":0},{"kind":"click","step_id":"show-installed-skills","selector":"button[data-track-id=skill_market_my_installed_open]","frames":0},{"kind":"wait","step_id":"installed-skills-ready","duration_ms":600,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"open-connectors-tab","selector":".um-header__tabs [role=tab]","index":2,"frames":0},{"kind":"wait","step_id":"connectors-ready","duration_ms":600,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"key","step_id":"dismiss-nav-menu","key":"Escape","frames":0},{"kind":"click","step_id":"open-skills-tab","selector":".um-header__tabs [role=tab]","index":1,"frames":0},{"kind":"wait","step_id":"skills-ready","duration_ms":600,"frames":1}]}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [".models-settings-panel__model-select", ".wb-select-popover", ".wb-select__panel", ".wb-select__search", ".wb-select__list"],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "close-provider-dropdown", "selector": ".models-settings-panel__provider-select", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "provider-dropdown-closed", "duration_ms": 180, "frames": 0 },
|
||||
{ "kind": "click", "step_id": "open-default-model-dropdown", "selector": ".models-settings-panel__model-select", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "default-model-dropdown-ready", "duration_ms": 250, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-home-model-picker", "selector": ".cr-model-selector__trigger", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "home-model-picker-ready", "duration_ms": 180, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-custom-model-settings", "selector": ".cr-model-selector__menu > .cr-menu-item:last-child", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "custom-model-settings-opening", "duration_ms": 300, "frames": 0 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [".models-settings-panel__provider-select", ".wb-select-popover", ".wb-select__panel", ".wb-select__search", ".wb-select__list"],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-provider-dropdown", "selector": ".models-settings-panel__provider-select", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "provider-dropdown-ready", "duration_ms": 250, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "close-model-dropdown", "selector": ".models-settings-panel__model-select", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "dropdown-closed", "duration_ms": 180, "frames": 0 },
|
||||
{ "kind": "click", "step_id": "cancel-unsaved-model-editor", "selector": ".models-settings-panel__editor-close", "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "unsaved-editor-closed", "duration_ms": 180, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "close-settings-window", "selector": ".settings-window-close", "frames": 0 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"actions":[{"kind":"click","step_id":"expert-tab","selector":".um-header__tabs [role=tab]","index":0,"frames":0},{"kind":"wait","step_id":"expert-tab-ready","duration_ms":350,"frames":0},{"kind":"click","step_id":"show-my-experts","selector":"button[class*='_topbarActionBtn_']","frames":0},{"kind":"wait","step_id":"my-experts-ready","duration_ms":600,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"include_fonts":false,"include_message_content":true,"track_selectors":[".wb-select-popover",".wb-select__list",".wb-select__option"],"actions":[{"kind":"text","step_id":"query-tencent-chinese","selector":".wb-select__search input","text":"腾讯","frames":0},{"kind":"wait","step_id":"query-tencent-result","duration_ms":180,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"include_fonts":false,"include_message_content":true,"track_selectors":[".wb-select-popover",".wb-select__list",".wb-select__option"],"actions":[{"kind":"text","step_id":"clear-provider-query","selector":".wb-select__search input","text":"","frames":0},{"kind":"wait","step_id":"provider-query-cleared","duration_ms":180,"frames":1},{"kind":"click","step_id":"close-provider","selector":".models-settings-panel__provider-select","frames":0},{"kind":"wait","step_id":"provider-closed","duration_ms":180,"frames":0},{"kind":"click","step_id":"open-model","selector":".models-settings-panel__model-select","frames":0},{"kind":"wait","step_id":"model-open","duration_ms":180,"frames":0},{"kind":"text","step_id":"query-model-case-insensitive","selector":".wb-select__search input","text":"minimax","frames":0},{"kind":"wait","step_id":"model-query-results","duration_ms":180,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"include_fonts":false,"include_message_content":true,"track_selectors":[".wb-select-popover",".wb-select__list",".wb-select__empty"],"actions":[{"kind":"text","step_id":"query-no-provider","selector":".wb-select__search input","text":"NO_SUCH_PROVIDER_1","frames":0},{"kind":"wait","step_id":"query-empty-result","duration_ms":180,"frames":1}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"include_fonts":true,"include_message_content":true,"track_selectors":[".wb-select-popover",".wb-select__search",".wb-select__list",".wb-select__option"],"actions":[{"kind":"click","step_id":"open-provider","selector":".models-settings-panel__provider-select","frames":0},{"kind":"wait","step_id":"provider-open","duration_ms":180,"frames":0},{"kind":"text","step_id":"query-custom-case-insensitive","selector":".wb-select__search input","text":"CUSTOM","frames":0},{"kind":"wait","step_id":"query-custom-result","duration_ms":180,"frames":1}]}
|
||||
@@ -0,0 +1,23 @@
|
||||
/** Static audit of REA-recovered artifacts only. Does not execute the expert or its scripts. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import assert from 'node:assert/strict'
|
||||
import {parseDocument} from 'yaml'
|
||||
const directory='.tmp/rea-project-manager-recovered-20261016'
|
||||
const evidence=JSON.parse(fs.readFileSync(path.join(directory,'evidence-manifest.json'),'utf8'))
|
||||
const sha=bytes=>crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
assert.equal(evidence.rootSha256,'304805c85b87e8266294fb3aaf283d45fc71d7977af064fcb1eed1199e9389e6')
|
||||
const byPath=new Map(evidence.files.map(file=>[file.path,file]))
|
||||
function text(relative){const item=byPath.get(relative);if(!item)return null;const bytes=fs.readFileSync(path.join(directory,relative));assert.equal(sha(bytes),item.sha256);assert.equal(bytes.length,item.bytes);return new TextDecoder('utf-8',{fatal:true}).decode(bytes)}
|
||||
function source(relative){const{path:filename,sha256,evidenceId,retainedExport}=byPath.get(relative);return{path:filename,sha256,evidenceId,retainedExport}}
|
||||
function frontmatter(value){const match=/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(value);assert.ok(match);const doc=parseDocument(match[1],{uniqueKeys:true,schema:'core'});assert.equal(doc.errors.length,0);return doc.toJS({maxAliasCount:30})}
|
||||
const pluginPath='.codebuddy-plugin/plugin.json',agentPath='agents/senior-project-manager.md',skillPath='skills/minimax-docx/SKILL.md'
|
||||
const plugin=JSON.parse(text(pluginPath)),agent=text(agentPath),skill=text(skillPath)
|
||||
const findings={rootSha256:evidence.rootSha256,metadata:{value:plugin,source:source(pluginPath)},agent:{frontmatter:frontmatter(agent),source:source(agentPath),workspacePrerequisites:['ai/memory-bank/site-setup.md','ai/memory-bank/tasks/[project-slug]-tasklist.md','ai/agents/pm.md','qa-playwright-capture.sh'],sourceBehavior:['Read exact specification; scope matches it','Write 30–60 minute actionable tasks with acceptance criteria','Extract technical stack and reference requirement sections','Word generation/editing routes to bundled minimax-docx']},skill:{frontmatter:frontmatter(skill),source:source(skillPath),pipelines:['A CREATE','B FILL-EDIT','C FORMAT-APPLY (OVERLAY or BASE-REPLACE)'],setup:['bash scripts/setup.sh','powershell scripts/setup.ps1','scripts/env_check.sh'],cli:'dotnet run --project scripts/dotnet/MiniMaxAIDocx.Cli --'},projects:[],scriptFindings:[],coverage:evidence.coverage}
|
||||
for(const file of evidence.files.filter(file=>/\.(?:csproj|slnx)$/.test(file.path))){const value=text(file.path);findings.projects.push({source:source(file.path),targetFramework:/<TargetFramework>([^<]+)/.exec(value)?.[1],packages:[...value.matchAll(/<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"/g)].map(match=>({name:match[1],version:match[2]})),projectReferences:[...value.matchAll(/<ProjectReference\s+Include="([^"]+)"/g)].map(match=>match[1])})}
|
||||
for(const file of evidence.files.filter(file=>/\.(?:sh|ps1)$/.test(file.path))){const value=text(file.path),lines=value.split(/\r?\n/);findings.scriptFindings.push({source:source(file.path),lines:lines.map((text,index)=>({line:index+1,text})).filter(({text})=>/dotnet|pandoc|soffice|libreoffice|python|pip |pip3|lxml|defused|apt|brew|winget|curl|https?:\/\/|api[_-]?key|token|node|npm|DOTNET_ROOT|PATH|System\.Drawing/i.test(text)).slice(0,90)})}
|
||||
const cli='skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs',cliBody=text(cli)
|
||||
if(cliBody)findings.cli={source:source(cli),dispatch:cliBody,runScriptDeclared:/case\s+"run-script"|"run-script"\s*=>/.test(cliBody)}
|
||||
fs.writeFileSync('.tmp/rea-project-manager-analysis-20261016.json',JSON.stringify(findings,null,2)+'\n')
|
||||
console.log(JSON.stringify({status:evidence.coverage.pending.length?'PARTIAL':'PASS',auditedFiles:evidence.files.length,projects:findings.projects,scriptCount:findings.scriptFindings.length,report:'.tmp/rea-project-manager-analysis-20261016.json'}))
|
||||
@@ -0,0 +1,87 @@
|
||||
/** All original access uses public REA. Local reads are retained evidence and recovery outputs only. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import assert from 'node:assert/strict'
|
||||
import {spawnSync} from 'node:child_process'
|
||||
|
||||
const root='C:/Users/Administrator/.workbuddy/plugins/marketplaces/experts/plugins/senior-project-manager'
|
||||
const rootSha256='304805c85b87e8266294fb3aaf283d45fc71d7977af064fcb1eed1199e9389e6'
|
||||
const inventoryPath='.tmp/rea-project-manager-inventory-20261016.json'
|
||||
const recovered='.tmp/rea-project-manager-recovered-20261016'
|
||||
const manifestPath=path.join(recovered,'evidence-manifest.json')
|
||||
const hash=bytes=>crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
const marker=file=>/(^|\/)(?:\.downloaded_at|\.created-by-session)$/.test(file)
|
||||
function rea(operation,args){
|
||||
const result=spawnSync(process.execPath,['.tmp/rea-css-runtime/scripts/rea.mjs',operation,...args],{encoding:'utf8',maxBuffer:48*1024*1024})
|
||||
if(result.status!==0)throw Error('REA '+operation+' failed: '+(result.stderr||result.stdout).slice(0,2000))
|
||||
const evidence=JSON.parse(result.stdout)
|
||||
assert.equal(evidence.normalized_result.root_sha256,rootSha256,'Pinned REA root identity')
|
||||
return evidence
|
||||
}
|
||||
if(process.argv.includes('--inventory')){
|
||||
const inventory=rea('inspect-selected-directory-text-root',[root])
|
||||
fs.writeFileSync(inventoryPath,JSON.stringify(inventory,null,2)+'\n')
|
||||
}
|
||||
const inventory=JSON.parse(fs.readFileSync(inventoryPath,'utf8'))
|
||||
assert.equal(inventory.normalized_result.root_sha256,rootSha256)
|
||||
assert.equal(inventory.normalized_result.coverage.status,'complete')
|
||||
const files=inventory.normalized_result.members.filter(member=>member.kind==='file')
|
||||
assert.equal(files.length,82)
|
||||
assert.equal(files.reduce((n,file)=>n+file.size_bytes,0),1879348)
|
||||
const wanted=files.filter(file=>!marker(file.path)),known=new Map(wanted.map(file=>[file.path,file]))
|
||||
function localPath(file){
|
||||
assert.equal(path.posix.normalize(file),file)
|
||||
assert.ok(!file.startsWith('/')&&!file.includes('\\')&&!file.split('/').includes('..'))
|
||||
const target=path.resolve(recovered,file),base=path.resolve(recovered)+path.sep
|
||||
assert.ok(target.startsWith(base),'Recovery output stays in task directory')
|
||||
return target
|
||||
}
|
||||
function verifyItem(item){
|
||||
const member=known.get(item.path)
|
||||
assert.ok(member&&!marker(item.path))
|
||||
assert.equal(item.sha256,member.sha256)
|
||||
const bytes=fs.readFileSync(localPath(item.path))
|
||||
assert.equal(bytes.length,member.size_bytes)
|
||||
assert.equal(hash(bytes),member.sha256)
|
||||
const retained=JSON.parse(fs.readFileSync(item.retainedExport,'utf8'))
|
||||
assert.equal(retained.evidence_id,item.evidenceId)
|
||||
assert.equal(retained.normalized_result.root_sha256,rootSha256)
|
||||
const original=retained.normalized_result.files.find(file=>file.path===item.path)
|
||||
assert.ok(original)
|
||||
const originalBytes=original.encoding==='base64'?Buffer.from(original.data_base64,'base64'):Buffer.from(original.text,'utf8')
|
||||
assert.equal(hash(originalBytes),member.sha256)
|
||||
assert.equal(original.sha256,member.sha256)
|
||||
assert.equal(originalBytes.length,member.size_bytes)
|
||||
}
|
||||
if(process.argv.includes('--check')){
|
||||
const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'))
|
||||
assert.equal(manifest.rootSha256,rootSha256)
|
||||
manifest.files.forEach(verifyItem)
|
||||
assert.equal(new Set(manifest.files.map(file=>file.path)).size,manifest.files.length)
|
||||
assert.equal(manifest.files.length,wanted.length,'All functional/license/package files recovered')
|
||||
assert.ok(!fs.existsSync(path.join(recovered,'.downloaded_at')))
|
||||
console.log(JSON.stringify({status:'PASS',rootSha256,files:manifest.files.length,bytes:manifest.files.reduce((n,file)=>n+file.bytes,0),coverage:manifest.coverage}))
|
||||
process.exit(0)
|
||||
}
|
||||
const supportedOnly=process.argv.includes('--supported-only')
|
||||
const textFiles=wanted.filter(file=>!file.path.endsWith('.png')&&(!supportedOnly||/\.(js|css|json|md|txt|yaml|yml|py)$/i.test(file.path)))
|
||||
const binaryFiles=wanted.filter(file=>file.path.endsWith('.png'))
|
||||
const manifest={rootSha256,inventoryEvidenceId:inventory.evidence_id,retainedInventory:inventoryPath,files:[],coverage:{inventory:inventory.normalized_result.coverage,totalSourceFiles:files.length,selectedFunctionalFiles:wanted.length,recoveredFiles:0,excludedHostMarkers:files.filter(file=>marker(file.path)).map(file=>({path:file.path,bytes:file.size_bytes,sha256:file.sha256})),pending:[]}}
|
||||
fs.mkdirSync(recovered,{recursive:true})
|
||||
function retain(evidence,kind){
|
||||
const retainedExport='.tmp/rea-project-manager-'+kind+'-'+evidence.evidence_id+'.json'
|
||||
fs.writeFileSync(retainedExport,JSON.stringify(evidence)+'\n')
|
||||
for(const file of evidence.normalized_result.files){
|
||||
const member=known.get(file.path),bytes=file.encoding==='base64'?Buffer.from(file.data_base64,'base64'):Buffer.from(file.text,'utf8')
|
||||
assert.ok(member&&!marker(file.path));assert.equal(file.sha256,member.sha256);assert.equal(hash(bytes),member.sha256);assert.equal(bytes.length,member.size_bytes)
|
||||
const target=localPath(file.path);fs.mkdirSync(path.dirname(target),{recursive:true});fs.writeFileSync(target,bytes)
|
||||
const item={path:file.path,bytes:bytes.length,sha256:file.sha256,evidenceId:evidence.evidence_id,retainedExport};verifyItem(item);manifest.files.push(item)
|
||||
}
|
||||
manifest.files.sort((a,b)=>a.path.localeCompare(b.path));manifest.coverage.recoveredFiles=manifest.files.length
|
||||
manifest.coverage.pending=wanted.filter(member=>!manifest.files.some(file=>file.path===member.path)).map(file=>({path:file.path,bytes:file.size_bytes,sha256:file.sha256}))
|
||||
fs.writeFileSync(manifestPath,JSON.stringify(manifest,null,2)+'\n')
|
||||
}
|
||||
for(let start=0;start<textFiles.length;start+=16)retain(rea('export-selected-directory-text',[root,rootSha256,JSON.stringify(textFiles.slice(start,start+16).map(file=>file.path))]),'text')
|
||||
for(let start=0;start<binaryFiles.length;start+=16)retain(rea('export-selected-directory-binary',[root,rootSha256,JSON.stringify(binaryFiles.slice(start,start+16).map(file=>({path:file.path,sha256:file.sha256})))]),'binary')
|
||||
console.log(JSON.stringify({status:manifest.coverage.pending.length?'PARTIAL':'PASS',rootSha256,recoveredFiles:manifest.files.length,bytes:manifest.files.reduce((n,file)=>n+file.bytes,0),pendingFiles:manifest.coverage.pending.length,manifestPath}))
|
||||
@@ -0,0 +1,204 @@
|
||||
// Recover literal SVG JSX from the existing REA export without executing captured code.
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parse } from '@babel/parser'
|
||||
|
||||
const inputPath = '.tmp/rea-runtime-scripts.json'
|
||||
const outputPath = 'src/renderer/src/workbuddy/message-actions/SourceActionIcons.tsx'
|
||||
const bundle = JSON.parse(await fs.readFile(inputPath, 'utf8'))
|
||||
const mainItem = bundle.items.find(item => item.file_path?.endsWith('src-0FGg1seX.js'))
|
||||
const chevronItem = bundle.items.find(item => item.file_path?.endsWith('chevron-down-icon-Ve_gE8h-.js'))
|
||||
|
||||
if (!mainItem?.source?.included || !chevronItem?.source?.included) {
|
||||
throw new Error('The REA export is missing one or more captured icon sources')
|
||||
}
|
||||
|
||||
function verifiedSource(item) {
|
||||
const source = item.source.artifact.text
|
||||
const sha256 = crypto.createHash('sha256').update(source).digest('hex')
|
||||
if (sha256 !== item.source.artifact.sha256) throw new Error(`REA source digest mismatch: ${item.file_path}`)
|
||||
return { source, sha256 }
|
||||
}
|
||||
|
||||
function findAssignment(ast, name) {
|
||||
const matches = []
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && node.left.name === name) {
|
||||
matches.push(node)
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
}
|
||||
visit(ast.program)
|
||||
if (matches.length !== 1) throw new Error(`Expected one assignment for ${name}, found ${matches.length}`)
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
function keyName(node) {
|
||||
if (node.type === 'Identifier') return node.name
|
||||
if (node.type === 'StringLiteral' || node.type === 'NumericLiteral') return String(node.value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
function staticValue(node) {
|
||||
if (node.type === 'StringLiteral') return { kind: 'string', value: node.value }
|
||||
if (node.type === 'NumericLiteral') return { kind: 'number', value: node.value }
|
||||
if (node.type === 'BooleanLiteral') return { kind: 'boolean', value: node.value }
|
||||
if (node.type === 'NullLiteral') return { kind: 'null' }
|
||||
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
||||
return { kind: 'string', value: node.quasis[0].value.cooked ?? node.quasis[0].value.raw }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function escapeAttribute(value) {
|
||||
return String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<')
|
||||
}
|
||||
|
||||
function isFactoryCall(node) {
|
||||
if (node.type !== 'CallExpression' || node.arguments.length < 2) return false
|
||||
const callee = node.callee
|
||||
return callee.type === 'SequenceExpression'
|
||||
|| callee.type === 'MemberExpression'
|
||||
|| callee.type === 'Identifier'
|
||||
}
|
||||
|
||||
function renderChild(node, depth, bindings) {
|
||||
if (!node || node.type === 'NullLiteral') return ''
|
||||
if (node.type === 'ArrayExpression') return node.elements.map(child => renderChild(child, depth, bindings)).filter(Boolean).join('\n')
|
||||
if (node.type === 'StringLiteral') return `${' '.repeat(depth)}${node.value}`
|
||||
if (!isFactoryCall(node)) throw new Error(`Unsupported captured SVG child node: ${node.type}`)
|
||||
const tagNode = node.arguments[0]
|
||||
const propsNode = node.arguments[1]
|
||||
const tag = staticValue(tagNode)
|
||||
if (tag?.kind !== 'string' || propsNode.type !== 'ObjectExpression') {
|
||||
throw new Error('Captured SVG element is not a literal JSX factory call')
|
||||
}
|
||||
|
||||
const attrs = []
|
||||
let children
|
||||
for (const property of propsNode.properties) {
|
||||
if (property.type === 'SpreadElement') {
|
||||
const binding = bindings.get(property.argument.name)
|
||||
if (!binding || binding.kind !== 'spread') throw new Error('Unexpected dynamic SVG props spread')
|
||||
attrs.push(`{...${binding.value}}`)
|
||||
continue
|
||||
}
|
||||
if (property.type !== 'ObjectProperty') throw new Error('Unsupported SVG props member')
|
||||
const name = keyName(property.key)
|
||||
if (!name || name === 'key') continue
|
||||
if (name === 'children') {
|
||||
children = property.value
|
||||
continue
|
||||
}
|
||||
const value = staticValue(property.value)
|
||||
if (value?.kind === 'string') {
|
||||
attrs.push(`${name}="${escapeAttribute(value.value)}"`)
|
||||
} else if (value?.kind === 'number') {
|
||||
attrs.push(`${name}={${value.value}}`)
|
||||
} else if (value?.kind === 'boolean') {
|
||||
attrs.push(value.value ? name : `${name}={false}`)
|
||||
} else if (value?.kind === 'null') {
|
||||
attrs.push(`${name}={null}`)
|
||||
} else if (property.value.type === 'Identifier') {
|
||||
const binding = bindings.get(property.value.name)
|
||||
if (!binding || binding.kind !== 'expression') throw new Error(`Unexpected dynamic SVG prop: ${name}`)
|
||||
attrs.push(`${name}={${binding.value}}`)
|
||||
} else {
|
||||
throw new Error(`Unsupported captured SVG attribute value: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
const pad = ' '.repeat(depth)
|
||||
const attrText = attrs.map(attribute => `${' '.repeat(depth + 1)}${attribute}`).join('\n')
|
||||
const renderedChildren = children ? renderChild(children, depth + 1, bindings) : ''
|
||||
if (!renderedChildren) return `${pad}<${tag.value}${attrs.length ? `\n${attrText}\n${pad}` : ''} />`
|
||||
return `${pad}<${tag.value}${attrs.length ? `\n${attrText}\n${pad}` : ''}>\n${renderedChildren}\n${pad}</${tag.value}>`
|
||||
}
|
||||
|
||||
function componentSource(assignment, exportedName, sourceLabel, sha256) {
|
||||
const arrow = assignment.right
|
||||
if (arrow.type !== 'ArrowFunctionExpression' || arrow.params.length !== 1) {
|
||||
throw new Error(`Unexpected icon component form: ${assignment.left.name}`)
|
||||
}
|
||||
const param = arrow.params[0]
|
||||
let signature
|
||||
let bindings
|
||||
if (param.type === 'ObjectPattern') {
|
||||
const propsBindings = new Map()
|
||||
for (const property of param.properties) {
|
||||
if (property.type === 'ObjectProperty' && property.value.type === 'Identifier') {
|
||||
const key = keyName(property.key)
|
||||
if (key === 'className' || key === 'style') propsBindings.set(property.value.name, { kind: 'expression', value: key })
|
||||
} else if (property.type === 'RestElement' && property.argument.type === 'Identifier') {
|
||||
propsBindings.set(property.argument.name, { kind: 'spread', value: 'props' })
|
||||
}
|
||||
}
|
||||
signature = '({ className, style, ...props }: IconProps)'
|
||||
bindings = propsBindings
|
||||
} else if (param.type === 'Identifier') {
|
||||
signature = '(props: IconProps)'
|
||||
bindings = new Map([[param.name, { kind: 'spread', value: 'props' }]])
|
||||
} else {
|
||||
throw new Error(`Unsupported icon component parameter: ${assignment.left.name}`)
|
||||
}
|
||||
const body = arrow.body
|
||||
if (!isFactoryCall(body)) throw new Error(`Unsupported icon component body: ${assignment.left.name}`)
|
||||
const svg = renderChild(body, 2, bindings)
|
||||
return `/** REA ${bundle.evidence_id}; ${sourceLabel} SHA-256 ${sha256}; UTF-16 [${assignment.start}, ${assignment.end}). */\nexport function ${exportedName}${signature}: JSX.Element {\n return (\n${svg}\n )\n}\n`
|
||||
}
|
||||
|
||||
const main = verifiedSource(mainItem)
|
||||
const chevron = verifiedSource(chevronItem)
|
||||
const mainAst = parse(main.source, { sourceType: 'module', plugins: ['jsx'] })
|
||||
const chevronAst = parse(chevron.source, { sourceType: 'module', plugins: ['jsx'] })
|
||||
const mainIcons = [
|
||||
['ThumbsUpIcon', 'SourceThumbsUpIcon'],
|
||||
['ThumbsUpSolidIcon', 'SourceThumbsUpSolidIcon'],
|
||||
['ThumbsDownIcon', 'SourceThumbsDownIcon'],
|
||||
['ThumbsDownSolidIcon', 'SourceThumbsDownSolidIcon'],
|
||||
['ResendIcon', 'SourceResendIcon'],
|
||||
['RevertIcon', 'SourceRevertIcon']
|
||||
]
|
||||
const snippets = [
|
||||
"import type { JSX, SVGProps } from 'react'",
|
||||
'',
|
||||
'type IconProps = SVGProps<SVGSVGElement>',
|
||||
'',
|
||||
'// No dedicated regenerate icon declaration was found in the captured bundle.',
|
||||
''
|
||||
]
|
||||
|
||||
for (const [sourceName, exportedName] of mainIcons) {
|
||||
const assignment = findAssignment(mainAst, sourceName)
|
||||
snippets.push(componentSource(
|
||||
assignment,
|
||||
exportedName,
|
||||
'src-0FGg1seX.js',
|
||||
main.sha256
|
||||
))
|
||||
}
|
||||
|
||||
const chevronAssignment = findAssignment(chevronAst, 'ChevronDownIconImpl')
|
||||
snippets.push(componentSource(
|
||||
chevronAssignment,
|
||||
'SourceFoldChevronIcon',
|
||||
'chevron-down-icon-Ve_gE8h-.js',
|
||||
chevron.sha256
|
||||
))
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await fs.writeFile(outputPath, `${snippets.join('\n')}\n`)
|
||||
console.log(JSON.stringify({
|
||||
outputPath,
|
||||
evidenceId: bundle.evidence_id,
|
||||
sources: [
|
||||
{ file: 'src-0FGg1seX.js', sha256: main.sha256, icons: mainIcons.length },
|
||||
{ file: 'chevron-down-icon-Ve_gE8h-.js', sha256: chevron.sha256, icons: 1 }
|
||||
],
|
||||
regenerate: 'no dedicated icon declaration found'
|
||||
}, null, 2))
|
||||
@@ -0,0 +1,36 @@
|
||||
/** Fixed retained REA only; never reads original WorkBuddy paths. */
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const hash=value=>crypto.createHash('sha256').update(value).digest('hex')
|
||||
const archive=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8'))
|
||||
const source=archive.items.find(item=>item.file_path.endsWith('src-DbE1_mgM.js'))?.source.artifact
|
||||
const sourceSha='6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4'
|
||||
if(!source||source.sha256!==sourceSha||hash(source.text)!==sourceSha)throw Error('REA script identity mismatch')
|
||||
const functions=new Map(),assignments=new Map()
|
||||
function walk(node){if(!node||typeof node!=='object')return;if(node.type==='FunctionDeclaration')functions.set(node.id.name,node);if(node.type==='AssignmentExpression'&&node.left.type==='Identifier')assignments.set(node.left.name,node);for(const[key,value]of Object.entries(node)){if(key==='loc'||key==='comments')continue;if(Array.isArray(value))value.forEach(walk);else if(value&&typeof value==='object')walk(value)}}walk(parse(source.text,{sourceType:'module'}).program)
|
||||
const manifest={version:'5.8.2',rootSha256:'035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',evidence:[archive.evidence_id,'ev_7e9604adb87a3d21570eb7a371001e48b3d316ee0388a03a97e2cb564d03aff7'],sources:[{file:'src-DbE1_mgM.js',sha256:sourceSha}],bodies:[],adaptations:['React/JSX singleton and pinned YCode FloatingUI host dependency replace bundled imports; original dependency version is unknown.','Original useInputBoxStore/useInputProvider/useInputBoxSelector are bound to typed controlled host. Non-null silent preparation is owned by the parent and is a no-op here; remove awaits the actual host and rolls back on failure.','The original chip has no click-to-open-picker behavior. No new click behavior is introduced.','Host scopeKey remount prevents stale clear rollback from crossing owners. Optional disabled state blocks removal during host busy/unavailable state.','Tooltip portal and recovered CSS are scoped to this component; original tooltip function/layer and helper bodies are retained.']}
|
||||
function body(name){const node=functions.get(name)??assignments.get(name);if(!node)throw Error('Missing source '+name);const raw=source.text.slice(node.start,node.end);manifest.bodies.push({name,start:node.start,end:node.end,sha256:hash(raw)});let result=(node.type==='AssignmentExpression'?'const ':'')+raw.replace(/\(0,[\w$]+\.(jsx|jsxs)\)/g,' JSX.$1')+';';if(name==='ExpertChip'){const anchor='let a=usePrefixCls(),c=useInputBoxStore()';if(!result.includes(anchor))throw Error('Disabled hook boundary changed');result=result.replace(anchor,'let blocked=useActiveExpertDisabled(),a=usePrefixCls(),c=useInputBoxStore()');const clear='onClick:h,"aria-label":';if(!result.includes(clear))throw Error('Disabled button boundary changed');result=result.replace(clear,'onClick:h,disabled:blocked,"aria-label":')}return result}
|
||||
let js=`import * as React from 'react';import * as JSX from 'react/jsx-runtime';import {useFloating,offset as D,flip as E,shift as O,arrow as k,autoUpdate as A,useInteractions,useDismiss,useTransitionStyles,FloatingPortal as NativeFloatingPortal} from '@floating-ui/react';
|
||||
import {useInputBoxStore,useInputProvider,useInputBoxSelector,useActiveExpertDisabled} from './SourceActiveExpertRuntime';
|
||||
const oG=React,Ei=React,mi=React,ci=React,Di=JSX,io={1:12,2:16},kM=8,gi=3,_i=8,li=9990,ui=10,di='default',fi='__ycodeActiveExpertFloatingLayer__',yi=4,bi=1000,xi=.1,Si='data-pointer-press-transparent',wi=new Set(['SVG','IMG','VIDEO','CANVAS','AUDIO']),Oi=new Map();let Ci=null;
|
||||
const usePrefixCls=()=>name=>'cr-'+name,clsx=(...values)=>values.flat().filter(Boolean).join(' '),FloatingPortal=({children})=>JSX.jsx(NativeFloatingPortal,{children:JSX.jsx('div',{className:'yc-source-active-expert','data-cr-theme':children?.props?.['data-cr-theme'],children})});
|
||||
`
|
||||
for(const name of ['getFloatingLayerState','getGroupStack','nextZIndex','useFloatingLayer','TooltipLayer','isPressTransparent','isHoverSuppressedByPress','suppressHoverUntilPointerMove','isElementTruncated','isSubtreeTruncated','normalizeVisibleText','collectVisibleText','hasVisibleTriggerText','hasTruncationStyle','shouldGateOnTruncation','isEmptyContent','resolveTooltipThemeMode','isVisibleFocus','Tooltip$1','LoadingSpinnerIcon','ChipCloseIcon','toNonEmptyString','resolveAvatarFallbackSeed','getAvatarFallbackBackground','isExpertSelectionDisplayable','hasText','isPreparingExpert','resolveExpertChipAvatar','ExpertAvatar','ExpertChip'])js+=body(name)+'\n'
|
||||
js+=`if(typeof document!=='undefined'){document.addEventListener('pointerdown',n=>{Ci=isPressTransparent(n.target)?null:{x:n.clientX,y:n.clientY,at:Date.now()}},true);document.addEventListener('pointermove',n=>{if(Ci&&(Math.abs(n.clientX-Ci.x)>yi||Math.abs(n.clientY-Ci.y)>yi))Ci=null},true)}
|
||||
export {ExpertChip,ExpertAvatar,resolveAvatarFallbackSeed,getAvatarFallbackBackground,isExpertSelectionDisplayable,isPreparingExpert,resolveExpertChipAvatar};
|
||||
`
|
||||
const cssManifest=JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json','utf8'))
|
||||
let css='/* Source ExpertChip/Tooltip declarations recovered from fixed REA CSS; selectors scoped. */\n'
|
||||
for(const[file,pin]of Object.entries({'src-CBaV_A3S.css':'2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c','src-CyXHA0IC.css':'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe'})){
|
||||
const text=fs.readFileSync('.tmp/rea-css-recovered/'+file,'utf8');if(hash(text)!==pin||cssManifest.files.find(item=>item.file===file)?.sha256!==pin)throw Error('CSS identity mismatch '+file);manifest.sources.push({file,sha256:pin,evidenceId:cssManifest.sourceEvidenceId});const tree=postcss.parse(text)
|
||||
tree.walkRules(rule=>{if(file==='src-CBaV_A3S.css'){const declarations=(rule.nodes??[]).filter(node=>node.type==='decl'&&/^--cr-(?:internal-)?(?:avatar-fallback-bg-|color-on-accent|text-primary|text-secondary|tooltip|chip-hover-bg|focus-border)/.test(node.prop));if(!declarations.length)return;const light=rule.selector.includes('data-cr-theme=light');const clone=rule.clone({nodes:declarations.map(node=>node.clone())});clone.selector=light?'.cr-theme[data-cr-theme=light] .yc-source-active-expert,.yc-source-active-expert[data-cr-theme=light],.light .yc-source-active-expert,[data-theme=light] .yc-source-active-expert':'.yc-source-active-expert';css+=clone.toString()+'\n';return}
|
||||
if(!/\.cr-(?:chip-|expert-chip-|tooltip(?:__|--|\b))/.test(rule.selector)||/cr-address|model-selector/.test(rule.selector))return;const clone=rule.clone();clone.selector=clone.selectors.map(selector=>'.yc-source-active-expert '+selector).join(',');if(rule.parent.type==='atrule'){const parent=rule.parent.clone({nodes:[]});parent.append(clone);css+=parent.toString()+'\n'}else css+=clone.toString()+'\n'})
|
||||
if(file==='src-CyXHA0IC.css')tree.walkAtRules('keyframes',rule=>{if(['cr-expert-chip-spin','cr-input-spin'].includes(rule.params))css+=rule.toString()+'\n'})
|
||||
}
|
||||
css+='/* Explicit YCode host busy state, separate from original source declarations. */\n.yc-source-active-expert{display:inline-flex;flex-shrink:0}.yc-source-active-expert[data-disabled=true] .cr-chip-base--static .cr-chip-icon--default{display:inline-flex}.yc-source-active-expert[data-disabled=true] .cr-chip-base--static .cr-chip-icon--hover{display:none}.yc-source-active-expert[data-disabled=true] .cr-chip-base--static:hover{background:transparent}\n'
|
||||
const dts=`import type {ComponentType} from 'react';import type {ActiveExpert} from './SourceActiveExpert';export const ExpertChip:ComponentType<{compact?:boolean}>;export const ExpertAvatar:ComponentType<{avatarUrl?:string;name?:string;preparing?:boolean}>;export function isExpertSelectionDisplayable(expert:ActiveExpert):boolean;export function isPreparingExpert(expert:ActiveExpert,id?:string):boolean;export function resolveExpertChipAvatar(expert:ActiveExpert):{avatarUrl?:string;seed:string};export function resolveAvatarFallbackSeed(expert:ActiveExpert):string;export function getAvatarFallbackBackground(name?:string):string;\n`
|
||||
const outputs=[['src/renderer/src/capabilities/SourceActiveExpertRecovered.mjs',js],['src/renderer/src/capabilities/SourceActiveExpertRecovered.d.mts',dts],['src/renderer/src/capabilities/SourceActiveExpertSource.css',css]]
|
||||
manifest.outputs=outputs.map(([file,text])=>({file,bytes:Buffer.byteLength(text),sha256:hash(text)}));outputs.push(['src/renderer/src/capabilities/SourceActiveExpertManifest.json',JSON.stringify(manifest,null,2)+'\n'])
|
||||
for(const[file,text]of outputs){if(process.argv.includes('--check')){if(fs.readFileSync(file,'utf8')!==text)throw Error('Recovery drift '+file)}else fs.writeFileSync(file,text)}console.log(JSON.stringify({status:'PASS',functions:manifest.bodies.length,sources:manifest.sources.length,outputs:outputs.length}))
|
||||
@@ -0,0 +1,35 @@
|
||||
// Recover only the avatar SVG from an already archived REA DOM observation.
|
||||
import fs from 'node:fs/promises'
|
||||
import crypto from 'node:crypto'
|
||||
const sourceFile = '.tmp/rea-assessment-initial/layout.json'
|
||||
const source = JSON.parse(await fs.readFile(sourceFile, 'utf8'))
|
||||
const document = source.layout.documents[0]
|
||||
const nodes = document.nodes, svg = document.svg_nodes
|
||||
const logo = nodes.find(node => /\bcr-agent__logo\b/.test(node.attributes?.class ?? ''))
|
||||
const root = svg.find(node => node.parent_index === logo?.node_index && node.node_name === 'svg')
|
||||
if (!root || root.attributes.viewBox !== '0 0 24 24') throw Error('Expected REA avatar SVG missing')
|
||||
const tree = node => ({ tag: node.node_name, attrs: node.attributes, children: svg.filter(child => child.parent_index === node.node_index).map(tree) })
|
||||
const payload = tree(root), serialized = JSON.stringify(payload)
|
||||
const payloadSha = crypto.createHash('sha256').update(serialized).digest('hex')
|
||||
const code = `// Generated from WorkBuddy 5.8.2 REA ${source.evidenceId}.
|
||||
// Avatar payload SHA256 ${payloadSha}; runtime SVG observation, not guessed artwork.
|
||||
import { createElement, useId, type ReactNode } from 'react'
|
||||
interface SvgNode { tag: string; attrs: Record<string, string>; children: SvgNode[] }
|
||||
const sourceSvg: SvgNode = ${JSON.stringify(payload, null, 2)}
|
||||
export default function SourceAgentAvatar(): React.JSX.Element {
|
||||
const clipId = 'cr-agent-logo-clip-' + useId().replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
const render = (node: SvgNode, key = 'root'): ReactNode => {
|
||||
const attrs = Object.fromEntries(Object.entries(node.attrs).map(([name, value]) => [
|
||||
name === 'clip-path' ? 'clipPath' : name === 'fill-rule' ? 'fillRule' : name,
|
||||
name === 'id' ? clipId : name === 'clip-path' ? 'url(#' + clipId + ')' : value
|
||||
]))
|
||||
return createElement(node.tag, { ...attrs, key, ...(key === 'root' ? { 'aria-hidden': true, style: { display: 'block' } } : {}) }, ...node.children.map((child, index) => render(child, key + ':' + index)))
|
||||
}
|
||||
return render(sourceSvg) as React.JSX.Element
|
||||
}
|
||||
`
|
||||
const output = 'src/renderer/src/components/SourceAgentAvatar.tsx'
|
||||
if (process.argv.includes('--check')) {
|
||||
if (await fs.readFile(output, 'utf8') !== code) throw Error('Recovered avatar differs from fixed REA SVG')
|
||||
} else await fs.writeFile(output, code)
|
||||
console.log(JSON.stringify({ evidenceId: source.evidenceId, nodes: svg.filter(node => node.node_index >= root.node_index && node.node_index < root.node_index + 10).length, payloadSha, output, check: process.argv.includes('--check') }))
|
||||
@@ -0,0 +1,39 @@
|
||||
// Reconstruct an inert SVG from saved REA exports; never accesses WorkBuddy.
|
||||
import fs from 'node:fs/promises'
|
||||
import crypto from 'node:crypto'
|
||||
const exportData = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const evidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
if (exportData.evidence_id !== evidenceId) throw new Error('REA runtime evidence changed')
|
||||
function source(file, expected) {
|
||||
const item = exportData.items.find(i => i.file_path?.replaceAll('\\', '/').endsWith('/' + file))
|
||||
const text = item?.source?.artifact?.text
|
||||
if (!text || crypto.createHash('sha256').update(text).digest('hex') !== expected || item.source.artifact.sha256 !== expected) throw new Error(`REA source digest mismatch: ${file}`)
|
||||
return text
|
||||
}
|
||||
const contextsHash = '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b'
|
||||
const iconHash = '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0'
|
||||
const contexts = source('contexts-BMeWSUHk.js', contextsHash)
|
||||
const icons = source('src-DPDj7_qD.js', iconHash)
|
||||
if (!contexts.includes('pl as yo') || !contexts.includes('(yo,{className:`conversation-high-credit-approval__option-arrow`}')) throw new Error('Approval icon binding changed')
|
||||
if (!icons.includes('uc as pl') || !icons.includes('uc=createIcon(lc)') || !icons.includes('yn={sm:14,md:16,lg:20,xl:24}')) throw new Error('Approval icon export or wrapper default changed')
|
||||
const start = icons.indexOf('lc=(0,sc.forwardRef)')
|
||||
const end = icons.indexOf(')),lc.displayName=', start) + 2
|
||||
if (start < 0 || end < start) throw new Error('Approval SVG definition is missing')
|
||||
const definition = icons.slice(start, end)
|
||||
function literal(key) {
|
||||
const match = definition.match(new RegExp(`${key}:\x60([^\x60]+)\x60`))
|
||||
if (!match) throw new Error(`Approval SVG attribute missing: ${key}`)
|
||||
return JSON.stringify(match[1])
|
||||
}
|
||||
const output = `// Generated from REA ${evidenceId}.
|
||||
// contexts-BMeWSUHk.js sha256 ${contextsHash}: yo -> pl -> uc -> lc.
|
||||
// src-DPDj7_qD.js sha256 ${iconHash}; UTF-16 [${start}, ${end}).
|
||||
// createIcon defaults to md=16; original approval supplies only className.
|
||||
export function ApprovalChevronIcon({ className }: { className?: string }) {
|
||||
return <svg className={className} width="16" height="16" viewBox={${literal('viewBox')}} fill={${literal('fill')}} aria-hidden="true">
|
||||
<path d={${literal('d')}} stroke={${literal('stroke')}} strokeWidth={${literal('strokeWidth')}} strokeLinecap={${literal('strokeLinecap')}} strokeLinejoin={${literal('strokeLinejoin')}} />
|
||||
</svg>
|
||||
}
|
||||
`
|
||||
await fs.writeFile('src/renderer/src/workbuddy/conversation/ApprovalSourceIcon.tsx', output)
|
||||
console.log(JSON.stringify({ evidenceId, icon: 'ApprovalChevronIcon', utf16Range: [start, end], sources: [contextsHash, iconHash] }))
|
||||
@@ -0,0 +1,34 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
const directory = '.tmp/rea-css-recovered'
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(directory, 'manifest.json'), 'utf8'))
|
||||
if (manifest.sourceEvidenceId !== 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94') throw Error('Unexpected source Evidence')
|
||||
const result = postcss.root(), sources = []
|
||||
const hash = value => crypto.createHash('sha256').update(value).digest('hex')
|
||||
for (const file of manifest.files) {
|
||||
const source = await fs.readFile(path.join(directory, file.file), 'utf8')
|
||||
if (hash(source) !== file.sha256) throw Error('Source CSS SHA mismatch')
|
||||
const selected = postcss.root()
|
||||
const project = node => {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = node.selectors.filter(selector => ['.conversation-finished-footer', '.cr-cancelled-indicator'].includes(selector) || /^\.artifact-slot-panel(?:\s*$|__(?:actions|action-btn|action-chevron)(?:[\s:>.#[]|$))/.test(selector))
|
||||
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes && !/keyframes$/.test(node.name)) {
|
||||
const nodes = node.nodes.map(project).filter(Boolean)
|
||||
return nodes.length ? node.clone({ nodes }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
for (const node of postcss.parse(source).nodes) { const part = project(node); if (part) selected.append(part) }
|
||||
if (selected.nodes.length) { sources.push({ file: file.file, sha256: file.sha256 }); result.append(selected.nodes) }
|
||||
}
|
||||
if (!sources.length || !result.toString().includes('.artifact-slot-panel__action-btn')) throw Error('Missing original artifact link rules')
|
||||
const text = `/* REA ${manifest.sourceEvidenceId}; WorkBuddy 5.8.2. Original rule bodies/order/conditions.
|
||||
* ${sources.map(source => source.file + ' sha256=' + source.sha256).join('\n * ')} */\n` + result.toString() + '\n'
|
||||
const output = 'src/renderer/src/components/source-artifact-link.css'
|
||||
if (process.argv.includes('--check')) { if (await fs.readFile(output, 'utf8') !== text) throw Error('Artifact link CSS changed') }
|
||||
else await fs.writeFile(output, text)
|
||||
console.log(JSON.stringify({ sources, output, sha256: hash(text), check: process.argv.includes('--check') }))
|
||||
@@ -0,0 +1,35 @@
|
||||
// Reads previously exported REA evidence only. Does not access or execute WorkBuddy.
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import generatePackage from '@babel/generator'
|
||||
const generate = generatePackage.default ?? generatePackage
|
||||
const bundle = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const script = bundle.items.find(item => item.file_path?.endsWith('src-Cng7axVa.js'))
|
||||
if (!script?.source?.included) throw new Error('Missing REA-captured Artifact source')
|
||||
const source = script.source.artifact.text
|
||||
const sha256 = crypto.createHash('sha256').update(source).digest('hex')
|
||||
if (sha256 !== script.source.artifact.sha256) throw new Error('REA source digest mismatch')
|
||||
const names = new Set(['ArtifactContentRenderer', 'MarkdownViewer', 'useCustomArtifactContent', 'useDiffContent',
|
||||
'shouldRenderHtmlPreview', 'isHtmlPreviewSizeOversized', 'resolveHtmlPreviewSizeLimit', 'isHtmlFileName',
|
||||
'shouldRemountMediaArtifactOnRefresh', 'rl', 'il', 'al'])
|
||||
const output = path.resolve('.tmp/rea-artifact-recovered')
|
||||
await fs.mkdir(output, { recursive: true })
|
||||
const records = []
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
const name = node.type === 'FunctionDeclaration' ? node.id?.name : node.type === 'AssignmentExpression' ? node.left?.name : undefined
|
||||
if (names.has(name)) records.push({ name, start: node.start, end: node.end, text: generate(node).code })
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
}
|
||||
visit(parse(source, { sourceType: 'module' }).program)
|
||||
for (const record of records) await fs.writeFile(path.join(output, `${record.name}.js`), record.text + '\n')
|
||||
const manifest = { evidenceId: bundle.evidence_id, sourcePath: script.file_path, sourceSha256: sha256,
|
||||
method: 'Babel parse/generate; reference only, not executed', units: 'UTF-16; end-exclusive',
|
||||
definitions: records.map(({ text, ...record }) => ({ ...record, file: `${record.name}.js` })) }
|
||||
await fs.writeFile(path.join(output, 'manifest.json'), JSON.stringify(manifest, null, 2))
|
||||
console.log(JSON.stringify(manifest))
|
||||
@@ -0,0 +1,338 @@
|
||||
// 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))
|
||||
@@ -0,0 +1,37 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const evidence = JSON.parse(await readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
if (evidence.evidence_id !== 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb') throw Error('REA script Evidence changed')
|
||||
const source = evidence.items.find(item => item.file_path.endsWith('media-preview-DMz4TW4q.js'))?.source?.artifact?.text
|
||||
if (!source || createHash('sha256').update(source).digest('hex') !== 'e8c19693ac7d8d06b45b531de405cc23c071fa19ebd5543b483a43dbdfce8111') throw Error('Browser source changed')
|
||||
const names = { BackIcon: 'SourceBrowserBackIcon', ForwardIcon: 'SourceBrowserForwardIcon', RefreshIcon: 'SourceBrowserRefreshIcon', BrowserPlaceholderIcon: 'SourceBrowserPlaceholderIcon' }
|
||||
const found = new Map()
|
||||
const walk = node => {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && names[node.left.name] && node.right.type === 'ArrowFunctionExpression') {
|
||||
let value = source.slice(node.right.start, node.right.end).replace(/\(0,[A-Za-z0-9_$]+\.(jsx|jsxs)\)/g, ' $1')
|
||||
value = value.replace(/Yr\.icon/g, '"_icon_vnue1_21"')
|
||||
found.set(node.left.name, `const ${names[node.left.name]} = ${value}`)
|
||||
}
|
||||
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(walk); else if (value && typeof value === 'object') walk(value)
|
||||
}
|
||||
walk(parse(source, { sourceType: 'module' }))
|
||||
if (found.size !== Object.keys(names).length) throw Error('Browser SVG family missing')
|
||||
const panePath = 'src/renderer/src/workbuddy/workbench/NativeBrowserPane.tsx'
|
||||
const pane = await readFile(panePath, 'utf8')
|
||||
await writeFile(panePath, pane.replace(/\/\/ BEGIN REA BROWSER SVG[\s\S]*?\/\/ END REA BROWSER SVG/, `// BEGIN REA BROWSER SVG\n// Unchanged SVG payloads from ${evidence.evidence_id}, media-preview-DMz4TW4q.js.\n${[...found.values()].join('\n')}\n// END REA BROWSER SVG`))
|
||||
const css = await readFile('.tmp/rea-css-recovered/media-preview-Dvr0Ekj9.css', 'utf8')
|
||||
if (createHash('sha256').update(css).digest('hex') !== '36758fadd61577145a3b46dd12bcd966c662ea0cfaa4e62dc3e31ba4bf7f5fb8') throw Error('Browser CSS changed')
|
||||
function filter(node) {
|
||||
if (node.type === 'rule') return /_rh4x8_|_x8iy7_|_vnue1_/.test(node.selector) ? node.clone() : null
|
||||
if (node.type !== 'atrule') return null
|
||||
const result = node.clone({ nodes: [] })
|
||||
for (const child of node.nodes ?? []) { const match = filter(child); if (match) result.append(match) }
|
||||
return result.nodes.length ? result : null
|
||||
}
|
||||
const selected = postcss.root()
|
||||
for (const node of postcss.parse(css).nodes) { const match = filter(node); if (match) selected.append(match) }
|
||||
await writeFile('src/renderer/src/workbuddy/workbench/browser-source.css', `/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; exact browser rules with parent conditions. */\n${selected.toString()}\n/* YCode native-surface bindings. */\n.yc-native-browser{--wb-color-border-tertiary:var(--yc-border-tertiary);--wb-color-border-solid:var(--yc-border-primary);--wb-color-border-focus:var(--yc-brand-8);min-height:0;flex:1;position:relative}\n.yc-native-browser button{border:0;background:transparent;padding:0;cursor:pointer}\n.yc-native-browser button:disabled{opacity:.35;cursor:default}\n.yc-native-browser__viewport{flex:1;min-height:0;background:white}\n.yc-native-browser__progress{height:2px;background:var(--yc-brand-8);position:absolute;top:47px;left:0;right:0;z-index:1;opacity:.6}\n:root[data-theme=dark] .yc-native-browser ._icon_vnue1_21{--nt-browser-chrome:#424242;--nt-browser-body:#333}\n`)
|
||||
console.log(`Recovered ${found.size} source browser SVGs and browser toolbar/placeholder CSS`)
|
||||
@@ -0,0 +1,193 @@
|
||||
/** Reproducible recovery from REA records only. No installed application reads or runtime actions. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const out = 'src/renderer/src/capabilities'
|
||||
const check = process.argv.includes('--check')
|
||||
const hash = text => crypto.createHash('sha256').update(text).digest('hex')
|
||||
const bundle = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const source = name => {
|
||||
const item = bundle.items.find(item => item.file_path.endsWith(name))
|
||||
if (!item || hash(item.source.artifact.text) !== item.source.artifact.sha256) throw new Error(`REA identity mismatch: ${name}`)
|
||||
return { ...item.source.artifact, file: name }
|
||||
}
|
||||
const c = source('contexts-BMeWSUHk.js')
|
||||
const icons = source('src-DPDj7_qD.js')
|
||||
const manifest = { evidenceId: bundle.evidence_id, version: '5.8.2', source: [
|
||||
{ file: c.file, sha256: c.sha256 }, { file: icons.file, sha256: icons.sha256 }
|
||||
], bodies: [], css: [], adaptations: [
|
||||
'React and JSX runtime use the YCode singleton; original module initialization is omitted.',
|
||||
'UnifiedMarketHeader keeps its source body; visible tabs, tooltip and icon URL resolver use explicit local host adapters.',
|
||||
'ExpertSearchBar keeps its source body and 300ms debounce/IME behavior; the input primitive uses the matching DOM contract.',
|
||||
'CSS declarations/order/media/keyframes are preserved and selectors scoped to .yc-capability-center; :root expert variables become local light-theme variables.'
|
||||
] }
|
||||
const assignments = new Map(), strings = new Map(), maps = new Map(), functions = new Map()
|
||||
function index(text) {
|
||||
const ast = parse(text, { sourceType: 'module' })
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'FunctionDeclaration' && node.id) functions.set(node.id.name, { node, text })
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
|
||||
assignments.set(node.left.name, { node, text })
|
||||
const r = node.right
|
||||
if (r.type === 'StringLiteral') strings.set(node.left.name, r.value)
|
||||
if (r.type === 'TemplateLiteral' && !r.expressions.length) strings.set(node.left.name, r.quasis[0].value.cooked)
|
||||
if (r.type === 'ObjectExpression') maps.set(node.left.name, r)
|
||||
}
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if (key === 'loc' || key === 'comments') continue
|
||||
if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
}
|
||||
visit(ast.program)
|
||||
}
|
||||
index(c.text)
|
||||
function recover(name, kind = 'assignment') {
|
||||
const { node, text } = (kind === 'function' ? functions : assignments).get(name) ?? {}
|
||||
if (!node) throw new Error(`Missing source body: ${name}`)
|
||||
const body = text.slice(node.start, node.end)
|
||||
manifest.bodies.push({ name, file: text === c.text ? c.file : icons.file, start: node.start, end: node.end, sha256: hash(body) })
|
||||
return kind === 'function' ? body : `const ${body};`
|
||||
}
|
||||
const classMaps = {}
|
||||
for (const name of ['tz', 'EI', 'UF', 'lI', 'tI', '_L', 'KF', 'Vvt', 'NR']) {
|
||||
const node = maps.get(name)
|
||||
if (!node) throw new Error(`Missing class map: ${name}`)
|
||||
classMaps[name] = Object.fromEntries(node.properties.filter(p => p.type === 'ObjectProperty').map(p => {
|
||||
const key = p.key.name ?? p.key.value
|
||||
const value = p.value.type === 'Identifier' ? strings.get(p.value.name)
|
||||
: p.value.type === 'StringLiteral' ? p.value.value
|
||||
: p.value.type === 'TemplateLiteral' && !p.value.expressions.length ? p.value.quasis[0].value.cooked : undefined
|
||||
if (typeof value !== 'string') throw new Error(`Unresolved class: ${name}.${key}`)
|
||||
return [key, value]
|
||||
}))
|
||||
}
|
||||
let body = `import * as React from 'react';\nimport * as JSX from 'react/jsx-runtime';\nconst FD=JSX,mI=React,Uvt=JSX,qF=JSX,nI=React,rI=JSX,YF=React,XF=JSX,JF=React,p_t=JSX,ivt='1200px',avt=4,xMe=8;\n`
|
||||
body += `const Vvt=${JSON.stringify(classMaps.Vvt)},KF=${JSON.stringify(classMaps.KF)},Wvt=300;\n`
|
||||
body += `const tI=${JSON.stringify(classMaps.tI)};\nfunction useExpertI18n(){return (key,values)=>({searchPlaceholder:'搜索专家',clearSearch:'清除搜索',summonShort:'召唤',summonExpertWithName:'查看专家 '+(values?.name??''),summonTeamWithName:'查看专家团 '+(values?.name??''),'card.paidTag':'付费'}[key]??key)}\n`
|
||||
body += `function useDebouncedValue$1(value,delay){const [v,set]=React.useState(value);React.useEffect(()=>{const t=setTimeout(()=>set(value),delay);return()=>clearTimeout(t)},[value,delay]);return v}\n`
|
||||
body += `let SearchPrimitive;export function setExpertSearchPrimitive(value){SearchPrimitive=value}\nconst ka={Search:props=>JSX.jsx(SearchPrimitive,props)};\n`
|
||||
body += `const _o=({content,children})=>React.cloneElement(children,{title:content});\nconst Ao=({iconUrl,fallback,size})=>iconUrl?JSX.jsx('img',{src:iconUrl,width:size,height:size,alt:''}):fallback;\nconst ID=20;\n`
|
||||
const headerBody = recover('UnifiedMarketHeader')
|
||||
const marketIcon = recover('MarketTabIcon')
|
||||
const searchBody = recover('ExpertSearchBar')
|
||||
const emptyBody = recover('ExpertListEmptyState')
|
||||
const capIcon = recover('GraduationCapIcon')
|
||||
const cardBody = recover('ExpertCard$1')
|
||||
const pure = ['formatExpertUseCount', 'formatExpertVersion', 'resolveExpertCardTitle', 'resolveExpertCardAuthorName',
|
||||
'textMatchesKeyword', 'getStrictLocaleSearchText', 'filterExpertsByKeyword', 'getLocalizedText$1', 'getLocalizedTag',
|
||||
'getLocalizedTagTexts', 'getAvatarFallbackBackground', 'resolveExpertPaidFlags', 'buildExpertCardVisibleTags',
|
||||
'getImportSecurityRiskText', 'useDeferredImageLoad', 'useRevealOnLoad', 'getFetchPriorityProps', 'ExpertCardAvatar'].map(name => recover(name, 'function')).join('\n')
|
||||
const highlight = ['escapeRegExp$1', 'highlightText'].map(name => recover(name, 'function')).join('\n')
|
||||
index(icons.text)
|
||||
for (const [name, react, jsx] of [['Fu', 'Nu', 'Pu'], ['GF', 'UF', 'WF'], ['xl', 'yl', 'bl'], ['EM', 'wM', 'TM'], ['Bn', 'Rn', 'zn'], ['Ym', 'qm', 'Jm']]) {
|
||||
body += `const ${react}=React,${jsx}=JSX;\n${recover(name)}\n`
|
||||
}
|
||||
body += `const useLocale$1=()=> 'zh';const al=()=>({reporter:{reportEvent:()=>{},Events:{}}});\nconst resolveExpertListAvatarUrl=value=>value;\nconst ImportSecurityRiskIcon=()=>null,ExpertCloudSyncBadge=()=>null;\n`
|
||||
body += `function useVisibleMarketTabs(){return [{key:'experts',label:'专家',tooltip:'发现和管理专家',Icon:Fu},{key:'skills',label:'技能',tooltip:'发现和管理技能',Icon:GF},{key:'connectors',label:'连接器',tooltip:'连接工具与服务',Icon:xl}]}\n`
|
||||
body += `${marketIcon}\n${headerBody}\n${searchBody}\n${emptyBody}\n${capIcon}\n${pure}\n${highlight}\n${cardBody}\nexport {UnifiedMarketHeader,ExpertSearchBar,ExpertListEmptyState,ExpertCard$1 as ExpertCard,Fu as ExpertIcon,GF as SkillIcon,xl as ConnectorIcon,EM as BackIcon,Bn as AddIcon,Ym as MyExpertIcon,filterExpertsByKeyword,formatExpertUseCount,formatExpertVersion};\n`
|
||||
|
||||
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json', 'utf8'))
|
||||
function cssSource(name) {
|
||||
const file = cssManifest.files.find(file => file.file === name)
|
||||
const text = fs.readFileSync(`.tmp/rea-css-recovered/${name}`, 'utf8')
|
||||
if (!file || hash(text) !== file.sha256) throw new Error(`REA CSS mismatch: ${name}`)
|
||||
return { text, file }
|
||||
}
|
||||
const contextsCss = cssSource('contexts-BkjwYSZT.css')
|
||||
manifest.source.push({ file: contextsCss.file.file, sha256: contextsCss.file.sha256, evidenceId: cssManifest.sourceEvidenceId })
|
||||
const tree = postcss.parse(contextsCss.text)
|
||||
const endRule = tree.nodes.find(n => n.type === 'rule' && n.selector.includes('._loadMoreSentinel_1yu3o_194'))
|
||||
if (!endRule) throw new Error('Expert CSS boundary missing')
|
||||
const end = endRule.source.end.offset
|
||||
function scoped(node) {
|
||||
const clone = node.clone()
|
||||
if (clone.type === 'rule') clone.selector = clone.selector.split(',').map(selector => `:where(.yc-capability-center) ${selector.trim().replace(/:root/g, '.expert-center-light')}`).join(',')
|
||||
else if (clone.type === 'atrule' && !/keyframes$/.test(clone.name)) clone.nodes?.forEach((child, index) => { clone.nodes[index] = scoped(child); clone.nodes[index].parent = clone })
|
||||
return clone
|
||||
}
|
||||
const common = [], expert = []
|
||||
const tokenCss = cssSource('tokens-Df-i-zsO.css')
|
||||
manifest.source.push({ file: tokenCss.file.file, sha256: tokenCss.file.sha256 })
|
||||
for (const node of postcss.parse(tokenCss.text).nodes) {
|
||||
if (node.type !== 'rule' || !node.nodes.every(n => n.type === 'decl' && n.prop.startsWith('--'))) continue
|
||||
let selector
|
||||
if (node.selector.includes(':root')) selector = '.yc-capability-center'
|
||||
else if (node.selector.startsWith('body[data-electron-desktop=true]')) selector = node.selector.replace('body', '.yc-capability-center')
|
||||
else if (node.selector.includes('[data-theme=dark]')) selector = '.yc-capability-center .expert-center-dark'
|
||||
if (!selector) continue
|
||||
const clone = node.clone({ selector })
|
||||
common.push(clone.toString())
|
||||
manifest.css.push({ file: tokenCss.file.file, offset: node.source.start.offset, sha256: hash(node.toString()), selector: node.selector })
|
||||
}
|
||||
const primitiveCss = cssSource('src-CBaV_A3S.css')
|
||||
manifest.source.push({ file: primitiveCss.file.file, sha256: primitiveCss.file.sha256 })
|
||||
for (const node of postcss.parse(primitiveCss.text).nodes) {
|
||||
if (node.type === 'rule' && /^\.wb-(input|button|segmented)(?:[\s.:#[_-]|$)/.test(node.selector)) common.push(scoped(node).toString())
|
||||
}
|
||||
const segmentedCss = cssSource('src-Bh3YZ35H.css')
|
||||
manifest.source.push({ file: segmentedCss.file.file, sha256: segmentedCss.file.sha256, evidenceId: cssManifest.sourceEvidenceId })
|
||||
for (const node of postcss.parse(segmentedCss.text).nodes) {
|
||||
if ((node.type === 'rule' && /^\.wb-(segmented|input|button)(?:[\s.:#[_-]|$)/.test(node.selector))
|
||||
|| (node.type === 'atrule' && /\.wb-(segmented|input|button)/.test(node.toString()))) {
|
||||
common.push(scoped(node).toString())
|
||||
manifest.css.push({ file: segmentedCss.file.file, offset: node.source.start.offset, sha256: hash(node.toString()), selector: node.selector })
|
||||
}
|
||||
}
|
||||
for (const node of tree.nodes) {
|
||||
const offset = node.source.start.offset
|
||||
const text = node.toString()
|
||||
if ((node.type === 'rule' || node.type === 'atrule') && /(?:\.um-header|\.um-tab|_1yu3o_)/.test(text)) common.push(scoped(node).toString())
|
||||
else if (offset >= 673321 && offset <= end) expert.push(scoped(node).toString())
|
||||
else if (node.type === 'rule' && /^\.wb-(input|button|segmented)(?:[\s.:#[_-]|$)/.test(node.selector)) common.push(scoped(node).toString())
|
||||
if ((offset >= 673321 && offset <= end) || (node.type === 'rule' && /\.um-(?:header|tab)/.test(node.selector))) {
|
||||
manifest.css.push({ offset, sha256: hash(text), type: node.type, selector: node.selector, name: node.name })
|
||||
}
|
||||
}
|
||||
common.push(`/* YCode host sizing; the source Electron token supplies the 64px topbar. */
|
||||
.yc-capability-center{height:100%;width:100%;min-height:0;min-width:0;display:flex;font-family:var(--wb-font-family);font-size:13px;isolation:isolate}
|
||||
.yc-capability-center>.expert-center-page{flex:1;min-width:0}
|
||||
.yc-capability-center .capability-page-host{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;overflow:hidden}
|
||||
.yc-capability-center .capability-title-slot{display:contents}
|
||||
.yc-capability-center .wb-icon{flex:none;display:block}
|
||||
.yc-capability-center .capability-icon-button{border:0;background:transparent;color:var(--wb-color-text-secondary);display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:6px;border-radius:6px}
|
||||
.yc-capability-center .capability-text-button{border:0;background:transparent;color:var(--wb-color-text-secondary);font-size:13px;cursor:pointer;padding:4px 8px;border-radius:6px}
|
||||
.yc-capability-center .capability-text-button:hover{background:var(--wb-color-bg-primary-hover)}
|
||||
@media(max-width:700px){.yc-capability-center .ec-topbar{flex-wrap:wrap;height:auto;min-height:64px;gap:8px;padding-top:12px;padding-bottom:12px}.yc-capability-center .ec-topbar__right{max-width:100%;flex-wrap:wrap}.yc-capability-center .um-header__tabs{gap:16px}.yc-capability-center .um-tab{font-size:16px}.yc-capability-center ._root_mbn4e_1{width:140px}}
|
||||
`)
|
||||
expert.push(`/* Explicit offline-service adapters; source cards and their body/CSS remain recovered above. */
|
||||
.yc-capability-center .capability-expert-body{flex:1;min-height:0;display:flex;flex-direction:column}
|
||||
.yc-capability-center .capability-market-panel{display:contents}
|
||||
.yc-capability-center .capability-expert-empty{flex:1;min-height:280px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px}
|
||||
.yc-capability-center .capability-service-note{font-size:13px;line-height:20px;color:var(--wb-color-text-tertiary);margin:0}
|
||||
.yc-capability-center .capability-catalog-status{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:8px 0 16px;flex-wrap:wrap}
|
||||
.yc-capability-center .ec-my-created-empty-title{margin:0}
|
||||
.capability-detail-host{position:fixed;inset:0;z-index:1000}
|
||||
`)
|
||||
const generated = [
|
||||
['assets/source-ui.mjs', body],
|
||||
['assets/source-ui.d.mts', `import type {ComponentType,SVGProps} from 'react';\nexport const UnifiedMarketHeader:ComponentType<{activeTab:'experts'|'skills'|'connectors';onTabChange(tab:'experts'|'skills'|'connectors'):void}>;\nexport const ExpertSearchBar:ComponentType<{value:string;onChange(value:string):void;placeholder?:string;variant?:string}>;\nexport const ExpertListEmptyState:ComponentType<{message:string;showIcon?:boolean}>;\nexport const ExpertCard:ComponentType<{expert:Record<string,unknown>;avatarUrl?:string;cardPosition:number;onSummon():void;onExpertClick(expert:Record<string,unknown>):void;appearance?:string;highlightKeyword?:string;hideTopSummonButton?:boolean;priorityAvatarLoad?:boolean;isFirst?:boolean}>;\nexport const ExpertIcon:ComponentType<SVGProps<SVGSVGElement>>;export const SkillIcon:typeof ExpertIcon;export const ConnectorIcon:typeof ExpertIcon;\nexport function setExpertSearchPrimitive(value:ComponentType<any>):void;\nexport function filterExpertsByKeyword<T>(items:T[],keyword:string,locale:string):T[];\nexport function formatExpertUseCount(value:number,locale:string):string;\nexport function formatExpertVersion(value:string):string;\n`],
|
||||
['assets/source-classes.json', JSON.stringify(classMaps, null, 2)+'\n'],
|
||||
['capability-center-source.css', '/* REA recovered declarations; generated by recover-capability-center.mjs. */\n'+common.join('\n')+'\n'],
|
||||
['expert-source.css', '/* REA recovered declarations; generated by recover-capability-center.mjs. */\n'+expert.join('\n')+'\n']
|
||||
]
|
||||
generated[1][1] += 'export const BackIcon:typeof ExpertIcon;export const AddIcon:typeof ExpertIcon;export const MyExpertIcon:typeof ExpertIcon;\n'
|
||||
const emptyEvidence = JSON.parse(fs.readFileSync('.tmp/rea-expert-empty-asset-evidence-20261016.json', 'utf8'))
|
||||
if (emptyEvidence.normalized_result.root_sha256 !== '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918') throw new Error('Empty icon root pin mismatch')
|
||||
const emptyFile = emptyEvidence.normalized_result.files[0]
|
||||
const emptySvg = Buffer.from(emptyFile.data_base64, 'base64')
|
||||
if (hash(emptySvg) !== emptyFile.sha256) throw new Error('Empty expert icon SHA mismatch')
|
||||
generated.push(['assets/empty-created-expert.svg', emptySvg.toString('utf8')])
|
||||
manifest.source.push({ file: emptyFile.path, sha256: emptyFile.sha256, evidenceId: emptyEvidence.evidence_id })
|
||||
manifest.outputs = generated.map(([file, text]) => ({ file, sha256: hash(text), bytes: Buffer.byteLength(text) }))
|
||||
generated.push(['assets/recovery-manifest.json', JSON.stringify(manifest, null, 2)+'\n'])
|
||||
for (const [file, text] of generated) {
|
||||
const target = path.join(out, file)
|
||||
if (check) { if (!fs.existsSync(target) || fs.readFileSync(target, 'utf8') !== text) throw new Error(`Recovery drift: ${target}`) }
|
||||
else { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, text) }
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'PASS', check, bodies: manifest.bodies.length, cssRules: manifest.css.length, outputs: generated.length }))
|
||||
@@ -0,0 +1,104 @@
|
||||
// Only read pinned REA exports. Do not read the original installation or execute source modules.
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const output = 'src/renderer/src/components'
|
||||
const evidence = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
if (evidence.evidence_id !== 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb') throw Error('REA script Evidence changed')
|
||||
const specs = [
|
||||
['src-DPDj7_qD.js', '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0',
|
||||
{ I_: 'SourceHeaderSearchIcon', Mv: 'SourceHeaderPanelIcon', xx: 'SourceHeaderPromptIcon' }],
|
||||
['src-0FGg1seX.js', 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178',
|
||||
{ ShareTaskIcon: 'SourceHeaderShareIcon', 'EditIcon$2': 'SourceHeaderEditIcon' }],
|
||||
['contexts-BMeWSUHk.js', '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b',
|
||||
{ sWe: 'SourceHeaderSearchUpIcon', cWe: 'SourceHeaderSearchDownIcon', lWe: 'SourceHeaderSearchCloseIcon' }],
|
||||
]
|
||||
const definitions = []
|
||||
const manifest = { scriptEvidenceId: evidence.evidence_id, icons: [], css: [] }
|
||||
for (const [file, sha, names] of specs) {
|
||||
const text = evidence.items.find(item => item.file_path.endsWith(file))?.source?.artifact?.text
|
||||
if (!text || createHash('sha256').update(text).digest('hex') !== sha) throw Error(`Source changed: ${file}`)
|
||||
const found = new Map()
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'AssignmentExpression' && Object.hasOwn(names, node.left.name)) found.set(node.left.name, node.right)
|
||||
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(visit); else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
visit(parse(text, { sourceType: 'module' }))
|
||||
for (const [name, exported] of Object.entries(names)) {
|
||||
const node = found.get(name)
|
||||
if (!node) throw Error(`Missing ${file}:${name}`)
|
||||
let value = text.slice(node.start, node.end)
|
||||
.replace(/\(0,[A-Za-z0-9_$]+\.(jsx|jsxs|forwardRef|useId)\)/g, ' $1')
|
||||
if (!/\b(?:jsx|jsxs)\(`svg`/.test(value)) throw Error(`Not a pure SVG: ${name}`)
|
||||
if (['sWe', 'cWe', 'lWe'].includes(name)) value = `()=>${value}`
|
||||
definitions.push(`export const ${exported} = ${value}`)
|
||||
manifest.icons.push({ file, sha256: sha, sourceBinding: name, exported, offset: node.start })
|
||||
}
|
||||
}
|
||||
await fs.writeFile(`${output}/SourceChatHeaderIcons.tsx`, `// @ts-nocheck\n// Pure SVG definitions recovered from pinned REA ${evidence.evidence_id}; no original module initialization.\nimport { jsx, jsxs } from 'react/jsx-runtime'\nimport { forwardRef, useId } from 'react'\n${definitions.join('\n')}\n`)
|
||||
const cssSpecs = [
|
||||
['src-Bh3YZ35H.css', '88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a', /\.wb-button(?:\b|__|--)/],
|
||||
['contexts-BkjwYSZT.css', '50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380', /workbuddy-topbar|workbuddy-prompt-list|cb-chat-search/],
|
||||
['src-CBaV_A3S.css', '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c', /cb-chat-search/],
|
||||
]
|
||||
const selected = postcss.root()
|
||||
let count = 0
|
||||
function scope(selector) {
|
||||
if (/\.cb-chat-search-hit/.test(selector)) return selector
|
||||
if (/\.workbuddy-topbar(?=[\s.:#\[>,+~]|$)/.test(selector)) return selector.replace(/\.workbuddy-topbar(?=[\s.:#\[>,+~]|$)/g, '.source-chat-header.workbuddy-topbar')
|
||||
if (/\.workbuddy-topbar--[\w-]+/.test(selector)) return selector.replace(/\.workbuddy-topbar--[\w-]+/, match => '.source-chat-header' + match)
|
||||
return `.source-chat-header ${selector}`
|
||||
}
|
||||
function select(node, pattern) {
|
||||
if (node.type === 'rule') {
|
||||
if (!pattern.test(node.selector)) return null
|
||||
const copy = node.clone(); copy.selectors = copy.selectors.map(scope); count++; return copy
|
||||
}
|
||||
if (node.type !== 'atrule' || /keyframes/.test(node.name)) return null
|
||||
const copy = node.clone({ nodes: [] })
|
||||
for (const child of node.nodes ?? []) { const match = select(child, pattern); if (match) copy.append(match) }
|
||||
return copy.nodes.length ? copy : null
|
||||
}
|
||||
for (const [file, sha, pattern] of cssSpecs) {
|
||||
const text = await fs.readFile(`.tmp/rea-css-recovered/${file}`, 'utf8')
|
||||
if (createHash('sha256').update(text).digest('hex') !== sha) throw Error(`CSS changed: ${file}`)
|
||||
const before = count
|
||||
for (const node of postcss.parse(text).nodes) { const match = select(node, pattern); if (match) selected.append(match) }
|
||||
manifest.css.push({ file, sha256: sha, rules: count - before })
|
||||
}
|
||||
// Include the exact source token closure for an independently renderable header.
|
||||
const tokenText = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8')
|
||||
if (createHash('sha256').update(tokenText).digest('hex') !== '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696') throw Error('Source tokens changed')
|
||||
const tokenRules = []; postcss.parse(tokenText).walkRules(rule => tokenRules.push(rule))
|
||||
const needed = new Set()
|
||||
const collect = text => { for (const match of text.matchAll(/var\((--[\w-]+)/g)) needed.add(match[1]) }
|
||||
collect(selected.toString())
|
||||
for (let pass = 0; pass < 10; pass++) for (const rule of tokenRules) rule.walkDecls(d => { if (needed.has(d.prop)) collect(d.value) })
|
||||
const light = new Map(), dark = new Map(), desktop = new Map()
|
||||
for (const rule of tokenRules) {
|
||||
const map = rule.selector.includes('dark') ? dark : rule.selector.includes(':root') ? light : null
|
||||
if (map) rule.walkDecls(d => { if (needed.has(d.prop)) map.set(d.prop, d.value) })
|
||||
if (rule.selector.includes('body[data-electron-desktop=true]')) rule.walkDecls(d => { if (needed.has(d.prop)) desktop.set(d.prop, d.value) })
|
||||
}
|
||||
const serialize = map => [...map].map(([name, value]) => `${name}:${value}`).join(';')
|
||||
await fs.writeFile(`${output}/source-chat-header-recovered.css`, `/* REA CSS ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94. Original rule bodies/order/at-rule conditions; selectors scoped to the independent desktop header, except original chat hit markers. Desktop token overrides are copied from body[data-electron-desktop=true]. */\n.source-chat-header{${serialize(new Map([...light, ...desktop]))}}\n:root[data-theme=dark] .source-chat-header,[data-cr-theme=dark] .source-chat-header{${serialize(dark)}}\n${selected.toString()}\n`)
|
||||
const labels = {}
|
||||
const zh = evidence.items.find(item => item.file_path.endsWith('zh-cn-Dfzn8myY.js'))?.source?.artifact?.text
|
||||
function labelsVisit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'ObjectProperty') {
|
||||
const key = node.key.value
|
||||
if (typeof key === 'string' && /^(chat\.search\.|topbar\.userPromptList|panel\.(?:hideProductPanel|showProductPanel)|contextMenu\.shareTask$|common\.(?:loadingMore|showMore)$)/.test(key)) {
|
||||
if (node.value.type === 'TemplateLiteral' && !node.value.expressions.length) labels[key] = node.value.quasis[0].value.cooked
|
||||
else if (node.value.type === 'StringLiteral') labels[key] = node.value.value
|
||||
}
|
||||
}
|
||||
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(labelsVisit); else if (value && typeof value === 'object') labelsVisit(value)
|
||||
}
|
||||
labelsVisit(parse(zh, { sourceType: 'module' }))
|
||||
await fs.writeFile(`${output}/source-chat-header-reference.ts`, `// Recovered labels from REA ${evidence.evidence_id}.\nexport const sourceChatHeaderLabels: Record<string,string> = ${JSON.stringify(labels, null, 2)}\n`)
|
||||
await fs.mkdir('.tmp/chat-header-reference', { recursive: true })
|
||||
await fs.writeFile('.tmp/chat-header-reference/manifest.json', JSON.stringify({ ...manifest, rules: count }, null, 2))
|
||||
console.log(JSON.stringify({ icons: definitions.length, rules: count, labels: Object.keys(labels).length }))
|
||||
@@ -0,0 +1,67 @@
|
||||
// Derive readable reference slices from a previously exported REA record only.
|
||||
// Never opens WorkBuddy's package or connects to its runtime; never executes its code.
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import generatePackage from '@babel/generator'
|
||||
|
||||
const generate = generatePackage.default ?? generatePackage
|
||||
const bundle = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const script = bundle.items.find((item) => item.file_path?.endsWith('src-DbE1_mgM.js'))
|
||||
if (!script?.source?.included || !script.source.artifact?.text) throw new Error('Missing REA-captured source')
|
||||
const source = script.source.artifact.text
|
||||
const digest = crypto.createHash('sha256').update(source).digest('hex')
|
||||
if (digest !== script.source.artifact.sha256) throw new Error('REA source digest mismatch')
|
||||
const ast = parse(source, { sourceType: 'module' })
|
||||
const output = path.resolve('.tmp/rea-chat-recovered')
|
||||
await fs.mkdir(output, { recursive: true })
|
||||
await fs.mkdir(path.join(output, 'definitions'), { recursive: true })
|
||||
const ranges = [[624800, 660500], [716400, 798700], [804500, 825000], [979000, 995000]]
|
||||
const declarations = []
|
||||
const definitions = []
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'AssignmentExpression' && node.operator === '=' && node.left.type === 'Identifier' &&
|
||||
ranges.some(([start, end]) => node.start >= start && node.end <= end) &&
|
||||
['ArrowFunctionExpression', 'FunctionExpression', 'ObjectExpression', 'ArrayExpression', 'NewExpression', 'RegExpLiteral'].includes(node.right.type)) {
|
||||
const name = node.left.name
|
||||
const filename = `definitions/${name}.js`
|
||||
awaitWrites.push(fs.writeFile(path.join(output, filename), `const ${name} = ${generate(node.right, { compact: false }).code};\n`))
|
||||
definitions.push({ name, start: node.start, end: node.end, filename })
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object' && value.type) visit(value)
|
||||
}
|
||||
}
|
||||
const awaitWrites = []
|
||||
visit(ast.program)
|
||||
await Promise.all(awaitWrites)
|
||||
for (const statement of ast.program.body) {
|
||||
const nodes = statement.type === 'VariableDeclaration' ? statement.declarations : [statement]
|
||||
for (const node of nodes) {
|
||||
if (!ranges.some(([start, end]) => node.start < end && node.end > start)) continue
|
||||
// Skip bundled library wrappers crossing a whole region; report them as boundaries.
|
||||
if (node.end - node.start > 60000) {
|
||||
declarations.push({ name: node.id?.name, start: node.start, end: node.end, skipped: 'large-library-wrapper' })
|
||||
continue
|
||||
}
|
||||
const name = node.id?.name ?? `${node.type}-${node.start}`
|
||||
const declaration = statement.type === 'VariableDeclaration'
|
||||
? { ...statement, declarations: [node] }
|
||||
: node
|
||||
const text = generate(declaration, { comments: true, compact: false }).code
|
||||
const filename = `${node.start}-${name}.js`
|
||||
await fs.writeFile(path.join(output, filename), text + '\n')
|
||||
declarations.push({ name, start: node.start, end: node.end, filename })
|
||||
}
|
||||
}
|
||||
const manifest = {
|
||||
evidenceId: bundle.evidence_id, sourcePath: script.file_path, sourceSha256: digest,
|
||||
ranges: ranges.map(([start, end]) => ({ start, end, units: 'UTF-16', endExclusive: true })),
|
||||
method: 'Babel parse/generate, split top-level declarations; reference only, not executed',
|
||||
declarations, definitions
|
||||
}
|
||||
await fs.writeFile(path.join(output, 'manifest.json'), JSON.stringify(manifest, null, 2))
|
||||
console.log(JSON.stringify({ evidenceId: manifest.evidenceId, sourceSha256: digest, declarations: declarations.length, definitions: definitions.length, manifest: path.join(output, 'manifest.json') }))
|
||||
@@ -0,0 +1,94 @@
|
||||
/** Recovery from fixed retained REA only; no original filesystem or runtime access. */
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import {parse} from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const sha=value=>crypto.createHash('sha256').update(value).digest('hex'),archive=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8')),cssManifest=JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json','utf8'))
|
||||
const manifest={version:'5.8.2',rootSha256:'035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',evidence:[archive.evidence_id,cssManifest.sourceEvidenceId],sources:[],bodies:[],adaptations:['Source AddMenuImpl parent, Popover, SubMenu, menu items, SVGs and tooltip bodies recovered with React/JSX singleton bindings.','Original provider-specific submenu content is replaced at one explicit boundary by typed host slots; entries/actions/availability are host data. No unavailable provider or service is simulated.','Host open state supports controlled open/onOpenChange; scopeKey unmount isolates owner. Source parent opening/closing and submenu state remain in the recovered body.','Default trigger retains source WBButton body and adds wb-composer__add-button plus aria-label 添加 for existing YCode automation.','MenuItem adds only data-add-menu-id and optional disabled metadata. Parent keyboard traversal/focus-first is intentionally absent as in current cr source; SubMenu arrow/Enter/touch/delays retain original source.','FloatingUI uses the existing pinned YCode host dependency; bundled source version is unknown. Portals/CSS are scoped; light theme selectors also map YCode data-theme.']}
|
||||
function index(name,pin){const source=archive.items.find(item=>item.file_path.endsWith(name))?.source.artifact;if(!source||source.sha256!==pin||sha(source.text)!==pin)throw Error('REA source identity mismatch '+name);manifest.sources.push({file:name,sha256:pin});const nodes=new Map(),ast=parse(source.text,{sourceType:'module'});function visit(node){if(!node||typeof node!=='object')return;if(node.type==='FunctionDeclaration')nodes.set(node.id.name,node);if(node.type==='AssignmentExpression'&&node.left.type==='Identifier')nodes.set(node.left.name,node);for(const[key,value]of Object.entries(node)){if(key==='loc'||key==='comments')continue;if(Array.isArray(value))value.forEach(visit);else if(value&&typeof value==='object')visit(value)}}visit(ast.program);return{...source,nodes,ast}}
|
||||
const core=index('src-DbE1_mgM.js','6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4'),button=index('src-DPDj7_qD.js','747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0')
|
||||
function body(source,name){const node=source.nodes.get(name);if(!node)throw Error('Missing body '+name);const original=source.text.slice(node.start,node.end);manifest.bodies.push({file:manifest.sources.find(item=>item.sha256===source.sha256).file,name,start:node.start,end:node.end,sha256:sha(original)});return(node.type==='AssignmentExpression'?'const ':'')+original.replace(/\(0,[\w$]+\.(jsx|jsxs)\)/g,' JSX.$1')+';'}
|
||||
const labels={};function visitLabels(node){if(!node||typeof node!=='object')return;if(node.type==='ObjectProperty'&&/^(?:chatInput\.(?:addMenu\.|attachment\.|mode\.|triggerSearch\.loading)|common\.(?:search|clearSearch)$)/.test(node.key.value??'')){const value=node.value.value??node.value.quasis?.[0]?.value.cooked;if(typeof value==='string'&&/[\u3400-\u9fff]/.test(value)&&!labels[node.key.value])labels[node.key.value]=value}for(const[key,value]of Object.entries(node)){if(key==='loc'||key==='comments')continue;if(Array.isArray(value))value.forEach(visitLabels);else if(value&&typeof value==='object')visitLabels(value)}}visitLabels(core.ast.program);if(!labels['chatInput.addMenu.title']||!labels['chatInput.mode.plan.label'])throw Error('Missing source labels')
|
||||
let js=`import * as React from 'react';import * as JSX from 'react/jsx-runtime';import{useFloating,offset as D,flip as E,shift as O,size as b,arrow as k,autoUpdate as A,useInteractions,useDismiss,useHover,useTransitionStyles,FloatingPortal as NativeFloatingPortal}from'@floating-ui/react';
|
||||
import{useInputProvider,useInputBoxStore,useInputBoxSelector,useAddMenuItems,useRenderHostSubmenu,useHostOpenState,useCrConfig,useRenderIcon}from'./runtime';import{SourceAddMenuConfigContext}from'./context';
|
||||
const pU=React,Gi=React,Vi=React,la=React,Ei=React,mi=React,ci=React,Gt=React,Di=JSX,ua=JSX,pUFragment=React.Fragment,io={1:12,2:16},li=9990,ui=10,di='default',fi='__ycodeComposerAddMenuLayers__',gi=3,_i=8,yi=4,bi=1000,xi=.1,Si='data-pointer-press-transparent',wi=new Set(['SVG','IMG','VIDEO','CANVAS','AUDIO']),Oi=new Map(),qi=React.createContext(null),$i=React.createContext(true),pa=React.createContext(null),yt=SourceAddMenuConfigContext,da='textarea,input:not([type]),input[type=text],input[type=search],input[type=email],input[type=url],input[type=tel],input[type=password],input[type=number],[contenteditable=true],[contenteditable=plaintext-only]',fa='[data-cr-sub-menu-overlay=true],[data-cr-submenu-overlay=true]',zi='[data-cr-submenu-overlay=true],.cr-sub-menu__overlay',Ui='[data-floating-ui-inert]';let Ci=null;
|
||||
const usePrefixCls=()=>name=>'cr-'+name,useT=()=>key=>(${JSON.stringify(labels)})[key]??key,useRenderDiag=()=>{},FloatingPortal=({children,root})=>JSX.jsx(NativeFloatingPortal,{root,children:JSX.jsx('div',{className:'yc-source-composer-add-menu','data-cr-theme':children?.props?.['data-cr-theme'],children})}),floatingAutoUpdateWithAnimationFrame=(n,a,c)=>A(n,a,c,{animationFrame:true});
|
||||
`
|
||||
// Tooltip is the same cr implementation used by this AddMenu, not the unrelated legacy tooltip.
|
||||
for(const name of ['r$1','clsx','getFloatingLayerState','getGroupStack','nextZIndex','useFloatingLayer','TooltipLayer','isPressTransparent','isHoverSuppressedByPress','suppressHoverUntilPointerMove','isElementTruncated','isSubtreeTruncated','normalizeVisibleText','collectVisibleText','hasVisibleTriggerText','hasTruncationStyle','shouldGateOnTruncation','isEmptyContent','resolveTooltipThemeMode','isVisibleFocus','Tooltip$1','resolvePopoverWhileElementsMounted','getNearestStackingZIndex','PopoverLayer','usePopoverContext','Popover','PopoverTrigger','PopoverContent','Menu','MenuItem','SubMenu','MenuDivider','ChevronRightIcon','AddFilesIcon','ReferenceFilesIcon','ModeIcon','ExpertsIcon','SkillsIcon','ConnectorsIcon','PlusIcon'])js+=body(core,name)+'\n'
|
||||
js+=body(button,'ButtonLoadingIcon')+'\n'+body(button,'qt')+'\nconst fe=qt;\n'
|
||||
let parent=body(core,'AddMenuImpl')
|
||||
const slotsStart=parent.indexOf('return w=a&&_?'),slotsEnd=parent.indexOf(',(0,mU.jsxs)(pU.Fragment',slotsStart)
|
||||
// JSX aliases have already been normalized by body().
|
||||
const normalizedEnd=parent.indexOf(', JSX.jsxs(pU.Fragment',slotsStart)
|
||||
const boundaryEnd=normalizedEnd>=0?normalizedEnd:slotsEnd
|
||||
if(slotsStart<0||boundaryEnd<slotsStart)throw Error('Provider submenu source boundary changed')
|
||||
parent=parent.slice(0,slotsStart)+'return w=renderHostSubmenu(n.id,{closeMenu:()=>k(!1)},d,handleSubmenuItemClick)'+parent.slice(boundaryEnd)
|
||||
if(!parent.includes('[C,w]=(0,pU.useState)(!1)'))throw Error('Open state source boundary changed')
|
||||
parent=parent.replace('[C,w]=(0,pU.useState)(!1)','[C,w]=useHostOpenState()')
|
||||
parent=parent.replace('let f=usePrefixCls(),p=useT()','let renderHostSubmenu=useRenderHostSubmenu(),f=usePrefixCls(),p=useT()')
|
||||
parent=parent.replace('className:clsx(f(`add-menu__trigger`),C&&f(`add-menu__trigger--open`))','className:clsx(f(`add-menu__trigger`),C&&f(`add-menu__trigger--open`),`wb-composer__add-button`)').replace('"aria-label":p(`chatInput.addMenu.title`)','"aria-label":`添加`')
|
||||
const row='icon:n.icon,onClick:()=>handleItemClick(n),children:n.label'
|
||||
if(parent.split(row).length!==2)throw Error('Parent row metadata boundary changed')
|
||||
parent=parent.replaceAll(row,'icon:n.icon,disabled:n.disabled,itemId:n.id,onClick:()=>handleItemClick(n),children:n.label')
|
||||
parent=parent.replace('icon:n.icon,extra: JSX.jsx(ChevronRightIcon','icon:n.icon,disabled:n.disabled,itemId:n.id,extra: JSX.jsx(ChevronRightIcon')
|
||||
parent=parent.replace('open:T===n.id,openDelay:0','open:!n.disabled&&T===n.id,openDelay:0').replace('onOpenChange:a=>{a&&T!==n.id','onOpenChange:a=>{if(n.disabled)return;a&&T!==n.id')
|
||||
parent=parent.replace('icon:n.icon,onClick:()=>handleSubmenuItemClick(n.id),children:n.label','icon:n.icon,disabled:n.disabled,itemId:n.id,onClick:()=>handleSubmenuItemClick(n.id),children:n.label')
|
||||
// MenuItem body gets only host metadata and keyboard reachability; source classes/SVG structure stay unchanged.
|
||||
js=js.replace('onClick:f,className:p,style:m})','onClick:f,className:p,style:m,itemId})').replace('"aria-disabled":u,tabIndex:-1,onClick:u?void 0:f','"aria-disabled":u,"data-add-menu-id":itemId,tabIndex:-1,onClick:u?void 0:f')
|
||||
js+='const dU={FILES:"add-files",REFERENCE_FILES:"reference-files",MODE:"mode",EXPERT:"experts",SKILL:"skills",CONNECTOR:"connectors"};\n'+parent+'\nexport{AddMenuImpl,Menu,MenuItem,MenuDivider,AddFilesIcon,ReferenceFilesIcon,ModeIcon,ExpertsIcon,SkillsIcon,ConnectorsIcon,PlusIcon};\n'
|
||||
js+=`const Ea=React,RH=React,CH=React,bH=React,ka=JSX,UH=JSX,wH=JSX,P_=React,HP=React,dL=React,hL=React,hH=React,_H=React,_n=React,yn={sm:14,md:16,lg:20,xl:24},useI18n=()=>({t:useT()}),getA11yLabel=()=>(${JSON.stringify(labels)})['common.clearSearch'],renderProviderIcon=icon=>icon,TH='cr-add-menu-connector-row';\n`
|
||||
for(const name of ['UploadIcon','LoadingSpinnerIcon','JumpIcon','ConnectLinkIcon','HighlightText','matchesKeyword','itemKey','isItemConfigured','isInputConnectorOn','isInputConnectorRepairing','useConnectorSubmenuSearch','ConnectorIcon','ConnectorSuffix','Toggle','ConnectorsSubmenu','resolveModeOptions','mH','hH','ModeSubmenu','ModeToggleRow','ModeToggle','splitFilename','ReferenceFilesSubmenu']){
|
||||
if(name==='hH'){const text=body(core,name).replace('const hH=','const sourceModeOptions=');js+=text+'\n';continue}js+=body(core,name)+'\n'
|
||||
}
|
||||
// The only host file provider currently available is local-file picking. Remote rows are not fabricated.
|
||||
js+='const IH=[{id:"local-files",labelKey:"chatInput.attachment.localFiles",icon:JSX.jsx(UploadIcon,{}),group:"own"}],BH={"local-files":"onPickLocalFiles"};\n'+body(core,'FilesSubmenu')+'\n'
|
||||
js=js.replace('n?.presentation===`add-menu`?hH:mH','n?.presentation===`add-menu`?sourceModeOptions:mH')
|
||||
for(const name of ['resolveSize','createIcon','bn','I_','WP','pL','useControllableInputValue','_L','useSwitchMotion','yH'])js+=body(button,name)+'\n'
|
||||
js+='const GP=createIcon(WP),de=_L,me=createIcon(I_),Te=yH;\n'+body(core,'SearchInput')+'\nexport{FilesSubmenu,ModeSubmenu,ConnectorsSubmenu,ReferenceFilesSubmenu,SearchInput,UploadIcon};\n'
|
||||
// Error-label maps are immutable source data, not service status fabrication.
|
||||
js+=body(core,'EH')+'\n'+body(core,'DH')+'\n'
|
||||
manifest.adaptations.push('FilesSubmenu body is original but its immutable catalog is restricted to the available local-file handler; extraItems accept ReactNode icons. No source remote-library service entries are manufactured. Reference-file icon callback maps to existing recovered WorkbenchFileIcon/ArtifactFolderSolidIcon. Mode setMode is a local projection; actual mutation is the explicit provider.onChange host. Optional connector views keep source searching/state callbacks and do not create or run connectors.')
|
||||
js+=`if(typeof document!=='undefined'){document.addEventListener('pointerdown',n=>{Ci=isPressTransparent(n.target)?null:{x:n.clientX,y:n.clientY,at:Date.now()}},true);document.addEventListener('pointermove',n=>{if(Ci&&(Math.abs(n.clientX-Ci.x)>yi||Math.abs(n.clientY-Ci.y)>yi))Ci=null},true)}\n`
|
||||
let css='/* Fixed REA current cr AddMenu, Menu/SubMenu/Popover, WB trigger, tooltip; scoped selectors. */\n'
|
||||
const scope='.yc-source-composer-add-menu'
|
||||
manifest.tokenSelectorMappings=[]
|
||||
function scopedTokenSelectors(rule){
|
||||
const mapped=[]
|
||||
for(const selector of rule.selectors){
|
||||
if(selector===':root'){mapped.push(scope);continue}
|
||||
if(selector.includes('.cr-theme')){
|
||||
mapped.push(selector.replace(/\.cr-theme\b/g,scope));continue
|
||||
}
|
||||
// Keep each original theme/platform condition. In particular, dark selectors must never become an unconditional scope rule.
|
||||
mapped.push(selector+' '+scope)
|
||||
if(/(?:data-theme=dark|\.cb-dark\b|IDE\\ Night|vscode-dark|\.dark\b)/.test(selector))mapped.push(scope+'[data-cr-theme=dark]')
|
||||
if(/IDE\\ Light/.test(selector))mapped.push(scope+'[data-cr-theme=light]')
|
||||
}
|
||||
// The original cr light alias is a class; map the host's explicit data-theme too, without overriding an explicit dark instance.
|
||||
if(rule.selectors.includes('.light .cr-theme'))mapped.push('[data-theme=light] '+scope+':not([data-cr-theme=dark])')
|
||||
return [...new Set(mapped)].join(',')
|
||||
}
|
||||
function appendScopedRule(rule,originalParent){
|
||||
let node=rule
|
||||
// Preserve all enclosing media/supports conditions as well as the selector itself.
|
||||
for(let parent=originalParent;parent?.type==='atrule';parent=parent.parent){const wrapper=parent.clone({nodes:[]});wrapper.append(node);node=wrapper}
|
||||
css+=node.toString()+'\n'
|
||||
}
|
||||
for(const[file,pin]of Object.entries({'src-CBaV_A3S.css':'2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c','src-CyXHA0IC.css':'a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe','src-Bh3YZ35H.css':'88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a'})){
|
||||
const text=fs.readFileSync('.tmp/rea-css-recovered/'+file,'utf8');if(sha(text)!==pin||cssManifest.files.find(item=>item.file===file)?.sha256!==pin)throw Error('CSS identity mismatch '+file);manifest.sources.push({file,sha256:pin});const tree=postcss.parse(text)
|
||||
tree.walkRules(rule=>{
|
||||
if(file==='src-CBaV_A3S.css'){const declarations=(rule.nodes??[]).filter(node=>node.type==='decl'&&(/^--cr-/.test(node.prop)||/^--wb-/.test(node.prop)));if(!declarations.length)return;const clone=rule.clone({nodes:declarations.map(node=>node.clone())});clone.selector=scopedTokenSelectors(rule);manifest.tokenSelectorMappings.push({file,original:rule.selector,scoped:clone.selector,declarations:declarations.length});appendScopedRule(clone,rule.parent);return}
|
||||
if(!/\.cr-(?:add-menu|files-submenu|reference-files-submenu|mode-submenu|connectors-submenu|search-input|menu\b|menu-|sub-menu|popover\b|tooltip(?:\b|__|--))|\.wb-(?:button|input|switch|icon)(?:\b|__|--)/.test(rule.selector)||/model-selector|address-tooltip/.test(rule.selector))return;const clone=rule.clone();clone.selector=clone.selectors.map(selector=>'.yc-source-composer-add-menu '+selector).join(',');if(rule.parent.type==='atrule'){const parent=rule.parent.clone({nodes:[]});parent.append(clone);css+=parent.toString()+'\n'}else css+=clone.toString()+'\n'
|
||||
});tree.walkAtRules('keyframes',rule=>{if(['cr-sub-menu-pop-in','wb-button-loading-spin','cr-input-spin','wb-switch-spin'].includes(rule.params))css+=rule.toString()+'\n'})
|
||||
}
|
||||
manifest.adaptations.push('Theme recovery preserves every original :root/html/body/theme selector condition and enclosing at-rule. WB dark token groups remain conditional; component-local custom properties stay on their original scoped component selectors rather than leaking to the root scope. Explicit data-cr-theme maps the host instance mode.')
|
||||
const observedPath='.tmp/rea-add-menu-open-settled-20261016/layout.json',observed=JSON.parse(fs.readFileSync(observedPath,'utf8')),observedId='ev_330f3e738f84f51e0bc5beb5a2ee5647a8bd567407048d1ff239384929ba324f'
|
||||
if(observed.evidenceId!==observedId)throw Error('Runtime override evidence changed')
|
||||
const observedShadow=observed.layout.documents.flatMap(document=>document.nodes).find(node=>node.attributes?.class==='cr-menu')?.styles['box-shadow']
|
||||
if(observedShadow!=='color(srgb 0 0 0 / 0.04) 0px 4px 12px -4px, color(srgb 0 0 0 / 0.04) 0px 3px 6px -8px')throw Error('Observed current light menu shadow changed')
|
||||
css+='/* Current light runtime overrides the generic cr theme shadow; retained REA computed evidence, not a recovered default-token declaration. */\n.yc-source-composer-add-menu[data-cr-theme=light] .cr-add-menu__popover .cr-menu{box-shadow:'+observedShadow+'}\n'
|
||||
manifest.runtimeOverrides=[{evidenceId:observedId,retained:observedPath,sha256:sha(fs.readFileSync(observedPath)),selector:'cr-menu',property:'box-shadow',value:observedShadow,theme:'light',limitation:'Original runtime override producer not located; generic cr dark theme declaration is preserved.'}]
|
||||
const dts=`import type{ComponentType,ReactNode}from'react';export const AddMenuImpl:ComponentType<{onClose?():void;onAction?(id:string):void;renderTrigger?(state:{isOpen:boolean}):ReactNode;className?:string;disabled?:boolean}>;export const Menu:ComponentType<{children?:ReactNode;className?:string}>;export const MenuItem:ComponentType<{children?:ReactNode;icon?:ReactNode;disabled?:boolean;itemId?:string;onClick?():void}>;export const MenuDivider:ComponentType;export const AddFilesIcon:ComponentType;export const ReferenceFilesIcon:ComponentType;export const ModeIcon:ComponentType;export const ExpertsIcon:ComponentType;export const SkillsIcon:ComponentType;export const ConnectorsIcon:ComponentType;export const PlusIcon:ComponentType;export const UploadIcon:ComponentType;\n`
|
||||
const submenuTypes=`import type{FilesProvider,ModeProvider,ConnectorProvider,ReferenceFilesProvider}from'./SourceSubmenus';export const FilesSubmenu:ComponentType<{provider:FilesProvider;onCloseMenu?():void}>;export const ModeSubmenu:ComponentType<{provider:ModeProvider}>;export const ConnectorsSubmenu:ComponentType<{provider:ConnectorProvider;onCloseMenu?():void}>;export const ReferenceFilesSubmenu:ComponentType<{provider:ReferenceFilesProvider;onCloseMenu?():void}>;export const SearchInput:ComponentType<{value:string;onChange(value:string):void;placeholder?:string}>;\n`
|
||||
const folder='src/renderer/src/workbuddy/composer/add-menu/',outputs=[[folder+'source.mjs',js],[folder+'source.d.mts',dts+submenuTypes],[folder+'source.css',css]];manifest.outputs=outputs.map(([file,text])=>({file,sha256:sha(text),bytes:Buffer.byteLength(text)}));outputs.push([folder+'manifest.json',JSON.stringify(manifest,null,2)+'\n']);for(const[file,text]of outputs){if(process.argv.includes('--check')){if(fs.readFileSync(file,'utf8')!==text)throw Error('Recovered output drift '+file)}else fs.writeFileSync(file,text)}console.log(JSON.stringify({result:'PASS',sources:manifest.sources.length,bodies:manifest.bodies.length,outputs:outputs.length}))
|
||||
@@ -0,0 +1,660 @@
|
||||
/**
|
||||
* Recovery of the WorkBuddy 5.8.2 conversation-pane model selector.
|
||||
*
|
||||
* Every recovered byte comes from the fixed REA export retained in .tmp/rea-runtime-scripts.json and
|
||||
* .tmp/rea-css-recovered/. The original install directory, CDP and runtime evaluation are never touched.
|
||||
* Source identities are pinned by SHA-256 and re-verified on every run; `--check` re-derives all
|
||||
* outputs and fails on any drift.
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import {parse} from '@babel/parser'
|
||||
import traverseModule from '@babel/traverse'
|
||||
import postcss from 'postcss'
|
||||
const traverse=traverseModule.default??traverseModule
|
||||
const sha=value=>crypto.createHash('sha256').update(value).digest('hex')
|
||||
const archive=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8'))
|
||||
const cssManifest=JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json','utf8'))
|
||||
const FOLDER='src/renderer/src/workbuddy/composer/model-selector/'
|
||||
const SCOPE='.yc-source-model-selector'
|
||||
const manifest={
|
||||
version:'5.8.2',
|
||||
rootSha256:'035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',
|
||||
evidence:[archive.evidence_id,cssManifest.sourceEvidenceId],
|
||||
sources:[],bodies:[],crossFileBindings:[],hostBindings:[],adaptations:[],observations:[],
|
||||
tokenSelectorMappings:[],outputs:[],
|
||||
}
|
||||
function index(name,pin){
|
||||
const item=archive.items.find(entry=>entry.file_path.endsWith(name))
|
||||
const artifact=item?.source.artifact
|
||||
if(!artifact||artifact.sha256!==pin||sha(artifact.text)!==pin)throw Error('REA source identity mismatch '+name)
|
||||
manifest.sources.push({file:name,sha256:pin,bytes:Buffer.byteLength(artifact.text)})
|
||||
return{text:artifact.text,ast:parse(artifact.text,{sourceType:'module'}),file:name}
|
||||
}
|
||||
const core=index('src-DbE1_mgM.js','6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4')
|
||||
const primitives=index('src-DPDj7_qD.js','747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0')
|
||||
// The corpus imports floating-ui primitives from its own bundled floating-ui/react chunk. The export
|
||||
// table of that chunk is stable in this export and maps the short local names to their real symbols, so
|
||||
// the same public names are taken from the pinned host package (the AddMenu precedent).
|
||||
// Only the entries the recovered bodies actually reference are listed; each is verified against the
|
||||
// retained export table below, so a chunk change fails the run instead of silently mis-binding.
|
||||
const FLOATING_EXPORT_TABLE=new Map(Object.entries({b:'isLastTraversableNode',c:'size',h:'getParentNode',n:'flip',o:'offset',s:'shift',t:'arrow',u:'autoUpdate',v:'isElement'}))
|
||||
const FLOATING_CHUNK_PIN=Object.freeze({file:'floating-ui.react-dom-BDD8Fyfd.js',sha256:'94036e3ed41ddb907b010cedbdb628a57e26e0585e2a9be45c9cbecfdd6ec978',bytes:27055})
|
||||
// Confirm the export table against the retained chunk, and bind the host import list from it.
|
||||
const floatingChunk=archive.items.find(entry=>entry.file_path.endsWith(FLOATING_CHUNK_PIN.file))?.source.artifact
|
||||
if(!floatingChunk||floatingChunk.sha256!==FLOATING_CHUNK_PIN.sha256||sha(floatingChunk.text)!==FLOATING_CHUNK_PIN.sha256)throw Error('floating-ui chunk identity mismatch')
|
||||
const floatingTableText=floatingChunk.text.slice(floatingChunk.text.lastIndexOf('export{'))
|
||||
for(const[local,exported]of FLOATING_EXPORT_TABLE){
|
||||
if(!new RegExp(`\\b${exported} as ${local.replace('$','\\$')}\\b`).test(floatingTableText))throw Error('floating-ui export table changed for '+local)
|
||||
}
|
||||
manifest.observations.push(`floating-ui primitives are aliased in the corpus through ${FLOATING_CHUNK_PIN.file} (${FLOATING_CHUNK_PIN.sha256.slice(0,12)}), whose export table maps each local name to the public symbol recorded in FLOATING_EXPORT_TABLE.`)
|
||||
|
||||
// --------------------------------------------------------------------------- binding index
|
||||
// The cr bundle declares `var a,b,c,M=n(()=>{...})`, where `n` is a lazy module initialiser. Because
|
||||
// that body must never run, values it computes are lifted into plain module-level assignments.
|
||||
function buildIndex(source){
|
||||
const index={declarator:new Map(),fn:new Map(),assignment:new Map(),imported:new Map(),lifted:new Map(),reactAlias:new Set(),jsxAlias:new Set()}
|
||||
traverse(source.ast,{
|
||||
ImportDeclaration(path){
|
||||
for(const specifier of path.node.specifiers){
|
||||
const local=specifier.local.name
|
||||
if(index.imported.has(local))continue
|
||||
index.imported.set(local,{name:specifier.type==='ImportSpecifier'?specifier.imported.name:'default',from:path.node.source.value})
|
||||
}
|
||||
},
|
||||
// Only module-scope bindings are indexed. The bundle reuses single-letter names for locals inside
|
||||
// hundreds of functions, so a whole-file scan would resolve a module-level reference to an unrelated
|
||||
// local that happens to share the name.
|
||||
VariableDeclarator(path){
|
||||
if(path.node.id.type!=='Identifier')return
|
||||
if(path.scope.block.type!=='Program')return
|
||||
if(!index.declarator.has(path.node.id.name))index.declarator.set(path.node.id.name,path)
|
||||
},
|
||||
FunctionDeclaration(path){
|
||||
if(!path.node.id)return
|
||||
// A FunctionDeclaration's own path scope is the function itself, so the parent is what matters.
|
||||
if(!path.parentPath.isProgram())return
|
||||
if(!index.fn.has(path.node.id.name))index.fn.set(path.node.id.name,path)
|
||||
},
|
||||
AssignmentExpression(path){
|
||||
if(path.node.left.type!=='Identifier')return
|
||||
// Values are assigned inside module initialiser arrow functions. The arrow may be nested, so walk up
|
||||
// to the enclosing `n(() => {...})` call rather than assuming the function is its direct argument.
|
||||
let enclosing=path.getFunctionParent(),inInitialiser=false
|
||||
for(let current=enclosing;current;current=current.getFunctionParent()){
|
||||
if(isModuleInit(current.parentPath?.node)){inInitialiser=true;break}
|
||||
}
|
||||
if(!inInitialiser)return
|
||||
if(!index.assignment.has(path.node.left.name))index.assignment.set(path.node.left.name,path)
|
||||
},
|
||||
CallExpression(path){
|
||||
const callee=path.node.callee
|
||||
if(callee.type!=='Identifier')return
|
||||
const initialiser=path.node.arguments[0]
|
||||
if(!initialiser||(initialiser.type!=='ArrowFunctionExpression'&&initialiser.type!=='FunctionExpression'))return
|
||||
// Only the top-level `X=n(()=>{...})` module initialisers are lifted.
|
||||
if(path.scope.block.type!=='Program')return
|
||||
const isJsxInitialiser=Boolean(index.imported.get(callee.name)?.from.includes('jsx-runtime'))
|
||||
const statements=Array.isArray(initialiser.body.body)?initialiser.body.body:[]
|
||||
for(const statement of statements){
|
||||
if(statement.type!=='ExpressionStatement')continue
|
||||
const pending=[statement.expression]
|
||||
while(pending.length){
|
||||
const current=pending.pop()
|
||||
if(!current)continue
|
||||
if(current.type==='SequenceExpression'){pending.push(...current.expressions);continue}
|
||||
if(current.type!=='AssignmentExpression'||current.left.type!=='Identifier')continue
|
||||
registerLifted(current,index,isJsxInitialiser)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
return index
|
||||
}
|
||||
function registerLifted(assignment,index,isJsxInitialiser){
|
||||
const name=assignment.left.name,right=assignment.right
|
||||
if(right.type==='CallExpression'&&right.callee.type==='Identifier'){
|
||||
const helper=index.imported.get(right.callee.name)
|
||||
if(isJsxInitialiser||helper?.from.includes('jsx-runtime')){index.jsxAlias.add(name);return}
|
||||
// `X=a(p())` is the corpus React namespace singleton: `a` is a transparent cr helper and `p` is the
|
||||
// React chunk namespace.
|
||||
const inner=right.arguments[0]
|
||||
if(inner?.type==='CallExpression'&&inner.callee.type==='Identifier'&&index.imported.get(inner.callee.name)?.from.includes('react-z__')){index.reactAlias.add(name);return}
|
||||
}
|
||||
if(!index.lifted.has(name))index.lifted.set(name,assignment)
|
||||
}
|
||||
const coreIndex=buildIndex(core),primitiveIndex=buildIndex(primitives)
|
||||
|
||||
// --------------------------------------------------------------------------- recovery plan
|
||||
// Grouped in dependency order. Every name resolves to exactly one verbatim corpus definition; the run
|
||||
// fails loudly when a name is absent so a source change can never yield a silently partial module.
|
||||
const SECTIONS=[
|
||||
{id:'cr-classname',target:core,names:['r$1','clsx']},
|
||||
{id:'cr-config',target:core,names:['getPrefixCls']},
|
||||
{id:'cr-floating-layer',target:core,names:['getFloatingLayerState','getGroupStack','nextZIndex','useFloatingLayer','li','ui','di','fi','gi','_i']},
|
||||
// Press tracking is a module-level `document` side effect in the corpus: importing it would install the
|
||||
// listener before any host could opt out. It is a host seam and the reported press state drives the
|
||||
// recovered tooltip logic unchanged.
|
||||
// ModelSelector hides the trigger tooltip after a press, so the corpus tooltip ships with it. Its
|
||||
// timing constants and the truncation probes are recovered; the pointer-press tracker is a host seam
|
||||
// because the corpus installs it with a module-level document listener at import time.
|
||||
{id:'cr-tooltip',target:core,names:['TooltipLayer','Ci','isHoverSuppressedByPress','suppressHoverUntilPointerMove','yi','bi','xi','wi','isEmptyContent','resolveTooltipThemeMode','isVisibleFocus','normalizeVisibleText','isElementTruncated','hasTruncationStyle','isSubtreeTruncated','collectVisibleText','hasVisibleTriggerText','shouldGateOnTruncation','Tooltip$1']},
|
||||
{id:'cr-popover',target:core,names:['Ri','zi','Oi','floatingAutoUpdateWithAnimationFrame','PopoverLayer','resolvePopoverWhileElementsMounted','getNearestStackingZIndex','Ui','qi','usePopoverContext','Popover','PopoverTrigger','PopoverContent']},
|
||||
{id:'cr-menu',target:core,names:['Menu','MenuItem','$i','SubMenu','da','fa','pa','MenuDivider','MenuGroup']},
|
||||
{id:'cr-icons',target:core,names:['CheckIcon$1','ChevronRightIcon','ErrorCircleIcon','EnterpriseIcon','MaxIcon','TierFlagshipIcon','TierMemberIcon','CongestedIcon']},
|
||||
{id:'cr-scroll-area',target:core,names:['useOverlayScrollbar','gl','xl']},
|
||||
{id:'wb-icon',target:primitives,names:['yn','resolveSize','bn']},
|
||||
{id:'wb-inline-svg',target:primitives,names:['isPureBlack','recolorBlackToCurrentColor','sanitizeSvg','isSameOriginQueryAllowed','normalizePathForPrefixCheck','warnRejectedSameOrigin','isFetchableSvgUrl','subscribeRemoteSvg','broadcastForUrl','ensureOnlineListener','un','fetchAndParse','fetchWithAuthFallback','scheduleRevalidate','ensureCached','useRemoteSvg','tn','nn','rn','an','on','sn','cn','ln','RemoteSvgFetchError','stripSizeFromStyle','normalizeInlineSvg','pn','mn','hn']},
|
||||
{id:'wb-icon-factory',target:primitives,names:['createIcon','_n','vn','Ks','qs','Js']},
|
||||
{id:'wb-chevron-down-icon',target:primitives,names:['Ys']},
|
||||
{id:'wb-ardot-icon',target:primitives,names:['Zr','Qr','$r','ei']},
|
||||
// The Grok/xai model icon: a themable image asset defined in the primitives chunk.
|
||||
{id:'wb-grok-icon',target:primitives,names:['Zd','$d']},
|
||||
{id:'wb-toggle-switch',target:primitives,names:['hH','useSwitchMotion','_H','vH','yH']},
|
||||
{id:'provider-icon',target:core,names:['isInlineSvgPayload','isImageIconPayload','isUrl','$F','XF','YF','getExtension','resolveFileTypeIcon','resolveFileIconFromMetadata','resolveBuiltinIcon','renderProviderIcon','VV','FileArchiveIcon','FileArdotIcon','FileAudioIcon','FileCodeIcon','FileDiagramIcon','FileDocIcon','FileFolderIcon','FileHtmlIcon','FileImageIcon','FileMarkdownIcon','FilePdfIcon','FileSheetIcon','FileSlideIcon','FileTxtIcon','FileUnknownIcon','FileVideoIcon','CommandIcon','SkillsIcon','LexiangIcon','TencentDocsIcon','WecomIcon','AgentIcon','ApplicationIcon','ChatBubbleIcon','DocsIcon','GitIcon','ProblemsIcon','RulesIcon','TerminalIcon$1','WebIcon']},
|
||||
// `De` is the corpus's own selection state (the cr flag used by the model-selector popover), which is
|
||||
// why it is recovered rather than injected.
|
||||
{id:'model-icons',target:core,names:['OpenAIIcon','AnthropicIcon','DeepSeekIcon','GeminiIcon','KimiIcon','HunyuanIcon','AutoModeIcon','GLMIcon','MinimaxIcon','DefaultModelIcon','FastModelIcon','BalancedModelIcon','PrimaryModelIcon','DeepModelIcon','nW','rW','getModelIconByName','resolveModelIcon']},
|
||||
{id:'model-options',target:core,names:['collectModelOptions','resolveSelectedModelId','resolveSelectedModelLabel','buildGroupedModels','getDefaultIsThinking','isReasoningConfigurable','formatCredits','formatContextWindow','getSupportedContextWindows','createModelSelection']},
|
||||
{id:'model-badge',target:core,names:['OverflowTitleSpan','resolvePromotionIconUrl','mW','hW','gW','ModelBadge','ToggleSwitch']},
|
||||
{id:'model-sub-panels',target:core,names:['ReasoningEffortPanel','ContextWindowPanel','xW']},
|
||||
{id:'model-sub-menu',target:core,names:['ModelSubMenu','TW']},
|
||||
{id:'model-tier-badge',target:core,names:['ModelTierBadge','OW','kW']},
|
||||
{id:'model-item',target:core,names:['ModelItem','NW']},
|
||||
{id:'model-group-item',target:core,names:['ModelGroupMenuItem']},
|
||||
{id:'model-popover',target:core,names:['ModelSelectPopover']},
|
||||
{id:'model-selector',target:core,names:['ModelSelectorImpl']},
|
||||
]
|
||||
// Host seams: identifiers that are neither corpus definitions nor recovered above. Each is declared in
|
||||
// the generated preamble. Nothing else may be missing.
|
||||
const HOST_FLOATING=['offset','flip','shift','size','arrow','autoUpdate','useFloating','useDismiss','useHover','useInteractions','useTransitionStyles']
|
||||
const HOST_FLOATING_IMPORT=[...HOST_FLOATING,'FloatingPortal as FloatingPortalPrimitive'].join(', ')
|
||||
const EXPORTS=['ModelSelectorImpl','ModelSelectPopover','ModelSubMenu','ModelItem','ModelGroupMenuItem','ModelTierBadge','Menu','MenuItem','MenuDivider','MenuGroup','Popover','PopoverTrigger','PopoverContent','Tooltip$1 as Tooltip','ToggleSwitch','ReasoningEffortPanel','ContextWindowPanel','resolveModelIcon','getModelIconByName','OverflowTitleSpan','ModelBadge','getDefaultIsThinking','isReasoningConfigurable','createModelSelection','collectModelOptions','resolveSelectedModelId','resolveSelectedModelLabel','formatCredits','formatContextWindow','getSupportedContextWindows','useOverlayScrollbar'].join(', ')
|
||||
const GLOBALS=new Set(('Array Boolean Date Error JSON Map Math Number Object Promise RegExp Set String URL URLSearchParams '+
|
||||
'WeakMap console document globalThis window process queueMicrotask requestAnimationFrame cancelAnimationFrame setTimeout clearTimeout '+
|
||||
'getComputedStyle ResizeObserver Node HTMLElement DOMParser XMLSerializer fetch navigator performance isNaN parseInt '+
|
||||
'parseFloat undefined arguments Uint8Array Symbol encodeURIComponent decodeURIComponent atob btoa CSS structuredClone '+
|
||||
'Element MutationObserver IntersectionObserver isFinite RangeError TypeError').split(' '))
|
||||
function isModuleInit(node){return node?.type==='CallExpression'&&node.callee.type==='Identifier'&&node.arguments.length===1&&(node.arguments[0].type==='ArrowFunctionExpression'||node.arguments[0].type==='FunctionExpression')}
|
||||
const resolved=new Map(),missing=[]
|
||||
function resolve(target,name){
|
||||
const table=target===core?coreIndex:primitiveIndex
|
||||
// Corpus React/JSX singletons are declared once in the preamble and must not also be emitted.
|
||||
if(table.reactAlias.has(name))return{kind:'react-singleton'}
|
||||
if(table.jsxAlias.has(name))return{kind:'jsx-singleton'}
|
||||
const declarator=table.declarator.get(name)
|
||||
if(declarator?.node.init&&!isModuleInit(declarator.node.init))return{kind:'declaration',start:declarator.node.start,end:declarator.node.end,text:target.text.slice(declarator.node.start,declarator.node.end)}
|
||||
const fn=table.fn.get(name)
|
||||
if(fn)return{kind:'function',start:fn.node.start,end:fn.node.end,text:target.text.slice(fn.node.start,fn.node.end)}
|
||||
// `var X,Y,Z` plus `X=<expr>` inside a module initialiser keeps `var`: several of these bindings are
|
||||
// reassigned by the recovered functions themselves (for example the pointer-press timestamp tracker).
|
||||
const lifted=table.lifted.get(name)
|
||||
if(lifted)return{kind:'lifted',start:lifted.start,end:lifted.end,text:`var ${name}=${target.text.slice(lifted.right.start,lifted.right.end)};`}
|
||||
const assignment=table.assignment.get(name)
|
||||
if(assignment)return{kind:'assignment',start:assignment.node.start,end:assignment.node.end,text:`var ${name}=${target.text.slice(assignment.node.right.start,assignment.node.right.end)};`}
|
||||
const imported=table.imported.get(name)
|
||||
if(imported)return{kind:'import',imported}
|
||||
return null
|
||||
}
|
||||
for(const section of SECTIONS){
|
||||
section.rows=[]
|
||||
for(const name of section.names){
|
||||
const entry=resolve(section.target,name)
|
||||
if(!entry){missing.push(section.id+':'+name);continue}
|
||||
if(entry.kind==='react-singleton'||entry.kind==='jsx-singleton')continue
|
||||
section.rows.push({name,file:section.target.file,...entry})
|
||||
resolved.set(name,entry)
|
||||
}
|
||||
}
|
||||
if(missing.length)throw Error('Unresolved corpus names: '+missing.join(', '))
|
||||
manifest.observations.push('src-DbE1_mgM.js is the conversation-pane chunk and src-DPDj7_qD.js the WorkBuddy primitive library. Both live in the fixed REA export and are SHA-256 verified on every run.')
|
||||
|
||||
// --------------------------------------------------------------------------- emit bodies
|
||||
// Bodies are emitted byte-for-byte. The corpus idiom `(0,X.jsx)(...)` is the minifier's this-free
|
||||
// indirect call and is already valid JavaScript, so no textual rewrite is applied to recovered code.
|
||||
// --------------------------------------------------------------------------- cross-file wiring
|
||||
// cr reaches primitives-chunk symbols through its minimised re-export table: `Te` is the WB switch, `Pe`
|
||||
// the ChevronDown icon, `je` the Ardot file icon and `pe` the SVG sanitiser. Each target is recovered
|
||||
// verbatim in this same module from src-DPDj7_qD.js and bound under the alias the call sites use.
|
||||
const CROSS=[
|
||||
{alias:'Te',target:'yH',symbol:'src-DPDj7_qD.js yH (wb-switch)',usedBy:'ToggleSwitch'},
|
||||
{alias:'Pe',target:'Ys',symbol:'src-DPDj7_qD.js Ys (ChevronDownIcon)',usedBy:'ModelSelectorImpl trigger arrow'},
|
||||
{alias:'je',target:'ei',symbol:'src-DPDj7_qD.js ei (ArdotFileIcon)',usedBy:'FileArdotIcon'},
|
||||
{alias:'pe',target:'sanitizeSvg',symbol:'src-DPDj7_qD.js sanitizeSvg',usedBy:'renderProviderIcon inline-svg sanitisation'},
|
||||
{alias:'De',target:'$d',symbol:'src-DPDj7_qD.js $d (GrokIcon)',usedBy:'model icon registry nW grok/xai entries'},
|
||||
]
|
||||
for(const binding of CROSS)if(!resolved.has(binding.target))throw Error('Cross-file target missing: '+binding.target)
|
||||
// Corpus floating-ui locals. The public symbol for each local comes from the retained export table of
|
||||
// the bundled floating-ui/react chunk, so these bind to the pinned host package.
|
||||
const FLOATING_LOCAL_BINDINGS=[['D','offset'],['E','flip'],['O','shift'],['b','size'],['k','arrow'],['A','autoUpdate']]
|
||||
manifest.observations.push('Corpus floating-ui locals D/E/O/b/k/A are bound to the public offset/flip/shift/size/arrow/autoUpdate symbols of the pinned host package, resolved through the retained export table instead of guessed.')
|
||||
// The bindings are emitted after the primitives sections and before the sections whose top-level values
|
||||
// read them (the model icon registry reads `De` while the module is still evaluating), otherwise the
|
||||
// recovered initialisers would hit a temporal dead zone.
|
||||
const CROSS_BLOCK=[
|
||||
'',
|
||||
'// cr re-export table and corpus floating-ui locals. Emitted after their targets and before the',
|
||||
'// sections whose top-level values consume them.',
|
||||
...CROSS.map(binding=>`// \`${binding.alias}\` -> ${binding.symbol}\nconst ${binding.alias}=${binding.target}`),
|
||||
...FLOATING_LOCAL_BINDINGS.map(([local,publicName])=>`const ${local}=${publicName}`),
|
||||
'',
|
||||
].join('\n')
|
||||
manifest.crossFileBindings=[...CROSS.map(binding=>({alias:binding.alias,target:binding.target,symbol:binding.symbol,usedBy:binding.usedBy,from:'src-DPDj7_qD.js',recovered:true})),...FLOATING_LOCAL_BINDINGS.map(([local,publicName])=>({alias:local,target:publicName,from:'@floating-ui/react',recovered:false,note:'Corpus floating-ui local resolved through the retained export table of the bundled floating-ui/react chunk.'}))]
|
||||
let recoveredJs=''
|
||||
for(const section of SECTIONS){
|
||||
if(section.id==='provider-icon')recoveredJs+=CROSS_BLOCK
|
||||
recoveredJs+=`\n// ---- ${section.id} — ${section.file}\n`
|
||||
for(const row of section.rows){
|
||||
recoveredJs+=row.text+'\n'
|
||||
manifest.bodies.push({section:section.id,file:row.file,name:row.name,kind:row.kind,utf16:[row.start,row.end],sha256:sha(row.text)})
|
||||
}
|
||||
}
|
||||
if(!recoveredJs.includes('const De='))throw Error('Cross-file block was not emitted')
|
||||
|
||||
// --------------------------------------------------------------------------- zh-CN labels
|
||||
const LABEL_OFFSET=172945
|
||||
const labels=(()=>{
|
||||
const start=172942
|
||||
const end=core.text.indexOf('}}))',start)
|
||||
if(start<0||end<0||Math.abs(start-LABEL_OFFSET)>16)throw Error('zh-CN locale object boundary changed')
|
||||
const block=core.text.slice(start,end),found={}
|
||||
for(const match of block.matchAll(/"chatInput\.modelSelector\.([\w.]+)":`((?:\\u[0-9A-Fa-f]{4}|[^`])*)`/g)){
|
||||
found[match[1]]=match[2].replace(/\\u([0-9A-Fa-f]{4})/g,(raw,code)=>String.fromCharCode(parseInt(code,16)))
|
||||
}
|
||||
const keys=Object.keys(found)
|
||||
if(keys.length!==26)throw Error('Model selector label count changed: '+keys.length)
|
||||
for(const required of['maxMode','maxModeTooltip','thinkingMode','thinkingEffort','thinking.on','thinking.off','enterprise','externalWarning','credits','creditsUnit','busy.label','busy.idle','busy.busy','busy.congested','busy.unavailable','busy.unavailableTip','contextWindow','configIssue.title','configIssue.reasonLine1','configIssue.reasonLine2','configIssue.fixAction','effort.low','effort.medium','effort.high','effort.xhigh','effort.max'])if(!Object.prototype.hasOwnProperty.call(found,required))throw Error('Missing source label '+required)
|
||||
return Object.fromEntries(keys.map(key=>['chatInput.modelSelector.'+key,found[key]]))
|
||||
})()
|
||||
manifest.observations.push('The zh-CN locale literal at UTF-16 offset '+LABEL_OFFSET+' of src-DbE1_mgM.js holds all 26 chatInput.modelSelector.* strings used here. Values are stored as \\uXXXX escapes and decoded verbatim. The decoded set contains no traditional-only characters (thinkingEffort=\u601d\u8003\u5f3a\u5ea6), so the recovered values are byte-faithful to the zh-CN table')
|
||||
|
||||
// --------------------------------------------------------------------------- alias declarations
|
||||
// The bundle is a flattened set of modules that reuse minified alias names, so a name can be bound twice
|
||||
// in the corpus. Each alias is classified by how the recovered call sites actually use it. `Fragment` is
|
||||
// deliberately not a discriminator: both namespaces expose it.
|
||||
const jsxCallers=new Set()
|
||||
for(const match of recoveredJs.matchAll(/(?<![\w$.])([A-Za-z$_][\w$]*)\.(?:jsx|jsxs)\b/g))jsxCallers.add(match[1])
|
||||
const reactCallers=new Set()
|
||||
for(const match of recoveredJs.matchAll(/(?<![\w$.])([A-Za-z$_][\w$]*)\.(?:useState|useRef|useCallback|useEffect|useLayoutEffect|useMemo|useId|useContext|useSyncExternalStore|createContext|forwardRef|memo|cloneElement|createElement|isValidElement|Children)\b/g))reactCallers.add(match[1])
|
||||
const ambiguous=[...jsxCallers].filter(name=>reactCallers.has(name))
|
||||
if(ambiguous.length)throw Error('Alias used as both React and JSX namespace: '+ambiguous.join(', '))
|
||||
const corpusReact=new Set([...coreIndex.reactAlias,...primitiveIndex.reactAlias])
|
||||
const corpusJsx=new Set([...coreIndex.jsxAlias,...primitiveIndex.jsxAlias])
|
||||
const undefinedAliases=[...jsxCallers,...reactCallers].filter(name=>!corpusJsx.has(name)&&!corpusReact.has(name))
|
||||
if(undefinedAliases.length)throw Error('Alias binding not present in the corpus: '+undefinedAliases.join(', '))
|
||||
const aliasDeclarations=[
|
||||
...[...reactCallers].sort().map(name=>`const ${name}=React`),
|
||||
...[...jsxCallers].sort().map(name=>`const ${name}=JSX`),
|
||||
]
|
||||
manifest.observations.push('Minified alias names are reused across the flattened modules of the retained bundle. Every alias declared in the generated module is classified from its recovered call sites and verified to be a corpus singleton; the React and JSX alias sets are disjoint.')
|
||||
|
||||
// --------------------------------------------------------------------------- module source
|
||||
const runtimeSource=`/* Host seam for the recovered model selector.
|
||||
The corpus reads draft state, locale and diagnostics from neighbouring input-box and i18n modules that
|
||||
are outside this recovery, and resolves provider icons through a host callback. Those edges are injected
|
||||
here instead of being simulated. */
|
||||
const defaultRuntime={
|
||||
mode:'light',
|
||||
useInputBoxStore(){throw new Error('ModelSelectorRuntime.useInputBoxStore is not configured')},
|
||||
useInputBoxSelector(selector,isEqual){const state={draft:{selections:{model:null}}};return selector(state,isEqual)},
|
||||
useRenderDiag(){},
|
||||
get prefixCls(){return 'cr'},
|
||||
}
|
||||
let activeRuntime=defaultRuntime
|
||||
export function configureModelSelectorRuntime(next){
|
||||
activeRuntime={...defaultRuntime,...next}
|
||||
return activeRuntime
|
||||
}
|
||||
export function readModelSelectorRuntime(){return activeRuntime}
|
||||
const RuntimeContext=React.createContext(null)
|
||||
export function ModelSelectorRuntimeProvider({runtime,children}){
|
||||
const value=React.useMemo(()=>({...defaultRuntime,...runtime}),[runtime])
|
||||
return JSX.jsx(RuntimeContext.Provider,{value,children})
|
||||
}
|
||||
export function useModelSelectorRuntime(){
|
||||
const injected=React.useContext(RuntimeContext)
|
||||
return injected??activeRuntime
|
||||
}
|
||||
export const modelSelectorLabels=${JSON.stringify(labels)}
|
||||
const ModelSelectorConfigContext=React.createContext({prefixCls:'cr',mode:'light',locale:'zh-CN',t:key=>modelSelectorLabels[key]??key})
|
||||
export const useModelSelectorConfig=()=>React.useContext(ModelSelectorConfigContext)
|
||||
const usePrefixCls=()=>{const{prefixCls}=useModelSelectorConfig();return React.useMemo(()=>getPrefixCls(prefixCls),[prefixCls])}
|
||||
const useI18n=()=>{const{t,locale}=useModelSelectorConfig();return{t,locale}}
|
||||
const useCrConfig=()=>useModelSelectorConfig()
|
||||
const useInputBoxStore=()=>useModelSelectorRuntime().useInputBoxStore()
|
||||
const useInputBoxSelector=(selector,isEqual)=>useModelSelectorRuntime().useInputBoxSelector(selector,isEqual)
|
||||
const useRenderDiag=(name,meta)=>{useModelSelectorRuntime().useRenderDiag?.(name,meta)}
|
||||
const yt=ModelSelectorConfigContext
|
||||
const FloatingPortal=({children,root})=>{
|
||||
const runtime=useModelSelectorRuntime()
|
||||
return JSX.jsx(FloatingPortalPrimitive,{root,children:JSX.jsx('span',{className:'yc-source-model-selector','data-cr-theme':runtime.mode,'data-theme':runtime.mode,children})})
|
||||
}
|
||||
const createDOMPurifyInstance=()=>{
|
||||
if(typeof createDOMPurify!=='function')return null
|
||||
if(typeof window==='undefined')return createDOMPurify
|
||||
return createDOMPurify(window)
|
||||
}
|
||||
// The corpus bundles purify.es and binds it to \`fe\`; sanitizeSvg calls fe.sanitize with the exact
|
||||
// configuration below. The host dependency supplies the same instance under its default export.
|
||||
const fe=createDOMPurifyInstance()??{sanitize:value=>value}
|
||||
`
|
||||
manifest.adaptations.push(
|
||||
'Host seam: useInputBoxStore / useInputBoxSelector (draft selection state), useRenderDiag (render instrumentation sink), usePrefixCls / useI18n / useCrConfig (cr config context) and the FloatingPortal theme wrapper are injected instead of the neighbouring input-box, i18n and tokens modules, which are outside this recovery. The recovered call sites, their argument shapes and all markup stay unchanged.',
|
||||
'The cr config context is reduced to prefixCls/mode/locale/t. The corpus seeds it with createT(locale,tables) over the zh-CN/zh-TW/en-US/pt-BR/id-ID message tables (about 250 KB) plus a token-to-CSS-variable projector; both values are dead weight for this component and nothing is invented.',
|
||||
'FloatingUI primitives use the pinned host @floating-ui/react package, matching the recovered AddMenu precedent. The corpus inlines its own floating-ui/react build and the bundled version is not recorded in the fixed export, so the host range is used as-is.',
|
||||
'DOMPurify comes from the pinned host dependency. The corpus bundles purify.es with DOMPurify.version="3.4.11"; sanitizeSvg keeps its exact configuration (USE_PROFILES svg+svgFilters, FORBID_TAGS script/foreignObject, FORBID_ATTR srcdoc/formaction). The host package is a newer patch release.',
|
||||
'ModelSelectorImpl is emitted verbatim. It reads `n.draft.selections.model` through useInputBoxSelector and commits through useInputBoxStore().api.setModel, both of which the host must provide; trigger markup, ARIA, class names, placements, delays and submenu state are unmodified.',
|
||||
)
|
||||
// The recovered text is concatenated, never interpolated inside a template literal: corpus code is full
|
||||
// of `${...}` sequences that must stay literal.
|
||||
const moduleSource=[
|
||||
'import * as React from \'react\'',
|
||||
'import * as JSX from \'react/jsx-runtime\'',
|
||||
`import {${HOST_FLOATING_IMPORT}} from '@floating-ui/react'`,
|
||||
'import createDOMPurify from \'dompurify\'',
|
||||
...aliasDeclarations,
|
||||
runtimeSource,
|
||||
recoveredJs,
|
||||
`export {${EXPORTS}}`,
|
||||
].join('\n')
|
||||
// The preamble may never shadow a recovered body: verbatim corpus code always wins.
|
||||
const preambleNames=['usePrefixCls','useI18n','useCrConfig','useInputBoxStore','useInputBoxSelector','useRenderDiag','yt','FloatingPortal','ModelSelectorConfigContext','useModelSelectorConfig','modelSelectorLabels','RuntimeContext','defaultRuntime','activeRuntime','createDOMPurifyInstance']
|
||||
const colliding=preambleNames.filter(name=>resolved.has(name))
|
||||
if(colliding.length)throw Error('Host seam collides with recovered body: '+colliding.join(', '))
|
||||
|
||||
// --------------------------------------------------------------------------- unbound-identifier audit
|
||||
// The emitted module is re-parsed and every referenced identifier is resolved against module-scope
|
||||
// bindings, imports and known globals. Anything else is a recovery gap and fails the run.
|
||||
function auditModule(source){
|
||||
const file=parse(source,{sourceType:'module'})
|
||||
const declared=new Set(),importedNames=new Set(),unboundCandidates=new Set(),sites=new Map()
|
||||
let references=0
|
||||
for(const statement of file.program.body){
|
||||
if(statement.type==='ImportDeclaration')for(const specifier of statement.specifiers)importedNames.add(specifier.local.name)
|
||||
}
|
||||
const lineOf=index=>source.slice(0,index).split('\n').length
|
||||
traverse(file,{
|
||||
Program(path){
|
||||
for(const name of Object.keys(path.scope.bindings))declared.add(name)
|
||||
path.traverse({
|
||||
ReferencedIdentifier(identifierPath){
|
||||
const name=identifierPath.node.name
|
||||
references+=1
|
||||
if(identifierPath.scope.hasBinding(name))return
|
||||
unboundCandidates.add(name)
|
||||
if(!sites.has(name))sites.set(name,`line ${lineOf(identifierPath.node.start)}: ${source.slice(Math.max(0,identifierPath.node.start-70),identifierPath.node.start+40).replace(/\n/g,' ')}`)
|
||||
},
|
||||
JSXIdentifier(identifierPath){
|
||||
if(identifierPath.parent.type!=='JSXOpeningElement'&&identifierPath.parent.type!=='JSXClosingElement')return
|
||||
const name=identifierPath.node.name
|
||||
if(/^[a-z]/.test(name))return
|
||||
references+=1
|
||||
if(identifierPath.scope.hasBinding(name))return
|
||||
unboundCandidates.add(name)
|
||||
if(!sites.has(name))sites.set(name,`line ${lineOf(identifierPath.node.start)}: ${source.slice(Math.max(0,identifierPath.node.start-70),identifierPath.node.start+40).replace(/\n/g,' ')}`)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
// `unboundCandidates` are references with no lexical binding. Each must be a declared import, a known
|
||||
// global, or one of the pinned floating-ui primitives; anything left over is a recovery gap.
|
||||
const hostProvided=[...unboundCandidates].filter(name=>!GLOBALS.has(name)&&!HOST_FLOATING.includes(name)&&!importedNames.has(name))
|
||||
return{unbound:hostProvided.sort(),sites,declared:declared.size,references,unboundCandidates:unboundCandidates.size,hostBindings:importedNames.size}
|
||||
}
|
||||
const audit=auditModule(moduleSource)
|
||||
if(audit.unbound.length){
|
||||
throw Error('Unbound identifiers in emitted module:\n'+audit.unbound.map(name=>' '+name+' -> '+(audit.sites.get(name)??'(preamble)')).join('\n'))
|
||||
}
|
||||
manifest.observations.push('Unbound-identifier audit over the emitted module: '+audit.references+' identifier references, '+audit.declared+' module-scope bindings, '+audit.hostBindings+' imported host bindings, '+audit.unboundCandidates+' references resolved lexically, 0 unresolved.')
|
||||
manifest.adaptations.push('Lifted module values keep `var` rather than `const`: several are reassigned by the recovered functions (the pointer-press timestamp tracker is the clearest case), so narrowing them would change behaviour. The corpus declares them with `var` for the same reason.')
|
||||
|
||||
// --------------------------------------------------------------------------- css
|
||||
function scopedTokenSelectors(rule){
|
||||
const mapped=[]
|
||||
for(const selector of rule.selectors){
|
||||
if(selector===':root'){mapped.push(SCOPE);continue}
|
||||
if(selector.includes('.cr-theme')){mapped.push(selector.replace(/\.cr-theme\b/g,SCOPE));continue}
|
||||
// Every original theme/platform condition is preserved; dark selectors never become unconditional.
|
||||
mapped.push(selector+' '+SCOPE)
|
||||
if(/(?:data-theme=dark|\.cb-dark\b|IDE\\ Night|vscode-dark|\.dark\b)/.test(selector))mapped.push(SCOPE+'[data-cr-theme=dark]')
|
||||
if(/IDE\\ Light/.test(selector))mapped.push(SCOPE+'[data-cr-theme=light]')
|
||||
}
|
||||
if(rule.selectors.includes('.light .cr-theme'))mapped.push('[data-theme=light] '+SCOPE+':not([data-cr-theme=dark])')
|
||||
return [...new Set(mapped)].join(',')
|
||||
}
|
||||
function wrapAtRules(node,parent){
|
||||
let result=node
|
||||
for(let current=parent;current?.type==='atrule';current=current.parent){const wrapper=current.clone({nodes:[]});wrapper.append(result);result=wrapper}
|
||||
return result.toString()
|
||||
}
|
||||
let css='/* Fixed REA cr model selector and its Menu/SubMenu/Popover, Tooltip, ScrollArea, WB switch and icon CSS; scoped selectors. */\n'
|
||||
const cssSources=[
|
||||
['src-CBaV_A3S.css','2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c','tokens'],
|
||||
['src-CyXHA0IC.css','a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe','components'],
|
||||
['src-Bh3YZ35H.css','88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a','wb-switch'],
|
||||
['desktop-sdk-setup-Bh0rg_xP.css','b2ee58c0caf560eeddc4eb8c91d86cabddbec634b8d37ed5d6b85d0644c0e142','runtime override'],
|
||||
]
|
||||
const COMPONENT_PATTERN=/\.cr-(?:model-selector|menu\b|menu-item|menu-group|sub-menu|popover\b|tooltip(?:\b|__|--)|slim-scroll)(?:__|--|\.|,|\s|:|\{|$)|\.wb-(?:switch|icon)(?:\b|__|--)/
|
||||
// Shared cr sheets also style sibling components. A selector that names one of them and does not also
|
||||
// name the model selector is dropped: under this scope it could never match, and copying it would
|
||||
// advertise coverage the module does not provide.
|
||||
const FOREIGN_COMPONENT=/\.cr-(?:add-menu|files-submenu|reference-files-submenu|mode-submenu|experts-submenu|skills-submenu|connectors-submenu|workspace-picker|permission-panel|context-usage|connector-chip|search-input|select\b|input\b|modal\b|drawer\b|tabs\b|badge\b)/
|
||||
const observedPath='.tmp/rea-model-select-open-settings-20261016/layout.json'
|
||||
const observed=JSON.parse(fs.readFileSync(observedPath,'utf8'))
|
||||
const observedId='ev_1c3853a9a4dd6f99cf76d1874ad57af7ad6fb56c33e6b4e5c2a2e770f91f462e'
|
||||
if(observed.evidenceId!==observedId)throw Error('Runtime layout evidence changed')
|
||||
const observedNodes=observed.layout.documents.flatMap(document=>document.nodes)
|
||||
const observedMenu=observedNodes.find(node=>node.attributes?.class==='cr-model-selector__menu cr-menu')
|
||||
const observedShadow=observedMenu?.styles?.['box-shadow']
|
||||
const observedBackground=observedMenu?.styles?.['background-color']
|
||||
for(const[file,pin,role]of cssSources){
|
||||
const text=fs.readFileSync('.tmp/rea-css-recovered/'+file,'utf8')
|
||||
if(sha(text)!==pin||cssManifest.files.find(item=>item.file===file)?.sha256!==pin)throw Error('CSS identity mismatch '+file)
|
||||
manifest.sources.push({file,sha256:pin,bytes:Buffer.byteLength(text)})
|
||||
const tree=postcss.parse(text)
|
||||
tree.walkRules(rule=>{
|
||||
if(role==='tokens'){
|
||||
const declarations=(rule.nodes??[]).filter(node=>node.type==='decl'&&(/^--cr-/.test(node.prop)||/^--wb-/.test(node.prop)||/^--cb-/.test(node.prop)))
|
||||
if(!declarations.length)return
|
||||
const clone=rule.clone({nodes:declarations.map(node=>node.clone())})
|
||||
clone.selector=scopedTokenSelectors(rule)
|
||||
manifest.tokenSelectorMappings.push({file,original:rule.selector,scoped:clone.selector,declarations:declarations.length})
|
||||
css+=wrapAtRules(clone,rule.parent)+'\n'
|
||||
return
|
||||
}
|
||||
if(role==='runtime override'){
|
||||
// This retained rule is a shared WorkBuddy override that also names AddMenu and several other
|
||||
// popovers. Only the model-selector selectors belong to this module; the rest are dropped rather than
|
||||
// copied under a scope where they could never apply.
|
||||
const selectors=rule.selectors.filter(selector=>selector.includes('cr-model-selector'))
|
||||
if(!selectors.length)return
|
||||
const clone=rule.clone()
|
||||
clone.selector=selectors.map(selector=>selector.replace(/\bbody\b/g,SCOPE)).join(',')
|
||||
manifest.tokenSelectorMappings.push({file,original:rule.selector,scoped:clone.selector,declarations:(clone.nodes??[]).filter(node=>node.type==='decl').length})
|
||||
css+=clone.toString()+'\n'
|
||||
return
|
||||
}
|
||||
if(!COMPONENT_PATTERN.test(rule.selector))return
|
||||
const clone=rule.clone()
|
||||
const selectors=clone.selectors.filter(selector=>!FOREIGN_COMPONENT.test(selector)||/cr-model-selector/.test(selector))
|
||||
if(!selectors.length)return
|
||||
clone.selector=selectors.map(selector=>selector.startsWith(SCOPE)?selector:SCOPE+' '+selector).join(',')
|
||||
css+=wrapAtRules(clone,rule.parent)+'\n'
|
||||
})
|
||||
tree.walkAtRules('keyframes',rule=>{if(['cr-model-pop-in','cr-sub-menu-pop-in','wb-switch-spin'].includes(rule.params))css+=rule.toString()+'\n'})
|
||||
}
|
||||
manifest.adaptations.push('CSS is scoped under '+SCOPE+', preserving every original media/theme condition including the dark-theme groups and the IDE Night/Light platform selectors. Token declarations are re-emitted on their original conditions instead of being flattened to the root scope.')
|
||||
if(observedShadow)css+=`/* Retained REA runtime override (${observedId}); the original override producer is not located in the fixed export. */\n${SCOPE} .cr-model-selector__menu.cr-menu{box-shadow:${observedShadow}}\n`
|
||||
if(observedBackground&&!observedBackground.startsWith('rgba(0, 0, 0, 0)'))css+=`${SCOPE} .cr-model-selector__menu.cr-menu{background:${observedBackground}}\n`
|
||||
manifest.observations.push('The open-state popover body has no REA geometry evidence beyond the retained desktop-sdk-setup override and the trigger node in '+observedPath+'. The menu rule therefore keeps the recovered source declarations, overlaid only with the observed box-shadow and background.')
|
||||
|
||||
// The conversation shell owns three rules the selector depends on and that this component sheet does not
|
||||
// carry: the shared .wb-icon flex base, the composer input-slot 50% icon radius, and the input-slot
|
||||
// --cr-text-primary bridge to --wb-color-text-primary that makes the trigger label #000 instead of the
|
||||
// panel default rgba(0,0,0,.9). They are recovered from the same fixed REA CSS export.
|
||||
const SHELL_RULES=[
|
||||
{file:'contexts-BkjwYSZT.css',sha256:'50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380',name:'wb-icon flex base',selectors:['.wb-icon'],expect:'justify-content:center;align-items:center;line-height:0;display:inline-flex'},
|
||||
{file:'contexts-BkjwYSZT.css',sha256:'50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380',name:'composer input-slot icon radius',selectors:['.wb-home-composer__input-slot [class*=icon]:not([class*=Labeled])','.wb-home-composer__input-slot .voice-mic-trigger'],expect:'border-radius:50%'},
|
||||
{file:'contexts-BkjwYSZT.css',sha256:'50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380',name:'composer input-slot token bridge',relation:'ancestor',selectors:['.wb-home-composer__input-slot .cr-input-toolbar'],expect:'--cr-text-primary:var(--wb-color-text-primary)'},
|
||||
]
|
||||
for(const spec of SHELL_RULES){
|
||||
const text=fs.readFileSync('.tmp/rea-css-recovered/'+spec.file,'utf8')
|
||||
if(sha(text)!==spec.sha256)throw Error('Shell CSS identity mismatch '+spec.file)
|
||||
if(!manifest.sources.some(item=>item.file===spec.file))manifest.sources.push({file:spec.file,sha256:spec.sha256})
|
||||
let found=false
|
||||
postcss.parse(text).walkRules(rule=>{
|
||||
if(found)return
|
||||
if(rule.selectors.length!==spec.selectors.length)return
|
||||
if(!spec.selectors.every((selector,index)=>rule.selectors[index]===selector))return
|
||||
const body=rule.nodes.filter(node=>node.type==='decl').map(node=>node.prop+':'+node.value).join(';')
|
||||
if(!body.includes(spec.expect))throw Error('Shell rule body changed for '+spec.name+': '+body)
|
||||
found=true
|
||||
const clone=rule.clone()
|
||||
// The original ancestor (.wb-home-composer__input-slot) is the composer shell. Two of these rules select
|
||||
// a descendant of that shell, so the shell prefix is replaced by this module scope. The token-bridge rule
|
||||
// instead targets the shell's own toolbar, which is an ancestor of this module; there the scope is appended
|
||||
// as a descendant of that toolbar, keeping the original element relationship intact either way.
|
||||
clone.selector=spec.selectors.map(selector=>{
|
||||
// Keep the shell's toolbar element and drop only the shell root, which YCode does not render.
|
||||
const withoutShell=selector.replace('.wb-home-composer__input-slot ','')
|
||||
return spec.relation==='ancestor' ? withoutShell+' '+SCOPE : SCOPE+' '+withoutShell
|
||||
}).join(',')
|
||||
css+='/* '+spec.name+' — recovered from '+spec.file+', original selector: '+spec.selectors.join(' , ')+' */\n'+clone.toString()+'\n'
|
||||
})
|
||||
if(!found)throw Error('Shell rule not found: '+spec.name)
|
||||
}
|
||||
manifest.adaptations.push('Three conversation-shell rules are recovered alongside the component sheet because the measured trigger depends on them: the shared .wb-icon flex base, the composer input-slot 50% icon radius, and the input-slot --cr-text-primary bridge to --wb-color-text-primary. All three keep their original declarations; only the shell ancestor is replaced by this module scope.')
|
||||
|
||||
// --------------------------------------------------------------------------- types
|
||||
const types=`/** Typed host contract for the recovered WorkBuddy 5.8.2 model selector. */
|
||||
import type {ComponentType, ReactNode} from 'react'
|
||||
export type ModelEffort='low'|'medium'|'high'|'xhigh'|'max'
|
||||
export type ModelBusyLevel='idle'|'normal'|'busy'|'congested'|'unavailable'|'unknown'
|
||||
export interface ModelBadgeSpec{label:string;color?:string;background?:string;icon?:string}
|
||||
export interface ModelTierBadgeSpec{label:string;level?:'flagship'|'member';color?:string;background?:string}
|
||||
export interface ModelPromotionFooter{badge?:ModelBadgeSpec;text?:string;actionLabel?:string}
|
||||
export interface ModelPromotionCredits{original:string;discounted:string;displayMode?:'replace'|'suffix'}
|
||||
export interface ModelTierFooter{text:string;actionLabel?:string}
|
||||
export interface ModelOption{
|
||||
id:string
|
||||
name:string
|
||||
iconUrl?:string
|
||||
icon?:unknown
|
||||
groupId?:string
|
||||
description?:string
|
||||
credits?:string
|
||||
disabled?:boolean
|
||||
configIssue?:boolean
|
||||
isExternal?:boolean
|
||||
supportsReasoning?:boolean
|
||||
onlyReasoning?:boolean
|
||||
canDisableThinking?:boolean
|
||||
isThinking?:boolean
|
||||
supportedEfforts?:readonly ModelEffort[]
|
||||
defaultEffort?:ModelEffort
|
||||
reasoningEffort?:ModelEffort
|
||||
busyLevel?:ModelBusyLevel
|
||||
badges?:readonly ModelBadgeSpec[]
|
||||
tierBadges?:readonly ModelTierBadgeSpec[]
|
||||
promotionCredits?:ModelPromotionCredits
|
||||
promotionFooter?:ModelPromotionFooter
|
||||
tierFooter?:ModelTierFooter
|
||||
tier?:string
|
||||
contextWindow?:string
|
||||
supportedContextWindows?:readonly number[]
|
||||
defaultContextWindow?:number
|
||||
selectedContextWindow?:number
|
||||
maxInputTokens?:number
|
||||
}
|
||||
export interface ModelGroup{id:string;label?:string;order?:number;icon?:unknown;models:readonly ModelOption[];trailingActions?:readonly {id:string;label:string;icon?:unknown}[]}
|
||||
export interface ModelSelection{id:string;name:string;isThinking:boolean;reasoningEffort?:ModelEffort;tier?:string}
|
||||
export interface ModelGroupMenuEntry{id:string;label:string;order?:number;icon?:unknown;models:readonly ModelOption[];trailingActions?:readonly {id:string;label:string;icon?:unknown}[]}
|
||||
export interface ModelSelectorFooterAction{label:string;icon?:unknown;disabled?:boolean;onClick?():void}
|
||||
export interface ModelSelectorProvider{
|
||||
options?:readonly ModelOption[]
|
||||
groups?:readonly ModelGroup[]
|
||||
groupMenuItems?:readonly ModelGroupMenuEntry[]
|
||||
selectedModel?:ModelOption
|
||||
disabledTooltip?:string
|
||||
isMaxMode?:boolean
|
||||
footerAction?:ModelSelectorFooterAction
|
||||
onChange?(selection:ModelSelection):void
|
||||
onOpenChange?(isOpen:boolean):void
|
||||
onMaxModeChange?(isMaxMode:boolean):void
|
||||
onContextWindowChange?(modelId:string,contextWindow:number):void
|
||||
onModelTierUpgrade?(modelId:string):void
|
||||
onAction?(modelId:string,action:'edit'):void
|
||||
onClick?():void
|
||||
}
|
||||
export interface ModelSelectorDraftState{draft:{selections:{model:ModelSelection|null}}}
|
||||
export interface ModelSelectorInputBoxStore{api:{setModel(selection:ModelSelection):void}}
|
||||
export interface ModelSelectorRuntime{
|
||||
mode:'light'|'dark'
|
||||
prefixCls?:string
|
||||
useInputBoxStore():ModelSelectorInputBoxStore
|
||||
useInputBoxSelector<T>(selector:(state:ModelSelectorDraftState)=>T,isEqual?:(left:T,right:T)=>boolean):T
|
||||
useRenderDiag?(name:string,meta:Record<string,unknown>):void
|
||||
}
|
||||
export interface ModelSelectorConfig{prefixCls:string;mode:'light'|'dark';locale:string;t(key:string):string}
|
||||
export declare const modelSelectorLabels:Record<string,string>
|
||||
export declare function configureModelSelectorRuntime(runtime:ModelSelectorRuntime):ModelSelectorRuntime
|
||||
export declare function readModelSelectorRuntime():ModelSelectorRuntime
|
||||
export declare function ModelSelectorRuntimeProvider(props:{runtime:ModelSelectorRuntime;children?:ReactNode}):ReactNode
|
||||
export declare function useModelSelectorRuntime():ModelSelectorRuntime
|
||||
export declare function useModelSelectorConfig():ModelSelectorConfig
|
||||
export interface ModelSelectorProps{provider?:ModelSelectorProvider;className?:string;compact?:boolean;popoverPlacement?:string}
|
||||
export declare const ModelSelectorImpl:ComponentType<ModelSelectorProps>
|
||||
export declare const ModelSelectPopover:ComponentType<Record<string,unknown>>
|
||||
export declare const ModelSubMenu:ComponentType<Record<string,unknown>>
|
||||
export declare const ModelItem:ComponentType<Record<string,unknown>>
|
||||
export declare const ModelGroupMenuItem:ComponentType<Record<string,unknown>>
|
||||
export declare const ModelTierBadge:ComponentType<Record<string,unknown>>
|
||||
export declare const Menu:ComponentType<{children?:ReactNode;className?:string;style?:React.CSSProperties;minHeight?:number|string;maxHeight?:number|string}>
|
||||
export declare const MenuItem:ComponentType<{children?:ReactNode;icon?:ReactNode;extra?:ReactNode;disabled?:boolean;active?:boolean;onClick?():void;className?:string;style?:React.CSSProperties}>
|
||||
export declare const MenuDivider:ComponentType<{className?:string;style?:React.CSSProperties}>
|
||||
export declare const MenuGroup:ComponentType<{title?:ReactNode;children?:ReactNode;className?:string}>
|
||||
export declare const Popover:ComponentType<Record<string,unknown>>
|
||||
export declare const PopoverTrigger:ComponentType<{children?:ReactNode}>
|
||||
export declare const PopoverContent:ComponentType<{children?:ReactNode;className?:string}>
|
||||
export declare const Tooltip:ComponentType<Record<string,unknown>>
|
||||
export declare const ToggleSwitch:ComponentType<{checked?:boolean;onChange?(checked:boolean):void;ariaLabel?:string}>
|
||||
export declare const ReasoningEffortPanel:ComponentType<Record<string,unknown>>
|
||||
export declare const ContextWindowPanel:ComponentType<Record<string,unknown>>
|
||||
export declare const OverflowTitleSpan:ComponentType<{text:string;className?:string}>
|
||||
export declare const ModelBadge:ComponentType<{badge:ModelBadgeSpec}>
|
||||
export declare const useOverlayScrollbar:(ref:React.RefObject<HTMLElement|null>)=>{visible:boolean;thumbHeight:number;thumbTop:number}
|
||||
export declare function resolveModelIcon(option:{name:string;icon?:unknown;iconUrl?:string}):ReactNode
|
||||
export declare function getModelIconByName(name:string):ReactNode
|
||||
export declare function getDefaultIsThinking(option:ModelOption):boolean|undefined
|
||||
export declare function isReasoningConfigurable(option:ModelOption):boolean
|
||||
export declare function createModelSelection(option:ModelOption,isThinking:boolean,effort?:ModelEffort):ModelSelection
|
||||
export declare function collectModelOptions(options?:readonly ModelOption[],groups?:readonly ModelGroupMenuEntry[]):ModelOption[]
|
||||
export declare function resolveSelectedModelId(input:{selection?:ModelSelection|null;selectedModel?:ModelOption|null}):string|undefined
|
||||
export declare function resolveSelectedModelLabel(input:{selection?:ModelSelection|null;options?:readonly ModelOption[];groupMenuItems?:readonly ModelGroupMenuEntry[];selectedModel?:ModelOption|null}):string
|
||||
export declare function formatCredits(credits:string):string
|
||||
export declare function formatContextWindow(length:number):string
|
||||
export declare function getSupportedContextWindows(option:ModelOption):number[]
|
||||
`
|
||||
|
||||
// --------------------------------------------------------------------------- write
|
||||
manifest.hostBindings=[
|
||||
{kind:'react-context-provider',name:'ModelSelectorRuntimeProvider',contract:'mode, useInputBoxStore, useInputBoxSelector, useRenderDiag'},
|
||||
{kind:'react-context',name:'ModelSelectorConfigContext / useModelSelectorConfig',contract:'prefixCls, mode, locale, t'},
|
||||
{kind:'npm',name:'@floating-ui/react',names:HOST_FLOATING},
|
||||
{kind:'npm',name:'dompurify',names:['default']},
|
||||
{kind:'npm',name:'react',names:['* as React','* as JSX (react/jsx-runtime)']},
|
||||
]
|
||||
const outputs=[[FOLDER+'source.mjs',moduleSource],[FOLDER+'source.d.mts',types],[FOLDER+'source.css',css]]
|
||||
manifest.outputs=outputs.map(([file,text])=>({file,sha256:sha(text),bytes:Buffer.byteLength(text)}))
|
||||
outputs.push([FOLDER+'manifest.json',JSON.stringify(manifest,null,2)+'\n'])
|
||||
const check=process.argv.includes('--check')
|
||||
for(const[file,text]of outputs){
|
||||
if(check){
|
||||
let current
|
||||
try{current=fs.readFileSync(file,'utf8')}catch{throw Error('Recovered output missing '+file)}
|
||||
if(current!==text)throw Error('Recovered output drift '+file)
|
||||
}else fs.writeFileSync(file,text)
|
||||
}
|
||||
const summary={result:check?'PASS':'WRITTEN',sources:manifest.sources.length,bodies:manifest.bodies.length,adaptations:manifest.adaptations.length,outputs:outputs.length,audit,corpusReactAliases:coreIndex.reactAlias.size+primitiveIndex.reactAlias.size,corpusJsxAliases:coreIndex.jsxAlias.size+primitiveIndex.jsxAlias.size}
|
||||
summary.bytes=manifest.outputs.reduce((total,item)=>total+item.bytes,0)
|
||||
console.log(JSON.stringify(summary))
|
||||
@@ -0,0 +1,307 @@
|
||||
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 exportDir = resolve(rootDir, '.tmp/rea-css-recovered')
|
||||
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/composer/composer-source.css')
|
||||
const evidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
|
||||
const sourceSpecs = [
|
||||
{
|
||||
file: 'src-CyXHA0IC.css',
|
||||
selectors: [
|
||||
'.cr-input-box',
|
||||
'.cr-input-box__main',
|
||||
'.cr-input-container',
|
||||
'.cr-input-editor-host',
|
||||
'.cr-input-toolbar',
|
||||
'.cr-input-toolbar__left',
|
||||
'.cr-input-toolbar__right',
|
||||
'.cr-input-toolbar__send',
|
||||
'.cr-input-footer-item',
|
||||
'.cr-input-footer-item:hover:not(:disabled)',
|
||||
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled)',
|
||||
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled) .cr-input-footer-item__label',
|
||||
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled) .cr-input-footer-item__chevron',
|
||||
'.cr-input-footer-item:focus-visible',
|
||||
'.cr-input-footer-item__icon',
|
||||
'.cr-input-footer-item__label',
|
||||
'.cr-input-footer-item__label:after',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__icon',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__label',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__chevron',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__icon',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__label',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__chevron',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__icon',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__label',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__chevron',
|
||||
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item',
|
||||
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item:hover:not(:disabled)',
|
||||
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item.cr-input-footer-item--open',
|
||||
'.cr-send-button',
|
||||
'.cr-send-button:hover:not(:disabled)',
|
||||
'.cr-send-button:active:not(:disabled)',
|
||||
'.cr-send-button:disabled',
|
||||
'.cr-send-button--sending',
|
||||
'.cr-send-button--stop',
|
||||
'.cr-send-button--custom',
|
||||
'.cr-send-button--custom:hover:not(:disabled)',
|
||||
'.cr-send-button__icon',
|
||||
'.cr-send-button__icon>svg',
|
||||
'.cr-voice-trigger',
|
||||
'.cr-voice-trigger:hover:not(:disabled)',
|
||||
'.cr-voice-trigger--recording',
|
||||
'.cr-voice-trigger--recording:hover:not(:disabled)',
|
||||
'.cr-voice-trigger--loading',
|
||||
'.cr-voice-trigger>svg'
|
||||
]
|
||||
},
|
||||
{
|
||||
file: 'contexts-BkjwYSZT.css',
|
||||
selectors: [
|
||||
'.conversation-input-area',
|
||||
'.conversation-input-area__custom-floating',
|
||||
'.conversation-input-area__input-stack',
|
||||
'.conversation-input .cr-input-box',
|
||||
'.conversation-input__box .cr-input-box__main',
|
||||
'.conversation-input__box .cr-input-container',
|
||||
'.conversation-input__box .cr-permission-setting:not(.cr-permission-setting--danger):not(:disabled)',
|
||||
'.conversation-input__box .cr-permission-setting:not(.cr-permission-setting--danger):not(:disabled) .cr-input-footer-item__label'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
function normalizeSelector(selector) {
|
||||
return selector.trim().replace(/\s*([>+~])\s*/g, '$1').replace(/\s+/g, ' ')
|
||||
}
|
||||
|
||||
function sourceHash(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function formatRule(selector, declarations, source, sha256, note) {
|
||||
const comment = `/* REA ${source.file}:1:${source.column}; sha256 ${sha256}${note ? `; ${note}` : ''} */`
|
||||
const body = declarations.map((declaration) => ` ${declaration}`).join('\n')
|
||||
return `${comment}\n${selector} {\n${body}\n}`
|
||||
}
|
||||
|
||||
function declarationText(rule) {
|
||||
return rule.nodes
|
||||
.filter((node) => node.type === 'decl')
|
||||
.map((node) => `${node.prop}: ${node.value}${node.important ? ' !important' : ''};`)
|
||||
}
|
||||
|
||||
function collectRules(root, source, selectors) {
|
||||
const allowed = new Set(selectors.map(normalizeSelector))
|
||||
const records = []
|
||||
root.walkRules((rule) => {
|
||||
const selected = postcss.list.comma(rule.selector)
|
||||
.map(normalizeSelector)
|
||||
.filter((selector) => allowed.has(selector))
|
||||
if (selected.length === 0) return
|
||||
|
||||
const declarations = declarationText(rule)
|
||||
if (declarations.length === 0) return
|
||||
records.push({
|
||||
source,
|
||||
column: rule.source?.start?.column ?? 1,
|
||||
selectors: selected,
|
||||
declarations
|
||||
})
|
||||
})
|
||||
return records
|
||||
}
|
||||
|
||||
function findRule(root, selector) {
|
||||
const expected = normalizeSelector(selector)
|
||||
let match
|
||||
root.walkRules((rule) => {
|
||||
if (match) return
|
||||
if (postcss.list.comma(rule.selector).some((item) => normalizeSelector(item) === expected)) match = rule
|
||||
})
|
||||
if (!match) throw new Error(`REA stylesheet is missing required selector: ${selector}`)
|
||||
return match
|
||||
}
|
||||
|
||||
function declaration(rule, property) {
|
||||
const match = rule.nodes.find((node) => node.type === 'decl' && node.prop === property)
|
||||
if (!match) throw new Error(`REA rule ${rule.selector} is missing ${property}`)
|
||||
return match.value
|
||||
}
|
||||
|
||||
function resolveToken(token, tokens, seen = new Set()) {
|
||||
if (seen.has(token)) throw new Error(`Cyclic REA token reference: ${token}`)
|
||||
const value = tokens.get(token)
|
||||
if (!value) throw new Error(`REA token is missing: ${token}`)
|
||||
const simpleVar = /^var\((--[\w-]+)\)$/.exec(value.trim())
|
||||
if (!simpleVar) return value.trim()
|
||||
seen.add(token)
|
||||
return resolveToken(simpleVar[1], tokens, seen)
|
||||
}
|
||||
|
||||
const manifestBytes = await readFile(resolve(exportDir, 'manifest.json'))
|
||||
const manifest = JSON.parse(manifestBytes.toString('utf8'))
|
||||
if (manifest.sourceEvidenceId !== evidenceId) {
|
||||
throw new Error(`REA CSS evidence changed: expected ${evidenceId}, found ${manifest.sourceEvidenceId ?? 'none'}`)
|
||||
}
|
||||
|
||||
const sources = new Map()
|
||||
for (const spec of sourceSpecs) {
|
||||
const bytes = await readFile(resolve(exportDir, spec.file))
|
||||
const record = manifest.files?.find((file) => file.file === spec.file)
|
||||
if (!record?.sha256) throw new Error(`REA manifest has no SHA-256 for ${spec.file}`)
|
||||
const actualHash = sourceHash(bytes)
|
||||
if (actualHash !== record.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch for ${spec.file}: manifest ${record.sha256}, actual ${actualHash}`)
|
||||
}
|
||||
sources.set(spec.file, {
|
||||
...spec,
|
||||
bytes,
|
||||
sha256: actualHash,
|
||||
root: postcss.parse(bytes.toString('utf8'), { from: spec.file })
|
||||
})
|
||||
}
|
||||
|
||||
const extraFiles = ['src-Bh3YZ35H.css', 'tokens-Df-i-zsO.css']
|
||||
for (const file of extraFiles) {
|
||||
const bytes = await readFile(resolve(exportDir, file))
|
||||
const record = manifest.files?.find((item) => item.file === file)
|
||||
if (!record?.sha256) throw new Error(`REA manifest has no SHA-256 for ${file}`)
|
||||
const actualHash = sourceHash(bytes)
|
||||
if (actualHash !== record.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch for ${file}: manifest ${record.sha256}, actual ${actualHash}`)
|
||||
}
|
||||
sources.set(file, {
|
||||
file,
|
||||
bytes,
|
||||
sha256: actualHash,
|
||||
root: postcss.parse(bytes.toString('utf8'), { from: file })
|
||||
})
|
||||
}
|
||||
|
||||
const cy = sources.get('src-CyXHA0IC.css')
|
||||
const contexts = sources.get('contexts-BkjwYSZT.css')
|
||||
const button = sources.get('src-Bh3YZ35H.css')
|
||||
const tokensRoot = sources.get('tokens-Df-i-zsO.css').root
|
||||
const tokens = new Map()
|
||||
tokensRoot.walkDecls((decl) => {
|
||||
if (decl.prop.startsWith('--wb-') && !tokens.has(decl.prop)) tokens.set(decl.prop, decl.value)
|
||||
})
|
||||
|
||||
const sourceRules = sourceSpecs.flatMap((spec) => {
|
||||
const source = sources.get(spec.file)
|
||||
return collectRules(source.root, source, spec.selectors)
|
||||
})
|
||||
for (const spec of sourceSpecs) {
|
||||
const found = new Set(sourceRules
|
||||
.filter((record) => record.source.file === spec.file)
|
||||
.flatMap((record) => record.selectors))
|
||||
const missing = spec.selectors.map(normalizeSelector).filter((selector) => !found.has(selector))
|
||||
if (missing.length) throw new Error(`REA stylesheet ${spec.file} is missing selected rules: ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
const slateEditorRule = findRule(cy.root, '.cr-input-editor-host [data-slate-editor]')
|
||||
const focusedContainerRule = findRule(cy.root, '.cr-input-container--focused')
|
||||
const sendButtonRule = findRule(cy.root, '.cr-send-button')
|
||||
const sendIconRule = findRule(cy.root, '.cr-send-button__icon')
|
||||
const nativeButtonRule = findRule(button.root, '.wb-button')
|
||||
const iconOnlyRule = findRule(button.root, '.wb-button--icon-only')
|
||||
const iconOnlyMediumRule = findRule(button.root, '.wb-button--icon-only.wb-button--medium')
|
||||
const plainButtonRule = findRule(button.root, '.wb-button--plain')
|
||||
const circleButtonRule = findRule(button.root, '.wb-button--circle')
|
||||
const footerItemRule = findRule(cy.root, '.cr-input-footer-item')
|
||||
const sendSize = resolveToken('--wb-button-height-medium', tokens)
|
||||
const sendRadius = declaration(circleButtonRule, 'border-radius')
|
||||
const iconOnlyWidth = declaration(iconOnlyMediumRule, 'width')
|
||||
const iconOnlyHeight = declaration(iconOnlyMediumRule, 'height')
|
||||
const footerItemHeight = declaration(footerItemRule, 'height')
|
||||
if (sendSize !== '32px' || footerItemHeight !== sendSize || iconOnlyWidth !== 'var(--wb-button-height-medium)' || iconOnlyHeight !== iconOnlyWidth || sendRadius !== '50%') {
|
||||
throw new Error(`Unexpected REA send-button sizing: height ${sendSize}, button width ${iconOnlyWidth}, height ${iconOnlyHeight}`)
|
||||
}
|
||||
if (declaration(sendButtonRule, '--wb-button-icon-size-medium') !== '32px' || declaration(sendIconRule, 'width') !== '32px') {
|
||||
throw new Error('REA send-button icon sizing changed; review the native-button projection before regenerating')
|
||||
}
|
||||
|
||||
const sourceNotes = [
|
||||
`CSS Evidence ID ${evidenceId}`,
|
||||
`capture target ${manifest.target?.title ?? 'unknown'}; provider ${manifest.provider?.id ?? 'unknown'}`,
|
||||
...[...sources.values()].map((source) => `${source.file} sha256=${source.sha256}`),
|
||||
'Only selected input-area rules are recovered; source sheets are not imported wholesale.',
|
||||
'WorkBuddy input-wrapper wb-color tokens use the recovered scoped theme declarations.',
|
||||
'The native textarea receives declarations selected from .cr-input-editor-host [data-slate-editor]; no Slate data attributes are added.',
|
||||
'The native send button receives the required .wb-button base, icon-only, medium, and plain declarations as a local projection.'
|
||||
]
|
||||
|
||||
const cssSections = [
|
||||
`/*\n * Generated by scripts/rea/recover-composer-styles.mjs.\n${sourceNotes.map((note) => ` * ${note}`).join('\n')}\n */`,
|
||||
...sourceRules.map((record) => formatRule(
|
||||
record.selectors.join(', '),
|
||||
record.declarations,
|
||||
{ file: record.source.file, column: record.column },
|
||||
record.source.sha256
|
||||
)),
|
||||
formatRule(
|
||||
'.cr-input-container:focus-within',
|
||||
declarationText(focusedContainerRule),
|
||||
{ file: cy.file, column: focusedContainerRule.source?.start?.column ?? 1 },
|
||||
cy.sha256,
|
||||
'projected from the original focus state class for a native focusable container'
|
||||
),
|
||||
formatRule(
|
||||
'.conversation-input__box .cr-input-container:focus-within',
|
||||
declarationText(findRule(contexts.root, '.conversation-input__box .cr-input-container--focused')),
|
||||
{ file: contexts.file, column: findRule(contexts.root, '.conversation-input__box .cr-input-container--focused').source?.start?.column ?? 1 },
|
||||
contexts.sha256,
|
||||
'selector projection of WorkBuddy focus-border class'
|
||||
),
|
||||
formatRule(
|
||||
'.chat-pane[data-cr-theme=dark] .conversation-input__box .cr-input-container:focus-within',
|
||||
declarationText(findRule(contexts.root, '.vscode-dark .conversation-input__box .cr-input-container--focused')),
|
||||
{ file: contexts.file, column: findRule(contexts.root, '.vscode-dark .conversation-input__box .cr-input-container--focused').source?.start?.column ?? 1 },
|
||||
contexts.sha256,
|
||||
'dark theme selector projection'
|
||||
),
|
||||
formatRule(
|
||||
'.cr-input-editor-host > .wb-composer__input',
|
||||
declarationText(slateEditorRule),
|
||||
{ file: cy.file, column: slateEditorRule.source?.start?.column ?? 1 },
|
||||
cy.sha256,
|
||||
'selector projection from [data-slate-editor] to the native textarea; attribute intentionally omitted'
|
||||
),
|
||||
formatRule(
|
||||
'.wb-composer__add-button.cr-input-footer-item',
|
||||
[
|
||||
`width: ${footerItemHeight};`,
|
||||
`min-width: ${footerItemHeight};`,
|
||||
'padding: 0;'
|
||||
],
|
||||
{ file: cy.file, column: footerItemRule.source?.start?.column ?? 1 },
|
||||
cy.sha256,
|
||||
'icon-only projection retains the original 32px footer-item height'
|
||||
),
|
||||
`/* Native button adapter from ${button.file} and ${sources.get('tokens-Df-i-zsO.css').file}; .wb-button base/icon-only/plain/circle declarations, resolved medium height ${sendSize}, border radius ${sendRadius}. */\n.cr-send-button {\n${[
|
||||
'box-sizing',
|
||||
'cursor',
|
||||
'white-space',
|
||||
'user-select',
|
||||
'border',
|
||||
'justify-content',
|
||||
'align-items',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'text-decoration',
|
||||
'display'
|
||||
].map((property) => ` ${property}: ${declaration(nativeButtonRule, property)};`).join('\n')}\n width: ${sendSize};\n min-width: ${declaration(iconOnlyRule, 'min-width')};\n height: ${sendSize};\n padding: ${declaration(iconOnlyRule, 'padding')};\n border-radius: ${sendRadius};\n color: ${declaration(plainButtonRule, 'color')};\n background-color: ${declaration(plainButtonRule, 'background-color')};\n}`
|
||||
]
|
||||
|
||||
const output = `${cssSections.join('\n\n')}\n`
|
||||
postcss.parse(output, { from: relative(rootDir, outputPath) })
|
||||
await writeFile(outputPath, output, 'utf8')
|
||||
console.log(`Wrote ${relative(rootDir, outputPath)} from ${sources.size} REA CSS exports (${evidenceId}).`)
|
||||
@@ -0,0 +1,35 @@
|
||||
// Select literal locale strings from previously exported REA evidence; never execute target scripts.
|
||||
import fs from 'node:fs/promises'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
const bundle = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const script = bundle.items.find(item => item.file_path?.endsWith('i18n-Cxy1Uuvv.js'))
|
||||
if (!script?.source?.included) throw new Error('Missing REA-captured conversation locale')
|
||||
const source = script.source.artifact.text
|
||||
const hash = crypto.createHash('sha256').update(source).digest('hex')
|
||||
if (hash !== script.source.artifact.sha256) throw new Error('REA source digest mismatch')
|
||||
const literal = node => node?.type === 'StringLiteral' ? node.value : node?.type === 'TemplateLiteral' && node.expressions.length === 0 ? node.quasis[0].value.cooked : undefined
|
||||
const objects = []
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'ObjectExpression') {
|
||||
const values = Object.fromEntries(node.properties.filter(p => p.type === 'ObjectProperty' && typeof p.key.value === 'string' && literal(p.value) !== undefined).map(p => [p.key.value, literal(p.value)]))
|
||||
if (values['task.creating']) objects.push(values)
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
}
|
||||
visit(parse(source, { sourceType: 'module' }).program)
|
||||
const chinese = objects.find(values => values['task.creating'] === '正在创建任务')
|
||||
const english = objects.find(values => values['task.creating'] === 'Creating task')
|
||||
if (!chinese || !english) throw new Error('Selected literal locale dictionaries not found')
|
||||
const wanted = key => /^(tool\.planCreate\.|tool\.task\.|task\.|action\.(accept|reject)$|message\.loading\.thinking$|common\.loading$)/.test(key)
|
||||
const zh = Object.fromEntries(Object.entries(chinese).filter(([key]) => wanted(key)))
|
||||
const en = Object.fromEntries(Object.entries(english).filter(([key]) => wanted(key)))
|
||||
const file = 'src/renderer/src/workbuddy/conversation/labels.ts'
|
||||
const header = `// Generated from REA ${bundle.evidence_id}.\n// i18n-Cxy1Uuvv.js SHA-256 ${hash}.\n// Default zh-cn matches the captured package default and YCode's current Chinese interface.\n`
|
||||
const text = header + `const zhCN = ${JSON.stringify(zh, null, 2)} as const\nconst en = ${JSON.stringify(en, null, 2)} as const\n\nexport function conversationLabel(key: keyof typeof zhCN, params: Record<string, string | number> = {}, locale: 'zh-cn' | 'en' = 'zh-cn'): string {\n const dictionary = locale === 'en' ? en : zhCN\n return dictionary[key].replace(/\\{([^{}]+)\\}/g, (match, name: string) => params[name] === undefined ? match : String(params[name]))\n}\n`
|
||||
await fs.writeFile(file, text)
|
||||
console.log(JSON.stringify({ file, evidence: bundle.evidence_id, sourceSha256: hash, keys: Object.keys(zh).length }))
|
||||
@@ -0,0 +1,83 @@
|
||||
// Project rules from already-exported REA CSS. Never accesses WorkBuddy.
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const directory = path.resolve('.tmp/rea-css-recovered')
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(directory, 'manifest.json'), 'utf8'))
|
||||
const destination = path.resolve('src/renderer/src/workbuddy/conversation/source-styles.css')
|
||||
const families = /\.(?:cr-(?:frame|agent|self-|user-content|collapse|reasoning|tool-exp|tool-head|message-list|document|toolbar|icon-button|text-block)|cb-plan-|cb-exploration-status|conversation-timeline|conversation-high-credit-approval)/
|
||||
const output = postcss.root()
|
||||
const selected = []
|
||||
for (const filename of ['src-CBaV_A3S.css', 'src-CyXHA0IC.css', 'contexts-BkjwYSZT.css']) {
|
||||
const metadata = manifest.files.find(file => file.file === filename)
|
||||
if (!metadata) throw new Error(`REA CSS not captured: ${filename}`)
|
||||
const bytes = await fs.readFile(path.join(directory, filename))
|
||||
if (crypto.createHash('sha256').update(bytes).digest('hex') !== metadata.sha256) throw new Error(`REA CSS digest mismatch: ${filename}`)
|
||||
const source = postcss.parse(bytes, { from: filename })
|
||||
let rules = 0
|
||||
function project(node) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = node.selectors ?? []
|
||||
const actionMap = { '._assistantFeedback_1jhpu_1': '.msg--assistant .wb-message-actions', '._copyButton_1ncbp_1': '.wb-message-action[data-action=copy]', '._ratingButton_13zum_7': '.wb-message-action[aria-pressed]', '._actionButton_jnle9_7': '.wb-message-action[data-action=regenerate]', '._iconOnly_jnle9_27': '.wb-message-action' }
|
||||
const actionSelectors = selectors.filter(selector => /\._(?:assistantFeedback_1jhpu_1|copyButton_1ncbp_1|ratingButton_13zum_7|actionButton_jnle9_7)/.test(selector)).map(selector => {
|
||||
for (const [from, to] of Object.entries(actionMap)) selector = selector.replaceAll(from, to)
|
||||
return selector
|
||||
})
|
||||
if (actionSelectors.length) { rules++; return node.clone({ selector: actionSelectors.join(',') }) }
|
||||
const userProjection = {
|
||||
'._editable_1cxd2_1': '.chat-pane .yc-user-editable',
|
||||
'._editable_1cxd2_1 *': '.chat-pane .yc-user-editable *',
|
||||
'._editable_1cxd2_1 p': '.chat-pane .yc-user-editable p',
|
||||
'._editable_1cxd2_1 span': '.chat-pane .yc-user-editable span',
|
||||
'._editable_1cxd2_1 [data-slate-node=text]': '.chat-pane .yc-user-leaf',
|
||||
'._editable_1cxd2_1[contenteditable=false] p': '.chat-pane .yc-user-editable p',
|
||||
'._editable_1cxd2_1[contenteditable=false] [data-slate-node=text]': '.chat-pane .yc-user-leaf',
|
||||
'._editable_1cxd2_1[contenteditable=false] [data-slate-leaf=true]': '.chat-pane .yc-user-leaf > span',
|
||||
'._editable_1cxd2_1[contenteditable=false] [data-slate-string=true]': '.chat-pane .yc-user-leaf > span',
|
||||
}
|
||||
const projected = selectors.filter(selector => userProjection[selector]).map(selector => userProjection[selector])
|
||||
if (projected.length) { rules++; return node.clone({ selector: [...new Set(projected)].join(',') }) }
|
||||
let theme
|
||||
if (selectors.includes(':root')) theme = '.chat-pane.cr-theme'
|
||||
else if (selectors.includes('.light') || selectors.includes(':root.light') || selectors.includes('.cr-theme[data-cr-theme=light]')) theme = '.chat-pane.cr-theme[data-cr-theme=light]'
|
||||
else if (selectors.includes('.dark') || selectors.includes(':root.dark') || selectors.includes('.cr-theme[data-cr-theme=dark]')) theme = '.chat-pane.cr-theme[data-cr-theme=dark]'
|
||||
if (theme) {
|
||||
const declarations = node.nodes.filter(d => d.type === 'decl' && d.prop.startsWith('--') && !/^--(?:wb-font-(?:size|line-height)-|cb-md-root$)/.test(d.prop))
|
||||
if (!declarations.length) return
|
||||
rules++
|
||||
return postcss.rule({ selector: theme, nodes: declarations.map(d => d.clone()) })
|
||||
}
|
||||
const relevant = selectors.filter(selector => families.test(selector) && !/hydration|virtual-|share-mode|loading-more/.test(selector))
|
||||
if (!relevant.length) return
|
||||
rules++
|
||||
return node.clone({ selector: relevant.join(',') })
|
||||
}
|
||||
if (node.type === 'atrule' && /keyframes$/.test(node.name)) return
|
||||
if (node.type === 'atrule' && node.nodes) {
|
||||
const children = node.nodes.map(project).filter(Boolean)
|
||||
if (children.length) return node.clone({ nodes: children })
|
||||
}
|
||||
}
|
||||
for (const node of source.nodes) {
|
||||
const result = project(node)
|
||||
if (result) output.append(result)
|
||||
}
|
||||
const animations = []
|
||||
output.walkDecls(d => { if (/^animation(?:-name)?$/.test(d.prop)) animations.push(d.value) })
|
||||
source.walkAtRules(/keyframes$/, node => {
|
||||
if (animations.some(value => value.split(/[\s,]+/).includes(node.params))) output.append(node.clone())
|
||||
})
|
||||
selected.push({ file: filename, sha256: metadata.sha256, asarComparison: metadata.asarComparison?.status ?? 'not-compared', rules })
|
||||
}
|
||||
const header = `/* Generated by recover-conversation-styles.mjs from REA ${manifest.sourceEvidenceId}.
|
||||
* Original selected rule bodies and conditional ancestors are preserved.
|
||||
* Readonly user-editor selectors are projected onto static paragraph/leaf wrappers.
|
||||
* Global theme declarations are scoped to .chat-pane; font-size controls remain inherited.
|
||||
* Runtime/ASAR mismatches remain recorded in the source manifest.
|
||||
*/\n`
|
||||
await fs.writeFile(destination, header + output.toString() + '\n')
|
||||
const record = { evidenceId: manifest.sourceEvidenceId, selected, destination, transformations: ['Theme selector scope only; custom-property values preserved.', 'Only listed conversation families selected; unimplemented virtualization excluded.', 'Conditional at-rule ancestors and animation keyframes retained.'] }
|
||||
await fs.writeFile(path.join(directory, 'conversation-projection.json'), JSON.stringify(record, null, 2) + '\n')
|
||||
console.log(JSON.stringify(record, null, 2))
|
||||
@@ -0,0 +1,239 @@
|
||||
/** Recovery consumes retained REA artifacts only; no original application runtime or filesystem reads. */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import assert from 'node:assert/strict'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const directory = 'src/renderer/src/capabilities'
|
||||
const check = process.argv.includes('--check')
|
||||
const sha = value => crypto.createHash('sha256').update(value).digest('hex')
|
||||
const bundle = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const pins = {
|
||||
'contexts-BMeWSUHk.js': '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b',
|
||||
'src-DPDj7_qD.js': '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0',
|
||||
'src-0FGg1seX.js': 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
||||
}
|
||||
const manifest = {
|
||||
version: 'WorkBuddy 5.8.2', rootSha256: '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',
|
||||
evidenceId: bundle.evidence_id, sources: [], bodies: [], dictionaries: [], css: [],
|
||||
adaptations: [
|
||||
'Only selected component/function bodies execute, using the host React singleton. Original module initialization and global service hooks never execute.',
|
||||
'CreatedExpertsPanel changes one static SVG URL to the previously REA-exported identical local SVG.',
|
||||
'The public facade omits all cloud/share callbacks; ic and oc explicitly return false. Cloud upload/share adapters cannot report success.',
|
||||
'Local host adapters provide portal positioning/dismiss/focus, tooltip title, locale, local avatar URL resolution, preview and IPC callbacks.',
|
||||
'Original Dropdown, DropdownMenuList/Label, ConfirmDialog and cb Button bodies are preserved; Popover is an explicitly bounded local host adapter.',
|
||||
'Original cb theme variable declarations are restored locally; IDE error foreground/background bridge to shipped WB semantic tokens. ConfirmDialog receives the actual host theme. This host bridge is not a runtime color-parity claim.',
|
||||
'Styles retain declarations and conditions with a local selector scope. Local portal wrappers carry both capability and created-expert source scopes.'
|
||||
]
|
||||
}
|
||||
function load(name) {
|
||||
const item = bundle.items.find(item => item.file_path.endsWith(name)), artifact = item?.source?.artifact
|
||||
assert.ok(artifact && sha(artifact.text) === pins[name] && artifact.sha256 === pins[name], `REA source identity: ${name}`)
|
||||
manifest.sources.push({ file: name, sha256: artifact.sha256 })
|
||||
const entries = new Map(), strings = new Map(), objects = new Map(), dictionaries = []
|
||||
function walk(n) {
|
||||
if (!n || typeof n !== 'object') return
|
||||
if (n.type === 'FunctionDeclaration' && n.id) entries.set(n.id.name, n)
|
||||
if (n.type === 'AssignmentExpression' && n.left.type === 'Identifier') {
|
||||
entries.set(n.left.name, n)
|
||||
if (n.right.type === 'ObjectExpression') objects.set(n.left.name, n.right)
|
||||
const value = literal(n.right)
|
||||
if (typeof value === 'string') strings.set(n.left.name, value)
|
||||
}
|
||||
if (n.type === 'ObjectExpression' && n.properties.some(p => p.key?.value === 'myExperts.created.ctxView')) dictionaries.push(n)
|
||||
for (const [key, value] of Object.entries(n)) {
|
||||
if (key === 'loc' || key === 'comments') continue
|
||||
if (Array.isArray(value)) value.forEach(walk)
|
||||
else if (value && typeof value === 'object') walk(value)
|
||||
}
|
||||
}
|
||||
walk(parse(artifact.text, { sourceType: 'module' }).program)
|
||||
return { ...artifact, name, entries, strings, objects, dictionaries }
|
||||
}
|
||||
function literal(n) {
|
||||
return n?.type === 'StringLiteral' ? n.value : n?.type === 'TemplateLiteral' && !n.expressions.length ? n.quasis[0].value.cooked : undefined
|
||||
}
|
||||
const contexts = load('contexts-BMeWSUHk.js'), icons = load('src-DPDj7_qD.js'), primitives = load('src-0FGg1seX.js')
|
||||
const emptyAsset=directory+'/assets/empty-created-expert.svg'
|
||||
assert.equal(sha(fs.readFileSync(emptyAsset)),'157f07ef40d6cdf5f3aed362509861151fbdde0a8a9c89c98c26de6f35f664b7')
|
||||
manifest.localDependencies=[{file:emptyAsset,sha256:sha(fs.readFileSync(emptyAsset)),evidenceId:'ev_e5515aa84117724605f73439d065222c2e23b6ab8226be5d03f1ee0c7b387a9b'},{file:directory+'/assets/source-ui.mjs',sha256:sha(fs.readFileSync(directory+'/assets/source-ui.mjs')),recovery:'scripts/rea/recover-capability-center.mjs'}]
|
||||
function recover(source, name, adapt = value => value) {
|
||||
const n = source.entries.get(name)
|
||||
assert.ok(n, `Source body: ${name}`)
|
||||
const original = source.text.slice(n.start, n.end), body = adapt(original)
|
||||
manifest.bodies.push({ file: source.name, name, start: n.start, end: n.end, sha256: sha(original), ...(body !== original ? { adaptedSha256: sha(body) } : {}) })
|
||||
return { code: n.type === 'FunctionDeclaration' ? body : `const ${body};`, body }
|
||||
}
|
||||
function classMap(source, name) {
|
||||
const n = source.objects.get(name)
|
||||
assert.ok(n, `Class map: ${name}`)
|
||||
return Object.fromEntries(n.properties.map(p => {
|
||||
const value = p.value.type === 'Identifier' ? source.strings.get(p.value.name) : literal(p.value)
|
||||
assert.equal(typeof value, 'string')
|
||||
return [p.key.name ?? p.key.value, value]
|
||||
}))
|
||||
}
|
||||
const ws = classMap(primitives, 'WS'), ei = classMap(contexts, 'EI')
|
||||
let selected = []
|
||||
for (const name of ['resolveCardI18nText', 'resolveShareExpertName', 'resolveLocalDeleteContentKey', 'MoreDotsIcon$1', 'CloudUploadIcon', 'CreatedExpertDeleteDialog', 'CreatedExpertCard']) selected.push(recover(contexts, name))
|
||||
selected.push(recover(contexts, 'CreatedExpertsPanel', text => text.replace('new URL(`empty-created-expert-BOI6XYXm.svg`,import.meta.url)', 'new URL(`./empty-created-expert.svg`,import.meta.url)')))
|
||||
for (const name of ['Dropdown', 'DropdownMenuList', 'DropdownItemLabel', 'highlightLabelText', 'useDropdownHoverClose', 'createIcon', 'resolveSize', 'bn']) selected.push(recover(icons, name))
|
||||
for (const name of ['ConfirmDialog', 'Ku', 'CloseIcon$2']) selected.push(recover(primitives, name))
|
||||
const sourceIconNames = ['Au', 'bF', 'Pl', 'wd', 'Zl', 'ShareIcon']
|
||||
for (const name of sourceIconNames) selected.push(recover(icons, name))
|
||||
assert.equal(manifest.bodies.find(item => item.name === 'CreatedExpertCard').sha256, 'ecbaba2a34fe1e6afdb608845bf52e3d1e58518c17f8f7108d4ef43b865d3128')
|
||||
assert.equal(manifest.bodies.find(item => item.name === 'CreatedExpertsPanel').sha256, '73d81b6f1c3205fa1b8c67639fa821af60f0b29d236be4d2260406a75f579e47')
|
||||
|
||||
const wantedKeys = new Set(['expertSection', 'teamSection', 'more', 'common.cancel', 'noTeamSearchResult', 'noSearchResult', 'myExperts.created.emptyTitle', 'myExperts.created.teamEmptyTitle'])
|
||||
for (const { body } of selected) for (const match of body.matchAll(/`((?:myExperts\.|share\.|common\.)[^`]+)`/g)) wantedKeys.add(match[1])
|
||||
const locales = {}
|
||||
for (const n of contexts.dictionaries) {
|
||||
const entries = Object.fromEntries(n.properties.filter(p => p.type === 'ObjectProperty').map(p => [p.key.name ?? p.key.value, literal(p.value)]))
|
||||
const locale = entries['myExperts.created.ctxView'] === '查看专家' ? 'zh' : entries['myExperts.created.ctxView'] === 'View Expert' ? 'en' : null
|
||||
if (!locale) continue
|
||||
locales[locale] = Object.fromEntries([...wantedKeys].filter(key => entries[key] !== undefined).map(key => [key, entries[key]]))
|
||||
manifest.dictionaries.push({ locale, file: contexts.name, start: n.start, end: n.end, sha256: sha(contexts.text.slice(n.start, n.end)), keys: Object.keys(locales[locale]) })
|
||||
}
|
||||
assert.equal(locales.zh['myExperts.created.ctxDelete'], '删除')
|
||||
const aliases = new Map()
|
||||
for (const { body } of selected) {
|
||||
for (const m of body.matchAll(/\b([\w$]+)\.(useState|useRef|useCallback|useMemo|useEffect|useLayoutEffect|useId|forwardRef|cloneElement|isValidElement)\b/g)) aliases.set(m[1], 'React')
|
||||
for (const m of body.matchAll(/\b([\w$]+)\.(jsx|jsxs)\b/g)) aliases.set(m[1], 'JSX')
|
||||
}
|
||||
aliases.set('RR', 'JSX'); aliases.set('BR', 'JSX'); aliases.set('qS', 'PortalHost')
|
||||
const bindingPreamble = [...aliases].map(([name, target]) => `const ${name}=${target};`).join('\n')
|
||||
const body = `// Generated by scripts/rea/recover-created-experts.mjs; pinned REA source identities are in the adjacent manifest.
|
||||
import * as React from 'react';
|
||||
import * as JSX from 'react/jsx-runtime';
|
||||
import {createPortal} from 'react-dom';
|
||||
import {computePosition,offset,flip,shift,autoUpdate} from '@floating-ui/react';
|
||||
import {ExpertCard as ExpertCard$1,ExpertListEmptyState,filterExpertsByKeyword} from './source-ui.mjs';
|
||||
import '../created-expert-source.css';
|
||||
const dictionaries=${JSON.stringify(locales)};
|
||||
const EI=${JSON.stringify(ei)},WS=${JSON.stringify(ws)};
|
||||
const safeAsset=value=>/^(?:data:image\\/(?:png|jpeg|webp|gif|svg\\+xml);|blob:|file:|ycode:)/i.test(value||'')?value:'';
|
||||
const defaultRef={current:{}};
|
||||
function PortalScope({children}){
|
||||
const [theme,setTheme]=React.useState(()=>document.documentElement.dataset.theme==='dark'?'dark':'light');
|
||||
React.useEffect(()=>{const observer=new MutationObserver(()=>setTheme(document.documentElement.dataset.theme==='dark'?'dark':'light'));observer.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});return()=>observer.disconnect()},[]);
|
||||
return JSX.jsx('div',{className:'yc-capability-center yc-created-expert-source expert-center-'+theme,style:{display:'contents'},children});
|
||||
}
|
||||
const PortalHost={createPortal:(children,container)=>createPortal(JSX.jsx(PortalScope,{children}),container)};
|
||||
function HostPopover({trigger,open,onOpenChange,disabled,placement='bottom-start',offsetDistance=4,className='',style,children,role='menu',ariaLabel,onFloatingMouseEnter,onFloatingMouseLeave}){
|
||||
const anchor=React.useRef(null),floating=React.useRef(null),id=React.useId(),[position,setPosition]=React.useState({left:0,top:0,visibility:'hidden'}),[phase,setPhase]=React.useState('initial'),[present,setPresent]=React.useState(open);
|
||||
React.useLayoutEffect(()=>{if(open){setPresent(true);setPhase('initial');const raf=requestAnimationFrame(()=>setPhase('open'));return()=>cancelAnimationFrame(raf)}setPhase('close');const timer=setTimeout(()=>setPresent(false),window.matchMedia('(prefers-reduced-motion: reduce)').matches?1:150);return()=>clearTimeout(timer)},[open]);
|
||||
React.useLayoutEffect(()=>{if(!present||!anchor.current||!floating.current)return;let cancelled=false;const update=()=>computePosition(anchor.current,floating.current,{strategy:'fixed',placement,middleware:[offset(offsetDistance),flip(),shift({padding:8})]}).then(({x,y})=>{if(!cancelled)setPosition({left:x,top:y,visibility:'visible'})});const stop=autoUpdate(anchor.current,floating.current,update);return()=>{cancelled=true;stop()}},[present,placement,offsetDistance]);
|
||||
React.useEffect(()=>{if(!open)return;const close=e=>{if(!anchor.current?.contains(e.target)&&!floating.current?.contains(e.target))onOpenChange?.(false)};const key=e=>{if(e.key==='Escape'){e.stopPropagation();onOpenChange?.(false);anchor.current?.focus()}if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)&&floating.current?.contains(document.activeElement)){const items=[...floating.current.querySelectorAll('[role=menuitem]:not([aria-disabled=true])')];if(!items.length)return;e.preventDefault();let index=items.indexOf(document.activeElement);index=e.key==='Home'?0:e.key==='End'?items.length-1:(index+(e.key==='ArrowDown'?1:-1)+items.length)%items.length;items[index]?.focus()}};document.addEventListener('pointerdown',close,true);document.addEventListener('keydown',key,true);return()=>{document.removeEventListener('pointerdown',close,true);document.removeEventListener('keydown',key,true)}},[open,onOpenChange]);
|
||||
const probe=React.cloneElement(trigger,{ref:anchor,'aria-expanded':!!open,'aria-haspopup':role,'aria-controls':open?id:undefined,onClick:e=>{trigger.props.onClick?.(e);if(!disabled)onOpenChange?.(!open)},onKeyDown:e=>{trigger.props.onKeyDown?.(e);if(['ArrowDown','ArrowUp'].includes(e.key)&&!disabled){e.preventDefault();onOpenChange?.(true);requestAnimationFrame(()=>floating.current?.querySelector('[role=menuitem]')?.focus())}}});
|
||||
return JSX.jsxs(JSX.Fragment,{children:[probe,present&&PortalHost.createPortal(JSX.jsx('div',{ref:floating,id,role,'aria-label':ariaLabel,className:'wb-popover wb-popover--placement-bottom wb-popover--expand-'+phase+' '+className,style:{...style,...position,position:'fixed',zIndex:100010,transformOrigin:'top left'},onClick:e=>e.stopPropagation(),onMouseEnter:onFloatingMouseEnter,onMouseLeave:onFloatingMouseLeave,children}),document.body)]});
|
||||
}
|
||||
function createBindings(hostRef){
|
||||
const host=()=>hostRef.current;
|
||||
function useLocale$1(){return host().locale==='en'?'en':'zh'}
|
||||
function useExpertI18n(){const locale=useLocale$1();return React.useCallback((key,values)=>{let text=dictionaries[locale]?.[key]??dictionaries.zh[key]??key;for(const[k,v]of Object.entries(values??{}))text=text.replaceAll('{'+k+'}',String(v));return text},[locale])}
|
||||
const J=({message,type})=>host().notify?.(message,type),Ua={warning:message=>J({message,type:'warning'})};
|
||||
const unavailable=()=>Promise.resolve({success:false,errorMessage:'云端专家服务暂不可用'});
|
||||
const resetSecurityState=()=>{},resolveSecurityDialog=()=>{};
|
||||
const useShareExpert=()=>({sharing:false,securityBlocked:false,shareExpert:unavailable,securityDialog:null,resetSecurityState,resolveSecurityDialog});
|
||||
const ic=()=>false,oc=()=>false;
|
||||
const al=()=>({adapter:{},reporter:{reportEvent:()=>{},Events:{}},wb:{platform:{type:'desktop'},shell:{openPath:async value=>{if(!host().openPath)throw Error('打开文件夹暂不可用');return host().openPath(value)}}}});
|
||||
const resolveExpertAssetUrl=value=>host().resolveAssetUrl?host().resolveAssetUrl(value):safeAsset(value);
|
||||
const useExpertAvatar=value=>({finalAvatarUrl:value});
|
||||
const Tooltip=({content,children})=>React.cloneElement(children,content?{title:typeof content==='string'?content:undefined}:{});
|
||||
const Popover=HostPopover,Az=160,yn={sm:14,md:16,lg:20,xl:24},hn=()=>null;
|
||||
const VirtualDropdownMenuList=()=>null,DropdownFooter=()=>null,ML=()=>null,L_=()=>null,Fs=()=>null,uc=()=>null;
|
||||
const ExpertCloudSyncBadge=()=>null,ExpertShareModal=()=>null,CreatedExpertCloudUploadConfirmModal=()=>null;
|
||||
const openExpertCloudUploadFlow=unavailable,shouldSkipExpertCloudUploadConfirm=()=>false,setSkipExpertCloudUploadConfirm=()=>{};
|
||||
const getCreateUploadSlotCount=()=>0,getQuotaExceededMessage=()=>'',createInitialUploadItemResults=()=>[],getBatchUploadItemResults=()=>[];
|
||||
const Uu={default:(...values)=>values.flatMap(value=>typeof value==='string'?[value]:value&&typeof value==='object'?Object.keys(value).filter(key=>value[key]):[]).filter(Boolean).join(' ')};
|
||||
const Z={'action.close':'关闭'};
|
||||
${bindingPreamble}
|
||||
${selected.map(s => s.code).join('\n')}
|
||||
const $te=createIcon(Au),lo=createIcon(bF),bo=createIcon(Pl),uo=createIcon(wd),ote=createIcon(Zl,{size:'sm'}),Oa=ShareIcon,Wa=Dropdown;
|
||||
function Ie(props){const[theme,setTheme]=React.useState(()=>document.documentElement.dataset.theme==='dark'?'dark':'light');React.useEffect(()=>{const o=new MutationObserver(()=>setTheme(document.documentElement.dataset.theme==='dark'?'dark':'light'));o.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});return()=>o.disconnect()},[]);return JSX.jsx(ConfirmDialog,{...props,theme})}
|
||||
function TeamDetail({expert,onClose,onSummon}){const locale=useLocale$1();React.useEffect(()=>{const key=e=>{if(e.key==='Escape'&&!e.isComposing){e.stopPropagation();onClose()}};document.addEventListener('keydown',key,true);return()=>document.removeEventListener('keydown',key,true)},[onClose]);const content=host().renderPreview?.({expert,onClose,onSummon})??PortalHost.createPortal(JSX.jsx('div',{className:WS.dialogOverlay,onClick:onClose,children:JSX.jsxs('section',{className:WS.dialogContainer,role:'dialog','aria-modal':true,'aria-label':resolveShareExpertName(expert,locale),onClick:e=>e.stopPropagation(),children:[JSX.jsx('div',{className:WS.dialogHeader,children:resolveShareExpertName(expert,locale)}),JSX.jsx('div',{className:WS.dialogContent,children:resolveCardI18nText(expert.description,locale)}),JSX.jsx(Ku,{onClick:onClose,children:'关闭'})]})}),document.body);return JSX.jsx('div',{style:{display:'contents'},onClick:e=>e.stopPropagation(),onKeyDown:e=>e.stopPropagation(),children:content})}
|
||||
return {Card:CreatedExpertCard,Panel:CreatedExpertsPanel};
|
||||
}
|
||||
const defaultBindings=createBindings(defaultRef),Context=React.createContext({ref:defaultRef,bindings:defaultBindings});
|
||||
export function CreatedExpertSourceProvider({host,children}){const ref=React.useRef(host??{});ref.current=host??{};const bindings=React.useMemo(()=>createBindings(ref),[]);const value=React.useMemo(()=>({ref,bindings}),[bindings]);return JSX.jsx(Context.Provider,{value,children:JSX.jsx('div',{className:'yc-created-expert-source',style:{display:'contents'},children})})}
|
||||
function useLocalCallbacks(props,ref){const fail=React.useCallback(()=>ref.current.notify?.('操作失败,请重试','warning'),[ref]);const onDelete=React.useCallback(async id=>{try{return await props.onDelete?.(id)}catch{fail();return false}},[props.onDelete,fail]);const onEdit=React.useCallback(expert=>{try{Promise.resolve(props.onEdit?.(expert)).catch(fail)}catch{fail()}},[props.onEdit,fail]);const onUse=React.useCallback((id,prompt)=>{try{Promise.resolve(props.onUse?.(id,prompt)).catch(fail)}catch{fail()}},[props.onUse,fail]);return{onUse,onDelete:props.onDelete?onDelete:undefined,onEdit:props.onEdit?onEdit:undefined}}
|
||||
export function CreatedExpertCard(props){const{ref,bindings}=React.useContext(Context);const callbacks=useLocalCallbacks(props,ref);return JSX.jsx(bindings.Card,{expert:props.expert,...callbacks,isMenuOpen:props.isMenuOpen,onToggleMenu:props.onToggleMenu,highlightKeyword:props.highlightKeyword})}
|
||||
export function CreatedExpertsPanel(props){const{ref,bindings}=React.useContext(Context);const callbacks=useLocalCallbacks(props,ref);return JSX.jsx(bindings.Panel,{createdExperts:props.createdExperts,activeTab:props.activeTab,onTabChange:props.onTabChange,...callbacks,searchKeyword:props.searchKeyword,toolbarExtra:props.toolbarExtra})}
|
||||
`
|
||||
|
||||
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json', 'utf8'))
|
||||
const cssNames = ['src-Bh3YZ35H.css', 'src-CBaV_A3S.css', 'contexts-BkjwYSZT.css']
|
||||
const classes = Object.values(ws).join('|').replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replaceAll('\\|', '|')
|
||||
const selectorMatch = new RegExp('ec-cr-|ec-my-|ec-delete-confirm|wb-dropdown|wb-popover|cb-button|'+classes)
|
||||
function scope(n) {
|
||||
const cloned = n.clone()
|
||||
if (cloned.type === 'rule') cloned.selector = cloned.selector.split(',').map(s => ':where(.yc-created-expert-source) '+s.trim()).join(',')
|
||||
else if (cloned.type === 'atrule' && !/keyframes$/.test(cloned.name)) cloned.nodes?.forEach((child,index)=>{cloned.nodes[index]=scope(child);cloned.nodes[index].parent=cloned})
|
||||
return cloned
|
||||
}
|
||||
let css = '/* Generated from pinned REA CSS. Source declarations retained; local scope and explicit host portal wrapper. */\n.yc-created-expert-source{display:contents}\n'
|
||||
for (const name of cssNames) {
|
||||
const entry = cssManifest.files.find(item => item.file === name), text = fs.readFileSync('.tmp/rea-css-recovered/'+name,'utf8')
|
||||
assert.equal(sha(text),entry.sha256)
|
||||
manifest.sources.push({file:name,sha256:entry.sha256,evidenceId:cssManifest.sourceEvidenceId})
|
||||
function include(node) {
|
||||
if (node.type === 'rule') return selectorMatch.test(node.selector)
|
||||
if (node.type === 'atrule' && /keyframes$/.test(node.name)) return /wb-popover|cb-button/.test(node.params)
|
||||
return node.nodes?.some(include)
|
||||
}
|
||||
function prune(node) {const copy=node.clone();if(copy.type==='atrule'&&!/keyframes$/.test(copy.name))copy.nodes=copy.nodes.filter(include).map(prune);return copy}
|
||||
const parsed=postcss.parse(text)
|
||||
if(name==='src-CBaV_A3S.css')for(const node of parsed.nodes){
|
||||
if(node.type!=='rule'||!node.nodes.some(n=>n.type==='decl'&&n.prop==='--cb-vscode-editor-background'))continue
|
||||
let selector=node.selector===':root'?'.yc-created-expert-source':node.selector==='body'?'.yc-created-expert-source':node.selector.startsWith('.light,')?'.expert-center-light .yc-created-expert-source,.yc-created-expert-source.expert-center-light':node.selector.startsWith(':root.dark,')?'.expert-center-dark .yc-created-expert-source,.yc-created-expert-source.expert-center-dark':null
|
||||
if(!selector)continue
|
||||
const original=text.slice(node.source.start.offset,node.source.end.offset+1),clone=node.clone({selector});clone.nodes=clone.nodes.filter(n=>n.type==='decl'&&n.prop.startsWith('--'))
|
||||
manifest.css.push({file:name,start:node.source.start.offset,end:node.source.end.offset+1,sha256:sha(original),adaptation:'Local cb theme token scope; declarations only'})
|
||||
css+=clone.toString()+'\n'
|
||||
}
|
||||
for (const node of parsed.nodes.filter(include)) {
|
||||
manifest.css.push({file:name,start:node.source.start.offset,end:node.source.end.offset+1,sha256:sha(text.slice(node.source.start.offset,node.source.end.offset+1))})
|
||||
css += scope(prune(node)).toString()+'\n'
|
||||
}
|
||||
}
|
||||
css+='/* Host IDE semantic bridge, not a recovered declaration. */\n.yc-created-expert-source{--vscode-errorForeground:var(--wb-color-text-error-default);--vscode-errorBackground:var(--wb-button-danger-fg-on-solid)}\n'
|
||||
const declaration = `import type {ComponentType,ReactNode} from 'react';
|
||||
export type ExpertLocaleText=string|{zh?:string;en?:string;[locale:string]:string|undefined};
|
||||
export interface CreatedExpertRecord {id:string;name:string;displayName?:ExpertLocaleText;profession?:ExpertLocaleText;description?:ExpertLocaleText;expertType?:'expert'|'team'|string;expertRootDir?:string;avatar?:string;version?:string;updatedAt?:number|string;tags?:unknown[];skills?:unknown[];[field:string]:unknown}
|
||||
export interface CreatedExpertPreviewProps {expert:CreatedExpertRecord;onClose():void;onSummon(id:string,unused?:unknown,initPrompt?:string):boolean|void}
|
||||
export interface CreatedExpertSourceHost {locale?:'zh'|'en';openPath?(path:string):Promise<string|void>;resolveAssetUrl?(value:string):string;notify?(message:string,type?:string):void;renderPreview?(props:CreatedExpertPreviewProps):ReactNode}
|
||||
export const CreatedExpertSourceProvider:ComponentType<{host?:CreatedExpertSourceHost;children?:ReactNode}>;
|
||||
export interface CreatedExpertCallbacks {onUse(id:string,initPrompt?:string):void|Promise<void>;onDelete?(id:string):boolean|void|Promise<boolean|void>;onEdit?(expert:CreatedExpertRecord):void|Promise<void>}
|
||||
export const CreatedExpertCard:ComponentType<CreatedExpertCallbacks&{expert:CreatedExpertRecord;isMenuOpen?:boolean;onToggleMenu?(id:string,open?:boolean):void;highlightKeyword?:string}>;
|
||||
export const CreatedExpertsPanel:ComponentType<CreatedExpertCallbacks&{createdExperts:CreatedExpertRecord[];activeTab:'expert'|'team';onTabChange(tab:'expert'|'team'):void;searchKeyword?:string;toolbarExtra?:ReactNode}>;
|
||||
`
|
||||
parse(body,{sourceType:'module'})
|
||||
const outputs = [[directory+'/assets/created-expert-source.mjs',body],[directory+'/assets/created-expert-source.d.mts',declaration],[directory+'/created-expert-source.css',css],[directory+'/assets/created-expert-source-manifest.json',JSON.stringify(manifest,null,2)+'\n']]
|
||||
for (const [file,text] of outputs) {
|
||||
if (check) assert.equal(fs.readFileSync(file,'utf8'),text,`Reproducible recovery: ${file}`)
|
||||
else {fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,text)}
|
||||
}
|
||||
console.log('Created expert source '+(check?'CHECK':'RECOVERY')+' PASS: '+manifest.bodies.length+' bodies, '+manifest.css.length+' CSS records')
|
||||
|
||||
// Self-contained native regression: explicit test-owned manifests and callbacks only.
|
||||
const nativeQaEntry="import React, { useState } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport {CreatedExpertSourceProvider,CreatedExpertsPanel} from '../../src/renderer/src/capabilities/assets/created-expert-source.mjs'\nimport '../../src/renderer/src/workbuddy/typography/tokens.css'\nimport '../../src/renderer/src/styles/tokens.css'\nimport '../../src/renderer/src/styles/app.css'\nimport '../../src/renderer/src/capabilities/capability-center-source.css'\nimport '../../src/renderer/src/capabilities/expert-source.css'\nimport '../../src/renderer/src/capabilities/created-expert-source.css'\nconst w=window as any\nw.events=[];w.deleteMode='false'\nfunction Test(){const [tab,setTab]=useState<'expert'|'team'>('expert'),[keyword,setKeyword]=useState(''),[items,setItems]=useState(w.fixture),[revision,setRevision]=useState(0)\nw.setKeyword=setKeyword;w.setFixture=setItems;w.refresh=()=>setRevision(n=>n+1)\nreturn <div className=\"yc-capability-center\" style={{height:'100vh'}}><section className={'expert-center-page expert-center-'+(document.documentElement.dataset.theme||'light')} style={{height:'100%',padding:24}} data-revision={revision}>\n<CreatedExpertSourceProvider host={{openPath:async path=>{w.events.push(['openPath',path]);return ''},notify:(message,type)=>w.events.push(['notify',message,type]),renderPreview:({expert,onClose,onSummon})=><div role=\"dialog\" data-qa-preview><h2>{expert.name}</h2><button onClick={onClose}>关闭预览</button><button onClick={()=>onSummon(expert.id,undefined,'QA prompt')}>使用预览</button></div>}}>\n<CreatedExpertsPanel createdExperts={items} activeTab={tab} onTabChange={setTab} searchKeyword={keyword} onUse={(id,prompt)=>{w.events.push(['use',id,prompt])}} onEdit={expert=>{w.events.push(['edit',expert.id])}} onDelete={async id=>{w.events.push(['delete',id]);if(w.deleteMode==='throw')throw Error('controlled QA failure');if(w.deleteMode==='false')return false;if(w.deleteMode==='pending')return new Promise(resolve=>{w.finishDelete=()=>resolve(false)});setItems(items=>items.filter(x=>x.id!==id));return true}} />\n</CreatedExpertSourceProvider></section></div>}\ncreateRoot(document.getElementById('root')!).render(<Test/>)\n"
|
||||
const nativeQaRunner="const fs=require('node:fs/promises'),path=require('node:path'),assert=require('node:assert/strict')\nif(!process.versions.electron){(async()=>{const env={...process.env};delete env.ELECTRON_RUN_AS_NODE;const c=require('node:child_process').spawn(require('electron'),['--no-sandbox',__filename],{env,windowsHide:true,stdio:['ignore','pipe','pipe']});let log='';c.stdout.on('data',b=>{log+=b;process.stdout.write(b)});c.stderr.on('data',b=>log+=b);const timer=setTimeout(()=>c.kill(),60000);c.once('exit',async code=>{clearTimeout(timer);await fs.writeFile(path.join(__dirname,'native.log'),log);if(code)console.error(log.slice(-4000));process.exitCode=code??1})})()}else{\nconst{app,BrowserWindow}=require('electron');app.disableHardwareAcceleration();app.commandLine.appendSwitch('use-angle','swiftshader');app.commandLine.appendSwitch('disable-gpu-compositing');app.setPath('userData',path.join(__dirname,'profile'));const checks=[],errors=[],requests=[],geometry=[];let win\nconst pause=ms=>new Promise(r=>setTimeout(r,ms)),read=code=>win.webContents.executeJavaScript(code)\nfunction check(label,actual,expected=true){const pass=JSON.stringify(actual)===JSON.stringify(expected);checks.push({label,actual,expected,pass});assert.ok(pass,label)}\nasync function wait(code){for(let i=0;i<200;i++){if(await read(code))return;await pause(20)}throw Error('wait '+code)}\nasync function item(text){await read(`(()=>{const e=[...document.querySelectorAll('[role=menuitem]')].find(e=>e.textContent.trim()===${JSON.stringify(text)});if(!e)throw Error('missing menu item');e.click()})()`);await pause(30)}\nasync function menu(index=0){await read(`document.querySelectorAll('.ec-cr-more-btn')[${index}].click()`);await wait(`document.querySelector('[role=menu]')?.getBoundingClientRect().width>0`);await pause(170)}\nasync function snap(name){await read(`Promise.all(document.getAnimations().map(a=>a.finished.catch(()=>{}))).then(()=>true)`);geometry.push({name,nodes:await read(`['.ec-my-created-toolbar','.ec-my-grid','.ec-cr-card','.ec-cr-more-btn','.wb-popover','.wb-dropdown__item','._dialogContainer_peiew_21','.ec-my-created-empty-icon'].map(selector=>{const e=document.querySelector(selector);if(!e)return{selector,missing:true};const r=e.getBoundingClientRect(),s=getComputedStyle(e);return{selector,bounds:[r.x,r.y,r.width,r.height],fontSize:s.fontSize,lineHeight:s.lineHeight,padding:s.padding,gap:s.gap}})`)});await fs.writeFile(path.join(__dirname,name+'.png'),(await win.webContents.capturePage()).toPNG())}\n;(async()=>{await app.whenReady();win=new BrowserWindow({width:1336,height:901,useContentSize:true,show:false,webPreferences:{contextIsolation:true,nodeIntegration:false}});win.webContents.on('console-message',e=>{if(e.level==='error'||e.level>=3)errors.push(e.message)});win.webContents.session.webRequest.onBeforeRequest((details,callback)=>{if(/^https?:/.test(details.url))requests.push(details.url);callback({})});await win.loadFile(path.join(__dirname,'index.html'));win.showInactive();await wait(`document.querySelectorAll('.ec-cr-card').length===2`);win.webContents.sendInputEvent({type:'mouseMove',x:100,y:100});await pause(180);await snap('cards')\ncheck('source local card without usage metadata height 128',await read(`document.querySelector('.ec-cr-card').getBoundingClientRect().height`),128)\ncheck('source card more button 24x24',await read(`(()=>{const r=document.querySelector('.ec-cr-more-btn').getBoundingClientRect();return[r.width,r.height]})()`),[24,24])\ncheck('source CreatedExpertsPanel toolbar is 24px',await read(`document.querySelector('.ec-my-created-toolbar').getBoundingClientRect().height`),24)\ncheck('local counts expert 2 and team 1',await read(`[...document.querySelectorAll('.ec-my-created-tab-count')].map(n=>n.textContent)`),['2','1'])\nawait menu();check('local menu exact four actions, no cloud/share',await read(`[...document.querySelectorAll('[role=menuitem]')].map(e=>e.textContent.trim())`),['查看专家','修改专家','打开文件夹','删除']);check('menu icons preserve 16x16 default',await read(`[...document.querySelectorAll('.wb-dropdown__icon svg')].map(n=>[n.getAttribute('width'),n.getAttribute('height')])`),[['16','16'],['16','16'],['16','16'],['16','16']]);await snap('menu')\ncheck('dropdown source item min height 32',await read(`document.querySelector('.wb-dropdown__item').getBoundingClientRect().height`),32)\ncheck('menu uses body portal scope',await read(`document.querySelector('[role=menu]').closest('.yc-created-expert-source').parentElement===document.body`));check('menu source bottom-start gap is 4px',await read(`document.querySelector('[role=menu]').getBoundingClientRect().top-document.querySelector('.ec-cr-more-btn').getBoundingClientRect().bottom`),4);await read(`document.querySelectorAll('.ec-cr-more-btn')[1].click()`);await pause(180);check('only one menu open across two cards',await read(`document.querySelectorAll('[role=menu]').length`),1);win.webContents.sendInputEvent({Line truncated
|
||||
|
||||
if(process.argv.includes('--native-qa')){
|
||||
const {build}=await import('esbuild'),{spawn}=await import('node:child_process')
|
||||
const qaDirectory=fs.mkdtempSync(path.resolve('.tmp/created-expert-source-'))
|
||||
fs.writeFileSync(path.join(qaDirectory,'entry.tsx'),nativeQaEntry)
|
||||
fs.writeFileSync(path.join(qaDirectory,'native.cjs'),nativeQaRunner)
|
||||
const records=[['qa-local-a','QA临时专家甲','expert'],['qa-local-b','QA临时专家乙','expert'],['qa-local-team','QA临时专家团','team']].map(([id,name,expertType])=>{
|
||||
const expertRootDir=path.join(qaDirectory,id);fs.mkdirSync(expertRootDir)
|
||||
const record={id,name,expertType,expertRootDir,profession:{zh:name},description:{zh:'仅用于独立组件验收的本机临时目录'},tags:['QA'],version:'1.0.0'}
|
||||
fs.writeFileSync(path.join(expertRootDir,'expert.json'),JSON.stringify(record));return record
|
||||
})
|
||||
await build({entryPoints:[path.join(qaDirectory,'entry.tsx')],bundle:true,platform:'browser',format:'esm',jsx:'automatic',outfile:path.join(qaDirectory,'fixture.js'),loader:{'.svg':'file','.woff2':'file','.woff':'file','.ttf':'file','.png':'file'},define:{'process.env.NODE_ENV':'"production"'},logLevel:'warning'})
|
||||
fs.copyFileSync(emptyAsset,path.join(qaDirectory,'empty-created-expert.svg'))
|
||||
fs.writeFileSync(path.join(qaDirectory,'index.html'),'<!doctype html><html data-theme="light"><head><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{height:100%;margin:0}body{overflow:hidden}</style></head><body><div id="root"></div><script>window.fixture='+JSON.stringify(records)+'</script><script type="module" src="fixture.js"></script></body></html>')
|
||||
await new Promise((resolve,reject)=>{const child=spawn(process.execPath,[path.join(qaDirectory,'native.cjs')],{stdio:'inherit',windowsHide:true});child.once('error',reject);child.once('exit',code=>code===0?resolve():reject(Error('Native QA failed: '+code)))})
|
||||
console.log('Native fixture report: '+path.join(qaDirectory,'report.json'))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Recover the Grid drawer classes, not the unrelated wb-drawer component.
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { resolve } from 'node:path'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const runtimeFile = 'src-0FGg1seX.js'
|
||||
const runtimeSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
||||
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
const cssFile = 'src-CBaV_A3S.css'
|
||||
const cssSha256 = '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c'
|
||||
const hash = value => createHash('sha256').update(value).digest('hex')
|
||||
const runtime = JSON.parse(await readFile(resolve('.tmp/rea-runtime-scripts.json'), 'utf8'))
|
||||
if (runtime.evidence_id !== runtimeEvidenceId) throw new Error('Fixed runtime Evidence changed')
|
||||
const matching = runtime.items.filter(item => item.file_path?.replaceAll('\\', '/').endsWith(`/renderer/assets/${runtimeFile}`))
|
||||
if (matching.length !== 1 || hash(matching[0].source.artifact.text) !== runtimeSha256 || matching[0].source.artifact.sha256 !== runtimeSha256) throw new Error('Grid drawer runtime SHA-256 mismatch')
|
||||
const runtimeText = matching[0].source.artifact.text
|
||||
for (const marker of ['drawerBackdrop:', 'gridViewDrawerItem:', 'drawerItemFullScreen:', 'showBackdrop!==!1', '--drawer-animation-duration', 'animationDuration??300']) {
|
||||
if (!runtimeText.includes(marker)) throw new Error(`Missing Grid drawer source marker ${marker}`)
|
||||
}
|
||||
const directory = resolve('.tmp/rea-css-recovered')
|
||||
const manifest = JSON.parse(await readFile(resolve(directory, 'manifest.json'), 'utf8'))
|
||||
if (manifest.sourceEvidenceId !== cssEvidenceId || manifest.files.find(file => file.file === cssFile)?.sha256 !== cssSha256) throw new Error('Fixed Grid CSS manifest changed')
|
||||
const sourceText = await readFile(resolve(directory, cssFile), 'utf8')
|
||||
if (hash(sourceText) !== cssSha256) throw new Error('Grid drawer CSS SHA-256 mismatch')
|
||||
const source = postcss.parse(sourceText)
|
||||
const result = postcss.root()
|
||||
let count = 0
|
||||
const declarations = []
|
||||
function selected(selector) { return /\._(?:drawer|gridViewDrawer)[\w]*_11lon_/.test(selector) }
|
||||
function project(node) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = node.selectors.filter(selected)
|
||||
if (!selectors.length) return null
|
||||
for (const selector of selectors) for (const match of selector.matchAll(/\.([\w]+_11lon_\d+)/g)) {
|
||||
if (!runtimeText.includes(match[1])) throw new Error(`CSS class absent from fixed runtime: ${match[1]}`)
|
||||
}
|
||||
const conditions = []
|
||||
let ancestor = node.parent
|
||||
while (ancestor) { if (ancestor.type === 'atrule') conditions.unshift(`@${ancestor.name} ${ancestor.params}`); ancestor = ancestor.parent }
|
||||
declarations.push({ selectors, bodySha256: hash(node.nodes.map(child => child.toString()).join(';')), conditions })
|
||||
count++
|
||||
return node.clone({ selector: selectors.map(selector => `.source-detail-layout ${selector}`).join(',') })
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes && !/keyframes$/.test(node.name)) {
|
||||
const children = node.nodes.map(project).filter(Boolean)
|
||||
return children.length ? node.clone({ nodes: children }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
for (const node of source.nodes) { const projected = project(node); if (projected) result.append(projected) }
|
||||
if (count !== 15) throw new Error(`Expected 15 source Grid drawer rules, found ${count}`)
|
||||
if (declarations.some(rule => rule.conditions.length)) throw new Error('Grid drawer source conditions changed; review before adapting')
|
||||
const text = `/* Generated by scripts/rea/recover-drawer-motion.mjs from fixed WorkBuddy 5.8.2 REA Evidence.
|
||||
* JSX: ${runtimeEvidenceId}; ${runtimeFile} sha256=${runtimeSha256}.
|
||||
* CSS: ${cssEvidenceId}; ${cssFile} sha256=${cssSha256}.
|
||||
* Original 15 Grid drawer rule bodies/order preserved; scope is .source-detail-layout.
|
||||
* Drawer/backdrop: var(--drawer-animation-duration,.3s) ease-out; source has no reduced-motion media override.
|
||||
* The host can set --drawer-animation-duration:0ms for reduced-motion without modifying these source declarations.
|
||||
* This file intentionally contains Grid classes; wb-drawer is a different component.
|
||||
*/\n${result.toString()}\n`
|
||||
const target = resolve('src/renderer/src/workbuddy/workbench/drawer-motion-source.css')
|
||||
if (process.argv.includes('--check')) {
|
||||
if (await readFile(target, 'utf8') !== text) throw new Error('Grid drawer CSS differs from fixed source recovery')
|
||||
} else await writeFile(target, text)
|
||||
const report = { version: '5.8.2', runtimeEvidenceId, runtimeFile, runtimeSha256, cssEvidenceId, cssFile, cssSha256, rules: count,
|
||||
sourceDeclarations: declarations, outputSha256: hash(text), reducedMotionOverrideInSource: false, check: process.argv.includes('--check') }
|
||||
if (!process.argv.includes('--check')) await writeFile(resolve('.tmp/drawer-motion-source-report.json'), JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify({ ...report, sourceDeclarations: undefined }))
|
||||
@@ -0,0 +1,40 @@
|
||||
/** Only fixed, saved REA evidence is read. --check verifies recovered outputs without writing. */
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const sha = value => crypto.createHash('sha256').update(value).digest('hex')
|
||||
const archive = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8'))
|
||||
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json','utf8'))
|
||||
const manifest = {version:'5.8.2',rootSha256:'035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',evidence:[archive.evidence_id,cssManifest.sourceEvidenceId],sources:[],bodies:[],adaptations:['Local React/JSX singleton; source component/function bodies are preserved except binding substitutions.', 'ExpertRow disabled adds the typed host item.disabled flag.', 'Source cloud search/recommendation/pin hooks are replaced by the explicit local typed host; no original cloud service or fake expert is invoked.', 'CSS declarations remain unchanged; selectors scoped to .yc-expert-picker.']}
|
||||
function index(name,expected){const a=archive.items.find(i=>i.file_path.endsWith(name))?.source.artifact;if(!a||a.sha256!==expected||sha(a.text)!==expected)throw Error('REA source identity mismatch '+name);manifest.sources.push({file:name,sha256:expected});const nodes=new Map(),strings=new Map();function visit(n){if(!n||typeof n!=='object')return;if(n.type==='FunctionDeclaration'&&n.id)nodes.set(n.id.name,n);if(n.type==='AssignmentExpression'&&n.left.type==='Identifier'){nodes.set(n.left.name,n);if(n.right.type==='TemplateLiteral'&&!n.right.expressions.length)strings.set(n.left.name,n.right.quasis[0].value.cooked);if(n.right.type==='StringLiteral')strings.set(n.left.name,n.right.value)}for(const[key,v]of Object.entries(n)){if(key==='loc'||key==='comments')continue;if(Array.isArray(v))v.forEach(visit);else if(v&&typeof v==='object')visit(v)}}const ast=parse(a.text,{sourceType:'module'});visit(ast.program);return{...a,nodes,strings,ast}}
|
||||
const input=index('wb-input-add-B045GnIg.js','7842fa4321d5351bf6c822bbd0253e0d6d6e68cc9cfae3f2470b3d493c23daf4')
|
||||
const context=index('contexts-BMeWSUHk.js','5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b')
|
||||
const locale=index('zh-cn-Dfzn8myY.js','05bdf17cc76e1c2547e84a2754d249405366eb5559149b1661837c9687e6efab')
|
||||
function getBody(source,name){const n=source.nodes.get(name);if(!n)throw Error('Missing body '+name);const original=source.text.slice(n.start,n.end);manifest.bodies.push({name,start:n.start,end:n.end,sha256:sha(original)});return (n.type==='AssignmentExpression'?'const ':'')+original.replace(/\(0,[\w$]+\.(jsx|jsxs)\)/g,' JSX.$1')+';'}
|
||||
function classMap(source,name){const n=source.nodes.get(name)?.right;if(n?.type!=='ObjectExpression')throw Error('Missing class map '+name);return Object.fromEntries(n.properties.map(p=>{const key=p.key.name??p.key.value;const value=p.value.type==='Identifier'?source.strings.get(p.value.name):p.value.type==='StringLiteral'?p.value.value:p.value.quasis?.[0]?.value.cooked;if(typeof value!=='string')throw Error('Unresolved class '+key);return[key,value]}))}
|
||||
const classes={expert:classMap(input,'V'),base:classMap(context,'_V')}
|
||||
const labels={};function labelsWalk(n){if(!n||typeof n!=='object')return;if(n.type==='ObjectProperty'&&n.key.type==='StringLiteral'&&n.key.value.startsWith('input.wbAdd.experts.')){const v=n.value.type==='StringLiteral'?n.value.value:n.value.type==='TemplateLiteral'&&!n.value.expressions.length?n.value.quasis[0].value.cooked:undefined;if(v!==undefined)labels[n.key.value]=v}for(const[key,v]of Object.entries(n)){if(key==='loc'||key==='comments')continue;if(Array.isArray(v))v.forEach(labelsWalk);else if(v&&typeof v==='object')labelsWalk(v)}}labelsWalk(locale.ast.program)
|
||||
if(Object.keys(labels).length<10)throw Error('Expert picker locale messages missing')
|
||||
let body=`import * as React from 'react';import * as JSX from 'react/jsx-runtime';\nimport {usePinnedExperts,useExpertsSuggestions,useExpertSearch,AvatarPrimitive,Tooltip,classNames} from './expert-picker-runtime';\nimport {PinIcon,UnpinIcon,SearchIcon,ClearFilledIcon} from './SkillSourceIcons';\nconst no=React,SV=React,U=JSX,CV=JSX,V=${JSON.stringify(classes.expert)},_V=${JSON.stringify(classes.base)},to={default:classNames},j=()=>key=>(${JSON.stringify(labels)})[key]??key,oe=AvatarPrimitive,C=Tooltip,we=PinIcon,Vt=UnpinIcon,Tt=value=>value;\n`
|
||||
body+=getBody(context,'CheckIcon$2')+'\nconst yt=CheckIcon$2,SearchIcon$1=SearchIcon;\n'+getBody(context,'wV')+'\nconst Pt=wV;\n'
|
||||
for(const name of ['readString','getExpertMenuDisplayName','normalizeKeyword','matchesKeyword','SummonMoreIcon','ExpertAvatar','ExpertRow','ExpertsSubmenu'])body+=getBody(input,name)+'\n'
|
||||
const disabledSource='disabled:i===`search`?!e?.onSummonSearchResult:!e?.onSelect'
|
||||
if(!body.includes(disabledSource))throw Error('Source disabled adaptation boundary changed')
|
||||
body=body.replace(disabledSource,'disabled:!!t.disabled||(i===`search`?!e?.onSummonSearchResult:!e?.onSelect)')
|
||||
const selectedShortCircuit='if(r===n.id){t?.();return}'
|
||||
if(body.split(selectedShortCircuit).length!==2)throw Error('Source selected-row adaptation boundary changed')
|
||||
body=body.replace(selectedShortCircuit,'if(r===n.id&&!e?.reselectSelected){t?.();return}')
|
||||
manifest.adaptations.push('Explicit host adaptation: injection.reselectSelected opts out of the legacy selected-ID close-only branch; YCode enables it for current cr ordinary user reactivation while preserving selected styling. This is not literal legacy behavior. Basis: docs/专家使用逻辑-REA核对-20261016.md, src-DbE1_mgM.js ExpertsSubmenu [1574199,1579192), SHA 6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4; current cr onSelect does not short-circuit the selected ID. The typed host retains its existing async success-close/failure-keep-open behavior.')
|
||||
const rowFocus='role:`menuitem`,"aria-disabled":n'
|
||||
if(!body.includes(rowFocus))throw Error('Source row keyboard boundary changed')
|
||||
body=body.replace(rowFocus,'role:`menuitem`,tabIndex:n?-1:0,"data-expert-id":e.id,"aria-disabled":n').replace('role:`menuitem`,onClick:handleSummonMore','role:`menuitem`,tabIndex:0,"data-expert-manage":true,onClick:handleSummonMore')
|
||||
manifest.adaptations.push('Expert row and summon-more receive explicit tabindex/data attributes for direct host keyboard navigation; source parent has no Arrow-key row traversal.')
|
||||
body+='export {ExpertsSubmenu,ExpertRow,getExpertMenuDisplayName,normalizeKeyword,matchesKeyword};\n'
|
||||
let css='/* REA fixed _14qob_ expert menu + _1mrhu_ search/base declarations. */\n'
|
||||
for(const [file,expected] of Object.entries({'contexts-BkjwYSZT.css':'50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380','wb-input-add-CDNucW11.css':'07566131dac43d1854af1ecb973bf9736a63723a0786ba0d2d5287ed6f45f2f6'})){const text=fs.readFileSync('.tmp/rea-css-recovered/'+file,'utf8'),entry=cssManifest.files.find(f=>f.file===file);if(!entry||sha(text)!==expected||entry.sha256!==expected)throw Error('CSS identity mismatch '+file);manifest.sources.push({file,sha256:entry.sha256});const tree=postcss.parse(text);tree.walkRules(rule=>{if(!/_14qob_|_1mrhu_/.test(rule.selector))return;const clone=rule.clone();clone.selector=clone.selectors.map(s=>'.yc-expert-picker '+s).join(',');if(rule.parent.type==='atrule'){const wrapper=rule.parent.clone({nodes:[]});wrapper.append(clone);css+=wrapper.toString()+'\n'}else css+=clone.toString()+'\n'})}
|
||||
const declarations=`import type {ComponentType} from 'react';import type {ExpertPickerItem} from './SourceExpertPicker';\nexport const ExpertsSubmenu:ComponentType<{injection:{selectedExpertId?:string|null;reselectSelected?:boolean;expertScope:string;recentExperts:readonly ExpertPickerItem[];loadFailed?:boolean;onSelect?:(item:ExpertPickerItem)=>unknown;onSummonSearchResult?:(item:ExpertPickerItem)=>unknown;onSummonMore?():void};onCloseMenu():void}>;\nexport function getExpertMenuDisplayName(item:ExpertPickerItem):string;export function normalizeKeyword(value:string):string;export function matchesKeyword(item:ExpertPickerItem,query:string):boolean;\n`
|
||||
const outputs=[['src/renderer/src/capabilities/expert-picker-source.mjs',body],['src/renderer/src/capabilities/expert-picker-source.d.mts',declarations],['src/renderer/src/capabilities/expert-picker-classes.json',JSON.stringify(classes,null,2)+'\n'],['src/renderer/src/capabilities/expert-picker-source.css',css]]
|
||||
manifest.outputs=outputs.map(([file,text])=>({file,sha256:sha(text),bytes:Buffer.byteLength(text)}));outputs.push(['src/renderer/src/capabilities/expert-picker-manifest.json',JSON.stringify(manifest,null,2)+'\n'])
|
||||
for(const[file,text]of outputs){if(process.argv.includes('--check')){if(fs.readFileSync(file,'utf8')!==text)throw Error('Recovered output drift '+file)}else fs.writeFileSync(file,text)}
|
||||
console.log(JSON.stringify({result:'PASS',sources:manifest.sources.length,bodies:manifest.bodies.length,labels:Object.keys(labels).length,outputs:outputs.length}))
|
||||
@@ -0,0 +1,241 @@
|
||||
// Recover the pure source MetaFold summary closure from retained REA evidence. Never execute target code.
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import generateModule from '@babel/generator'
|
||||
|
||||
const generate = generateModule.default ?? generateModule
|
||||
const EVIDENCE_ID = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const SOURCE_SHA256 = '6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4'
|
||||
const SOURCE_FILE = 'renderer/assets/src-DbE1_mgM.js'
|
||||
const SOURCE_OFFSETS = {
|
||||
isToolCallContent: 952112,
|
||||
normalizeToolName: 952201,
|
||||
getToolDescriptor: 952401,
|
||||
descriptorTable: 952539,
|
||||
objectHelpers: 961954,
|
||||
extractAtom: 963323,
|
||||
summaryDecision: 966216,
|
||||
summaryRender: 967441,
|
||||
computeFoldSummary: 969838
|
||||
}
|
||||
const BUNDLE_PATH = '.tmp/rea-runtime-scripts.json'
|
||||
const OUTPUT_DIR = 'src/renderer/src/workbuddy/conversation/recovered-fold-summary'
|
||||
|
||||
const bundle = JSON.parse(fs.readFileSync(BUNDLE_PATH, 'utf8'))
|
||||
if (bundle.evidence_id !== EVIDENCE_ID) throw new Error(`Unexpected REA Evidence: ${bundle.evidence_id}`)
|
||||
const sourceItems = bundle.items.filter(item => item.file_path?.replaceAll('\\', '/').endsWith(`/${SOURCE_FILE}`))
|
||||
if (sourceItems.length !== 1 || !sourceItems[0].source?.included) throw new Error(`Expected one included REA artifact ${SOURCE_FILE}`)
|
||||
const artifact = sourceItems[0].source.artifact
|
||||
const source = artifact.text
|
||||
const digest = crypto.createHash('sha256').update(source).digest('hex')
|
||||
if (artifact.sha256 !== SOURCE_SHA256 || digest !== SOURCE_SHA256) throw new Error(`REA source identity mismatch: ${digest}`)
|
||||
|
||||
const ast = parse(source, { sourceType: 'unambiguous', errorRecovery: true, plugins: ['jsx', 'decorators-legacy'] })
|
||||
if (ast.errors.length) throw new Error(`Babel reported parse errors: ${ast.errors.map(error => error.message).join('; ')}`)
|
||||
|
||||
const declarations = new Map()
|
||||
const assignments = new Map()
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object' || typeof node.type !== 'string') return
|
||||
if (node.type === 'FunctionDeclaration' && node.id) {
|
||||
const matches = declarations.get(node.id.name) ?? []
|
||||
matches.push(node)
|
||||
declarations.set(node.id.name, matches)
|
||||
}
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
|
||||
const matches = assignments.get(node.left.name) ?? []
|
||||
matches.push(node)
|
||||
assignments.set(node.left.name, matches)
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach(visit)
|
||||
else if (value && typeof value === 'object' && typeof value.type === 'string') visit(value)
|
||||
}
|
||||
}
|
||||
visit(ast.program)
|
||||
|
||||
function code(node) {
|
||||
return generate(node, { compact: false, concise: false, comments: false }).code
|
||||
}
|
||||
|
||||
function uniqueFunction(name, start, end) {
|
||||
const matches = (declarations.get(name) ?? []).filter(node => node.start >= start && node.start < end)
|
||||
if (matches.length !== 1) throw new Error(`Expected one source function ${name} in [${start}, ${end}), found ${matches.length}`)
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
function uniqueAssignment(name, predicate, start, end) {
|
||||
const matches = (assignments.get(name) ?? []).filter(node => node.start >= start && node.start < end && predicate(node.right))
|
||||
if (matches.length !== 1) throw new Error(`Expected one anchored source initializer ${name} in [${start}, ${end}), found ${matches.length}`)
|
||||
return matches[0].right
|
||||
}
|
||||
|
||||
function staticString(node) {
|
||||
if (node?.type === 'StringLiteral') return node.value
|
||||
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) return node.quasis[0].value.cooked
|
||||
return undefined
|
||||
}
|
||||
|
||||
function staticObjectValues(node) {
|
||||
if (node?.type !== 'ObjectExpression') return new Map()
|
||||
return new Map(node.properties
|
||||
.filter(property => property.type === 'ObjectProperty')
|
||||
.map(property => [staticString(property.key) ?? property.key.name, staticString(property.value)])
|
||||
.filter(([key]) => key !== undefined))
|
||||
}
|
||||
|
||||
const descriptorTable = uniqueAssignment('Nw', node => {
|
||||
if (node.type !== 'ArrayExpression' || node.elements.length < 30) return false
|
||||
const first = staticObjectValues(node.elements[0])
|
||||
return first.get('canonical') === 'read_file' && first.get('category') === 'file_read' && first.get('group') === 'read'
|
||||
}, 952500, 961600)
|
||||
if (descriptorTable.elements.length !== 40) throw new Error(`Unexpected source descriptor count: ${descriptorTable.elements.length}`)
|
||||
|
||||
const sourceConstants = [
|
||||
['Pw', node => staticString(node) === '__automation_update', 961580, 961650],
|
||||
['Fw', node => staticObjectValues(node).get('canonical') === 'unknown' && staticObjectValues(node).get('category') === 'unknown', 961600, 961800],
|
||||
['zw', node => node.type === 'NewExpression' && node.callee.name === 'Set', 963650, 963800],
|
||||
['Bw', node => node.type === 'NewExpression' && node.callee.name === 'Set', 963750, 963850],
|
||||
['Vw', node => node.type === 'NewExpression' && node.callee.name === 'Set', 963800, 963900],
|
||||
['Hw', node => node.type === 'NumericLiteral' && node.value === 16, 963850, 963900],
|
||||
['Uw', node => node.type === 'NumericLiteral' && node.value === 6, 963850, 963920],
|
||||
['Gw', node => node.type === 'ArrayExpression' && staticString(node.elements[0]) === '现在', 965580, 965950],
|
||||
['Kw', node => node.type === 'ArrayExpression' && staticString(node.elements[0]) === '已完成', 965900, 966100],
|
||||
['qw', node => node.type === 'RegExpLiteral' && node.pattern.includes('u4EFB'), 966050, 966220],
|
||||
['Jw', node => node.type === 'NumericLiteral' && node.value === 10, 966190, 966250],
|
||||
['Yw', node => node.type === 'NumericLiteral' && node.value === 0.7, 966190, 966250],
|
||||
['Qw', node => node.type === 'ObjectExpression' && staticObjectValues(node).get('read') === 'metaFold.summary.group.read', 968480, 968930],
|
||||
['$w', node => node.type === 'ObjectExpression' && staticObjectValues(node).get('read') === 'metaFold.summary.groupNoTopic.read', 968900, 969420],
|
||||
['eT', node => node.type === 'ObjectExpression' && staticObjectValues(node).get('read') === 'metaFold.summary.groupVerb.read', 969350, 969850]
|
||||
]
|
||||
const constants = sourceConstants.map(([name, predicate, start, end]) => [name, uniqueAssignment(name, predicate, start, end)])
|
||||
|
||||
const functionNames = [
|
||||
'isToolCallContent', 'normalizeToolName$1', 'getToolDescriptor',
|
||||
'hasCJK', 'compressPath', 'compressUrl', 'truncateObject', 'displayText', 'compressObject',
|
||||
'extractObject', 'resolveStatus', 'extractAtom', 'bucketByGroup', 'dominantBucket',
|
||||
'stripLeadingFillers', 'stripTrailingModifiers', 'extractKeyPhrase', 'topicFromAdjacentText',
|
||||
'deriveTopic', 'aggregateStatus', 'hasUsefulInfo', 'selectSummary', 'compose', 'groupText',
|
||||
'renderBase', 'renderTemplate', 'computeFoldSummary'
|
||||
]
|
||||
const summaryFunctions = functionNames.map(name => {
|
||||
const node = name === 'isToolCallContent' || name === 'normalizeToolName$1' || name === 'getToolDescriptor'
|
||||
? uniqueFunction(name, 952000, 952510)
|
||||
: uniqueFunction(name, 961900, 970050)
|
||||
return { name, node }
|
||||
})
|
||||
const expectedOffsets = {
|
||||
isToolCallContent: 952112,
|
||||
'normalizeToolName$1': 952201,
|
||||
getToolDescriptor: 952401,
|
||||
hasCJK: 961954,
|
||||
extractAtom: 963323,
|
||||
aggregateStatus: 966216,
|
||||
compose: 967441,
|
||||
computeFoldSummary: 969838
|
||||
}
|
||||
for (const { name, node } of summaryFunctions) {
|
||||
const expected = expectedOffsets[name]
|
||||
if (expected !== undefined && node.start !== expected) throw new Error(`${name} source offset changed: ${node.start}`)
|
||||
}
|
||||
|
||||
const constantCode = [
|
||||
`const Nw = ${code(descriptorTable)};`,
|
||||
...constants.map(([name, value]) => `const ${name} = ${code(value)};`),
|
||||
'const Iw = new Map();',
|
||||
'const Lw = new Map();',
|
||||
'for (const descriptor of Nw) {',
|
||||
' Lw.set(descriptor.canonical, descriptor);',
|
||||
' Iw.set(descriptor.canonical.toLowerCase(), descriptor.canonical);',
|
||||
' for (const alias of descriptor.aliases) Iw.set(alias.toLowerCase(), descriptor.canonical);',
|
||||
'}'
|
||||
].join('\n')
|
||||
|
||||
const recoveredFunctions = summaryFunctions.map(({ node }) => code(node)).join('\n\n')
|
||||
const generated = `// Generated from REA ${EVIDENCE_ID}; source SHA-256 ${SOURCE_SHA256}.
|
||||
// AST-scoped pure MetaFold summary closure; source initialization code and the application translator are excluded.
|
||||
${constantCode}
|
||||
|
||||
${recoveredFunctions}
|
||||
|
||||
// The source function falls back to its application-level xa translator. This adapter always supplies t.
|
||||
export function computeSourceFoldSummary(parts, options) {
|
||||
if (typeof options?.t !== 'function') throw new TypeError('A source-label translator must be provided')
|
||||
return computeFoldSummary(parts, {
|
||||
adjacentText: options.adjacentText,
|
||||
isRunning: options.isRunning,
|
||||
t: options.t
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
const declaration = `// Generated from REA ${EVIDENCE_ID}; source SHA-256 ${SOURCE_SHA256}.
|
||||
export type SourceFoldSummaryStatus = 'waiting' | 'cancelled' | 'running' | 'success'
|
||||
|
||||
export interface SourceFoldSummary {
|
||||
text: string
|
||||
status: SourceFoldSummaryStatus
|
||||
isFallback: boolean
|
||||
}
|
||||
|
||||
export interface SourceFoldSummaryTool {
|
||||
name?: string
|
||||
args?: unknown
|
||||
result?: unknown
|
||||
status?: string
|
||||
pendingConfirmation?: boolean
|
||||
}
|
||||
|
||||
export interface SourceFoldSummaryPart {
|
||||
type?: string
|
||||
tool?: SourceFoldSummaryTool
|
||||
}
|
||||
|
||||
export interface SourceFoldSummaryOptions {
|
||||
adjacentText?: string
|
||||
isRunning?: boolean
|
||||
t: (key: string, params?: Readonly<Record<string, string | number>>) => string
|
||||
}
|
||||
|
||||
export function computeSourceFoldSummary(
|
||||
parts: readonly SourceFoldSummaryPart[],
|
||||
options: SourceFoldSummaryOptions
|
||||
): SourceFoldSummary
|
||||
`
|
||||
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true })
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, 'index.mjs'), generated)
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, 'index.d.ts'), declaration)
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, 'index.d.mts'), declaration)
|
||||
|
||||
const outputDigest = crypto.createHash('sha256').update(generated).digest('hex')
|
||||
const manifest = {
|
||||
evidenceId: EVIDENCE_ID,
|
||||
artifact: SOURCE_FILE,
|
||||
sha256: SOURCE_SHA256,
|
||||
parser: '@babel/parser; source was not executed',
|
||||
generator: '@babel/generator',
|
||||
sourceOffsetsUtf16: {
|
||||
...SOURCE_OFFSETS,
|
||||
descriptorTableEndExclusive: descriptorTable.end,
|
||||
descriptorCount: descriptorTable.elements.length,
|
||||
constants: Object.fromEntries(constants.map(([name, value]) => [name, { start: value.start, endExclusive: value.end }]))
|
||||
},
|
||||
extractedFunctions: summaryFunctions.map(({ name, node }) => ({ name, start: node.start, endExclusive: node.end })),
|
||||
applicationDependenciesExcluded: ['xa translator fallback (adapter requires options.t)', 'lazy module initialization', 'React and tool icon rendering'],
|
||||
mapInitialization: 'source descriptor literal Nw plus exact canonical/alias Map population loop',
|
||||
generatedFile: path.join(OUTPUT_DIR, 'index.mjs').replaceAll('\\', '/'),
|
||||
generatedSha256: outputDigest
|
||||
}
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
console.log(JSON.stringify({
|
||||
generatedFile: manifest.generatedFile,
|
||||
evidenceId: EVIDENCE_ID,
|
||||
sha256: SOURCE_SHA256,
|
||||
generatedSha256: outputDigest,
|
||||
descriptorCount: descriptorTable.elements.length,
|
||||
functionCount: summaryFunctions.length,
|
||||
sourceOffsetsUtf16: manifest.sourceOffsetsUtf16
|
||||
}))
|
||||
@@ -0,0 +1,52 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import postcss from 'postcss'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverseModule from '@babel/traverse'
|
||||
const traverse = traverseModule.default ?? traverseModule
|
||||
const root = process.cwd(), evidence = JSON.parse(fs.readFileSync(path.join(root, '.tmp/rea-runtime-scripts.json'), 'utf8'))
|
||||
const cssDir = path.join(root, '.tmp/rea-css-recovered'), target = path.join(root, 'src/renderer/src/components')
|
||||
const cache = new Map()
|
||||
function source(base) { return evidence.items.find((item) => item.file_path.endsWith(base))?.source.artifact.text }
|
||||
function ast(base) { if (!cache.has(base)) cache.set(base, parse(source(base), { sourceType: 'module' })); return cache.get(base) }
|
||||
function wrapRule(rule) { let copy=rule.clone(); for (let parent=rule.parent;parent?.type==='atrule';parent=parent.parent) { const wrapper=parent.clone({nodes:[]});wrapper.append(copy);copy=wrapper }return copy.toString() }
|
||||
function recoverCss(files, pattern, scope) {
|
||||
const output=[], variables=new Set()
|
||||
for (const file of files) postcss.parse(fs.readFileSync(path.join(cssDir,file),'utf8')).walkRules((rule) => {
|
||||
if (!pattern.test(rule.selector)) return
|
||||
if (rule.parent.type==='atrule' && /keyframes/.test(rule.parent.name)) return
|
||||
const copy=rule.clone();copy.selectors=copy.selectors.map((selector)=>scope+' '+selector)
|
||||
let tree=copy;for(let parent=rule.parent;parent?.type==='atrule';parent=parent.parent){const wrapper=parent.clone({nodes:[]});wrapper.append(tree);tree=wrapper}output.push(tree.toString())
|
||||
})
|
||||
for (const file of files) postcss.parse(fs.readFileSync(path.join(cssDir,file),'utf8')).walkAtRules((rule)=>{if(/keyframes/.test(rule.name)&&/home|quick-action/.test(rule.params))output.push(rule.toString())})
|
||||
function collect(text){for(const match of text.matchAll(/var\((--[\w-]+)/g))variables.add(match[1])}collect(output.join(''))
|
||||
const rules=[];postcss.parse(fs.readFileSync(path.join(cssDir,'tokens-Df-i-zsO.css'),'utf8')).walkRules((rule)=>rules.push(rule))
|
||||
for(let pass=0;pass<10;pass++)for(const rule of rules)rule.walkDecls((decl)=>{if(variables.has(decl.prop))collect(decl.value)})
|
||||
const light=new Map(),dark=new Map();for(const rule of rules){const map=rule.selector.includes('dark')?dark:rule.selector.includes(':root')?light:null;if(map)rule.walkDecls((decl)=>{if(variables.has(decl.prop))map.set(decl.prop,decl.value)})}
|
||||
const serialize=(map)=>[...map].map(([key,value])=>key+':'+value).join(';')
|
||||
return '/* Recovered from REA CSS ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; conditions and order retained. */\n'+scope+'{'+serialize(light)+'}\n:root[data-theme=dark] '+scope+'{'+serialize(dark)+'}\n'+output.join('\n')+'\n'
|
||||
}
|
||||
const home=recoverCss(['contexts-BkjwYSZT.css','home-D6iylhBV.css','src-Bh3YZ35H.css'],/\.(?:wb-home-(?:page|header|composer|prompt-list|route)|wb-scene-tabs|wb-segmented|quick-actions)/,'.source-home')
|
||||
const picker=recoverCss(['src-CyXHA0IC.css'],/\.cr-(?:model-selector|menu)/,'.source-model-picker')
|
||||
fs.writeFileSync(path.join(target,'source-home-recovered.css'),home)
|
||||
fs.writeFileSync(path.join(target,'source-model-picker-recovered.css'),picker)
|
||||
const icons={}
|
||||
for(const [kind,local]of Object.entries({work:'zte',code:'hne',design:'Ote',prompt:'hte'})){
|
||||
let member,base
|
||||
traverse(ast('contexts-BMeWSUHk.js'),{ImportDeclaration(p){for(const s of p.node.specifiers)if(s.local.name===local){member=s.imported.name;base=p.node.source.value.replace('./','')}}})
|
||||
let binding
|
||||
traverse(ast(base),{ExportNamedDeclaration(p){for(const s of p.node.specifiers)if(s.exported.name===member)binding=s.local.name}})
|
||||
let declaration
|
||||
function find(name){traverse(ast(base),{AssignmentExpression(p){if(p.node.left.name===name)declaration=p},FunctionDeclaration(p){if(p.node.id?.name===name)declaration=p}})}find(binding)
|
||||
if(declaration?.node.right?.type==='CallExpression'){const inner=declaration.node.right.arguments[0];if(inner?.type==='Identifier')find(inner.name)}
|
||||
const svg={props:{},paths:[]}
|
||||
declaration?.traverse({CallExpression(p){const args=p.node.arguments;if(args[0]?.type!=='TemplateLiteral')return;const tag=args[0].quasis[0].value.cooked;if(!['svg','path'].includes(tag))return;const values={};for(const prop of args[1]?.properties??[]){if(prop.type!=='ObjectProperty')continue;const key=prop.key.name??prop.key.value,value=prop.value;if(value.type==='TemplateLiteral'&&!value.expressions.length)values[key]=value.quasis[0].value.cooked;else if(['NumericLiteral','StringLiteral'].includes(value.type))values[key]=value.value}if(tag==='svg')svg.props=values;else svg.paths.push(values)}})
|
||||
if(svg.paths.length)icons[kind]=svg
|
||||
}
|
||||
for(const[kind,name]of Object.entries({folder:'FolderIcon',check:'CheckIcon$1',down:'ChevronDownSmallIcon'})){
|
||||
let declaration;traverse(ast('src-DbE1_mgM.js'),{AssignmentExpression(p){if(p.node.left.name===name)declaration=p},FunctionDeclaration(p){if(p.node.id?.name===name)declaration=p}})
|
||||
const svg={props:{},paths:[]};declaration?.traverse({CallExpression(p){const args=p.node.arguments;if(args[0]?.type!=='TemplateLiteral')return;const tag=args[0].quasis[0].value.cooked;if(!['svg','path'].includes(tag))return;const values={};for(const prop of args[1]?.properties??[]){if(prop.type!=='ObjectProperty')continue;const key=prop.key.name??prop.key.value,value=prop.value;if(value.type==='TemplateLiteral'&&!value.expressions.length)values[key]=value.quasis[0].value.cooked;else if(['NumericLiteral','StringLiteral'].includes(value.type))values[key]=value.value}if(tag==='svg')svg.props=values;else svg.paths.push(values)}});if(svg.paths.length)icons[kind]=svg
|
||||
}
|
||||
const labels={};traverse(ast('zh-cn-Dfzn8myY.js'),{ObjectProperty(p){const key=p.node.key.value;if(typeof key!=='string'||!(/^(?:home\.|welcome\.mode\.|chatInput\.modelSelector\.)/.test(key)))return;const value=p.node.value;if(value.type==='TemplateLiteral'&&!value.expressions.length)labels[key]=value.quasis[0].value.cooked;else if(value.type==='StringLiteral')labels[key]=value.value}})
|
||||
fs.writeFileSync(path.join(target,'source-home-reference.ts'),'// REA '+evidence.evidence_id+'; static labels/SVG only. No source runtime initialization.\nexport const sourceSceneIcons: Record<string,{props:Record<string,string|number>;paths:Record<string,string|number>[]}> = '+JSON.stringify(icons,null,2)+'\nconst labels: Record<string,string> = '+JSON.stringify(labels,null,2)+'\nexport const sourceHomeLabel = (key:string):string => labels[key]??key\n')
|
||||
console.log(JSON.stringify({homeBytes:home.length,pickerBytes:picker.length,icons:Object.keys(icons),labels:Object.keys(labels).length}))
|
||||
@@ -0,0 +1,184 @@
|
||||
// Recover only literal locale data from the retained REA source; never execute target scripts.
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
|
||||
const EVIDENCE_ID = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const SOURCE_SHA256 = '6c6ce63c93854783270e855572797d75e536e3b8d4548410e31fe9d147e1feb4'
|
||||
const BUNDLE_PATH = '.tmp/rea-runtime-scripts.json'
|
||||
const OUTPUT_PATH = 'src/renderer/src/workbuddy/conversation/source-labels.ts'
|
||||
const MANIFEST_PATH = '.tmp/rea-modern-conversation-labels-manifest.json'
|
||||
|
||||
const bundle = JSON.parse(fs.readFileSync(BUNDLE_PATH, 'utf8'))
|
||||
if (bundle.evidence_id !== EVIDENCE_ID) throw new Error(`Unexpected REA Evidence: ${bundle.evidence_id}`)
|
||||
const sourceItems = bundle.items.filter((item) => item.file_path?.replaceAll('\\', '/').endsWith('/renderer/assets/src-DbE1_mgM.js'))
|
||||
if (sourceItems.length !== 1 || !sourceItems[0].source?.included) throw new Error('Expected exactly one included REA source artifact src-DbE1_mgM.js')
|
||||
const artifact = sourceItems[0].source.artifact
|
||||
const source = artifact.text
|
||||
const digest = crypto.createHash('sha256').update(source).digest('hex')
|
||||
if (artifact.sha256 !== SOURCE_SHA256 || digest !== SOURCE_SHA256) throw new Error(`REA source identity mismatch: ${digest}`)
|
||||
|
||||
const ast = parse(source, { sourceType: 'unambiguous', errorRecovery: true, plugins: ['jsx', 'decorators-legacy'] })
|
||||
if (ast.errors.length) throw new Error(`Babel reported parse errors: ${ast.errors.map((error) => error.message).join('; ')}`)
|
||||
|
||||
const parents = new WeakMap()
|
||||
const objectAssignments = []
|
||||
function visit(node, parent) {
|
||||
if (!node || typeof node !== 'object' || typeof node.type !== 'string') return
|
||||
if (parent) parents.set(node, parent)
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && node.right.type === 'ObjectExpression') {
|
||||
objectAssignments.push({ binding: node.left.name, object: node.right, assignment: node })
|
||||
}
|
||||
for (const value of Object.values(node)) {
|
||||
if (Array.isArray(value)) value.forEach((child) => visit(child, node))
|
||||
else if (value && typeof value === 'object' && typeof value.type === 'string') visit(value, node)
|
||||
}
|
||||
}
|
||||
visit(ast.program)
|
||||
|
||||
function staticKey(node) {
|
||||
if (node?.type === 'Identifier') return node.name
|
||||
if (node?.type === 'StringLiteral') return node.value
|
||||
return undefined
|
||||
}
|
||||
|
||||
function staticString(node) {
|
||||
if (node?.type === 'StringLiteral') return node.value
|
||||
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) return node.quasis[0].value.cooked
|
||||
return undefined
|
||||
}
|
||||
|
||||
function propertiesOf(object) {
|
||||
return new Map(object.properties
|
||||
.filter((property) => property.type === 'ObjectProperty')
|
||||
.map((property) => [staticKey(property.key), property.value])
|
||||
.filter(([key]) => key !== undefined))
|
||||
}
|
||||
|
||||
const englishAnchors = {
|
||||
'tool.command.confirm': 'Run this command?',
|
||||
'tool.command.run': 'Run',
|
||||
'tool.command.skip': 'Skip',
|
||||
'tool.command.reject': 'Reject',
|
||||
'tool.writeFile.editing': 'Editing',
|
||||
'tool.writeFile.create': 'Create',
|
||||
'tool.writeFile.accept': 'Accept',
|
||||
'fold.completedWithDuration': 'Completed {duration}',
|
||||
'chatInput.modelSelector.thinkingMode': 'Thinking mode'
|
||||
}
|
||||
const chineseAnchors = {
|
||||
'tool.command.confirm': '确认执行命令?',
|
||||
'tool.command.run': '运行',
|
||||
'tool.command.skip': '跳过',
|
||||
'tool.command.reject': '拒绝',
|
||||
'tool.writeFile.editing': '修改中',
|
||||
'tool.writeFile.create': '创建',
|
||||
'tool.writeFile.accept': '接受',
|
||||
'fold.completedWithDuration': '已完成 {duration}',
|
||||
'chatInput.modelSelector.thinkingMode': '思考模式'
|
||||
}
|
||||
|
||||
function anchoredAssignment(binding, anchors) {
|
||||
const candidates = objectAssignments.filter((record) => {
|
||||
if (record.binding !== binding) return false
|
||||
const properties = propertiesOf(record.object)
|
||||
return Object.entries(anchors).every(([key, value]) => staticString(properties.get(key)) === value)
|
||||
})
|
||||
if (candidates.length !== 1) throw new Error(`Expected one ${binding} locale object matching its literal anchors, found ${candidates.length}`)
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
const localeMappings = []
|
||||
for (const record of objectAssignments) {
|
||||
const properties = propertiesOf(record.object)
|
||||
const simplified = properties.get('zh-CN')
|
||||
const english = properties.get('en-US')
|
||||
if (simplified?.type !== 'Identifier' || english?.type !== 'Identifier') continue
|
||||
try {
|
||||
const zh = anchoredAssignment(simplified.name, chineseAnchors)
|
||||
const en = anchoredAssignment(english.name, englishAnchors)
|
||||
localeMappings.push({ record, properties, zh, en })
|
||||
} catch {
|
||||
// Other locale-shaped objects in the bundle are unrelated to these source anchors.
|
||||
}
|
||||
}
|
||||
if (localeMappings.length !== 1) throw new Error(`Expected one locale map bound to the anchored zh-CN/en-US objects, found ${localeMappings.length}`)
|
||||
const { record: localeMap, properties: localeProperties, zh: zhAssignment, en: enAssignment } = localeMappings[0]
|
||||
|
||||
let defaultLocale
|
||||
let defaultLocaleBinding
|
||||
let cursor = parents.get(localeMap.object)
|
||||
while (cursor && cursor.type !== 'SequenceExpression') cursor = parents.get(cursor)
|
||||
if (cursor) {
|
||||
const mapExpression = parents.get(localeMap.object)
|
||||
const mapIndex = cursor.expressions.indexOf(mapExpression)
|
||||
const precedingLocale = cursor.expressions.slice(0, mapIndex).findLast((expression) =>
|
||||
expression.type === 'AssignmentExpression' && expression.left.type === 'Identifier' && staticString(expression.right) === 'zh-CN'
|
||||
)
|
||||
if (precedingLocale) {
|
||||
defaultLocale = staticString(precedingLocale.right)
|
||||
defaultLocaleBinding = precedingLocale.left.name
|
||||
}
|
||||
}
|
||||
if (defaultLocale !== 'zh-CN') throw new Error(`Could not verify zh-CN from the source locale initializer; found ${defaultLocale ?? 'none'}`)
|
||||
if (localeProperties.get(defaultLocale)?.name !== zhAssignment.binding) throw new Error('Source default locale is not bound to the anchored zh-CN dictionary')
|
||||
|
||||
function readDictionary(record, inherited = new Map()) {
|
||||
const dictionary = new Map(inherited)
|
||||
for (const property of record.object.properties) {
|
||||
if (property.type === 'SpreadElement') {
|
||||
if (property.argument.type !== 'Identifier' || property.argument.name !== enAssignment.binding) {
|
||||
throw new Error(`Unsupported locale dictionary spread at source offset ${property.start}`)
|
||||
}
|
||||
for (const [key, value] of readDictionary(enAssignment)) dictionary.set(key, value)
|
||||
continue
|
||||
}
|
||||
if (property.type !== 'ObjectProperty') throw new Error(`Unsupported locale property at source offset ${property.start}`)
|
||||
const key = staticKey(property.key)
|
||||
const value = staticString(property.value)
|
||||
if (key === undefined || value === undefined) throw new Error(`Non-literal locale entry at source offset ${property.start}`)
|
||||
dictionary.set(key, value)
|
||||
}
|
||||
return dictionary
|
||||
}
|
||||
|
||||
const enUS = readDictionary(enAssignment)
|
||||
const zhCN = readDictionary(zhAssignment)
|
||||
if (enUS.size !== 971 || zhCN.size !== 971) throw new Error(`Unexpected key count: en-US=${enUS.size}, zh-CN=${zhCN.size}`)
|
||||
const englishKeys = [...enUS.keys()].sort()
|
||||
const chineseKeys = [...zhCN.keys()].sort()
|
||||
if (englishKeys.some((key, index) => key !== chineseKeys[index])) throw new Error('zh-CN and en-US locale key sets differ')
|
||||
for (const [key, expected] of Object.entries(englishAnchors)) {
|
||||
if (enUS.get(key) !== expected) throw new Error(`English anchor changed: ${key}`)
|
||||
}
|
||||
for (const [key, expected] of Object.entries(chineseAnchors)) {
|
||||
if (zhCN.get(key) !== expected) throw new Error(`Chinese anchor changed: ${key}`)
|
||||
}
|
||||
|
||||
const enObject = Object.fromEntries(enUS)
|
||||
const zhObject = Object.fromEntries(zhCN)
|
||||
const sourceComment = `// Generated from REA ${EVIDENCE_ID}; source SHA-256 ${SOURCE_SHA256}.\n// Locale map ${localeMap.binding} at UTF-16 offset ${localeMap.object.start}; en-US ${enAssignment.binding} at ${enAssignment.object.start}; zh-CN ${zhAssignment.binding} at ${zhAssignment.object.start}.\n// The source default is ${defaultLocale} (${defaultLocaleBinding}); fallback order is requested locale, en-US, zh-CN, then the key.\n`
|
||||
const output = `${sourceComment}const enUS = ${JSON.stringify(enObject, null, 2)} as const\nconst zhCN = ${JSON.stringify(zhObject, null, 2)} as const\n\nexport const SOURCE_CONVERSATION_DEFAULT_LOCALE = '${defaultLocale}' as const\nexport const SOURCE_CONVERSATION_LOCALES = ['zh-CN', 'en-US'] as const\nexport type SourceConversationLocale = typeof SOURCE_CONVERSATION_LOCALES[number]\nexport type SourceConversationLabelKey = keyof typeof enUS & keyof typeof zhCN\n\nexport function sourceConversationLabel(\n key: SourceConversationLabelKey,\n params?: Readonly<Record<string, string | number>>,\n locale: SourceConversationLocale = SOURCE_CONVERSATION_DEFAULT_LOCALE\n): string {\n const localized = locale === 'en-US' ? enUS[key] : zhCN[key]\n const template = localized ?? enUS[key] ?? zhCN[key] ?? key\n if (!params) return template\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = params[name]\n return value == null ? match : String(value)\n })\n}\n`
|
||||
fs.writeFileSync(OUTPUT_PATH, output)
|
||||
|
||||
const manifest = {
|
||||
evidenceId: EVIDENCE_ID,
|
||||
artifact: 'renderer/assets/src-DbE1_mgM.js',
|
||||
sha256: SOURCE_SHA256,
|
||||
parser: 'Babel AST; source was not executed',
|
||||
sourceOffsetsUtf16: {
|
||||
englishDictionary: { binding: enAssignment.binding, start: enAssignment.object.start, endExclusive: enAssignment.object.end },
|
||||
simplifiedChineseDictionary: { binding: zhAssignment.binding, start: zhAssignment.object.start, endExclusive: zhAssignment.object.end },
|
||||
localeMap: { binding: localeMap.binding, start: localeMap.object.start, endExclusive: localeMap.object.end },
|
||||
defaultLocale: { binding: defaultLocaleBinding, value: defaultLocale }
|
||||
},
|
||||
localeBindings: { 'zh-CN': zhAssignment.binding, 'en-US': enAssignment.binding },
|
||||
keyCount: enUS.size,
|
||||
matchingKeySets: true,
|
||||
anchors: Object.fromEntries(Object.keys(englishAnchors).map((key) => [key, { 'en-US': enUS.get(key), 'zh-CN': zhCN.get(key) }])),
|
||||
interpolation: { sourcePattern: String.raw`\{(\w+)\}`, missingParameter: 'preserve placeholder' },
|
||||
fallbackOrder: ['requested locale', 'en-US', 'zh-CN', 'key'],
|
||||
generatedFile: OUTPUT_PATH
|
||||
}
|
||||
fs.writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ generatedFile: OUTPUT_PATH, manifest: MANIFEST_PATH, evidenceId: EVIDENCE_ID, sha256: SOURCE_SHA256, sourceOffsetsUtf16: manifest.sourceOffsetsUtf16, keyCount: enUS.size }))
|
||||
@@ -0,0 +1,139 @@
|
||||
// Recover pure panel geometry and source motion CSS from fixed REA artifacts only.
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { resolve } from 'node:path'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
const runtimeFile = 'contexts-BMeWSUHk.js'
|
||||
const runtimeSha256 = '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b'
|
||||
const motionFile = 'desktop-sdk-setup-Bh0rg_xP.css'
|
||||
const motionSha256 = 'b2ee58c0caf560eeddc4eb8c91d86cabddbec634b8d37ed5d6b85d0644c0e142'
|
||||
const tokenFile = 'tokens-Df-i-zsO.css'
|
||||
const tokenSha256 = '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696'
|
||||
const hash = text => createHash('sha256').update(text).digest('hex')
|
||||
const runtime = JSON.parse(await readFile(resolve('.tmp/rea-runtime-scripts.json'), 'utf8'))
|
||||
if (runtime.evidence_id !== runtimeEvidenceId) throw new Error('Fixed runtime Evidence changed')
|
||||
const matches = runtime.items.filter(item => item.file_path?.replaceAll('\\', '/').endsWith(`/renderer/assets/${runtimeFile}`))
|
||||
if (matches.length !== 1) throw new Error('Expected exactly one panel layout runtime source')
|
||||
const artifact = matches[0].source?.artifact
|
||||
if (!matches[0].source.included || !artifact?.text || hash(artifact.text) !== runtimeSha256 || artifact.sha256 !== runtimeSha256) throw new Error('Fixed panel runtime SHA-256 mismatch')
|
||||
const source = artifact.text
|
||||
const ast = parse(source, { sourceType: 'module' })
|
||||
const constantNames = ['Rw', 'zw', 'Bw', 'Vw', 'JQe', 'f$e', 'p$e']
|
||||
const definitions = {
|
||||
normalizeInset: { parameters: 'e: number', result: 'number' },
|
||||
clampSidebarWidth: { parameters: 'e: number', result: 'number' },
|
||||
resolveSidebarDragAction: { parameters: 'e: number, t: boolean', result: "'expand' | 'keep-collapsed' | 'collapse' | 'resize'" },
|
||||
resolveChatAreaAvailableWidth: { parameters: 'e: PanelAvailableWidthInput', result: 'number' },
|
||||
resolveDetailDrawerWidth: { parameters: 'e: DetailDrawerWidthInput', result: 'number' },
|
||||
resolveLeadingInsetRebalanceWidth: { parameters: 'e: LeadingInsetRebalanceInput', result: 'number | undefined' },
|
||||
resolveDetailPanelLayoutWidths: { parameters: 'e: DetailPanelLayoutInput', result: 'DetailPanelLayoutWidths | undefined' },
|
||||
resolveRememberedDetailWidth: { parameters: 'e: readonly (number | null | undefined)[], t: number', implementationParameters: 'e: readonly any[], t: number', result: 'number' },
|
||||
resolveChatAreaWidth: { parameters: 'e: number, t: number', result: 'number' },
|
||||
getNarrowState: { parameters: 'e: number, t: number = 0', result: 'NarrowLayoutState' },
|
||||
stickyNarrow: { parameters: 'e: boolean, t: number, n: number', result: 'boolean' },
|
||||
resolveNarrowState: { parameters: 'e: NarrowLayoutState | null | undefined, t: number, n: number = 0', result: 'NarrowLayoutState' }
|
||||
}
|
||||
const functions = new Map(), constants = new Map()
|
||||
function walk(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'FunctionDeclaration' && definitions[node.id?.name]) {
|
||||
if (functions.has(node.id.name)) throw new Error(`Duplicate source function ${node.id.name}`)
|
||||
functions.set(node.id.name, node)
|
||||
}
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && constantNames.includes(node.left.name)
|
||||
&& (node.right.type === 'ObjectExpression' || (['f$e', 'p$e'].includes(node.left.name) && node.right.type === 'BinaryExpression'))) {
|
||||
if (constants.has(node.left.name)) throw new Error(`Duplicate panel constant ${node.left.name}`)
|
||||
constants.set(node.left.name, node.right)
|
||||
}
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if (key === 'loc' || key === 'extra') continue
|
||||
if (Array.isArray(value)) value.forEach(walk)
|
||||
else if (value && typeof value === 'object') walk(value)
|
||||
}
|
||||
}
|
||||
walk(ast)
|
||||
for (const name of Object.keys(definitions)) if (!functions.has(name)) throw new Error(`Missing source function ${name}`)
|
||||
for (const name of constantNames) if (!constants.has(name)) throw new Error(`Missing source constant ${name}`)
|
||||
|
||||
const cssDirectory = resolve('.tmp/rea-css-recovered')
|
||||
const manifest = JSON.parse(await readFile(resolve(cssDirectory, 'manifest.json'), 'utf8'))
|
||||
if (manifest.sourceEvidenceId !== cssEvidenceId) throw new Error('Fixed CSS Evidence changed')
|
||||
async function stylesheet(file, expected) {
|
||||
const content = await readFile(resolve(cssDirectory, file), 'utf8')
|
||||
if (manifest.files.find(item => item.file === file)?.sha256 !== expected || hash(content) !== expected) throw new Error(`Fixed ${file} SHA-256 mismatch`)
|
||||
return postcss.parse(content, { from: file })
|
||||
}
|
||||
const motion = await stylesheet(motionFile, motionSha256)
|
||||
const tokens = await stylesheet(tokenFile, tokenSha256)
|
||||
const tokenNames = ['--wb-motion-duration-medium', '--wb-motion-duration-fast', '--wb-motion-easing-decelerate', '--wb-motion-easing-accelerate']
|
||||
const tokenValues = new Map()
|
||||
tokens.walkDecls(decl => { if (tokenNames.includes(decl.prop) && decl.parent.selectors?.includes(':root')) tokenValues.set(decl.prop, decl.value) })
|
||||
for (const name of tokenNames) if (!tokenValues.has(name)) throw new Error(`Missing motion token ${name}`)
|
||||
const outputCss = postcss.root()
|
||||
outputCss.append(postcss.rule({ selector: '.source-detail-layout', nodes: tokenNames.map(name => postcss.decl({ prop: name, value: tokenValues.get(name) })) }))
|
||||
let rules = 0
|
||||
const selected = selector => /\.detail-panel-motion-[\w-]+|\.detail-panel-container(?:[\s.:#[]|$)/.test(selector)
|
||||
function scope(selector) {
|
||||
if (selector.startsWith('.detail-panel-motion-')) return `.source-detail-layout${selector}`
|
||||
return `.source-detail-layout ${selector}`
|
||||
}
|
||||
function project(node) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = node.selectors.filter(selected)
|
||||
if (!selectors.length) return null
|
||||
rules++
|
||||
return node.clone({ selector: selectors.map(scope).join(',') })
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes && !/keyframes$/.test(node.name)) {
|
||||
const children = node.nodes.map(project).filter(Boolean)
|
||||
return children.length ? node.clone({ nodes: children }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
for (const node of motion.nodes) { const projected = project(node); if (projected) outputCss.append(projected) }
|
||||
|
||||
const bodies = Object.fromEntries([...functions].map(([name, fn]) => [name, { offset: fn.start, bodySha256: hash(source.slice(fn.body.start, fn.body.end)) }]))
|
||||
const identities = {
|
||||
version: '5.8.2', runtimeEvidenceId, runtimeFile, runtimeSha256, cssEvidenceId, motionFile, motionSha256, tokenFile, tokenSha256,
|
||||
functions: bodies, constants: Object.fromEntries([...constants].map(([name, node]) => [name, hash(source.slice(node.start, node.end))]))
|
||||
}
|
||||
const interfaces = `export interface PanelAvailableWidthInput { containerWidth: number; sidebarWidth: number; leadingInset?: number }
|
||||
export interface DetailDrawerWidthInput extends PanelAvailableWidthInput { isMobile?: boolean; preferredWidth?: number }
|
||||
export interface DetailPanelLayoutInput extends PanelAvailableWidthInput { requestedDetailWidth: number }
|
||||
export interface DetailPanelLayoutWidths { mainWidth: number; detailWidth: number }
|
||||
export interface LeadingInsetRebalanceInput { previousInset: number; nextInset: number; isNarrowForDetail: boolean; isPanelVisible: boolean; rememberedDetailWidth: number | null; chatAreaWidth: number }
|
||||
export interface NarrowLayoutState { isMobile: boolean; isNarrowForDetail: boolean; isNarrowForSidebar: boolean; screenWidth: number }
|
||||
`
|
||||
const constantText = constantNames.map(name => `export const ${name} = ${source.slice(constants.get(name).start, constants.get(name).end)}${constants.get(name).type === 'ObjectExpression' ? ' as const' : ''}`).join('\n')
|
||||
const functionText = Object.entries(definitions).map(([name, spec]) => {
|
||||
const fn = functions.get(name)
|
||||
const overload = spec.implementationParameters ? `/** Preserve source JavaScript null-to-number comparisons behind the typed public overload. */\nexport function ${name}(${spec.parameters}): ${spec.result}\n` : ''
|
||||
return `${overload}export function ${name}(${spec.implementationParameters ?? spec.parameters}): ${spec.result} ${source.slice(fn.body.start, fn.body.end)}`
|
||||
}).join('\n\n')
|
||||
const toMilliseconds = value => value.endsWith('ms') ? Number.parseFloat(value) : Number.parseFloat(value) * 1000
|
||||
const motionConfig = { openMs: toMilliseconds(tokenValues.get('--wb-motion-duration-medium')), closeMs: toMilliseconds(tokenValues.get('--wb-motion-duration-fast')),
|
||||
openEasing: tokenValues.get('--wb-motion-easing-decelerate'), closeEasing: tokenValues.get('--wb-motion-easing-accelerate') }
|
||||
const typescript = `/* Generated by scripts/rea/recover-panel-layout.mjs from fixed REA Evidence.
|
||||
* Source function bodies and constant object expressions are unchanged; only exports/types are added.
|
||||
* The source runtime leading-inset store is replaced by the explicit zero-default adapter below.
|
||||
* Callers with a registered leading inset must pass leadingInset. Inputs otherwise retain source behavior.
|
||||
*/\n${interfaces}\nexport const SOURCE_PANEL_LAYOUT_EVIDENCE = ${JSON.stringify(identities, null, 2)} as const\n\n${constantText}\n\nexport const SIDEBAR_LAYOUT = Rw\nexport const CHAT_LAYOUT = zw\nexport const DETAIL_BREAKPOINT = Bw\nexport const DETAIL_PANEL_LAYOUT = Vw\nexport const DETAIL_NARROW_WIDTH = f$e\nexport const SIDEBAR_NARROW_WIDTH = p$e\nexport const DETAIL_PANEL_MOTION = ${JSON.stringify(motionConfig, null, 2)} as const\n\n/** Host adapter: the source getter returns the currently registered inset; local default is zero. */\nfunction getMainContentLeadingInset(): number { return 0 }\n\n${functionText}\n`
|
||||
const css = `/* Generated by scripts/rea/recover-panel-layout.mjs; WorkBuddy 5.8.2.
|
||||
* REA CSS ${cssEvidenceId}; ${motionFile} sha256=${motionSha256}.
|
||||
* Motion tokens ${tokenFile} sha256=${tokenSha256}.
|
||||
* ${rules} selected rules, original bodies/order and all enclosing media conditions preserved.
|
||||
* .source-detail-layout carries motion phase classes on the same element.
|
||||
*/\n${outputCss.toString()}\n`
|
||||
postcss.parse(css)
|
||||
const targetDirectory = resolve('src/renderer/src/workbuddy/workbench')
|
||||
for (const [file, contents] of [['source-panel-layout.ts', typescript], ['panel-motion-source.css', css]]) {
|
||||
const target = resolve(targetDirectory, file)
|
||||
if (process.argv.includes('--check')) {
|
||||
if (await readFile(target, 'utf8') !== contents) throw new Error(`${file} differs from the fixed source recovery`)
|
||||
} else await writeFile(target, contents)
|
||||
}
|
||||
console.log(JSON.stringify({ runtimeEvidenceId, cssEvidenceId, functions: functions.size, constants: constants.size, rules, motion: motionConfig, check: process.argv.includes('--check') }))
|
||||
@@ -0,0 +1,423 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverseModule from '@babel/traverse'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const EXPECTED_ROOT_SHA256 = '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
|
||||
const MAIN_EVIDENCE_PATH = '.tmp/rea-selected-editor-evidence.json'
|
||||
const DEPENDENCY_EVIDENCE_PATH = '.tmp/rea-selected-pierre-dependencies.json'
|
||||
const LEAF_EVIDENCE_PATH = '.tmp/rea-selected-pierre-leaf-dependencies.json'
|
||||
const LANGUAGE_EVIDENCE_PATH = '.tmp/rea-selected-pierre-ui-languages.json'
|
||||
const OUTPUT_DIRECTORY = path.resolve('src/renderer/src/workbuddy/artifacts/recovered-pierre')
|
||||
const VITE_PRELOAD_HELPER = '__vite__mapDeps'
|
||||
const RECOVERED_PRELOAD_HELPER = '__ycodePierreMapDeps'
|
||||
const traverse = traverseModule.default
|
||||
|
||||
const SOURCE_FILES = {
|
||||
'renderer/assets/codeEditor-D3P9alks.js': 'main',
|
||||
'renderer/assets/codeEditor-Bxmo_AI3.css': 'css',
|
||||
'package.json': 'package',
|
||||
'renderer/assets/rolldown-runtime-BJCASTsb.js': 'asset',
|
||||
'renderer/assets/preload-helper-D30X3e06.js': 'asset',
|
||||
'renderer/assets/dist-BAqWIeie.js': 'asset',
|
||||
'renderer/assets/dist-BUGqZtx5.js': 'asset',
|
||||
'renderer/assets/worker-portable-D1lwTqsk.js': 'asset',
|
||||
'renderer/assets/zwitch-B8a51bts.js': 'asset',
|
||||
'renderer/assets/stringify-entities-i2atvqYU.js': 'asset',
|
||||
'renderer/assets/hast-util-whitespace-DbScqzu3.js': 'asset',
|
||||
'renderer/assets/pierre-dark-Daa2n9wr.js': 'asset',
|
||||
'renderer/assets/pierre-light-D_hZXvUj.js': 'asset',
|
||||
'renderer/assets/javascript-AfZCJ9Uc.js': 'asset',
|
||||
'renderer/assets/typescript-4sRsPriL.js': 'asset',
|
||||
'renderer/assets/diff-lAxbRYdS.js': 'asset',
|
||||
'renderer/assets/css-NFwnoVns.js': 'asset',
|
||||
'renderer/assets/html-Dq1ixqYb.js': 'asset',
|
||||
'renderer/assets/json-CKKhFIOr.js': 'asset',
|
||||
'renderer/assets/markdown-BSrOIEaF.js': 'asset',
|
||||
}
|
||||
|
||||
const COPYING_ASSETS = Object.keys(SOURCE_FILES).filter((sourcePath) => SOURCE_FILES[sourcePath] === 'asset')
|
||||
const REACT_IMPORTS = new Map([
|
||||
['./react-z__Ncn__.js', 's'],
|
||||
['./react-dom-mnR6psRT.js', 'c'],
|
||||
['./jsx-runtime-DM1VED__.js', 'f'],
|
||||
])
|
||||
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
|
||||
const fail = (message) => {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
async function loadEvidence(filePath) {
|
||||
const evidence = JSON.parse(await fs.readFile(filePath, 'utf8'))
|
||||
if (evidence.authority !== 'shipped-artifact') fail(`Evidence is not shipped-artifact authority: ${filePath}`)
|
||||
if (evidence.normalized_result?.root_sha256 !== EXPECTED_ROOT_SHA256) {
|
||||
fail(`Evidence root SHA-256 does not match the selected WorkBuddy archive: ${filePath}`)
|
||||
}
|
||||
const files = new Map()
|
||||
for (const file of evidence.normalized_result.files) {
|
||||
const actualBytes = Buffer.byteLength(file.text, 'utf8')
|
||||
const actualSha256 = sha256(file.text)
|
||||
if (actualBytes !== file.size_bytes || actualSha256 !== file.sha256) {
|
||||
fail(`Evidence text failed size/SHA validation: ${file.path}`)
|
||||
}
|
||||
files.set(file.path, file)
|
||||
}
|
||||
return { evidence, files }
|
||||
}
|
||||
|
||||
function collectPatternNames(node, names = []) {
|
||||
if (!node) return names
|
||||
if (node.type === 'Identifier') names.push(node.name)
|
||||
else if (node.type === 'RestElement') collectPatternNames(node.argument, names)
|
||||
else if (node.type === 'AssignmentPattern') collectPatternNames(node.left, names)
|
||||
else if (node.type === 'ArrayPattern') node.elements.forEach((entry) => collectPatternNames(entry, names))
|
||||
else if (node.type === 'ObjectPattern') {
|
||||
for (const property of node.properties) {
|
||||
if (property.type === 'RestElement') collectPatternNames(property.argument, names)
|
||||
else collectPatternNames(property.value, names)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
function collectDynamicImportPaths(source) {
|
||||
const ast = parse(source, { sourceType: 'module' })
|
||||
const paths = new Set()
|
||||
traverse(ast, {
|
||||
ImportExpression(pathRef) {
|
||||
const sourceNode = pathRef.node.source
|
||||
if (sourceNode.type === 'StringLiteral') paths.add(sourceNode.value)
|
||||
else if (sourceNode.type === 'TemplateLiteral' && sourceNode.expressions.length === 0) {
|
||||
paths.add(sourceNode.quasis[0].value.cooked)
|
||||
}
|
||||
},
|
||||
})
|
||||
return paths
|
||||
}
|
||||
|
||||
function renameTopLevelBinding(source, fromName, toName, sourcePath) {
|
||||
if (!source.includes(fromName)) return { source, renameCount: 0 }
|
||||
if (source.includes(toName)) fail(`Reserved recovery identifier already exists in ${sourcePath}`)
|
||||
|
||||
const ast = parse(source, { sourceType: 'module' })
|
||||
let foundBinding = false
|
||||
traverse(ast, {
|
||||
Program(pathRef) {
|
||||
pathRef.scope.crawl()
|
||||
if (pathRef.scope.getBinding(fromName)) {
|
||||
pathRef.scope.rename(fromName, toName)
|
||||
foundBinding = true
|
||||
}
|
||||
pathRef.stop()
|
||||
},
|
||||
})
|
||||
if (!foundBinding) return { source, renameCount: 0 }
|
||||
|
||||
const edits = []
|
||||
traverse(ast, {
|
||||
Identifier(pathRef) {
|
||||
const { start, end, name } = pathRef.node
|
||||
if (name === toName && source.slice(start, end) === fromName) edits.push({ start, end })
|
||||
},
|
||||
})
|
||||
if (edits.length < 2) fail(`Babel scope rename did not capture the binding and its references in ${sourcePath}`)
|
||||
let renamedSource = source
|
||||
for (const { start, end } of edits.sort((left, right) => right.start - left.start)) {
|
||||
renamedSource = `${renamedSource.slice(0, start)}${toName}${renamedSource.slice(end)}`
|
||||
}
|
||||
return { source: renamedSource, renameCount: edits.length }
|
||||
}
|
||||
|
||||
function closeBindings(source) {
|
||||
const ast = parse(source, { sourceType: 'module' })
|
||||
let programPath
|
||||
traverse(ast, {
|
||||
Program(pathRef) {
|
||||
programPath = pathRef
|
||||
pathRef.scope.crawl()
|
||||
pathRef.stop()
|
||||
},
|
||||
})
|
||||
|
||||
const included = new Map()
|
||||
const queue = []
|
||||
const imported = new Map()
|
||||
const enqueue = (binding) => {
|
||||
if (!binding || binding.scope !== programPath.scope || included.has(binding.identifier.name)) return
|
||||
included.set(binding.identifier.name, binding)
|
||||
queue.push(binding)
|
||||
}
|
||||
const visitReferences = (pathRef) => {
|
||||
pathRef.traverse({
|
||||
Identifier(reference) {
|
||||
if (!reference.isReferencedIdentifier() && !reference.isBindingIdentifier()) return
|
||||
enqueue(reference.scope.getBinding(reference.node.name))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
enqueue(programPath.scope.getBinding('Po'))
|
||||
while (queue.length > 0) {
|
||||
const binding = queue.shift()
|
||||
if (binding.kind === 'module') {
|
||||
const declaration = binding.path.findParent((candidate) => candidate.isImportDeclaration())
|
||||
if (declaration) {
|
||||
const locals = imported.get(declaration.node.source.value) ?? new Set()
|
||||
locals.add(binding.identifier.name)
|
||||
imported.set(declaration.node.source.value, locals)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (binding.path.isFunctionDeclaration() || binding.path.isClassDeclaration()) {
|
||||
visitReferences(binding.path)
|
||||
continue
|
||||
}
|
||||
if (!binding.path.isVariableDeclarator()) continue
|
||||
|
||||
if (binding.path.node.init) visitReferences(binding.path.get('init'))
|
||||
else {
|
||||
for (const violation of binding.constantViolations) {
|
||||
visitReferences(violation)
|
||||
const owner = violation.findParent((candidate) => candidate.isVariableDeclarator())
|
||||
if (owner && owner !== binding.path && owner.node.id.type === 'Identifier') {
|
||||
enqueue(programPath.scope.getBinding(owner.node.id.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const declarationChunks = []
|
||||
for (const statement of ast.program.body) {
|
||||
if (statement.type === 'ImportDeclaration') {
|
||||
if (REACT_IMPORTS.has(statement.source.value)) continue
|
||||
const names = imported.get(statement.source.value)
|
||||
if (!names || names.size === 0) continue
|
||||
const specifiers = statement.specifiers.filter((specifier) => names.has(specifier.local.name))
|
||||
if (specifiers.length === 0) continue
|
||||
const defaultImport = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier')
|
||||
const namespaceImport = specifiers.find((specifier) => specifier.type === 'ImportNamespaceSpecifier')
|
||||
const namedImports = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier')
|
||||
const parts = []
|
||||
if (defaultImport) parts.push(defaultImport.local.name)
|
||||
if (namespaceImport) parts.push(`* as ${namespaceImport.local.name}`)
|
||||
if (namedImports.length > 0) {
|
||||
parts.push(`{ ${namedImports.map((specifier) => {
|
||||
const importedName = specifier.imported.name ?? specifier.imported.value
|
||||
return importedName === specifier.local.name ? importedName : `${importedName} as ${specifier.local.name}`
|
||||
}).join(', ')} }`)
|
||||
}
|
||||
declarationChunks.push(`import ${parts.join(', ')} from ${JSON.stringify(statement.source.value)};`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (statement.type === 'VariableDeclaration') {
|
||||
for (const declaration of statement.declarations) {
|
||||
const names = collectPatternNames(declaration.id)
|
||||
if (!names.some((name) => included.has(name))) continue
|
||||
declarationChunks.push(`${statement.kind} ${source.slice(declaration.start, declaration.end)};`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
|
||||
included.has(statement.id?.name)) {
|
||||
declarationChunks.push(source.slice(statement.start, statement.end))
|
||||
}
|
||||
}
|
||||
|
||||
const reactImports = [
|
||||
`import * as __ycodeReact from 'react';`,
|
||||
`import * as __ycodeReactDOM from 'react-dom';`,
|
||||
`import * as __ycodeJsxRuntime from 'react/jsx-runtime';`,
|
||||
`const s = () => __ycodeReact;`,
|
||||
`const c = () => __ycodeReactDOM;`,
|
||||
`const f = () => __ycodeJsxRuntime;`,
|
||||
]
|
||||
const prefix = [
|
||||
`// Generated from REA shipped-artifact Evidence; edits belong in recover-pierre-diff.mjs.`,
|
||||
`// Source: renderer/assets/codeEditor-D3P9alks.js; sha256: 5e9a16dca97c102afc3c73cd7dd32ba63daf5e7ddff12707ee1ea63e7a427b19.`,
|
||||
...reactImports,
|
||||
].join('\n')
|
||||
const body = declarationChunks.join('\n')
|
||||
const suffix = [
|
||||
`Po();`,
|
||||
`export { No as PierreDiffEditor, buildFileDiff, processFile, parseDiffFromFile, getFiletypeFromFileName };`,
|
||||
].join('\n')
|
||||
return { code: `${prefix}\n${body}\n${suffix}\n`, bindingCount: included.size, imports: new Set(imported.keys()) }
|
||||
}
|
||||
|
||||
async function writeIfAbsentOrSame(filePath, text) {
|
||||
try {
|
||||
const previous = await fs.readFile(filePath, 'utf8')
|
||||
if (previous === text) return
|
||||
const generatedEngine = path.basename(filePath) === 'engine.mjs' &&
|
||||
previous.startsWith('// Generated from REA shipped-artifact Evidence; edits belong in recover-pierre-diff.mjs.')
|
||||
let generatedManifest = false
|
||||
if (path.basename(filePath) === 'manifest.json') {
|
||||
try {
|
||||
generatedManifest = JSON.parse(previous).kind === 'rea-selected-pierre-diff-recovery'
|
||||
} catch {
|
||||
generatedManifest = false
|
||||
}
|
||||
}
|
||||
if (!generatedEngine && !generatedManifest) fail(`Refusing to overwrite changed recovered artifact: ${filePath}`)
|
||||
await fs.writeFile(filePath, text, 'utf8')
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error
|
||||
await fs.writeFile(filePath, text, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
const [mainSource, dependencies, leafDependencies, uiLanguages] = await Promise.all([
|
||||
loadEvidence(MAIN_EVIDENCE_PATH),
|
||||
loadEvidence(DEPENDENCY_EVIDENCE_PATH),
|
||||
loadEvidence(LEAF_EVIDENCE_PATH),
|
||||
loadEvidence(LANGUAGE_EVIDENCE_PATH),
|
||||
])
|
||||
const dynamicEvidencePaths = (await fs.readdir('.tmp'))
|
||||
.filter((fileName) => /^rea-selected-pierre-chunks-\d{3}\.json$/u.test(fileName))
|
||||
.map((fileName) => path.join('.tmp', fileName))
|
||||
.sort()
|
||||
if (dynamicEvidencePaths.length === 0) fail('No REA-selected Pierre dynamic chunk Evidence was found')
|
||||
const dynamicEvidence = await Promise.all(dynamicEvidencePaths.map(loadEvidence))
|
||||
const evidenceSet = [mainSource, dependencies, leafDependencies, uiLanguages, ...dynamicEvidence]
|
||||
const sources = new Map()
|
||||
for (const evidence of evidenceSet) {
|
||||
for (const [sourcePath, file] of evidence.files) {
|
||||
const existing = sources.get(sourcePath)
|
||||
if (existing && (existing.sha256 !== file.sha256 || existing.size_bytes !== file.size_bytes)) {
|
||||
fail(`Conflicting REA Evidence for selected file ${sourcePath}`)
|
||||
}
|
||||
sources.set(sourcePath, file)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [sourcePath, expectedKind] of Object.entries(SOURCE_FILES)) {
|
||||
if (!sources.has(sourcePath)) fail(`Required selected REA source is missing (${expectedKind}): ${sourcePath}`)
|
||||
}
|
||||
const appPackage = JSON.parse(sources.get('package.json').text)
|
||||
if (appPackage.version !== '5.8.2') fail(`Unexpected WorkBuddy app version: ${appPackage.version}`)
|
||||
|
||||
const mainText = sources.get('renderer/assets/codeEditor-D3P9alks.js').text
|
||||
const dynamicImportPaths = collectDynamicImportPaths(mainText)
|
||||
const dynamicAssetPaths = [...dynamicImportPaths].map((importPath) => {
|
||||
if (!importPath.startsWith('./') || !importPath.endsWith('.js')) {
|
||||
fail(`Unexpected non-local dynamic chunk path in CodeView: ${importPath}`)
|
||||
}
|
||||
return `renderer/assets/${importPath.slice(2)}`
|
||||
})
|
||||
for (const sourcePath of dynamicAssetPaths) {
|
||||
if (!sources.has(sourcePath)) fail(`REA dynamic chunk evidence is missing ${sourcePath}`)
|
||||
}
|
||||
const extracted = closeBindings(mainText)
|
||||
if (extracted.bindingCount < 700) fail(`Pierre closure is unexpectedly incomplete: ${extracted.bindingCount} bindings`)
|
||||
if (extracted.imports.has('./src-0FGg1seX.js') || extracted.imports.has('./monaco-init-CE4MbM4x.js')) {
|
||||
fail('Pierre closure unexpectedly depends on app shared code or Monaco initialization')
|
||||
}
|
||||
const renamedEngine = renameTopLevelBinding(
|
||||
extracted.code,
|
||||
VITE_PRELOAD_HELPER,
|
||||
RECOVERED_PRELOAD_HELPER,
|
||||
'recovered engine.mjs',
|
||||
)
|
||||
|
||||
const cssSource = sources.get('renderer/assets/codeEditor-Bxmo_AI3.css').text
|
||||
const cssAst = postcss.parse(cssSource)
|
||||
const cssRules = []
|
||||
cssAst.walkRules((rule) => {
|
||||
if (rule.selectors?.some((selector) => selector.includes('_pierre-diff-editor'))) {
|
||||
cssRules.push(cssSource.slice(rule.source.start.offset, rule.source.end.offset))
|
||||
}
|
||||
})
|
||||
if (cssRules.length !== 8) fail(`Expected eight exact Pierre CSS module rules, found ${cssRules.length}`)
|
||||
|
||||
await fs.mkdir(OUTPUT_DIRECTORY, { recursive: true })
|
||||
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, 'engine.mjs'), renamedEngine.source)
|
||||
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, 'engine.css'), `${cssRules.join('\n')}\n`)
|
||||
|
||||
const assetRecords = []
|
||||
const copyingAssetPaths = [...new Set([...COPYING_ASSETS, ...dynamicAssetPaths])]
|
||||
for (const sourcePath of copyingAssetPaths) {
|
||||
const source = sources.get(sourcePath)
|
||||
if (!source) fail(`Required selected REA asset is missing: ${sourcePath}`)
|
||||
const fileName = path.posix.basename(sourcePath)
|
||||
const renamedAsset = renameTopLevelBinding(
|
||||
source.text,
|
||||
VITE_PRELOAD_HELPER,
|
||||
RECOVERED_PRELOAD_HELPER,
|
||||
sourcePath,
|
||||
)
|
||||
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, fileName), renamedAsset.source)
|
||||
assetRecords.push({
|
||||
path: fileName,
|
||||
bytes: source.size_bytes,
|
||||
sha256: source.sha256,
|
||||
renamed_top_level_preload_helper_references: renamedAsset.renameCount,
|
||||
})
|
||||
}
|
||||
|
||||
const outputs = ['engine.mjs', 'engine.css', ...assetRecords.map((asset) => asset.path)]
|
||||
const outputRecords = []
|
||||
for (const fileName of outputs) {
|
||||
const content = await fs.readFile(path.join(OUTPUT_DIRECTORY, fileName))
|
||||
outputRecords.push({ path: fileName, bytes: content.length, sha256: sha256(content) })
|
||||
}
|
||||
const manifest = {
|
||||
kind: 'rea-selected-pierre-diff-recovery',
|
||||
target: { name: appPackage.name, version: appPackage.version, root_sha256: EXPECTED_ROOT_SHA256 },
|
||||
evidence: {
|
||||
renderer: mainSource.evidence.evidence_id,
|
||||
dependencies: dependencies.evidence.evidence_id,
|
||||
leaf_dependencies: leafDependencies.evidence.evidence_id,
|
||||
ui_languages: uiLanguages.evidence.evidence_id,
|
||||
dynamic_chunks: dynamicEvidence.map(({ evidence }) => evidence.evidence_id),
|
||||
},
|
||||
renderer_source: {
|
||||
path: 'renderer/assets/codeEditor-D3P9alks.js',
|
||||
sha256: '5e9a16dca97c102afc3c73cd7dd32ba63daf5e7ddff12707ee1ea63e7a427b19',
|
||||
closure_program_bindings: extracted.bindingCount,
|
||||
imports: [...extracted.imports].sort(),
|
||||
dynamic_import_count: dynamicImportPaths.size,
|
||||
dynamic_import_files: dynamicAssetPaths,
|
||||
renamed_top_level_preload_helper: {
|
||||
from: VITE_PRELOAD_HELPER,
|
||||
to: RECOVERED_PRELOAD_HELPER,
|
||||
references: renamedEngine.renameCount,
|
||||
},
|
||||
},
|
||||
css_source: {
|
||||
path: 'renderer/assets/codeEditor-Bxmo_AI3.css',
|
||||
sha256: '1c8f2210c731a25f9051088df0db78b9de5b087c802b3a9e36828c1b6b54c353',
|
||||
exact_pierre_rules: cssRules.length,
|
||||
},
|
||||
dynamic_chunk_assets: dynamicAssetPaths.length,
|
||||
vite_preload_helper_rename: {
|
||||
from: VITE_PRELOAD_HELPER,
|
||||
to: RECOVERED_PRELOAD_HELPER,
|
||||
affected_assets: assetRecords.filter((asset) => asset.renamed_top_level_preload_helper_references > 0)
|
||||
.map((asset) => ({ path: asset.path, references: asset.renamed_top_level_preload_helper_references })),
|
||||
},
|
||||
limitations: [
|
||||
'The original package manifest contains no Pierre or Shiki package version; this is a source-locked bundle recovery, not an npm package pin.',
|
||||
'The optional shiki-wasm branch references renderer/assets/wasm-B9ZqxnKj.js, which the REA selector reports absent; the recovered component config uses the shiki-js engine.',
|
||||
'Every static dynamic import in codeEditor-D3P9alks.js was selected through REA and copied; unselected app-shared and Monaco bundles are excluded from this component closure.',
|
||||
],
|
||||
assets: assetRecords,
|
||||
outputs: outputRecords,
|
||||
}
|
||||
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
|
||||
console.log(JSON.stringify({
|
||||
outputDirectory: OUTPUT_DIRECTORY,
|
||||
closureProgramBindings: extracted.bindingCount,
|
||||
closureBytes: Buffer.byteLength(extracted.code, 'utf8'),
|
||||
renamedPreloadHelperReferences: renamedEngine.renameCount,
|
||||
importedModules: [...extracted.imports].sort(),
|
||||
dynamicChunks: dynamicImportPaths.size,
|
||||
cssRules: cssRules.length,
|
||||
assets: assetRecords.length,
|
||||
manifest: path.join(OUTPUT_DIRECTORY, 'manifest.json'),
|
||||
}, null, 2))
|
||||
@@ -0,0 +1,133 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const runtimeFile = 'src-0FGg1seX.js'
|
||||
const runtimeSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
||||
const localIconsPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionSourceIcons.tsx')
|
||||
const questionPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionFloating.tsx')
|
||||
|
||||
const iconSpecs = [
|
||||
{ sourceName: 'HelpCircleIcon', localName: 'QuestionHelpCircleIcon', start: 911540, end: 912018 },
|
||||
{ sourceName: 'ChevronLeftIcon', localName: 'QuestionChevronLeftIcon', start: 912019, end: 912255 },
|
||||
{ sourceName: 'ChevronRightIcon', localName: 'QuestionChevronRightIcon', start: 912256, end: 912492 },
|
||||
{ sourceName: 'CloseIcon$2', localName: 'QuestionCloseIcon', start: 245582, end: 246061 },
|
||||
{ sourceName: 'RightArrowSubmitIcon', localName: 'QuestionRightArrowSubmitIcon', start: 912493, end: 912815 },
|
||||
{ sourceName: 'SendUpIcon', localName: 'QuestionSendUpIcon', start: 912816, end: 913845 },
|
||||
{ sourceName: 'CheckboxIcon', localName: 'QuestionCheckboxIcon', start: 913846, end: 914482 },
|
||||
{ sourceName: 'EditorIcon', localName: 'QuestionEditorIcon', start: 256368, end: 257356 }
|
||||
]
|
||||
|
||||
function sha256(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function findDefinition(source, name, from = 0) {
|
||||
const start = source.indexOf(`${name}=`, from)
|
||||
if (start < 0 || /[\w$]/.test(source[start - 1] || '')) throw new Error(`REA runtime definition not found: ${name}`)
|
||||
|
||||
const singleQuote = String.fromCharCode(39)
|
||||
const doubleQuote = String.fromCharCode(34)
|
||||
const backtick = String.fromCharCode(96)
|
||||
const backslash = String.fromCharCode(92)
|
||||
let round = 0
|
||||
let curly = 0
|
||||
let square = 0
|
||||
let quote = ''
|
||||
let escaped = false
|
||||
|
||||
for (let offset = start + name.length + 1; offset < source.length; offset += 1) {
|
||||
const character = source[offset]
|
||||
if (quote) {
|
||||
if (escaped) escaped = false
|
||||
else if (character === backslash) escaped = true
|
||||
else if (character === quote) quote = ''
|
||||
continue
|
||||
}
|
||||
if (character === singleQuote || character === doubleQuote || character === backtick) {
|
||||
quote = character
|
||||
continue
|
||||
}
|
||||
if (round === 0 && curly === 0 && square === 0 && (character === ',' || character === '}')) {
|
||||
return { start, end: offset, text: source.slice(start, offset) }
|
||||
}
|
||||
if (character === '(') round += 1
|
||||
else if (character === ')') round -= 1
|
||||
else if (character === '{') curly += 1
|
||||
else if (character === '}') curly -= 1
|
||||
else if (character === '[') square += 1
|
||||
else if (character === ']') square -= 1
|
||||
if (round < 0 || curly < 0 || square < 0) throw new Error(`Unbalanced REA runtime definition: ${name}`)
|
||||
}
|
||||
throw new Error(`REA runtime definition has no boundary: ${name}`)
|
||||
}
|
||||
|
||||
function localFunctionBody(source, name) {
|
||||
const start = source.indexOf(`export function ${name}`)
|
||||
if (start < 0) throw new Error(`Local source icon is missing: ${name}`)
|
||||
const end = source.indexOf('\n}\n', start)
|
||||
if (end < 0) throw new Error(`Local source icon is unterminated: ${name}`)
|
||||
return source.slice(start, end + 2)
|
||||
}
|
||||
|
||||
function sourceGeometry(definition) {
|
||||
const values = []
|
||||
const geometryProperty = /(?:d|points|x|y|cx|cy|r|rx|ry|x1|y1|x2|y2):`([^`]+)`/g
|
||||
for (const match of definition.matchAll(geometryProperty)) values.push(match[1])
|
||||
for (const match of definition.matchAll(/`(var\(--cb-question-floating-[^)]+\))`/g)) values.push(match[1])
|
||||
return [...new Set(values)]
|
||||
}
|
||||
|
||||
const runtimeExport = JSON.parse(await readFile(resolve(rootDir, '.tmp/rea-runtime-scripts.json'), 'utf8'))
|
||||
if (runtimeExport.evidence_id !== runtimeEvidenceId) {
|
||||
throw new Error(`REA runtime Evidence ID changed: expected ${runtimeEvidenceId}, found ${runtimeExport.evidence_id ?? 'none'}`)
|
||||
}
|
||||
const runtimeItem = runtimeExport.items.find(item => item.file_path?.replaceAll('\\', '/').endsWith(`/assets/${runtimeFile}`))
|
||||
const runtimeSource = runtimeItem?.source?.artifact?.text
|
||||
if (!runtimeSource) throw new Error(`REA runtime script is missing: ${runtimeFile}`)
|
||||
const actualRuntimeSha = sha256(Buffer.from(runtimeSource, 'utf8'))
|
||||
if (runtimeItem.source.artifact.sha256 !== runtimeSha256 || actualRuntimeSha !== runtimeSha256) {
|
||||
throw new Error(`REA runtime script digest mismatch: expected ${runtimeSha256}, export ${runtimeItem.source.artifact.sha256 ?? 'none'}, actual ${actualRuntimeSha}`)
|
||||
}
|
||||
|
||||
const localIcons = await readFile(localIconsPath, 'utf8')
|
||||
const recovered = iconSpecs.map(spec => {
|
||||
const definition = findDefinition(runtimeSource, spec.sourceName, spec.start - 1)
|
||||
if (definition.start !== spec.start || definition.end !== spec.end) {
|
||||
throw new Error(`${spec.sourceName} UTF-16 range changed: expected [${spec.start}, ${spec.end}), found [${definition.start}, ${definition.end})`)
|
||||
}
|
||||
|
||||
const body = localFunctionBody(localIcons, spec.localName)
|
||||
const rangeNote = `UTF-16 [${definition.start}, ${definition.end})`
|
||||
if (!localIcons.includes(rangeNote)) throw new Error(`${spec.localName} is missing its REA source range comment`)
|
||||
const missingGeometry = sourceGeometry(definition.text).filter(value => !body.includes(value))
|
||||
if (missingGeometry.length) throw new Error(`${spec.localName} does not match ${spec.sourceName}: ${missingGeometry.join(' | ')}`)
|
||||
return {
|
||||
sourceName: spec.sourceName,
|
||||
localName: spec.localName,
|
||||
utf16Range: [definition.start, definition.end],
|
||||
definitionSha256: sha256(Buffer.from(definition.text, 'utf8'))
|
||||
}
|
||||
})
|
||||
|
||||
const questionSource = await readFile(questionPath, 'utf8')
|
||||
const footerOffset = runtimeSource.indexOf('QuestionFloatingFooter=')
|
||||
const footerSource = runtimeSource.slice(footerOffset, footerOffset + 1200)
|
||||
if (footerOffset < 0 || !footerSource.includes('j=s?SendUpIcon:RightArrowSubmitIcon')) {
|
||||
throw new Error('REA QuestionFloatingFooter no longer selects SendUpIcon for the last question')
|
||||
}
|
||||
if (!questionSource.includes('QuestionSendUpIcon') || !questionSource.includes('<QuestionSendUpIcon />') || questionSource.includes('RuntimeSendIcon')) {
|
||||
throw new Error('QuestionFloating must use the recovered QuestionSendUpIcon for its send action')
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
runtimeEvidenceId,
|
||||
runtimeFile,
|
||||
runtimeSha256,
|
||||
footerOffset,
|
||||
footerSelection: 'j=s?SendUpIcon:RightArrowSubmitIcon',
|
||||
recovered,
|
||||
}, null, 2))
|
||||
@@ -0,0 +1,472 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const exportDir = resolve(rootDir, '.tmp/rea-css-recovered')
|
||||
const runtimePath = resolve(rootDir, '.tmp/rea-runtime-scripts.json')
|
||||
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/question-source.css')
|
||||
|
||||
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
||||
const runtimeFile = 'src-0FGg1seX.js'
|
||||
const runtimeSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
||||
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
const inlineResetFile = 'inline-90c5e4f0726a5658.css'
|
||||
const inlineResetSha256 = '90c5e4f0726a56583f83dcffecc1fe87b5ad54ec6d0d593008ac033c8fe931c9'
|
||||
const optionRuntimeClassOffset = 1043927
|
||||
|
||||
const sourceSpecs = [
|
||||
{
|
||||
file: 'src-CBaV_A3S.css',
|
||||
sha256: '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c'
|
||||
},
|
||||
{
|
||||
file: 'cb-bridge-jsj-8pWS.css',
|
||||
sha256: '71007d54555800c62d65d6188c743d71481cd9034e0609d3c63bb2626f9d5446'
|
||||
}
|
||||
]
|
||||
|
||||
const classAliases = {
|
||||
questionFloating: 'question-floating',
|
||||
questionFloatingSlideUp: null,
|
||||
header: 'question-floating__header',
|
||||
headerLeft: 'question-floating__heading',
|
||||
title: 'question-floating__title',
|
||||
titleHelp: 'question-floating__hint',
|
||||
headerRight: 'question-floating__header-actions',
|
||||
pagination: 'question-floating__pagination',
|
||||
pageBtn: 'question-floating__page-button',
|
||||
pageInfo: 'question-floating__pagination-info',
|
||||
skipAllBtn: 'question-floating__dismiss',
|
||||
content: 'question-floating__content',
|
||||
options: 'question-floating__options',
|
||||
optionItem: 'question-floating__option',
|
||||
optionArrow: 'question-floating__option-arrow',
|
||||
optionNumber: 'question-floating__option-number',
|
||||
selected: 'question-floating__option--selected',
|
||||
checkbox: 'question-floating__checkbox',
|
||||
disabled: 'question-floating__option--disabled',
|
||||
optionText: 'question-floating__option-copy',
|
||||
optionLabel: 'question-floating__option-label',
|
||||
optionDesc: 'question-floating__option-description',
|
||||
optionHelp: 'question-floating__option-help',
|
||||
customInputRow: 'question-floating__custom',
|
||||
customInputIcon: 'question-floating__custom-icon',
|
||||
focused: 'question-floating__custom--focused',
|
||||
customInput: 'question-floating__custom-input',
|
||||
footer: 'question-floating__footer',
|
||||
footerSingle: 'question-floating__footer--single',
|
||||
selectedCount: 'question-floating__selected-count',
|
||||
footerActions: 'question-floating__footer-actions',
|
||||
skipBtn: 'question-floating__skip',
|
||||
iconBtn: 'question-floating__advance',
|
||||
iconBtnDisabled: 'question-floating__advance--disabled',
|
||||
iconBtnActive: 'question-floating__advance--active',
|
||||
iconBtnSend: 'question-floating__advance--send',
|
||||
confirmVariant: 'question-floating--confirm'
|
||||
}
|
||||
|
||||
function sha256(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function sourceComment({ file, hash, offset, selectors, evidenceId, note }) {
|
||||
return postcss.comment({
|
||||
text: ` REA evidence=${evidenceId}; source=${file}; sha256=${hash}; offset=${offset}; original-selectors=${JSON.stringify(selectors)}${note ? `; ${note}` : ''} `
|
||||
})
|
||||
}
|
||||
|
||||
function isGlobalButtonOrUniversalSelector(selector) {
|
||||
const normalized = selector
|
||||
.replace(/:where\(\s*(button|input|select|textarea)\s*\)/gi, '$1')
|
||||
.replace(/:is\(\s*(button|input|select|textarea)\s*\)/gi, '$1')
|
||||
return normalized === '*' || /^(?:(?:html|body|:root)\s+)*(?:button|input|select|textarea)(?=$|[:[])/i.test(normalized)
|
||||
}
|
||||
|
||||
function getClassMap(source) {
|
||||
const footerOffset = source.indexOf('QuestionFloatingFooter=')
|
||||
if (footerOffset < 0) throw new Error(`Runtime source is missing QuestionFloatingFooter: ${runtimeFile}`)
|
||||
|
||||
const candidates = [...source.matchAll(/cH=\{([^{}]+)\}/g)]
|
||||
.filter(match => match.index < footerOffset && footerOffset - match.index < 12000)
|
||||
const match = candidates.at(-1)
|
||||
if (!match) throw new Error(`Runtime source is missing the QuestionFloating CSS module map: ${runtimeFile}`)
|
||||
const componentOffset = source.indexOf('xH=(0,yH.forwardRef', match.index)
|
||||
if (componentOffset < match.index) {
|
||||
throw new Error(`Runtime source is missing the QuestionFloating component after its CSS module map: ${runtimeFile}`)
|
||||
}
|
||||
|
||||
const bindings = Object.fromEntries(
|
||||
[...match[1].matchAll(/([A-Za-z_$][\w$]*):([A-Za-z_$][\w$]*)/g)]
|
||||
.map(item => [item[1], item[2]])
|
||||
)
|
||||
const classNames = {}
|
||||
for (const [key, variable] of Object.entries(bindings)) {
|
||||
const assignment = variable + '=`'
|
||||
const position = source.lastIndexOf(assignment, match.index)
|
||||
if (position < 0 || /[\w$]/.test(source[position - 1] || '')) {
|
||||
throw new Error(`Runtime source is missing the CSS module binding ${variable} for ${key}`)
|
||||
}
|
||||
const start = position + assignment.length
|
||||
const end = source.indexOf('`', start)
|
||||
if (end < 0) throw new Error(`Runtime source has an unterminated CSS module binding for ${key}`)
|
||||
classNames[key] = source.slice(start, end)
|
||||
}
|
||||
|
||||
const missingAliases = Object.keys(bindings).filter(key => !(key in classAliases))
|
||||
const extraAliases = Object.keys(classAliases).filter(key => !(key in bindings))
|
||||
if (missingAliases.length || extraAliases.length) {
|
||||
throw new Error(`QuestionFloating module map changed: missing aliases [${missingAliases.join(', ')}], stale aliases [${extraAliases.join(', ')}]`)
|
||||
}
|
||||
|
||||
const rootClass = classNames.questionFloating
|
||||
const moduleId = /_([A-Za-z0-9]+)_\d+$/.exec(rootClass)?.[1]
|
||||
if (!moduleId || !rootClass.startsWith('_questionFloating_')) {
|
||||
throw new Error(`Unexpected QuestionFloating module class: ${rootClass}`)
|
||||
}
|
||||
for (const [key, className] of Object.entries(classNames)) {
|
||||
if (!className.includes(`_${moduleId}_`)) {
|
||||
throw new Error(`QuestionFloating class ${key} is outside CSS module ${moduleId}: ${className}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { componentOffset, footerOffset, moduleMapOffset: match.index, moduleId, classNames }
|
||||
}
|
||||
|
||||
function classPattern(className) {
|
||||
return new RegExp(`\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`)
|
||||
}
|
||||
|
||||
function rewriteSelector(selector, classNames) {
|
||||
let rewritten = selector
|
||||
for (const [key, sourceClass] of Object.entries(classNames)) {
|
||||
const alias = classAliases[key]
|
||||
if (!alias) continue
|
||||
rewritten = rewritten.replace(
|
||||
new RegExp(`\\.${sourceClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`, 'g'),
|
||||
`.${alias}`
|
||||
)
|
||||
}
|
||||
return `.chat-pane.cr-theme ${rewritten}`
|
||||
}
|
||||
|
||||
function projectModuleNode(node, source, classNames, selectedRules) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = postcss.list.comma(node.selector).map(selector => selector.trim())
|
||||
const selected = selectors.filter(selector => Object.entries(classNames).some(([key, className]) => (
|
||||
classAliases[key] && classPattern(className).test(selector)
|
||||
)))
|
||||
if (selected.length === 0) return []
|
||||
|
||||
const projected = selected.map(selector => rewriteSelector(selector, classNames))
|
||||
selectedRules.push({ file: source.file, originalSelectors: selected, offset: node.source?.start?.offset ?? 0 })
|
||||
return [
|
||||
sourceComment({
|
||||
file: source.file,
|
||||
hash: source.sha256,
|
||||
offset: node.source?.start?.offset ?? 0,
|
||||
selectors: selected,
|
||||
evidenceId: cssEvidenceId,
|
||||
note: `runtime stylesheet ASAR comparison=${source.asarComparison}`
|
||||
}),
|
||||
node.clone({ selector: projected.join(',') })
|
||||
]
|
||||
}
|
||||
|
||||
if (node.type !== 'atrule' || !node.nodes) return []
|
||||
const children = node.nodes.flatMap(child => projectModuleNode(child, source, classNames, selectedRules))
|
||||
if (children.length === 0) return []
|
||||
const projected = node.clone()
|
||||
projected.removeAll()
|
||||
projected.append(children)
|
||||
return [projected]
|
||||
}
|
||||
|
||||
function projectThemeTokens(root, source, output, records) {
|
||||
root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
const isDark = selectors.some(selector => /(?:^|[.:])(?:dark|cb-dark)(?:$|[.: ])/.test(selector))
|
||||
const isLight = selectors.some(selector => /(?:^|[.:])(?:light|cb-light)(?:$|[.: ])/.test(selector))
|
||||
const theme = isDark ? 'dark' : isLight ? 'light' : null
|
||||
if (!theme) return
|
||||
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--cb-question-floating-'))
|
||||
if (declarations.length === 0) return
|
||||
const selector = `.chat-pane.cr-theme[data-cr-theme=${theme}]`
|
||||
const offset = rule.source?.start?.offset ?? 0
|
||||
records.push({ file: source.file, originalSelectors: selectors, offset, projectedSelector: selector })
|
||||
output.append(sourceComment({ file: source.file, hash: source.sha256, offset, selectors, evidenceId: cssEvidenceId, note: `theme token projection=${theme}` }))
|
||||
output.append(postcss.rule({ selector, nodes: declarations.map(node => node.clone()) }))
|
||||
})
|
||||
}
|
||||
|
||||
function projectBridgeTokens(root, source, output, records) {
|
||||
root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--cb-question-floating-'))
|
||||
if (declarations.length === 0) return
|
||||
|
||||
let projectedSelector
|
||||
if (selectors.includes(':root') || selectors.some(selector => selector.startsWith(':root.'))) {
|
||||
projectedSelector = '.chat-pane.cr-theme'
|
||||
} else if (selectors.some(selector => selector.includes('[class*=questionFloating]'))) {
|
||||
projectedSelector = '.chat-pane.cr-theme .question-floating'
|
||||
} else if (selectors.some(selector => selector.includes('[class*=skipBtn]'))) {
|
||||
projectedSelector = '.chat-pane.cr-theme .question-floating__skip'
|
||||
}
|
||||
if (!projectedSelector) return
|
||||
|
||||
const offset = rule.source?.start?.offset ?? 0
|
||||
records.push({ file: source.file, originalSelectors: selectors, offset, projectedSelector })
|
||||
output.append(sourceComment({ file: source.file, hash: source.sha256, offset, selectors, evidenceId: cssEvidenceId, note: `theme bridge projection=${projectedSelector}` }))
|
||||
output.append(postcss.rule({ selector: projectedSelector, nodes: declarations.map(node => node.clone()) }))
|
||||
})
|
||||
}
|
||||
|
||||
function formatContainer(container, depth) {
|
||||
if (!container.nodes) return
|
||||
container.raws.semicolon = true
|
||||
for (const child of container.nodes) {
|
||||
child.raws.before = `\n${' '.repeat(Math.max(depth + 1, 0))}`
|
||||
if (child.type === 'decl') {
|
||||
child.raws.between = ': '
|
||||
continue
|
||||
}
|
||||
if (child.nodes) {
|
||||
child.raws.between = ' '
|
||||
formatContainer(child, depth + 1)
|
||||
}
|
||||
}
|
||||
container.raws.after = `\n${' '.repeat(Math.max(depth, 0))}`
|
||||
}
|
||||
|
||||
const runtimeBytes = await readFile(runtimePath)
|
||||
const runtimeExport = JSON.parse(runtimeBytes.toString('utf8'))
|
||||
if (runtimeExport.evidence_id !== runtimeEvidenceId) {
|
||||
throw new Error(`REA runtime Evidence ID changed: expected ${runtimeEvidenceId}, found ${runtimeExport.evidence_id ?? 'none'}`)
|
||||
}
|
||||
const runtimeItem = runtimeExport.items.find(item => item.file_path?.replaceAll('\\', '/').endsWith(`/assets/${runtimeFile}`))
|
||||
if (!runtimeItem?.source?.artifact?.text) throw new Error(`REA runtime script is missing: ${runtimeFile}`)
|
||||
const runtimeSource = runtimeItem.source.artifact.text
|
||||
const actualRuntimeSha = sha256(Buffer.from(runtimeSource, 'utf8'))
|
||||
if (runtimeItem.source.artifact.sha256 !== runtimeSha256 || actualRuntimeSha !== runtimeSha256) {
|
||||
throw new Error(`REA runtime script digest mismatch for ${runtimeFile}: expected ${runtimeSha256}, export ${runtimeItem.source.artifact.sha256 ?? 'none'}, actual ${actualRuntimeSha}`)
|
||||
}
|
||||
|
||||
const { componentOffset, footerOffset, moduleMapOffset, moduleId, classNames } = getClassMap(runtimeSource)
|
||||
const cssManifest = JSON.parse(await readFile(resolve(exportDir, 'manifest.json'), 'utf8'))
|
||||
if (cssManifest.sourceEvidenceId !== cssEvidenceId) {
|
||||
throw new Error(`REA CSS Evidence ID changed: expected ${cssEvidenceId}, found ${cssManifest.sourceEvidenceId ?? 'none'}`)
|
||||
}
|
||||
|
||||
const globalResetRules = []
|
||||
const globalButtonTextAlignRules = []
|
||||
for (const record of cssManifest.files ?? []) {
|
||||
const bytes = await readFile(resolve(exportDir, record.file))
|
||||
const actualHash = sha256(bytes)
|
||||
if (!record.sha256 || actualHash !== record.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch during global reset scan for ${record.file}: manifest ${record.sha256 ?? 'none'}, actual ${actualHash}`)
|
||||
}
|
||||
const root = postcss.parse(bytes.toString('utf8'), { from: record.file })
|
||||
root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
const relevantSelectors = selectors.filter(isGlobalButtonOrUniversalSelector)
|
||||
if (relevantSelectors.length === 0) return
|
||||
const declarations = rule.nodes.filter(node => node.type === 'decl')
|
||||
const textAlign = declarations.filter(node => node.prop.toLowerCase() === 'text-align')
|
||||
if (relevantSelectors.includes('*')) {
|
||||
globalResetRules.push({
|
||||
file: record.file,
|
||||
sha256: actualHash,
|
||||
originalSelectors: selectors,
|
||||
offset: rule.source?.start?.offset ?? 0,
|
||||
declarations: declarations.map(node => node.clone())
|
||||
})
|
||||
}
|
||||
if (textAlign.length) {
|
||||
globalButtonTextAlignRules.push({
|
||||
file: record.file,
|
||||
sha256: actualHash,
|
||||
originalSelectors: selectors,
|
||||
relevantSelectors,
|
||||
offset: rule.source?.start?.offset ?? 0,
|
||||
declarations: textAlign.map(node => ({ property: node.prop, value: node.value }))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sources = new Map()
|
||||
for (const spec of sourceSpecs) {
|
||||
const bytes = await readFile(resolve(exportDir, spec.file))
|
||||
const manifestRecord = cssManifest.files?.find(record => record.file === spec.file)
|
||||
const actualHash = sha256(bytes)
|
||||
if (!manifestRecord?.sha256 || manifestRecord.sha256 !== spec.sha256 || actualHash !== spec.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch for ${spec.file}: expected ${spec.sha256}, manifest ${manifestRecord?.sha256 ?? 'none'}, actual ${actualHash}`)
|
||||
}
|
||||
sources.set(spec.file, {
|
||||
file: spec.file,
|
||||
sha256: actualHash,
|
||||
asarComparison: manifestRecord.asarComparison?.status ?? 'not-compared',
|
||||
root: postcss.parse(bytes.toString('utf8'), { from: spec.file })
|
||||
})
|
||||
}
|
||||
|
||||
const componentCss = sources.get('src-CBaV_A3S.css')
|
||||
const bridgeCss = sources.get('cb-bridge-jsj-8pWS.css')
|
||||
const optionClassName = classNames.optionItem
|
||||
const optionCssRules = []
|
||||
componentCss.root.walkRules(rule => {
|
||||
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
||||
if (selectors.includes(`.${optionClassName}`)) {
|
||||
optionCssRules.push({
|
||||
originalSelectors: selectors,
|
||||
offset: rule.source?.start?.offset ?? 0,
|
||||
textAlign: rule.nodes.filter(node => node.type === 'decl' && node.prop.toLowerCase() === 'text-align').map(node => node.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
if (optionCssRules.length !== 1 || optionCssRules[0].offset !== 650985 || optionCssRules[0].textAlign.length > 0) {
|
||||
throw new Error(`QuestionFloating option CSS evidence changed: ${JSON.stringify(optionCssRules)}`)
|
||||
}
|
||||
|
||||
const inlineResetRule = globalResetRules.find(record => record.file === inlineResetFile && record.originalSelectors.includes('*'))
|
||||
if (!inlineResetRule || inlineResetRule.sha256 !== inlineResetSha256 || inlineResetRule.offset !== 5) {
|
||||
throw new Error(`REA global reset evidence changed: ${JSON.stringify(inlineResetRule ?? null)}`)
|
||||
}
|
||||
if (!inlineResetRule.declarations.some(node => node.prop === 'box-sizing' && node.value === 'border-box')) {
|
||||
throw new Error('REA global universal reset no longer declares box-sizing: border-box')
|
||||
}
|
||||
|
||||
const optionClassOffset = runtimeSource.indexOf('cH.optionItem')
|
||||
if (optionClassOffset !== optionRuntimeClassOffset) {
|
||||
throw new Error(`QuestionFloating option JSX offset changed: expected ${optionRuntimeClassOffset}, found ${optionClassOffset}`)
|
||||
}
|
||||
const sourceOptionStart = runtimeSource.lastIndexOf('(0,bH.jsxs)(`div`,{', optionClassOffset)
|
||||
const sourceOptionSnippet = runtimeSource.slice(sourceOptionStart, optionClassOffset + 700)
|
||||
if (sourceOptionStart < 0 || !sourceOptionSnippet.includes('role:`button`') || !sourceOptionSnippet.includes('tabIndex:O?-1:0') || !sourceOptionSnippet.includes('"aria-disabled":O||void 0') || !sourceOptionSnippet.includes('onKeyDown:')) {
|
||||
throw new Error('REA QuestionFloating option no longer uses the verified div/button keyboard structure')
|
||||
}
|
||||
|
||||
const output = postcss.root()
|
||||
const optionUniversalResetRules = globalResetRules.filter(record => record.originalSelectors.includes('*'))
|
||||
for (const reset of optionUniversalResetRules) {
|
||||
output.append(sourceComment({
|
||||
file: reset.file,
|
||||
hash: reset.sha256,
|
||||
offset: reset.offset,
|
||||
selectors: reset.originalSelectors,
|
||||
evidenceId: cssEvidenceId,
|
||||
note: 'global universal reset projection=.chat-pane.cr-theme .question-floating__option'
|
||||
}))
|
||||
output.append(postcss.rule({
|
||||
selector: '.chat-pane.cr-theme .question-floating__option',
|
||||
nodes: reset.declarations.map(node => node.clone())
|
||||
}))
|
||||
}
|
||||
const moduleRules = []
|
||||
for (const node of componentCss.root.nodes) {
|
||||
output.append(...projectModuleNode(node, componentCss, classNames, moduleRules))
|
||||
}
|
||||
|
||||
const sourceModuleClasses = Object.entries(classNames)
|
||||
.filter(([key]) => classAliases[key])
|
||||
.map(([, className]) => className)
|
||||
const missingRules = sourceModuleClasses.filter(className => !moduleRules.some(record => record.originalSelectors.some(selector => classPattern(className).test(selector))))
|
||||
if (missingRules.length) throw new Error(`REA CSS is missing QuestionFloating module rules for: ${missingRules.join(', ')}`)
|
||||
|
||||
const animations = []
|
||||
output.walkDecls(/^animation(?:-name)?$/, declaration => {
|
||||
animations.push(...declaration.value.split(/[\s,]+/))
|
||||
})
|
||||
let projectedAnimationRuleCount = 0
|
||||
componentCss.root.walkAtRules(/keyframes$/, atRule => {
|
||||
if (!animations.includes(atRule.params)) return
|
||||
const offset = atRule.source?.start?.offset ?? 0
|
||||
output.append(sourceComment({
|
||||
file: componentCss.file,
|
||||
hash: componentCss.sha256,
|
||||
offset,
|
||||
selectors: [`@${atRule.name} ${atRule.params}`],
|
||||
evidenceId: cssEvidenceId,
|
||||
note: `runtime stylesheet ASAR comparison=${componentCss.asarComparison}`
|
||||
}))
|
||||
output.append(atRule.clone())
|
||||
projectedAnimationRuleCount += 1
|
||||
})
|
||||
|
||||
const themeRules = []
|
||||
projectThemeTokens(componentCss.root, componentCss, output, themeRules)
|
||||
const bridgeRules = []
|
||||
projectBridgeTokens(bridgeCss.root, bridgeCss, output, bridgeRules)
|
||||
|
||||
const header = postcss.comment({
|
||||
text: ` Generated by scripts/rea/recover-question-styles.mjs. Runtime component evidence=${runtimeEvidenceId}; source=${runtimeFile}; sha256=${runtimeSha256}; QuestionFloating offset=${componentOffset}; CSS module map offset=${moduleMapOffset}; footer offset=${footerOffset}; module=${moduleId}. CSS evidence=${cssEvidenceId}; ${componentCss.file} sha256=${componentCss.sha256}; ASAR comparison=${componentCss.asarComparison}; ${bridgeCss.file} sha256=${bridgeCss.sha256}; global reset=${inlineResetFile}@${inlineResetRule.offset}; global button text-align matches=${globalButtonTextAlignRules.length}. Selectors are scoped to .chat-pane.cr-theme and translated from the runtime CSS module map. `
|
||||
})
|
||||
output.prepend(header)
|
||||
formatContainer(output, -1)
|
||||
|
||||
await writeFile(outputPath, output.toString() + '\n')
|
||||
const localComponent = await readFile(resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionFloating.tsx'), 'utf8')
|
||||
const localOptionClassOffset = localComponent.indexOf("'question-floating__option'")
|
||||
const localOptionStart = localComponent.lastIndexOf('<div', localOptionClassOffset)
|
||||
const localOptionSnippet = localComponent.slice(localOptionStart, localOptionClassOffset + 900)
|
||||
if (localOptionStart < 0 || !localOptionSnippet.includes('role="button"') || !localOptionSnippet.includes('tabIndex=') || !localOptionSnippet.includes('aria-disabled=') || !localOptionSnippet.includes('onKeyDown=')) {
|
||||
throw new Error('Local QuestionFloating option must preserve the recovered div/button keyboard structure')
|
||||
}
|
||||
const localClasses = [...new Set([...localComponent.matchAll(/['"](question-floating(?:__[\w-]+|--[\w-]+)?)['"]/g)].map(match => match[1]))]
|
||||
const mappedClasses = new Set(Object.values(classAliases).filter(Boolean))
|
||||
const unmappedLocalClasses = localClasses.filter(className => !mappedClasses.has(className))
|
||||
const projectedRuleCount = moduleRules.length + themeRules.length + bridgeRules.length + projectedAnimationRuleCount + optionUniversalResetRules.length
|
||||
console.log(JSON.stringify({
|
||||
runtimeEvidenceId,
|
||||
runtimeFile,
|
||||
runtimeSha256,
|
||||
componentOffset,
|
||||
moduleMapOffset,
|
||||
footerOffset,
|
||||
cssEvidenceId,
|
||||
moduleId,
|
||||
projectedRuleCount,
|
||||
projectedModuleRuleCount: moduleRules.length,
|
||||
projectedThemeTokenRuleCount: themeRules.length,
|
||||
projectedBridgeTokenRuleCount: bridgeRules.length,
|
||||
projectedAnimationRuleCount,
|
||||
projectedUniversalResetRuleCount: optionUniversalResetRules.length,
|
||||
globalButtonTextAlignRules,
|
||||
globalResetSource: {
|
||||
evidenceId: cssEvidenceId,
|
||||
file: inlineResetFile,
|
||||
sha256: inlineResetSha256,
|
||||
originalSelectors: inlineResetRule.originalSelectors,
|
||||
offset: inlineResetRule.offset,
|
||||
declarations: inlineResetRule.declarations.map(node => ({ property: node.prop, value: node.value }))
|
||||
},
|
||||
optionSource: {
|
||||
runtimeEvidenceId,
|
||||
file: runtimeFile,
|
||||
sha256: runtimeSha256,
|
||||
jsxClassOffset: optionClassOffset,
|
||||
element: 'div',
|
||||
role: 'button',
|
||||
cssEvidenceId,
|
||||
cssFile: componentCss.file,
|
||||
cssSha256: componentCss.sha256,
|
||||
cssOriginalSelectors: optionCssRules[0].originalSelectors,
|
||||
cssOffset: optionCssRules[0].offset,
|
||||
cssTextAlignDeclarations: optionCssRules[0].textAlign,
|
||||
localElement: 'div',
|
||||
localRole: 'button',
|
||||
localClassOffset: localOptionClassOffset
|
||||
},
|
||||
globalCssFilesScanned: cssManifest.files?.length ?? 0,
|
||||
sources: sourceSpecs.map(spec => ({
|
||||
file: spec.file,
|
||||
sha256: spec.sha256,
|
||||
asarComparison: cssManifest.files.find(record => record.file === spec.file)?.asarComparison?.status ?? 'not-compared'
|
||||
})),
|
||||
unmappedLocalClasses,
|
||||
outputPath
|
||||
}, null, 2))
|
||||
@@ -0,0 +1,40 @@
|
||||
// Recover inert geometric SVG attributes already retained by REA; no target access.
|
||||
import fs from 'node:fs/promises'
|
||||
import crypto from 'node:crypto'
|
||||
const reference = JSON.parse(await fs.readFile('.tmp/rea-visual-reference/layout.json', 'utf8'))
|
||||
const evidence = JSON.parse(await fs.readFile(reference.archive, 'utf8'))
|
||||
if (evidence.evidence_id !== reference.evidenceId || JSON.stringify(evidence.normalized_result.visuals.layout) !== JSON.stringify(reference.layout)) throw new Error('Visual export does not match archived REA Evidence')
|
||||
const document = reference.layout.documents[0]
|
||||
const nodes = new Map([...document.nodes, ...document.svg_nodes].map(n => [n.node_index, n]))
|
||||
function ancestor(node, matcher) {
|
||||
for (let p = node.parent_index; nodes.has(p); p = nodes.get(p).parent_index) if (matcher(nodes.get(p).attributes?.class ?? '')) return true
|
||||
return false
|
||||
}
|
||||
const svg = document.svg_nodes.filter(n => n.node_name.toLowerCase() === 'svg')
|
||||
const specs = [
|
||||
['RuntimeSendIcon', svg.filter(n => ancestor(n, c => c.split(' ').includes('cr-send-button__icon')))],
|
||||
['RuntimeMicrophoneIcon', svg.filter(n => ancestor(n, c => c.split(' ').includes('cr-voice-trigger')))],
|
||||
['RuntimePermissionIcon', svg.filter(n => ancestor(n, c => c.split(' ').includes('cr-permission-setting')) && ancestor(n, c => c.split(' ').includes('cr-input-footer-item__icon')))],
|
||||
['RuntimeRegenerateIcon', svg.filter(n => ancestor(n, c => c.includes('_actionButton_')) && n.attributes.viewBox === '0 0 14 14')],
|
||||
]
|
||||
function subtree(node) {
|
||||
return { ...node, children: document.svg_nodes.filter(child => child.parent_index === node.node_index).map(subtree) }
|
||||
}
|
||||
function jsx(node, root = false) {
|
||||
const tag = node.node_name.toLowerCase() === 'clippath' ? 'clipPath' : node.node_name
|
||||
const attrs = Object.entries(node.attributes).filter(([key]) => key !== 'class').map(([key, value]) => `${key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}={${JSON.stringify(value)}}`).join(' ')
|
||||
return `<${tag} ${attrs}${root ? ' aria-hidden="true" focusable="false" {...props}' : ''}>${node.children.map(child => jsx(child)).join('')}</${tag}>`
|
||||
}
|
||||
let output = `// Generated from REA visual Evidence ${reference.evidenceId}; SVG geometry only.\nimport type { SVGProps } from 'react'\n`
|
||||
const records = []
|
||||
for (const [name, matches] of specs) {
|
||||
if (matches.length !== 1) throw new Error(`${name}: expected one captured SVG, found ${matches.length}`)
|
||||
const tree = subtree(matches[0])
|
||||
const hash = crypto.createHash('sha256').update(JSON.stringify(tree)).digest('hex')
|
||||
output += `\n// Captured node ${tree.node_index}; geometry SHA-256 ${hash}.\nexport function ${name}(props: SVGProps<SVGSVGElement>) { return (${jsx(tree, true)}) }\n`
|
||||
records.push({ name, nodeIndex: tree.node_index, sha256: hash })
|
||||
}
|
||||
const destination = 'src/renderer/src/workbuddy/message-actions/RuntimeIcons.tsx'
|
||||
await fs.writeFile(destination, output)
|
||||
await fs.writeFile('.tmp/rea-visual-reference/icon-projection.json', JSON.stringify({ evidenceId: reference.evidenceId, destination, records }, null, 2) + '\n')
|
||||
console.log(JSON.stringify({ evidenceId: reference.evidenceId, destination, icons: records.length }))
|
||||
@@ -0,0 +1,148 @@
|
||||
// Recover only pure UI definitions from pinned REA exports. Never initialize an original module.
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import postcss from 'postcss'
|
||||
const out = 'src/renderer/src/components/source-select'
|
||||
await fs.mkdir(`${out}/provider-assets`, { recursive: true })
|
||||
const evidence = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const panel = JSON.parse(await fs.readFile('.tmp/rea-assessment-model-source.json', 'utf8'))
|
||||
const cache = new Map()
|
||||
function source(file) {
|
||||
if (cache.has(file)) return cache.get(file)
|
||||
const artifact = evidence.items.find(item => item.file_path.endsWith(file))?.source?.artifact
|
||||
if (!artifact?.text || createHash('sha256').update(artifact.text).digest('hex') !== artifact.sha256) throw Error(`Invalid REA artifact ${file}`)
|
||||
const ast = parse(artifact.text, { sourceType: 'module' }), definitions = new Map(), exports = new Map(), imports = new Map()
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return
|
||||
if (node.type === 'FunctionDeclaration') definitions.set(node.id.name, node)
|
||||
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') definitions.set(node.left.name, node.right)
|
||||
if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && node.init) definitions.set(node.id.name, node.init)
|
||||
if (node.type === 'ExportNamedDeclaration') for (const entry of node.specifiers) exports.set(entry.exported.name, entry.local.name)
|
||||
if (node.type === 'ImportDeclaration') for (const entry of node.specifiers) if (entry.imported) imports.set(entry.local.name, { file: node.source.value.replace(/^\.\//, ''), exported: entry.imported.name })
|
||||
for (const value of Object.values(node)) if (Array.isArray(value)) value.forEach(visit); else if (value && typeof value === 'object') visit(value)
|
||||
}
|
||||
visit(ast)
|
||||
const result = { artifact, definitions, exports, imports }; cache.set(file, result); return result
|
||||
}
|
||||
const shared = source('src-DPDj7_qD.js'), contexts = source('contexts-BMeWSUHk.js')
|
||||
if (shared.artifact.sha256 !== '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0' || contexts.artifact.sha256 !== '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b') throw Error('Pinned source changed')
|
||||
const reactNames = 'jsx|jsxs|forwardRef|useRef|useState|useMemo|useEffect|useLayoutEffect|useCallback|useId|useImperativeHandle|useContext|cloneElement|isValidElement'
|
||||
const convert = text => text.replace(new RegExp(`\\(0,[A-Za-z0-9_$]+\\.(${reactNames})\\)`, 'g'), ' $1')
|
||||
.replace(new RegExp(`\\b[A-Za-z0-9_$]+\\.(${reactNames})\\b`, 'g'), '$1').replace(/\b[A-Za-z0-9_$]+\.Fragment\b/g, 'React.Fragment')
|
||||
const manifest = { sourceVersion: '5.8.2', rootSha256: panel.normalized_result.root_sha256, scriptEvidenceId: evidence.evidence_id, panelEvidenceId: panel.evidence_id, definitions: [], icons: [], assets: [], css: [] }
|
||||
const floating = source('floating-ui.react-dom-BDD8Fyfd.js')
|
||||
if (floating.artifact.sha256 !== '94036e3ed41ddb907b010cedbdb628a57e26e0585e2a9be45c9cbecfdd6ec978') throw Error('Floating source changed')
|
||||
for (const name of ['getClippingElementAncestors', 'isContainingBlock', 'hasFixedPositionAncestor']) {
|
||||
const node = floating.definitions.get(name)
|
||||
manifest.definitions.push({file:'floating-ui.react-dom-BDD8Fyfd.js',sha256:floating.artifact.sha256,name,offset:node.start,length:node.end-node.start,hostAdapter:'SourceFloatingPlatform.ts'})
|
||||
}
|
||||
const core = []
|
||||
for (const name of ['isOptionGroup', 'flattenOptions', 'defaultFilter', 'OptionLabel', 'OptionStringLabel', 'Select', 'pL', 'useControllableInputValue', '_L', 'CheckboxTick', 'bB']) {
|
||||
const node = shared.definitions.get(name)
|
||||
if (!node) throw Error(`Missing UI definition ${name}`)
|
||||
let text = convert(shared.artifact.text.slice(node.start, node.end))
|
||||
if (name === 'Select') text = text.replace('type:`button`,className:ae', 'type:`button`,"aria-label":r.ariaLabel,className:ae')
|
||||
core.push(node.type === 'FunctionDeclaration' ? text : `const ${name} = ${text}`)
|
||||
manifest.definitions.push({ file: 'src-DPDj7_qD.js', sha256: shared.artifact.sha256, name, offset: node.start, length: node.end - node.start })
|
||||
}
|
||||
await fs.writeFile(`${out}/RecoveredSelectCore.tsx`, `// @ts-nocheck\n// Pure Select/Input/Checkbox definitions recovered from REA ${evidence.evidence_id}.\nimport * as React from 'react'\nimport {forwardRef,useRef,useState,useMemo,useEffect,useLayoutEffect,useCallback,useId,useImperativeHandle,useContext,createContext} from 'react'\nimport {jsx,jsxs} from 'react/jsx-runtime'\nimport { SourceSelectPopover as Popover, SourceSelectTooltip as Tooltip } from './SourceSelectPopover'\nimport {SelectSearchIcon as L_,SelectClearIcon as GP,SelectCaretIcon as Ys,SelectCheckIcon as Fs,SelectLoadingIcon as Hf} from './SourceSelectIcons'\nconst getA11yLabel=(key)=>({clear:'清除',search:'搜索',remove:'移除'}[key]??key)\nconst gB=createContext(null)\n${core.join('\n')}\nconst ML=_L,EB=bB\nexport const RecoveredSelect=Select\n`)
|
||||
const tooltip = []
|
||||
for (const name of ['isPressTransparent', 'isHoverSuppressedByPress', 'isElementTruncated', 'isSubtreeTruncated', 'normalizeVisibleText', 'collectVisibleText', 'hasVisibleTriggerText', 'hasTruncationStyle', 'shouldGateOnTruncation', 'snapToDevicePixel', 'Tooltip']) {
|
||||
const node = shared.definitions.get(name)
|
||||
let text = convert(shared.artifact.text.slice(node.start,node.end)).replaceAll('s.ref','s.props.ref')
|
||||
tooltip.push(node.type === 'FunctionDeclaration' ? text : `const ${name}=${text}`)
|
||||
manifest.definitions.push({file:'src-DPDj7_qD.js',sha256:shared.artifact.sha256,name,offset:node.start,length:node.end-node.start})
|
||||
}
|
||||
await fs.writeFile(`${out}/RecoveredSelectTooltip.tsx`, `// @ts-nocheck\n// Literal source Tooltip and truncation helpers; React 19 ref access and scoped portal are host adapters.\nimport * as React from 'react'\nimport {useState,useRef,useEffect,useMemo,cloneElement,isValidElement} from 'react'\nimport {jsx,jsxs} from 'react/jsx-runtime'\nimport {useFloating,offset as v,flip as _,shift as x,size as g,arrow as S,hide as y,autoUpdate as C,useMergeRefs as ee,useHover as q,safePolygon as te,useFocus as ne,useDismiss as ae,useRole as de,useInteractions as le,useTransitionStatus as G,FloatingPortal} from '@floating-ui/react'\nimport {sourceFloatingPlatform} from './SourceFloatingPlatform'\nconst oe=({layer,...options})=>useFloating({...options,platform:sourceFloatingPlatform})\nconst ce=({children})=>jsx(FloatingPortal,{children:jsx('span',{className:'source-select-host',children})})\nconst uz=.1,dz=new Set(['SVG','IMG','VIDEO','CANVAS','AUDIO']),hz=3,gz=8,_z=12,vz=240,yz=150,bz=100,xz=8,Sz=72,Cz=8,wz=new Map,Tz=new Map,wR=4,TR=1000,ER='data-pointer-press-transparent';let DR=null\n${tooltip.join('\n')}\n// Shipped tooltip press suppression, without executing the original bundle initializer.\nif(typeof document!=='undefined'){document.addEventListener('pointerdown',r=>{DR=isPressTransparent(r.target)?null:{x:r.clientX,y:r.clientY,at:Date.now()}},true);document.addEventListener('pointermove',r=>{if(DR&&(Math.abs(r.clientX-DR.x)>wR||Math.abs(r.clientY-DR.y)>wR))DR=null},true)}\nexport const RecoveredSelectTooltip=Tooltip\n`)
|
||||
function resolve(file, name) {
|
||||
const entry = source(file); let node = entry.definitions.get(name)
|
||||
if (!node && entry.imports.has(name)) { const imported = entry.imports.get(name), target = source(imported.file); return resolve(imported.file, target.exports.get(imported.exported)) }
|
||||
if (!node) throw Error(`Missing ${file}:${name}`)
|
||||
for (let count = 0; count < 8; count++) {
|
||||
if (node.type === 'Identifier') return resolve(file, node.name)
|
||||
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'createIcon') {
|
||||
if (node.arguments[0].type === 'ObjectExpression') return { ...entry, node: node.arguments[0], file, asset: true }
|
||||
if (node.arguments[0].type !== 'Identifier') throw Error(`Unknown icon ${file}:${name}`)
|
||||
return resolve(file, node.arguments[0].name)
|
||||
}
|
||||
break
|
||||
}
|
||||
return { ...entry, node, file }
|
||||
}
|
||||
const icons = []
|
||||
function icon(file, binding, exported) {
|
||||
const item = resolve(file, binding), text = convert(item.artifact.text.slice(item.node.start, item.node.end))
|
||||
if (!/\b(?:jsx|jsxs)\(`svg`/.test(text)) throw Error(`Not a pure SVG ${file}:${binding}`)
|
||||
icons.push(`export const ${exported} = ${item.node.type === 'FunctionDeclaration' ? text.replace(/^function [^(]+/, 'function') : text}`)
|
||||
manifest.icons.push({ file, sha256: item.artifact.sha256, binding, exported, offset: item.node.start })
|
||||
}
|
||||
for (const [binding, exported] of [['L_', 'SelectSearchGlyph'], ['GP', 'SelectClearGlyph'], ['Ys', 'SelectCaretGlyph'], ['Fs', 'SelectCheckGlyph'], ['Hf', 'SelectLoadingGlyph']]) icon('src-DPDj7_qD.js', binding, exported)
|
||||
await fs.writeFile(`${out}/SourceSelectIcons.tsx`, `// @ts-nocheck\n// Exact SVG paths/transforms from pinned REA ${evidence.evidence_id}.\nimport {jsx,jsxs} from 'react/jsx-runtime'\nimport {forwardRef,useId} from 'react'\n${icons.join('\n')}\nconst wrap=(Glyph)=>(props={})=>{const {size=16,spin,className='',...rest}=props;return jsx(Glyph,{width:size,height:size,...rest,className:['wb-icon',spin?'wb-icon--spin':'',className].filter(Boolean).join(' ')})}\nexport const SelectSearchIcon=wrap(SelectSearchGlyph),SelectClearIcon=wrap(SelectClearGlyph),SelectCaretIcon=wrap(SelectCaretGlyph),SelectCheckIcon=wrap(SelectCheckGlyph),SelectLoadingIcon=wrap(SelectLoadingGlyph)\n`)
|
||||
// Provider UI imports are resolved from the source panel's icon map, not invented vendor marks.
|
||||
const vendors = { glm: ['contexts-BMeWSUHk.js', 'tE'], kimi: ['src-DPDj7_qD.js', 'tc'], minimax: ['contexts-BMeWSUHk.js', 'iE'],
|
||||
deepseek: ['contexts-BMeWSUHk.js', 'QT'], openai: ['contexts-BMeWSUHk.js', 'oE'], gemini: ['contexts-BMeWSUHk.js', 'nE'],
|
||||
mistral: ['contexts-BMeWSUHk.js', 'aE'], groq: ['contexts-BMeWSUHk.js', 'rE'], 'together-ai': ['contexts-BMeWSUHk.js', 'cE'],
|
||||
'fireworks-ai': ['contexts-BMeWSUHk.js', 'eE'], perplexity: ['contexts-BMeWSUHk.js', 'sE'], custom: ['src-DPDj7_qD.js', 'ua'] }
|
||||
const vendorDefinitions = []
|
||||
for (const [vendor, [file, exportedName]] of Object.entries(vendors)) {
|
||||
const entry = source(file), binding = entry.exports.get(exportedName)
|
||||
let item = resolve(file, binding)
|
||||
// Contexts reexports import bindings; follow only the original static import edge.
|
||||
if (!item.node) throw Error(`No icon ${vendor}`)
|
||||
let text = convert(item.artifact.text.slice(item.node.start, item.node.end))
|
||||
if (item.asset) {
|
||||
const resource = JSON.parse(await fs.readFile('.tmp/rea-model-select-icons.json', 'utf8'))
|
||||
if (resource.normalized_result.root_sha256 !== panel.normalized_result.root_sha256 || vendor !== 'kimi') throw Error('Unknown asset icon')
|
||||
const file = resource.normalized_result.files.find(file => file.path === 'renderer/assets/kimi-DiLyRSiJ.svg')
|
||||
const bytes = Buffer.from(file.data_base64, 'base64')
|
||||
if (createHash('sha256').update(bytes).digest('hex') !== file.sha256) throw Error('Icon bytes changed')
|
||||
await fs.writeFile(`${out}/provider-assets/kimi.svg`, bytes)
|
||||
vendorDefinitions.push(`import ProviderKimiAsset from './provider-assets/kimi.svg'\nconst ProviderKimiIcon=(props)=>jsx('img',{...props,src:ProviderKimiAsset,alt:'Kimi',draggable:false})`)
|
||||
manifest.assets.push({ vendor, evidenceId: resource.evidence_id, path: file.path, bytes: bytes.length, sha256: file.sha256 })
|
||||
continue
|
||||
}
|
||||
if (!/\b(?:jsx|jsxs)\(`svg`/.test(text)) throw Error(`Provider ${vendor} needs explicit dependency recovery`)
|
||||
const name = 'Provider' + vendor.replace(/(^|-)([a-z])/g, (_all, _dash, char) => char.toUpperCase()) + 'Icon'
|
||||
vendorDefinitions.push(`const ${name} = ${item.node.type === 'FunctionDeclaration' ? text.replace(/^function [^(]+/, 'function') : text}`)
|
||||
manifest.icons.push({ file: item.file, sha256: item.artifact.sha256, binding, exported: name, vendor, offset: item.node.start })
|
||||
}
|
||||
for (const [vendor, binding] of [['tencent-cloud', 'Kkt'], ['openrouter', 'Wkt'], ['ollama', 'Hkt']]) {
|
||||
const node = contexts.definitions.get(binding), value = node?.type === 'TemplateLiteral' && !node.expressions.length ? node.quasis[0].value.cooked : ''
|
||||
if (!value.startsWith('data:image/png;base64,')) throw Error(`Provider PNG not inline: ${vendor}`)
|
||||
const bytes = Buffer.from(value.slice(value.indexOf(',') + 1), 'base64')
|
||||
await fs.writeFile(`${out}/provider-assets/${vendor}.png`, bytes)
|
||||
manifest.assets.push({ vendor, binding, offset: node.start, bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') })
|
||||
}
|
||||
await fs.writeFile(`${out}/SourceProviderGlyphs.tsx`, `// @ts-nocheck\n// Pure source provider SVGs from REA ${evidence.evidence_id}.\nimport {jsx,jsxs} from 'react/jsx-runtime'\nimport {forwardRef,useId} from 'react'\n${vendorDefinitions.join('\n')}\nexport const sourceProviderGlyphs = {${Object.keys(vendors).map(vendor => `${JSON.stringify(vendor)}:Provider${vendor.replace(/(^|-)([a-z])/g, (_all, _dash, char) => char.toUpperCase())}Icon`).join(',')}}\n`)
|
||||
const cssFile = '.tmp/rea-css-recovered/src-Bh3YZ35H.css', cssText = await fs.readFile(cssFile, 'utf8')
|
||||
if (createHash('sha256').update(cssText).digest('hex') !== '88dbdc1cd3f59d0e8b75824512a465a2be714bc7af481d790fe3a9484f85e01a') throw Error('Shared CSS changed')
|
||||
const selected = postcss.root()
|
||||
function select(node) {
|
||||
if (node.type === 'rule') {
|
||||
if (!/\.wb-(?:select|popover|input|checkbox|tooltip)|\.wb-icon/.test(node.selector)) return null
|
||||
const copy = node.clone(); copy.selectors = copy.selectors.map(selector => '.source-select-host ' + selector); return copy
|
||||
}
|
||||
if (node.type !== 'atrule') return null
|
||||
if (/keyframes/.test(node.name)) return /spin|popover|tooltip/.test(node.params) ? node.clone() : null
|
||||
const copy = node.clone({ nodes: [] }); for (const child of node.nodes ?? []) { const match = select(child); if (match) copy.append(match) }
|
||||
return copy.nodes.length ? copy : null
|
||||
}
|
||||
for (const node of postcss.parse(cssText).nodes) { const match = select(node); if (match) selected.append(match) }
|
||||
const modelCss = panel.normalized_result.files.find(file => file.path.endsWith('ModelsSettingsPanel-FfiX-Du7.css')).text
|
||||
postcss.parse(modelCss).walkRules(rule => { if (/models-settings-panel__provider-(?:option|icon-image)/.test(rule.selector)) { const copy = rule.clone(); copy.selectors = copy.selectors.map(selector => '.source-select-host ' + selector); selected.append(copy) } })
|
||||
const needed = new Set(), collect = text => { for (const match of text.matchAll(/var\((--[\w-]+)/g)) needed.add(match[1]) }
|
||||
collect(selected.toString())
|
||||
collect(tooltip.join('\n'))
|
||||
const tokenText = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8'), tokenRules = []
|
||||
manifest.css = [{file:'src-Bh3YZ35H.css',sha256:createHash('sha256').update(cssText).digest('hex')},{file:'tokens-Df-i-zsO.css',sha256:createHash('sha256').update(tokenText).digest('hex')},{file:'ModelsSettingsPanel-FfiX-Du7.css',sha256:createHash('sha256').update(modelCss).digest('hex')}]
|
||||
if (manifest.css[1].sha256 !== '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696') throw Error('Source tokens changed')
|
||||
for (const text of [tokenText, cssText]) postcss.parse(text).walkRules(rule => tokenRules.push(rule))
|
||||
for (let pass = 0; pass < 12; pass++) for (const rule of tokenRules) rule.walkDecls(d => { if (needed.has(d.prop)) collect(d.value) })
|
||||
const light = new Map(), dark = new Map()
|
||||
for (const rule of tokenRules) { const map = /dark/.test(rule.selector) ? dark : rule.selector.includes(':root') ? light : undefined; if (map) rule.walkDecls(d => { if (needed.has(d.prop)) map.set(d.prop, d.value) }) }
|
||||
const serialize = map => [...map].map(([key, value]) => key + ':' + value).join(';')
|
||||
await fs.writeFile(`${out}/source-select-recovered.css`, `/* Exact selected rule bodies/order/conditions from REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94. */\n.source-select-host{${serialize(light)}}\n:root[data-theme=dark] .source-select-host,[data-cr-theme=dark] .source-select-host{${serialize(dark)}}\n${selected.toString()}\n`)
|
||||
await fs.mkdir('.tmp/source-select-recovery', { recursive: true })
|
||||
await fs.writeFile('.tmp/source-select-recovery/manifest.json', JSON.stringify(manifest, null, 2))
|
||||
console.log(JSON.stringify({ definitions: manifest.definitions.length, icons: manifest.icons.length, assets: manifest.assets.length }))
|
||||
@@ -0,0 +1,38 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverseModule from '@babel/traverse'
|
||||
import postcss from 'postcss'
|
||||
const traverse=traverseModule.default??traverseModule, evidence=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8')),cache=new Map()
|
||||
function source(base){return evidence.items.find((item)=>item.file_path.endsWith(base))?.source.artifact.text}
|
||||
function ast(base){if(!cache.has(base))cache.set(base,parse(source(base),{sourceType:'module'}));return cache.get(base)}
|
||||
const context='contexts-BMeWSUHk.js',staticValue=(node)=>!node?undefined:node.type==='TemplateLiteral'&&!node.expressions.length?node.quasis[0].value.cooked:node.type==='StringLiteral'||node.type==='NumericLiteral'?node.value:undefined
|
||||
function object(node){return Object.fromEntries(node.properties.filter((prop)=>prop.type==='ObjectProperty').map((prop)=>[prop.key.name??prop.key.value,staticValue(prop.value)]))}
|
||||
let tabs,groups
|
||||
traverse(ast(context),{AssignmentExpression(p){if(p.node.left.name==='DC')tabs=p.node.right.elements.map(object);if(p.node.left.name==='cXe')groups=p.node.right.elements.map(object)}})
|
||||
const labels={};traverse(ast('zh-cn-Dfzn8myY.js'),{ObjectProperty(p){const key=p.node.key.value;if(typeof key==='string'&&/^(settings\.nav\.|settings\.search\.)/.test(key)){const value=staticValue(p.node.value);if(typeof value==='string')labels[key]=value}}})
|
||||
const names={settings:'Hee',account:'ute',subscriptionUsage:'Ute',appearance:'qre',personalization:'bne',memory:'Na',extensions:'Une',models:'ModelNavIcon',claw:'ClawSettingsIcon',laboratory:'wre',deviceConnections:'MobileConnectIcon',dataManagement:'Kte',keyboardShortcuts:'Qne',buddyApps:'_ne',securityCenter:'Vte',systemPermissions:'$a',softwareConfig:'ro',helpFeedback:'co',getHelp:'One'}
|
||||
function resolve(base,name,depth=0){
|
||||
if(depth>8)return null
|
||||
let imported,decl
|
||||
traverse(ast(base),{ImportDeclaration(p){for(const spec of p.node.specifiers)if(spec.local.name===name&&spec.type==='ImportSpecifier')imported={base:p.node.source.value.replace('./',''),member:spec.imported.name}},FunctionDeclaration(p){if(p.node.id?.name===name)decl=p},AssignmentExpression(p){if(p.node.left.name===name)decl=p},VariableDeclarator(p){if(p.node.id?.name===name&&p.node.init)decl=p}})
|
||||
if(imported&&source(imported.base)){let local;traverse(ast(imported.base),{ExportNamedDeclaration(p){for(const spec of p.node.specifiers)if(spec.exported.name===imported.member)local=spec.local.name}});if(local)return resolve(imported.base,local,depth+1)}
|
||||
if(!decl)return null
|
||||
const right=decl.node.right??decl.node.init
|
||||
if(right?.type==='CallExpression'&&typeof staticValue(right.arguments[0])==='string'&&right.arguments[1]?.type==='Identifier') {
|
||||
let nodes;traverse(ast(base),{AssignmentExpression(p){if(p.node.left.name===right.arguments[1].name&&p.node.right.type==='ArrayExpression')nodes=p.node.right.elements}})
|
||||
if(nodes){let defaults={};traverse(ast('createLucideIcon-1goKJVIH.js'),{AssignmentExpression(p){if(p.node.left.name==='i'&&p.node.right.type==='ObjectExpression')defaults=object(p.node.right)}});return{props:defaults,children:nodes.map((entry)=>({tag:staticValue(entry.elements[0]),props:object(entry.elements[1])}))}}
|
||||
}
|
||||
if(right?.type==='CallExpression'&&right.arguments[0]?.type==='Identifier')return resolve(base,right.arguments[0].name,depth+1)
|
||||
let svg=null,paths=[],next
|
||||
decl.traverse({CallExpression(p){const tag=p.node.arguments[0];if(tag?.type==='Identifier'){next=tag.name;return}const kind=staticValue(tag);if(!['svg','path','circle','rect','line','polyline','polygon','ellipse'].includes(kind))return;const props={};for(const prop of p.node.arguments[1]?.properties??[]){if(prop.type!=='ObjectProperty')continue;const value=staticValue(prop.value);if(value!==undefined)props[prop.key.name??prop.key.value]=value}if(kind==='svg')svg=props;else paths.push({tag:kind,props})}})
|
||||
if(svg&&paths.length)return{props:svg,children:paths}
|
||||
return next?resolve(base,next,depth+1):null
|
||||
}
|
||||
const icons={};for(const[id,name]of Object.entries(names)){const svg=resolve(context,name);if(svg)icons[id]=svg}
|
||||
fs.writeFileSync('src/renderer/src/components/source-settings-navigation-reference.ts','// REA '+evidence.evidence_id+'; SettingsNavigation/DC/cXe and source SVG geometry only.\nexport const sourceSettingsTabs = '+JSON.stringify(tabs,null,2)+'\nexport const sourceSettingsGroups = '+JSON.stringify(groups,null,2)+'\nexport const sourceSettingsIcons: Record<string,{props:Record<string,string|number>;children:{tag:string;props:Record<string,string|number>}[]}> = '+JSON.stringify(icons,null,2)+'\nconst labels:Record<string,string> = '+JSON.stringify(labels,null,2)+'\nexport const sourceSettingsLabel=(key:string):string=>labels[key]??key\n')
|
||||
const root=postcss.parse(fs.readFileSync('.tmp/rea-css-recovered/contexts-BkjwYSZT.css','utf8')),css=[]
|
||||
root.walkRules((rule)=>{if(!/\.settings-(?:navigation|search)/.test(rule.selector))return;let copy=rule.clone();copy.selectors=copy.selectors.map((s)=>'.source-settings-navigation '+s);let tree=copy;for(let parent=rule.parent;parent?.type==='atrule';parent=parent.parent){const wrap=parent.clone({nodes:[]});wrap.append(tree);tree=wrap}css.push(tree.toString())})
|
||||
const variables=new Set();const collect=(text)=>{for(const match of text.matchAll(/var\((--[\w-]+)/g))variables.add(match[1])};collect(css.join(''));const tokenRules=[];postcss.parse(fs.readFileSync('.tmp/rea-css-recovered/tokens-Df-i-zsO.css','utf8')).walkRules((rule)=>tokenRules.push(rule));for(let pass=0;pass<10;pass++)for(const rule of tokenRules)rule.walkDecls((decl)=>{if(variables.has(decl.prop))collect(decl.value)});const light=new Map(),dark=new Map();for(const rule of tokenRules){const target=rule.selector.includes('dark')?dark:rule.selector.includes(':root')?light:null;if(target)rule.walkDecls((decl)=>{if(variables.has(decl.prop))target.set(decl.prop,decl.value)})}const serialize=(map)=>[...map].map(([key,value])=>key+':'+value).join(';');css.unshift('.source-settings-navigation{'+serialize(light)+'}',':root[data-theme=dark] .source-settings-navigation{'+serialize(dark)+'}');
|
||||
fs.writeFileSync('src/renderer/src/components/source-settings-navigation-recovered.css','/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; media conditions retained. */\n'+css.join('\n')+'\n')
|
||||
console.log(JSON.stringify({tabs:tabs.length,groups:groups.length,icons:Object.keys(icons),unknownIcons:Object.keys(names).filter((key)=>!icons[key]),rules:css.length}))
|
||||
@@ -0,0 +1,27 @@
|
||||
// Recover only already-retained REA CSS; never access the installed application here.
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
const sha = value => createHash('sha256').update(value).digest('hex')
|
||||
const evidenceId = 'ev_40da6df0424193923bae64a8ccb6fca97ea67813d7de671986d4d7e154b848c1'
|
||||
const evidence = JSON.parse(await fs.readFile(`.tmp/rea-css-evidence/${evidenceId}.json`, 'utf8'))
|
||||
if (evidence.evidence_id !== evidenceId) throw new Error('REA identity mismatch')
|
||||
const module = evidence.normalized_result.styles.items.find(item => /settings-window-perf-ILa933LI.css$/.test(item.file_path))
|
||||
const windowCss = module.source.artifact.text
|
||||
if (sha(windowCss) !== '8036657ec27f8948c6bad60c835d62469e958db4a5b9c25e22e5a9e60c0ea986') throw new Error('Window CSS changed')
|
||||
const contexts = await fs.readFile('.tmp/rea-css-recovered/contexts-BkjwYSZT.css', 'utf8')
|
||||
if (sha(contexts) !== '50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380') throw new Error('Contexts CSS changed')
|
||||
function project(node) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = node.selectors.filter(selector => /\.settings-(modal|navigation)/.test(selector))
|
||||
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes) {
|
||||
const nodes = node.nodes.map(project).filter(Boolean)
|
||||
return nodes.length ? node.clone({ nodes }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
const root = postcss.root({ nodes: postcss.parse(contexts).nodes.map(project).filter(Boolean) })
|
||||
await fs.writeFile('src/renderer/src/components/settings-shell-source.css', `/* REA ${evidenceId}; settings-window CSS sha256=${sha(windowCss)}; contexts sha256=${sha(contexts)}. */\n${root.toString()}\n${windowCss}\n`)
|
||||
console.log('Recovered settings shell CSS with original media conditions')
|
||||
@@ -0,0 +1,17 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
const file = '.tmp/rea-css-recovered/contexts-BkjwYSZT.css'
|
||||
const text = await fs.readFile(file, 'utf8')
|
||||
if (createHash('sha256').update(text).digest('hex') !== '50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380') throw new Error('REA sidebar CSS identity changed')
|
||||
function select(node) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = node.selectors.filter(selector => /\.conversation-list(?:[ >:.#[\]-]|$)|\.conversation-list-(?:header|logo|brand|version|tabs|tab-|content|footer)|\.industry-template-switcher/.test(selector))
|
||||
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes) { const nodes = node.nodes.map(select).filter(Boolean); return nodes.length ? node.clone({ nodes }) : null }
|
||||
return null
|
||||
}
|
||||
const root = postcss.root({ nodes: postcss.parse(text).nodes.map(select).filter(Boolean) })
|
||||
await fs.writeFile('src/renderer/src/components/sidebar-shell-source.css', `/* Fixed REA CSS ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; contexts SHA 50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380. */\n${root.toString()}\n`)
|
||||
console.log('Recovered sidebar shell including original media conditions')
|
||||
@@ -0,0 +1,47 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
const sha = text => createHash('sha256').update(text).digest('hex')
|
||||
const archive = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const source = archive.items.find(item => item.file_path.endsWith('contexts-BMeWSUHk.js')).source.artifact
|
||||
if (sha(source.text) !== '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b') throw Error('REA context SHA mismatch')
|
||||
function definition(name) {
|
||||
const start = source.text.indexOf(name + '=')
|
||||
let quote = '', escaped = false, depth = 0
|
||||
for (let i = start + name.length + 1; i < source.text.length; i++) {
|
||||
const char = source.text[i]
|
||||
if (quote) { if (escaped) escaped = false; else if (char === '\\') escaped = true; else if (char === quote) quote = ''; continue }
|
||||
if (['`', '"', "'"].includes(char)) { quote = char; continue }
|
||||
if (!depth && [',', '}'].includes(char)) return { start, end: i, value: source.text.slice(start + name.length + 1, i) }
|
||||
if ('([{'.includes(char)) depth++
|
||||
if (')]}'.includes(char)) depth--
|
||||
}
|
||||
throw Error('Unbounded source icon ' + name)
|
||||
}
|
||||
let icons = `/* REA ${archive.evidence_id}; contexts-BMeWSUHk.js ${source.sha256}. Literal SVG bodies recovered from archived source. */\nimport { useId, type SVGProps } from 'react'\nimport { jsx as sourceJsx, jsxs as sourceJsxs } from 'react/jsx-runtime'\n`
|
||||
for (const name of ['SkillsIcon', 'PinIcon', 'UnpinIcon', 'SettingsIcon', 'SearchIcon$1', 'ClearFilledIcon']) {
|
||||
const item = definition(name)
|
||||
const body = item.value.replace(/^e=>/, '(e: SVGProps<SVGSVGElement>) =>').replaceAll('(0,bV.jsx)', 'sourceJsx').replaceAll('(0,bV.jsxs)', 'sourceJsxs').replaceAll('returnsource', 'return source').replaceAll('_Nt.useId', 'useId').replaceAll('...xV', 'width:16,height:16,xmlns:"http://www.w3.org/2000/svg"')
|
||||
if (/bV\.|xV/.test(body)) throw Error('Unresolved source icon binding')
|
||||
icons += `// UTF-16 [${item.start}, ${item.end})\nexport const ${name.replace('$1', '')} = ${body}\n`
|
||||
}
|
||||
await fs.mkdir('src/renderer/src/capabilities', { recursive: true })
|
||||
await fs.writeFile('src/renderer/src/capabilities/SkillSourceIcons.tsx', icons)
|
||||
const manifest = JSON.parse(await fs.readFile('.tmp/rea-css-recovered/manifest.json', 'utf8'))
|
||||
let css = `/* REA ${manifest.sourceEvidenceId}; source modules _1mrhu_ and _s54xa_ only. */\n`
|
||||
for (const file of ['contexts-BkjwYSZT.css', 'wb-input-add-CDNucW11.css']) {
|
||||
const bytes = await fs.readFile('.tmp/rea-css-recovered/' + file)
|
||||
// The export manifest pins the source digest; never silently consume a changed sheet.
|
||||
if (!JSON.stringify(manifest).includes(sha(bytes))) throw Error('Unpinned CSS ' + file)
|
||||
css += `/* ${file} sha256=${sha(bytes)} */\n`
|
||||
const parsed = postcss.parse(bytes.toString())
|
||||
parsed.walkRules(rule => {
|
||||
if (!rule.selector.includes('_1mrhu_') && !rule.selector.includes('_s54xa_')) return
|
||||
const clone = rule.clone()
|
||||
clone.selector = clone.selectors.map(selector => '.yc-skill-picker ' + selector).join(',')
|
||||
if (rule.parent.type === 'atrule') { const wrapper = rule.parent.clone({ nodes: [] }); wrapper.append(clone); css += wrapper.toString() + '\n' }
|
||||
else css += clone.toString() + '\n'
|
||||
})
|
||||
}
|
||||
await fs.writeFile('src/renderer/src/capabilities/skill-picker-source.css', css)
|
||||
console.log(JSON.stringify({ source: archive.evidence_id, css: manifest.sourceEvidenceId, cssBytes: css.length }))
|
||||
@@ -0,0 +1,150 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverseModule from '@babel/traverse'
|
||||
import { exportSelectedArtifactText } from '../../.tmp/rea-css-runtime/dist/artifacts/SelectedArtifactText.js'
|
||||
const traverse = traverseModule.default, hash = value => createHash('sha256').update(value).digest('hex')
|
||||
const rootSha = '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
|
||||
const archive = 'C:/Users/Administrator/AppData/Local/Programs/WorkBuddy/resources/app.asar'
|
||||
const target = path.resolve('src/renderer/src/workbuddy/workbench/source-code-assets')
|
||||
const runtime = JSON.parse(await fs.readFile('.tmp/rea-runtime-scripts.json', 'utf8'))
|
||||
const selected = JSON.parse(await fs.readFile('.tmp/rea-selected-editor-evidence.json', 'utf8'))
|
||||
const cssManifest = JSON.parse(await fs.readFile('.tmp/rea-css-recovered/manifest.json', 'utf8'))
|
||||
if (selected.normalized_result.root_sha256 !== rootSha) throw Error('Source archive identity changed')
|
||||
const files = new Map(), graph = {}, records = []
|
||||
for (const item of runtime.items) {
|
||||
const a = item.source?.artifact
|
||||
if (!a?.text) continue
|
||||
if (hash(a.text) !== a.sha256) throw Error('Runtime source digest changed')
|
||||
files.set(path.basename(item.file_path.replaceAll('\\', '/')), { text: a.text, sha256: a.sha256, evidenceId: runtime.evidence_id })
|
||||
}
|
||||
for (const f of selected.normalized_result.files) {
|
||||
if (hash(f.text) !== f.sha256) throw Error('Selected editor digest changed')
|
||||
files.set(path.basename(f.path), { text: f.text, sha256: f.sha256, evidenceId: selected.evidence_id })
|
||||
}
|
||||
for (const name of (await fs.readdir('.tmp')).filter(name => /^rea-monaco-chunks-\d+\.json$/.test(name)).sort()) {
|
||||
const evidencePath = path.resolve('.tmp', name), evidence = JSON.parse(await fs.readFile(evidencePath, 'utf8'))
|
||||
if (evidence.authority !== 'shipped-artifact' || evidence.normalized_result.root_sha256 !== rootSha) throw Error('Cached REA identity changed')
|
||||
records.push({ evidenceId: evidence.evidence_id, path: evidencePath })
|
||||
for (const f of evidence.normalized_result.files) {
|
||||
const body = f.text ?? Buffer.from(f.data_base64, 'base64')
|
||||
if (hash(body) !== f.sha256) throw Error('Cached REA source digest changed')
|
||||
files.set(path.basename(f.path), { [f.text === undefined ? 'binary' : 'text']: body, sha256: f.sha256, evidenceId: evidence.evidence_id })
|
||||
}
|
||||
}
|
||||
for (const f of cssManifest.files) {
|
||||
const text = await fs.readFile(path.join('.tmp/rea-css-recovered', f.file), 'utf8')
|
||||
if (hash(text) !== f.sha256) throw Error('CSS digest changed')
|
||||
files.set(f.file, { text, sha256: f.sha256, evidenceId: cssManifest.sourceEvidenceId })
|
||||
}
|
||||
function dependencies(name, text) {
|
||||
const deps = new Set()
|
||||
const add = value => { if (value?.startsWith('./') && /\.(?:js|css|ttf|woff2?)$/.test(value)) deps.add(value.slice(2)) }
|
||||
if (name.endsWith('.css')) for (const m of text.matchAll(/url\(['"]?(\.\/[^'"\)]+)['"]?\)/g)) add(m[1])
|
||||
else if (name.endsWith('.js')) {
|
||||
const ast = parse(text, { sourceType: 'module' })
|
||||
traverse(ast, { ImportDeclaration(p) { add(p.node.source.value) }, NewExpression(p) {
|
||||
if (p.node.callee.type !== 'Identifier' || p.node.callee.name !== 'URL') return
|
||||
const n = p.node.arguments[0], value = n?.type === 'StringLiteral' ? n.value : n?.type === 'TemplateLiteral' && !n.expressions.length ? n.quasis[0].value.cooked : undefined
|
||||
if (value && /^[\w.-]+\.(?:js|css)$/.test(value)) add('./' + value)
|
||||
else add(value)
|
||||
}, CallExpression(p) {
|
||||
if (p.node.callee.type !== 'Import') return
|
||||
const n = p.node.arguments[0]; add(n.type === 'StringLiteral' ? n.value : n.type === 'TemplateLiteral' && !n.expressions.length ? n.quasis[0].value.cooked : undefined)
|
||||
}, AssignmentExpression(p) {
|
||||
if (p.node.left.type === 'MemberExpression' && p.node.left.property.name === 'f' && p.node.right.type === 'ArrayExpression') for (const n of p.node.right.elements) if (n?.type === 'StringLiteral') add(n.value)
|
||||
} })
|
||||
}
|
||||
return [...deps].filter(name => name !== 'react-z__Ncn__.js')
|
||||
}
|
||||
const wanted = new Set(['editor.main-POO0Vbhx.js', 'monaco-init-CE4MbM4x.js', 'editor-gwlzV742.css', 'codeEditor-Bxmo_AI3.css'])
|
||||
for (;;) {
|
||||
const missing = [...wanted].filter(name => !files.has(name))
|
||||
for (let offset = 0; offset < missing.length; offset += 16) {
|
||||
const batch = missing.slice(offset, offset + 16)
|
||||
for (const binary of [false, true]) {
|
||||
const names = batch.filter(name => /\.(?:ttf|woff2?)$/.test(name) === binary)
|
||||
if (!names.length) continue
|
||||
const evidence = await exportSelectedArtifactText(archive, rootSha, names.map(name => 'renderer/assets/' + name), binary)
|
||||
const evidencePath = path.resolve(`.tmp/rea-monaco-chunks-${records.length.toString().padStart(3, '0')}.json`)
|
||||
await fs.writeFile(evidencePath, JSON.stringify(evidence))
|
||||
records.push({ evidenceId: evidence.evidence_id, path: evidencePath })
|
||||
for (const f of evidence.normalized_result.files) {
|
||||
const body = binary ? Buffer.from(f.data_base64, 'base64') : f.text
|
||||
if (hash(body) !== f.sha256) throw Error('REA export digest mismatch')
|
||||
files.set(path.basename(f.path), { [binary ? 'binary' : 'text']: body, sha256: f.sha256, evidenceId: evidence.evidence_id })
|
||||
}
|
||||
}
|
||||
}
|
||||
const before = wanted.size
|
||||
for (const name of wanted) {
|
||||
const file = files.get(name)
|
||||
if (!file) continue
|
||||
graph[name] = file.text ? dependencies(name, file.text) : []
|
||||
for (const dep of graph[name]) wanted.add(dep)
|
||||
}
|
||||
if (wanted.size === before && [...wanted].every(name => files.has(name))) break
|
||||
}
|
||||
await fs.mkdir(target, { recursive: true })
|
||||
const assets = []
|
||||
for (const name of wanted) {
|
||||
const file = files.get(name); let text = file.text, adapters = []
|
||||
if (name.endsWith('.js') && text.includes('__vite__mapDeps')) {
|
||||
const ast = parse(text, { sourceType: 'module' }), edits = []
|
||||
traverse(ast, { VariableDeclarator(p) {
|
||||
if (p.node.id.name === '__vite__mapDeps') edits.push({ start: p.node.start, end: p.node.end, text: '__ycodeMonacoMapDeps=()=>[]' })
|
||||
}, Identifier(p) {
|
||||
if (p.node.name === '__vite__mapDeps' && p.parent.type === 'CallExpression' && p.parent.callee === p.node) edits.push({ start: p.node.start, end: p.node.end, text: '__ycodeMonacoMapDeps' })
|
||||
} })
|
||||
for (const edit of edits.sort((a, b) => b.start - a.start)) text = text.slice(0, edit.start) + edit.text + text.slice(edit.end)
|
||||
adapters.push('Original bundler preload table replaced by empty host preload adapter; Vite owns imports/CSS, avoiding its reserved helper binding and obsolete original URLs.')
|
||||
}
|
||||
if (name === 'monaco-init-CE4MbM4x.js') {
|
||||
const fallback = 'P.config({paths:{vs:`https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs`}})'
|
||||
if (text.split(fallback).length !== 3) throw Error('Monaco local-only fallback boundary changed')
|
||||
text = text.replaceAll(fallback, '(()=>{throw Error(`YCode local Monaco assets failed to load`)})()')
|
||||
adapters.push('Two CDN fallback branches fail locally; source local loader/theme/React host preserved.')
|
||||
}
|
||||
const bytes = file.binary ?? text
|
||||
await fs.writeFile(path.join(target, name), bytes)
|
||||
assets.push({ name, sourceSha256: file.sha256, outputSha256: hash(bytes), evidenceId: file.evidenceId, adapters })
|
||||
}
|
||||
// Use YCode's existing React instance, never initialize the original application's React globals.
|
||||
await fs.writeFile(path.join(target, 'react-z__Ncn__.js'), "import React from 'react';export function t(){return React}\n")
|
||||
const source = files.get('codeEditor-D3P9alks.js').text
|
||||
const ast = parse(source, { sourceType: 'module' }), snippets = {}
|
||||
traverse(ast, { AssignmentExpression(p) {
|
||||
const n = p.node, name = n.left.type === 'Identifier' ? n.left.name : ''
|
||||
if ((name === 're' && n.start > 14000 && n.start < 15000) || (name === 'ee' && n.start > 11000 && n.start < 12000)
|
||||
|| (['R', 'B'].includes(name) && n.start > 8000 && n.start < 11624) || (name === 'q' && n.start > 14287 && n.start < 14737)
|
||||
|| ['UpArrowIcon', 'DownArrowIcon'].includes(name)) snippets[name] = { text: source.slice(n.start, n.end), start: n.start, end: n.end }
|
||||
}, FunctionDeclaration(p) { if (['getMonacoLanguage', 'getFontSizeOffset$1'].includes(p.node.id?.name)) snippets[p.node.id.name] = { text: source.slice(p.node.start, p.node.end), start: p.node.start, end: p.node.end } } })
|
||||
for (const name of ['re', 'ee', 'R', 'B', 'q', 'UpArrowIcon', 'DownArrowIcon', 'getMonacoLanguage', 'getFontSizeOffset$1']) if (!snippets[name]) throw Error('Missing source closure ' + name)
|
||||
const locale = files.get('zh-cn-Dfzn8myY.js'), labels = {}
|
||||
for (const m of locale.text.matchAll(/"(search\.[^"]+)":(`(?:\\.|[^`])*`)/g)) {
|
||||
const literal = parse(m[2], { sourceType: 'module' }).program.body[0].expression
|
||||
if (literal.type !== 'TemplateLiteral' || literal.expressions.length) throw Error('Locale must be a static string')
|
||||
labels[m[1]] = literal.quasis[0].value.cooked
|
||||
}
|
||||
const media = files.get('media-preview-DMz4TW4q.js').text
|
||||
let extensionBody
|
||||
traverse(parse(media, { sourceType: 'module' }), { AssignmentExpression(p) { if (p.node.left.name === 'getLanguageFromFileName') extensionBody = media.slice(p.node.start, p.node.end) } })
|
||||
if (!extensionBody || hash(extensionBody) !== '666dfd63d32a89ed66020d0a4fb6d1a41c25be41692c3538e6b167c157fa1465') throw Error('Language source body changed')
|
||||
await fs.writeFile(path.join(target, 'extension.mjs'), 'let getLanguageFromFileName;' + extensionBody + ';export {getLanguageFromFileName};\n')
|
||||
await fs.writeFile(path.join(target, 'extension.d.mts'), 'export function getLanguageFromFileName(name:string):string;\n')
|
||||
const wrapper = `// Generated source bodies; host bindings are explicit adapters below. No WorkBuddy services.\nimport * as K from 'react';import * as ne from 'react/jsx-runtime';import {c as O,d as A,r as F} from './monaco-init-CE4MbM4x.js';\nconst U=K,W=ne;const classes=(...a)=>a.flatMap(x=>typeof x==='string'?[x]:x&&typeof x==='object'?Object.keys(x).filter(k=>x[k]):[]).join(' ');const H={default:classes},te=H;const labels=${JSON.stringify(labels)};function u(){return key=>labels[key]||key}\nconst g=K.forwardRef(({value,onChange,placeholder,className},ref)=>ne.jsx('span',{className:'wb-input-wrapper wb-input-wrapper--small wb-input-wrapper--borderless '+className,children:ne.jsx('input',{ref,value,placeholder,className:'wb-input__inner',onChange:e=>onChange(e.target.value)})}));\nconst p=({variant='ghost',size='small',iconOnly,className,children,...props})=>ne.jsx('button',{...props,type:'button',className:classes('wb-button','wb-button--'+variant,'wb-button--'+size,iconOnly&&'wb-button--icon-only',className),children});const m=p;const h=()=>ne.jsx('svg',{width:16,height:16,viewBox:'0 0 16 16',fill:'none',children:ne.jsx('path',{d:'M4 4l8 8M12 4l-8 8',stroke:'currentColor',strokeWidth:1.2,strokeLinecap:'round'})});\nlet R,B,q,ee,re,UpArrowIcon,DownArrowIcon;\n${['R','B','q','UpArrowIcon','DownArrowIcon','ee','getMonacoLanguage','getFontSizeOffset$1','re'].map(n=>snippets[n].text+';').join('\n')}\nexport {re as MonacoEditor,O as configureMonaco,F as themeManager};\n`
|
||||
const sourceUi = files.get('src-0FGg1seX.js').text
|
||||
let closeBody
|
||||
traverse(parse(sourceUi, { sourceType: 'module' }), { AssignmentExpression(p) { if (p.node.left.name === 'CloseIcon$2' && p.node.start === 245582) closeBody = sourceUi.slice(p.node.start, p.node.end) } })
|
||||
if (!closeBody) throw Error('Source close icon changed')
|
||||
const closeAdapter = `const Lm=ne;let CloseIcon$2;${closeBody};const h=CloseIcon$2;`
|
||||
const finalWrapper = wrapper.replace(/const h=\(\)=>ne\.jsx\('svg',[^\n]+?\);\nlet R/, closeAdapter + '\nlet R')
|
||||
if (finalWrapper === wrapper) throw Error('Close icon adapter replacement failed')
|
||||
await fs.writeFile(path.join(target, 'wrapper.mjs'), finalWrapper)
|
||||
await fs.writeFile(path.join(target, 'wrapper.d.mts'), `import type {ComponentType} from 'react';export const MonacoEditor:ComponentType<{value:string;language?:string;readOnly?:boolean;minimap?:boolean;lineNumbers?:string;wordWrap?:string;fontSize?:number;height?:string;onMount?:(editor:any,monaco:any)=>void}>;export function configureMonaco():Promise<void>;export function themeManager():{setTheme(name:string):void};\n`)
|
||||
await fs.writeFile(path.join(target, 'manifest.json'), JSON.stringify({ rootSha256: rootSha, version: '5.8.2', monacoPackageVersion: 'unconfirmed; exact shipped chunks used', records, assets, graph,
|
||||
wrapper: Object.fromEntries(Object.entries(snippets).map(([name, body]) => [name, { start: body.start, end: body.end, sha256: hash(body.text) }])),
|
||||
sourceCloseIcon: { offset: 245582, sha256: hash(closeBody), evidenceId: runtime.evidence_id },
|
||||
hostAdapters: ['YCode React singleton', 'native input/button/className/i18n bindings', 'local-only failure instead of CDN fallback', 'Vite owns preloading paths'] }, null, 2))
|
||||
console.log(JSON.stringify({ result: 'PASS', assets: assets.length, newEvidence: records.length, target, wrapperBody: snippets.re.end - snippets.re.start }))
|
||||
@@ -0,0 +1,96 @@
|
||||
// Recover only the loading component rules used by StreamingFooter from REA-exported CSS.
|
||||
// This reads the saved REA export, never the WorkBuddy installation or runtime directly.
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const sourceDirectory = path.resolve('.tmp/rea-css-recovered')
|
||||
const destination = path.resolve('src/renderer/src/workbuddy/conversation/streaming-footer-source.css')
|
||||
const evidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
const manifest = JSON.parse(await readFile(path.join(sourceDirectory, 'manifest.json'), 'utf8'))
|
||||
assert.equal(manifest.sourceEvidenceId, evidenceId)
|
||||
|
||||
const sourceFiles = [
|
||||
{ file: 'src-CBaV_A3S.css', sha256: '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c' },
|
||||
{ file: 'contexts-BkjwYSZT.css', sha256: '50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380' }
|
||||
]
|
||||
const classNames = [
|
||||
'assistant-message',
|
||||
'_loadingMessage_13r4w_60',
|
||||
'_loadingContent_13r4w_69',
|
||||
'_loadingText_13r4w_81',
|
||||
'_mainText_13r4w_94',
|
||||
'_tipPhrase_13r4w_108',
|
||||
'_tipDivider_13r4w_121',
|
||||
'_tipText_13r4w_126',
|
||||
'_tipDismissButton_13r4w_140',
|
||||
'_trailingTipPhrase_13r4w_179',
|
||||
'_loadingWithCredit_ttf8p_463',
|
||||
'cb-loading-tip--trailing'
|
||||
]
|
||||
|
||||
const output = postcss.root()
|
||||
const usedAnimations = new Set()
|
||||
const sourceIdentities = []
|
||||
|
||||
function referencesClass(selector) {
|
||||
return classNames.some(name => new RegExp(`\\.${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`).test(selector))
|
||||
}
|
||||
|
||||
function copyRelevantNodes(nodes) {
|
||||
const copied = []
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = (node.selectors ?? []).filter(referencesClass)
|
||||
if (!selectors.length) continue
|
||||
const clone = node.clone({ selector: selectors.join(',') })
|
||||
clone.walkDecls(/^animation(?:-name)?$/, declaration => {
|
||||
for (const token of declaration.value.split(/[\s,]+/)) usedAnimations.add(token)
|
||||
})
|
||||
copied.push(clone)
|
||||
continue
|
||||
}
|
||||
if (node.type === 'atrule' && /keyframes$/.test(node.name)) continue
|
||||
if (node.type === 'atrule' && node.nodes) {
|
||||
const children = copyRelevantNodes(node.nodes)
|
||||
if (children.length) copied.push(node.clone({ nodes: children }))
|
||||
}
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
for (const expected of sourceFiles) {
|
||||
const metadata = manifest.files.find(file => file.file === expected.file)
|
||||
assert.ok(metadata, `REA CSS export must contain ${expected.file}`)
|
||||
assert.equal(metadata.sha256, expected.sha256, `REA CSS manifest digest changed for ${expected.file}`)
|
||||
const bytes = await readFile(path.join(sourceDirectory, expected.file))
|
||||
assert.equal(createHash('sha256').update(bytes).digest('hex'), expected.sha256, `REA CSS bytes changed for ${expected.file}`)
|
||||
const source = postcss.parse(bytes, { from: expected.file })
|
||||
for (const node of copyRelevantNodes(source.nodes)) output.append(node)
|
||||
source.walkAtRules(/^keyframes$/, node => {
|
||||
if (usedAnimations.has(node.params)) output.append(node.clone())
|
||||
})
|
||||
sourceIdentities.push({ file: expected.file, sha256: expected.sha256, asarComparison: metadata.asarComparison?.status ?? 'not-compared' })
|
||||
}
|
||||
|
||||
for (const name of classNames) {
|
||||
assert.ok(output.toString().includes(`.${name}`), `REA CSS export is missing the ${name} rule`)
|
||||
}
|
||||
assert.ok(output.toString().includes('@keyframes _cb-shining-text-sweep_13r4w_1'))
|
||||
assert.ok(output.toString().includes('@keyframes _cb-tip-fade-in_13r4w_1'))
|
||||
|
||||
const header = `/* Generated from REA ${evidenceId} by recover-streaming-footer-styles.mjs.
|
||||
* Selected WorkBuddy 5.8.2 loading component rules are preserved verbatim.
|
||||
* Runtime CSS identities: ${sourceIdentities.map(item => `${item.file}=${item.sha256} (${item.asarComparison})`).join('; ')}
|
||||
*/\n`
|
||||
const result = header + output.toString() + '\n'
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
assert.equal(await readFile(destination, 'utf8'), result, 'Recovered streaming footer styles are stale')
|
||||
console.log(`streaming footer styles check passed (${classNames.length} class families; ${sourceIdentities.length} REA CSS assets)`)
|
||||
} else {
|
||||
await writeFile(destination, result, 'utf8')
|
||||
console.log(`recovered streaming footer styles to ${destination}`)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const evidencePath = resolve('.tmp/rea-runtime-scripts.json')
|
||||
const outputPath = resolve('src/renderer/src/workbuddy/conversation/streaming-loading-tips.ts')
|
||||
const evidence = JSON.parse(readFileSync(evidencePath, 'utf8'))
|
||||
assert.equal(evidence.evidence_id, 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb')
|
||||
|
||||
const sourceRecord = evidence.items.find(item => item.file_path.endsWith('src-0FGg1seX.js'))
|
||||
assert.ok(sourceRecord?.source?.artifact?.text, 'REA evidence must contain the recovered conversation module')
|
||||
assert.equal(sourceRecord.source.artifact.sha256, 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178')
|
||||
|
||||
const source = sourceRecord.source.artifact.text
|
||||
const localeFunction = source.indexOf('function normalizeLocale')
|
||||
assert.ok(localeFunction > 0, 'Source locale resolver must be present')
|
||||
|
||||
function extractTips(name, delimiter) {
|
||||
const tick = String.fromCharCode(96)
|
||||
const assignment = source.lastIndexOf(`${name}=${tick}`, localeFunction)
|
||||
assert.ok(assignment >= 0, `Source ${name} tip list must be present`)
|
||||
const contentStart = assignment + name.length + 2
|
||||
const contentEnd = source.indexOf(tick, contentStart)
|
||||
assert.ok(contentEnd > contentStart, `Source ${name} template literal must be closed`)
|
||||
assert.ok(source.startsWith(`${tick}.split(${tick}${delimiter}${tick})`, contentEnd),
|
||||
`Source ${name} list must use its recorded ${JSON.stringify(delimiter)} separator`)
|
||||
|
||||
const values = source.slice(contentStart, contentEnd)
|
||||
.split(delimiter)
|
||||
.filter(Boolean)
|
||||
.map(value => value
|
||||
.replace(/\\u([0-9a-f]{4})/gi, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
||||
.replace(/\\"/g, '"'))
|
||||
assert.ok(values.length > 0, `Source ${name} tip list must not be empty`)
|
||||
return values
|
||||
}
|
||||
|
||||
const englishTips = [...extractTips('dD', '.'), ...extractTips('fD', '(')]
|
||||
const chineseTips = [...extractTips('mD', '.'), ...extractTips('hD', '.')]
|
||||
const output = `// Generated from REA ${evidence.evidence_id}; src-0FGg1seX.js SHA-256 ${sourceRecord.source.artifact.sha256}.\n` +
|
||||
'// WorkBuddy 5.8.2 getLoadingTips() arrays; brand substitution happens in the host component.\n' +
|
||||
'export const SOURCE_LOADING_TIPS = {\n' +
|
||||
` en: ${JSON.stringify(englishTips, null, 2).replaceAll('\n', '\n ')},\n` +
|
||||
` zh: ${JSON.stringify(chineseTips, null, 2).replaceAll('\n', '\n ')}\n` +
|
||||
'} as const\n'
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
assert.equal(readFileSync(outputPath, 'utf8'), output, 'Recovered streaming loading tips are stale')
|
||||
console.log(`streaming loading tips check passed (${englishTips.length} en / ${chineseTips.length} zh)`)
|
||||
} else {
|
||||
writeFileSync(outputPath, output, 'utf8')
|
||||
console.log(`recovered streaming loading tips (${englishTips.length} en / ${chineseTips.length} zh) to ${outputPath}`)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
const file='.tmp/rea-css-recovered/src-CyXHA0IC.css'
|
||||
const css=fs.readFileSync(file,'utf8'),sha=crypto.createHash('sha256').update(css).digest('hex')
|
||||
if(sha!=='a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe')throw Error('Unexpected pinned REA table CSS source')
|
||||
const rules=[]
|
||||
postcss.parse(css).walkRules(rule=>{
|
||||
const selectors=rule.selectors.filter(selector=>/\.cr-table-block|^\.cr-markdown (?:th|td)(?:\b|>)/.test(selector))
|
||||
if(!selectors.length)return
|
||||
const selected=rule.clone();selected.selectors=selectors.map(selector=>selector.includes('.cr-markdown')?selector:'.cr-markdown '+selector)
|
||||
let tree=selected
|
||||
for(let parent=rule.parent;parent?.type==='atrule';parent=parent.parent){const outer=parent.clone({nodes:[]});outer.append(tree);tree=outer}
|
||||
rules.push(tree.toString())
|
||||
})
|
||||
fs.writeFileSync('src/renderer/src/workbuddy/markdown/source-table.css',`/* WorkBuddy 5.8.2; REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94.
|
||||
* src-CyXHA0IC.css SHA-256 ${sha}; table selectors scoped to .cr-markdown.
|
||||
* Native geometry evidence ev_d6e6920ff4e47f73a182fac5312c5f02393366a035445d18c8a6733a8c8a11ed. */\n`+rules.join('\n')+'\n')
|
||||
console.log(JSON.stringify({sha,rules:rules.length}))
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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))
|
||||
Loaded 100 of 1118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user