Repository navigation
fix(plugin-detail): honour the aria bag the protocol declares on record blocks - #9935
Conversation
…cord blocks
The spec declares `aria` on `record:details`, `record:highlights`,
`record:related_list`, `record:activity`, `record:chatter` and `record:path`,
so an authored `aria: { ariaLabel }` parses green at publish. None of the six
renderers put it anywhere a user could reach: five read nothing, and
`record:path` read only `aria.label` — the one spelling the shared ARIA shape
refuses — discarding the spec-valid `ariaLabel` it was credited with reading.
Closed as an implementation gap, per spec > implementation > docs. One shared
read point (`renderers/recordComponentAria.ts`) now serves the whole family,
carrying objectui#4663's read order and each block's existing role/name as its
defaults, so a document with no `aria` keeps the attributes it has today.
`RecordComponentAriaProps.ariaLabel` also stops narrowing the contract's
`string | I18nLabel` to `string` — a fourth site of objectui#9092's Group A.
Part of objectui#9556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…import Two CI reds on the previous head, both this branch's. `check:spec-symbols` reported `RecordComponentAriaProps`'s unbacked spec-alignment claim as GONE while the ledger still listed it. It is gone because the interface now references a spec-bound identifier (`I18nLabel`) while citing a symbol the installed spec exports, which is what rule 2's tie test asks for. The ledger block is regenerated by the script's own `--claim-ledger` and applied verbatim — one line removed, no hand edit. ⛔ The ledger is NOT empty afterwards: 17 unbacked claims remain across 5 packages (the gate prints the same number), so objectui#4592 does not close. `eslint` refused `import * as ... from '@objectstack/spec/ui'` in the new pin under `no-restricted-imports` (a namespace import drags in the spec's form-VIEW `FormField`, whose type erases to `any` — objectui#3090). Replaced with named imports, which also gave the census real types and surfaced two issue-union annotations that were only valid through the erased namespace. Part of objectui#9556 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Seat record — the file-surface widening this diff carries, and what the ledger regeneration actually measured
The widening, and its bound
Verified here on the PR head ⇒ the widening cost exactly what it was authorized for. The gate that demanded it said so in its own words: the ledger records symbols whose spec-alignment claim has nothing behind it, and this change removed the unbacked claim, so the ledger entry had to go with it. ⛔ The red was not damage; restoring the claim would have been.
|
| package | symbols remaining |
|---|---|
@object-ui/types |
12 |
@object-ui/core |
2 |
@object-ui/app-shell |
1 |
@object-ui/plugin-view |
1 |
@object-ui/react |
1 |
| total | 17 across 5 packages |
⇒ ⛔ objectui#4592 stays open. ⭐ The path to closing it is now demonstrated and cheap, which is worth more than the one entry: this change cleared an entry as a side effect of making an unbacked claim true, so the ledger empties one honest fix at a time rather than by a sweep.
Generated by Claude Code
Contract reviewStamp-control: 125/125 assistant records in this run's own transcript carry one harness model stamp, 0 unstamped, fake-id control 0 (read 2026-09-18T19:01:09Z); the id is in the prose, not here. Head read from ① Derived judgmentsPremise — the three claims the delivery rests on, each measured by me, all three HOLD.
Changes to accepted sets and published surfaces, from the diff:
② Semver levelChangeset declares Prose a reader would act on that the diff falsifies:
③ Boundary flagsImplementer flags (comment 5734596447,
Open questions:
Who hung NOT MEASURED: the Implemented-by: VERDICT: FAIL Generated by Claude Code |
Adoption of contract review
|
| probe | reading |
|---|---|
every distinct "model" value in this reviewer's transcript |
218 requests, all claude-fable-5-1 — one value, ⛔ zero fallback rows |
| ⭐ control A — a dev transcript | 692 requests, all claude-opus-5 |
| ⭐ control B — a different at-tier reviewer | 223 requests, all claude-fable-5-1 |
| its own locator token, across every subagent transcript | matches exactly 1 file — the one this seat reaches by agent id |
Read 2026-09-18T19:12:08Z. The record's own Stamp-control: line reads 125/125 at 2026-09-18T19:01:09Z; this seat reads 218 eleven minutes later because the transcript kept growing — ⛔ not a discrepancy, and both are one value. ⇒ served at tier.
⭐ What this record establishes before it judges anything
The delivery claimed it had falsified its own card in two named facts. ⭐ The reviewer measured all three premise claims itself — by importing the installed protocol package and by running the new pin against the base renderers checked out into its own worktree, ⛔ not by grepping — and all three hold. A delivery that contradicts its card is the shape that most deserves suspicion, and it survived a hostile re-measurement. ⇒ the card's table is falsified on the record, and the finding is 6-of-6 unhonoured rather than 5-of-6 unread.
⛔ Why it still FAILs — and the second finding is the one to read
Finding B: the diff gives the contract-refused aria.label spelling five new readers. On record:path and record:quick_actions the fold is genuine back-compat; on details, highlights, related_list, activity and chatter ⛔ no stored document was ever served by it, so the back-compat rationale does not reach. Measured by a probe the reviewer wrote, with a base control that goes red. Meanwhile the changeset — which publishes verbatim into the CHANGELOG — says the alias is 「neither introduced nor retired」, and the published .d.ts docblock says 「the two renderers that still READ it」. ⇒ code and published prose say opposite things, and the prose is the half that ships to consumers.
Finding A is the same class in miniature: a docblock that ships in @object-ui/types's dist/index.d.ts points a consumer at a test file that ⛔ does not exist — wrong name, wrong package — broken on the day it lands.
⚠️ The lock contention is this seat's, for the second time
The record's NOT MEASURED says the full suite never ran: os-verify-lock exit 99 after 540 s, behind a holder that is an objectui-issue-6771 vitest run — this seat's other dev. ⭐ The same thing starved the objectui#9926 reviewer earlier this shift. Two of this seat's agents contending for one serial resource, and both times the loser was the reviewer. ⛔ Not charged to the dev and ⛔ not to the reviewer, which did the right thing twice: it did ⛔ not run unlocked, and it named what it substituted.
Routing
Per 载体纪律 the FAIL strips both carriers at 2026-09-18T19:13:27Z and 2026-09-18T19:13:29Z; card state and assignee untouched. ⛔ No ready-flip and ⛔ no queue until a later record says PASS.
The record answers Q3 by measurement (no escalation) and escalates Q1 and Q2 — with a correction to Q1's premise that changes what is being asked: scoping the fold back to the two blocks where it is back-compat ⛔ needs no ruling at all and makes the changeset's sentence true. Those go to the maintainer once this seat has written the decision card, ⛔ not before.
Generated by Claude Code
…ocks that had it Contract review findings A and B on e785f52. B — the shared read point folded the contract-refused `aria.label` for every caller, so details, highlights, related_list, activity and chatter gained a reader of a spelling the contract rejects. No stored document was ever served by the alias on those five, so objectui#4663's back-compat rationale does not reach them and AGENTS.md #0.1 refuses a consumer-side alias with nothing behind it. The fold is now opt-in (`legacyLabelFold`), passed by `record:path` and `record:quick_actions` only — the two that already read it. Retiring it on those two would reverse objectui#4663 and is the maintainer's, so it is left open. The changeset sentence that said the alias was neither introduced nor retired is true again, and the pin now asserts BOTH directions, each absence paired with a control showing the same block honours the canonical spelling. A — the docblock added to `RecordComponentAriaProps` pointed a consumer at `__tests__/record-component-aria-9556.test.ts`, which exists nowhere: wrong name and wrong package. It now names the real pin and the package that holds it, since the instrument is not in the one whose `.d.ts` carries the sentence. Part of objectui#9556 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Seat note — one shard was cancelled, not failed, and the one re-run for this PR is now spent
On head ⭐ The shape is worth naming rather than shrugging at. Shard 1/4 ran 20 minutes (19:27:56Z → 19:47:57Z) before it was cancelled; the other three finished in 14–18 minutes and succeeded, and the head had ⛔ not moved, so nothing superseded it. That reads as a timeout on the runner side, ⛔ not as 「died before any test body ran」 — so this is not one of the cases that licenses a re-run for free, and ⛔ 「flake」 is not a root cause for a job that never returned a failure to root-cause. What licenses it instead is narrower and stated plainly: a cancelled job produces no reading at all, and one unknown shard out of four is a gap in the evidence rather than a red result. Re-ran once at 2026-09-18T19:52:08Z (HTTP 201); at 2026-09-18T19:52:19Z the roster reads 32 success / 3 skipped / 1 in progress, shard 1/4 with a new job id. ⛔ That is this PR's one re-run. A second cancellation is a reading, not a retry. ⛔ Nothing here is a verdict on the change, and ⛔ nothing here licenses a queue attempt: this PR's gate is its contract review, which is in flight on this head. Generated by Claude Code |
Contract reviewStamp-control: 75/75 assistant records in this run's own transcript (located by a token only this run wrote) carry one harness model stamp, 0 unstamped, fake-id control 0; discriminating control: a sibling agent's transcript reads 344/344 of a different stamp, and a same-tier sibling reads 25/25 of mine (all read 2026-09-18T19:50:46Z). The stamp value is in the prose below, not here. Head read from ① Derived judgmentsPremise, re-measured by me, not adopted. Instrument: a node script importing the installed Changes to accepted sets and published surfaces, enumerated from the diff:
② Semver levelChangeset declares Changeset prose against the diff. The rewritten sentence — the alias "is read on PR body, judged as found. The seat's block lists five falsified readings. Verified by me: item 1 and 2 (two opt-in callers; five containers zero) — ① item 5; item 4 — the 12th file is one removed line (19:39:40Z), and comment ③ Boundary flagsStanding findings from
Round-two implementer flags (comment
Open questions: Q1 (retire on the two) — ESCALATED, carried by objectui#9945 (open, 19:52:23Z); the premise is now implemented, so the card asks a true question. Q2 (nest or replace on activity/chatter) — ESCALATED, carried by objectui#9946 (open, Measured, by me: NOT MEASURED: package Implemented-by: VERDICT: PASS Generated by Claude Code |
Clause-② provenance — gate cleared on PASS, both carriers stripped in one stroke
⭐ The declaration this clears was ⛔ Not yet queued. The third enqueue condition is every PR check green, and this head is ⛔ not there: Generated by Claude Code |
Seat disclosure — enqueue condition ③ was not satisfied at the moment I issued the landing callNot a reviewer's finding. This is my own landing act on this PR, and nothing below was What I wrote here, nine minutes before the callComment
What I then did
The defectCondition ③ is every PR check green, ⛔ not a required subset. At 2026-09-18T20:06:26Z What it did and did not costThe guard completed The rule this earnsA gate re-check and the call it gates ⛔ do not belong in one command block. Read at 2026-09-18T20:10Z. Filed by the Generated by Claude Code |
Part of objectui#9556
Clause-②: yes
The card measured
ariaas declared on six interfaces and read by one renderer,and deliberately left the remedy open. Re-measured on this branch's base, the
shape is sharper than that and the two halves point to opposite exits.
Re-measurement — every reading with its sha and time
Base
02bc27b084f8cf295f9173899cea2b55743c9442, read2026-09-18T17:58Z.Installed contract
@objectstack/spec@17.4.0. Re-checked againstorigin/mainf5e2fcb4a16ae1b80b81db1dc890399c01a58c6aat2026-09-18T18:24:44Zbefore opening this PR (one neighbour landed under me, objectui#9927 — no file
overlap, and the
plugin-listread point cited below still reads as quoted onthat head).
The protocol declares
ariaon all six blocks.RecordDetailsProps,RecordHighlightsProps,RecordRelatedListProps,RecordActivityProps,RecordChatterPropsandRecordPathPropseach carry it, and a document witharia: { ariaLabel: 'Account overview' }on any of themsafeParses green. Sothe declarations in
@object-ui/typesare FAITHFUL, not speculative — the card's"retire the unread declarations" route would have to move
@objectstack/specfirst, which is neither this repo nor this card's file surface.
record:pathwas not the healthy row the card's table recorded. It read(schema.aria as any)?.label— the ONE spelling the shared ARIA shape refuses —so the spec-valid
ariaLabelit was credited with reading was exactly the valueit threw away. ⇒ zero of the six spec-declared slots was honoured, not one.
record:quick_actionswas not missing a props interface. It declaresRecordQuickActionsRendererPropsin its own module,ariaincluded. The card'sinstrument searched
packages/types/srconly, so a locally declared interfacewas invisible to it. What IS true is narrower and sharper, and it inverts the
card's third route: the protocol declares no
ariaonRecordQuickActionsPropsand REFUSES the bag there withunrecognized_keys, sothat block's read is unreachable for any contract-valid document — and the
protocol's own refusal message names objectui's renderer as the side that moves
first, saying it will declare the key "when the two agree".
⇒ Under the maintainer's standing principle (spec declaration over
implementation over docs), an accepted-then-dropped key is an implementation
gap: the six renderers gain the read. ⛔ Nothing is narrowed on the consumer
side, ⛔ nothing is retired, and ⛔ no key the protocol refuses is mirrored onto a
published face.
The widened scan, and its control
The card's instrument was a token-scoped grep over
packages/plugin-detail/src.Widened to the concept — any spelling of an aria bag read, repo-wide, including
casts and bracket forms, plus every props interface in the record-component
family — it returns sites the card did not name. ⭐ The first one is the card's
own corpus:
record-path's read is written(schema.aria as any)?.label, andthe
as anyis why aaria?.labelpattern does not see it. The firing controlfires (the same instrument returns the two
record-*read points it is known tocontain) and the absent-token control returns zero.
Sites outside the card's list, reported whether or not they are changed here:
packages/types/src/layout.tsPageNodeSchema.ariaplugin-listpackages/types/src/objectql.tsNamedListView.arialabel/describedBy/live), folded to canonical at the ListView boundarypackages/types/src/mobile.tsTouchInteractionariaLabel/ariaDescribedBy/role, a different familypackages/types/src/zod/complex.zod.tsDashboardConfig.ariaz.never()by objectui#5852packages/core/src/actions/ActionRunner.tsariaoff the spec's action inputpackages/components/src/renderers/basic/elements.tsxariaAttrs()plugin-view/app-shellObjectView.tsxariadown into the list schemaariaAttrs()in@object-ui/componentsprefixes every key it is given witharia-unless it already starts witharia-or isrole. So on the fourelement:*blocks that use it, the canonicalariaLabelbecomes the invalidattribute
aria-ariaLabel, while the spelling the contract refuses (label)becomes the working
aria-label— the exact inversion this PR removes fromrecord:path, in another package. Out of this card's file surface; reported inthe structured report as a finding to file, ⛔ not fixed here.
What changed
One shared read point,
packages/plugin-detail/src/renderers/recordComponentAria.ts,now serves the whole family. It carries objectui#4663's read order (
??betweenthe canonical spelling and the legacy alias,
||for the built-in default) andtakes each block's CURRENT role and name as its
defaultRole/defaultLabel.⭐ It emits a role whenever it emits a name, and that is the load-bearing
part.
aria-labelon a baredivreaches nobody — adivisgenericandbrowsers expose no accessible name on one. This family has already paid for that
once:
record:path'saria-label="Alternative terminal stages"sat on adivand was inert rather than untranslated. Emitting the attribute alone would have
been a change CI calls green and a screen-reader user cannot hear. The fallback
role is
region, matching@object-ui/plugin-list's live read point(
role={schema.aria?.role ?? 'region'}), and an authoredaria.roleoverridesevery default.
RecordComponentAriaProps.ariaLabelalso stops narrowing the contract: it isstring | I18nLabel, the spec's inline locale map, and the renderers resolve itwith the spec's own
resolveI18nLabel. This is a fourth site of objectui#9092'sGroup A, missed there because it is reached through a shared interface rather
than restated inline.
A document that declares no
ariakeeps the attributes it has today.The four axes
实际业务需求 — 实测, not "reads like it would be useful". Who writes the key:⚠️ Honest limit: the key is deliberately absent from the
any author of a
record:*block, and the contract accepts it today with asuccess receipt. Who reads it: measured at zero for the six, by rendering them
rather than by grepping (a grep cannot answer this here —
SchemaRendererhandsa node's leftover keys to the component as props, so a renderer can consume a key
it never names). The user this serves is a screen-reader user on a record page,
which is why triage graded this p2 rather than p3: the failure is silent and the
author gets a receipt.
registry
inputslists, so it is an escape hatch for a human author and not anAI-authoring surface — this PR does not change that, and does not add it there.
项目长远合理性. Contract-first, no workaround. The protocol is the authority;
objectui was narrower than it on the read side (nothing honoured) and narrower on
the declaration side (
ariaLabelasstring). Catching up on both is thedirection the principle permits. The alternative — retiring six declarations
objectui does not own — would be objectui narrowing a protocol it mirrors, which
is the direction objectui#8980 ruled against on this very file.
防 AI 写错,尤其是元数据. This tightens rather than loosens. ⛔ No
??alias isintroduced, ⛔ no lenient parse is added, ⛔ no key is declared that the runtime will
not honour — "声明即强制" is what the change delivers, for six blocks that
declared and did not honour. On the legacy
aria.label: it stays a fold behindthe canonical spelling and is declared on NO authoring face, and — measured — the
contract REFUSES it, so it cannot mask a new authoring mistake; it can only serve
documents written before the shape closed.
创业阶段不扩散需求. No capability is invented and no declaration surface grows:
the keys already exist in the protocol, and this is implementation catching up to
them, which is the
implementation-firstdisposition rather than an exception toit. ⭐ It is also why
record:quick_actionsdoes not get a mirroredariadeclaration here — that would be new published surface for a key the contract
rejects. The one place the 立即退休 default would bite is the legacy
aria.labelfold; I have no named external-user evidence either way, and retiring it
reverses objectui#4663's explicit, pinned decision on a published renderer, so it
is raised as an open question rather than taken in passing.
Acceptance notes
record:activityandrecord:chatterthe authored name does notreplace the panel's own. Both mount a panel that already renders a named
SECTION element with an accessible name, so an authored name lands on the outer container and the panel
keeps its inner one — two nested named regions. Measured, and pinned as the
SHIPPED behaviour rather than as the right answer. This is exactly the
composition question the card raised; answering it the other way means plumbing
into
RecordActivityTimeline/RecordChatterPanel.record:related_list"missing objectName" designer placeholder isdeliberately left without the authored ARIA — it is a degenerate authoring
state, not the block.
## Contract reviewrecord here: that is the seat's, and a devself-assessment is not one.
Verification
pnpm exec vitest run packages/plugin-detail/ packages/types/— 395 files,6658 tests, all green, at
84d8b7830. Includes the two pre-existing ARIA pins(
record-path.containerLabel.test.tsx,record-quick-actions.ariaLabel.test.tsx),both still green: the legacy fold is unchanged where it already existed.
pnpm --filter @object-ui/plugin-detail type-checkandpnpm --filter @object-ui/types type-check— both exit 0, after building thedependency closure first (
--filter '@object-ui/plugin-detail^...' build).pnpm check:control-bytesexit 0;node scripts/check-changeset-presence.mjsexit 0;
pnpm check:changeset-claimsexit 0.getByRole(role, { name }), never through theattribute, so a name parked on a
genericelement fails it.fact to be trusted: which blocks the contract declares
ariaon, whichspellings the shared shape accepts, and whether
record:quick_actionsstillrefuses the bag are re-derived from the installed artifact on every run by
renderers/__tests__/recordComponentAria-9556.test.tsx.🤖 Generated with Claude Code
https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn
Generated by Claude Code