fix(types): refuse breadcrumbs by name on the page node (objectui#8871) - #8930
Conversation
…#8871) `PageNodeSchema` never declared `breadcrumbs` and no renderer ever read it, so an authored trail survived purely through `BaseSchema`'s `.passthrough()`: the document parsed green and drew nothing. It is now an ADR-0049 retirement tombstone with the remedy in the message, plus a `breadcrumbs?: never` twin so `tsc` refuses it at the authoring site. objectui#7926 refused `actions` on this same node and deliberately left this key parsing, with a pin saying a later retirement had to say so out loud. That pin is flipped rather than deleted. Its maintainer ruling is NOT borrowed - it covers `actions` only; the authority here is the standing ADR-0049 enforce-or-remove gate, which this package already applies to this face. Measured with a point-access probe, never a bare word: `\.breadcrumbs` scores 0 tree-wide against 10 files for `\.breadcrumb\b` as the lit control. Three author sites, all teaching passages in content/docs/guide/layout.md - correcting #7926's "1 site", whose census filtered on `page`-tagged objects while two of the three passages carry no `type` at all. All three are rewritten onto the `breadcrumb` NODE, which is a registered renderer taking the same item shape. The node is NOT strict, and the census leg that says so is re-asserted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review at
|
| changeset says | patch, citing #7926 |
patch was ruling-specified with Clause-②: no |
|
this card carries Clause-②: yes — I declared it on the claim |
|
| the changeset's own lead sentence | "Accept-set change, deliberately" |
| AGENTS.md 版本号策略 | objectui's own breaking changes take minor |
| precedents sharing this reading — #5905 (both), #4919, #5453 | all minor |
| CI's Changeset Bump Policy | only enforces no-major ⇒ nothing mechanical decides |
⇒ The bump must agree with the Clause-② reading the card actually keeps, and that reading is yes. minor. ⛔ Not patch — citing #7926 for the level while carrying the opposite clause-② verdict imports a precedent's conclusion without its premise.
F3 / F4 — nits, fixed in the same round
Stale control numbers in prose (\.breadcrumb\b is 16 files tree-wide / 13 in packages/, not 10 — the pin asserts > 3 so it is robust) and the spec-parity pin has moved from :203 to :229.
Verified, no action
Mechanism (layout.zod.ts:585 tombstone + layout.ts:800 ?: never, not a deletion); the remedy's road really exists — breadcrumb is registered at components/src/renderers/data-display/breadcrumb.tsx:77 and reads exactly items / separator / maxItems / item.icon|label|href, with the doc target present; the deferral comment resolved in place in the same commit as all three passage rewrites; the page node is not .strict() (control: 2 .strict() elsewhere in that file); both re-grade triggers measured as not firing, ⭐ the reviewer extending the →p2 census into the sibling objectstack repo and finding only a QA-checklist prose string.
Both ablations reproduced independently, blob hashes matching the dev's readings exactly, including the TS leg's TS2578: Unused '@ts-expect-error' directive.
⇒ Patch round dispatched for F1 + F2 + F3 + F4. ⛔ Not enqueued; needs:contract-review stays hung on both carriers until the delta confirms.
Generated by Claude Code
…crumbs changeset to minor Contract-review patch round (PR comment 5611939627, objectui#8871), F1-F4, docs/changeset/comments only: - F1: "two of the three carry no `type`" was wrong for one of the two undercounted sites. `layout.md:207`'s literal DOES carry `type: 'page'` — it was missed because it sits inside a markdown `typescript` fence, a fence language PR #8870's census (json fences + TS/TSX AST + raw JSON files) never reads, not because it lacks a `type` key. `:676` was already correctly described (a `json`-fenced, untagged fragment). Corrected in all six locations: the PR body, this changeset, both `layout.zod.ts` docblocks, and both refusal test docblocks. - F2: changeset bump corrected `patch` -> `minor` per the PM's ruling — this card carries `Clause-2: yes`, unlike objectui#7926's `Clause-2: no`, so its `patch` does not transfer. Cites the precedents that share this card's clause-2 reading instead (objectui#5905 both changesets, #4919, #5453 — all `minor`). Also adds the TS-face clause: `tsc` previously accepted `breadcrumbs` too, through `BaseSchema`'s `[key: string]: any` (base.ts:467), before this narrowed both faces together. - F3: `\.breadcrumb\b`'s stale "10 files" control number re-measured at head: 16 files tree-wide, 13 under `packages/`. The pin itself asserts `> 3`, so it was never at risk — only the prose was stale. - F4: confirmed `page-app-dashboard-spec-parity.test.ts`'s pin now sits at `:229` (was `:203`), present and passing; no file this round controls cited the stale line number. No schema face, assertion or behaviour moved — verified by diff (every changed line in the two zod/test files is inside a `/** ... */` comment). `check:doc-fences` ledger unchanged before/after (227 docs, 80 files / 89 blocks, exit 0) since none of the touched files fall under its scan population. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Delta re-review: PASS WITH FINDINGS — ⛔ adopted as a reading, and reported as a SELF-REVIEW
Tier fuse — run before anything was adoptedThe review ran as an isolated subagent with the model passed explicitly. ⛔ A self-reported tier is not a reading, so the transcript was measured, with every exit code captured into a file before any pipe: ⇒ The tier is verified. Only two actions were legal on this record — adopt it verbatim, or void it whole. It is adopted. ⛔ …and then classified, which is a separate actThe reviewer disclosed, unprompted and in its own words:
⇒ Under ⇒ What this record IS, meanwhile: eight independent measurements this PR did not have before, two of which found real defects the patch round introduced or missed. Those are actionable regardless of how the independence question lands, and a patch round is dispatched for them now. The record, adopted verbatimVerdict: PASS WITH FINDINGS — no blocking findings. Two commits on the branch, F1 — the fence-language correction is TRUE, corrected in the six named places, but a seventh site still teaches the old wrong mechanism. Non-blocking; ⛔ not closed.Verified against the base, not the dev's text ( Six locations corrected, verified positively — firing control Residue — the seventh site. F2 —
|
| tree | \.breadcrumb\b tree-wide |
same, -- packages/ |
\.breadcrumbs tree-wide |
|---|---|---|---|
base 93127bd6f |
12 (rc 0) | 10 (rc 0) | 0, rc 1 |
prior head 67e365b3 |
16 (rc 0) | 13 (rc 0) | — |
head 24a14c27 |
16 (rc 0) | 13 (rc 0) | 6 lines, rc 0 |
| head, minus the pin's own 8 exclusions | 11 (rc 0) | — | 0, rc 1 |
Controls: silent git grep -l 'zzqxjv_no_such_token_8930' → rc 1 / 0 files; firing, bare-word breadcrumbs → 17 files, rc 0.
The four files accounting for 12→16 and 10→13 are this PR's own — the changeset, the refusal test, layout.ts, layout.zod.ts — each matching only because it quotes the probe string. So layout.zod.ts:522-527 now reads "on this branch's base … \.breadcrumbs returns nothing (exit 1); \.breadcrumb\b returns 16 files tree-wide (13 under packages/)" — and no single tree satisfies that sentence: on the base the control is 12/10, on the head the \.breadcrumbs probe is rc 0. Same pairing in the changeset :27 and the test docblock :34-35. Meanwhile layout.ts:783 still says "against 10 files" — the patch round did not touch it. Before the patch round all four agreed on 10; now three say 16/13 and one says 10.
The pin is unaffected (toBeGreaterThan(3), 11 at head with its exclusions). Smaller instance of the same frame, consistent and needing no action: "63 changesets cite ADR-0049" is the base reading, 64 at head.
F4 — closed. Verified.
grep -n "still passes unknown renderer props through" page-app-dashboard-spec-parity.test.ts → :229 at head (:216 at base; :203 on the older main the card cited). Tree-wide git grep -n 'spec-parity\.test\.ts:203' → rc 1; broader 'spec-parity\.test\.ts:[0-9]' → rc 1 — no line-cited reference to that file exists anywhere. Firing control: the bare filename is cited in 6 files, rc 0.
F5 (new, drift) — the base→main window is no longer empty, and one new commit edits the same two files. Non-blocking, informational.
93127bd6f..origin/main (a5abc0c1d) = 5 commits; PR is mergeable_state: behind. 24d1eddb9 (PR #8914, objectui#8310 — PageNodeSchema.body becomes union([node, array])) touches both layout.ts and zod/layout.zod.ts. Measured rather than assumed: git merge-tree --write-tree rc 0, no textual conflict, and the merged PageNodeSchema carries both tombstones beside the body union. main's 29 moved files author no breadcrumbs (rc 1) and read no .breadcrumbs (rc 1), with body authored 10× in the new page-body-arity-8310.test.ts as the firing control on the same instrument. A local merge in the reviewer's worktree only (⛔ never pushed): @object-ui/types build rc 0, type-check rc 0 with 0 error TS, vitest run packages/types/ 165 files / 3218 tests rc 0, and — the addendum that closed the one outstanding reading — the 8 non-types guide readers on that merged tree, 8 files / 79 tests rc 0. ⇒ The PR body's "window is empty" pre-flight is stale by 5 commits.
The patch round itself (67e365b3..24a14c27)
Scope claim verified: over the three source/test files, changed lines that are not *-prefixed comment lines → 0 (rc 1); firing control, 46 comment lines changed in layout.zod.ts. PAGE_BREADCRUMBS_REFUSAL has no export; index.ts, index.zod.ts and package.json are byte-identical to base. ⇒ the round moved no face. check:doc-fences rc 0 with the identical ledger line (227 documents / 80 files / 89 blocks) — the dev's before/after claim reproduces. Defects it introduced: F3's frame inconsistency and the layout.ts / layout.zod.ts disagreement, which agreed before. Defect it missed: F1's seventh site, pre-existing.
Correctness vs the ruling, re-checked on this head
Refusal measured on the built dist (node against packages/types/dist/zod/layout.zod.js): retired doc → success=false, code invalid_type, path breadcrumbs, message names breadcrumb / body / page:header; positive control (same doc minus the key) → true; {type:'page', someRendererProp:42} → true, so ⛔ not strict (.strict() appears in layout.zod.ts only inside comments, 0× in the PageNodeSchema block); the remedy breadcrumb node in body → true; the actions sibling still refused; shape contains breadcrumbs. dist/layout.d.ts:768 carries breadcrumbs?: never, twin of :738's actions?: never.
Ablation 1 (zod arm), trap-restored: HEAD blob f9e48139… = disk blob, anchor 1 → 0, disk blob → c76062b2…, pin → rc 1, 4 failed / 11 passed, red by name; restore → blob back to f9e48139…, anchor 1, git diff HEAD 0 lines. Ablation 2 (TS twin): blob 1c813f91… → 918b3eb2…, anchor 1 → 0, type-check → rc 2, TS2578 at page-breadcrumbs-refusal-8871.test.ts(173,7); restored, 0 lines. Worktree porcelain clean after both. Both reproduce the dev's readings exactly.
Pins on the pure head: 4 files → 78 tests rc 0. All 10 tests that read guide/layout.md — derived by git grep -l "guide/layout" -- '*.test.ts*', ⛔ not the body's list of 8 → 10 files / 105 tests rc 0. Whole packages/types: 164 files / 3209 tests rc 0. Remedy really exists: breadcrumb.tsx:77 registers breadcrumb and reads schema.items / separator / maxItems; the doc page and the guide anchor are present. Gates here: build rc 0, type-check rc 0, check:doc-fences rc 0, check:doc-types rc 0 (898 literals, every documented type registered), changeset gates 0/0/0, governed surface NOT GOVERNED rc 0 with AGENTS.md → rc 3 as the firing control. CI on the head: 33 checks — 30 success, 3 skipped by design, 0 failed, 0 in progress.
⭐ An operational finding that lands on the PM, not on this PR
The PR body opens with the bare Fixes #8871, which is right. But node scripts/pm/check-clause2-carriers.mjs --pair 8930 returns rc 2 = UNJUDGED — and its own provenance line says it read objectstack-ai/objectstack, the script's default board. That is the other repo's #8930, not a verdict on this pair. Retargeted with PM_SWEEP_REPO=objectstack-ai/objectui, the same command returns rc 0: "PR #8930 / card #8871 — the clause-② declaration is readable in the fixed spelling and both carriers agree". --self-test → rc 0, 299 cases, as the control.
⇒ This seat's landing pre-check must set PM_SWEEP_REPO, or a green pair reads as UNJUDGED. ⛔ I have been running it bare. Recorded here and carried into the next dispatch.
NOT MEASURED — stated, ⛔ not counted green
check:doc-snippets, check:doc-examples, check:skill-examples, docs:check-links and the full-farm lint were not run locally (each needs the 35-package closure). CI green on this head substitutes for Doc Snippet Type Check, Skill Example Check, Internal Docs Link Check and Lint. check:doc-examples has no CI job identifiable by name, so it rests on the dev's claim alone. PR #8870's 91-object / 23-key passthrough census was not re-run; the prior review accepted it and nothing in the patch round bears on it.
Housekeeping: the reviewer's scratch worktree /home/user/objectui-delta-8930 sits at the PR head with a clean porcelain; the local merge commit e4bff32f is unreferenced and was never pushed.
FAIL discipline
⛔ Not triggered — the verdict is PASS WITH FINDINGS, so no carrier stripping is owed on this record. A second patch round on the same head lineage is not a FAIL.
This seat's disposition
F1's seventh site and F3's frame are going back for a patch round, not being filed. Neither is cosmetic: F1 leaves the wrong mechanism taught at the assertion whose whole job is to embody the correction, and F3 leaves layout.ts and layout.zod.ts — twin faces — stating different numbers for the same probe, which they did not do before this PR. Both are comment rewrites. F2's precedent overstatement is mine and goes with them; F5 needs no action beyond the body's stale pre-flight sentence.
⛔ Unchanged: draft, not enqueued, needs:contract-review hung on the PR and on objectui#8871, pending the self-review escalation.
Generated by Claude Code
…the census blind spot correctly (objectui#8871) Second patch round on the contract-review record (PR comment 5612273040). Comments, docblocks and changeset prose only: over layout.ts, zod/layout.zod.ts and page-breadcrumbs-refusal-8871.test.ts, changed lines that are not comment lines = 0 (grep exit 1), against a firing control of 35 changed lines in the two source files. F1 - the seventh site. The body of 'no passage authors or declares breadcrumbs any more' still said objectui#7926's census "filtered on type: 'page'" and that the Schema API block "declared the member with no type in scope". Re-verified on the base: guide/layout.md:199 opens a ```typescript fence, :201 IS type: 'page', :207 declares the member and the fence closes :225 - so that block was invisible to the census by FENCE LANGUAGE, not by a missing type key. :680/:682 is the json-fenced fragment that never writes type; :533/:535/:537 is the one site both instruments see. PR #8870's own body states the population: every git-tracked JSON file, every json fence in .md/.mdx, every TS/TSX object literal via the TypeScript AST. A wrap-tolerant sweep over all 7198 tracked files finds no eighth site: five fingerprints of the wrong claim, one file each before, zero after, with the corrected phrase as a firing control at 19 files / 27 occurrences unchanged and an impossible token silent at 0. F3 - the frame. Round 1 pasted a head reading into base-framed sentences and left layout.ts at the old number, so the twin faces disagreed. Re-derived, exit codes captured before any pipe: on base 93127bd, \.breadcrumbs is exit 1 tree-wide against 12 files (10 under packages/) for \.breadcrumb\b; at head those read 16 and 13 and \.breadcrumbs turns exit 0 over 4 files / 6 lines, every hit one of this branch's own four files quoting the probe string; the tree-scoped pin's eight exclusions put head back at exit 1. All four sites now state the frame in one sentence. No assertion changed. F2 - the precedent list. Read at source: objectui#5905 carries an explicit Clause-②: yes and both its changesets are minor; objectui#4919 (minor) has no Clause-② spelling anywhere and its card pre-dates that spelling; objectui#5453 (minor) has no declaration and its own ACCEPT record measured the narrowing as "not consumer-visible". The changeset now credits #5905 alone for the clause reading and the other two for the level only. The minor level does not move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Second patch round landed on
|
| tree | \.breadcrumb\b tree-wide |
-- packages/ |
\.breadcrumbs tree-wide |
|---|---|---|---|
base 93127bd6f |
12 files / 48 lines (rc 0) | 10 files / 45 lines (rc 0) | 0, rc 1 |
head 24a14c272 |
16 / 54 (rc 0) | 13 / 50 (rc 0) | 4 files / 6 lines (rc 0) |
| head minus the pin's 8 exclusions | 11 / 46 (rc 0) | — | 0, rc 1 |
⭐ comm -13 over the two file lists gives exactly four additions, every one this PR's own — the changeset, the refusal test, layout.ts, zod/layout.zod.ts — each matching only because it quotes the probe string; and comm -23 is EMPTY, so nothing at base left the set. That is what turns "the head number is inflated by our own files" from an argument into a reading. Controls: silent token rc 1 / 0 files on both trees; firing bare-word breadcrumbs 13 files at base, 17 at head. The frame is now stated in one sentence at all four sites, base numbers as the measurement and head numbers as this branch's echo — ⛔ no sentence mixes trees, and layout.ts and zod/layout.zod.ts agree again. ⛔ No assertion changed; the four pin files are 78/78 green.
F2 — my precedent list, corrected at source. objectui#5905: 8 Clause-② hits across body + 18 comments, including the ruling's "Clause-②: yes, correctly declared"; both changesets minor. objectui#4919: 0 hits; card closed 2026-08-24 while the earliest Clause-② spelling is 2026-08-31 — same shape, ⛔ not a literal precedent. objectui#5453: 0 hits, and its ACCEPT record 5450663069 reads verbatim "the narrowing is not consumer-visible". ⇒ The changeset now credits #5905 alone for the clause reading and demotes the other two to corroboration of the level. ⛔ The minor level does not move.
F5 — window re-derived off the real merge-base. merge-base 93127bd6f (unchanged); main had moved two commits past what the review measured. Window 7 commits / 36 files; ours 2 commits / 7 files; true intersection 2 files — layout.ts and zod/layout.zod.ts — moved only by 24d1eddb9 (PR #8914, objectui#8310). HEAD..origin/main reads 41 files — five more than the window — because it folds this PR's own edits back in. git merge-tree --write-tree rc 0, no conflict; self-comparison control byte-equal to HEAD^{tree}; and — ⭐ the part that makes rc 0 a measurement rather than a silent probe — a firing control: a synthetic commit off the merge-base rewriting the same line PR #8914 rewrote (mutation proven on disk by anchor count before reading any result) makes merge-tree return rc 1 with a real CONFLICT (content). Built through a scratch GIT_INDEX_FILE: ⛔ no ref moved, no index touched, ⛔ no merge taken.
Scope proof. Over both source files and the test file, changed lines that are not comment lines = 0 (grep rc 1), against a firing control of 35 total changed lines. Gates at 073026a4f: lint rc 0, build rc 0 (128 emitted files verified), type-check rc 0 with 0 error TS, the four pins 78/78, vitest run packages/types/ 164 files / 3209 tests rc 0, check:doc-fences rc 0 with the ledger identical (227 documents / 80 files / 89 blocks), all four changeset gates rc 0.
check:doc-snippets, check:doc-examples, check:skill-examples, docs:check-links — this round edits nothing under content/docs or skills, and check:doc-fences, the gate that reads that population, is green on an identical ledger. Declared to CI. CI convergence on 073026a4f was not waited on by the round.
⛔ Where this PR now stands
All five items are closed and the round moved no face. ⛔ It still does not land. The delta review that produced these findings is a SELF-REVIEW under contract-review.md — implementer and reviewer both trace to this session — and re-reviewing the new head from this same seat would only reproduce that. So needs:contract-review stays hung on this PR and on objectui#8871, the PR stays draft, ⛔ not enqueued. It is parked on the same escalation as PR #8865, which is the maintainer's or the director seat's to resolve.
One PM-owed item from the round: objectui#8871's card body still calls objectui#8310 "already open" — it closed at 03:11:41Z when PR #8914 merged. Card bodies are mine; fixing it now.
Generated by Claude Code
Fixes #8871
Step 1 was a measurement, and its verdict is COVERED
Triage did not rule this retirement — it ruled that a measurement decides whether a ruling is needed at all: does ADR-0049's enforce-or-remove cover this repository's
packages/typeszod mirror face? It does, and the evidence is on this exact node.packages/types/src/zod/tombstone.zod.tsretirementTombstone(), whose own docstring opens "Declare an ADR-0049 RETIREMENT TOMBSTONE", and which is "Internal to this package's zod modules" — machinery built for this face and no othergit grep -l "ADR-0049" -- .changeset/'@object-ui/types'PageNodeSchemaitselfactions: retirementTombstone(PAGE_ACTIONS_REFUSAL)— landed by PR #8870 as "an ADR-0049 refusal arm". Same file, same node, same.passthrough()survival mechanism, one key overdocs/audits/2026-09-plugin-detail-downstream-consumer-census.md:22⇒ Not a fork requiring a ruling. A retirement executed under standing discipline.
Counter-evidence, weighed and reported rather than suppressed — it does not flip the verdict, but a reviewer should see it
ADR-0049's own Decision text is scoped narrowly: "A spec property that names a security/access-control boundary must be in exactly one of three states." Its Non-goals place "the P2 (spec hygiene) clusters of #1878 — non-security, governed separately" outside. And its 2026-09-04 amendment's Scope note says "No new scope is claimed for this ADR by this amendment."
In the same breath, that amendment records as fact that "the repo cites this ADR as the enforce-or-remove policy for spec-property retirement generally" — and does not disown it. objectui adopted it exactly that way: none of
MobileOverrides, gridwrap,ComponentInput.inputTypeorpage.actionsis a security property, and all four cite this ADR.Triage's question was whether the mirror face is in range. It is, demonstrably, by landed precedent on this very node. The property-class nuance is recorded here so it is a known reading rather than a silent assumption.
⛔ The probe shape, because the bare word lies here
93127bd6f. At head the same probes read differently, and only because this branch's own four files quote the probe strings — the round-2 section below gives both trees side by side.A bare-word probe reports "5 readers" and every one is false:
app-shell/src/observability/sentry.tsis Sentry's own unrelated breadcrumbs concept, andcore/src/utils/record-title.ts:14/layout/src/NavigationRenderer.tsx:806are comments listing UI surfaces. Three prose sites, no reader, no declaration.The card and triage both record "authored once, in one documentation fence." Measured on this branch's base, it is three teaching passages, all in
content/docs/guide/layout.md::207Schema API blockbreadcrumbsas an optional array of label-plus-href objects outright — the strongest teacher, inside a```typescriptfence (opened:199) whose literal does carrytype: 'page'(:201):537"Detail Page with Actions"pagenode, inside a```jsonfence taggedtype: 'page':676Best Practices §2```jsonfence, on an untagged fragmentWhy #7926 undercounted, exactly: its census (PR #8870) reads every git-tracked JSON file, every
```jsonfence in.md/.mdx, and every TS/TSX object literal via the TypeScript AST — and it missed two of the three sites for two different reasons.:207's literal does carrytype: 'page', but it sits inside a markdown```typescriptfence — a fence language the census'sjson-fence reader never visits, so it was never read at all; that is the instrument's blind spot, not an absenttypekey.:676's literal sits inside a```jsonfence the census does read, but that literal never writestypeat all, so apage-tagged filter correctly excludes it. Both are blind spots in the instrument, not a change in the tree — reported, not graded.Re-grade triggers: neither fires.
-> p2requires authoring outside the documentation (customer metadata, examples, templates) — there is none: no example app, catalog fixture, template or customer document writes the key.-> closerequires the doc fence and thelayout.zod.ts:477deferral comment both to go — this PR resolves both, but as a retirement, which is the opposite of orphaning. ⛔ Grading is triage's.What lands
A tombstone + named refusal, never a plain deletion — under
.passthrough()a dropped key is kept, not refused, so deleting a declaration that never existed would leave the silent accept exactly as it was.zod/layout.zod.ts—breadcrumbs: retirementTombstone(PAGE_BREADCRUMBS_REFUSAL)layout.ts—breadcrumbs?: never, the twinzod-mirror-parity.test.tscompareslayout.zod.ts:477deferral comment is resolved, not merely removed — and the doc fence goes in the same change, so the deferral is not hiddenWhy a refusal and not a reader. The remedy already ships:
breadcrumbis a registered renderer (ComponentRegistry.register('breadcrumb', …)) whoseitemstake the very{ label, href }shape these passages authored, plusseparator,maxItemsand per-itemicon. This is objectui#5453's "no second road to a consumer" test run in the opposite direction: there the key was retired because there was no road; here because the road that exists is the one that draws.{ "type": "page", "title": "Acme Corporation", "body": [ { "type": "breadcrumb", "items": [{ "label": "Home", "href": "/" }] } ] }⛔ Not
.strict()— kept, per both boundaries. Only 2 of 23 passthrough-surviving undeclared keys land on a real SDUIpagenode; strictness would have taken the live pinpage-app-dashboard-spec-parity.test.ts("the component envelope still passes unknown renderer props through") with it. That pin stays green and its census leg is re-asserted from this card's side.Pins — one flipped, never deleted
page-actions-refusal-7926.test.ts:129-141asserted thatbreadcrumbsstill parsed, and said in its own comment that "if a later card retires it, this line is the one that says so out loud instead of the change happening by accident here." This is that card. The leg is flipped tofalse, so the closure stays asserted instead of becoming a silent absence.New:
page-breadcrumbs-refusal-8871.test.ts— parse refusal at thebreadcrumbspath,invalid_typecode (distinguishingretirementTombstonefrom itscustom-coded siblinghandlerKeyRefusal), the remedy in the message, a positive control (same document minus the key parses green), the remedy actually parsing, the not-strict leg, and a@ts-expect-errortwin that is self-proving — an unused directive is itself a compile error, sotscgreen is the refusal firing.Plus a ⭐ tree-scoped absence pin (never file-scoped), with its exclusions spelled beside their reasons rather than in an allow-list file, and a lit control (
\.breadcrumb\bmust still find the live singular key) so a brokengit grep, a wrong cwd or an over-broad exclusion cannot read as green.Ablation — the pin can actually fail
093f43d931d188728be2f857c41e4a2824c9d06egrep -c1 -> 0 (⛔ not agit diff --stat, which an equal-length edit reads as zero)7b72f62aa8829acbccced2f2be74ec790f78b3b1— different, so the edit reached disk093f43d9…, anchor1,git diff HEADemptyRestoration is verified by state, not by an exit code — a no-op cleanup step also exits 0. The restore names
HEADexplicitly (a baregit checkout -- PATHrestores from the index, which held the mutation), and the trap uses an absoluteREPO_ROOT.Two further firings were observed, not staged: the tree-scoped pin caught the declaration files on its first run, and
page-app-dashboard-spec-parity's drift guard went red untilbreadcrumbswas entered in itslocalledger row — the row now carries a ⛔ note that a third refusal member means the strictness census must be re-run rather than the list grown reflexively.Verification
Every exit code captured before any pipe.
pnpm --filter @object-ui/types builddist completeness: 1 package(s) complete (128 emitted files verified)vitest run packages/types/Test Files 164 passed (164) · Tests 3209 passed (3209)type-check— types + components + runnerDone; PATHS read, not just the codetsc -p tsconfig.test.json --listFiles@ts-expect-erroris measured, not excludedguide/layout.mdTest Files 8 passed (8) · Tests 79 passed (79)— includes #7926's render halfcheck:doc-types✅ Every documented component type is registered(898typeliterals; the newbreadcrumbfences judged)check:doc-snippets638 of 638 block(s) judged, 0 failedcheck:doc-examplesEvery covered @example compiles, or fails exactly as its ledger row declarescheck:skill-examplesEvery marked skill example holds up against the built typescheck:doc-fencesevery TypeScript block in 227 document(s) is fenced ts/tsx/typescriptcheck:control-bytesOK (scanned 7111 tracked text file(s))check-changeset-presencedocs:check-linksdocs:check-links;check:doc-linksdoes not exist)check:doc-snippets,check:doc-examplesandcheck:skill-examplesfirst returned exit 2 = PRECONDITION NOT MET — read as NOT MEASURED, not as a pass. Each names its own scoped build; that build was run (35 successful, 35 total) and all three re-run at 0. The table reports the measured run.Ledger check with control (objectui#8614's
file:linehazard):UNGATED_EXAMPLESis keyed byPATH:LINE SYMBOLoverpackages/**source JSDoc — 0 of its 90 rows citecontent/docs/, so a markdown line-shift cannot stale a row. TheUNGATED_DOCSledgers of all three doc gates hold 0layout.mdrows (exit 1) against populated controls of 39 / 11 / 83content/docs/rows.doc-version-claimshas onelayout.mdrow, keyed byfile+ claim string, not by line, and about thepage:headericon tombstone — untouched. No re-keying needed.Importer census (the retirement's blast radius).
PageNodeSchemaon both faces:packages/types(owner),packages/components(page.tsx, type-only),packages/runner(App.tsx,MetadataLoader.ts, type-only) — all three type-checked green.packages/core,packages/layout,packages/reactreference it only in README prose and comments.packages/runner— the package a sibling round forgot — is explicitly in the sweep.Base -> main pre-flight: re-derived in round 2 — it is not empty. See the round-2 section below for the window, the true intersection and the conflict probe with its two controls.
Not done, deliberately
⛔ The node was not made strict. ⛔ objectui#7926's ruling was not borrowed. ⛔ The card was not re-graded or closed as an orphan. ⛔ Nothing was enqueued;
needs:contract-reviewis hung on both carriers and neither is cleared.Full-farm
pnpm lintand the whole-tree suite are declared to CI — this run verified the affected packages, every reader of the edited guide, and every gate the diff touches.Patch round — F1/F2/F3/F4 (contract review, PR comment 5611939627)
⭐ The retirement itself is not reopened — the review confirmed the step-1 COVERED verdict, the mechanism, the remedy's truth, the deferral resolution, the three-site count and both re-grade measurements. This round fixes four non-blocking findings.
type" explanation was wrong for one of the two sites, and is corrected above and in six places (this body, the changeset, bothlayout.zod.tsdocblocks, and both refusal test docblocks).:207's literal does carrytype: 'page'— it was missed because it sits inside a markdowntypescriptfence, a fence language PR fix(types,docs): refuseactionsby name on thepagenode, teach the shape that draws (objectui#7926) #8870's census does not read (onlyjsonfences), not because it lacks atypekey.:676was already correctly described (ajson-fenced, untagged fragment). The count (three) and the conclusion (instrument blind spot) were already right; only the mechanism was misnamed.minor, notpatch. This card carriesClause-②: yes; objectui#7926'spatchwas ruling-specified withClause-②: noand does not transfer. The changeset now cites objectui#5905 — the one precedent that literally carriesClause-②: yes, both of whose changesets tookminor— and, after round 2, names finding(types):MobileOverrides.navigation发布了三值导航词表,全仓零读点 —— 与 #3985 同族的另一处「声明即全部」 #4919 and [finding] ObjectGrid forwards a per-columnwrapinto the DataTable column object, but nothing indata-table.tsxever reads it #5453 as same-shape corroboration of the level only (⛔ neither carries the declaration; read at source in the round-2 section). It also adds the TS-face clause the reviewer asked for:tscpreviously acceptedbreadcrumbstoo, throughBaseSchema's[key: string]: any(packages/types/src/base.ts:467), before this narrowed both faces together.\.breadcrumb\bcontrol number. Re-measured at head as 16 files tree-wide / 13 underpackages/.layout.tsat the old number — round 2 corrects the frame rather than the digits, at all four sites. The pin itself asserts> 3, so it was never at risk; only the prose was wrong.page-app-dashboard-spec-parity.test.ts's pin has moved from:203to:229(its ledger comment grew). Present and passing at head (re-run:Test Files 1 passed, Tests 20 passed). No file this round controls (this body, the changeset, or the two refusal test files) cited the stale line number with a:203suffix, so no additional prose edit was needed beyond confirming the new line by measurement.Scope of this round: documentation, changeset frontmatter and comments only — no schema face, assertion or behaviour moved (confirmed by diff: every changed line in the two source files sits inside a
/** ... */comment).check:doc-fencesledger identical before and after (227 documents, 80 files / 89 blocks, exit 0), since none of the four touched files fall under its scan population (content/docs/**+ READMEs).Patch round 2 — F1 residue, F3 frame, F2 precedents, F5 pre-flight (contract review, PR comment 5612273040)
⭐ Prose, comments and changeset text only. ⛔ No schema face, no assertion, no behaviour moved. Measured, not asserted: over
layout.ts,zod/layout.zod.tsandpage-breadcrumbs-refusal-8871.test.ts, changed lines that are not comment lines = 0 (grepexit 1), against a firing control of 35 changed lines in the two source files alone.F3 — one frame, stated at all four sites
Round 1 pasted a head reading into sentences framed "on this branch's base", and did not touch
layout.tsat all — so the twin faces, which agreed before, disagreed after. Every number below re-derived here, each exit code captured into a file before any pipe:93127bd6f24a14c272\.breadcrumb\btree-wide\.breadcrumb\b-- packages/\.breadcrumbstree-wide\.breadcrumbs-- packages/Controls on the same instrument: silent
zzqxjv_no_such_token_8930-> rc 1 / 0 files on both trees; firing bare-wordbreadcrumbs-> 13 files rc 0 at base, 17 files rc 0 at head.Why head moves at all.
comm -13over the two file lists gives exactly four additions, and every one is this branch's own —.changeset/8871-page-node-refuses-breadcrumbs.md,packages/types/src/__tests__/page-breadcrumbs-refusal-8871.test.ts,packages/types/src/layout.ts,packages/types/src/zod/layout.zod.ts— each matching only because it quotes the probe string.comm -23is empty: nothing at base left the set. So the base column is the measurement and the head column is this branch's echo of it. All four sites now say that explicitly, and the two declaration faces agree again.⛔ No assertion changed; the pin is still⚠️ One reading handed back rather than acted on: that pin's
toBeGreaterThan(3).grepTree()runsgit grep -n, so its.lengthcounts lines — 46 at head with the exclusions, not the 11 files. Both clear the threshold, so the verdict is unaffected; recorded so the number is not misread later.F1 — the seventh site, and the sweep for an eighth
page-breadcrumbs-refusal-8871.test.ts's'no passage authors or declares breadcrumbs any more'still taught the old, wrong mechanism, contradicting the corrected docblock ~160 lines above it in the same file. Re-verified on the base before rewriting (git show 93127bd6f:content/docs/guide/layout.md)::199/:201/:207/:225```typescriptfence that does carrytype: 'page', declaresbreadcrumbs?: Array<...>at:207, and closes at:225:680/:682```jsonfence whose fragment never writestypeat all:533/:535/:537```jsonfence tagged"type": "page"— the site both instruments seePR #8870's census population, quoted from its own body: "over every git-tracked JSON file, every
```jsonfence in.md/.mdx, and every TS/TSX object literal (TypeScript AST, so keys are read rather than grepped)" — atypescriptfence is in none of the three. ⇒ the Schema API block was invisible to that census by fence language, ⛔ not by a missingtypekey. The comment now says that.Sweep for an eighth site — wrap-tolerant, ⛔ not a single-line grep. All 7198 tracked files, comment leaders stripped and whitespace normalised so a phrase that wraps across lines still matches (which is exactly how the seventh site escaped a line-scoped grep), over five fingerprints of the wrong claim:
no `type` in scopefiltered on `type: 'page'`counted ONE siteauthored it on an untagged fragmentdeclared the member with no `type`Every hit was the same file and the same five-line comment. ⇒ There is no eighth site. The PR body was scanned on the same instrument: 0 hits. Controls: firing — the corrected phrase
`typescript` fence-> 19 files / 27 occurrences, byte-identical before and after (the review's narrower literal reproduces tree-wide at 13 files / 16 hits, of which this PR's own four files carry exactly the 5 the review recorded); silent — an impossible token -> 0 files.F2 — the precedent citation, each one read at source
Clause-②on the card5540306033"Clause-②: expected yes", ruling5541469968"Clause-②: yes, correctly declared"5905-componentinput-inputtype-tombstone.mdminor (+plugin-markdownminor) and5905-componentinput-retire-constraint-keys.mdminorClause-②spelling across body + 3 comments; card closed2026-08-24, while the spelling first appears on #5905 on2026-08-314919-retire-mobile-overrides.mdminor (types+mobile)5453-retire-grid-column-wrap-forward.mdminor (plugin-grid)5450663069measured that narrowing as "not consumer-visible"⇒ Only #5905 literally supports "shares this card's
Clause-②: yesreading". The changeset now says exactly that and demotes the other two to corroboration of the level. ⛔ Theminorlevel itself does not move: AGENTS.md:240plus #5905 carry it without them.F5 — the base -> main pre-flight, re-derived
⛔ Derived from two
..-scoped diffs off the real merge-base, never a raw two-dotHEAD..origin/main— that instrument folds this PR's own edits back in and reports 41 files where the window has 36.git merge-base HEAD origin/main93127bd6ffc405784320c00009563a1feadec7c0— the same base this PR was cut fromorigin/mainat measurement4eaa835b193127bd6f..origin/main93127bd6f..HEADpackages/types/src/layout.tsandpackages/types/src/zod/layout.zod.ts24d1eddb9only (PR #8914, objectui#8310 —PageNodeSchema.bodybecomesunion([node, array])). objectui#8310 is closed, PR #8914 merged2026-09-10T03:11:39Zgit merge-tree --write-tree HEAD origin/main1cd53003…, 0CONFLICT/changed in bothlinesgit merge-tree --write-tree HEAD HEADcc7a7491…— byte-equal togit rev-parse HEAD^{tree}, so the probe returns a real tree rather than a stubbody:line #8914 rewroteCONFLICT (content): Merge conflict in packages/types/src/zod/layout.zod.tsThe firing control is what makes that rc 0 a measurement: the same instrument, given a pair that genuinely diverges on that line, returns rc 1 and names the file. The control commit was built through a scratch
GIT_INDEX_FILE, so it moved no ref and touched no index —git diff HEADandgit status --porcelainboth 0 lines afterwards. ⛔ No merge was taken; the queue rebuilds on currentmain.Round-2 verification — every exit code captured before any pipe
pnpm --filter @object-ui/types lint✖ 273 problems (0 errors, 273 warnings)— every warning a pre-existingno-explicit-anypnpm --filter @object-ui/types builddist completeness: 1 package(s) complete (128 emitted files verified)pnpm --filter @object-ui/types type-checktscpasses;grep -c 'error TS'= 0Test Files 4 passed (4) · Tests 78 passed (78)vitest run packages/types/Test Files 164 passed (164) · Tests 3209 passed (3209)check:doc-fencesevery TypeScript block in 227 document(s) … 80 declared file(s) carrying 89 block(s)— ledger identicalcheck-changeset-presence5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-majorNo changeset declares a major bump.check-changeset-fixedAll workspace packages are in the changeset fixed group.check-changeset-overwriteNo pre-existing changeset was modified or deleted.check-control-bytesOK (scanned 7113 tracked text file(s); skipped 85 binary)Control-byte self-scan beyond the gate, over the four changed files:⚠️ A TAB does not fire it — TAB is deliberately outside the class — so a TAB-bearing control would have read as a clean instrument that is in fact silent.
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'-> rc 1, 0 lines; firing control on the same instrument (a vertical tab) -> rc 0, 1 line.⛔ Unchanged by this round: draft, not enqueued,
needs:contract-reviewhung on both carriers, and the retirement itself untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code