feat(spec): check (c) proves a guidance-route retirement on a reachable def (#18301) - #18529
Conversation
…le def Check (c) of the authorable-surface deletion gate admitted a deleted baseline line on three proofs. A key retired by DELETING it from the shape and leaving its prescription in the closed shape's `guidance` table never carries the `[RETIRED]` mark proof 1 starts from, so on a reachable def it had no proof shape at all -- not "has not aged yet" but "has no clock". Adds proof 4: the def's emitted JSON Schema is closed (`additionalProperties: false`) AND the one `strictObject` declaration that matches the def by shape identity names the key in `guidance` (or enumerates it in a `guidanceSets` entry). Both facts are read from this build's own tree -- the declaration registry `strict-object.ts` records at construction -- never argued in a PR description, on the same discipline as the other three proofs. Two narrowings, both fail-closed: an empty shape carries no identity and is excluded, and a `guidanceSets` RegExp claims a family whose members were never written down, so it does not NAME a key and does not count. Measured on the shipped graph: 1525 emitted defs, 1117 closed, 144 carrying a route at all, naming 772 keys between them. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…ement
`data/Metric:filters` is a completed guidance-route retirement in the tree,
so the positive leg needs no synthetic key: the leaf really has left
`MetricSchema`'s shape and its prescription really is in the closed shape's
`guidance` table. Four keys in one run, because a proof that admits
everything and a proof that admits nothing both pass a one-legged test:
- the real retirement is admitted, by proof 4 and specifically not proof 2;
- the SAME def with an unprescribed key is still refused, which is what
separates a proof from a waiver for the def;
- `integration/DataSyncConfig:schedule` -- the tombstone a maintainer
ruling deliberately withheld -- is still refused, so this card adds a
proof beside that ruling rather than reversing it;
- a genuinely unreachable def is still waived by proof 2, in proof 2's
words, which would move if proof 4 had been written as a widening of it.
The `beforeAll` guard reads the tree fact as a lit/dark PAIR: the retired
key's rejection carries a prescription bullet and an undeclared neighbour's
does not. A guard that only asserted the rejection would pass on a shape
that rejects everything and prescribes for nothing.
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
…oof 1 keeps its clock CI reddened `pnpm run test:repo` on the previous head, in the file this card edits: `data/Object:compactLayout` is BOTH a `[RETIRED]` baseline entry (the #5898 aged-tombstone fixture) AND a real `guidance` key on the same def. With proof 4 placed ahead of the tombstone chain it admitted that deletion, which took it off proof 1's aging clock -- a tombstone could then be deleted early by writing a `guidance` line beside it. Proof 4 now lives on the `!wasRetired` branch and only there. That is not a patch over the symptom: a guidance-route retirement deletes the key from the shape instead of leaving a `retiredKey()` in it, so it never earned the mark, and "the entry at baseRev was LIVE" is the true description of every member of the class. Requiring the entry to be un-marked is therefore the class's own property, and it makes the two proofs disjoint rather than merely different. Pinned where it failed: the specimen is now a fifth key in the #18301 case, asserted to fall to the tombstone chain and specifically NOT to proof 4. The remedy's route 4 states the un-marked requirement, and says a marked key stays on route 1's clock. Also corrects an escaping slip in the first commit -- the proof-4 message carried real newlines inside its template literal where the file's convention is `\n` escapes. Same bytes on stdout, different source. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Reason, in one line: ① item 3 — the proof's stated "door is closed" condition is a false description of what the gate computes, contradicted by the repo's own recorded measurement, unpinned, and it is the basis on which the PR dismisses its own flagged boundary (note 1); the accept set on this head is nonetheless as declared and nothing published moves, so the fix is confined to the card's two files. Generated by Claude Code |
… the artifact Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
…ot why Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Reason, in one line: the FAIL's one verdict-bearing item is closed — closure is decided at the def by a probe that, executed here through the head's own function, refuses every open form and admits only a delivered prescription; the false safeguard claim is gone from code, docblock, remedy and body; the real-specimen pin discriminates and is green in CI; nothing published moves and both declarations are true — with two census figures in prose (258; "7 of the 8 defs") measured wrong and owed a one-line correction on the next touch. Generated by Claude Code |
… not the registered set (objectstack-ai#18581) Fixes objectstack-ai#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 objectstack-ai#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, objectstack-ai#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 (objectstack-ai#6931) and its own docblock declares: > `[objectstack-ai#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 objectstack-ai#4001 campaign count. And the precedent is already landed, in the second consumer the dispatch warned about: `reachabilityRootTypes()` in `scripts/build-schemas.ts` (objectstack-ai#17356, PR objectstack-ai#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." ⇒ objectstack-ai#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 (objectstack-ai#18301 / PR objectstack-ai#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 `objectstack-ai#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; - objectstack-ai#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 objectstack-ai#18353 / objectstack-ai#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 (objectstack-ai#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 objectstack-ai#6245 fallback, which resolves the identical schema instance. Harmless and load-bearing as documentation; folding it away is the objectstack-ai#3490 reassessment's business, not this card's. - `listUnregisteredKindSchemaTypes()`'s docblock still lists `theme` among "today's" entries; `theme` was retired at objectstack-ai#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>
Fixes #18301
Clause-②: no
Executes the C half of the #17356 ruling (batch #135 item 3, maintainer 「135 同意」). A is already landed (PR #18485 advanced the deletion-gate anchor); B and D were refused. This card adds a proof — it retires nothing, and it reverses nothing.
What was wrong
Check (c) of the
authorable-surface/deletion gate (packages/spec/scripts/build-schemas.ts) admitted a deleted baseline line on three proofs: an aged-out[RETIRED]tombstone, an unreachable def, or a def the build no longer emits.A key retired the strict-schema / guidance way — deleted from the shape outright, its prescription moved into the closed shape's
guidancetable — never carries the[RETIRED]mark, because there is nothing left in the shape to mark. Proof 1 therefore could not apply to it at any major: not "has not aged yet" but "has no clock". On a reachable def that left the whole class with no proof shape at all.The class was invisible until now because proof 2 was answering for these defs — the BFS root set omitted the four unregistered kinds, so whole families read as unreachable and every deletion under them was waived as over-collection. #18131 repaired the root set, and the repair is what exposes the gap.
The assumption that did not hold
The review swept for a def that could satisfy proof 4's conditions while silently STRIPPING the author's write, found none, and recorded the hole as latent. The dispatch asked for that to be re-measured. It was, with the gate's own instrument rather than by grep — a census pass over all 1525 emitted defs, running proof 4's declaration match and then asking each def what it does with the key. It is not latent:
additionalPropertieskeyByshared/RateLimitConfigfalseroot-graphsystem/ServerRateLimitConfigfalsederived-cloneServerRateLimitConfigSchemais declaredstrictObject({… guidance: { keyBy, store } }, RateLimitConfigSchema.shape)— built FROM the open schema's own shape object (packages/spec/src/system/stack-server.zod.ts,packages/spec/src/shared/http.zod.ts). So one declaration answers for two emitted defs, and every fact the first cut of proof 4 read says they are the same def. Two keys (keyBy,store) on a root-reachable def: had either baseline line been deleted, the shipped implementation would have waived it while an author who keeps writing the key has it silently dropped.That is the review's "strip-mode clone shares a strict shape" case in the spelling the tree actually holds — shape sharing in the other direction, which is why a sweep for
.strip(),z.object(X.shape)andstrictObjectError()found nothing. No wrong verdict has shipped: proof 4 is not onmain, and neither key is a pending deletion. What changes is that the fix is now mandatory rather than prophylactic, and the fixture below is a real specimen rather than a synthetic one.What this adds
Proof 4. A deleted baseline line is legitimate when, on a def that is emitted and reachable, all three of these hold in this build's own tree:
[RETIRED]— a guidance-route retirement deletes the key from the shape instead of leaving aretiredKey()in it, so it never earned the mark. This is a property of the class, not a guard bolted on, and it is what keeps proofs 1 and 4 disjoint.strictObjectdeclaration promises a prescription for the key — the def resolves to exactly oneStrictObjectDeclarationby shape identity, and that declaration'sguidancenames the exact key, or one of itsguidanceSetsenumerates it. This half says which text is owed.safeParseof that key against the schemazodByDefKeyholds raises anunrecognized_keysissue naming it, and that issue's message carries the declared text verbatim. This half is the door.Condition 3 replaces the condition the review failed. Nothing else in the gate moves.
Why the artifact read is gone rather than restated
The failed version proved "the door is closed" by reading
additionalProperties === falseoff the emitted JSON Schema. This repo had already measured that this does not distinguish a closed door from a silent strip and written it down:build-schemas.tsconverts with the defaultio: 'output', and in output mode zod emitsadditionalProperties: falsefor a.strip()object too — verified indocs/audits/2026-07-unknown-key-strictness-ledger.mdby regenerating both ways to a byte-identical artifact. A condition that answers the same for both cases cannot be the one that excludes one of them, so it is removed, and the docblock and the author-facing remedy now say so in the gate's own words.The subtler half, which the review named and which the census above confirms: shape identity is not a door test either.
strictObjectError()registers a declaration without closing the shape,.strip()andz.object(Strict.shape)clone a shape without its door, andstrictObject(opts, Open.shape)— the live case — puts a closed declaration and an open def on the same shape entries. The identity match stays, because it is how the owed text is found; it is no longer asked to prove closure.Why the probe reads
unrecognized_keys, and why it reads the messageunrecognized_keysis the only issue code aguidancetable is ever consulted from (strictUnknownKeyErrorreturns undefined for every other code), and the prescription is appended to that message verbatim, one bullet per key. So the issue's presence is exactly "this def refused the write", and the declared text appearing in its message is exactly "the error map this def parses through is the one holding that table" — which shape identity alone cannot tell, since a clone can share a shape without sharing a map. No message WORDING is pinned by this: the needle is read out of the tree, from the very declaration the structural half matched, so a rewritten prescription moves both sides together.The alternative the review offered — reading
catchallof typeneveroff the instance — was measured to give identical verdicts on all four shapes tried (strict,.strip()clone, plainz.object,catchall(z.string())). It was not chosen because it proves a spelling of the door rather than the delivery of the prescription, and it would still have admitted a strict clone built without the declaration's error map. The other alternative — recordingstrictObject()andstrictObjectError()distinctly in the registry — is apackages/spec/src/shared/strict-object.tsedit, outside this card's two files and across the clause-② path limb, and it would not have caught the live case above at all (both twins' declaration comes from the samestrictObjectcall).A third verdict, and what it deliberately does not say
A key a declaration names but the def does not answer for now gets its own violation line instead of the generic "was LIVE (never tombstoned)" — its
guidanceentry already exists, and what is missing is a door to deliver it through, so the generic verdict would send its reader to write something already written. That line states only that the prescription did not arrive, never why: on the shipped graph 7 of the 8 defs in that state are unions, where "the door is open" would be a guess this gate has not measured — the mistake the first cut made aboutadditionalProperties, one layer down.Two narrowings, both deliberate, both fail-closed
guidanceSetsRegExp does not count. Only an enumeratedkeyslist NAMES the key; a pattern claims a family whose members were never written down.Measured population — why this is a proof and not a blanket waiver
Census over the shipped graph, run with the gate's own code (tree
944d773b8;packages/spec/srcis byte-identical at the head this PR now carries,git diff --name-onlyover that path returns 0 lines):additionalProperties: falseOf the 9: 2 are the live case above; 7 are union defs the probe cannot drive to a single door, all of which the superseded artifact condition also excluded, so no verdict moves for them.
integration/DataSyncConfighas no route at all (its shape is a plainz.objectand nothing prescribes forschedule), so this proof cannot reach the 2026-09-10 ruling that withheld that tombstone.Evidence
The pins (
build-schemas-check-mode.test.ts)data/Metric:filtersdata/Metric:zzNotPrescribed18301integration/DataSyncConfig:scheduleapi/SessionResponse:zzOverCollected4650data/Object:compactLayout [RETIRED]system/ServerRateLimitConfig:keyByshared/RateLimitConfig:keyByThe last two are ONE run and ONE declaration, which is what makes them a discriminator rather than two assertions. The
beforeAllguard holds the tree fact they model in four loud halves: the two twins declare the same key SET, share every shape ENTRY by instance identity, the open twin ACCEPTSkeyByand the parsed output does not contain it, and the closed twin rejects it with a prescription bullet. If any half rots, the pin says so instead of going quietly green.Every negative assertion in the proof-4 cases was also corrected: they were written as
KEY — TOKENwhere the gate emitsKEY — def REACH; TOKEN, so they could not have matched even on an admitted key. They now carry thedef .*span and fail when they should.Ablations — both directions, on-disk proof, restored
Both legs prove the mutation reached disk before any colour is read, and both restores are proved by
git hash-objectagainst the HEAD blob plus a whole-treegit status --porcelain. Each script arms atrapon EXIT, INT and TERM that restores the file from HEAD, against an absolute path resolved fromgit rev-parse --show-toplevel.Ablation C — blind the door probe (
delivers()returnstrueunconditionally, which is the superseded implementation's behaviour for this def):322938f2to682ce658— the mutation is on disk.eager.statuscame back 0. With the door blinded the gate WAIVESshared/RateLimitConfig:keyByand the whole run exits green — the hole, executed, not argued. The other two check (c) has no proof shape for a guidance-route retirement on a reachable def — add a fourth proof soUNKNOWN_KEY_GUIDANCEretirements prove themselves (batch #135 item 3, C) #18301 cases stayed green, correctly: neither tests the door.322938f2, marker back to 0,git diff HEAD0 bytes,git status --porcelain0 lines.Ablation D — deafen the door probe (
delivers()returnsfalseunconditionally):322938f2to3096b1af.data/Metric:filtersandsystem/ServerRateLimitConfig:keyByboth printeda \strictObject` declaration NAMES …, but writing it`. So the probe is load-bearing for the admissions too; proof 4 is not the declaration match wearing a new name.322938f2, marker back to 0,git diff HEAD0 bytes,git status --porcelain0 lines.The previous round's ablations A and B were run against the superseded implementation (their anchor,
prescribed?.has(leaf), no longer exists) and are not carried forward as evidence for this head.scripts/ablation-dist-preflight.mjsstill reportsno dist/for this package and is NOT MEASURED, not red, for the same reason as the previous round: the test spawnstsxoverscripts/build-schemas.tsin a sandbox that SYMLINKS the realpackages/spec/src, so nothing here resolves throughdist/. The instrument that applies is the on-disk marker count plus the run's own colour, both recorded above.Runs
Long runs went through
scripts/pm/os-verify-lock.sh; exit codes were captured by redirect-then-$?, never through a pipe.origin/mainwas merged into this branch (79a046f8c) before this body was written, and every reading below is on the merged head.pnpm --filter @objectstack/spec run test:repoVERDICT command-exit 0— 31 files, 529 passedpnpm --filter @objectstack/spec typecheckVERDICT command-exit 0(tsc --noEmit+check:scripts-typecheck+check:test-typecheck)pnpm --filter @objectstack/spec run check:authorable-surfacepnpm lint(the repo-wideeslint . --no-inline-config)9e0324f80node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandspnpm lintis normally CI's to run; it completed here, so the reading is the whole population eslint's own config selects rather than a subset — no narrowing claim is being made and none needs checking.All 61 derived gates were run and reconciled with
--ran, each line carrying its exit code. 55 exit 0. Five exit 3 (PREREQUISITE NOT MET) and are NOT MEASURED — each needs a builtdist/, which this worktree has never had, and none can be moved by a diff confined topackages/spec/scripts/**:check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content,check:type-check-debt.pnpm check:pm-dispatch-gatesneeded 807s and was recorded asexit 124on a first pass whose 600s wrapper fired; it was re-run without the cap and exits 0. The record carries the real code, not the timeout.packages/lint/scripts/check-reference-carrier-shape.mjsis still present on this head and exits 0 — PR #18503, which retires it, had not landed when this list was derived. The list was re-derived here rather than inherited from the dispatch, exactly because of that.Scope and publishing
packages/spec/scripts/**matches none of the package'sfiles[]entries (dist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json), and it is not atsupentry — the onlyscripts/string inpackages/spec/tsup.config.tsis a repo-root import, against a lit control of 22src/occurrences. Nothing publishes, soClause-②: noandskip-changeset.The diff is the two files the card fenced and no others:
git diff --name-onlyagainst the merge base returns exactly those two. In particular the fix did not needpackages/spec/src/**— the dispatch's stop condition on that point does not fire.Acceptance notes
Out-of-scope observations, noted and deliberately not filed — none is a reproducible defect, a declared-contract breach, or a trap that makes an author write metadata the runtime rejects or silently drops:
additionalProperties: falseoff the emitted artifact. That was wrong, per the review and per this repo's own ledger, and the section above is what replaces it. Nothing about the registry is "worked around" now: closure is decided at the def, and the registry is asked only for the owed text.strictObject()andstrictObjectError()are indistinguishable instrictObjectDeclarations(), so the registry alone still cannot answer a door question. This proof no longer asks it one. Recording the two call shapes distinctly would let a future reader ask directly. Carrier: whoever next readsstrictObjectDeclarations()for a door question. (packages/spec/src/shared/strict-object.ts)additionalProperties: falseon the emitted artifact. Per the review, that counts artifacts whose TOP-LEVEL field is notfalse— unions, loose objects, pipes — and is not the 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 ledger's strip-site population, whichcheck-strictness-ledger.mtscounts by AST. Carrier: the strictness-ledger worklist, which already owns that surface.scripts/ablation-dist-preflight.mjsreportsno dist/as a refusal, which is correct for a dist-mediated ablation and reads as an accusation for one that resolves through source. Carrier: none today — the script's header already prescribes the property-read alternative by hand.There is one observation this round declined to file and flags for the reviewing seat rather than burying:
shared/RateLimitConfigis an openz.objectwhose shape is reused, closed, byServerRateLimitConfigSchema, and theguidanceentries forkeyBy/storetherefore prescribe to nobody on the open twin — an author writingkeyByon an API endpoint'srateLimithas it dropped in silence. That is #4001's own failure mode on a live authorable surface, and it sits inpackages/spec/src/**, outside this card's fence. It is a candidate class-(c) card for the triage seat, not a finding this PR may act on.Generated by Claude Code
Landing note (seat, 2026-09-17)
Contract review at
CONTRACT_REVIEW_TIERon head9e0324f807: PASS — record is comment5707796462. It supersedes the earlier FAIL (5706880661), which bound head121465ba16and does not bind this one.⭐ The re-review did not read this code, it ran it. With no
node_moduleson the box it materialisedzod@4.4.3andesbuildout of pnpm's content-addressed store,git archived this head'spackages/spec/src(archived tree hash verified equal togit rev-parse 9e0324f8:packages/spec/src), bundled, and executed this head's owncomputeGuidanceRoutes— verbatim,diff-checked — against 11 synthetic door shapes and a full 1525-def census, with the OLD head's function alongside as the control.The FAIL's one verdict-bearing item is closed, measured rather than argued:
Strict→ prescribed, butStrict.strip(),z.object(Strict.shape),Strict.loose(), an error-map object without.strict(), andz.object(Strict.shape).strict()without the map are all refused. Every one of those stripping forms also emitsadditionalProperties: false— which is the superseded condition's blindness demonstrated on the instance instead of quoted from the ledger. And the live twin executed both ways:shared/RateLimitConfig:keyByreadsprescribedthrough the OLD function (the hole, run) anddeclared-but-silentthrough this one.The row 「defs resolving to exactly one declaration that names an undeclared key: 258」 is mislabelled. That population measures 147; 258 counts defs resolving to exactly one declaration whether or not it names anything. Corrected here because this repo squashes and the body becomes the permanent commit message. A second figure, the docblock's 「7 of the 8 defs in that state are unions」, is also wrong (9 keys on 4 defs, 3 unions) but lives in code — both are carried by #18579 rather than fixed in-branch, because a third push would move the head and void the review described above.⚠️ Neither figure moves a verdict or describes a safeguard, and the rationale they support (unions dominate the not-delivered set) survives the corrected arithmetic.
Seat ruling on the process question the review referred here
The review declined to rule on whether a dev may read a stop instruction by its stated rationale, and named it the seat's. Ruling: the dev was right, and the dispatch order was at fault.
That order said 「if a live member exists, STOP AND REPORT — on the reading that it would mean a wrong verdict is shipping」. That bundles a trigger with a rationale. The dev measured the trigger TRUE, then measured the rationale FALSE (proof 4 is not on
main; aguidance-only key is never in the shape, so it was never a baseline line and no deletion could ever put it to proof 4 —keyBy/store0 in the baseline against a lit control of 1 forenabled), and disclosed both rather than quietly proceeding. Stopping there would have parked a proven-wrong proof in a draft and delayed a fix that had to land before this PR anyway.⛔ This is not a general licence to reason past a fence. The correction belongs on the seat's side: a stop condition must be written as a condition, with its rationale separate and non-operative. The general rule stands — where a dev cannot measure the rationale false, the trigger governs and it stops.
Out of scope, correctly handed over rather than acted on: the live trap the round found —
shared/RateLimitConfigis an openz.objectwhose shape is reused closed byServerRateLimitConfig, so an authoredkeyByis dropped in silence — is filed as #18578. It lives inpackages/spec/src/**, outside this card's fence, and ⛔ was not folded in.Pre-landing checks: ① review PASS on record ✅ · ②
--pair 18529exit 0; ⛔ no carriers hung (Clause-②: no, verified a true declaration against both limbs) ✅ · ③ re-taken at landing time ✅. Governed-surface predicate: 0 of 2 paths hit the register ⇒ ordinary queue landing.Generated by Claude Code