| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /**
- * Generate `docs/persistence-catalog.md` from every `SessionEventMap` merge and
- * the owning event-envelope types. This is the durable-record vocabulary, not
- * the live Cordis bus. Event declarations must be unique, explicitly typed,
- * documented, inheritance-free, and free of Cordis-only `@mode` tags; every
- * surface-union member must resolve to one. `--check` verifies the artifact.
- */
- import { readFileSync, writeFileSync } from 'node:fs'
- import { resolve } from 'node:path'
- import { githubSlug } from './verify-md-links.ts'
- import {
- annotateSurface, collectEventEnvelopeTypes, collectLogEvents, collectSurfaceEventTypes,
- type AnnotatedLogEventEntry, type EventEnvelopeTypeEntry,
- } from './persistence-catalog-source.ts'
- import { extractPersistenceSchema } from './persistence-schema.ts'
- import { persistenceCatalogText, type PersistenceCatalogLocale } from './persistence-catalog-text.ts'
- import { renderPersistencePair, type PersistenceArtifact } from './persistence-artifacts.ts'
- export { annotateSurface, collectEventEnvelopeTypes, collectLogEvents, collectSurfaceEventTypes } from './persistence-catalog-source.ts'
- export type { AnnotatedLogEventEntry, EventEnvelopeTypeEntry, LogEventEntry } from './persistence-catalog-source.ts'
- import type { PersistenceSchemaInventory } from './persistence-schema-model.ts'
- import { renderPersistenceSchemaDefinitions, renderPersistenceSchemaIndex } from './render-persistence-schema.ts'
- const root = resolve(import.meta.dirname, '..')
- const OUT = 'docs/persistence-catalog.md'
- const OUT_RUNTIME_TYPES = 'packages/core/session/src/known-event-types.ts'
- const OUT_SCHEMA = 'docs/persistence-schema.json'
- /** The fenced-block info string for generated declaration blocks (skipped by
- * doc-typecheck, since their imported types are not standalone-compilable). */
- const FENCE = 'ts persistence-catalog'
- /** Documentation target, relative to `docs/`, for linked payload types. */
- const LINK_MAP: Record<string, string> = {
- ToolCallId: 'subsystems/core.md',
- ContentBlock: 'subsystems/core.md',
- MessageSource: 'subsystems/core.md',
- ScheduleChange: 'subsystems/schedule.md',
- StreamChunk: 'subsystems/llm-streaming.md',
- TokenUsage: 'subsystems/llm-streaming.md',
- TodoItem: 'subsystems/todo.md',
- WorkspaceChangesSummary: 'subsystems/deliverables.md',
- PresentedFile: 'subsystems/deliverables.md',
- TurnTrigger: 'subsystems/session.md',
- TurnEndReason: 'subsystems/session.md',
- SessionTitleEventData: 'subsystems/session-title.md',
- SessionTitleLlmRequestEventData: 'subsystems/session-title.md',
- SessionTitleModelIdentity: 'subsystems/session-title.md',
- SessionTitleProviderId: 'subsystems/session-title.md',
- SessionTitleSource: 'subsystems/session-title.md',
- TeamId: 'subsystems/agent-team.md',
- TeamMemberSnapshot: 'subsystems/agent-team.md',
- TeamMessageId: 'subsystems/agent-team.md',
- TeamMessageSnapshot: 'subsystems/agent-team.md',
- TeamTaskSnapshot: 'subsystems/agent-team.md',
- }
- /** Render the cross-link "Types:" line for a payload, or '' if none apply. */
- function typeLinks(payload: string, locale: PersistenceCatalogLocale): string {
- const seen = new Set<string>()
- for (const name of Object.keys(LINK_MAP)) {
- if (new RegExp(`\\b${name}\\b`).test(payload)) seen.add(name)
- }
- if (seen.size === 0) return ''
- const links = Object.entries(LINK_MAP).filter(([name]) => seen.has(name)).sort(([left], [right]) => left.localeCompare(right))
- .map(([name, path]) => {
- return `[${name}](${locale === 'zh' ? path.replace(/\.md$/u, '.zh.md') : path})`
- })
- return `${persistenceCatalogText[locale].types}${links.join(' · ')}`
- }
- /** Render one log event entry. */
- function renderEvent(e: AnnotatedLogEventEntry, locale: PersistenceCatalogLocale): string[] {
- const heading = `${e.name} — ${e.surface ? 'surface' : 'log-only'}`
- const out = [`<a id="${githubSlug(heading)}"></a>`, '', `#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, '']
- out.push('```' + FENCE, e.declaration, '```', '')
- const links = typeLinks(e.payload, locale)
- if (links) out.push(links, '')
- out.push(`${persistenceCatalogText[locale].source}[\`${e.source}\`](../${e.source.split(':')[0]})`, '')
- return out
- }
- /** Render the full catalog (pure, deterministic given the collected inputs). */
- export function render(
- events: AnnotatedLogEventEntry[],
- envelopeTypes: EventEnvelopeTypeEntry[],
- schema?: PersistenceSchemaInventory,
- locale: PersistenceCatalogLocale = 'en',
- ): string {
- const text = persistenceCatalogText[locale]
- const lines: string[] = [
- '<!-- Generated by scripts/gen-persistence-catalog.ts — do not edit by hand.',
- ' Run `pnpm run gen-persistence-catalog` to regenerate. -->',
- '',
- `# ${text.title}`,
- '',
- ...(locale === 'zh' ? ['[English](persistence-catalog.md) | 中文', ''] : []),
- text.intro,
- '',
- text.generation,
- '',
- text.envelopeIntro,
- '',
- ...(schema ? [renderPersistenceSchemaIndex(schema, locale)] : []),
- `## ${text.envelope}`,
- '',
- '```' + FENCE,
- envelopeTypes.map(entry => entry.declaration).join('\n\n'),
- '```',
- '',
- `${text.sources}${envelopeTypes.map(entry => `[\`${entry.source}\`](../${entry.source.split(':')[0]})`).join(' · ')}`,
- '',
- `## ${text.events}`,
- '',
- ]
- const scopes = [...new Set(events.map(e => e.scope))].sort()
- for (const scope of scopes) {
- lines.push(`### \`${scope}/*\``, '')
- for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) {
- lines.push(...renderEvent(e, locale))
- }
- }
- if (schema) lines.push(renderPersistenceSchemaDefinitions(schema, locale))
- return lines.join('\n')
- }
- /**
- * Render the runtime known-vocabulary module: every event type the packages in
- * this repo can write, as a generated `ReadonlySet` the read path checks
- * unknown-type refusal against (`SessionEvent.ignorable` contract).
- */
- export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string {
- const names = [...new Set(events.map(e => e.name))].sort()
- return [
- '/**',
- ' * GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run',
- ' * `pnpm run gen-persistence-catalog` to regenerate (verified fresh by',
- ' * `pnpm run verify-persistence-catalog`, part of `doc-sync`).',
- ' * @module @deepseek-ai/dsh-session/known-event-types',
- ' */',
- '',
- '/**',
- ' * Every `SessionEventMap` member declared in this repository — the event',
- ' * vocabulary this build understands. The persistence read path refuses to',
- ' * interpret a log containing a type outside this set unless the event',
- ' * carries the envelope\'s `ignorable` marker (see `SessionEvent.ignorable`',
- ' * in `./types.ts`): such a log was likely written by a newer harness, and',
- ' * silently skipping a required event would reconstruct a wrong session.',
- ' * Downstream (out-of-repo) plugin events are outside this list by',
- ' * construction. The persisted `SessionEvent.ignorable` marker is the',
- ' * compatibility mechanism; event-name registration was rejected because',
- ' * it does not classify omission safety and would make reads',
- ' * composition-dependent. The rationale is in',
- ' * `.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md`.',
- ' */',
- 'export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([',
- ...names.map(name => ` '${name}',`),
- '])',
- '',
- '/** Event types whose model-visible effects require an explicit pure interpreter. */',
- 'export const MESSAGE_PROJECTION_EVENT_TYPES: ReadonlySet<string> = new Set([',
- ...events.filter(event => event.messageProjection).map(event => ` '${event.name}',`).sort(),
- '])',
- '',
- ].join('\n')
- }
- /**
- * Render the complete generated persistence reference from one extracted inventory.
- * @param scanRoot - checkout whose source event declarations supply JSDoc.
- * @param schema - already extracted source schemas; shared with record validation.
- * @returns both catalog languages, pairing metadata, runtime names and machine schemas.
- */
- export function persistenceCatalogArtifacts(scanRoot: string, schema: PersistenceSchemaInventory): PersistenceArtifact[] {
- const events = annotateSurface(collectLogEvents(scanRoot), collectSurfaceEventTypes(scanRoot))
- const envelope = collectEventEnvelopeTypes(scanRoot)
- return [
- ...renderPersistencePair(scanRoot, OUT, render(events, envelope, schema), render(events, envelope, schema, 'zh')),
- { path: OUT_RUNTIME_TYPES, content: renderKnownEventTypes(events) },
- { path: OUT_SCHEMA, content: `${JSON.stringify(schema, null, 2)}\n` },
- ]
- }
- /** CLI entry: default writes the artifacts, `--check` fails if a committed copy
- * is stale. Guarded behind an entry-point check so importing this module for
- * tests neither regenerates the committed files nor calls process.exit. */
- function main(): void {
- const schema = extractPersistenceSchema(root)
- const artifacts = persistenceCatalogArtifacts(root, schema)
- if (process.argv.includes('--check')) {
- const stale = artifacts.filter((artifact) => {
- let committed: string | null = null
- try {
- committed = readFileSync(resolve(root, artifact.path), 'utf8')
- } catch {
- // Only ENOENT (not yet generated) is expected; a present-but-unreadable
- // file is not a state this repo produces. Either way the remedy is the
- // same — regenerate — so treat a read failure as "stale".
- committed = null
- }
- return committed !== artifact.content
- })
- if (stale.length === 0) {
- console.log(`gen-persistence-catalog: ${artifacts.map(a => a.path).join(', ')} are up to date.`)
- process.exit(0)
- }
- console.error(`gen-persistence-catalog: ${stale.map(a => a.path).join(', ')} stale. Run \`pnpm run gen-persistence-catalog\` and commit the result.`)
- process.exit(1)
- }
- for (const artifact of artifacts) {
- writeFileSync(resolve(root, artifact.path), artifact.content)
- console.log(`gen-persistence-catalog: wrote ${artifact.path}.`)
- }
- }
- if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
- main()
- }
|