Merge remote-tracking branch 'origin/master' into worktree/pr343-retarget-latest-master

# Conflicts:
#	docs/i18n/README.i18n.yaml
#	docs/i18n/README.zh.md
This commit is contained in:
Tianyi Cui
2026-07-23 23:43:08 +08:00
360 changed files with 12190 additions and 1335 deletions

View File

@@ -7,7 +7,7 @@ import { spawn } from 'node:child_process'
// Each UI's node invocation matches its base demo script plus the overlay config.
const UIS = new Map([
['tui', ['--expose-internals', '--import', 'tsx', 'packages/examples/tui-demo/src/bin.ts', 'examples/tui-agent/code-mode.cordis.yml']],
['tui', ['--import', 'tsx', 'packages/examples/tui-demo/src/bin.ts', 'examples/tui-agent/code-mode.cordis.yml']],
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/code-mode.cordis.yml']],
])

View File

@@ -0,0 +1,23 @@
<translation>
---
layout: doc
---
# 快照说明
agent智能体执行一个步骤。
</translation>
<review>
- 无修正
</review>
<final>
---
layout: doc
---
# 快照说明
agent智能体执行一个步骤。
</final>

View File

@@ -0,0 +1,7 @@
---
layout: doc
---
# Snapshot note
The agent performs one step.

View File

@@ -94,6 +94,7 @@ export const LINK_MAP: Record<string, string> = {
CreateSessionOptions: 'persistence.md',
SessionHeader: 'persistence.md',
SessionLocation: 'persistence.md',
SessionPersistenceSnapshot: 'persistence.md',
ConfinedArgv: 'sandbox.md',
SandboxExecutionPolicy: 'sandbox.md',
SandboxMode: 'sandbox.md',
@@ -120,11 +121,20 @@ export const LINK_MAP: Record<string, string> = {
TurnTrigger: 'session.md',
SessionEventReadRequest: 'session-query.md',
SessionEventRecord: 'session-query.md',
SessionEventResultFilter: 'session-query.md',
SessionEventSearchDocument: 'session-query.md',
SessionEventSearchHit: 'session-query.md',
SessionEventSearchRequest: 'session-query.md',
SessionEventTrace: 'session-query.md',
SessionEventTraceRequest: 'session-query.md',
SessionEventWindow: 'session-query.md',
SessionLineageTrace: 'session-query.md',
SessionRecord: 'session-query.md',
SessionResultFilter: 'session-query.md',
SessionSearchExecContext: 'session-query.md',
SessionSearchHit: 'session-query.md',
SessionSearchPage: 'session-query.md',
SessionSearchRequest: 'session-query.md',
SessionTitleProvider: 'session-title.md',
SessionTitleSnapshot: 'session-title.md',
SkillDefinition: 'skills.md',

View File

@@ -112,7 +112,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session',
title: 'In-memory session store',
mode: 'core',
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'subagent-inprocess'],
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
@@ -129,16 +129,17 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Durable session persistence seam',
mode: 'seam',
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'acp', 'session-query'],
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'acp', 'session-query', 'session-query-sqlite'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
key: 'sessionQuery',
pkg: 'session-query',
title: 'Exact session-history reads and traces',
title: 'Session reads, traces, filters, and search',
mode: 'seam',
implementations: ['session-query-sqlite'],
consumers: ['session-reference'],
note: 'Resolves live and optional persisted logs into one logical corpus for exact reads and relationship traces.',
note: 'The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations on the same service.',
},
{
key: 'sessionReferences',
@@ -978,7 +979,7 @@ function renderToolPipeline(): string {
const maintenance = 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs'
return [
...generatedHeader('Tool Execution Pipeline'),
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering fit without changing the loop. The transformable extension points are the `tools/pre-execute`, `tools/execute`, and `tools/post-execute` waterfalls; monotonic guards and `tools/result` are the owner-enforced boundaries around them.',
'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering fit without changing the loop. The transformable extension points are the `tools/pre-execute`, `tools/execute`, and `tools/post-execute` waterfalls; monotonic guards, definition-owned `finalizeContent`, and `tools/result` are the owner-enforced boundaries around them.',
'',
'```mermaid',
'flowchart TD',
@@ -994,6 +995,8 @@ function renderToolPipeline(): string {
` fsGate["${mermaidCode('fs/write-intent')} or ${mermaidCode('fs/edit-intent')}<br/>tool-fs mutations only"]`,
` owned["Tool-owned session events<br/>${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/code-dispatch')}"]`,
` post["${mermaidCode('tools/post-execute')} waterfall<br/>accept, block, replace, add context"]`,
' normalized["Registry outer normalization<br/>pipeline/result snapshot throws become isError"]',
' finalize["ToolDefinition.finalizeContent<br/>last content-only invariant"]',
` final["${mermaidCode('tools/result')} synchronous notification<br/>frozen authoritative outcome"]`,
' context["Active-batch additionalContexts FIFO<br/>context/message after recorded tool results"]',
` toolResult["Session event: ${mermaidCode('tool/result')}<br/>single model-facing outcome"]`,
@@ -1005,25 +1008,32 @@ function renderToolPipeline(): string {
' pre -->|allow| guards',
' guards -->|allow| around',
' guards -->|deny| denied',
' guards -.->|throw| normalized',
' around --> toolBody',
' pre -->|deny| denied',
' pre -->|ask| approval',
' approval -->|allowed-once| guards',
' approval -->|rejected, cancelled, unavailable| denied',
' approval -.->|throw| normalized',
' denied --> post',
' pre -.->|throw| normalized',
' toolBody --> fsGate',
' fsGate --> toolBody',
' toolBody --> owned',
' toolBody --> around',
' around --> post',
' post --> final',
' around -.->|wrapper throws| normalized',
' post -.->|throw| normalized',
' post --> finalize',
' normalized --> finalize',
' finalize --> final',
' final --> toolResult',
' toolResult --> presentResult',
' toolResult --> allResults',
' allResults --> context',
'```',
'',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`, while `tools/result` observes the immutable outcome after transforms, lossless-JSON validation, and outer error normalization. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. Code Mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, surface denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.',
'',
...maintenanceFooter(maintenance),
].join('\n')

View File

@@ -379,9 +379,9 @@ function coverageGate(): Gate {
})
}
// The snapshot suite boots the example bins in `lib` mode (built artifact under plain Node,
// plugins via real exports) — CI and check-all already build, so they exercise what ships rather
// than the tsx/source path dev uses. It therefore waits on `build`.
// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node,
// plugins via real exports); repository-script snapshots execute their real source entry path.
// CI and check-all already build before either class runs, so the suite waits on `build`.
function snapshotGate(): Gate {
return pnpmScript('snapshot', 'test:snapshot', {
env: { DSH_EXAMPLE_MODE: 'lib' },

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
/** Runnable keyless snapshot for the assembled translation request and consumed response. */
import { execFile } from 'node:child_process'
import { access, mkdir, writeFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { promisify } from 'node:util'
import { describe, expect, it } from 'vitest'
const execFileAsync = promisify(execFile)
const root = resolve(import.meta.dirname, '..')
const expected = join(root, 'scripts/snapshots/translation-prompt-v4/request-response.expected.json')
const refreshing = process.env.DSH_SNAPSHOT === 'record' || process.env.DSH_SNAPSHOT === 'refresh'
describe('translation prompt runnable snapshot', () => {
it('assembles the reviewed examples and consumes a recorded new-pair response', async () => {
const { stdout, stderr } = await execFileAsync(process.execPath, [
join(root, 'scripts/verify-translation-prompt.ts'),
'--snapshot',
], { cwd: root, maxBuffer: 4 * 1024 * 1024 })
expect(stderr).toBe('')
expect(() => {
JSON.parse(stdout)
}).not.toThrow()
if (refreshing) {
await mkdir(dirname(expected), { recursive: true })
await writeFile(expected, stdout)
} else {
await access(expected)
}
await expect(stdout).toMatchFileSnapshot(expected)
})
})

View File

@@ -1,76 +1,200 @@
/** Regression tests for the executable translation prompt contract. */
/** Unit tests for the prompt-v4 renderer and three-section response parser. */
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
consumeTranslationResponse,
parseTranslationResponse,
renderTranslationPrompt,
renderTranslationRequest,
renderTranslationResponse,
} from './translation-prompt.ts'
const document = `# Wrapper
## 模板正文
\`\`\`\`text
{{source_lang}} to {{target_lang}}
{{translation_rules}}
{{terminology}}
[English]({{source_filename}}) | [中文]({{source_filename_zh}})
\`\`\`\`
`
const root = resolve(import.meta.dirname, '..')
const document = readFileSync(join(root, 'docs/i18n/translation-prompt.md'), 'utf8')
const terminology = '| English | 中文 |\n|---|---|\n| agent | agent |'
describe('translation prompt rendering', () => {
it('renders every supported placeholder without recursively rewriting injected rules', () => {
const rendered = renderTranslationPrompt(document, {
it('renders both directions with every placeholder resolved', () => {
const en = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
expect(en).toContain('from English to Chinese')
expect(en).toContain(terminology)
expect(en).not.toContain('{{')
expect(en).toContain('plain source stays plain (必须)')
expect(en).toContain('When the target language is English, use the "English" column without a Chinese gloss')
expect(en).toContain('for a Chinese target, use an established Chinese rendering')
expect(en).toContain('for an English target, use the established English technical term')
expect(en).toContain('does an English target use established English terminology')
expect(en).toContain('The parser removes exactly one framing escape')
const zh = renderTranslationPrompt(document, { sourceLanguage: 'Chinese', sourceFilename: 'guide.zh.md', terminology })
expect(zh).toContain('from Chinese to English')
})
it('rejects a template with unknown or missing placeholders', () => {
const alien = document.replaceAll('{{terminology}}', '{{terms_prompt}}')
expect(() => renderTranslationPrompt(alien, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })).toThrow(/unsupported placeholder/)
const missing = document.replaceAll('{{terminology}}', '')
expect(() => renderTranslationPrompt(missing, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })).toThrow(/required placeholder/)
})
it('rejects unmatched placeholder delimiters', () => {
for (const delimiter of ['{{', '}}']) {
const malformed = document.replace('Your task is to translate', `Your task ${delimiter} is to translate`)
expect(() => renderTranslationPrompt(malformed, {
sourceLanguage: 'English',
sourceFilename: 'guide.md',
terminology,
})).toThrow(/malformed placeholder syntax/)
}
})
it('assembles bare few-shot turns before the real source document', () => {
const request = renderTranslationRequest(document, {
sourceLanguage: 'English',
sourceFilename: 'guide.md',
translationRules: 'A literal {{source_lang}} in injected rules.',
terminology: '| English | 中文 |',
sourceDocument: '# Guide\n\nNew source.',
terminology,
examples: [{ english: '# Example\n\nEnglish.', chinese: '# 示例\n\n中文。' }],
})
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)')
})
expect(request.targetFilename).toBe('guide.zh.md')
expect(request.messages.map(message => message.role)).toEqual(['system', 'user', 'assistant', 'user'])
expect(request.messages.slice(1).map(message => message.content)).toEqual([
'# Example\n\nEnglish.',
'# 示例\n\n中文。',
'# Guide\n\nNew source.',
])
it('rejects a filename whose suffix contradicts the source language', () => {
expect(() => renderTranslationPrompt(document, {
const reverse = renderTranslationRequest(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')
sourceFilename: 'guide.zh.md',
sourceDocument: '# 指南\n\n新源文。',
terminology,
examples: [{ english: '# Example\n\nEnglish.', chinese: '# 示例\n\n中文。' }],
})
expect(reverse.targetFilename).toBe('guide.md')
expect(reverse.messages.slice(1).map(message => message.content)).toEqual([
'# 示例\n\n中文。',
'# Example\n\nEnglish.',
'# 指南\n\n新源文。',
])
})
})
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.',
}
describe('translation response sections', () => {
it('round-trips Markdown bodies', () => {
const response = { translation: '# 标题\n\n正文 **加粗**。', review: '- [Tone] 修正一处。\n- 无修正', final: '# 标题\n\n定稿。' }
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')
it('tolerates a fenced xml wrapper around the whole response', () => {
const fenced = '```xml\n<translation>\nA\n</translation>\n\n<review>\n- 无修正\n</review>\n\n<final>\nA\n</final>\n```'
expect(parseTranslationResponse(fenced).final).toBe('A')
})
it('keeps an inline close tag inside prose from terminating the section', () => {
const doc = { translation: 'the wire format uses </translation> as its close tag', review: '- 无修正', final: 'F' }
expect(parseTranslationResponse(renderTranslationResponse(doc))).toEqual(doc)
})
it('round-trips wrapper-tag lines inside Markdown bodies', () => {
const doc = {
translation: '```xml\n</translation>\n```',
review: '- [Structure] Preserved `<final>` on its own line.',
final: 'literal delimiters\n</final>\n\\</final>',
}
const rendered = renderTranslationResponse(doc)
expect(parseTranslationResponse(rendered)).toEqual(doc)
expect(() => parseTranslationResponse(rendered.replace('\\</translation>', '</translation>'))).toThrow(/duplicate <translation>/)
})
it('rejects a duplicate section appearing before final', () => {
const early = '<translation>\nA\n</translation>\n<translation>\nB\n</translation>\n<review>\nR\n</review>\n<final>\nF\n</final>'
expect(() => parseTranslationResponse(early)).toThrow(/duplicate <translation>/)
})
it('rejects missing, unterminated, or duplicated sections', () => {
expect(() => parseTranslationResponse('<translation>\nA\n</translation>')).toThrow(/missing or unterminated <review>/)
expect(() => parseTranslationResponse('<translation>\nA')).toThrow(/missing or unterminated <translation>/)
const dup = '<translation>\nA\n</translation>\n<review>\nR\n</review>\n<final>\nF\n</final>\n<final>\nG\n</final>'
expect(() => parseTranslationResponse(dup)).toThrow(/duplicate <final>/)
expect(() => parseTranslationResponse(`${renderTranslationResponse({ translation: 'A', review: 'R', final: 'F' })}\nstray`))
.toThrow(/content is not allowed outside/)
})
it('inserts or corrects the target switcher after parsing a new-pair response', () => {
const response = renderTranslationResponse({
translation: '# 指南\n\n初稿。',
review: '- 无修正',
final: '# 指南\n\nEnglish | [中文](guide.zh.md)\n\n定稿。',
})
expect(consumeTranslationResponse(response, { sourceLanguage: 'English', sourceFilename: 'guide.md' }).final).toBe([
'# 指南',
'',
'[English](guide.md) | 中文',
'',
'定稿。',
'',
].join('\n'))
})
it('preserves YAML frontmatter before inserting the target switcher', () => {
const response = renderTranslationResponse({
translation: '# 指南\n\n初稿。',
review: '- 无修正',
final: [
'---',
'layout: home',
'---',
'',
'# 指南',
'',
'定稿。',
].join('\n'),
})
expect(consumeTranslationResponse(response, { sourceLanguage: 'English', sourceFilename: 'guide.md' }).final).toBe([
'---',
'layout: home',
'---',
'',
'# 指南',
'',
'[English](guide.md) | 中文',
'',
'定稿。',
'',
].join('\n'))
})
it('rejects unterminated YAML frontmatter before the target H1', () => {
const response = renderTranslationResponse({
translation: '# 指南\n\n初稿。',
review: '- 无修正',
final: '---\nlayout: home\n\n# 指南\n\n定稿。',
})
expect(() => consumeTranslationResponse(response, {
sourceLanguage: 'English',
sourceFilename: 'guide.md',
})).toThrow(/unterminated YAML frontmatter/)
})
it('rejects a source filename that contradicts the translation direction', () => {
expect(() => renderTranslationPrompt(document, {
sourceLanguage: 'Chinese',
sourceFilename: 'guide.md',
terminology,
})).toThrow(/does not match source language Chinese/)
})
it('inserts the English target switcher for a Chinese source', () => {
const response = renderTranslationResponse({
translation: '# Guide\n\nDraft.',
review: '- [None] No corrections.',
final: '# Guide\n\nFinal.',
})
expect(consumeTranslationResponse(response, {
sourceLanguage: 'Chinese',
sourceFilename: 'guide.zh.md',
}).final).toContain('\n\nEnglish | [中文](guide.zh.md)\n\n')
})
})

View File

@@ -1,20 +1,18 @@
/**
* Executable renderer and strict response parser for the committed
* documentation-translation prompt contract.
* Executable renderer and response parser for the committed
* documentation-translation prompt contract (prompt-v4).
*
* The v4 contract: three placeholders (`source_lang`, `target_lang`,
* `terminology`), whole-document translation, and a three-section response
* (`<translation>`, `<review>`, `<final>` in order, bare XML tags with raw
* Markdown bodies). The pipeline retains filename context outside the model
* request and corrects the final language switcher after parsing.
*/
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
export const TRANSLATION_PROMPT_PLACEHOLDERS = ['source_lang', 'target_lang', 'terminology'] as const
type TranslationPromptPlaceholder = (typeof TRANSLATION_PROMPT_PLACEHOLDERS)[number]
@@ -26,13 +24,35 @@ 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. */
/** One reviewed whole-document example available in both directions. */
export interface TranslationExample {
english: string
chinese: string
}
/** Inputs for one complete model request. */
export interface TranslationRequestInput extends TranslationPromptInput {
sourceDocument: string
examples: TranslationExample[]
}
/** One model message in the provider-neutral translation request. */
interface TranslationMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
/** Fully assembled request plus the filename that receives the final body. */
export interface TranslationRequest {
targetFilename: string
messages: TranslationMessage[]
}
/** Parsed contents of the three-section response. */
export interface TranslationResponse {
translation: string
review: string
@@ -42,7 +62,35 @@ export interface TranslationResponse {
const PLACEHOLDER = /{{([a-z_]+)}}/g
const TEMPLATE_OPEN = '## 模板正文\n\n````text\n'
const TEMPLATE_CLOSE = '\n````'
const RESPONSE_CHILDREN = ['translation', 'review', 'final'] as const
const RESPONSE_SECTIONS = ['translation', 'review', 'final'] as const
const RESPONSE_DELIMITERS = new Set(RESPONSE_SECTIONS.flatMap(section => [`<${section}>`, `</${section}>`]))
const LANGUAGE_SWITCHER = /^(?:English \| \[中文\]\(.+\)|\[English\]\(.+\) \| 中文)$/
interface TranslationFiles {
targetFilename: string
targetSwitcher: string
}
function translationFiles(input: Pick<TranslationPromptInput, 'sourceFilename' | 'sourceLanguage'>): TranslationFiles {
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')
const sourceIsEnglish = input.sourceFilename.endsWith('.md') && !sourceIsChinese
if (input.sourceLanguage === 'Chinese' ? !sourceIsChinese : !sourceIsEnglish) {
throw new Error(`translation prompt: ${input.sourceFilename} does not match source language ${input.sourceLanguage}`)
}
if (sourceIsChinese) {
return {
targetFilename: input.sourceFilename.replace(/\.zh\.md$/, '.md'),
targetSwitcher: `English | [中文](${input.sourceFilename})`,
}
}
return {
targetFilename: input.sourceFilename.replace(/\.md$/, '.zh.md'),
targetSwitcher: `[English](${input.sourceFilename}) | 中文`,
}
}
/** Extract the machine-consumed text fence from `translation-prompt.md`. */
function extractTranslationPrompt(document: string): string {
@@ -61,25 +109,14 @@ export function documentedTranslationPromptPlaceholders(document: string): strin
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. */
/** Render one system prompt from the checked-in template. */
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}`)
}
translationFiles(input)
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, '')
@@ -95,77 +132,128 @@ export function renderTranslationPrompt(document: string, input: TranslationProm
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[>')
/**
* Assemble the calibrated system prompt, reviewed bare-text examples, and source document.
*
* @param document - Checked-in translation prompt asset.
* @param input - Direction, filename, terminology, examples, and source document.
* @returns Provider-neutral messages and the target basename.
*/
export function renderTranslationRequest(document: string, input: TranslationRequestInput): TranslationRequest {
const files = translationFiles(input)
const sourceKey = input.sourceLanguage === 'English' ? 'english' : 'chinese'
const targetKey = input.sourceLanguage === 'English' ? 'chinese' : 'english'
const messages: TranslationMessage[] = [{ role: 'system', content: renderTranslationPrompt(document, input) }]
for (const example of input.examples) {
messages.push(
{ role: 'user', content: example[sourceKey] },
{ role: 'assistant', content: example[targetKey] },
)
}
messages.push({ role: 'user', content: input.sourceDocument })
return { targetFilename: files.targetFilename, messages }
}
/** Serialize a response using the exact XML wire contract in the prompt. */
function escapeResponseBody(value: string): string {
return value.split('\n').map((line) => {
const delimiter = line.replace(/^\\+/, '')
return RESPONSE_DELIMITERS.has(delimiter) ? `\\${line}` : line
}).join('\n')
}
function unescapeResponseBody(value: string): string {
return value.split('\n').map((line) => {
if (!line.startsWith('\\')) return line
const candidate = line.slice(1)
return RESPONSE_DELIMITERS.has(candidate.replace(/^\\+/, '')) ? candidate : line
}).join('\n')
}
/** Serialize a response in the exact escaped three-section shape the prompt requests. */
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')
return RESPONSE_SECTIONS.map(section => `<${section}>\n${escapeResponseBody(response[section])}\n</${section}>`).join('\n\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 })
/**
* Parse the three-section response. Sections must each appear exactly once
* and in order; escaped delimiter lines in Markdown bodies are restored.
* A fenced ```xml wrapper around the whole response is tolerated, matching
* the shape some models echo back from the prompt's own example.
*/
export function parseTranslationResponse(text: string): TranslationResponse {
let body = text.trim()
const fenced = /^```(?:xml)?\n([\s\S]*?)\n```$/.exec(body)
if (fenced?.[1] !== undefined) body = fenced[1].trim()
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`)
const values: Partial<Record<(typeof RESPONSE_SECTIONS)[number], string>> = {}
const lines = body.split('\n')
let previousCloseEnd = 0
for (const [index, section] of RESPONSE_SECTIONS.entries()) {
const open = `<${section}>`
const close = `</${section}>`
const openCount = lines.filter(line => line === open).length
const closeCount = lines.filter(line => line === close).length
if (openCount === 0 || closeCount === 0) {
throw new Error(`translation response: missing or unterminated <${section}> section`)
}
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 (openCount > 1 || closeCount > 1) throw new Error(`translation response: duplicate <${section}> section`)
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`)
const openStart = body.search(new RegExp(`^<${section}>$`, 'm'))
const closeStart = body.search(new RegExp(`^</${section}>$`, 'm'))
const separator = body.slice(previousCloseEnd, openStart)
if (closeStart < openStart || (index === 0 ? separator !== '' : !/^\n+$/.test(separator))) {
throw new Error('translation response: sections must appear in translation, review, final order')
}
let contentStart = openStart + open.length
if (body[contentStart] === '\n') contentStart++
let contentEnd = closeStart
if (body[contentEnd - 1] === '\n') contentEnd--
values[section] = unescapeResponseBody(body.slice(contentStart, contentEnd))
previousCloseEnd = closeStart + close.length
}
return values
if (previousCloseEnd !== body.length) throw new Error('translation response: content is not allowed outside response sections')
return values as TranslationResponse
}
function correctLanguageSwitcher(markdown: string, switcher: string): string {
const lines = markdown.replaceAll('\r\n', '\n').split('\n')
while (lines.at(-1) === '') lines.pop()
let headingIndex = 0
if (lines[0] === '---') {
const frontmatterEnd = lines.indexOf('---', 1)
if (frontmatterEnd === -1) throw new Error('translation response: final document has unterminated YAML frontmatter')
headingIndex = frontmatterEnd + 1
while (lines[headingIndex] === '') headingIndex++
}
if (!/^#\s+\S/.test(lines[headingIndex] ?? '')) {
throw new Error('translation response: final document must start with an H1 heading')
}
let contentStart = headingIndex + 1
while (lines[contentStart] === '') contentStart++
if (LANGUAGE_SWITCHER.test(lines[contentStart] ?? '')) contentStart++
while (lines[contentStart] === '') contentStart++
const output = [...lines.slice(0, headingIndex), lines[headingIndex] as string, '', switcher]
const content = lines.slice(contentStart)
if (content.length > 0) output.push('', ...content)
return `${output.join('\n')}\n`
}
/**
* Parse a model response and make its consumed final document target-path correct.
*
* @param text - Raw three-section model response.
* @param input - Source direction and basename retained by the pipeline.
* @returns Parsed response whose `final` body has the canonical target switcher.
*/
export function consumeTranslationResponse(
text: string,
input: Pick<TranslationPromptInput, 'sourceFilename' | 'sourceLanguage'>,
): TranslationResponse {
const parsed = parseTranslationResponse(text)
const files = translationFiles(input)
return { ...parsed, final: correctLanguageSwitcher(parsed.final, files.targetSwitcher) }
}

View File

@@ -1148,6 +1148,61 @@
"doc": "docs/core-data-structures/lsp.md",
"symbol": "LspService",
"source": "packages/lsp/lsp/src/types.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "SessionPersistenceRevision",
"source": "packages/session-persistence/session-persistence/src/revision.ts"
},
{
"doc": "docs/core-data-structures/persistence.md",
"symbol": "SessionPersistenceSnapshot",
"source": "packages/session-persistence/session-persistence/src/index.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionResultFilter",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventResultFilter",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventSearchDocument",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionSearchCursor",
"source": "packages/session-query/session-query/src/cursor.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionSearchRequest",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventSearchRequest",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionSearchPage",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionEventSearchHit",
"source": "packages/session-query/session-query/src/types.ts"
},
{
"doc": "docs/core-data-structures/session-query.md",
"symbol": "SessionSearchHit",
"source": "packages/session-query/session-query/src/types.ts"
}
]
}

View File

@@ -53,6 +53,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/i18n': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
@@ -76,6 +77,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/sdk/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },
'packages/sdk/telemetry': { kind: 'none', reason: 'The launcher-side reporter sends developer-cycle telemetry and registers no live agent or model surface.' },
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers no model surface.' },
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },

View File

@@ -3,11 +3,14 @@
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import {
consumeTranslationResponse,
documentedTranslationPromptPlaceholders,
parseTranslationResponse,
renderTranslationPrompt,
renderTranslationRequest,
renderTranslationResponse,
TRANSLATION_PROMPT_PLACEHOLDERS,
type TranslationExample,
} from './translation-prompt.ts'
const root = resolve(import.meta.dirname, '..')
@@ -17,38 +20,76 @@ function read(path: string): string {
}
try {
const mode = process.argv[2]
if (mode !== undefined && mode !== '--snapshot') throw new Error(`unsupported argument ${JSON.stringify(mode)}`)
const document = read('docs/i18n/translation-prompt.md')
const translationRules = read('docs/i18n/translation-rules.md')
const terminology = read('docs/i18n/terminology.md')
const examplePaths = [
['README.md', 'README.zh.md'],
['docs/development.md', 'docs/development.zh.md'],
['docs/i18n/README.md', 'docs/i18n/README.zh.md'],
['docs/i18n/translation-rules.md', 'docs/i18n/translation-rules.zh.md'],
[
'.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md',
'.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md',
],
] as const
const examples: TranslationExample[] = examplePaths.map(([english, chinese]) => ({
english: read(english),
chinese: read(chinese),
}))
const sourceDocument = read('scripts/fixtures/translation-prompt/snapshot-note.md')
const recordedResponse = read('scripts/fixtures/translation-prompt/response.txt')
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 englishInput = { sourceLanguage: 'English' as const, sourceFilename: 'snapshot-note.md', terminology }
const englishSource = renderTranslationPrompt(document, englishInput)
const chineseSource = renderTranslationPrompt(document, {
sourceLanguage: 'Chinese',
sourceFilename: 'example.zh.md',
translationRules,
sourceFilename: 'snapshot-note.zh.md',
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')
if (englishSource.includes('{{') || chineseSource.includes('{{')) throw new Error('rendered prompt contains an unresolved placeholder')
if (!englishSource.includes('from English to Chinese')) throw new Error('English-source render does not translate into Chinese')
if (!chineseSource.includes('from Chinese to English')) throw new Error('Chinese-source render does not translate into English')
const example = /```xml\n([\s\S]*?)\n```/.exec(englishSource)?.[1]
if (example === undefined) throw new Error('rendered prompt has no XML response example')
if (example === undefined) throw new Error('rendered prompt has no three-section response example')
parseTranslationResponse(example)
const roundTrip = { translation: 'first ]]> pass', review: '- [None] No corrections.', final: 'final ]]> text' }
const roundTrip = { translation: 'first pass\n\nwith **markdown**', review: '- 无修正', 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')
if (JSON.stringify(parsed) !== JSON.stringify(roundTrip)) throw new Error('three-section response does not round-trip')
console.log('verify-translation-prompt: both directions render and the XML response contract parses.')
const request = renderTranslationRequest(document, { ...englishInput, sourceDocument, examples })
if (request.targetFilename !== 'snapshot-note.zh.md') throw new Error('English request resolves the wrong target filename')
const expectedRoles = ['system', ...examples.flatMap(() => ['user', 'assistant']), 'user']
if (request.messages.map(message => message.role).join('\n') !== expectedRoles.join('\n')) {
throw new Error('reviewed examples are not assembled as system, example pairs, then source')
}
const consumed = consumeTranslationResponse(recordedResponse, englishInput)
const expectedFinalPrefix = [
'---',
'layout: doc',
'---',
'',
'# 快照说明',
'',
'[English](snapshot-note.md) | 中文',
'',
].join('\n')
if (!consumed.final.startsWith(expectedFinalPrefix)) {
throw new Error('recorded frontmatter response does not preserve metadata and receive the canonical target switcher')
}
if (mode === '--snapshot') {
process.stdout.write(`${JSON.stringify({ request, response: consumed }, null, 2)}\n`)
} else {
console.log('verify-translation-prompt: both directions render, reviewed examples assemble, and the consumed response is target-path correct.')
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`verify-translation-prompt: ${message}`)