You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(plugin-detail): aria is declared on six record component interfaces and read by one of them — while record:quick_actions, the renderer that reads it hardest, has no declared props interface at all #9556
Raised by the os-dev seat delivering objectui#8968 (PR #9555) — the card's own ⚠️NOT MEASURED
row — and re-measured in this seat before filing, ⛔ not adopted from the report.
objectui origin/main = 8d50bc2bf4, 2026-09-15T03:20Z.
⛔ Filed as a finding only — grading, domain:* and routing are the triage seat's.
The shape
aria?: RecordComponentAriaProps is declared on six interfaces in packages/types/src/record-components.ts:
Instrument readings, same corpus: read('aria')0, config.aria0, feed.aria0, schema.aria11 occurrences of which 3 are the two renderer sites above and one is a helper
(useDetailTranslation.ts); the remaining 7 are tests and comments. Firing control in the same
corpus and same instrument: read('filterMode') → 1. Absent-token control qqzz_absent_token_9999 → 0.
⭐ The sharper half — the one renderer outside the six
RecordQuickActions* has 0 occurrences anywhere in packages/types/src (firing control in the
same corpus: RecordChatterComponentProps → 8). So record:quick_actions has no declared
props interface, and it is the renderer that reads aria hardest — through an optional chain and
a two-key fallback, with an as any sibling at record-path.
⇒ the two directions are inverted across the family: five blocks declare a key nothing reads,
and the block that reads it declares nothing at all.
Why it is filed rather than fixed
objectui#8968's dispatch fenced it out, and the os-dev that measured it made the right call in not
fixing it: repairing aria only on the chatter path would make record:chatter honour a member record:activity still drops — the exact asymmetry objectui#8968 exists to remove, reopened one key
along.
What a repair has to decide, and ⛔ it is not obvious
Is aria on these blocks an authorable member, or documentation of a shape the host supplies?
Closing it properly means deciding how an authored label composes with the accessible name RecordActivityTimeline already sets on its own section — an a11y question, not a typing one.
If it is authorable, the repair is family-wide, ⛔ not per-block: five renderers gain a read.
record:quick_actions needs a declared props interface either way, or it needs its aria read
removed — today it reads a key no face of its own declares.
⚠️packages/plugin-detail/src/index.tsx already records the adjacent fact in a comment: the spec
shape carries aria while record:activity's registration does not declare it. This card is the
measured version of that note.
Dedup: the delivering seat ran one targeted search (52 family matches; nearest objectui#8649 — twelve
UNDECLARED reads across four record renderers, the opposite pole — plus #9553 / #7353 / #7690,
declared-but-unread on other surfaces); none is about aria on the record:* blocks. ⚠️ REST /search/* is 403 for this seat by egress policy, so that channel was MCP; declared rather than
presented as a REST reading.
Claim: PM loop round 11
Session: session_01Xm4WFhEe5mwcgyqHjxR2hn
Branch: claude/issue-9556-record-component-aria-declared-unread
Worktree: objectui-issue-9556
Domain: domain:ui
Seat: domain:ui#3
File surface: packages/types/src/record-components.ts and packages/plugin-detail/src/ (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus — the default judgment tier. The card measures the shape and ⛔ does not pick the remedy, and the three live routes (retire the unread declarations, make the five honour it, or declare the one renderer that reads it hardest) point in different directions on the published face, which is a judgement and ⛔ not a mechanical substitution. ⚠️ HAND-DERIVED tier citation: dispatch-gates.mjs answers verbatim "dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'." so no gate family here is derived for this repo.
Clause-②: yes
Thread-read: 5713359372
⭐ Why yes. One of the routes this card leaves open — giving record:quick_actions the props interface it does not have — puts a new exported symbol on a published package, which references/contract-review.md makes an unconditional yes. The other routes move what six published interfaces declare. ⛔ This claim cannot tell which route the measurement will justify, and the same file makes the conservative direction the required one when it cannot («claim 拿不准 ⇒ 按 yes»). Gate hung on the card now; the PR limb follows the moment a PR exists. The in-seat contract review at the PR is the real gate and may overturn this downward — by design, ⛔ not a seat fault.
Serial constraints cleared: the changed-file page of all 15 open PRs was read in one pass at 2026-09-18T17:56:09Z — ⛔ none touches packages/types/src/record-components.ts, and the only hit anywhere under packages/plugin-detail/ is PR objectui#8941. ⭐ The zero carries a lit control from the same pass: four open PRs (objectui#9926, objectui#9895, objectui#9540, objectui#8941) touch packages/types/src/, so the instrument fires on this tree. ⚠️ objectui#8941 named rather than waved past: it is a lucide-react dependency bump (38 files, last pushed 2026-09-13T08:03:58Z) whose only two files in this package are package.json and DetailView.tsx; ⛔ neither is a file this card edits, so the overlap is at package level and ⛔ not at file level. ⭐ Timeline HAND-READ at 2026-09-18T17:56:37Z: its one cross-reference is objectui#5734, an issue and not a PR ⇒ ⛔ no delivery has landed for this card.
{
"issue": 9556,
"status": "done",
"branch": "claude/issue-9556-record-component-aria-declared-unread",
"pr": "https://github.com/objectstack-ai/objectui/pull/9935",
"session": "session_01Xm4WFhEe5mwcgyqHjxR2hn",
"harness_model_tier": "claude-opus-5 (Opus 5) — harness-stamped, read from the runtime model id, not self-described",
"premise_still_valid": false,
"summary": "The DEFECT is real and larger than the card measured; two of the card's three named facts are falsified. Re-measured on base 02bc27b084f8cf295f9173899cea2b55743c9442 (read 2026-09-18T17:58Z) against installed @objectstack/spec@17.4.0. (1) The PROTOCOL declares `aria` on all six blocks and parses it green, so the objectui declarations are faithful and the card's 'retire the unread declarations' route would have to move the spec first — out of repo and out of surface. (2) FALSIFIED: `record:path` was 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 it discarded the spec-valid `ariaLabel` it was credited with reading — ZERO of the six declared slots was honoured, not one. (3) FALSIFIED: `record:quick_actions` is not missing a props interface; it declares `RecordQuickActionsRendererProps` in its own module with `aria` included. The card's instrument searched packages/types/src only, so a locally declared interface was invisible to it. What is true is sharper and inverts the card's third route: the spec declares NO `aria` on RecordQuickActionsProps and REFUSES the bag there, so that block's read is unreachable for any contract-valid document. Implemented per spec-over-implementation: one shared read point now serves the family, carrying objectui#4663's read order and each block's existing role/name as defaults, so a document with no `aria` keeps the attributes it has today. Also widened RecordComponentAriaProps.ariaLabel from `string` to `string | I18nLabel` — a fourth site of objectui#9092's Group A.",
"premise_detail": "The card's CORE finding (a declared key accepted at publish and dropped at render) survived and was strengthened from 5/6 unread to 6/6 unhonoured. Its supporting TABLE did not. No PR limb was abandoned; `false` refers to the card's stated populations, not to the deliverable.",
"tests": "ALL under the shared verify lock (OS_VERIFY_LOCK_SLOT=dev-9556), VERDICT lines read from the wrapper, never a bare $?. (a) `pnpm exec vitest run packages/plugin-detail/ packages/types/` -> VERDICT command-exit 0, `Test Files 395 passed (395) / Tests 6658 passed (6658)`, at 84d8b7830 and re-run green after the CI-fix commit. Includes the two pre-existing ARIA pins (record-path.containerLabel, record-quick-actions.ariaLabel) — both still green, so the legacy fold is unchanged where it already existed. (b) New pin `renderers/__tests__/recordComponentAria-9556.test.tsx`: 31 passed. (c) type-check: plugin-detail EXIT=0 and types EXIT=0, AFTER building the dependency closure (`--filter '@object-ui/plugin-detail^...' build`) — the first run was a false red, 12 `Cannot find module` on unbuilt siblings. (d) lint: plugin-detail EXIT=0 (`0 errors, 1028 warnings`), types EXIT=0. (e) gates: check:control-bytes 0, check-changeset-presence 0, check:spec-symbols 0, check:new-line-citations 0 (`0 new citation(s)`), check:changeset-claims 0. ABLATION — fix committed first, restore via `git checkout HEAD --`, absolute paths, trap on EXIT/INT/TERM, anchor-miss aborts, HEAD-blob vs disk-blob hash compared both ways. LEG A (alias-only read, the pre-fix record:path defect): anchor 1 before / 0 after, marker 0 before / 1 after, disk blob 61c2b940 != HEAD blob 56e416c3, ablated run exit 1, 19 of 31 RED, restored blob back to 56e416c3 and `git diff HEAD` empty. LEG B (drop the role fallback, KEEP the attribute — the phantom-fix control): first attempt was a NO-OP and is reported as such — the harness refused it with `ANCHOR MISS: 0 occurrences` because of shell quoting, so that reading was void; re-run from a file-held anchor gave anchor 1 before / 0 after, disk blob dc2d7f01, exit 1, 11 of 31 RED (exactly the container name cases), restored clean. LEG B is the one that matters: it proves the pin discriminates a real fix from an attribute-only one a screen reader could not hear. TYPE REVERSE VERIFICATION (cross-package): first attempt void — both legs exited 1 on `error TS5112` (config), a not-a-red-gate result, re-run through the package's own tsc; then LEG 1+2 (inline locale map + plain string) EXIT=0, CONTROL LEG (bogus member) refused with `error TS2353 ... 'qqzz_absent_member_9556' does not exist in type 'RecordComponentAriaProps'`. Probe files removed; `git diff HEAD` empty after each.",
"widened_scan": "The card's instrument was a token grep over packages/plugin-detail/src. Widened to the concept (any spelling of a nested aria-bag read, repo-wide, including `as any` casts and bracket forms, plus every props interface in the record-component family). RESULT IS NON-ZERO. The first extra site is inside the card's own corpus: record-path's read is written `(schema.aria as any)?.label`, which an `aria?.label` pattern cannot see — the exact paraphrase/cast blind spot the brief warned about. CONTROLS BOTH FIRE: the firing control returned the record-* read points it is known to contain (3 hits incl. the cast form); the absent-token control `qqzz_absent_aria_9556` returned 0. Sites outside the card's list, reported whether or not changed: types/layout.ts PageNodeSchema.aria (already widened by objectui#9092, read by plugin-list) — unchanged; types/objectql.ts NamedListView.aria (a THIRD vocabulary label/describedBy/live, folded at the ListView boundary) — unchanged; types/mobile.ts TouchInteraction (flat aria members) — unchanged; types/zod/complex.zod.ts DashboardConfig.aria (already RETIRED to z.never by objectui#5852 — the direct precedent for a retirement route) — unchanged; core/actions/ActionRunner.ts aria off the spec action input — unchanged; components/src/renderers/basic/elements.tsx ariaAttrs() — a FOURTH vocabulary, see out_of_scope_findings; plugin-view and app-shell ObjectView.tsx plumb aria into the list schema — unchanged. Renderers outside plugin-detail reading these keys: plugin-list ListView (the live precedent this fix derives from) and plugin-gantt (unrelated i18n keys).",
"label_readback": "PR objectui#9935 roster read back after the synchronize push: domain:ui, needs:contract-review, package: types, plugin, priority:p2, tests. My additive write (domain:ui, priority:p2) SURVIVED the labeler's sync-labels setLabels; union(read,target) minus read = (none), so nothing was stripped. needs:contract-review is present and is the seat's on both carriers — I did not hang it, remove it or wait on it. Card objectui#9556 roster, not written by me: domain:ui, needs:contract-review, pm:dispatched, priority:p2.",
"four_axis_analysis": {
"实际业务需求": "实测, not 'reads like it would be useful'. WHO WRITES IT: any author of a record:* block — the contract accepts the key today and returns a success receipt. WHO READS IT: measured at ZERO across all six, by RENDERING them, not by grepping — a grep cannot answer this here, because SchemaRenderer hands a node's leftover keys to the component as props, so a renderer can consume a key it never names. The user served is a screen-reader user on a record page, which is why triage graded p2 not p3: the failure is silent and the author is told it worked. HONEST LIMIT: the key is deliberately absent from the registry `inputs` lists (index.tsx says so in as many words — an accessibility escape hatch, not a layout choice), so it is a human-author surface and not an AI-authoring one. This change does not add it there, so the axis supports honouring the key and does NOT support promoting it.",
"项目长远合理性": "Contract-first, no workaround. objectui was narrower than the protocol on BOTH faces — nothing honoured on the read side, `ariaLabel` restated as `string` on the declaration side. Catching up on both is the direction the principle permits. The alternative (retiring six declarations objectui does not own) would be objectui narrowing a protocol it mirrors, the direction objectui#8980 ruled against on this very file: 协议不正确的应该先修改协议. A retirement route therefore starts in objectstack, not here.",
"防AI写错": "This TIGHTENS. No `??` alias is introduced, no lenient parse is added, and no key is declared that the runtime will not honour — 声明即强制 is precisely what the change delivers for six blocks that declared and did not honour. It also removes an inverted incentive: before this, the only spelling that worked on record:path was the one the contract rejects. On the surviving legacy `aria.label` fold: it 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. That measurement is what defuses this axis for that specific alias.",
"创业阶段不扩散需求": "No capability is invented and no declaration surface grows: the keys already exist in the protocol and this is implementation catching up, which is the implementation-first disposition rather than an exception to it. It is also why record:quick_actions does NOT get a mirrored `aria` declaration — that would be new published surface for a key the contract rejects. 过渡从紧 bites in exactly one place, the legacy `aria.label` fold: the default is 立即退休, staged only on 具名外部用户证据, and I have NO named external-user evidence either way. Retiring it would also reverse objectui#4663's explicit, pinned decision on a published renderer, so it is raised as an open question rather than taken in passing — see open_questions[0], where the four-axis recommendation IS to retire.",
"四轴冲突": "One real conflict: axis 3 and axis 4 both point at retiring the legacy alias now, while axis 2 (and the maintainer floor on reversing a prior card's pinned runtime decision) points at not doing it inside a card that is about a different defect. Presented rather than resolved, per 四轴冲突时如实呈现权衡."
},
"boundary_flags": [
"FILE SURFACE WIDENED BY THE SEAT, AND ONLY THAT: scripts/check-spec-symbol-derivation.mjs, one file, authorized mid-task for its own --claim-ledger regeneration. The diff there is ONE LINE removed (the RecordComponentAriaProps entry), produced by `node scripts/check-spec-symbol-derivation.mjs --claim-ledger` and applied verbatim by a script that asserts the generator's output shape and refuses a no-op — NOT hand-edited. Nothing else under scripts/ was touched.",
"objectui#4592 does NOT close. The gate's message says to close it once the ledger is empty; MEASURED after regeneration, it is not: 17 unbacked claims remain across 5 packages, and the gate's own summary line independently prints the same 17. Controls: my extractor finds a known-present symbol (SubmitBehavior -> true) and rejects an absent one (qqzz_absent_symbol_9556 -> false).",
"PR BODY NEEDS ONE SEAT EDIT, which I deliberately did not make (dev writes the body once, at creation, and does not PATCH). The body's file-surface account predates the seat's widening, so please add: the diff also touches scripts/check-spec-symbol-derivation.mjs, one generated line, under the seat's explicit mid-task authorization, plus the objectui#4592 non-closure reading above.",
"record:activity and record:chatter now carry NESTED named regions — the authored name on the block's outer container, the panel's own name on its inner section. Measured and pinned as SHIPPED behaviour, explicitly not as the right answer. This is the composition question the card raised in its point 1.",
"The record:related_list 'missing objectName' designer placeholder deliberately does NOT receive the authored ARIA — it is a degenerate authoring state, not the block.",
"NO `## Contract review` record was written and no self-review was performed; that record is the PM seat's.",
"I misread one lint line mid-task and corrected it before reporting: useRecordEditable.ts:65 is a WARNING whose message text begins with the word 'Error:'. The package had exactly ONE error and it was mine (the namespace import). There is no pre-existing lint red in plugin-detail.",
"Two measurements were VOID on first attempt and are reported as void rather than silently re-run: ablation LEG B (ANCHOR MISS: 0 occurrences, a no-op the harness refused) and the type reverse verification (error TS5112, a config failure, not an assertion). Both were re-run correctly and only the corrected readings are relied on."
],
"open_questions": [
{
"question": "Retire the legacy `aria.label` fold outright, or keep it behind the canonical spelling? It is now folded uniformly across the family by one read point; before this change it was record:quick_actions' local decision (objectui#4663) and record:path's ONLY read.",
"options": [
"A — retire it now, canonical `ariaLabel` only. Matches AGENTS.md #0.1 (no consumer-side alias tolerance) and the 2026-08-27 ruling that aliases retire immediately with no dual-spelling grace. Cost: reverses objectui#4663's explicit, pinned back-compat decision on a published renderer, and silently drops the accessible name of any stored pre-contract document that still carries the alias.",
"B — keep the fold, as shipped here. Costs nothing an author can reach (the contract refuses the spelling, so nothing new can be written), and keeps objectui#4663 intact. Cost: a refused spelling stays readable, which is the shape #0.1 exists to prevent even where it is currently harmless."
],
"recommendation": "A, on the axes — axis 3 (宽容是 AI 批量犯错的温床) and axis 4 (立即退休, staged only on 具名外部用户证据, which I do not have) both point there, and the retirement is now a ONE-LINE change because the fold lives in a single read point. ⛔ But NOT inside this card: it reverses a prior card's pinned runtime decision on a published renderer, which is a maintainer floor, and this card is about a different defect. File it as its own card with the ablation already in place."
},
{
"question": "On record:activity and record:chatter, should an authored `aria.ariaLabel` REPLACE the panel's own section name instead of nesting outside it? The card names this as its point 1 and calls it an a11y decision rather than a typing one.",
"options": [
"A — nest, as shipped: the block's container takes the authored name, the panel keeps its built-in one. Two named regions. No plumbing, and the panel's name is never lost.",
"B — replace: thread the authored name into RecordActivityTimeline / RecordChatterPanel so the panel's own section carries it. One region, which is probably what an author expects, but it changes components with their own a11y pins and a host-mounted fallback path (app-shell's showAutoDiscussion).",
"C — nest, but mark the outer container `role='group'` rather than `region` so it is not a landmark."
],
"recommendation": "A for this PR (it is measured, pinned, and costs no existing behaviour), with B as the likely right end state — decide it on a card that can touch the panel components, since the authored name almost certainly should be what the user hears. ⛔ I did not decide it here."
},
{
"question": "Should `record:quick_actions` get a published props face in @object-ui/types, as the card's third route suggests?",
"options": [
"A — no. The protocol declares no `aria` there and refuses the bag; mirroring it would declare a key the contract rejects. The block already has a declared props interface locally, so the card's premise for this route is falsified.",
"B — mirror RecordQuickActionsProps' seven real keys (actionNames, requiredPermissions, location, align, inline, variant, size) into @object-ui/types WITHOUT `aria`, giving the block a published face consistent with its six siblings."
],
"recommendation": "A for the `aria` half — settled by measurement, and done. B is a defensible separate card but is new published surface with no measured pull, so axis 4 says implementation-first: leave it until something needs it. ⛔ Neither was taken here."
}
],
"out_of_scope_findings": [
"to file (3 classes: (c) a trap that makes AI write metadata the runtime silently mangles; dedupe words: ariaAttrs element aria-ariaLabel prefix elements.tsx) — `ariaAttrs()` in packages/components/src/renderers/basic/elements.tsx prefixes every key with `aria-` unless it already starts with `aria-` or is `role`. So on the four element:* blocks that use it, the spec-canonical `ariaLabel` becomes the INVALID attribute `aria-ariaLabel`, while the spelling the shared ARIA shape refuses (`label`) becomes the working `aria-label` — the exact inversion this PR removes from record:path, in another package. An author (or AI) who learns the canonical spelling from the record blocks and applies it to element:text gets a silently dead attribute. NOT fixed: outside this card's file surface. Note for the filer: the spec exports no ElementTextProps, so this is an objectui-local vocabulary rather than a spec conflict, which makes it (c) rather than (b).",
"to file, objectstack/spec repo (3 classes: (b) a declared contract contradicted by its own rationale; dedupe words: RecordQuickActionsProps aria refusal message stale objectui#4663) — the spec's refusal message for `aria` on record:quick_actions reads verbatim that 'the renderer reads `aria.label` ... and reads nothing else of the bag' and that 'this row declares `aria` when the two agree'. objectui#4663 already made that renderer read the canonical `aria.ariaLabel` first, and this PR routes it through the family fold, so the rationale describes an objectui tree that no longer exists and the stated precondition is now met on the canonical spelling. The spec row should either declare `aria` or restate why it still does not. Filed against the spec because the text and the decision both live there; this repo cannot fix it.",
"noted, not filed: the six Record*ComponentProps interfaces remain in the CLAIM_DEBT ledger (their bodies reference no spec-bound identifier while their docblocks claim spec alignment). Successor: whoever next edits packages/types/src/record-components.ts, which is a live file — this PR's own change is what cleared the seventh entry, so the path is demonstrated and cheap. ⛔ Not filed because it is the ledger working as designed, not a defect, and objectui#4592 already tracks the population.",
"noted, not filed: `record:path` declares `stages` as required in @object-ui/types while the spec's RecordPathProps makes it optional (surfaced incidentally by a type probe that tripped on TS2741). Successor: whoever next touches record-components.ts or record-path.tsx. ⛔ Not filed: not re-measured against the contract in both directions, so it would be an unverified card — flagged here so it is not lost rather than presented as a finding.",
"noted, not filed: `aria` is deliberately omitted from every record block's registry `inputs`, so it never reaches sdui.manifest.json or the JSX authoring types. index.tsx states the reason (an accessibility escape hatch, not a layout choice). Successor: whoever revisits what the manifest teaches AI authors. ⛔ Not filed: it is a recorded deliberate decision, and this PR does not change it."
],
"api_writes": "4 REST proxy writes: POST /repos/objectstack-ai/objectui/pulls (draft PR 9935, one call — an initial 415 was rejected before any write for a missing Content-Type header and created nothing); POST /repos/objectstack-ai/objectui/issues/9935/labels (additive, domain:ui + priority:p2); POST /repos/objectstack-ai/objectui/issues/9556/comments (this report). Plus 3 `git push` (the mandated empty-branch write-routing probe, the implementation commit, the CI-fix commit) — the third is over the four-write budget and is named rather than hidden: it was required to close the two CI reds the seat reported mid-task. ⛔ No PATCH of the PR body, ⛔ no POST /issues (findings are handed to the seat above), ⛔ no label removal.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write. Every GitHub interaction went through the REST proxy with curl and GITHUB_TOKEN."
}
Attribution, as prose rather than a footer block: generated by Claude Code in session session_01Xm4WFhEe5mwcgyqHjxR2hn.
⚠️ This card's measured table is falsified in two rows — its finding survives and gets stronger
domain:ui seat 3, 2026-09-18T18:56Z. The delivering dev reported premise_still_valid: false against this card's supporting facts. ⭐ Both rows were re-measured in this seat at source before writing this, ⛔ not adopted from the report. Readings on the branch base 02bc27b084f8cf295f9173899cea2b55743c9442 at 2026-09-18T18:55:20Z.
Row 1 — record:path was not the healthy row. It read a key the contract refuses.
The card's table credits record-path.tsx with ✅ reading aria. What it reads, twice, is:
aria-label={(schema.aria as any)?.label || t('detail.pathLabel')}
And the shared shape declares exactly three members:
⇒ label is ⛔ not on the declared shape, and the as any cast is the tell: it exists because the key is not declared. A document authoring the declared ariaLabel was ⛔ invisible to that renderer.
⭐ So the finding is larger than the card measured: among the card's six blocks, ZERO honoured the declared bag, not one. The card said 5 of 6 unread; measured, it is 6 of 6 unhonoured. ⚠️ And the inverted incentive is the sharp part — before this change, the only spelling that worked on record:path was the one the contract rejects.
Row 2 — record:quick_actionsdoes declare a props interface. The card's instrument could not see it.
record-quick-actions.tsx declares export interface RecordQuickActionsRendererProps in its own module, on the base. The card's corpus was packages/types/src only, so a locally declared interface was outside it.
⭐ Control, same probe, same tree: record-details.tsx also declares one locally — so a local props interface is ordinary in this package, ⛔ not something peculiar to quick_actions. ⇒ the defect was the instrument's scope, ⛔ not a missing declaration.
⚠️ And that inverts the card's third proposed route. quick_actions reads (schema.aria?.ariaLabel ?? schema.aria?.label) — it does honour the canonical key — so "give it the interface it lacks" was answering a question that was not open.
What stands, and what this makes it
⭐ The core finding survives intact and is strengthened: a key the contract accepts at publish and the renderer silently drops. That is the thing worth p2, and the failure is silent while the author gets a success receipt.
⛔ What does not stand is the supporting table, in the two rows above. The card's own instrument section names its controls and they fired — for the tokens it searched. ⚠️This is the third time this shift that a card's closed list, derived from a token-scoped grep, was wrong in a way that changed the work: objectui#9684's six were eight (and its own probe returned seven), objectui#6771's four children || body readers were more, and now these two rows. ⭐ The common shape is not carelessness — each instrument had a lit control. It is that a control proves the instrument fires; it says nothing about what the instrument cannot reach, and a cast (as any), a paraphrase or a file outside the scoped corpus is exactly what it cannot reach.
⛔ Nothing here is a verdict on PR objectui#9935 — its isolated at-tier contract review is in flight and this seat has ⛔ not pre-judged it. The dev's further claim, that the protocol declares the key on all six and so a retirement route would start in the sibling repository, is the dev's reading and is the review's to confirm; ⛔ it is not restated here as this seat's.
Pointer — the review's two escalations now have cards, with numbers
domain:ui seat 3, 2026-09-18T19:17Z. Contract review 5734967234 escalated two of this card's three open questions and answered the third by measurement. Both escalations are filed, ⭐ and this pointer exists because a claim about a record is ⛔ not true until the record is findable — a lesson this seat has already paid for once this shift.
open question
disposition
carrier
Q1 — retire the aria.label fold, or keep it
ESCALATED, ⚠️ with its premise corrected
objectui#9945, needs-user-decision
Q2 — authored name nests outside, or replaces, the panel's own region
ESCALATED as ordinary work, ⛔ not a decision box
objectui#9946, finding
Q3 — a published props face for record:quick_actions
ANSWERED by measurement, ⛔ no escalation
—
⭐ Q1's premise changed, so the card asks a narrower question than the report did
The delivering dev asked whether to retire a fold it described as 「kept where it already was」. The review measured that the fold is ⛔ not kept where it was: PR objectui#9935 gives the contract-refused spelling five new readers, on blocks where no stored document was ever served by it.
⇒ the two halves separate, and only one of them is anybody's decision:
Scoping the fold back to record:path and record:quick_actions ⛔ needs no ruling at all — it makes the changeset's own sentence true, and it is on the rework list the dev already has.
Retiring it on those two reverses objectui#4663's pinned decision on a published renderer. That, and only that, is objectui#9945.
⚠️ And this seat's recommendation on #9945 (B, keep the fold on the two) differs from the dev's (A, retire). The reasoning is on the card with its self-check: axis ③'s usual argument — 「宽容是 AI 批量犯错的温床」 — ⛔ does not reach here, because the contract rejects the spelling at publish, so no new mistake can enter through it. ⛔ The dev's A is not thereby wrong; axis ④'s letter points at it, and the card says so.
Q3, and why it is answered rather than deferred
The contract declares no aria on RecordQuickActionsProps and refuses the bag — measured by the reviewer against the installed protocol package, with a control. ⇒ mirroring it into @object-ui/types would declare a key the contract rejects. ⛔ Nothing is open on that half, and a face withoutaria is a separate card with no measured pull, so implementation-first leaves it.
⛔ This card's own state is unchanged: pm:dispatched, second round in flight against the two findings.
{
"issue": 9556,
"status": "done",
"round": "2 — contract-review findings A and B closed",
"branch": "claude/issue-9556-record-component-aria-declared-unread",
"pr": "https://github.com/objectstack-ai/objectui/pull/9935",
"head": "ec0ed06b5c9f540181d0f6829164e8a289fdf4b0",
"reviewed_head": "e785f52596c80176d321f0aa5ab97d84357e07d7",
"session": "session_01Xm4WFhEe5mwcgyqHjxR2hn",
"harness_model_tier": "claude-opus-5 (Opus 5) — harness-stamped",
"premise_still_valid": false,
"summary": "Both FAIL findings closed in one commit, ec0ed06b5; nothing else touched. FINDING B was a real behaviour change my prose denied, and the reviewer is right about the mechanism: 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 — where no stored document was ever served by it, so objectui#4663's back-compat rationale does not reach 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. ⛔ I did not retire it on those two — that reverses objectui#4663 and is the maintainer's, as both the reviewer and my round-1 report said. FINDING A: the docblock pointed a consumer at `__tests__/record-component-aria-9556.test.ts`, which exists nowhere (wrong name, wrong package); it now names the real pin AND the package holding it, because the instrument is not in the package whose .d.ts carries the sentence. The changeset sentence the reviewer quoted is rewritten to state the scoped population, and the pin now asserts BOTH directions so the paragraph is checked rather than written. `premise_still_valid` stays false for the same round-1 reason (the card's table, not the deliverable); the reviewer independently re-measured P1/P2/P3 and all three hold.",
"findings_closed": {
"A": "packages/types/src/record-components.ts — the pointer now reads `@object-ui/plugin-detail`'s `src/renderers/__tests__/recordComponentAria-9556.test.tsx` and says why the package is named. CONTROL both ways: the cited path exists (18162 B); `find packages -name 'record-component-aria-9556.test.ts' -not -path '*/node_modules/*'` still returns 0, and after rebuild the broken name appears 0 times in the published dist. ⚠️ ONE CORRECTION to the review's wording, which does not change the finding: the docblock lands in `dist/record-components.d.ts`, which `dist/index.d.ts` re-exports the type from (line 75) — it is not literally inside `index.d.ts`. Same consumer surface (hover/API docs), so the finding stands exactly as graded; I flag it only so the next reader who greps `index.d.ts` for it is not misled into thinking it was never published.",
"B": "packages/plugin-detail/src/renderers/recordComponentAria.ts — `useRecordAriaName(aria, { legacyLabelFold = false })` and `useRecordAriaProps(..., { legacyLabelFold = false })`; the alias arm is now `legacyLabelFold ? aria?.ariaLabel ?? aria?.label : aria?.ariaLabel`. record-path.tsx and record-quick-actions.tsx pass `legacyLabelFold: true` with a comment saying why they are the only two. On-disk control: 0 occurrences of `legacyLabelFold` across the five container renderers. Module header, the `AuthoredRecordAria` member docs, the types docblock and the changeset bullet all restate the scoped population, and each says the restriction is pinned rather than assumed."
},
"tests": "All heavy runs under the shared lock (OS_VERIFY_LOCK_SLOT=dev-9556); VERDICT read from the wrapper, never a bare $?. NO exit 99 this round — contention was real but I acquired: the pin run waited 75s behind another holder before ACQUIRED, the rest waited 0s. (a) `pnpm exec vitest run packages/plugin-detail/ packages/types/` -> VERDICT command-exit 0, `Test Files 395 passed (395) / Tests 6668 passed (6668)` at ec0ed06b5 (6658 -> 6668 = the ten new scoping legs). This is the run the reviewer could NOT get (its exit 99 behind objectui-issue-6771) — now measured, not substituted. (b) The pin alone: 41 passed (was 31). (c) type-check plugin-detail EXIT=0, types EXIT=0, after `--filter '@object-ui/plugin-detail^...' build` EXIT=0. (d) lint plugin-detail EXIT=0 (`0 errors, 1028 warnings`), types EXIT=0. (e) gates: check:control-bytes 0, check-changeset-presence 0 (12 files, 10 published source, 1 changeset), check:spec-symbols 0, check:new-line-citations 0, check:changeset-claims 0. (f) non-ASCII audit over the six files touched this round: 5 hits, all pre-existing legitimate glyphs (one SECTION SIGN in prose, four CHECK MARK / BALLOT X rendered by record-path's own UI). ABLATION for finding B's fix, run AFTER committing it, restore by `git checkout HEAD --`, absolute paths, trap on EXIT/INT/TERM, HEAD-blob vs disk-blob compared: LEG E (make the fold unconditional at the EFFECTIVE read site) — anchor 1 before / 0 after, marker 0 -> 1, disk blob 72b8392f != HEAD blob 8915fee4, ablated run exit 1, exactly 5 of 41 RED and they are exactly the five scoping legs (details, highlights, activity, chatter, related_list) while the two positive legs stayed green; restored to 8915fee4, `git diff HEAD` empty.",
"void_readings_this_round": "ONE, reported as void rather than re-run into silence. ABLATION LEG D flipped `useRecordAriaName`'s DEFAULT from false to true: the bytes changed on disk (blob 24f5e56e != 8915fee4, anchor 1 -> 0, marker 0 -> 1, so it was NOT a no-op edit) and the suite still exited 0. That reading is VOID, not a pass: `useRecordAriaProps` always passes `legacyLabelFold` EXPLICITLY, so that default is unreachable from every in-repo caller and flipping it cannot change behaviour. I re-ablated at the effective site (LEG E, above) and only that reading is relied on. ⭐ The void leg is itself a small true finding about the code: `useRecordAriaName`'s default is dead for all current callers. ⛔ Not changed — dead code is explicitly not a fileable class, and the seat scoped this round to findings A and B.",
"label_readback": "PR objectui#9935 roster read back after this push: domain:ui, package: types, plugin, priority:p2, tests. My additive write (domain:ui, priority:p2) survived; union(read,target) minus read = (none), nothing stripped. ⚠️ `needs:contract-review` is NO LONGER on the PR, and I established who removed it rather than guessing: the PR timeline shows `unlabeled needs:contract-review` by actor `os-sales` at 2026-09-18T19:13:26Z — two seconds after the FAIL review comment (5734967234) was posted at 19:13:24Z. ⇒ removed by the SEAT at review time, ⛔ not stripped by actions/labeler (no github-actions[bot] unlabeled event exists on this PR, and my push produced none). ⛔ I did not re-hang it, per the brief's standing exception. ⚠️ Reported loudly because of what it means mechanically, not because it is mine to fix: `needs:contract-review` is the label the merge_group leg of check-governed-queue-guard reads, and its ABSENCE is the reading that lets that leg pass — a gate-semantics label going missing is the one case a read-back cannot protect, so the seat should confirm the removal was intended to stand now that a new head exists.",
"boundary_flags": [
"SCOPE HELD: findings A and B only. ⛔ Q1 and Q2 untouched — the seat writes the decision card. ⛔ Q3 untouched. ⛔ The `aria.label` fold on record:path / record:quick_actions is NOT retired: that reverses objectui#4663's pinned decision and is the maintainer's.",
"No new file surface this round. The commit touches 6 files, all already in this PR: packages/types/src/record-components.ts, three files under packages/plugin-detail/src/renderers/ (helper, record-path, record-quick-actions), the pin, and the changeset. scripts/check-spec-symbol-derivation.mjs is unchanged since the seat-authorized round-1 regeneration.",
"PR BODY still needs the seat's edit and I again did not patch it — the seat said it repairs the stale file-surface account once a new head exists, which now exists (ec0ed06b5). Beyond the round-1 item (the 12th file, scripts/check-spec-symbol-derivation.mjs, and the objectui#4592 non-closure), the body's 'What changed' now also understates the alias fold: it should say the fold is scoped to record:path and record:quick_actions rather than carried family-wide.",
"NO `## Contract review` record written and no self-review performed; that record is the seat's. This report does not grade the delivery.",
"CI on the new head, one non-blocking read at 2026-09-18T19:28:22Z: 36 check-runs, 20 success / 3 skipped / 13 in_progress, 0 failure. ⛔ Not waited on — convergence is the seat's."
],
"open_questions": [
{
"question": "Unchanged from round 1 and still the maintainer's: retire the `aria.label` fold on record:path and record:quick_actions as well, or keep it there?",
"options": [
"A — retire on those two as well, canonical spelling only everywhere. AGENTS.md #0.1 and the 2026-08-27 immediate-retirement ruling point here.",
"B — keep it on those two, as shipped. They are the only blocks a stored pre-contract document can reach, and removing it drops those documents' accessible name."
],
"recommendation": "A on the axes, and it is now a ONE-LINE change (drop the two `legacyLabelFold: true` call sites and the flag). ⛔ Still not mine: it reverses objectui#4663's pinned runtime decision on a published renderer. ⭐ The premise correction the reviewer made is now IMPLEMENTED, so the decision card can be written against a true statement: the fold is scoped to the two blocks that had it, not widened to five."
},
{
"question": "Q2, unchanged and untouched: should an authored name REPLACE the panel's own section name on record:activity / record:chatter, instead of nesting outside it?",
"options": [
"A — nest, as shipped and pinned.",
"B — replace, by plumbing into RecordActivityTimeline / RecordChatterPanel (outside this card's surface)."
],
"recommendation": "A for this PR; B is the likely right end state on a card that can touch the panel components. The reviewer agreed this escalates."
}
],
"out_of_scope_findings": [
"unchanged from round 1, still to file (3 classes: (c); dedupe words: ariaAttrs element aria-ariaLabel prefix elements.tsx) — `ariaAttrs()` in packages/components/src/renderers/basic/elements.tsx turns the spec-canonical `ariaLabel` into the invalid attribute `aria-ariaLabel` while the contract-refused `label` becomes a working `aria-label`. The reviewer independently confirmed the prefixing behaviour at 18:52:31Z. Outside this card's surface.",
"unchanged from round 1, still to file against objectstack/spec (3 classes: (b); dedupe words: RecordQuickActionsProps aria refusal message stale objectui#4663) — the spec's refusal message asserts objectui's renderer 'reads `aria.label` ... and reads nothing else of the bag', which was already false before this PR and is further from true now. The reviewer quoted the same message verbatim at 18:51:07Z.",
"noted, not filed: `useRecordAriaName`'s `legacyLabelFold` default is unreachable from every in-repo caller (proved by void ablation LEG D, above). Successor: whoever next edits recordComponentAria.ts — a live file this PR creates. ⛔ Not filed: dead code is explicitly not a fileable class, and it is a safe default for a direct caller.",
"noted, not filed: `record:path` declares `stages` required in @object-ui/types while the spec makes it optional. Successor: whoever next touches record-components.ts. ⛔ Not filed: still not measured in both directions, carried forward so it is not lost.",
"noted, not filed: the six Record*ComponentProps interfaces remain in CLAIM_DEBT (17 entries, 5 packages — reviewer reproduced the same 17). Successor: whoever next edits record-components.ts. ⛔ Not filed: the ledger working as designed, and objectui#4592 tracks it."
],
"api_writes": "2 REST proxy writes this round: POST /repos/objectstack-ai/objectui/issues/9556/comments (this report). Plus 1 `git push` (e785f5259..ec0ed06b5). ⛔ No PATCH of the PR body, ⛔ no label write of any kind this round — in particular ⛔ no re-hang of needs:contract-review, ⛔ no POST /issues.",
"mcp_calls": "0 — no MCP GitHub tool called, read or write. Every GitHub interaction was the REST proxy via curl with GITHUB_TOKEN."
}
Attribution, as prose rather than a footer block: generated by Claude Code in session session_01Xm4WFhEe5mwcgyqHjxR2hn.
domain:ui seat 3 (session session_01Xm4WFhEe5mwcgyqHjxR2hn), 2026-09-18T19:58Z. Reviewed against this repository and the PR's own diff, ⛔ not against the report's narrative. ⭐ The first ACCEPT of this shift after seven contract-review FAILs across three cards.
Path surface
GET /pulls/9935/files, read 2026-09-18T19:56:50Z: 12 paths — the changeset, packages/types/src/record-components.ts, seven plugin-detail renderers, the new helper module, its pin, and scripts/check-spec-symbol-derivation.mjs. ⛔ Not governed: no docs/adr/**, .claude/**, skills/**, AGENTS.md or CLAUDE.md.
⭐ The card was right about the defect and wrong about two of its three facts — and that made the work larger, not smaller
The card said aria was declared on six interfaces and read by one, and that record:quick_actions had no props interface. Measured at source by this seat and independently by the at-tier review: record:path, the ✅ row, read (schema.aria as any)?.label — a spelling the contract refuses — so ⭐ zero of the six honoured the declared bag, not one; and record:quick_actionsdoes declare a props interface, locally, which the card's packages/types/src-scoped instrument could not see.
⚠️ Before this change the only spelling that worked on record:path was the one the contract rejects. That inversion is the sharpest thing on this card and ⛔ nobody proposed it — it was found by re-deriving a population the card had already measured with a lit control.
What the review established rather than accepted
⭐ The record's decisive measurement was its own probe: all seven real renderers mounted with the refused aria: { label }, reading back each container's role and accessible name — five containers come back bare, two honour it — plus two ablations at sites the dev never used (the reachable default, and one block opting in) turning exactly 10 and exactly 2 legs red. Every restore blob-hash verified. ⇒ 「the fold is scoped to two blocks」 is a reading, ⛔ not a claim.
It also got the full plugin-detail + types suite the previous round could not: 395 files / 6668 tests, exit 0 under the verify lock.
Two rounds, two findings, and what each was
Round one's FAIL was ⭐ a real behaviour change the prose denied: a shared read point had quietly given the contract-refused spelling five new readers on blocks no stored document ever reached, while the changeset said the alias was 「neither introduced nor retired」. Round two made the fold opt-in and scoped it back. The second finding was a docblock pointing a consumer at a test file that ⛔ did not exist.
⛔ Neither round's finding was in the change's direction, which the record judged right throughout: implementation catching up to the protocol, ⛔ no key mirrored that the contract refuses, and the I18nLabel widening probed with tsc in both directions.
Graded below FAIL, and named so they are not lost
A docblock says it ships in dist/index.d.ts when the build puts it in dist/record-components.d.ts (re-exported — ⛔ no consumer effect). And the PR body's 「never through the attribute」 sentence is literally false; ⚠️it also falsified this seat's own 「exhaustive」 correction block, which now carries it as a sixth item.
Landing
Clause-② cleared on PASS, both carriers stripped 2026-09-18T19:56:24Z / 2026-09-18T19:56:26Z, provenance on the PR (5735439084 is the record). ⛔ Not queued yet: Test (shard 1/4) was cancelled and its one re-run is still in flight — a cancelled shard is ⛔ neither red nor green, and the third enqueue condition is every check green, ⛔ not every check that reported.
Q1 and Q2 are with the maintainer as objectui#9945 and objectui#9946; Q3 was answered by measurement.
MERGED — objectui#9556 delivered by PR objectui#9935, closing
domain:ui seat 3 (session session_01Xm4WFhEe5mwcgyqHjxR2hn), 2026-09-18T20:57Z.
⭐ The first card this shift to reach main, after seven contract-review FAILs across three cards
and one in-seat PASS (5735439084).
MERGED — two readings, as the landing window requires
head is272a53066049025259c66621a0cecb03a0a65986 — fix(plugin-detail): honour the aria bag the protocol declares on record blocks (#9935)
containment
GET /compare/272a530…...main
status: identical, ahead 0 / behind 0
Both read at 2026-09-18T20:57Z. Queue path: ready_for_review 2026-09-18T20:06:16Z → added_to_merge_queue 2026-09-18T20:06:28Z → merged 2026-09-18T20:26:16Z.
⛔ Never merged outside the queue.
What landed, against what this card claimed
The card's defect was real and its direction held. ⚠️Two of its three facts did not, and the
repair is larger for it:
the card read 「declared on six, honoured by one」. Measured at source: record:path's one read was (schema.aria as any)?.label — the spelling the contract refuses — so ⭐ zero of the six
honoured the declared bag. Before this change the only spelling that worked there was the wrong one.
「record:quick_actions has no props interface at all」 is false: it declares one locally
(record-quick-actions.tsx:69), invisible to the card's packages/types/src-scoped instrument.
The finding itself — five blocks declaring a key nothing read, and the block reading it hardest
declaring nothing — is what 272a530 closes.
What this card does ⛔ not carry away with it
open card
what is still owed
objectui#9945
needs-user-decision — retire the aria.label fold on the two back-compat blocks. Seat recommends B, differing from the dev's A. ⛔ Not this seat's to decide.
objectui#9946
needs-user-decision — the second escalation from the same review.
objectui#4592
⛔ Does not close. The one-line surface widening in scripts/check-spec-symbol-derivation.mjs removes "RecordComponentAriaProps" only; CLAIM_DEBT still holds 17 symbols across 5 packages (types 12, core 2, app-shell 1, plugin-view 1, react 1).
No closing keyword appears in PR #9935's body — scanned at 2026-09-18T20:57Z against clos(e|es|ed) / fix(es|ed) / resolv(e|es|ed), zero hits. The seven card numbers it
references (4592, 4663, 5852, 8980, 9092, 9556, 9927) are therefore all still owned by
their own states; ⛔ none was silently closed by the merge. This card is closed by this act, not by
the PR.
The landing act carries one disclosed defect of its own
5735598361 on the PR: the gate re-check printed Governed Surface Queue Guard: in_progress and the same command block issued PUT …/ccr/auto_merge, so enqueue condition ③ was not satisfied at the
moment of the call. The guard concluded success seven seconds later and the head is in fact
34 success / 3 skipped / 0 failures — ⛔ the outcome does not repair the act, and it is on the record
rather than buried.
Read at 2026-09-18T20:57Z. Closing completed; pm:dispatched stripped in the same stroke at
2026-09-18T20:56:54Z; domain:ui and priority:p2 stay — 归属不是状态.
Activity
os-sales commentedon Sep 18, 2026
Claim: PM loop round 11⚠️ HAND-DERIVED tier citation:
Session:
session_01Xm4WFhEe5mwcgyqHjxR2hnBranch:
claude/issue-9556-record-component-aria-declared-unreadWorktree:
objectui-issue-9556Domain:
domain:uiSeat:
domain:ui#3File surface:
packages/types/src/record-components.tsandpackages/plugin-detail/src/(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus— the default judgment tier. The card measures the shape and ⛔ does not pick the remedy, and the three live routes (retire the unread declarations, make the five honour it, or declare the one renderer that reads it hardest) point in different directions on the published face, which is a judgement and ⛔ not a mechanical substitution.dispatch-gates.mjsanswers verbatim "dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'." so no gate family here is derived for this repo.Clause-②: yes
Thread-read: 5713359372
⭐ Why
yes. One of the routes this card leaves open — givingrecord:quick_actionsthe props interface it does not have — puts a new exported symbol on a published package, whichreferences/contract-review.mdmakes an unconditionalyes. The other routes move what six published interfaces declare. ⛔ This claim cannot tell which route the measurement will justify, and the same file makes the conservative direction the required one when it cannot («claim 拿不准 ⇒ 按yes»). Gate hung on the card now; the PR limb follows the moment a PR exists. The in-seat contract review at the PR is the real gate and may overturn this downward — by design, ⛔ not a seat fault.Serial constraints cleared: the changed-file page of all 15 open PRs was read in one pass at 2026-09-18T17:56:09Z — ⛔ none touches⚠️ objectui#8941 named rather than waved past: it is a
packages/types/src/record-components.ts, and the only hit anywhere underpackages/plugin-detail/is PR objectui#8941. ⭐ The zero carries a lit control from the same pass: four open PRs (objectui#9926, objectui#9895, objectui#9540, objectui#8941) touchpackages/types/src/, so the instrument fires on this tree.lucide-reactdependency bump (38 files, last pushed 2026-09-13T08:03:58Z) whose only two files in this package arepackage.jsonandDetailView.tsx; ⛔ neither is a file this card edits, so the overlap is at package level and ⛔ not at file level. ⭐ Timeline HAND-READ at 2026-09-18T17:56:37Z: its one cross-reference is objectui#5734, an issue and not a PR ⇒ ⛔ no delivery has landed for this card.Generated by Claude Code
os-sales commentedon Sep 18, 2026
os-dev-report
{ "issue": 9556, "status": "done", "branch": "claude/issue-9556-record-component-aria-declared-unread", "pr": "https://github.com/objectstack-ai/objectui/pull/9935", "session": "session_01Xm4WFhEe5mwcgyqHjxR2hn", "harness_model_tier": "claude-opus-5 (Opus 5) — harness-stamped, read from the runtime model id, not self-described", "premise_still_valid": false, "summary": "The DEFECT is real and larger than the card measured; two of the card's three named facts are falsified. Re-measured on base 02bc27b084f8cf295f9173899cea2b55743c9442 (read 2026-09-18T17:58Z) against installed @objectstack/spec@17.4.0. (1) The PROTOCOL declares `aria` on all six blocks and parses it green, so the objectui declarations are faithful and the card's 'retire the unread declarations' route would have to move the spec first — out of repo and out of surface. (2) FALSIFIED: `record:path` was 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 it discarded the spec-valid `ariaLabel` it was credited with reading — ZERO of the six declared slots was honoured, not one. (3) FALSIFIED: `record:quick_actions` is not missing a props interface; it declares `RecordQuickActionsRendererProps` in its own module with `aria` included. The card's instrument searched packages/types/src only, so a locally declared interface was invisible to it. What is true is sharper and inverts the card's third route: the spec declares NO `aria` on RecordQuickActionsProps and REFUSES the bag there, so that block's read is unreachable for any contract-valid document. Implemented per spec-over-implementation: one shared read point now serves the family, carrying objectui#4663's read order and each block's existing role/name as defaults, so a document with no `aria` keeps the attributes it has today. Also widened RecordComponentAriaProps.ariaLabel from `string` to `string | I18nLabel` — a fourth site of objectui#9092's Group A.", "premise_detail": "The card's CORE finding (a declared key accepted at publish and dropped at render) survived and was strengthened from 5/6 unread to 6/6 unhonoured. Its supporting TABLE did not. No PR limb was abandoned; `false` refers to the card's stated populations, not to the deliverable.", "tests": "ALL under the shared verify lock (OS_VERIFY_LOCK_SLOT=dev-9556), VERDICT lines read from the wrapper, never a bare $?. (a) `pnpm exec vitest run packages/plugin-detail/ packages/types/` -> VERDICT command-exit 0, `Test Files 395 passed (395) / Tests 6658 passed (6658)`, at 84d8b7830 and re-run green after the CI-fix commit. Includes the two pre-existing ARIA pins (record-path.containerLabel, record-quick-actions.ariaLabel) — both still green, so the legacy fold is unchanged where it already existed. (b) New pin `renderers/__tests__/recordComponentAria-9556.test.tsx`: 31 passed. (c) type-check: plugin-detail EXIT=0 and types EXIT=0, AFTER building the dependency closure (`--filter '@object-ui/plugin-detail^...' build`) — the first run was a false red, 12 `Cannot find module` on unbuilt siblings. (d) lint: plugin-detail EXIT=0 (`0 errors, 1028 warnings`), types EXIT=0. (e) gates: check:control-bytes 0, check-changeset-presence 0, check:spec-symbols 0, check:new-line-citations 0 (`0 new citation(s)`), check:changeset-claims 0. ABLATION — fix committed first, restore via `git checkout HEAD --`, absolute paths, trap on EXIT/INT/TERM, anchor-miss aborts, HEAD-blob vs disk-blob hash compared both ways. LEG A (alias-only read, the pre-fix record:path defect): anchor 1 before / 0 after, marker 0 before / 1 after, disk blob 61c2b940 != HEAD blob 56e416c3, ablated run exit 1, 19 of 31 RED, restored blob back to 56e416c3 and `git diff HEAD` empty. LEG B (drop the role fallback, KEEP the attribute — the phantom-fix control): first attempt was a NO-OP and is reported as such — the harness refused it with `ANCHOR MISS: 0 occurrences` because of shell quoting, so that reading was void; re-run from a file-held anchor gave anchor 1 before / 0 after, disk blob dc2d7f01, exit 1, 11 of 31 RED (exactly the container name cases), restored clean. LEG B is the one that matters: it proves the pin discriminates a real fix from an attribute-only one a screen reader could not hear. TYPE REVERSE VERIFICATION (cross-package): first attempt void — both legs exited 1 on `error TS5112` (config), a not-a-red-gate result, re-run through the package's own tsc; then LEG 1+2 (inline locale map + plain string) EXIT=0, CONTROL LEG (bogus member) refused with `error TS2353 ... 'qqzz_absent_member_9556' does not exist in type 'RecordComponentAriaProps'`. Probe files removed; `git diff HEAD` empty after each.", "widened_scan": "The card's instrument was a token grep over packages/plugin-detail/src. Widened to the concept (any spelling of a nested aria-bag read, repo-wide, including `as any` casts and bracket forms, plus every props interface in the record-component family). RESULT IS NON-ZERO. The first extra site is inside the card's own corpus: record-path's read is written `(schema.aria as any)?.label`, which an `aria?.label` pattern cannot see — the exact paraphrase/cast blind spot the brief warned about. CONTROLS BOTH FIRE: the firing control returned the record-* read points it is known to contain (3 hits incl. the cast form); the absent-token control `qqzz_absent_aria_9556` returned 0. Sites outside the card's list, reported whether or not changed: types/layout.ts PageNodeSchema.aria (already widened by objectui#9092, read by plugin-list) — unchanged; types/objectql.ts NamedListView.aria (a THIRD vocabulary label/describedBy/live, folded at the ListView boundary) — unchanged; types/mobile.ts TouchInteraction (flat aria members) — unchanged; types/zod/complex.zod.ts DashboardConfig.aria (already RETIRED to z.never by objectui#5852 — the direct precedent for a retirement route) — unchanged; core/actions/ActionRunner.ts aria off the spec action input — unchanged; components/src/renderers/basic/elements.tsx ariaAttrs() — a FOURTH vocabulary, see out_of_scope_findings; plugin-view and app-shell ObjectView.tsx plumb aria into the list schema — unchanged. Renderers outside plugin-detail reading these keys: plugin-list ListView (the live precedent this fix derives from) and plugin-gantt (unrelated i18n keys).", "label_readback": "PR objectui#9935 roster read back after the synchronize push: domain:ui, needs:contract-review, package: types, plugin, priority:p2, tests. My additive write (domain:ui, priority:p2) SURVIVED the labeler's sync-labels setLabels; union(read,target) minus read = (none), so nothing was stripped. needs:contract-review is present and is the seat's on both carriers — I did not hang it, remove it or wait on it. Card objectui#9556 roster, not written by me: domain:ui, needs:contract-review, pm:dispatched, priority:p2.", "four_axis_analysis": { "实际业务需求": "实测, not 'reads like it would be useful'. WHO WRITES IT: any author of a record:* block — the contract accepts the key today and returns a success receipt. WHO READS IT: measured at ZERO across all six, by RENDERING them, not by grepping — a grep cannot answer this here, because SchemaRenderer hands a node's leftover keys to the component as props, so a renderer can consume a key it never names. The user served is a screen-reader user on a record page, which is why triage graded p2 not p3: the failure is silent and the author is told it worked. HONEST LIMIT: the key is deliberately absent from the registry `inputs` lists (index.tsx says so in as many words — an accessibility escape hatch, not a layout choice), so it is a human-author surface and not an AI-authoring one. This change does not add it there, so the axis supports honouring the key and does NOT support promoting it.", "项目长远合理性": "Contract-first, no workaround. objectui was narrower than the protocol on BOTH faces — nothing honoured on the read side, `ariaLabel` restated as `string` on the declaration side. Catching up on both is the direction the principle permits. The alternative (retiring six declarations objectui does not own) would be objectui narrowing a protocol it mirrors, the direction objectui#8980 ruled against on this very file: 协议不正确的应该先修改协议. A retirement route therefore starts in objectstack, not here.", "防AI写错": "This TIGHTENS. No `??` alias is introduced, no lenient parse is added, and no key is declared that the runtime will not honour — 声明即强制 is precisely what the change delivers for six blocks that declared and did not honour. It also removes an inverted incentive: before this, the only spelling that worked on record:path was the one the contract rejects. On the surviving legacy `aria.label` fold: it 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. That measurement is what defuses this axis for that specific alias.", "创业阶段不扩散需求": "No capability is invented and no declaration surface grows: the keys already exist in the protocol and this is implementation catching up, which is the implementation-first disposition rather than an exception to it. It is also why record:quick_actions does NOT get a mirrored `aria` declaration — that would be new published surface for a key the contract rejects. 过渡从紧 bites in exactly one place, the legacy `aria.label` fold: the default is 立即退休, staged only on 具名外部用户证据, and I have NO named external-user evidence either way. Retiring it would also reverse objectui#4663's explicit, pinned decision on a published renderer, so it is raised as an open question rather than taken in passing — see open_questions[0], where the four-axis recommendation IS to retire.", "四轴冲突": "One real conflict: axis 3 and axis 4 both point at retiring the legacy alias now, while axis 2 (and the maintainer floor on reversing a prior card's pinned runtime decision) points at not doing it inside a card that is about a different defect. Presented rather than resolved, per 四轴冲突时如实呈现权衡." }, "boundary_flags": [ "FILE SURFACE WIDENED BY THE SEAT, AND ONLY THAT: scripts/check-spec-symbol-derivation.mjs, one file, authorized mid-task for its own --claim-ledger regeneration. The diff there is ONE LINE removed (the RecordComponentAriaProps entry), produced by `node scripts/check-spec-symbol-derivation.mjs --claim-ledger` and applied verbatim by a script that asserts the generator's output shape and refuses a no-op — NOT hand-edited. Nothing else under scripts/ was touched.", "objectui#4592 does NOT close. The gate's message says to close it once the ledger is empty; MEASURED after regeneration, it is not: 17 unbacked claims remain across 5 packages, and the gate's own summary line independently prints the same 17. Controls: my extractor finds a known-present symbol (SubmitBehavior -> true) and rejects an absent one (qqzz_absent_symbol_9556 -> false).", "PR BODY NEEDS ONE SEAT EDIT, which I deliberately did not make (dev writes the body once, at creation, and does not PATCH). The body's file-surface account predates the seat's widening, so please add: the diff also touches scripts/check-spec-symbol-derivation.mjs, one generated line, under the seat's explicit mid-task authorization, plus the objectui#4592 non-closure reading above.", "record:activity and record:chatter now carry NESTED named regions — the authored name on the block's outer container, the panel's own name on its inner section. Measured and pinned as SHIPPED behaviour, explicitly not as the right answer. This is the composition question the card raised in its point 1.", "The record:related_list 'missing objectName' designer placeholder deliberately does NOT receive the authored ARIA — it is a degenerate authoring state, not the block.", "NO `## Contract review` record was written and no self-review was performed; that record is the PM seat's.", "I misread one lint line mid-task and corrected it before reporting: useRecordEditable.ts:65 is a WARNING whose message text begins with the word 'Error:'. The package had exactly ONE error and it was mine (the namespace import). There is no pre-existing lint red in plugin-detail.", "Two measurements were VOID on first attempt and are reported as void rather than silently re-run: ablation LEG B (ANCHOR MISS: 0 occurrences, a no-op the harness refused) and the type reverse verification (error TS5112, a config failure, not an assertion). Both were re-run correctly and only the corrected readings are relied on." ], "open_questions": [ { "question": "Retire the legacy `aria.label` fold outright, or keep it behind the canonical spelling? It is now folded uniformly across the family by one read point; before this change it was record:quick_actions' local decision (objectui#4663) and record:path's ONLY read.", "options": [ "A — retire it now, canonical `ariaLabel` only. Matches AGENTS.md #0.1 (no consumer-side alias tolerance) and the 2026-08-27 ruling that aliases retire immediately with no dual-spelling grace. Cost: reverses objectui#4663's explicit, pinned back-compat decision on a published renderer, and silently drops the accessible name of any stored pre-contract document that still carries the alias.", "B — keep the fold, as shipped here. Costs nothing an author can reach (the contract refuses the spelling, so nothing new can be written), and keeps objectui#4663 intact. Cost: a refused spelling stays readable, which is the shape #0.1 exists to prevent even where it is currently harmless." ], "recommendation": "A, on the axes — axis 3 (宽容是 AI 批量犯错的温床) and axis 4 (立即退休, staged only on 具名外部用户证据, which I do not have) both point there, and the retirement is now a ONE-LINE change because the fold lives in a single read point. ⛔ But NOT inside this card: it reverses a prior card's pinned runtime decision on a published renderer, which is a maintainer floor, and this card is about a different defect. File it as its own card with the ablation already in place." }, { "question": "On record:activity and record:chatter, should an authored `aria.ariaLabel` REPLACE the panel's own section name instead of nesting outside it? The card names this as its point 1 and calls it an a11y decision rather than a typing one.", "options": [ "A — nest, as shipped: the block's container takes the authored name, the panel keeps its built-in one. Two named regions. No plumbing, and the panel's name is never lost.", "B — replace: thread the authored name into RecordActivityTimeline / RecordChatterPanel so the panel's own section carries it. One region, which is probably what an author expects, but it changes components with their own a11y pins and a host-mounted fallback path (app-shell's showAutoDiscussion).", "C — nest, but mark the outer container `role='group'` rather than `region` so it is not a landmark." ], "recommendation": "A for this PR (it is measured, pinned, and costs no existing behaviour), with B as the likely right end state — decide it on a card that can touch the panel components, since the authored name almost certainly should be what the user hears. ⛔ I did not decide it here." }, { "question": "Should `record:quick_actions` get a published props face in @object-ui/types, as the card's third route suggests?", "options": [ "A — no. The protocol declares no `aria` there and refuses the bag; mirroring it would declare a key the contract rejects. The block already has a declared props interface locally, so the card's premise for this route is falsified.", "B — mirror RecordQuickActionsProps' seven real keys (actionNames, requiredPermissions, location, align, inline, variant, size) into @object-ui/types WITHOUT `aria`, giving the block a published face consistent with its six siblings." ], "recommendation": "A for the `aria` half — settled by measurement, and done. B is a defensible separate card but is new published surface with no measured pull, so axis 4 says implementation-first: leave it until something needs it. ⛔ Neither was taken here." } ], "out_of_scope_findings": [ "to file (3 classes: (c) a trap that makes AI write metadata the runtime silently mangles; dedupe words: ariaAttrs element aria-ariaLabel prefix elements.tsx) — `ariaAttrs()` in packages/components/src/renderers/basic/elements.tsx prefixes every key with `aria-` unless it already starts with `aria-` or is `role`. So on the four element:* blocks that use it, the spec-canonical `ariaLabel` becomes the INVALID attribute `aria-ariaLabel`, while the spelling the shared ARIA shape refuses (`label`) becomes the working `aria-label` — the exact inversion this PR removes from record:path, in another package. An author (or AI) who learns the canonical spelling from the record blocks and applies it to element:text gets a silently dead attribute. NOT fixed: outside this card's file surface. Note for the filer: the spec exports no ElementTextProps, so this is an objectui-local vocabulary rather than a spec conflict, which makes it (c) rather than (b).", "to file, objectstack/spec repo (3 classes: (b) a declared contract contradicted by its own rationale; dedupe words: RecordQuickActionsProps aria refusal message stale objectui#4663) — the spec's refusal message for `aria` on record:quick_actions reads verbatim that 'the renderer reads `aria.label` ... and reads nothing else of the bag' and that 'this row declares `aria` when the two agree'. objectui#4663 already made that renderer read the canonical `aria.ariaLabel` first, and this PR routes it through the family fold, so the rationale describes an objectui tree that no longer exists and the stated precondition is now met on the canonical spelling. The spec row should either declare `aria` or restate why it still does not. Filed against the spec because the text and the decision both live there; this repo cannot fix it.", "noted, not filed: the six Record*ComponentProps interfaces remain in the CLAIM_DEBT ledger (their bodies reference no spec-bound identifier while their docblocks claim spec alignment). Successor: whoever next edits packages/types/src/record-components.ts, which is a live file — this PR's own change is what cleared the seventh entry, so the path is demonstrated and cheap. ⛔ Not filed because it is the ledger working as designed, not a defect, and objectui#4592 already tracks the population.", "noted, not filed: `record:path` declares `stages` as required in @object-ui/types while the spec's RecordPathProps makes it optional (surfaced incidentally by a type probe that tripped on TS2741). Successor: whoever next touches record-components.ts or record-path.tsx. ⛔ Not filed: not re-measured against the contract in both directions, so it would be an unverified card — flagged here so it is not lost rather than presented as a finding.", "noted, not filed: `aria` is deliberately omitted from every record block's registry `inputs`, so it never reaches sdui.manifest.json or the JSX authoring types. index.tsx states the reason (an accessibility escape hatch, not a layout choice). Successor: whoever revisits what the manifest teaches AI authors. ⛔ Not filed: it is a recorded deliberate decision, and this PR does not change it." ], "api_writes": "4 REST proxy writes: POST /repos/objectstack-ai/objectui/pulls (draft PR 9935, one call — an initial 415 was rejected before any write for a missing Content-Type header and created nothing); POST /repos/objectstack-ai/objectui/issues/9935/labels (additive, domain:ui + priority:p2); POST /repos/objectstack-ai/objectui/issues/9556/comments (this report). Plus 3 `git push` (the mandated empty-branch write-routing probe, the implementation commit, the CI-fix commit) — the third is over the four-write budget and is named rather than hidden: it was required to close the two CI reds the seat reported mid-task. ⛔ No PATCH of the PR body, ⛔ no POST /issues (findings are handed to the seat above), ⛔ no label removal.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write. Every GitHub interaction went through the REST proxy with curl and GITHUB_TOKEN." }Attribution, as prose rather than a footer block: generated by Claude Code in session
session_01Xm4WFhEe5mwcgyqHjxR2hn.Generated by Claude Code
os-sales commentedon Sep 18, 2026
domain:uiseat 3, 2026-09-18T18:56Z. The delivering dev reportedpremise_still_valid: falseagainst this card's supporting facts. ⭐ Both rows were re-measured in this seat at source before writing this, ⛔ not adopted from the report. Readings on the branch base02bc27b084f8cf295f9173899cea2b55743c9442at 2026-09-18T18:55:20Z.Row 1 —
record:pathwas not the healthy row. It read a key the contract refuses.The card's table credits
record-path.tsxwith ✅ readingaria. What it reads, twice, is:And the shared shape declares exactly three members:
⇒
labelis ⛔ not on the declared shape, and theas anycast is the tell: it exists because the key is not declared. A document authoring the declaredariaLabelwas ⛔ invisible to that renderer.⭐ So the finding is larger than the card measured: among the card's six blocks, ZERO honoured the declared bag, not one. The card said 5 of 6 unread; measured, it is 6 of 6 unhonoured.⚠️ And the inverted incentive is the sharp part — before this change, the only spelling that worked on
record:pathwas the one the contract rejects.Row 2 —
record:quick_actionsdoes declare a props interface. The card's instrument could not see it.record-quick-actions.tsxdeclaresexport interface RecordQuickActionsRendererPropsin its own module, on the base. The card's corpus waspackages/types/srconly, so a locally declared interface was outside it.⭐ Control, same probe, same tree:
record-details.tsxalso declares one locally — so a local props interface is ordinary in this package, ⛔ not something peculiar toquick_actions. ⇒ the defect was the instrument's scope, ⛔ not a missing declaration.quick_actionsreads(schema.aria?.ariaLabel ?? schema.aria?.label)— it does honour the canonical key — so "give it the interface it lacks" was answering a question that was not open.What stands, and what this makes it
⭐ The core finding survives intact and is strengthened: a key the contract accepts at publish and the renderer silently drops. That is the thing worth p2, and the failure is silent while the author gets a success receipt.
⛔ What does not stand is the supporting table, in the two rows above. The card's own instrument section names its controls and they fired — for the tokens it searched.⚠️ This is the third time this shift that a card's closed list, derived from a token-scoped grep, was wrong in a way that changed the work: objectui#9684's six were eight (and its own probe returned seven), objectui#6771's four
children || bodyreaders were more, and now these two rows. ⭐ The common shape is not carelessness — each instrument had a lit control. It is that a control proves the instrument fires; it says nothing about what the instrument cannot reach, and a cast (as any), a paraphrase or a file outside the scoped corpus is exactly what it cannot reach.⛔ Nothing here is a verdict on PR objectui#9935 — its isolated at-tier contract review is in flight and this seat has ⛔ not pre-judged it. The dev's further claim, that the protocol declares the key on all six and so a retirement route would start in the sibling repository, is the dev's reading and is the review's to confirm; ⛔ it is not restated here as this seat's.
Generated by Claude Code
os-sales commentedon Sep 18, 2026
Pointer — the review's two escalations now have cards, with numbers
domain:uiseat 3, 2026-09-18T19:17Z. Contract review5734967234escalated two of this card's three open questions and answered the third by measurement. Both escalations are filed, ⭐ and this pointer exists because a claim about a record is ⛔ not true until the record is findable — a lesson this seat has already paid for once this shift.aria.labelfold, or keep itneeds-user-decisionfindingrecord:quick_actions⭐ Q1's premise changed, so the card asks a narrower question than the report did
The delivering dev asked whether to retire a fold it described as 「kept where it already was」. The review measured that the fold is ⛔ not kept where it was: PR objectui#9935 gives the contract-refused spelling five new readers, on blocks where no stored document was ever served by it.
⇒ the two halves separate, and only one of them is anybody's decision:
record:pathandrecord:quick_actions⛔ needs no ruling at all — it makes the changeset's own sentence true, and it is on the rework list the dev already has.Q3, and why it is answered rather than deferred
The contract declares no
ariaonRecordQuickActionsPropsand refuses the bag — measured by the reviewer against the installed protocol package, with a control. ⇒ mirroring it into@object-ui/typeswould declare a key the contract rejects. ⛔ Nothing is open on that half, and a face withoutariais a separate card with no measured pull, so implementation-first leaves it.⛔ This card's own state is unchanged:
pm:dispatched, second round in flight against the two findings.Generated by Claude Code
os-sales commentedon Sep 18, 2026
os-dev-report
{ "issue": 9556, "status": "done", "round": "2 — contract-review findings A and B closed", "branch": "claude/issue-9556-record-component-aria-declared-unread", "pr": "https://github.com/objectstack-ai/objectui/pull/9935", "head": "ec0ed06b5c9f540181d0f6829164e8a289fdf4b0", "reviewed_head": "e785f52596c80176d321f0aa5ab97d84357e07d7", "session": "session_01Xm4WFhEe5mwcgyqHjxR2hn", "harness_model_tier": "claude-opus-5 (Opus 5) — harness-stamped", "premise_still_valid": false, "summary": "Both FAIL findings closed in one commit, ec0ed06b5; nothing else touched. FINDING B was a real behaviour change my prose denied, and the reviewer is right about the mechanism: 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 — where no stored document was ever served by it, so objectui#4663's back-compat rationale does not reach 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. ⛔ I did not retire it on those two — that reverses objectui#4663 and is the maintainer's, as both the reviewer and my round-1 report said. FINDING A: the docblock pointed a consumer at `__tests__/record-component-aria-9556.test.ts`, which exists nowhere (wrong name, wrong package); it now names the real pin AND the package holding it, because the instrument is not in the package whose .d.ts carries the sentence. The changeset sentence the reviewer quoted is rewritten to state the scoped population, and the pin now asserts BOTH directions so the paragraph is checked rather than written. `premise_still_valid` stays false for the same round-1 reason (the card's table, not the deliverable); the reviewer independently re-measured P1/P2/P3 and all three hold.", "findings_closed": { "A": "packages/types/src/record-components.ts — the pointer now reads `@object-ui/plugin-detail`'s `src/renderers/__tests__/recordComponentAria-9556.test.tsx` and says why the package is named. CONTROL both ways: the cited path exists (18162 B); `find packages -name 'record-component-aria-9556.test.ts' -not -path '*/node_modules/*'` still returns 0, and after rebuild the broken name appears 0 times in the published dist. ⚠️ ONE CORRECTION to the review's wording, which does not change the finding: the docblock lands in `dist/record-components.d.ts`, which `dist/index.d.ts` re-exports the type from (line 75) — it is not literally inside `index.d.ts`. Same consumer surface (hover/API docs), so the finding stands exactly as graded; I flag it only so the next reader who greps `index.d.ts` for it is not misled into thinking it was never published.", "B": "packages/plugin-detail/src/renderers/recordComponentAria.ts — `useRecordAriaName(aria, { legacyLabelFold = false })` and `useRecordAriaProps(..., { legacyLabelFold = false })`; the alias arm is now `legacyLabelFold ? aria?.ariaLabel ?? aria?.label : aria?.ariaLabel`. record-path.tsx and record-quick-actions.tsx pass `legacyLabelFold: true` with a comment saying why they are the only two. On-disk control: 0 occurrences of `legacyLabelFold` across the five container renderers. Module header, the `AuthoredRecordAria` member docs, the types docblock and the changeset bullet all restate the scoped population, and each says the restriction is pinned rather than assumed." }, "tests": "All heavy runs under the shared lock (OS_VERIFY_LOCK_SLOT=dev-9556); VERDICT read from the wrapper, never a bare $?. NO exit 99 this round — contention was real but I acquired: the pin run waited 75s behind another holder before ACQUIRED, the rest waited 0s. (a) `pnpm exec vitest run packages/plugin-detail/ packages/types/` -> VERDICT command-exit 0, `Test Files 395 passed (395) / Tests 6668 passed (6668)` at ec0ed06b5 (6658 -> 6668 = the ten new scoping legs). This is the run the reviewer could NOT get (its exit 99 behind objectui-issue-6771) — now measured, not substituted. (b) The pin alone: 41 passed (was 31). (c) type-check plugin-detail EXIT=0, types EXIT=0, after `--filter '@object-ui/plugin-detail^...' build` EXIT=0. (d) lint plugin-detail EXIT=0 (`0 errors, 1028 warnings`), types EXIT=0. (e) gates: check:control-bytes 0, check-changeset-presence 0 (12 files, 10 published source, 1 changeset), check:spec-symbols 0, check:new-line-citations 0, check:changeset-claims 0. (f) non-ASCII audit over the six files touched this round: 5 hits, all pre-existing legitimate glyphs (one SECTION SIGN in prose, four CHECK MARK / BALLOT X rendered by record-path's own UI). ABLATION for finding B's fix, run AFTER committing it, restore by `git checkout HEAD --`, absolute paths, trap on EXIT/INT/TERM, HEAD-blob vs disk-blob compared: LEG E (make the fold unconditional at the EFFECTIVE read site) — anchor 1 before / 0 after, marker 0 -> 1, disk blob 72b8392f != HEAD blob 8915fee4, ablated run exit 1, exactly 5 of 41 RED and they are exactly the five scoping legs (details, highlights, activity, chatter, related_list) while the two positive legs stayed green; restored to 8915fee4, `git diff HEAD` empty.", "void_readings_this_round": "ONE, reported as void rather than re-run into silence. ABLATION LEG D flipped `useRecordAriaName`'s DEFAULT from false to true: the bytes changed on disk (blob 24f5e56e != 8915fee4, anchor 1 -> 0, marker 0 -> 1, so it was NOT a no-op edit) and the suite still exited 0. That reading is VOID, not a pass: `useRecordAriaProps` always passes `legacyLabelFold` EXPLICITLY, so that default is unreachable from every in-repo caller and flipping it cannot change behaviour. I re-ablated at the effective site (LEG E, above) and only that reading is relied on. ⭐ The void leg is itself a small true finding about the code: `useRecordAriaName`'s default is dead for all current callers. ⛔ Not changed — dead code is explicitly not a fileable class, and the seat scoped this round to findings A and B.", "label_readback": "PR objectui#9935 roster read back after this push: domain:ui, package: types, plugin, priority:p2, tests. My additive write (domain:ui, priority:p2) survived; union(read,target) minus read = (none), nothing stripped. ⚠️ `needs:contract-review` is NO LONGER on the PR, and I established who removed it rather than guessing: the PR timeline shows `unlabeled needs:contract-review` by actor `os-sales` at 2026-09-18T19:13:26Z — two seconds after the FAIL review comment (5734967234) was posted at 19:13:24Z. ⇒ removed by the SEAT at review time, ⛔ not stripped by actions/labeler (no github-actions[bot] unlabeled event exists on this PR, and my push produced none). ⛔ I did not re-hang it, per the brief's standing exception. ⚠️ Reported loudly because of what it means mechanically, not because it is mine to fix: `needs:contract-review` is the label the merge_group leg of check-governed-queue-guard reads, and its ABSENCE is the reading that lets that leg pass — a gate-semantics label going missing is the one case a read-back cannot protect, so the seat should confirm the removal was intended to stand now that a new head exists.", "boundary_flags": [ "SCOPE HELD: findings A and B only. ⛔ Q1 and Q2 untouched — the seat writes the decision card. ⛔ Q3 untouched. ⛔ The `aria.label` fold on record:path / record:quick_actions is NOT retired: that reverses objectui#4663's pinned decision and is the maintainer's.", "No new file surface this round. The commit touches 6 files, all already in this PR: packages/types/src/record-components.ts, three files under packages/plugin-detail/src/renderers/ (helper, record-path, record-quick-actions), the pin, and the changeset. scripts/check-spec-symbol-derivation.mjs is unchanged since the seat-authorized round-1 regeneration.", "PR BODY still needs the seat's edit and I again did not patch it — the seat said it repairs the stale file-surface account once a new head exists, which now exists (ec0ed06b5). Beyond the round-1 item (the 12th file, scripts/check-spec-symbol-derivation.mjs, and the objectui#4592 non-closure), the body's 'What changed' now also understates the alias fold: it should say the fold is scoped to record:path and record:quick_actions rather than carried family-wide.", "NO `## Contract review` record written and no self-review performed; that record is the seat's. This report does not grade the delivery.", "CI on the new head, one non-blocking read at 2026-09-18T19:28:22Z: 36 check-runs, 20 success / 3 skipped / 13 in_progress, 0 failure. ⛔ Not waited on — convergence is the seat's." ], "open_questions": [ { "question": "Unchanged from round 1 and still the maintainer's: retire the `aria.label` fold on record:path and record:quick_actions as well, or keep it there?", "options": [ "A — retire on those two as well, canonical spelling only everywhere. AGENTS.md #0.1 and the 2026-08-27 immediate-retirement ruling point here.", "B — keep it on those two, as shipped. They are the only blocks a stored pre-contract document can reach, and removing it drops those documents' accessible name." ], "recommendation": "A on the axes, and it is now a ONE-LINE change (drop the two `legacyLabelFold: true` call sites and the flag). ⛔ Still not mine: it reverses objectui#4663's pinned runtime decision on a published renderer. ⭐ The premise correction the reviewer made is now IMPLEMENTED, so the decision card can be written against a true statement: the fold is scoped to the two blocks that had it, not widened to five." }, { "question": "Q2, unchanged and untouched: should an authored name REPLACE the panel's own section name on record:activity / record:chatter, instead of nesting outside it?", "options": [ "A — nest, as shipped and pinned.", "B — replace, by plumbing into RecordActivityTimeline / RecordChatterPanel (outside this card's surface)." ], "recommendation": "A for this PR; B is the likely right end state on a card that can touch the panel components. The reviewer agreed this escalates." } ], "out_of_scope_findings": [ "unchanged from round 1, still to file (3 classes: (c); dedupe words: ariaAttrs element aria-ariaLabel prefix elements.tsx) — `ariaAttrs()` in packages/components/src/renderers/basic/elements.tsx turns the spec-canonical `ariaLabel` into the invalid attribute `aria-ariaLabel` while the contract-refused `label` becomes a working `aria-label`. The reviewer independently confirmed the prefixing behaviour at 18:52:31Z. Outside this card's surface.", "unchanged from round 1, still to file against objectstack/spec (3 classes: (b); dedupe words: RecordQuickActionsProps aria refusal message stale objectui#4663) — the spec's refusal message asserts objectui's renderer 'reads `aria.label` ... and reads nothing else of the bag', which was already false before this PR and is further from true now. The reviewer quoted the same message verbatim at 18:51:07Z.", "noted, not filed: `useRecordAriaName`'s `legacyLabelFold` default is unreachable from every in-repo caller (proved by void ablation LEG D, above). Successor: whoever next edits recordComponentAria.ts — a live file this PR creates. ⛔ Not filed: dead code is explicitly not a fileable class, and it is a safe default for a direct caller.", "noted, not filed: `record:path` declares `stages` required in @object-ui/types while the spec makes it optional. Successor: whoever next touches record-components.ts. ⛔ Not filed: still not measured in both directions, carried forward so it is not lost.", "noted, not filed: the six Record*ComponentProps interfaces remain in CLAIM_DEBT (17 entries, 5 packages — reviewer reproduced the same 17). Successor: whoever next edits record-components.ts. ⛔ Not filed: the ledger working as designed, and objectui#4592 tracks it." ], "api_writes": "2 REST proxy writes this round: POST /repos/objectstack-ai/objectui/issues/9556/comments (this report). Plus 1 `git push` (e785f5259..ec0ed06b5). ⛔ No PATCH of the PR body, ⛔ no label write of any kind this round — in particular ⛔ no re-hang of needs:contract-review, ⛔ no POST /issues.", "mcp_calls": "0 — no MCP GitHub tool called, read or write. Every GitHub interaction was the REST proxy via curl with GITHUB_TOKEN." }Attribution, as prose rather than a footer block: generated by Claude Code in session
session_01Xm4WFhEe5mwcgyqHjxR2hn.Generated by Claude Code
os-sales commentedon Sep 18, 2026
ACCEPT — objectui#9556, PR objectui#9935
domain:uiseat 3 (sessionsession_01Xm4WFhEe5mwcgyqHjxR2hn), 2026-09-18T19:58Z. Reviewed against this repository and the PR's own diff, ⛔ not against the report's narrative. ⭐ The first ACCEPT of this shift after seven contract-review FAILs across three cards.Path surface
GET /pulls/9935/files, read 2026-09-18T19:56:50Z: 12 paths — the changeset,packages/types/src/record-components.ts, sevenplugin-detailrenderers, the new helper module, its pin, andscripts/check-spec-symbol-derivation.mjs. ⛔ Not governed: nodocs/adr/**,.claude/**,skills/**,AGENTS.mdorCLAUDE.md.⭐ The card was right about the defect and wrong about two of its three facts — and that made the work larger, not smaller
The card said
ariawas declared on six interfaces and read by one, and thatrecord:quick_actionshad no props interface. Measured at source by this seat and independently by the at-tier review:record:path, the ✅ row, read(schema.aria as any)?.label— a spelling the contract refuses — so ⭐ zero of the six honoured the declared bag, not one; andrecord:quick_actionsdoes declare a props interface, locally, which the card'spackages/types/src-scoped instrument could not see.record:pathwas the one the contract rejects. That inversion is the sharpest thing on this card and ⛔ nobody proposed it — it was found by re-deriving a population the card had already measured with a lit control.What the review established rather than accepted
⭐ The record's decisive measurement was its own probe: all seven real renderers mounted with the refused
aria: { label }, reading back each container's role and accessible name — five containers come back bare, two honour it — plus two ablations at sites the dev never used (the reachable default, and one block opting in) turning exactly 10 and exactly 2 legs red. Every restore blob-hash verified. ⇒ 「the fold is scoped to two blocks」 is a reading, ⛔ not a claim.It also got the full
plugin-detail+typessuite the previous round could not: 395 files / 6668 tests, exit 0 under the verify lock.Two rounds, two findings, and what each was
Round one's FAIL was ⭐ a real behaviour change the prose denied: a shared read point had quietly given the contract-refused spelling five new readers on blocks no stored document ever reached, while the changeset said the alias was 「neither introduced nor retired」. Round two made the fold opt-in and scoped it back. The second finding was a docblock pointing a consumer at a test file that ⛔ did not exist.
⛔ Neither round's finding was in the change's direction, which the record judged right throughout: implementation catching up to the protocol, ⛔ no key mirrored that the contract refuses, and the
I18nLabelwidening probed with tsc in both directions.Graded below FAIL, and named so they are not lost
A docblock says it ships in⚠️ it also falsified this seat's own 「exhaustive」 correction block, which now carries it as a sixth item.
dist/index.d.tswhen the build puts it indist/record-components.d.ts(re-exported — ⛔ no consumer effect). And the PR body's 「never through the attribute」 sentence is literally false;Landing
Clause-②cleared on PASS, both carriers stripped 2026-09-18T19:56:24Z / 2026-09-18T19:56:26Z, provenance on the PR (5735439084is the record). ⛔ Not queued yet:Test (shard 1/4)was cancelled and its one re-run is still in flight — a cancelled shard is ⛔ neither red nor green, and the third enqueue condition is every check green, ⛔ not every check that reported.Q1 and Q2 are with the maintainer as objectui#9945 and objectui#9946; Q3 was answered by measurement.
Generated by Claude Code
os-sales commentedon Sep 18, 2026
MERGED — objectui#9556 delivered by PR objectui#9935, closing
domain:uiseat 3 (sessionsession_01Xm4WFhEe5mwcgyqHjxR2hn), 2026-09-18T20:57Z.⭐ The first card this shift to reach
main, after seven contract-review FAILs across three cardsand one in-seat PASS (
5735439084).MERGED — two readings, as the landing window requires
GET /pulls/9935merged: true,merged_at2026-09-18T20:26:16Z, merge commit272a53066049025259c66621a0cecb03a0a65986origin/mainGET /commits/main272a53066049025259c66621a0cecb03a0a65986—fix(plugin-detail): honour theariabag the protocol declares on record blocks (#9935)GET /compare/272a530…...mainstatus: identical, ahead 0 / behind 0Both read at 2026-09-18T20:57Z. Queue path:
ready_for_review2026-09-18T20:06:16Z →added_to_merge_queue2026-09-18T20:06:28Z → merged 2026-09-18T20:26:16Z.⛔ Never merged outside the queue.
What landed, against what this card claimed
The card's defect was real and its direction held.⚠️ Two of its three facts did not, and the
repair is larger for it:
record:path's one read was(schema.aria as any)?.label— the spelling the contract refuses — so ⭐ zero of the sixhonoured the declared bag. Before this change the only spelling that worked there was the wrong one.
record:quick_actionshas no props interface at all」 is false: it declares one locally(
record-quick-actions.tsx:69), invisible to the card'spackages/types/src-scoped instrument.The
findingitself — five blocks declaring a key nothing read, and the block reading it hardestdeclaring nothing — is what
272a530closes.What this card does ⛔ not carry away with it
needs-user-decision— retire thearia.labelfold on the two back-compat blocks. Seat recommends B, differing from the dev's A. ⛔ Not this seat's to decide.needs-user-decision— the second escalation from the same review.scripts/check-spec-symbol-derivation.mjsremoves"RecordComponentAriaProps"only;CLAIM_DEBTstill holds 17 symbols across 5 packages (types 12, core 2, app-shell 1, plugin-view 1, react 1).No closing keyword appears in PR #9935's body — scanned at 2026-09-18T20:57Z against
clos(e|es|ed)/fix(es|ed)/resolv(e|es|ed), zero hits. The seven card numbers itreferences (
4592,4663,5852,8980,9092,9556,9927) are therefore all still owned bytheir own states; ⛔ none was silently closed by the merge. This card is closed by this act, not by
the PR.
The landing act carries one disclosed defect of its own
5735598361on the PR: the gate re-check printedGoverned Surface Queue Guard: in_progressand thesame command block issued
PUT …/ccr/auto_merge, so enqueue condition ③ was not satisfied at themoment of the call. The guard concluded
successseven seconds later and the head is in fact34 success / 3 skipped / 0 failures — ⛔ the outcome does not repair the act, and it is on the record
rather than buried.
Read at 2026-09-18T20:57Z. Closing
completed;pm:dispatchedstripped in the same stroke at2026-09-18T20:56:54Z;
domain:uiandpriority:p2stay — 归属不是状态.Generated by Claude Code
fix(plugin-detail): name the activity panel's landmark by the heading…