Files
ycode/scripts/rea/recover-modern-conversation-labels.mjs

185 lines
10 KiB
JavaScript

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