docs/persistence-catalog/log-events.md enumerates every SessionEventMap member — the owning dsh-session vocabulary plus the dsh-compact and dsh-hook-protocol declaration merges — with payload, surface/log-only badge, JSDoc prose, and declaration site. scripts/gen-persistence-catalog.ts is a pure AST pass in the gen-cordis-catalog mold: verify-persistence-catalog (--check) joins doc-sync, so a stale committed catalog fails pre-push and CI. The walk enforces JSDoc completeness (every member needs description prose; @mode is rejected as a category error — log events do not dispatch on the cordis bus), derives the surface badge from the SurfaceEventType union with a stale-member cross-check, and hard-errors on duplicate declarations. Payloads render through the TypeScript printer so newline-separated multi-line type literals still emit valid one-line fragments. Documented the five previously JSDoc-less core events (turn/step boundaries, tool/call), removed the two stray @mode tags on the hook/* merges, and replaced the hand-restated event enumerations (session.md hook/* table, compact README table, hook-protocol README bullets, session README name-list — whose merge note had already drifted) with links to the catalog. RFC: docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md.
5.4 KiB
RFC: Generated persistence log event catalog
Status: implemented (accepted 2026-07-04)
Context
The session event log is the harness's on-disk contract: every SessionEventMap member is a record a persistence backend writes verbatim and a replay reconstructs from, and adding one that breaks the durability rules is a breaking change to the on-disk format. Yet the vocabulary had no single reference. The declarations are split across three files — the owning interface in @deepseek-ai/dsh-session plus declaration merges in @deepseek-ai/dsh-compact and @deepseek-ai/dsh-hook-protocol — and the doc surfaces covered it with hand-copies: a hook/* payload table in session.md, a compact/* payload table in the compact README, payload bullets in the hook-protocol README, and a name-list in the session README. The name-list's merge note had already drifted (it named the compaction merge and omitted the hook merge entirely), and nothing could catch the next merge going undocumented: a hand-copy only checks the names someone already wrote down. This is the same gap the cordis catalog closed for bus events and the tool catalog closed for model-facing tools — and log events are covered by neither: a SessionEventMap member is not a cordis Events declaration (it reaches listeners via the single session/event emit), so it has no cordis-catalog row by design.
Decision
Generate docs/persistence-catalog/log-events.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).
scripts/gen-persistence-catalog.ts is a pure TypeScript-AST pass, like gen-cordis-catalog.ts and unlike the boot-based tool catalog — the right technique because log events ARE statically knowable: every member is a string-literal-named property with a static type annotation, so the AST is the whole truth. The walk collects every interface SessionEventMap declaration under packages/*/*/src — the owning top-level interface and every declare module '@deepseek-ai/dsh-session' merge — so a brand-new event, core or merged, appears in the next regenerate and an un-regenerated file fails --check (verify-persistence-catalog, a doc-sync member, so pre-push and CI both run it). Each entry renders the member's JSDoc prose, its payload (printed through the TypeScript printer, so a newline-separated multi-line type literal still yields a valid one-line fragment), a surface badge, cross-links into core-data-structures, and the declaration's source pointer, grouped by scope.
Specific choices:
- JSDoc completeness, enforced. Every member must carry description prose — the JSDoc becomes the catalog entry, the same forcing function the cordis catalog applies to bus events. An
@modetag on a member is a hard error: dispatch modes belong to cordis bus events, and a log event has none — the tag would misread as "this fires on the bus with mode X". 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. Payload blocks use a
```ts persistence-cataloginfo string thatdoc-typecheckrecognizes and skips, excluded from the opt-out ratio — the same treatment asts cordis-catalog(a bare payload fragment is 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. Nothing else in the repo may name an interface
SessionEventMap— the walk treats every such declaration as the merged vocabulary, and a duplicate member across declarations is a hard error.
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.
Consequences
- The catalog cannot drift: a vocabulary change the committed file doesn't reflect fails
verify-persistence-catalogin the pre-push hook and 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; thin JSDoc yields a thin catalog entry, pressuring authors to document at the source.
- 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.