Skip to content

Commit bb9794a

Browse files
os-billobjectstack-agentclaude
authored
fix(spec): the liveness governance denominator is the authorable set, not the registered set (#18581)
Fixes #18133 Clause-②: no This card **tightens** governance coverage: the denominator the liveness ratchet divides by grows, three previously-unreachable types become nameable, and no accept set, public export or schema moves. The registry itself is byte-untouched. ## The defect, measured first-hand against `origin/main` `check-liveness.mts` built `report.ungoverned` from `listMetadataTypeSchemaTypes()` under a comment stating that function returns *"exactly the set of authorable metadata types"*. That sentence is the declared contract, and it is false — the same sentence #17356 measured false for the reachability gate, one gate over. | reading (probe run at base `879b51270`) | result | |---|---| | `listMetadataTypeSchemaTypes()` | 26 names; `analytics_cube` / `connector` / `sharing_rule` / `webhook` absent from all four | | `listUnregisteredKindSchemaTypes()` | exactly those four | | firing control on the same probe | `view` and `flow` present in the registered set; `view` absent from the unregistered set | | `getMetadataTypeSchema(t)` for each of the four | resolves a schema (third fallback, #6245) | | `GOVERNED` / `PENDING_GOVERNANCE` membership | `webhook` governed; the other three in **neither** map; `PENDING_GOVERNANCE` was `{}` | | `packages/spec/liveness/` | no `connector.json`, no `sharing_rule.json`, no `analytics_cube.json` | ⇒ a type in neither map produces no row in **any** of this gate's lists, so `ungoverned: []` read identically whether the gate had looked and found nothing or had never looked at all. That indistinguishability is the finding; the count of what it hid is deliberately not claimed. **One card correction.** The card attributes `webhook`'s manual patch to an `EXTRA_SCHEMAS` row. That identifier does not exist anywhere in this repository — the mechanism is `SPEC_ONLY_SCHEMAS` in `check-liveness.mts`, plus `liveness/webhook.json`. The claim is right, the symbol name is not. ## Why the repair is local, on the merits The dispatch flagged a possible fork into `packages/spec/src/kernel/metadata-type-schemas.ts`. It is not one, and the target file says so itself. `listUnregisteredKindSchemaTypes()` already exists there (#6931) and its own docblock declares: > `[#6931]` This exists so a check can ENUMERATE that map, and for nothing else. > ⚠️ Being listed by this function grants NOTHING. It returns names, not schemas, not descriptors: no `MetadataTypeSchema` enum membership, no `DEFAULT_METADATA_TYPE_REGISTRY` entry, no create seed, no authorization verdict, no place in the #4001 campaign count. And the precedent is already landed, in the second consumer the dispatch warned about: `reachabilityRootTypes()` in `scripts/build-schemas.ts` (#17356, PR #18131) is this exact union, computed **inside the consuming gate**, whose docblock states that `listMetadataTypeSchemaTypes()` "answers its own question correctly and this file does not touch it." ⇒ #6245's guarantee survives byte-for-byte: `metadata-type-schemas.ts` is not in this diff. The gate that was asking the wrong question is where the question is fixed. **Not yet one shared spelling.** The card suggests a single helper read by both gates. `scripts/build-schemas.ts` is held by seat 1 (#18301 / PR #18529), so folding the two together is a follow-up; until then each docblock cross-references the other by name. ## What the gate now prints that it did not before Green run, before → after: ``` before: (no coverage line at all — it printed only when PENDING_GOVERNANCE was non-empty) after: governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 27 governed, 3 awaiting a ledger (analytics_cube, connector, sharing_rule) — a worklist, not a merge gate. (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS — not metadata types, so the override IS their governance; 36 governed in total.) ``` The success sentence changed too: "every **registered** type is governed or explicitly pending" is now "every **authorable** type — registered kind or unregistered-kind stack collection — is governed or explicitly pending". **Does it turn any existing check red? No, and here is the arithmetic.** The three newly visible types are recorded as declared debts in `PENDING_GOVERNANCE` with a reason and an issue number apiece — the disposition the gate's own failure text prescribes, and the one that keeps them *stated* rather than *silent*. `pnpm --filter @objectstack/spec check:liveness` exits **0** on this branch, and `state-counts.md` and the README state table are both still current (36 rows, unchanged — the ledger set did not move). Leaving them in `report.ungoverned` instead would exit 1 on every PR in the repo, which is a broken `main`, not a delivered finding. ## Reverse verification — two legs, both fired, both restored Run against the committed implementation; each leg proved its mutation reached disk by anchor count before the gate ran, and each restored via `git checkout HEAD -- PATH` under a `trap ... EXIT INT TERM`, verified by blob hash rather than by an exit code. **Leg A — the new denominator really reaches an unregistered kind.** Removed the `connector` row from `PENDING_GOVERNANCE` (anchor count 1 → 0): ``` ✗ 1 AUTHORABLE metadata type(s) governed by nothing: connector ``` exit **1**. Under the old denominator no edit to that map could have produced this row for `connector` — it would have been reported *stale* instead, because the type was not in the set at all. **Leg C — the union line is load-bearing, not decorative.** Reverted `authorableTypes()` to the registered set alone, keeping the three rows (anchor count 1 → 0, injected marker count 1): ``` ✗ 3 stale PENDING_GOVERNANCE row(s) — the debt is already paid: analytics_cube connector sharing_rule ``` exit **1**. That output is precisely the old code's whole vocabulary for these three: *"not a type I know"*, never *"a type nobody governs"*. Restore proof for both legs: on-disk blob `b3c8aded6e38ce73bd8097dba62554da19ec97ff` equals `HEAD:packages/spec/scripts/liveness/check-liveness.mts`, and `git diff HEAD --stat` for that path is empty. No permanent test file was left behind by either leg. ## Tests `scripts/liveness/check-liveness.test.ts` gains a `#18133` block that holds the **gate's output answerable to the live registry** rather than to a copied list — a hard-coded expectation would pass unchanged if the gate stopped reading the registry at all, which is the regression class this is for. - a control that both sides of the union are non-empty and disjoint (without it, "the denominator omits nothing" is satisfied by a registry that enumerates nothing); - the denominator contains every `listUnregisteredKindSchemaTypes()` name, is **strictly larger** than the registered set, and equals the union exactly — this is the assertion that goes red the moment somebody simplifies the union away; - every member is governed or explicitly pending, `ungoverned` and `stalePending` both empty; - the composition line is printed on a **green** run; - #6245's guarantee asserted from the gate that had the motive to break it: the unregistered kinds are still absent from `listMetadataTypeSchemaTypes()`. ## Verification Final commit `7c98551bac`; every reading below is from that tree. - `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/check-liveness.test.ts` — 58 passed (was 53). - `pnpm --filter @objectstack/spec test` — **482 files passed, 1 skipped; 13776 tests passed, 1 skipped**. - `pnpm --filter @objectstack/spec typecheck` — exit 0. Both edited files are proven in a tsc program: `tsc -p tsconfig.scripts.json --listFiles` names `scripts/liveness/check-liveness.mts` and `scripts/liveness/check-liveness.test.ts`. - `pnpm --filter @objectstack/spec check:liveness` — exit 0. `check:empty-state` — exit 0. - **Gate families**: derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` off the merge base (never a hand-fed path list), reconciled with `--ran`: **68 derived, 63 run green, 5 NOT MEASURED, 0 UNRUN**. Every exit code was landed to disk before being read. - The 5 NOT MEASURED all exit **3** — `PREREQUISITE NOT MET`, each refusing because no package has a `dist/`: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt`. They need a repo-wide build; this diff contains no built-output source, and CI covers them. ⛔ Neither pass nor finding. - `pnpm check:cross-package-test-inputs` exits **0** here — consistent with the known `packages/spec`-is-built behaviour already filed as #18353 / #18440; nothing new is filed for it. - **Lint, narrowed and the narrowing proven.** `eslint --no-inline-config --format json` over the two changed TS files: **2 files linted, 0 errors, 0 warnings** (count read from the JSON, not from prose). Population read from `eslint.config.mjs` itself: the global block is `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` minus `NEVER_LINTED`, so the two `.md` paths in this diff are outside eslint's population entirely. Invariance: that config declares in its own words that this repo "runs one `eslint.config.mjs`, which never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file" — so nothing in this diff can move the verdict on a file it does not touch. The repo-wide sweep is CI's. - Control-character scan beyond `check:nul-bytes`: `grep -naP` over all four changed files — no matches. ## Declared deviation — one file outside the dispatched surface The dispatched surface was `check-liveness.mts` · its test · any `liveness/*.json` made owed · `.changeset/*.md`. This diff also edits **`packages/spec/liveness/README.md`**, and that is a deliberate, declared addition rather than an oversight: 1. that README carries a **second copy of the same false sentence** — "i.e. exactly the set of *authorable* metadata types" — so repairing only the script would have left the card's own warning realised: the sentence reappearing at a third door; 2. more decisively, this change makes an existing published sentence there **factually false**: "`PENDING_GOVERNANCE` in `check-liveness.mts` is empty". Shipping a diff that falsifies shipped documentation is the shape this ledger's own history (#7257) exists to stop — a completeness sentence no build can fail. `packages/spec`'s `files[]` ships `liveness/`, so that README is published — which is also why this PR carries a `patch` changeset rather than `skip-changeset`. Nothing under `scripts/` ships; had the README not been owed, no published byte would have moved. Both edits are prose only: the state table, its heading count (36) and `state-counts.md` are untouched, and `check:liveness` reconciles all three green. ## Acceptance notes Observed while measuring, **not** filed and **not** fixed here: - The `SPEC_ONLY_SCHEMAS` row `webhook: WebhookSchema` is now redundant with `getMetadataTypeSchema()`'s #6245 fallback, which resolves the identical schema instance. Harmless and load-bearing as documentation; folding it away is the #3490 reassessment's business, not this card's. - `listUnregisteredKindSchemaTypes()`'s docblock still lists `theme` among "today's" entries; `theme` was retired at #10485 and the map now holds four. Stale prose in a read-only file — a doc nit, not one of the three filable classes. - Under Leg C's ablation the new coverage line reads "26 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack collection(s)", i.e. visibly self-contradictory, because the count and the composition are two independent reads. That is a tell, not a defect: a reverted union announces itself in the line's own arithmetic. - `pnpm --filter @objectstack/spec test` was invoked with a trailing `-- --maxWorkers=2`; vitest discards everything after a bare `--`, so the suite ran at default concurrency. Recorded because the reading is the suite's, not the flag's. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: objectstack-agent <agent@objectstack.ai> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5ed7ad9 commit bb9794a

4 files changed

Lines changed: 337 additions & 41 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The liveness ledger's published README no longer claims the metadata-type registry is "exactly the set of authorable metadata types" — the governance denominator is now that set, and every run prints it
6+
7+
`check-liveness.mts` built its coverage denominator from
8+
`listMetadataTypeSchemaTypes()` under a comment stating that function returns
9+
"exactly the set of *authorable* metadata types", and the ledger README carried
10+
the same sentence. It is false in a specific, load-bearing way: that function
11+
deliberately does not enumerate `UNREGISTERED_KIND_SCHEMAS` — enrolling those
12+
entries there "would claim a status this change is careful not to grant" — while
13+
the kinds bound in that map are authored on every boot through their stack
14+
collections (`connectors:`, `sharingRules:`, `analyticsCubes:`, `webhooks:`) and
15+
on every write through `PUT /api/v1/meta/:type/:name`, whose `resolveOverlaySchema`
16+
resolves them through `getMetadataTypeSchema()`.
17+
18+
So `connector`, `sharing_rule` and `analytics_cube` sat in **neither** `GOVERNED`
19+
**nor** `PENDING_GOVERNANCE`, and a type in no bucket produces no row in any of
20+
this gate's lists. The blindness was therefore invisible in the gate's own
21+
output: `ungoverned: []` read exactly the same whether the gate had looked and
22+
found nothing or had never looked at all.
23+
24+
The denominator is now `authorableTypes()` — the registered kinds UNION
25+
`listUnregisteredKindSchemaTypes()`, the enumeration helper that exists so a check
26+
can read that map and which grants nothing by listing a name. The registry itself
27+
is untouched: no kind is registered, no enum grows, no create seed is demanded and
28+
no accept set moves, and the same split already landed one gate over as
29+
`reachabilityRootTypes()` in `build-schemas.ts`. The three newly visible types are
30+
recorded as declared debts with a reason and an issue number apiece, which is what
31+
the ratchet asks for and what the README now says; the direction of travel is out
32+
of that map and into `GOVERNED`.
33+
34+
Every run also prints the denominator and its composition unconditionally. That
35+
line used to appear only when `PENDING_GOVERNANCE` was non-empty, so the one state
36+
worth reporting — "N authorable types looked at, none unaccounted for" — rendered
37+
as nothing at all, which is the same silence an unseen type produces.

‎packages/spec/liveness/README.md‎

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,26 @@ route decides the disposition — see `scripts/liveness/orphans.mts` and
2828
The gate reads `BUILTIN_METADATA_TYPE_SCHEMAS` (`packages/spec/src/kernel/metadata-type-schemas.ts`)
2929
via `listMetadataTypeSchemaTypes()` / `getMetadataTypeSchema()` — **the same registry the
3030
runtime `/api/v1/meta/types/:type` endpoint and the Studio metadata-admin forms use**,
31-
i.e. exactly the set of *authorable* metadata types. It walks each type's Zod schema
31+
i.e. the set of *registered* metadata KINDS. It walks each type's Zod schema
3232
directly (not `z.toJSONSchema`, which throws on `object`/`action`).
3333

3434
This matters: the older gate read the generated `json-schema/` directory, which omits
3535
most top-level authorable types (object/field/flow/action/...) — so it was blind to the
36-
core surface. The registry is complete.
36+
core surface. The registry is complete *as a registry*.
37+
38+
⛔ **Registered is not the same set as authorable, and this page used to say it was** —
39+
"i.e. exactly the set of *authorable* metadata types" was the sentence #17356 measured
40+
false for the reachability gate and #18133 for this one. `listMetadataTypeSchemaTypes()`
41+
deliberately does not enumerate `UNREGISTERED_KIND_SCHEMAS` (#6245: enrolling those
42+
entries there "would claim a status this change is careful not to grant"), yet the kinds
43+
bound in that map are authored on every boot through their stack collections
44+
(`connectors:` / `sharingRules:` / `analyticsCubes:` / `webhooks:`) and on every write
45+
through `PUT /api/v1/meta/:type/:name`. So the WALK is registry-rooted, as above, while
46+
the **governance denominator** — whom a ledger must exist for — is the registered kinds
47+
UNION `listUnregisteredKindSchemaTypes()` (#6931), computed by `authorableTypes()` in
48+
`check-liveness.mts`. Every run prints that denominator and how it is composed, because
49+
a type in no bucket produces no row anywhere: without the printed count, "nothing
50+
ungoverned here" and "never looked" are the same output.
3751

3852
**Spec-only exception (`SPEC_ONLY_SCHEMAS`).** A type can be authorable yet deliberately
3953
*not* registered — `webhook` is the case: its schema is authored on a Stack/connector but
@@ -929,12 +943,21 @@ misleading entry carries `authorWarn` so authors hear about it at compile time
929943
(governed types with warn entries must also be registered in the CLI lint's
930944
`TYPE_COLLECTIONS` — see lint-liveness-properties.ts).
931945

932-
**Coverage is complete as of #4488**: every type in the metadata-type registry
933-
is governed, and `PENDING_GOVERNANCE` in `check-liveness.mts` is empty. The map
934-
itself stays, because the ratchet is the point — registering a new type without
935-
a ledger fails CI with instructions to govern it or record the debt (reason +
936-
issue number). The paragraph that used to sit here, listing nine ungoverned
937-
types as prose, is precisely how the gap survived for a year: prose cannot fail
938-
a build. Now the gate compares `GOVERNED` against the registry in both
939-
directions (an ungoverned registered type fails; so does a stale pending row
940-
whose debt is already paid).
946+
**Every registered type has been governed since #4488**, which emptied
947+
`PENDING_GOVERNANCE` of all nine debts the map opened with. The map itself stays,
948+
because the ratchet is the point — registering a new type without a ledger fails
949+
CI with instructions to govern it or record the debt (reason + issue number). The
950+
paragraph that used to sit here, listing nine ungoverned types as prose, is
951+
precisely how the gap survived for a year: prose cannot fail a build. Now the gate
952+
compares `GOVERNED` against the denominator in both directions (an ungoverned
953+
authorable type fails; so does a stale pending row whose debt is already paid).
954+
955+
⚠️ **The map is no longer empty, and that is #18133's finding rather than a
956+
regression.** Widening the denominator from the registered kinds to the authorable
957+
set (see the ⛔ note under *Source of truth* above) made three types visible that
958+
had been in **neither** `GOVERNED` **nor** `PENDING_GOVERNANCE` — `connector`,
959+
`sharing_rule` and `analytics_cube` — and therefore produced no row in any of the
960+
gate's lists while the report read complete. They are now declared debts with a
961+
reason and an issue number apiece, which is the state this ratchet exists to
962+
produce; the direction of travel is out of that map and into `GOVERNED`, exactly
963+
as it was for the nine. ⛔ Their presence is not a licence to leave them there.

0 commit comments

Comments
 (0)