fix(spec): the liveness governance denominator is the authorable set, not the registered set - #18581
Merged
os-bill merged 3 commits intoSep 17, 2026
Merged
Conversation
…e set Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <[email protected]>
…registry Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <[email protected]>
… and every run prints it Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <[email protected]>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 17, 2026
os-bill
marked this pull request as ready for review
September 17, 2026 04:17
os-bill
deleted the
claude/issue-18133-liveness-governance-denominator
branch
September 17, 2026 04:42
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…e first of objectstack-ai#18582's three debts (objectstack-ai#18587) Part of objectstack-ai#18582 Clause-②: no Seeds `packages/spec/liveness/sharing_rule.json` and moves `sharing_rule` out of `PENDING_GOVERNANCE` into `GOVERNED`. This is **one of the three debts** on that card; `connector` and `analytics_cube` stay on it, which is why the first line is `Part of` and not a closing keyword. ## What the coverage line says, before and after PR objectstack-ai#18581 made this line print on **every** run precisely so a change like this is visible. Measured on this branch: ``` before (origin/main @ bb9794a) 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 … 36 governed in total.) after (this branch) governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack collection(s) (analytics_cube, connector, sharing_rule, webhook); 28 governed, 2 awaiting a ledger (analytics_cube, connector) — a worklist, not a merge gate. (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS … 37 governed in total.) ``` The other counters move the way a seeded type should move them, and one of them deliberately does not: | counter | before | after | |---|---|---| | classified properties | (no `sharing_rule` row) | `sharing_rule 17 classified (live 16, planned 1)` | | repo-local evidence paths resolved | 505 / 505 | 524 / 524 | | `path#symbol` anchors resolved | 583 / 583 | 617 / 617 | | key-mention pairs anchored | 504 of 505 asked, 1 exempt | 523 of 524 asked, 1 exempt | | live entries citing a PRODUCER | 39 | 48 | | **container coverage (undrilled child keys)** | **113 containers / 578 keys** | **113 containers / 578 keys** | The last row is the point of drilling `sharedWith`: the new type adds **zero** rows to `undrilled-containers.baseline.json`. ## The classification, and the one row that is not `live` Ten authored keys (plus the seven ADR-0010 envelope fields the gate auto-classifies). Nine are `live`; `type` is `planned`. **Every row carries a `producer`, and that is the substance of the work.** The authoring shape is not the enforced shape: ADR-0057 D6 makes the `sys_sharing_rule` row canonical and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time. So a consumer pointer alone would prove only that a **column** is read, never that the authored value reaches it. That is the `seed.env` shape (objectstack-ai#4837) applied to a whole type rather than to one key. **`type` is `planned`, deliberately neither `live` nor `dead`.** `SharingRuleType` has exactly one member and `CriteriaSharingRuleSchema` pins the key as `z.literal('criteria')`, so every value an author can write is the same value. Its only reader in this repo is a defensive `if (r.type === 'owner')` in the seeder, unreachable for anything the schema admits (`owner` rules left the authoring surface with ADR-0078). - not `live` — nothing dispatches on it; a comparison against a value the schema rejects is Prime Directive objectstack-ai#10's `case` label with no reachable call site; - not `dead` — the key is **required**, so it is not a silent no-op an author can get wrong, and the schema header records the intent (kept as the discriminant so a future enforced rule type re-joins as a union member). Marking it `dead` would put a required literal on the ADR-0049 enforce-or-remove worklist, where removing it breaks every authored rule to delete nothing. - precedent: `action.operation`, the same shape — a one-member discriminator held `planned` until a runtime half dispatched on it, then flipped to `live` (objectstack-ai#15080). The negative rests on a census with a lit control, not a bare grep: the population that reads a declared rule **item** is the seeder, four `packages/lint` validators and objectui's create-door client validation; searching that population for `r.type` / `rule.type` returns the one defensive comparison and nothing else, while the same search for `r.condition` / `rule.condition` returns hits in three of them. **Preview read points enumerated, per the objectstack-ai#7131 mechanical rule** — `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is **not** one of them. Recorded in the ledger rather than skipped, because "the type has no registered preview" is the sentence a later sweep needs. What objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES`). **One decoy named so the next census does not trip on it:** objectui's own `SharingRuleConfig` (`packages/types/src/permissions.ts`) is a different shape entirely, re-exported twice and read by nothing. It matches this type by name only. ## File surface — three deviations, all mechanically forced, none discretionary The dispatch scoped this to the ledger, the two `check-liveness.mts` rows and a changeset, with `liveness/README.md` **read-only**. Three files outside that surface had to move, because the gates read them off `GOVERNED` and off the ledger set: 1. **`packages/spec/liveness/README.md`** — `reconcileReadmeTable` fails on a `GOVERNED` type with no row in the "Current state" table, and the heading's `N governed types` is checked three ways against the rows and against `GOVERNED.length`. A `check:liveness` that exits 0 and a read-only README are not both reachable. Row written by measurement; heading 36 → 37; the tail paragraph that described the map as holding three debts now says which one is paid. 2. **`packages/spec/liveness/state-counts.md`** — generated, `merge=os-regen`, proved fresh by the same gate. Regenerated with `pnpm --filter @objectstack/spec gen:liveness-counts`, never hand-edited. 3. **`docs/qa/platform-checklist/coverage.json`** — this ratchet **derives its universe from `packages/spec/liveness/*.json`**, so seeding a ledger is exactly what makes the kind `UNCLASSIFIED` there (`check:platform-checklist` exit 1, reproduced before and after). Mapped to the three items that already exercise the surface — `access-security.sharing-rules-widen`, `access-security.sharing-rule-authoring-ui`, `access-security.record-share-grant-revoke` — rather than waived. **No checklist item is authored here**, and a waiver would have been false. A changeset **is** owed and is not `skip-changeset`: `packages/spec`'s `files[]` includes `liveness`, so the new ledger ships inside the tarball. `patch`. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run with each exit code landed on disk before being read, then reconciled with `--ran`: ``` dispatch-gates --ran: 65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN. ``` - **`check:liveness` exits 0**, with the coverage line above. - `check:generated` — **exit 0 on a built tree** (15 of 15 artifacts current). It exits 1 on an unbuilt one and says so itself; `packages/spec` was built under the shared verify lock before the verdict was read. - `check:platform-checklist` — exit 1 before the coverage entry, exit 0 after: `36 kinds mapped, 1 waived`. - `check:pm-dispatch-gates` exit 0 (1746 self-test cases; the battery takes ~520s on this box). - `packages/spec` — `typecheck` exit 0, `test` exit 0 (483 files / 13780 tests). The liveness script suites the edited gate owns, plus every test whose text names `check-liveness`, run as their own slice: 13 files / 378 tests, exit 0. - **NOT MEASURED (2)** — `check:dual-build-cjs-loads` and `check:lean-entry-closure`, both `exit 3 · PREREQUISITE NOT MET`: they read a whole-repo `dist/` this container has not built. Neither pass nor finding. CI builds the closure. - **Known and already filed, not a finding here** — `check:cross-package-test-inputs` exits 1 once `packages/spec` has been built and 0 when it has not (objectstack-ai#18353 / objectstack-ai#18440). Recorded in both states; its message names `packages/spec/dist/` and `packages/cli/test/init-created-files-summary.e2e.test.ts`, neither of which this diff touches. - **`pnpm lint` is CI's whole-repo run.** The narrowing here is a measurement, not a skip: eslint's own `files` declaration is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so five of the six changed files (`.md`, `.json`) are outside its universe by that declaration; the sixth, `packages/spec/scripts/liveness/check-liveness.mts`, linted clean — `--format json` reports **1 file, 0 errors, 0 warnings**. `eslint.config.mjs` states in its own prose that this repo "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. Gate numbers are quoted from runs at `2f932b09`. ## Acceptance notes Noted, not filed, and one to file: - **To file** — the four sharing-related entries in `packages/spec/scripts/liveness/proof-registry.mts` record a `blockedReason` whose premise this PR falsifies: `bu-hierarchy-sharing`, `sharing-rule-criteria-required`, `declarative-rbac-seeding` and `sharing-rule-org-scoped-listing` each say sharing rules are "authored at STACK level, which is not a governed metadata type … so there is no ledger entry to ratchet". There is one now. `showcase-declarative-rbac-seeding` in particular authors `sharingRules[]` on the showcase stack and asserts the seeded row's `object_name`, `recipient_type`, `recipient_id` and translated `criteria_json` — i.e. it exercises five of these keys end to end and is a genuine ADR-0054 binding candidate. ⛔ Deliberately **not** done here: binding a high-risk class is a separate ADR-0054 §3 act, one class at a time, and `sharing-rule-criteria-required` is the counter-example that makes it a judgement rather than a sweep — it POSTs the runtime body to `/sharing/rules` and never authors the spec key, so it must **not** bind `condition`. - Noted, not filed — `packages/spec/liveness/README.md`'s "Adding a type" recipe lists four steps and none of them is the README row, the heading count, the generated `state-counts.md` or the platform-checklist coverage entry, all four of which the gates now require. The recipe predates those three checks. Whoever seeds `connector` or `analytics_cube` next will walk into the same three red gates this PR did; that seat is the one that will carry it. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/maincheck-liveness.mtsbuiltreport.ungovernedfromlistMetadataTypeSchemaTypes()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.879b51270)listMetadataTypeSchemaTypes()analytics_cube/connector/sharing_rule/webhookabsent from all fourlistUnregisteredKindSchemaTypes()viewandflowpresent in the registered set;viewabsent from the unregistered setgetMetadataTypeSchema(t)for each of the fourGOVERNED/PENDING_GOVERNANCEmembershipwebhookgoverned; the other three in neither map;PENDING_GOVERNANCEwas{}packages/spec/liveness/connector.json, nosharing_rule.json, noanalytics_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 anEXTRA_SCHEMASrow. That identifier does not exist anywhere in this repository — the mechanism isSPEC_ONLY_SCHEMASincheck-liveness.mts, plusliveness/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:And the precedent is already landed, in the second consumer the dispatch warned about:
reachabilityRootTypes()inscripts/build-schemas.ts(#17356, PR #18131) is this exact union, computed inside the consuming gate, whose docblock states thatlistMetadataTypeSchemaTypes()"answers its own question correctly and this file does not touch it."⇒ #6245's guarantee survives byte-for-byte:
metadata-type-schemas.tsis 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.tsis 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:
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_GOVERNANCEwith 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:livenessexits 0 on this branch, andstate-counts.mdand the README state table are both still current (36 rows, unchanged — the ledger set did not move). Leaving them inreport.ungovernedinstead would exit 1 on every PR in the repo, which is a brokenmain, 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 -- PATHunder atrap ... 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
connectorrow fromPENDING_GOVERNANCE(anchor count 1 → 0):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):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
b3c8aded6e38ce73bd8097dba62554da19ec97ffequalsHEAD:packages/spec/scripts/liveness/check-liveness.mts, andgit diff HEAD --statfor that path is empty. No permanent test file was left behind by either leg.Tests
scripts/liveness/check-liveness.test.tsgains a#18133block 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.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;ungovernedandstalePendingboth empty;PUT /metawrite hole (#2657 audit, option A) #6245's guarantee asserted from the gate that had the motive to break it: the unregistered kinds are still absent fromlistMetadataTypeSchemaTypes().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 --listFilesnamesscripts/liveness/check-liveness.mtsandscripts/liveness/check-liveness.test.ts.pnpm --filter @objectstack/spec check:liveness— exit 0.check:empty-state— exit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsoff 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.PREREQUISITE NOT MET, each refusing because no package has adist/: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-inputsexits 0 here — consistent with the knownpackages/spec-is-built behaviour already filed as [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [finding]check:cross-package-test-inputsanswers 1 or 0 depending on whetherpackages/spechas been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440; nothing new is filed for it.eslint --no-inline-config --format jsonover the two changed TS files: 2 files linted, 0 errors, 0 warnings (count read from the JSON, not from prose). Population read fromeslint.config.mjsitself: the global block isfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']minusNEVER_LINTED, so the two.mdpaths in this diff are outside eslint's population entirely. Invariance: that config declares in its own words that this repo "runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) 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.check:nul-bytes:grep -naPover all four changed files — no matches.Declared deviation — one file outside the dispatched surface
The dispatched surface was
check-liveness.mts· its test · anyliveness/*.jsonmade owed ·.changeset/*.md. This diff also editspackages/spec/liveness/README.md, and that is a deliberate, declared addition rather than an oversight:PENDING_GOVERNANCEincheck-liveness.mtsis empty". Shipping a diff that falsifies shipped documentation is the shape this ledger's own history (spec(liveness): the README state table has no rows forapi/capability— the table silently fell behind its own registry #7257) exists to stop — a completeness sentence no build can fail.packages/spec'sfiles[]shipsliveness/, so that README is published — which is also why this PR carries apatchchangeset rather thanskip-changeset. Nothing underscripts/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) andstate-counts.mdare untouched, andcheck:livenessreconciles all three green.Acceptance notes
Observed while measuring, not filed and not fixed here:
SPEC_ONLY_SCHEMASrowwebhook: WebhookSchemais now redundant withgetMetadataTypeSchema()'s meta: bind Zod schemas for webhook / connector / sharing_rule WITHOUT registering the kinds — close the unvalidatedPUT /metawrite hole (#2657 audit, option A) #6245 fallback, which resolves the identical schema instance. Harmless and load-bearing as documentation; folding it away is the Flip webhook liveness ledger dead→live now that #3461 bridged the authoring surface (+ reassess type registration) #3490 reassessment's business, not this card's.listUnregisteredKindSchemaTypes()'s docblock still liststhemeamong "today's" entries;themewas retired atdefineStack({ themes })is parsed, ingested and never applied — the last hop from a storedthemeitem to the theme engine does not exist #10485 and the map now holds four. Stale prose in a read-only file — a doc nit, not one of the three filable classes.pnpm --filter @objectstack/spec testwas 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.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Generated by Claude Code