mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Traversal ordinals and first-owner paths gave shared persistence types unstable display names. Keep authored names and source locations as metadata, use structural digests for identity, and render complete kind/form groups with bounded array labels. Preserve historical rendering and stored graphs. Record supported source compatibility promises with their property binding, policy version, discriminator, preservation rule, and qualified kind set. The extractor and classifier require explicit opt-ins in both compared schemas; ordinary or unsupported changes retain strict classification. Policy-bearing graphs use the format-2 fingerprint domain while unannotated graphs keep their format-1 fingerprints. Runtime owners activate bindings only with the corresponding reader behavior; this tooling layer activates none and changes no package source or Session generation. Provide a read-only before/after review command that groups shared structural evidence without replacing authoritative compatibility diagnostics. Match changed alternatives only through unique literal discriminators and retain ambiguous additions and removals as independent facts. At the X baseline, all 61 roots and 472 schemas, digests, and source fields are unchanged. The inventory remains format 1 and removes only 296 synthetic names. Existing acknowledgement history still matches every root. All 195 focused tooling tests, Host build/full lint, and bilingual pairing pass.
221 lines
10 KiB
TypeScript
221 lines
10 KiB
TypeScript
/**
|
|
* 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, undefined, 2, 'current')] : []),
|
|
`## ${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, undefined, 2, 'current'))
|
|
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()
|
|
}
|