From e79137611f0d3041ad836c21e49a95bf7ef1001e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 30 Aug 2026 15:31:29 +0000
Subject: [PATCH 1/9] feat(reports): leftover-map axis share on grouping
comparison strip (v2.50.0)
Caption persisted leftover-map axis share on GET /api/reports/compare/{period} through leftoverMapCompareAxisShare (ADR 0293). Distinct aria-label Leftover map comparison axis share. Omit missing or non-finite share. Do not invent leftover-map axis share from leftover-map singular value. No SQL. No leftover score or theta.
---
AGENTS.md | 8 +
ARCHITECTURE.md | 2 +
.../2.50.0-leftover-map-compare-axis-share.md | 39 ++++
CHANGELOG.md | 39 ++++
CLAUDE.md | 2 +-
backend/app/report_ingestion.py | 25 +++
backend/tests/test_api.py | 9 +
.../0293-leftover-map-compare-axis-share.md | 180 ++++++++++++++++++
docs/product-technical-gap-baseline.md | 71 +++++++
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 | 2 +
frontend/src/i18n.ts | 8 +
frontend/src/leftoverMapCompareAxis.test.ts | 65 +++++++
frontend/src/leftoverMapCompareAxis.ts | 34 ++++
frontend/src/leftoverMapPlotLayout.ts | 5 +-
pyproject.toml | 2 +-
21 files changed, 549 insertions(+), 7 deletions(-)
create mode 100644 CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
create mode 100644 docs/adr/0293-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 30380bc66..9f55e640a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -416,6 +416,14 @@ dropped count that contradicts usable item complete-case integers, omits that le
comparison incomplete item note and does not invent dropped criteria from scored minus
used or from leftover pair count. Dropped `0` is shown when persisted. The strip
does not gain the leftover-map graphic.
+ADR 0293 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
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 346afb687..dd6bc8a4e 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -675,6 +675,8 @@ ADR 0291 captions the grouping comparison strip with persisted leftover-map inco
post coverage through leftoverMapIncompletePostCount.
ADR 0292 captions the grouping comparison strip with persisted leftover-map incomplete
item coverage through leftoverMapIncompleteItemCount.
+ADR 0293 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
diff --git a/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md b/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
new file mode 100644
index 000000000..683f6bf52
--- /dev/null
+++ b/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
@@ -0,0 +1,39 @@
+## 2.50.0 — Leftover-map axis share on the grouping comparison strip
+
+- Caption leftover-map axis share on the grouping comparison strip through
+ leftoverMapCompareAxisShare (ADR 0293). 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, and a grouping comparison leftover-map axis share badge that
+ matches leftover-axis report badges; 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, 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 a416d2f8b..2706f7c31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,45 @@ All notable changes to this project are documented here. Format follows
### Added
+- Period leftover pairs now caption the grouping comparison strip with
+ persisted leftover-map axis share through leftoverMapCompareAxisShare
+ (ADR 0293 / v2.50.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, and a grouping comparison leftover-map axis share badge that
+ matches leftover-axis report badges; 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, 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 or a theta. No new
+ columns.
+
- Period leftover pairs now caption the grouping comparison strip with
persisted leftover-map incomplete item coverage through
leftoverMapIncompleteItemCount (ADR 0292 / v2.49.0). After `make seed`,
diff --git a/CLAUDE.md b/CLAUDE.md
index 3a6fbb514..1963b8114 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), 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), 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 d675fe3ab..58c6c2dc4 100644
--- a/backend/app/report_ingestion.py
+++ b/backend/app/report_ingestion.py
@@ -1086,6 +1086,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"])
@@ -1132,6 +1147,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["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"]), []
+ )
+ ],
}
)
return payload
diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py
index 273a7ff94..95431c425 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/0293-leftover-map-compare-axis-share.md b/docs/adr/0293-leftover-map-compare-axis-share.md
new file mode 100644
index 000000000..f7fd01b5a
--- /dev/null
+++ b/docs/adr/0293-leftover-map-compare-axis-share.md
@@ -0,0 +1,180 @@
+# ADR 0293 — Name leftover-map axis share on the grouping comparison strip
+
+**Decision status:** Accepted
+**Date:** 2026-08-30
+
+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 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. The grouping comparison strip still names leftover pairs, `d`, and
+leftover-map coverage 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, 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 **0293** so it does not collide with
+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 post complete-case coverage fail-closed on the pair list
+(0288), leftover-map incomplete item coverage on the pair list (0287),
+leftover-map incomplete post coverage on the pair list (0286), leftover-map
+item complete-case coverage on the pair list (0285), leftover-map incomplete
+item coverage on the graphic display (0284), leftover-map incomplete post
+coverage on the graphic display (0283), leftover-map item complete-case
+coverage on the graphic display (0282), leftover-map complete-case coverage
+on the graphic display (0281), leftover-map rank on pair segments (0280),
+or the dashboard stacks.
+
+## 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 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, 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`. 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 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, and leftover-map singular values.
+
+## 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 f0befa176..6958ec3d1 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,76 @@
# Product & Technical Gap Baseline
+> Exact-head loop overlay: 2026-08-30 23:45 KST. Protected `main` is
+> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
+> of leftover-map coordinates (v2.24.0 / #782). Revert #808
+> (`revert-pr782-unauthorized` @ `1af3e53e`) restores `main` toward
+> `fc13acaa` (v2.23.0) and still needs independent APPROVE. GitHub
+> writes work (comment/close/create-PR/push). Token still has empty
+> `X-OAuth-Scopes`; repo permission is ADMIN. Open leftover stack
+> still lacks independent APPROVE. Do not self-approve.
+> #782 leftover-map coordinates through pair-segment distance
+> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained
+> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29
+> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair
+> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross
+> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map
+> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is
+> `e3fdfd74`. #806 leftover residual `R` on pair segments (v2.34.0 /
+> ADR 0277) is `33f6c3dd`. #809 leftover observed `Y` on pair segments
+> (v2.35.0 / ADR 0278) is `b334b00e`. #810 leftover expected `E` on pair
+> segments (v2.36.0 / ADR 0279) is `6e37757a`. #811 leftover-map rank on
+> pair segments (v2.37.0 / ADR 0280) is `e626a1d0`. #812 leftover-map
+> complete-case coverage on the graphic (v2.38.0 / ADR 0281) is `64964cb6`.
+> #813 leftover-map item complete-case coverage on the graphic (v2.39.0 /
+> ADR 0282) is `de2a8a8b`. #814 leftover-map incomplete post coverage on
+> the graphic (v2.40.0 / ADR 0283) is `77292872`. #815 leftover-map
+> incomplete item coverage on the graphic (v2.41.0 / ADR 0284) is
+> `63092de`. #816 leftover-map item complete-case coverage on the pair
+> list (v2.42.0 / ADR 0285) is `1e3d13e`. #817 leftover-map incomplete
+> post coverage on the pair list (v2.43.0 / ADR 0286) is `ef30930`.
+> #818 leftover-map incomplete item coverage on the pair list
+> (v2.44.0 / ADR 0287) is `aa32089`. #819 leftover-map post complete-case
+> coverage fail-closed on the pair list (v2.45.0 / ADR 0288) is `a2c96551`.
+> #821 leftover-map complete-case coverage on the grouping comparison
+> strip (v2.46.0 / ADR 0289) is `11a78553`. #822 leftover-map item
+> complete-case coverage on the grouping comparison strip (v2.47.0 /
+> ADR 0290) is `6726353a`. #823 leftover-map singular values on leftover-axis
+> badges (v2.47.0 / ADR 0290, plot stack) is `eb172c40`. #824 leftover-map
+> incomplete post coverage on the grouping comparison strip (v2.48.0 /
+> ADR 0291) is `8b2ed95`. #825 leftover-map incomplete item coverage on
+> the grouping comparison strip (v2.49.0 / ADR 0292) is `bca6baa`. Leave
+> #782/#802/#803/#804/#805/#806/#808/#809/#810/#811/#812/#813/#814/#815/#816/#817/#818/#819/#821/#822/#823/#824/#825 open for
+> independent review. Do not squash-merge stacked leftover PRs onto an
+> unprotected leftover base. Do not merge #808 without independent APPROVE.
+> Strix on #782 failed closed at `Run Strix (quick)`; do not weaken
+> fail-closed on `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError`
+> classifier is already on ContextualWisdomLab/.github. Copilot review is
+> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
+> closed. Only collaborator is `seonghobae`; no independent reviewer can
+> be requested from this token.
+>
+> Next buyer increment on this cycle: leftover-map axis share on the
+> grouping comparison strip (ADR 0293 / v2.50.0) delivered locally on
+> `feat/leftover-map-compare-axis-share-v2500`. 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. 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, 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. Stack
+> onto leftover branch `feat/leftover-map-compare-incomplete-item-v2490` /
+> #825; leave the PR open for independent review. Do not squash-merge onto
+> the leftover base. Do not persist leftover-map inner product, cosine, or
+> length as separate columns.
+
+
> Exact-head loop overlay: 2026-08-30 23:28 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
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 8fa819fcf..497940a94 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.49.0",
+ "version": "2.50.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 283e671ac..129526d0f 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 },
+ ],
},
{
grouping_kind: "corporate_entity",
@@ -932,6 +936,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 },
+ ],
},
],
}),
@@ -4323,6 +4331,34 @@ describe("App, authenticated", () => {
"Leftover-map graphic incomplete items",
),
).not.toBeInTheDocument();
+ 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: "Compare Business unit (PU): Demo Report High, mean θ 0.81" }),
).toHaveTextContent("mean θ 0.81");
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1255ff43c..7d77c77eb 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 "./App.css";
const AdminPanel = lazy(() => import("./components/AdminPanel").then((module) => ({ default: module.AdminPanel })));
@@ -4049,6 +4054,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 a6a097bc1..fed808ca9 100644
--- a/frontend/src/components/LeftoverPairList.tsx
+++ b/frontend/src/components/LeftoverPairList.tsx
@@ -87,7 +87,9 @@ export type LeftoverPairListProps = {
* item complete-case coverage on the grouping comparison strip, not this pair
* list. ADR 0291 names persisted leftover-map incomplete post coverage on the
* grouping comparison strip, not this pair list. ADR 0292 names persisted leftover-map
- * incomplete item coverage on the grouping comparison strip, not this pair list. Every badge still
+ * incomplete item coverage on the grouping comparison strip, not this pair list. ADR 0293
+ * 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 68a7ffc9f..c63d1b5b2 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",
@@ -81,6 +82,7 @@ describe("i18n", () => {
"Leftover map comparison item coverage",
"Leftover map comparison incomplete posts",
"Leftover map comparison incomplete items",
+ "Leftover map comparison axis share",
"Leftover-map graphic item coverage",
"Leftover map item coverage",
"Leftover map incomplete posts",
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index 95d7f3e33..3f186c2c2 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -272,6 +272,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison item coverage": "잔여 지도 비교 기준 포함 범위",
"Leftover map comparison incomplete posts": "잔여 지도 비교 불완전 글",
"Leftover map comparison incomplete items": "잔여 지도 비교 불완전 기준",
+ "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": "잔여 지도 불완전 기준",
@@ -882,6 +884,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison item coverage": "残差地图比较准则覆盖范围",
"Leftover map comparison incomplete posts": "残差地图比较不完整帖文",
"Leftover map comparison incomplete items": "残差地图比较不完整准则",
+ "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": "残差地图不完整准则",
@@ -1513,6 +1517,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison item coverage": "残差マップの比較基準対象範囲",
"Leftover map comparison incomplete posts": "残差マップの比較不完全投稿",
"Leftover map comparison incomplete items": "残差マップの比較不完全基準",
+ "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": "残差マップの不完全基準",
@@ -2123,6 +2129,8 @@ const TRANSLATIONS: Partial>> = {
"Leftover map comparison item coverage": "Phạm vi so sánh tiêu chí bản đồ phần dư",
"Leftover map comparison incomplete posts": "Bài không đầy đủ so sánh trên bản đồ phần dư",
"Leftover map comparison incomplete items": "Tiêu chí không đầy đủ so sánh trên 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..0ce1c330e
--- /dev/null
+++ b/frontend/src/leftoverMapCompareAxis.ts
@@ -0,0 +1,34 @@
+/** Caption leftover-map axis share on the grouping comparison strip (ADR 0293). */
+
+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 b96176152..082764847 100644
--- a/frontend/src/leftoverMapPlotLayout.ts
+++ b/frontend/src/leftoverMapPlotLayout.ts
@@ -19,8 +19,9 @@
* coverage (ADR 0287), grouping comparison complete-case coverage
* (ADR 0289), grouping comparison item complete-case coverage
* (ADR 0290), grouping comparison incomplete post coverage
- * (ADR 0291), and grouping comparison incomplete item coverage
- * (ADR 0292) caption the pair list or the grouping comparison strip,
+ * (ADR 0291), grouping comparison incomplete item coverage
+ * (ADR 0292), and grouping comparison leftover-map axis share
+ * (ADR 0293) caption the pair list or the grouping comparison strip,
* not this graphic layout.
*/
diff --git a/pyproject.toml b/pyproject.toml
index 3da74d89c..1ed881f7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.49.0"
+version = "2.50.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 cb0a9895f46cf7987c922f48b19d1282c7ea895a Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 6 Sep 2026 05:51:51 +0900
Subject: [PATCH 2/9] test(adr): fail on axis-share ADR 0293 collision
---
...t_leftover_map_axis_share_adr_authority.py | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
create mode 100644 tests/test_leftover_map_axis_share_adr_authority.py
diff --git a/tests/test_leftover_map_axis_share_adr_authority.py b/tests/test_leftover_map_axis_share_adr_authority.py
new file mode 100644
index 000000000..1304fe4e8
--- /dev/null
+++ b/tests/test_leftover_map_axis_share_adr_authority.py
@@ -0,0 +1,20 @@
+"""ADR authority regression for the grouping-comparison axis-share decision."""
+
+from pathlib import Path
+
+_ROOT = Path(__file__).resolve().parents[1]
+_ADR_DIRECTORY = _ROOT / "docs" / "adr"
+
+
+def test_axis_share_decision_uses_unclaimed_proposed_adr_0367() -> None:
+ """Keep the parallel axis-share decision off reconstruction ADR 0293."""
+ colliding = _ADR_DIRECTORY / "0293-leftover-map-compare-axis-share.md"
+ canonical = _ADR_DIRECTORY / "0367-leftover-map-compare-axis-share.md"
+
+ assert not colliding.exists(), "axis-share must not reuse reconstruction ADR 0293"
+ assert canonical.exists(), "axis-share decision must move to unclaimed ADR 0367"
+
+ content = canonical.read_text(encoding="utf-8")
+ assert content.startswith("# ADR 0367 —")
+ assert "**Decision status:** Proposed" in content
+ assert "**Decision status:** Accepted" not in content
From 97ee2eb76fc753816284b2fd29acb355876995ef Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 6 Sep 2026 05:55:06 +0900
Subject: [PATCH 3/9] 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 bc10896f2587e609dc6c2483c8d7f6c40ecbe16c Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 6 Sep 2026 11:09:24 +0900
Subject: [PATCH 4/9] fix(reports): repair axis-share authority and visibility
---
AGENTS.md | 2 +-
ARCHITECTURE.md | 2 +-
CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md | 2 +-
CHANGELOG.md | 2 +-
CLAUDE.md | 2 +-
backend/app/main.py | 5 +++++
...axis-share.md => 0367-leftover-map-compare-axis-share.md} | 4 ++--
docs/product-technical-gap-baseline.md | 2 +-
8 files changed, 13 insertions(+), 8 deletions(-)
rename docs/adr/{0293-leftover-map-compare-axis-share.md => 0367-leftover-map-compare-axis-share.md} (99%)
diff --git a/AGENTS.md b/AGENTS.md
index 9f55e640a..51ca21e95 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -416,7 +416,7 @@ dropped count that contradicts usable item complete-case integers, omits that le
comparison incomplete item note and does not invent dropped criteria from scored minus
used or from leftover pair count. Dropped `0` is shown when persisted. The strip
does not gain the leftover-map graphic.
-ADR 0293 captions the grouping comparison strip with persisted leftover-map
+ADR 0367 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
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index dd6bc8a4e..ab57d31a7 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -675,7 +675,7 @@ ADR 0291 captions the grouping comparison strip with persisted leftover-map inco
post coverage through leftoverMapIncompletePostCount.
ADR 0292 captions the grouping comparison strip with persisted leftover-map incomplete
item coverage through leftoverMapIncompleteItemCount.
-ADR 0293 captions the grouping comparison strip with persisted leftover-map axis share
+ADR 0367 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
diff --git a/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md b/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
index 683f6bf52..efde4c41e 100644
--- a/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
+++ b/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
@@ -1,7 +1,7 @@
## 2.50.0 — Leftover-map axis share on the grouping comparison strip
- Caption leftover-map axis share on the grouping comparison strip through
- leftoverMapCompareAxisShare (ADR 0293). After `make seed`, closest and
+ leftoverMapCompareAxisShare (ADR 0367). 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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2706f7c31..11f1d3c07 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 caption the grouping comparison strip with
persisted leftover-map axis share through leftoverMapCompareAxisShare
- (ADR 0293 / v2.50.0). After `make seed`, closest and farthest leftover
+ (ADR 0367 / v2.50.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
diff --git a/CLAUDE.md b/CLAUDE.md
index 1963b8114..53168be71 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), 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 / 0367), 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/0293-leftover-map-compare-axis-share.md b/docs/adr/0367-leftover-map-compare-axis-share.md
similarity index 99%
rename from docs/adr/0293-leftover-map-compare-axis-share.md
rename to docs/adr/0367-leftover-map-compare-axis-share.md
index f7fd01b5a..ea0d67939 100644
--- a/docs/adr/0293-leftover-map-compare-axis-share.md
+++ b/docs/adr/0367-leftover-map-compare-axis-share.md
@@ -1,6 +1,6 @@
-# ADR 0293 — Name leftover-map axis share on the grouping comparison strip
+# ADR 0367 — Name leftover-map axis share on the grouping comparison strip
-**Decision status:** Accepted
+**Decision status:** Proposed
**Date:** 2026-08-30
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 6958ec3d1..4d6e89a24 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -50,7 +50,7 @@
> be requested from this token.
>
> Next buyer increment on this cycle: leftover-map axis share on the
-> grouping comparison strip (ADR 0293 / v2.50.0) delivered locally on
+> grouping comparison strip (ADR 0367 / v2.50.0) delivered locally on
> `feat/leftover-map-compare-axis-share-v2500`. Include persisted
> leftover_map_axes on GET /api/reports/compare/{period} and caption each
> grouping row through leftoverMapCompareAxisShare with distinct
From f28b5da6f92984483fbe1e9a0fc82f0bb24ecf57 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sun, 6 Sep 2026 13:51:26 +0900
Subject: [PATCH 5/9] docs(reports): align axis-share review contracts
---
ARCHITECTURE.md | 5 +++--
CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md | 3 ++-
CHANGELOG.md | 3 ++-
docs/product-technical-gap-baseline.md | 9 +++------
docs/storybook-inventory.md | 2 +-
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index ab57d31a7..5fdf07a7d 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -696,8 +696,9 @@ including on the pair list. Results persist to
`GET /api/reports/compare/{period}` is the home-page grouping strip
and carries the same ABAC-filtered leftover pairs (ADR 0149) plus persisted
leftover-map complete-case coverage (ADR 0289), leftover-map item complete-case
-coverage (ADR 0290), leftover-map incomplete post coverage (ADR 0291), and leftover-map
-incomplete item coverage (ADR 0292);
+coverage (ADR 0290), leftover-map incomplete post coverage (ADR 0291), leftover-map
+incomplete item coverage (ADR 0292), and persisted `leftover_map_axes` consumed through
+`leftoverMapCompareAxisShare` under the full-visible-grouping rule (ADR 0367);
`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.50.0-leftover-map-compare-axis-share.md b/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
index efde4c41e..8cedf46c0 100644
--- a/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
+++ b/CHANGELOG.d/2.50.0-leftover-map-compare-axis-share.md
@@ -25,7 +25,8 @@
comparison note that matches leftover-map incomplete post coverage, a
grouping comparison note that matches leftover-map incomplete item
coverage, and a grouping comparison leftover-map axis share badge that
- matches leftover-axis report badges; click a post marker or a pair button
+ names the same persisted share as leftover-axis report badges but uses
+ distinct comparison copy and accessible naming; 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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11f1d3c07..f86c0d275 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,7 +34,8 @@ All notable changes to this project are documented here. Format follows
comparison note that matches leftover-map incomplete post coverage, a
grouping comparison note that matches leftover-map incomplete item
coverage, and a grouping comparison leftover-map axis share badge that
- matches leftover-axis report badges; click a post marker or a pair button
+ names the same persisted share as leftover-axis report badges but uses
+ distinct comparison copy and accessible naming; 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
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 4d6e89a24..e6efbb3fb 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -69,8 +69,7 @@
> #825; leave the PR open for independent review. Do not squash-merge onto
> the leftover base. Do not persist leftover-map inner product, cosine, or
> length as separate columns.
-
-
+>
> Exact-head loop overlay: 2026-08-30 23:28 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
@@ -269,8 +268,7 @@
> `feat/leftover-map-compare-coverage-v2460` / #821; leave the PR open for
> independent review. Do not squash-merge onto the leftover base. Do not
> persist leftover-map inner product, cosine, or length as separate columns.
-
-
+>
> Exact-head loop overlay: 2026-08-30 22:52 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
@@ -332,8 +330,7 @@
> / #819; leave the PR open for independent review. Do not squash-merge
> onto the leftover base. Do not persist leftover-map inner product, cosine,
> or length as separate columns.
-
-
+>
> Exact-head loop overlay: 2026-08-30 22:30 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md
index 6852fca40..7712d8aa8 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, 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` |
+| `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 each coverage label only when its own helper returns a usable value. The axis-share label is independent: it is shown when leftoverMapCompareAxisShare returns a finite persisted leftover_share, including zero and finite negative values, and is omitted only when that share is missing or non-finite. | `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` |
From 78a464806f275bc5afc7012455a066b83f34d420 Mon Sep 17 00:00:00 2001
From: LineageWeave automation
Date: Mon, 7 Sep 2026 06:06:29 +0000
Subject: [PATCH 6/9] docs(reports): remove blank quoted gap-baseline
separators
---
docs/product-technical-gap-baseline.md | 32 --------------------------
1 file changed, 32 deletions(-)
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index e6efbb3fb..978a803c9 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -48,7 +48,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map axis share on the
> grouping comparison strip (ADR 0367 / v2.50.0) delivered locally on
> `feat/leftover-map-compare-axis-share-v2500`. Include persisted
@@ -69,7 +68,6 @@
> #825; leave the PR open for independent review. Do not squash-merge onto
> the leftover base. Do not persist leftover-map inner product, cosine, or
> length as separate columns.
->
> Exact-head loop overlay: 2026-08-30 23:28 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
@@ -117,7 +115,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map incomplete item
> coverage on the grouping comparison strip (ADR 0292 / v2.49.0) delivered
> locally on `feat/leftover-map-compare-incomplete-item-v2490`. Caption each
@@ -185,7 +182,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map incomplete post
> coverage on the grouping comparison strip (ADR 0291 / v2.48.0) delivered
> locally on `feat/leftover-map-compare-incomplete-post-v2480`. Caption each
@@ -250,7 +246,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map item complete-case
> coverage on the grouping comparison strip (ADR 0290 / v2.47.0) delivered
> locally on `feat/leftover-map-compare-item-coverage-v2470`. Caption each
@@ -268,7 +263,6 @@
> `feat/leftover-map-compare-coverage-v2460` / #821; leave the PR open for
> independent review. Do not squash-merge onto the leftover base. Do not
> persist leftover-map inner product, cosine, or length as separate columns.
->
> Exact-head loop overlay: 2026-08-30 22:52 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
@@ -310,7 +304,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map complete-case coverage
> on the grouping comparison strip (ADR 0289 / v2.46.0) delivered
> locally on `feat/leftover-map-compare-coverage-v2460`. Include persisted
@@ -330,7 +323,6 @@
> / #819; leave the PR open for independent review. Do not squash-merge
> onto the leftover base. Do not persist leftover-map inner product, cosine,
> or length as separate columns.
->
> Exact-head loop overlay: 2026-08-30 22:30 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
@@ -370,7 +362,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map post complete-case
> coverage fail-closed on the pair list (ADR 0288 / v2.45.0) delivered
> locally on `feat/leftover-map-list-post-coverage-helper-v2450`. Caption
@@ -427,7 +418,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map incomplete item
> coverage on the pair list (ADR 0287 / v2.44.0) delivered locally on
> `feat/leftover-map-list-incomplete-item-v2440`. Caption the pair-list
@@ -483,7 +473,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map incomplete post
> coverage on the pair list (ADR 0286 / v2.43.0) delivered locally on
> `feat/leftover-map-list-incomplete-post-v2430`. Caption the pair-list
@@ -537,7 +526,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map item complete-case
> coverage on the pair list (ADR 0285 / v2.42.0) delivered locally on
> `feat/leftover-map-list-item-coverage-v2420`. Caption the pair-list
@@ -588,7 +576,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map incomplete item
> coverage on the graphic display (ADR 0284 / v2.41.0) delivered locally on
> `feat/leftover-map-plot-incomplete-item-v2410`. Caption the leftover-map
@@ -641,7 +628,6 @@
> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map incomplete post
> coverage on the graphic display (ADR 0283 / v2.40.0) delivered locally on
> `feat/leftover-map-plot-incomplete-v2400`. Caption the leftover-map
@@ -691,7 +677,6 @@
> independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map item complete-case
> coverage on the graphic display (ADR 0282 / v2.39.0) delivered locally on
> `feat/leftover-map-plot-item-coverage-v2390`. Caption the leftover-map
@@ -737,7 +722,6 @@
> independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map complete-case coverage
> on the graphic display (ADR 0281 / v2.38.0) delivered locally on
> `feat/leftover-map-plot-coverage-v2380`. Caption the leftover-map graphic
@@ -782,7 +766,6 @@
> independent APPROVE. Issues #79 and #87 stay open. #96 is already
> closed. Only collaborator is `seonghobae`; no independent reviewer can
> be requested from this token.
->
> Next buyer increment on this cycle: leftover-map rank on
> graphic-display pair segments (ADR 0280 / v2.37.0) delivered locally on
> `feat/leftover-map-segment-rank-v2370`. Caption each closest/farthest
@@ -824,7 +807,6 @@
> independent APPROVE. Issues #79 and #87 stay open. #96 does not exist
> on this repository. Only collaborator is `seonghobae`; no independent
> reviewer can be requested from this token.
->
> Next buyer increment on this cycle: leftover expected `E` on
> graphic-display pair segments (ADR 0279 / v2.36.0) delivered locally on
> `feat/leftover-map-segment-expected-v2360`. Caption each
@@ -864,7 +846,6 @@
> Copilot review is not independent APPROVE. Issues #79 and #87 stay open.
> #96 closed as weaker duplicate of #91. Only collaborator is
> `seonghobae`; no independent reviewer can be requested from this token.
->
> Next buyer increment on this cycle: leftover observed `Y` on
> graphic-display pair segments (ADR 0278 / v2.35.0) delivered locally on
> `feat/leftover-map-segment-observed-v2350`. Caption each
@@ -898,7 +879,6 @@
> classifier is already on ContextualWisdomLab/.github. Copilot review is
> not independent APPROVE. Issues #79 and #87 stay open. Only collaborator
> is `seonghobae`; no independent reviewer can be requested from this token.
->
> Next buyer increment on this cycle: leftover residual `R` on
> graphic-display pair segments (ADR 0277 / v2.34.0) delivered locally on
> `feat/leftover-map-segment-residual-v2340`. Caption each
@@ -931,7 +911,6 @@
> `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError` classifier is
> already on ContextualWisdomLab/.github. Copilot review is not
> independent APPROVE. Issues #79 and #87 stay open.
->
> Next buyer increment on this cycle: leftover-map unexplained leftover
> `U` on graphic-display pair segments (ADR 0276 / v2.33.0) delivered
> locally on `feat/leftover-map-segment-unexplained-leftover-v2330`.
@@ -966,7 +945,6 @@
> with the number of 96`). GitHub writes through `gh`/MCP succeed
> (comment/create-PR/auto-merge). Copilot review is not independent
> APPROVE. Do not self-approve.
->
> Next buyer increment on this cycle: leftover-map cross share `x` on
> graphic-display pair segments (ADR 0275 / v2.32.0) delivered locally on
> `feat/leftover-map-segment-cross-share-v2320`. Caption each
@@ -999,7 +977,6 @@
> (`state_reason` still 403). GitHub writes through `gh`/MCP succeed
> (comment/create-PR/auto-merge). Copilot review is not independent
> APPROVE. Do not self-approve.
->
> Next buyer increment on this cycle: leftover-map unexplained leftover
> share `s` on graphic-display pair segments (ADR 0274 / v2.31.0)
> delivered locally on
@@ -1032,7 +1009,6 @@
> 403). GitHub writes through `gh`/MCP succeed (comment/create-PR/
> auto-merge). Copilot review is not independent APPROVE. Do not
> self-approve.
->
> Next buyer increment on this cycle: leftover-map explained leftover
> share `e` on graphic-display pair segments (ADR 0273 / v2.30.0)
> delivered locally on
@@ -1062,7 +1038,6 @@
> `gh`/MCP succeed (comment/create-branch/auto-merge). Copilot review is
> not independent APPROVE. Do not self-approve. Do not `gh pr merge`
> stacked leftover PRs onto an unprotected leftover base.
->
> Next buyer increment on this cycle: leftover-map reconstruction `R̂`
> on graphic-display pair segments (ADR 0272 / v2.29.0). Caption each
> closest/farthest segment with persisted leftover-map reconstruction
@@ -1086,7 +1061,6 @@
> `gh`/MCP succeed. Copilot review is not independent APPROVE. Do not
> self-approve. Do not `gh pr merge` stacked leftover PRs onto an
> unprotected leftover base.
->
> Next buyer increment on this cycle: leftover-map distance on
> graphic-display pair segments (ADR 0271 / v2.28.0). Caption each
> closest/farthest segment with persisted leftover-map distance `d` so
@@ -1108,7 +1082,6 @@
> `gh`/MCP succeed (comment/create-branch/auto-merge). `git push` HTTPS
> still fails (empty `X-OAuth-Scopes`). Copilot review is not
> independent APPROVE. Do not self-approve.
->
> Next buyer increment on this cycle: leftover-map coordinate ticks
> (ADR 0270 / v2.27.0). Tick leftover-map axes at the origin and at each
> unique finite persisted `ξ` / `ζ` so pair-row `ξ (x, y) ζ (x, y)`
@@ -1127,7 +1100,6 @@
> closed as a weaker duplicate of #91. GitHub writes through MCP succeed
> (comment/create-branch/git push/auto-merge). Copilot review is not
> independent APPROVE. Do not self-approve.
->
> Next buyer increment on this cycle: leftover-map axis share on the
> graphic display (ADR 0269 / v2.26.0). Caption plot axes with persisted
> ADR 0148 `leftover_map_axes` inertia `σ_k² / Σ_j σ_j²`. UI-only; no
@@ -1143,7 +1115,6 @@
> `e2d13019004a5d8c019fecf7a39ceeef4093b8dd`; Strix fail-closed and no
> independent APPROVE. Drafts remain dirty against `main`. #96 stays
> closed as a weaker duplicate of #91. GitHub writes through MCP succeed.
->
> Next buyer increment on this cycle: leftover-map graphic display
> of already-persisted `ξ_{1:2}` / `ζ_{1:2}` (ADR 0268 / v2.25.0).
> UI-only; no new columns. `R̂` and `d` already are inner product and
@@ -1155,7 +1126,6 @@
> explained leftover share, #775). Open ready PRs still lack independent
> APPROVE. Drafts remain dirty against `main`. #96 stays closed as a
> weaker duplicate of #91. GitHub writes through `gh` succeed.
->
> Next buyer increment on this cycle: leftover-map coordinates
> `ξ_{1:2}` / `ζ_{1:2}` (ADR 0267 / migration 0245 / v2.24.0) so
> `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖` are buyer-auditable. Do not name
@@ -1171,7 +1141,6 @@
> the Grok GitHub App now succeed (comment/close/auto-merge/update-branch)
> despite empty `X-OAuth-Scopes`; git push is the remaining probe this
> cycle. This overlay supersedes every older queue count below.
->
> Next buyer increment on this cycle: leftover-map explained leftover
> share `e = R̂² / R²` (ADR 0266 / migration 0244 / v2.23.0) so
> `e + s + x = 1` is buyer-auditable. Do not persist leftover-map
@@ -1187,7 +1156,6 @@
> `main`. Central ruleset 18156473 and repository no-force-push ruleset
> 21065108 remain active. This overlay supersedes every older queue count below.
> Checks from older heads, stacked bases, or merged PRs are not transferred.
->
> Current-runtime boundary: the official Compose project was healthy at the
> HTTP health route, but its PostgreSQL schema did not yet contain
> `source_post_voice`; therefore no current Voice-history aggregate,
From 2171e12f94035593b7c0624c261f1e5184a6232a Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 8 Sep 2026 07:52:13 +0900
Subject: [PATCH 7/9] test(a11y): RED generic axis-share badge must not carry
aria-label
---
.../leftoverMapCompareAxisAccessibility.test.ts | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
create mode 100644 frontend/src/leftoverMapCompareAxisAccessibility.test.ts
diff --git a/frontend/src/leftoverMapCompareAxisAccessibility.test.ts b/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
new file mode 100644
index 000000000..68fb44eec
--- /dev/null
+++ b/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
@@ -0,0 +1,17 @@
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { describe, expect, it } from "vitest";
+
+const appSource = readFileSync(fileURLToPath(new URL("./App.tsx", import.meta.url)), "utf8");
+
+describe("grouping comparison axis-share accessibility", () => {
+ it("keeps persisted axis-share text visible without naming a generic span", () => {
+ const marker = "{comparisonAxisShare !== null ? (";
+ expect(appSource).toContain(marker);
+ const shareBlock = appSource.split(marker, 2)[1]?.split(") : null}", 1)[0] ?? "";
+
+ expect(shareBlock).toContain('className="post-badge"');
+ expect(shareBlock).toContain("LEFTOVER_MAP_COMPARE_AXIS_SHARE, comparisonAxisShare");
+ expect(shareBlock).not.toContain("aria-label=");
+ });
+});
From 809b9d3ad63db7a3d1a4ecf0b31fc1ff07aa49fd Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 8 Sep 2026 07:58:56 +0900
Subject: [PATCH 8/9] test(a11y): make #828 source RED hermetic under Vitest
---
frontend/src/leftoverMapCompareAxisAccessibility.test.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/leftoverMapCompareAxisAccessibility.test.ts b/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
index 68fb44eec..24aa570ee 100644
--- a/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
+++ b/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
@@ -1,8 +1,11 @@
+///
import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
-const appSource = readFileSync(fileURLToPath(new URL("./App.tsx", import.meta.url)), "utf8");
+const here = dirname(fileURLToPath(import.meta.url));
+const appSource = readFileSync(join(here, "App.tsx"), "utf8");
describe("grouping comparison axis-share accessibility", () => {
it("keeps persisted axis-share text visible without naming a generic span", () => {
From 1472d98b7513c360ca1c7cf6da4a401a716bc53c Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 8 Sep 2026 08:58:41 +0900
Subject: [PATCH 9/9] test(ui): target current axis-share render block
---
frontend/src/leftoverMapCompareAxisAccessibility.test.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/leftoverMapCompareAxisAccessibility.test.ts b/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
index 24aa570ee..b5902b8a5 100644
--- a/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
+++ b/frontend/src/leftoverMapCompareAxisAccessibility.test.ts
@@ -9,12 +9,12 @@ const appSource = readFileSync(join(here, "App.tsx"), "utf8");
describe("grouping comparison axis-share accessibility", () => {
it("keeps persisted axis-share text visible without naming a generic span", () => {
- const marker = "{comparisonAxisShare !== null ? (";
+ const marker = "const comparisonAxisShare = leftoverMapCompareAxisShare(axis);";
expect(appSource).toContain(marker);
- const shareBlock = appSource.split(marker, 2)[1]?.split(") : null}", 1)[0] ?? "";
+ const shareBlock = appSource.split(marker, 2)[1]?.split("{row.leftover_pairs", 1)[0] ?? "";
expect(shareBlock).toContain('className="post-badge"');
expect(shareBlock).toContain("LEFTOVER_MAP_COMPARE_AXIS_SHARE, comparisonAxisShare");
expect(shareBlock).not.toContain("aria-label=");
});
-});
+});
\ No newline at end of file