Skip to content

fix(components): page:header stops drawing an empty breadcrumb slot; the key is ignored until the spec retires it (objectui#11166) - #11172

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11166-page-header-breadcrumb-retire
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11166-page-header-breadcrumb-retire

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11166
Clause-②: no

Implements the renderer and docs half of triage 5903152845 (ruling RETIRE). The spec half is objectstack-ai/objectstack#20758; this PR does not depend on it. Dispatched by the domain:ui seat 2 PM, session https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec, branch claude/issue-11166-page-header-breadcrumb-retire.

What changed

  • Renderer. PageHeaderRenderer in packages/components/src/renderers/layout/containers.tsx no longer reads breadcrumb. It drops both empty data-page-breadcrumb-slot divs: the record-chip layout's (above RecordTitleChip) and the bare layout's (above the title row). An authored breadcrumb (true or false, in properties or on the node) renders exactly like an absent key: no slot and no error. ⛔ No trail is drawn in its place.
  • Registration. inputs keeps { name: 'breadcrumb', type: 'boolean' } and adds a description saying the key is ignored. It stays declared while the installed spec (17.4.0) still accepts the key; the measured reasons are in Zone 2 item 2 below. It leaves at the pin bump that brings objectstack#20758's tombstone, when the parity file's forward direction will name it.
  • Docs. content/docs/layout/page-header.mdx drops the key's prop-table row, the "renders an empty slot element" paragraph, and the layout diagram's (breadcrumb slot) line. One sentence replaces the paragraph. I chose one sentence over none because the spec still publishes the key as "Show breadcrumb", default true, and this page is where an author learns it draws nothing.
  • Declared surface extension: content/docs/guide/layout.md. It is outside the claimed surface, but this change makes it false, so it is a must-fix.
    • Its page:header Schema API excerpt listed breadcrumb?: boolean // default true — the breadcrumb slot above the title. That line goes.
    • Three callouts called the key "a boolean display toggle (for the slot above the title)". They now say it is a boolean the renderer ignores.
    • Same file, same defect class: the overview bullet "PageHeader - Consistent page headers with title, breadcrumbs, and actions" drops "breadcrumbs". Neither the canonical renderer nor @object-ui/layout's PageHeader draws one; the latter's render body has no breadcrumb element.
    • The claim did not check open PRs against this file. Please check for overlap before landing.
  • Changeset. '@object-ui/components': patch, which names the visible spacing shift.

Zone 2 measurements (base c2a8d23c6)

  1. Sites. All confirmed by content:

    • the read const breadcrumb = (schema?.breadcrumb ?? schema?.properties?.breadcrumb) !== false;;
    • the two {breadcrumb && (...data-page-breadcrumb-slot...)} blocks;
    • the "breadcrumb/action slots" section comment, now "action slot".

    Root class strings are byte-unchanged. The bare root is flex flex-col gap-2 pb-4 border-b, and the record root keeps gap-3 sm:gap-4. The demo pin pageheader-with-actions.test.tsx stays green on gap-2 / pb-4 / border-b.

    The rendered spacing does move. Measured in Chromium 1194 (headless) with plain CSS equal to the Tailwind utilities, not the built stylesheet:

    Layout Title offset in the header Header height
    bare 8px, now 0px 56px, now 48px
    record 4px, now 0px 60px, now 56px

    The empty div was a flex item in the root's gap-2 column (bare), or carried mb-1 (record). A header that already set breadcrumb: false does not move.

  2. Registration inputs list breadcrumb. ⛔ The parity file apps/console/src/__tests__/registry-inputs-spec-parity.test.ts is untouched.

    • It has no page:header.breadcrumb row. NO_READ_SITE_TO_PIN lives in its member-pin ledger, which covers array- and object-armed inputs only, and breadcrumb is boolean. So removing the read moves no row, and the file stays green at head.
    • Why the input must stay, measured with a one-off probe (not kept):
      • with the input declared, an authored breadcrumb: true draws zero manifest diagnostics;
      • with the input filtered out, it draws unknown-prop, whose message says page:header has no prop "breadcrumb";
      • the control, the key absent with the input filtered out, draws zero.
    • Dropping the input would also be demanded back by the parity file's reverse direction, which reads every accepted spec key. Covering it needs an UNPUBLISHED_EXEMPTIONS entry in that file, which belongs to objectui#11086.
  3. Docs. The key leaves all three sites. One sentence was chosen (see above).

  4. Existing pins. None moved.

    • git grep -n breadcrumb -- '*.test.*' under packages/components and examples/schema-catalog hits only ui:breadcrumb and config-panel suites.
    • The objectui#11162 demo pins assert the root class strings, which did not change.
    • The actions-slot pins are green: the full components suite is below, and the new pin asserts data-page-actions-slot in both layouts.

Tests (all at head 8ef26fe20, unless marked)

  • New pin: packages/components/src/__tests__/page-header-breadcrumb-ignored-11166.test.tsx. It mounts through the real SchemaRenderer and registry, in both layouts.

    • In each layout, three authored spellings are tested: properties.breadcrumb: true, breadcrumb: true on the node, and properties.breadcrumb: false.
    • Each renders no slot and no breadcrumb text anywhere in the header, with no console.error.
    • Each renders the same header markup as the absent-key CONTROL, with React ids blanked. The control asserts the h1 and data-page-actions-slot, so the test cannot pass on an empty header.
    • Result: Tests 8 passed (8), run at 26bc72b6e, where containers.tsx has the same blob as head.
  • @object-ui/components plus examples/schema-catalog. Run as pnpm exec vitest run packages/components/ examples/schema-catalog/ --shard=N/4, four shards under the verify lock. The unsharded run hit the 560s timeout on the shared box, so it was sharded.

    Shard Test files Tests
    1/4 94 passed 1354 passed, 7 skipped
    2/4 94 passed 1534 passed, 10 skipped
    3/4 93 passed 1796 passed
    4/4 92 passed, 1 skipped 827 passed, 7 skipped

    That is 374 files, which equals the 376 *.test.* files git ls-files lists minus two .snap files.

  • Cross-package readers: 75 files, 75 passed, 2006 tests. This covers every test outside those two trees that reads containers.tsx or the two edited docs pages from disk, or names page:header. That includes:

    • registry-inputs-spec-parity, public-contract and component-input-union-specimens;
    • widget-dom-leak-sweep, block-config* and PageBlockInspector.i18n;
    • the RecordDetailView header suites, page-breadcrumbs-refusal-8871 and zod-mirror-parity;
    • scripts/__tests__/check-doc-component-types and doc-version-claims.
  • Type-check. pnpm --filter @object-ui/components type-check exits 0, after pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build. tsc -p tsconfig.test.json --listFiles includes the new pin.

Ablation (fix committed first; mutation and restore via ablation-replace.mjs WRAP mode)

Each leg re-planted the old read-plus-slot in one layout only.

  • Bare leg: 4 failed, 4 passed. Exactly the four bare rows went red: the control, both true spellings, and false because it no longer matches the control.
  • Record leg: 4 failed, 4 passed. Exactly the four record rows went red.
  • Restore. After both legs, the blob equals HEAD (c07608e4cc2a), git diff HEAD is empty, and git status is clean.

Gates

Pass (exit 0):

  • check:doc-types
  • check:doc-snippets: 679 of 679 blocks judged, 0 failed. It ran after turbo run build over its own --build-filter closure (35/35 tasks); the first run exited 2 on unbuilt packages.
  • check:doc-examples: exited 2 on the first run, then passed after the same build.
  • docs:check-links
  • check:doc-fences
  • check:doc-example-ids
  • check:doc-example-readers
  • check-doc-expression-carriage
  • check:control-bytes, plus a self-scan of the five changed files for control bytes, which found none
  • check:new-line-citations
  • check-changeset-presence
  • changeset:check
  • check-changeset-overwrite
  • check:changeset-claims
  • check:pending-changeset-literals
  • check:installed-pin-claims
  • check:handler-key-reads
  • check:action-forward-parity
  • check:element-data-source-declaration
  • check:designer-field-key-parity
  • check:prompt-keys
  • check:spec-symbols
  • check:unreferenced-sources
  • check:readme-exports
  • check:test-path-roots
  • check:vi-mock-specifiers / inherit / override-shape
  • lint:coverage
  • type-check:coverage
  • check:phantom-deps
  • check:self-import
  • check:i18n-designer-parity
  • check:metadata-write-doors

Not governed. check-governed-queue-guard --test over the five paths reports NOT GOVERNED. The PR stays draft because it was dispatched.

NOT MEASURED: check:sdui-registration-pins. Reason: it needs a console bundle (apps/console/dist/assets) and exits 2 without one. This diff adds or removes no registration and moves no sideEffects. Left to CI.

ESLint, a proven narrowing of CI's pnpm lint:

  • Population, read from eslint's own config: of the five changed files, only the two .tsx files match a configuration. For the .md / .mdx / changeset files eslint reports "File ignored because no matching configuration was supplied".
  • Count, read from --format json: 2 files, 0 errors.
    • The new pin has 0 messages.
    • containers.tsx has 97 warnings, identical to base by rule: no-explicit-any 89, only-export-components 7, exhaustive-deps 1. Base was read via --stdin at the same path.
  • Invariance: eslint.config.js enables no type-aware linting (no parserOptions.project / projectService), so this diff cannot move a verdict on an untouched file.

Acceptance notes (not filed)

Out-of-scope finding (reported to the seat for filing, class c)

The Studio page designer still offers a "Show breadcrumb" toggle for page:header.

  • The producer is BLOCK_CONFIG['page:header'] in packages/app-shell/src/views/metadata-admin/previews/block-config.ts, labelled by engine.inspector.pageBlock.field.page:header.breadcrumb (en "Show breadcrumb", zh "显示面包屑").
  • It writes breadcrumb into stored page metadata, which the renderer now ignores. Once objectstack#20758's tombstone is installed, the platform will refuse the whole node by name.
  • That file's own icon precedent pin says "A retirement that leaves its producer standing makes the failure worse, not better."
  • block-config.test.ts pins the field set as ['title', 'subtitle', 'breadcrumb'].
  • It is not fixed here: it adds packages/app-shell and i18n to the verification surface, which fails condition ④ of the bounded in-place fix.

Generated by Claude Code

The renderer no longer reads `breadcrumb` and drops both empty
`data-page-breadcrumb-slot` divs (bare and record-chip layouts). Nothing
ever filled the slot, and the console's app header already draws the
trail, so triage ruled RETIRE (the spec half is objectstack#20758). The
spec still accepts the key until that lands, so an authored value keeps
rendering without error: it is ignored. The registration keeps the input
declared, now described as inert, because the parity ledger's reverse
direction reads inputs against the accepted spec keys.

Pinned in both layouts through the real SchemaRenderer: `breadcrumb: true`
(in `properties` and on the node) and `false` render no slot and the same
header as the absent key, with no console error.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
`page-header.mdx` stops listing the key: the prop-table row, the "renders
an empty slot element" paragraph and the diagram's slot line go, replaced
by one sentence saying the key is accepted until the spec retires it and
is ignored. `guide/layout.md` described the key as a display toggle for
the slot above the title, which this change makes false: its Schema API
line goes and the three cross-references now say it draws nothing. Its
overview bullet stops promising breadcrumbs from PageHeader, which draws
none in either renderer.

Changeset: '@object-ui/components' patch, naming the visible shift (the
title moves up by the removed gap-2 / mb-1).

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: components tests labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 8 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6771-retire-body-child-list-dialect.md

  • names renderers/layout/containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    Non-rendering readers keep their arm on the same rule, and none of them renders anything: while a renderer still reaches stored body content, a reader that must see the SAME content keeps its arm, or the renderer draws what the reader cannot find. Those are the two tab-subtree walkers in renderers/layout/containers.tsx, app-shell's pageSchemaIntrospect (CONTAINER_KEYS) and PageBlockInspector (STRUCTURAL_PROP_KEYS, the inspector half of a stored properties.body), and the CLI's OBJECTUI_STRUCTURAL_KEYS — a file-IDENTIFICATION marker, where keeping body is what lets an old file still be recognised as an ObjectUI node and therefore refused, instead of silently not judged.

.changeset/6872-app-shell-branding-title-jsdoc.md

  • names content/docs/guide/layout.md → content/docs/guide/layout.md — edited by this change

    Correct the AppShellBranding.title doc comment. It read "Page title suffix (sets document.title)" while useAppShellBranding assigns document.title = title wholesale — nothing is appended; the caller composes the whole string (the console passes "App label — Product name"). That comment ships in dist/index.d.ts and is the only description a consumer sees on editor hover, so a reader who followed it passed a suffix-only fragment and got a truncated title with no error. The comment now carries the same wording as content/docs/layout/app-shell.mdx, and agrees with the AppShellProps tables in the package README and content/docs/guide/layout.md. No runtime behaviour changes; the wholesale assignment and the four-surface agreement are now pinned by tests.

.changeset/7182-declared-action-ids-one-rule.md

  • names containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    New on @object-ui/types, beside actionRendersAt: the pure resolveDeclaredActionIds(elements, registeredActions), with the DeclaredActionsResolution / DeclaredActionsRefusal result types (the shape classifier stays module-internal: called with no registry, the function already returns the registry-independent verdict a renderer needs before its lookup). Both renderers call it; the whole-array switch in record-quick-actions.tsx and the per-element normalisation in containers.tsx are gone. The rule is closed: a string is an id, a non-null non-array object is an inline definition, and any other element (null, a number, a nested array) is refused at its index too. An all-id array resolves by name in authored order, first registration winning on a duplicate name; ids that name nothing are reported back with their index for the caller to warn about once its lookup has settled.

.changeset/7926-page-node-refuses-actions.md

  • names content/docs/guide/layout.md → content/docs/guide/layout.md — edited by this change

    Scope. One key, by name; the node is NOT strict. A census over this tree read 91 authored page-tagged objects with a blind-spot reading of 8 unreadable sites, and found only actions (3 sites, all in content/docs/guide/layout.md) and breadcrumbs (1 site, its own question, untouched) surviving passthrough on a real page node — every other undeclared key belongs to a different declaration that merely spells type: 'page'. PageNodeSchema still passes unknown renderer props through.

.changeset/8155-app-root-residue-swept.md

  • names containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    Swept as a class, not as two coordinates. Every other place in this tree that stated app was a bound expression-scope root is corrected in the same change — the diagnostic copy and its byte-pin, the ambient-scope docblocks in @object-ui/react (SchemaRenderer, useExpression), @object-ui/core (ActionRunner.ParamDef.visible, RowPredicateOptions.scope), @object-ui/components (form.tsx, containers.tsx), @object-ui/plugin-detail, @object-ui/plugin-form (docblock and README), @object-ui/app-shell and the console app, plus fourteen test fixtures that transcribed the old bag with an app key. The fixtures in @object-ui/app-shell now call buildExpressionScope instead of transcribing it, so that pair cannot drift again.

.changeset/8871-page-node-refuses-breadcrumbs.md

  • names content/docs/guide/layout.md → content/docs/guide/layout.md — edited by this change

    Three author sites, all teaching passages in content/docs/guide/layout.md, and that count corrects the actions refusal's "1 site": its census reads every git-tracked JSON file, every json fence in .md/.mdx, and every TS/TSX object literal via the TypeScript AST (PR fix(types,docs): refuse actions by name on the page node, teach the shape that draws (objectui#7926) #8870), and it undercounted for two different reasons. The Schema API block declared breadcrumbs?: ArrayANGLE-BRACKETS({ label, href }) outright and its literal does carry type: 'page', but that literal sits inside a markdown typescript fence — a fence language the census's json-fence reader never visits, so it was never read at all. Best Practices §2 authored it on a fragment inside a json fence the census does read, but that fragment never writes type, so a page-tagged filter correctly excluded it. No example app, catalog fixture, template or customer document writes the key, so the refusal strands no authored document in this tree.

.changeset/9174-interpolate-fastpath-trim.md

  • names packages/components/src/renderers/layout/containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    At this change page:header's title/subtitle (and the record-title titleFormat) all went through interpolate() in packages/components/src/renderers/layout/containers.tsx. When the template contained a {token} the function collapsed and trimmed whitespace before returning; when it contained no { at all it returned the raw string untouched. A whitespace-only authored title (e.g. ' ') has no token, so it came back unchanged — truthy — and PageHeaderRenderer's {explicitTitle && ANGLE-BRACKETS(h1)} gate drew a blank h1. Because literalTitleText (page.tsx), which decides whether PageRenderer delegates its own heading to the authored header, already trimmed and correctly read "no title", PageRenderer also drew its own implicit heading — two ANGLE-BRACKETS(h1) elements on one document, the broken outline objectui#3434 closed, arriving through a different door.

.changeset/record-picker-label-placeholder-i18n-5637.md

  • names renderers/layout/containers.tsx → packages/components/src/renderers/layout/containers.tsx — edited by this change

    KNOWN GAP, unchanged by this release: the sibling label read sites in renderers/layout/containers.tsx compose translateLabel(pickLocalized(…), language), and that second helper is not applied here — translateLabel and its KNOWN_LABEL_DICT are module-private to that file. Only the locale-map resolution lands in this change; a plain-English string label is still rendered verbatim in every language, exactly as before.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 957f69520 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1785 pending declaration(s) that publish a body (2392 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3569.6 KB 3607.4 KB
Main entry chunk (gzip) 149.5 KB 350 KB
Entry file index-C60mfeKM.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.08KB 62.96KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 229.83KB 63.15KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8ef26fe20d40109318f4f6f0a2be26df23373456
Local-runs: none

Inputs: card #11166 (body; triage 5903152845; claim 5903543704; dev report 5904174524), PR #11172 (body, the 5-file list, the net diff against main at the head), the head's check-runs read with filter=latest, and head or base file reads for context through contents/PATH?ref=SHA (containers.tsx, registry-inputs-spec-parity.test.ts, block-config.ts and its test, placeholders.tsx, packages/layout/src/PageHeader.tsx, page-header.mdx, guide/layout.md at base, the 8871 test) plus one tarball read of the same head for a tree-wide grep. Nothing built, run or re-run.

Check-runs on the head (filter=latest, read 2026-09-30T04:51Z): 43 runs, every one completed — 40 success, 3 skipped (the two coverage-matrix placeholders and dependabot), 0 failure. Ten were still in progress when this review started (the eight test shards, Type Check, Spec Main Shape Gate, and Lint); all finished green within the wait. Those conclusions are the gate verdicts: the test shards answer the pre-existing pins and check:sdui-registration-pins, which the dev left to CI; Type Check, Lint, Build Docs, Doc Snippet Type Check, Build & E2E, Test (dist pins) and the four changeset gates are green.

① Derived judgments

  1. The render — right. The diff removes the read at containers.tsx:1508 and both data-page-breadcrumb-slot blocks (record layout above RecordTitleChip, bare layout above the title row). A tree-wide grep of the head finds breadcrumb-slot only in the new pin, the replacement comment and the changeset, and no .breadcrumb reader remains in containers.tsx, so every authored spelling (true / false, in properties or on the node) takes the absent key's path: nothing reads it, nothing renders it, and with the input still declared (item 2) the manifest raises nothing either. The shift is real and stated accurately: the removed bare div was a flex item of the root's gap-2 column (0.5rem, 8px at the 16px root size), the removed record div carried mb-1 (0.25rem, 4px) inside a column with no gap, and an empty block box has zero height, so the title moves up by exactly the gap or margin and the header heights the PR body tables (56 to 48, 60 to 56) follow. The root class strings are not in the diff and read byte-identical at head (flex flex-col gap-2 pb-4 border-b; ... gap-3 sm:gap-4 pb-4 border-b). A patch may carry this: it removes dead spacing under a RETIRE ruling, moves no accepted key, no class string and no export, and the only documented behaviour it drops is the empty element the reference page itself said nothing filled; the changeset names the shift in its own bold paragraph, and the Changeset Bump Policy and Changeset Claim Re-read runs are green.
  2. The registration — right, and required. The breadcrumb input stays declared (containers.tsx:2419) with a description saying it is ignored. Three readings agree that it must: (a) the installed spec (@objectstack/spec ^17.0.0, 17.4.0 resolved per the dev) still accepts PageHeaderProps.breadcrumb, and the parity file's reverse direction demands that a covered block publish every accepted spec key unless UNPUBLISHED_EXEMPTIONS names it, which would be an edit to objectui#11086's file; (b) the dev's probe shows that without the declaration an authored value draws unknown-prop, a diagnostic where the triage requires "renders without error" and silence; (c) the parity ledgers carry no page:header.breadcrumb row, NO_READ_SITE_TO_PIN is a member-pin reason for array and object arms only, and the file is untouched, so removing the read moves no row. The description is additive manifest text on an in-scope declaration (the claim named "the registration's declared inputs, if they list breadcrumb"). The entry becomes a forward-direction red the moment the pin carries objectstack#20758's tombstone, which is the correct exit and is written beside it.
  3. The docs — right. page-header.mdx at head no longer lists the key: the prop-table row, the "renders an empty slot element" paragraph and the diagram's (breadcrumb slot) line are gone, and the one remaining mention (:70) is a warning sentence, not a listing. One sentence over none is the right call while the spec still publishes the key as "Show breadcrumb", default true: without it the reference page leaves an author with the spec's promise and no correction. It is dated by name to objectstack#20758 and leaves at the pin bump (③ 6).
  4. The pins and the two-leg ablation — right. The new pin drives both layouts through the real SchemaRenderer and registry with three authored spellings against an absent-key CONTROL, and asserts no slot, no breadcrumb text in the header markup, no console.error, and byte-identical markup with React ids blanked; the control asserts the h1 and the data-page-actions-slot, so an empty header cannot pass. Each ablation leg re-planted the old read-plus-slot in one layout and reddened exactly that layout's four rows (false reddens because it stops matching the control), with the restore proven blob-equal to HEAD. The actions-slot pins stay green: the new pin asserts the slot in both layouts, and the head's test shards are the verdict on the pre-existing suites.
  5. No other public-surface change. The tree-wide grep at head finds no other published page, skill, README or renderer naming the slot; packages/layout/src/PageHeader.tsx's render body draws no breadcrumb element (its line-16 comment only paraphrases the spec node), so the guide/layout.md overview bullet was false before this PR and is true after it.

② Semver level

'@object-ui/components': patch — right. What the diff publishes: one renderer stops drawing an empty element (a 4px or 8px title shift, stated), one input gains a description, the accepted key set is unchanged because the contract still accepts breadcrumb and the registration still declares it, and no export, prop type or class string moves. content/docs is not a package. Clause-②: no — right: no accepted set moves in objectui; the key leaves the contract on the spec side (objectstack#20758) and reaches this repo through a later pin bump, which carries its own changeset. Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read and Changeset Fixed Group Check are green on the head.

③ Boundary flags

open_questions: [] — nothing to answer there. Each declared deviation:

  1. Surface extension into content/docs/guide/layout.md — forced, and minimal. At base the file said in three callouts (:202, :321, :701) and the Schema API excerpt (:303) that the key is a boolean display toggle for the slot above the title; the diff makes that false, and a published guide left false by a change is a rule-3 must-fix. Each hunk changes one phrase or line and nothing else. The overview bullet (:16) is the same defect class, was already false at base, and the fix is bounded to the word "breadcrumbs". Overlap with draft PR objectui#11069 (seat board read): its one hunk is @@ -179,9 +179,10 @@, base lines 179 to 187; this PR's nearest hunk is @@ -199,8 +199,8 @@, base lines 199 to 206, with 11 untouched lines (188 to 198) between them, and its other hunks (13, 300, 318, 698) lie farther away. A three-way merge conflicts only on overlapping or adjacent regions, so the two are disjoint, and the gap is wide enough to survive the small line drift a different base commit could introduce. mergeable_state: behind is a rebase question, not a conflict.
  2. The widened test gate — accepted. packages/components/ plus examples/schema-catalog/ is a superset of the lead renderers/layout/, and the page-header suites live in src/__tests__, so the widening was needed to reach them. Four --shard=N/4 runs partition one file set; 374 files run equals the 376 git-listed *.test.* entries minus two .snap. CI's own shards are the verdict of record.
  3. The input description — accepted. In the claimed surface (the registration), additive, and the only honest way to keep a declared-but-unread input from reading as live in the manifest.
  4. The amended-before-first-push commit — accepted. The amend preceded the first push, so no shared history was rewritten and no force-push occurred; the PR shows two commits.

Out-of-scope notes:

  1. (class c) The Studio "Show breadcrumb" toggle — real at head, a named producer, and it needs its own card. Escalated to the seat for filing. packages/app-shell/src/views/metadata-admin/previews/block-config.ts:401 declares { name: 'breadcrumb', kind: 'boolean' } on BLOCK_CONFIG['page:header'], labelled engine.inspector.pageBlock.field.page:header.breadcrumb (metadata-admin/i18n.ts:772 "Show breadcrumb", :3732 the zh label), and the inspector writes the key into stored page metadata; previews/__tests__/block-config.test.ts:409 pins the field set as ['title', 'subtitle', 'breadcrumb'], and PageBlockInspector.i18n.test.tsx:207-209 pins the label. The same test file's icon precedent (:396) states the rule: "A retirement that leaves its producer standing makes the failure worse, not better." Today the toggle authors a key the renderer ignores; once the pin carries objectstack#20758's tombstone it authors a node the platform refuses by name. The card must land before or with that pin bump; nothing blocks it, so it can land now. Not fixing it here was right: it adds packages/app-shell and i18n to the verification surface.
  2. The pin-bump carrier note — right. The breadcrumb inputs entry (the parity file's forward direction will red it), the "accepts it until objectstack#20758 retires it" sentence in page-header.mdx and its twin in guide/layout.md all leave with the objectui @objectstack/spec pin-bump PR that installs the tombstone. Noted for that carrier; no card is needed.
  3. The nav:breadcrumb inference — relayed, with a correction. The parity file's EXPECTED_WITHOUT_INPUTS does list nav:breadcrumb as registered and propless, but the registration is placeholders.tsx's PlaceholderRenderer (PALETTE_PLACEHOLDER_BLOCKS, a dashed scaffold registered only when no real implementation exists), not a renderer that draws a trail. The triage's "no objectui renderer" and the ledger's "registered" are both true. For objectstack#20758's measurement, through the seat; nothing for this PR.
  4. The inert gap-2 and the stale test comment — accepted as-is. The bare root now has one child, so gap-2 spaces nothing; keeping it is what "no class string moves" means and what the objectui#11162 demo pin asserts. page-breadcrumbs-refusal-8871.test.ts:311 still calls .breadcrumb "the live page:header toggle": an unpublished comment inside a LIT CONTROL that still fires (the designer field, the i18n keys and the pins keep the singular key above its threshold). Carrier none is acceptable for both.

Implemented-by: claude/issue-11166-page-header-breadcrumb-retire
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation package: components tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[seam] page:header's breadcrumb ("Show breadcrumb", default true) draws only an empty slot — the declared capability is never honoured

2 participants