From eb172c40d8ecd08f3e1c220cbf3117b488cddb30 Mon Sep 17 00:00:00 2001
From: seonghobae
Date: Sun, 30 Aug 2026 14:14:37 +0000
Subject: [PATCH] feat(reports): leftover-map singular values on leftover-axis
badges (v2.47.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 when finite and non-negative (ADR 0290). Pair-list leftover-axis
badges stay distinct from leftover-map graphic axis captions. Do not invent
σ_k from leftover-map axis share. UI-only; no SQL; no new columns. Never
invent a leftover score. Never invent a theta.
---
AGENTS.md | 18 +-
ARCHITECTURE.md | 5 +-
.../2.47.0-leftover-map-axis-singular.md | 13 ++
CHANGELOG.md | 11 ++
CLAUDE.md | 2 +-
docs/adr/0148-leftover-map-axis-share.md | 4 +-
docs/adr/0290-leftover-map-axis-singular.md | 160 ++++++++++++++++++
docs/product-technical-gap-baseline.md | 57 +++++++
frontend/package.json | 2 +-
frontend/src/App.test.tsx | 4 +-
frontend/src/App.tsx | 31 +++-
frontend/src/i18n.test.ts | 15 +-
frontend/src/i18n.ts | 20 ++-
frontend/src/leftoverMapAxisBadge.test.ts | 38 +++++
frontend/src/leftoverMapAxisBadge.ts | 27 +++
pyproject.toml | 2 +-
16 files changed, 380 insertions(+), 29 deletions(-)
create mode 100644 CHANGELOG.d/2.47.0-leftover-map-axis-singular.md
create mode 100644 docs/adr/0290-leftover-map-axis-singular.md
create mode 100644 frontend/src/leftoverMapAxisBadge.test.ts
create mode 100644 frontend/src/leftoverMapAxisBadge.ts
diff --git a/AGENTS.md b/AGENTS.md
index e877a967c..8f5553841 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) 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) 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
@@ -299,7 +299,11 @@ captions those leftover-map axes with persisted leftover-map singular
values `σ_k` when finite and non-negative, including rank-0 zero
singular values; a missing, non-finite, or negative singular value omits
that `σ` badge independently of leftover-map axis share and does not
-invent `σ_k` from leftover-map axis share. ADR 0270
+invent `σ_k` from leftover-map axis share. ADR 0290 captions leftover-axis
+report badges with persisted leftover-map singular values `σ_k` when finite
+and non-negative, independently of leftover-map graphic axes; a missing,
+non-finite, or negative singular value omits that `σ` badge and keeps
+`leftover axis {k} {share}%`. ADR 0270
ticks leftover-map axes at the origin and at each unique finite
persisted `ξ` / `ζ` coordinate so the pair-row badge matches the
plot; rank-0 unused axes name only `0` and do not invent drawing-scale
@@ -395,6 +399,12 @@ singular values `σ_k` so leftover-map axis share is not read as leftover-map
structure without the Gabriel scale that produced it; a missing, non-finite,
or negative singular value omits that `σ` badge independently of leftover-map
axis share and does not invent `σ_k` from leftover-map axis share.
+ADR 0290 captions leftover-axis report badges with persisted leftover-map
+singular values `σ_k` so leftover-map axis share is not read as leftover-map
+structure without the Gabriel scale that produced it; a missing, non-finite,
+or negative singular value omits that `σ` badge and keeps
+`leftover axis {k} {share}%` and does not invent `σ_k` 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
@@ -407,7 +417,9 @@ residual SVD axes 1 and 2 and persists to `report_leftover_map_axis`.
Rank-0 residuals emit two zero-share axes; the shares are report-level
and are not a leftover score. Leftover-map singular values on the graphic
display (ADR 0289) name persisted `σ_k` on those leftover-map axes when
-finite. Complete-case coverage (ADR 0168) persists to
+finite. Leftover-map singular values on leftover-axis badges (ADR 0290) name
+the same persisted `σ_k` on leftover-axis report badges when finite.
+Complete-case coverage (ADR 0168) persists to
`report_leftover_map_coverage` and captions the pair list and the leftover-map
graphic with how many scored posts entered the map. Pair-list post complete-case
coverage (ADR 0288) fail-closes that pair-list note through leftoverMapCoverageCounts.
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 2025a85e5..4413960de 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings
persist to `report_item_information`. After those IRT main effects,
residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021;
ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 /
-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) persist to `report_leftover_pair` with signed residual `R`,
+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) persist to `report_leftover_pair` with signed residual `R`,
observed `Y`, expected `E[Y|θ, item]`, full leftover-map rank, unexplained
leftover, ADR 0201 reconstruction evidence, ADR 0185 cross-share evidence,
ADR 0233 unexplained leftover share `s`, ADR 0266 explained leftover
@@ -669,7 +669,8 @@ item complete-case coverage. ADR 0286 captions the leftover pair list with persi
incomplete post coverage. ADR 0287 captions the leftover pair list with persisted leftover-map
incomplete item coverage. ADR 0288 fail-closes leftover-map post complete-case coverage
on the leftover pair list through leftoverMapCoverageCounts. ADR 0289 captions leftover-map
-graphic axes with persisted leftover-map singular values `σ_k`. Click a post marker or a pair button
+graphic axes with persisted leftover-map singular values `σ_k`. ADR 0290 captions leftover-axis
+report badges 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
(Gabriel inertia of residual SVD axes 1 and 2; ADR 0148) persists to
diff --git a/CHANGELOG.d/2.47.0-leftover-map-axis-singular.md b/CHANGELOG.d/2.47.0-leftover-map-axis-singular.md
new file mode 100644
index 000000000..020e3c94f
--- /dev/null
+++ b/CHANGELOG.d/2.47.0-leftover-map-axis-singular.md
@@ -0,0 +1,13 @@
+## 2.47.0 — Leftover-map singular values on leftover-axis badges
+
+- Caption leftover-axis report badges with persisted leftover-map singular
+ values `σ_k` (ADR 0290). After `make seed`, leftover-axis badges name
+ persisted leftover-map singular values when finite together with persisted
+ leftover-map axis share, leftover-map graphic axes name those same singular
+ values, leftover-map axis ticks match `ξ (x, y) ζ (x, y)` on the pair row,
+ and leftover pairs sit above the member list; click a leftover pair opens
+ that post. 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`. Pair-list leftover-axis badges stay distinct from leftover-map
+ graphic axis captions. Do not invent `σ_k` from leftover-map axis share.
+ Never invent a leftover score. Never invent a theta. No new columns.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62d09380f..b49f6695f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,17 @@ 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` (ADR 0290 / v2.47.0). After `make seed`,
+ leftover-axis badges name persisted leftover-map singular values when finite
+ together with persisted leftover-map axis share; leftover-map graphic axes
+ name those same singular values; click a leftover pair opens that post. 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`.
+ Pair-list leftover-axis badges stay distinct from leftover-map graphic axis
+ captions. Do not invent `σ_k` from leftover-map axis share. Never invent a
+ leftover score or a theta. No new columns.
+
- Period leftover pairs now caption leftover-map graphic axes with persisted
leftover-map singular values `σ_k` (ADR 0289 / v2.46.0). After `make seed`,
closest and farthest leftover pairs sit above the member list with the
diff --git a/CLAUDE.md b/CLAUDE.md
index 4ce0f23a3..32f806ba9 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), 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), 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 4b4e4d485..2b23e5af3 100644
--- a/docs/adr/0148-leftover-map-axis-share.md
+++ b/docs/adr/0148-leftover-map-axis-share.md
@@ -5,7 +5,9 @@
**Amended by:** [ADR 0269](0269-leftover-map-axis-share-plot.md)
(leftover-map axis share on the graphic display);
[ADR 0289](0289-leftover-map-plot-singular.md)
-(leftover-map singular values on the graphic display)
+(leftover-map singular values on the graphic display);
+[ADR 0290](0290-leftover-map-axis-singular.md)
+(leftover-map singular values on leftover-axis badges)
## Context
diff --git a/docs/adr/0290-leftover-map-axis-singular.md b/docs/adr/0290-leftover-map-axis-singular.md
new file mode 100644
index 000000000..8136e690a
--- /dev/null
+++ b/docs/adr/0290-leftover-map-axis-singular.md
@@ -0,0 +1,160 @@
+# ADR 0290 — Name leftover-map singular values on leftover-axis badges
+
+**Decision status:** Accepted
+**Date:** 2026-08-30
+
+Amends leftover-map axis share persistence
+([ADR 0148](0148-leftover-map-axis-share.md)). Independent of leftover-map
+singular values on the graphic display
+([ADR 0289](0289-leftover-map-plot-singular.md)), fail-close leftover-map post
+complete-case coverage on the pair list
+([ADR 0288](0288-leftover-map-list-post-coverage-helper.md)), leftover-map
+incomplete item coverage on the pair list
+([ADR 0287](0287-leftover-map-list-incomplete-item.md)), leftover-map incomplete
+post coverage on the pair list
+([ADR 0286](0286-leftover-map-list-incomplete-post.md)), leftover-map item
+complete-case coverage on the pair list
+([ADR 0285](0285-leftover-map-list-item-coverage.md)), leftover-map incomplete
+item coverage on the graphic display
+([ADR 0284](0284-leftover-map-plot-incomplete-item.md)), leftover-map incomplete
+post coverage on the graphic display
+([ADR 0283](0283-leftover-map-plot-incomplete.md)), leftover-map item
+complete-case coverage on the graphic display
+([ADR 0282](0282-leftover-map-plot-item-coverage.md)), leftover-map complete-case
+coverage on the graphic display
+([ADR 0281](0281-leftover-map-plot-coverage.md)), leftover-map rank on pair
+segments ([ADR 0280](0280-leftover-map-segment-rank.md)), leftover expected on pair
+segments ([ADR 0279](0279-leftover-map-segment-expected.md)), leftover observed on
+pair segments ([ADR 0278](0278-leftover-map-segment-observed.md)), leftover
+residual on pair segments ([ADR 0277](0277-leftover-map-segment-residual.md)),
+leftover-map unexplained leftover on pair segments
+([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map
+cross share on pair segments ([ADR 0275](0275-leftover-map-segment-cross-share.md)),
+leftover-map unexplained leftover share on pair segments
+([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map
+explained leftover share on pair segments
+([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map
+reconstruction on pair segments
+([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map
+distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)),
+leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)),
+leftover-map axis share on the graphic display
+([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual
+disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover
+observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)),
+leftover-map explained leftover share persistence
+([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map
+unexplained leftover share persistence
+([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map
+reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)),
+leftover-map cross share persistence
+([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover
+persistence ([ADR 0182](0182-leftover-map-unexplained.md)), leftover-map rank
+persistence ([ADR 0164](0164-leftover-map-rank.md)), leftover-map complete-case
+coverage persistence ([ADR 0168](0168-leftover-map-complete-case-coverage.md)),
+and leftover-map graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)).
+
+## Context
+
+ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map
+axis share `σ_k² / Σ_j σ_j²` and captions leftover-axis report badges with
+share only. ADR 0289 already captions leftover-map graphic axes with persisted
+`σ_k`. Those leftover-axis badges still hide `σ_k`, so Gabriel (1971)
+singular values appear on the plot and never on the report-level leftover-axis
+badges that name the same axes. Hiding `σ_k` on the badges 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.
+
+This increment captions leftover-axis report badges with already persisted
+leftover-map singular values. It does not add columns. It does not invent
+`σ_k` from leftover-map axis share. It does not persist leftover-map inner
+product, cosine, or length. It does not land Post quality on the leftover
+criterion. Leftover-map distance stays two-axis Euclidean. Do not invent a
+leftover score. Do not invent a theta.
+
+The dashboard stack already used neighbouring leftover facts under other
+numbers. This protected increment uses **0290** so it does not collide with
+leftover-map singular values on the graphic display (0289), fail-close
+leftover-map post complete-case coverage on the pair list (0288), leftover-map
+incomplete item coverage on the pair list (0287), leftover-map incomplete post
+coverage on the pair list (0286), leftover-map item complete-case coverage on
+the pair list (0285), leftover-map incomplete item coverage on the graphic
+display (0284), leftover-map incomplete post coverage on the graphic display
+(0283), leftover-map item complete-case coverage on the graphic display (0282),
+leftover-map complete-case coverage on the graphic display (0281), leftover-map
+rank on pair segments (0280), leftover expected on pair segments (0279),
+leftover observed on pair segments (0278), leftover residual on pair segments
+(0277), leftover-map unexplained leftover on pair segments (0276), leftover-map
+cross share on pair segments (0275), leftover-map unexplained leftover share on
+pair segments (0274), leftover-map explained leftover share on pair segments
+(0273), leftover-map reconstruction on pair segments (0272), leftover-map
+distance on pair segments (0271), leftover-map coordinate ticks (0270),
+leftover-map axis share on the graphic display (0269), leftover-map graphic
+display (0268), leftover-map coordinates (0267 / migration 0245), leftover-map
+explained leftover share persistence (0266 / migration 0244), leftover-map
+unexplained leftover share persistence (0233 / migration 0233), leftover-map
+reconstruction persistence (0201 / migration 0206), leftover-map cross share
+(0185), leftover residual disclosure, leftover observed `Y` / expected `E`,
+leftover-map rank, two-axis leftover-map distance, leftover coverage,
+leftover-map axis share persistence (0148), leftover interaction-map
+persistence, occupational construct catalog search (0265), 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. Pair-list labels stay
+distinct from graphic labels: leftover-axis badges stay
+`leftover axis {k} σ {value} {share}%`; leftover-map graphic axes stay
+`leftover-map axis {k} σ {value} ({share}%)`. A missing, non-finite, or
+negative singular value omits that `σ` badge and keeps the existing
+`leftover 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. The grouping comparison strip (ADR 0149) stays on its reduced
+leftover payload and does not gain this caption.
+
+Do not add SQL. Do not edit shipped migrations. Do not persist inner
+product, cosine, or length as separate columns. Do not invent a leftover
+score. Do not invent a theta.
+
+## Consequences
+
+After `make seed`, leftover-axis report badges 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 `σ 0.00`. Leftover-map graphic axes (ADR 0289) remain.
+
+## Related
+
+Independent of leftover interaction-map persistence, leftover-criterion
+evaluation landing, leftover residual disclosure, leftover observed
+`Y` / expected `E`, leftover-map complete-case coverage, leftover-map
+axis share persistence, leftover pairs on the grouping comparison
+strip, two-axis leftover-map distance, leftover-map rank, leftover-map
+inner product, leftover-map cosine, leftover-map length, leftover-map
+reconstruction, leftover-map unexplained leftover, leftover-map cross
+share, leftover-map unexplained leftover share, leftover-map explained
+leftover share, leftover-map coordinate persistence, leftover-map
+graphic display, leftover-map coordinate ticks, leftover-map axis share
+on the graphic display, leftover-map complete-case coverage on the graphic
+display, leftover-map item complete-case coverage on the graphic display,
+leftover-map incomplete post coverage on the graphic display, leftover-map
+incomplete item coverage on the graphic display, leftover-map item
+complete-case coverage on the pair list, leftover-map incomplete post
+coverage on the pair list, leftover-map incomplete item coverage on the
+pair list, fail-close leftover-map post complete-case coverage on the
+pair list, and leftover-map singular values on the graphic display.
+
+## 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 b79f4be8b..c26659310 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,62 @@
# Product & Technical Gap Baseline
+> Exact-head loop overlay: 2026-08-30 22:45 KST. Protected `main` is
+> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
+> of leftover-map coordinates (v2.24.0 / #782). Revert #808
+> (`revert-pr782-unauthorized` @ `1af3e53e`) restores `main` toward
+> `fc13acaa` (v2.23.0) and still needs independent APPROVE. GitHub
+> writes work (comment/close/create-PR/push). Token still has empty
+> `X-OAuth-Scopes`; repo permission is ADMIN. Open leftover stack
+> still lacks independent APPROVE. Do not self-approve.
+> #782 leftover-map coordinates through pair-segment distance
+> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained
+> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29
+> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair
+> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross
+> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map
+> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is
+> `e3fdfd74`. #806 leftover residual `R` on pair segments (v2.34.0 /
+> ADR 0277) is `33f6c3dd`. #809 leftover observed `Y` on pair segments
+> (v2.35.0 / ADR 0278) is `b334b00e`. #810 leftover expected `E` on pair
+> segments (v2.36.0 / ADR 0279) is `6e37757a`. #811 leftover-map rank on
+> pair segments (v2.37.0 / ADR 0280) is `e626a1d0`. #812 leftover-map
+> complete-case coverage on the graphic (v2.38.0 / ADR 0281) is `64964cb6`.
+> #813 leftover-map item complete-case coverage on the graphic (v2.39.0 /
+> ADR 0282) is `de2a8a8b`. #814 leftover-map incomplete post coverage on
+> the graphic (v2.40.0 / ADR 0283) is `77292872`. #815 leftover-map
+> incomplete item coverage on the graphic (v2.41.0 / ADR 0284) is
+> `63092de`. #816 leftover-map item complete-case coverage on the pair
+> list (v2.42.0 / ADR 0285) is `1e3d13e`. #817 leftover-map incomplete
+> post coverage on the pair list (v2.43.0 / ADR 0286) is `ef30930`.
+> #818 leftover-map incomplete item coverage on the pair list
+> (v2.44.0 / ADR 0287) is `aa32089`. #819 leftover-map post complete-case
+> coverage fail-closed on the pair list (v2.45.0 / ADR 0288) is `a2c96551`.
+> #820 leftover-map singular values on graphic axes (v2.46.0 / ADR 0289)
+> is `b7222713`. Leave
+> #782/#802/#803/#804/#805/#806/#808/#809/#810/#811/#812/#813/#814/#815/#816/#817/#818/#819/#820 open for
+> independent review. Do not squash-merge stacked leftover PRs onto an
+> unprotected leftover base. Do not merge #808 without independent APPROVE.
+> Strix on #782 failed closed at `Run Strix (quick)`; do not weaken
+> fail-closed on `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError`
+> classifier is already on ContextualWisdomLab/.github. Copilot review is
+> not independent APPROVE. Issues #79 and #87 stay open. #96 is already
+> closed. Only collaborator is `seonghobae`; no independent reviewer can
+> be requested from this token.
+>
+> Next buyer increment on this cycle: leftover-map singular values on
+> leftover-axis badges (ADR 0290 / v2.47.0) delivered locally on
+> `feat/leftover-map-axis-singular-v2470`. Caption leftover-axis report
+> badges with persisted leftover-map singular values `σ_k` when finite and
+> non-negative. UI-only; no new columns. Missing, non-finite, or negative
+> singular values omit that `σ` badge and keep `leftover axis {k} {share}%`.
+> Rank-0 unused axes still name `σ 0.00`. Pair-list leftover-axis badges stay
+> distinct from leftover-map graphic axis captions. Do not invent `σ_k` from
+> leftover-map axis share. Do not invent leftover scores. Stack onto leftover
+> branch `feat/leftover-map-plot-singular-v2460` / #820; leave the PR open
+> for independent review. Do not squash-merge onto the leftover base. Do not
+> persist leftover-map inner product, cosine, or length as separate columns.
+> Grouping comparison strip (ADR 0149) does not gain this caption.
+
> Exact-head loop overlay: 2026-08-30 22:37 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash
> of leftover-map coordinates (v2.24.0 / #782). Revert #808
diff --git a/frontend/package.json b/frontend/package.json
index 32a794de7..607d4f001 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.46.0",
+ "version": "2.47.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 40d17fe8e..465e7de6c 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -4127,8 +4127,8 @@ describe("App, authenticated", () => {
expect(screen.getByText(/TEST-PU-REPORT/)).toBeInTheDocument();
expect(screen.getAllByText("shared metric").length).toBeGreaterThan(0);
expect(screen.getAllByText(/CAT: sales-lead I=0\.70/).length).toBeGreaterThan(0);
- expect(screen.getAllByText(/leftover axis 1 82%/).length).toBeGreaterThan(0);
- expect(screen.getAllByText(/leftover axis 2 18%/).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.getByLabelText("Leftover-map axis share")).toHaveTextContent(
"Open a leftover pair to read the post–criterion cell",
);
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b17ee11ac..b943d1c4b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -133,6 +133,12 @@ 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 "./App.css";
const AdminPanel = lazy(() => import("./components/AdminPanel").then((module) => ({ default: module.AdminPanel })));
@@ -3880,18 +3886,25 @@ function ReportsPanel({
{tf(LEFTOVER_MAP_PLOT_INCOMPLETE_ITEM, incompleteItemCount)}
) : null}
- {report.leftover_map_axes?.map((axis) => (
-
- {tf("leftover axis {axis} {share}%", {
- axis: axis.axis_index,
- share: (axis.leftover_share * 100).toFixed(0),
- })}
-
- ))}
+ {report.leftover_map_axes?.map((axis) => {
+ const singular = leftoverMapAxisBadgeSingular(axis);
+ const share = leftoverMapAxisBadgeShare(axis.leftover_share);
+ 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,
+ })}
+
+ );
+ })}
{report.leftover_map_axes && report.leftover_map_axes.length > 0 && (
{t(
- "Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.",
+ "Leftover-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/i18n.test.ts b/frontend/src/i18n.test.ts
index 31d2cefd9..e2fe908f7 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -43,8 +43,9 @@ describe("i18n", () => {
"Page",
"Answer",
"leftover axis {axis} {share}%",
+ "leftover axis {axis} σ {value} {share}%",
"Leftover-map axis share",
- "Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.",
+ "Leftover-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.",
"Leftover pairs",
"Closest leftover",
"Farthest leftover",
@@ -588,6 +589,18 @@ describe("i18n", () => {
).toBe(expected);
});
+ it.each([
+ ["ko", "잔차 축 1 σ 1.84 82%"],
+ ["zh", "残差轴 1 σ 1.84 82%"],
+ ["ja", "残差軸 1 σ 1.84 82%"],
+ ["vi", "trục phần dư 1 σ 1.84 82%"],
+ ] as const)("formats leftover-axis badge singular value with share in %s", (locale, expected) => {
+ setLocale(locale);
+ expect(
+ tf("leftover 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 d80e81349..26c5a27d5 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -566,9 +566,10 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "유효 시작",
"Valid to": "유효 종료",
"leftover axis {axis} {share}%": "잔차 축 {axis} {share}%",
+ "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. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.":
- "잔차 지도 축 비율은 잔차 SVD 축 1과 2의 Gabriel 관성입니다. 글–기준 셀을 읽으려면 잔차 쌍을 여세요. 이 비율은 잔차 점수를 만들어내지 않습니다.",
+ "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.":
+ "잔차 지도 축 비율은 잔차 SVD 축 1과 2의 Gabriel 관성입니다. 잔차 지도 특이값은 그 축의 Gabriel 척도입니다. 글–기준 셀을 읽으려면 잔차 쌍을 여세요. 이 비율과 특이값은 잔차 점수를 만들어내지 않습니다.",
"Leftover pairs": "잔여 쌍",
"Closest leftover": "가장 가까운 잔여",
"Farthest leftover": "가장 먼 잔여",
@@ -1170,9 +1171,10 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "有效起始",
"Valid to": "有效结束",
"leftover axis {axis} {share}%": "残差轴 {axis} {share}%",
+ "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. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.":
- "残差图轴占比是残差 SVD 第 1、2 轴的 Gabriel 惯量。打开一个残差配对可查看文章–准则单元格。这些占比不会虚构残差分数。",
+ "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.":
+ "残差图轴占比是残差 SVD 第 1、2 轴的 Gabriel 惯量。残差图奇异值是这些轴的 Gabriel 尺度。打开一个残差配对可查看文章–准则单元格。这些占比和奇异值不会虚构残差分数。",
"Leftover pairs": "残余配对",
"Closest leftover": "最近残余",
"Farthest leftover": "最远残余",
@@ -1778,9 +1780,10 @@ const TRANSLATIONS: Partial>> = {
"Valid from": "有効開始",
"Valid to": "有効終了",
"leftover axis {axis} {share}%": "残差軸 {axis} {share}%",
+ "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. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.":
- "残差マップ軸の比率は、残差 SVD の第1軸と第2軸の Gabriel 慣性です。投稿–基準セルを読むには残差ペアを開いてください。この比率から残差スコアを作りません。",
+ "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.":
+ "残差マップ軸の比率は、残差 SVD の第1軸と第2軸の Gabriel 慣性です。残差マップ特異値はそれらの軸の Gabriel 尺度です。投稿–基準セルを読むには残差ペアを開いてください。この比率と特異値から残差スコアを作りません。",
"Leftover pairs": "残差ペア",
"Closest leftover": "最も近い残差",
"Farthest leftover": "最も遠い残差",
@@ -2386,9 +2389,10 @@ 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} {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. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.":
- "Tỷ trọng trục bản đồ phần dư là quán tính Gabriel của các trục SVD phần dư 1 và 2. Mở một cặp phần dư để đọc ô bài viết–tiêu chí. Các tỷ trọng này không tạo ra điểm 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.":
+ "Tỷ trọng trục bản đồ phần dư là quán tính Gabriel của các trục SVD phần dư 1 và 2. Giá trị kỳ dị bản đồ phần dư là thang Gabriel của các trục đó. Mở một cặp phần dư để đọc ô bài viết–tiêu chí. Các tỷ trọng và giá trị kỳ dị này không tạo ra điểm phần dư.",
"Leftover pairs": "Cặp phần dư",
"Closest leftover": "Phần dư gần nhất",
"Farthest leftover": "Phần dư xa nhất",
diff --git a/frontend/src/leftoverMapAxisBadge.test.ts b/frontend/src/leftoverMapAxisBadge.test.ts
new file mode 100644
index 000000000..821f8246f
--- /dev/null
+++ b/frontend/src/leftoverMapAxisBadge.test.ts
@@ -0,0 +1,38 @@
+import { describe, expect, it } from "vitest";
+import {
+ leftoverMapAxisBadgeShare,
+ leftoverMapAxisBadgeSingular,
+} from "./leftoverMapAxisBadge";
+
+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");
+ });
+});
+
+describe("leftoverMapAxisBadgeSingular", () => {
+ it("reads persisted leftover-map singular values without inventing a leftover score", () => {
+ expect(
+ leftoverMapAxisBadgeSingular({ axis_index: 1, leftover_singular_value: 1.84 }),
+ ).toBe("1.84");
+ expect(
+ leftoverMapAxisBadgeSingular({ axis_index: 2, leftover_singular_value: 0.86 }),
+ ).toBe("0.86");
+ });
+
+ it("names a rank-0 zero leftover-map singular value", () => {
+ expect(leftoverMapAxisBadgeSingular({ axis_index: 1, leftover_singular_value: 0 })).toBe("0.00");
+ });
+
+ it("omits leftover-map singular values that are missing, non-finite, or negative", () => {
+ expect(leftoverMapAxisBadgeSingular({ axis_index: 1 })).toBeNull();
+ expect(
+ leftoverMapAxisBadgeSingular({ axis_index: 1, leftover_singular_value: Number.NaN }),
+ ).toBeNull();
+ expect(
+ leftoverMapAxisBadgeSingular({ axis_index: 1, leftover_singular_value: -0.01 }),
+ ).toBeNull();
+ });
+});
diff --git a/frontend/src/leftoverMapAxisBadge.ts b/frontend/src/leftoverMapAxisBadge.ts
new file mode 100644
index 000000000..7d2b377df
--- /dev/null
+++ b/frontend/src/leftoverMapAxisBadge.ts
@@ -0,0 +1,27 @@
+/** Caption leftover-axis report badges with persisted Gabriel singular values. */
+
+import type { LeftoverMapAxis } from "./api";
+import {
+ formatLeftoverMapPlotAxisSingular,
+ leftoverSingularForAxis,
+} from "./leftoverMapPlotAxisSingular";
+
+export const LEFTOVER_MAP_AXIS_BADGE_SHARE = "leftover axis {axis} {share}%";
+
+export const LEFTOVER_MAP_AXIS_BADGE_SINGULAR = "leftover axis {axis} σ {value} {share}%";
+
+export function leftoverMapAxisBadgeShare(
+ leftoverShare: LeftoverMapAxis["leftover_share"] | null | undefined,
+): string {
+ return ((leftoverShare ?? Number.NaN) * 100).toFixed(0);
+}
+
+export function leftoverMapAxisBadgeSingular(
+ axis: Pick & {
+ leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null;
+ },
+): string | null {
+ return formatLeftoverMapPlotAxisSingular(
+ leftoverSingularForAxis([axis], axis.axis_index),
+ );
+}
diff --git a/pyproject.toml b/pyproject.toml
index 1ae5db2cb..00cf605cd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.46.0"
+version = "2.47.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" }