Skip to content

Retire check-reference-carrier-shape; refuse an unreadable reference carrier at the reader - #18503

Merged
os-litant merged 4 commits into
mainfrom
claude/issue-18095-retire-reference-carrier-shape
Sep 17, 2026
Merged

os-litant merged 4 commits into
mainfrom
claude/issue-18095-retire-reference-carrier-shape

Conversation

@os-warren

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

Copy link
Copy Markdown
Collaborator

Fixes #18095

Executes the maintainer ruling on this card (comment 5699305034, letter E): retire check-reference-carrier-shape, and move the defect class it guarded into the reader, which now refuses a carrier it cannot read instead of answering "no target". The direction is ruled; this PR is execution.


⭐ First step of the dispatch — the reader census

Two different populations, and they are not the same size. Both re-derived on the merge base, both with a same-subject control.

reading value control
git grep -l 'refOf(' -- 'packages/' 5 files git grep -l 'FieldSchema' -- 'packages/' = 236 files
git grep -l -E '\.reference\b' over packages/** TS/TSX/MJS 73 files, 151 lines bracket form ['reference'] = 6 lines; same-syntax control ['type'] = 36 files

Both refOf( numbers reproduce the seat's pre-dispatch measurement exactly (5 and 236). The classification behind the 5 does not. Only two of those five files read FieldSchema.reference:

file is it a FieldSchema.reference reader?
packages/lint/src/validate-security-posture.ts ✅ yes — the reader in the #13053 incident
packages/lint/src/data-model-rules.ts ✅ yes — its own docblock says it mirrors the above
packages/lint/scripts/check-reference-carrier-shape.mjs ❌ the gate being retired (prose + one self-test fixture string)
packages/metadata-core/src/contract-suite.ts name collision — a local refOf = (overrides) => MetaRef building {org, type, name}. Nothing to do with fields
packages/metadata-protocol/src/sys-metadata-repository.contract.test.ts ❌ a comment referring to that MetaRef builder

So refOf( is a good handle on the named instance and a poor census of "every reader". The real census is the 73-file .reference population, classified below.

The census, by what each reader does with a non-string today

A — changed here (6 read sites, 4 files). Routed through one refusal:

  • packages/spec/src/data/field-value.zod.tsreferenceTargetOf, the declared single arbiter
  • packages/lint/src/validate-security-posture.tsrefOf
  • packages/lint/src/data-model-rules.tsrefOf, plus the R8 options-source read and the R7 summary-target read
  • packages/lint/src/object-graph.tsgraphFieldOf, the slice every other lint rule reads downstream

B — inherits the refusal with no edit, because it already asks the arbiter (referenceTargetOf, 16 files by grep): objectql/src/engine.ts ($expand), objectql/src/integrity/dangling-reference-audit.ts, objectql/src/record-title.ts, metadata-protocol/src/protocol.ts, service-analytics/src/dimension-labels.ts.

C — measured, deliberately unchanged, listed so the boundary is visible:

  • C1, explicit typeof === 'string' narrowing (the same silence, spelled differently): plugin-audit/src/audit-writers.ts (×4), rest/src/export-format.ts, cli/src/commands/doctor.ts, spec/src/kernel/functional-completeness.ts.
  • C2, truthiness or equality reads that never answer "no target" (an object value is truthy, and === 'sys_user' is false either way): driver-mongodb/src/mongodb-schema.ts, plugin-sharing/src/sharing-rule-service.ts, objectql/src/engine.ts:8989, service-analytics/src/plugin.ts, spec/src/data/default-value-shape.ts, rest/src/export-format.ts:185, plugin-approvals/src/approval-service.ts.
  • C3, raw reads that DO still answer undefined silently — the measured residue, 10 sites: objectql/src/engine.ts:13052 and :13491 (cascade delete), rest/src/rest-server.ts:10835, metadata-protocol/src/seed-loader.ts:701, lint/src/validate-expressions.ts:380, lint/src/validate-field-consumers.ts:552, lint/src/validate-object-references.ts:297 and :316, lint/src/validate-sharing-rule-enforceability.ts:261, lint/src/validate-preset-comparands.ts:431, verify/src/derive.ts:136. Not silently left out — see What this PR does not close below.

D — must NOT throw, on purpose: the schema's own superRefine validators (spec/src/data/field.zod.ts, spec/src/ui/action.zod.ts, spec/src/automation/builtin-node-config.zod.ts). A throw inside a refinement makes safeParse throw instead of returning {success: false} — that would destroy the loudness at the contract door this whole change leans on.

E — ⛔ not touched (ruling item 3): LOOKUP_TARGET_COLUMN in service-automation/src/builtin/screen-nodes.ts (#17306). The constant is byte-unchanged; only its docblock's now-stale reference to the retired gate was corrected.


The ruling's premise, verified rather than assumed

The ruling rests on "the protocol already refuses the shape at the contract door". Measured against the built spec, not recalled:

CONTROL string reference   -> ObjectSchema.safeParse success = true
CLAIM   object reference   -> success = false
  issue: code=invalid_type path=["fields","invoice","reference"]
         message=Invalid input: expected string, received object
CLAIM   array  reference   -> success = false, same path
FieldSchema alone, object  -> success = false ; CONTROL string -> success = true

The premise holds: an object-valued carrier is refused, located, at load. reference is z.string().optional() at packages/spec/src/data/field.zod.ts:1251, and InlineGridColumn's at :874.

That the reader change ends #13053's class

(#13053 is referenced here as the incident this change answers. This PR does not close it — the wording below deliberately keeps every closing keyword away from its number, and GitHub's link table confirms it: this PR's only closing keyword is Fixes #18095. ⚠️ Corrected at landing: an earlier revision of this body said #13053 「remains open」. It does not — #13053 was closed 2026-08-29 with state_reason: completed. The claim was wrong when written; the contract review measured it.)

The worked example is the fixture the retiring gate's own header names. packages/cli/test/data-model-rules.test.ts used to assert that reference: { object: 'project' } resolved to nothing and produced relationship/missing-reference — a finding about the wrong thing, since the target is not missing, it is unreadable. It now asserts the refusal, with two controls:

  • TypeError, message matching /`reference` is an object/ and /FieldSchema declares it as an optional STRING/ — ⛔ not a bare toThrow(), which an unrepaired reader throwing any Error on any input would satisfy;
  • control: the identical object with reference: 'project' lints clean, so the throw is about the carrier's shape;
  • control: an absent carrier is still the ordinary missing-reference finding, not a throw — absence and unreadability stay different answers.

null, undefined and '' are absence and never throw. That is the retiring gate's own documented position ("null is not a wrong carrier — it is an absent one"), and StrictField declares reference nullable.

What newly throws across the tree

Nothing, measured. The retiring gate's final census, taken on the merge base immediately before deleting it:

check-reference-carrier-shape: OK — 6796 file(s) scanned, 681 `reference` site(s).
  field-def carrier position:  584 string literal(s), 0 non-string literal(s) — 20 unjudged
  provably not a carrier:      20 site(s)
  position unresolved:         45 site(s) + 12 conflicting — 0 material
EXIT 0

Zero non-string literals at a field-def carrier position. The one non-literal counter-example in the tree is the cli fixture above, which is re-keyed here. No real (non-fixture) call site passes a non-string today.


Removal hygiene

lint.yml: the step and its comment block removed (32 lines). package.json: no check: alias existed — the gate was invoked by path (grep exit 1; control check:doc-security-posture resolves in 5 files).

Tree sweep after removal, hard-wrap-safe (grep -rlz, because this prose wraps the filename across lines and a line-oriented grep returns a false zero):

needle files control
check-reference-carrier-shape 3 check-doc-security-posture = 4
check:reference-carrier-shape 3 check:doc-security-posture = 5

All six survivors are prose, now past-tense and stating the retirement. No roster entry, no workflow line, no package.json alias, no import. Comment-masked counts: check-self-test-wired.mjs 2 in code, dispatch-gates.mjs 1 in code — both are synthetic fixture strings feeding pure-text matchers (no existsSync, no spawn), and they are the cases that now hold the grammar.

⚠️ What this retirement costs, stated rather than buried

This gate was the tree's only package-local by-path gate invocation, and three other gates pinned it by name as their live specimen. Measured:

packageLocal before: ["packages/lint/scripts/check-reference-carrier-shape.mjs"]   population 214
packageLocal after:  []                                                            population 213
live direct root invocations after: 143 across 80 scripts, all exist  (the control)

So the lane did not move, it emptied — and the pins' own instruction ("re-point this pin at the new specimen") has no specimen to point at. Five live assertions were converted, each keeping what it could still hold:

  • scripts/check-self-test-wired.mjs — the export pin now holds the derivation (packageLocal is exactly the part of population the root walk did not produce), which is true at zero and at one; the corpus pin holds the anchor against minting a climbing key. The synthetic battery('left boundary') still drives the grammar.
  • scripts/check-self-test-workflow-commands.mjs — both pins now quantify over the whole imported population, so they hold at zero members and start judging the day one returns.
  • scripts/pm/dispatch-gates.mjs — both live pins become a zero with its control (no packages/… direct invocation, against 143 root ones from the same extraction), so an extraction that stopped matching is still caught.

What is genuinely weaker: no live reading now proves the package-local admission path end-to-end. The grammar is exercised only synthetically until some future gate is invoked by a package-local path.

What this PR does not close

The C3 residue above — 10 raw .reference reads that still answer undefined silently. Routing them means touching objectql, rest, metadata-protocol and verify, several on hot runtime paths, and each needs its own judgement about absence vs unreadability. They are measured and named here rather than swept in; the arbiter change already covers every consumer that asks referenceTargetOf.

Acceptance notes

  • packages/lint/scripts/ keeps two sibling gates (check-doc-formula-expressions, check-doc-security-posture), both invoked as pnpm --filter @objectstack/lint run check:* and therefore outside collectInvocations' population entirely. Pre-existing, not this PR's; recorded so the next reader does not derive it as a wiring gap.
  • The claim comment's declared file surface named six paths; the executed surface is sixteen. The additions are scripts/check-self-test-wired.mjs, scripts/check-self-test-workflow-commands.mjs, packages/lint/src/object-graph.ts, packages/spec/src/data/field-value.{zod,test}.ts, the two regenerated spec artifacts, screen-nodes.ts and builtin-node-config.test.ts (both comment-only), and the changeset — every one of them required by ruling item 1's "every roster/family that names it" or item 2's reader change.

Verification

run result
pnpm --filter @objectstack/spec test 483 files, 13775 tests, all pass
pnpm --filter @objectstack/lint test 103 files, 3844 tests, all pass
pnpm --filter @objectstack/cli exec vitest run --project unit test/data-model-rules.test.ts 56 tests pass
pnpm --filter @objectstack/spec check:generated all 15 artifacts up to date (api-surface/ + export-origins/ regenerated: 0 breaking, 1 added)
node scripts/check-self-test-wired.mjs + --self-test 0 / 0 — "213 script(s) … 0 of them package-local gate(s) CI names by path"
node scripts/check-self-test-workflow-commands.mjs --self-test 0 — 8 batteries, 37 cases
node scripts/pm/dispatch-gates.mjs --self-test 0 — 1746 cases pass
21 further derived gates (symbol anchors, step collectors, workflow quoting, published-files, changeset family, nul-bytes, …) all 0

packages/cli's integration tier is declared to CI: the diff touches no spawn entry point. The repo-wide pnpm lint sweep is CI's.

Clause-②

The seat declared yes (claim 5700063579) and hangs needs:contract-review itself. The api-surface reading substantiates it: 1 added, 0 breakingreferenceCarrierOf is a new published export on @objectstack/spec/data. Changeset is minor for @objectstack/spec and @objectstack/lint, as a yes requires. No Clause-② line is written into this body; the carrier is the card's.

Governed-surface predicate on the final file list: 0 of 16 hit the register — ordinary queue landing.


Generated by Claude Code


Landing note (seat, 2026-09-17)

Contract review at CONTRACT_REVIEW_TIER on head 016bdeaa43: PASS — record is comment 5706700015 on this PR.

⚠️ The review named one item as the maintainer's to accept rather than the seat's: whether arbiter + lint routing satisfies ruling E item 2 (「every reader … throw」), given this PR leaves a measured residue of 9 raw reads. The residue is now tracked by #18550, filed BEFORE this PR lands so that Fixes #18095 closing the card cannot lose it. ⛔ The seat did not treat landing as an answer to that question — it is carried to the maintainer on #18550 and in the round report.

The review also re-measured two counts this body states: the C3 residue is 9 sites, not 10 (validate-preset-comparands.ts:431 reads the GraphField slice and is already covered by the arbiter change), and spec/src/kernel/functional-completeness.ts:167 is misfiled under C1 — it reports a non-string reference as an incompleteness finding rather than answering silently. Both corrections are recorded on #18550 for whoever takes it.

Non-blocking, recorded not fixed: the changeset's upgrade sentence names 「a hand-built fixture or a raw registry entry」 but not os lint, which by design does not Zod-parse before running the rules and so is a third, author-facing path that now surfaces the refusal as the command's catch-all.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. ⚠️ 3 changed file(s) yielded no anchor (packages/services/service-automation/src/builtin/screen-nodes.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/services/service-automation/src/builtin/screen-nodes.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 136 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 fed4a15ab5b50633553a939e4a9793e5a17ac530packageMentionDocs.

Which tree this was computed on

This run read content/docs from fe14164448ffa74afaea9f896174162b1d3b695d — the merge of head 016bdeaa43e5398ac163e15e184cf226bff8c6cc into base fed4a15ab5b50633553a939e4a9793e5a17ac530, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fe14164448ffa74afaea9f896174162b1d3b695d && git checkout fe14164448ffa74afaea9f896174162b1d3b695d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fed4a15ab5b50633553a939e4a9793e5a17ac530 016bdeaa43e5398ac163e15e184cf226bff8c6cc && git checkout -B drift-repro fed4a15ab5b50633553a939e4a9793e5a17ac530 && git merge --no-ff 016bdeaa43e5398ac163e15e184cf226bff8c6cc

node scripts/docs-audit/affected-docs.mjs --json fed4a15ab5b50633553a939e4a9793e5a17ac530

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Collaborator Author

At-tier contract review cannot run right now — the tier itself is refusing, and ⛔ the seat is not downgrading around it

Two isolated at-tier review attempts on this PR have now died before producing a record. Neither death is a verdict about this PR, and there is nothing to adopt from either.

attempt outcome
1st, ~17:50Z killed by this session's own usage limit (HTTP 429 rate_limit) mid-run
2nd, ~19:18Z killed by an account-level limit on the contract-review tier itself — HTTP 429 rate_limit, request id req_011Cf7fBVgG5xo49HoVCRhGV, message: "You've reached your … limit. Switch to another model, or manage usage credits … to continue."

Why there is no lower-tier review instead

references/contract-review.md, the downgrade fuse, L60 — verbatim:

契约复核 ⛔ 不适用额度耗尽豁免降档:豁免对象是派发,复核 ⛔ 不随派发档位免除。

The quota-exhaustion downgrade exemption covers dispatch, never review. And L54 names the state to sit in:

读数不达档 ⇒ 改走转录核验的复核子代理;标签原样留置,队列外等待是安全态。

So: needs:contract-review stays hung on both carriers (this PR and card #18095), this PR stays draft and out of the merge queue, and the wait is the correct state rather than a stall the seat invented. ⛔ No review at a lower tier, ⛔ no seat self-review, ⛔ no landing on the strength of green CI alone — CI is condition ③ of three, and condition ① (an at-tier PASS record naming this head) has not been met.

What is and is not affected

  • The work is not blocked: development continues, and this PR's head 016bdeaa43 has not moved since the review was first dispatched, so when the tier is available the review starts on the same tree rather than a re-derived one.
  • The landing is blocked, and not only here: every PR in this lane carrying a Clause-②: yes declaration needs the same tier before it can land.
  • ⛔ The seat is not re-dispatching in a loop against a limit that says "manage usage credits" — that spends the same refusal repeatedly. It re-dispatches once the tier answers, and reports the refusal to the maintainer rather than working around it.

Recorded here with endpoint and status so the next reader can tell a refusal from a verdict.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Tier re-measured on the seat's hourly check-in — still refusing, and the elapsed time is the new information

One at-tier review was dispatched for this PR at 2026-09-16T22:56Z (one, ⛔ not a loop — the seat re-measures once per check-in and does not spend repeated calls against a limit whose own message says "manage usage credits"). It terminated on the same account-level refusal:

  • HTTP 429 rate_limit, request id req_011Cf7woTApCE3JPpmPaV7Xz.
  • Previous refusals on this tier: ~19:18Z (request id req_011Cf7fBVgG5xo49HoVCRhGV) and ~17:50Z on the session's own limit.

~3.6 hours elapsed between the first refusal and this one, with no change. That is the reading worth recording: this is not a short window that waiting out is a strategy for. ⛔ No fourth attempt is planned before the next check-in.

State is unchanged and deliberately so: needs:contract-review stays on both carriers, this PR stays draft and out of the queue (references/contract-review.md L54's named safe state), and ⛔ no lower-tier review is substituted (L60: 「豁免对象是派发,复核 ⛔ 不随派发档位免除」).

⚠️ The head has not moved since the first attempt (016bdeaa43), so when the tier answers, the review starts on the same tree rather than a re-derived one — nothing is lost by the wait except time.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 016bdeaa43e5398ac163e15e184cf226bff8c6cc

① Derived judgments

Public surface

  1. @objectstack/spec/data gains one export, referenceCarrierOf(def, reader?) — a WIDENING. Verified on the head tree: src/data/index.ts:145 re-exports field-value.zod, package.json publishes ./data, api-surface/data.json and export-origins/data.json each +1 line / 0 removed. PR characterisation ("1 added, 0 breaking") is RIGHT. This item alone makes the card's yes correct, whatever one thinks of the gate argument.
  2. referenceTargetOf — signature unchanged, behaviour narrowed: a non-string, non-null reference now throws TypeError where it returned undefined. The carrier read now precedes the type check, so a non-reference field type (e.g. text) carrying a malformed reference also throws where it previously returned undefined without looking. The PR's test pins this deliberately; the PR body says only "inherits the refusal". RIGHT in direction, under-stated in reach.
  3. @objectstack/lint (publishes dist): the named exports lintDataModel, validateSecurityPosture, indexObjectGraph and, through graphFieldOf, the seven graph-building validators (validate-dataset-references, -list-view-field-refs, -object-field-refs, -preset-comparands, -rls-predicate-enforceability, -security-posture, -widget-bindings) now throw the same TypeError on such input. No export added, none removed. PR's "6 read sites, 4 files, plus the slice every other rule reads downstream" is RIGHT by class.
  4. Error surface: a new TypeError whose message names the caller, the shape and the fix. No new error code, no new key on any payload, no wire-shape change. RIGHT — the PR does not claim a code and none exists.
  5. Repo CI gate removed: packages/lint/scripts/check-reference-carrier-shape.mjs (584 lines) and its lint.yml step (32 lines). No package.json alias existed (0 hits, control 1). At the head, 7 files still spell the name and all are prose, docblocks or synthetic fixture strings; .github/ has 0 (control 1). This is the ONE genuine LOOSENING in the PR: a non-string LITERAL at a field-def carrier position in a fixture that never reaches a reader is no longer refused by anything on main. It is exactly what the maintainer ruled (letter E, item 1), and the PR's "What newly throws: nothing" is a runtime statement that does not deny it. RIGHT as ruled; named here so the loosening is on record.

Accept set

  1. referenceCarrierOf admits undefined, null and '' as ABSENCE (returns undefined), a string as itself, anything else is refused. null is admitted although FieldSchema.reference is .optional() not .nullable(); the PR cites StrictField, and that holds (solution-blueprint.zod.ts:319 strictIdentOrNull). Every prior reader already treated null as absent, so this is no change of accept set. RIGHT.
  2. os lint (CLI): by its own design comment it does not Zod-parse before running lintDataModel (lint.ts:597) and the rule registry (:669). An author's file with reference: { object: … } therefore used to produce a lint report carrying a wrong relationship/missing-reference row; it now produces the command's catch-all — {error: "data-model-rules refOf: reference is an object, and FieldSchema declares it as an optional STRING …"} exit 1 in --json, printed error exit 1 otherwise. os build, os validate and scaffold-validate exit on the failed safeParse before any rule runs (compile.ts:309–330, validate.ts:279–300, scaffold-validate.ts:76–82) and are unaffected. The changeset's upgrade note names only "a hand-built fixture or a raw registry entry"; this third, author-facing path is not named. Direction RIGHT (ruling item 2 asks for exactly this refusal, and the message carries the fix); reach UNDER-DECLARED. Not a wrong yes/no and not a wrong level; a changeset-text gap.
  3. Class B runtime consumers ($expand, record-title, dangling-reference audit, metadata-protocol/protocol.ts, analytics dimension labels) inherit item 2's throw with no edit. engine.ts has no ObjectSchema parse call of its own, so how far the throw reaches at runtime depends on the loader; the PR concedes raw registry entries and rehydrated rows. RIGHT.
  4. Class C accuracy notes, immaterial to the verdict: C1 is described as "the same silence, spelled differently" but is neither changed nor listed under "does not close"; one of its members, spec/src/kernel/functional-completeness.ts:167, is not silence — it reports a non-string reference as an incompleteness finding. In C3, validate-preset-comparands.ts:431 reads verdict.meta?.reference where meta is the GraphField slice graphFieldOf now builds (object-graph.ts:307), so it is already covered by item 3; the true residue is 9 raw sites, not 10.
  5. LOOKUP_TARGET_COLUMN (screen-nodes.ts) and builtin-node-config.test.ts: 0 non-comment lines changed. Ruling item 3 honoured. RIGHT.
  6. Clause-② declaration test: the card declared yes (claim 5700063579) against the ruling's "no expected". The new export (item 1) makes yes RIGHT on the public-surface axis; the gate retirement (item 5) is a second, ruled ground on the accept-set axis. A no would have been wrong. Both carriers carry needs:contract-review (read on the PR and on the card this run).

② Semver level

Changeset .changeset/18095-retire-reference-carrier-shape-gate.md: @objectstack/spec: minor, @objectstack/lint: minor. CONSISTENT with ①. yes requires at least minor (AGENTS.md L1067) — met on both moved publishers; the new export is a minor-level addition; both packages are in the changesets fixed group. The reader refusal touches only input the published contract already refused at parse, so under the repo's own arm wording ("the accept set a consumer writes against", changeset 18305) no (narrowing) arm and no **BREAKING**/ADR-0087 marker is owed — the precedents that carried them (15110, 16870, 16421) refused shapes the contract previously ACCEPTED at write. Under the stricter observable-behaviour reading (item 7), the launch-window convention still lands on minor (check-changeset-no-major.mjs header), so the level is right under both readings; only the marker question differs, and the repo's definition answers it not owed. Non-blocking: the upgrade sentence should name os lint beside fixtures and raw registry entries.

③ Boundary flags

  1. Dev open_questions — the package-local by-path gate lane is now EMPTY and the three pins that named the retired gate as their live specimen were converted (A accept / B fixture root / C retire the widening). Answer: A is right for this PR. The converted pins hold what is still true (the packageLocal derivation; whole-population quantifiers; a zero with its 143-root-invocation control) and CI exercised all three self-tests on this head (Lint & Repo Gates green; lint.yml:907, :1597, :1640). B is a domain:devx follow-up the seat may file; C is refused as the dev says. Not a contract matter; no escalation.
  2. "What this PR does not close" — the C3 residue (9 raw reads after item 9) plus the 7 C1 sites left unchanged. On the contract axis this is a non-change: nothing there loosens. On ruling execution it is a declared narrowing of item 2's "every reader of FieldSchema.reference found by grep throw", with Fixes #18095 still attached and ruling item 4 closing the card on landing, and no successor card is named for the residue (the dev report's only "to file" entry is a different finding). That acceptance is the maintainer's to give, not the dev's or the seat's: must escalate to the maintainer — either arbiter + lint routing is accepted as satisfying item 2 and the seat files the residue as a follow-up card before the card closes, or the PR extends. It does not change this verdict.
  3. PR body claim "finding(lint): a runtime-gate test fixture spells reference: { object: ... }, a carrier ObjectSchema refuses and the rule's own reader ignores #13053 … is not addressed by this PR and remains open": WRONG on the second half — finding(lint): a runtime-gate test fixture spells reference: { object: ... }, a carrier ObjectSchema refuses and the rule's own reader ignores #13053 is closed (2026-08-29, state_reason: completed). GitHub's own link table confirms the PR does not close it (closed_by_pull_requests empty; control: [finding] check-reference-carrier-shape blind spot for a JSON-Schema properties map and its literal-only predicate — REPORTED, NOT VERIFIED; a reading is owed (half B of #18061) #18095's lists Retire check-reference-carrier-shape; refuse an unreadable reference carrier at the reader #18503), and the only closing keyword is Fixes #18095. Immaterial to the contract; a body-accuracy correction.
  4. Declared file surface grew 6 to 16; every addition is required by ruling item 1 or 2 and the single-writer-path check is green. Answered.
  5. Dev out-of-scope "to file" (check:cross-package-test-inputs reddens locally after a spec build): unrelated to this contract; the seat files it.
  6. Docs drift bot: 3 changed files yielded no anchor (two generated artifacts, one comment-only source file); the tree sweep finds no hand-written page naming the gate. Answered.

Implemented-by: claude/issue-18095-retire-reference-carrier-shape
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Clause-② carriers cleared — provenance

Both carriers stripped in one stroke by the dispatching seat, session_01LvwGppdonww4zGLWZo5rho (domain:spec execution seat), 2026-09-17T00:48Z.

Review of record comment 5706700015 on this PR
Head judged 016bdeaa43e5398ac163e15e184cf226bff8c6cc
Served tier CONTRACT_REVIEW_TIER
Verdict PASS
Carriers cleared PR #18503 ✅ · card #18095 ✅ (both read back after the write)

How this verdict was produced, since the seat is below tier. This seat's measured served model is claude-opus-5; CONTRACT_REVIEW_TIER is claude-fable-5-1 and the comparison is exact ⇒ the seat is not at tier. Per the downgrade fuse it therefore routed the verdict to an isolated review subagent running at CONTRACT_REVIEW_TIER, fed only the card, its rulings and the PR itself, with an adversarial brief — ⛔ never the dispatch order, ⛔ never this seat's conclusions. The record above is adopted verbatim; the seat's only options were verbatim adoption or wholesale voiding. ⛔ This was not an in-seat review and ⛔ not a quota downgrade — references/contract-review.md forbids exempting review from tier.

Independence pair: Implemented-by: claude/issue-18095-retire-reference-carrier-shape (a mode:subagent dev's branch) against Reviewed-by: session_01LvwGppdonww4zGLWZo5rho (the seat adopting the isolated reviewer's verdict). Different kinds ⇒ no SELF-REVIEW.

What the review changed about this PR before it lands:

  1. It measured the body's claim that finding(lint): a runtime-gate test fixture spells reference: { object: ... }, a carrier ObjectSchema refuses and the rule's own reader ignores #13053 「remains open」 as falsefinding(lint): a runtime-gate test fixture spells reference: { object: ... }, a carrier ObjectSchema refuses and the rule's own reader ignores #13053 closed 2026-08-29. The body is corrected, because this repo squashes and the body becomes the permanent commit message on main.
  2. It re-measured two counts the body states: the C3 residue is 9 sites, not 10, and one C1 member is misfiled. Both corrections are carried on [finding] 9 raw .reference reads still answer undefined silently after #18503 routed the carrier through one arbiter — the measured residue of ruling E item 2 #18550.
  3. It named one item as the maintainer's to accept, not the seat's: whether arbiter + lint routing satisfies ruling E item 2. [finding] 9 raw .reference reads still answer undefined silently after #18503 routed the carrier through one arbiter — the measured residue of ruling E item 2 #18550 was filed before this PR lands, so Fixes #18095 closing the card cannot lose the residue whichever way that question is answered. ⛔ The seat did not treat landing as the answer.

Pre-landing checks at this head: ① review PASS on record ✅ · ② check-clause2-carriers --pair 18503 exit 0, and it confirms a review of record names this head ✅ · ③ re-taken at landing time, latest-run-per-check-name, all success or an EXPECTED_SKIPS roster skip ✅. Governed-surface predicate: 0 of 16 paths hit the register ⇒ ordinary queue landing, ⛔ no human-merge regime.

⚠️ Note for the record: editing this body re-triggered Check Changeset, so ③ was re-taken again after that run completed rather than trusted from before the edit.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 17, 2026 00:49
@os-litant
os-litant added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit e64ae15 Sep 17, 2026
58 checks passed
@os-litant
os-litant deleted the claude/issue-18095-retire-reference-carrier-shape branch September 17, 2026 07:39
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…le def (objectstack-ai#18301) (objectstack-ai#18529)

Fixes objectstack-ai#18301
Clause-②: no

Executes the **C** half of the objectstack-ai#17356 ruling (batch objectstack-ai#135 item 3,
maintainer 「135 同意」). A is already landed (PR objectstack-ai#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. objectstack-ai#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):

- marker occurrences 0 to 1, blob `322938f2` to `682ce658` — the
mutation is on disk.
- run **RED**, and in the sharpest possible direction: `eager.status`
came back **0**. With the door blinded the gate WAIVES
`shared/RateLimitConfig:keyBy` and the whole run exits green — the hole,
executed, not argued. The other two objectstack-ai#18301 cases stayed green,
correctly: neither tests the door.
- restored: blob back to `322938f2`, marker back to 0, `git diff HEAD` 0
bytes, `git status --porcelain` 0 lines.

**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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_

---

## 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 archive`d this head's
`packages/spec/src` (archived tree hash verified equal to `git rev-parse
9e0324f: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
**objectstack-ai#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 **objectstack-ai#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](https://claude.ai/code)_

---------

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

Projects

None yet

3 participants