Merge branch 'codex/simp-unify-agent-session-id' into codex/simp-ui-identity-residue

This commit is contained in:
Tianyi Cui
2026-07-15 20:59:34 +08:00
29 changed files with 723 additions and 316 deletions

View File

@@ -294,6 +294,7 @@ function docSyncLeafGates(): Gate[] {
pnpmScript('rfc-classification', 'verify-rfc-classification', { label: 'rfc classification' }),
pnpmScript('rfc-format', 'verify-rfc-format', { label: 'rfc format' }),
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }),

View File

@@ -0,0 +1,95 @@
/** Regression tests for the bilingual cutoff and structural signature. */
import { describe, expect, it } from 'vitest'
import {
datedDocumentDate,
isIsoDate,
parseTranslationMarkdown,
parseTranslationPairingManifest,
requiresPairByDate,
translationStructureDiff,
translationStructureSignature,
} from './translation-pairing.ts'
function signature(markdown: string) {
return translationStructureSignature(parseTranslationMarkdown(markdown), 'counterpart.zh.md')
}
describe('translation pairing manifest', () => {
it('accepts a real ISO cutoff and string-array fields', () => {
expect(parseTranslationPairingManifest(JSON.stringify({
requiredSince: '2026-07-14',
required: ['README.md'],
excluded: ['docs/generated/'],
}))).toEqual({
requiredSince: '2026-07-14',
required: ['README.md'],
excluded: ['docs/generated/'],
})
})
it.each(['2026-7-14', '2026-02-29', '2026-13-01', 'not-a-date'])('rejects invalid cutoff %s', (cutoff) => {
expect(isIsoDate(cutoff)).toBe(false)
expect(() => parseTranslationPairingManifest(JSON.stringify({
requiredSince: cutoff,
required: [],
excluded: [],
}))).toThrow('requiredSince must be a valid YYYY-MM-DD date')
})
it('rejects non-string manifest arrays', () => {
expect(() => parseTranslationPairingManifest(JSON.stringify({
requiredSince: '2026-07-14',
required: [42],
excluded: [],
}))).toThrow('required must be an array of strings')
})
})
describe('date-based pairing frontier', () => {
const cutoff = '2026-07-14'
it('enforces the cutoff day and every later day, but not the preceding day', () => {
expect(requiresPairByDate('docs/rfc/2026-07-13-before.md', cutoff)).toBe(false)
expect(requiresPairByDate('docs/rfc/2026-07-14-at-cutoff.md', cutoff)).toBe(true)
expect(requiresPairByDate('docs/rfc/2026-07-15-after.md', cutoff)).toBe(true)
})
it('matches only a date at the start of the basename', () => {
expect(datedDocumentDate('docs/rfc/2026-07-14-proposal.md')).toBe('2026-07-14')
expect(datedDocumentDate('docs/release-notes-2026-07-14-alpha.md')).toBeUndefined()
expect(requiresPairByDate('docs/release-notes-2026-07-14-alpha.md', cutoff)).toBe(false)
})
})
describe('translation structural signature', () => {
it('accepts matching list kinds, starts, and item counts', () => {
const source = signature('3. One\n4. Two\n\n- A\n- B\n')
const counterpart = signature('3. 一\n4. 二\n\n- 甲\n- 乙\n')
expect(translationStructureDiff(source, counterpart)).toEqual([])
})
it('rejects an altered ordered-list start', () => {
const source = signature('3. One\n4. Two\n\n- A\n- B\n')
const counterpart = signature('1. 一\n2. 二\n\n- 甲\n- 乙\n')
expect(translationStructureDiff(source, counterpart)).toEqual([
'list (kind, start, item count) #1 diverges between the pair: "ordered:start=3:items=2" vs "ordered:start=1:items=2"',
])
})
it('rejects a missing list item', () => {
const source = signature('- A\n- B\n')
const counterpart = signature('- 甲\n')
expect(translationStructureDiff(source, counterpart)).toEqual([
'list (kind, start, item count) #1 diverges between the pair: "bullet:items=2" vs "bullet:items=1"',
])
})
it('rejects altered table row or column counts', () => {
const source = signature('| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |\n')
const counterpart = signature('| 甲 | 乙 |\n|---|---|\n| 一 | 二 |\n')
expect(translationStructureDiff(source, counterpart)).toEqual([
'table (row x column count) #1 diverges between the pair: "3x2" vs "2x2"',
])
})
})

View File

@@ -0,0 +1,164 @@
/**
* Pure parsing and structural helpers for the bilingual-document pairing
* gate. Kept separate from the CLI so cutoff and signature behavior can be
* regression-tested without reading or mutating the repository tree.
*/
import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
/** Validated shape of `scripts/translation-pairing.manifest.json`. */
export interface TranslationPairingManifest {
required: string[]
excluded: string[]
/** Date-named documents on or after this day must merge bilingual. */
requiredSince: string
}
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
const DATED_DOCUMENT = /(?:^|\/)(\d{4}-\d{2}-\d{2})-[^/]*\.md$/
/** Whether a string names one real calendar day in canonical ISO form. */
export function isIsoDate(value: string): boolean {
if (!ISO_DATE.test(value)) return false
const date = new Date(`${value}T00:00:00.000Z`)
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
}
/** Read one manifest string-array field or fail before enforcement starts. */
function stringArrayField(record: Record<string, unknown>, field: 'required' | 'excluded'): string[] {
const value = record[field]
if (!Array.isArray(value)) {
throw new Error(`translation-pairing.manifest.json: ${field} must be an array of strings`)
}
const entries: unknown[] = value
if (!entries.every((entry): entry is string => typeof entry === 'string')) {
throw new Error(`translation-pairing.manifest.json: ${field} must be an array of strings`)
}
return entries
}
/** Parse and validate the checked-in bilingual manifest. */
export function parseTranslationPairingManifest(content: string): TranslationPairingManifest {
const value: unknown = JSON.parse(content)
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error('translation-pairing.manifest.json: expected an object')
}
const record = value as Record<string, unknown>
const requiredSince = record.requiredSince
if (typeof requiredSince !== 'string' || !isIsoDate(requiredSince)) {
throw new Error(`translation-pairing.manifest.json: requiredSince must be a valid YYYY-MM-DD date; got ${JSON.stringify(requiredSince)}`)
}
return {
required: stringArrayField(record, 'required'),
excluded: stringArrayField(record, 'excluded'),
requiredSince,
}
}
/** Return the leading date of a `yyyy-mm-dd-*.md` basename, if present. */
export function datedDocumentDate(file: string): string | undefined {
return DATED_DOCUMENT.exec(file)?.[1]
}
/** Whether a date-named document falls on or after the pairing cutoff. */
export function requiresPairByDate(file: string, requiredSince: string): boolean {
const date = datedDocumentDate(file)
return date !== undefined && date >= requiredSince
}
/** The structural surface compared between the two sides of a pair. */
export interface TranslationStructureSignature {
/** Heading depths in document order (h2 -> 2). */
headings: number[]
/** Fenced code blocks verbatim: info string plus content, in order. */
code: string[]
/** Row and column count of each table, in order. */
tables: string[]
/** Kind, ordered-list start, and direct item count of each list, in order. */
lists: string[]
/** Every link target in order; the language switcher is excluded. */
links: string[]
}
/** Parse Markdown with the same GFM extensions used by the pairing gate. */
export function parseTranslationMarkdown(content: string): Nodes {
return fromMarkdown(content, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
}
/** Whether the tree contains a link to exactly `target`. */
export function linksTo(tree: Nodes, target: string): boolean {
let found = false
const visit = (node: Nodes): void => {
if (node.type === 'link' && node.url === target) found = true
if ('children' in node) for (const child of node.children) visit(child)
}
visit(tree)
return found
}
/** Collect the ordered structural signature, skipping one switcher target. */
export function translationStructureSignature(tree: Nodes, switcherTarget: string): TranslationStructureSignature {
const sig: TranslationStructureSignature = { headings: [], code: [], tables: [], lists: [], links: [] }
const visit = (node: Nodes): void => {
switch (node.type) {
case 'heading':
sig.headings.push(node.depth)
break
case 'code':
sig.code.push(`\`\`\`${node.lang ?? ''}${node.meta ? ` ${node.meta}` : ''}\n${node.value}`)
break
case 'table':
sig.tables.push(`${node.children.length}x${node.children[0]?.children.length ?? 0}`)
break
case 'list':
sig.lists.push(node.ordered
? `ordered:start=${node.start ?? 1}:items=${node.children.length}`
: `bullet:items=${node.children.length}`)
break
case 'link':
if (node.url !== switcherTarget) sig.links.push(node.url)
break
default:
// Every other node kind is prose or a container, not part of the signature.
break
}
if ('children' in node) for (const child of node.children) visit(child)
}
visit(tree)
return sig
}
/** Render a signature element for an error message, truncated for readability. */
function show(value: string | number | undefined): string {
if (value === undefined) return 'nothing'
const text = JSON.stringify(value)
return text.length > 72 ? `${text.slice(0, 72)}` : text
}
/** Return the first divergence for each structural field; empty means equal. */
export function translationStructureDiff(
source: TranslationStructureSignature,
zh: TranslationStructureSignature,
): string[] {
const out: string[] = []
const fields: [string, (string | number)[], (string | number)[]][] = [
['heading (depth)', source.headings, zh.headings],
['code block', source.code, zh.code],
['table (row x column count)', source.tables, zh.tables],
['list (kind, start, item count)', source.lists, zh.lists],
['link target', source.links, zh.links],
]
for (const [field, sourceValues, zhValues] of fields) {
const length = Math.max(sourceValues.length, zhValues.length)
for (let index = 0; index < length; index++) {
if (sourceValues[index] !== zhValues[index]) {
out.push(`${field} #${index + 1} diverges between the pair: ${show(sourceValues[index])} vs ${show(zhValues[index])}`)
break
}
}
}
return out
}

View File

@@ -0,0 +1,76 @@
/** Regression tests for the executable translation prompt contract. */
import { describe, expect, it } from 'vitest'
import {
parseTranslationResponse,
renderTranslationPrompt,
renderTranslationResponse,
} from './translation-prompt.ts'
const document = `# Wrapper
## 模板正文
\`\`\`\`text
{{source_lang}} to {{target_lang}}
{{translation_rules}}
{{terminology}}
[English]({{source_filename}}) | [中文]({{source_filename_zh}})
\`\`\`\`
`
describe('translation prompt rendering', () => {
it('renders every supported placeholder without recursively rewriting injected rules', () => {
const rendered = renderTranslationPrompt(document, {
sourceLanguage: 'English',
sourceFilename: 'guide.md',
translationRules: 'A literal {{source_lang}} in injected rules.',
terminology: '| English | 中文 |',
})
expect(rendered).toContain('English to Chinese')
expect(rendered).toContain('A literal {{source_lang}} in injected rules.')
expect(rendered).toContain('[English](guide.md) | [中文](guide.zh.md)')
})
it('rejects a filename whose suffix contradicts the source language', () => {
expect(() => renderTranslationPrompt(document, {
sourceLanguage: 'Chinese',
sourceFilename: 'guide.md',
translationRules: 'rules',
terminology: 'terms',
})).toThrow('does not match source language Chinese')
})
it('rejects malformed template placeholders before injecting rule contents', () => {
expect(() => renderTranslationPrompt(document.replace('{{source_lang}}', '{{source-lang}}'), {
sourceLanguage: 'English',
sourceFilename: 'guide.md',
translationRules: 'A literal {{source_lang}} in injected rules.',
terminology: '| English | 中文 |',
})).toThrow('template contains malformed placeholder syntax')
})
})
describe('translation response XML', () => {
it('round-trips Markdown and the CDATA terminator', () => {
const response = {
translation: '# Draft\n\nA ]]> marker.',
review: '- [Tone] Fixed.',
final: '# Final\n\nA ]]> marker.',
}
expect(parseTranslationResponse(renderTranslationResponse(response))).toEqual(response)
})
it('rejects missing, reordered, nested, attributed, or non-CDATA children', () => {
expect(() => parseTranslationResponse('<dsh-translation-response version="1"/>')).toThrow('translation, review, and final')
expect(() => parseTranslationResponse('<dsh-translation-response version="1"><review><![CDATA[x]]></review></dsh-translation-response>'))
.toThrow('expected translation, got review')
expect(() => parseTranslationResponse(renderTranslationResponse({ translation: 'x', review: 'y', final: 'z' })
.replace('<translation><![CDATA[x]]></translation>', '<translation><b><![CDATA[x]]></b></translation>')))
.toThrow('nested element b is not allowed')
expect(() => parseTranslationResponse(renderTranslationResponse({ translation: 'x', review: 'y', final: 'z' }).replace('<review>', '<review lang="en">')))
.toThrow('review must not have attributes')
expect(() => parseTranslationResponse(renderTranslationResponse({ translation: 'x', review: 'y', final: 'z' }).replace('<![CDATA[x]]>', 'x')))
.toThrow('all response field content must be inside CDATA')
})
})

View File

@@ -0,0 +1,171 @@
/**
* Executable renderer and strict response parser for the committed
* documentation-translation prompt contract.
*/
import { basename } from 'node:path'
import { SaxesParser } from 'saxes'
/** Placeholder names supported by the committed translation prompt. */
export const TRANSLATION_PROMPT_PLACEHOLDERS = [
'source_lang',
'target_lang',
'translation_rules',
'terminology',
'source_filename',
'source_filename_zh',
] as const
type TranslationPromptPlaceholder = (typeof TRANSLATION_PROMPT_PLACEHOLDERS)[number]
/** Languages accepted by the bidirectional prompt. */
type TranslationLanguage = 'English' | 'Chinese'
/** Inputs that vary for one rendered translation request. */
export interface TranslationPromptInput {
sourceLanguage: TranslationLanguage
/** Source basename, including `.md` or `.zh.md`. */
sourceFilename: string
/** Complete current `translation-rules.md` contents. */
translationRules: string
/** Complete current `terminology.md` contents. */
terminology: string
}
/** Parsed contents of the three-element XML response. */
export interface TranslationResponse {
translation: string
review: string
final: string
}
const PLACEHOLDER = /{{([a-z_]+)}}/g
const TEMPLATE_OPEN = '## 模板正文\n\n````text\n'
const TEMPLATE_CLOSE = '\n````'
const RESPONSE_CHILDREN = ['translation', 'review', 'final'] as const
/** Extract the machine-consumed text fence from `translation-prompt.md`. */
function extractTranslationPrompt(document: string): string {
const start = document.indexOf(TEMPLATE_OPEN)
if (start === -1) throw new Error('translation prompt: missing `## 模板正文` text fence')
const contentStart = start + TEMPLATE_OPEN.length
const end = document.indexOf(TEMPLATE_CLOSE, contentStart)
if (end === -1) throw new Error('translation prompt: missing closing four-backtick fence')
return document.slice(contentStart, end)
}
/** Read the placeholder names documented in the prompt's contract table. */
export function documentedTranslationPromptPlaceholders(document: string): string[] {
const preambleEnd = document.indexOf(TEMPLATE_OPEN)
if (preambleEnd === -1) throw new Error('translation prompt: missing template body')
return [...document.slice(0, preambleEnd).matchAll(/^\| `{{([a-z_]+)}}` \|/gm)].map(match => match[1] ?? '')
}
/** Render one system prompt from the checked-in template and canonical rules. */
export function renderTranslationPrompt(document: string, input: TranslationPromptInput): string {
if (basename(input.sourceFilename) !== input.sourceFilename) {
throw new Error(`translation prompt: sourceFilename must be a basename; got ${JSON.stringify(input.sourceFilename)}`)
}
const sourceIsChinese = input.sourceFilename.endsWith('.zh.md')
if (input.sourceLanguage === 'Chinese' ? !sourceIsChinese : sourceIsChinese || !input.sourceFilename.endsWith('.md')) {
throw new Error(`translation prompt: ${input.sourceFilename} does not match source language ${input.sourceLanguage}`)
}
const targetLanguage: TranslationLanguage = input.sourceLanguage === 'English' ? 'Chinese' : 'English'
const sourceFilenameZh = sourceIsChinese ? input.sourceFilename : input.sourceFilename.replace(/\.md$/, '.zh.md')
const values: Record<TranslationPromptPlaceholder, string> = {
source_lang: input.sourceLanguage,
target_lang: targetLanguage,
translation_rules: input.translationRules,
terminology: input.terminology,
source_filename: input.sourceFilename,
source_filename_zh: sourceFilenameZh,
}
const template = extractTranslationPrompt(document)
const placeholderFreeTemplate = template.replace(PLACEHOLDER, '')
if (placeholderFreeTemplate.includes('{{') || placeholderFreeTemplate.includes('}}')) {
throw new Error('translation prompt: template contains malformed placeholder syntax')
}
const names = [...template.matchAll(PLACEHOLDER)].map(match => match[1] ?? '')
const unknown = names.filter(name => !TRANSLATION_PROMPT_PLACEHOLDERS.includes(name as TranslationPromptPlaceholder))
if (unknown.length > 0) throw new Error(`translation prompt: unsupported placeholder(s): ${[...new Set(unknown)].join(', ')}`)
const missing = TRANSLATION_PROMPT_PLACEHOLDERS.filter(name => !names.includes(name))
if (missing.length > 0) throw new Error(`translation prompt: template does not use required placeholder(s): ${missing.join(', ')}`)
return template.replace(PLACEHOLDER, (_token, name: string) => values[name as TranslationPromptPlaceholder])
}
/** Escape one value so it remains byte-identical inside an XML CDATA field. */
function escapeTranslationCdata(value: string): string {
return value.replaceAll(']]>', ']]]]><![CDATA[>')
}
/** Serialize a response using the exact XML wire contract in the prompt. */
export function renderTranslationResponse(response: TranslationResponse): string {
return [
'<dsh-translation-response version="1">',
`<translation><![CDATA[${escapeTranslationCdata(response.translation)}]]></translation>`,
`<review><![CDATA[${escapeTranslationCdata(response.review)}]]></review>`,
`<final><![CDATA[${escapeTranslationCdata(response.final)}]]></final>`,
'</dsh-translation-response>',
].join('\n')
}
/** Parse and validate the exact XML response shape emitted by the model. */
export function parseTranslationResponse(xml: string): TranslationResponse {
const values: TranslationResponse = { translation: '', review: '', final: '' }
const stack: string[] = []
const cdataFields = new Set<string>()
let rootSeen = false
let childIndex = 0
const fail = (message: string): never => {
throw new Error(`translation response: ${message}`)
}
const parser = new SaxesParser({ xmlns: false })
parser.on('opentag', (tag) => {
if (stack.length === 0) {
if (rootSeen) fail('contains more than one root element')
if (tag.name !== 'dsh-translation-response') fail(`expected dsh-translation-response root, got ${tag.name}`)
const attributes = Object.keys(tag.attributes)
if (attributes.length !== 1 || tag.attributes.version !== '1') fail('root must have only version="1"')
rootSeen = true
} else if (stack.length === 1) {
const expected = RESPONSE_CHILDREN[childIndex]
if (tag.name !== expected) fail(`expected ${expected ?? 'no more children'}, got ${tag.name}`)
if (Object.keys(tag.attributes).length !== 0) fail(`${tag.name} must not have attributes`)
childIndex++
} else {
fail(`nested element ${tag.name} is not allowed`)
}
stack.push(tag.name)
})
parser.on('text', (value) => {
if (stack.length <= 1 && value.trim() === '') return
fail('all response field content must be inside CDATA')
})
parser.on('cdata', (value) => {
const field = stack.at(-1)
if (field === undefined || !RESPONSE_CHILDREN.includes(field as (typeof RESPONSE_CHILDREN)[number])) {
fail('CDATA is allowed only inside translation, review, or final')
}
const key = field as (typeof RESPONSE_CHILDREN)[number]
values[key] += value
cdataFields.add(key)
})
parser.on('closetag', (tag) => {
const expected = stack.pop()
if (expected !== tag.name) fail(`closing ${tag.name} does not match ${expected ?? 'nothing'}`)
})
parser.on('comment', () => fail('comments are not allowed'))
parser.on('doctype', () => fail('doctypes are not allowed'))
parser.on('processinginstruction', () => fail('processing instructions are not allowed'))
parser.on('error', error => fail(`invalid XML: ${error.message}`))
parser.write(xml).close()
if (childIndex !== RESPONSE_CHILDREN.length) fail('translation, review, and final must each appear exactly once and in order')
for (const field of RESPONSE_CHILDREN) {
if (!cdataFields.has(field)) fail(`${field} must contain a CDATA section`)
}
return values
}

View File

@@ -10,10 +10,15 @@
import { createHash } from 'node:crypto'
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
import { basename, join, resolve, sep } from 'node:path'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import {
datedDocumentDate,
linksTo,
parseTranslationMarkdown,
parseTranslationPairingManifest,
requiresPairByDate,
translationStructureDiff,
translationStructureSignature,
} from './translation-pairing.ts'
const root = resolve(import.meta.dirname, '..')
const listMode = process.argv.includes('--list')
@@ -22,14 +27,7 @@ const writeMode = process.argv.includes('--write')
/** Scope of the bilingual contract: the root README, the docs tree, and the Python SDK tree. */
const SCOPE_PATTERNS = ['README.md', 'README.zh.md', 'README.i18n.yaml', 'docs/**/*.md', 'docs/**/*.i18n.yaml', 'python/**/*.md', 'python/**/*.i18n.yaml']
/** The enforcement frontier and the never-paired set (docs/i18n/README.md § Scope). */
interface Manifest {
required: string[]
excluded: string[]
/** Date-named documents (yyyy-mm-dd-*.md, i.e. RFCs) dated on/after this day must merge bilingual. */
requiredSince: string
}
const manifest = JSON.parse(readFileSync(join(root, 'scripts/translation-pairing.manifest.json'), 'utf8')) as Manifest
const manifest = parseTranslationPairingManifest(readFileSync(join(root, 'scripts/translation-pairing.manifest.json'), 'utf8'))
/**
* An excluded entry ending in `/` excludes the whole directory. The trailing
@@ -81,98 +79,6 @@ function renderMeta(source: string, sourceHash: string, zh: string, zhHash: stri
].join('\n')
}
/**
* The structural signature the two sides must share, as ordered sequences so
* a swap or a level change is caught, not just a count change. Prose is
* deliberately absent: the gate checks shape, never wording.
*/
interface Signature {
/** Heading depths in document order (h2 → 2). */
headings: number[]
/** Fenced code blocks verbatim: info string + content, in order. */
code: string[]
/** Column count of each table, in order. */
tables: number[]
/** Each list's kind (ordered vs bullet), in order. */
lists: string[]
/** Every link target in order, the language switcher's excluded. */
links: string[]
}
/** Whether the tree contains a link to exactly `target` (the switcher check). */
function linksTo(tree: Nodes, target: string): boolean {
let found = false
const visit = (node: Nodes): void => {
if (node.type === 'link' && node.url === target) found = true
if ('children' in node) for (const child of node.children) visit(child)
}
visit(tree)
return found
}
/** Collect the structural signature, skipping links to `switcherTarget`. */
function signatureOf(tree: Nodes, switcherTarget: string): Signature {
const sig: Signature = { headings: [], code: [], tables: [], lists: [], links: [] }
const visit = (node: Nodes): void => {
switch (node.type) {
case 'heading':
sig.headings.push(node.depth)
break
case 'code':
sig.code.push(`\`\`\`${node.lang ?? ''}${node.meta ? ` ${node.meta}` : ''}\n${node.value}`)
break
case 'table':
sig.tables.push(node.children[0]?.children.length ?? 0)
break
case 'list':
sig.lists.push(node.ordered ? 'ordered' : 'bullet')
break
case 'link':
if (node.url !== switcherTarget) sig.links.push(node.url)
break
default:
// Every other node kind is prose or container — not part of the signature.
break
}
if ('children' in node) for (const child of node.children) visit(child)
}
visit(tree)
return sig
}
/** Render a signature element for an error message, truncated for readability. */
function show(value: string | number | undefined): string {
if (value === undefined) return 'nothing'
const text = JSON.stringify(value)
return text.length > 72 ? `${text.slice(0, 72)}` : text
}
/** First divergence between two signatures, as messages; empty when identical. */
function signatureDiff(source: Signature, zh: Signature): string[] {
const out: string[] = []
const fields: [string, (string | number)[], (string | number)[]][] = [
['heading (depth)', source.headings, zh.headings],
['code block', source.code, zh.code],
['table (column count)', source.tables, zh.tables],
['list (kind)', source.lists, zh.lists],
['link target', source.links, zh.links],
]
for (const [field, s, z] of fields) {
const length = Math.max(s.length, z.length)
for (let i = 0; i < length; i++) {
if (s[i] !== z[i]) {
out.push(`${field} #${i + 1} diverges between the pair: ${show(s[i])} vs ${show(z[i])}`)
break
}
}
}
return out
}
function parse(content: string): Nodes {
return fromMarkdown(content, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
}
// Enumerate the scope once.
const files = new Set<string>()
for (const pattern of SCOPE_PATTERNS) {
@@ -218,14 +124,13 @@ for (const req of manifest.required) {
// 2. Date-named documents (RFCs) dated on/after the requiredSince cutoff merge
// bilingual: a new RFC lands with its pair or not at all. Deterministic from
// the filename alone — no git history, so it holds on shallow CI checkouts.
const DATED = /(?:^|\/)(\d{4}-\d{2}-\d{2})-[^/]*\.md$/
for (const source of sources) {
if (isExcluded(source)) continue
const dated = DATED.exec(source)
if (!dated?.[1] || dated[1] < manifest.requiredSince) continue
const date = datedDocumentDate(source)
if (!requiresPairByDate(source, manifest.requiredSince) || date === undefined) continue
const { zh } = pairPaths(source)
if (!existsSync(join(root, zh))) {
errors.push(`${source}: dated ${dated[1]} — documents dated on/after ${manifest.requiredSince} merge bilingual (docs/i18n/README.md); add the counterpart and record the pair`)
errors.push(`${source}: dated ${date} — documents dated on/after ${manifest.requiredSince} merge bilingual (docs/i18n/README.md); add the counterpart and record the pair`)
state.set(source, 'missing')
}
}
@@ -273,15 +178,18 @@ for (const source of [...pairAnchors].sort()) {
continue
}
const sourceTree = parse(sourceContent.toString('utf8'))
const zhTree = parse(zhContent.toString('utf8'))
const sourceTree = parseTranslationMarkdown(sourceContent.toString('utf8'))
const zhTree = parseTranslationMarkdown(zhContent.toString('utf8'))
if (!linksTo(zhTree, basename(source))) {
errors.push(`${zh}: missing language switcher — no link to ${basename(source)}`)
}
if (!linksTo(sourceTree, basename(zh))) {
errors.push(`${source}: missing language switcher — no link back to ${basename(zh)}`)
}
for (const divergence of signatureDiff(signatureOf(sourceTree, basename(zh)), signatureOf(zhTree, basename(source)))) {
for (const divergence of translationStructureDiff(
translationStructureSignature(sourceTree, basename(zh)),
translationStructureSignature(zhTree, basename(source)),
)) {
errors.push(`${source}${zh}: ${divergence}`)
}
if (!state.has(source)) state.set(source, 'ok')
@@ -297,8 +205,7 @@ if (listMode) {
const rows = [...state.entries()].sort((a, b) => order[a[1]] - order[b[1]] || a[0].localeCompare(b[0]))
for (const [file, status] of rows) {
const required = manifest.required.includes(file)
const date = DATED.exec(file)?.[1]
const tag = required ? ' (required)' : date && date >= manifest.requiredSince ? ' (required by date)' : ' (backlog)'
const tag = required ? ' (required)' : requiresPairByDate(file, manifest.requiredSince) ? ' (required by date)' : ' (backlog)'
console.log(`${status.padEnd(11)} ${file}${status === 'missing' ? tag : ''}`)
}
const counts = { 'ok': 0, 'out-of-sync': 0, 'missing': 0 }

View File

@@ -0,0 +1,56 @@
/** Verify that the committed translation prompt renders and parses as documented. */
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import {
documentedTranslationPromptPlaceholders,
parseTranslationResponse,
renderTranslationPrompt,
renderTranslationResponse,
TRANSLATION_PROMPT_PLACEHOLDERS,
} from './translation-prompt.ts'
const root = resolve(import.meta.dirname, '..')
function read(path: string): string {
return readFileSync(join(root, path), 'utf8')
}
try {
const document = read('docs/i18n/translation-prompt.md')
const translationRules = read('docs/i18n/translation-rules.md')
const terminology = read('docs/i18n/terminology.md')
const documented = documentedTranslationPromptPlaceholders(document)
if (documented.join('\n') !== TRANSLATION_PROMPT_PLACEHOLDERS.join('\n')) {
throw new Error(`placeholder table must list exactly: ${TRANSLATION_PROMPT_PLACEHOLDERS.join(', ')}`)
}
const englishSource = renderTranslationPrompt(document, {
sourceLanguage: 'English',
sourceFilename: 'example.md',
translationRules,
terminology,
})
const chineseSource = renderTranslationPrompt(document, {
sourceLanguage: 'Chinese',
sourceFilename: 'example.zh.md',
translationRules,
terminology,
})
if (!englishSource.includes('[English](example.md) | 中文')) throw new Error('English-source render does not carry the Chinese switcher instruction')
if (!chineseSource.includes('English | [中文](example.zh.md)')) throw new Error('Chinese-source render does not carry the English switcher instruction')
const example = /```xml\n([\s\S]*?)\n```/.exec(englishSource)?.[1]
if (example === undefined) throw new Error('rendered prompt has no XML response example')
parseTranslationResponse(example)
const roundTrip = { translation: 'first ]]> pass', review: '- [None] No corrections.', final: 'final ]]> text' }
const parsed = parseTranslationResponse(renderTranslationResponse(roundTrip))
if (JSON.stringify(parsed) !== JSON.stringify(roundTrip)) throw new Error('CDATA split rule does not round-trip response content')
console.log('verify-translation-prompt: both directions render and the XML response contract parses.')
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`verify-translation-prompt: ${message}`)
process.exit(1)
}