Files
deepseek-harness/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md
Tianyi Cui 232f314c3a Add generated persistence log event catalog with freshness + completeness gates
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.
2026-07-04 22:58:28 +08:00

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 @mode tag 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 carry surfaceOp — 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-catalog info string that doc-typecheck recognizes and skips, excluded from the opt-out ratio — the same treatment as ts 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-catalog in 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 SurfaceEventType union 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.