* fix(#2691): repair five dangling references in the ADR corpus and contributor docs
Found by the 2026-07-24 ADR corpus audit; each mechanism re-reproduced live
against next @ 3eb1cede before filing.
1. docs/adr/1239-gsd-embeddable-orchestration-engine.md linked the
host-integration capability matrix as `reference/...` from inside
docs/adr/, which resolves to the nonexistent docs/adr/reference/.
Three occurrences (the #2584 amendment added two after the audit).
All now `../reference/...`.
2. src/plan-drift-guard.cts cited docs/adr/0022-source-grounding-drift-guard.md,
a path that has never existed (git log --all --diff-filter=A returns
nothing). Corrected to docs/adr/22-plan-drift-guard.md. Comments survive
tsc and ADR-457 builds at publish, so the bad citation shipped to users --
verified by grepping the compiled gsd-core/bin/lib/plan-drift-guard.cjs.
3. CONTRIBUTING.md and docs/contributor-standards.md illustrated the ADR
naming convention with issue #3485 -- a pre-rename number from the
predecessor repo (get-shit-done-redux) that does not resolve in
open-gsd/gsd-core. It is dangling, not invented: ADR filenames 3524 and
3660 show pre-rename numbering reached the 3000s. The worked example now
uses #2264, which resolves; the one genuinely historical mention is
annotated rather than rewritten.
4. docs/adr/857-capability-system.md's H1 carried a stale [Proposed] bracket
contradicting its "Accepted -- ratified 2026-07-17" Status field.
gen-adr-index.cjs:213 strips the bracket rather than comparing it, so the
contradiction was invisible to the gate; it is also the only in-repo
consumer, so removal leaves the rendered index byte-identical.
5. gen-adr-index.cjs's back-link comment still described ADR-857 as Proposed
and its claim over ADR-0011/ADR-58 as a supersession. Both were restated
at ratification (the claim became Subsumes; the reciprocals were added).
Regression coverage folds into tests/adr-index-gate.test.cjs rather than a
new bug-* file (lint-regression-test-names): four cases covering link
resolution, H1-bracket-vs-Status agreement, the plan-drift-guard citation,
and the naming worked example. All four fail at the pre-fix tree.
No behavior change. lint:ci exit 0; adr-index-gate 35/35; index regenerates
unchanged (67 ADRs).
* chore(#2691): add the required pr: field to the changeset fragment
docs-lint rejected the fragment with fail_malformed_fragment / missing_pr: the
frontmatter needs both `type:` and `pr:`. The fragment was hand-authored before
the PR existed, so it carried only `type:`.
Note for future work: neither lint:docs nor lint:changeset is part of the
lint:ci chain, so a green lint:ci does not cover these two CI checks. Both were
run directly before this push:
ok docs-lint: ok_no_triggering_fragments
ok changeset-lint: ok_fragment_present
* fix(#2691): drop the false branch clause and repair two more matrix links
Review round 2 on #2692, both blocking findings.
F1: the worked example at CONTRIBUTING.md:101 asserted
'on branch docs/2264-golden-parity-redesign'. That branch never existed --
the ADR file carries the epic number (#2264) while the branch and commit
carry the Phase-0 sub-issue number (#2265, PR #2270, branch
docs/2265-golden-parity-adr). #2264 is therefore the one ADR in the corpus
where filename and branch numbers deliberately disagree, making it the worst
available illustration of 'the issue number becomes your prefix and your
branch'. The branch clause is dropped; the surviving claim is verified
(#2264 is open-and-approved with approved-enhancement, and the file is
2264-golden-parity-redesign.md).
F3: docs/how-to/install-on-your-runtime.md:460 and :476 linked the
host-integration capability matrix as a bare filename from inside
docs/how-to/, which resolves to the nonexistent
docs/how-to/host-integration-capability-matrix.md. Same defect class as
repair #1 in this PR. Both now use ../reference/..., matching the form
already used by the sibling add-or-update-a-host-integration.md:167.
* fix(#2691): take the review minors -- anchors, bracket, ADR path, token order
Review round 2 on #2692, non-blocking findings.
F4: ADR-1239:198,206 read '[capability matrix §codex](...matrix.md)' -- the
link text promised a section, the target carried no fragment. '## codex'
exists at docs/reference/host-integration-capability-matrix.md:85, and the
repo already uses that form (#zcode, #pi in the how-to).
F5: docs/adr/857-capability-system.md:1 had its stale [Proposed] bracket
dropped rather than corrected, making 857 the only ratified ADR with no H1
bracket while four other Accepted ADRs carry one. Restored as [Accepted],
which satisfies the H1-vs-Status invariant and preserves consistency. No
recorded rule mandates the bracket, so this is style, not contract.
F7: docs/CONFIGURATION.md:808 cited adr/1244-runtime-capability-registry-
overlay.md; the actual file is adr/1244-capability-ecosystem.md. Same defect
class as repair #2, one directory over.
F8: test 33 resolved the Status token with STATUS_TOKENS.find(), which
matches by array order rather than by position in the line. Since
STATUS_TOKENS[0] === 'Accepted', a future '- **Status:** Superseded by ADR-X
(was Accepted ...)' paired with an H1 [Superseded] would have reported a
false mismatch. Now resolved by earliest index in the line. No ADR has that
shape today, so this is latent; ADR-857's two-token Status line resolves to
'Accepted' under both the old and new rule.
Changeset updated: five repairs -> seven, adding the two how-to matrix links
and the CONFIGURATION.md ADR path, plus the (#2691) issue backlink that 46
of the other 47 fragments carry.
Not addressed here: F2 (widening the guard to resolve anchors and walk docs/
recursively) is filed separately as #2704, approved-enhancement.
---------
Co-authored-by: CI Rebase Check <ci@gsd-redux>
This commit is contained in:
5
.changeset/adr-corpus-reference-repairs.md
Normal file
5
.changeset/adr-corpus-reference-repairs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2692
|
||||
---
|
||||
**Seven dangling references in the ADR corpus and contributor docs now resolve** — (1) `docs/adr/1239-gsd-embeddable-orchestration-engine.md` linked the host-integration capability matrix as `reference/…` from inside `docs/adr/`, resolving to the nonexistent `docs/adr/reference/`; all three occurrences now use `../reference/…`, and the two whose link text promises `§codex` now carry the matching `#codex` fragment. (2) `src/plan-drift-guard.cts` cited `docs/adr/0022-source-grounding-drift-guard.md`, a path that has never existed — corrected to the real `docs/adr/22-plan-drift-guard.md`; because the file is compiled into the shipped payload, the bad citation was shipping to users. (3) `CONTRIBUTING.md` and `docs/contributor-standards.md` illustrated the ADR naming convention with issue `#3485`, a pre-rename number from `get-shit-done-redux` that does not resolve in `open-gsd/gsd-core` — the worked example now uses `#2264`, which does, and the one genuinely historical `#3485` reference is annotated rather than rewritten. (4) `docs/adr/857-capability-system.md`'s H1 still carried a `[Proposed]` status bracket contradicting its `Accepted — ratified 2026-07-17` Status field; the ADR index generator strips the bracket for display, so the contradiction was invisible to the gate. (5) `scripts/gen-adr-index.cjs`'s back-link comment still described ADR-857 as `Proposed` and its claim over ADR-0011/ADR-58 as a supersession — both restated at the 2026-07-17 ratification, when the claim became `Subsumes` and the reciprocal back-links were added. (6) `docs/how-to/install-on-your-runtime.md` linked that same capability matrix as a bare `host-integration-capability-matrix.md` from inside `docs/how-to/` in its ZCode and pi sections — the identical defect as (1), so both now use `../reference/…`. (7) `docs/CONFIGURATION.md` cited ADR-1244 as `adr/1244-runtime-capability-registry-overlay.md`; the file is `adr/1244-capability-ecosystem.md`. (#2691)
|
||||
@@ -98,7 +98,7 @@ An ADR (Architecture Decision Record) documents a significant architectural deci
|
||||
|
||||
**Do not compute a "next number" locally.** Any PR that uses the legacy `NNNN-*` sequential pattern for a *new* ADR or PRD will be asked to rename the file to the `<issue#>-<slug>.md` format before merge.
|
||||
|
||||
**Example:** Issue #3485 was opened, approved, and its number became the prefix: `docs/adr/3485-adr-prd-naming-convention.md` on branch `docs/3485-adr-prd-naming-convention`.
|
||||
**Example:** Issue #2264 was opened, approved, and its number became the prefix: `docs/adr/2264-golden-parity-redesign.md`.
|
||||
|
||||
**Rejection reasons:** Issue not approved before file was created, filename uses local-compute sequential number instead of issue#, multiple decisions bundled in one PR, file placed in wrong directory (`docs/adr/` vs `docs/prd/`).
|
||||
|
||||
|
||||
@@ -805,7 +805,7 @@ If a skipped or load-failed overlay capability (for example, one whose `engines.
|
||||
|
||||
Config keys declared in an overlay capability's `.config` slice federate into the `loadConfig` return value via the same Federated Config channel as first-party capability keys. They appear as valid keys in `config-schema.cjs` (`isValidConfigKey`) and in the runtime config schema, so overlay capabilities can declare project-local config toggles without editing the central config schema.
|
||||
|
||||
> **See also:** [`docs/reference/capability-manifest.md`](reference/capability-manifest.md) for the full `capability.json` schema, [`docs/how-to/import-a-capability-from-a-url.md`](how-to/import-a-capability-from-a-url.md) for installation steps, and [ADR-1244](adr/1244-runtime-capability-registry-overlay.md) for the design record.
|
||||
> **See also:** [`docs/reference/capability-manifest.md`](reference/capability-manifest.md) for the full `capability.json` schema, [`docs/how-to/import-a-capability-from-a-url.md`](how-to/import-a-capability-from-a-url.md) for installation steps, and [ADR-1244](adr/1244-capability-ecosystem.md) for the design record.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ Phase A is **implemented** and Phases B–E have landed, so this ADR is **Accept
|
||||
- **Overlap resolutions (explicit):** `commandStyle` (GSD emission style, retained) ⊥ `commandSurface` (host surface type); `hookEvents` dialect ⊥ `hookBus` ownership (a host with `hooksSurface:none` may still be `hookBus:host` — e.g. opencode); `runtimeCompat` (feature→host) stays an independent override, orthogonal to these runtime→engine axes.
|
||||
- **`extensionEvents` vocabulary (amendment, #1943).** The OpenCode extension-system event subset is a SEPARATE descriptor field + closed vocabulary, **not** a `hookEvents` value. `hookEvents` is the *managed-hook* dialect only (`claude`/`gemini`) — the event names GSD writes into a declarative host's settings.json. `extensionEvents` is the *plugin/extension-system* event surface an imperative host exposes: `{ opencode, pi, none }` (OpenCode ~25 plugin events; pi ~30 fine-grained events; `none` = the host exposes no extension surface and the engine owns the bus, e.g. VS Code). The former `opencode-subset` `hookEvents` value was this concept misfiled; it is now `extensionEvents: opencode`. Keeping them separate preserves the `hooksSurface:"none" ⇔ no-hookEvents` invariant (OpenCode declares `extensionEvents`, not `hookEvents`). Resolved by `extensionEventSurfaceFor` in `src/host-integration.cts`, validated by `VALID_EXTENSION_EVENTS` in `capability-validator.cjs`.
|
||||
|
||||
**Every per-host axis value is documentation-sourced, with citations.** Each of the 8 axes for all 16 installed CLIs was determined from that CLI's authoritative documentation (Context7 + the official dev docs/source), never inferred. The full per-CLI, per-axis matrix — value, source, and an evidence quote — is recorded in [`docs/reference/host-integration-capability-matrix.md`](reference/host-integration-capability-matrix.md), the deployment source-of-truth that Phases B–E build on. Where a CLI's docs genuinely do not state an axis, the descriptor carries the explicit `undocumented` sentinel (which `negotiateHostCapabilities` fail-closes on) rather than a guessed value — 22 such markers exist today, each with its search trail in the matrix. Two findings corrected this ADR's original appendix matrix: (1) current OpenAI **Codex** docs document slash-commands, so its `commandSurface` is `slash-file`, not `prose-only`; (2) several hosts run non-Node runtimes (opencode & kilo on **bun**; hermes & kimi on **python**; antigravity on **go**), so the `runtime` axis vocabulary was widened to `node|bun|sandboxed-web|python|go|rust|electron|other`. The documented `embeddingMode` split (9 imperative / 7 declarative, above) likewise reflects each CLI's real plugin/extension API, not a profile assumption.
|
||||
**Every per-host axis value is documentation-sourced, with citations.** Each of the 8 axes for all 16 installed CLIs was determined from that CLI's authoritative documentation (Context7 + the official dev docs/source), never inferred. The full per-CLI, per-axis matrix — value, source, and an evidence quote — is recorded in [`docs/reference/host-integration-capability-matrix.md`](../reference/host-integration-capability-matrix.md), the deployment source-of-truth that Phases B–E build on. Where a CLI's docs genuinely do not state an axis, the descriptor carries the explicit `undocumented` sentinel (which `negotiateHostCapabilities` fail-closes on) rather than a guessed value — 22 such markers exist today, each with its search trail in the matrix. Two findings corrected this ADR's original appendix matrix: (1) current OpenAI **Codex** docs document slash-commands, so its `commandSurface` is `slash-file`, not `prose-only`; (2) several hosts run non-Node runtimes (opencode & kilo on **bun**; hermes & kimi on **python**; antigravity on **go**), so the `runtime` axis vocabulary was widened to `node|bun|sandboxed-web|python|go|rust|electron|other`. The documented `embeddingMode` split (9 imperative / 7 declarative, above) likewise reflects each CLI's real plugin/extension API, not a profile assumption.
|
||||
|
||||
No consumer wires the negotiated result yet — Phase A is interface-definition only; the engine↔host boundary (Phase B) and the adapters (Phase C) are where it is consumed.
|
||||
|
||||
@@ -195,7 +195,7 @@ Because OpenCode consumes MCP, the **companion MCP server** (the MemPalace patte
|
||||
|
||||
## Codex binding (worked host-plugin)
|
||||
|
||||
> **Amendment — Codex worked binding + `dispatch.isolation` capability (#2584, 2026-07-24).** Two things: (1) it introduces a **general, per-host-negotiated capability** — the `dispatch.isolation` sub-field, which declares how each host isolates concurrent same-wave executors (six hosts have confirmed support; see the support table below), enabling parallel `execute-phase` waves on hosts beyond Claude; and (2) it consolidates Codex's Phase-D-shipped six-point Host-Integration binding (#2088) into this ADR — the sibling of the OpenCode binding above — because Codex is the first *orchestrator-managed* consumer of the new capability. Per-axis values are the [capability matrix §codex](reference/host-integration-capability-matrix.md). Sequencing: the *code* builds on the worktree merge/cleanup gauntlet (open bug #2556) and lands after it; this design + the descriptor/matrix change do not.
|
||||
> **Amendment — Codex worked binding + `dispatch.isolation` capability (#2584, 2026-07-24).** Two things: (1) it introduces a **general, per-host-negotiated capability** — the `dispatch.isolation` sub-field, which declares how each host isolates concurrent same-wave executors (six hosts have confirmed support; see the support table below), enabling parallel `execute-phase` waves on hosts beyond Claude; and (2) it consolidates Codex's Phase-D-shipped six-point Host-Integration binding (#2088) into this ADR — the sibling of the OpenCode binding above — because Codex is the first *orchestrator-managed* consumer of the new capability. Per-axis values are the [capability matrix §codex](../reference/host-integration-capability-matrix.md#codex). Sequencing: the *code* builds on the worktree merge/cleanup gauntlet (open bug #2556) and lands after it; this design + the descriptor/matrix change do not.
|
||||
|
||||
### What Codex integration actually is (the binding substrate)
|
||||
|
||||
@@ -203,7 +203,7 @@ Codex is a **declarative-CLI** host (ADR-1239 profile `declarative-cli`). It exp
|
||||
|
||||
### Six interface points → Codex primitives
|
||||
|
||||
Codex's full negotiated binding — the per-axis value, its documentation citation, and the evidence quote for all six interface points — is the **[capability matrix §codex](reference/host-integration-capability-matrix.md)**, the deployment source of truth. In summary: `embeddingMode: declarative` · `commandSurface: slash-file` · `modelMode: passive` · `hookBus: host` (10-event `hooks.json`) · `stateIO: filesystem` · `transport: mcp` · `runtime: node` · `effortSurface: argv`. Integration is **Phase-D-dogfood complete** (#2088): declarative embedding adapter, install/uninstall byte-parity-gated (`golden-install-parity/codex.json`), the `runtime === 'codex'` projection folded into `hostBehaviors`.
|
||||
Codex's full negotiated binding — the per-axis value, its documentation citation, and the evidence quote for all six interface points — is the **[capability matrix §codex](../reference/host-integration-capability-matrix.md#codex)**, the deployment source of truth. In summary: `embeddingMode: declarative` · `commandSurface: slash-file` · `modelMode: passive` · `hookBus: host` (10-event `hooks.json`) · `stateIO: filesystem` · `transport: mcp` · `runtime: node` · `effortSurface: argv`. Integration is **Phase-D-dogfood complete** (#2088): declarative embedding adapter, install/uninstall byte-parity-gated (`golden-install-parity/codex.json`), the `runtime === 'codex'` projection folded into `hostBehaviors`.
|
||||
|
||||
The one interface point this amendment changes is **Dispatch** — specifically executor isolation for wave parallelism, below.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR-857: Capability system — five-step loop as core, features as plug-ins behind Loop Extension Points [Proposed]
|
||||
# ADR-857: Capability system — five-step loop as core, features as plug-ins behind Loop Extension Points [Accepted]
|
||||
|
||||
- **Status:** Accepted — ratified 2026-07-17 (originally Proposed 2026-06-08); see "Ratification" below
|
||||
- **Date:** 2026-06-08
|
||||
|
||||
@@ -96,11 +96,11 @@ docs/adr/<issue#>-<kebab-slug>.md (new ADRs)
|
||||
docs/prd/<issue#>-<kebab-slug>.md (new PRDs)
|
||||
```
|
||||
|
||||
Example: `docs/adr/3485-adr-prd-naming-convention.md`.
|
||||
Example: `docs/adr/2264-golden-parity-redesign.md`.
|
||||
|
||||
**Why:** GitHub issue numbers are server-assigned and atomic — the reservation mechanism already exists because the issue-first rule requires it. Promoting the issue# to the artifact ID eliminates the entire collision class that the `NNNN-*` local-compute scheme created (see the `0010-*` × 2 and `0011-*` × 3 duplicates on disk).
|
||||
|
||||
**Migration policy:** Legacy ADRs `0001-*` through `0011-*` keep their numbers as immutable historical record. The new convention applies to all ADRs and PRDs created on or after the merge of the implementing PR (#3485). Do not renumber legacy files.
|
||||
**Migration policy:** Legacy ADRs `0001-*` through `0011-*` keep their numbers as immutable historical record. The new convention applies to all ADRs and PRDs created on or after the merge of the implementing PR (#3485 — a pre-rename number from `get-shit-done-redux`; it does not resolve in `open-gsd/gsd-core`, whose issue numbering restarted at the rename). Do not renumber legacy files.
|
||||
|
||||
For the end-to-end workflow — opening the issue, waiting for approval, creating the file, and submitting the PR — see **[CONTRIBUTING.md — "Proposing an ADR or PRD"](../CONTRIBUTING.md#proposing-an-adr-or-prd)**.
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ npx @opengsd/gsd-core@latest --zcode --global
|
||||
|
||||
ZCode's skill format is identical to Claude Code's, so no runtime-specific converter is required — GSD lands as a pure declarative descriptor with no hardcoded installer branches. ZCode also natively imports skills and MCP config from `~/.claude`; if you install GSD for **both** Claude and ZCode, you may see duplicate GSD skills inside ZCode, which is expected. To connect ZCode's MCP servers to GSD's companion server, see [how to connect the GSD MCP server](connect-gsd-mcp-server.md).
|
||||
|
||||
GSD's hook-automation and native-MCP-registration integrations are not yet wired for ZCode — both are blocked on ZCode not yet publishing the on-disk config format for its plugin `Hook` component or the settings filename/schema for its MCP store. See the [`## zcode`](host-integration-capability-matrix.md#zcode) section of the host-integration capability matrix for the cited source URLs.
|
||||
GSD's hook-automation and native-MCP-registration integrations are not yet wired for ZCode — both are blocked on ZCode not yet publishing the on-disk config format for its plugin `Hook` component or the settings filename/schema for its MCP store. See the [`## zcode`](../reference/host-integration-capability-matrix.md#zcode) section of the host-integration capability matrix for the cited source URLs.
|
||||
|
||||
---
|
||||
|
||||
@@ -473,7 +473,7 @@ npx @opengsd/gsd-core@latest --pi --global
|
||||
|
||||
The `.js` suffix is load-bearing: pi auto-discovers extensions by scanning that directory and keeping only names ending in `.ts` or `.js`, and it skips anything else **silently** — no error, no log line. GSD shipped the file as `gsd.cjs` through 1.7.0, which pi therefore never loaded, so `/gsd` never appeared ([#2470](https://github.com/open-gsd/gsd-core/issues/2470)). Upgrading removes the stale `gsd.cjs`; if you had added a manual `extensions` entry in `~/.pi/agent/settings.json` as a workaround, you can drop it.
|
||||
|
||||
The extension registers a `/gsd` command and a `gsd_invoke` tool that dispatch GSD commands via a bounded subprocess call to `gsd-core/bin/gsd-tools.cjs` (no fully-populated in-process command-routing hub exists — see the matrix's Stage 2 note). This is a **plugin-only install**: pi has no shared-settings hook surface (`hooksSurface: none`) and, unlike Claude/OpenCode/Kilo, no host-read markdown surface at all — pi's `/gsd` command is registered programmatically by the extension, not discovered from files, so GSD installs the extension plus its universal `gsd-core/` engine payload and the shared `hooks/`/`hooks/lib/` bundle (spawned by the extension itself, not by any config-file hook bus), and does **not** write any `commands/`, `agents/`, or `skills/` directory for pi. The extension bridges GSD's `session_start`/`before_agent_start`/`session_before_compact`/`tool_call` lifecycle events to those staged `hooks/` scripts as bounded, fail-open subprocesses, and steers pi's active model (`modelMode: active`) to a tier-resolved bare anthropic id via `pi.on('before_provider_request', ...)`. See the [`## pi`](host-integration-capability-matrix.md#pi) section of the host-integration capability matrix for the negotiated axes and citations.
|
||||
The extension registers a `/gsd` command and a `gsd_invoke` tool that dispatch GSD commands via a bounded subprocess call to `gsd-core/bin/gsd-tools.cjs` (no fully-populated in-process command-routing hub exists — see the matrix's Stage 2 note). This is a **plugin-only install**: pi has no shared-settings hook surface (`hooksSurface: none`) and, unlike Claude/OpenCode/Kilo, no host-read markdown surface at all — pi's `/gsd` command is registered programmatically by the extension, not discovered from files, so GSD installs the extension plus its universal `gsd-core/` engine payload and the shared `hooks/`/`hooks/lib/` bundle (spawned by the extension itself, not by any config-file hook bus), and does **not** write any `commands/`, `agents/`, or `skills/` directory for pi. The extension bridges GSD's `session_start`/`before_agent_start`/`session_before_compact`/`tool_call` lifecycle events to those staged `hooks/` scripts as bounded, fail-open subprocesses, and steers pi's active model (`modelMode: active`) to a tier-resolved bare anthropic id via `pi.on('before_provider_request', ...)`. See the [`## pi`](../reference/host-integration-capability-matrix.md#pi) section of the host-integration capability matrix for the negotiated axes and citations.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -347,10 +347,12 @@ function validate({ adrs, byFile, byId, nonConforming }) {
|
||||
//
|
||||
// Only a RATIFIED (Accepted) claimant is owed the back-link. A Proposed ADR's
|
||||
// supersession claim is prospective — it has not taken effect, so stamping its
|
||||
// target as superseded would assert something untrue (ADR-857 is Proposed and
|
||||
// claims to generalize ADR-0011/ADR-58, both of which are Accepted and live).
|
||||
// When such an ADR is ratified to Accepted, this check starts demanding the
|
||||
// back-links at exactly the right moment.
|
||||
// target as superseded would assert something untrue. When such an ADR is
|
||||
// ratified to Accepted, this check starts demanding the back-links at exactly
|
||||
// the right moment — as ADR-857 shows: it was Proposed when this guard was
|
||||
// written, was ratified to Accepted on 2026-07-17 (its claim over ADR-0011 /
|
||||
// ADR-58 restated as "Subsumes", since both remain Accepted and live), and
|
||||
// both targets now carry the reciprocal "Subsumed by" back-link this demands.
|
||||
const OPPOSITE = { out: 'in', in: 'out' };
|
||||
for (const a of adrs) {
|
||||
for (const kind of Object.keys(RELATION_SPEC)) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* ADR-22 Drift-Guard Decision Module
|
||||
*
|
||||
* Implements the authority ladder and severity classification table from
|
||||
* ADR-22 (docs/adr/0022-source-grounding-drift-guard.md).
|
||||
* ADR-22 (docs/adr/22-plan-drift-guard.md).
|
||||
*
|
||||
* Design constraints:
|
||||
* - Pure module: no I/O, no require() calls, no side effects.
|
||||
|
||||
@@ -436,3 +436,97 @@ test('the real repo corpus is clean and its index is current', () => {
|
||||
const res = run(REPO_ROOT, ['--check']);
|
||||
assert.equal(res.status, 0, `docs/adr/ must satisfy its own gate:\n${res.stderr}`);
|
||||
});
|
||||
|
||||
// --- regressions -----------------------------------------------------------
|
||||
//
|
||||
// The --check gate above passes on a corpus that still carries dangling
|
||||
// references: it validates naming, relation symmetry, and index freshness, but
|
||||
// it never resolves a link target and it STRIPS the H1 status bracket
|
||||
// (gen-adr-index.cjs) rather than comparing it. Both defect classes below were
|
||||
// green under `--check` while broken. These assert on the real corpus, so
|
||||
// reverting the repair re-reds them.
|
||||
|
||||
const ADR_DIR = path.join(REPO_ROOT, 'docs', 'adr');
|
||||
const STATUS_TOKENS = ['Accepted', 'Proposed', 'Superseded', 'Legacy', 'Retired'];
|
||||
|
||||
function adrMarkdownFiles() {
|
||||
return fs.readdirSync(ADR_DIR).filter((f) => f.endsWith('.md'));
|
||||
}
|
||||
|
||||
test('every relative markdown link in docs/adr/ resolves to a file that exists', () => {
|
||||
const dangling = [];
|
||||
for (const file of adrMarkdownFiles()) {
|
||||
const body = fs.readFileSync(path.join(ADR_DIR, file), 'utf8');
|
||||
for (const match of body.matchAll(/\]\(([^)#:\s]+\.md)(?:#[^)]*)?\)/g)) {
|
||||
const target = match[1];
|
||||
if (!fs.existsSync(path.resolve(ADR_DIR, target))) {
|
||||
dangling.push(`${file} -> ${target}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
dangling,
|
||||
[],
|
||||
`dangling relative links in docs/adr/ (a link written as reference/x.md from inside docs/adr/ resolves to the nonexistent docs/adr/reference/):\n${dangling.join('\n')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('no ADR H1 status bracket contradicts its Status field', () => {
|
||||
// The index generator strips a trailing "[Proposed]"-style bracket for
|
||||
// display instead of comparing it, so a stale bracket is invisible to the
|
||||
// gate while still being the first thing a reader sees.
|
||||
const mismatches = [];
|
||||
for (const file of adrMarkdownFiles()) {
|
||||
if (file === 'README.md') continue;
|
||||
const lines = fs.readFileSync(path.join(ADR_DIR, file), 'utf8').split(/\r?\n/);
|
||||
const heading = lines.find((l) => /^#\s/.test(l)) || '';
|
||||
const bracket = heading.match(/\[(Proposed|Accepted|Superseded|Legacy|Retired)\]\s*$/i);
|
||||
if (!bracket) continue;
|
||||
const statusLine = lines.find((l) => /^\s*[-*]?\s*\*\*Status/.test(l)) || '';
|
||||
// Resolve by earliest position in the line, not by STATUS_TOKENS order: a
|
||||
// Status field like "Superseded by ADR-X (was Accepted ...)" mentions two
|
||||
// tokens, and array order would pick 'Accepted' and report a false mismatch
|
||||
// against a correct [Superseded] bracket.
|
||||
let token;
|
||||
let tokenAt = Infinity;
|
||||
for (const s of STATUS_TOKENS) {
|
||||
const at = statusLine.search(new RegExp(`\\b${s}\\b`, 'i'));
|
||||
if (at !== -1 && at < tokenAt) {
|
||||
tokenAt = at;
|
||||
token = s;
|
||||
}
|
||||
}
|
||||
if (token && token.toLowerCase() !== bracket[1].toLowerCase()) {
|
||||
mismatches.push(`${file}: H1 says [${bracket[1]}], Status field says ${token}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(mismatches, [], `H1 bracket contradicts Status:\n${mismatches.join('\n')}`);
|
||||
});
|
||||
|
||||
test('the ADR path cited by src/plan-drift-guard.cts exists', () => {
|
||||
// This module is compiled into the published payload, so a wrong citation
|
||||
// here ships to users.
|
||||
const src = fs.readFileSync(path.join(REPO_ROOT, 'src', 'plan-drift-guard.cts'), 'utf8');
|
||||
const cited = [...src.matchAll(/docs\/adr\/([A-Za-z0-9._-]+\.md)/g)].map((m) => m[1]);
|
||||
assert.notEqual(cited.length, 0, 'expected plan-drift-guard.cts to cite its governing ADR');
|
||||
for (const name of cited) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(ADR_DIR, name)),
|
||||
`src/plan-drift-guard.cts cites docs/adr/${name}, which does not exist`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the ADR naming worked example names an ADR file that exists', () => {
|
||||
for (const rel of ['CONTRIBUTING.md', path.join('docs', 'contributor-standards.md')]) {
|
||||
const body = fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
|
||||
const examples = [...body.matchAll(/docs\/adr\/(\d+-[a-z0-9-]+\.md)/g)].map((m) => m[1]);
|
||||
assert.notEqual(examples.length, 0, `expected ${rel} to show a worked ADR-naming example`);
|
||||
for (const name of examples) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(ADR_DIR, name)),
|
||||
`${rel} illustrates the naming convention with docs/adr/${name}, which does not exist`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user