diff --git a/AGENTS.md b/AGENTS.md index ee5eb7d6f0..c5e7f5c701 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,9 +188,11 @@ pnpm run verify-doc-refs # assert every docs/*.md path cited in a packages|exam # TypeScript comment resolves (catches a moved/renamed doc) pnpm run verify-package-paths # assert every packages/ cited in Markdown or a # TypeScript comment resolves when it names a real (moved) package +pnpm run gen-rfc-index # regenerate the docs/rfc/README.md index tables from the + # RFC tree (marker-delimited; rows from path + H1 + filename date) pnpm run verify-rfc-classification # assert every RFC lives in a valid - # {lifecycle}/{class}/ folder and docs/rfc/README.md lists it - # under the matching heading (closed class set + index completeness) + # {lifecycle}/{class}/ folder and the generated README index + # regions are fresh (closed class set + index freshness) pnpm run verify-translation-pairing # assert the bilingual pairing contract # (docs/i18n/README.md): required docs have a complete pair # (foo.md + foo.zh.md + foo.i18n.yaml); every pair matches its diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 6124c94ffc..d53684bf6b 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -16,7 +16,7 @@ The date in the filename is when the topic was **first proposed** (per git histo ## Classification -Each RFC is filed under exactly one **class** — the kind of decision it records. The class is encoded in the path (the folder *is* the label, so a file's location declares its class) and the set is **closed**: `scripts/verify-rfc-classification.ts` rejects any folder outside the set and asserts this index lists every RFC under the heading matching its path. Adding a new class means amending that gate and this section, not just dropping a new folder. See [the classification RFC](implemented/process/2026-06-20-rfc-classification.md) for why the taxonomy is path-encoded and gated. +Each RFC is filed under exactly one **class** — the kind of decision it records. The class is encoded in the path (the folder *is* the label, so a file's location declares its class) and the set is **closed**: `scripts/rfc-index.ts` owns the canonical set, `scripts/verify-rfc-classification.ts` rejects any folder outside it, and the index tables below are **generated** from the tree (`pnpm run gen-rfc-index` rewrites the marker-delimited regions from each RFC's path, H1 title, and filename date; the gate fails when they are stale). Adding a new class means amending that `const` and this section, not just dropping a new folder. See [the classification RFC](implemented/process/2026-06-20-rfc-classification.md) for why the taxonomy is path-encoded and gated, and [the index-generation RFC](implemented/process/2026-07-04-generate-rfc-index-tables.md) for why the tables are generated while this prose stays curated. | Class | What it covers | |---|---| @@ -37,11 +37,12 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r ## Proposed + ### Feature | Title | First proposed | |---|---| -| [Agent Client Protocol (ACP) support for external editors](proposed/feature/2026-06-14-acp-agent-client-protocol.md) | 2026-06-14 | +| [Agent Client Protocol (ACP) support — drive the coding agent from external editors](proposed/feature/2026-06-14-acp-agent-client-protocol.md) | 2026-06-14 | | [Multiplex concurrent ACP sessions over one connection](proposed/feature/2026-06-14-acp-multi-session.md) | 2026-06-14 | | [Optional Code Mode — model writes TypeScript against an SDK of all tools](proposed/feature/2026-06-15-optional-code-mode.md) | 2026-06-15 | | [Pre-tool input rewrite — a consistent design](proposed/feature/2026-06-30-pre-tool-input-rewrite.md) | 2026-06-30 | @@ -51,17 +52,17 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | Title | First proposed | |---|---| | [Unify the agent id and the session id](proposed/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 | -| [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 | +| [Drop the `image` content block until a path can honor it](proposed/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 | | [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](proposed/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 | | [Drop the unconsumed web observation surface — the `providers-change` event and the status methods](proposed/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) | 2026-07-04 | -| [Drop the `image` content block until a path can honor it](proposed/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 | -| [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 | -| [Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback](proposed/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md) | 2026-07-04 | -| [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | -| [Share the app bins' boot glue instead of maintaining twin copies](proposed/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 | -| [Remove the `agent/steering` mirror emit](proposed/simplification/2026-07-04-remove-agent-steering-mirror.md) | 2026-07-04 | -| [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](proposed/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 | | [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 | +| [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | +| [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 | +| [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 | +| [Remove the `agent/steering` mirror emit](proposed/simplification/2026-07-04-remove-agent-steering-mirror.md) | 2026-07-04 | +| [Share the app bins' boot glue instead of maintaining twin copies](proposed/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 | +| [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](proposed/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 | +| [Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback](proposed/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md) | 2026-07-04 | ### Architecture @@ -74,47 +75,48 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | Title | First proposed | |---|---| -| [Architectural conformance — dependency rules and the adapter kit](proposed/process/2026-06-11-architectural-conformance.md) | 2026-06-11 | | [API extractor reports](proposed/process/2026-06-11-api-extractor-reports.md) | 2026-06-11 | +| [Architectural conformance — dependency rules and the adapter kit](proposed/process/2026-06-11-architectural-conformance.md) | 2026-06-11 | | [Supply chain checks and vendor drift verification](proposed/process/2026-06-11-supply-chain-and-vendor-drift.md) | 2026-06-11 | | [Discover package inventories instead of maintaining static lists](proposed/process/2026-06-20-discover-package-inventory.md) | 2026-06-20 | -| [Generate the RFC index tables](proposed/process/2026-07-04-generate-rfc-index-tables.md) | 2026-07-04 | ### Testing | Title | First proposed | |---|---| -| [Mutation testing as the coverage counterweight](proposed/testing/2026-06-11-mutation-testing.md) | 2026-06-11 | | [Deterministic tests, the replay invariant fixture, and race stress](proposed/testing/2026-06-11-deterministic-and-stress-testing.md) | 2026-06-11 | +| [Mutation testing as the coverage counterweight](proposed/testing/2026-06-11-mutation-testing.md) | 2026-06-11 | | [Single-source the acp-agent replay config](proposed/testing/2026-07-04-single-source-acp-replay-config.md) | 2026-07-04 | + ## Implemented + ### Feature | Title | First proposed | |---|---| | [Filesystem tool schemas — model-facing read/write/edit shapes](implemented/feature/2026-06-17-filesystem-tool-schemas.md) | 2026-06-17 | -| [Rich ACP bash rendering — the terminal card (`_meta`) and command classification](implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) | 2026-06-18 | +| [Rich ACP bash rendering — the terminal card via the `_meta` convention](implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) | 2026-06-18 | | [Compaction as a capability seam (abstract contract + basic backend)](implemented/feature/2026-06-18-compaction-capability-seam.md) | 2026-06-18 | | [Subagent capability seam](implemented/feature/2026-06-21-subagent-capability-seam.md) | 2026-06-21 | | [ACP subagent backend (out-of-process delegation)](implemented/feature/2026-06-22-acp-subagent-backend.md) | 2026-06-22 | | [The `todo_write` tool — model task list as event-sourced session state](implemented/feature/2026-06-29-todo-write-tool.md) | 2026-06-29 | +| [dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges](implemented/feature/2026-06-30-hook-bridges.md) | 2026-06-30 | +| [dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core](implemented/feature/2026-06-30-hook-protocol-lib.md) | 2026-06-30 | | [Interception seams — the typed-Decision surface a hook programs against](implemented/feature/2026-06-30-interception-seams.md) | 2026-06-30 | | [Subagent lifecycle enrichment — lastAssistantMessage (observe-only)](implemented/feature/2026-06-30-subagent-observe-enrich.md) | 2026-06-30 | -| [dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core](implemented/feature/2026-06-30-hook-protocol-lib.md) | 2026-06-30 | -| [dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges](implemented/feature/2026-06-30-hook-bridges.md) | 2026-06-30 | ### Simplification | Title | First proposed | |---|---| | [Drop the mutable session summary](implemented/simplification/2026-06-19-drop-mutable-session-summary.md) | 2026-06-19 | -| [Drop unconsumed assembled LLM convenience surfaces](implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md) | 2026-06-20 | +| [Fold trace-only session facts into load-bearing events](implemented/simplification/2026-06-20-collapse-trace-only-session-events.md) | 2026-06-20 | | [Drop the unconsumed `llm/adapter-change` event](implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md) | 2026-06-20 | +| [Drop unconsumed assembled LLM convenience surfaces](implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md) | 2026-06-20 | | [Prune dead methods from the persistence seam](implemented/simplification/2026-06-20-prune-dead-seam-methods.md) | 2026-06-20 | | [Keep one public stop primitive](implemented/simplification/2026-06-20-public-agent-stop-surface.md) | 2026-06-20 | -| [Fold trace-only session facts into load-bearing events](implemented/simplification/2026-06-20-collapse-trace-only-session-events.md) | 2026-06-20 | | [Stop mirroring durable boundaries as agent events](implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 | | [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 | | [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 | @@ -123,50 +125,51 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | Title | First proposed | |---|---| -| [Microkernel: extension via Cordis event taxonomy, one concrete loop](implemented/architecture/2026-06-11-microkernel-event-taxonomy.md) | 2026-06-11 | -| [Event-sourced sessions with derived message history](implemented/architecture/2026-06-11-event-sourced-sessions.md) | 2026-06-11 | | [Provider-neutral content-block vocabulary owned by dsh-llm](implemented/architecture/2026-06-11-content-block-vocabulary.md) | 2026-06-11 | | [Custom typed tool-schema DSL instead of schemastery](implemented/architecture/2026-06-11-custom-schema-dsl.md) | 2026-06-11 | -| [Tool schemas are part of the system-prompt assembly](implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md) | 2026-06-11 | -| [Runtime arg validation at the model boundary](implemented/architecture/2026-06-11-runtime-arg-validation.md) | 2026-06-11 | | [Dev-mode invariants over compile-time deep-readonly](implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md) | 2026-06-11 | +| [Event-sourced sessions with derived message history](implemented/architecture/2026-06-11-event-sourced-sessions.md) | 2026-06-11 | +| [Microkernel — extension via Cordis event taxonomy, one concrete loop](implemented/architecture/2026-06-11-microkernel-event-taxonomy.md) | 2026-06-11 | +| [Runtime arg validation at the model boundary](implemented/architecture/2026-06-11-runtime-arg-validation.md) | 2026-06-11 | | [Structured error taxonomy](implemented/architecture/2026-06-11-structured-error-taxonomy.md) | 2026-06-11 | +| [Tool schemas are part of the system-prompt assembly](implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md) | 2026-06-11 | | [Capability seams — interface / implementation / consumer split](implemented/architecture/2026-06-13-capability-seams.md) | 2026-06-13 | | [Two LLM adapters as a design-verification twin](implemented/architecture/2026-06-13-twin-llm-adapters.md) | 2026-06-13 | -| [Session persistence as an abstract service over `SessionEvent`](implemented/architecture/2026-06-14-session-persistence.md) | 2026-06-14 | +| [Session persistence as an abstract service over the existing `SessionEvent`](implemented/architecture/2026-06-14-session-persistence.md) | 2026-06-14 | | [Every session event is enclosed in a turn](implemented/architecture/2026-06-15-turn-enclosure-invariant.md) | 2026-06-15 | | [Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools](implemented/architecture/2026-06-17-filesystem-capability-seam.md) | 2026-06-17 | -| [Shared persistence write coordinator](implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) | 2026-06-18 | | [Agent lifecycle and ownership seams](implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md) | 2026-06-18 | | [Session surface — a linked list over the event log for LLM message derivation](implemented/architecture/2026-06-18-session-surface.md) | 2026-06-18 | -| [Reorganize packages into a modular hierarchy](implemented/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 | +| [Shared persistence write coordinator](implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) | 2026-06-18 | | [Branded IDs everywhere they belong](implemented/architecture/2026-06-20-branded-ids.md) | 2026-06-20 | | [Extract example apps into packages](implemented/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 | -| [Web capability seam — provider registry and model-facing web tools](implemented/architecture/2026-06-24-web-capability-seam.md) | 2026-06-24 | +| [Reorganize packages into a modular hierarchy](implemented/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 | +| [Web capability seam - stable tools over multiple providers](implemented/architecture/2026-06-24-web-capability-seam.md) | 2026-06-24 | | [Make `dsh-fs-policy` an event-gate plugin, not a method interface](implemented/architecture/2026-06-26-file-context-as-event-gate.md) | 2026-06-26 | -| [Event-domain semantics — session is the fact log, agent is the live surface](implemented/architecture/2026-06-30-event-domain-semantics.md) | 2026-06-30 | | [stdin + extra env on the bash seam](implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) | 2026-06-30 | +| [Event-domain semantics — session is the fact log, agent is the live surface](implemented/architecture/2026-06-30-event-domain-semantics.md) | 2026-06-30 | | [Resolve filesystem paths against the caller's session cwd](implemented/architecture/2026-07-02-fs-per-session-cwd.md) | 2026-07-02 | -| [Tagged render-intent union for tool-call presentation](implemented/architecture/2026-07-02-tool-render-intent-union.md) | 2026-07-02 | | [Result-time applied-hunk diffs for file mutations](implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md) | 2026-07-02 | +| [Tagged render-intent union for tool-call presentation](implemented/architecture/2026-07-02-tool-render-intent-union.md) | 2026-07-02 | | [Add direct directory listing to the filesystem seam](implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md) | 2026-07-03 | ### Process | Title | First proposed | |---|---| -| [Vendor Cordis as source, not npm dependencies](implemented/process/2026-06-11-vendor-cordis-as-source.md) | 2026-06-11 | +| [Doc-sync enforcement](implemented/process/2026-06-11-doc-sync-enforcement.md) | 2026-06-11 | | [Mechanical quality gates over prose guidelines](implemented/process/2026-06-11-quality-gates.md) | 2026-06-11 | | [tsdown for JS bundling instead of dumble](implemented/process/2026-06-11-tsdown-over-dumble.md) | 2026-06-11 | -| [Doc-sync enforcement](implemented/process/2026-06-11-doc-sync-enforcement.md) | 2026-06-11 | +| [Vendor Cordis as source, not npm dependencies](implemented/process/2026-06-11-vendor-cordis-as-source.md) | 2026-06-11 | | [pnpm as the package manager instead of Yarn 4](implemented/process/2026-06-16-pnpm-over-yarn.md) | 2026-06-16 | | [TSC-first build and one tsconfig](implemented/process/2026-06-17-ts-build-config.md) | 2026-06-17 | | [Markdown cross-link validity linting](implemented/process/2026-06-18-markdown-cross-link-lint.md) | 2026-06-18 | | [Core-data-structures catalog and the `ts type-equiv` drift gate](implemented/process/2026-06-20-core-data-structures-catalog.md) | 2026-06-20 | | [Generated cordis events + services catalog](implemented/process/2026-06-20-generated-cordis-catalog.md) | 2026-06-20 | | [Classify RFCs by kind via path-encoded subdirectories](implemented/process/2026-06-20-rfc-classification.md) | 2026-06-20 | -| [Generated tool-schema catalog (boot-and-harvest)](implemented/process/2026-07-02-tool-schema-catalog.md) | 2026-07-02 | | [Bilingual documentation via paired sibling files and a pairing gate](implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md) | 2026-07-02 | +| [Generated tool-schema catalog (boot-and-harvest)](implemented/process/2026-07-02-tool-schema-catalog.md) | 2026-07-02 | +| [Generate the RFC index tables](implemented/process/2026-07-04-generate-rfc-index-tables.md) | 2026-07-04 | ### Testing @@ -176,13 +179,15 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [ACP snapshot tests — record-once / replay-deterministic](implemented/testing/2026-06-19-acp-snapshot-tests.md) | 2026-06-19 | | [Real-API e2e in CI against the external DeepSeek API](implemented/testing/2026-06-19-real-api-e2e-ci.md) | 2026-06-19 | | [Use `session.jsonl` as the only snapshot session-log artifact](implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.md) | 2026-06-20 | -| [Per-session snapshot replay for nested agents](implemented/testing/2026-06-22-subagent-snapshot-replay.md) | 2026-06-22 | | [Persist the seed boundary so fork-child replay routes correctly](implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md) | 2026-06-22 | | [Record fork and mixed spawn+fork snapshot scenarios](implemented/testing/2026-06-22-fork-snapshot-scenarios.md) | 2026-06-22 | +| [Per-session snapshot replay for nested agents](implemented/testing/2026-06-22-subagent-snapshot-replay.md) | 2026-06-22 | | [Hook snapshot matrix — end-to-end goldens for both bridges](implemented/testing/2026-07-04-hook-snapshot-matrix.md) | 2026-07-04 | + ## Rejected + ### Simplification | Title | First proposed | @@ -206,3 +211,4 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r |---|---| | [Deep-readonly public surfaces](rejected/architecture/2026-06-11-immutable-public-surfaces.md) | 2026-06-11 | | [Make the shared example base providerless](rejected/architecture/2026-06-20-providerless-example-base.md) | 2026-06-20 | + diff --git a/docs/rfc/implemented/process/2026-06-20-rfc-classification.md b/docs/rfc/implemented/process/2026-06-20-rfc-classification.md index d3fc95399b..21d4129fac 100644 --- a/docs/rfc/implemented/process/2026-06-20-rfc-classification.md +++ b/docs/rfc/implemented/process/2026-06-20-rfc-classification.md @@ -29,18 +29,18 @@ The `architecture` / `process` line: **architecture** is about the source we shi Both are `doc-sync` members, in the `verify-md-wrap` style (tsx ESM, verify-don't-generate, exit non-zero on the first violation): -- **`scripts/verify-rfc-classification.ts`** — the closed set and index completeness. It asserts every file under a lifecycle folder lives in a class folder from the canonical set (a loose `.md` at a lifecycle root, or an unknown class folder, fails), and that `README.md` lists every RFC exactly once under the `###` heading matching its `{lifecycle}/{class}` path. The canonical class set lives as a `const` in this script — the machine source of truth — and [the index](../../README.md) documents it in prose; the two are kept in sync by hand (the README's completeness is gated, its class *descriptions* are not). This mirrors `verify-event-taxonomy`, which checks a doc table against source. +- **`scripts/verify-rfc-classification.ts`** — the closed set and index freshness. It asserts every file under a lifecycle folder lives in a class folder from the canonical set (a loose `.md` at a lifecycle root, or an unknown class folder, fails), and that the README's marker-delimited index regions byte-match a fresh render from the tree (see [generate the RFC index tables](2026-07-04-generate-rfc-index-tables.md)). The canonical class set lives as a `const` in `scripts/rfc-index.ts` — the machine source of truth shared with the generator — and [the index](../../README.md) documents it in prose; the README's class *descriptions* stay hand-written, its tables are generated. - **`scripts/verify-doc-refs.ts`** — source comments that cite docs. RFC paths are referenced not only from Markdown but from TypeScript doc comments (root-relative prose like `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`). `verify-md-links` never saw those, so the reorg could have silently orphaned them. This gate scans repo-authored `.ts` under `packages/**` and `examples/**` (excluding built `lib/` and `vendor/`) for `docs/….md` tokens, resolves each root-relative, and asserts it exists. It requires the `.md` extension so extensionless prose (`docs/postmortem/0001`, `docs/architecture.md § plugin checklist`) is left alone. ### Rejected alternatives - **A `Classification:` prose line** in each file (next to `Status:`), parsed by the gate. Workable, but it duplicates into the file a fact the path can already carry, and a line can disagree with its folder. Path-encoding makes the label and its storage the same thing — there is nothing to keep in sync. - **A `refactor` class.** It overlaps `simplification` almost entirely; the only discriminator anyone reached for was "does observable behavior change?", which `simplification` already encodes (it does not). One class, not two. -- **Auto-generating the README index** from the filesystem. Rejected to keep the index hand-written like every other doc here; the completeness gate gives the same drift-protection without generated Markdown in a curated file. +- **Auto-generating the README index** from the filesystem. Rejected here to keep the index hand-written; superseded by [generate the RFC index tables](2026-07-04-generate-rfc-index-tables.md) once stacked proposal waves made the hand-written tables the repo's most conflict-prone docs region — the tables are now generated between markers while the surrounding prose stays curated. ## Consequences - Every RFC now sits under a class folder, and the index groups by class within each lifecycle. A reader scans one heading to see all simplifications, or all testing decisions. - Two more fast tsx scripts in the `doc-sync` chain; no new dependency (the mdast/GFM stack was already present for `verify-md-wrap`/`verify-md-links`). -- Adding a class is a deliberate act: amend the `const` in `verify-rfc-classification.ts` and the [Classification section](../../README.md#classification), not just `mkdir` a folder. The gate rejects an unknown folder, so an ad-hoc class can't slip in. +- Adding a class is a deliberate act: amend the `const` in `scripts/rfc-index.ts` and the [Classification section](../../README.md#classification), not just `mkdir` a folder. The gate rejects an unknown folder, so an ad-hoc class can't slip in. - Source-comment doc references are now gated too — a moved or renamed doc that a `.ts` comment cites fails the pre-push hook, closing a drift class `verify-md-links` structurally could not see. diff --git a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md new file mode 100644 index 0000000000..8039dd50f4 --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md @@ -0,0 +1,26 @@ +# RFC: Generate the RFC index tables + +Status: implemented + +## Problem + +`docs/rfc/README.md`'s per-lifecycle/per-class tables list facts that are fully derivable: an RFC's path encodes lifecycle and class, its filename encodes the first-proposed date, and its H1 carries the title. A hand-maintained copy of those facts is also the repo's highest-contention docs hotspot: every proposal wave appends rows to the same few lines, so concurrent RFC branches conflict precisely there while agreeing everywhere else, and each conflict is resolved by hand-merging rows whose content the filesystem already knows. [The classification RFC](2026-06-20-rfc-classification.md) originally kept the index hand-written for curation's sake — but the curated part of the README is the prose, and the prose never conflicts; only the mechanical tables do. + +## Decision + +Keep the curated prose; generate the tables. [`scripts/rfc-index.ts`](../../../../scripts/rfc-index.ts) is the shared source of truth — the tree walker (owning the closed lifecycle/class sets and the structure rules, including a parseable-H1 requirement) and the renderer (rows from H1 title with any `RFC: ` prefix stripped, plus the filename date, sorted by date then filename, grouped as `### {Class}` sections in canonical class order). Two thin consumers share it: + +- [`scripts/gen-rfc-index.ts`](../../../../scripts/gen-rfc-index.ts) (`pnpm run gen-rfc-index`) rewrites the three marker-delimited regions in the README (`` … `end`), one per `## {Lifecycle}` section, leaving everything outside the markers untouched. +- [`scripts/verify-rfc-classification.ts`](../../../../scripts/verify-rfc-classification.ts) (a `doc-sync` member) checks structure and asserts the committed regions byte-match a fresh render — the `gen-cordis-catalog`/`verify-cordis-catalog` pattern. Freshness subsumes the index-completeness check: a generated-from-disk table is definitionally complete and correctly headed. + +Adding, moving, or deleting an RFC means editing only the RFC file and running the generator; the classification RFC's rejected-alternatives record carries the supersession cross-link. + +## Why not the verifier-only model? + +It catches mistakes but still makes every proposal edit a shared hotspot, and a failed verifier is strictly more annoying than a generator for a purely mechanical row: the author has already named and placed the file; the index copy adds no information. This is the same hand-list-versus-derivation judgment the [package-inventory proposal](../../proposed/process/2026-06-20-discover-package-inventory.md) applies to tsconfig references and knip stanzas — applied to the one list that demonstrably conflicts. + +## Consequences + +- The generated regions are explicit: marker comments make script ownership obvious to reviewers, and the generator refuses to run on a structurally invalid tree. +- A malformed or missing H1 is a hard error in both the generator and the gate — the H1 is now load-bearing as the index title source. +- Concurrent RFC branches resolve index conflicts by rerunning the generator, never by hand-merging rows. diff --git a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md b/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md index 3e731af4b9..a3bb6d4718 100644 --- a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md +++ b/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md @@ -1,4 +1,4 @@ -# RFC: Pre-tool input rewrite — a consistent design (proposed) +# RFC: Pre-tool input rewrite — a consistent design Status: proposed (2026-06-30) diff --git a/docs/rfc/proposed/process/2026-07-04-generate-rfc-index-tables.md b/docs/rfc/proposed/process/2026-07-04-generate-rfc-index-tables.md deleted file mode 100644 index b3c88c47ad..0000000000 --- a/docs/rfc/proposed/process/2026-07-04-generate-rfc-index-tables.md +++ /dev/null @@ -1,27 +0,0 @@ -# RFC: Generate the RFC index tables - -Status: proposed - -## Problem - -`docs/rfc/README.md`'s per-lifecycle/per-class tables are hand-maintained even though every fact in them is derivable: an RFC's path encodes lifecycle and class, its filename encodes the first-proposed date, and its H1 carries the title. `scripts/verify-rfc-classification.ts` already walks the tree and cross-checks the index — the expensive parsing exists; it reports instead of writing. - -The tables are also the repo's highest-contention docs hotspot: every proposal wave appends rows to the same few lines, so concurrent RFC branches conflict precisely there while agreeing everywhere else, and each conflict is resolved by hand-merging rows whose content the filesystem already knows. [The classification RFC](../../implemented/process/2026-06-20-rfc-classification.md) records rejecting auto-generation to keep the file curated — but the curated part of the README is the prose, and the prose never conflicts; only the mechanical tables do. - -## Proposal - -Keep the curated prose; generate the tables. Add a `gen-rfc-index` mode (a `--write` flag on `verify-rfc-classification.ts`, or a sibling script sharing its walker) that scans the RFC tree, reads each H1, derives the date from the filename, and rewrites the table rows under stable generated markers per `## {Lifecycle}` / `### {Class}` section; `verify-rfc-classification` asserts freshness — the `gen-cordis-catalog`/`verify-cordis-catalog` pattern. The class and lifecycle sets stay closed in the script. The implementing PR amends the classification RFC's rejected-alternatives record per [implemented/AGENTS.md](../../implemented/AGENTS.md), since this supersedes that recorded choice. - -## Why not keep the verifier-only model? - -It catches mistakes but still makes every proposal edit a shared hotspot, and a failed verifier is strictly more annoying than a generator for a purely mechanical row: the author has already named and placed the file; the index copy adds no information. This is the same hand-list-versus-derivation judgment the [package-inventory proposal](2026-06-20-discover-package-inventory.md) applies to tsconfig references and knip stanzas — applied to the one list that demonstrably conflicts. - -## Acceptance criteria - -- `pnpm run gen-rfc-index` (or the chosen spelling) rewrites only the generated table regions; `verify-rfc-classification` fails when they are stale and passes after regeneration. -- Adding, moving, or deleting an RFC requires editing only the RFC file itself; the rows are produced from path + H1 + filename date. -- The prose outside the generated markers is untouched by the generator; `pnpm run doc-sync` passes. - -## Risks - -Generated regions inside a curated file need explicit markers so ownership is obvious to reviewers. Reading H1s makes a malformed header a generator error — useful pressure, and it should fail clearly. This supersedes an implemented process decision; amending that RFC's record is part of the change, not optional. diff --git a/package.json b/package.json index 893b0059f6..a9d9545b6c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts", "verify-node-next-types": "tsx scripts/verify-node-next-types.ts", "gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts", + "gen-rfc-index": "tsx scripts/gen-rfc-index.ts", "verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check", "gen-tool-catalog": "tsx scripts/gen-tool-catalog.ts", "verify-tool-catalog": "tsx scripts/gen-tool-catalog.ts --check", diff --git a/scripts/gen-rfc-index.ts b/scripts/gen-rfc-index.ts new file mode 100644 index 0000000000..ad7aeb6588 --- /dev/null +++ b/scripts/gen-rfc-index.ts @@ -0,0 +1,30 @@ +/** + * Regenerate the RFC index tables in `docs/rfc/README.md` from the RFC tree + * (see [rfc-index.ts](./rfc-index.ts) for the layout contract and rendering + * rules). Rewrites ONLY the marker-delimited regions; the curated prose is + * untouched. Freshness is asserted by `verify-rfc-classification.ts` (a + * `doc-sync` member), so a stale committed index fails CI. + * + * Run: `pnpm run gen-rfc-index`. + */ + +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { rfcRoot, spliceReadme, walkRfcTree } from './rfc-index.ts' + +const { rfcs, errors } = walkRfcTree() +if (errors.length > 0) { + console.error('gen-rfc-index: refusing to generate from a structurally invalid tree:') + for (const e of errors) console.error(` ${e}`) + process.exit(1) +} + +const readmePath = resolve(rfcRoot, 'README.md') +const readme = readFileSync(readmePath, 'utf8') +const next = spliceReadme(readme, rfcs) +if (next === readme) { + console.log(`gen-rfc-index: docs/rfc/README.md is up to date (${rfcs.length} RFCs).`) +} else { + writeFileSync(readmePath, next) + console.log(`gen-rfc-index: docs/rfc/README.md regenerated (${rfcs.length} RFCs).`) +} diff --git a/scripts/rfc-index.ts b/scripts/rfc-index.ts new file mode 100644 index 0000000000..62d9cb0fcf --- /dev/null +++ b/scripts/rfc-index.ts @@ -0,0 +1,141 @@ +/** + * Shared source of truth for the RFC index: the tree walker (structure rules) + * and the README table renderer. `gen-rfc-index.ts` writes the generated + * regions; `verify-rfc-classification.ts` checks structure and asserts the + * committed regions are fresh. Pure module — no side effects on import. + * + * The layout contract ([the classification RFC](../docs/rfc/implemented/process/2026-06-20-rfc-classification.md)): + * every RFC lives at `docs/rfc/{lifecycle}/{class}/yyyy-mm-dd-topic.md`, the + * folder IS the label, and both sets are CLOSED — extending either means + * amending this module AND the README's Classification prose. + * + * The README's per-lifecycle tables are GENERATED between marker comments + * (`` … `end`): section headings and + * rows are derived from each RFC's path (lifecycle/class), H1 (title, with an + * optional `RFC: ` prefix stripped), and filename date, sorted by date then + * filename. Prose outside the markers is curated by hand and never touched. + */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { globSync } from 'node:fs' + +export const rfcRoot = resolve(import.meta.dirname, '../docs/rfc') + +/** The closed set of RFC lifecycles (top-level folders under docs/rfc/). */ +export const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const + +/** + * The closed set of RFC classes (nested folder under each lifecycle). Adding a + * class is a deliberate act: extend this list AND the README's Classification + * section. The gate rejects any folder not listed here. + */ +export const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const + +/** Non-RFC Markdown allowed to sit directly at a lifecycle root. */ +const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md']) + +/** Title-case a class/lifecycle folder name for a README heading. */ +export const heading = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1) + +/** One RFC file, as discovered by the walker. */ +export interface Rfc { + lifecycle: string + cls: string + base: string + /** Path relative to docs/rfc — the README link target. */ + rel: string + /** H1 text with any `RFC: ` prefix stripped — the README row title. */ + title: string + /** `yyyy-mm-dd` from the filename — the "First proposed" column. */ + date: string +} + +/** + * Walk the RFC tree, enforcing the structure rules. Returns every valid RFC + * plus one error string per violation (unknown class folder, bad depth, bad + * filename, missing/malformed H1). Callers treat a non-empty error list as + * fatal — the index is only generated from a structurally valid tree. + */ +export function walkRfcTree(): { rfcs: Rfc[]; errors: string[] } { + const rfcs: Rfc[] = [] + const errors: string[] = [] + for (const lifecycle of LIFECYCLES) { + for (const match of globSync(`${lifecycle}/**/*.md`, { cwd: rfcRoot }).sort()) { + const segs = match.split('/') + // Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md). + if (segs.length === 2 && ROOT_ALLOWLIST.has(segs[1] ?? '')) continue + // A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME RFC, + // indexed via its English filename; the pairing gate owns its consistency. + if (match.endsWith('.zh.md')) continue + const cls = segs[1] + const base = segs[2] + if (segs.length !== 3 || cls === undefined || base === undefined) { + errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`) + continue + } + if (!(CLASSES as readonly string[]).includes(cls)) { + errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`) + continue + } + if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) { + errors.push(`structure: ${match} — filename must be yyyy-mm-dd-topic.md`) + continue + } + const firstLine = readFileSync(resolve(rfcRoot, match), 'utf8').split('\n', 1)[0] ?? '' + const h1 = /^#\s+(?:RFC:\s+)?(.+?)\s*$/.exec(firstLine) + if (!h1?.[1]) { + errors.push(`title: ${match} — first line must be an H1 (\`# RFC: \` or \`# <title>\`), got: ${JSON.stringify(firstLine)}`) + continue + } + rfcs.push({ lifecycle, cls, base, rel: match, title: h1[1], date: base.slice(0, 10) }) + } + } + return { rfcs, errors } +} + +/** The begin/end marker lines that delimit one lifecycle's generated region. */ +export const markers = (lifecycle: string): { begin: string; end: string } => ({ + begin: `<!-- gen-rfc-index:begin ${lifecycle} -->`, + end: `<!-- gen-rfc-index:end ${lifecycle} -->`, +}) + +/** + * Render one lifecycle's generated region body: a `### {Class}` heading plus a + * `| Title | First proposed |` table for every non-empty class, in CLASSES + * order, rows sorted by date then filename. + */ +export function renderLifecycle(rfcs: Rfc[], lifecycle: string): string { + const sections: string[] = [] + for (const cls of CLASSES) { + const rows = rfcs + .filter(r => r.lifecycle === lifecycle && r.cls === cls) + .sort((a, b) => a.date.localeCompare(b.date) || a.base.localeCompare(b.base)) + if (rows.length === 0) continue + const table = rows.map(r => `| [${r.title}](${r.rel}) | ${r.date} |`).join('\n') + sections.push(`### ${heading(cls)}\n\n| Title | First proposed |\n|---|---|\n${table}`) + } + return sections.join('\n\n') +} + +/** + * Splice freshly rendered regions into the README text. Throws when a marker + * pair is missing, duplicated, or out of order — the markers are part of the + * curated prose and must exist exactly once per lifecycle. + */ +export function spliceReadme(readme: string, rfcs: Rfc[]): string { + let out = readme + for (const lifecycle of LIFECYCLES) { + const { begin, end } = markers(lifecycle) + const beginAt = out.indexOf(begin) + const endAt = out.indexOf(end) + if (beginAt === -1 || endAt === -1 || endAt < beginAt) { + throw new Error(`README.md is missing the ${JSON.stringify(begin)} … ${JSON.stringify(end)} marker pair`) + } + if (out.indexOf(begin, beginAt + 1) !== -1 || out.indexOf(end, endAt + 1) !== -1) { + throw new Error(`README.md has a duplicated ${lifecycle} index marker`) + } + out = `${out.slice(0, beginAt + begin.length)}\n${renderLifecycle(rfcs, lifecycle)}\n${out.slice(endAt)}` + } + return out +} diff --git a/scripts/verify-rfc-classification.ts b/scripts/verify-rfc-classification.ts index 4ff4264731..b6f1ed2eb5 100644 --- a/scripts/verify-rfc-classification.ts +++ b/scripts/verify-rfc-classification.ts @@ -1,158 +1,50 @@ /** * Doc-sync gate: enforce the RFC classification scheme - * ([the classification RFC](../docs/rfc/implemented/process/2026-06-20-rfc-classification.md)). + * ([the classification RFC](../docs/rfc/implemented/process/2026-06-20-rfc-classification.md)) + * and the freshness of the generated index tables + * ([the index-generation RFC](../docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md)). * Every RFC is filed at `docs/rfc/{lifecycle}/{class}/yyyy-mm-dd-topic.md`; the * folder IS the label. This gate is the machine source of truth for the closed * class set and keeps the README index honest. * - * Two checks: + * Two checks (both against [rfc-index.ts](./rfc-index.ts), the shared walker + * and renderer): * * 1. STRUCTURE — every `.md` under a lifecycle folder lives in a class folder - * from CLASSES, named `yyyy-mm-dd-*.md`. A loose `.md` directly under a - * lifecycle root (other than the README/AGENTS allowlist) fails; an unknown - * class folder fails; a stray file at an unexpected depth fails. This is what - * makes the set CLOSED: a new class folder can't appear without amending - * CLASSES here (and the README's Classification section, per the RFC). + * from CLASSES, is named `yyyy-mm-dd-*.md`, and opens with a parseable H1. + * A loose `.md` directly under a lifecycle root (other than the + * README/AGENTS allowlist) fails; an unknown class folder fails; a stray + * file at an unexpected depth fails. This is what makes the set CLOSED: a + * new class folder can't appear without amending CLASSES (and the README's + * Classification section, per the RFC). * - * 2. COMPLETENESS — `docs/rfc/README.md` lists every RFC exactly once, under the - * `### {Class}` heading inside the `## {Lifecycle}` section that matches the - * file's path. A missing entry, a duplicate, or an entry under the wrong - * heading fails. This mirrors `verify-event-taxonomy`: a curated doc table - * checked against the on-disk source of truth, so the index can't drift. - * - * The class DESCRIPTIONS in the README prose are not checked (they are - * explanatory text); only the per-class index tables are. This is checker, not - * fixer: it reports and never rewrites. + * 2. FRESHNESS — the marker-delimited index regions in `docs/rfc/README.md` + * byte-match a fresh render from the tree, so every RFC is listed exactly + * once, under the heading matching its path, with its H1 title and filename + * date. The fix for a stale index is `pnpm run gen-rfc-index`, never a hand + * edit. This is checker, not fixer: it reports and never rewrites. * * Run: `tsx scripts/verify-rfc-classification.ts`. */ import { readFileSync } from 'node:fs' -import { relative, resolve } from 'node:path' -import { glob } from 'node:fs/promises' +import { resolve } from 'node:path' +import { rfcRoot, spliceReadme, walkRfcTree } from './rfc-index.ts' -const root = resolve(import.meta.dirname, '..') -const rfcRoot = resolve(root, 'docs/rfc') +const { rfcs, errors } = walkRfcTree() -/** The closed set of RFC lifecycles (top-level folders under docs/rfc/). */ -const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const - -/** - * The closed set of RFC classes (nested folder under each lifecycle). Adding a - * class is a deliberate act: extend this list AND the README's Classification - * section. The gate rejects any folder not listed here. - */ -const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const - -/** Non-RFC Markdown allowed to sit directly at a lifecycle root. */ -const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md']) - -/** Title-case a class/lifecycle folder name for README heading comparison. */ -const heading = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1) - -const errors: string[] = [] - -// --- Check 1: structure ----------------------------------------------------- -// Every Markdown file anywhere under a lifecycle folder, at any depth. -interface Rfc { - lifecycle: string - cls: string - base: string - /** Path relative to docs/rfc, for the README link check. */ - rel: string -} -const rfcs: Rfc[] = [] - -for (const lifecycle of LIFECYCLES) { - for await (const match of glob(`${lifecycle}/**/*.md`, { cwd: rfcRoot })) { - const segs = match.split('/') - // Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md). - if (segs.length === 2 && ROOT_ALLOWLIST.has(segs[1] ?? '')) continue - // A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME RFC, - // indexed via its English filename; the pairing gate owns its consistency. - if (match.endsWith('.zh.md')) continue - const cls = segs[1] - const base = segs[2] - if (segs.length !== 3 || cls === undefined || base === undefined) { - errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`) - continue - } - if (!(CLASSES as readonly string[]).includes(cls)) { - errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`) - continue - } - if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) { - errors.push(`structure: ${match} — filename must be yyyy-mm-dd-topic.md`) - continue - } - rfcs.push({ lifecycle, cls, base, rel: match }) - } -} - -// --- Check 2: README completeness ------------------------------------------- -// Parse the index into (lifecycle, class) -> set of linked rel paths, by -// tracking the current `## {Lifecycle}` and `### {Class}` headings and reading -// every `](path)` link target underneath. A link target is normalized to its -// path relative to docs/rfc. const readmePath = resolve(rfcRoot, 'README.md') const readme = readFileSync(readmePath, 'utf8') -const lifecycleByHeading = new Map(LIFECYCLES.map((l): [string, string] => [heading(l), l])) -const classByHeading = new Map(CLASSES.map((c): [string, string] => [heading(c), c])) - -/** README-listed RFC link targets, keyed `lifecycle/class` -> set of rel paths. */ -const listed = new Map<string, Set<string>>() -let curLifecycle: string | null = null -let curClass: string | null = null - -for (const line of readme.split('\n')) { - const h2 = /^##\s+(.+?)\s*$/.exec(line) - if (h2?.[1] !== undefined) { - curLifecycle = lifecycleByHeading.get(h2[1].trim()) ?? null - curClass = null - continue - } - const h3 = /^###\s+(.+?)\s*$/.exec(line) - if (h3?.[1] !== undefined) { - curClass = classByHeading.get(h3[1].trim()) ?? null - continue - } - if (!curLifecycle || !curClass) continue - // Collect every relative .md link target on this line. - for (const m of line.matchAll(/\]\(([^)]+\.md)[^)]*\)/g)) { - const target = m[1] - if (target === undefined) continue - // README links are relative to docs/rfc; normalize and key by location. - const rel = relative(rfcRoot, resolve(rfcRoot, target)) - const key = `${curLifecycle}/${curClass}` - const set = listed.get(key) ?? new Set<string>() - set.add(rel) - listed.set(key, set) - } -} - -// Every on-disk RFC must be listed under the heading matching its path. -const seenOnDisk = new Set<string>() -for (const rfc of rfcs) { - seenOnDisk.add(rfc.rel) - const key = `${rfc.lifecycle}/${rfc.cls}` - if (!listed.get(key)?.has(rfc.rel)) { - errors.push( - `index: ${rfc.rel} is not listed in README under "## ${heading(rfc.lifecycle)}" → "### ${heading(rfc.cls)}"`, - ) - } -} - -// Every README entry must point at a real RFC under that same heading (catches a -// misfiled or stale row). -for (const [key, targets] of listed) { - for (const rel of targets) { - if (!seenOnDisk.has(rel)) { - errors.push(`index: README lists "${rel}" under "${key}", but no such RFC exists`) +if (errors.length === 0) { + try { + if (spliceReadme(readme, rfcs) !== readme) { + errors.push('index: docs/rfc/README.md is stale — run `pnpm run gen-rfc-index` and commit the result') } + } catch (error) { + errors.push(`index: ${error instanceof Error ? error.message : String(error)}`) } } -// --- Report ----------------------------------------------------------------- if (errors.length === 0) { console.log(`verify-rfc-classification: ${rfcs.length} RFC(s) checked, structure and index consistent.`) process.exit(0)