Skip to content

Commit fe2e15a

Browse files
os-zhuangclaude
andauthored
feat(spec): gate the liveness README's state table against GOVERNED (#7257) (#7374)
The ledger README's "Current state" table is the liveness ledger's own index — one row per governed type, counts regenerated from the gate's --json report, a hand-written Notes cell per row. Its heading, `## Current state — N governed types (complete registry coverage)`, is a completeness CLAIM that nothing could falsify: N was the count of ROWS, not of governed types, and the two agreed only by coincidence. They stopped. `api` and `capability` were both in GOVERNED, both had ledgers, both were counted by the gate, and neither had a row. Same shape the file spends 500 lines warning about, one level up: `dashboard. widgets` asserted in prose that its 22 child keys were classified in a subtree that never existed, and survived a release because prose cannot fail a build (#4956). Every other claim in the file became data the gate resolves; the index was the last one riding on a human reading it. - readme-table.mts — the fourth direction, pure and unit-tested for the same reason as orphans.mts: on a green tree the table is complete, so a passing gate proves nothing about whether the check can fire. Fails (not warns) on a missing row, an orphan row, a duplicated row, or a heading N that disagrees with either the row count or GOVERNED.length. All three heading legs, because two of them agreeing is exactly the state this issue found. - No new check:/gen: script — it rides inside check:liveness, which the Spec property liveness workflow already runs on packages/spec/**. check:generated --reconcile-only stays green with no ledger change. - The documented regeneration snippet now reads the table back as well as the report, so a governed type with no row prints a SKELETON row: the omission is visible at regeneration time as well as at CI time. - The two missing rows are back-filled from their seeding PRs' own measurements — counts from --json, prose from what #5271/PR #5312 and #5961/PR #6540 actually measured. The Notes cell is the one thing the gate deliberately does not check, which is why they were filed rather than fixed on the spot. - Also removes a stray blank line that had been splitting the table in two. Reverse verification is pinned in CI, not just performed once: three cases in check-liveness.test.ts drive the REAL gate against a copy of the real README with a row deleted, the heading skewed, and an ungoverned row added, and read exit 1. Closes #7257 Co-authored-by: Claude <noreply@anthropic.com>
1 parent a87c5cd commit fe2e15a

6 files changed

Lines changed: 663 additions & 11 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`check:liveness` reconciles the ledger README's "Current state" table against `GOVERNED`,
6+
and the two rows it was missing (`api`, `capability`) are back-filled.
7+
8+
That table is the liveness ledger's own index — one row per governed type, counts
9+
regenerated from the gate's `--json` report, and a hand-written Notes cell recording how
10+
each type got where it is. It opens with a heading of the form
11+
`## Current state — N governed types (complete registry coverage)`, which is a completeness
12+
CLAIM, and nothing could falsify it: `N` was the count of ROWS, not of governed types, so
13+
the two agreed only by coincidence. They stopped. `api` (seeded 2026-08-04, #5271/#5206,
14+
PR #5312) and `capability` (seeded 2026-08-08, #5961, PR #6540) were both in `GOVERNED`,
15+
both had ledgers, both were counted by the gate, and neither had a row — under a heading
16+
that still read as true to every subsequent reader (#7257).
17+
18+
This is the shape the file spends 500 lines warning about, one level up. `dashboard.widgets`
19+
asserted in prose that its 22 child keys were "classified in the DashboardWidgetSchema
20+
subtree" — a subtree that never existed — and survived a release because **prose cannot fail
21+
a build** (#4956). Every other claim in that file has since become data the gate resolves:
22+
schema → ledger, ledger → schema, container → declared disposition, `GOVERNED` → the
23+
metadata-type registry in both directions. The index was the last one riding on a human
24+
reading it.
25+
26+
So it becomes the gate's fourth direction (`scripts/liveness/readme-table.mts`, pure and
27+
unit-tested for the same reason as `orphans.mts`: on a green tree the table is complete, so
28+
a passing run proves nothing about whether the check can fire). It FAILS — not warns — when
29+
a `GOVERNED` type has no row, when a row exists that `GOVERNED` does not back, when a type is
30+
claimed by two rows, or when `N` disagrees with either the row count or `GOVERNED.length`.
31+
All three heading legs are checked, because two of them agreeing is exactly the state #7257
32+
found. No new `check:`/`gen:` script: it rides inside `check:liveness`, which the
33+
`Spec property liveness` workflow already runs on every PR touching `packages/spec/**`.
34+
35+
The documented regeneration snippet now reads the table back as well as the report, so a
36+
governed type with no row prints a **skeleton row** instead of silently not being printed
37+
next to its siblings — the omission surfaces at regeneration time as well as at CI time.
38+
Both were needed: the count columns get regenerated far more often than the row set gets
39+
audited.
40+
41+
What the gate deliberately does not check is the Notes cell, which is hand-written
42+
measurement — a manufactured one is worse than a missing row, and that is why the two rows
43+
were filed rather than fixed on the spot. Both are back-filled here from their seeding PRs'
44+
own measurements: counts from `--json`, prose from what #5271/#5312 and #5961/#6540 actually
45+
measured. Both types turn out to be the same worked example — **enforced but undeclared**,
46+
the mirror of this ledger's usual `declared ≠ enforced`: each was already being consumed at
47+
runtime while absent from the metadata-type registry, so `saveMetaItem` stored arbitrary JSON
48+
against it. Docs and tooling only; no runtime behaviour changes.

0 commit comments

Comments
 (0)