Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(components): make `button`'s container-declaration exception permanent and reasoned by claude[bot] · Pull Request #6901 · objectstack-ai/objectui · GitHub
Skip to content

fix(components): make button's container-declaration exception permanent and reasoned - #6901

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason
Aug 30, 2026
Merged

fix(components): make button's container-declaration exception permanent and reasoned#6901
os-sam merged 2 commits into
mainfrom
claude/issue-6804-button-container-exception-reason

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#6804

Executes the maintainer's 2026-08-30 ruling (comment 5468762497): option B — button does not declare isContainer — 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.reason in scripts/container-declaration-baseline.json now carries all three grounds:

  1. SemanticsisContainer means layout containment, not "this tag renders children". button reads schema.children only as a fallback for schema.label, so declaring it would make one predicate mean two different things.
  2. Costbutton is the only public-tier member of the 45, and react-page.tsx skips containers when building the JSX scope, so declaring the flag deletes the Button identifier from every kind:'react' page. Immediate, user-visible capability loss.
  3. Pull — zero measured. Nothing records anyone wanting to nest layout content inside a button.

It also states what is not allowed: declaring the flag, or moving button into undeclared, both override the ruling.

The issue field

The 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 reason as 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: button only — no entry was added for the other 13

The ruling's answer covers 14 tags, but adding 14 excluded entries turns the pin red, and I re-derived that rather than taking it on trust:

  • :341 pins excluded to exact equality with ['button'];
  • the 13 body-only renderers (badge, alert, 11 sidebar-*) read renderChildren(schema.body) and never touch schema.children, so they do not violate today:390-403 pins exactly that;
  • the baseline's own note makes the list red in both directions: "a listed tag that no longer violates -> red". Listing 13 non-violators is precisely that red.

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 body dialect gives one a children read, that is when it earns an entry.

packages/components/src/renderers/form/button.tsx:58 was 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 button were the only public tag anywhere in this story. Measured over the live registry on this branch, it is not:

tagpublic?renders children?
button✅ (violates — the listed one)
badge❌ (body reader)
alert❌ (body reader)
11 × sidebar-*❌ (body readers)

3 of the ruled 14 are published contract, not 1. PUBLIC_BLOCKS (ADR-0080) carries badge and alert as bare keys and both are registered bare; the 11 sidebar-* keys are not public — the public sidebar is the namespaced page:sidebar, a different registration, which already declares isContainer: true. This is a warning to whoever implements the body-dialect retirement: two of the tags they are about to give a children read 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 day badge or alert leaves 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, so page:sidebar enters the public set under that spelling while the bare sidebar registration 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 > 0 plus button as 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 from PUBLIC_BLOCKS. Proven on disk in both directions, by count and by hash — PRE_badge_entries=1 → POST=0 → RESTORED=1, blob 92197f3ce → 745a74a00 → 92197f3ce, and git diff HEAD 0 bytes for that path after restore (restore via git 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:9 with 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/core to packages/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 page also depends on badge being 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 excluded key was written to disk (disk blob 4fb9fdb4ba79210583, injected-marker count 1 — the mutation was proven landed, not inferred from an editor exit code), and the run failed at exactly container-declaration-ratchet.test.tsx:341:461 failed | 44 passed (45). Restored via git checkout HEAD -- PATH: disk blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty for that path.

Green union (repo-root vitest invocation per AGENTS.md — not pnpm --filter, which is trap #1 there):

pnpm exec vitest run --reporter=verbose \
packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx \
packages/components/src/renderers/__tests__/container-declaration-census.test.tsx \
packages/components/src/__tests__/layout-containers-declare-containment.test.tsx
→ Test Files 3 passed (3) · Tests 46 passed (46)

13 of those 46 are in the ratchet file, all named individually in the verbose output, including the renamed button block. Counts, not just an exit code.

gateverdict line
pins (3 files)Test Files 3 passed (3) / Tests 46 passed (46)
turbo run type-check --filter=@object-ui/componentsTasks: 9 successful, 9 total
check-control-bytes.mjs✅ OK (scanned 5749 tracked text file(s))
check-changeset-presence.mjs✅ 1 source file(s) … declares 1 changeset(s) (empty frontmatter)
eslint (changed file)1 file linted, 0 errors, 0 warnings

The test file is genuinely type-checked, not merely adjacent to a green run: tsc -p tsconfig.test.json --listFiles puts container-declaration-ratchet.test.tsx in the program (1 match of 1888 files), and that project is chained from the package's type-check script.

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/.tsx file in this diff is that one (the .json and .md are outside the population entirely); ② the file count is read from --format json, not assumed: FILES_LINTED=1; ③ the config declares no parserOptions.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-changeset is 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

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

Copy link
Copy Markdown
Collaborator

Review — ACCEPT, pending CI. ⛔ No clause-② review owed on this one, and that is a decision rather than an omission.

domain:ui execution seat, PM session session_013hfmP9hoMd3dJwTh85J4yB. Reviewed at head b2e0b4ead.

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 reason string, test prose and comments, and an empty-frontmatter changeset. No registration's metadata moves, no accept set moves, no exported type changes, and every assertion that keeps the exception honest is unchanged. Recording the reasoning so the absence is legible rather than looking like a skipped step — the same way #6897's human-floor routing was recorded.

⭐ The scope call was the hard part, and the PR got it right for the stated reason

The 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 fab4802e3:

  • :341 still reads expect(Object.keys(baseline.excluded)).toEqual(['button'])exact equality, unchanged;
  • the 13 body-only readers stay pinned as non-violators at :390-403, bodyReaders.length === 13;
  • the baseline's shrink-only note is intact, so "a listed tag that no longer violates → red" still holds.

⇒ 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 body-dialect retirement actually makes them violate, is the correct reading of a ruling whose 14 is the scope of the answer, not of the ledger.

packages/components/src/renderers/form/button.tsx:58 is untouched — confirmed in the file list. That line's edit right stays with #6771, as the ruling directs.

What I verified rather than accepted

✅ The issue field moved on both sides together. The ledger value goes objectui#6779objectui#6804and:342's assertion moves with it. That pairing is the whole risk in this edit: moving one without the other is a red, and moving the assertion to match a stale ledger would have been a silent lie. Both moved, in one commit.

since was correctly left at 2026-08-29. Subtle and easy to get wrong: the entry was admitted on the 29th, and the note's own rule is that "a line dated later than this file's first commit was added after the ruling and needs one of its own." Bumping it to the 30th would have made this entry look newly admitted and demanded a fresh ruling for itself. Leaving it is right.

✅ The page:sidebar claim — checked, because it is baked into a durable artifact. The note and changeset both assert that the public sidebar "already declares isContainer: true". That is a fact I had not measured when I dispatched, so I did not let it through on the report. Confirmed at packages/components/src/renderers/layout/containers.tsx:1878:

ComponentRegistry.register('sidebar',PageSidebarRenderer,{namespace: 'page',skipFallback: true,label: 'Page Sidebar',category: 'layout',isContainer: true,inputs: PAGE_CONTAINER_INPUTS,});

And consistently, its renderer reads renderChildren(schema?.children || schema?.body) — it genuinely renders children and correctly declares the flag. The distinction the note draws between the namespaced page:sidebar and the 11 bare sidebar-* keys holds.

✅ The red-then-green control is real. A deliberate 14th excluded key was proven on disk (blob 4fb9fdb4ba79210583, injected-marker count 1) rather than inferred from an editor's exit code, failed at exactly :341:46 (1 failed | 44 passed), and the restore is proven both ways (blob back to 4fb9fdb4b, marker count 0, git diff HEAD empty). That is the control that the suite can say "no" about precisely this edit.

⭐ The reason field now says what is not allowed, and that is the part that closes the class

The ruling's ⛔ 例外必须带依据字段 clause exists because an exception with no recorded ground is evidentially indistinguishable from a missed one — the documented mechanism behind three independent rediscoveries (#3900 / #6740 / #6764). The new reason carries all three grounds (semantics / cost / zero pull) and an explicit prohibition — do not declare the flag, do not move button into undeclared, both override the ruling — plus a pointer to the pin that would go red if any premise changes.

⇒ 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 read

The note previously implied button was the only public tag in the containment story. It is the only public tag among the 45 measured there — a materially narrower claim. Of the ruled 14, three are published contract (button, badge, alert). Folding that into the ledger note puts the warning in front of whoever implements #6771, who is about to give a children read to two tags that are public surface. Confirmed: no assertion was changed to fold it in.

Status

CI at b2e0b4ead, counted from total_count30 total · 16 success · 3 skipped · 11 in progress · 0 failures. ⛔ Not terminal, so this ACCEPT is on the diff, not on CI. I will land it on green; the PR stays draft until then.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.0 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-BBahf6Vp.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should button declare isContainer? — the one public hit among the 53, carved out of #6779 by ruling

2 participants

@os-sam@claude