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({
);