Repository navigation
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>
`needsRecordRead` — C6's population gate, and the sweep's read budget — owed a record only in the completed state (a cleared `Clause-②: yes`). Under the lane rule the spec and skills lanes owe the contract review on EVERY round they deliver, `no` included, and a `no` round hangs no carrier to mark its review pending: PRs #18530 / #18529 (spec-lane `no` rounds) read 0 here with no record on either head. The population now adds the `no` rounds of the two lanes, read off the card's `domain:*` labels (`LANES_OWING_REVIEW`, `laneOwesReview`; unreadable labels stay UNJUDGED). A `no` elsewhere still owes nothing. A cleared `yes` outside the two lanes keeps its row and its exit — the `yes` is a limb hit and limb-hit work is owed — but its remedy is lane routing (re-lane to spec, or correct a false `yes`), never a default-tier self-review and never an at-tier subagent from that lane. The C6-RECORD note prescribes the clear-citation only where a clear exists. New floored battery, 34 cases; roster floor 32 → 33. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
… beside its population (objectstack-ai#18863) Fixes objectstack-ai#18579 Clause-②: no Two census figures in the proof-4 prose of `packages/spec/scripts/build-schemas.ts` label a population they do not measure. Both are corrected, and both now carry their population — and the tree they were read on — in the same sentence, which is what the triage note on the card asked of whoever took it. ## The two figures **(1) The docblock's not-delivered rationale.** It said *"on the shipped graph 7 of the 8 defs in that state are unions"*. Measured: that state holds **9 keys on 4 defs**, and **3 of those 4 defs** are unions, carrying **7 of the 9 keys**. The 7 was the KEY count wearing the DEF label. **(2) The closing line of the same docblock**, which sent its reader to PR objectstack-ai#18529's body *"for the census run"*. That body's census row labels *"defs resolving to exactly one declaration that names an undeclared key"* with **258** — but 258 is the count of defs resolving to exactly one declaration **whether or not it names anything**. The labelled population measures **147**. That row lives in a merged PR body and is not editable from here, so the docblock now records the census in the tree and tells its reader not to re-derive it from that body. The argument both figures support is unchanged — unions do dominate the not-delivered set (3 of 4 defs, 7 of 9 keys). Only the arithmetic moved. ## Re-measured on this head, not copied from the card The card's numbers were taken on branch `9e0324f807` and `packages/spec/src` has moved on `main` since, so nothing was copied. The whole census was re-derived with **this head's own verbatim `computeGuidanceRoutes`**: a byte-identical copy of `scripts/build-schemas.ts` (prefix proven equal by `git hash-object`: `0803422391…` on both) with a census block appended, run as the real generator so `zodByDefKey`, `generatedSchemas` and the declaration registry are the ones the gate itself sees. The copy was deleted before the first commit; it is in no diff. **Tree measured: `objectstack-ai/objectstack` at `88aa326deb`** — the merge base of this branch with `origin/main`. | population | count | |---|---| | emitted defs (`zodByDefKey`, = bundled `$defs`) | 1527 | | of those, emitted artifact carries `additionalProperties: false` | 1117 | | of those 1527, defs resolving to exactly one declaration (naming anything or not) | 258 | | of those 258, defs whose declaration NAMES a key the def does not declare | **147** | | keys those 147 defs are promised | 779 | | of those 779, keys the def delivers (`prescribed`) | 770 | | of those 779, keys not delivered (`declared-but-silent`) | **9** | | of those 9 keys, keys on `shared/RateLimitConfig` | 2 | | defs carrying a not-delivered key | **4** | | of those 4 defs, unions | **3** (`ui/ChartGroupBy`, `ui/ViewItem`, `ui/RecordHighlightsField`) | | of the 9 not-delivered keys, keys on those 3 union defs | **7** | | defs with an empty shape (excluded outright) | 6 | | declarations carrying an empty shape (what each of those 6 would answer to) | 9 | | defs whose match is ambiguous | 0 | | defs with no derivable shape | 304 | **What validates the instrument is the rest matching, not the conclusion agreeing.** Reproduced exactly against the card: 1117 · 258 · 147 · 779 · 770 · 9 · 2 · 6 · 9 · 0 ambiguous · 3-of-4 · 7-of-9. Drifted with `src`, as the card predicted: 1525 → **1527** emitted defs, 303 → **304** no-shape, 408 → **410** not-`additionalProperties`-false. Independent corroboration of the 1527/1538 pair from the generator's own summary line: `check:authorable-surface` prints `bundled schema: objectstack.json (1527 definitions)` / `Successfully generated 1538 schemas`. **Cross-check tying the partition to the gate's own function:** every one of the 779 promised (def, key) pairs the census derived was fed back through the head's verbatim `verdictFor` — **0** came back `none`. A replication that invented a pair the real lookup does not hold would show up there. ## Sequencing with PR objectstack-ai#18861 PR objectstack-ai#18861 (card objectstack-ai#18578) repairs the `shared/RateLimitConfig` open-twin defect and therefore moves this exact partition (its author measures delivered 770 → 772, not delivered 9 → 7). It is **absent from the tree measured here**, proven twice: `git merge-base --is-ancestor 36adeca HEAD` exits 1 on a non-shallow checkout with a lit control leg (`88aa326deb` → exit 0), and the census itself still lists `shared/RateLimitConfig:keyBy` and `:store` as not-delivered, which is precisely the defect that PR removes. So both replacement sentences are written as readings of a **named commit** (`at 88aa326 that state held …`), not as claims about whatever `main` holds today. They stay true when objectstack-ai#18861 lands; they become stale, visibly, with the commit that says so right there. The docblock also now tells its next reader that which defs sit in that state is a fact about the graph rather than a property of the proof — "closing an open door moves it" — and to re-measure rather than re-date. Nothing here waits on or depends on that branch. ## Not taken: the `delivers()` fixture The card's optional fourth item — a fixture for the `message`-includes leg, the one refusing a strict clone built without the declaration's error map — is **handed back, with a reason rather than a shrug**. Three routes exist and each is blocked by a rule this repo states out loud: 1. **A synthetic def on the shipped graph.** `packages/spec/src` is published (`files[]` carries `src/**/*.zod.ts`), so this ships a fake schema to consumers. 2. **A synthetic def in the test sandbox.** `build-schemas-check-mode.test.ts` builds each sandbox with `fs.symlinkSync` for `src` — its own comment says *"`src/` is the fixture's own, so the population a run observes is the repo's"*. A fixture def would have to be written into the real `src`, i.e. route 1. Copying `src` per sandbox instead is a structural change to a 4466-line harness. 3. **Exporting the leg for a unit test.** `delivers()` is a closure over module-level state, and that file's header rule is *no test-only seam*. So it is not a fixture-sized job; it is a restructuring, and the card is explicit that this PR should not become two things. Recommendation for whoever files it: route 2, as its own card, because it also unlocks fixtures for the other fail-closed legs. The census zero that defends the leg today is re-stated above with its tree, so the next reader can see what it rests on. ## Verification - `pnpm --filter @objectstack/spec build` — green (runs `gen:schema`, i.e. the edited generator, end to end) - `pnpm --filter @objectstack/spec typecheck` — green (all three legs: `tsc --noEmit`, `check:scripts-typecheck` which compiles the edited file, `check:test-typecheck`) - `pnpm --filter @objectstack/spec test` — 487 files / **14055 tests** passed - `pnpm --filter @objectstack/spec exec vitest run --project repo scripts/build-schemas-check-mode.test.ts` — 1 file / **85 tests** passed. This is the suite that spawns the edited generator, and it is **not** in the package's `pnpm test`: that script runs `--project local` only, and vitest's filter guard says so out loud when you aim the wrong project at the file. - `scripts/file-description.test.ts` + `scripts/def-key-collisions.test.ts` (`--project repo`) — 128 passed; these are the only other tests that read this script by name. - **53 gate families** derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — 51 green. Two exit **3**, which is that gate's own NOT-MEASURED code and neither a pass nor a failure: `check:dual-build-cjs-loads` and `check:lean-entry-closure` both need a whole-repo `pnpm build` that this worktree does not carry. Neither can move on a comment in an unpublished script; CI builds fresh. - `pnpm lint` equivalent run whole, not narrowed: `npx eslint . --no-inline-config --format json` at `8f9a42d8a2` — **6846 files, 0 errors, 0 warnings**. - Control-byte self-scan over the edited file (`grep -naP` over the C0 range plus DEL) — no match; `pnpm check:nul-bytes` green. ## No changeset (`skip-changeset`) Nothing published moves. Measured rather than asserted: `@objectstack/spec`'s `files[]` is `dist · json-schema · liveness · prompts · llms.txt · README.md · src/**/*.zod.ts · CHANGELOG.md · api-surface · spec-changes.json` — **no `scripts/` entry** — and grepping every shipped path for `computeGuidanceRoutes` returns **0 hits**, against a lit positive control (`keySetMatches`, which is found in `dist/shared/index.js` and four more). The diff is comment-only inside that unpublished file, so the blast radius is the gate's next reader, not an author and not a consumer. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…and skills owe it on every round, other lanes owe none (objectstack-ai#18903) Fixes objectstack-ai#18536 Clause-②: no ## What this lands The maintainer's lane rule, restated by the director record on the card (comment 5717169811), quoted verbatim and untranslated: > 「曾经要求只有 spec 和 skills 需要 fable,其他 opus 就够了,理论上其他车道不需要契约复审」「即使项目经理跑在 opus 上,也可以支持起 fable 子 agent 复审,这样最快。除非子 agent 用 fable 启动失败,也就是没有 fable」 carried into the governed text and the two machine readers: 1. **Spec and skills lanes** — every delivered round gets the contract review at `CONTRACT_REVIEW_TIER`: in-seat when the seat's served tier is that tier, otherwise by the at-tier review subagent the seat spawns (the 09-17 route stays). The record is the same-shape comment with `Served-tier:`, on the PR or the card. 2. **Every other lane** — no contract review. The clause-② limbs (path `packages/spec/src/**`, declaration `Clause-②: yes`) survive unchanged and now answer the LANE question: a limb hit is spec-lane work and moves there; a `Clause-②: no` PR outside the contract surface lands on the three pre-checks and the gates. No default-tier self-review record is demanded, and no other lane spawns the at-tier subagent. 3. **Tier unavailable** (the at-tier subagent cannot start) — the review cannot be produced: the PR stays draft and out of the queue, and the maintainer's own review is the only bypass, by their word each time. The lane key is the 2026-09-10 key restored (PR objectstack-ai#17294) with the 2026-09-17 subagent route kept (PR objectstack-ai#18511); the 2026-09-16 tier key (PR objectstack-ai#18363) is superseded. Not a byte-revert: both spec AND skills owe the review, and 「余席默认档自审加门禁」 is gone — other lanes owe nothing. ## Charter (commit 1) — net 0 lines per file | file | before → after | ratchet ceiling | |:--|:--|:--| | `.claude/skills/pm-dispatch/SKILL.md` | 812 → 812 | 812 | | `references/contract-review.md` | 60 → 60 | 60 | | `references/core-rules.md` | 151 → 151 | 151 | Every edited line is at most 120 bytes; `check:pm-skill-ratchet` is green on `bc0c2ec41`. Rule text carries no issue numbers (`check:pm-skill-id-lint` green); the four-axis frame block is untouched (`check:skill-frame-sync` / `-freshness` green). **SKILL.md** — five in-place rewrites, one added line, one retired line: - :512 强制条款② — 「达档复核归派发席席内」 → 「命中即 spec 车道的活」. - :522 席位档策略 — 「按实测档:达档席内审;未达档席 ⛔ 不自审,起隔离达档子代理转录核档采信」 → 「按车道:spec 与 skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理」 (the transcript-verified tier of the subagent stays stated in contract-review.md :54). - :636 入队闸门 — 「无席内条款②复核 PASS 在案」 → 「无达档条款②复核 PASS 在案」; the limbs at :637 / :638 are untouched. - :639 交付后复核 — 「归派发席:达档席内审,未达档循保险丝起子代理;记录 = 同形评论落 PR 或卡」 → 「只 spec 与 skills 车道欠,每轮达档:席内审或起子代理;双肢命中即 spec 车道」 (the record's shape lives in contract-review.md :27–:29). - :640 ADDED (rule 3, where the enqueue gate lives) — 「子代理起不来 ⇒ 复核缺席,PR 留 draft 队列外等档;唯一旁路是维护者亲审,逐次为准。」 - :646 pointer — 「席内复核的适用面」 → 「契约复核的适用面」. - RETIRED, paying for :640 — the former :672 「报告席记条款②默认档 FAIL 率入复审清单;超改制前达档史值 ⇒ 决策卡交维护者定回退。」 Reason: it meters the FAIL rate of DEFAULT-TIER clause-② reviews, and under rules 1–2 no lane performs one (spec and skills review at tier; other lanes review nothing), so the metric's population is empty. It sits outside the claim's declared SKILL.md bands; it is the one tier-keyed line in the file the ruling empties, and paying in-file required it. Re-wrap is not currency: no line was re-flowed. **contract-review.md** — thirteen in-place rewrites: - Title and :23 — 「(席内)」 → 「(按车道)」. - :9, :11, :12, :14 — 「席内契约复核 / 席内复核」 → 「达档契约复核 / 达档复核」: the review is at tier wherever it happens, and "in-seat" was the 09-16 key. - :25 — 「交付后收集复核当轮席内完成;借复核不移卡,新 spec 工作恒归 spec 席」 → 「交付后收集复核当轮完成;只 spec 与 skills 车道欠,新 spec 工作恒归 spec 席」. The "borrow the isolated review" route is what rule 2 closes for other lanes. - :26 — 「按档位:达档席内审契约增量;未达档 ⛔ 不自审,走保险丝路;豁免仅独立性件与保险丝」 → 「按车道:spec 与 skills 席审契约增量;达档席内审,未达档 ⛔ 不自审,起达档子代理」. - :27 — 「达档与默认档同形」 → 「席内与子代理同形」: there is no default-tier record any more; one shape holds between in-seat and subagent (the two scripts' quotations of this line are updated in the same PR). - :32 — the independence case → 「独立性件(契约真分叉、dev 挂旗)与保险丝只免席内审,不免复核:起隔离达档子代理」. This is the 「豁免仅独立性件与保险丝」 rewrite the ruling asked for: the independence case and the fuse exempt the seat from IN-SEAT review only, never from the review's existence. The former third trigger 「派发后的跨车道面(含 spec)」 is lane routing now (:25). - :40 landing pre-check ① — 「席内条款②复核 PASS 在案 …(档位按实测)」 → 「达档条款②复核 PASS 在案 …(spec、skills 每轮)」. - :50 the fuse — 「管每个出条款②裁决的席 … 未达档 ⛔ 不自审」 → 「只管 spec 与 skills 席的条款②复核」 (the 不自审 clause stays at :26). - :52 — 「至席内复核完成」 → 「至达档复核完成」. - :53 — 「标签原样留置,队列外等待是安全态」 → 「起不来即无复核,标签原样、队列外等档」 (rule 3 on the fuse side). **core-rules.md** — one rewrite: :112 「契约卡达档复核归派发席」 → 「契约复核只 spec、skills 欠」. Trace by REST `GET /pulls/N/files`: PR objectstack-ai#18363 (09-16) touched SKILL.md, contract-review.md and platform-readings.md only, so the 09-16 re-key never landed in core-rules.md; :112's tier key came with PR objectstack-ai#18511 (09-17), replacing PR objectstack-ai#17294's 「归 spec 席」. :122 「`references/` 席内达档复核后入队」 is left as the mirror of SKILL.md :625 (see acceptance notes). ## Machine side **commit 2 — `scripts/pm/dispatch-gates.mjs`.** The clause-② note and the suspect tail that `--tier` prints (quoted into claim comments) said 「spec seat; default-tier build」 and 「in the spec seat」 — the 09-10 seat key, which PR objectstack-ai#18363 never re-keyed. They now name the spec and skills lanes, the in-seat-or-subagent route, and 「a hit outside those lanes is spec-lane work and moves there」; the docblock above `MANDATORY_TIER_GLOBS` carries the rule. Four self-test pins hold both renderings to the lane key and refuse the two retired spellings. `--self-test`: 1852 cases pass (baseline on `0b31d90`: 1848). **commit 3 — `scripts/pm/check-clause2-carriers.mjs`.** `needsRecordRead` — C6's population gate and the sweep's read budget — owed a record only in the completed state (a cleared `yes`), so a spec-lane `Clause-②: no` round read 0 with no record on its head: exactly PRs objectstack-ai#18530 / objectstack-ai#18529, the card's measured pair. Now: - `LANES_OWING_REVIEW` (`domain:spec`, `domain:skills`, frozen) and `laneOwesReview(pair)`, read off the CARD's `domain:*` labels; unreadable labels stay UNJUDGED through the existing labels gap. - Population: the completed state (unchanged) OR a declared `no` on a spec/skills card (new). A `no` anywhere else still owes nothing — pinned. - C6's row on a spec/skills `no` round with no record: exit 4; remedy = the lane's review at tier (in-seat or by the at-tier subagent), with the unavailable-tier state and its one bypass named. - A cleared `yes` OUTSIDE the two lanes keeps its row and its exit 4 (the `yes` is a limb hit, and limb-hit work is owed), but its remedy is lane ROUTING: re-lane the item to `domain:spec` (`pm:retriage`, or split the contract work to a spec-lane card or PR per 「新 `packages/spec` 工作恒由 `domain:spec` 席收口」), or correct a false `yes` with a `Clause-②-correction:` comment — never a default-tier self-review, never an at-tier subagent from that lane. - The C6-RECORD note prescribes the clear-citation only where a clear exists; on a spec/skills `no` round with its record it says the lane owes the record and it exists. - New floored battery, 34 cases (floor 30); roster floor 32 → 33. `--self-test`: 977 cases pass (baseline 941). Deviation from the dispatch's suggested case (c) — a `yes` pair on a `domain:cli` card as 「not owed as a record, plus a note」 at exit 0 — implemented instead as owed at exit 4 with the routing remedy. Reasons: contract-review.md :42 promises 「0 = … head 上有记录」; a cli seat clearing a `yes` pair and landing it from that lane is never a legal workflow under rule 2 (the work 「moves there」), so an exit-0 note would be the 0-with-a-message the file's own header bans; and the row's exit is unchanged from today (only the remedy text moves), so no legal workflow is re-blocked. The four-axis reading is in the report's `open_questions`; the PR is draft for the maintainer's word either way. ## Gates (run on `bc0c2ec41`, the final commit) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 46 families. 45 ran with exit 0 (each captured redirect-then-`$?`), among them `check:pm-skill-ratchet`, `check:pm-skill-id-lint`, `check:skill-frame-sync`, `check:pm-governed-prose`, `check:pm-clause2-carriers`, `check:declared-population-live`, `check:pm-half-states`, `check:nul-bytes`. `check:doc-formula-expressions` first exited 3 — PREREQUISITE NOT MET (`@objectstack/formula` / `@objectstack/lint` not built; nothing measured) — and reruns green after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock (22 + 9 + 14 examples judged clean). `check:pm-dispatch-gates` (430–450 s) was still running detached when this body was written; its verdict and the `--ran` reconciliation are in the `os-dev-report` comment on objectstack-ai#18536. Both edited scripts' own `--self-test` pass (above). The four package tests that mention `dispatch-gates.mjs` do so in docblocks only and read nothing from it, so they are not owed. The derivation printed a STALE TREE warning (the tree is behind an `origin/main` that moved during the run); the PM's re-derivation after the report reads the true list. `skip-changeset`: nothing published moves — `.claude/**` and `scripts/pm/**` ship in no package's `files[]`. ## Acceptance notes Lines outside this card's declared file surface that carry seat or tier wording — reported, not edited (承接者: the skills seat's next SKILL.md round; the hot-file queue behind this card holds objectstack-ai#18743 · objectstack-ai#18755 · objectstack-ai#18665 · objectstack-ai#18469 PR-B · objectstack-ai#18489): - SKILL.md :231 「派发后发现的跨车道面(含 spec)不移卡,认领席借契约复审档隔离复核」 and core-rules.md :62 (its mirror). The "borrow the isolated at-tier review" route is what rule 2 closes for other lanes (no other lane spawns the at-tier subagent). The ruling's 「moves there」 and SKILL.md :234 「已派发卡 ⛔ 不因触 spec 转席」 reconcile as "the contract WORK moves to the spec lane; the card is not re-seated", which is how contract-review.md :25 now reads. Suggested rewrite of :231, same byte budget: 「认领即跟到 MERGED:派发后发现的跨车道面(含 spec)不移卡,契约面工作交 spec 车道达档复核。」 - SKILL.md :608 「技能面 hunk 须由契约复审档的席复核,档外席先交 skills 席」, :623 「由本席按达档自审」, :625 「经席内达档复核后 ready → 入队」 and core-rules.md :122 「`references/` 席内达档复核后入队」: consistent with rule 1 read as "the skills lane's review at tier, in-seat or by subagent"; the word 席内 there is the 09-16 spelling and could be read as in-seat-only. - `references/lanes/spec.md` :19–:33 and `references/lanes/skills.md`: no contradiction found; `lanes/skills.md` :13 「契约复核归派发席席内」 is ownership wording (the seat owns the record it adopts) and stays true. - The follow-up the ruling assigns to the lane seats after this lands, not to this card: re-read the `needs:contract-review` carriers hung outside the spec/skills lanes (the director's ledger ⑫) and re-lane or strip each. - Sweep-mode cost: `check-clause2-carriers.mjs` without `--pair` now buys the PR thread for every spec/skills-lane `no` pair too (one read per PR, cached per PR); the budget paragraph in the file's header is unchanged in shape. ## 维护者速读(草稿) **改了什么**:把契约复核的归属重新按「车道」写回章程:只有 spec 与 skills 两条车道的每一轮交付都要过契约复审档的复核——席位达档就席内审,不达档就起达档子代理;其余车道零契约复核,条款②命中即 spec 车道的活;达档子代理起不来时 PR 留 draft、队列外等档,唯一旁路是您亲审。三份章程文本行数不变(812 / 60 / 151),两个机读脚本(`--tier` 的提示行、`--pair` 的复核记录人口)同步改成按车道判。 **为什么改**:9-16 那次把「只在 spec 席」改成了「按席位实测档」,席位名单没了,于是同一张 `Clause-②: no` 的 spec 车道 PR 在两条细则下答案相反(objectstack-ai#18536 的两种读法),而账号级 429 让「等档位」在两种读法里含义完全不同。您 9-17 的裁决把车道规则说回来了,本 PR 只是把它落到文本和脚本上。 **风险与代价(含回滚)**:`--pair` 从此对 spec/skills 车道的 `no` 轮也要求 head 上有复核记录,没有就退 4——这正是 objectstack-ai#18530 / objectstack-ai#18529 该有的读数,但意味着这两条车道的 `no` 轮在记录落下前都不能入队;其它车道不受影响,`yes` 挂在别的车道上的旧读数(退 4)不变,只是补救措施从「自审」改成「改道 spec」。回滚 = revert 这三个 commit,文本与脚本一起回到 `0b31d90`。 **席位意见**:(留空) **你要做的**:确认本 PR 是否如实落了您的车道规则,是则合并;合并后各车道席按裁决去清理挂在 spec/skills 之外的 `needs:contract-review` 载体。 --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ --------- 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