Skip to content

feat(spec): check (c) proves a guidance-route retirement on a reachable def (#18301) - #18529

Merged
os-litant merged 8 commits into
mainfrom
claude/issue-18301-check-c-fourth-proof
Sep 17, 2026
Merged

os-litant merged 8 commits into
mainfrom
claude/issue-18301-check-c-fourth-proof

Conversation

@os-warren

@os-warren os-warren commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

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.

Patch round. The contract review of record (comment 5706880661, served CONTRACT_REVIEW_TIER) returned FAIL on ① item 3: the proof's stated "the door is closed" condition described something the gate did not compute. This round replaces that condition, corrects every place the claim was made, and pins the case that was unpinned. Re-measuring the review's own sweep falsified its latency finding — see "The assumption that did not hold" below.

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 guidance table — 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:

def artifact additionalProperties matches one declaration by shape identity reachable writing keyBy
shared/RateLimitConfig false yes root-graph parse SUCCEEDS, key dropped
system/ServerRateLimitConfig false yes (the SAME declaration) derived-clone refused, with the prescription

ServerRateLimitConfigSchema is declared strictObject({… 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) and strictObjectError() found nothing. No wrong verdict has shipped: proof 4 is not on main, 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:

  1. the baseline entry was not [RETIRED] — a guidance-route retirement deletes the key from the shape instead of leaving a retiredKey() 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.
  2. a strictObject declaration promises a prescription for the key — the def resolves to exactly one StrictObjectDeclaration by shape identity, and that declaration's guidance names the exact key, or one of its guidanceSets enumerates it. This half says which text is owed.
  3. the def keeps that promise — safeParse of that key against the schema zodByDefKey holds raises an unrecognized_keys issue 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 === false off 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.ts converts with the default io: 'output', and in output mode zod emits additionalProperties: false for a .strip() object too — verified in docs/audits/2026-07-unknown-key-strictness-ledger.md by 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() and z.object(Strict.shape) clone a shape without its door, and strictObject(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 message

unrecognized_keys is the only issue code a guidance table is ever consulted from (strictUnknownKeyError returns 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 catchall of type never off the instance — was measured to give identical verdicts on all four shapes tried (strict, .strip() clone, plain z.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 — recording strictObject() and strictObjectError() distinctly in the registry — is a packages/spec/src/shared/strict-object.ts edit, 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 same strictObject call).

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 guidance entry 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 about additionalProperties, one layer down.

Two narrowings, both deliberate, both fail-closed

  • Exactly one matching declaration. An empty shape is excluded outright — it matches every other empty shape. Where two declarations still answer, the lookup returns "no evidence" rather than unioning them.
  • A guidanceSets RegExp does not count. Only an enumerated keys list 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/src is byte-identical at the head this PR now carries, git diff --name-only over that path returns 0 lines):

reading value
emitted defs 1525
defs whose emitted artifact carries additionalProperties: false 1117
defs resolving to exactly one declaration that names an undeclared key 258
keys those declarations promise 779
keys the def actually delivers — what proof 4 admits 770
keys promised and NOT delivered — what proof 4 refuses 9

Of 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/DataSyncConfig has no route at all (its shape is a plain z.object and nothing prescribes for schedule), so this proof cannot reach the 2026-09-10 ruling that withheld that tombstone.

Evidence

The pins (build-schemas-check-mode.test.ts)

fixture expected what it would catch
data/Metric:filters admitted by proof 4, explicitly not proof 2 a proof that never fires
data/Metric:zzNotPrescribed18301 still refused, and NOT with the third verdict a waiver keyed off the DEF instead of the KEY
integration/DataSyncConfig:schedule still refused a silent reversal of the 2026-09-10 ruling
api/SessionResponse:zzOverCollected4650 still waived by proof 2, in proof 2's words proof 4 written as a widening of proof 2
data/Object:compactLayout [RETIRED] falls to the tombstone chain, not proof 4 the disjointness — it satisfies every other condition proof 4 tests
system/ServerRateLimitConfig:keyBy admitted by proof 4 — the lit half of the new pair
shared/RateLimitConfig:keyBy REFUSED, with the third verdict, and not waived by proof 2 either the review's finding: one declaration, two defs, and a gate that reads the registry instead of the door admits the open one

The last two are ONE run and ONE declaration, which is what makes them a discriminator rather than two assertions. The beforeAll guard 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 ACCEPTS keyBy and 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 — TOKEN where the gate emits KEY — def REACH; TOKEN, so they could not have matched even on an admitted key. They now carry the def .* 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-object against the HEAD blob plus a whole-tree git status --porcelain. Each script arms a trap on EXIT, INT and TERM that restores the file from HEAD, against an absolute path resolved from git rev-parse --show-toplevel.

Ablation C — blind the door probe (delivers() returns true unconditionally, which is the superseded implementation's behaviour for this def):

Ablation D — deafen the door probe (delivers() returns false unconditionally):

  • marker occurrences 0 to 1, blob 322938f2 to 3096b1af.
  • run RED, 2 cases: both positive legs fall to the third verdict — data/Metric:filters and system/ServerRateLimitConfig:keyBy both printed a \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.
  • restored: blob back to 322938f2, marker back to 0, git diff HEAD 0 bytes, git status --porcelain 0 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.mjs still reports no dist/ for this package and is NOT MEASURED, not red, for the same reason as the previous round: the test spawns tsx over scripts/build-schemas.ts in a sandbox that SYMLINKS the real packages/spec/src, so nothing here resolves through dist/. 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/main was merged into this branch (79a046f8c) before this body was written, and every reading below is on the merged head.

command verdict
pnpm --filter @objectstack/spec run test:repo VERDICT command-exit 0 — 31 files, 529 passed
pnpm --filter @objectstack/spec typecheck VERDICT command-exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck)
pnpm --filter @objectstack/spec run check:authorable-surface exit 0
pnpm lint (the repo-wide eslint . --no-inline-config) exit 0 — the FULL run, not a narrowing, at 9e0324f80
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands 61 families derived ON THIS HEAD, not inherited

pnpm lint is 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 built dist/, which this worktree has never had, and none can be moved by a diff confined to packages/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-gates needed 807s and was recorded as exit 124 on 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.mjs is 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's files[] entries (dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json), and it is not a tsup entry — the only scripts/ string in packages/spec/tsup.config.ts is a repo-root import, against a lit control of 22 src/ occurrences. Nothing publishes, so Clause-②: no and skip-changeset.

The diff is the two files the card fenced and no others: git diff --name-only against the merge base returns exactly those two. In particular the fix did not need packages/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:

  • Superseded. The previous round's note here claimed proof 4 "works around" the registry's door-blindness by reading additionalProperties: false off 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() and strictObjectError() are indistinguishable in strictObjectDeclarations(), 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 reads strictObjectDeclarations() for a door question. (packages/spec/src/shared/strict-object.ts)
  • 408 of the 1525 emitted defs do not carry additionalProperties: false on the emitted artifact. Per the review, that counts artifacts whose TOP-LEVEL field is not false — unions, loose objects, pipes — and is not the 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 ledger's strip-site population, which check-strictness-ledger.mts counts by AST. Carrier: the strictness-ledger worklist, which already owns that surface.
  • scripts/ablation-dist-preflight.mjs reports no 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/RateLimitConfig is an open z.object whose shape is reused, closed, by ServerRateLimitConfigSchema, and the guidance entries for keyBy / store therefore prescribe to nobody on the open twin — an author writing keyBy on an API endpoint's rateLimit has it dropped in silence. That is #4001's own failure mode on a live authorable surface, and it sits in packages/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_TIER on head 9e0324f807: PASS — record is comment 5707796462. It supersedes the earlier FAIL (5706880661), which bound head 121465ba16 and does not bind this one.

⭐ The re-review did not read this code, it ran it. With no node_modules on the box it materialised zod@4.4.3 and esbuild out of pnpm's content-addressed store, git archived this head's packages/spec/src (archived tree hash verified equal to git rev-parse 9e0324f8:packages/spec/src), bundled, and executed this head's own computeGuidanceRoutes — 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, but Strict.strip(), z.object(Strict.shape), Strict.loose(), an error-map object without .strict(), and z.object(Strict.shape).strict() without the map are all refused. Every one of those stripping forms also emits additionalProperties: 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:keyBy reads prescribed through the OLD function (the hole, run) and declared-but-silent through this one.

⚠️ Correction to this body

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; a guidance-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/store 0 in the baseline against a lit control of 1 for enabled), 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/RateLimitConfig is an open z.object whose shape is reused closed by ServerRateLimitConfig, so an authored keyBy is dropped in silence — is filed as #18578. It lives in packages/spec/src/**, outside this card's fence, and ⛔ was not folded in.

Pre-landing checks: ① review PASS on record ✅ · ② --pair 18529 exit 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

…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>
@os-warren os-warren added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 16, 2026 — with Claude
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 298e9dde15e4e7c3b340323215e084fd60107cc3 → packageMentionDocs.

…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>

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 121465ba1681b5369725d56b172406d9629594b1

① Derived judgments

  1. Gate accept set — NEW ADMISSION (proof 4). A deleted authorable-surface/ baseline line D:k is now admitted when, at head: D is emitted; D is reachable (proof 2 did not fire); the base entry was NOT [RETIRED]; the emitted JSON Schema for D has additionalProperties === false; zodShapeOf(D) is non-empty and matches exactly one strictObjectDeclarations() entry by sorted key set plus per-entry instance identity; and k is in that declaration's guidance map, or in an enumerated (non-RegExp) guidanceSets.keys, and k is not in the shape. Before this PR every such line was the violation "the entry at baseRev was LIVE (never tombstoned)". Verified at head l.1390-1440 and l.2438-2461. Characterisation: correct as to the enumerated conditions, with one exception in item 3.

  2. Gate accept set — UNCHANGED. Proof 1 (the wasRetired branch, RETIRED_KEYS_BY_MAJOR dating, aging), proof 2 (unreachable), proof 3 (vanished def via the manifest gate), checks (a)/(b), the defaults ratchet and the anchor writer are byte-untouched outside the hunks listed above. Proof 4 lives only under !wasRetired, so a marked tombstone cannot be shortened by a guidance line — pinned by the data/Object:compactLayout [RETIRED] fixture. Refusals pinned in one spawned run: same-def unnamed key, integration/DataSyncConfig:schedule (letter B), unreachable key still waived in proof 2's words. Characterisation: correct.

  3. Mis-declared: the "door is closed" condition. The PR (docblock l.1345-1352, body, and out-of-scope note 1) states that additionalProperties: false on the emitted artifact proves the runtime door is closed and that "on an OPEN shape the author's write is stripped in silence" is thereby excluded. The repo's own ledger (docs/audits/2026-07-unknown-key-strictness-ledger.md:1535-1541) records, measured, that under the generator's io: 'output' projection a strip-mode z.object() ALSO emits additionalProperties: false. So condition (b) does not distinguish a closed door from a silent strip; what actually excludes open shapes today is condition (c)'s identity match against a declaration that strictObject() built (which always .strict()s). That mechanism is unstated, and it has a hole the docblock promises is closed: a strip-mode clone sharing the exact shape object (Strict.strip(), z.object(Strict.shape)), or an exported open shape registered through strictObjectError(), would pass both (b) and (c) and be admitted while the author's write is dropped — the one direction the docblock says "this gate must not err in". On the head tree the sweep above finds no live member (5 .strip() sites all extend with new keys; 0 shape-sharing z.object; the sole strictObjectError shape is unexported), so the accept set as of this head is exactly as declared and no published verdict is wrong today. The declaration of the safeguard is nonetheless false against the repo's own measurement, it is not pinned (no fixture has an open def sharing a strict shape), and the PR's answer to its own flagged boundary rests on it. Verdict-bearing.

  4. Generator output. No published artifact moves: the emission loop is untouched, json-schema/ is gitignored and regenerated from that loop, authorable-surface/, the manifest, api-surface, liveness, spec-changes.json are not in the diff, and Build Core plus check:authorable-surface are green on the head. Console text changes only: a new "allowed" line shape and route 4 in the remedy. Note the deletion gate runs in build mode as well as --check (pre-existing; not gated by CHECK), so gen:schema verdicts also gain proof 4. Characterisation ("nothing publishes"): correct.

  5. Public surface. The script exports nothing before or after; nothing imports it; the two new imports (strictObjectDeclarations, StrictObjectDeclaration, keySetMatches) are pre-existing exports of src modules already in the eager graph; 0 lines change in packages/spec/src; exit-code semantics unchanged; no error code added or removed. Characterisation: correct.

  6. Clause-②: no. Path limb misses (packages/spec/scripts/**); content limb misses (no authorable schema's accept set moves; no *.zod.ts touched). The accept set that widens is the repo gate's, by the maintainer's ruling, not the metadata contract's. The no is a true declaration.

  7. Unverified self-narration (not contradicted, not measured): the population figures (1525/1117/144/772 defs and keys; 55 enumerated of 103 sets; 6 empty-shape defs × 9 declarations), the ablation blob hashes, and the local run tallies. Source: PR body and os-dev-report. CI on the head is the evidence for the test results.

② Semver level

skip-changeset is legitimate. Checked: files[] (10 entries, no scripts); tsup.config.ts (1 scripts/ hit, a repo-root helper import, against 22 src/); diff name-status (2 files, both packages/spec/scripts/**); json-schema/ gitignored with 0 tracked files and an untouched emission path; .changeset/ diff 0; AGENTS.md:1066 rule; Check Changeset skipped by the label as designed. Nothing from a released package publishes. No changeset is owed. A patch would be wrong, not merely unnecessary — there is no consumer-visible behaviour to describe.

③ Boundary flags

  • open_questions: [] — none declared by the round; none needed beyond the items below.
  • Out-of-scope note 1 (registry cannot tell a door from a non-door; "proof 4 works around this by reading additionalProperties: false off the emitted artifact") — the answer is wrong, per ① item 3: under io: 'output' that field is false for strip objects too (ledger l.1535-1541). The gap is latent on this head (sweep above) but the stated defence is not real. Remedy inside the card's fence: decide closure on the Zod instance (catchall of type never on the schema zodByDefKey holds) or restrict the match to declarations recorded by strictObject() rather than strictObjectError(); correct the docblock, PR body and remedy text; add a sandbox fixture where a strip-mode clone shares a strict shape and assert it is refused. Must be closed before landing, or this note must be re-filed as a card with the docblock corrected.
  • Out-of-scope note 2 (408 of 1525 emitted defs "not closed") — the figure is measured with the same instrument, so it counts artifacts whose top-level additionalProperties is not false (unions, loose objects, pipes), not the 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 ledger's strip-site population, which is counted by AST (check-strictness-ledger.mts). Not a defect; the carrier statement should not be read as the strip count.
  • Out-of-scope note 3 (ablation-dist-preflight.mjs reports "no dist/" as a refusal) — tooling; correctly declared NOT MEASURED; nothing to answer here.
  • Letter B (integration/DataSyncConfig:schedule) not reversed — verified: DataSyncConfigSchema is plain z.object (connector.zod.ts:238), so no declaration matches and proof 4 returns null; pinned as WITHHELD_TOMBSTONE. Closed.
  • ADR-0087 disposition — proof 4 does not consult RETIRED_KEYS_BY_MAJOR; the real specimen has an entry (registry.ts:12843) but the proof does not require one. The card's ruling defines the proof as the guidance entry only, and the disposition is enforced on the retiring PR by the clause-② limbs (path limb fires on packages/spec/src/**) and check:adr-0087-registration, not by check (c). Consistent with the ruling; recorded so nobody reads proof 4 as also proving the disposition.
  • Enumerated guidanceSets — the PR refuses RegExp sets because a pattern "cannot distinguish retired-with-a-prescription from spelled-like-a-wrong-layer-pointer"; an enumerated wrong-layer set has the same ambiguity and IS honoured. Within the card's definition (the author gets a prescriptive rejection), so accepted; noted as a design boundary.
  • charter conflict: does a Clause-②: no PR that touches no contract surface still owe an in-seat review before it can land? #18536 (charter: does a Clause-②: no, non-contract PR owe an in-seat review) — a process ruling, not a code question; must escalate to the maintainer. This record is an at-tier review of the PR; whether it satisfies the charter is charter conflict: does a Clause-②: no PR that touches no contract surface still owe an in-seat review before it can land? #18536's to answer.
  • Docs — no page restates the three-proof rule (0 hits, control 2), so nothing is left stale.
  • Test "lazy" leg — build-schemas.ts sets OS_EAGER_SCHEMAS=1 at l.6, but ESM import hoisting means the imported schema graph evaluates first, so the env-less spawn is genuinely lazy for the graph; a pre-existing, documented pattern in the test file (l.527-535), not this PR's.

Implemented-by: claude/issue-18301-check-c-fourth-proof
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

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

@github-actions github-actions Bot added size/l and removed size/m labels Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9e0324f8075b168f479204c428e3dd2f2a6c4f58

① Derived judgments

  1. Accept set, proof 4 as now built. A deleted authorable-surface/ line D:k is admitted when, at head: D is emitted; D is reachable (proof 2 did not fire); the base entry was NOT [RETIRED]; zodShapeOf(D) is non-empty and matches exactly one strictObjectDeclarations() entry by sorted key set and per-entry instance identity; that declaration's guidance names k, or an enumerated (non-RegExp) guidanceSets.keys does, and k is not in the shape; AND schema.safeParse({ [k]: null }) fails with an unrecognized_keys issue whose keys include k and whose message includes the declared prescription text verbatim. Verified at head l.1452-1553 and l.2554-2596. The artifact read (emitted.additionalProperties !== false, old head l.1405) is gone from code; it survives only as prose saying it does not distinguish. Characterisation: correct.

  2. Does the closure test distinguish a closed door from a silent strip? Measured yes. Through the head's own computeGuidanceRoutes, injected as fake defs on the head tree: Strict → prescribed; Strict.strip() → refused; z.object(Strict.shape) → refused; Strict.loose() → refused; z.object(shape, { error: strictObjectError(…) }) with no .strict() → refused; z.union([Strict, Strict.strip()]) → refused; z.object(Strict.shape).strict() without the error map → refused (the case remedy (i) would admit); Strict.optional() → prescribed; Strict.transform(v ⇒ v) → prescribed; z.union([Strict, z.string()]) → refused (conservative). Every stripping form also emits additionalProperties: false, which is the old condition's blindness shown on the instance. Can it still admit a key whose write is dropped? No, by construction: delivers() returns false on result.success === true before any message is read, so a parse that carries or drops the key can never be prescribed; strictUnknownKeyError appends each prescription verbatim as a bullet (suggestions.zod.ts l.489-494), so the includes needle is sound.

  3. Refuse set. Two refusals for the un-marked branch instead of one: none keeps the byte-identical "was LIVE (never tombstoned)" line; declared-but-silent is a new violation line. Both exit 1, so the third verdict changes wording only, never admission. Its text lists possible reasons without asserting one. Proofs 1-3, checks (a)/(b), the defaults ratchet and the anchor writer are byte-untouched outside the listed hunks; proof 4 stays under !wasRetired, pinned by the data/Object:compactLayout [RETIRED] leg (my census: that key IS prescribed at the def, and the call-site gate is what keeps it on proof 1's clock). Characterisation: correct.

  4. The live specimen, executed. Both twins are emitted with additionalProperties: false; both resolve to the SAME declaration object (dOpen === dClosed true, surface server.security.rateLimit, and declaration.shape === RateLimitConfigSchema.shape true). shared/RateLimitConfig: safeParse({keyBy:null}) succeeds, keyBy absent from the output, HEAD verdict declared-but-silent, OLD head prescribed = true (the hole executed through the old code). system/ServerRateLimitConfig: parse fails, unrecognized_keys present, message carries the keyBy prescription verbatim, HEAD prescribed. Same for store. data/Metric:filters prescribed; data/Metric:zzNotPrescribed18301 none; integration/DataSyncConfig:schedule none (plain open object, key dropped on parse — letter B's territory, untouched).

  5. Census, re-derived with the head's verbatim function. Emitted defs 1525 ✓; artifacts with additionalProperties === false 1117 ✓; keys promised 779 ✓; delivered 770 ✓; not delivered 9 ✓; of the 9, 2 on shared/RateLimitConfig ✓ and 7 keys on 3 union defs (ui/ChartGroupBy ×2, ui/RecordHighlightsField ×1, ui/ViewItem ×4). The old head's figures reproduce exactly (144 defs / 772 keys), which validates the instrument. Two figures do not reproduce: the PR body row "defs resolving to exactly one declaration that names an undeclared key: 258" measures 147; 258 is the count of defs resolving to exactly one declaration whether or not it names anything (ambiguous matches 0, empty-shape 6, no-shape 303). And the shipped docblock (l.1440-1441) says "7 of the 8 defs in that state are unions"; measured, the state holds 9 keys on 4 defs, 3 of them unions. Both are census prose, not safeguard claims, and the rationale they support (unions dominate the not-delivered set) still holds; both should be corrected on the next touch. Reachability labels (root-graph / derived-clone) NOT MEASURED, source: PR body.

  6. Generator output and public surface. Emission loop untouched; json-schema/ gitignored; authorable-surface/, manifest, api-surface, liveness, spec-changes.json not in the diff; packages/spec/src tree hash unchanged; the script exports nothing; the imports (strictObjectDeclarations, StrictObjectDeclaration, keySetMatches) are pre-existing src exports. Console text: one new "allowed" line shape, one new violation shape, route 4 in the remedy. CI on head: Build Core, all Type Check jobs, Test Core (6 shards + aggregate), Governed Surface Queue Guard, Spec property liveness, Dogfood, Temporal Conformance all success; Lint & Repo Gates in progress at last poll; 0 failures. Characterisation ("nothing publishes"): correct.

  7. Clause-②: no. Path limb (packages/spec/src/**, the clause-② suspect glob at dispatch-gates.mjs:22241) misses packages/spec/scripts/**; content limb misses (no *.zod.ts touched, no authorable schema's accept set moves). The accept set that widens is the repo gate's, by the [finding] the authorable-key deletion gate's reachability BFS reports integration/DataSyncConfig unreachable from the metadata-type roots — it is two hops from the connector root, so #4650 proof 2 waives a tombstone for a key a stack manifest really authors #17356 ruling. A true declaration.

  8. Pre-existing vacuous negatives — verified on the old head. The five rx(K, 'shape is CLOSED') negatives (eager l.1403/1409/1415/1425, lazy l.1437) could never match: the old gate emitted K — def REACH, and its shape is CLOSED…, and the regex demanded K — shape is CLOSED; applied to the old head's own admitted line for data/Metric:filters it returns false. The claim "every negative" is one short: the sixth (def not reachable from the, l.1393) was live. What the earlier green meant: each key emits exactly one verdict line (continue after every push), and the eager positives pinned each key's correct verdict, so the vacuous negatives were redundant there; only the lazy-leg agedKey negative (l.1437) had no positive counterpart and asserted nothing. The new negatives verified able to hit their own admitted and third-verdict lines, and the allowed regex verified not to match the third verdict.

② Semver level

skip-changeset remains legitimate at +694/-11. Checked: files[] (10 entries, no scripts); tsup.config.ts (1 scripts/ hit, the repo-root tsup-drop-sources-content.mjs import, against 22 src/); three-dot name-status is the two packages/spec/scripts/** files and nothing under .changeset/; AGENTS.md:1066 (skip-changeset is for a diff that publishes nothing from any released package); Check Changeset skipped by the label as designed. Size is not the criterion; publication is, and nothing publishes. No changeset is owed and a patch would describe no consumer-visible behaviour.

③ Boundary flags

  • Continued past the stop instruction — judged sound. Antecedent measured true: the old code admits shared/RateLimitConfig:keyBy (OLD prescribed = true in my run). Consequent measured false: the PR is draft and unmerged; keyBy/store are 0 in the baseline and 0 in authorable-surface/ against a lit control of 1 for enabled; a guidance-only key was never in the shape, so it was never a baseline line and no deletion could ever ask proof 4 about it, even had the old head merged. Stopping would have left a proven-wrong proof in a draft; the dev reported the deviation instead of concealing it and handed the real trap to triage rather than acting on it. The residual is process, not code: whether a dev may read a stop instruction by its stated rationale is the seat's to rule, and it was disclosed for exactly that.
  • Neither remedy taken — the load-bearing claim about (ii) is TRUE. Both twins match the same declaration object, which strictObject() itself registered (strictObject is z.object(shape, { error: strictObjectError(...) }).strict(), one declarationStore().push per call), so restricting the match to strictObject()-recorded declarations admits both twins; and it would be a strict-object.ts edit across the path limb. On (i): catchall-of-never does separate the twins (open false, closed true) and agrees with the probe on 776 of 779 promised keys; it diverges on 3 (security/ObjectPermission, a transform pipe where instance unwrapping fails, fail-closed) and it admits z.object(Strict.shape).strict() without the map, which the probe refuses. On the card's own definition ("a prescriptive rejection"), the probe is strictly stronger. Refusal reasons hold.
  • Real specimen instead of a synthetic fixture — pins the same property and IS a discriminator. Open def, strict declaration, identical shape entries, refused; closed twin admitted; one run, one declaration, opposite verdicts; the beforeAll guard holds the four tree facts loudly. The sandbox symlinks src/ (test l.455), so a synthetic case would have needed a src mutation or a new seam. I ran the synthetic forms myself (item ① 2) and all are refused, but note the residual: the message-includes half of delivers() (strict clone without the map) is pinned by no fixture — defended by construction and by census (0 such defs on the shipped graph).
  • Third verdict and the negatives fix — in scope and fail-closed. A violation cannot widen admission; the wording states only that the prescription did not arrive. The negatives fix is inside the card's files and its claim is verified above.
  • Ablations C and D — NOT re-run; consistent by inspection. delivers → true makes every promised key prescribed, so the twins case's two injected keys are both admitted and the run exits 0; delivers → false makes every promised key declared-but-silent, so both positive legs fall to the third verdict. My OLD-vs-HEAD verdicts on the open twin are the same fact as C's outcome. Blob hashes, marker counts and exit codes: source os-dev-report.
  • Gate reconciliation. dispatch-gates.mjs --commands over the two paths derives 61 families here (from the shared checkout's tree at 582d3e54e, which the tool flags as 7 commits stale; the stale file is not in the list), including the five dist-needing gates and check-reference-carrier-shape (still present on origin/main at 879b5127). Deriving on c38660f5f rather than 9e0324f8 does not matter: the derivation is path-based and the path set is byte-identical; the report's "derived ON THIS HEAD" is imprecise by one wording-only commit. Which gates were re-run after that final commit is stated only for test:repo, typecheck, check:authorable-surface and lint; only test:repo could see that commit. The 5 exit-3 gates are correctly NOT MEASURED.
  • 124 then 0 — honest, not a re-roll. The repo's own dispatch-gates.mjs (l.12070-12109) classifies exit 124 as "a timeout wrapper fired and signalled the child", a kill rather than a verdict; the gate is deterministic over the tree; both readings were disclosed. Timing NOT MEASURED.
  • mcp_calls: 0 NOT MEASURABLE from this seat. The 3 REST writes are consistent with observables: PR body updated_at 02:32:01Z; report comment created 02:33:15Z and updated 02:34:24Z (one PATCH); labels size/l, tests, tooling, skip-changeset with size/l from the labeler.
  • Two census misstatements owed a correction (item ① 5): PR body "258" mislabeled (147 for that label) and docblock "7 of the 8 defs" (9 keys on 4 defs, 3 unions). Neither moves a verdict or describes a safeguard.
  • Fail-closed edges, not defects: (a) promisesFor skips RegExp sets before the !owed.has precedence check while strictUnknownKeyError walks all sets in declaration order, so a RegExp set declared before an enumerated set naming the same key would refuse a legitimate retirement — none on the shipped graph (all 9 non-deliveries explained); (b) an empty-string prescription would pass includes('') at the gate while falling through at runtime — 0 exist against 135 tables; (c) computeGuidanceRoutes() snapshots the registry at call time and my run saw it grow 553→557 as z.lazy getters fired later, a declaration built after the snapshot reads 'none' — in the gate the JSON-Schema pass precedes the call, as the docblock relies on; (d) 7 union keys refused although a member refuses with the prescription — deliberate.
  • Unchanged from the FAIL record: proof 4 does not consult RETIRED_KEYS_BY_MAJOR (not a disposition proof); letter B is not reversed (DataSyncConfig has no route, measured); no doc restates the proof rule; the --self-test in the card's suggested acceptance is delivered as vitest cases, the gate having no such mode.
  • Declared out of scope, correctly: the open twin drops keyBy/store in silence on a live authorable surface (packages/spec/src/**), handed to triage as a class-(c) card. packages/spec/src on origin/main has since moved in expression.zod.ts and translation.zod.ts; irrelevant to this head, the census may shift post-merge. charter conflict: does a Clause-②: no PR that touches no contract surface still owe an in-seat review before it can land? #18536 (whether a Clause-②: no PR owes this review) stays a charter question for the maintainer.

Implemented-by: claude/issue-18301-check-c-fourth-proof
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

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

@os-litant
os-litant deleted the claude/issue-18301-check-c-fourth-proof branch September 17, 2026 07:39
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… 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>
os-elon-musk pushed a commit that referenced this pull request Sep 18, 2026
`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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

3 participants