Files
ycode/scripts/rea/recover-question-icons.mjs
T

134 lines
6.5 KiB
JavaScript

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