Skip to content

feat: generate the MANIFEST inventory and gate it against the corpus - #177

Merged
mbeacom merged 4 commits into
mainfrom
mbeacom-manifest-inventory-determinism
Aug 27, 2026
Merged

feat: generate the MANIFEST inventory and gate it against the corpus#177
mbeacom merged 4 commits into
mainfrom
mbeacom-manifest-inventory-determinism

Conversation

@mbeacom

@mbeacom mbeacom commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Closes #131. Closes #111.

MANIFEST.md's record inventory is a pure function of docs/adr/, and it was hand-maintained: in #129 it was found publishing six records' worth of contradictory governance state. It was corrected by hand, which fixed the symptom. This fixes the cause.

What changed

Generated, then asserted unchanged. scripts/emit-manifest.ts renders the record table and the status counts from adr graph --format json between stable markers, and clean-clone-builds runs bun run emit:manifest && git diff --exit-code MANIFEST.md next to the existing schema-emit and Action-bundle guards. A guard that merely reported the divergence would still leave a human to hand-edit the table, which is exactly how it drifted.

No public CLI surface was added. adr graph --format json already emits id, title and status — the whole of the derived state — so this is a renderer plus a gate, following the established read-only-CLI/redirect/gate pattern (site:queue) and the reasoning scripts/check-doc-cli-versions.ts states for living in scripts/. The CLI's write surface stays new and migrate, so the Spec Kit adapter's tested "hooks reach only non-writing commands" invariant is untouched (#132).

Real titles, not a label map. Per #131's preference — self-maintaining, and a label map would itself need a completeness check.

Judgment prose stays hand-written. Only the marked block is generated. The repository tree diagram, the planning sources, and anything editorial are outside the markers, and the section says so.

adr queue no longer goes silent on a deadline without a tier (#111). item.tier-absent now fires whenever the routing tier cannot be determined on a record that has entered review — review present, or top-level reviewBy present.

The failure mode the gate had to avoid

regenerate && git diff --exit-code has exactly one dangerous failure: a generator that writes nothing leaves the diff clean, so the gate reports green while checking nothing — the ADR-0016 shape this repository keeps finding. Every way of writing nothing is therefore an error rather than a silent no-op: absent markers, a single surviving marker, duplicated markers, inverted markers, and an empty node list all throw.

The generator also re-sorts nodes with compareCodeUnits instead of trusting the order buildAdrGraph supplied. That order is localeCompare's, so identical records would render in a different row order under a different ICU locale and the no-diff gate would fail with no record having changed.

Observed failing before being relied on (ADR-0016)

Permanent negative cases live in scripts/emit-manifest.test.ts and packages/core/test/queue/kernel.test.ts. The two end-to-end observations against the committed tree:

1. A stale committed inventory — exit 1, with the drift named:

$ bun run emit:manifest && git diff --exit-code MANIFEST.md
emit-manifest: wrote MANIFEST.md inventory for 34 records
-There are 33 records, ids `0001`-`0033`, alongside
+There are 34 records, ids `0001`-`0034`, alongside
+| `0034` | accepted | Extend the portable agent plugin with decision backfill |
exit=1

2. The markers deleted — the no-op that would otherwise pass green:

$ bun run emit:manifest && git diff --exit-code MANIFEST.md
emit-manifest: MANIFEST.md must contain exactly one <!-- BEGIN GENERATED: adr-inventory -->
and one <!-- END GENERATED: adr-inventory --> (found 0 and 0). Without them nothing is
generated and the no-diff gate would pass while checking nothing.
exit=1

3. The #111 case, against the pre-fix kernel:

(fail) buildQueueReport — item findings > item.tier-absent when reviewBy is present
       with no review block (the ADR-0022 shape)
 28 pass, 1 fail

#111 in detail: why this is a contract reading, not a preference

The spec's carve-out has two conditions and a justification that names them both — specs/007-arb-queue/spec.md:

review block and top-level reviewBy both absent on a proposed record: treated as not-queued. This is a valid, expected state for records that have been proposed but not yet entered into the review workflow.

The implementation tested only the first. contracts/kernel.md states the finding's condition with no carve-out at all (review?.tier absent), and research.md the same. So the silence rule was applied to a record it was never written to cover: ADR-0022 sat in the queue as blastRadius: cross-team with reviewBy: 2027-02-08, tier=None, findings=[], and was ratified without a routing tier. A record carrying an explicit review deadline has entered the review workflow by any reading, and the spec's very next sentence gives that exact combination SLA treatment.

The three sources now say the same thing as the code. Severity stays info, so no exit code changes; a proposed record with neither field remains not-queued and silent, and there is a test asserting that carve-out survives.

Deliberately deferred

#115's affects/** remainder is not in this PR. packages/core/src/affects/** is pinned byte-identical by feature 010's FR-004 guard, which names any change there a violation and routes legitimate changes to separately-authorized later work. Unblocking it needs that authorization plus regenerating the freeze-hash pins in packages/catalog-envelope/ — a different change with a different review.

graph/build.ts's localeCompare sorts are not migrated either, and this is the interesting one. ADR-0033 clause 8 pins them explicitly:

The graph JSON shape remains exactly { nodes, edges }, with existing node and edge fields, historical locale ordering, and missing-target omission unchanged.

Migrating them is a change to an accepted decision, not a defect fix, so it is reported rather than made here — this repository governs itself, and quietly reordering a pinned output would be the exact move it exists to prevent. The MANIFEST.md gate does not depend on that order (the generator normalizes it), so nothing here is blocked on the answer. Decision needed: should clause 8's "historical locale ordering" be amended to code-unit ordering, given that the id grammar admits mixed-case ULIDs where the two comparators genuinely disagree? It is a one-line change plus a superseding or amending record; it is not mine to assume.

What was done on that axis: packages/core/test/ordering-contract.test.ts now scans src/queue as well, on QueueReport v1's own byte-for-byte contract (SC-001), and states in its header exactly why graph/ is excluded and what a clean run therefore does and does not mean — the same honesty the file already carries about affects/.

One thing the existing gates caught

The first CI run failed on git diff --exit-code packages/ci/dist: the committed queue Action bundle embeds the kernel, so the #111 change had to be rebuilt into packages/ci/dist/queue-action.js. Rebuilt with the repository-pinned Bun 1.3.14 — a newer bundler rewrites unrelated runtime helpers throughout both bundles and would fail the same gate. Caught by a guard rather than by review, which is the point of having it.

Validation

  • bun test — 2678 pass, 1 skip, 0 fail
  • bun run typecheck, bun run lint — clean
  • bun run adr lint — 34 records, 0 errors, 0 warnings
  • bun run check:changelog, bun run check:deps — ok
  • bun run emit:manifest is idempotent: a second run reports already matches the corpus
  • CI on this branch: all checks green, including clean-clone-builds with the new gate

MANIFEST.md's record inventory is a pure function of docs/adr/, and it was
hand-maintained: it drifted six records before anyone noticed (#131). A guard
that merely reported the divergence would still leave a human to hand-edit the
table, which is how it drifted, so this generates the block and asserts the tree
is unchanged afterwards — the same shape as the schema emit and the committed
Action bundle.

`scripts/emit-manifest.ts` renders the record table and status counts from
`adr graph --format json` between stable markers; `bun run emit:manifest &&
git diff --exit-code MANIFEST.md` joins the existing guards in
`clean-clone-builds`. No public CLI surface is added: the CLI stays read-only
and hook-safe, and the writing is a repo-local script (#132). Prose outside the
markers — including judgment about what is open — stays hand-written.

Every way of generating nothing is an error rather than a silent no-op, since a
generator that writes nothing leaves the diff clean and the gate green while
checking nothing (ADR-0016): absent, duplicated, or inverted markers and an
empty node list all throw. The generator re-sorts nodes with `compareCodeUnits`
rather than trusting `buildAdrGraph`'s `localeCompare` order, which ADR-0033
clause 8 pins, so the no-diff gate cannot fail because two contributors ran it
under different ICU locales.

Also fires `item.tier-absent` when a proposed record has entered review but its
routing tier cannot be determined (#111). The spec's carve-out is
two-conditioned — `review` block *and* top-level `reviewBy` both absent, on the
reasoning that the record has "not yet entered into the review workflow" — but
only the first condition was implemented. ADR-0022 therefore sat in the queue
as cross-team with `reviewBy: 2027-02-08`, `tier=None`, and no finding, and was
ratified without a routing tier. spec.md, research.md, and contracts/kernel.md
now state the same rule as the code. Severity stays `info`, so no exit code
changes, and a proposed record with neither field stays silent.

Observed failing before being relied on, per ADR-0016: the queue case against
the pre-fix kernel, and the marker, empty-corpus, and stale-inventory cases as
permanent negative tests.

Closes #131
Closes #111

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Copilot AI balanced review requested due to automatic review settings August 26, 2026 21:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The committed Action bundle remains stale, and valid multiline or large numeric values can produce incorrect manifest output.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Generates MANIFEST.md inventory from the ADR corpus and fixes missing queue-tier findings.

Changes:

  • Adds and gates deterministic manifest generation.
  • Handles review deadlines without routing tiers.
  • Updates contracts, documentation, and tests.
File summaries
File Description
.github/workflows/ci.yml Gates generated inventory drift.
CHANGELOG.md Documents both changes.
MANIFEST.md Adds generated ADR inventory.
docs/RELEASING.md Updates release guidance.
package.json Adds the manifest script.
packages/core/src/queue/kernel.ts Expands tier-absence detection.
packages/core/test/ordering-contract.test.ts Extends deterministic-order scanning.
packages/core/test/queue/kernel.test.ts Covers deadline-only review entry.
scripts/emit-manifest.test.ts Tests generation and failure cases.
scripts/emit-manifest.ts Implements inventory generation.
specs/007-arb-queue/contracts/kernel.md Updates the kernel contract.
specs/007-arb-queue/research.md Clarifies finding conditions.
specs/007-arb-queue/spec.md Clarifies the review carve-out.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +125 to +126
const enteredReview = review !== undefined || frontmatter.reviewBy != null;
if (enteredReview && review?.tier == null) {
Comment thread scripts/emit-manifest.ts
Comment on lines +71 to +74
const numbers = ids.map(Number);
for (let index = 1; index < numbers.length; index += 1) {
if (numbers[index]! !== numbers[index - 1]! + 1) return null;
}
Comment thread scripts/emit-manifest.ts Outdated
Comment on lines +93 to +96
/** Markdown table cells cannot carry a raw `|`, and titles are free text. */
function tableCell(value: string): string {
return value.replaceAll('\\', '\\\\').replaceAll('|', '\\|');
}
`packages/ci/dist/queue-action.js` embeds the queue kernel, so the #111 change
to `computeItemFindings` has to be rebuilt into it. Caught by the existing
`git diff --exit-code packages/ci/dist` gate rather than by review, which is
the gate working.

Built with the repository-pinned Bun 1.3.14; a newer bundler rewrites unrelated
runtime helpers throughout both bundles and would fail the same gate in CI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

Decisions governing this change

  • 0007 — Isolate integrations as optional adapters and build only against public surfaces
    • via path: .github/workflows/**
  • 0010 — Use Bun as the package manager and test runner while publishing Node-targeted artifacts
    • via path: .github/workflows/**
    • via path: package.json
  • 0014 — Stage phase-landing evidence across a three-rung validation ladder
    • via path: specs/**/spec.md
  • 0016 — Require every check to be observed failing before it counts as coverage
    • via path: packages/*/test/**
  • 0017 — Keep dependency audit scope explicit and release-scoped
    • via path: .github/workflows/ci.yml
  • 0025 — Ship badges as recipes over existing output, not a new CLI surface
    • via path: .github/workflows/ci.yml
    • via path: package.json
  • 0029 — Scope Backstage publication as a downstream consumer, tiered on the entity-ownership mapping
    • via path: packages/core/src/queue/**
  • 0030 — Keep extension surfaces that carry a dependency tree outside this repository
    • via path: package.json
    • via path: packages/ci/**
  • 0031 — Publish a narrow consumer SDK as the contract, and document the CLI JSON as its sibling
    • via path: docs/RELEASING.md
  • 0032 — Publish one lockstep OCI image after the coordinated release succeeds
    • via path: .github/workflows/ci.yml
    • via path: docs/RELEASING.md
  • 0035 — Execute the gates that certify a pull request from the default branch
    • via path: .github/workflows/**
    • via path: packages/ci/**
    • via path: scripts/**

mbeacom and others added 2 commits August 26, 2026 17:54
Two defects found in review of #177, both of which let a *valid* ADR record
break or corrupt the generated block it is rendered into.

`replaceGeneratedBlock` counted marker substrings anywhere in the file. `title`
is `z.string().min(3).max(120)` — free text long enough to hold
`<!-- END GENERATED: adr-inventory -->` — so a record carrying a marker in its
title would have it rendered into a table row inside the block, and every later
run would throw `found 1 and 2`. One corpus record could permanently disable the
generator whose job is to render it, and the fix would have been to edit the
record. Markers are now recognized only as standalone lines, tolerating
indentation, trailing whitespace, and CRLF, so a marker in prose or in a table
cell is content rather than a delimiter.

`tableCell` escaped `\` and `|` but not CR/LF. A YAML block scalar satisfies the
title schema with an embedded newline, and a raw newline splits one table row
into two lines of markdown — which regeneration then reproduces byte-for-byte,
so the no-diff gate stays green over a broken table, and a multiline title could
place a marker on a line of its own. It now follows `@adrkit/core`'s queue
formatter exactly (`packages/core/src/queue/format.ts` `escapeCell`): CRLF to
LF, CR to space, LF to `<br>`, then `\`, `|`, and a backtick, with the backslash
escaped first so it cannot combine with an introduced escape.

The two halves compose: newline normalization means content can never start a
line, and line-anchoring means an inline occurrence is never a marker.

Observed failing before being relied on (ADR-0016): the five new cases fail
against the pre-fix script — marker in a cell, marker in prose, multiline title,
forged marker line, and backtick escaping — and pass after. A sixth asserts a
genuinely indented, trailing-whitespace, CRLF marker is still recognized, so the
new anchoring cannot silently become too strict.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
Resolve documentation and generated inventory overlap while preserving trusted gates, queue determinism, and release operations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom mbeacom added the gate-change-acknowledged A maintainer has seen and accepted this PR's change to the CI gate surface (ADR-0035) label Aug 27, 2026
@mbeacom
mbeacom merged commit b5749f3 into main Aug 27, 2026
16 of 17 checks passed
@mbeacom
mbeacom deleted the mbeacom-manifest-inventory-determinism branch August 27, 2026 01:02
mbeacom added a commit that referenced this pull request Aug 27, 2026
release: prepare v0.12.0

Cut the unreleased changelog into `## [0.12.0] - 2026-08-27` and move every
lockstep version-bearing surface from 0.11.0 to 0.12.0. `@adrkit/spec-kit`
stays at 0.1.3 and the agent plugin at 0.2.0: both are independently versioned
under ADR-0007 and neither moves with the repository release.

Two changelog corrections rather than a mechanical rename. The trusted-gates
trio was authored under `### Added` in #179, but #177 later inserted a `###
Fixed` heading above it and silently swallowed all three entries into the wrong
section; they are restored to `### Added`, and `### Fixed` now follows `###
Changed` as in every prior release section. The
`docs/repository-trust-operations.md` entry said those controls "cannot be
applied until this lands" — no longer true once #180 recorded the deployed
state, so it now states what is actually deployed: `trusted-dco` and
`gate-integrity` are among the ten required contexts, the pull-request-
controlled `dco` context was removed from that set only after the trusted one
reported green on real pull requests, and both gates were observed red then
green on real pull requests rather than in a fixture (ADR-0016).

`bun.lock`'s diff is exactly the four workspace `version` lines, edited
directly rather than by regenerating the lockfile, so no transitive drift rides
along in the release commit.

Validation: 2790 tests pass; typecheck, lint, `adr lint` (35 records, 0
errors), actionlint, and every `check:*` gate green; schema, MANIFEST, and
committed Action bundle no-diff gates clean. The committed `packages/ci/dist`
bundles were confirmed byte-identical to a canonical Linux Bun 1.3.14 rebuild
and were restored after `release:pack`'s non-frozen build rewrote them under
local Bun 1.4.0. `release:pack --tag v0.12.0` produced five artifacts (four
lockstep at 0.12.0 plus spec-kit at 0.1.3); installed-tarball smoke passed on
Node 22.22.2 and 24.16.0; `release:publish --dry-run` exited 0 and skipped
`@adrkit/spec-kit@0.1.3` as registry-identical, confirmed independently against
the registry shasum; `npm audit` over the packed consumer tree found 0
vulnerabilities against an empty `KNOWN_CONSUMER_ADVISORY_ACCEPTANCES`; and
both OCI targets built and smoked, with the MCP image serving both protocol
eras read-only and networkless.

Signed-off-by: Mark Beacom <m@beacom.dev>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-change-acknowledged A maintainer has seen and accepted this PR's change to the CI gate surface (ADR-0035)

Projects

None yet

2 participants