From 5a6243900da5705c1a6e6609640efd2f015215de Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:29:42 +0800 Subject: [PATCH] fix(doc-sync): close verify-type-equiv scan gap; correct persistence prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found verify-type-equiv only scanned docs the manifest already named, so a type-equiv block in an unmanifested doc was silently skipped — defeating the 1:1 guarantee. Scan all docs in the markdown glob scope instead, so an orphan block in any doc is caught. Also parse `abstract class` in blockSymbol (matches sourceDeclaration's class support). persistence.md listed the SessionPersistence surface as create/append/load/list; the abstract service also exposes has/delete. AGENTS.md's doc-sync command summary omitted verify-md-links and verify-type-equiv. --- AGENTS.md | 2 +- docs/core-data-structures/persistence.md | 4 +-- scripts/verify-type-equiv.ts | 35 ++++++++++++++++++------ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 00e83edb6b..d6c56e2e9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ pnpm run verify-event-taxonomy # assert the event-taxonomy table in docs/archit # matches the interface Events declarations in source pnpm run verify-md-wrap # assert no hard-wrapped prose paragraphs in README.md, # docs/**/*.md, packages/*/README.md, AGENTS.md (one line per paragraph) -pnpm run doc-sync # doc-typecheck + verify-event-taxonomy + verify-md-wrap (CI runs this) +pnpm run doc-sync # doc-typecheck + verify-event-taxonomy + verify-md-wrap + verify-md-links + verify-type-equiv (CI runs this) pnpm run demo:echo # run examples/echo-agent (no API key; type "echo hi" to # see a tool call) — the mock skeleton pnpm run demo:coding # run examples/coding-agent — the real agent (needs diff --git a/docs/core-data-structures/persistence.md b/docs/core-data-structures/persistence.md index 8bc132ffb2..f232fae1c9 100644 --- a/docs/core-data-structures/persistence.md +++ b/docs/core-data-structures/persistence.md @@ -2,7 +2,7 @@ The **durability seam** for the event log. [session.md](session.md) describes the in-memory `Session` — the append-only `SessionEvent` log that is the source of truth. This page describes how that log is made durable: the abstract `SessionPersistence` service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. -The seam is a textbook [capability seam](../rfc/implemented/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence), `ctx.sessionPersistence`) defining create/append/load/list over the existing `SessionEvent` — **no parallel persisted type** — and two interchangeable backends that pass the same `runPersistenceContract` suite. See the [session-persistence RFC](../rfc/implemented/2026-06-14-session-persistence.md). +The seam is a textbook [capability seam](../rfc/implemented/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence), `ctx.sessionPersistence`) defining create/append/load/list/has/delete over the existing `SessionEvent` — **no parallel persisted type** — and two interchangeable backends that pass the same `runPersistenceContract` suite. See the [session-persistence RFC](../rfc/implemented/2026-06-14-session-persistence.md). ## The flush checkpoint @@ -55,7 +55,7 @@ Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resumi ## The backends -Both implement the same abstract `SessionPersistence` (create/append/load/list over `SessionEvent`) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic: +Both implement the same abstract `SessionPersistence` (create/append/load/list/has/delete over `SessionEvent`) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic: - **[dsh-session-persistence-jsonl](../../packages/session-persistence-jsonl)** — an append-only JSONL log per session with crash-safe atomic writes, the interrupted-turn crash recovery above, and a read/replay path. - **[dsh-session-persistence-sqlite](../../packages/session-persistence-sqlite)** — `node:sqlite`, one row per `SessionEvent`. The row shape `(session_id, seq, type, time, data)` maps 1:1 onto the event, so there is no parallel persisted schema to keep in sync. diff --git a/scripts/verify-type-equiv.ts b/scripts/verify-type-equiv.ts index 5de48b97f9..54938e8b66 100644 --- a/scripts/verify-type-equiv.ts +++ b/scripts/verify-type-equiv.ts @@ -23,11 +23,21 @@ */ import { readFileSync, existsSync } from 'node:fs' -import { relative, resolve } from 'node:path' +import { resolve } from 'node:path' +import { glob } from 'node:fs/promises' import ts from 'typescript' const root = resolve(import.meta.dirname, '..') +/** + * Markdown globs scanned for ` ```ts type-equiv ` blocks — the SAME scope + * doc-typecheck uses. Scanning every doc (not only the docs the manifest names) + * is what makes the 1:1 guarantee real in both directions: a type-equiv block + * added to a doc with NO manifest entry is still discovered here and reported as + * an orphan, instead of being silently skipped. + */ +const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/README.md'] + /** One manifest entry: a documented type-equiv block and its source symbol. */ interface ManifestEntry { /** Doc file (repo-relative) containing the ` ```ts type-equiv ` block. */ @@ -71,7 +81,7 @@ function stripExport(code: string): string { /** Parse the declared symbol name from a type-equiv block body. */ function blockSymbol(code: string): string | null { - const m = /(?:export\s+(?:default\s+)?)?(?:interface|type|class|enum)\s+([A-Za-z0-9_]+)/.exec(code) + const m = /(?:export\s+(?:default\s+)?)?(?:abstract\s+)?(?:interface|type|class|enum)\s+([A-Za-z0-9_]+)/.exec(code) return m?.[1] ?? null } @@ -133,13 +143,22 @@ const entries = manifest.entries // doc, but at most once per doc). const keyOf = (x: { doc: string; symbol: string }): string => `${x.doc}::${x.symbol}` -// Collect every type-equiv block across the docs the manifest references. -const docFiles = [...new Set(entries.map(e => e.doc))] -const missingDocs = docFiles.filter(d => !existsSync(resolve(root, d))) -const blocks: EquivBlock[] = docFiles.filter(d => existsSync(resolve(root, d))).flatMap(extractEquivBlocks) +// Collect every type-equiv block across ALL docs in scope — not only the docs +// the manifest names — so a block in an unmanifested doc is found and reported +// as an orphan rather than silently skipped. +const docSet = new Set() +for (const pattern of MARKDOWN_GLOBS) { + for await (const match of glob(pattern, { cwd: root })) docSet.add(match) +} +const blocks: EquivBlock[] = [...docSet].sort().flatMap(extractEquivBlocks) const errors: string[] = [] -for (const d of missingDocs) errors.push(`manifest references ${d}, which does not exist`) +// A manifest entry naming a doc that does not exist (or is outside the scanned +// scope, so no block could ever match it) is an error in its own right. +for (const d of [...new Set(entries.map(e => e.doc))]) { + if (!existsSync(resolve(root, d))) errors.push(`manifest references ${d}, which does not exist`) + else if (!docSet.has(d)) errors.push(`manifest references ${d}, which is outside the scanned markdown scope (${MARKDOWN_GLOBS.join(', ')})`) +} // Duplicate-block guard: the same symbol twice in one doc is ambiguous. const blockByKey = new Map() @@ -204,5 +223,5 @@ if (errors.length === 0) { console.error('verify-type-equiv: type-equiv verification failed:') for (const e of errors) console.error(` ${e}`) -console.error(`\n(checked ${blocks.length} block(s) across ${docFiles.map(d => relative(root, resolve(root, d))).length} doc(s); manifest at scripts/type-equiv.manifest.json)`) +console.error(`\n(checked ${blocks.length} block(s) across ${new Set(blocks.map(b => b.doc)).size} doc(s); manifest at scripts/type-equiv.manifest.json)`) process.exit(1)