const assert = require('node:assert/strict') const crypto = require('node:crypto') const fs = require('node:fs') const path = require('node:path') const repoRoot = path.resolve(__dirname, '..', '..') const tmpDir = path.join(repoRoot, '.tmp') const evidence = { home: { path: path.join(tmpDir, 'rea-four-modules-library-stable-20261017.json'), id: 'ev_82a9ad8ec38c807e465646d749e2cedbb150fec62e30234c97acd0bb309c349e' }, local: { path: path.join(tmpDir, 'rea-four-modules-library-local-20261017.json'), id: 'ev_b7fe1b4adac2311b04dfa454126363ca382869d6a29664152d497a920c2e8cad' } } const mediaEvidence = { path: path.join( tmpDir, 'rea-css-evidence', 'ev_d650991b41fe1831a4caf5dd225398e29db8e91205d00c2ac3047b00baa1998e.json' ), id: 'ev_d650991b41fe1831a4caf5dd225398e29db8e91205d00c2ac3047b00baa1998e' } const illustrations = [ { component: 'RobotGuideIllustration', importName: 'agentGuideIllustration', assetFile: 'wb-home-guide-card-agent-illustration.svg', selector: '.wb-home-guide-card-agent-pc .wb-home-guide-card-illustration-pc', sourceUrl: 'https://static.workbuddy.cn/web/library/5609e40689e0ddd5b7e8ede0a4e43acd7b1ba74d/js/federation/releases/e7fafd8eaa71ac3f/guide-card-agent-illustration.1abe0c58.svg', bytes: 2857, sha256: '1b0984754f470c4529b09d2fa56a957f23df3bd7fc16cc27d74989b1112ff815' }, { component: 'HtmlGuideIllustration', importName: 'htmlGuideIllustration', assetFile: 'wb-home-guide-card-html-illustration.svg', selector: '.wb-home-guide-card-html-pc .wb-home-guide-card-illustration-pc', sourceUrl: 'https://static.workbuddy.cn/web/library/5609e40689e0ddd5b7e8ede0a4e43acd7b1ba74d/js/federation/releases/e7fafd8eaa71ac3f/guide-card-html-illustration.0c0d6493.svg', bytes: 1413, sha256: '1af18b1901999725af4114997bba4c779c6d04f09712c44d34add010433d1e47' } ] const icons = [ { name: 'ClockIcon', evidence: 'home', rootIndex: 872, parentClass: 'memory-nav-item-icon-pc' }, { name: 'FolderLineIcon', evidence: 'home', rootIndex: 878, parentClass: 'memory-nav-item-icon-pc' }, { name: 'MoreIcon', evidence: 'home', rootIndex: 1066, parentClass: 'wb-feed-more-btn-pc' }, { name: 'InfoIcon', evidence: 'home', rootIndex: 1290, parentClass: 'wb-home-guide-title-pc' }, { name: 'LightbulbIcon', evidence: 'home', rootIndex: 1323, parentClass: 'wb-home-guide-playbook-title-pc' }, { name: 'BackIcon', evidence: 'local', rootIndex: 848, parentClass: 'kb-back-to-library__btn' }, { name: 'SortArrowsIcon', evidence: 'local', rootIndex: 878, parentClass: 'my-files-flat-col-name my-files-flat-sortable', variant: 'sort' }, { name: 'ShareIcon', evidence: 'local', rootIndex: 954, parentClass: 'my-files-list-action-btn' }, { name: 'TableMoreIcon', evidence: 'local', rootIndex: 959, parentClass: 'my-files-list-action-btn' }, { name: 'TaskIcon', evidence: 'local', rootIndex: 967, parentClass: 'my-files-flat-col-name' } ] const notes = { star_icon: { status: 'unresolved', reason: 'No star/favorite SVG exists in the captured library UI. WorkBuddy uses a CSS checkbox for the favorite filter and renders share plus more actions in file rows.', evidence_ids: Object.values(evidence).map((item) => item.id), searched_classes: [ 'my-files-favorite-checkbox', 'my-files-favorite-checkbox__label', 'my-files-list-action-btn' ] }, rejected_reusable_home_media: { status: 'not_applicable', reason: 'A previously archived image body exists, but it belongs to the older .wb-home-route__growth-buddy img selector and is not evidence for either current guide-card illustration.', evidence_id: 'ev_474129797ce3ab73eaaab2cd41247653e7f128c3b89b598158b57b74e1f17e88', selector: '.wb-home-route__growth-buddy img', source_url: 'https://staging-download.codebuddy.cn/operation-platform/assets/_general/ffca4ecbc52c1e3e.png', artifact_sha256: '2713ed5bbf1288d56ebeaca49bfb0d622c967914ab9c98955cc995932049e7e8', bytes: 13426 } } const svgAttributeNames = new Map([ ['class', 'className'], ['clip-path', 'clipPath'], ['clip-rule', 'clipRule'], ['fill-rule', 'fillRule'], ['stroke-dasharray', 'strokeDasharray'], ['stroke-dashoffset', 'strokeDashoffset'], ['stroke-linecap', 'strokeLinecap'], ['stroke-linejoin', 'strokeLinejoin'], ['stroke-miterlimit', 'strokeMiterlimit'], ['stroke-width', 'strokeWidth'], ['transform-origin', 'transformOrigin'] ]) const svgTagNames = new Map([ ['clippath', 'clipPath'], ['lineargradient', 'linearGradient'], ['radialgradient', 'radialGradient'] ]) function sha256(value) { return crypto.createHash('sha256').update(value).digest('hex') } function readEvidence(spec) { const value = JSON.parse(fs.readFileSync(spec.path, 'utf8').replace(/^\uFEFF/, '')) assert.equal(value.evidence_id, spec.id) assert.equal(value.operation, 'inspect_electron_page') return value } function jsxAttributeName(name) { return svgAttributeNames.get(name) ?? name } function jsxAttributes(attributes) { return Object.entries(attributes) .map(([name, value]) => `${jsxAttributeName(name)}=${JSON.stringify(value)}`) .join(' ') } function recoverIcon(document, spec) { const layoutNodes = new Map(document.nodes.map((node) => [node.node_index, node])) const svgByIndex = new Map(document.svg_nodes.map((node) => [node.node_index, node])) const childrenByParent = new Map() for (const node of document.svg_nodes) { const children = childrenByParent.get(node.parent_index) ?? [] children.push(node) childrenByParent.set(node.parent_index, children) } const root = svgByIndex.get(spec.rootIndex) assert.ok(root, `missing SVG root ${spec.rootIndex} for ${spec.name}`) assert.equal(root.node_name, 'svg') const parent = layoutNodes.get(root.parent_index) assert.ok(parent, `missing SVG parent for ${spec.name}`) assert.equal(parent.attributes?.class, spec.parentClass) const subtree = [] const visit = (node) => { subtree.push({ node_index: node.node_index, parent_index: node.parent_index, node_name: node.node_name, attributes: node.attributes }) for (const child of childrenByParent.get(node.node_index) ?? []) visit(child) } visit(root) return { root, parent, childrenByParent, hash: sha256(JSON.stringify(subtree)) } } function renderNode(recovery, node, isRoot = false, spec = null) { const tag = svgTagNames.get(node.node_name.toLowerCase()) ?? node.node_name const attributes = { ...node.attributes } let prefix = '' if (isRoot) { delete attributes.width delete attributes.height if (spec?.variant === 'sort') { delete attributes.class prefix = `{...svgProps} width={width ?? ${JSON.stringify(recovery.root.attributes.width)}} height={height ?? ${JSON.stringify(recovery.root.attributes.height)}} className={mergeClassNames("my-files-flat-sort-icon", direction, className)} ` } else if ('class' in attributes) { const sourceClass = attributes.class delete attributes.class prefix = `{...svgProps} width={width ?? ${JSON.stringify(recovery.root.attributes.width)}} height={height ?? ${JSON.stringify(recovery.root.attributes.height)}} className={mergeClassNames(${JSON.stringify(sourceClass)}, className)} ` } else { prefix = `{...svgProps} width={width ?? ${JSON.stringify(recovery.root.attributes.width)}} height={height ?? ${JSON.stringify(recovery.root.attributes.height)}} ` } } const serialized = jsxAttributes(attributes) const opener = `<${tag} ${prefix}${serialized}` const children = recovery.childrenByParent.get(node.node_index) ?? [] if (!children.length) return `${opener} />` return `${opener}>${children.map((child) => renderNode(recovery, child)).join('')}` } function functionSource(name, recovery, evidenceId, version, spec) { const provenance = [ `WorkBuddy ${version}`, `REA ${evidenceId}`, `parent .${spec.parentClass.replaceAll(' ', '.')}`, `root ${spec.rootIndex}`, `subtree sha256 ${recovery.hash}` ].join('; ') if (spec.variant === 'sort') { return `// Exact SVG recovery: ${provenance}\nexport function ${name}({ direction, className, width, height, ...svgProps }: SortIconProps): React.JSX.Element {\n return ${renderNode(recovery, recovery.root, true, spec)}\n}` } return `// Exact SVG recovery: ${provenance}\nexport function ${name}(props: IconProps): React.JSX.Element {\n const { className, width, height, ...svgProps } = props\n return ${renderNode(recovery, recovery.root, true, spec)}\n}` } function mergeClassNames(...values) { return values.filter(Boolean).join(' ') || undefined } function readMediaEvidence(spec) { const value = JSON.parse(fs.readFileSync(spec.path, 'utf8').replace(/^\uFEFF/, '')) assert.equal(value.evidence_id, spec.id) const items = value.normalized_result?.visuals?.media_assets?.items assert.ok(Array.isArray(items)) return items } function recoverIllustration(item, spec) { assert.equal(item.selector, spec.selector) assert.equal(item.status, 'exported') assert.equal(item.source?.url, spec.sourceUrl) assert.equal(item.source?.mime_type, 'image/svg+xml') assert.equal(item.artifact?.mime_type, 'image/svg+xml') assert.equal(item.artifact?.bytes, spec.bytes) assert.equal(item.artifact?.sha256, spec.sha256) const bytes = Buffer.from(item.artifact.data_base64, 'base64') assert.equal(bytes.length, spec.bytes) assert.equal(sha256(bytes), spec.sha256) const svg = bytes.toString('utf8') assert.match(svg.trimStart(), /^ `import ${spec.importName} from './assets/${spec.assetFile}'`) .join('\n') const components = records.map(({ record, spec }) => { const provenance = [ `REA ${mediaEvidence.id}`, `selector ${spec.selector}`, `source ${spec.sourceUrl}`, `${spec.bytes} bytes`, `sha256 ${spec.sha256}` ].join('; ') return `// Exact WorkBuddy asset recovery: ${provenance}\nexport function ${spec.component}(): React.JSX.Element {\n return (\n \n )\n}` }).join('\n\n') return `// Generated by scripts/rea/recover-library-icons.cjs. Do not hand edit.\n${imports}\n\n${components}\n` } function main() { const loaded = Object.fromEntries( Object.entries(evidence).map(([name, spec]) => [name, readEvidence(spec)]) ) const iconRecords = [] const iconSources = [] for (const spec of icons) { const value = loaded[spec.evidence] const document = value.normalized_result.visuals.layout.documents[0] const recovery = recoverIcon(document, spec) const version = value.normalized_result.browser.user_agent.match(/WorkBuddy\/([^\s]+)/)?.[1] ?? 'unknown' iconSources.push(functionSource(spec.name, recovery, value.evidence_id, version, spec)) iconRecords.push({ name: spec.name, evidence_id: value.evidence_id, evidence_file: path.relative(repoRoot, evidence[spec.evidence].path).replaceAll('\\', '/'), root_node_index: spec.rootIndex, parent_class: recovery.parent.attributes.class, source_sha256: recovery.hash, svg_attributes: recovery.root.attributes }) } const iconOutput = `// Generated by scripts/rea/recover-library-icons.cjs. Do not hand edit.\nimport type { SVGProps } from 'react'\n\ntype IconProps = SVGProps\ntype SortIconProps = IconProps & { direction?: 'asc' | 'desc' }\n\nfunction mergeClassNames(...values: Array): string | undefined {\n return values.filter(Boolean).join(' ') || undefined\n}\n\n${iconSources.join('\n\n')}\n` const iconDestination = path.join(repoRoot, 'src', 'renderer', 'src', 'library', 'SourceLibraryIcons.tsx') const mediaItems = readMediaEvidence(mediaEvidence) const illustrationRecoveries = illustrations.map((spec) => { const item = mediaItems.find((candidate) => candidate.selector === spec.selector) assert.ok(item, `missing media item ${spec.selector}`) return { spec, ...recoverIllustration(item, spec) } }) const illustrationOutput = illustrationSource(illustrationRecoveries) const illustrationDestination = path.join( repoRoot, 'src', 'renderer', 'src', 'library', 'SourceLibraryIllustrations.tsx' ) const assetDirectory = path.join(repoRoot, 'src', 'renderer', 'src', 'library', 'assets') const manifestPath = path.join(tmpDir, 'library-icon-recovery-manifest.json') const manifest = { icon_source_file: path.relative(repoRoot, iconDestination).replaceAll('\\', '/'), icon_file_sha256: sha256(iconOutput), illustration_source_file: path.relative(repoRoot, illustrationDestination).replaceAll('\\', '/'), illustration_file_sha256: sha256(illustrationOutput), icons: iconRecords, illustrations: illustrationRecoveries.map(({ record }) => record), notes } if (process.argv.includes('--check')) { assert.equal(fs.readFileSync(iconDestination, 'utf8'), iconOutput) assert.equal(fs.readFileSync(illustrationDestination, 'utf8'), illustrationOutput) for (const { bytes, record } of illustrationRecoveries) { assert.deepEqual(fs.readFileSync(path.join(repoRoot, record.asset_file)), bytes) } assert.deepEqual(JSON.parse(fs.readFileSync(manifestPath, 'utf8')), manifest) } else { fs.mkdirSync(assetDirectory, { recursive: true }) fs.writeFileSync(iconDestination, iconOutput) fs.writeFileSync(illustrationDestination, illustrationOutput) for (const { bytes, spec } of illustrationRecoveries) { fs.writeFileSync(path.join(assetDirectory, spec.assetFile), bytes) } fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`) } process.stdout.write(`${JSON.stringify({ status: 'PASS', recovered_icons: iconRecords.length, recovered_illustrations: illustrationRecoveries.length, unresolved: Object.entries(notes) .filter(([, note]) => note.status === 'unresolved') .map(([name]) => name), icon_file: iconDestination, icon_file_sha256: manifest.icon_file_sha256, illustration_file: illustrationDestination, illustration_file_sha256: manifest.illustration_file_sha256, manifest: manifestPath }, null, 2)}\n`) } main()