Found while implementing #5956 + #5957 (PR on branch claude/issue-5956-record-path-a11y-residue). Outside both cards' fences — both are about what a stage says; this is about which stages get classified as the goal in the first place. Filing rather than folding in.
The asymmetry
packages/plugin-detail/src/renderers/record-path.tsx renders two rows from the same stages[], and they do not agree on which stage is a won terminus.
Desktop restricts it to the last forward stage:
constisWonTerminus=forwardKinds[idx]==='won'&&idx===last;// ...terminal: isWonTerminus ? 'won' : undefined,
Mobile passes the classification through for every won-classified stage:
constkind=stageKinds[idx];// ...terminal: kind,
renderStage hands that same terminal to both railClass and stageAriaLabel, so the two rows diverge in the paint and, after #5957, in the accessible name too.
Why it is reachable
classify() reaches won through the WON_TOKENS heuristic (won|success|成交|赢|完成) as well as an explicit terminal: 'won', and 完成 is an ordinary mid-path word. A path like 草稿 → 完成 → 已归档 classifies index 1 as won while last is 2, so:
- desktop: index 1 gets
terminal: undefined → bg-muted, announces {{stage}}, upcoming - mobile: index 1 gets
terminal: 'won' → bg-emerald-500/30, announces {{stage}}, goal stage, not reached
One control, one record, two answers, chosen by viewport width.
Not a regression from #5957
The paint half predates it — railClass has always consumed the same asymmetric terminal. #5957's fix deliberately derives the name from the very value railClass receives, precisely so the name tracks the paint on each row rather than introducing a second classification. That makes the name correct per row and leaves this underlying disagreement exactly as it was, now visible in two channels instead of one.
What needs deciding
Which row is right. Two defensible readings, and picking one is a product-ish call about what the heuristic is for, which is why this is not folded in:
- Desktop is right — "the goal" is the terminus of the forward path, so only the last forward stage can be it, and a mid-path
完成 is a false positive of the heuristic. Fix: mobile adopts the idx === last restriction. - Mobile is right — any stage the heuristic calls
won is a successful outcome worth marking. Fix: desktop drops the idx === last restriction.
Reading 1 also narrows the heuristic's blast radius, which is the more conservative direction. Either way the two rows should share one classification, computed once, rather than each deriving its own.
Related: #5916 (the accessible-name mechanism), #5956 and #5957 (the two residues, being implemented together).
Generated by Claude Code
Found while implementing #5956 + #5957 (PR on branch
claude/issue-5956-record-path-a11y-residue). Outside both cards' fences — both are about what a stage says; this is about which stages get classified as the goal in the first place. Filing rather than folding in.The asymmetry
packages/plugin-detail/src/renderers/record-path.tsxrenders two rows from the samestages[], and they do not agree on which stage is awonterminus.Desktop restricts it to the last forward stage:
Mobile passes the classification through for every won-classified stage:
renderStagehands that sameterminalto bothrailClassandstageAriaLabel, so the two rows diverge in the paint and, after #5957, in the accessible name too.Why it is reachable
classify()reacheswonthrough theWON_TOKENSheuristic (won|success|成交|赢|完成) as well as an explicitterminal: 'won', and完成is an ordinary mid-path word. A path like草稿 → 完成 → 已归档classifies index 1 aswonwhilelastis 2, so:terminal: undefined→bg-muted, announces{{stage}}, upcomingterminal: 'won'→bg-emerald-500/30, announces{{stage}}, goal stage, not reachedOne control, one record, two answers, chosen by viewport width.
Not a regression from #5957
The paint half predates it —
railClasshas always consumed the same asymmetricterminal. #5957's fix deliberately derives the name from the very valuerailClassreceives, precisely so the name tracks the paint on each row rather than introducing a second classification. That makes the name correct per row and leaves this underlying disagreement exactly as it was, now visible in two channels instead of one.What needs deciding
Which row is right. Two defensible readings, and picking one is a product-ish call about what the heuristic is for, which is why this is not folded in:
完成is a false positive of the heuristic. Fix: mobile adopts theidx === lastrestriction.wonis a successful outcome worth marking. Fix: desktop drops theidx === lastrestriction.Reading 1 also narrows the heuristic's blast radius, which is the more conservative direction. Either way the two rows should share one classification, computed once, rather than each deriving its own.
Related: #5916 (the accessible-name mechanism), #5956 and #5957 (the two residues, being implemented together).
Generated by Claude Code