Merge remote-tracking branch 'origin/master' into worktree/docs-website

# Conflicts:
#	.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
#	docs/AGENTS.md
#	docs/rfc/INDEX.md
#	docs/user/develop/basic/index.zh.md
#	docs/user/develop/basic/tool.zh.md
#	docs/user/develop/framework/index.zh.md
#	docs/user/develop/framework/service.zh.md
#	docs/user/develop/practice/index.zh.md
#	docs/user/guide/index.zh.md
#	docs/user/guide/quickstart.zh.md
#	knip.json
#	package.json
#	pnpm-lock.yaml
#	scripts/doc-typecheck.ts
#	scripts/gen-website-api.ts
#	scripts/translation-pairing.manifest.json
#	scripts/verify-type-equiv.ts
#	website/zh-CN/api/cordis/context.md
#	website/zh-CN/api/cordis/events.md
#	website/zh-CN/api/cordis/fiber.md
#	website/zh-CN/api/cordis/registry.md
#	website/zh-CN/api/cordis/service.md
#	website/zh-CN/api/harness/agent-loop.md
#	website/zh-CN/api/harness/agents.md
#	website/zh-CN/api/harness/approval.md
#	website/zh-CN/api/harness/bash-env.md
#	website/zh-CN/api/harness/bash.md
#	website/zh-CN/api/harness/code-runtime.md
#	website/zh-CN/api/harness/compact.md
#	website/zh-CN/api/harness/events.md
#	website/zh-CN/api/harness/fs.md
#	website/zh-CN/api/harness/llm.md
#	website/zh-CN/api/harness/permission.md
#	website/zh-CN/api/harness/sandbox.md
#	website/zh-CN/api/harness/session-persistence.md
#	website/zh-CN/api/harness/session-query.md
#	website/zh-CN/api/harness/sessions.md
#	website/zh-CN/api/harness/skills.md
#	website/zh-CN/api/harness/spill-store.md
#	website/zh-CN/api/harness/subagents.md
#	website/zh-CN/api/harness/system-prompt.md
#	website/zh-CN/api/harness/tasks.md
#	website/zh-CN/api/harness/token-meter.md
#	website/zh-CN/api/harness/tools.md
#	website/zh-CN/api/harness/user-interaction.md
#	website/zh-CN/api/harness/web.md
#	website/zh-CN/api/harness/workflows.md
#	website/zh-CN/api/index.md
#	website/zh-CN/design/effects-coeffects.md
#	website/zh-CN/design/index.md
#	website/zh-CN/guide/config.md
This commit is contained in:
Yichen Jiang
2026-07-20 10:21:36 +08:00
937 changed files with 29863 additions and 8289 deletions

View File

@@ -0,0 +1,78 @@
/**
* Shared structural source of truth for the Agent Note tree. Lifecycle and class
* sets are closed under `.agents/notes/README.md`; importing this module is pure.
*/
import { globSync, readdirSync } from 'node:fs'
import { resolve, sep } from 'node:path'
export const agentNoteRoot = resolve(import.meta.dirname, '../.agents/notes')
/** The closed set of Agent Note lifecycles (top-level folders under .agents/notes/). */
const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const
/**
* The closed set of Agent Note classes (nested folder under each lifecycle). Adding a
* class is a deliberate act: extend this list AND the README's Classification
* section. The gate rejects any folder not listed here.
*/
const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const
/** Non-Agent Note Markdown allowed to sit directly at a lifecycle root. */
const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md'])
/** One Agent Note file, as discovered by the walker. */
export interface AgentNote {
lifecycle: string
/** Path relative to .agents/notes. */
rel: string
/** `yyyy-mm-dd` from the filename. */
date: string
}
/**
* Walk the Agent Note tree, enforcing the structure rules. Returns every valid Agent Note
* plus one error string per violation (unknown lifecycle or class folder, bad
* depth, or bad filename). Callers treat a non-empty error list as fatal.
*/
export function walkAgentNoteTree(): { notes: AgentNote[]; errors: string[] } {
const notes: AgentNote[] = []
const errors: string[] = []
// The lifecycle set is closed too: any directory under .agents/notes/ that is not
// a known lifecycle would otherwise hold Agent Notes invisible to the walk below.
for (const entry of readdirSync(agentNoteRoot, { withFileTypes: true })) {
if (entry.name === 'INDEX.md') {
errors.push('structure: INDEX.md — centralized Agent Note indexes are forbidden; browse the lifecycle/class tree or search the repository')
continue
}
if (entry.isDirectory() && !(LIFECYCLES as readonly string[]).includes(entry.name)) {
errors.push(`structure: ${entry.name}/ — unknown lifecycle folder (allowed: ${LIFECYCLES.join(', ')})`)
}
}
for (const lifecycle of LIFECYCLES) {
for (const match of globSync(`${lifecycle}/**/*.md`, { cwd: agentNoteRoot }).map(path => path.split(sep).join('/')).sort()) {
const segs = match.split('/')
// Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md).
if (segs.length === 2 && ROOT_ALLOWLIST.has(segs[1] ?? '')) continue
// A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME Agent Note,
// indexed via its English filename; the pairing gate owns its consistency.
if (match.endsWith('.zh.md')) continue
const cls = segs[1]
const base = segs[2]
if (segs.length !== 3 || cls === undefined || base === undefined) {
errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`)
continue
}
if (!(CLASSES as readonly string[]).includes(cls)) {
errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`)
continue
}
if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) {
errors.push(`structure: ${match} — filename must be yyyy-mm-dd-topic.md`)
continue
}
notes.push({ lifecycle, rel: match, date: base.slice(0, 10) })
}
}
return { notes, errors }
}

View File

@@ -1,7 +1,7 @@
/**
* Build the SDK runtime executables and Python node carrier. The fixed
* `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by
* docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
* .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
* The staged closure is symlink-free, and whole-tree assets cover Cordis's
* runtime imports that pkg cannot discover statically.
*/
@@ -69,7 +69,7 @@ class Target {
readonly nodeRange: string,
/**
* pkg platform tag. Windows is a documented non-goal
* (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
* (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
*/
readonly platform: Platform,
/** pkg CPU tag. */
@@ -190,7 +190,7 @@ class BuildCli {
' --dry-run print every command and config patch without executing.',
' --help print this help.',
'',
`Build route: ${PKG_SPEC} --sea; see docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.`,
`Build route: ${PKG_SPEC} --sea; see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.`,
`Stages the node carrier in ${PYTHON_RUNTIME_DIR}/${PYTHON_NODE_SUBDIR} and writes executables to ${OUT_DIR}/.`,
].join('\n')
}

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
# Vendored upstream paths follow vendor/README.md instead of repository naming policy.
root=$(git rev-parse --show-toplevel)
candidate_file=$(mktemp)
trap 'unlink "$candidate_file"' EXIT
git -C "$root" ls-files -z -- \
':(icase,glob)*golden*' \
':(icase,glob)**/*golden*' \
':(exclude,glob)vendor/**' > "$candidate_file"
violations=()
while IFS= read -r -d '' path; do
violations+=("$path")
done < "$candidate_file"
if (( ${#violations[@]} == 0 )); then
echo 'check-expected-filenames: no tracked non-vendor filename contains "golden".'
exit 0
fi
echo 'check-expected-filenames: tracked non-vendor filenames must not contain "golden":' >&2
printf ' %s\n' "${violations[@]}" >&2
echo 'Rename each file with an accurate term such as "expected".' >&2
exit 1

View File

@@ -1,7 +1,7 @@
/**
* Boot the REPL or ACP Code Mode overlay, defaulting to REPL. Each overlay
* Boot the REPL, TUI, or ACP Code Mode overlay, defaulting to REPL. Each overlay
* includes its base example, selects Code Mode, and adds the worker runtime.
* Both require a DeepSeek API key; unsupported arguments fail with usage.
* All require a DeepSeek API key; unsupported arguments fail with usage.
*/
import { spawn } from 'node:child_process'
@@ -9,14 +9,15 @@ import { spawn } from 'node:child_process'
// the overlay config (the stdio bin keeps --expose-internals for the cordis
// Loader's HMR path).
const UIS = new Map([
['repl', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-demo/src/bin.ts', 'examples/coding-agent/code-mode.cordis.yml']],
['repl', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-demo/src/bin.ts', 'examples/repl-agent/code-mode.cordis.yml']],
['tui', ['--expose-internals', '--import', 'tsx', 'packages/examples/stdio-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']],
])
const ui = process.argv[2] ?? 'repl'
const args = UIS.get(ui)
if (!args || process.argv.length > 3) {
console.error('usage: pnpm run demo:code-mode [repl|acp]')
console.error('usage: pnpm run demo:code-mode [repl|tui|acp]')
process.exit(2)
}

View File

@@ -1,11 +1,11 @@
{
"AGENTS.md": 1500,
"docs/AGENTS.md": 1100,
"AGENTS.md": 1600,
"docs/AGENTS.md": 1150,
"docs/architecture.md": 1790,
"docs/cordis-primer.md": 600,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 960,
"examples/AGENTS.md": 310,
"packages/AGENTS.md": 290,
"packages/AGENTS.md": 650,
"packages/README.md": 760
}

View File

@@ -1,6 +1,6 @@
/**
* Typecheck Markdown `ts` fences against the workspace API. `ignore-check` fences are reported as
* opt-outs; generated catalog fragments and `type-equiv` blocks are skipped here because their
* opt-outs; generated catalog fragments and source-equivalence blocks are skipped here because their
* owning gates verify them. A build-coordinated mode consumes existing declarations without emit.
*/
@@ -33,6 +33,7 @@ const KIND_BY_INFO: Record<string, BlockKind> = {
'ts': 'check',
'ts ignore-check': 'ignore',
'ts type-equiv': 'type-equiv',
'ts public-api': 'type-equiv',
'ts cordis-catalog': 'cordis-catalog',
'ts persistence-catalog': 'persistence-catalog',
'ts config-catalog': 'config-catalog',
@@ -191,7 +192,7 @@ function remapBlockPaths(output: string, blocks: Block[]): string {
})
}
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const files: string[] = []
for (const pattern of markdownGlobs) {

View File

@@ -837,7 +837,7 @@ export function render(entries: CatalogEntry[]): string {
'',
'## Seam packages (not directly loadable)',
'',
'Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](rfc/implemented/architecture/2026-06-13-capability-seams.md)).',
'Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).',
'',
...entries.filter(e => e.kind === 'seam').map(e => renderTerse(e, ` — abstract \`${e.className ?? ''}\``)),
'',

View File

@@ -1,8 +1,9 @@
/**
* Generate the model-facing Cordis API data module from the same event/service
* collector as the documentation catalogs. It emits first-sentence docs, raw
* signatures, transitive public type shapes, and inherited context entries,
* without source pointers; output is deterministic and `--check` verifies it.
* collector as the documentation catalogs. It emits original declaration
* JSDoc, first-sentence summaries, raw signatures, transitive public type
* shapes, and inherited context entries, without source pointers; output is
* deterministic and `--check` verifies it.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -80,7 +81,7 @@ function referencedTypes(seeds: string[], decls: Map<string, string>): { name: s
function render(): string {
const services = collectServices()
const events = collectEvents().sort((a, b) => a.name.localeCompare(b.name))
const types = referencedTypes(services.flatMap(service => service.methods), collectTypeDecls())
const types = referencedTypes(services.flatMap(service => service.methods.map(method => method.signature)), collectTypeDecls())
const lines: string[] = [
'/**',
' * Generated by scripts/gen-cordis-api.ts — do not edit by hand; run',
@@ -88,22 +89,30 @@ function render(): string {
' * `pnpm run verify-cordis-api` in doc-sync).',
' *',
' * The machine-readable cordis API catalog `cordis_inspect` serves to the',
' * model: harness services (summary + public method signatures), harness',
' * events (mode + signature), and the inherited `ctx` surface. Produced by',
' * model: harness services (summary + public method signatures/JSDoc),',
' * harness events (mode + signature/JSDoc), and the inherited `ctx` surface. Produced by',
' * the same AST walk as docs/cordis-catalog, so this data and the rendered',
' * docs cannot diverge.',
' *',
' * @module @deepseek-ai/dsh-tool-cordis/api-catalog',
' */',
'',
'/** One harness `ctx.<key>` service: its one-line summary and public method signatures. */',
'/** One public service method and its source-owned contract. */',
'export interface ServiceApiMethod {',
' /** Public method signature with its body stripped. */',
' signature: string',
' /** Original method JSDoc, with only container indentation removed. */',
' jsDoc: string',
'}',
'',
'/** One harness `ctx.<key>` service: its one-line summary and public methods. */',
'export interface ServiceApiEntry {',
' /** The `ctx.<key>` name, e.g. `tools`. */',
' key: string',
' /** First sentence of the service class JSDoc. */',
' summary: string',
' /** Public method signatures, bodies stripped, in source order. */',
' methods: readonly string[]',
' /** Public methods, bodies stripped, in source order. */',
' methods: readonly ServiceApiMethod[]',
'}',
'',
'/** One harness event: its dispatch mode, exact signature, and one-line summary. */',
@@ -114,6 +123,8 @@ function render(): string {
' mode: string',
' /** The exact listener signature, whitespace-normalized. */',
' signature: string',
' /** Original event JSDoc, with only container indentation removed. */',
' jsDoc: string',
' /** First sentence of the event JSDoc. */',
' summary: string',
'}',
@@ -145,7 +156,12 @@ function render(): string {
lines.push(' methods: [],')
} else {
lines.push(' methods: [')
for (const method of service.methods) lines.push(` ${quote(method)},`)
for (const method of service.methods) {
lines.push(' {')
lines.push(` signature: ${quote(method.signature)},`)
lines.push(` jsDoc: ${quote(method.jsDoc)},`)
lines.push(' },')
}
lines.push(' ],')
}
lines.push(' },')
@@ -161,6 +177,7 @@ function render(): string {
lines.push(` name: ${quote(event.name)},`)
lines.push(` mode: ${quote(event.mode)},`)
lines.push(` signature: ${quote(event.signature)},`)
lines.push(` jsDoc: ${quote(event.jsDoc)},`)
lines.push(` summary: ${quote(firstSentence(event.doc))},`)
lines.push(' },')
}

View File

@@ -1,8 +1,8 @@
/**
* Generate the Cordis event and service catalogs from static declarations.
* The walk enforces event modes plus JSDoc parameter/return completeness;
* inherited Cordis services come from the curated table below. `--check`
* verifies both committed artifacts.
* The walk enforces event modes, JSDoc parameter/return completeness, and
* signature type-link coverage; inherited Cordis services come from the
* curated table below. `--check` verifies both committed artifacts.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -20,48 +20,199 @@ const OUT_SERVICES = 'docs/cordis-catalog/services.md'
const FENCE = 'ts cordis-catalog'
/**
* One primary core-data-structures page per signature type, shared by the
* Cordis and config catalogs; union names intentionally do not reuse the
* type-equivalence manifest's map-symbol entries.
* One primary core-data-structures page per project type used by a generated
* signature. This stays curated because union names intentionally do not
* reuse the type-equivalence manifest's map-symbol entries and some symbols
* appear on more than one page.
*/
// TODO(catalog-type-links): verify or generate link-map coverage.
export const LINK_MAP: Record<string, string> = {
Agent: 'core.md',
AgentOptions: 'core.md',
AgentStatus: 'core.md',
ContentBlock: 'core.md',
Message: 'core.md',
MessageSource: 'core.md',
ContinuationDecision: 'core.md',
ContinuationStop: 'core.md',
GenerateOptions: 'core.md',
LlmCallConfig: 'core.md',
LlmModelInfo: 'core.md',
LlmProviderInfo: 'core.md',
Message: 'core.md',
MessageSource: 'core.md',
PromptDecision: 'core.md',
RequestError: 'core.md',
RequestErrorDecision: 'core.md',
SessionEvent: 'core.md',
SessionId: 'core.md',
SessionStartSource: 'core.md',
StreamChunk: 'llm-streaming.md',
TurnEndReason: 'session.md',
ToolDefinition: 'tools.md',
ToolExecution: 'tools.md',
ToolExecutionMode: 'tools.md',
ToolExecutionInput: 'tools.md',
ToolExecutionResult: 'tools.md',
ToolExecutionToken: 'tools.md',
ApprovalOutcome: 'approval.md',
ApprovalPolicy: 'approval.md',
ApprovalRequest: 'approval.md',
ApprovalService: 'approval.md',
BashExecRequest: 'bash.md',
BashExecSpec: 'bash.md',
BashProcess: 'bash.md',
BashRunResult: 'bash.md',
ConfinedArgv: 'sandbox.md',
SandboxMode: 'sandbox.md',
SandboxPolicy: 'sandbox.md',
DshEnvironment: 'bash.md',
CodeRunRequest: 'code-runtime.md',
CodeRunResult: 'code-runtime.md',
CompactionResult: 'compaction.md',
CompactionTrigger: 'compaction.md',
FileReadOutcome: 'filesystem.md',
FsDirEntry: 'filesystem.md',
FsEditOutcome: 'filesystem.md',
FsEditRequest: 'filesystem.md',
FsInfo: 'filesystem.md',
FsPathInfo: 'filesystem.md',
FsPolicyExec: 'filesystem.md',
FsTarget: 'filesystem.md',
FsVersion: 'filesystem.md',
FsWriteIntent: 'filesystem.md',
FsWriteOutcome: 'filesystem.md',
FsPolicyExec: 'filesystem.md',
FileReadOutcome: 'filesystem.md',
LlmAdapter: 'llm-streaming.md',
LlmService: 'llm-streaming.md',
StreamChunk: 'llm-streaming.md',
CreateSessionOptions: 'persistence.md',
SessionHeader: 'persistence.md',
SessionLocation: 'persistence.md',
ConfinedArgv: 'sandbox.md',
SandboxMode: 'sandbox.md',
SandboxPolicy: 'sandbox.md',
ScopeKey: 'scope.md',
Scoped: 'scope.md',
EpochHeader: 'session.md',
Session: 'session.md',
TurnEndReason: 'session.md',
SessionEventReadRequest: 'session-query.md',
SessionEventRecord: 'session-query.md',
SessionEventTrace: 'session-query.md',
SessionEventTraceRequest: 'session-query.md',
SessionEventWindow: 'session-query.md',
SessionLineageTrace: 'session-query.md',
SessionRecord: 'session-query.md',
SkillDefinition: 'skills.md',
SkillLookupOptions: 'skills.md',
SkillProvider: 'skills.md',
SkillRegistration: 'skills.md',
SkillSummary: 'skills.md',
SaveTextSpill: 'spill.md',
SpillRef: 'spill.md',
SubagentProvider: 'subagent.md',
SubagentRun: 'subagent.md',
SubagentService: 'subagent.md',
SubagentStartRequest: 'subagent.md',
AssembleContext: 'system-prompt.md',
PromptSection: 'system-prompt.md',
SystemPrompt: 'system-prompt.md',
ToolProviderResult: 'system-prompt.md',
TaskDoneListener: 'tasks.md',
TaskId: 'tasks.md',
TaskRead: 'tasks.md',
TaskSnapshot: 'tasks.md',
TaskStart: 'tasks.md',
TokenMeasurement: 'token-meter.md',
PostToolDecision: 'tools.md',
PreToolDecision: 'tools.md',
ToolDefinition: 'tools.md',
ToolExecution: 'tools.md',
ToolExecutionInput: 'tools.md',
ToolExecutionMode: 'tools.md',
ToolExecutionResult: 'tools.md',
ToolExecutionToken: 'tools.md',
ToolGuard: 'tools.md',
ToolRegistry: 'tools.md',
ToolRestriction: 'tools.md',
ToolSchema: 'tools.md',
AskUserQuestionAnswer: 'user-interaction.md',
AskUserQuestionRequest: 'user-interaction.md',
UserInteractionProvider: 'user-interaction.md',
WebFetchProvider: 'web.md',
WebFetchRequest: 'web.md',
WebFetchResult: 'web.md',
WebSearchProvider: 'web.md',
WebSearchRequest: 'web.md',
WebSearchResult: 'web.md',
WorkflowRun: 'workflow.md',
WorkflowRunInfo: 'workflow.md',
WorkflowStartRequest: 'workflow.md',
}
/** TypeScript lib and pinned framework types that have no repository-owned data page. */
const FOUNDATION_TYPE_NAMES = new Set([
'AbortSignal',
'AsyncIterable',
'Context',
'Error',
'Pick',
'Promise',
'Readonly',
])
/** Project types deliberately documented outside the core-data catalog. */
const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
AgentFactory: 'agent creation seam is owned by packages/core/agent/README.md',
AgentHandle: 'agent ownership handle is owned by packages/core/agent/README.md',
BashEnvContributor: 'service-local extension type is owned by packages/bash/tool-bash/src/index.ts',
BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts',
CompactAgentContext: 'compaction service input is owned by packages/compact/compact/src/index.ts',
CreateAgentOptions: 'agent creation contract is owned by packages/core/agent/README.md',
PresetOption: 'deployment menu metadata is owned by packages/ui/permission/README.md',
PresetSpec: 'deployment preset composition is owned by packages/ui/permission/README.md',
PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
ResumeAgentOptions: 'agent resume contract is owned by packages/core/agent/README.md',
SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
SubagentRunEndInfo: 'event-local snapshot is owned by packages/subagent/subagent/src/index.ts',
SubagentRunInfo: 'event-local snapshot is owned by packages/subagent/subagent/src/index.ts',
WorkflowAgentEndInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkflowAgentInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts',
}
/** Collect named references from parameter, generic-constraint/default, and return types. */
function signatureTypeNames(member: ts.MethodSignature | ts.MethodDeclaration, sf: ts.SourceFile): string[] {
const declared = new Set(member.typeParameters?.map(parameter => parameter.name.text) ?? [])
const referenced = new Set<string>()
const visit = (node: ts.Node): void => {
if (ts.isTypeReferenceNode(node)) referenced.add(node.typeName.getText(sf))
if (ts.isTypeQueryNode(node)) referenced.add(node.exprName.getText(sf))
ts.forEachChild(node, visit)
}
for (const parameter of member.typeParameters ?? []) {
if (parameter.constraint) visit(parameter.constraint)
if (parameter.default) visit(parameter.default)
}
for (const parameter of member.parameters) {
if (parameter.type) visit(parameter.type)
}
if (member.type) visit(member.type)
return [...referenced].filter(name => !declared.has(name)).sort()
}
/** Append fail-closed signature type-link violations with actionable ownership choices. */
function checkTypeLinks(
where: string,
member: ts.MethodSignature | ts.MethodDeclaration,
sf: ts.SourceFile,
violations: string[],
): void {
for (const name of signatureTypeNames(member, sf)) {
if (Object.hasOwn(LINK_MAP, name)
|| FOUNDATION_TYPE_NAMES.has(name)
|| Object.hasOwn(TYPE_LINK_EXEMPTIONS, name)) continue
violations.push(
`${where} references unclassified type '${name}'. Add it to LINK_MAP with its core-data-structures page, `
+ 'to FOUNDATION_TYPE_NAMES if TypeScript or Cordis owns it, or to TYPE_LINK_EXEMPTIONS with '
+ 'the non-catalog documentation owner.',
)
}
}
/** Throw one aggregated diagnostic for every unclassified signature type. */
function reportTypeLinkViolations(gate: string, violations: string[]): void {
if (violations.length === 0) return
throw new Error(
`${gate}: ${violations.length} signature type-link coverage violation(s):\n`
+ violations.map(violation => ` ${violation}`).join('\n'),
)
}
/** One harness event, extracted from an `interface Events` block. */
@@ -72,6 +223,8 @@ interface EventEntry {
scope: string
/** Full signature text (the method-signature member, JSDoc stripped). */
signature: string
/** Original declaration JSDoc, dedented from its containing interface. */
jsDoc: string
/** Dispatch mode from the `@mode` tag. */
mode: Mode
/** Description prose (JSDoc minus the `@mode` tag), one line per paragraph. */
@@ -80,6 +233,14 @@ interface EventEntry {
source: string
}
/** One public service method and the source contract attached to it. */
interface ServiceMethodEntry {
/** Public method signature (body stripped). */
signature: string
/** Original method JSDoc, dedented from its containing class. */
jsDoc: string
}
/** One harness service, extracted from an `interface Context` block. */
interface ServiceEntry {
/** The `ctx.<key>` name, e.g. `llm`. */
@@ -90,8 +251,8 @@ interface ServiceEntry {
abstract: boolean
/** Class-level JSDoc prose, one line per paragraph. */
doc: string
/** Public method signatures (bodies stripped), in source order. */
methods: string[]
/** Public methods (bodies stripped), in source order. */
methods: ServiceMethodEntry[]
/** Source pointer of the class declaration. */
source: string
}
@@ -114,6 +275,22 @@ function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.Source
return sig.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim()
}
/**
* Copy a node's original JSDoc while removing only the indentation imposed by
* its containing interface or class.
*/
function jsDocText(text: string, sf: ts.SourceFile, node: ts.Node): string {
const raw = rawJsDoc(text, node)
if (!raw) return ''
const start = text.lastIndexOf(raw, node.getStart(sf))
const { line } = sf.getLineAndCharacterOfPosition(start)
const lineStart = sf.getPositionOfLineAndCharacter(line, 0)
const indent = text.slice(lineStart, start)
return raw.split('\n')
.map((lineText, index) => index > 0 && lineText.startsWith(indent) ? lineText.slice(indent.length) : lineText)
.join('\n')
}
/** Walk every harness `interface Events` block and extract its events, hard-
* erroring (aggregated) on any JSDoc-completeness violation: a missing/
* contradicted `@mode`, missing description prose, or an undocumented payload
@@ -121,6 +298,7 @@ function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.Source
export function collectEvents(scanRoot: string = root): EventEntry[] {
const entries: EventEntry[] = []
const violations: string[] = []
const typeLinkViolations: string[] = []
for (const rel of globSync('packages/*/*/src/*.ts', { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) {
const abs = resolve(scanRoot, rel)
const text = readFileSync(abs, 'utf8')
@@ -134,6 +312,7 @@ export function collectEvents(scanRoot: string = root): EventEntry[] {
const { doc, mode } = parseJsDoc(raw)
const src = pointer(rel, sf, member)
const where = `event '${name}' (${src})`
checkTypeLinks(where, member, sf, typeLinkViolations)
if (!mode) {
violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial' to its JSDoc (see AGENTS.md).`)
}
@@ -154,10 +333,11 @@ export function collectEvents(scanRoot: string = root): EventEntry[] {
const { params } = parseTags(raw)
checkParams(where, 'event', member.parameters, params, sf,
p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations)
if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, mode, doc, source: src })
if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, jsDoc: jsDocText(text, sf, member), mode, doc, source: src })
}
}
reportViolations('gen-cordis-catalog', violations)
reportTypeLinkViolations('gen-cordis-catalog', typeLinkViolations)
return entries
}
@@ -170,6 +350,7 @@ export function collectEvents(scanRoot: string = root): EventEntry[] {
export function collectServices(scanRoot: string = root): ServiceEntry[] {
const entries: ServiceEntry[] = []
const violations: string[] = []
const typeLinkViolations: string[] = []
for (const rel of globSync('packages/*/*/src/index.ts', { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) {
const abs = resolve(scanRoot, rel)
const text = readFileSync(abs, 'utf8')
@@ -179,7 +360,7 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] {
if (!body) continue
// Resolve each ctx key to its service class (shared walk) and emit an entry.
for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) {
const methods: string[] = []
const methods: ServiceMethodEntry[] = []
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member)) continue
// Only instance methods callable through `ctx.<key>` are surface;
@@ -192,9 +373,10 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] {
if (nonPublic) continue
const memberName = member.name.getText(sf)
if (memberName.startsWith('[')) continue // computed/symbol members
methods.push(memberSignature(member, sf))
const where = `service method ctx.${key}.${memberName} (${pointer(rel, sf, member)})`
checkTypeLinks(where, member, sf, typeLinkViolations)
const raw = rawJsDoc(text, member)
methods.push({ signature: memberSignature(member, sf), jsDoc: jsDocText(text, sf, member) })
if (!raw) { violations.push(`${where} has no JSDoc.`); continue }
if (!parseJsDoc(raw).doc) violations.push(`${where} has no description prose above its block tags.`)
const { params, returns } = parseTags(raw)
@@ -216,6 +398,7 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] {
}
}
reportViolations('gen-cordis-catalog', violations)
reportTypeLinkViolations('gen-cordis-catalog', typeLinkViolations)
return entries.sort((a, b) => a.key.localeCompare(b.key))
}
@@ -274,7 +457,7 @@ function typeLinks(signature: string): string {
function renderEvent(e: EventEntry): string[] {
const out = [`### \`${e.name}\`${e.mode}`, '']
if (e.doc) out.push(e.doc, '')
out.push('```' + FENCE, e.signature, '```', '')
out.push('```' + FENCE, e.jsDoc, e.signature, '```', '')
const links = typeLinks(e.signature)
if (links) out.push(links, '')
out.push(`Source: [\`${e.source}\`](../../${e.source.split(':')[0]})`, '')
@@ -287,8 +470,13 @@ function renderService(s: ServiceEntry): string[] {
const out = [`## \`ctx.${s.key}\`\`${s.type}\`${kind}`, '']
if (s.doc) out.push(s.doc, '')
if (s.methods.length) {
out.push('```' + FENCE, ...s.methods, '```', '')
const links = typeLinks(s.methods.join('\n'))
const declarations = s.methods.flatMap((method, index) => [
...(index > 0 ? [''] : []),
method.jsDoc,
method.signature,
])
out.push('```' + FENCE, ...declarations, '```', '')
const links = typeLinks(s.methods.map(method => method.signature).join('\n'))
if (links) out.push(links, '')
}
out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '')
@@ -303,15 +491,15 @@ const BANNER = [
]
/** The shared GENERATED + freshness-gate + fence notice paragraph. */
const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them.'
const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.'
/** Render the events catalog (pure, deterministic given sorted inputs). */
function renderEvents(events: EventEntry[]): string {
export function renderEvents(events: EventEntry[]): string {
const lines: string[] = [
...BANNER,
'# Cordis Events Catalog',
'',
'Every cordis event a plugin can listen to: exact signature, dispatch mode, and the declaration\'s JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.<key>` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.',
'Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.<key>` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.',
'',
GATE_NOTICE,
'',
@@ -341,12 +529,12 @@ function renderEvents(events: EventEntry[]): string {
}
/** Render the services catalog (pure, deterministic given sorted inputs). */
function renderServices(services: ServiceEntry[]): string {
export function renderServices(services: ServiceEntry[]): string {
const lines: string[] = [
...BANNER,
'# Cordis Services Catalog',
'',
'Every `ctx.<key>` service a plugin can call: the exact public interface plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.',
'Every `ctx.<key>` service a plugin can call: the exact public interface with original method JSDoc, plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.',
'',
GATE_NOTICE,
'',

View File

@@ -100,7 +100,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'session',
title: 'In-memory session store',
mode: 'core',
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'subagent-inprocess', 'invariants'],
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'subagent-inprocess', 'invariants'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
@@ -156,10 +156,10 @@ const SERVICE_ROLES: ServiceRole[] = [
{
key: 'agents',
pkg: 'agent',
title: 'Agent registry',
title: 'Agent service',
mode: 'core',
consumers: ['agent-loop', 'acp', 'subagent-inprocess', 'stdio-demo', 'invariants'],
note: 'Owns live Agent handles and the create/resume factory seam.',
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess', 'stdio-demo', 'invariants'],
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
},
{
key: 'agentLoop',
@@ -238,14 +238,14 @@ const SERVICE_ROLES: ServiceRole[] = [
mode: 'seam',
implementations: ['compact-basic'],
consumers: ['compact-basic'],
note: 'The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred.',
note: 'The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred.',
},
{
key: 'subagents',
pkg: 'subagent',
title: 'Subagent provider registry',
mode: 'seam',
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-mock'],
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp'],
consumers: ['tool-subagent'],
note: 'Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name.',
},
@@ -427,12 +427,28 @@ const APP_EXAMPLES = [
summary: 'The echo demo swaps in a local mock LLM and teaching echo tool, then loads the stdio app package for the shared spine and terminal front door.',
},
{
id: 'coding',
rel: 'examples/coding-agent/composition.md',
title: 'Coding Agent App Composition',
label: 'examples/coding-agent',
config: 'examples/coding-agent/cordis.yml',
summary: 'The coding REPL demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.',
id: 'repl',
rel: 'examples/repl-agent/composition.md',
title: 'REPL Agent App Composition',
label: 'examples/repl-agent',
config: 'examples/repl-agent/cordis.yml',
summary: 'The REPL agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.',
},
{
id: 'tui',
rel: 'examples/tui-agent/composition.md',
title: 'TUI Agent App Composition',
label: 'examples/tui-agent',
config: 'examples/tui-agent/cordis.yml',
summary: 'The TUI agent reuses the repl-agent backend and tool composition while fixing the shared terminal app to the full-screen dsh-tui front door.',
},
{
id: 'headless',
rel: 'examples/headless-agent/composition.md',
title: 'Headless Agent App Composition',
label: 'examples/headless-agent',
config: 'examples/headless-agent/cordis.yml',
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
},
{
id: 'cordis',
@@ -454,13 +470,20 @@ const APP_EXAMPLES = [
type AppExample = typeof APP_EXAMPLES[number]
function renderAppExpansion(lines: string[], appNode: string, pluginName: string): void {
function renderAppExpansion(lines: string[], appNode: string, pluginName: string, exampleId: string): void {
const agentCore = nodeId('bundle', 'agent_core')
const jsonl = nodeId('bundle', 'jsonl')
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
if (pluginName === '@deepseek-ai/dsh-stdio-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["readline UI<br/>console logger<br/>pre-created main agent"]`)
const frontDoor = exampleId === 'tui'
? '@deepseek-ai/dsh-tui<br/>pre-created main agent'
: exampleId === 'repl'
? '@deepseek-ai/dsh-stdio<br/>pre-created main agent'
: 'dsh-tui (TTY) / dsh-stdio (pipes)<br/>pre-created main agent'
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'stdio')}["${frontDoor}"]`)
} else if (pluginName === '@deepseek-ai/dsh-cli-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'cli')}["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]`)
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>JSON-RPC stdio bridge<br/>sessions created by client"]`)
}
@@ -487,8 +510,8 @@ function renderAppComposition(example: AppExample): string {
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
lines.push(` cfg --> ${pluginNode}`)
if (plugin.name === '@deepseek-ai/dsh-stdio-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
renderAppExpansion(lines, pluginNode, plugin.name)
if (plugin.name === '@deepseek-ai/dsh-stdio-demo' || plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
renderAppExpansion(lines, pluginNode, plugin.name, example.id)
}
}
lines.push(
@@ -845,6 +868,11 @@ function renderLifecycle(): string {
' LLM-->>Driver: StreamChunk*',
` Driver->>Session: ${mermaidCode('assistant/chunk')}*`,
` Session-->>SDK: ${mermaidCode('session/event')} ${mermaidCode('assistant/chunk')}*`,
' alt final adapter or terminal in-band request failure',
` Driver->>Session: ${mermaidCode('step/end')}`,
` Driver->>Hooks: ${mermaidCode('agent/request-error')} waterfall`,
' Hooks-->>Driver: retry in a new step or preserve the original error',
' else model request succeeded',
` Driver->>Hooks: ${mermaidCode('agent/step-result')} waterfall`,
` Driver->>Session: ${mermaidCode('assistant/message')}`,
' Driver->>Tools: classify pending call by executionMode',
@@ -859,9 +887,12 @@ function renderLifecycle(): string {
` Driver->>Session: ${mermaidCode('tool/result')}`,
' end',
' end',
' Driver->>Session: post-tool context and steering',
` Driver->>Hooks: ${mermaidCode('agent/post-step')} serial checkpoint`,
` Driver->>Session: ${mermaidCode('step/end')}`,
` Driver->>Hooks: ${mermaidCode('agent/turn-continuation')} waterfall`,
` Driver->>Hooks: ${mermaidCode('agent/turn-stop')} serial terminal checkpoint`,
' end',
` Driver->>Session: ${mermaidCode('turn/end')}`,
` Driver->>Persistence: ${mermaidCode('session/flush')} parallel checkpoint`,
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
@@ -869,6 +900,8 @@ function renderLifecycle(): string {
'',
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
'',
'`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Recovery compacts between the closed failed step and a fresh retry step, and returns retry only when the surface replacement generation advances; otherwise the original request error remains authoritative.',
'',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.',
'',
...maintenanceFooter(maintenance),
@@ -943,14 +976,14 @@ function renderSnapshotReplay(): string {
' participant Workspace',
' participant Replay as llm-replay adapter',
' participant ACP as acp-agent subprocess',
' participant Golden as stdout golden',
' participant Expected as stdout expected output',
' Recorder->>Fixture: session.jsonl + workspace inputs',
' Fixture->>Workspace: seed files and hook configs',
' Fixture->>Replay: recorded StreamChunk script',
` Replay->>ACP: deterministic ${mermaidCode('llm/stream')} chunks`,
' ACP->>Workspace: bash, fs, and hook side effects',
' ACP->>Golden: normalized sessionUpdate stream',
' Golden-->>ACP: diff must be empty',
' ACP->>Expected: normalized sessionUpdate stream',
' Expected-->>ACP: diff must be empty',
'```',
'',
'The fs and hook snapshot matrix is valuable because it proves world state, hook decisions, and failed tool-card rendering, not just that replay returns text.',
@@ -977,7 +1010,9 @@ function renderIndex(docs: GraphDoc[]): string {
const labels: Record<string, string> = {
'docs/capability-seams.md': 'capability seams and core services',
'examples/echo-agent/composition.md': 'echo-agent app composition',
'examples/coding-agent/composition.md': 'coding-agent app composition',
'examples/repl-agent/composition.md': 'repl-agent app composition',
'examples/headless-agent/composition.md': 'headless-agent app composition',
'examples/tui-agent/composition.md': 'tui-agent app composition',
'examples/cordis-agent/composition.md': 'cordis-agent app composition',
'examples/acp-agent/composition.md': 'acp-agent app composition',
'docs/event-producer-consumer.md': 'event producer/consumer matrix',
@@ -988,7 +1023,9 @@ function renderIndex(docs: GraphDoc[]): string {
const modes: Record<string, string> = {
'docs/capability-seams.md': 'hybrid generated',
'examples/echo-agent/composition.md': 'hybrid generated',
'examples/coding-agent/composition.md': 'hybrid generated',
'examples/repl-agent/composition.md': 'hybrid generated',
'examples/headless-agent/composition.md': 'hybrid generated',
'examples/tui-agent/composition.md': 'hybrid generated',
'examples/cordis-agent/composition.md': 'hybrid generated',
'examples/acp-agent/composition.md': 'hybrid generated',
'docs/event-producer-consumer.md': 'hybrid generated',
@@ -1009,7 +1046,7 @@ function renderIndex(docs: GraphDoc[]): string {
...generatedHeader('Documentation Graph Index'),
'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md).',
'',
'The process decision behind this index is recorded in [the documentation graph RFC](rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).',
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md).',
'',
'| Graph | Mode |',
'| --- | --- |',

View File

@@ -349,7 +349,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
'',
'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](core-data-structures/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](core-data-structures/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).',
'',
'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog RFC](rfc/implemented/process/2026-07-04-persistence-log-catalog.md).',
'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md).',
'',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'',

View File

@@ -1,36 +0,0 @@
/**
* Regenerate `docs/rfc/INDEX.md` — the fully generated RFC index — from the
* RFC tree (see [rfc-index.ts](./rfc-index.ts) for the layout contract and
* rendering rules). The whole file is generated state; the curated prose lives
* in `docs/rfc/README.md`. Freshness is asserted by
* `verify-rfc-classification.ts` (a `doc-sync` member), so a stale committed
* index fails CI.
*
* Run: `pnpm run gen-rfc-index`.
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { renderIndex, rfcRoot, walkRfcTree } from './rfc-index.ts'
const { rfcs, errors } = walkRfcTree()
if (errors.length > 0) {
console.error('gen-rfc-index: refusing to generate from a structurally invalid tree:')
for (const e of errors) console.error(` ${e}`)
process.exit(1)
}
const indexPath = resolve(rfcRoot, 'INDEX.md')
const next = renderIndex(rfcs)
let current: string | undefined
try {
current = readFileSync(indexPath, 'utf8')
} catch {
// Missing INDEX.md is the fresh-generation case, not an error: fall through and write it.
}
if (next === current) {
console.log(`gen-rfc-index: docs/rfc/INDEX.md is up to date (${rfcs.length} RFCs).`)
} else {
writeFileSync(indexPath, next)
console.log(`gen-rfc-index: docs/rfc/INDEX.md regenerated (${rfcs.length} RFCs).`)
}

View File

@@ -3,7 +3,7 @@
* plugin. Runtime registration is the source of truth for computed schemas;
* the manifest is checked against every on-disk `tool-*` package. `--check`
* verifies the committed artifact. Rationale and ownership live in
* `docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md`.
* `.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md`.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
@@ -19,7 +19,7 @@ import WebService from '@deepseek-ai/dsh-web'
import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentMock from '@deepseek-ai/dsh-subagent-mock'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
import TaskService from '@deepseek-ai/dsh-tasks'
@@ -39,6 +39,17 @@ import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/tool-catalog.md'
/** Register the descriptor needed to mount schema-producing consumers. */
function registerCatalogSubagentProvider(ctx: Context, name: string): void {
const provider: SubagentProvider = {
name,
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: () => Promise.reject(new Error('tool-catalog provider cannot start a child')),
}
ctx.subagents.registerProvider(provider)
}
/**
* Tool package plus its hand-maintained boot recipe. The caller mounts the
* prompt and registry; each recipe supplies only package-specific seams and
@@ -108,7 +119,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
toolsConfig: { mode: 'code' },
async mount() {},
note:
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
},
{
pkg: '@deepseek-ai/dsh-tool-bash',
@@ -133,7 +144,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolCordis)
},
note:
'Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes.',
'Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes.',
},
{
pkg: '@deepseek-ai/dsh-tool-fs',
@@ -191,12 +202,11 @@ const TOOL_PACKAGES: ToolPackage[] = [
shippedNames: ['subagent', 'subagent_fork'],
async mount(ctx) {
await ctx.plugin(SubagentService)
// Register a scripted provider under the name the tool delegates to.
await ctx.plugin(SubagentMock, { name: 'mock' })
registerCatalogSubagentProvider(ctx, 'mock')
await ctx.plugin(ToolSubagent, { provider: 'mock' })
},
note:
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/coding-agent/cordis.yml` and `examples/acp-agent/cordis.yml`.',
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/repl-agent/cordis.yml` and `examples/acp-agent/cordis.yml`.',
},
{
pkg: '@deepseek-ai/dsh-tool-tasks',
@@ -234,7 +244,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
// subagent provider to satisfy it. The schema does not depend on which
// provider backs the engine.
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentMock, { name: 'mock' })
registerCatalogSubagentProvider(ctx, 'mock')
await ctx.plugin(VmWorkflowEngine, { provider: 'mock' })
await ctx.plugin(ToolWorkflow)
},
@@ -357,7 +367,7 @@ export function render(catalog: ToolCatalog): string {
'',
'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered.',
'',
'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md).',
'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md).',
'',
'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.',
'',

View File

@@ -1,130 +0,0 @@
/**
* Shared source of truth for the RFC index: the tree walker (structure rules) and the README
* table renderer. `gen-rfc-index.ts` writes the generated regions;
* `verify-rfc-classification.ts` checks structure and asserts the committed regions are fresh.
* Lifecycle and class sets are closed under `docs/rfc/README.md`; rows derive
* from path, H1, and filename date and sort deterministically. Import is pure.
*/
import { readFileSync, readdirSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import { globSync } from 'node:fs'
export const rfcRoot = resolve(import.meta.dirname, '../docs/rfc')
/** The closed set of RFC lifecycles (top-level folders under docs/rfc/). */
const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const
/**
* The closed set of RFC classes (nested folder under each lifecycle). Adding a
* class is a deliberate act: extend this list AND the README's Classification
* section. The gate rejects any folder not listed here.
*/
const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const
/** Non-RFC Markdown allowed to sit directly at a lifecycle root. */
const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md'])
/** Title-case a class/lifecycle folder name for a README heading. */
const heading = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1)
/** One RFC file, as discovered by the walker. */
export interface Rfc {
lifecycle: string
cls: string
base: string
/** Path relative to docs/rfc — the README link target. */
rel: string
/** H1 text with any `RFC: ` prefix stripped — the README row title. */
title: string
/** `yyyy-mm-dd` from the filename — the "First proposed" column. */
date: string
}
/**
* Walk the RFC tree, enforcing the structure rules. Returns every valid RFC
* plus one error string per violation (unknown lifecycle or class folder, bad
* depth, bad filename, missing/malformed H1). Callers treat a non-empty error
* list as fatal — the index is only generated from a structurally valid tree.
*/
export function walkRfcTree(): { rfcs: Rfc[]; errors: string[] } {
const rfcs: Rfc[] = []
const errors: string[] = []
// The lifecycle set is closed too: any directory under docs/rfc/ that is not
// a known lifecycle would otherwise hold RFCs invisible to the walk below.
for (const entry of readdirSync(rfcRoot, { withFileTypes: true })) {
if (entry.isDirectory() && !(LIFECYCLES as readonly string[]).includes(entry.name)) {
errors.push(`structure: ${entry.name}/ — unknown lifecycle folder (allowed: ${LIFECYCLES.join(', ')})`)
}
}
for (const lifecycle of LIFECYCLES) {
for (const match of globSync(`${lifecycle}/**/*.md`, { cwd: rfcRoot }).map(path => path.split(sep).join('/')).sort()) {
const segs = match.split('/')
// Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md).
if (segs.length === 2 && ROOT_ALLOWLIST.has(segs[1] ?? '')) continue
// A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME RFC,
// indexed via its English filename; the pairing gate owns its consistency.
if (match.endsWith('.zh.md')) continue
const cls = segs[1]
const base = segs[2]
if (segs.length !== 3 || cls === undefined || base === undefined) {
errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`)
continue
}
if (!(CLASSES as readonly string[]).includes(cls)) {
errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`)
continue
}
if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) {
errors.push(`structure: ${match} — filename must be yyyy-mm-dd-topic.md`)
continue
}
const firstLine = readFileSync(resolve(rfcRoot, match), 'utf8').split('\n', 1)[0] ?? ''
const h1 = /^#\s+(?:RFC:\s+)?(.+?)\s*$/.exec(firstLine)
if (!h1?.[1]) {
errors.push(`title: ${match} — first line must be an H1 (\`# RFC: <title>\` or \`# <title>\`), got: ${JSON.stringify(firstLine)}`)
continue
}
rfcs.push({ lifecycle, cls, base, rel: match, title: h1[1], date: base.slice(0, 10) })
}
}
return { rfcs, errors }
}
/**
* Render one lifecycle's section body: a `### {Class}` heading plus a
* `| Title | First proposed |` table for every non-empty class, in CLASSES
* order, rows sorted by date then filename.
*/
function renderLifecycle(rfcs: Rfc[], lifecycle: string): string {
const sections: string[] = []
for (const cls of CLASSES) {
const rows = rfcs
.filter(r => r.lifecycle === lifecycle && r.cls === cls)
.sort((a, b) => a.date.localeCompare(b.date) || a.base.localeCompare(b.base))
if (rows.length === 0) continue
const table = rows.map(r => `| [${r.title}](${r.rel}) | ${r.date} |`).join('\n')
sections.push(`### ${heading(cls)}\n\n| Title | First proposed |\n|---|---|\n${table}`)
}
return sections.join('\n\n')
}
/**
* Render the complete `docs/rfc/INDEX.md` content: a generated-file banner
* followed by one `## {Lifecycle}` section per lifecycle in canonical order.
* The whole file is generated state — there is no curated region to preserve.
*/
export function renderIndex(rfcs: Rfc[]): string {
const parts = [
'# RFC index',
'',
'Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; `verify-rfc-classification` fails when this file is stale. The curated front door — layout, classification, when to write one, and the in-file format — is [README.md](README.md).',
]
for (const lifecycle of LIFECYCLES) {
parts.push('', `## ${heading(lifecycle)}`, '', renderLifecycle(rfcs, lifecycle))
}
return `${parts.join('\n')}\n`
}
/** Matches an index-shaped table row (a `| [title](lifecycle/…) |` line) — generated state that must not appear in curated prose. */
export const INDEX_ROW = /^\|\s*\[[^\]]+\]\((?:proposed|implemented|rejected)\//

View File

@@ -341,8 +341,8 @@ function docSyncLeafGates(options: {
pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }),
pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }),
pnpmScript('mermaid', 'verify-mermaid'),
pnpmScript('rfc-classification', 'verify-rfc-classification', { label: 'rfc classification' }),
pnpmScript('rfc-format', 'verify-rfc-format', { label: 'rfc format' }),
pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification' }),
pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note 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' }),
@@ -397,7 +397,9 @@ function builtBinSmokeGate(): Gate {
'--config',
'vitest.e2e.config.ts',
'packages/examples/stdio-demo/tests/built-bin.e2e.ts',
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
// The worker-entry packages' built bundles: the only automated proof
// that lib/index.js resolves its sibling lib/worker.cjs under plain node
// (the e2e lane runs unbuilt, so these files self-skip there).

View File

@@ -628,7 +628,7 @@ def build_snapshot_files(
child_ids: list[str],
cwd: Path,
) -> dict[str, str]:
"""Render the SDK result and three persisted logs into stable goldens."""
"""Render the SDK result and three persisted logs into stable expected outputs."""
replacements = [(str(cwd), "{{cwd}}"), (SNAPSHOT_SESSION_ID, "{{parent}}")]
for index, child_id in enumerate(child_ids, start=1):
replacements.append((child_id, f"{{{{child-{index}}}}}"))

View File

@@ -23,13 +23,15 @@
"docs/user/guide/index.md",
"docs/user/guide/quickstart.md",
"docs/user/index.md",
"docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md",
"docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md",
".agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md",
".agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md",
"python/README.md",
"python/sdk-runtime/README.md",
"python/sdk/README.md"
],
"excluded": [
".agents/notes/AGENTS.md",
".agents/notes/implemented/AGENTS.md",
"docs/AGENTS.md",
"docs/config-catalog.md",
"docs/cordis-catalog/",

View File

@@ -50,13 +50,13 @@ 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)
expect(requiresPairByDate('.agents/notes/2026-07-13-before.md', cutoff)).toBe(false)
expect(requiresPairByDate('.agents/notes/2026-07-14-at-cutoff.md', cutoff)).toBe(true)
expect(requiresPairByDate('.agents/notes/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('.agents/notes/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)
})

View File

@@ -1,5 +1,5 @@
{
"comment": "Maps each ` ```ts type-equiv ` block (by doc + declared symbol) to the source symbol it must match verbatim. verify-type-equiv.ts enforces a 1:1 correspondence: every type-equiv block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a type-equiv block; remove it when you remove the block.",
"comment": "Maps each ` ```ts type-equiv ` or ` ```ts public-api ` block (by doc + declared symbol + projection) to the source declaration and original JSDoc it must match. Omit projection for the complete declaration; use public-api with a ` ```ts public-api ` block for a body-stripped public class declaration. verify-type-equiv.ts enforces a 1:1 correspondence: every source-equivalence block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a source-equivalence block; remove it when you remove the block.",
"entries": [
{ "doc": "docs/core-data-structures/core.md", "symbol": "Branded", "source": "packages/util/brand/src/index.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
@@ -18,6 +18,8 @@
{ "doc": "docs/core-data-structures/core.md", "symbol": "HookContext", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "PromptDecision", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContinuationDecision", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "RequestError", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "RequestErrorDecision", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContinuationStop", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "SessionStartSource", "source": "packages/core/agent/src/types.ts" },
@@ -33,6 +35,8 @@
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "TokenUsage", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "AppIdentity", "source": "packages/llm/llm/src/attribution.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "BlockAssembler", "source": "packages/llm/llm/src/assembler.ts", "projection": "public-api" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "LlmAdapter", "source": "packages/llm/llm/src/index.ts", "projection": "public-api" },
{ "doc": "docs/core-data-structures/token-meter.md", "symbol": "TokenMeasurement", "source": "packages/llm/token-meter/src/types.ts" },
{ "doc": "docs/core-data-structures/token-meter.md", "symbol": "TokenSurfaceNode", "source": "packages/llm/token-meter/src/types.ts" },
@@ -47,8 +51,10 @@
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceEventType", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceOp", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceIntent", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionSurface", "source": "packages/core/session/src/surface.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldReplacement", "source": "packages/core/session/src/surface.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldResult", "source": "packages/core/session/src/surface.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "Session", "source": "packages/core/session/src/index.ts", "projection": "public-api" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" },
@@ -151,6 +157,7 @@
{ "doc": "docs/core-data-structures/skills.md", "symbol": "Config", "source": "packages/skill/skill/src/index.ts" },
{ "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionResult", "source": "packages/compact/compact/src/types.ts" },
{ "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionTrigger", "source": "packages/compact/compact/src/index.ts" },
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentCapabilities", "source": "packages/subagent/subagent/src/types.ts" },
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentStartRequest", "source": "packages/subagent/subagent/src/types.ts" },

View File

@@ -0,0 +1,27 @@
/**
* Enforce Agent Note lifecycle/class paths and dated filenames. Structural rules
* are shared with `agent-note-tree.ts`; the closed classification contract lives
* in `.agents/notes/README.md`.
*/
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
import { walkAgentNoteTree } from './agent-note-tree.ts'
const { notes, errors } = walkAgentNoteTree()
// Keep the former homes unavailable so new notes cannot silently escape this tree.
for (const legacyRoot of ['docs/rfc', 'docs/rfcs']) {
if (existsSync(resolve(import.meta.dirname, '..', legacyRoot))) {
errors.push(`legacy-path: ${legacyRoot}/ is forbidden — put Agent Notes under .agents/notes/`)
}
}
if (errors.length === 0) {
console.log(`verify-agent-note-classification: ${notes.length} Agent Note(s) checked, structure consistent.`)
process.exit(0)
}
console.error('verify-agent-note-classification: violations found:')
for (const e of errors) console.error(` ${e}`)
process.exit(1)

View File

@@ -1,22 +1,22 @@
/**
* Enforce RFC headers, lifecycle-specific sections, alternatives, and retired
* Enforce Agent Note headers, lifecycle-specific sections, alternatives, and retired
* marker rules. Classification and filenames belong to the sibling tree gate;
* translation structure belongs to the pairing gate. Exact format and
* grandfathering rules live in `docs/rfc/README.md`.
* grandfathering rules live in `.agents/notes/README.md`.
*/
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { rfcRoot, walkRfcTree } from './rfc-index.ts'
import { agentNoteRoot, walkAgentNoteTree } from './agent-note-tree.ts'
/** The date the format contract landed; the grandfather comment is valid only before it. */
const FORMAT_ADOPTED = '2026-07-05'
/** The exact comment a pre-format RFC carries in place of `## Alternatives considered`. */
const GRANDFATHER = '<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->'
/** The exact comment a pre-format Agent Note carries in place of `## Alternatives considered`. */
const GRANDFATHER = '<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->'
/** The retired debt marker that flagged pre-format bodies; banned so it cannot creep back. */
const LEGACY_MARKER = 'XXX: legacy ADR/RFC body format'
const LEGACY_MARKERS = ['XXX: legacy ADR/RFC body format', 'XXX: legacy ADR/Agent Note body format']
/** Status-line grammar per lifecycle folder. */
const STATUS: Record<string, RegExp> = {
@@ -35,13 +35,13 @@ const REQUIRED: Record<string, string[]> = {
/** Headings banned in `implemented/` — proposal-era spec-speak per the slop checklist. */
const BANNED_IMPLEMENTED = /^## (?:Proposal\b|Plan\b|Migration plan\b|Acceptance criteria\b)/i
const { rfcs, errors } = walkRfcTree()
const { notes, errors } = walkAgentNoteTree()
for (const rfc of rfcs) {
for (const note of notes) {
const fail = (msg: string): void => {
errors.push(`format: ${rfc.rel}${msg}`)
errors.push(`format: ${note.rel}${msg}`)
}
const lines = readFileSync(resolve(rfcRoot, rfc.rel), 'utf8').split('\n')
const lines = readFileSync(resolve(agentNoteRoot, note.rel), 'utf8').split('\n')
// Format tokens inside fenced examples are not document structure.
let inFence = false
const prose = lines.filter((l) => {
@@ -52,11 +52,11 @@ for (const rfc of rfcs) {
return !inFence
})
if (!/^# RFC: \S/.test(lines[0] ?? '')) fail('line 1 must be `# RFC: <title>`')
if (!/^# Agent Note: \S/.test(lines[0] ?? '')) fail('line 1 must be `# Agent Note: <title>`')
if (lines[1] !== '') fail('line 2 must be blank')
const status = STATUS[rfc.lifecycle]
const status = STATUS[note.lifecycle]
if (status !== undefined && !status.test(lines[2] ?? '')) {
fail(`line 3 must match the ${rfc.lifecycle} status grammar (${String(status)})`)
fail(`line 3 must match the ${note.lifecycle} status grammar (${String(status)})`)
}
if (lines[3] !== '') fail('line 4 must be blank')
const statusLines = prose.filter(l => l.startsWith('Status:') && l !== lines[2])
@@ -66,29 +66,29 @@ for (const rfc of rfcs) {
const h2s = prose.filter(l => l.startsWith('## ')).map(l => l.trimEnd())
if (h2s[0] !== '## Problem') fail(`the first section must be \`## Problem\` (got ${JSON.stringify(h2s[0] ?? '<none>')})`)
for (const required of REQUIRED[rfc.lifecycle] ?? []) {
for (const required of REQUIRED[note.lifecycle] ?? []) {
if (!h2s.includes(required)) fail(`missing the required \`${required}\` section`)
}
if (rfc.lifecycle === 'implemented') {
if (note.lifecycle === 'implemented') {
for (const h2 of h2s.filter(h => BANNED_IMPLEMENTED.test(h))) {
fail(`\`${h2}\` is a proposal-era heading; an implemented RFC states what is (fold it into Decision/Consequences/Testing)`)
fail(`\`${h2}\` is a proposal-era heading; an implemented Agent Note states what is (fold it into Decision/Consequences/Testing)`)
}
}
const hasSection = h2s.includes('## Alternatives considered')
const hasGrandfather = prose.includes(GRANDFATHER)
if (hasSection && hasGrandfather) fail('carries both `## Alternatives considered` and the grandfather comment — drop the comment')
if (!hasSection && !hasGrandfather) fail('missing `## Alternatives considered` (a pre-format RFC whose alternatives are not reconstructible carries the grandfather comment instead — see docs/rfc/README.md § The file format)')
if (hasGrandfather && rfc.date >= FORMAT_ADOPTED) fail(`the grandfather comment is only valid for RFCs dated before ${FORMAT_ADOPTED}`)
if (!hasSection && !hasGrandfather) fail('missing `## Alternatives considered` (a pre-format Agent Note whose alternatives are not reconstructible carries the grandfather comment instead — see .agents/notes/README.md § The file format)')
if (hasGrandfather && note.date >= FORMAT_ADOPTED) fail(`the grandfather comment is only valid for Agent Notes dated before ${FORMAT_ADOPTED}`)
if (prose.some(l => l.includes(LEGACY_MARKER))) fail('carries the retired legacy-format debt marker')
if (prose.some(line => LEGACY_MARKERS.some(marker => line.includes(marker)))) fail('carries the retired legacy-format debt marker')
}
if (errors.length === 0) {
console.log(`verify-rfc-format: ${rfcs.length} RFC(s) checked, all conform to docs/rfc/README.md § The file format.`)
console.log(`verify-agent-note-format: ${notes.length} Agent Note(s) checked, all conform to .agents/notes/README.md § The file format.`)
process.exit(0)
}
console.error('verify-rfc-format: violations found:')
console.error('verify-agent-note-format: violations found:')
for (const e of errors) console.error(` ${e}`)
process.exit(1)

View File

@@ -1,7 +1,8 @@
/**
* Verify root-relative `docs/*.md` tokens in repo-authored TypeScript. The
* textual scan requires the extension, checks matching string literals too,
* and excludes built declarations and vendored source.
* Verify root-relative documentation paths in repo-authored TypeScript. The
* textual scan covers `docs/*.md` and `.agents/notes/*.md`, requires the
* extension, checks matching string literals too, and excludes built
* declarations and vendored source.
*/
import { existsSync } from 'node:fs'
@@ -18,9 +19,9 @@ const isExcluded = (p: string): boolean =>
p.includes('/lib/') || p.endsWith('.d.ts') || p.startsWith('vendor/')
/** Root-relative Markdown path token, excluding trailing prose. */
const DOC_REF = /\bdocs\/[A-Za-z0-9._/-]+\.md/g
const DOC_REF = /(?:\bdocs|\.agents\/notes)\/[A-Za-z0-9._/-]+\.md/g
/** Find every broken `docs/….md` reference in one TypeScript file. */
/** Find every broken root-relative documentation reference in one TypeScript file. */
function findViolations(absPath: string): Violation[] {
return findReferenceViolations(root, absPath, DOC_REF, ref => ref, ref => !existsSync(resolve(root, ref)))
}
@@ -30,11 +31,11 @@ const all = files.flatMap(file => findViolations(file.abs))
const checked = files.length
if (all.length === 0) {
console.log(`verify-doc-refs: ${checked} file(s) checked, all docs/*.md references resolve.`)
console.log(`verify-doc-refs: ${checked} file(s) checked, all documentation references resolve.`)
process.exit(0)
}
console.error('verify-doc-refs: broken docs/*.md references found in source comments (target does not exist):')
console.error('verify-doc-refs: broken documentation references found in source comments (target does not exist):')
for (const v of all) {
console.error(` ${v.file}:${v.line} ${v.ref}`)
}

View File

@@ -17,6 +17,7 @@ const root = resolve(import.meta.dirname, '..')
const PATTERNS = [
'README.md',
'README.zh.md',
'.agents/notes/**/*.md',
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',

View File

@@ -14,15 +14,16 @@ import { uniqueRepoFiles } from './repo-files.ts'
const root = resolve(import.meta.dirname, '..')
/** Files to check: doc-typecheck's scope, prompt goldens, and the AGENTS.md pair. */
/** Files to check: doc-typecheck's scope, system-prompt expected outputs, and the AGENTS.md pair. */
const PATTERNS = [
'README.md',
'README.zh.md',
'.agents/notes/**/*.md',
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',
'examples/**/system-prompt.golden.md',
'packages/**/system-prompt.golden.md',
'examples/**/system-prompt.expected.md',
'packages/**/system-prompt.expected.md',
'AGENTS.md',
'packages/AGENTS.md',
]

View File

@@ -17,6 +17,7 @@ const root = resolve(import.meta.dirname, '..')
const PATTERNS = [
'README.md',
'README.zh.md',
'.agents/notes/**/*.md',
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',

View File

@@ -14,6 +14,7 @@ const root = resolve(import.meta.dirname, '..')
/** Markdown + repo-authored TypeScript that may cite package paths. */
const PATTERNS = [
'README.md',
'.agents/notes/**/*.md',
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',

View File

@@ -2,7 +2,7 @@
* Doc-sync gate for the canonical package-README limitations section. It scans
* package manifests, rejects missing or variant sections, and requires one
* top-level bullet; audited packages in {@link NO_LIMITATIONS} must omit it.
* See the [limitations RFC](../docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md).
* See the [limitations Agent Note](../.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md).
*/
import { existsSync, globSync, readFileSync } from 'node:fs'

View File

@@ -1,8 +1,8 @@
/**
* Doc-sync gate for package README Model Experience sections. It validates
* audited package classifications, context-surface fields, package-owned text
* blocks, generated-catalog links, and final-section order. See the
* [Model Experience RFC](../docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md).
* audited package classifications, model/token/KV-cache fields, package-owned
* text blocks, generated-catalog links, and final-section order. See the
* [Model Experience Agent Note](../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md).
*/
import { existsSync, globSync, readFileSync } from 'node:fs'
@@ -12,8 +12,10 @@ import { markdownHeadingLines, markdownProseLines, type MarkdownProseLine } from
const root = resolve(import.meta.dirname, '..')
const HEADING = '## Model Experience'
const LIMITATIONS_HEADING = '## Known Limitations and Deferred Work'
const MODEL_VIEW_LABEL = '**What the model sees**'
const TOKEN_EFFECT_LABEL = '**Token effect**'
const MODEL_VIEW_HEADING = '#### What the model sees'
const TOKEN_EFFECT_HEADING = '#### Token effect'
const KV_CACHE_EFFECT_HEADING = '#### KV Cache effect'
const FIELD_HEADINGS = [MODEL_VIEW_HEADING, TOKEN_EFFECT_HEADING, KV_CACHE_EFFECT_HEADING] as const
type SentenceKind = 'none' | 'indirect'
@@ -34,9 +36,9 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
}
/**
* Packages whose Model Experience is simple enough for one gated sentence.
* Every other package must carry canonical context-surface blocks. A package
* moves on or off this list with the change to its context behavior.
* Packages whose Model Experience is simple enough for one gated sentence plus
* a KV-cache field. Every other package must carry canonical context-surface
* blocks. A package moves on or off this list with its context behavior.
*/
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/bash/bash': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
@@ -53,6 +55,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
'packages/sdk/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
'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/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.' },
@@ -65,7 +68,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
'packages/support/subagent-mock': { kind: 'indirect', reason: 'Only dsh-tool-subagent renders its configured test outcome.' },
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and control-surface plugins own all model rendering over the task registry.' },
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
'packages/ui/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
@@ -92,35 +94,41 @@ interface ContextSurface {
heading: Line
modelView: Line
tokenEffect: Line
kvCacheEffect: Line
title: string
modelViewVerbatimBlocks: number
verbatimBlocks: number
}
/** Validate H4-plus-markdown literals nested after one context surface's fields. */
function validateNestedVerbatim(raw: readonly string[]): { blocks: number; error?: string } {
interface ParsedField {
value: Line
verbatimBlocks: number
}
/** Validate H5-plus-markdown literals nested under one Model Experience field. */
function validateNestedVerbatim(raw: readonly string[], fragments: Set<string>): { blocks: number; error?: string } {
let cursor = 0
while (raw[cursor]?.trim().length === 0) cursor += 1
if (cursor === raw.length) return { blocks: 0 }
let blocks = 0
const fragments = new Set<string>()
while (true) {
while (raw[cursor]?.trim().length === 0) cursor += 1
if (cursor === raw.length) break
if (!/^#### \S/.test(raw[cursor] ?? '')) {
return { blocks, error: 'content after Token effect must be a titled H4 verbatim block' }
if (!/^##### \S/.test(raw[cursor] ?? '')) {
return { blocks, error: 'content after a field paragraph must be a titled H5 verbatim block' }
}
const title = (raw[cursor] as string).slice('#### '.length)
const title = (raw[cursor] as string).slice('##### '.length)
const fragment = headingFragment(title)
if (fragment.length === 0) return { blocks, error: 'verbatim H4 title must be non-empty' }
if (fragment.length === 0) return { blocks, error: 'verbatim H5 title must be non-empty' }
if (fragments.has(fragment)) {
return { blocks, error: `verbatim H4 title ${JSON.stringify(title)} is duplicated within its context surface` }
return { blocks, error: `verbatim H5 title ${JSON.stringify(title)} is duplicated within its context surface` }
}
fragments.add(fragment)
cursor += 1
while (raw[cursor]?.trim().length === 0) cursor += 1
if (raw[cursor] !== '```markdown') {
return { blocks, error: 'each nested verbatim H4 requires an exact ```markdown fence' }
return { blocks, error: 'each nested verbatim H5 requires an exact ```markdown fence' }
}
cursor += 1
const contentStart = cursor
@@ -133,7 +141,7 @@ function validateNestedVerbatim(raw: readonly string[]): { blocks: number; error
return { blocks }
}
/** GitHub-style fragment for the simple ASCII H4 titles allowed by this contract. */
/** GitHub-style fragment for the simple ASCII nested titles allowed by this contract. */
function headingFragment(title: string): string {
return title.toLowerCase().replaceAll('`', '').replaceAll(/[^a-z0-9 _-]/g, '').trim().replaceAll(/\s+/g, '-')
}
@@ -166,6 +174,7 @@ let indirectCount = 0
let verbatimBlockCount = 0
let systemPromptSurfaceCount = 0
let toolSchemaSurfaceCount = 0
let kvCacheEffectCount = 0
for (const [pkg, reason] of Object.entries(NO_MODEL_EXPERIENCE_SECTION)) {
if (!scannedPackages.has(pkg)) {
@@ -259,13 +268,31 @@ for (const packageJson of packageJsons) {
if (sentenceContract !== undefined) {
const pattern = sentenceContract.kind === 'none' ? /^None, as .+\.$/ : /^Indirectly, through .+\.$/
const rawContent = rawSection.filter(line => line.trim().length > 0)
if (content.length !== 1 || rawContent.length !== 1 || !pattern.test(content[0]?.raw ?? '')) {
const sentence = content[0]
const kvCacheHeading = content[1]
const kvCacheEffect = content[2]
if (content.length !== 3 || rawContent.length !== 3 || !pattern.test(sentence?.raw ?? '')) {
const prefix = sentenceContract.kind === 'none' ? 'None, as ' : 'Indirectly, through '
failures.push({ path: readme, message: `must contain exactly one sentence beginning ${JSON.stringify(prefix)} and ending with a period` })
failures.push({ path: readme, message: `must contain exactly one sentence beginning ${JSON.stringify(prefix)} and ending with a period, followed by ${KV_CACHE_EFFECT_HEADING} and one non-empty paragraph` })
continue
}
if (kvCacheHeading?.raw !== KV_CACHE_EFFECT_HEADING
|| kvCacheEffect === undefined
|| /^#{1,6} /.test(kvCacheEffect.raw)
|| kvCacheEffect.raw.trim().length === 0) {
failures.push({ path: readme, message: `line ${kvCacheHeading?.index ?? sentence?.index ?? modelHeading.index}: short Model Experience form requires exact ${KV_CACHE_EFFECT_HEADING} and one non-empty paragraph` })
continue
}
if (sentence === undefined
|| sentence.index !== modelHeading.index + 2
|| kvCacheHeading.index !== sentence.index + 2
|| kvCacheEffect.index !== kvCacheHeading.index + 2) {
failures.push({ path: readme, message: 'short Model Experience sentence, KV-cache H4, and paragraph require one blank line between each element' })
continue
}
if (sentenceContract.kind === 'none') explainedNoneCount += 1
else indirectCount += 1
kvCacheEffectCount += 1
continue
}
@@ -291,8 +318,6 @@ for (const packageJson of packageJsons) {
const end = surfaceStarts[surfaceIndex + 1]?.index ?? content.length
const entries = content.slice(start.index, end)
const heading = entries[0] as Line
const modelView = entries[1]
const tokenEffect = entries[2]
const title = heading.raw.slice('### '.length)
const fragment = headingFragment(title)
if (fragment.length === 0) {
@@ -305,56 +330,100 @@ for (const packageJson of packageJsons) {
surfaceError = true
break
}
if (modelView === undefined || !modelView.raw.startsWith(`${MODEL_VIEW_LABEL}: `) || modelView.raw.slice(`${MODEL_VIEW_LABEL}: `.length).trim().length === 0) {
failures.push({ path: readme, message: `line ${modelView?.index ?? heading.index}: context surface requires non-empty ${MODEL_VIEW_LABEL}: text` })
surfaceError = true
break
}
if (tokenEffect === undefined || !tokenEffect.raw.startsWith(`${TOKEN_EFFECT_LABEL}: `) || tokenEffect.raw.slice(`${TOKEN_EFFECT_LABEL}: `.length).trim().length === 0) {
failures.push({ path: readme, message: `line ${tokenEffect?.index ?? heading.index}: context surface requires non-empty ${TOKEN_EFFECT_LABEL}: text` })
const fieldStarts = entries
.map((line, index) => ({ line, index }))
.filter(entry => /^#### \S/.test(entry.line.raw))
if (fieldStarts.length !== FIELD_HEADINGS.length || fieldStarts[0]?.index !== 1) {
failures.push({ path: readme, message: `line ${heading.index}: context surface requires exactly three ordered H4 fields: ${FIELD_HEADINGS.join(', ')}` })
surfaceError = true
break
}
if ((surfaceIndex === 0 && heading.index !== modelHeading.index + 2)
|| rawLines[heading.index - 2]?.trim().length !== 0
|| modelView.index !== heading.index + 2
|| tokenEffect.index !== modelView.index + 2) {
failures.push({ path: readme, message: `line ${heading.index}: context-surface heading and fields require one blank line between each element` })
|| fieldStarts[0].line.index !== heading.index + 2) {
failures.push({ path: readme, message: `line ${heading.index}: context-surface heading and first field require one blank line between them` })
surfaceError = true
break
}
const unexpected = entries.slice(3).find(line => !/^#### \S/.test(line.raw))
if (unexpected !== undefined) {
failures.push({ path: readme, message: `line ${unexpected.index}: content after ${TOKEN_EFFECT_LABEL} must be a titled H4 plus \`markdown\` fence inside this context surface` })
surfaceError = true
break
const parsedFields: ParsedField[] = []
const verbatimFragments = new Set<string>()
for (let fieldIndex = 0; fieldIndex < FIELD_HEADINGS.length; fieldIndex += 1) {
const fieldStart = fieldStarts[fieldIndex] as { line: Line; index: number }
const expectedHeading = FIELD_HEADINGS[fieldIndex] as string
if (fieldStart.line.raw !== expectedHeading) {
failures.push({ path: readme, message: `line ${fieldStart.line.index}: expected exact field heading ${JSON.stringify(expectedHeading)}, found ${JSON.stringify(fieldStart.line.raw)}` })
surfaceError = true
break
}
const fieldEnd = fieldStarts[fieldIndex + 1]?.index ?? entries.length
const fieldEntries = entries.slice(fieldStart.index, fieldEnd)
const value = fieldEntries[1]
if (value === undefined || /^#{1,6} /.test(value.raw) || value.raw.trim().length === 0) {
failures.push({ path: readme, message: `line ${fieldStart.line.index}: ${expectedHeading} requires one non-empty paragraph` })
surfaceError = true
break
}
if (value.index !== fieldStart.line.index + 2) {
failures.push({ path: readme, message: `line ${fieldStart.line.index}: ${expectedHeading} and its paragraph require one blank line between them` })
surfaceError = true
break
}
const unexpected = fieldEntries.slice(2).find(line => !/^##### \S/.test(line.raw))
if (unexpected !== undefined) {
failures.push({ path: readme, message: `line ${unexpected.index}: content after ${expectedHeading} paragraph must be a titled H5 plus \`markdown\` fence owned by that field` })
surfaceError = true
break
}
const nextHeadingLine = fieldStarts[fieldIndex + 1]?.line.index
?? surfaceStarts[surfaceIndex + 1]?.line.index
?? nextH2Line
if (rawLines[nextHeadingLine - 2]?.trim().length !== 0) {
failures.push({ path: readme, message: `line ${nextHeadingLine}: Model Experience headings require a preceding blank line` })
surfaceError = true
break
}
const verbatim = validateNestedVerbatim(rawLines.slice(value.index, nextHeadingLine - 1), verbatimFragments)
if (verbatim.error !== undefined) {
failures.push({ path: readme, message: `line ${value.index}: ${verbatim.error}` })
surfaceError = true
break
}
if (fieldEntries.length - 2 !== verbatim.blocks) {
failures.push({ path: readme, message: `line ${value.index}: every nested H5 must own exactly one \`markdown\` fence` })
surfaceError = true
break
}
parsedFields.push({ value, verbatimBlocks: verbatim.blocks })
}
const nextHeadingLine = surfaceStarts[surfaceIndex + 1]?.line.index ?? nextH2Line
const verbatim = validateNestedVerbatim(rawLines.slice(tokenEffect.index, nextHeadingLine - 1))
if (verbatim.error !== undefined) {
failures.push({ path: readme, message: `line ${tokenEffect.index}: ${verbatim.error}` })
surfaceError = true
break
}
if (entries.length - 3 !== verbatim.blocks) {
failures.push({ path: readme, message: `line ${tokenEffect.index}: every nested H4 must own exactly one \`markdown\` fence` })
surfaceError = true
break
}
if (/\]\(#[^)]+\)/.test(modelView.raw) || /\]\(#[^)]+\)/.test(tokenEffect.raw)) {
failures.push({ path: readme, message: `line ${heading.index}: Model Experience fields must not link between local subsections; nest the H4 in its owning H3` })
if (surfaceError) break
const modelViewField = parsedFields[0] as ParsedField
const tokenEffectField = parsedFields[1] as ParsedField
const kvCacheEffectField = parsedFields[2] as ParsedField
const modelView = modelViewField.value
const tokenEffect = tokenEffectField.value
const kvCacheEffect = kvCacheEffectField.value
if (/\]\(#[^)]+\)/.test(modelView.raw) || /\]\(#[^)]+\)/.test(tokenEffect.raw) || /\]\(#[^)]+\)/.test(kvCacheEffect.raw)) {
failures.push({ path: readme, message: `line ${heading.index}: Model Experience fields must not link between local subsections; nest the H5 in its owning H4 field` })
surfaceError = true
break
}
surfaceFragments.add(fragment)
surfaces.push({ heading, modelView, tokenEffect, title, verbatimBlocks: verbatim.blocks })
surfaces.push({
heading,
modelView,
tokenEffect,
kvCacheEffect,
title,
modelViewVerbatimBlocks: modelViewField.verbatimBlocks,
verbatimBlocks: parsedFields.reduce((total, field) => total + field.verbatimBlocks, 0),
})
}
if (surfaceError) continue
const promptWithoutVerbatim = surfaces.find(surface => isDirectSystemPromptSurface(surface.title)
&& surface.verbatimBlocks === 0)
&& surface.modelViewVerbatimBlocks === 0)
if (promptWithoutVerbatim !== undefined) {
failures.push({ path: readme, message: `line ${promptWithoutVerbatim.heading.index}: system-prompt surface must contain a titled H4 plus verbatim \`markdown\` block` })
failures.push({ path: readme, message: `line ${promptWithoutVerbatim.heading.index}: system-prompt surface must contain a titled H5 plus verbatim \`markdown\` block under ${MODEL_VIEW_HEADING}` })
continue
}
const hasConcreteLiteral = surfaces.some(surface => surface.verbatimBlocks > 0
@@ -386,11 +455,12 @@ for (const packageJson of packageJsons) {
contextSurfaceCount += surfaces.length
systemPromptSurfaceCount += surfaces.filter(surface => isDirectSystemPromptSurface(surface.title)).length
toolSchemaSurfaceCount += surfaces.filter(surface => /\bschemas?\b/i.test(surface.title)).length
kvCacheEffectCount += surfaces.length
structuredCount += 1
}
if (failures.length === 0) {
console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${omittedSectionCount} audited omissions, ${structuredCount} structured, ${contextSurfaceCount} context surfaces, ${systemPromptSurfaceCount} fenced system-prompt surfaces, ${toolSchemaSurfaceCount} catalog-linked tool-schema surfaces, ${explainedNoneCount} explained none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim markdown blocks), all conform.`)
console.log(`verify-package-readme-model-experience: ${packageJsons.length} README(s) checked (${omittedSectionCount} audited omissions, ${structuredCount} structured, ${contextSurfaceCount} context surfaces, ${kvCacheEffectCount} KV-cache fields, ${systemPromptSurfaceCount} fenced system-prompt surfaces, ${toolSchemaSurfaceCount} catalog-linked tool-schema surfaces, ${explainedNoneCount} explained none, ${indirectCount} indirect, ${verbatimBlockCount} verbatim markdown blocks), all conform.`)
process.exit(0)
}

View File

@@ -1,39 +0,0 @@
/**
* Enforce RFC lifecycle/class paths, dated filenames, and titles; verify the
* generated index and reject index rows in the curated README. Structural rules
* and rendering are shared with `rfc-index.ts`; the closed classification
* contract lives in `docs/rfc/README.md`.
*/
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { INDEX_ROW, renderIndex, rfcRoot, walkRfcTree } from './rfc-index.ts'
const { rfcs, errors } = walkRfcTree()
if (errors.length === 0) {
let index: string | undefined
try {
index = readFileSync(resolve(rfcRoot, 'INDEX.md'), 'utf8')
} catch {
// A missing INDEX.md is reported below as staleness, exactly like a drifted one.
}
if (renderIndex(rfcs) !== index) {
errors.push('index: docs/rfc/INDEX.md is stale or missing — run `pnpm run gen-rfc-index` and commit the result')
}
const readme = readFileSync(resolve(rfcRoot, 'README.md'), 'utf8')
for (const line of readme.split('\n')) {
if (INDEX_ROW.test(line)) {
errors.push(`readme: index-shaped row in the curated README (the list lives in INDEX.md): ${JSON.stringify(line.slice(0, 80))}`)
}
}
}
if (errors.length === 0) {
console.log(`verify-rfc-classification: ${rfcs.length} RFC(s) checked, structure and index consistent.`)
process.exit(0)
}
console.error('verify-rfc-classification: violations found:')
for (const e of errors) console.error(` ${e}`)
process.exit(1)

View File

@@ -24,8 +24,18 @@ const root = resolve(import.meta.dirname, '..')
const listMode = process.argv.includes('--list')
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']
/** Scope of the bilingual contract: root docs, Agent Notes, the docs tree, and the Python SDK tree. */
const SCOPE_PATTERNS = [
'README.md',
'README.zh.md',
'README.i18n.yaml',
'.agents/notes/**/*.md',
'.agents/notes/**/*.i18n.yaml',
'docs/**/*.md',
'docs/**/*.i18n.yaml',
'python/**/*.md',
'python/**/*.i18n.yaml',
]
const manifest = parseTranslationPairingManifest(readFileSync(join(root, 'scripts/translation-pairing.manifest.json'), 'utf8'))
@@ -121,8 +131,8 @@ 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
// 2. Date-named documents (Agent Notes) dated on/after the requiredSince cutoff merge
// bilingual: a new Agent Note lands with its pair or not at all. Deterministic from
// the filename alone — no git history, so it holds on shallow CI checkouts.
for (const source of sources) {
if (isExcluded(source)) continue

View File

@@ -1,7 +1,10 @@
/**
* Verify every `ts type-equiv` block against the source symbol named by the
* manifest. Blocks and entries have a one-to-one relationship; comparison
* ignores comments and whitespace but preserves declaration structure.
* Verify every `ts type-equiv` and `ts public-api` block against the source
* symbol named by the manifest. Ordinary entries preserve the complete
* declaration; `public-api` entries preserve a class's body-stripped public
* declaration. Blocks and entries have a one-to-one relationship; comparison
* ignores whitespace and non-JSDoc comments but preserves declaration
* structure and every original JSDoc comment.
*/
import { globSync, readFileSync, existsSync } from 'node:fs'
@@ -11,35 +14,35 @@ import ts from 'typescript'
const root = resolve(import.meta.dirname, '..')
/** Scan doc-typecheck's full Markdown scope so unmanifested blocks also fail. */
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const MARKDOWN_GLOBS = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
/** One manifest entry: a documented type-equiv block and its source symbol. */
/** One manifest entry: a source-equivalence block and its source symbol. */
interface ManifestEntry {
/** Doc file (repo-relative) containing the ` ```ts type-equiv ` block. */
/** Doc file (repo-relative) containing the source-equivalence block. */
doc: string
/** The declared symbol the block must match (e.g. `SessionEvent`). */
symbol: string
/** Source file (repo-relative) that exports the symbol. */
source: string
/** Complete declaration (default), or a body-stripped public class API. */
projection?: 'public-api'
}
/** One extracted ` ```ts type-equiv ` block. */
/** One extracted ` ```ts type-equiv ` or ` ```ts public-api ` block. */
interface EquivBlock {
doc: string
/** 1-based line of the opening fence (for diagnostics). */
line: number
/** Symbol name parsed from the block's declaration. */
symbol: string
/** Complete declaration (default), or a body-stripped public class API. */
projection?: 'public-api'
/** Block body (the pasted declaration). */
code: string
}
/**
* Remove comments and normalize whitespace so prose-only edits do not drift
* structural copies. This is intentionally not a general tokenizer: repo type
* declarations do not contain comment delimiters inside string literals.
*/
function normalize(code: string): string {
/** Normalize declaration structure independently of comments and whitespace. */
function normalizeStructure(code: string): string {
return code
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1')
@@ -47,23 +50,38 @@ function normalize(code: string): string {
.trim()
}
/**
* Extract normalized JSDoc comments in source order. Type declarations in this
* repository do not contain comment delimiters inside string literals.
*/
function normalizeJSDoc(code: string): string[] {
return [...code.matchAll(/\/\*\*[\s\S]*?\*\//g)]
.map(match => match[0].replace(/\s+/g, ' ').trim())
}
/** Strip source-only export modifiers. */
function stripExport(code: string): string {
return code.replace(/^export\s+(default\s+)?/, '')
}
/** Parse the declared symbol name from a type-equiv block body. */
/** Parse the declared symbol name from a source-equivalence block body. */
function blockSymbol(code: string): string | null {
const m = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?(?:interface|type|class|enum)\s+([A-Za-z0-9_]+)/.exec(code)
return m?.[1] ?? null
const sf = ts.createSourceFile('type-equiv.ts', code, ts.ScriptTarget.Latest, /* setParentNodes */ false, ts.ScriptKind.TS)
for (const stmt of sf.statements) {
const named =
ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)
|| ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt)
if (named && stmt.name) return stmt.name.text
}
return null
}
/** Extract every ` ```ts type-equiv ` block from one Markdown file. */
/** Extract every source-equivalence block from one Markdown file. */
function extractEquivBlocks(docRel: string): EquivBlock[] {
const text = readFileSync(resolve(root, docRel), 'utf8')
const lines = text.split('\n')
const blocks: EquivBlock[] = []
let open: { line: number; body: string[] } | null = null
let open: { line: number; body: string[]; projection?: 'public-api' } | null = null
for (let i = 0; i < lines.length; i++) {
const raw = lines[i] ?? ''
@@ -78,21 +96,33 @@ function extractEquivBlocks(docRel: string): EquivBlock[] {
if (!symbol) {
throw new Error(`verify-type-equiv: ${docRel}:${open.line} — type-equiv block has no parseable interface/type/class declaration`)
}
blocks.push({ doc: docRel, line: open.line, symbol, code })
blocks.push({
doc: docRel,
line: open.line,
symbol,
code,
...(open.projection === undefined ? {} : { projection: open.projection }),
})
open = null
continue
}
if ((fence[2] ?? '').trim() === 'ts type-equiv') open = { line: i + 1, body: [] }
const info = (fence[2] ?? '').trim()
if (info === 'ts type-equiv public-api') {
throw new Error(`verify-type-equiv: ${docRel}:${i + 1} — use the concise \`ts public-api\` fence`)
}
if (info === 'ts type-equiv') open = { line: i + 1, body: [] }
if (info === 'ts public-api') open = { line: i + 1, body: [], projection: 'public-api' }
}
if (open) throw new Error(`verify-type-equiv: ${docRel}:${open.line} — unterminated type-equiv block`)
return blocks
}
/** The declaration text of `symbol` in `sourceRel`, with `export` stripped, or
/**
* The declaration text of `symbol` in `sourceRel`, with `export` stripped, or
* null when the symbol is not declared there. Uses the TS parser so it spans
* interfaces, type aliases (including mapped/generic ones), classes, and enums
* uniformly, and excludes the leading JSDoc (getStart skips leading trivia)
* while keeping inline member comments. */
* uniformly while including declaration and member JSDoc.
*/
function sourceDeclaration(sourceRel: string, symbol: string): string | null {
const abs = resolve(root, sourceRel)
const text = readFileSync(abs, 'utf8')
@@ -102,19 +132,89 @@ function sourceDeclaration(sourceRel: string, symbol: string): string | null {
ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)
|| ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt)
if (named && stmt.name?.text === symbol) {
return stripExport(stmt.getText(sf))
const declarationStart = stmt.getStart(sf)
const jsDoc = ts.getJSDocCommentsAndTags(stmt)
.filter(ts.isJSDoc)
.map(doc => text.slice(doc.pos, doc.end))
.join('\n')
const declaration = stripExport(text.slice(declarationStart, stmt.getEnd()))
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
}
}
return null
}
/** Leading source JSDoc attached to one declaration or member. */
function sourceJSDoc(text: string, node: ts.Node): string {
return ts.getJSDocCommentsAndTags(node)
.filter(ts.isJSDoc)
.map(doc => text.slice(doc.pos, doc.end))
.join('\n')
}
/** Whether a class member is part of its public declaration. */
function isPublicMember(member: ts.ClassElement): boolean {
if (ts.isClassStaticBlockDeclaration(member)) return false
const name = ts.getNameOfDeclaration(member)
if (name && ts.isPrivateIdentifier(name)) return false
const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined
return !(modifiers?.some(modifier =>
modifier.kind === ts.SyntaxKind.PrivateKeyword
|| modifier.kind === ts.SyntaxKind.ProtectedKeyword,
) ?? false)
}
/** Remove an implementation body while retaining the source signature. */
function bodylessMember(text: string, sf: ts.SourceFile, member: ts.ClassElement): string {
const start = member.getStart(sf)
let end = member.end
if (ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member)
|| ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) {
if (member.body) end = member.body.getStart(sf)
}
if (ts.isPropertyDeclaration(member) && member.initializer) end = member.initializer.getStart(sf)
const signature = text.slice(start, end).trimEnd().replace(/;$/, '').replace(/=\s*$/, '').trimEnd()
return `${signature};`
}
/**
* Render a class as an ambient declaration containing only its public fields,
* constructor, accessors, and methods. Implementation bodies and private or
* protected members are deliberately absent; original class/member JSDoc is
* retained so the projection is the source-owned public contract.
*/
function sourcePublicApi(sourceRel: string, symbol: string): string | null {
const abs = resolve(root, sourceRel)
const text = readFileSync(abs, 'utf8')
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /* setParentNodes */ true)
for (const stmt of sf.statements) {
if (!ts.isClassDeclaration(stmt) || stmt.name?.text !== symbol) continue
const classDoc = sourceJSDoc(text, stmt)
const abstract = stmt.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AbstractKeyword) ? 'abstract ' : ''
const typeParameters = stmt.typeParameters?.map(parameter => parameter.getText(sf)).join(', ')
const heritage = stmt.heritageClauses?.map(clause => clause.getText(sf)).join(' ')
const header = `declare ${abstract}class ${symbol}${typeParameters ? `<${typeParameters}>` : ''}${heritage ? ` ${heritage}` : ''} {`
const members = stmt.members
.filter(isPublicMember)
.map((member) => {
const jsDoc = sourceJSDoc(text, member)
const declaration = bodylessMember(text, sf, member)
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
})
const declaration = [header, ...members.map(member => member.split('\n').map(line => ` ${line}`).join('\n')), '}'].join('\n')
return classDoc === '' ? declaration : `${classDoc}\n${declaration}`
}
return null
}
const manifestRaw = readFileSync(resolve(root, 'scripts/type-equiv.manifest.json'), 'utf8')
const manifest = JSON.parse(manifestRaw) as { entries: ManifestEntry[] }
const entries = manifest.entries
// Key a block/entry by doc + symbol (a symbol may be documented in more than one
// doc, but at most once per doc).
const keyOf = (x: { doc: string; symbol: string }): string => `${x.doc}::${x.symbol}`
// Key a block/entry by doc + symbol + projection. A symbol may be documented in
// more than one doc, and a doc may carry both complete and projected forms.
const keyOf = (x: { doc: string; symbol: string; projection?: 'public-api' }): string =>
`${x.doc}::${x.symbol}::${x.projection ?? 'declaration'}`
// Collect every type-equiv block across ALL docs in scope — not only the docs
// the manifest names — so a block in an unmanifested doc is found and reported
@@ -133,7 +233,7 @@ for (const d of [...new Set(entries.map(e => e.doc))]) {
else if (!docSet.has(d)) errors.push(`manifest references ${d}, which is outside the scanned markdown scope (${MARKDOWN_GLOBS.join(', ')})`)
}
// Duplicate-block guard: the same symbol twice in one doc is ambiguous.
// Duplicate-block guard: the same projected symbol twice in one doc is ambiguous.
const blockByKey = new Map<string, EquivBlock>()
for (const b of blocks) {
const k = keyOf(b)
@@ -173,16 +273,25 @@ let verified = 0
for (const e of entries) {
const b = blockByKey.get(keyOf(e))
if (!b) continue // already reported as an orphan entry
const decl = sourceDeclaration(e.source, e.symbol)
const decl = e.projection === 'public-api'
? sourcePublicApi(e.source, e.symbol)
: sourceDeclaration(e.source, e.symbol)
if (decl === null) {
errors.push(`symbol ${e.symbol} not found in ${e.source} (manifest entry for ${e.doc})`)
continue
}
if (normalize(decl) !== normalize(stripExport(b.code))) {
const doc = stripExport(b.code)
const sourceStructure = normalizeStructure(decl)
const docStructure = normalizeStructure(doc)
const sourceJSDoc = normalizeJSDoc(decl)
const docJSDoc = normalizeJSDoc(doc)
if (sourceStructure !== docStructure || JSON.stringify(sourceJSDoc) !== JSON.stringify(docJSDoc)) {
errors.push(
`DRIFT: ${e.doc}:${b.line} — type-equiv block for ${e.symbol} does not match ${e.source}.\n`
+ ` source: ${normalize(decl)}\n`
+ ` doc: ${normalize(stripExport(b.code))}`,
+ ` source structure: ${sourceStructure}\n`
+ ` doc structure: ${docStructure}\n`
+ ` source JSDoc: ${JSON.stringify(sourceJSDoc)}\n`
+ ` doc JSDoc: ${JSON.stringify(docJSDoc)}`,
)
continue
}
@@ -190,7 +299,7 @@ for (const e of entries) {
}
if (errors.length === 0) {
console.log(`verify-type-equiv: ${verified} type-equiv block(s) match source (1:1 with manifest).`)
console.log(`verify-type-equiv: ${verified} type-equiv block(s) match source structure and JSDoc (1:1 with manifest).`)
process.exit(0)
}