Uh oh!
There was an error while loading. Please reload this page.
fix(components): make button's container-declaration exception permanent and reasoned - #6901
Conversation
…anent and reasoned objectui#6779's ruling (2026-08-29) excluded `button` from the container-declaration ratchet list "pending a separate card". That card is objectui#6804, and the maintainer ruled it 2026-08-30: `button` does not declare `isContainer`. This writes the ruling's ground into the ledger so the exception can no longer be mistaken for an oversight — which is the mechanism behind this defect class's three independent rediscoveries (objectui#3900 / objectui#6740 / objectui#6764). - `scripts/container-declaration-baseline.json`: `excluded.button.reason` now carries the three grounds (semantics: `isContainer` means layout containment, and `button` reads `children` only as a `schema.label` fallback; cost: it is the one public-tier member of the 45, so declaring it deletes the `Button` identifier from every `kind:'react'` page; pull: zero measured) instead of a forward reference to a card that had not been ruled yet. `issue` moves to the card that answered the question. - The note records why only 1 of the ruling's 14 tags is listed: the other 13 do not violate today, and listing a non-violator trips this file's own other red. - The note also corrects an implication: `button` is the only public tag among the 45 listed, but ADR-0080's `PUBLIC_BLOCKS` carries `badge` and `alert` as bare keys too, so 3 of the ruled 14 are published contract (measured over the live registry). - The pin's provisional framing is updated; every assertion that keeps the exception honest is unchanged, and its owning-issue assertion follows the ledger value. No registration metadata is altered and no renderer is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
os-sam
commented
Aug 30, 2026
Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.
Why no contract review: clause ② fires on a change to contract accept/reject behaviour or a widening of the published surface, judged from content. This diff is a ledger ⭐ The scope call was the hard part, and the PR got it right for the stated reasonThe ruling says the answer covers 14 tags. Executed literally as 14 ledger rows it turns the pin red, and the PR re-derived that rather than taking my dispatch note on trust. Verified independently at
⇒ Listing the 13 today would have tripped the baseline's other red on the day it was written. Recording them in the note and the pin comments with their ground, and giving them an entry only when #6771's
What I verified rather than accepted✅ The ✅ ✅ The ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});And consistently, its renderer reads ✅ The red-then-green control is real. A deliberate 14th ⭐ The reason field now says what is not allowed, and that is the part that closes the classThe ruling's ⇒ A reader three months out cannot mistake this for an oversight, which is the whole deliverable. The provisional "pending a separate card" framing is gone from the ledger, the describe block and the file header alike. The public-tier correction is now recorded where it will be readThe note previously implied StatusCI at Generated by Claude Code |
✅ 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
|
…ow makes The note's⚠️ paragraph tells whoever retires the `body` dialect that two of the 13 body-only renderers they are about to give a `children` read are published contract. That is a measurement, and this file's own convention — set by the 44's paragraph, "measured; the pin asserts it" — is that a measured claim in the ledger names the pin holding it. Unpinned it goes quietly false the day `badge` or `alert` leaves the public tier, in the one sentence written to prevent an unmeasured public-tier change. Adds one assertion holding the three facts the paragraph rests on, read off the LIVE REGISTRY rather than off `PUBLIC_BLOCKS` — the list is the input, the registry is the fact, and this is exactly a population where they differ: `getPublicConfigs()` keys the contract by the curated tag, so the namespaced `page:sidebar` enters the public set under that spelling while the bare `sidebar` registration never does. - `badge` and `alert` are in the public set; - the 11 bare `sidebar-*` keys are not, and the family is still 11; - `page:sidebar` is public and already declares `isContainer`, which is what makes "the public sidebar is not in this story" true; - with a direction control (`isPublic.size > 0` plus `button` as a known positive), so the absences cannot pass vacuously if the public reader ever resolves nothing. The note now names this pin. The four existing assertions are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
✅ 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
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6804
Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B —
buttondoes not declareisContainer— and makes its exception in the container-declaration ratchet permanent and reasoned instead of provisional.#6779's ruling had excluded
button"pending a separate card". This is that card, and the exclusion now stands on its own recorded ground rather than on a forward reference to an unruled card.Why the reason field matters more than the verdict
An exception with no recorded ground is evidentially indistinguishable from a missed one. That indistinguishability is the documented mechanism behind this defect class's three independent rediscoveries (#3900 / #6740 / #6764), and it is what the ruling's
⛔ 例外必须带依据字段clause exists to stop.excluded.button.reasoninscripts/container-declaration-baseline.jsonnow carries all three grounds:isContainermeans layout containment, not "this tag renders children".buttonreadsschema.childrenonly as a fallback forschema.label, so declaring it would make one predicate mean two different things.buttonis the only public-tier member of the 45, andreact-page.tsxskips containers when building the JSX scope, so declaring the flag deletes theButtonidentifier from everykind:'react'page. Immediate, user-visible capability loss.It also states what is not allowed: declaring the flag, or moving
buttonintoundeclared, both override the ruling.The
issuefieldThe type is a single string, so the two candidate cards could not both be named there. I pointed it at #6804 — the card that ruled it — and kept #6779 named in the
reasonas provenance, rather than widening the type. A reader following that pointer wants the decision, not the deferral; and widening a ratchet's schema for one entry buys nothing the prose does not already carry.:342's assertion follows the ledger value.⛔ Scope:
buttononly — no entry was added for the other 13The ruling's answer covers 14 tags, but adding 14
excludedentries turns the pin red, and I re-derived that rather than taking it on trust::341pinsexcludedto exact equality with['button'];badge,alert, 11sidebar-*) readrenderChildren(schema.body)and never touchschema.children, so they do not violate today —:390-403pins exactly that;So the 13 are recorded — in the note and in the pin's comments, with their ground — but get no baseline entry until they actually violate. If #6771's retirement of the
bodydialect gives one achildrenread, that is when it earns an entry.packages/components/src/renderers/form/button.tsx:58was not edited; per the ruling that line's edit right sits with #6771 (out of scope here).Public-tier measurement folded into the note
The note previously read as though
buttonwere the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:buttonbadgealertsidebar-*⇒ 3 of the ruled 14 are published contract, not 1.
PUBLIC_BLOCKS(ADR-0080) carriesbadgeandalertas bare keys and both are registered bare; the 11sidebar-*keys are not public — the public sidebar is the namespacedpage:sidebar, a different registration, which already declaresisContainer: true. This is a warning to whoever implements thebody-dialect retirement: two of the tags they are about to give achildrenread are published contract, not a mechanical fix.This claim is pinned, not just written down. The baseline's own convention — set by the paragraph covering the 44, "None of the 44 below is public (measured; the pin asserts it)" — is that a measured claim in the ledger names the pin holding it. A new assertion (
the public tier of the ruled 14 is THREE, not one) holds all three facts, and the note now names it. Without that, the warning goes quietly false the daybadgeoralertleaves the public tier — inside the one sentence written to prevent an unmeasured public-tier change, which is this card's own defect shape one level up.It reads the live registry, not
PUBLIC_BLOCKS: the list is the input, the registry is the fact, and this is exactly a population where they differ —getPublicConfigs()keys the contract by the curated tag, sopage:sidebarenters the public set under that spelling while the baresidebarregistration never does. Grepping the list would score all 12 sidebar keys off one entry belonging to none of them. It also carries a direction control (isPublic.size > 0plusbuttonas a known positive), the way the 44's block does, so "the 11 are not public" cannot pass vacuously if the reader ever resolves nothing.⛔ The four pre-existing assertions are untouched — only added to.
Verification — measured at
17e2cbfbe(head, worktree clean)Ablation on the new pin — a pin that cannot fail is worse than none. The mutation is on the fact, not the assertion:
'badge',removed fromPUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash —PRE_badge_entries=1 → POST=0 → RESTORED=1, blob92197f3ce → 745a74a00 → 92197f3ce, andgit diff HEAD0 bytes for that path after restore (restore viagit checkout HEAD -- PATH, the named-ref form, so a written index cannot feed the mutation back). Result: 2 failed | 44 passed (46), the new pin red at:473:9with its own message —`badge` left the public tier — the baseline's ⚠️ public-tier paragraph is now false, fix it.The mutation reaches the subject through source, not a stale build: the root vitest config aliases
@object-ui/coretopackages/core/src, so no rebuild is in the path. The second red is honest collateral and worth naming —layout-containers-declare-containment.test.tsx > leaf blocks stay injectable into a react pagealso depends onbadgebeing public, which is itself evidence the mutation moved a real shared fact rather than something test-local.Red-then-green control (first commit). The suite can say "no": a deliberate 14th
excludedkey was written to disk (disk blob4fb9fdb4b→a79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactlycontainer-declaration-ratchet.test.tsx:341:46— 1 failed | 44 passed (45). Restored viagit checkout HEAD -- PATH: disk blob back to4fb9fdb4b, marker count 0,git diff HEADempty for that path.Green union (repo-root vitest invocation per AGENTS.md — not
pnpm --filter, which is trap #1 there):13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed
buttonblock. Counts, not just an exit code.Test Files 3 passed (3)/Tests 46 passed (46)turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 totalcheck-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s)(empty frontmatter)The test file is genuinely type-checked, not merely adjacent to a green run:
tsc -p tsconfig.test.json --listFilesputscontainer-declaration-ratchet.test.tsxin the program (1 match of 1888 files), and that project is chained from the package'stype-checkscript.Declared narrowing: eslint was run on the changed file rather than repo-wide. Justification, all three parts: ① the lint population comes from eslint's own config —
files: ['**/*.{ts,tsx}']— and the only.ts/.tsxfile in this diff is that one (the.jsonand.mdare outside the population entirely); ② the file count is read from--format json, not assumed:FILES_LINTED=1; ③ the config declares noparserOptions.project/projectService, so linting is not type-aware and this diff cannot move the verdict of any untouched file. CI runs the full farm regardless.Changeset: empty frontmatter — ledger and test prose only, no published behaviour change. (
skip-changesetis deliberately not applied: in this repo that label is read by nothing, and the empty-frontmatter changeset is the declaration mechanism the gate accepts.)Generated by Claude Code
Generated by Claude Code