From 12e40c93acb8473075199d8c8124b85b3d39adff Mon Sep 17 00:00:00 2001
From: seonghobae
Date: Sun, 30 Aug 2026 18:44:39 +0000
Subject: [PATCH 1/3] feat(reports): leftover-map axis share on grouping
comparison strip (v2.61.0)
Return persisted leftover_map_axes on GET /api/reports/compare/{period} and
caption each grouping row through leftoverMapCompareAxisShare (ADR 0304).
Share 0 is shown, including rank-0 unused axes. Missing or non-finite share
omits that leftover-map comparison axis share badge. Do not invent leftover-map
axis share from leftover-map singular value. Do not add leftover-map graphic
or singular values to the strip. No new columns.
---
AGENTS.md | 10 +
ARCHITECTURE.md | 7 +-
.../2.61.0-leftover-map-compare-axis-share.md | 47 ++++
CHANGELOG.md | 58 +++++
CLAUDE.md | 2 +-
backend/app/report_ingestion.py | 25 ++
backend/tests/test_api.py | 9 +
docs/adr/0148-leftover-map-axis-share.md | 4 +-
.../0304-leftover-map-compare-axis-share.md | 230 ++++++++++++++++++
docs/product-technical-gap-baseline.md | 35 +++
docs/storybook-inventory.md | 2 +-
frontend/package.json | 2 +-
frontend/src/App.test.tsx | 36 +++
frontend/src/App.tsx | 20 ++
frontend/src/api.ts | 1 +
frontend/src/components/LeftoverPairList.tsx | 4 +-
frontend/src/i18n.test.ts | 15 ++
frontend/src/i18n.ts | 8 +
frontend/src/leftoverMapCompareAxis.test.ts | 65 +++++
frontend/src/leftoverMapCompareAxis.ts | 34 +++
frontend/src/leftoverMapPlotLayout.ts | 5 +-
pyproject.toml | 2 +-
22 files changed, 611 insertions(+), 10 deletions(-)
create mode 100644 CHANGELOG.d/2.61.0-leftover-map-compare-axis-share.md
create mode 100644 docs/adr/0304-leftover-map-compare-axis-share.md
create mode 100644 frontend/src/leftoverMapCompareAxis.test.ts
create mode 100644 frontend/src/leftoverMapCompareAxis.ts
diff --git a/AGENTS.md b/AGENTS.md
index d7b5f781e..c9111aec6 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 0304 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 0304) 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..7f68ae50d 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 0304 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 0304);
`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..342eb6ffc
--- /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 0304). 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..f43cd7dc0 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 0304 / 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..d0da895bd 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 / 0304), 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..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..95bb8354c 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 0304](0304-leftover-map-compare-axis-share.md)
+(leftover-map axis share on the grouping comparison strip)
## Context
diff --git a/docs/adr/0304-leftover-map-compare-axis-share.md b/docs/adr/0304-leftover-map-compare-axis-share.md
new file mode 100644
index 000000000..f54656a45
--- /dev/null
+++ b/docs/adr/0304-leftover-map-compare-axis-share.md
@@ -0,0 +1,230 @@
+# ADR 0304 — Name leftover-map axis share on the grouping comparison strip
+
+**Decision status:** Accepted
+**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..583f433d4 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 0304 / 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 && (
{row.leftover_pairs.map((pair) => {
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/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx
index 0db0667fd..609d67a02 100644
--- a/frontend/src/components/LeftoverPairList.tsx
+++ b/frontend/src/components/LeftoverPairList.tsx
@@ -104,7 +104,9 @@ export type LeftoverPairListProps = {
* buttons, not this pair list. ADR 0302 names persisted leftover-map
* coordinates on grouping comparison leftover-pair buttons, not this
* pair list. ADR 0303 returns persisted leftover-map coordinates on
- * grouping comparison leftover pairs, not this pair list. Every badge still
+ * grouping comparison leftover pairs, not this pair list. ADR 0304
+ * names persisted leftover-map axis share on the grouping comparison
+ * strip, not this pair list. Every badge still
* renders together before opening the named post.
*/
export function LeftoverPairList({
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts
index ff57dc970..71bbcc04c 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -43,6 +43,7 @@ describe("i18n", () => {
"Page",
"Answer",
"leftover axis {axis} {share}%",
+ "leftover map comparison axis {axis} {share}%",
"Leftover-map axis share",
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.",
"Leftover pairs",
@@ -91,6 +92,7 @@ describe("i18n", () => {
"Leftover map comparison expected",
"Leftover map comparison rank",
"Leftover map comparison coordinates",
+ "Leftover map comparison axis share",
"Leftover-map graphic item coverage",
"Leftover map item coverage",
"Leftover map incomplete posts",
@@ -561,6 +563,19 @@ describe("i18n", () => {
},
);
+ it.each([
+ ["ko", "잔여 지도 비교 축 비율"],
+ ["zh", "残差地图比较轴占比"],
+ ["ja", "残差マップの比較軸の比率"],
+ ["vi", "Tỷ trọng trục so sánh bản đồ phần dư"],
+ ] as const)(
+ "formats leftover map comparison axis share label in %s",
+ (locale, expected) => {
+ setLocale(locale);
+ expect(t("Leftover map comparison axis share")).toBe(expected);
+ },
+ );
+
it.each([
["ko", "잔여 지도 그림 기준 포함 범위"],
["zh", "残差图图形准则覆盖范围"],
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index c304c0c5e..563cf264e 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -282,6 +282,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison expected": "잔여 지도 비교 기대",
"Leftover map comparison rank": "잔여 지도 비교 순위",
"Leftover map comparison coordinates": "잔여 지도 비교 좌표",
+ "Leftover map comparison axis share": "잔여 지도 비교 축 비율",
+ "leftover map comparison axis {axis} {share}%": "잔여 지도 비교 축 {axis} {share}%",
"Leftover map item coverage": "잔여 지도 기준 포함 범위",
"Leftover map incomplete posts": "잔여 지도 불완전 글",
"Leftover map incomplete items": "잔여 지도 불완전 기준",
@@ -902,6 +904,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison expected": "残差地图比较期望",
"Leftover map comparison rank": "残差地图比较秩",
"Leftover map comparison coordinates": "残差地图比较坐标",
+ "Leftover map comparison axis share": "残差地图比较轴占比",
+ "leftover map comparison axis {axis} {share}%": "残差地图比较轴 {axis} {share}%",
"Leftover map item coverage": "残差地图准则覆盖范围",
"Leftover map incomplete posts": "残差地图不完整帖文",
"Leftover map incomplete items": "残差地图不完整准则",
@@ -1543,6 +1547,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison expected": "残差マップの比較期待",
"Leftover map comparison rank": "残差マップの比較階数",
"Leftover map comparison coordinates": "残差マップの比較座標",
+ "Leftover map comparison axis share": "残差マップの比較軸の比率",
+ "leftover map comparison axis {axis} {share}%": "残差マップの比較軸 {axis} {share}%",
"Leftover map item coverage": "残差マップの基準対象範囲",
"Leftover map incomplete posts": "残差マップの不完全投稿",
"Leftover map incomplete items": "残差マップの不完全基準",
@@ -2163,6 +2169,8 @@ 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 axis share": "Tỷ trọng trục so sánh bản đồ phần dư",
+ "leftover map comparison axis {axis} {share}%": "trục so sánh bản đồ phần dư {axis} {share}%",
"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/leftoverMapCompareAxis.test.ts b/frontend/src/leftoverMapCompareAxis.test.ts
new file mode 100644
index 000000000..1a9ac333d
--- /dev/null
+++ b/frontend/src/leftoverMapCompareAxis.test.ts
@@ -0,0 +1,65 @@
+import { describe, expect, it } from "vitest";
+import {
+ leftoverMapCompareAxisShare,
+ LEFTOVER_MAP_COMPARE_AXIS_SHARE,
+ LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL,
+ LEFTOVER_MAP_LIST_AXIS_SHARE,
+ LEFTOVER_MAP_PLOT_AXIS_SHARE,
+} from "./leftoverMapCompareAxis";
+
+describe("leftoverMapCompareAxisShare", () => {
+ it("names persisted leftover-map axis share without inventing a leftover score", () => {
+ expect(
+ leftoverMapCompareAxisShare({ axis_index: 1, leftover_share: 0.82 }),
+ ).toEqual({ axis: 1, share: "82" });
+ expect(
+ leftoverMapCompareAxisShare({ axis_index: 2, leftover_share: 0.18 }),
+ ).toEqual({ axis: 2, share: "18" });
+ });
+
+ it("names a rank-0 zero leftover-map axis share", () => {
+ expect(leftoverMapCompareAxisShare({ axis_index: 1, leftover_share: 0 })).toEqual({
+ axis: 1,
+ share: "0",
+ });
+ });
+
+ it("names a finite negative leftover-map axis share without clamping", () => {
+ expect(leftoverMapCompareAxisShare({ axis_index: 1, leftover_share: -0.1 })).toEqual({
+ axis: 1,
+ share: "-10",
+ });
+ });
+
+ it("omits leftover-map axis share that is missing or non-finite", () => {
+ expect(leftoverMapCompareAxisShare(null)).toBeNull();
+ expect(leftoverMapCompareAxisShare(undefined)).toBeNull();
+ expect(
+ leftoverMapCompareAxisShare({ axis_index: 1, leftover_share: Number.NaN }),
+ ).toBeNull();
+ expect(
+ leftoverMapCompareAxisShare({ axis_index: 1, leftover_share: Number.POSITIVE_INFINITY }),
+ ).toBeNull();
+ });
+
+ it("does not invent leftover-map axis share from leftover-map singular value", () => {
+ expect(
+ leftoverMapCompareAxisShare({
+ axis_index: 1,
+ leftover_share: Number.NaN,
+ }),
+ ).toBeNull();
+ expect(leftoverMapCompareAxisShare({ axis_index: 1, leftover_share: 0.82 })).not.toEqual({
+ axis: 1,
+ share: "1.84",
+ });
+ });
+
+ it("keeps the grouping comparison leftover-map axis share label distinct from leftover-axis badges and graphic axes", () => {
+ expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL).toBe("Leftover map comparison axis share");
+ expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL).not.toBe("Leftover-map axis share");
+ expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).toBe("leftover map comparison axis {axis} {share}%");
+ expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).not.toBe(LEFTOVER_MAP_LIST_AXIS_SHARE);
+ expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE);
+ });
+});
diff --git a/frontend/src/leftoverMapCompareAxis.ts b/frontend/src/leftoverMapCompareAxis.ts
new file mode 100644
index 000000000..6bc4ffadc
--- /dev/null
+++ b/frontend/src/leftoverMapCompareAxis.ts
@@ -0,0 +1,34 @@
+/** Caption leftover-map axis share on the grouping comparison strip (ADR 0304). */
+
+import type { LeftoverMapAxis } from "./api";
+
+export const LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL = "Leftover map comparison axis share";
+
+export const LEFTOVER_MAP_COMPARE_AXIS_SHARE = "leftover map comparison axis {axis} {share}%";
+
+export const LEFTOVER_MAP_LIST_AXIS_SHARE = "leftover axis {axis} {share}%";
+
+export const LEFTOVER_MAP_PLOT_AXIS_SHARE = "leftover-map axis {axis} ({share}%)";
+
+export type LeftoverMapCompareAxisShare = {
+ axis: number;
+ share: string;
+};
+
+export function leftoverMapCompareAxisShare(
+ axis: Pick | null | undefined,
+): LeftoverMapCompareAxisShare | null {
+ if (axis == null) {
+ return null;
+ }
+ if (!Number.isInteger(axis.axis_index) || axis.axis_index < 1) {
+ return null;
+ }
+ if (axis.leftover_share == null || !Number.isFinite(axis.leftover_share)) {
+ return null;
+ }
+ return {
+ axis: axis.axis_index,
+ share: (axis.leftover_share * 100).toFixed(0),
+ };
+}
diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts
index d801bcc97..ee0158f34 100644
--- a/frontend/src/leftoverMapPlotLayout.ts
+++ b/frontend/src/leftoverMapPlotLayout.ts
@@ -30,8 +30,9 @@
* (ADR 0299), grouping comparison leftover expected
* (ADR 0300), grouping comparison leftover-map rank
* (ADR 0301), grouping comparison leftover-map coordinates
- * (ADR 0302), and grouping comparison leftover-map coordinates payload
- * (ADR 0303) caption the pair list or the grouping comparison strip,
+ * (ADR 0302), grouping comparison leftover-map coordinates payload
+ * (ADR 0303), and grouping comparison leftover-map axis share
+ * (ADR 0304) caption the pair list or the grouping comparison strip,
* not this graphic layout.
*/
diff --git a/pyproject.toml b/pyproject.toml
index d9e27eb25..355fbe0c7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.60.0"
+version = "2.61.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" }
From 93f52af35e5643e77cfe5dff4501c5ee06ac4932 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 6 Sep 2026 05:57:09 +0900
Subject: [PATCH 2/3] test(auth): fail closed mixed-visibility axis aggregate
---
tests/test_compare_axis_share_visibility.py | 93 +++++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 tests/test_compare_axis_share_visibility.py
diff --git a/tests/test_compare_axis_share_visibility.py b/tests/test_compare_axis_share_visibility.py
new file mode 100644
index 000000000..ed72a2ee7
--- /dev/null
+++ b/tests/test_compare_axis_share_visibility.py
@@ -0,0 +1,93 @@
+"""Authorization regression for grouping-comparison leftover-map aggregates."""
+
+from __future__ import annotations
+
+import pytest
+
+from backend.app import main
+from backend.app.auth import CurrentAccount
+
+
+class _Acquire:
+ """Return one inert connection through an async pool context."""
+
+ async def __aenter__(self) -> object:
+ return object()
+
+ async def __aexit__(self, *_args: object) -> bool:
+ return False
+
+
+class _Pool:
+ """Provide the minimal pool protocol used by the compare handler."""
+
+ def acquire(self) -> _Acquire:
+ return _Acquire()
+
+
+def _account() -> CurrentAccount:
+ """Authorize one private entity/process-unit scope and report reads."""
+ return CurrentAccount(
+ "account-1",
+ "subject-1",
+ "Analyst",
+ None,
+ frozenset({"entity-visible"}),
+ frozenset({"unit-visible"}),
+ frozenset({"post_read"}),
+ )
+
+
+@pytest.mark.anyio
+async def test_compare_omits_full_group_axis_aggregate_when_population_is_partly_hidden(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """Never disclose persisted axis aggregates computed with inaccessible members."""
+
+ async def _fetch_period_comparison(_conn: object, _period_code: str) -> list[dict[str, object]]:
+ return [
+ {
+ "grouping_kind": "corporate_entity",
+ "grouping_key": "group-1",
+ "grouping_label": "Group 1",
+ "mean_theta": 0.25,
+ "post_count": 2,
+ "link_method": "anchor",
+ "members": [
+ {
+ "visibility_code": "private",
+ "corporate_entity_id": "entity-visible",
+ "process_unit_id": "unit-visible",
+ "has_real_source_context": True,
+ },
+ {
+ "visibility_code": "private",
+ "corporate_entity_id": "entity-hidden",
+ "process_unit_id": "unit-hidden",
+ "has_real_source_context": True,
+ },
+ ],
+ "leftover_pairs": [],
+ "leftover_map_coverage": None,
+ "leftover_map_axes": [
+ {
+ "axis_index": 1,
+ "leftover_singular_value": 3.0,
+ "leftover_share": 0.8,
+ }
+ ],
+ }
+ ]
+
+ async def _has_real_source_context(_conn: object, _entity_ids: list[str]) -> bool:
+ return False
+
+ monkeypatch.setattr(main, "fetch_period_comparison", _fetch_period_comparison)
+ monkeypatch.setattr(main, "has_real_source_context", _has_real_source_context)
+
+ result = await main.compare_period_groupings("2026-W35", _account(), _Pool())
+
+ assert len(result["groupings"]) == 1
+ grouping = result["groupings"][0]
+ assert grouping["post_count"] == 1
+ assert grouping["leftover_map_axes"] == []
From f013dfb53b255e8e0be1dfb783cb41dca7eeb83e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 6 Sep 2026 11:48:14 +0900
Subject: [PATCH 3/3] fix(reports): repair axis-share visibility and ADR
authority
---
AGENTS.md | 4 +--
ARCHITECTURE.md | 4 +--
.../2.61.0-leftover-map-compare-axis-share.md | 2 +-
CHANGELOG.md | 2 +-
CLAUDE.md | 2 +-
backend/app/main.py | 5 +++
docs/adr/0148-leftover-map-axis-share.md | 2 +-
...> 0368-leftover-map-compare-axis-share.md} | 4 +--
docs/product-technical-gap-baseline.md | 2 +-
frontend/src/components/LeftoverPairList.tsx | 2 +-
frontend/src/leftoverMapCompareAxis.ts | 2 +-
frontend/src/leftoverMapPlotLayout.ts | 2 +-
.../test_compare_axis_share_adr_authority.py | 32 +++++++++++++++++++
13 files changed, 51 insertions(+), 14 deletions(-)
rename docs/adr/{0304-leftover-map-compare-axis-share.md => 0368-leftover-map-compare-axis-share.md} (99%)
create mode 100644 tests/test_compare_axis_share_adr_authority.py
diff --git a/AGENTS.md b/AGENTS.md
index c9111aec6..a7432b861 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -482,7 +482,7 @@ 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 0304 captions the grouping comparison strip with persisted leftover-map
+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
@@ -534,7 +534,7 @@ 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 0304) captions the grouping comparison
+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
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 7f68ae50d..d31674480 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -697,7 +697,7 @@ 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 0304 captions the grouping comparison strip with persisted leftover-map axis share
+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
@@ -731,7 +731,7 @@ incomplete item coverage (ADR 0292), leftover-map reconstruction `R̂`
(ADR 0301), leftover-map coordinates `ξ` / `ζ`
(ADR 0302), leftover-map coordinates on the compare leftover-pair payload
(ADR 0303), and leftover-map axis share on the grouping comparison strip
-(ADR 0304);
+(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
index 342eb6ffc..c33fc22bb 100644
--- a/CHANGELOG.d/2.61.0-leftover-map-compare-axis-share.md
+++ b/CHANGELOG.d/2.61.0-leftover-map-compare-axis-share.md
@@ -2,7 +2,7 @@
- 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 0304). After `make seed`, closest
+ `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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f43cd7dc0..af9ccd3f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ All notable changes to this project are documented here. Format follows
- 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 0304 / v2.61.0).
+ 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
diff --git a/CLAUDE.md b/CLAUDE.md
index d0da895bd..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 / 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 / 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/docs/adr/0148-leftover-map-axis-share.md b/docs/adr/0148-leftover-map-axis-share.md
index 95bb8354c..b7099d44a 100644
--- a/docs/adr/0148-leftover-map-axis-share.md
+++ b/docs/adr/0148-leftover-map-axis-share.md
@@ -4,7 +4,7 @@
**Date:** 2026-08-24
**Amended by:** [ADR 0269](0269-leftover-map-axis-share-plot.md)
(leftover-map axis share on the graphic display),
-[ADR 0304](0304-leftover-map-compare-axis-share.md)
+[ADR 0368](0368-leftover-map-compare-axis-share.md)
(leftover-map axis share on the grouping comparison strip)
## Context
diff --git a/docs/adr/0304-leftover-map-compare-axis-share.md b/docs/adr/0368-leftover-map-compare-axis-share.md
similarity index 99%
rename from docs/adr/0304-leftover-map-compare-axis-share.md
rename to docs/adr/0368-leftover-map-compare-axis-share.md
index f54656a45..08bdb6ce1 100644
--- a/docs/adr/0304-leftover-map-compare-axis-share.md
+++ b/docs/adr/0368-leftover-map-compare-axis-share.md
@@ -1,6 +1,6 @@
-# ADR 0304 — Name leftover-map axis share on the grouping comparison strip
+# ADR 0368 — Name leftover-map axis share on the grouping comparison strip
-**Decision status:** Accepted
+**Decision status:** Proposed
**Date:** 2026-08-31
Amends leftover pairs on the grouping comparison strip
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 583f433d4..19b261f69 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -10,7 +10,7 @@
> 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 0304 / v2.61.0) delivered locally on
+> (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
diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx
index 609d67a02..287e36d9f 100644
--- a/frontend/src/components/LeftoverPairList.tsx
+++ b/frontend/src/components/LeftoverPairList.tsx
@@ -104,7 +104,7 @@ export type LeftoverPairListProps = {
* buttons, not this pair list. ADR 0302 names persisted leftover-map
* coordinates on grouping comparison leftover-pair buttons, not this
* pair list. ADR 0303 returns persisted leftover-map coordinates on
- * grouping comparison leftover pairs, not this pair list. ADR 0304
+ * grouping comparison leftover pairs, not this pair list. ADR 0368
* names persisted leftover-map axis share on the grouping comparison
* strip, not this pair list. Every badge still
* renders together before opening the named post.
diff --git a/frontend/src/leftoverMapCompareAxis.ts b/frontend/src/leftoverMapCompareAxis.ts
index 6bc4ffadc..5944e94d4 100644
--- a/frontend/src/leftoverMapCompareAxis.ts
+++ b/frontend/src/leftoverMapCompareAxis.ts
@@ -1,4 +1,4 @@
-/** Caption leftover-map axis share on the grouping comparison strip (ADR 0304). */
+/** Caption leftover-map axis share on the grouping comparison strip (ADR 0368). */
import type { LeftoverMapAxis } from "./api";
diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts
index ee0158f34..430589937 100644
--- a/frontend/src/leftoverMapPlotLayout.ts
+++ b/frontend/src/leftoverMapPlotLayout.ts
@@ -32,7 +32,7 @@
* (ADR 0301), grouping comparison leftover-map coordinates
* (ADR 0302), grouping comparison leftover-map coordinates payload
* (ADR 0303), and grouping comparison leftover-map axis share
- * (ADR 0304) caption the pair list or the grouping comparison strip,
+ * (ADR 0368) caption the pair list or the grouping comparison strip,
* not this graphic layout.
*/
diff --git a/tests/test_compare_axis_share_adr_authority.py b/tests/test_compare_axis_share_adr_authority.py
new file mode 100644
index 000000000..12893dfcf
--- /dev/null
+++ b/tests/test_compare_axis_share_adr_authority.py
@@ -0,0 +1,32 @@
+"""Governance contract for grouping-comparison axis-share ADR identity."""
+
+from pathlib import Path
+
+
+_ROOT = Path(__file__).parents[1]
+_AXIS_ADR = _ROOT / "docs" / "adr" / "0368-leftover-map-compare-axis-share.md"
+_COLLIDING_ADR = _ROOT / "docs" / "adr" / "0304-leftover-map-compare-axis-share.md"
+
+
+def test_axis_share_uses_unclaimed_proposed_adr_identity() -> None:
+ """Keep the axis-share decision distinct from sibling ADR 0304."""
+ assert _AXIS_ADR.exists()
+ assert not _COLLIDING_ADR.exists()
+
+ text = _AXIS_ADR.read_text(encoding="utf-8")
+ assert text.startswith("# ADR 0368")
+ assert "**Decision status:** Proposed" in text
+ assert "**Decision status:** Accepted" not in text
+
+
+def test_axis_share_owned_references_follow_adr_0368() -> None:
+ """Do not leave branch-local axis-share authority split across ADR ids."""
+ expected = {
+ "docs/adr/0148-leftover-map-axis-share.md": "0368-leftover-map-compare-axis-share.md",
+ "frontend/src/leftoverMapCompareAxis.ts": "ADR 0368",
+ "frontend/src/components/LeftoverPairList.tsx": "ADR 0368",
+ "frontend/src/leftoverMapPlotLayout.ts": "ADR 0368",
+ }
+ for relative_path, marker in expected.items():
+ content = (_ROOT / relative_path).read_text(encoding="utf-8")
+ assert marker in content, relative_path