Skip to content

fix(plugin-detail): the summary chip beside the H1 names its field by its label (#8729) - #9093

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-8729-summary-chip-accessible-name
Sep 11, 2026
Merged

fix(plugin-detail): the summary chip beside the H1 names its field by its label (#8729)#9093
os-steve merged 1 commit into
mainfrom
claude/issue-8729-summary-chip-accessible-name

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #8729

The summaryFields chips beside the record H1 carry no visible label, so
FIELD: VALUE is the whole of what a screen-reader user hears the field
called — and the name half was the raw stored column. One band of the page
exposed the database spelling, and it was the band only a screen-reader user
perceives.

Placeholders are spelled as words (FIELD, VALUE, NAMESPACE) throughout
this body: angle-bracket-shaped fragments are deleted when a body is saved,
including inside backticks and fenced blocks, so a table written with them
renders as though nothing changed (AGENTS.md, "GitHub 会改写你写进 issue/PR 正文
的字节").

The ruling, and why the implementation follows from it

Every sibling band on the same page already resolves a label through one
shared helper
: HeaderHighlight calls fieldLabel(objectName, field.name, field.label), DetailSection calls it with field.label || field.name. So
this is a divergence from an established in-repo pattern, not a missing
capability — the chip now makes the same call rather than growing a fourth
resolution path.

The chip is addressed by NAME (summaryFields: ['owner_ref']), so, unlike its
siblings — whose inputs are field objects that already carry a label — it has to
find the fallback it hands that helper. That chain is this render's own field
resolution, not a new one: the author's view entry, then the object schema, then
the stored name as the floor — the precedence enrichDetailField states and the
floor DetailSection spells. autoSummaryFields already merges the same two
sources to PICK the chip.

Census — the chip has THREE name sites, not one

A fix applied only to aria-label would have left one behind:

branch where the name lives moved
string the Badge's aria-label yes
percent (objectui#9072) the Badge's aria-label yes
cell-renderer (objectui#8732) an sr-only prefix span, not aria-label yes

data-summary-chip is a fourth appearance of the raw name and deliberately
keeps it: it is a machine handle for tests and automation, where the stored
column is the right answer.

PM assumptions — what held, what did not

  • A — fieldLabel callable here: HELD, with one correction. objectName IS
    in scope as schema.objectName (the same value the option-label branch three
    lines below already passes to fieldOptionLabel), and it is optional, so the
    call uses the siblings' own objectName || '' spelling. ⚠️ The card's note
    that "fieldLabel is in scope at the render site" was not accurate: the file
    called useSafeFieldLabel() but destructured only fieldOptionLabel.
    Adding fieldLabel to that destructuring is one of the two source hunks.
  • B — the objectui#8394 pin: the hazard is REAL, and BROADER than recorded.
    Measured, see below.
  • C — aria-label is the whole accessible name: HELD for two branches, FALSE
    for the third, and measured rather than assumed.
    The chip carries no
    title and no aria-labelledby anywhere. For the string and percent branches
    the computed accessible name (jest-dom over dom-accessibility-api) is the
    aria-label verbatim. For the renderer-backed branch it is the empty
    string
    : that Badge sets no aria-label by design, and a plain div is
    role="generic", which takes no name from its content — so the sr-only
    prefix buys what the user is READ, not an accessible name. The new pin asserts
    each branch by the mechanism that is actually live on it, and says so.
  • D — one spot: FALSE. Three, as censused above.
  • E — the card's anchors are stale: HELD. Re-derived by symbol
    (effectiveSummaryFields.map); no line is cited anywhere in this change.
  • F — a changeset is owed: HELD. check-changeset-presence printed
    3 source file(s) of 1 released package(s) changed, and this change adds no changeset before, and 4 source file(s) ... declares 1 changeset(s) after. A
    real patch bump, not an empty-frontmatter declaration: the accessible name
    of a published component changes for input it already accepts.

The pin hazard — MEASURED, and NOT re-baselined silently

The filing seat recorded that this change would move a pin objectui#8394 owns.
It does, and a census found it moves a second one the card did not name.
Both owning cards have landed — objectui#8394 via PR #8457, objectui#8464 via
PR #8732, both merged — so no in-flight work is being reached into.

Measured by running the four chip pins against the fixed source before touching
any instrument — 6 assertions in 2 files:

detailPage.emptinessAuthority-8394.test.tsx — 2 cases. Its chipFor
navigated by an aria-label prefix selector built from the STORED name, i.e. by
exactly the thing this change moves, so it matched nothing:

AssertionError: CONTROL: the filled summary field still renders its chip: expected null not to be null
AssertionError: the auto-detected status slot goes to `stage`, ... : expected null not to be null

⭐ That file's own docblock already records this failure shape, from
objectui#8506: "the harness navigated by exactly the thing that changed". The
remedy is the one the card itself suggested: chipFor now navigates by the
data-summary-chip handle. That is strictly better here than either spelling of
the name — it is a handle rather than a reader-facing string, so no display or
naming decision can move it, and it also finds the renderer-backed chips, which
carry no aria-label at all and which the old selector could never have seen.

That card's subject did not move. Every emptiness answer — which chip
renders, which does not, and that the picker and the render agree — is asserted
on the same fields with the same data. Exactly one assertion about the chip's
NAME is re-pinned, stage: Won to Stage: Won; its stated purpose, "the chip
that took the slot reads its value", is preserved.

summaryChip.objectValue-8464.test.tsx — 4 cases, all fixtures whose
authored label differs from their stored name:

expected 'Status: Negotiation' to be 'status: Negotiation'
expected 'Owner: Ada Lovelace'  to be 'owner: Ada Lovelace'
expected 'Stage: Negotiation'   to be 'stage: Negotiation'
expected 'Ratio: 12.3%'         to be 'ratio: 12.3%'

The VALUE half of every one of those strings is byte-identical, and that card's
whole RENDERER_BACKED table is untouched down to the byte because it declares
label: field — the two spellings coincide there, so the naming change is
invisible to it. That is itself the measurement that the change is confined to
the name. Both instruments carry a docblock note saying what moved and why.

objectui#9072's seam held exactly as its dev described it.
summaryChip.percentOneRule-8728.test.tsx asserts only that the name ENDS WITH
the on-screen percentage, and it is green before, after, and on both ablation
legs. Its percentage half is re-asserted from this side in the new pin, so this
card cannot take it along.

The new pin — agreement, not a string

summaryChip.fieldLabel-8729.test.tsx (9 cases). The headline case reads the
label out of the highlight strip's own DOM and requires the chip's computed
accessible name to be that label plus the value — so a repair that humanized the
name differently from its siblings is red, which a per-chip toBe(...) would
not catch. Controls: FLOOR (no label anywhere keeps the stored name),
STORED NAME IS THE LABEL (byte-identical announcement), and
THE PERCENTAGE HALF.

Ablation — predicted in writing, then run

From the committed fix; mutation proven by blob hash against the HEAD blob,
restore proven by hash plus an empty git diff HEAD, both legs restored under
an EXIT INT TERM trap. No build step is involved: the root vitest config
aliases these packages to source, which is why a source edit alone moves the
result.

leg prediction measured
A — fix removed, all three sites print the stored column 7 of 9 red; the two byte-identical controls green; the objectui#9072 seam file fully green exactly that (7 failed / 13 passed of 20 across both files)
B — label read locally instead of through the siblings' resolver only A TRANSLATED SESSION red exactly that (1 failed / 19 passed)

Leg B is the one that matters for the ruling: it is what discriminates "called
the siblings' helper" from "read the label off the schema", and only the
translated-session case can tell them apart.

Verification

  • pnpm exec vitest run packages/plugin-detail/164 files, 1537 tests, all
    passed
    (whole affected package, the only package this diff touches).
  • eslint packages/plugin-detail219 files, 0 errors (986 warnings, all
    pre-existing no-explicit-any). Narrowing evidence: the universe is
    per-package eslint . under turbo run lint, and the whole affected package
    was linted rather than only the 4 changed files; the count is read from
    --format json; and the flat config declares no parserOptions.project or
    projectService, so type-aware linting is off and a diff inside one package
    cannot move any untouched file's verdict. ⚠️ A first pass with
    --no-inline-config reported 3 errors in files this PR does not touch — that
    flag is an objectstack-ism, not this repo's invocation, and it was disabling
    legitimate inline disables. Not a finding.
  • node scripts/check-changeset-presence.mjs, check-changeset-no-major.mjs,
    check-control-bytes.mjs, check-new-cross-file-line-citations.mjs — all
    green; verdict lines quoted in the report.
  • node scripts/check-governed-queue-guard.mjs --test on this diff's paths:
    NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.
  • pnpm --filter "@object-ui/plugin-detail^..." build then
    pnpm --filter @object-ui/plugin-detail run type-checkboth green
    (tsc --noEmit plus tsc -p tsconfig.test.json). ⚠️ The closure build is not
    optional: a first type-check against an unbuilt closure reported
    Cannot find module '@object-ui/components' for the whole package — the
    stale-dist trap, not a reading. The new pin is proven to be INSIDE the
    type-checked set rather than assumed to be: tsc -p tsconfig.test.json --listFiles lists summaryChip.fieldLabel-8729.test.tsx.

Acceptance notes

  • The ablation legs are one-off proofs; no ablation artefact is left in the
    tree.
  • Session reference, as a code span so it survives a body edit:
    https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB.
  • needs:contract-review is hung on both carriers (this PR and card
    objectui#8729) per the claim's Clause-②: yes; the PM seat clears both on
    PASS.
  • Out of scope, noted and not filed: none. The no-explicit-any warnings in
    this package are a repo-wide style state, not a defect; the
    --no-inline-config reading above was my own instrument error.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB


Generated by Claude Code

… its label

The `summaryFields` chips beside the record H1 carry no visible label, so
`<field>: <value>` is the WHOLE of what a screen-reader user hears the field
called — and the name half was `fieldName`, the raw stored column. One band of
the page exposed the database spelling, and it was the band only a
screen-reader user perceives.

Every sibling on the same page already resolves a label through one shared
helper: `HeaderHighlight` calls `fieldLabel(objectName, field.name,
field.label)`, `DetailSection` calls it with `field.label || field.name`. This
is a divergence from an in-repo pattern, not a missing capability, so the chip
now makes the same call rather than growing a fourth resolution path.

The chip is addressed by NAME, so it resolves the fallback it hands that helper
from this render's own field resolution — view entry, then object schema, then
the stored name as the floor. All three of the chip's name sites move together:
the string branch's `aria-label`, the percent branch's `aria-label`, and the
renderer-backed branch's `sr-only` prefix. `data-summary-chip` keeps the raw
column: it is a machine handle, not a name for a reader.

Two instruments owned by other cards move, deliberately and not silently:

- `detailPage.emptinessAuthority-8394.test.tsx` navigated its chips by
  `[aria-label^="<storedName>: "]` — i.e. by the very thing this change moves.
  Re-derived onto the `data-summary-chip` handle (the objectui#8506 shape
  recorded in that file's own docblock), which no display or naming decision
  can move and which also reaches the renderer-backed chips the old selector
  could never see. Its emptiness answers are asserted on the same fields with
  the same data; one assertion about the chip's NAME is re-pinned.
- `summaryChip.objectValue-8464.test.tsx` re-pins four expected names whose
  fixtures author a label different from the stored name. The VALUE half of
  every one is byte-identical, and its whole `RENDERER_BACKED` table is
  untouched because it declares `label: field`.

objectui#9072's percentage-half assertion is untouched and re-asserted from
this side.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3491.2 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-Cqrt3SF0.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.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 500.20KB 114.67KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 208.91KB 57.80KB
fields (index.js) 247.14KB 62.34KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.50KB 19.97KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.21KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.59KB 65.23KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.51KB 40.89KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.07KB 14.32KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
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.66KB 3.50KB
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) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
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.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 14.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator Author

条款② 席内复核 — PASS on substance. ⛔ Not armed yet: CI is still running

Reviewing seat: domain:ui @ objectui, the dispatching seat for card objectui#8729 (contract-review.md:27). First review on this PR — comments read before writing (㊽).

⏳ As of this comment the four test shards, Type Check and Lint are in progress; everything else is green. This verdict is on the clause-② substance and the diff. Arming happens on all-green, not on this comment — and a red shard would reopen it.

The declaration is right

Clause-②: yes. The accessible name of a published component changes for input it already acceptsstatus: Negotiation becomes Status: Negotiation. That is the "changes a published function's observable answer" limb. The changeset is a real patch bump rather than an empty-frontmatter declaration, which is the correct reading: a behaviour change, not merely a test-file touch.

Why this is the non-escalating class

A divergence from an established in-repo pattern, not a new capability. HeaderHighlight calls fieldLabel(objectName, field.name, field.label); DetailSection calls it with field.label || field.name. The chip was the one band on the page that did not, and it is the band with no visible label — so FIELD: VALUE is the whole of what a screen-reader user hears the field called, and the name half was the database spelling. Making the fourth band make the same call is 恢复不变量.

⭐ The part I scrutinised hardest: it edits two OTHER cards' pins

This is normally where a diff goes wrong, and it does not.

The licence first. Both owning cards have landed — objectui#8394 closed by merged PR #8457 (verified at source, not accepted from the report), and objectui#8464's instrument is present on main as a modified file rather than an added one. ⇒ nothing in flight was reached into.

objectui#8394 — the harness was re-pointed, and to something strictly better. chipFor navigated by [aria-label^="<storedName>: "], i.e. by exactly the thing this change moves, so it matched nothing and two cases failed while the emptiness authority they guard was untouched. It now navigates by data-summary-chip, the machine handle. That is better than either spelling of the name: a handle no display or naming decision can move, and it finds the renderer-backed chips which carry no aria-label at all and which the old selector could never have seen.

⭐ That file's own docblock already records this failure shape from objectui#8506 — "the harness navigated by exactly the thing that changed" — so the remedy is the one the instrument itself prescribes, not an improvisation.

And the one NAME assertion was RE-PINNED, not loosened. stage: WonStage: Won, with the reason in place: "Re-pinned here rather than loosened, so this line keeps failing for a render-only fix." ⇒ the instrument is no weaker than it was. That is the distinction that matters and it was made deliberately.

objectui#8464 — the control is elegant and unprompted. Four expected names moved; every VALUE half is byte-identical, and the whole RENDERER_BACKED table is untouched down to the byte because it declares label: field, so the two spellings coincide there. ⭐ The dev's own reading is the right one: that table staying byte-identical is itself the measurement that the change is confined to the name half. A weaker diff would have asserted that; this one demonstrates it.

Every touched instrument carries a docblock note saying what moved, why, and whose card it belongs to. ⛔ No assertion deleted, none relaxed.

Assumptions falsified — three of six, and all three usefully

  • D — "one spot": FALSE. The chip has three name sites: the string branch's aria-label, the percent branch's aria-label, and the renderer-backed branch's sr-only prefix. A fix applied only to aria-label would have looked complete and left a third of the surface behind. The dispatch said one; the census said three.
  • C — "aria-label is the whole accessible name": HELD for two branches, FALSE for the third, and measured rather than reasoned. The renderer-backed Badge sets no aria-label and a plain div is role="generic", which takes no name from content ⇒ its computed accessible name is the empty string, and the sr-only prefix is what a reader is read, not a name. The new pin asserts each branch by the mechanism actually live on it, which is ㉞ applied without being told.
  • A — "fieldLabel is already in scope": inaccurate. The file called useSafeFieldLabel() but destructured only fieldOptionLabel. Small, and reported rather than silently absorbed.

Those were my assumptions, written into the brief as mine. All three being wrong is the ZONE 2 block doing its job.

Also credited

  • Ablation leg B is the leg that discriminates the ruling — "called the siblings' helper" vs "read the label off the schema" are indistinguishable except on a translated session, and that is the single case leg B predicts red. Predicted 1 red, measured 1 red. Choosing a mutation that can only fail for the right reason is harder than choosing one that fails loudly.
  • The --no-inline-config reading was retracted as the dev's own instrument error, not filed as a finding — correct, and it is a genuine cross-repo trap: that flag is an objectstack-ism and it disables legitimate inline disables here.
  • The stale-dist trap named, not worked around: a type-check against an unbuilt closure reported Cannot find module '@object-ui/components' for the whole package. Recorded as a prerequisite failure rather than a reading — ㉛.
  • objectui#9072's seam held, exactly as that PR's dev described it when they left it: the percent pin asserts only that the name ends with the on-screen percentage, so this card could take the name half without disturbing it. A handoff honoured on both ends.
  • The body spells placeholders as words because angle-bracket fragments are stripped from bodies even inside backticks — the live half of ㉜, correctly worked around.

Outstanding before this lands

  1. ⏳ CI green on 9bb5eca6 — four shards, Type Check, Lint.
  2. ⚠️ mergeable_state: behind. Not blocking on its own and the merge queue rebases — but ⛔ if a shard comes back red, ㊼ applies before any re-run: check whether a fix already exists on main and this base predates it (objectui#9075 landed at 16fc4cf20 and objectui#9080 is queued ahead of this).
  3. Carriers cleared on this PASS, then ready-for-review, then arm.

PM seat · domain:ui @ objectui · seat post objectui#5560 §0a · in-seat review of record, default tier


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 11, 2026 00:40

Copy link
Copy Markdown
Collaborator Author

needs:contract-review CLEARED on both carriers — provenance

Per contract-review.md:38 (放行 = 清标即落地:PASS ⇒ 同席同笔剥双载体):

carrier before after
PR objectui#9093 plugin, tests, needs:contract-review plugin, tests
card objectui#8729 pm:dispatched, priority:p3, domain:ui, needs:contract-review pm:dispatched, priority:p3, domain:ui

Authority: the in-seat clause-② review of record, verdict PASS — comment 5627590282.

Pre-landing check ② (:42) satisfied on the record. The dev hung both carriers additively per os-dev.md:288, read both back, and reported check-clause2-carriers --pair 9093 exit 0. Full hang → in-seat review → clear sequence, in that order. Fourth pair this session to run it correctly end to end.

Marked ready for review; auto-merge armed, which merges when the remaining checks pass rather than now.

⛔ Standing down for the day — read this before assuming someone is watching

This seat is going off shift on the maintainer's instruction. State left deliberately:

  • CI was still running at review time (four shards, Type Check, Lint; everything else green). Auto-merge will land this only if they all pass. ⚠️ If any shard comes back red, nobody is watching it — this PR then needs a human or the next seat, and ⛔ ㊼ applies BEFORE any re-run: check whether a fix already exists on main and this branch's base predates it. mergeable_state was behind, objectui#9075 landed at 16fc4cf20, and objectui#9080 is queued ahead of this one.
  • pm:dispatched and the assignee are still on card objectui#8729 and must be stripped explicitly when this lands — Fixes has never stripped either (29 for 29 this session).

Everything else is recorded in seat post objectui#5560 (R14).

PM seat · domain:ui @ objectui · seat post objectui#5560 §0a


Generated by Claude Code

@os-steve
os-steve enabled auto-merge September 11, 2026 00:41
@os-steve
os-steve added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 0b138da Sep 11, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-8729-summary-chip-accessible-name branch September 11, 2026 01:10
Sign up for free to 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.

bug(plugin-detail): the record page's summary chip names its field by the raw column name — the one band beside the H1 that does not resolve a label

2 participants