diff --git a/AGENTS.md b/AGENTS.md index d7b5f781e..a7432b861 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -482,6 +482,14 @@ compares leftover pairs can match the pair-row coordinate badge on live responses; a missing axis stays null and does not invent coordinates from leftover-map rank, leftover-map distance, leftover-map reconstruction, or leftover residual. The strip does not gain the leftover-map graphic. +ADR 0368 captions the grouping comparison strip with persisted leftover-map +axis share through leftoverMapCompareAxisShare so a buyer who compares leftover +pairs can read Gabriel inertia of residual SVD axes 1 and 2 for that grouping; +a missing or non-finite share omits that leftover-map comparison axis share +badge and does not invent leftover-map axis share from leftover-map singular +value, leftover pair count, or plotted marker count. Share `0` is shown when +persisted, including rank-0 unused axes. The strip does not gain leftover-map +singular values or the leftover-map graphic. When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, `e + s + x = 1`. When `Y`, `E`, and `R` are finite, `Y − E = R`. When `R`, `R̂`, and `U` are @@ -526,6 +534,8 @@ Grouping comparison leftover-map coordinates (ADR 0302) captions grouping compar buttons with persisted leftover-map coordinates `ξ` / `ζ`. Grouping comparison leftover-map coordinates payload (ADR 0303) returns persisted leftover-map coordinates `ξ` / `ζ` on `GET /api/reports/compare/{period}` leftover pairs. +Grouping comparison leftover-map axis share (ADR 0368) captions the grouping comparison +strip with persisted leftover-map axis share. Item complete-case coverage (ADR 0282) captions the leftover-map graphic with how many scored criteria entered the map. Item complete-case coverage on the pair list (ADR 0285) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0b32d177f..d31674480 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -697,6 +697,8 @@ ADR 0302 captions grouping comparison leftover-pair buttons with persisted lefto coordinates `ξ` / `ζ` through formatLeftoverMapCoordinates. ADR 0303 returns persisted leftover-map coordinates `ξ` / `ζ` on `GET /api/reports/compare/{period}` leftover pairs. +ADR 0368 captions the grouping comparison strip with persisted leftover-map axis share +through leftoverMapCompareAxisShare. Click a post marker or a pair button opens that post. Those ADRs are the normative mathematical, storage, and display contracts. Leftover-map axis share @@ -727,8 +729,9 @@ incomplete item coverage (ADR 0292), leftover-map reconstruction `R̂` (ADR 0299), leftover expected `E` (ADR 0300), leftover-map rank (ADR 0301), leftover-map coordinates `ξ` / `ζ` -(ADR 0302), and leftover-map coordinates on the compare leftover-pair payload -(ADR 0303); +(ADR 0302), leftover-map coordinates on the compare leftover-pair payload +(ADR 0303), and leftover-map axis share on the grouping comparison strip +(ADR 0368); `POST .../rebuild` scores every grouping kind (post_admin). `make seed` folds A-100/B-200 Event Lineage fixtures (and the Riverbend calendar post) that already have constructed IRT cells into the same shared diff --git a/CHANGELOG.d/2.61.0-leftover-map-compare-axis-share.md b/CHANGELOG.d/2.61.0-leftover-map-compare-axis-share.md new file mode 100644 index 000000000..c33fc22bb --- /dev/null +++ b/CHANGELOG.d/2.61.0-leftover-map-compare-axis-share.md @@ -0,0 +1,47 @@ +## 2.61.0 — Leftover-map axis share on the grouping comparison strip + +- Caption leftover-map axis share on the grouping comparison strip through + leftoverMapCompareAxisShare and return persisted leftover_map_axes on + `GET /api/reports/compare/{period}` (ADR 0368). After `make seed`, closest + and farthest leftover pairs sit above the member list with the Gabriel + biplot of already-named coordinates, leftover-map axis share when finite, + axis ticks that match `ξ (x, y) ζ (x, y)` on the pair row, pair segments + that match `d` on the pair row, pair segments that match `R̂` on the pair + row, pair segments that match `R̂²/R²` on the pair row, pair segments that + match `U²/R²` on the pair row, pair segments that match `2R̂U/R²` on the + pair row, pair segments that match `U` on the pair row, pair segments that + match `R` on the pair row, pair segments that match `Y` on the pair row, + pair segments that match `E` on the pair row, pair segments that match + leftover-map rank on the pair row, a plot caption that matches leftover-map + complete-case coverage above the pair list, a plot caption that matches + leftover-map item complete-case coverage, a plot caption that matches + leftover-map incomplete post coverage, a plot caption that matches + leftover-map incomplete item coverage, a pair-list note that matches + leftover-map post complete-case coverage, a pair-list note that matches + leftover-map item complete-case coverage, a pair-list note that matches + leftover-map incomplete post coverage, a pair-list note that matches + leftover-map incomplete item coverage, a grouping comparison note that + matches leftover-map post complete-case coverage, a grouping comparison + note that matches leftover-map item complete-case coverage, a grouping + comparison note that matches leftover-map incomplete post coverage, a + grouping comparison note that matches leftover-map incomplete item + coverage, grouping comparison leftover-pair buttons that match leftover-map + reconstruction `R̂`, leftover-map explained leftover share `e`, leftover-map + unexplained leftover share `s`, leftover-map cross share `x`, leftover-map + unexplained leftover `U`, leftover residual `R`, leftover observed `Y`, + leftover expected `E`, leftover-map rank, leftover-map coordinates `ξ` / `ζ` + on the pair row, a live compare payload that carries those persisted leftover-map + coordinates, and a grouping comparison leftover-map axis share badge that + matches leftover-axis report badges from the live compare leftover_map_axes + payload; click a post marker or a pair button opens that post. A missing or + non-finite share omits that leftover-map comparison axis share badge. + Share `0` is shown when that persisted share is a finite number, including + rank-0 unused axes. A finite negative share is shown; do not clamp to + nonnegative. Do not invent leftover-map axis share from leftover-map + singular value, leftover pair count, plotted marker count, leftover-map + distance, leftover-map rank, leftover-map post coverage, leftover-map item + coverage, leftover-map incomplete post coverage, leftover-map incomplete + item coverage, leftover-map reconstruction, leftover-map unexplained leftover, + leftover-map coordinates, or the count of unused axes. The strip does not + gain leftover-map singular values or the leftover-map graphic. Never invent + a leftover score. Never invent a theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index 274d6b28c..af9ccd3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,64 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now return persisted leftover-map axis share on + `GET /api/reports/compare/{period}` grouping rows and caption the grouping + comparison strip through leftoverMapCompareAxisShare (ADR 0368 / v2.61.0). + After `make seed`, closest and farthest leftover pairs sit above the member + list with the Gabriel biplot of already-named coordinates, leftover-map axis + share when finite, axis ticks that match `ξ (x, y) ζ (x, y)` on the pair + row, pair segments that match `d` on the pair row, pair segments that match + `R̂` on the pair row, pair segments that match `R̂²/R²` on the pair row, pair + segments that match `U²/R²` on the pair row, pair segments that match + `2R̂U/R²` on the pair row, pair segments that match `U` on the pair row, pair + segments that match `R` on the pair row, pair segments that match `Y` on the + pair row, pair segments that match `E` on the pair row, pair segments that + match leftover-map rank on the pair row, a plot caption that matches leftover-map + complete-case coverage above the pair list, a plot caption that matches + leftover-map item complete-case coverage, a plot caption that matches + leftover-map incomplete post coverage, a plot caption that matches leftover-map + incomplete item coverage, a pair-list note that matches leftover-map post + complete-case coverage, a pair-list note that matches leftover-map item + complete-case coverage, a pair-list note that matches leftover-map incomplete + post coverage, a pair-list note that matches leftover-map incomplete item + coverage, a grouping comparison note that matches leftover-map post + complete-case coverage, a grouping comparison note that matches leftover-map + item complete-case coverage, a grouping comparison note that matches leftover-map + incomplete post coverage, a grouping comparison note that matches leftover-map + incomplete item coverage, grouping comparison leftover-pair buttons that match + leftover-map reconstruction `R̂` on the pair row, grouping comparison leftover-pair + buttons that match leftover-map explained leftover share `e` on the pair row, + grouping comparison leftover-pair buttons that match leftover-map unexplained + leftover share `s` on the pair row, grouping comparison leftover-pair buttons + that match leftover-map cross share `x` on the pair row, grouping comparison + leftover-pair buttons that match leftover-map unexplained leftover `U` on the + pair row, grouping comparison leftover-pair buttons that match leftover residual + `R` on the pair row, grouping comparison leftover-pair buttons that match leftover + observed `Y` on the pair row, grouping comparison leftover-pair buttons that match + leftover expected `E` on the pair row, grouping comparison leftover-pair buttons + that match leftover-map rank on the pair row, grouping comparison leftover-pair + buttons that match leftover-map coordinates `ξ` / `ζ` on the pair row, a live + compare payload that carries those persisted leftover-map coordinates, and a + grouping comparison leftover-map axis share badge that matches leftover-axis + report badges from the live compare leftover_map_axes payload; click a post + marker or a pair button opens that post. A missing or non-finite share omits + that leftover-map comparison axis share badge and keeps leftover-map coordinates + when those axes are finite, leftover-map rank when that rank is a non-negative + integer, leftover expected `E` when finite, leftover observed `Y` when finite, + leftover residual `R` when finite, leftover-map unexplained leftover `U` when + finite, leftover-map cross share `x` when finite, leftover-map unexplained leftover + share `s` when finite, leftover-map explained leftover share `e` when finite, + leftover-map reconstruction `R̂` when finite, leftover-map distance `d`, plus the + strip coverage notes. Share `0` is shown when that persisted share is a finite + number, including rank-0 unused axes. Do not invent leftover-map axis share from + leftover-map singular value, leftover pair count, plotted marker count, leftover-map + distance, leftover-map rank, leftover-map post coverage, leftover-map item coverage, + leftover-map incomplete post coverage, leftover-map incomplete item coverage, + leftover-map reconstruction, leftover-map unexplained leftover, leftover-map + coordinates, or the count of unused axes. A finite negative leftover is shown, + never clamped. The strip does not gain leftover-map singular values or the + leftover-map graphic. Never invent a leftover score or a theta. No new columns. + - Period leftover pairs now return persisted leftover-map coordinates `ξ` / `ζ` on `GET /api/reports/compare/{period}` leftover pairs (ADR 0303 / v2.60.0). After `make seed`, closest and farthest leftover diff --git a/CLAUDE.md b/CLAUDE.md index 799da2d5b..3bcaaff25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ cutoff. Global Ask optional `knowledge_cutoff` uses the same cover Create/start endpoint rules (ADR 0017 / 0021), tie-vs-miss similarity (ADR 0026), R&R catalog ids (ADR 0019 / 0027), leftover pairs -(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0368), occupational construct catalog search (ADR 0257), the text-channel embedding swap and cosine clamp (ADR 0190), per-edge channel-score persistence (ADR 0195), token-backed status notices (ADR 0220), diff --git a/backend/app/main.py b/backend/app/main.py index 122165990..f78f74f84 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3052,6 +3052,11 @@ async def compare_period_groupings( **row, "members": [], "leftover_pairs": leftover_pairs, + "leftover_map_axes": ( + row.get("leftover_map_axes", []) + if len(members) == len(row["members"]) + else [] + ), "post_count": len(members), } ) diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index a4912fecd..3d4ad6f00 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -1088,6 +1088,21 @@ async def fetch_period_comparison( leftover_coverage_by_key = { (row["grouping_kind"], row["grouping_key"]): row for row in leftover_coverage } + leftover_axes = await conn.fetch( + """ + select grouping_kind, grouping_key, axis_index, leftover_singular_value, leftover_share + from report_leftover_map_axis + where period_code = $1 and rubric_version = $2 + and grouping_kind = any($3::text[]) + order by grouping_kind, grouping_key, axis_index + """, + period_code, + RUBRIC_VERSION, + list(GROUPING_KINDS), + ) + leftover_axes_by_key: dict[tuple[str, str], list[asyncpg.Record]] = defaultdict(list) + for row in leftover_axes: + leftover_axes_by_key[(row["grouping_kind"], row["grouping_key"])].append(row) payload: list[dict[str, Any]] = [] for row in rows: label = await resolve_grouping_label(conn, row["grouping_kind"], row["grouping_key"]) @@ -1151,6 +1166,16 @@ async def fetch_period_comparison( } for pair in leftover_by_key.get((row["grouping_kind"], row["grouping_key"]), []) ], + "leftover_map_axes": [ + { + "axis_index": int(axis["axis_index"]), + "leftover_singular_value": float(axis["leftover_singular_value"]), + "leftover_share": float(axis["leftover_share"]), + } + for axis in leftover_axes_by_key.get( + (row["grouping_kind"], row["grouping_key"]), [] + ) + ], "leftover_map_coverage": _leftover_map_coverage_payload( leftover_coverage_by_key.get((row["grouping_kind"], row["grouping_key"])) ), diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 9e581e27a..6b7da3b78 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -5836,6 +5836,15 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, assert leftover_compare_coverage["incomplete_post_count"] == ( leftover_compare_coverage["scored_post_count"] - leftover_compare_coverage["map_post_count"] ) + leftover_compare_axes = leftover_thread.get("leftover_map_axes", []) + assert [axis["axis_index"] for axis in leftover_compare_axes] == [1, 2] + assert all(isinstance(axis["leftover_share"], (int, float)) for axis in leftover_compare_axes) + assert all( + axis["leftover_share"] == axis["leftover_share"] for axis in leftover_compare_axes + ) + assert leftover_compare_axes[0]["leftover_share"] != leftover_compare_axes[0][ + "leftover_singular_value" + ] or leftover_compare_axes[0]["leftover_share"] in {0, 1} assert leftover_compare_coverage["map_item_count"] <= leftover_compare_coverage["scored_item_count"] assert leftover_compare_coverage["map_post_count"] != len( leftover_thread.get("leftover_pairs", []) diff --git a/docs/adr/0148-leftover-map-axis-share.md b/docs/adr/0148-leftover-map-axis-share.md index a9d554176..b7099d44a 100644 --- a/docs/adr/0148-leftover-map-axis-share.md +++ b/docs/adr/0148-leftover-map-axis-share.md @@ -3,7 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-24 **Amended by:** [ADR 0269](0269-leftover-map-axis-share-plot.md) -(leftover-map axis share on the graphic display) +(leftover-map axis share on the graphic display), +[ADR 0368](0368-leftover-map-compare-axis-share.md) +(leftover-map axis share on the grouping comparison strip) ## Context diff --git a/docs/adr/0368-leftover-map-compare-axis-share.md b/docs/adr/0368-leftover-map-compare-axis-share.md new file mode 100644 index 000000000..08bdb6ce1 --- /dev/null +++ b/docs/adr/0368-leftover-map-compare-axis-share.md @@ -0,0 +1,230 @@ +# ADR 0368 — Name leftover-map axis share on the grouping comparison strip + +**Decision status:** Proposed +**Date:** 2026-08-31 + +Amends leftover pairs on the grouping comparison strip +([ADR 0149](0149-leftover-pairs-on-comparison-strip.md)) and leftover-map +axis share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). +Independent of leftover-map coordinates on grouping comparison leftover-pair +payload ([ADR 0303](0303-leftover-map-compare-coordinates-payload.md)), leftover-map +coordinates on grouping comparison strip pair rows +([ADR 0302](0302-leftover-map-compare-coordinates.md)), leftover-map rank on +grouping comparison strip pair rows +([ADR 0301](0301-leftover-map-compare-rank.md)), leftover expected on grouping +comparison strip pair rows +([ADR 0300](0300-leftover-map-compare-expected.md)), leftover observed on grouping +comparison strip pair rows +([ADR 0299](0299-leftover-map-compare-observed.md)), leftover residual on grouping +comparison strip pair rows +([ADR 0298](0298-leftover-map-compare-residual.md)), leftover-map unexplained leftover +on grouping comparison strip pair rows +([ADR 0297](0297-leftover-map-compare-unexplained.md)), leftover-map cross share on +grouping comparison strip pair rows +([ADR 0296](0296-leftover-map-compare-cross-share.md)), leftover-map unexplained leftover +share on grouping comparison strip pair rows +([ADR 0295](0295-leftover-map-compare-unexplained-share.md)), leftover-map explained leftover +share on grouping comparison strip pair rows +([ADR 0294](0294-leftover-map-compare-explained-share.md)), leftover-map reconstruction +on grouping comparison strip pair rows +([ADR 0293](0293-leftover-map-compare-reconstruction.md)), leftover-map incomplete item +coverage on the grouping comparison strip +([ADR 0292](0292-leftover-map-compare-incomplete-item.md)), leftover-map incomplete +post coverage on the grouping comparison strip +([ADR 0291](0291-leftover-map-compare-incomplete-post.md)), leftover-map item +complete-case coverage on the grouping comparison strip +([ADR 0290](0290-leftover-map-compare-item-coverage.md)), leftover-map complete-case +coverage on the grouping comparison strip +([ADR 0289](0289-leftover-map-compare-coverage.md)), leftover-map post complete-case +coverage fail-closed on the pair list +([ADR 0288](0288-leftover-map-list-post-coverage-helper.md)), leftover-map incomplete +item coverage on the pair list +([ADR 0287](0287-leftover-map-list-incomplete-item.md)), leftover-map incomplete +post coverage on the pair list +([ADR 0286](0286-leftover-map-list-incomplete-post.md)), leftover-map item +complete-case coverage on the pair list +([ADR 0285](0285-leftover-map-list-item-coverage.md)), leftover-map incomplete +item coverage on the graphic display +([ADR 0284](0284-leftover-map-plot-incomplete-item.md)), leftover-map incomplete +post coverage on the graphic display +([ADR 0283](0283-leftover-map-plot-incomplete.md)), leftover-map item complete-case +coverage on the graphic display +([ADR 0282](0282-leftover-map-plot-item-coverage.md)), leftover-map complete-case +coverage on the graphic display +([ADR 0281](0281-leftover-map-plot-coverage.md)), leftover-map rank on pair +segments ([ADR 0280](0280-leftover-map-segment-rank.md)), leftover expected on pair +segments ([ADR 0279](0279-leftover-map-segment-expected.md)), leftover observed on +pair segments ([ADR 0278](0278-leftover-map-segment-observed.md)), leftover +residual on pair segments ([ADR 0277](0277-leftover-map-segment-residual.md)), +leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments ([ADR 0275](0275-leftover-map-segment-cross-share.md)), +leftover-map unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover +observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)), +leftover-map explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover +persistence ([ADR 0182](0182-leftover-map-unexplained.md)), leftover-map rank +persistence ([ADR 0164](0164-leftover-map-rank.md)), leftover-map complete-case +coverage persistence ([ADR 0168](0168-leftover-map-complete-case-coverage.md)), +and leftover-map graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)). + +## Context + +ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map +axis share `σ_k² / Σ_j σ_j²` on `report_leftover_map_axis` and captions +leftover-axis report badges with share. ADR 0269 already captions leftover-map +graphic axes with that same persisted share. ADR 0149 already carries +ABAC-filtered leftover pairs on `GET /api/reports/compare/{period}`. ADR 0289 +already includes leftover-map complete-case coverage on that comparison +payload. ADR 0303 already returns leftover-map coordinates on those leftover +pairs. The grouping comparison strip still names leftover pairs, `d`, leftover-map +coverage, reconstruction, share identities, `Y`/`E`/`R`/`U`/`rank`, and +coordinates without naming leftover-map axis share for that grouping, so a +buyer who compares PU / corp / thread leftover pairs can treat a rank-0 unused +axis as if it had the same Gabriel inertia as an 82% first axis. + +This increment includes persisted leftover-map axes on the comparison payload +and captions each grouping row through leftoverMapCompareAxisShare. It does +not add columns. It does not invent leftover-map axis share from leftover-map +singular value, leftover pair count, plotted marker count, leftover-map +distance, leftover-map rank, leftover-map post coverage, leftover-map item +coverage, leftover-map incomplete post coverage, leftover-map incomplete item +coverage, leftover-map reconstruction, leftover-map unexplained leftover, +leftover-map coordinates, or the count of unused axes. It does not persist leftover-map +inner product, cosine, or length as separate columns. Do not invent a leftover +score. Do not invent a theta. + +The dashboard stack already used neighbouring leftover facts under other +numbers. This protected increment uses **0304** so it does not collide with +leftover-map coordinates on grouping comparison leftover-pair payload (0303), +leftover-map coordinates on grouping comparison strip pair rows (0302), leftover-map +rank on grouping comparison strip pair rows (0301), leftover expected on grouping +comparison strip pair rows (0300), leftover observed on grouping comparison strip +pair rows (0299), leftover residual on grouping comparison strip pair rows (0298), +leftover-map unexplained leftover on grouping comparison strip pair rows (0297), +leftover-map cross share on grouping comparison strip pair rows (0296), leftover-map +unexplained leftover share on grouping comparison strip pair rows (0295), leftover-map +explained leftover share on grouping comparison strip pair rows (0294), leftover-map +reconstruction on grouping comparison strip pair rows (0293), leftover-map incomplete +item coverage on the grouping comparison strip (0292), leftover-map incomplete post +coverage on the grouping comparison strip (0291), leftover-map item complete-case +coverage on the grouping comparison strip (0290), leftover-map complete-case coverage +on the grouping comparison strip (0289), leftover-map axis share on the graphic +display (0269), leftover-map axis share persistence (0148), leftover-map rank on +pair segments (0280), leftover-map coordinate ticks (0270), leftover-map graphic +display (0268), or the dashboard stacks. Competing grouping-comparison leftover-map +axis share work on an earlier incomplete-item base used 0293; reconstruction already +owns 0293 on this stack. + +## Decision + +On the grouping comparison strip, include persisted `leftover_map_axes` on +each comparison row and caption leftover-map axis `k` as +`leftover map comparison axis {k} {share}%` only when leftoverMapCompareAxisShare +returns a usable share from persisted `leftover_share`. Use the distinct +accessible name `Leftover map comparison axis share` so the strip caption is +not the leftover-axis report badge (`leftover axis {k} {share}%`) and is not +the graphic caption (`leftover-map axis {k} ({share}%)`). A missing axis row +or a missing or non-finite share omits that leftover-map comparison axis +share badge and keeps the strip leftover-map post coverage note, leftover-map +item coverage note, leftover-map incomplete post note, leftover-map +incomplete item note, leftover pairs, leftover-map reconstruction `R̂`, leftover-map +explained leftover share `e`, leftover-map unexplained leftover share `s`, leftover-map +cross share `x`, leftover-map unexplained leftover `U`, leftover residual `R`, leftover +observed `Y`, leftover expected `E`, leftover-map rank, leftover-map coordinates +`ξ` / `ζ`, leftover-map distance `d`, and any leftover-map captions on the pair +list and graphic. Share `0` is shown when that persisted share is a finite +number, including rank-0 unused axes. A finite negative share is shown; do not +clamp to nonnegative. Axis 1 and axis 2 stay independently named. Do not invent +leftover-map axis share from leftover-map singular value, leftover pair count, +plotted marker count, leftover-map distance, leftover-map rank, leftover-map post +coverage, leftover-map item coverage, leftover-map incomplete post coverage, +leftover-map incomplete item coverage, leftover-map reconstruction, leftover-map +unexplained leftover, leftover-map coordinates, or the count of unused axes. Do +not caption leftover-map singular values or the leftover-map graphic on the strip +in this increment. Click a leftover pair on the strip to open that post. + +Do not add SQL migrations. Do not edit shipped migrations. Do not persist inner +product, cosine, or length as separate columns. Do not invent a leftover +score. Do not invent a theta. + +## Consequences + +After `make seed`, the grouping comparison strip names persisted leftover-map +post complete-case coverage, persisted leftover-map item complete-case +coverage, persisted leftover-map incomplete post coverage, persisted +leftover-map incomplete item coverage, and persisted leftover-map axis share +on each grouping row when leftoverMapCoverageCounts / +leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / +leftoverMapIncompleteItemCount / leftoverMapCompareAxisShare return usable +values, then names leftover pairs with leftover-map distance `d`, reconstruction +`R̂`, leftover-map explained leftover share `e`, leftover-map unexplained leftover +share `s`, leftover-map cross share `x`, leftover-map unexplained leftover `U`, leftover +residual `R`, leftover observed `Y`, leftover expected `E`, leftover-map rank, and +leftover-map coordinates `ξ` / `ζ`. Closest and farthest leftover pairs still sit +above the member list with the leftover-map graphic display; click a post marker +or a pair button opens that post. +Hidden posts stay hidden. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover-map +complete-case coverage persistence, leftover-map axis share persistence, +leftover pairs on the grouping comparison strip, leftover-map complete-case +coverage on the grouping comparison strip, leftover-map item complete-case +coverage on the grouping comparison strip, leftover-map incomplete post +coverage on the grouping comparison strip, leftover-map incomplete item +coverage on the grouping comparison strip, leftover-map reconstruction on +grouping comparison strip pair rows, leftover-map explained leftover share +on grouping comparison strip pair rows, leftover-map unexplained leftover +share on grouping comparison strip pair rows, leftover-map cross share on +grouping comparison strip pair rows, leftover-map unexplained leftover on +grouping comparison strip pair rows, leftover residual on grouping +comparison strip pair rows, leftover observed on grouping comparison strip +pair rows, leftover expected on grouping comparison strip pair rows, +leftover-map rank on grouping comparison strip pair rows, leftover-map +coordinates on grouping comparison strip pair rows, leftover-map coordinates +on grouping comparison leftover-pair payload, leftover-map inner product, +leftover-map cosine, leftover-map length, leftover-map graphic display, +leftover-map item complete-case coverage on the graphic display, leftover-map +item complete-case coverage on the pair list, leftover-map incomplete post +coverage on the graphic display, leftover-map incomplete post coverage on the +pair list, leftover-map incomplete item coverage on the graphic display, +leftover-map incomplete item coverage on the pair list, leftover-map post +complete-case coverage fail-closed on the pair list, leftover-map axis share +on the graphic display, leftover-map singular values, leftover-map rank +persistence, leftover-map coordinate persistence, and leftover-map coordinate +ticks. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 +(LSIRM interaction `−γ‖ξ_j − ζ_i‖` after main effects +`α_j − β_i`; typically `p = 2` for the interaction map. Leftover-map +axis share is Gabriel inertia `σ_k² / Σ_j σ_j²` of residual SVD axes; +grouping comparison leftover-map axis share names that persisted share +for that grouping only when leftoverMapCompareAxisShare returns a usable +share.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 98d95bb2a..19b261f69 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,40 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-31 03:10 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work (close/comment/create-PR/push). +> Token still has empty `X-OAuth-Scopes`; GitHub App user-to-server writes succeed. +> Do not self-approve. #840 leftover-map coordinates `ξ` / `ζ` on +> `GET /api/reports/compare/{period}` leftover pairs (v2.60.0 / ADR 0303) is +> `cbf994462ec787d4d3e7a2749f913f62fd9c96d0`, stacked on #839 @ `51bf33a`. +> Devin COMMENTED; no independent APPROVE. Competing #828 leftover-map axis +> share on grouping comparison (v2.50.0 / ADR 0293) sits on the incomplete-item +> dead-end, not this stack (0293 here is reconstruction). Next buyer increment +> on this cycle: leftover-map axis share on the grouping comparison strip +> (ADR 0368 / v2.61.0) delivered locally on +> `feat/leftover-map-compare-axis-share-v2610`. Include persisted +> leftover_map_axes on GET /api/reports/compare/{period} and caption each +> grouping row through leftoverMapCompareAxisShare with distinct aria-label +> `Leftover map comparison axis share` and copy +> `leftover map comparison axis {axis} {share}%`. UI + compare payload; +> no new columns. Missing or non-finite share omits that leftover-map +> comparison axis share badge independently of coverage notes, reconstruction, +> share identities, `Y`/`E`/`R`/`U`/`rank`, and coordinates. Share `0` is +> shown when persisted, including rank-0 unused axes. Do not invent leftover-map +> axis share from leftover-map singular value, leftover pair count, plotted +> marker count, leftover-map distance, leftover-map rank, leftover-map post +> coverage, leftover-map item coverage, leftover-map incomplete post coverage, +> leftover-map incomplete item coverage, leftover-map reconstruction, +> leftover-map unexplained leftover, leftover-map coordinates, or the count of +> unused axes. Do not caption leftover-map singular values or the leftover-map +> graphic on the strip. Do not invent leftover scores. Do not persist leftover-map +> inner product, cosine, or length. Stack onto leftover branch +> `feat/leftover-map-compare-coordinates-payload-v2600` / #840; leave the PR +> open for independent review. Do not squash-merge onto the leftover base. +> Issues #79 and #87 stay open. #96 is already closed. Do not merge #808 without +> independent APPROVE. Only collaborator is `seonghobae`; no independent +> reviewer can be requested from this token. + + > Exact-head loop overlay: 2026-08-31 02:30 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not > self-approve. #839 leftover-map coordinates `ξ` / `ζ` on grouping diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 485085734..6852fca40 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,7 +6,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. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, and `MissingAxisShare` 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, and missing share that keeps existing leftover-map axis text. 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` | +| `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, leftover-map incomplete item coverage, and leftover-map axis share with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisShare return usable values. | `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` | | `Post/Recorded perspectives` | Read the imported primary and every evidence-connected additional Voice with its recorded truth state instead of flattening them into one compound category. `CombinedEvidence`, `RejectedEvidence`, and `NarrowViewport` cover desktop, rejected-evidence, and narrow layouts. | `VoicePerspectiveList`, `ticket-list`, `post-meta` | diff --git a/frontend/package.json b/frontend/package.json index 1dcee04fd..2049affe5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.60.0", + "version": "2.61.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 15580f204..1b546f87c 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -898,6 +898,10 @@ describe("App, authenticated", () => { incomplete_post_count: 0, incomplete_item_count: 0, }, + leftover_map_axes: [ + { axis_index: 1, leftover_singular_value: 0, leftover_share: 0 }, + { axis_index: 2, leftover_singular_value: 0, leftover_share: 0 }, + ], leftover_pairs: [ { pair_kind: "closest", @@ -954,6 +958,10 @@ describe("App, authenticated", () => { incomplete_post_count: 1, incomplete_item_count: 0, }, + leftover_map_axes: [ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, + { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, + ], }, ], }), @@ -4500,6 +4508,34 @@ describe("App, authenticated", () => { "Leftover map comparison coordinates", ), ).toHaveLength(1); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByLabelText("Leftover-map axis share"), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison axis share", + ), + ).toHaveLength(4); + expect( + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison axis share", + )[0], + ).toHaveTextContent("leftover map comparison axis 1 0%"); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText("leftover map comparison axis 1 82%"), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText("leftover map comparison axis 2 18%"), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText("leftover axis 1 82%"), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText(/leftover-map axis 1/), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText(/σ 1\.84/), + ).not.toBeInTheDocument(); expect( screen.getByRole("button", { name: /open leftover closest pair from comparison: specification revision requested/i, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d3d32cfad..329216da4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -137,6 +137,11 @@ import { LEFTOVER_MAP_PLOT_INCOMPLETE_POST, LEFTOVER_MAP_PLOT_ITEM_COVERAGE, } from "./leftoverMapCoverage"; +import { + leftoverMapCompareAxisShare, + LEFTOVER_MAP_COMPARE_AXIS_SHARE, + LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL, +} from "./leftoverMapCompareAxis"; import { formatLeftoverMapReconstruction, LEFTOVER_MAP_COMPARE_RECONSTRUCTION_LABEL, @@ -4087,6 +4092,21 @@ function ReportsPanel({ {tf(LEFTOVER_MAP_PLOT_INCOMPLETE_ITEM, comparisonIncompleteItemCount)}

) : null} + {row.leftover_map_axes?.map((axis) => { + const comparisonAxisShare = leftoverMapCompareAxisShare(axis); + if (comparisonAxisShare === null) { + return null; + } + return ( + + {tf(LEFTOVER_MAP_COMPARE_AXIS_SHARE, comparisonAxisShare)} + + ); + })} {row.leftover_pairs && row.leftover_pairs.length > 0 && (