docs(session): separate experimental persistence catalogue
This commit is contained in:
1 parent
bb405d51fa
commit
c954a07535
23 files changed
+164
-68
No files matched your search
@@ -3,7 +3,8 @@
|
||||
* 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.
|
||||
* surface-union member must resolve to one. Experimental plugin records have
|
||||
* a separate current-source catalog. `--check` verifies every generated artifact.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
@@ -25,6 +26,7 @@ import { renderPersistenceSchemaDefinitions, renderPersistenceSchemaIndex } from
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/persistence-catalog.md'
|
||||
const OUT_PLUGIN_RECORDS = 'docs/experimental-persistence-catalog.md'
|
||||
const OUT_RUNTIME_TYPES = 'packages/core/session/src/known-event-types.ts'
|
||||
const OUT_SCHEMA = 'docs/persistence-schema.json'
|
||||
|
||||
@@ -88,17 +90,38 @@ function recordCode(value: string): string {
|
||||
.replaceAll('|', '|').replaceAll('`', '`').replace(/\r?\n/gu, '<br>')}</code>`
|
||||
}
|
||||
|
||||
/** Render the current plugin record inventory without expanding payload types. */
|
||||
function renderPluginRecords(records: readonly PluginRecordEntry[], locale: PersistenceCatalogLocale): string[] {
|
||||
/**
|
||||
* Render the standalone current-source plugin record catalog without expanding payload types.
|
||||
* @param records - validated experimental package record declarations.
|
||||
* @param locale - language of the page's headings and explanatory prose.
|
||||
* @returns the complete Markdown page, including its language switcher and main catalog link.
|
||||
*/
|
||||
export function renderPluginRecordCatalog(records: readonly PluginRecordEntry[], locale: PersistenceCatalogLocale = 'en'): string {
|
||||
const text = persistenceCatalogText[locale]
|
||||
const lines = [`## ${text.pluginRecords}`, '', text.pluginRecordsIntro, '']
|
||||
if (records.length === 0) return [...lines, text.pluginRecordsEmpty, '']
|
||||
const lines = [
|
||||
'<!-- Generated by scripts/gen-persistence-catalog.ts — do not edit by hand.',
|
||||
' Run `pnpm run gen-persistence-catalog` to regenerate. -->',
|
||||
'',
|
||||
`# ${text.pluginRecords}`,
|
||||
'',
|
||||
locale === 'en'
|
||||
? 'English | [中文](experimental-persistence-catalog.zh.md)'
|
||||
: '[English](experimental-persistence-catalog.md) | 中文',
|
||||
'',
|
||||
text.pluginRecordsIntro,
|
||||
'',
|
||||
text.pluginRecordsGeneration,
|
||||
'',
|
||||
text.pluginRecordsMainLink,
|
||||
'',
|
||||
]
|
||||
if (records.length === 0) return [...lines, text.pluginRecordsEmpty, ''].join('\n')
|
||||
lines.push(text.pluginRecordColumns, '|---|---|---|---|')
|
||||
for (const record of [...records].sort((left, right) => left.owner.localeCompare(right.owner) || left.name.localeCompare(right.name))) {
|
||||
const doc = record.doc.replaceAll('|', '\\|').replace(/\s*\n\s*/gu, ' ')
|
||||
lines.push(`| \`${record.name}\` | \`${record.owner}\` | ${doc} | ${recordCode(record.payload)} · [\`${record.source}\`](../${record.source}) |`)
|
||||
}
|
||||
return [...lines, '']
|
||||
return [...lines, ''].join('\n')
|
||||
}
|
||||
|
||||
/** Render the full catalog (pure, deterministic given the collected inputs). */
|
||||
@@ -107,7 +130,6 @@ export function render(
|
||||
envelopeTypes: EventEnvelopeTypeEntry[],
|
||||
schema?: PersistenceSchemaInventory,
|
||||
locale: PersistenceCatalogLocale = 'en',
|
||||
pluginRecords: readonly PluginRecordEntry[] = [],
|
||||
): string {
|
||||
const text = persistenceCatalogText[locale]
|
||||
const lines: string[] = [
|
||||
@@ -119,6 +141,8 @@ export function render(
|
||||
...(locale === 'zh' ? ['[English](persistence-catalog.md) | 中文', ''] : []),
|
||||
text.intro,
|
||||
'',
|
||||
text.pluginRecordsLink,
|
||||
'',
|
||||
text.generation,
|
||||
'',
|
||||
text.envelopeIntro,
|
||||
@@ -142,7 +166,6 @@ export function render(
|
||||
lines.push(...renderEvent(e, locale))
|
||||
}
|
||||
}
|
||||
lines.push(...renderPluginRecords(pluginRecords, locale))
|
||||
if (schema) lines.push(renderPersistenceSchemaDefinitions(schema, locale, undefined, 2, 'current'))
|
||||
return lines.join('\n')
|
||||
}
|
||||
@@ -192,14 +215,15 @@ export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string
|
||||
* 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.
|
||||
* @returns both catalog pairs and their 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)
|
||||
const pluginRecords = collectPluginRecords(scanRoot)
|
||||
return [
|
||||
...renderPersistencePair(scanRoot, OUT, render(events, envelope, schema, 'en', pluginRecords), render(events, envelope, schema, 'zh', pluginRecords)),
|
||||
...renderPersistencePair(scanRoot, OUT, render(events, envelope, schema), render(events, envelope, schema, 'zh')),
|
||||
...renderPersistencePair(scanRoot, OUT_PLUGIN_RECORDS, renderPluginRecordCatalog(pluginRecords), renderPluginRecordCatalog(pluginRecords, 'zh')),
|
||||
{ path: OUT_RUNTIME_TYPES, content: renderKnownEventTypes(events) },
|
||||
{ path: OUT_SCHEMA, content: `${JSON.stringify(persistenceSchemaSnapshot(schema), null, 2)}\n` },
|
||||
]
|
||||
|
||||
@@ -5,12 +5,15 @@ export type PersistenceCatalogLocale = 'en' | 'zh'
|
||||
|
||||
const english = {
|
||||
title: 'Session Persistence Event Catalog',
|
||||
intro: 'Every repository-declared durable Session event appears here with its source declaration and resolved types. The catalog covers the logical and physical headers, event envelopes, every SessionEventMap declaration merge, and a separate inventory of experimental plugin records. See [Session](subsystems/session.md) for replay and [persistence](subsystems/persistence.md) for storage.',
|
||||
intro: 'Every repository-declared durable Session event appears here with its source declaration and resolved types. The catalog covers the logical and physical headers, event envelopes, and every SessionEventMap declaration merge. See [Session](subsystems/session.md) for replay and [persistence](subsystems/persistence.md) for storage.',
|
||||
generation: 'Run `pnpm run gen-persistence-catalog` to regenerate both catalog languages, their pairing record, the known-event module, and the machine schema inventory. `pnpm run verify-persistence-catalog` checks all generated files. Declaration fences preserve source JSDoc and type references; resolved definitions expose their transitive structure.',
|
||||
envelopeIntro: 'The envelope carries `type`, `seq`, `time`, `data`, optional `ignorable`, and conditional `surfaceOp` / `sourceEventSeqs`. A **surface** event produces model history; a **log-only** event does not. The inventory covers this repository; external plugin types require their own declarations and are outside this catalog.',
|
||||
envelope: 'Event envelope', events: 'Events', sources: 'Sources: ', source: 'Source: ', types: 'Types: ',
|
||||
pluginRecords: 'Experimental plugin records',
|
||||
pluginRecords: 'Experimental Plugin Record Catalog',
|
||||
pluginRecordsLink: 'Experimental records appear in the separate [experimental plugin record catalog](experimental-persistence-catalog.md).',
|
||||
pluginRecordsIntro: 'These current-source PluginRecordMap declarations describe ignorable, log-only records owned by experimental packages. Payload annotations link to their owning source without expanding referenced types. Records stay outside the known-event set, persistence schema fingerprints, and compatibility history; format migrations retain them on a best-effort basis.',
|
||||
pluginRecordsGeneration: 'Run `pnpm run gen-persistence-catalog` to regenerate both persistence catalogs, their language pairs, and associated artifacts. `pnpm run verify-persistence-catalog` checks all generated files.',
|
||||
pluginRecordsMainLink: 'Shared Session event declarations and resolved schemas appear in the [Session persistence event catalog](persistence-catalog.md).',
|
||||
pluginRecordsEmpty: 'No production package currently declares a plugin record.',
|
||||
pluginRecordColumns: '| Record | Owner | Purpose | Payload and source |',
|
||||
fingerprints: 'Persistence type fingerprints',
|
||||
@@ -30,12 +33,15 @@ const english = {
|
||||
|
||||
const chinese: Record<keyof typeof english, string> = {
|
||||
title: '会话持久化事件目录',
|
||||
intro: '本目录列出仓库声明的每个持久化 Session 事件及其源码声明和解析类型,覆盖逻辑与物理 header、事件信封、所有 SessionEventMap 声明合并,以及单独列出的实验性插件记录。回放规则参见 [Session](subsystems/session.zh.md),存储规则参见[持久化](subsystems/persistence.zh.md)。',
|
||||
intro: '本目录列出仓库声明的每个持久化 Session 事件及其源码声明和解析类型,覆盖逻辑与物理 header、事件信封及所有 SessionEventMap 声明合并。回放规则参见 [Session](subsystems/session.zh.md),存储规则参见[持久化](subsystems/persistence.zh.md)。',
|
||||
generation: '运行 `pnpm run gen-persistence-catalog` 可重新生成目录的两种语言、配对记录、已知事件模块和机器 schema 目录。`pnpm run verify-persistence-catalog` 检查所有生成文件。声明围栏保留源码 JSDoc 和类型引用;解析后的定义展开其传递引用结构。',
|
||||
envelopeIntro: '信封包含 `type`、`seq`、`time`、`data`、可选的 `ignorable` 以及条件字段 `surfaceOp` / `sourceEventSeqs`。**surface** 事件产生模型历史,**log-only** 事件不产生模型历史。目录覆盖本仓库;外部插件类型需要独立声明,不属于本目录。',
|
||||
envelope: '事件信封', events: '事件', sources: '来源:', source: '来源:', types: '类型:',
|
||||
pluginRecords: '实验性插件记录',
|
||||
pluginRecords: '实验性插件记录目录',
|
||||
pluginRecordsLink: '实验性记录单独列在[实验性插件记录目录](experimental-persistence-catalog.zh.md)中。',
|
||||
pluginRecordsIntro: '这些当前源码中的 PluginRecordMap 声明描述实验性包拥有的 ignorable、仅日志记录。载荷类型注解链接到所属源码,不展开引用类型。记录不进入已知事件集合、持久化 schema 指纹或兼容性历史;格式迁移以尽力而为的方式保留它们。',
|
||||
pluginRecordsGeneration: '运行 `pnpm run gen-persistence-catalog` 可重新生成两份持久化目录及其中英配对和关联制品。`pnpm run verify-persistence-catalog` 检查所有生成文件。',
|
||||
pluginRecordsMainLink: '共享 Session 事件声明与解析后的 schema 见[会话持久化事件目录](persistence-catalog.zh.md)。',
|
||||
pluginRecordsEmpty: '当前没有生产包声明插件记录。',
|
||||
pluginRecordColumns: '| 记录 | 所属包 | 用途 | 载荷与源码 |',
|
||||
fingerprints: '持久化类型指纹',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { collectPluginRecords } from './plugin-record-catalog.ts'
|
||||
import { annotateSurface, collectLogEvents, render, renderKnownEventTypes } from './gen-persistence-catalog.ts'
|
||||
import { persistenceCatalogArtifacts, render, renderPluginRecordCatalog } from './gen-persistence-catalog.ts'
|
||||
import { extractPersistenceSchema } from './persistence-schema.ts'
|
||||
import { removeFixtureSafely } from './test-fixture-cleanup.ts'
|
||||
|
||||
@@ -124,8 +124,26 @@ describe('plugin record declarations', () => {
|
||||
|
||||
describe('plugin record catalogue output', () => {
|
||||
it('renders honest empty states in both languages', () => {
|
||||
expect(render([], [])).toContain('No production package currently declares a plugin record.')
|
||||
expect(render([], [], undefined, 'zh')).toContain('当前没有生产包声明插件记录。')
|
||||
const english = renderPluginRecordCatalog([])
|
||||
const chinese = renderPluginRecordCatalog([], 'zh')
|
||||
expect(english).toContain('# Experimental Plugin Record Catalog')
|
||||
expect(english).toContain('English | [中文](experimental-persistence-catalog.zh.md)')
|
||||
expect(english).toContain('No production package currently declares a plugin record.')
|
||||
expect(english).toContain('[Session persistence event catalog](persistence-catalog.md)')
|
||||
expect(chinese).toContain('[English](experimental-persistence-catalog.md) | 中文')
|
||||
expect(chinese).toContain('当前没有生产包声明插件记录。')
|
||||
expect(chinese).toContain('[会话持久化事件目录](persistence-catalog.zh.md)')
|
||||
})
|
||||
|
||||
it('links the main catalogue to the experimental page without embedding its inventory', () => {
|
||||
const english = render([], [])
|
||||
const chinese = render([], [], undefined, 'zh')
|
||||
expect(english).toContain('[experimental plugin record catalog](experimental-persistence-catalog.md)')
|
||||
expect(chinese).toContain('[实验性插件记录目录](experimental-persistence-catalog.zh.md)')
|
||||
expect(english).not.toContain('No production package currently declares')
|
||||
expect(english).not.toContain('| Record | Owner |')
|
||||
expect(chinese).not.toContain('当前没有生产包声明')
|
||||
expect(chinese).not.toContain('| 记录 | 所属包 |')
|
||||
})
|
||||
|
||||
it('renders compact, sorted rows with escaped annotations and no line-number churn', () => {
|
||||
@@ -134,7 +152,7 @@ describe('plugin record catalogue output', () => {
|
||||
'plugin:bridge/state': { value: string | null; marker: 'a b' }
|
||||
${ENTRY}
|
||||
`) }))
|
||||
const output = render([], [], undefined, 'en', [...records].reverse())
|
||||
const output = renderPluginRecordCatalog([...records].reverse())
|
||||
expect(output).toContain('| `plugin:bridge/entry` | `@deepseek-ai/dsh-experimental-bridge` | Retains a bridge entry. | <code>SavedEntry</code>')
|
||||
expect(output).toContain('Retains state \\| metadata.')
|
||||
expect(output).toContain("<code>{ value: string | null; marker: 'a b'; }</code>")
|
||||
@@ -143,7 +161,7 @@ describe('plugin record catalogue output', () => {
|
||||
expect(output).not.toMatch(/records\.ts:\d/u)
|
||||
})
|
||||
|
||||
it('keeps record declarations and payload changes out of persisted schemas and runtime names', () => {
|
||||
it('limits record declaration changes to the experimental artifact pair and its metadata', () => {
|
||||
const root = fixture({
|
||||
'tsconfig.host.json': JSON.stringify({ compilerOptions: {
|
||||
target: 'es2024', module: 'esnext', moduleResolution: 'bundler', strict: true, types: [],
|
||||
@@ -155,23 +173,43 @@ export interface SessionEventMap {
|
||||
/** One ordinary log event. */
|
||||
'test/record': { value: string }
|
||||
}
|
||||
export type SurfaceEventType = never
|
||||
/** Declared ordinary event names. */
|
||||
export type SessionEventType = keyof SessionEventMap
|
||||
/** Surface placement. */
|
||||
export type SurfaceOp = 'append'
|
||||
/** Events which can produce model history. */
|
||||
export type SurfaceEventType = 'test/record'
|
||||
/** One persisted event envelope. */
|
||||
export type SessionEvent<T extends keyof SessionEventMap = keyof SessionEventMap> = {
|
||||
[K in keyof SessionEventMap]: { type: K; seq: number; time: number; data: SessionEventMap[K] }
|
||||
[K in keyof SessionEventMap]: { type: K; seq: number; time: number; data: SessionEventMap[K]; surfaceOp: SurfaceOp }
|
||||
}[T]
|
||||
`,
|
||||
'packages/session/session-persistence-jsonl/src/format.ts': "interface HeaderLine {type: 'session'; version: number; id: string; delegationDepth: number}\nexport {}\n",
|
||||
})
|
||||
const schema = extractPersistenceSchema(root)
|
||||
const known = renderKnownEventTypes(annotateSurface(collectLogEvents(root), []))
|
||||
const empty = render([], [], undefined, 'en', collectPluginRecords(root))
|
||||
const empty = persistenceCatalogArtifacts(root, schema)
|
||||
const experimentalPaths = [
|
||||
'docs/experimental-persistence-catalog.md',
|
||||
'docs/experimental-persistence-catalog.zh.md',
|
||||
'docs/experimental-persistence-catalog.i18n.yaml',
|
||||
]
|
||||
expect(empty.map(artifact => artifact.path)).toEqual([
|
||||
'docs/persistence-catalog.md', 'docs/persistence-catalog.zh.md', 'docs/persistence-catalog.i18n.yaml',
|
||||
...experimentalPaths,
|
||||
'packages/core/session/src/known-event-types.ts', 'docs/persistence-schema.json',
|
||||
])
|
||||
put(root, RECORDS, `interface SavedEntry { value: string }\n${augmentation(ENTRY)}`)
|
||||
const declared = render([], [], undefined, 'en', collectPluginRecords(root))
|
||||
expect(declared).not.toBe(empty)
|
||||
const declared = persistenceCatalogArtifacts(root, schema)
|
||||
expect(declared.filter((artifact, index) => artifact.content !== empty[index]?.content).map(artifact => artifact.path))
|
||||
.toEqual(experimentalPaths)
|
||||
expect(extractPersistenceSchema(root)).toEqual(schema)
|
||||
put(root, RECORDS, `interface SavedEntry { nested: { count: number } }\n${augmentation(ENTRY)}`)
|
||||
expect(render([], [], undefined, 'en', collectPluginRecords(root))).toBe(declared)
|
||||
const nullable = augmentation(ENTRY.replace(': SavedEntry', ': SavedEntry | null'))
|
||||
put(root, RECORDS, `interface SavedEntry { value: string }\n${nullable}`)
|
||||
const changedAnnotation = persistenceCatalogArtifacts(root, schema)
|
||||
expect(changedAnnotation.filter((artifact, index) => artifact.content !== declared[index]?.content).map(artifact => artifact.path))
|
||||
.toEqual(experimentalPaths)
|
||||
put(root, RECORDS, `interface SavedEntry { nested: { count: number } }\n${nullable}`)
|
||||
expect(persistenceCatalogArtifacts(root, schema)).toEqual(changedAnnotation)
|
||||
expect(extractPersistenceSchema(root)).toEqual(schema)
|
||||
expect(renderKnownEventTypes(annotateSurface(collectLogEvents(root), []))).toBe(known)
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@
|
||||
* Verify that only experimental packages call `appendPluginRecord` from production source.
|
||||
*
|
||||
* `appendPluginRecord` in `@deepseek-ai/dsh-session` appends an ignorable `plugin:` record that the
|
||||
* persistence catalogue lists separately and that a Session format migration keeps only on a best-effort
|
||||
* experimental persistence catalogue lists and that a Session format migration keeps only on a best-effort
|
||||
* basis. Experimental packages may rely on that; a release package must declare its events instead.
|
||||
* The owning module and test files, which exercise the operation, are exempt.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user