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('') || 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))