From 95538f9980dcba70a6d6d174500d00c7af01df6c Mon Sep 17 00:00:00 2001 From: seonghobae Date: Sun, 30 Aug 2026 18:55:17 +0000 Subject: [PATCH 1/3] feat(reports): leftover-map axis share on grouping comparison graphic (v2.62.0) Return persisted leftover-map axes on GET /api/reports/compare/{period} and caption leftover-map axis share on the grouping comparison leftover-map graphic when finite (ADR 0305). Comparison copy stays distinct from hyphen leftover-map axis share on the period-report graphic. Rank-0 unused axes still name zero leftover-map axis share. Do not invent leftover-map axis share from leftover-map rank, leftover-map distance, or leftover-map reconstruction. Never invent a leftover score. Never invent a theta. --- AGENTS.md | 12 +- ARCHITECTURE.md | 9 +- ....0-leftover-map-compare-plot-axis-share.md | 15 ++ CHANGELOG.md | 61 +++++ CLAUDE.md | 2 +- backend/app/report_ingestion.py | 27 ++ backend/tests/test_api.py | 7 + docs/adr/0269-leftover-map-axis-share-plot.md | 7 +- docs/adr/0304-leftover-map-compare-graphic.md | 3 + ...05-leftover-map-compare-plot-axis-share.md | 235 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 17 ++ frontend/package.json | 2 +- frontend/src/App.test.tsx | 42 +++- frontend/src/App.tsx | 2 + frontend/src/api.ts | 1 + .../src/components/LeftoverMapPlot.test.tsx | 72 ++++++ frontend/src/components/LeftoverMapPlot.tsx | 36 ++- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 34 +++ frontend/src/i18n.ts | 28 +++ frontend/src/leftoverMapPlotAxisShare.test.ts | 17 ++ frontend/src/leftoverMapPlotAxisShare.ts | 7 + frontend/src/leftoverMapPlotLayout.test.ts | 13 + frontend/src/leftoverMapPlotLayout.ts | 16 +- pyproject.toml | 2 +- uv.lock | 2 +- 26 files changed, 649 insertions(+), 23 deletions(-) create mode 100644 CHANGELOG.d/2.62.0-leftover-map-compare-plot-axis-share.md create mode 100644 docs/adr/0305-leftover-map-compare-plot-axis-share.md diff --git a/AGENTS.md b/AGENTS.md index b8dc96b39..eeba070b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,7 +273,7 @@ stops startup instead of leaving a healthy-looking partial schema, and application code must not compensate for a missing table. Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0158 / 0162 / -0163 / 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 / 0304) are computed in `lineageweave/leftover_pairs.py` from the +0163 / 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 / 0304 / 0305) are computed in `lineageweave/leftover_pairs.py` from the residual after a real GRM/GPCM score, never invented. Distances are Euclidean on the two-dimensional Gabriel leftover map; missing cells stay out of the factorization. Closest and farthest post–criterion pairs @@ -487,7 +487,13 @@ buttons when four leftover-map axes are finite; a missing or non-finite axis omits that pair from the comparison graphic and does not invent coordinates from leftover-map rank, leftover-map distance, leftover-map reconstruction, or leftover residual. Omit the comparison graphic when no leftover pair has four -finite leftover-map coordinates. +finite leftover-map coordinates. ADR 0305 returns persisted leftover-map axes on +`GET /api/reports/compare/{period}` and captions leftover-map axis share on the +grouping comparison leftover-map graphic when that share is finite; a missing or +non-finite share omits that leftover-map comparison axis share badge and keeps +leftover map comparison axis text. Do not invent leftover-map axis share from +leftover-map rank, leftover-map distance, leftover-map reconstruction, leftover +residual, leftover pair count, or the count of unused axes. 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 @@ -535,6 +541,8 @@ coordinates `ξ` / `ζ` on `GET /api/reports/compare/{period}` leftover pairs. Grouping comparison leftover-map graphic display (ADR 0304) draws the leftover-map graphic of those already-named coordinates above grouping comparison leftover-pair buttons when four leftover-map axes are finite. +Grouping comparison leftover-map axis share (ADR 0305) captions that grouping +comparison leftover-map graphic 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 21a698856..527a5e0e9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -700,6 +700,10 @@ ADR 0303 returns persisted leftover-map coordinates `ξ` / `ζ` on ADR 0304 draws the leftover-map graphic display of those already-named coordinates above grouping comparison leftover-pair buttons when four leftover-map axes are finite. +ADR 0305 returns persisted leftover-map axes on +`GET /api/reports/compare/{period}` and captions leftover-map axis share +on that grouping comparison leftover-map graphic when the persisted share +is finite. 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 @@ -731,8 +735,9 @@ incomplete item coverage (ADR 0292), leftover-map reconstruction `R̂` (ADR 0300), leftover-map rank (ADR 0301), leftover-map coordinates `ξ` / `ζ` (ADR 0302), leftover-map coordinates on the compare leftover-pair payload -(ADR 0303), and leftover-map graphic display on the grouping comparison strip -(ADR 0304); +(ADR 0303), leftover-map graphic display on the grouping comparison strip +(ADR 0304), and leftover-map axis share on the grouping comparison leftover-map +graphic (ADR 0305); `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.62.0-leftover-map-compare-plot-axis-share.md b/CHANGELOG.d/2.62.0-leftover-map-compare-plot-axis-share.md new file mode 100644 index 000000000..9f496fab3 --- /dev/null +++ b/CHANGELOG.d/2.62.0-leftover-map-compare-plot-axis-share.md @@ -0,0 +1,15 @@ +## 2.62.0 — Leftover-map axis share on grouping comparison leftover-map graphic + +- Name leftover-map axis share on the grouping comparison leftover-map + graphic (ADR 0305). After `make seed`, closest and farthest leftover pairs + sit above the member list with the Gabriel biplot of already-named + coordinates, and each leftover-map comparison axis names its persisted + Gabriel inertia share when finite; 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 comparison axis text. + Rank-0 unused axes still name zero leftover-map axis share. Do not invent + leftover-map axis share from leftover-map rank, leftover-map distance, or + leftover-map reconstruction. A finite negative leftover is shown, never + clamped. This increment does not caption leftover-map coverage on the + comparison graphic. Never invent a leftover score. Never invent a theta. + No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index 885a3ce75..c16d24665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,67 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now name leftover-map axis share on the grouping + comparison leftover-map graphic (ADR 0305 / v2.62.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, leftover-map axis share on the grouping comparison leftover-map + graphic 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, the + grouping comparison leftover-map graphic of those already-named coordinates when + four leftover-map axes are finite, and leftover-map axis share on that grouping + comparison leftover-map graphic when the persisted share is finite; click a post + marker or a pair button opens that post. A missing or non-finite leftover-map + axis share omits that leftover-map comparison axis share badge and keeps leftover + map comparison axis text, 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. Rank-0 origin cells still plot at `(0, 0)` and still + name zero leftover-map axis share when those persisted axes are stored. Do not + invent leftover-map axis share from leftover-map rank, leftover-map distance, + leftover expected, leftover observed, leftover residual, leftover-map + reconstruction, leftover-map unexplained leftover, leftover-map post coverage, + leftover-map item coverage, leftover-map incomplete post coverage, leftover-map + incomplete item coverage, leftover pair count, or the count of unused axes. A + finite negative leftover is shown, never clamped. Omit the comparison graphic + when no leftover pair has four finite leftover-map coordinates. This increment + does not caption leftover-map coverage on the comparison graphic. Never invent + a leftover score or a theta. No new columns. + - Period leftover pairs now draw the leftover-map graphic display of persisted leftover-map coordinates `ξ` / `ζ` above grouping comparison leftover-pair buttons (ADR 0304 / v2.61.0). After `make seed`, closest diff --git a/CLAUDE.md b/CLAUDE.md index d0da895bd..faac96d2c 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 / 0304), 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 / 0304 / 0305), 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/report_ingestion.py b/backend/app/report_ingestion.py index a4912fecd..21a40527b 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -1088,6 +1088,23 @@ 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 axis_row in leftover_axes: + leftover_axes_by_key[(axis_row["grouping_kind"], axis_row["grouping_key"])].append( + axis_row + ) payload: list[dict[str, Any]] = [] for row in rows: label = await resolve_grouping_label(conn, row["grouping_kind"], row["grouping_key"]) @@ -1154,6 +1171,16 @@ async def fetch_period_comparison( "leftover_map_coverage": _leftover_map_coverage_payload( leftover_coverage_by_key.get((row["grouping_kind"], row["grouping_key"])) ), + "leftover_map_axes": [ + { + "axis_index": int(axis_row["axis_index"]), + "leftover_singular_value": float(axis_row["leftover_singular_value"]), + "leftover_share": float(axis_row["leftover_share"]), + } + for axis_row in leftover_axes_by_key.get( + (row["grouping_kind"], row["grouping_key"]), [] + ) + ], } ) return payload diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 9e581e27a..228fb8764 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -5833,6 +5833,13 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, assert leftover_compare_coverage is not None assert leftover_compare_coverage["map_post_count"] <= leftover_compare_coverage["scored_post_count"] assert leftover_compare_coverage["scored_post_count"] > 0 + leftover_compare_axes = leftover_thread.get("leftover_map_axes", []) + assert [axis["axis_index"] for axis in leftover_compare_axes] == [1, 2] + assert all(axis["leftover_singular_value"] >= 0 for axis in leftover_compare_axes) + assert all(0.0 <= axis["leftover_share"] <= 1.0 for axis in leftover_compare_axes) + assert leftover_compare_axes[0]["leftover_share"] != len( + leftover_thread.get("leftover_pairs", []) + ) assert leftover_compare_coverage["incomplete_post_count"] == ( leftover_compare_coverage["scored_post_count"] - leftover_compare_coverage["map_post_count"] ) diff --git a/docs/adr/0269-leftover-map-axis-share-plot.md b/docs/adr/0269-leftover-map-axis-share-plot.md index 9d5f1ea50..f69429f79 100644 --- a/docs/adr/0269-leftover-map-axis-share-plot.md +++ b/docs/adr/0269-leftover-map-axis-share-plot.md @@ -3,6 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-28 +**Amended by:** [ADR 0305](0305-leftover-map-compare-plot-axis-share.md) +(leftover-map axis share on the grouping comparison leftover-map graphic) + Amends [ADR 0148](0148-leftover-map-axis-share.md) and [ADR 0268](0268-leftover-map-graphic-display.md). Independent of leftover-map explained leftover share ([ADR 0266](0266-leftover-map-explained-share.md)), @@ -53,8 +56,8 @@ or non-finite share omits that axis badge and keeps the existing leftover-map axis text. Axis 1 and axis 2 stay independently named: one missing share does not hide the other. Click a post marker to open that post. Criterion markers are not post buttons. The grouping -comparison strip (ADR 0149) stays on its reduced leftover payload and -does not gain this caption. +comparison strip later captions leftover-map axis share on its leftover-map +graphic from already-named leftover-map axes (ADR 0305). Do not add SQL. Do not edit shipped migrations. Do not persist inner product, cosine, or length as separate columns. Do not invent a diff --git a/docs/adr/0304-leftover-map-compare-graphic.md b/docs/adr/0304-leftover-map-compare-graphic.md index aa08bd6e5..f0cbd192c 100644 --- a/docs/adr/0304-leftover-map-compare-graphic.md +++ b/docs/adr/0304-leftover-map-compare-graphic.md @@ -3,6 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-31 +**Amended by:** [ADR 0305](0305-leftover-map-compare-plot-axis-share.md) +(leftover-map axis share on the grouping comparison leftover-map graphic) + Amends leftover pairs on the grouping comparison strip ([ADR 0149](0149-leftover-pairs-on-comparison-strip.md)), leftover-map graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)), leftover-map diff --git a/docs/adr/0305-leftover-map-compare-plot-axis-share.md b/docs/adr/0305-leftover-map-compare-plot-axis-share.md new file mode 100644 index 000000000..84245f236 --- /dev/null +++ b/docs/adr/0305-leftover-map-compare-plot-axis-share.md @@ -0,0 +1,235 @@ +# ADR 0305 — Name leftover-map axis share on the grouping comparison leftover-map graphic + +**Decision status:** Accepted +**Date:** 2026-08-31 + +Amends leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover-map graphic +display on the grouping comparison strip +([ADR 0304](0304-leftover-map-compare-graphic.md)), leftover-map axis share +persistence ([ADR 0148](0148-leftover-map-axis-share.md)), leftover pairs on +the grouping comparison strip +([ADR 0149](0149-leftover-pairs-on-comparison-strip.md)), leftover-map +coordinates on the compare 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 residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover +observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)), leftover-map unexplained leftover persistence ([ADR 0182](0182-leftover-map-unexplained.md)), +leftover-map reconstruction persistence +([ADR 0201](0201-leftover-map-reconstruction.md)), leftover-map rank +([ADR 0164](0164-leftover-map-rank.md)), leftover-map rank on pair segments +([ADR 0280](0280-leftover-map-segment-rank.md)), leftover-map coordinate ticks +([ADR 0270](0270-leftover-map-coordinate-ticks.md)), leftover-map graphic display +([ADR 0268](0268-leftover-map-graphic-display.md)), and leftover-map coordinates +([ADR 0267](0267-leftover-map-coordinates.md)). Independent of 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 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 cross share persistence ([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map explained leftover +share persistence ([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +incomplete item coverage on the grouping comparison leftover-map graphic, and leftover-map +complete-case coverage on the grouping comparison leftover-map graphic. + +## Context + +ADR 0148 already persists leftover-map axis share +`σ_k² / Σ_j σ_j²` on `report_leftover_map_axis`. ADR 0269 already +captions the period-report leftover-map graphic with +`leftover-map axis {k} ({share}%)` when that share is finite. ADR 0304 +already draws the leftover-map graphic of persisted `ξ_{1:2}` / +`ζ_{1:2}` above grouping comparison leftover-pair buttons. The compare +payload still omits leftover-map axes, so every grouping comparison +graphic fail-closes to uncaptioned leftover-map axis text. Hiding +Gabriel inertia on the comparison graphic lets leftover-map rank, +leftover expected `E`, leftover observed `Y`, leftover residual `R`, +leftover-map unexplained leftover `U`, leftover-map cross share `x`, +leftover-map unexplained leftover share `s`, leftover-map explained leftover +share `e`, leftover-map reconstruction `R̂`, leftover-map distance `d`, +or the coordinate badge be read as leftover-map structure without a +next action. + +This increment returns persisted leftover-map axes on +`GET /api/reports/compare/{period}` and captions the grouping comparison +leftover-map graphic with already-named leftover-map axis share. Comparison +copy uses the space form `leftover map comparison axis {k} ({share}%)` +so it stays distinct from the hyphen period-report graphic +`leftover-map axis {k} ({share}%)`. It does not add columns. It does not +recompute leftover-map axis share from leftover-map rank, leftover-map +distance, leftover expected, leftover observed, leftover residual, +leftover-map reconstruction, leftover-map unexplained leftover, +leftover-map post coverage, leftover-map item coverage, leftover-map +incomplete post coverage, leftover-map incomplete item coverage, plotted +marker count, leftover pair count, 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 **0305** so it does not collide with +leftover-map graphic display on the grouping comparison strip (0304), leftover-map +coordinates on the compare 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-map axis share +on the graphic display (0269), leftover-map axis share persistence (0148), +leftover-map graphic display (0268), leftover-map coordinates (0267), or +the dashboard stacks. + +## Decision + +On `GET /api/reports/compare/{period}`, include persisted leftover-map +axes (`axis_index`, `leftover_singular_value`, `leftover_share`) on each +grouping row from `report_leftover_map_axis`, converting finite database +numerics the same way as `GET /api/reports/{grouping}/{period}`. On the +grouping comparison leftover-map graphic, caption leftover-map axis `k` +with persisted leftover-map axis share when that share is finite, +including rank-0 zero-share axes. The caption is +`leftover map comparison axis {k} ({share}%)` from `σ_k² / Σ_j σ_j²`. +A missing or non-finite share omits that leftover-map comparison axis +share badge and keeps `leftover map comparison axis {k}`. Axis 1 and +axis 2 stay independently named: one missing share does not hide the +other. The grouping comparison leftover-map graphic uses distinct +figure, caption, and SVG labels (`Leftover map comparison graphic`, +`Leftover map comparison`) so it does not share +`Leftover-map graphic display` or `leftover-map axis {k}` with the +period-report graphic. Click a post marker to open that post. + +This increment does not caption leftover-map coverage on the comparison +graphic. Those notes already sit on the strip through ADR 0289–0292. +Do not invent leftover-map axis share from leftover-map rank, leftover-map +distance, leftover expected, leftover observed, leftover residual, +leftover-map reconstruction, leftover-map unexplained leftover, +leftover-map explained leftover share, leftover-map unexplained leftover +share, leftover-map cross share, leftover-map post coverage, leftover-map +item coverage, leftover-map incomplete post coverage, leftover-map +incomplete item coverage, plotted marker count, leftover pair count, or +the count of unused axes. A finite negative leftover on neighbouring +fields is shown, never clamped. + +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`, grouping comparison leftover pairs that already show +the leftover-map graphic of persisted `ξ` / `ζ` also name leftover-map +axis share on that graphic when the persisted share is finite. Rank-0 +unused axes still plot at the origin and still name zero leftover-map +axis share. Click a post marker or a pair button opens that post. +Hidden posts stay hidden. When `Y`, `E`, and `R` are finite, +`Y − E = R`. When `R`, `R̂`, and `U` are finite, `U + R̂ = R`. When `R`, +`R̂`, `U`, `x`, `s`, and `e` are finite, `e + s + x = 1`. When coordinates, +reconstruction, and distance are finite, `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖`. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover-map +complete-case coverage 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 the compare leftover-pair payload, leftover-map +inner product, leftover-map cosine, leftover-map length, leftover residual +on pair segments, leftover observed on pair segments, leftover expected on +pair segments, leftover-map rank on pair segments, leftover-map unexplained +leftover on pair segments, leftover-map unexplained leftover persistence, +leftover-map cross share on pair segments, leftover-map cross share +persistence, leftover-map unexplained leftover share on pair segments, +leftover-map unexplained leftover share persistence, leftover-map explained +leftover share on pair segments, leftover-map explained leftover share +persistence, leftover-map reconstruction on pair segments, leftover-map +reconstruction persistence, 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 rank persistence, leftover-map coordinate +persistence, leftover-map coordinate ticks, leftover-map complete-case +coverage on the graphic display, leftover-map item complete-case coverage +on the graphic display, leftover-map incomplete post coverage on the graphic +display, leftover-map incomplete item coverage on the graphic display, and +leftover-map coverage on the grouping comparison leftover-map graphic. + +## References + +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 persisted Gabriel inertia `σ_k² / Σ_j σ_j²` of residual +SVD axes 1 and 2 after IRT main effects. Grouping comparison leftover-map +axis share captions those persisted shares on the grouping comparison +leftover-map graphic only when the stored share is finite. Rank-0 unused +axes still name zero leftover-map axis share when those axes are stored.) + +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 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 653e2f5fb..d204a40eb 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,22 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-31 03:45 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not +> self-approve. #841 leftover-map graphic on the grouping comparison strip +> (v2.61.0 / ADR 0304) is `ee467df`, stacked on #840 @ `cbf9944`. Next +> buyer increment on this cycle: leftover-map axis share on the grouping +> comparison leftover-map graphic (ADR 0305 / v2.62.0) delivered locally on +> `feat/leftover-map-compare-plot-axis-share-v2620`. Return persisted +> leftover-map axes on `GET /api/reports/compare/{period}` and caption +> leftover map comparison axis `{k} ({share}%)` when finite. Distinct from +> hyphen leftover-map axis share on the period-report graphic. Rank-0 unused +> axes still name zero leftover-map axis share. Do not invent leftover-map +> axis share from leftover-map rank, leftover-map distance, or leftover-map +> reconstruction. Stack onto #841. Leave open for independent review. Do not +> squash-merge. Issues #79 and #87 stay open. #96 is already closed. Do not +> merge #808 without independent APPROVE. + + > Exact-head loop overlay: 2026-08-31 03:25 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not > self-approve. #840 leftover-map coordinates `ξ` / `ζ` on diff --git a/frontend/package.json b/frontend/package.json index 2049affe5..fbbea5673 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.61.0", + "version": "2.62.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7ac02010e..c18eb3abc 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -946,6 +946,18 @@ describe("App, authenticated", () => { leftover_map_item_axis_2: -0.02, }, ], + 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, + }, + ], leftover_map_coverage: { map_post_count: 2, scored_post_count: 3, @@ -4181,9 +4193,12 @@ describe("App, authenticated", () => { expect(screen.getByRole("button", { name: /open report post: public post/i })).toHaveTextContent("Open"); expect(screen.getByRole("button", { name: /open report post: public post/i })).toHaveTextContent("due 2026-01-12"); expect(await screen.findByLabelText("Leftover pairs")).toBeInTheDocument(); - expect(screen.getAllByLabelText("Leftover-map graphic display")).toHaveLength(2); + expect(screen.getByLabelText("Leftover-map graphic display")).toBeInTheDocument(); + 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.getAllByRole("button", { name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, @@ -4280,14 +4295,35 @@ describe("App, authenticated", () => { expect(await screen.findByLabelText("Grouping comparison")).toBeInTheDocument(); expect( await within(screen.getByLabelText("Grouping comparison")).findByLabelText( - "Leftover-map graphic display", + "Leftover map comparison graphic", ), ).toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( - "Leftover-map graphic display", + "Leftover map comparison graphic", ), ).toHaveLength(1); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByLabelText( + "Leftover-map graphic display", + ), + ).not.toBeInTheDocument(); + 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-map axis 1 (82%)"), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText("leftover-map axis 2 (18%)"), + ).not.toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 30dfa2d99..0370acc97 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4093,7 +4093,9 @@ function ReportsPanel({ { onSelectPost(pair.post_id, { fromLeftoverPair: { diff --git a/frontend/src/api.ts b/frontend/src/api.ts index e4781c206..2bbf06fa8 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1280,6 +1280,7 @@ export interface GroupingComparisonRow { post_count: number; link_method: string; leftover_pairs?: LeftoverPair[]; + leftover_map_axes?: LeftoverMapAxis[]; leftover_map_coverage?: LeftoverMapCoverage | null; } diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 272ed9b1f..3a239b484 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -821,4 +821,76 @@ describe("LeftoverMapPlot", () => { "Leftover map used 2 of 2 scored criteria (complete-case)", ); }); + + it("captions leftover map comparison axes with persisted leftover-map axis share", () => { + render( + , + ); + expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); + expect(screen.queryByLabelText("Leftover-map graphic display")).not.toBeInTheDocument(); + expect(screen.getByLabelText("Leftover map comparison")).toBeInTheDocument(); + expect( + screen.getByText( + "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(); + expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 2 (18%)")).not.toBeInTheDocument(); + }); + + it("keeps leftover map comparison axis text when share is missing or non-finite", () => { + render( + , + ); + expect(screen.getByText("leftover map comparison axis 1")).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 2 \(/)).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 1")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 2")).not.toBeInTheDocument(); + }); + + it("names rank-0 zero leftover-map axis share on the comparison graphic", () => { + render( + , + ); + expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 46621aa30..375e4d5a2 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -18,11 +18,17 @@ import { import { formatLeftoverMapPlotAxisShare, leftoverShareForAxis, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_1, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_2, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, LEFTOVER_MAP_PLOT_AXIS_SHARE, } from "../leftoverMapPlotAxisShare"; import { firstPlottablePairForPost, layoutLeftoverMapPlot, + LEFTOVER_MAP_COMPARE_PLOT_CAPTION, + LEFTOVER_MAP_COMPARE_PLOT_LABEL, + LEFTOVER_MAP_COMPARE_PLOT_SVG, LEFTOVER_MAP_PLOT_CAPTION, LEFTOVER_MAP_PLOT_POST_ACTION, LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, @@ -39,12 +45,15 @@ import { } from "../leftoverMapPlotLayout"; import "./LeftoverMapPlot.css"; +export type LeftoverMapPlotVariant = "report" | "comparison"; + export type LeftoverMapPlotProps = { pairs: LeftoverPair[]; leftoverMapAxes?: LeftoverMapAxis[]; leftoverMapCoverage?: LeftoverMapCoverage | null; criterionLabel: (criterionCode: string) => string; onSelectPost: (pair: LeftoverPair) => void; + variant?: LeftoverMapPlotVariant; }; function diamondPoints(x: number, y: number, radius: number): string { @@ -54,10 +63,17 @@ function diamondPoints(x: number, y: number, radius: number): string { function leftoverMapPlotAxisText( axisIndex: 1 | 2, leftoverMapAxes: LeftoverMapAxis[] | undefined, + variant: LeftoverMapPlotVariant, ): string { const percent = formatLeftoverMapPlotAxisShare( leftoverShareForAxis(leftoverMapAxes, axisIndex), ); + if (variant === "comparison") { + 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 axis 1" : "leftover-map axis 2"); } @@ -103,7 +119,9 @@ function leftoverMapPlotAxisText( * 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 and does not caption leftover-map - * axis share or leftover-map coverage on that comparison plot. + * coverage on that comparison plot. ADR 0305 captions leftover-map axis + * share on that comparison graphic from already-named leftover-map axes + * with distinct leftover map comparison axis labels. * Never invent a leftover score. */ export function LeftoverMapPlot({ @@ -112,6 +130,7 @@ export function LeftoverMapPlot({ leftoverMapCoverage, criterionLabel, onSelectPost, + variant = "report", }: LeftoverMapPlotProps) { const layout = layoutLeftoverMapPlot(pairs, criterionLabel); if (layout === null) { @@ -130,8 +149,13 @@ export function LeftoverMapPlot({ }; return ( -
-
{t(LEFTOVER_MAP_PLOT_CAPTION)}
+
+
+ {t(variant === "comparison" ? LEFTOVER_MAP_COMPARE_PLOT_CAPTION : LEFTOVER_MAP_PLOT_CAPTION)} +
{coverageCounts !== null ? (

- {leftoverMapPlotAxisText(1, leftoverMapAxes)} + {leftoverMapPlotAxisText(1, leftoverMapAxes, variant)} - {leftoverMapPlotAxisText(2, leftoverMapAxes)} + {leftoverMapPlotAxisText(2, leftoverMapAxes, variant)} {layout.ticks.map((tick) => ( { "Leftover map comparison expected", "Leftover map comparison rank", "Leftover map comparison coordinates", + "Leftover map comparison graphic", + "Leftover map comparison", + "leftover map comparison axis 1", + "leftover map comparison axis 2", + "leftover map comparison axis {axis} ({share}%)", + "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.", "Leftover-map graphic item coverage", "Leftover map item coverage", "Leftover map incomplete posts", @@ -561,6 +567,34 @@ describe("i18n", () => { }, ); + it.each([ + ["ko", "잔여 지도 비교 그림"], + ["zh", "残差地图比较图形"], + ["ja", "残差マップの比較図"], + ["vi", "Đồ họa so sánh bản đồ phần dư"], + ] as const)( + "formats leftover map comparison graphic label in %s", + (locale, expected) => { + setLocale(locale); + expect(t("Leftover map comparison graphic")).toBe(expected); + }, + ); + + it.each([ + ["ko", "잔여 지도 비교 축 1 (82%)"], + ["zh", "残差地图比较轴 1 (82%)"], + ["ja", "残差マップの比較軸 1 (82%)"], + ["vi", "trục so sánh bản đồ phần dư 1 (82%)"], + ] as const)( + "formats leftover map comparison axis share label in %s", + (locale, expected) => { + setLocale(locale); + expect(tf("leftover map comparison axis {axis} ({share}%)", { axis: 1, share: "82" })).toBe( + expected, + ); + }, + ); + it.each([ ["ko", "잔여 지도 그림 기준 포함 범위"], ["zh", "残差图图形准则覆盖范围"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index c304c0c5e..ef1999fdc 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -282,6 +282,13 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison expected": "잔여 지도 비교 기대", "Leftover map comparison rank": "잔여 지도 비교 순위", "Leftover map comparison coordinates": "잔여 지도 비교 좌표", + "Leftover map comparison graphic": "잔여 지도 비교 그림", + "Leftover map comparison": "잔여 지도 비교", + "leftover map comparison axis 1": "잔여 지도 비교 축 1", + "leftover map comparison axis 2": "잔여 지도 비교 축 2", + "leftover map comparison axis {axis} ({share}%)": "잔여 지도 비교 축 {axis} ({share}%)", + "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.": + "이미 이름 붙은 좌표의 잔여 지도 비교 그림입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "Leftover map item coverage": "잔여 지도 기준 포함 범위", "Leftover map incomplete posts": "잔여 지도 불완전 글", "Leftover map incomplete items": "잔여 지도 불완전 기준", @@ -902,6 +909,13 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison expected": "残差地图比较期望", "Leftover map comparison rank": "残差地图比较秩", "Leftover map comparison coordinates": "残差地图比较坐标", + "Leftover map comparison graphic": "残差地图比较图形", + "Leftover map comparison": "残差地图比较", + "leftover map comparison axis 1": "残差地图比较轴 1", + "leftover map comparison axis 2": "残差地图比较轴 2", + "leftover map comparison axis {axis} ({share}%)": "残差地图比较轴 {axis} ({share}%)", + "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.": + "已命名坐标的残差地图比较图形。点击帖文标记以打开该帖文。此图不编造残差分数。", "Leftover map item coverage": "残差地图准则覆盖范围", "Leftover map incomplete posts": "残差地图不完整帖文", "Leftover map incomplete items": "残差地图不完整准则", @@ -1543,6 +1557,13 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison expected": "残差マップの比較期待", "Leftover map comparison rank": "残差マップの比較階数", "Leftover map comparison coordinates": "残差マップの比較座標", + "Leftover map comparison graphic": "残差マップの比較図", + "Leftover map comparison": "残差マップの比較", + "leftover map comparison axis 1": "残差マップの比較軸 1", + "leftover map comparison axis 2": "残差マップの比較軸 2", + "leftover map comparison axis {axis} ({share}%)": "残差マップの比較軸 {axis} ({share}%)", + "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.": + "既に名付けられた座標の残差マップ比較図です。投稿マーカーをクリックするとその投稿を開きます。この図は残差スコアを作りません。", "Leftover map item coverage": "残差マップの基準対象範囲", "Leftover map incomplete posts": "残差マップの不完全投稿", "Leftover map incomplete items": "残差マップの不完全基準", @@ -2163,6 +2184,13 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison expected": "Kỳ vọng so sánh bản đồ phần dư", "Leftover map comparison rank": "Hạng so sánh bản đồ phần dư", "Leftover map comparison coordinates": "Tọa độ so sánh bản đồ phần dư", + "Leftover map comparison graphic": "Đồ họa so sánh bản đồ phần dư", + "Leftover map comparison": "So sánh bản đồ phần dư", + "leftover map comparison axis 1": "trục so sánh bản đồ phần dư 1", + "leftover map comparison axis 2": "trục so sánh bản đồ phần dư 2", + "leftover map comparison axis {axis} ({share}%)": "trục so sánh bản đồ phần dư {axis} ({share}%)", + "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.": + "Đồ họa so sánh bản đồ phần dư của tọa độ đã được đặt tên. Nhấn dấu bài để mở bài đó. Đồ họa không tạo ra điểm phần dư.", "Leftover map item coverage": "Phạm vi tiêu chí bản đồ phần dư", "Leftover map incomplete posts": "Bài không đầy đủ trên bản đồ phần dư", "Leftover map incomplete items": "Tiêu chí không đầy đủ trên bản đồ phần dư", diff --git a/frontend/src/leftoverMapPlotAxisShare.test.ts b/frontend/src/leftoverMapPlotAxisShare.test.ts index 08611b707..6c99dac66 100644 --- a/frontend/src/leftoverMapPlotAxisShare.test.ts +++ b/frontend/src/leftoverMapPlotAxisShare.test.ts @@ -2,6 +2,10 @@ import { describe, expect, it } from "vitest"; import { formatLeftoverMapPlotAxisShare, leftoverShareForAxis, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_1, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_2, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, + LEFTOVER_MAP_PLOT_AXIS_SHARE, } from "./leftoverMapPlotAxisShare"; describe("leftoverShareForAxis", () => { @@ -48,3 +52,16 @@ describe("formatLeftoverMapPlotAxisShare", () => { expect(formatLeftoverMapPlotAxisShare(Number.NEGATIVE_INFINITY)).toBeNull(); }); }); + +describe("leftover map comparison axis share labels", () => { + it("stays distinct from leftover-map graphic axis share copy", () => { + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE).toBe( + "leftover map comparison axis {axis} ({share}%)", + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_1).toBe("leftover map comparison axis 1"); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_2).toBe("leftover map comparison axis 2"); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_1).not.toBe("leftover-map axis 1"); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_2).not.toBe("leftover-map axis 2"); + }); +}); diff --git a/frontend/src/leftoverMapPlotAxisShare.ts b/frontend/src/leftoverMapPlotAxisShare.ts index 57d45aef4..3964ab6b5 100644 --- a/frontend/src/leftoverMapPlotAxisShare.ts +++ b/frontend/src/leftoverMapPlotAxisShare.ts @@ -4,6 +4,13 @@ import type { LeftoverMapAxis } from "./api"; export const LEFTOVER_MAP_PLOT_AXIS_SHARE = "leftover-map axis {axis} ({share}%)"; +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE = + "leftover map comparison axis {axis} ({share}%)"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 = "leftover map comparison axis 1"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_2 = "leftover map comparison axis 2"; + export type LeftoverMapPlotAxisShare = { axis_index: LeftoverMapAxis["axis_index"]; leftover_share?: LeftoverMapAxis["leftover_share"] | null; diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index 9421a6e5f..a8eed5b16 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -5,6 +5,9 @@ import { formatLeftoverMapDistance, hasLeftoverMapPlotCoordinates, layoutLeftoverMapPlot, + LEFTOVER_MAP_COMPARE_PLOT_CAPTION, + LEFTOVER_MAP_COMPARE_PLOT_LABEL, + LEFTOVER_MAP_COMPARE_PLOT_SVG, PLOT_HEIGHT, PLOT_PADDING, PLOT_WIDTH, @@ -1192,3 +1195,13 @@ describe("firstPlottablePairForPost", () => { expect(firstPlottablePairForPost([pair({ leftover_map_person_axis_1: null })], "post-demo-public")).toBeNull(); }); }); + +describe("leftover map comparison graphic labels", () => { + it("stays distinct from leftover-map graphic display copy", () => { + expect(LEFTOVER_MAP_COMPARE_PLOT_LABEL).toBe("Leftover map comparison graphic"); + expect(LEFTOVER_MAP_COMPARE_PLOT_SVG).toBe("Leftover map comparison"); + expect(LEFTOVER_MAP_COMPARE_PLOT_CAPTION).toContain("already-named coordinates"); + expect(LEFTOVER_MAP_COMPARE_PLOT_LABEL).not.toBe("Leftover-map graphic display"); + expect(LEFTOVER_MAP_COMPARE_PLOT_SVG).not.toBe("Leftover map"); + }); +}); diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 4946c96d8..7681dfdd7 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -31,9 +31,12 @@ * (ADR 0300), grouping comparison leftover-map rank * (ADR 0301), grouping comparison leftover-map coordinates * (ADR 0302), grouping comparison leftover-map coordinates payload - * (ADR 0303), and grouping comparison leftover-map graphic display - * (ADR 0304) caption the pair list or the grouping comparison strip. - * ADR 0304 reuses this graphic layout on the grouping comparison strip. + * (ADR 0303), grouping comparison leftover-map graphic display + * (ADR 0304), and grouping comparison leftover-map axis share on that + * graphic (ADR 0305) caption the pair list or the grouping comparison + * strip. ADR 0304 reuses this graphic layout on the grouping comparison + * strip. ADR 0305 captions leftover-map axis share on that comparison + * graphic from already-named leftover-map axes. */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; @@ -53,6 +56,13 @@ import type { LeftoverPair } from "./api"; export const LEFTOVER_MAP_PLOT_CAPTION = "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, 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, and leftover-map rank. 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. Click a post marker to open that post. The plot does not invent a leftover score."; +export const LEFTOVER_MAP_COMPARE_PLOT_LABEL = "Leftover map comparison graphic"; + +export const LEFTOVER_MAP_COMPARE_PLOT_CAPTION = + "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score."; + +export const LEFTOVER_MAP_COMPARE_PLOT_SVG = "Leftover map comparison"; + export const LEFTOVER_MAP_PLOT_POST_ACTION = "Open leftover-map post {title} at ξ {person}"; diff --git a/pyproject.toml b/pyproject.toml index 355fbe0c7..f9750f557 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.61.0" +version = "2.62.0" description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication." readme = "README.md" license = { text = "MIT" } diff --git a/uv.lock b/uv.lock index 0e7d2ce9d..6a91f195f 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.37.0" +version = "2.62.0" source = { editable = "." } dependencies = [ { name = "certifi" }, From de017c6828a4292a02b9a8f00627351da5774ae0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 10:51:15 +0900 Subject: [PATCH 2/3] chore(stack): inherit #841 ADR status contract --- tests/test_compare_graphic_adr_status.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/test_compare_graphic_adr_status.py diff --git a/tests/test_compare_graphic_adr_status.py b/tests/test_compare_graphic_adr_status.py new file mode 100644 index 000000000..641399a49 --- /dev/null +++ b/tests/test_compare_graphic_adr_status.py @@ -0,0 +1,19 @@ +"""Governance contract for the grouping-comparison graphic ADR state.""" + +from pathlib import Path + + +_ADR = ( + Path(__file__).parents[1] + / "docs" + / "adr" + / "0304-leftover-map-compare-graphic.md" +) + + +def test_compare_graphic_adr_remains_proposed_until_independent_acceptance() -> None: + """A Draft candidate cannot claim an accepted architecture decision.""" + text = _ADR.read_text(encoding="utf-8") + assert text.startswith("# ADR 0304") + assert "**Decision status:** Proposed" in text + assert "**Decision status:** Accepted" not in text From d4e13fdb2dcd49e26ab6404ce468c0209f6794be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 10:51:50 +0900 Subject: [PATCH 3/3] chore(stack): preserve #841 ADR governance in #844 --- docs/adr/0304-leftover-map-compare-graphic.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/adr/0304-leftover-map-compare-graphic.md b/docs/adr/0304-leftover-map-compare-graphic.md index f0cbd192c..ca6ec1746 100644 --- a/docs/adr/0304-leftover-map-compare-graphic.md +++ b/docs/adr/0304-leftover-map-compare-graphic.md @@ -1,6 +1,6 @@ # ADR 0304 — Show leftover-map graphic display on grouping comparison strip -**Decision status:** Accepted +**Decision status:** Proposed **Date:** 2026-08-31 **Amended by:** [ADR 0305](0305-leftover-map-compare-plot-axis-share.md) @@ -208,8 +208,7 @@ leftover share on pair segments, leftover-map unexplained leftover share persistence, leftover-map explained leftover share on pair segments, leftover-map explained leftover share persistence, leftover-map reconstruction on pair segments, leftover-map reconstruction persistence, 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 +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