# Agent Note: 核心数据结构目录与 `ts type-equiv` 漂移门禁

Status: implemented

[English](2026-06-20-core-data-structures-catalog.md) | 中文

## 问题

试图理解 harness 的读者可以在 [architecture.md](../../../../docs/architecture.md) 中找到它的*行为*（服务图、会话/轮次/步骤生命周期、事件分类），却找不到一个统一描述其*词汇*的地方，也就是这些行为所传递的数据结构。类型形状只存在于源码中，散落在 `packages/*/src/types.ts` 各处，因此要理解“什么是 `Message`、`SessionEvent`、`StreamChunk`”，就必须直接阅读声明。文字目录会有所帮助，但复述或复制粘贴类型定义的目录会在字段发生变化时立即腐化，而不同步的类型文档比没有文档更糟，因为读者会信任它。

因此，这项工作有两个相互交织的问题：**这样的目录应包含什么**（范围问题——harness 有数十种跨包（package）边界的类型，把它们全部倾倒进来对谁都没有帮助），以及**如何避免粘贴的类型定义发生漂移**（持久性问题）。本 Agent Note（agent 决策记录）记下了这两项决策。与它配套的[生成式 Cordis 事件与服务目录](2026-06-20-generated-cordis-catalog.md)从*接线*维度形成补充：本文对数据结构编目，另一篇则对传递这些结构的事件和服务编目。

## 决策

新增的 `docs/core-data-structures/` 目录对这些词汇编目，并配有新的 `verify-type-equiv` 文档同步门禁，使每个粘贴的类型声明及其 JSDoc 与源码保持同步。

### 何为"核心"——主干与 seam 的分界线

范围界定并非自上而下拍定，而是将候选定义逐一对照具体的边界类型反复测试，直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`：bash 是一个能力 *seam*，不属于 agent loop（智能体循环）主干；如果这些算"核心"，那么"核心"就意味着*所有跨包词汇*，目录沦为平铺罗列；如果不算，"核心"就意味着*中央主干*，bash 词汇归入子页面。后者胜出，由此确定了整体结构：一个**分层文件夹**，而非一份平铺文档。

确定其余案例的规则是：***你编写、持有或接收的类型是核心；为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下：

- 一个数据结构是**核心**的，如果它流经 agent loop 主干——无论加载了哪些插件，循环在每个轮次都会持有、派生、流式输出或记录它（`Message`、`StreamChunk`、`SessionEvent`、`Agent` 句柄）——**或者**它是插件作者面对某条流水线时编写的唯一标志性类型（`ToolDefinition`）。
- `ToolDefinition` 是核心（它是每个工具作者编写的东西），**即使循环从不持有它**——对于这一个标志性类型，撰写重要性压过了严格的"流经主干"规则。但它的类型推导机制——`ValueSchemaSpec`、`ParameterSchemaSpec`、`InferValue` 与 `InferArgs`——是子页面细节。这就是主干与 seam 分界线的精确表述。
- `ToolSchema` 是核心（它是流经每个步骤的模型请求 `GenerateOptions` 的一个字段），即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时，前者胜出。
- 工具展示词汇（`ToolCallView`/`ToolResultView` 等）、`SessionPersistence` 持久性 seam 以及 bash 词汇是子页面。

`core.md` 是一份**自包含的主干文档**：它给出每个主干结构的确切类型定义，辅以最少的行文，并链接到子页面获取各 seam 的细节。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`（沿内存模型与持久性 seam 的分界线从会话页面拆出）、`tools.md` 和 `bash.md`。

### `ts type-equiv` 机制——既逐字又防漂移

持久性要求很具体：文档展示当前类型声明与原始 JSDoc 的**逐字**内容（让读者看到真实形状和源码契约，而非复述），**并且**以机械方式保证其与源码匹配。仓库已经会编译 ` ```ts ` 围栏块（`doc-typecheck`），但真正接受类型检查的块需要导入噪音，而且只能证明*可赋值性*——字段改名或 JSDoc 变化仍可能通过。因此：

- 完整的类型声明及其 JSDoc 会逐字粘贴到专用的 ` ```ts type-equiv ` 围栏中。简洁的 ` ```ts public-api ` 围栏承载与源码等价的类环境投影，用于实现体不应进入目录的类。`doc-typecheck` 会识别并跳过这两种围栏（裸声明无法独立编译），并且**将它们排除在 opt-out 比例之外**——它们是单独受检的类别，而不是未经检查的草图。
- 新增的 `scripts/verify-type-equiv.ts` 通过 TypeScript 解析器提取每个块，并断言其声明结构和每条 JSDoc 注释都与所声明的符号匹配，只忽略格式空白和非 JSDoc 注释。普通块保留完整声明。`public-api` 投影保留类的公共字段、构造函数、访问器和方法及其原始 JSDoc，同时移除实现体以及私有或受保护成员。之所以选择它而非编译式 `_Check` 断言，是因为目录所保留的是源码名称与文档一致性，而不是可赋值性。
- 来源信息存放在集中的 `scripts/type-equiv.manifest.json`（`{ doc, symbol, source }` 条目）中，**而非**行文中的指令注释。脚本在每个主 type-equiv 块与一条 manifest 条目之间强制执行 **1:1 对应**，因此一个块永远不会被静默漏检，一条条目也永远不会腐烂。只有当配对 `.zh.md` 块的完整受跟踪围栏序列在顺序、类型和按字节精确的正文上均与无后缀兄弟文件匹配时，才会复用后者的条目；否则门禁会独立检查该块，发现没有 manifest 条目后失败。
- 接入 `doc-sync`，因此相关文档变更会在本地运行它，CI 也会与其他文档检查一起运行它。

### 维护是作者的职责，门禁作为兜底

`verify-type-equiv` 能捕获已记录类型的*粘贴漂移*，但无法告诉你一个全新的核心类型没有被记录。因此 AGENTS.md 和 `dsh-code-review` skill（技能）已更新，要求在变更添加或重塑已记录类型时同步更新目录——门禁处理漂移，人处理新增表面。

## 曾考虑的替代方案

- **平铺罗列所有跨包词汇**：`BashExecRequest` 测试案例否决了它。如果 seam 词汇算"核心"，目录对谁都没帮助；分层的主干与 seam 结构胜出。
- **用编译式 `_Check` 可赋值性断言**代替源码匹配：否决。可赋值性不会保留名称或 JSDoc；同类型字段改名或契约注释变化仍会通过。
- **来源信息作为行文中的指令注释**：否决，改用集中 manifest；其强制的 1:1 对应确保一个块永远不会被静默漏检，一条条目也永远不会腐烂。

## 验证教训

主干与 seam 规则在采纳前经过了 `BashExecRequest`、工具 schema 与定义、schema DSL、展示类型以及会话/持久化拆分的逐一测试。

`verify-type-equiv` 必须扫描完整的 Markdown 范围，而不仅是清单点名的文档。否则，未列入清单的 `type-equiv` 块就会逃过所宣称的一一检查。因此，门禁会将此类块报告为孤儿。本 Agent Note 将这条失败关闭扫描规则，连同主干与 seam 的分界决策及逐字匹配决策一并记录；生成式 Cordis 目录在[其 Agent Note](2026-06-20-generated-cordis-catalog.md) 中有对称的设计记录。

## 后果

- 这些词汇现在有一个**无法悄然漂移**的唯一归属：源码中的字段或公共类成员发生变化后，`doc-sync` 和 CI 中的 `verify-type-equiv` 会持续失败，直至粘贴内容刷新。Cordis 服务方法仍由生成式服务目录负责，而不会在此重复。
- 主干与 seam 分界线是一个可复用的范围界定工具，而非一次性的：同一条「你编写/持有/接收的东西是核心；为其提供类型推导/渲染/持久化的机制是细节」规则，后来也被用于界定事件/服务目录的 harness 层与继承层分层。
- `ts type-equiv` 围栏是继 ` ```ts `（编译）和 ` ```ts ignore-check `（草稿）之后的第三种文档块类别。后续的姊妹门禁又增加了第四种 ` ```ts cordis-catalog `（生成签名），复用了相同的跳过并排除处理。
- 添加或重塑核心类型现在附带一项文档义务，作者必须履行（门禁无法检测缺失的*新*类型），由 `dsh-code-review` 检查清单兜底。
