diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5db914a52..fdeca0d63 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,13 @@ # Product & Technical Gap Baseline +> Leftover-pair accessible-name overlay: 2026-09-07 KST. Issue #976 stacks +> onto leftover-map single-writer `#802` at `32f1cda10` so pair-button +> names include the same finite leftover evidence badges already rendered +> (`R`, `Y/E`, rank, unexplained, shares, reconstruction, coordinates, +> distance). Grouping-comparison pair actions stay on `#829`/`#830` and +> are not expanded here. This branch is Draft; it is not protected-main +> or independently approved evidence. +> > Exact-head rendered-UX overlay: 2026-08-31 12:16 KST. PR #802 > implementation revision `1e972d7f6` retains persisted-only `d`, `R̂`, and > `e` projection while moving dense segment captions off their plot segments diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 790c4da69..07c30ed61 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4175,7 +4175,11 @@ describe("App, authenticated", () => { expect(closestPair).toHaveTextContent("R̂ +0.25"); expect(closestPair).toHaveTextContent("ξ (+0.50, +0.10) ζ (+0.50, −0.02)"); expect(closestPair).toHaveTextContent("d 0.12"); - expect(closestPair).toHaveAccessibleName("Open leftover closest pair: Public post · sales-lead"); + expect(closestPair).toHaveAccessibleName(/Open leftover closest pair: Public post · sales-lead/); + expect(closestPair).toHaveAccessibleName(/R \+0\.40/); + expect(closestPair).toHaveAccessibleName(/Y 2\.40 · E 2\.00/); + expect(closestPair).toHaveAccessibleName(/rank 1/); + expect(closestPair).toHaveAccessibleName(/d 0\.12/); expect(farthestPair).toHaveTextContent("Farthest leftover: Specification revision requested · negative"); expect(farthestPair).toHaveTextContent( "Leftover map places this post at ξ (+0.90, +0.80) and the criterion at ζ (−0.70, −0.40) after IRT main effects. Open this post to read negative.", diff --git a/frontend/src/components/LeftoverPairList.test.tsx b/frontend/src/components/LeftoverPairList.test.tsx index 74f4e8a64..19972cfb6 100644 --- a/frontend/src/components/LeftoverPairList.test.tsx +++ b/frontend/src/components/LeftoverPairList.test.tsx @@ -46,9 +46,13 @@ describe("LeftoverPairList", () => { expect(screen.getByLabelText("Leftover pairs")).toBeInTheDocument(); const closest = screen.getByRole("button", { - name: "Open leftover closest pair: Public post · sales-lead", + name: /Open leftover closest pair: Public post · sales-lead/, }); expect(closest).toHaveTextContent("Closest leftover: Public post · sales-lead"); + expect(closest).toHaveAccessibleName(/R \+0\.40/); + expect(closest).toHaveAccessibleName(/Y 2\.40 · E 2\.00/); + expect(closest).toHaveAccessibleName(/rank 1/); + expect(closest).toHaveAccessibleName(/d 0\.12/); expect(closest).toHaveTextContent( "Read leftover map rank 1, observed Y 2.40, and expected E 2.00 after IRT main effects, then open this post.", ); @@ -58,8 +62,9 @@ describe("LeftoverPairList", () => { expect(closest).toHaveTextContent("d 0.12"); const farthest = screen.getByRole("button", { - name: "Open leftover farthest pair: Specification revision requested · negative", + name: /Open leftover farthest pair: Specification revision requested · negative/, }); + expect(farthest).toHaveAccessibleName(/R −1\.10/); expect(farthest).toHaveTextContent("R −1.10"); expect(farthest).toHaveTextContent("Y 0.90 · E 2.00"); expect(farthest).toHaveTextContent("rank 1"); @@ -78,6 +83,31 @@ describe("LeftoverPairList", () => { ); }); + it("omits non-finite leftover residual and distance from the pair action", () => { + render( + , + ); + + const closest = screen.getByRole("button", { + name: /Open leftover closest pair: Public post · sales-lead/, + }); + expect(closest).toHaveAccessibleName(/rank 1/); + expect(closest).not.toHaveAccessibleName(/R /); + expect(closest).not.toHaveAccessibleName(/\bd /); + expect(closest).not.toHaveTextContent("R —"); + expect(closest).not.toHaveTextContent("d NaN"); + }); + it("keeps residual guidance for an older payload without rank or Y/E", () => { render( { ); const closest = screen.getByRole("button", { - name: "Open leftover closest pair: Public post · sales-lead", + name: /Open leftover closest pair: Public post · sales-lead/, }); + expect(closest).toHaveAccessibleName(/R \+0\.40/); + expect(closest).toHaveAccessibleName(/R̂²\/R² 0\.76/); + expect(closest).toHaveAccessibleName(/ξ \(\+0\.50, \+0\.10\) ζ \(\+0\.50, −0\.02\)/); expect(closest).toHaveTextContent( "Leftover map places this post at ξ (+0.50, +0.10) and the criterion at ζ (+0.50, −0.02) after IRT main effects. Open this post to read sales-lead.", ); diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index 75e500821..8380161d3 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -33,6 +33,7 @@ import { formatLeftoverMapUnexplainedShare, LEFTOVER_MAP_UNEXPLAINED_SHARE_ACTION, } from "../leftoverMapUnexplainedShare"; +import { formatLeftoverMapDistance } from "../leftoverMapPlotLayout"; import { LeftoverMapPlot } from "./LeftoverMapPlot"; export type LeftoverPairListProps = { @@ -42,6 +43,11 @@ export type LeftoverPairListProps = { onSelectPost: (pair: LeftoverPair) => void; }; +/** Join finite leftover evidence already selected by the pair-row formatters. */ +function leftoverPairAccessibleName(parts: Array): string { + return parts.filter((part): part is string => typeof part === "string" && part.length > 0).join(" "); +} + /** * Closest and farthest leftover post–criterion pairs after IRT main effects. * @@ -89,6 +95,8 @@ export function LeftoverPairList({ pair.pair_kind === "farthest" ? t("Farthest leftover") : t("Closest leftover"); const criterion = criterionLabel(pair.criterion_code); const residual = formatLeftoverResidual(pair.leftover_residual); + const residualBadge = Number.isFinite(pair.leftover_residual) ? `R ${residual}` : null; + const distanceBadge = formatLeftoverMapDistance(pair.leftover_distance); const observedExpected = formatLeftoverObservedExpected( pair.observed_response, pair.expected_response, @@ -216,11 +224,23 @@ export function LeftoverPairList({ );