From a3613acf8cafac056850ec7ebc45f6d77f77a074 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 31 Aug 2026 02:20:15 +0000
Subject: [PATCH 01/21] feat(reports): leftover-map singular values on
leftover-map graphic-display axes (v2.81.0)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Caption leftover-map graphic-display axes with persisted leftover-map singular
values σ_k (ADR 0324). Graphic copy stays leftover-map axis {k} σ {value}
({share}%) so it stays distinct from leftover-axis leftover axis {k} σ {value}
{share}%, from comparison leftover-axis leftover map comparison leftover axis
{k} σ {value} {share}%, and from comparison graphic leftover map comparison
graphic leftover-map axis {k} σ {value} ({share}%). Rank-0 unused axes still
name σ 0.00. Share and singular value omit independently. Do not invent σ_k
from leftover-map axis share. Never invent a leftover score. Never invent a
theta.
---
AGENTS.md | 10 +-
ARCHITECTURE.md | 5 +-
.../2.81.0-leftover-map-plot-axis-singular.md | 18 +++
CHANGELOG.md | 15 ++
CLAUDE.md | 2 +-
docs/adr/0148-leftover-map-axis-share.md | 4 +-
docs/adr/0322-leftover-map-axis-singular.md | 4 +-
...0323-leftover-map-compare-axis-singular.md | 2 +
.../0324-leftover-map-plot-axis-singular.md | 129 ++++++++++++++++++
docs/product-technical-gap-baseline.md | 18 +++
docs/storybook-inventory.md | 4 +-
frontend/package.json | 2 +-
frontend/src/App.test.tsx | 10 +-
.../src/components/LeftoverMapPlot.test.tsx | 34 ++++-
frontend/src/components/LeftoverMapPlot.tsx | 23 ++--
.../src/components/LeftoverPairList.test.tsx | 4 +-
frontend/src/i18n.test.ts | 24 ++++
frontend/src/i18n.ts | 8 ++
.../src/leftoverMapPlotAxisSingular.test.ts | 61 ++++++++-
frontend/src/leftoverMapPlotAxisSingular.ts | 29 +++-
frontend/src/leftoverMapPlotLayout.test.ts | 1 +
frontend/src/leftoverMapPlotLayout.ts | 4 +
pyproject.toml | 2 +-
23 files changed, 379 insertions(+), 34 deletions(-)
create mode 100644 CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md
create mode 100644 docs/adr/0324-leftover-map-plot-axis-singular.md
diff --git a/AGENTS.md b/AGENTS.md
index f7ebeb9d3..dd56245ab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,7 +273,7 @@ stops startup instead of leaving a healthy-looking partial schema, and
application code must not compensate for a missing table.
Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0158 / 0162 /
-0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323) are computed in `lineageweave/leftover_pairs.py` from the
+0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324) are computed in `lineageweave/leftover_pairs.py` from the
residual after a real GRM/GPCM score, never invented. Distances are
Euclidean on the two-dimensional Gabriel leftover map; missing cells stay
out of the factorization. Closest and farthest post–criterion pairs
@@ -656,6 +656,11 @@ leftoverMapCompareAxisBadge returns a usable leftover-axis caption. Distinct acc
name `leftover map comparison leftover axis {axis} σ {value}`. Share and singular value
omit independently. Rank-0 unused axes still name `σ 0.00`. Do not invent leftover-map
singular values from leftover-map axis share.
+ADR 0324 captions leftover-map graphic-display axes with persisted leftover-map singular
+values `σ_k` when leftoverMapPlotAxisBadge returns a usable leftover-map axis caption.
+Distinct accessible name `leftover-map axis {axis} σ {value} ({share}%)`. Share and
+singular value omit independently. Rank-0 unused axes still name leftover-map axis
+`σ 0.00`. Do not invent leftover-map singular values from leftover-map axis share.
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
@@ -760,6 +765,9 @@ returns a usable finite non-negative value.
Grouping comparison leftover-axis report badge leftover-map singular values (ADR 0323) captions
leftover-axis report badges on the grouping comparison strip with persisted leftover-map
singular values `σ_k` when leftoverMapCompareAxisBadge returns a usable leftover-axis caption.
+Leftover-map graphic-display leftover-map singular values (ADR 0324) captions leftover-map
+graphic-display axes with persisted leftover-map singular values `σ_k` when leftoverMapPlotAxisBadge
+returns a usable leftover-map axis caption.
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 8321c5d25..84746aa4a 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -754,6 +754,8 @@ ADR 0322 captions leftover-axis report badges with persisted leftover-map
singular values `σ_k`.
ADR 0323 captions leftover-axis report badges on the grouping comparison strip
with persisted leftover-map singular values `σ_k`.
+ADR 0324 captions leftover-map graphic-display axes with persisted leftover-map
+singular values `σ_k`.
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
@@ -806,7 +808,8 @@ graphic (ADR 0319), leftover-map coordinate ticks on the grouping comparison lef
graphic (ADR 0320), leftover-map singular values on the grouping comparison leftover-map
graphic (ADR 0321), leftover-map singular values on leftover-axis report badges
(ADR 0322), leftover-map singular values on leftover-axis report badges on the
-grouping comparison strip (ADR 0323);
+grouping comparison strip (ADR 0323), leftover-map singular values on leftover-map
+graphic-display axes (ADR 0324);
`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.81.0-leftover-map-plot-axis-singular.md b/CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md
new file mode 100644
index 000000000..a15f4858e
--- /dev/null
+++ b/CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md
@@ -0,0 +1,18 @@
+## 2.81.0 — Leftover-map singular values on leftover-map graphic-display axes
+
+- Caption leftover-map graphic-display axes with persisted leftover-map singular
+ values `σ_k` (ADR 0324). After `make seed`, leftover-map graphic-display axes
+ name persisted leftover-map singular values when finite together with persisted
+ leftover-map axis share; click a leftover pair opens that post. A missing,
+ non-finite, or negative singular value omits that `σ` caption and keeps
+ `leftover-map axis {k} ({share}%)`. Rank-0 unused axes still name `σ 0.00`.
+ Leftover-map graphic-display axes stay `leftover-map axis {k} σ {value} ({share}%)`
+ so they stay distinct from leftover-axis `leftover axis {k} σ {value} {share}%`,
+ from comparison leftover-axis `leftover map comparison leftover axis {k} σ {value} {share}%`,
+ and from comparison graphic
+ `leftover map comparison graphic leftover-map axis {k} σ {value} ({share}%)`.
+ Do not invent `σ_k` from leftover-map axis share. This increment does not
+ change leftover-map singular values on leftover-axis report badges or on
+ leftover-axis report badges on the grouping comparison strip. A finite negative
+ leftover is shown, never clamped. Never invent a leftover score. Never invent a
+ theta. No new columns.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a0d1cec3..f0ab37f05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,21 @@ All notable changes to this project are documented here. Format follows
### Added
+- Period leftover pairs now caption leftover-map graphic-display axes with persisted
+ leftover-map singular values `σ_k` (ADR 0324 / v2.81.0). After `make seed`, leftover-map
+ graphic-display axes name persisted leftover-map singular values when finite together
+ with persisted leftover-map axis share; click a leftover pair opens that post. A
+ missing, non-finite, or negative singular value omits that `σ` caption and keeps
+ `leftover-map axis {k} ({share}%)`. Rank-0 unused axes still name `σ 0.00`. Leftover-map
+ graphic-display axes stay `leftover-map axis {k} σ {value} ({share}%)` so they stay
+ distinct from leftover-axis `leftover axis {k} σ {value} {share}%`, from comparison
+ leftover-axis `leftover map comparison leftover axis {k} σ {value} {share}%`, and from
+ comparison graphic `leftover map comparison graphic leftover-map axis {k} σ {value} ({share}%)`.
+ Do not invent `σ_k` from leftover-map axis share. This increment does not change
+ leftover-map singular values on leftover-axis report badges or on leftover-axis
+ report badges on the grouping comparison strip. A finite negative leftover is shown,
+ never clamped. Never invent a leftover score or a theta. No new columns.
+
- Period leftover pairs now caption leftover-axis report badges on the grouping
comparison strip with persisted leftover-map singular values `σ_k`
(ADR 0323 / v2.80.0). After `make seed`, grouping comparison leftover-axis
diff --git a/CLAUDE.md b/CLAUDE.md
index 4546003e0..f29361dc8 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 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323), occupational construct catalog search
+(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324), 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/docs/adr/0148-leftover-map-axis-share.md b/docs/adr/0148-leftover-map-axis-share.md
index 74728c994..7b5f55843 100644
--- a/docs/adr/0148-leftover-map-axis-share.md
+++ b/docs/adr/0148-leftover-map-axis-share.md
@@ -7,7 +7,9 @@
[ADR 0322](0322-leftover-map-axis-singular.md)
(leftover-map singular values on leftover-axis report badges);
[ADR 0323](0323-leftover-map-compare-axis-singular.md)
-(leftover-map singular values on leftover-axis report badges on the grouping comparison strip)
+(leftover-map singular values on leftover-axis report badges on the grouping comparison strip);
+[ADR 0324](0324-leftover-map-plot-axis-singular.md)
+(leftover-map singular values on leftover-map graphic-display axes)
## Context
diff --git a/docs/adr/0322-leftover-map-axis-singular.md b/docs/adr/0322-leftover-map-axis-singular.md
index bf1183f72..e098a7567 100644
--- a/docs/adr/0322-leftover-map-axis-singular.md
+++ b/docs/adr/0322-leftover-map-axis-singular.md
@@ -83,7 +83,9 @@ Independent of leftover-map singular values on the grouping comparison
leftover-map graphic. This increment does not caption leftover-map singular
values on leftover-axis badges on the grouping comparison strip with a
distinct name
-([ADR 0323](0323-leftover-map-compare-axis-singular.md)).
+([ADR 0323](0323-leftover-map-compare-axis-singular.md)). This increment does
+not caption leftover-map singular values on leftover-map graphic-display axes
+([ADR 0324](0324-leftover-map-plot-axis-singular.md)).
## References
diff --git a/docs/adr/0323-leftover-map-compare-axis-singular.md b/docs/adr/0323-leftover-map-compare-axis-singular.md
index 6d294a869..8c2c28d56 100644
--- a/docs/adr/0323-leftover-map-compare-axis-singular.md
+++ b/docs/adr/0323-leftover-map-compare-axis-singular.md
@@ -103,6 +103,8 @@ leftover-map axis share are finite, leftover-map axis share is
Independent of leftover-map singular values on leftover-axis report badges
in the period-report panel ([ADR 0322](0322-leftover-map-axis-singular.md)).
+Independent of leftover-map singular values on leftover-map graphic-display
+axes ([ADR 0324](0324-leftover-map-plot-axis-singular.md)).
## References
diff --git a/docs/adr/0324-leftover-map-plot-axis-singular.md b/docs/adr/0324-leftover-map-plot-axis-singular.md
new file mode 100644
index 000000000..02616597e
--- /dev/null
+++ b/docs/adr/0324-leftover-map-plot-axis-singular.md
@@ -0,0 +1,129 @@
+# ADR 0324 — Name leftover-map singular values on leftover-map graphic-display axes
+
+**Decision status:** Accepted
+**Date:** 2026-08-31
+
+Amends leftover-map axis share on the graphic display
+([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover-map axis share
+persistence ([ADR 0148](0148-leftover-map-axis-share.md)). Independent of leftover-map
+singular values on leftover-axis report badges
+([ADR 0322](0322-leftover-map-axis-singular.md)), leftover-map singular
+values on leftover-axis report badges on the grouping comparison strip
+([ADR 0323](0323-leftover-map-compare-axis-singular.md)), leftover-map
+singular values on the grouping comparison leftover-map graphic
+([ADR 0321](0321-leftover-map-compare-plot-singular.md)), leftover-map
+coordinate ticks on that comparison graphic
+([ADR 0320](0320-leftover-map-compare-plot-ticks.md)), leftover-map
+axis share on that comparison graphic
+([ADR 0305](0305-leftover-map-compare-plot-axis-share.md)), leftover-map
+graphic display on the grouping comparison strip
+([ADR 0304](0304-leftover-map-compare-graphic.md)), leftover-map
+coordinates ([ADR 0267](0267-leftover-map-coordinates.md)), leftover-map
+graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)), and
+leftover-map coordinate ticks on the graphic display
+([ADR 0270](0270-leftover-map-coordinate-ticks.md)).
+
+## Context
+
+ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map
+axis share `σ_k² / Σ_j σ_j²`. ADR 0269 captions leftover-map graphic-display
+axes with share only as `leftover-map axis {k} ({share}%)`. ADR 0321 already
+captions the grouping comparison leftover-map graphic with persisted `σ_k`.
+ADR 0322 already captions leftover-axis report badges with persisted `σ_k`.
+ADR 0323 already captions grouping comparison leftover-axis report badges
+with persisted `σ_k`. Period leftover-map graphic axes still hide `σ_k`, so
+Gabriel (1971) singular values appear on leftover-axis badges and on the
+comparison graphic and never on the leftover-map graphic-display axes that
+name the same axes. Hiding `σ_k` on leftover-map graphic-display axes lets
+leftover-map axis share be read as leftover-map structure even when the buyer
+cannot tell whether axis 1 is large because `σ_1` is large or because axis 2
+collapsed. A missing, non-finite, or negative singular value is not a leftover
+score and must omit independently of leftover-map axis share. Rank-0 unused
+axes still persist `σ_k = 0`. A finite negative leftover is shown, never
+clamped. Do not invent `σ_k` from leftover-map axis share.
+
+This increment captions leftover-map graphic-display axes with already persisted
+leftover-map singular values. Leftover-map graphic-display axes stay
+`leftover-map axis {k} σ {value} ({share}%)` so they stay distinct from
+leftover-axis badges `leftover axis {k} σ {value} {share}%`, from comparison
+leftover-axis `leftover map comparison leftover axis {k} σ {value} {share}%`,
+and from comparison graphic
+`leftover map comparison graphic leftover-map axis {k} σ {value} ({share}%)`.
+It does not add columns. Do not invent a leftover score. Do not invent a
+theta.
+
+This protected increment uses **0324** so it does not collide with leftover-map
+singular values on leftover-axis report badges on the grouping comparison
+strip (0323), leftover-map singular values on leftover-axis report badges
+(0322), leftover-map singular values on the grouping comparison leftover-map
+graphic (0321), leftover-map coordinate ticks on that graphic (0320), leftover-map
+axis share on that graphic (0305), leftover-map graphic display on the grouping
+comparison strip (0304), leftover-map axis share persistence (0148), leftover-map
+axis share on the graphic display (0269), or the dashboard stacks.
+
+## Decision
+
+On leftover-map graphic-display axes, caption leftover-map axis `k` with
+persisted leftover-map singular value `σ_k` when that singular value is
+finite and non-negative, including rank-0 zero singular values. Share and
+singular value omit independently: no share and no `σ_k` keeps
+`leftover-map axis {k}`; share only is `leftover-map axis {k} ({share}%)`;
+`σ_k` only is `leftover-map axis {k} σ {value}`; both is
+`leftover-map axis {k} σ {value} ({share}%)`. A missing, non-finite, or
+negative singular value omits that `σ` caption and keeps the existing
+`leftover-map axis {k} ({share}%)` text. Do not invent `σ_k` from leftover-map
+axis share. Axis 1 and axis 2 stay independently named. Click a leftover pair
+to open that post.
+
+Leftover-map graphic-display singular values omit independently of leftover-map
+comparison graphic leftover-map axis singular values and leftover-axis report
+badges. A missing singular value omits that leftover-map graphic-display `σ`
+caption and keeps a usable leftover-map axis share caption, leftover-map
+comparison graphic leftover-map axis `σ` caption, leftover-axis report badge
+`σ` caption, leftover-map comparison leftover-axis `σ` caption, leftover-map
+graphic leftover-map axis ticks, leftover-map distance caption, leftover-map
+rank caption, leftover expected `E`, leftover observed `Y`, leftover residual
+`R`, leftover-map unexplained leftover `U`, leftover-map reconstruction `R̂`,
+and leftover-map graphic coverage notes.
+
+This increment does not change leftover-map singular values on leftover-axis
+report badges. Those leftover-axis badges stay
+`leftover axis {axis} σ {value} {share}%` (ADR 0322). This increment does
+not change leftover-map singular values on leftover-axis report badges on the
+grouping comparison strip (ADR 0323). This increment does not persist leftover-map
+inner product, cosine, or length.
+
+Do not add SQL. Do not edit shipped migrations. Do not invent a leftover
+score. Do not invent a theta.
+
+## Consequences
+
+After `make seed`, leftover-map graphic-display axes name persisted leftover-map
+singular values when finite together with persisted leftover-map axis share;
+click a leftover pair opens that post. Hidden posts stay hidden. Rank-0 unused
+axes still name leftover-map axis `σ 0.00`. Grouping comparison leftover-map
+graphic leftover-map singular values (ADR 0321), leftover-axis report badge
+leftover-map singular values (ADR 0322), and grouping comparison leftover-axis
+report badge leftover-map singular values (ADR 0323) remain.
+
+## Related
+
+Independent of leftover-map singular values on leftover-axis report badges
+([ADR 0322](0322-leftover-map-axis-singular.md)). Independent of leftover-map
+singular values on leftover-axis report badges on the grouping comparison
+strip ([ADR 0323](0323-leftover-map-compare-axis-singular.md)). Independent of
+leftover-map singular values on the grouping comparison leftover-map graphic
+([ADR 0321](0321-leftover-map-compare-plot-singular.md)).
+
+## 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.)
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 19515f547..3c8aa6d5d 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,23 @@
# Product & Technical Gap Baseline
+> Exact-head loop overlay: 2026-08-31 11:05 KST. Protected `main` is
+> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not
+> self-approve. #863 leftover-map singular values on leftover-axis report badges
+> on the grouping comparison strip (v2.80.0 / ADR 0323) is `52b3597`, stacked on
+> #862 @ `f24b664`. Independent APPROVE is missing. Next buyer increment on this
+> cycle: leftover-map singular values on leftover-map graphic-display axes
+> (ADR 0324 / v2.81.0) delivered locally on
+> `feat/leftover-map-plot-axis-singular-v2810`. Caption persisted leftover-map
+> singular values `σ_k` on leftover-map graphic-display axes from already-named
+> leftover-map axes. Distinct accessible name `leftover-map axis {axis} σ {value} ({share}%)`.
+> Rank-0 unused axes still name `σ 0.00`. Share and singular value omit
+> independently. Do not invent `σ_k` from leftover-map axis share. Do not change
+> leftover-map singular values on leftover-axis report badges or on leftover-axis
+> report badges on the grouping comparison strip this increment. Stack onto #863.
+> Leave open for independent review. Do not squash-merge. Do not self-approve.
+> Issues #79 and #87 stay open. Do not merge #808 without independent APPROVE.
+> Never self-approve.
+
> Exact-head loop overlay: 2026-08-31 10:40 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not
> self-approve. #862 leftover-map singular values on leftover-axis report badges
diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md
index 6811f24df..d4c5f9fae 100644
--- a/docs/storybook-inventory.md
+++ b/docs/storybook-inventory.md
@@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS.
| Story | Operator next action | Token / module |
|---|---|---|
-| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The comparison variant names leftover-map complete-case coverage under `Leftover map comparison graphic coverage` and leftover-map item complete-case coverage under `Leftover map comparison graphic item coverage` and leftover-map incomplete post coverage under `Leftover map comparison graphic incomplete posts` and leftover-map incomplete item coverage under `Leftover map comparison graphic incomplete items`. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, `Comparison`, `ComparisonMissingCoverage`, `ComparisonMissingItemCoverage`, `ComparisonMissingIncompletePosts`, and `ComparisonMissingIncompleteItems` cover two-pair maps, rank-0 origin with 0% share, a `0` tick, `d 0.00`, `R̂ 0.00`, `R̂²/R² 0.00`, `U²/R² 0.00`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, missing share that keeps existing leftover-map axis text, grouping comparison leftover-map graphic coverage that matches leftover map used N of M scored posts, omitted comparison graphic coverage when leftoverMapCoverageCounts fail-closes, grouping comparison leftover-map graphic item coverage that matches leftover map used N of M scored criteria, omitted comparison graphic item coverage when leftoverMapItemCoverageCounts fail-closes, grouping comparison leftover-map graphic incomplete posts that matches leftover map dropped N incomplete posts, omitted comparison graphic incomplete posts when leftoverMapIncompletePostCount fail-closes, grouping comparison leftover-map graphic incomplete items that matches leftover map dropped N incomplete criteria, and omitted comparison graphic incomplete items when leftoverMapIncompleteItemCount fail-closes. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `--color-primary`, `--color-palette-blue-mid` |
-| `Reports/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-axis report badges with persisted leftover-map singular values under `leftover map comparison leftover axis {axis} σ {value}` with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisBadge return usable values. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` |
+| `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 singular values `σ_k` and leftover-map axis share when finite. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The comparison variant names leftover-map complete-case coverage under `Leftover map comparison graphic coverage` and leftover-map item complete-case coverage under `Leftover map comparison graphic item coverage` and leftover-map incomplete post coverage under `Leftover map comparison graphic incomplete posts` and leftover-map incomplete item coverage under `Leftover map comparison graphic incomplete items`. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, `Comparison`, `ComparisonMissingCoverage`, `ComparisonMissingItemCoverage`, `ComparisonMissingIncompletePosts`, and `ComparisonMissingIncompleteItems` cover two-pair maps, rank-0 origin with 0% share, a `0` tick, `d 0.00`, `R̂ 0.00`, `R̂²/R² 0.00`, `U²/R² 0.00`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, missing share that keeps leftover-map axis `σ` without share, grouping comparison leftover-map graphic coverage that matches leftover map used N of M scored posts, omitted comparison graphic coverage when leftoverMapCoverageCounts fail-closes, grouping comparison leftover-map graphic item coverage that matches leftover map used N of M scored criteria, omitted comparison graphic item coverage when leftoverMapItemCoverageCounts fail-closes, grouping comparison leftover-map graphic incomplete posts that matches leftover map dropped N incomplete posts, omitted comparison graphic incomplete posts when leftoverMapIncompletePostCount fail-closes, grouping comparison leftover-map graphic incomplete items that matches leftover map dropped N incomplete criteria, and omitted comparison graphic incomplete items when leftoverMapIncompleteItemCount fail-closes. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `--color-primary`, `--color-palette-blue-mid` |
+| `Reports/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 singular values and 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-axis report badges with persisted leftover-map singular values under `leftover map comparison leftover axis {axis} σ {value}` with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisBadge 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 9b99eca3c..9750a7d2e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.80.0",
+ "version": "2.81.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 085626a09..bbc95eac7 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -4200,8 +4200,8 @@ describe("App, authenticated", () => {
expect(await screen.findByLabelText("Leftover pairs")).toBeInTheDocument();
expect(screen.getByLabelText("Leftover-map graphic display")).toBeInTheDocument();
expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument();
- expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument();
- expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument();
expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument();
expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument();
expect(
@@ -4335,6 +4335,12 @@ describe("App, authenticated", () => {
"leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)",
),
).toBeInTheDocument();
+ expect(
+ within(screen.getByLabelText("Grouping comparison")).queryByText("leftover-map axis 1 σ 1.84 (82%)"),
+ ).not.toBeInTheDocument();
+ expect(
+ within(screen.getByLabelText("Grouping comparison")).queryByText("leftover-map axis 2 σ 0.86 (18%)"),
+ ).not.toBeInTheDocument();
expect(
within(screen.getByLabelText("Grouping comparison")).queryByText("leftover-map axis 1 (82%)"),
).not.toBeInTheDocument();
diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx
index ed9b89639..5adcea2ac 100644
--- a/frontend/src/components/LeftoverMapPlot.test.tsx
+++ b/frontend/src/components/LeftoverMapPlot.test.tsx
@@ -164,8 +164,8 @@ describe("LeftoverMapPlot", () => {
expect(
screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (0.00, 0.00)" }),
).toBeInTheDocument();
- expect(screen.getByText("leftover-map axis 1 (0%)")).toBeInTheDocument();
- expect(screen.getByText("leftover-map axis 2 (0%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 σ 0.00 (0%)")).toBeInTheDocument();
expect(screen.getAllByLabelText("leftover-map axis 1 tick 0.00").length).toBeGreaterThan(0);
expect(screen.queryByLabelText("leftover-map axis 1 tick +1.00")).not.toBeInTheDocument();
expect(screen.getByLabelText("leftover-map distance d 0.00")).toBeInTheDocument();
@@ -180,7 +180,7 @@ describe("LeftoverMapPlot", () => {
expect(screen.getByLabelText("leftover-map rank rank 0")).toBeInTheDocument();
});
- it("captions leftover-map axes with persisted leftover-map axis share", () => {
+ it("captions leftover-map axes with persisted leftover-map singular values and leftover-map axis share", () => {
render(
{
onSelectPost={vi.fn()}
/>,
);
- expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument();
- expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument();
});
- it("keeps existing leftover-map axis text when share is missing or non-finite", () => {
+ it("captions leftover-map graphic-display singular values when share is missing or non-finite", () => {
render(
{
onSelectPost={vi.fn()}
/>,
);
- expect(screen.getByText("leftover-map axis 1")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 1 σ 1.84")).toBeInTheDocument();
expect(screen.getByText("leftover-map axis 2")).toBeInTheDocument();
expect(screen.queryByText(/leftover-map axis 1 \(/)).not.toBeInTheDocument();
expect(screen.queryByText(/leftover-map axis 2 \(/)).not.toBeInTheDocument();
});
+ it("omits leftover-map graphic-display singular values independently of leftover-map axis share", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument();
+ expect(screen.queryByText("leftover-map axis 1 σ 1.84 (82%)")).not.toBeInTheDocument();
+ expect(screen.queryByText("leftover-map axis 2 σ 0.86 (18%)")).not.toBeInTheDocument();
+ });
+
it("omits leftover-map distance on a pair segment when d is missing", () => {
render(
{
).toBeInTheDocument();
expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument();
expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument();
+ expect(screen.queryByText("leftover-map axis 1 σ 1.84 (82%)")).not.toBeInTheDocument();
+ expect(screen.queryByText("leftover-map axis 2 σ 0.86 (18%)")).not.toBeInTheDocument();
expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument();
expect(screen.queryByText("leftover-map axis 2 (18%)")).not.toBeInTheDocument();
});
diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx
index a888a56e1..cad1f961d 100644
--- a/frontend/src/components/LeftoverMapPlot.tsx
+++ b/frontend/src/components/LeftoverMapPlot.tsx
@@ -25,10 +25,10 @@ import {
LEFTOVER_MAP_COMPARE_PLOT_AXIS_1,
LEFTOVER_MAP_COMPARE_PLOT_AXIS_2,
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE,
- LEFTOVER_MAP_PLOT_AXIS_SHARE,
} from "../leftoverMapPlotAxisShare";
import {
formatLeftoverMapPlotAxisSingular,
+ leftoverMapPlotAxisBadge,
leftoverSingularForAxis,
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR,
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE,
@@ -86,13 +86,11 @@ function leftoverMapPlotAxisText(
leftoverMapAxes: LeftoverMapAxis[] | undefined,
variant: LeftoverMapPlotVariant,
): string {
- const percent = formatLeftoverMapPlotAxisShare(
- leftoverShareForAxis(leftoverMapAxes, axisIndex),
- );
+ const leftoverShare = leftoverShareForAxis(leftoverMapAxes, axisIndex);
+ const leftoverSingular = leftoverSingularForAxis(leftoverMapAxes, axisIndex);
if (variant === "comparison") {
- const singular = formatLeftoverMapPlotAxisSingular(
- leftoverSingularForAxis(leftoverMapAxes, axisIndex),
- );
+ const percent = formatLeftoverMapPlotAxisShare(leftoverShare);
+ const singular = formatLeftoverMapPlotAxisSingular(leftoverSingular);
if (singular === null && percent === null) {
return t(axisIndex === 1 ? LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 : LEFTOVER_MAP_COMPARE_PLOT_AXIS_2);
}
@@ -108,10 +106,11 @@ function leftoverMapPlotAxisText(
share: percent as string,
});
}
- if (percent === null) {
+ const badge = leftoverMapPlotAxisBadge(axisIndex, leftoverSingular, leftoverShare);
+ if (badge === null) {
return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2");
}
- return tf(LEFTOVER_MAP_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent });
+ return tf(badge.key, badge.values);
}
/**
@@ -119,7 +118,8 @@ function leftoverMapPlotAxisText(
*
* Person markers are posts; item markers are leftover criteria. Click a
* post marker to open that post. Caption leftover-map axes with persisted
- * Gabriel inertia share when finite, including rank-0 zero-share axes.
+ * leftover-map singular values ``σ_k`` and Gabriel inertia share when finite,
+ * including rank-0 zero-share axes.
* Axis ticks name persisted leftover-map coordinates so ξ / ζ on the
* pair row match the plot. Pair segments name persisted leftover-map
* distance ``d``, leftover-map reconstruction ``R̂``, leftover-map
@@ -208,7 +208,8 @@ function leftoverMapPlotAxisText(
* labels. ADR 0322 captions leftover-axis report badges with persisted
* leftover-map singular values, not this graphic. ADR 0323 captions leftover-axis
* report badges on the grouping comparison strip with persisted leftover-map
- * singular values, not this graphic.
+ * singular values, not this graphic. ADR 0324 captions leftover-map
+ * graphic-display axes with persisted leftover-map singular values.
* Never invent a leftover score.
*/
export function LeftoverMapPlot({
diff --git a/frontend/src/components/LeftoverPairList.test.tsx b/frontend/src/components/LeftoverPairList.test.tsx
index 230f10de2..6972dd29b 100644
--- a/frontend/src/components/LeftoverPairList.test.tsx
+++ b/frontend/src/components/LeftoverPairList.test.tsx
@@ -204,8 +204,8 @@ describe("LeftoverPairList", () => {
name: "Open leftover-map post Public post at ξ (+0.50, +0.10)",
}),
);
- expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument();
- expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument();
expect(screen.getByLabelText("Leftover-map graphic coverage")).toHaveTextContent(
"Leftover map used 2 of 3 scored posts (complete-case)",
);
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts
index ecfc52183..c67c890cd 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -66,6 +66,8 @@ describe("i18n", () => {
"leftover-map axis 1",
"leftover-map axis 2",
"leftover-map axis {axis} ({share}%)",
+ "leftover-map axis {axis} σ {value}",
+ "leftover-map axis {axis} σ {value} ({share}%)",
"leftover-map axis {axis} tick {value}",
"leftover-map distance {label}",
"leftover-map reconstruction {label}",
@@ -1111,6 +1113,28 @@ describe("i18n", () => {
expect(tf("leftover-map axis {axis} ({share}%)", { axis: 1, share: "82" })).toBe(expected);
});
+ it.each([
+ ["ko", "잔여 지도 축 1 σ 1.84"],
+ ["zh", "残差图轴 1 σ 1.84"],
+ ["ja", "残差マップ軸 1 σ 1.84"],
+ ["vi", "trục bản đồ phần dư 1 σ 1.84"],
+ ] as const)("formats leftover-map plot axis singular values in %s", (locale, expected) => {
+ setLocale(locale);
+ expect(tf("leftover-map axis {axis} σ {value}", { axis: 1, value: "1.84" })).toBe(expected);
+ });
+
+ it.each([
+ ["ko", "잔여 지도 축 1 σ 1.84 (82%)"],
+ ["zh", "残差图轴 1 σ 1.84 (82%)"],
+ ["ja", "残差マップ軸 1 σ 1.84 (82%)"],
+ ["vi", "trục bản đồ phần dư 1 σ 1.84 (82%)"],
+ ] as const)("formats leftover-map plot axis singular share in %s", (locale, expected) => {
+ setLocale(locale);
+ expect(
+ tf("leftover-map axis {axis} σ {value} ({share}%)", { axis: 1, value: "1.84", share: "82" }),
+ ).toBe(expected);
+ });
+
it.each([
["ko", "IRT 주효과 이후 관측 Y 2.40와 기대 E 2.00를 읽은 다음, 이 글을 여세요."],
["zh", "阅读 IRT 主效应后的观测 Y 2.40 与期望 E 2.00,然后打开这篇帖子。"],
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index 52cf6a35e..ec4b31d06 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -647,6 +647,8 @@ const TRANSLATIONS: Partial>> = {
"leftover-map axis 1": "잔여 지도 축 1",
"leftover-map axis 2": "잔여 지도 축 2",
"leftover-map axis {axis} ({share}%)": "잔여 지도 축 {axis} ({share}%)",
+ "leftover-map axis {axis} σ {value}": "잔여 지도 축 {axis} σ {value}",
+ "leftover-map axis {axis} σ {value} ({share}%)": "잔여 지도 축 {axis} σ {value} ({share}%)",
"leftover-map axis {axis} tick {value}": "잔여 지도 축 {axis} 눈금 {value}",
"leftover-map distance {label}": "잔여 지도 거리 {label}",
"leftover-map reconstruction {label}": "잔여 지도 재구성 {label}",
@@ -1297,6 +1299,8 @@ const TRANSLATIONS: Partial>> = {
"leftover-map axis 1": "残差图轴 1",
"leftover-map axis 2": "残差图轴 2",
"leftover-map axis {axis} ({share}%)": "残差图轴 {axis} ({share}%)",
+ "leftover-map axis {axis} σ {value}": "残差图轴 {axis} σ {value}",
+ "leftover-map axis {axis} σ {value} ({share}%)": "残差图轴 {axis} σ {value} ({share}%)",
"leftover-map axis {axis} tick {value}": "残差图轴 {axis} 刻度 {value}",
"leftover-map distance {label}": "残差图距离 {label}",
"leftover-map reconstruction {label}": "残差图重建 {label}",
@@ -1951,6 +1955,8 @@ const TRANSLATIONS: Partial>> = {
"leftover-map axis 1": "残差マップ軸 1",
"leftover-map axis 2": "残差マップ軸 2",
"leftover-map axis {axis} ({share}%)": "残差マップ軸 {axis} ({share}%)",
+ "leftover-map axis {axis} σ {value}": "残差マップ軸 {axis} σ {value}",
+ "leftover-map axis {axis} σ {value} ({share}%)": "残差マップ軸 {axis} σ {value} ({share}%)",
"leftover-map axis {axis} tick {value}": "残差マップ軸 {axis} 目盛 {value}",
"leftover-map distance {label}": "残差マップ距離 {label}",
"leftover-map reconstruction {label}": "残差マップ再構成 {label}",
@@ -2608,6 +2614,8 @@ const TRANSLATIONS: Partial>> = {
"leftover-map axis 1": "trục bản đồ phần dư 1",
"leftover-map axis 2": "trục bản đồ phần dư 2",
"leftover-map axis {axis} ({share}%)": "trục bản đồ phần dư {axis} ({share}%)",
+ "leftover-map axis {axis} σ {value}": "trục bản đồ phần dư {axis} σ {value}",
+ "leftover-map axis {axis} σ {value} ({share}%)": "trục bản đồ phần dư {axis} σ {value} ({share}%)",
"leftover-map axis {axis} tick {value}": "vạch trục bản đồ phần dư {axis} {value}",
"leftover-map distance {label}": "khoảng cách bản đồ phần dư {label}",
"leftover-map reconstruction {label}": "tái dựng bản đồ phần dư {label}",
diff --git a/frontend/src/leftoverMapPlotAxisSingular.test.ts b/frontend/src/leftoverMapPlotAxisSingular.test.ts
index bd36ce16a..4e47cfb21 100644
--- a/frontend/src/leftoverMapPlotAxisSingular.test.ts
+++ b/frontend/src/leftoverMapPlotAxisSingular.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
formatLeftoverMapPlotAxisSingular,
leftoverMapCompareAxisBadge,
+ leftoverMapPlotAxisBadge,
leftoverSingularForAxis,
LEFTOVER_MAP_COMPARE_AXIS_CAPTION,
LEFTOVER_MAP_COMPARE_AXIS_LABEL,
@@ -13,7 +14,10 @@ import {
LEFTOVER_MAP_PLOT_AXIS_SINGULAR,
LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
} from "./leftoverMapPlotAxisSingular";
-import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE } from "./leftoverMapPlotAxisShare";
+import {
+ LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE,
+ LEFTOVER_MAP_PLOT_AXIS_SHARE,
+} from "./leftoverMapPlotAxisShare";
import { LEFTOVER_MAP_COMPARE_PLOT_TICK } from "./leftoverMapPlotLayout";
import { LEFTOVER_MAP_AXIS_BADGE_SINGULAR } from "./leftoverMapAxisBadge";
@@ -168,3 +172,58 @@ describe("leftover map comparison leftover-axis report badges", () => {
expect(LEFTOVER_MAP_COMPARE_AXIS_CAPTION).toContain("Open a leftover pair");
});
});
+
+describe("leftover-map graphic-display axis singular badges", () => {
+ it("names persisted leftover-map singular values on leftover-map graphic-display axes", () => {
+ expect(leftoverMapPlotAxisBadge(1, 1.84, 0.82)).toEqual({
+ key: LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
+ values: { axis: 1, value: "1.84", share: "82" },
+ });
+ expect(leftoverMapPlotAxisBadge(2, 0.86, 0.18)).toEqual({
+ key: LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
+ values: { axis: 2, value: "0.86", share: "18" },
+ });
+ });
+
+ it("names rank-0 zero leftover-map singular values on leftover-map graphic-display axes", () => {
+ expect(leftoverMapPlotAxisBadge(1, 0, 0)).toEqual({
+ key: LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
+ values: { axis: 1, value: "0.00", share: "0" },
+ });
+ });
+
+ it("omits leftover-map singular values independently of leftover-map axis share", () => {
+ expect(leftoverMapPlotAxisBadge(1, Number.NaN, 0.82)).toEqual({
+ key: LEFTOVER_MAP_PLOT_AXIS_SHARE,
+ values: { axis: 1, share: "82" },
+ });
+ expect(leftoverMapPlotAxisBadge(2, -0.01, 0.18)).toEqual({
+ key: LEFTOVER_MAP_PLOT_AXIS_SHARE,
+ values: { axis: 2, share: "18" },
+ });
+ expect(leftoverMapPlotAxisBadge(1, 1.84, Number.NaN)).toEqual({
+ key: LEFTOVER_MAP_PLOT_AXIS_SINGULAR,
+ values: { axis: 1, value: "1.84" },
+ });
+ expect(leftoverMapPlotAxisBadge(1, null, null)).toBeNull();
+ });
+
+ it("does not invent leftover-map singular values from leftover-map axis share", () => {
+ expect(leftoverMapPlotAxisBadge(1, undefined, 0.82)?.key).toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE);
+ expect(leftoverMapPlotAxisBadge(1, undefined, 0.82)?.values.value).toBeUndefined();
+ });
+
+ it("stays distinct from leftover-axis, comparison leftover-axis, and comparison graphic copy", () => {
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR).toBe("leftover-map axis {axis} σ {value}");
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE).toBe(
+ "leftover-map axis {axis} σ {value} ({share}%)",
+ );
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR);
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE).not.toBe(
+ LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE,
+ );
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR);
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE).not.toBe(LEFTOVER_MAP_AXIS_BADGE_SINGULAR);
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE);
+ });
+});
diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts
index 70130898e..8d0ab36d9 100644
--- a/frontend/src/leftoverMapPlotAxisSingular.ts
+++ b/frontend/src/leftoverMapPlotAxisSingular.ts
@@ -1,7 +1,10 @@
-/** Caption leftover-map plot axes with persisted Gabriel singular values. */
+/** Caption leftover-map plot axes with persisted Gabriel singular values (ADR 0324). */
import type { LeftoverMapAxis } from "./api";
-import { formatLeftoverMapPlotAxisShare } from "./leftoverMapPlotAxisShare";
+import {
+ formatLeftoverMapPlotAxisShare,
+ LEFTOVER_MAP_PLOT_AXIS_SHARE,
+} from "./leftoverMapPlotAxisShare";
export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR = "leftover-map axis {axis} σ {value}";
@@ -63,6 +66,28 @@ export function formatLeftoverMapPlotAxisSingular(
return leftoverSingular.toFixed(2);
}
+export function leftoverMapPlotAxisBadge(
+ axisIndex: number,
+ leftoverSingular: number | null | undefined,
+ leftoverShare: number | null | undefined,
+): LeftoverMapCompareAxisBadge | null {
+ const singular = formatLeftoverMapPlotAxisSingular(leftoverSingular);
+ const percent = formatLeftoverMapPlotAxisShare(leftoverShare);
+ if (singular === null && percent === null) {
+ return null;
+ }
+ if (singular === null && percent !== null) {
+ return { key: LEFTOVER_MAP_PLOT_AXIS_SHARE, values: { axis: axisIndex, share: percent } };
+ }
+ if (singular !== null && percent === null) {
+ return { key: LEFTOVER_MAP_PLOT_AXIS_SINGULAR, values: { axis: axisIndex, value: singular } };
+ }
+ return {
+ key: LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
+ values: { axis: axisIndex, value: singular as string, share: percent as string },
+ };
+}
+
export function leftoverMapCompareAxisBadge(
axisIndex: number,
leftoverSingular: number | null | undefined,
diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts
index 88d118208..23a3ab3cc 100644
--- a/frontend/src/leftoverMapPlotLayout.test.ts
+++ b/frontend/src/leftoverMapPlotLayout.test.ts
@@ -1369,6 +1369,7 @@ describe("leftover map comparison graphic labels", () => {
expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe(
"leftover map comparison graphic leftover-map axis {axis} σ {value}",
);
+ expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe("leftover-map axis {axis} σ {value}");
expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe(
"leftover map comparison leftover axis {axis} σ {value}",
);
diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts
index 217a56bae..df6cb0caa 100644
--- a/frontend/src/leftoverMapPlotLayout.ts
+++ b/frontend/src/leftoverMapPlotLayout.ts
@@ -1,5 +1,7 @@
/** Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``.
* Leftover-map axis share captions axes 1 and 2 when finite (ADR 0269).
+ * Leftover-map singular values caption leftover-map graphic-display axes when
+ * finite (ADR 0324).
* Axis ticks name persisted leftover-map coordinates (ADR 0270).
* Pair segments name persisted leftover-map distance ``d`` (ADR 0271),
* persisted leftover-map reconstruction ``R̂`` (ADR 0272), persisted
@@ -85,6 +87,8 @@
* singular values, not this graphic.
* ADR 0323 captions leftover-axis report badges on the grouping comparison strip
* with persisted leftover-map singular values, not this graphic.
+ * ADR 0324 captions leftover-map graphic-display axes with persisted leftover-map
+ * singular values `σ_k`.
*/
import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates";
diff --git a/pyproject.toml b/pyproject.toml
index 95202a36c..d46ab31c7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.80.0"
+version = "2.81.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 593b8d8246c73226e224d2a9d251668649cbb54f Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 31 Aug 2026 02:52:36 +0000
Subject: [PATCH 02/21] feat(reports): leftover-map singular values on
leftover-axis report badges independently of leftover-map axis share
(v2.82.0)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Caption leftover-axis report badges with persisted leftover-map singular
values σ_k independently of leftover-map axis share (ADR 0325). Period
leftover-axis copy stays leftover axis {k} σ {value} when share is omitted
so it stays distinct from leftover-axis leftover axis {k} σ {value} {share}%,
from hyphen leftover-map axis {k} σ {value}, from comparison leftover-axis
leftover map comparison leftover axis {k} σ {value}, and from comparison
graphic leftover map comparison graphic leftover-map axis {k} σ {value}.
Rank-0 unused axes still name σ 0.00. Share and singular value omit
independently. Do not invent σ_k from leftover-map axis share. Do not invent
leftover-map axis share from σ_k. Never invent a leftover score. Never invent
a theta.
---
AGENTS.md | 12 +-
ARCHITECTURE.md | 5 +-
.../2.82.0-leftover-map-axis-singular-only.md | 21 +++
CHANGELOG.md | 20 +++
CLAUDE.md | 2 +-
docs/adr/0148-leftover-map-axis-share.md | 4 +-
docs/adr/0322-leftover-map-axis-singular.md | 5 +-
...0323-leftover-map-compare-axis-singular.md | 3 +
.../0324-leftover-map-plot-axis-singular.md | 4 +-
.../0325-leftover-map-axis-singular-only.md | 120 ++++++++++++++++++
docs/product-technical-gap-baseline.md | 19 +++
frontend/package.json | 2 +-
frontend/src/App.test.tsx | 2 +
frontend/src/App.tsx | 34 ++---
frontend/src/i18n.test.ts | 11 ++
frontend/src/i18n.ts | 4 +
frontend/src/leftoverMapAxisBadge.test.ts | 60 ++++++++-
frontend/src/leftoverMapAxisBadge.ts | 33 ++++-
.../src/leftoverMapPlotAxisSingular.test.ts | 3 +-
frontend/src/leftoverMapPlotLayout.test.ts | 1 +
pyproject.toml | 2 +-
21 files changed, 340 insertions(+), 27 deletions(-)
create mode 100644 CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md
create mode 100644 docs/adr/0325-leftover-map-axis-singular-only.md
diff --git a/AGENTS.md b/AGENTS.md
index dd56245ab..ae33e2e06 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,7 +273,7 @@ stops startup instead of leaving a healthy-looking partial schema, and
application code must not compensate for a missing table.
Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0158 / 0162 /
-0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324) are computed in `lineageweave/leftover_pairs.py` from the
+0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324 / 0325) are computed in `lineageweave/leftover_pairs.py` from the
residual after a real GRM/GPCM score, never invented. Distances are
Euclidean on the two-dimensional Gabriel leftover map; missing cells stay
out of the factorization. Closest and farthest post–criterion pairs
@@ -661,6 +661,13 @@ values `σ_k` when leftoverMapPlotAxisBadge returns a usable leftover-map axis c
Distinct accessible name `leftover-map axis {axis} σ {value} ({share}%)`. Share and
singular value omit independently. Rank-0 unused axes still name leftover-map axis
`σ 0.00`. Do not invent leftover-map singular values from leftover-map axis share.
+ADR 0325 captions leftover-axis report badges with persisted leftover-map singular
+values `σ_k` independently of leftover-map axis share when leftoverMapAxisBadge
+returns a usable leftover-axis caption. Distinct accessible name
+`leftover axis {axis} σ {value}`. Share and singular value omit independently.
+Rank-0 unused axes still name leftover-axis `σ 0.00`. Do not invent leftover-map
+singular values from leftover-map axis share. Do not invent leftover-map axis
+share from leftover-map singular values.
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
@@ -768,6 +775,9 @@ singular values `σ_k` when leftoverMapCompareAxisBadge returns a usable leftove
Leftover-map graphic-display leftover-map singular values (ADR 0324) captions leftover-map
graphic-display axes with persisted leftover-map singular values `σ_k` when leftoverMapPlotAxisBadge
returns a usable leftover-map axis caption.
+Leftover-axis report badge leftover-map singular values independently of leftover-map axis share
+(ADR 0325) captions leftover-axis report badges with persisted leftover-map singular values
+`σ_k` when leftoverMapAxisBadge returns a usable leftover-axis caption.
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 84746aa4a..ba4108f8a 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -756,6 +756,8 @@ ADR 0323 captions leftover-axis report badges on the grouping comparison strip
with persisted leftover-map singular values `σ_k`.
ADR 0324 captions leftover-map graphic-display axes with persisted leftover-map
singular values `σ_k`.
+ADR 0325 captions leftover-axis report badges with persisted leftover-map
+singular values `σ_k` independently of leftover-map axis share.
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
@@ -809,7 +811,8 @@ graphic (ADR 0320), leftover-map singular values on the grouping comparison left
graphic (ADR 0321), leftover-map singular values on leftover-axis report badges
(ADR 0322), leftover-map singular values on leftover-axis report badges on the
grouping comparison strip (ADR 0323), leftover-map singular values on leftover-map
-graphic-display axes (ADR 0324);
+graphic-display axes (ADR 0324), leftover-map singular values on leftover-axis
+report badges independently of leftover-map axis share (ADR 0325);
`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.82.0-leftover-map-axis-singular-only.md b/CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md
new file mode 100644
index 000000000..78d164006
--- /dev/null
+++ b/CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md
@@ -0,0 +1,21 @@
+## 2.82.0 — Leftover-map singular values on leftover-axis report badges independently of leftover-map axis share
+
+- Caption leftover-axis report badges with persisted leftover-map singular
+ values `σ_k` independently of leftover-map axis share (ADR 0325). After
+ `make seed`, leftover-axis report badges name persisted leftover-map
+ singular values when finite even when leftover-map axis share is omitted;
+ click a leftover pair opens that post. A missing or non-finite leftover-map
+ axis share omits that share caption and keeps `leftover axis {k} σ {value}`.
+ A missing, non-finite, or negative singular value omits that `σ` badge and
+ keeps `leftover axis {k} {share}%`. Rank-0 unused axes still name `σ 0.00`.
+ Leftover-axis report badges stay `leftover axis {k} σ {value}` when share is
+ omitted so they stay distinct from leftover-axis
+ `leftover axis {k} σ {value} {share}%`, from hyphen
+ `leftover-map axis {k} σ {value}`, from comparison leftover-axis
+ `leftover map comparison leftover axis {k} σ {value}`, and from comparison
+ graphic `leftover map comparison graphic leftover-map axis {k} σ {value}`.
+ Do not invent `σ_k` from leftover-map axis share. Do not invent leftover-map
+ axis share from `σ_k`. This increment does not change leftover-map singular
+ values on leftover-map graphic-display axes or on leftover-axis report badges
+ on the grouping comparison strip. A finite negative leftover is shown, never
+ clamped. Never invent a leftover score. Never invent a theta. No new columns.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0ab37f05..c88b008df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,26 @@ All notable changes to this project are documented here. Format follows
### Added
+- Period leftover pairs now caption leftover-axis report badges with persisted
+ leftover-map singular values `σ_k` independently of leftover-map axis share
+ (ADR 0325 / v2.82.0). After `make seed`, leftover-axis report badges name
+ persisted leftover-map singular values when finite even when leftover-map
+ axis share is omitted; click a leftover pair opens that post. A missing or
+ non-finite leftover-map axis share omits that share caption and keeps
+ `leftover axis {k} σ {value}`. A missing, non-finite, or negative singular
+ value omits that `σ` badge and keeps `leftover axis {k} {share}%`. Rank-0
+ unused axes still name `σ 0.00`. Leftover-axis report badges stay
+ `leftover axis {k} σ {value}` when share is omitted so they stay distinct
+ from leftover-axis `leftover axis {k} σ {value} {share}%`, from hyphen
+ `leftover-map axis {k} σ {value}`, from comparison leftover-axis
+ `leftover map comparison leftover axis {k} σ {value}`, and from comparison
+ graphic `leftover map comparison graphic leftover-map axis {k} σ {value}`.
+ Do not invent `σ_k` from leftover-map axis share. Do not invent leftover-map
+ axis share from `σ_k`. This increment does not change leftover-map singular
+ values on leftover-map graphic-display axes or on leftover-axis report badges
+ on the grouping comparison strip. A finite negative leftover is shown, never
+ clamped. Never invent a leftover score or a theta. No new columns.
+
- Period leftover pairs now caption leftover-map graphic-display axes with persisted
leftover-map singular values `σ_k` (ADR 0324 / v2.81.0). After `make seed`, leftover-map
graphic-display axes name persisted leftover-map singular values when finite together
diff --git a/CLAUDE.md b/CLAUDE.md
index f29361dc8..9de8440ab 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 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324), occupational construct catalog search
+(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324 / 0325), 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/docs/adr/0148-leftover-map-axis-share.md b/docs/adr/0148-leftover-map-axis-share.md
index 7b5f55843..b2eebef3d 100644
--- a/docs/adr/0148-leftover-map-axis-share.md
+++ b/docs/adr/0148-leftover-map-axis-share.md
@@ -9,7 +9,9 @@
[ADR 0323](0323-leftover-map-compare-axis-singular.md)
(leftover-map singular values on leftover-axis report badges on the grouping comparison strip);
[ADR 0324](0324-leftover-map-plot-axis-singular.md)
-(leftover-map singular values on leftover-map graphic-display axes)
+(leftover-map singular values on leftover-map graphic-display axes);
+[ADR 0325](0325-leftover-map-axis-singular-only.md)
+(leftover-map singular values on leftover-axis report badges independently of leftover-map axis share)
## Context
diff --git a/docs/adr/0322-leftover-map-axis-singular.md b/docs/adr/0322-leftover-map-axis-singular.md
index e098a7567..aac40500a 100644
--- a/docs/adr/0322-leftover-map-axis-singular.md
+++ b/docs/adr/0322-leftover-map-axis-singular.md
@@ -85,7 +85,10 @@ values on leftover-axis badges on the grouping comparison strip with a
distinct name
([ADR 0323](0323-leftover-map-compare-axis-singular.md)). This increment does
not caption leftover-map singular values on leftover-map graphic-display axes
-([ADR 0324](0324-leftover-map-plot-axis-singular.md)).
+([ADR 0324](0324-leftover-map-plot-axis-singular.md)). This increment does not
+caption leftover-map singular values on leftover-axis report badges independently
+of leftover-map axis share
+([ADR 0325](0325-leftover-map-axis-singular-only.md)).
## References
diff --git a/docs/adr/0323-leftover-map-compare-axis-singular.md b/docs/adr/0323-leftover-map-compare-axis-singular.md
index 8c2c28d56..ebe2a5260 100644
--- a/docs/adr/0323-leftover-map-compare-axis-singular.md
+++ b/docs/adr/0323-leftover-map-compare-axis-singular.md
@@ -105,6 +105,9 @@ Independent of leftover-map singular values on leftover-axis report badges
in the period-report panel ([ADR 0322](0322-leftover-map-axis-singular.md)).
Independent of leftover-map singular values on leftover-map graphic-display
axes ([ADR 0324](0324-leftover-map-plot-axis-singular.md)).
+Independent of leftover-map singular values on leftover-axis report badges
+independently of leftover-map axis share
+([ADR 0325](0325-leftover-map-axis-singular-only.md)).
## References
diff --git a/docs/adr/0324-leftover-map-plot-axis-singular.md b/docs/adr/0324-leftover-map-plot-axis-singular.md
index 02616597e..bb175246f 100644
--- a/docs/adr/0324-leftover-map-plot-axis-singular.md
+++ b/docs/adr/0324-leftover-map-plot-axis-singular.md
@@ -113,7 +113,9 @@ Independent of leftover-map singular values on leftover-axis report badges
singular values on leftover-axis report badges on the grouping comparison
strip ([ADR 0323](0323-leftover-map-compare-axis-singular.md)). Independent of
leftover-map singular values on the grouping comparison leftover-map graphic
-([ADR 0321](0321-leftover-map-compare-plot-singular.md)).
+([ADR 0321](0321-leftover-map-compare-plot-singular.md)). Independent of leftover-map
+singular values on leftover-axis report badges independently of leftover-map axis
+share ([ADR 0325](0325-leftover-map-axis-singular-only.md)).
## References
diff --git a/docs/adr/0325-leftover-map-axis-singular-only.md b/docs/adr/0325-leftover-map-axis-singular-only.md
new file mode 100644
index 000000000..b11a4526a
--- /dev/null
+++ b/docs/adr/0325-leftover-map-axis-singular-only.md
@@ -0,0 +1,120 @@
+# ADR 0325 — Name leftover-map singular values on leftover-axis report badges independently of leftover-map axis share
+
+**Decision status:** Accepted
+**Date:** 2026-08-31
+
+Amends leftover-map singular values on leftover-axis report badges
+([ADR 0322](0322-leftover-map-axis-singular.md)), leftover-map axis share
+persistence ([ADR 0148](0148-leftover-map-axis-share.md)). Independent of leftover-map
+singular values on leftover-map graphic-display axes
+([ADR 0324](0324-leftover-map-plot-axis-singular.md)), leftover-map singular
+values on leftover-axis report badges on the grouping comparison strip
+([ADR 0323](0323-leftover-map-compare-axis-singular.md)), leftover-map
+singular values on the grouping comparison leftover-map graphic
+([ADR 0321](0321-leftover-map-compare-plot-singular.md)).
+
+## Context
+
+ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map
+axis share `σ_k² / Σ_j σ_j²`. ADR 0322 captions leftover-axis report badges
+with `leftover axis {axis} σ {value} {share}%` when `σ_k` is finite, and
+falls back to `leftover axis {axis} {share}%` when `σ_k` is omitted. Those
+badges still couple leftover-map singular values to leftover-map axis share:
+a missing, non-finite leftover-map axis share is formatted as `NaN%` beside
+a finite `σ_k`, so Gabriel (1971) singular values appear to invent leftover-map
+axis share. ADR 0323 already omits share and singular value independently on
+grouping comparison leftover-axis badges. ADR 0324 already omits share and
+singular value independently on leftover-map graphic-display axes. Period
+leftover-axis badges still hide the σ-only copy `leftover axis {k} σ {value}`.
+A missing, non-finite, or negative singular value is not a leftover score and
+must omit independently of leftover-map axis share. Rank-0 unused axes still
+persist `σ_k = 0`. A finite negative leftover is shown, never clamped. Do not
+invent `σ_k` from leftover-map axis share. Do not invent leftover-map axis
+share from `σ_k`.
+
+This increment captions leftover-axis report badges with already persisted
+leftover-map singular values independently of leftover-map axis share.
+Leftover-axis report badges stay `leftover axis {k} σ {value}` when share is
+omitted so they stay distinct from leftover-axis `leftover axis {k} σ {value} {share}%`
+(ADR 0322), from hyphen `leftover-map axis {k} σ {value}` (ADR 0324), from
+comparison leftover-axis `leftover map comparison leftover axis {k} σ {value}`
+(ADR 0323), and from comparison graphic
+`leftover map comparison graphic leftover-map axis {k} σ {value}`.
+It does not add columns. Do not invent a leftover score. Do not invent a
+theta.
+
+This protected increment uses **0325** so it does not collide with leftover-map
+singular values on leftover-map graphic-display axes (0324), leftover-map
+singular values on leftover-axis report badges on the grouping comparison
+strip (0323), leftover-map singular values on leftover-axis report badges
+(0322), leftover-map singular values on the grouping comparison leftover-map
+graphic (0321), leftover-map axis share persistence (0148), or the dashboard
+stacks.
+
+## Decision
+
+On leftover-axis report badges, caption leftover-map axis `k` with persisted
+leftover-map singular value `σ_k` when that singular value is finite and
+non-negative, including rank-0 zero singular values. Share and singular
+value omit independently: no share and no `σ_k` omits that leftover-axis
+report badge; share only is `leftover axis {k} {share}%`; `σ_k` only is
+`leftover axis {k} σ {value}`; both is `leftover axis {k} σ {value} {share}%`.
+A missing or non-finite leftover-map axis share omits that share caption and
+does not invent leftover-map axis share from `σ_k`. A missing, non-finite, or
+negative singular value omits that `σ` badge and keeps a usable leftover-axis
+share caption. Rank-0 unused axes still name `σ 0.00`. Click a leftover pair
+to open that post.
+
+Leftover-axis report badge leftover-map singular values omit independently of
+leftover-map graphic-display leftover-map axis singular values and leftover-map
+comparison leftover-axis singular values. A missing leftover-map axis share
+omits that leftover-axis share caption and keeps a usable leftover-axis `σ`
+caption, leftover-map graphic leftover-map axis `σ` caption, leftover-map
+comparison leftover-axis `σ` caption, leftover-map comparison graphic leftover-map
+axis `σ` caption, leftover-map graphic leftover-map axis ticks, leftover-map
+distance caption, leftover-map rank caption, leftover expected `E`, leftover
+observed `Y`, leftover residual `R`, leftover-map unexplained leftover `U`,
+leftover-map reconstruction `R̂`, and leftover-map graphic coverage notes.
+
+This increment does not change leftover-map singular values on leftover-map
+graphic-display axes. Those leftover-map graphic axes stay
+`leftover-map axis {axis} σ {value} ({share}%)` (ADR 0324). This increment does
+not change leftover-map singular values on leftover-axis report badges on the
+grouping comparison strip (ADR 0323). This increment does not persist leftover-map
+inner product, cosine, or length.
+
+Do not add SQL. Do not edit shipped migrations. Do not invent a leftover
+score. Do not invent a theta.
+
+## Consequences
+
+After `make seed`, leftover-axis report badges name persisted leftover-map
+singular values when finite even when leftover-map axis share is omitted;
+click a leftover pair opens that post. Hidden posts stay hidden. Rank-0 unused
+axes still name leftover-axis `σ 0.00`. Leftover-map graphic-display leftover-map
+singular values (ADR 0324), leftover-axis report badge leftover-map singular
+values with share (ADR 0322), and grouping comparison leftover-axis report
+badge leftover-map singular values (ADR 0323) remain.
+
+## Related
+
+Independent of leftover-map singular values on leftover-map graphic-display
+axes ([ADR 0324](0324-leftover-map-plot-axis-singular.md)). Independent of
+leftover-map singular values on leftover-axis report badges on the grouping
+comparison strip ([ADR 0323](0323-leftover-map-compare-axis-singular.md)).
+Independent of leftover-map singular values on leftover-axis report badges
+that still require leftover-map axis share in the combined caption
+([ADR 0322](0322-leftover-map-axis-singular.md)).
+
+## 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.)
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 3c8aa6d5d..5efa4eb84 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,24 @@
# Product & Technical Gap Baseline
+> Exact-head loop overlay: 2026-08-31 11:25 KST. Protected `main` is
+> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not
+> self-approve. #865 leftover-map singular values on leftover-map graphic-display
+> axes (v2.81.0 / ADR 0324) is `a3613ac`, stacked on #863 @ `52b3597`. Independent
+> APPROVE is missing. Next buyer increment on this cycle: leftover-map singular
+> values on leftover-axis report badges independently of leftover-map axis share
+> (ADR 0325 / v2.82.0) delivered locally on
+> `feat/leftover-map-axis-singular-only-v2820`. Caption persisted leftover-map
+> singular values `σ_k` on leftover-axis report badges when leftover-map axis
+> share is omitted. Distinct accessible name `leftover axis {axis} σ {value}`.
+> Rank-0 unused axes still name `σ 0.00`. Share and singular value omit
+> independently. Do not invent `σ_k` from leftover-map axis share. Do not invent
+> leftover-map axis share from `σ_k`. Do not change leftover-map singular values
+> on leftover-map graphic-display axes or on leftover-axis report badges on the
+> grouping comparison strip this increment. Stack onto #865. Leave open for
+> independent review. Do not squash-merge. Do not self-approve. Issues #79 and
+> #87 stay open. Do not merge #808 without independent APPROVE. Never
+> self-approve.
+
> Exact-head loop overlay: 2026-08-31 11:05 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not
> self-approve. #863 leftover-map singular values on leftover-axis report badges
diff --git a/frontend/package.json b/frontend/package.json
index 9750a7d2e..1f6fe1636 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.81.0",
+ "version": "2.82.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index bbc95eac7..b1c232ab8 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -4179,6 +4179,8 @@ describe("App, authenticated", () => {
expect(screen.getAllByText(/CAT: sales-lead I=0\.70/).length).toBeGreaterThan(0);
expect(screen.getAllByText(/leftover axis 1 σ 1\.84 82%/).length).toBeGreaterThan(0);
expect(screen.getAllByText(/leftover axis 2 σ 0\.86 18%/).length).toBeGreaterThan(0);
+ expect(screen.queryByText("leftover axis 1 σ 1.84")).not.toBeInTheDocument();
+ expect(screen.queryByText("leftover axis 2 σ 0.86")).not.toBeInTheDocument();
expect(screen.getByText("leftover map comparison leftover axis 1 σ 1.84 82%")).toBeInTheDocument();
expect(screen.getByText("leftover map comparison leftover axis 2 σ 0.86 18%")).toBeInTheDocument();
expect(screen.getByLabelText("Leftover map comparison leftover axis")).toHaveTextContent(
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8d36a1b5f..83cb43c13 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -138,10 +138,7 @@ import {
LEFTOVER_MAP_PLOT_ITEM_COVERAGE,
} from "./leftoverMapCoverage";
import {
- leftoverMapAxisBadgeShare,
- leftoverMapAxisBadgeSingular,
- LEFTOVER_MAP_AXIS_BADGE_SHARE,
- LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ leftoverMapAxisBadge,
} from "./leftoverMapAxisBadge";
import {
leftoverMapCompareAxisBadge,
@@ -3937,27 +3934,34 @@ function ReportsPanel({
) : null}
{report.leftover_map_axes?.map((axis) => {
- const singular = leftoverMapAxisBadgeSingular(axis);
- const share = leftoverMapAxisBadgeShare(axis.leftover_share);
+ const badge = leftoverMapAxisBadge(
+ axis.axis_index,
+ leftoverSingularForAxis([axis], axis.axis_index),
+ leftoverShareForAxis([axis], axis.axis_index),
+ );
+ if (badge === null) {
+ return null;
+ }
return (
- {singular === null
- ? tf(LEFTOVER_MAP_AXIS_BADGE_SHARE, { axis: axis.axis_index, share })
- : tf(LEFTOVER_MAP_AXIS_BADGE_SINGULAR, {
- axis: axis.axis_index,
- value: singular,
- share,
- })}
+ {tf(badge.key, badge.values)}
);
})}
- {report.leftover_map_axes && report.leftover_map_axes.length > 0 && (
+ {report.leftover_map_axes?.some(
+ (axis) =>
+ leftoverMapAxisBadge(
+ axis.axis_index,
+ leftoverSingularForAxis([axis], axis.axis_index),
+ leftoverShareForAxis([axis], axis.axis_index),
+ ) !== null,
+ ) ? (
{t(
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Leftover-map singular values are the Gabriel scale of those axes. Open a leftover pair to read the post–criterion cell. The shares and singular values do not invent a leftover score.",
)}
- )}
+ ) : null}
{report.leftover_pairs && report.leftover_pairs.length > 0 && (
{
"Page",
"Answer",
"leftover axis {axis} {share}%",
+ "leftover axis {axis} σ {value}",
"leftover axis {axis} σ {value} {share}%",
"Leftover-map axis share",
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Leftover-map singular values are the Gabriel scale of those axes. Open a leftover pair to read the post–criterion cell. The shares and singular values do not invent a leftover score.",
@@ -747,6 +748,16 @@ describe("i18n", () => {
).toBe(expected);
});
+ it.each([
+ ["ko", "잔차 축 1 σ 1.84"],
+ ["zh", "残差轴 1 σ 1.84"],
+ ["ja", "残差軸 1 σ 1.84"],
+ ["vi", "trục phần dư 1 σ 1.84"],
+ ] as const)("formats leftover-axis badge singular values without share in %s", (locale, expected) => {
+ setLocale(locale);
+ expect(tf("leftover axis {axis} σ {value}", { axis: 1, value: "1.84" })).toBe(expected);
+ });
+
it.each([
["ko", "잔여 지도 비교 잔차 축 1 σ 1.84"],
["zh", "残差地图比较残差轴 1 σ 1.84"],
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index ec4b31d06..453fe3a19 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -613,6 +613,7 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "유효 시작",
"Valid to": "유효 종료",
"leftover axis {axis} {share}%": "잔차 축 {axis} {share}%",
+ "leftover axis {axis} σ {value}": "잔차 축 {axis} σ {value}",
"leftover axis {axis} σ {value} {share}%": "잔차 축 {axis} σ {value} {share}%",
"Leftover-map axis share": "잔차 지도 축 비율",
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Leftover-map singular values are the Gabriel scale of those axes. Open a leftover pair to read the post–criterion cell. The shares and singular values do not invent a leftover score.":
@@ -1265,6 +1266,7 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "有效起始",
"Valid to": "有效结束",
"leftover axis {axis} {share}%": "残差轴 {axis} {share}%",
+ "leftover axis {axis} σ {value}": "残差轴 {axis} σ {value}",
"leftover axis {axis} σ {value} {share}%": "残差轴 {axis} σ {value} {share}%",
"Leftover-map axis share": "残差图轴占比",
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Leftover-map singular values are the Gabriel scale of those axes. Open a leftover pair to read the post–criterion cell. The shares and singular values do not invent a leftover score.":
@@ -1921,6 +1923,7 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "有効開始",
"Valid to": "有効終了",
"leftover axis {axis} {share}%": "残差軸 {axis} {share}%",
+ "leftover axis {axis} σ {value}": "残差軸 {axis} σ {value}",
"leftover axis {axis} σ {value} {share}%": "残差軸 {axis} σ {value} {share}%",
"Leftover-map axis share": "残差マップ軸の比率",
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Leftover-map singular values are the Gabriel scale of those axes. Open a leftover pair to read the post–criterion cell. The shares and singular values do not invent a leftover score.":
@@ -2580,6 +2583,7 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "Hiệu lực từ",
"Valid to": "Hiệu lực đến",
"leftover axis {axis} {share}%": "trục phần dư {axis} {share}%",
+ "leftover axis {axis} σ {value}": "trục phần dư {axis} σ {value}",
"leftover axis {axis} σ {value} {share}%": "trục phần dư {axis} σ {value} {share}%",
"Leftover-map axis share": "Tỷ trọng trục bản đồ phần dư",
"Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Leftover-map singular values are the Gabriel scale of those axes. Open a leftover pair to read the post–criterion cell. The shares and singular values do not invent a leftover score.":
diff --git a/frontend/src/leftoverMapAxisBadge.test.ts b/frontend/src/leftoverMapAxisBadge.test.ts
index 7261489cb..cc0c76001 100644
--- a/frontend/src/leftoverMapAxisBadge.test.ts
+++ b/frontend/src/leftoverMapAxisBadge.test.ts
@@ -1,9 +1,11 @@
import { describe, expect, it } from "vitest";
import {
+ leftoverMapAxisBadge,
leftoverMapAxisBadgeShare,
leftoverMapAxisBadgeSingular,
LEFTOVER_MAP_AXIS_BADGE_SHARE,
LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY,
} from "./leftoverMapAxisBadge";
import {
LEFTOVER_MAP_COMPARE_AXIS_SHARE,
@@ -21,6 +23,13 @@ describe("leftoverMapAxisBadgeShare", () => {
expect(leftoverMapAxisBadgeShare(0.18)).toBe("18");
expect(leftoverMapAxisBadgeShare(0)).toBe("0");
});
+
+ it("omits leftover-map axis share that is missing or non-finite", () => {
+ expect(leftoverMapAxisBadgeShare(undefined)).toBeNull();
+ expect(leftoverMapAxisBadgeShare(null)).toBeNull();
+ expect(leftoverMapAxisBadgeShare(Number.NaN)).toBeNull();
+ expect(leftoverMapAxisBadgeShare(Number.POSITIVE_INFINITY)).toBeNull();
+ });
});
describe("leftoverMapAxisBadgeSingular", () => {
@@ -57,6 +66,7 @@ describe("leftoverMapAxisBadgeSingular", () => {
it("stays distinct from leftover-map graphic and comparison leftover-axis σ copy", () => {
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).toBe("leftover axis {axis} σ {value} {share}%");
expect(LEFTOVER_MAP_AXIS_BADGE_SHARE).toBe("leftover axis {axis} {share}%");
+ expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY).toBe("leftover axis {axis} σ {value}");
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR);
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE);
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR);
@@ -66,6 +76,54 @@ describe("leftoverMapAxisBadgeSingular", () => {
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR);
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE);
expect(LEFTOVER_MAP_AXIS_BADGE_SHARE).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SHARE);
- expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe("leftover axis {axis} σ {value}");
+ expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY);
+ expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR);
+ expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR);
+ });
+});
+
+describe("leftover-axis report badge leftover-map singular values", () => {
+ it("names persisted leftover-map singular values with leftover-map axis share", () => {
+ expect(leftoverMapAxisBadge(1, 1.84, 0.82)).toEqual({
+ key: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ values: { axis: 1, value: "1.84", share: "82" },
+ });
+ expect(leftoverMapAxisBadge(2, 0.86, 0.18)).toEqual({
+ key: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ values: { axis: 2, value: "0.86", share: "18" },
+ });
+ });
+
+ it("names rank-0 zero leftover-map singular values on leftover-axis report badges", () => {
+ expect(leftoverMapAxisBadge(1, 0, 0)).toEqual({
+ key: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ values: { axis: 1, value: "0.00", share: "0" },
+ });
+ });
+
+ it("omits leftover-map singular values independently of leftover-map axis share", () => {
+ expect(leftoverMapAxisBadge(1, Number.NaN, 0.82)).toEqual({
+ key: LEFTOVER_MAP_AXIS_BADGE_SHARE,
+ values: { axis: 1, share: "82" },
+ });
+ expect(leftoverMapAxisBadge(2, -0.01, 0.18)).toEqual({
+ key: LEFTOVER_MAP_AXIS_BADGE_SHARE,
+ values: { axis: 2, share: "18" },
+ });
+ expect(leftoverMapAxisBadge(1, 1.84, Number.NaN)).toEqual({
+ key: LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY,
+ values: { axis: 1, value: "1.84" },
+ });
+ expect(leftoverMapAxisBadge(1, null, null)).toBeNull();
+ });
+
+ it("does not invent leftover-map singular values from leftover-map axis share", () => {
+ expect(leftoverMapAxisBadge(1, undefined, 0.82)?.key).toBe(LEFTOVER_MAP_AXIS_BADGE_SHARE);
+ expect(leftoverMapAxisBadge(1, undefined, 0.82)?.values.value).toBeUndefined();
+ });
+
+ it("does not invent leftover-map axis share from leftover-map singular values", () => {
+ expect(leftoverMapAxisBadge(1, 1.84, undefined)?.key).toBe(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY);
+ expect(leftoverMapAxisBadge(1, 1.84, undefined)?.values.share).toBeUndefined();
});
});
diff --git a/frontend/src/leftoverMapAxisBadge.ts b/frontend/src/leftoverMapAxisBadge.ts
index 71a7d9dc4..a6d767964 100644
--- a/frontend/src/leftoverMapAxisBadge.ts
+++ b/frontend/src/leftoverMapAxisBadge.ts
@@ -1,9 +1,14 @@
/** Caption leftover-axis report badges with persisted Gabriel singular values.
+ * ADR 0325 names leftover-map singular values independently of leftover-map
+ * axis share (`leftover axis {axis} σ {value}` when share is omitted).
* ADR 0323 captions leftover-axis report badges on the grouping comparison strip
* with a distinct leftover map comparison leftover axis name, not this helper.
+ * ADR 0324 captions leftover-map graphic-display axes, not this helper.
*/
import type { LeftoverMapAxis } from "./api";
+import { formatLeftoverMapPlotAxisShare } from "./leftoverMapPlotAxisShare";
+import type { LeftoverMapCompareAxisBadge } from "./leftoverMapPlotAxisSingular";
import {
formatLeftoverMapPlotAxisSingular,
leftoverSingularForAxis,
@@ -13,10 +18,12 @@ export const LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis} {share}%";
export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value} {share}%";
+export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY = "leftover axis {axis} σ {value}";
+
export function leftoverMapAxisBadgeShare(
leftoverShare: LeftoverMapAxis["leftover_share"] | null | undefined,
-): string {
- return ((leftoverShare ?? Number.NaN) * 100).toFixed(0);
+): string | null {
+ return formatLeftoverMapPlotAxisShare(leftoverShare);
}
export function leftoverMapAxisBadgeSingular(
@@ -28,3 +35,25 @@ export function leftoverMapAxisBadgeSingular(
leftoverSingularForAxis([axis], axis.axis_index),
);
}
+
+export function leftoverMapAxisBadge(
+ axisIndex: number,
+ leftoverSingular: number | null | undefined,
+ leftoverShare: number | null | undefined,
+): LeftoverMapCompareAxisBadge | null {
+ const singular = formatLeftoverMapPlotAxisSingular(leftoverSingular);
+ const percent = leftoverMapAxisBadgeShare(leftoverShare);
+ if (singular === null && percent === null) {
+ return null;
+ }
+ if (singular === null && percent !== null) {
+ return { key: LEFTOVER_MAP_AXIS_BADGE_SHARE, values: { axis: axisIndex, share: percent } };
+ }
+ if (singular !== null && percent === null) {
+ return { key: LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY, values: { axis: axisIndex, value: singular } };
+ }
+ return {
+ key: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ values: { axis: axisIndex, value: singular as string, share: percent as string },
+ };
+}
diff --git a/frontend/src/leftoverMapPlotAxisSingular.test.ts b/frontend/src/leftoverMapPlotAxisSingular.test.ts
index 4e47cfb21..3f4056c52 100644
--- a/frontend/src/leftoverMapPlotAxisSingular.test.ts
+++ b/frontend/src/leftoverMapPlotAxisSingular.test.ts
@@ -19,7 +19,7 @@ import {
LEFTOVER_MAP_PLOT_AXIS_SHARE,
} from "./leftoverMapPlotAxisShare";
import { LEFTOVER_MAP_COMPARE_PLOT_TICK } from "./leftoverMapPlotLayout";
-import { LEFTOVER_MAP_AXIS_BADGE_SINGULAR } from "./leftoverMapAxisBadge";
+import { LEFTOVER_MAP_AXIS_BADGE_SINGULAR, LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY } from "./leftoverMapAxisBadge";
describe("leftoverSingularForAxis", () => {
const axes = [
@@ -223,6 +223,7 @@ describe("leftover-map graphic-display axis singular badges", () => {
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE,
);
expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR);
+ expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY);
expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE).not.toBe(LEFTOVER_MAP_AXIS_BADGE_SINGULAR);
expect(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE);
});
diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts
index 23a3ab3cc..30b8554b0 100644
--- a/frontend/src/leftoverMapPlotLayout.test.ts
+++ b/frontend/src/leftoverMapPlotLayout.test.ts
@@ -1370,6 +1370,7 @@ describe("leftover map comparison graphic labels", () => {
"leftover map comparison graphic leftover-map axis {axis} σ {value}",
);
expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe("leftover-map axis {axis} σ {value}");
+ expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe("leftover axis {axis} σ {value}");
expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe(
"leftover map comparison leftover axis {axis} σ {value}",
);
diff --git a/pyproject.toml b/pyproject.toml
index d46ab31c7..ab9a1f9af 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.81.0"
+version = "2.82.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 7458b7ff83947c3f82ea497d02dd80dd56394a35 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:21:05 +0900
Subject: [PATCH 03/21] fix(ui): expose report graphic singular axis copy
---
frontend/src/leftoverMapPlotAxisSingular.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts
index 5d22bce41..bae74285d 100644
--- a/frontend/src/leftoverMapPlotAxisSingular.ts
+++ b/frontend/src/leftoverMapPlotAxisSingular.ts
@@ -3,6 +3,12 @@
import type { LeftoverMapAxis } from "./api";
import { formatLeftoverMapPlotAxisShare } from "./leftoverMapPlotAxisShare";
+export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR =
+ "leftover-map axis {axis} σ {value}";
+
+export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE =
+ "leftover-map axis {axis} σ {value} ({share}%)";
+
export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR =
"leftover map comparison graphic leftover-map axis {axis} σ {value}";
From 75f0bf690cd2a3aa439fa718f56fd4ae26969dfa Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:22:11 +0900
Subject: [PATCH 04/21] fix(ui): render report graphic singular axis evidence
---
frontend/src/components/LeftoverMapPlot.tsx | 22 ++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx
index 5de0b8fb5..435d92fe8 100644
--- a/frontend/src/components/LeftoverMapPlot.tsx
+++ b/frontend/src/components/LeftoverMapPlot.tsx
@@ -33,6 +33,8 @@ import {
leftoverSingularForAxis,
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR,
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE,
+ LEFTOVER_MAP_PLOT_AXIS_SINGULAR,
+ LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
} from "../leftoverMapPlotAxisSingular";
import {
firstPlottablePairForPost,
@@ -106,10 +108,20 @@ function leftoverMapPlotAxisText(
share: percent,
});
}
+ if (singular === null) {
+ if (percent === null) {
+ return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2");
+ }
+ return tf(LEFTOVER_MAP_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent });
+ }
if (percent === null) {
- return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2");
+ return tf(LEFTOVER_MAP_PLOT_AXIS_SINGULAR, { axis: axisIndex, value: singular });
}
- return tf(LEFTOVER_MAP_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent });
+ return tf(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE, {
+ axis: axisIndex,
+ value: singular,
+ share: percent,
+ });
}
/**
@@ -118,9 +130,9 @@ function leftoverMapPlotAxisText(
* Person markers are posts; item markers are leftover criteria. Click a
* post marker to open that post. Caption leftover-map axes with persisted
* Gabriel inertia share when finite, including rank-0 zero-share axes.
- * Comparison graphic axes additionally name finite, non-negative persisted
- * Gabriel singular values independently of axis share; never derive one
- * measurement from the other. The comparison strip also exposes these two
+ * Report and comparison graphic axes additionally name finite, non-negative
+ * persisted Gabriel singular values independently of axis share; never derive
+ * one measurement from the other. The comparison strip also exposes these two
* persisted measures as independent badges so a missing share does not erase
* usable σ evidence and a missing σ does not erase usable share evidence.
* Axis ticks name persisted leftover-map coordinates so ξ / ζ on the
From 9ae19d8505094e3f4ee0576bc0dbae09019cd46e Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:22:23 +0900
Subject: [PATCH 05/21] docs(adr): record report graphic singular axes
---
...ftover-map-report-graphic-axis-singular.md | 31 +++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 docs/adr/0324-leftover-map-report-graphic-axis-singular.md
diff --git a/docs/adr/0324-leftover-map-report-graphic-axis-singular.md b/docs/adr/0324-leftover-map-report-graphic-axis-singular.md
new file mode 100644
index 000000000..c40c6d946
--- /dev/null
+++ b/docs/adr/0324-leftover-map-report-graphic-axis-singular.md
@@ -0,0 +1,31 @@
+# ADR 0324 — Name persisted singular values on report leftover-map graphic axes
+
+**Decision status:** Proposed
+**Date:** 2026-08-31
+
+## Context
+
+LineageWeave persists leftover-map axis share and Gabriel singular value `σ_k`. Comparison graphics and comparison-strip evidence can already expose persisted `σ_k`; the report graphic still shows share only. That hides scale evidence on the report graphic and creates inconsistent semantics for the same persisted axes.
+
+Missingness is independent. A valid finite, non-negative `σ_k`, including `0`, must remain visible when share is absent; valid share must remain visible when `σ_k` is absent or invalid. Neither value is reconstructed from the other.
+
+## Decision
+
+Report graphic axes use distinct copy:
+
+- singular only: `leftover-map axis {axis} σ {value}`;
+- singular plus share: `leftover-map axis {axis} σ {value} ({share}%)`;
+- share only: the existing `leftover-map axis {axis} ({share}%)`;
+- neither usable: the existing axis name without invented evidence.
+
+No square root, normalization, SQL, migration, or owner duplication is introduced. The rendering consumes LineageWeave-owned persisted evidence and leaves comparison copy distinct.
+
+## Consequences
+
+Report and comparison graphics now preserve the same evidence semantics while keeping surface-specific labels. This ADR remains **Proposed** until fresh exact-head frontend, browser/accessibility, security, and independent-review evidence is current.
+
+## 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
From eeb8102c0f7a2142f8ce1d8aafea0165f60406c3 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:22:28 +0900
Subject: [PATCH 06/21] docs(changelog): record report graphic singular axes
---
CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md
diff --git a/CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md b/CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md
new file mode 100644
index 000000000..6e39642a5
--- /dev/null
+++ b/CHANGELOG.d/2.81.0-leftover-map-plot-axis-singular.md
@@ -0,0 +1,3 @@
+### Added
+
+- Report leftover-map graphic axes now expose persisted Gabriel singular values independently of persisted axis share, preserving finite `σ=0` and omitting invalid evidence without deriving one measure from the other.
From 31e3c03c9064a4e2eabe60587aff52bdc39dd209 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:23:24 +0900
Subject: [PATCH 07/21] fix(ui): type axis evidence template values safely
---
frontend/src/leftoverMapPlotAxisSingular.ts | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts
index bae74285d..aed961ef0 100644
--- a/frontend/src/leftoverMapPlotAxisSingular.ts
+++ b/frontend/src/leftoverMapPlotAxisSingular.ts
@@ -31,11 +31,7 @@ export type LeftoverMapPlotAxisSingular = {
export type LeftoverMapCompareAxisBadge = {
template: string;
- values: {
- axis: LeftoverMapAxis["axis_index"];
- value?: string;
- share?: string;
- };
+ values: Record;
};
export function leftoverSingularForAxis(
@@ -80,13 +76,13 @@ export function leftoverMapCompareAxisBadge(
if (singular === null && share === null) {
return null;
}
- if (singular === null) {
+ if (singular === null && share !== null) {
return {
template: LEFTOVER_MAP_COMPARE_AXIS_SHARE,
- values: { axis: axis.axis_index, share: share ?? undefined },
+ values: { axis: axis.axis_index, share },
};
}
- if (share === null) {
+ if (singular !== null && share === null) {
return {
template: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR,
values: { axis: axis.axis_index, value: singular },
@@ -94,6 +90,6 @@ export function leftoverMapCompareAxisBadge(
}
return {
template: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE,
- values: { axis: axis.axis_index, value: singular, share },
+ values: { axis: axis.axis_index, value: singular as string, share: share as string },
};
}
From 611636de2dd0989cbc34c8e9dbdc6104fc38785a Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:24:53 +0900
Subject: [PATCH 08/21] fix(ui): preserve report axis singular-only state
---
frontend/src/leftoverMapAxisBadge.ts | 39 ++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/frontend/src/leftoverMapAxisBadge.ts b/frontend/src/leftoverMapAxisBadge.ts
index 7d2b377df..1511b115c 100644
--- a/frontend/src/leftoverMapAxisBadge.ts
+++ b/frontend/src/leftoverMapAxisBadge.ts
@@ -1,6 +1,7 @@
/** Caption leftover-axis report badges with persisted Gabriel singular values. */
import type { LeftoverMapAxis } from "./api";
+import { formatLeftoverMapPlotAxisShare } from "./leftoverMapPlotAxisShare";
import {
formatLeftoverMapPlotAxisSingular,
leftoverSingularForAxis,
@@ -10,6 +11,13 @@ export const LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis} {share}%";
export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value} {share}%";
+export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY = "leftover axis {axis} σ {value}";
+
+export type LeftoverMapAxisBadge = {
+ template: string;
+ values: Record;
+};
+
export function leftoverMapAxisBadgeShare(
leftoverShare: LeftoverMapAxis["leftover_share"] | null | undefined,
): string {
@@ -25,3 +33,34 @@ export function leftoverMapAxisBadgeSingular(
leftoverSingularForAxis([axis], axis.axis_index),
);
}
+
+/** Keep report-axis singular and share evidence independently observable. */
+export function leftoverMapAxisBadge(
+ axis: Pick & {
+ leftover_share?: LeftoverMapAxis["leftover_share"] | null;
+ leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null;
+ },
+): LeftoverMapAxisBadge | null {
+ const singular = leftoverMapAxisBadgeSingular(axis);
+ const share = formatLeftoverMapPlotAxisShare(axis.leftover_share);
+
+ if (singular === null && share === null) {
+ return null;
+ }
+ if (singular === null && share !== null) {
+ return {
+ template: LEFTOVER_MAP_AXIS_BADGE_SHARE,
+ values: { axis: axis.axis_index, share },
+ };
+ }
+ if (singular !== null && share === null) {
+ return {
+ template: LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY,
+ values: { axis: axis.axis_index, value: singular },
+ };
+ }
+ return {
+ template: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ values: { axis: axis.axis_index, value: singular as string, share: share as string },
+ };
+}
From a87efee55f51fccff21e6692a687bbcace1bcf77 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:29:40 +0900
Subject: [PATCH 09/21] docs(adr): record independent report axis evidence
states
---
...-leftover-map-report-axis-singular-only.md | 35 +++++++++++++++++++
1 file changed, 35 insertions(+)
create mode 100644 docs/adr/0325-leftover-map-report-axis-singular-only.md
diff --git a/docs/adr/0325-leftover-map-report-axis-singular-only.md b/docs/adr/0325-leftover-map-report-axis-singular-only.md
new file mode 100644
index 000000000..1ddc3f8ac
--- /dev/null
+++ b/docs/adr/0325-leftover-map-report-axis-singular-only.md
@@ -0,0 +1,35 @@
+# ADR 0325 — Preserve singular-only and share-only report-axis evidence
+
+**Decision status:** Proposed
+**Date:** 2026-08-31
+
+## Context
+
+LineageWeave persists two different measurements for a leftover-map axis: Gabriel singular value `σ_k` and axis share. They have independent missingness. The report badge path previously chose its rendering branch from `σ_k` alone and formatted a missing share through numeric arithmetic, which can surface `NaN%` and erase the distinction between unavailable evidence and a measured value.
+
+A finite, non-negative persisted `σ_k`, including `0`, must remain buyer-visible when share is unavailable. A finite persisted share must remain visible when `σ_k` is unavailable. Neither measurement may be reconstructed, normalized, clamped, or inferred from the other.
+
+## Decision
+
+Define one LineageWeave-owned report-axis badge projection with four states:
+
+- combined: `leftover axis {axis} σ {value} {share}%`;
+- singular only: `leftover axis {axis} σ {value}`;
+- share only: `leftover axis {axis} {share}%`;
+- neither usable: omit the badge.
+
+The projection consumes persisted axis values only. `σ_k` is accepted only when finite and non-negative; `σ=0` remains explicit. Share uses the existing finite-value formatter. Missing or invalid evidence fails closed independently.
+
+This decision changes presentation composition only. It does not add persistence, cross-service SQL, psychometric estimation, or source copies from fast-mlsirm, TEPP, or another canonical owner.
+
+## Consequences
+
+The report no longer needs to represent missing share as `NaN%`, and a singular-only axis remains inspectable. Existing combined and share-only buyer copy remains unchanged. The helper is a deterministic projection that can be unit-tested without synthetic domain data.
+
+This ADR remains **Proposed** until the exact-head buyer path actually consumes the projection and fresh frontend, browser/accessibility, security, and independent-review evidence is current.
+
+## 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
From e3cce3815ad7c585f114e5b0c1132ad5fa915574 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 17:29:47 +0900
Subject: [PATCH 10/21] docs(changelog): record singular-only report axis
evidence
---
CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md
diff --git a/CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md b/CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md
new file mode 100644
index 000000000..e8b7973de
--- /dev/null
+++ b/CHANGELOG.d/2.82.0-leftover-map-axis-singular-only.md
@@ -0,0 +1,3 @@
+### Fixed
+
+- Report leftover-axis evidence now has an explicit singular-only state so a valid persisted Gabriel `σ` remains visible when axis share is unavailable; share-only, combined, and empty states remain independent and missing evidence is not rendered as `NaN%`.
From 7c570b484b4298fbd0a699003f05980aee6235e8 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 23:50:44 +0900
Subject: [PATCH 11/21] test(reports): expose unwired report-axis singular-only
RED
---
...est_leftover_axis_report_singular_only_contract.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/tests/test_leftover_axis_report_singular_only_contract.py b/tests/test_leftover_axis_report_singular_only_contract.py
index c02a27f3c..6357a3ca9 100644
--- a/tests/test_leftover_axis_report_singular_only_contract.py
+++ b/tests/test_leftover_axis_report_singular_only_contract.py
@@ -5,6 +5,7 @@
ROOT = Path(__file__).resolve().parents[1]
BADGE_SOURCE = ROOT / "frontend" / "src" / "leftoverMapAxisBadge.ts"
+APP_SOURCE = ROOT / "frontend" / "src" / "App.tsx"
def test_report_axis_badge_preserves_singular_when_share_is_missing() -> None:
@@ -31,3 +32,13 @@ def test_report_axis_badge_keeps_sigma_and_share_missingness_independent() -> No
assert "LEFTOVER_MAP_AXIS_BADGE_SHARE" in source
assert "LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY" in source
assert "Math.sqrt" not in source
+
+
+def test_report_axis_rendering_consumes_the_independent_badge_projection() -> None:
+ """The report UI must render the four-state projection instead of rebuilding σ/share coupling."""
+ app_source = APP_SOURCE.read_text(encoding="utf-8")
+
+ assert "leftoverMapAxisBadge," in app_source
+ assert "const badge = leftoverMapAxisBadge(axis);" in app_source
+ assert "badge === null ? null" in app_source
+ assert "tf(badge.template, badge.values)" in app_source
From 3422a234a5bfe885040000492f3a2f901c769c88 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 23:52:18 +0900
Subject: [PATCH 12/21] fix(reports): keep report-axis sigma visible without
share
---
frontend/src/leftoverMapAxisBadge.ts | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/frontend/src/leftoverMapAxisBadge.ts b/frontend/src/leftoverMapAxisBadge.ts
index 1511b115c..4a00d425f 100644
--- a/frontend/src/leftoverMapAxisBadge.ts
+++ b/frontend/src/leftoverMapAxisBadge.ts
@@ -7,9 +7,9 @@ import {
leftoverSingularForAxis,
} from "./leftoverMapPlotAxisSingular";
-export const LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis} {share}%";
+export const LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis}{share}";
-export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value} {share}%";
+export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value}{share}";
export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY = "leftover axis {axis} σ {value}";
@@ -18,10 +18,16 @@ export type LeftoverMapAxisBadge = {
values: Record;
};
+/**
+ * Format report-axis share as an optional suffix consumed directly by the
+ * report template. Missing or non-finite share stays absent instead of
+ * leaking a synthetic `NaN%` value into buyer-visible evidence.
+ */
export function leftoverMapAxisBadgeShare(
leftoverShare: LeftoverMapAxis["leftover_share"] | null | undefined,
): string {
- return ((leftoverShare ?? Number.NaN) * 100).toFixed(0);
+ const share = formatLeftoverMapPlotAxisShare(leftoverShare);
+ return share === null ? "" : ` ${share}%`;
}
export function leftoverMapAxisBadgeSingular(
@@ -42,18 +48,18 @@ export function leftoverMapAxisBadge(
},
): LeftoverMapAxisBadge | null {
const singular = leftoverMapAxisBadgeSingular(axis);
- const share = formatLeftoverMapPlotAxisShare(axis.leftover_share);
+ const share = leftoverMapAxisBadgeShare(axis.leftover_share);
- if (singular === null && share === null) {
+ if (singular === null && share === "") {
return null;
}
- if (singular === null && share !== null) {
+ if (singular === null) {
return {
template: LEFTOVER_MAP_AXIS_BADGE_SHARE,
values: { axis: axis.axis_index, share },
};
}
- if (singular !== null && share === null) {
+ if (share === "") {
return {
template: LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY,
values: { axis: axis.axis_index, value: singular },
@@ -61,6 +67,6 @@ export function leftoverMapAxisBadge(
}
return {
template: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
- values: { axis: axis.axis_index, value: singular as string, share: share as string },
+ values: { axis: axis.axis_index, value: singular, share },
};
}
From 2efcf06184dcee7815233465fcd9efa32d65aabe Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 23:52:48 +0900
Subject: [PATCH 13/21] test(reports): cover report-axis missingness states
---
frontend/src/leftoverMapAxisBadge.test.ts | 63 ++++++++++++++++++++---
1 file changed, 55 insertions(+), 8 deletions(-)
diff --git a/frontend/src/leftoverMapAxisBadge.test.ts b/frontend/src/leftoverMapAxisBadge.test.ts
index 579273f88..66d5d099c 100644
--- a/frontend/src/leftoverMapAxisBadge.test.ts
+++ b/frontend/src/leftoverMapAxisBadge.test.ts
@@ -1,9 +1,11 @@
import { describe, expect, it } from "vitest";
import {
+ leftoverMapAxisBadge,
leftoverMapAxisBadgeShare,
leftoverMapAxisBadgeSingular,
LEFTOVER_MAP_AXIS_BADGE_SHARE,
LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY,
} from "./leftoverMapAxisBadge";
import {
leftoverMapCompareAxisBadge,
@@ -17,10 +19,13 @@ import {
} from "./leftoverMapPlotAxisSingular";
describe("leftoverMapAxisBadgeShare", () => {
- it("formats leftover-axis share percent without inventing a leftover score", () => {
- expect(leftoverMapAxisBadgeShare(0.82)).toBe("82");
- expect(leftoverMapAxisBadgeShare(0.18)).toBe("18");
- expect(leftoverMapAxisBadgeShare(0)).toBe("0");
+ it("formats report-axis share as an optional suffix", () => {
+ expect(leftoverMapAxisBadgeShare(0.82)).toBe(" 82%");
+ expect(leftoverMapAxisBadgeShare(0.18)).toBe(" 18%");
+ expect(leftoverMapAxisBadgeShare(0)).toBe(" 0%");
+ expect(leftoverMapAxisBadgeShare(undefined)).toBe("");
+ expect(leftoverMapAxisBadgeShare(Number.NaN)).toBe("");
+ expect(leftoverMapAxisBadgeShare(Number.POSITIVE_INFINITY)).toBe("");
});
});
@@ -55,16 +60,58 @@ describe("leftoverMapAxisBadgeSingular", () => {
expect(leftoverMapAxisBadgeSingular({ axis_index: 1 })).toBeNull();
});
- it("stays distinct from leftover-map graphic and comparison graphic leftover-map axis σ copy", () => {
- expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).toBe("leftover axis {axis} σ {value} {share}%");
- expect(LEFTOVER_MAP_AXIS_BADGE_SHARE).toBe("leftover axis {axis} {share}%");
+ it("keeps report copy distinct from plot and comparison copy", () => {
+ expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).toBe("leftover axis {axis} σ {value}{share}");
+ expect(LEFTOVER_MAP_AXIS_BADGE_SHARE).toBe("leftover axis {axis}{share}");
+ expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY).toBe("leftover axis {axis} σ {value}");
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR);
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE);
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR);
expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe(
LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE,
);
- expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe("leftover axis {axis} σ {value}");
+ });
+});
+
+describe("leftoverMapAxisBadge", () => {
+ it("keeps singular-only, share-only, combined, and empty states independent", () => {
+ expect(
+ leftoverMapAxisBadge({
+ axis_index: 1,
+ leftover_singular_value: 1.24,
+ leftover_share: 0.42,
+ }),
+ ).toEqual({
+ template: LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
+ values: { axis: 1, value: "1.24", share: " 42%" },
+ });
+ expect(
+ leftoverMapAxisBadge({
+ axis_index: 1,
+ leftover_singular_value: 0,
+ leftover_share: null,
+ }),
+ ).toEqual({
+ template: LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY,
+ values: { axis: 1, value: "0.00" },
+ });
+ expect(
+ leftoverMapAxisBadge({
+ axis_index: 2,
+ leftover_singular_value: null,
+ leftover_share: 0.18,
+ }),
+ ).toEqual({
+ template: LEFTOVER_MAP_AXIS_BADGE_SHARE,
+ values: { axis: 2, share: " 18%" },
+ });
+ expect(
+ leftoverMapAxisBadge({
+ axis_index: 2,
+ leftover_singular_value: Number.NaN,
+ leftover_share: Number.POSITIVE_INFINITY,
+ }),
+ ).toBeNull();
});
});
From c267657779dce655e6a7a604e93a9799d395cc2f Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Tue, 15 Sep 2026 23:53:02 +0900
Subject: [PATCH 14/21] test(reports): pin report-axis sigma/share rendering
contract
---
...over_axis_report_singular_only_contract.py | 22 ++++++++++++-------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/tests/test_leftover_axis_report_singular_only_contract.py b/tests/test_leftover_axis_report_singular_only_contract.py
index 6357a3ca9..3a68fe925 100644
--- a/tests/test_leftover_axis_report_singular_only_contract.py
+++ b/tests/test_leftover_axis_report_singular_only_contract.py
@@ -17,8 +17,10 @@ def test_report_axis_badge_preserves_singular_when_share_is_missing() -> None:
'LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY = "leftover axis {axis} σ {value}"'
in source
)
- assert "leftoverMapAxisBadge(" in source
- assert "LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY" in source
+ assert 'LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value}{share}"' in source
+ assert "formatLeftoverMapPlotAxisShare" in source
+ assert 'return share === null ? "" : ` ${share}%`;' in source
+ assert "Number.NaN" not in source
def test_report_axis_badge_keeps_sigma_and_share_missingness_independent() -> None:
@@ -31,14 +33,18 @@ def test_report_axis_badge_keeps_sigma_and_share_missingness_independent() -> No
assert "LEFTOVER_MAP_AXIS_BADGE_SINGULAR" in source
assert "LEFTOVER_MAP_AXIS_BADGE_SHARE" in source
assert "LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY" in source
+ assert 'singular === null && share === ""' in source
+ assert 'share === ""' in source
assert "Math.sqrt" not in source
-def test_report_axis_rendering_consumes_the_independent_badge_projection() -> None:
- """The report UI must render the four-state projection instead of rebuilding σ/share coupling."""
+def test_report_axis_rendering_consumes_the_repaired_formatting_primitives() -> None:
+ """The report path must consume helpers whose missing share no longer becomes `NaN%`."""
app_source = APP_SOURCE.read_text(encoding="utf-8")
- assert "leftoverMapAxisBadge," in app_source
- assert "const badge = leftoverMapAxisBadge(axis);" in app_source
- assert "badge === null ? null" in app_source
- assert "tf(badge.template, badge.values)" in app_source
+ assert "leftoverMapAxisBadgeShare," in app_source
+ assert "leftoverMapAxisBadgeSingular," in app_source
+ assert "const singular = leftoverMapAxisBadgeSingular(axis);" in app_source
+ assert "const share = leftoverMapAxisBadgeShare(axis.leftover_share);" in app_source
+ assert "LEFTOVER_MAP_AXIS_BADGE_SHARE" in app_source
+ assert "LEFTOVER_MAP_AXIS_BADGE_SINGULAR" in app_source
From e98abe27eb01f40c2162bffedca14b0eb03516ec Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 16 Sep 2026 00:17:35 +0900
Subject: [PATCH 15/21] test(reports): require empty report-axis evidence
omission
---
...eftover_axis_report_singular_only_contract.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/tests/test_leftover_axis_report_singular_only_contract.py b/tests/test_leftover_axis_report_singular_only_contract.py
index 3a68fe925..873f2e04b 100644
--- a/tests/test_leftover_axis_report_singular_only_contract.py
+++ b/tests/test_leftover_axis_report_singular_only_contract.py
@@ -38,13 +38,13 @@ def test_report_axis_badge_keeps_sigma_and_share_missingness_independent() -> No
assert "Math.sqrt" not in source
-def test_report_axis_rendering_consumes_the_repaired_formatting_primitives() -> None:
- """The report path must consume helpers whose missing share no longer becomes `NaN%`."""
+def test_report_axis_rendering_consumes_the_four_state_projection() -> None:
+ """The report path must omit a badge when neither persisted axis datum is usable."""
app_source = APP_SOURCE.read_text(encoding="utf-8")
- assert "leftoverMapAxisBadgeShare," in app_source
- assert "leftoverMapAxisBadgeSingular," in app_source
- assert "const singular = leftoverMapAxisBadgeSingular(axis);" in app_source
- assert "const share = leftoverMapAxisBadgeShare(axis.leftover_share);" in app_source
- assert "LEFTOVER_MAP_AXIS_BADGE_SHARE" in app_source
- assert "LEFTOVER_MAP_AXIS_BADGE_SINGULAR" in app_source
+ assert "leftoverMapAxisBadge," in app_source
+ assert "const badge = leftoverMapAxisBadge(axis);" in app_source
+ assert "badge === null ? null" in app_source
+ assert "tf(badge.template, badge.values)" in app_source
+ assert "leftoverMapAxisBadgeShare," not in app_source
+ assert "leftoverMapAxisBadgeSingular," not in app_source
From ee04d469a3ab869d3ad5a4a3f732e4791cb219ed Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 16 Sep 2026 01:04:36 +0900
Subject: [PATCH 16/21] chore(stack): adopt #863 localized comparison tick
contract
---
..._grouping_comparison_graphic_tick_contract.py | 16 +++++-----------
1 file changed, 5 insertions(+), 11 deletions(-)
diff --git a/tests/test_grouping_comparison_graphic_tick_contract.py b/tests/test_grouping_comparison_graphic_tick_contract.py
index 92ddc2fbd..d0c3af61e 100644
--- a/tests/test_grouping_comparison_graphic_tick_contract.py
+++ b/tests/test_grouping_comparison_graphic_tick_contract.py
@@ -10,23 +10,17 @@
def test_comparison_graphic_names_ticks_with_distinct_accessible_copy() -> None:
- """Comparison tick names must not reuse the report-graphic tick key."""
+ """Comparison ticks compose existing localized comparison and tick copy."""
plot_source = PLOT_SOURCE.read_text(encoding="utf-8")
- layout_source = LAYOUT_SOURCE.read_text(encoding="utf-8")
- assert (
- 'export const LEFTOVER_MAP_COMPARE_PLOT_TICK =\n'
- ' "leftover map comparison graphic leftover-map axis {axis} tick {value}";'
- in layout_source
- )
- assert "LEFTOVER_MAP_COMPARE_PLOT_TICK" in plot_source
+ assert "LEFTOVER_MAP_COMPARE_PLOT_LABEL" in plot_source
+ assert "LEFTOVER_MAP_PLOT_TICK" in plot_source
assert re.search(
- r'variant\s*===\s*"comparison"\s*\?\s*LEFTOVER_MAP_COMPARE_PLOT_TICK\s*:\s*LEFTOVER_MAP_PLOT_TICK',
+ r'variant\s*===\s*"comparison"\s*\?\s*`\$\{t\(LEFTOVER_MAP_COMPARE_PLOT_LABEL\)\}:\s*\$\{tf\(LEFTOVER_MAP_PLOT_TICK,\s*\{\s*axis:\s*tick\.axis,\s*value:\s*tick\.label,?\s*\}\)\}`\s*:\s*tf\(LEFTOVER_MAP_PLOT_TICK,\s*\{\s*axis:\s*tick\.axis,\s*value:\s*tick\.label,?\s*\}\)',
plot_source,
re.DOTALL,
)
- assert "axis: tick.axis" in plot_source
- assert "value: tick.label" in plot_source
+ assert "LEFTOVER_MAP_COMPARE_PLOT_TICK" not in plot_source
def test_tick_positions_come_from_persisted_coordinates_not_distance() -> None:
From 7e9e83b0b71ecfd3cb329a7f1eba023cc4d2b656 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Wed, 16 Sep 2026 01:05:32 +0900
Subject: [PATCH 17/21] chore(stack): adopt #863 localized comparison tick
repair
---
frontend/src/components/LeftoverMapPlot.tsx | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx
index 435d92fe8..9a055d076 100644
--- a/frontend/src/components/LeftoverMapPlot.tsx
+++ b/frontend/src/components/LeftoverMapPlot.tsx
@@ -211,8 +211,10 @@ function leftoverMapPlotAxisText(
* comparison-graphic and rank labels rather than adding SPA translation debt.
* ADR 0319 captions leftover-map distance on that comparison graphic using
* the same localized composition boundary rather than adding another static
- * comparison-only translation key. ADR 0321 adds persisted comparison-axis
- * singular evidence without deriving it from axis share.
+ * comparison-only translation key. ADR 0320 names comparison coordinate ticks
+ * through the same composition boundary, preserving localized generic tick copy.
+ * ADR 0321 adds persisted comparison-axis singular evidence without deriving it
+ * from axis share.
* Never invent a leftover score.
*/
export function LeftoverMapPlot({
@@ -355,7 +357,14 @@ export function LeftoverMapPlot({
Date: Wed, 16 Sep 2026 09:02:18 +0900
Subject: [PATCH 18/21] test(reports): assert four-state badge semantics, not
import punctuation
---
tests/test_leftover_axis_report_singular_only_contract.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/test_leftover_axis_report_singular_only_contract.py b/tests/test_leftover_axis_report_singular_only_contract.py
index 873f2e04b..2232a8d8c 100644
--- a/tests/test_leftover_axis_report_singular_only_contract.py
+++ b/tests/test_leftover_axis_report_singular_only_contract.py
@@ -42,9 +42,9 @@ def test_report_axis_rendering_consumes_the_four_state_projection() -> None:
"""The report path must omit a badge when neither persisted axis datum is usable."""
app_source = APP_SOURCE.read_text(encoding="utf-8")
- assert "leftoverMapAxisBadge," in app_source
+ assert "leftoverMapAxisBadge" in app_source
assert "const badge = leftoverMapAxisBadge(axis);" in app_source
assert "badge === null ? null" in app_source
assert "tf(badge.template, badge.values)" in app_source
- assert "leftoverMapAxisBadgeShare," not in app_source
- assert "leftoverMapAxisBadgeSingular," not in app_source
+ assert "leftoverMapAxisBadgeShare" not in app_source
+ assert "leftoverMapAxisBadgeSingular" not in app_source
From 6935ac4d3850ae24d23fab76be70fe22102ef4ee Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 02:55:50 +0000
Subject: [PATCH 19/21] fix(reports): consume report-axis badge projection
---
frontend/src/App.tsx | 20 ++++----------------
1 file changed, 4 insertions(+), 16 deletions(-)
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index ecd927785..ce2842434 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -137,12 +137,7 @@ import {
LEFTOVER_MAP_PLOT_INCOMPLETE_POST,
LEFTOVER_MAP_PLOT_ITEM_COVERAGE,
} from "./leftoverMapCoverage";
-import {
- leftoverMapAxisBadgeShare,
- leftoverMapAxisBadgeSingular,
- LEFTOVER_MAP_AXIS_BADGE_SHARE,
- LEFTOVER_MAP_AXIS_BADGE_SINGULAR,
-} from "./leftoverMapAxisBadge";
+import { leftoverMapAxisBadge } from "./leftoverMapAxisBadge";
import {
formatLeftoverMapReconstruction,
LEFTOVER_MAP_COMPARE_RECONSTRUCTION_LABEL,
@@ -3930,17 +3925,10 @@ function ReportsPanel({
) : null}
{report.leftover_map_axes?.map((axis) => {
- const singular = leftoverMapAxisBadgeSingular(axis);
- const share = leftoverMapAxisBadgeShare(axis.leftover_share);
- return (
+ const badge = leftoverMapAxisBadge(axis);
+ return badge === null ? null : (
- {singular === null
- ? tf(LEFTOVER_MAP_AXIS_BADGE_SHARE, { axis: axis.axis_index, share })
- : tf(LEFTOVER_MAP_AXIS_BADGE_SINGULAR, {
- axis: axis.axis_index,
- value: singular,
- share,
- })}
+ {tf(badge.template, badge.values)}
);
})}
From 0c13fd833cab9498d4e3739a0adf36c9bb62f371 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 19 Sep 2026 23:26:51 +0900
Subject: [PATCH 20/21] test(reporting): verify report-axis copy semantically
---
..._leftover_map_report_graphic_axis_singular_contract.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/tests/test_leftover_map_report_graphic_axis_singular_contract.py b/tests/test_leftover_map_report_graphic_axis_singular_contract.py
index 211081f4c..10ecc77f2 100644
--- a/tests/test_leftover_map_report_graphic_axis_singular_contract.py
+++ b/tests/test_leftover_map_report_graphic_axis_singular_contract.py
@@ -14,12 +14,8 @@ def test_report_graphic_axis_has_distinct_singular_and_singular_share_copy() ->
source = SINGULAR_SOURCE.read_text(encoding="utf-8")
plot_source = PLOT_SOURCE.read_text(encoding="utf-8")
- assert 'LEFTOVER_MAP_PLOT_AXIS_SINGULAR = "leftover-map axis {axis} σ {value}"' in source
- assert (
- 'LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE =\n'
- ' "leftover-map axis {axis} σ {value} ({share}%)"'
- in source
- )
+ assert '"leftover-map axis {axis} σ {value}"' in source
+ assert '"leftover-map axis {axis} σ {value} ({share}%)"' in source
assert "LEFTOVER_MAP_PLOT_AXIS_SINGULAR" in plot_source
assert "LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE" in plot_source
From 6f75308695fd110a5ebf9919d10a0d5493a543da Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 19 Sep 2026 23:27:14 +0900
Subject: [PATCH 21/21] test(reporting): align report badge contract with
independent share suffix
---
tests/test_leftover_axis_report_singular_contract.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/tests/test_leftover_axis_report_singular_contract.py b/tests/test_leftover_axis_report_singular_contract.py
index 5319a7113..192efc04f 100644
--- a/tests/test_leftover_axis_report_singular_contract.py
+++ b/tests/test_leftover_axis_report_singular_contract.py
@@ -13,13 +13,12 @@ def test_report_axis_badge_keeps_singular_value_and_share_semantically_distinct(
assert BADGE_SOURCE.exists(), "report-axis singular-value badge helper is missing"
badge_source = BADGE_SOURCE.read_text(encoding="utf-8")
- assert 'LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis} {share}%"' in badge_source
- assert (
- 'LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value} {share}%"'
- in badge_source
- )
+ assert 'LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis}{share}"' in badge_source
+ assert 'LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value}{share}"' in badge_source
+ assert 'LEFTOVER_MAP_AXIS_BADGE_SINGULAR_ONLY = "leftover axis {axis} σ {value}"' in badge_source
assert "leftoverSingularForAxis" in badge_source
assert "formatLeftoverMapPlotAxisSingular" in badge_source
+ assert "formatLeftoverMapPlotAxisShare" in badge_source
assert "leftover map comparison graphic" not in badge_source