Generated persistence log event catalog
生成式持久化日志事件目录
`SessionEventMap` is the on-disk vocabulary, but its declarations are split across the owning session package and declaration merges. The generated persistence catalog is the single reference for every event, its complete payload declaration and source JSDoc, and the shared `SessionEvent` envelope; hand-maintained tables drift and are removed. These records are not Cordis events—observers receive them through the sin
English
Problem
SessionEventMap is the on-disk vocabulary, but its declarations are split across the owning session package and declaration merges. The generated persistence catalog is the single reference for every event, its complete payload declaration and source JSDoc, and the shared SessionEvent envelope; hand-maintained tables drift and are removed. These records are not Cordis events—observers receive them through the single session/event bus event—so the Cordis catalog cannot cover them. The generator discovers all declarations and the doc-sync freshness gate rejects omissions or stale output.
Decision
Generate docs/persistence-catalog.md from source, with a freshness gate, as the fourth reference surface: the records a persisted session log can contain, complementing the cordis catalog (wiring), core-data-structures (vocabulary), and the tool catalog (tools).
gen-persistence-catalog.ts scans every owning and declaration-merged SessionEventMap with the TypeScript AST. It renders each member from its leading JSDoc through the complete payload type, retaining nested property comments and removing only its containing indentation, and also pastes the owning SessionEventType, SurfaceEventType, SurfaceOp, and SessionEvent declarations that compose the persisted envelope. Derived surface badges, reference links, and source locations remain outside the declaration blocks. The doc-sync freshness check rejects a vocabulary or envelope change whose catalog was not regenerated.
Specific choices:
- JSDoc completeness, enforced. Every member and rendered envelope type must carry description prose, and the full source JSDoc stays attached to its declaration in the catalog. An
@modetag is a hard error: dispatch modes belong to cordis bus events, and persisted records have none. Violations aggregate into one error listing every offender. - The surface badge is derived, not hand-listed.
SurfaceEventType— the subset that produces LLM messages and may carrysurfaceOp— is parsed from its union declaration in the owning package; a union member naming no declared event is a hard error (a stale union member would otherwise silently badge nothing). Everything else renders log-only. - A dedicated fence. Declaration blocks use a
```ts persistence-cataloginfo string thatdoc-typecheckrecognizes and skips, excluded from the opt-out ratio — the same treatment asts cordis-catalog(the declarations reference types from their owning modules and are not standalone-compilable). - Repo scope. The catalog enumerates the packages in this repo, matching the siblings' packages-only scope; a downstream plugin can merge further event types, which are outside the catalog by construction. The walk defends its own assumptions with hard errors: the owning top-level
interface SessionEventMapmust be the single exported declaration in@deepseek-ai/dsh-session(an unrelated, local, or duplicate same-named interface cannot be catalogued as the on-disk vocabulary), no declaration may carryextends(inherited keys would joinkeyof SessionEventMapwithout a catalog row), every member must be a property signature with an explicit payload type (a method-form member would joinkeyofyet slip past a silent walk), and a duplicate member across declarations fails.
This supersedes the hand-copies: the session.md hook/* table, the compact README's event table, the hook-protocol README's payload bullets, and the session README's name-list now link the catalog instead of restating payloads (the surrounding semantics prose stays where it was). The two stray @mode emit tags on the hook-protocol merge members are removed — the new gate rejects them as the category error they were.
Alternatives considered
- A boot-based generator, like the tool catalog's — the log vocabulary is fully static, so the AST pass reads the whole truth without booting anything.
- Keeping the hand-copies — a hand-copy only checks the names someone already wrote down; the session README's merge note had already drifted when the catalog landed.
Consequences
- The catalog cannot drift: a vocabulary or envelope change the committed file doesn't reflect fails
verify-persistence-catalogindoc-syncand CI, and a new merged event with no JSDoc fails the generator outright — a plugin can no longer add an undocumented on-disk record type. - Event prose has a single home, the JSDoc at the declaration; the catalog preserves that JSDoc and any nested field comments without flattening or paraphrasing them.
- The
SurfaceEventTypeunion is now structurally load-bearing for docs: renaming an event without updating the union (or vice versa) fails the generator, not just the compiler. - The badge derivation assumes the union stays a closed set of string literals with exactly one owner; a refactor away from that shape must update the generator in the same change.
中文
问题
SessionEventMap 是磁盘格式的词汇,但其声明分散在所属的会话包(package)和声明合并中。生成式持久化目录是所有事件、各自完整 payload 声明与源码 JSDoc,以及共享 SessionEvent 信封的唯一参考;手工维护的表格会发生漂移,因此被移除。这些记录不是 Cordis 事件——观察者通过唯一的 session/event 总线事件接收它们——所以 Cordis 目录无法覆盖。生成器会发现所有声明,文档同步新鲜度门禁会拒绝遗漏或陈旧输出。
决策
从源码生成 docs/persistence-catalog.md,配合新鲜度门禁,作为第四个参考面:持久化会话日志可以包含的记录,与 Cordis 目录(接线)、核心数据结构(词汇)和工具目录(工具)互补。
gen-persistence-catalog.ts 使用 TypeScript AST 扫描每个所属及声明合并的 SessionEventMap。它从前置 JSDoc 开始渲染每个成员,直至完整的 payload 类型,保留嵌套属性注释且只移除其容器缩进;同时粘贴构成持久化信封的所属 SessionEventType、SurfaceEventType、SurfaceOp 和 SessionEvent 声明。派生的 surface 徽章、参考链接和源码位置仍位于声明块之外。文档同步新鲜度检查会拒绝目录尚未重新生成的词汇或信封变更。
具体选择:
- 强制保证 JSDoc 完整性。 每个成员和渲染出的信封类型都必须带有描述正文,完整的源码 JSDoc 会在目录中保持附着于其声明。
@mode标签是硬错误:分派模式属于 Cordis 总线事件,持久化记录没有这种模式。所有违规会汇总为一条错误,列出每个违规项。 - surface 徽章由派生得出,而非手工列举。
SurfaceEventType(产生 LLM(大语言模型)消息且可能携带surfaceOp的子集)从拥有方包中的 union 声明解析;如果 union 成员命名了一个未声明的事件,则为硬错误(否则陈旧的 union 成员会静默地不标注任何内容)。其余一律渲染为 log-only。 - 专用围栏。 声明块使用
```ts persistence-catalog信息字符串,doc-typecheck会识别并跳过这些块,将其排除在 opt-out 比例之外——处理方式与ts cordis-catalog相同(这些声明引用所属模块中的类型,无法独立编译)。 - 仓库范围。 目录枚举本仓库中的包,与兄弟文档的 packages-only 范围一致;下游插件可以合并更多事件类型,它们在设计上不在目录范围内。遍历过程用硬错误保护自身假设:拥有方的顶层
interface SessionEventMap必须是@deepseek-ai/dsh-session中唯一的导出声明(无关的、局部的或同名重复的接口不能被当作磁盘词汇编入目录);任何声明不得携带extends(继承的键会加入keyof SessionEventMap却没有对应的目录行);每个成员必须是带有显式 payload 类型的属性签名(方法形式的成员会加入keyof却在静默遍历中被漏过);跨声明的重复成员也会失败。
本方案取代了手工副本:session.md 的 hook/* 表格、精简版 README 的事件表格、hook-protocol README 的 payload 条目列表,以及会话 README 的名称列表现在链接到目录,而不再重述 payload(周围的语义说明文字保留原位)。hook-protocol 合并成员上的两个误加的 @mode emit 标签已被移除——新门禁将它们作为类别错误拒绝。
曾考虑的替代方案
- 基于启动的生成器(类似工具目录):日志词汇完全是静态的,AST 遍历无需启动任何东西即可读取全部真相。
- 保留手工副本:手工副本只能检查作者已经写下的名称;目录落地时,会话 README 的合并说明已经漂移。
后果
- 目录不会发生漂移:提交文件未反映的词汇或信封变化会使
doc-sync和 CI 中的verify-persistence-catalog失败,而没有 JSDoc 的新增合并事件会直接使生成器失败——插件不能再添加未记录的磁盘记录类型。 - 事件正文只有一个归属,即声明处的 JSDoc;目录会保留该 JSDoc 和所有嵌套字段注释,不会将其扁平化或复述。
SurfaceEventTypeunion 现在对文档具有结构性承载作用:重命名事件而不更新 union(或反过来)会导致生成器失败,而不仅仅是编译器失败。- 徽章派生假设 union 始终是一组封闭的字符串字面量且只有一个拥有方;如果重构偏离了这一形状,必须在同一个变更中更新生成器。