diff --git a/docs/screenshots/leftover-map-comparison-singular-desktop.png b/docs/screenshots/leftover-map-comparison-singular-desktop.png new file mode 100644 index 000000000..8e1fac9c4 Binary files /dev/null and b/docs/screenshots/leftover-map-comparison-singular-desktop.png differ diff --git a/docs/screenshots/leftover-map-comparison-singular-mobile.png b/docs/screenshots/leftover-map-comparison-singular-mobile.png new file mode 100644 index 000000000..7f9bac155 Binary files /dev/null and b/docs/screenshots/leftover-map-comparison-singular-mobile.png differ diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 26b00286f..c1b9649b6 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,7 +5,7 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The comparison variant names leftover-map complete-case coverage under `Leftover map comparison graphic coverage` and leftover-map item complete-case coverage under `Leftover map comparison graphic item coverage` and leftover-map incomplete post coverage under `Leftover map comparison graphic incomplete posts` and leftover-map incomplete item coverage under `Leftover map comparison graphic incomplete items`. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, `Comparison`, `ComparisonMissingCoverage`, `ComparisonMissingItemCoverage`, `ComparisonMissingIncompletePosts`, and `ComparisonMissingIncompleteItems` cover two-pair maps, rank-0 origin with 0% share, a `0` tick, `d 0.00`, `R̂ 0.00`, `R̂²/R² 0.00`, `U²/R² 0.00`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, missing share that keeps existing leftover-map axis text, grouping comparison leftover-map graphic coverage that matches leftover map used N of M scored posts, omitted comparison graphic coverage when leftoverMapCoverageCounts fail-closes, grouping comparison leftover-map graphic item coverage that matches leftover map used N of M scored criteria, omitted comparison graphic item coverage when leftoverMapItemCoverageCounts fail-closes, grouping comparison leftover-map graphic incomplete posts that matches leftover map dropped N incomplete posts, omitted comparison graphic incomplete posts when leftoverMapIncompletePostCount fail-closes, grouping comparison leftover-map graphic incomplete items that matches leftover map dropped N incomplete criteria, and omitted comparison graphic incomplete items when leftoverMapIncompleteItemCount fail-closes. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `--color-primary`, `--color-palette-blue-mid` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. The comparison variant keeps exact persisted σ and share together and right-bounds both axis captions inside the SVG. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The comparison variant names leftover-map complete-case coverage under `Leftover map comparison graphic coverage` and leftover-map item complete-case coverage under `Leftover map comparison graphic item coverage` and leftover-map incomplete post coverage under `Leftover map comparison graphic incomplete posts` and leftover-map incomplete item coverage under `Leftover map comparison graphic incomplete items`. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, `Comparison`, `ComparisonMissingCoverage`, `ComparisonMissingItemCoverage`, `ComparisonMissingIncompletePosts`, and `ComparisonMissingIncompleteItems` cover two-pair maps, rank-0 origin with 0% share, a `0` tick, `d 0.00`, `R̂ 0.00`, `R̂²/R² 0.00`, `U²/R² 0.00`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, missing share that keeps existing leftover-map axis text, grouping comparison leftover-map graphic coverage that matches leftover map used N of M scored posts, omitted comparison graphic coverage when leftoverMapCoverageCounts fail-closes, grouping comparison leftover-map graphic item coverage that matches leftover map used N of M scored criteria, omitted comparison graphic item coverage when leftoverMapItemCoverageCounts fail-closes, grouping comparison leftover-map graphic incomplete posts that matches leftover map dropped N incomplete posts, omitted comparison graphic incomplete posts when leftoverMapIncompletePostCount fail-closes, grouping comparison leftover-map graphic incomplete items that matches leftover map dropped N incomplete criteria, and omitted comparison graphic incomplete items when leftoverMapIncompleteItemCount fail-closes. Desktop and 390×844 evidence is retained in `docs/screenshots/leftover-map-comparison-singular-{desktop,mobile}.png`. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `--color-primary`, `--color-palette-blue-mid` | | `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The grouping comparison strip names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount return usable integers. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | | `Workspace/OperationsDashboard` | Compare Event and post counts, inspect external-information coverage, then open the cited source behind a claim, handover, or repeat-issue fact. `EvidenceReady`, `NarrowViewport`, `AnalysisPendingAndMissingEvidence`, `AnalysisFailed`, and `LoadError` cover populated, mobile, unavailable-evidence, analysis-pending, retryable failure, and transport-error states. | `--color-dashboard-*`, `OperationsDashboard` | | `Post/SimilarVocPanel` | Compare ontology/semantic similar VOC and prior action evidence, then open the source; unavailable states show no fabricated TEPP theta or weight. | `SimilarVocPanel.css`, `SimilarVocPanel` | diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 544f89488..153bb9af2 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4197,8 +4197,8 @@ describe("App, authenticated", () => { expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.86, 18%)")).toBeInTheDocument(); expect( screen.getAllByRole("button", { name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, @@ -4322,12 +4322,12 @@ describe("App, authenticated", () => { ).not.toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 1 (82%)", + "leftover map comparison axis 1 (σ 1.84, 82%)", ), ).toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 2 (18%)", + "leftover map comparison axis 2 (σ 0.86, 18%)", ), ).toBeInTheDocument(); expect( diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 56258c883..42e6bf0f6 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -843,8 +843,16 @@ describe("LeftoverMapPlot", () => { "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + const axis1 = screen.getByText( + "leftover map comparison axis 1 (σ 1.84, 82%)", + ); + const axis2 = screen.getByText( + "leftover map comparison axis 2 (σ 0.86, 18%)", + ); + expect(axis1).toHaveAttribute("x", "472"); + expect(axis1).toHaveAttribute("text-anchor", "end"); + expect(axis2).toHaveAttribute("x", "472"); + expect(axis2).toHaveAttribute("text-anchor", "end"); expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2 (18%)")).not.toBeInTheDocument(); }); @@ -861,9 +869,11 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 1 (σ 1.84)"), + ).toBeInTheDocument(); expect(screen.getByText("leftover map comparison axis 2")).toBeInTheDocument(); - expect(screen.queryByText(/leftover map comparison axis 1 \(/)).not.toBeInTheDocument(); + expect(screen.queryByText(/leftover map comparison axis 1 \(σ 1\.84,/)).not.toBeInTheDocument(); expect(screen.queryByText(/leftover map comparison axis 2 \(/)).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2")).not.toBeInTheDocument(); @@ -890,8 +900,12 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)"), + ).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 2 (σ 0.00, 0%)"), + ).toBeInTheDocument(); }); it("names leftover-map coverage on the comparison graphic with a distinct accessible name", () => { @@ -935,8 +949,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover map comparison incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover map comparison incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.86, 18%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic coverage when coverage is missing or not usable", () => { @@ -954,7 +968,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic coverage")).not.toBeInTheDocument(); expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { "Leftover map used 2 of 2 scored criteria (complete-case)", ); expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic item coverage when item coverage is missing or not usable", () => { @@ -1009,7 +1023,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete posts when incomplete post coverage is missing or not usable", () => { @@ -1067,7 +1081,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete items when incomplete item coverage is missing or not usable", () => { @@ -1128,7 +1142,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names leftover-map comparison graphic coverage 0 of M on a rank-0 origin when that persisted used count is a non-negative integer", () => { @@ -1209,8 +1223,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map reconstruction on the comparison graphic with a distinct accessible name", () => { @@ -1246,7 +1260,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic incomplete items")).toHaveTextContent( "Leftover map dropped 0 incomplete criteria", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic reconstruction when R̂ is missing or not usable", () => { @@ -1280,7 +1294,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin reconstruction on the comparison graphic when that persisted value is finite", () => { @@ -1323,7 +1337,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map explained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1359,7 +1373,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic explained leftover share when e is missing or not usable", () => { @@ -1396,7 +1410,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin explained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1443,7 +1457,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1482,7 +1496,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover share when s is missing or not usable", () => { @@ -1522,7 +1536,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin unexplained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1573,7 +1587,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map cross share on the comparison graphic with a distinct accessible name", () => { @@ -1615,7 +1629,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic cross share when x is missing or not usable", () => { @@ -1658,7 +1672,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map cross share on the comparison graphic when that persisted value is finite", () => { @@ -1713,7 +1727,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover on the comparison graphic with a distinct accessible name", () => { @@ -1758,7 +1772,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover when U is missing or not usable", () => { @@ -1804,7 +1818,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map unexplained leftover on the comparison graphic when that persisted value is finite", () => { @@ -1863,7 +1877,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover residual on the comparison graphic with a distinct accessible name", () => { @@ -1911,7 +1925,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover residual when R is missing or not usable", () => { @@ -1960,7 +1974,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover residual on the comparison graphic when that persisted value is finite", () => { @@ -2022,7 +2036,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover observed on the comparison graphic with a distinct accessible name", () => { @@ -2073,7 +2087,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover observed when Y is missing or not usable", () => { @@ -2125,7 +2139,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover observed on the comparison graphic when that persisted value is finite", () => { @@ -2191,7 +2205,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover expected on the comparison graphic with a distinct accessible name", () => { @@ -2241,7 +2255,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover expected when E is missing or not usable", () => { @@ -2296,7 +2310,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover expected on the comparison graphic when that persisted value is finite", () => { @@ -2366,6 +2380,6 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index ef6ab3044..e199fab76 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -27,6 +27,12 @@ import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, LEFTOVER_MAP_PLOT_AXIS_SHARE, } from "../leftoverMapPlotAxisShare"; +import { + formatLeftoverMapPlotAxisSingular, + leftoverSingularForAxis, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, +} from "../leftoverMapPlotAxisSingular"; import { firstPlottablePairForPost, layoutLeftoverMapPlot, @@ -80,11 +86,24 @@ function leftoverMapPlotAxisText( const percent = formatLeftoverMapPlotAxisShare( leftoverShareForAxis(leftoverMapAxes, axisIndex), ); + const singular = formatLeftoverMapPlotAxisSingular( + leftoverSingularForAxis(leftoverMapAxes, axisIndex), + ); if (variant === "comparison") { + if (singular === null) { + if (percent === null) { + return t(axisIndex === 1 ? LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 : LEFTOVER_MAP_COMPARE_PLOT_AXIS_2); + } + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); + } if (percent === null) { - return t(axisIndex === 1 ? LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 : LEFTOVER_MAP_COMPARE_PLOT_AXIS_2); + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, { axis: axisIndex, value: singular }); } - return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, { + axis: axisIndex, + value: singular, + share: percent, + }); } if (percent === null) { return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2"); @@ -98,6 +117,9 @@ function leftoverMapPlotAxisText( * Person markers are posts; item markers are leftover criteria. Click a * post marker to open that post. Caption leftover-map axes with persisted * Gabriel inertia share when finite, including rank-0 zero-share axes. + * Comparison graphic axes additionally name finite, non-negative persisted + * Gabriel singular values independently of axis share; never derive one + * measurement from the other. * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the * pair row match the plot. Pair segments name persisted leftover-map * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map @@ -126,8 +148,8 @@ function leftoverMapPlotAxisText( * caption when incomplete post coverage is missing or not a usable integer. * Omit that leftover-map incomplete item caption when incomplete item * coverage is missing or not a usable integer. - * Omit that axis badge when share is - * missing or non-finite and keep the existing leftover-map axis text. + * Omit each axis evidence badge independently when its persisted evidence is + * unusable and keep the remaining buyer-visible axis evidence intact. * Omit the plot when no pair has four finite leftover-map coordinates. * ADR 0304 reuses this graphic on the grouping comparison strip from * already-named leftover-map coordinates. ADR 0305 captions leftover-map axis @@ -176,6 +198,8 @@ function leftoverMapPlotAxisText( * the same localized composition boundary rather than adding another static * comparison-only translation key. ADR 0320 names comparison coordinate ticks * through the same composition boundary, preserving localized generic tick copy. + * ADR 0321 adds persisted comparison-axis singular evidence without deriving it + * from axis share. * Never invent a leftover score. */ export function LeftoverMapPlot({ @@ -296,7 +320,12 @@ export function LeftoverMapPlot({ {leftoverMapPlotAxisText(1, leftoverMapAxes, variant)} - + {leftoverMapPlotAxisText(2, leftoverMapAxes, variant)} {layout.ticks.map((tick) => ( diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts new file mode 100644 index 000000000..267522cbf --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -0,0 +1,39 @@ +/** Project persisted leftover-map singular-value evidence into buyer-visible axis captions. */ + +import type { LeftoverMapAxis } from "./api"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = + "leftover map comparison axis {axis} (σ {value})"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE = + "leftover map comparison axis {axis} (σ {value}, {share}%)"; + +export type LeftoverMapPlotAxisSingular = { + axis_index: LeftoverMapAxis["axis_index"]; + leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; +}; + +export function leftoverSingularForAxis( + axes: ReadonlyArray | null | undefined, + axisIndex: number, +): number | null { + const axis = axes?.find((candidate) => candidate.axis_index === axisIndex); + if ( + axis == null || + axis.leftover_singular_value == null || + !Number.isFinite(axis.leftover_singular_value) || + axis.leftover_singular_value < 0 + ) { + return null; + } + return axis.leftover_singular_value; +} + +export function formatLeftoverMapPlotAxisSingular( + leftoverSingular: number | null | undefined, +): string | null { + if (leftoverSingular == null || !Number.isFinite(leftoverSingular) || leftoverSingular < 0) { + return null; + } + return leftoverSingular.toFixed(2); +} diff --git a/tests/test_grouping_comparison_graphic_singular_contract.py b/tests/test_grouping_comparison_graphic_singular_contract.py new file mode 100644 index 000000000..d153db856 --- /dev/null +++ b/tests/test_grouping_comparison_graphic_singular_contract.py @@ -0,0 +1,70 @@ +"""Executable contract for persisted singular values on the comparison graphic.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PLOT_SOURCE = ROOT / "frontend" / "src" / "components" / "LeftoverMapPlot.tsx" +SINGULAR_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotAxisSingular.ts" +APP_TEST_SOURCE = ROOT / "frontend" / "src" / "App.test.tsx" + + +def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None: + """Comparison-axis σ copy must remain distinct from report, strip, share, and tick copy.""" + assert SINGULAR_SOURCE.exists(), ( + "comparison graphic singular-value helper is missing; do not infer σ from axis share" + ) + singular_source = SINGULAR_SOURCE.read_text(encoding="utf-8") + plot_source = PLOT_SOURCE.read_text(encoding="utf-8") + + assert ( + 'export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR =\n' + ' "leftover map comparison axis {axis} (σ {value})";' + in singular_source + ) + assert ( + 'export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE =\n' + ' "leftover map comparison axis {axis} (σ {value}, {share}%)";' + in singular_source + ) + assert "LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR" in plot_source + assert "LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE" in plot_source + + +def test_singular_value_is_read_from_axis_evidence_and_fails_closed() -> None: + """Persisted σ=0 stays explicit; missing, non-finite, or negative σ omits independently.""" + assert SINGULAR_SOURCE.exists(), "persisted singular-value projection helper is missing" + singular_source = SINGULAR_SOURCE.read_text(encoding="utf-8") + + assert "leftover_singular_value" in singular_source + assert "Number.isFinite" in singular_source + assert "< 0" in singular_source + assert "return null" in singular_source + assert ".toFixed(2)" in singular_source + assert "leftover_share" not in singular_source + assert "Math.max" not in singular_source + assert "Math.min" not in singular_source + + +def test_app_acceptance_requires_exact_comparison_sigma_and_share_copy() -> None: + """App acceptance must assert the persisted comparison σ and share together, exactly.""" + app_test_source = APP_TEST_SOURCE.read_text(encoding="utf-8") + + stale_share_only = ( + '"leftover map comparison axis 1 (82%)"', + '"leftover map comparison axis 2 (18%)"', + ) + expected_sigma_share = ( + '"leftover map comparison axis 1 (σ 1.84, 82%)"', + '"leftover map comparison axis 2 (σ 0.86, 18%)"', + ) + + for stale_copy in stale_share_only: + assert stale_copy not in app_test_source, ( + "comparison-axis App acceptance still asserts pre-ADR-0321 share-only copy" + ) + for expected_copy in expected_sigma_share: + assert app_test_source.count(expected_copy) >= 2, ( + "comparison-axis App acceptance must cover exact σ+share copy in both report and " + "grouping-comparison integration paths" + )