Generated cordis events + services catalog
生成的 Cordis 事件与服务目录
A plugin author needs two reference surfaces that no single document gave them: every cordis **event** they can listen to (with its exact signature and dispatch mode) and every `ctx.<key>` **service** they can call (with its exact interface). The pieces existed but were scattered — a hand-maintained event-taxonomy *table* in `docs/architecture.md` (names + prose Mode/Purpose, name-set-checked by `verify-event-taxonom
English
Problem
A plugin author needs two reference surfaces that no single document gave them: every cordis event they can listen to (with its exact signature and dispatch mode) and every ctx.<key> service they can call (with its exact interface). The pieces existed but were scattered — a hand-maintained event-taxonomy table in docs/architecture.md (names + prose Mode/Purpose, name-set-checked by verify-event-taxonomy), a Service-map table (8 rows of role prose), and the interface Events / interface Context declarations themselves. The taxonomy table also could not catch a brand-new undocumented event: a name-set verifier only checks the names that are already in the table on both sides.
This is the wiring-axis complement to the core-data-structures catalog (its Agent Note): that one catalogs the data structures the loop moves around (verified hand-pastes); this one catalogs the events and services that move them.
Decision
Generate the catalog from source instead of hand-maintaining a table and verifying a subset.
scripts/gen-cordis-catalog.ts uses the TypeScript compiler API to emit separate event and service references from declarations and source JSDoc. Events include dispatch modes and their original member JSDoc; services include public signatures with each method's original JSDoc. Deterministic --write and --check modes make both pages generated artifacts, with freshness enforced by doc-sync.
Pure generation is correct here because the codebase is disciplined enough that the AST is the whole truth: every event/service name is a string literal that round-trips to a static declaration — there are no dynamically-named events and no runtime-only services. So a generated doc cannot be wrong, and it closes the undocumented-event gap structurally (generation enumerates source rather than checking a hand-written subset).
Specific choices:
@modetag, cross-checked. Each harness event's JSDoc carries an explicit@mode emit|waterfall|parallel|serialtag; the generator hard-errors on a missing tag. Where the signature shape is conclusive — a trailingnext: () => …parameter is structurally a waterfall — it asserts the tag agrees and hard-errors on a contradiction. The emit/parallel/serial distinction is not structurally visible (session/flushreturnsPromise<void> | voidwith nonext, as does the orderedagent/pre-stepcheckpoint), so it is trusted from the tag. The authoring rule lives in AGENTS.md.- Tiered scope. The harness tier (the 8
@deepseek-ai/dsh-*services + their events) is rendered in full from source. The inherited tier (cordis-corectx.on/emit/effect/provide/…+ theinternal/*events + loader/hmr/timer) is pinned vendor source a plugin also sees; it is rendered tersely (name + one-line + source pointer) from a curated table in the generator, NOT walked from the vendor AST — the cordis-coreContextmixes true ctx members with non-service fields (root,baseUrl,logger), and the vendor surface changes only on a deliberate vendor sync. - Cross-links to the data-structure catalog. Every repository-owned type name in a signature (
GenerateOptions,StreamChunk,ToolDefinition, …) links to its primary core-data-structures page through a curated map. The AST walk is fail-closed: each parameter, generic constraint/default, and return-type reference must be mapped, be the signature's own type parameter, be a named TypeScript/Cordis foundation type, or carry a named exception with its non-catalog documentation owner. Violations aggregate with source pointers and name the appropriate owning lists. The map does NOT reusetype-equiv.manifest.json, which documents…Mapsymbols while signatures reference derived union names and lists some symbols on multiple pages. - A dedicated fence. Signature blocks use a
```ts cordis-cataloginfo string and place the original event or public-method JSDoc immediately before its declaration.doc-typecheckrecognizes and skips the bare fragments, excluding them from the opt-out ratio — the same treatmenttype-equivblocks get.
This supersedes the event-taxonomy half of doc-sync enforcement: verify-event-taxonomy and its docs/architecture.md table are retired (the architecture.md heading stays, its body now points at the catalog; the Service-map role table stays as curated prose). doc-typecheck, verify-md-wrap, verify-md-links, and verify-type-equiv are unchanged.
Alternatives considered
- Verify-don't-generate, as the retired taxonomy check did — reversed for this surface only: the data here is mechanically complete, so generation is strictly stronger (full signatures, cannot drift, catches undocumented events) than a name-set check of a hand-maintained table.
- Walking the vendor AST for the inherited tier — rejected for the curated table: the cordis-core
Contextmixes true ctx members with non-service fields, and the pinned vendor surface changes only on a deliberate sync. - Reusing
type-equiv.manifest.jsonas the signature cross-link map — rejected for a complete curated const plus fail-closed coverage: the manifest documents…Mapsymbols while signatures reference derived union names, and it lists some symbols on multiple pages. The explicit map makes each rendered destination and each non-catalog exception a reviewable decision.
Consequences
- The catalog cannot drift: a source change that the committed file doesn't reflect fails
verify-cordis-catalogindoc-syncand CI. A new event with no@modetag, a tag that contradicts its signature, or an unclassified signature type fails the generator outright. - Event and service-method contracts have a single home — the JSDoc at the declaration. The catalog repeats that original JSDoc inside its generated signature block and uses its description portion as entry prose, so thin source documentation yields a thin catalog entry.
- The inherited tier is hand-summarized, so a vendor sync that adds/renames a cordis-core event or
ctxmember needs a matching edit to the curated table ingen-cordis-catalog.ts. This is the deliberate cost of not walking pinned vendor source; it changes rarely and is called out in the generator. verify-event-taxonomy.tsis deleted and thedocs/architecture.mdevent table is gone; anyone who linked to a specific table row now lands on the generated catalog instead.
中文
问题
插件作者需要两类此前没有任何单一文档能同时提供的参考信息:他们可以监听的每一个 Cordis 事件(含精确签名与分发模式),以及他们可以调用的每一个 ctx.<key> 服务(含精确接口)。相关信息虽然存在,但散落各处:docs/architecture.md 中一张手工维护的事件分类体系表格(名称 + 行文描述的 Mode/Purpose,由 verify-event-taxonomy 做名称集合校验)、一张服务映射表(8 行角色描述),以及 interface Events / interface Context 声明本身。分类体系表格还有一个盲区:它无法捕获全新的未记录事件——名称集合校验器只检查两侧已有的名称。
这是对核心数据结构目录(其 Agent Note)在接线维度上的补充:前者对循环传递的数据结构编目(经验证的手工粘贴),本文则对传递它们的事件和服务编目。
决策
从源码生成目录,取代手工维护表格并校验子集的方式。
scripts/gen-cordis-catalog.ts 使用 TypeScript 编译器 API,根据声明和源码 JSDoc 分别生成事件与服务参考。事件包含分派模式及其原始成员 JSDoc;服务包含公共签名及各方法的原始 JSDoc。确定性的 --write 和 --check 模式使两个页面成为生成产物,并由 doc-sync 强制检查新鲜度。
完全通过生成来构建目录在此处是正确的,因为代码库足够规范,AST 包含全部事实:每个事件/服务名称都是字符串字面量,可以往返映射到静态声明——不存在动态命名的事件,也不存在仅运行时的服务。因此生成的文档不可能出错,且从结构上消除了未记录事件的缺口(生成器枚举源码,而非校验手写子集)。
具体选择:
@mode标签,交叉校验。 每个 harness 事件的 JSDoc 携带一个显式的@mode emit|waterfall|parallel|serial标签;缺少标签时生成器直接报错。当签名形状具有决定性时——尾部参数为next: () => …在结构上即为 waterfall(瀑布式事件)——生成器断言标签与之一致,矛盾时直接报错。emit/parallel/serial 的区别在结构上不可见(session/flush返回Promise<void> | void且无next,有序的agent/pre-step检查点亦然),因此信任标签。编写规则见 AGENTS.md。- 分层范围。 harness 层(8 个
@deepseek-ai/dsh-*服务及其事件)从源码完整渲染。继承层(cordis-core 的ctx.on/emit/effect/provide/…+internal/*事件 + loader/hmr/timer)是插件同样可见的固定版本的 vendor 源码;它从生成器中一张人工维护的表格简洁渲染(名称 + 一行描述 + 源码位置),而非遍历 vendor AST。原因是 cordis-core 的Context混合了真正的 ctx 成员与非服务字段(root、baseUrl、logger),且 vendor 接口仅在有意的 vendor 同步时才变化。 - 指向数据结构目录的交叉链接。 签名中由仓库拥有的每个类型名(
GenerateOptions、StreamChunk、ToolDefinition……)都会通过人工维护的映射链接到其主要核心数据结构页面。AST 遍历采用默认拒绝放行的策略:每个参数、泛型约束/默认值和返回类型引用都必须已映射、是签名自身的类型参数、是点名的 TypeScript/Cordis 基础类型,或带有点名的例外及其非目录文档归属。违规会连同源码位置汇总报告,并点明相应的归属列表。该映射不会复用type-equiv.manifest.json,因为后者记录…Map符号,而签名引用派生的联合类型名,并且会在多个页面列出某些符号。 - 专用围栏。 签名块使用
```ts cordis-catalog信息字符串,并把原始事件或公共方法 JSDoc 直接放在其声明之前。doc-typecheck会识别并跳过这些裸片段,将其排除在 opt-out 比例之外——与type-equiv块的处理相同。
本决策取代 doc-sync 强制中事件分类体系的那一半:verify-event-taxonomy 及其 docs/architecture.md 表格退役(architecture.md 的标题保留,正文改为指向目录;服务映射的角色表格作为人工行文保留)。doc-typecheck、verify-md-wrap、verify-md-links 和 verify-type-equiv 不受影响。
曾考虑的替代方案
- 校验而非生成(退役的分类体系检查所做的事):仅对本参考面反转了这一策略。此处的数据可以机械地完整获取,因此生成严格强于对手工表格做名称集合校验(完整签名、不会漂移、能捕获未记录事件)。
- 遍历 vendor AST 以获取继承层:否决,改用人工维护表格。cordis-core 的
Context混合了真正的 ctx 成员与非服务字段,且固定的 vendor 接口仅在有意同步时才变化。 - 复用
type-equiv.manifest.json作为签名交叉链接映射:否决,改用完整的人工维护常量和默认拒绝放行的覆盖检查。manifest 记录…Map符号,而签名引用派生的联合类型名,并且会在多个页面列出某些符号。显式映射让每个渲染目标和每个非目录例外都成为可评审的决策。
后果
- 目录不会发生漂移:提交文件未反映的源码变化会使
doc-sync和 CI 中的verify-cordis-catalog失败。新事件缺少@mode标签、标签与其签名冲突,或签名类型未分类,都会直接使生成器失败。 - 事件与服务方法契约只有一个归属——声明处的 JSDoc。目录会在生成的签名块中重复该原始 JSDoc,并使用其描述部分作为条目正文,因此单薄的源码文档只会生成单薄的目录条目。
- 继承层是手工摘要,因此 vendor 同步若新增或重命名了 cordis-core 事件或
ctx成员,需要同步编辑gen-cordis-catalog.ts中的人工维护表格。这是不遍历固定版本的 vendor 源码的有意代价;它很少变化,且在生成器中有明确标注。 verify-event-taxonomy.ts被删除,docs/architecture.md的事件表格也已移除;之前链接到特定表格行的人现在会落在生成目录上。