diff --git a/AGENTS.md b/AGENTS.md
index f5f687a98..e877a967c 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) 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) 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
@@ -294,7 +294,12 @@ buttons; omit the plot when coordinates are missing; click a post
marker to open that post. ADR 0269 captions those leftover-map axes
with persisted leftover-map axis share `σ_k² / Σ_j σ_j²` when finite,
including rank-0 zero-share axes; a missing or non-finite share omits
-that axis badge and keeps the existing leftover-map axis text. ADR 0270
+that axis badge and keeps the existing leftover-map axis text. ADR 0289
+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
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
@@ -385,6 +390,11 @@ non-integer payload cannot caption a contradictory `used N of M scored posts`
note; a missing, non-integer, negative-used, non-positive-scored, or
used-greater-than-scored coverage omits that leftover-map coverage note and
does not invent post coverage from plotted marker count.
+ADR 0289 captions leftover-map graphic axes 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 independently of leftover-map
+axis 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
@@ -395,7 +405,9 @@ list so a click opens that post with the leftover criterion current
in Post quality (ADR 0158). Leftover-map axis share (ADR 0148) is Gabriel inertia of
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. Complete-case coverage (ADR 0168) persists to
+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
`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 d741206c1..2025a85e5 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -637,14 +637,15 @@ 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) 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) 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
share `e`, and ADR 0267 leftover-map coordinates `ξ_{1:2}` / `ζ_{1:2}`.
ADR 0268 draws those persisted coordinates as the leftover-map graphic
display above the pair buttons. ADR 0269 captions leftover-map axes 1
-and 2 with persisted leftover-map axis share. ADR 0270 ticks those
+and 2 with persisted leftover-map axis share. ADR 0289 captions leftover-map
+axes 1 and 2 with persisted leftover-map singular values `σ_k`. ADR 0270 ticks those
leftover-map axes at persisted `ξ` / `ζ` coordinates so the pair-row
badge matches the plot. ADR 0271 captions leftover-map pair segments
with persisted leftover-map distance `d`. ADR 0272 captions leftover-map
@@ -667,7 +668,8 @@ item coverage. ADR 0285 captions the leftover pair list with persisted leftover-
item complete-case coverage. ADR 0286 captions the leftover pair list with persisted leftover-map
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. Click a post marker or a pair button
+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
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.46.0-leftover-map-plot-singular.md b/CHANGELOG.d/2.46.0-leftover-map-plot-singular.md
new file mode 100644
index 000000000..0d8265ab1
--- /dev/null
+++ b/CHANGELOG.d/2.46.0-leftover-map-plot-singular.md
@@ -0,0 +1,28 @@
+## 2.46.0 — Leftover-map singular values on the graphic display
+
+- Caption leftover-map graphic axes with persisted leftover-map singular
+ values `σ_k` (ADR 0289). After `make seed`, closest and farthest leftover
+ pairs sit above the member list with the Gabriel biplot of already-named
+ coordinates, leftover-map singular values when finite, leftover-map axis
+ share when finite, axis ticks that match `ξ (x, y) ζ (x, y)` on the pair
+ row, pair segments that match `d` on the pair row, pair segments that
+ match `R̂` on the pair row, pair segments that match `R̂²/R²` on the pair
+ row, pair segments that match `U²/R²` on the pair row, pair segments that
+ match `2R̂U/R²` on the pair row, pair segments that match `U` on the pair
+ row, pair segments that match `R` on the pair row, pair segments that
+ match `Y` on the pair row, pair segments that match `E` on the pair row,
+ pair segments that match leftover-map rank on the pair row, a plot caption
+ that matches leftover-map complete-case coverage above the pair list, a
+ plot caption that matches leftover-map item complete-case coverage, a plot
+ caption that matches leftover-map incomplete post coverage, a plot caption
+ that matches leftover-map incomplete item coverage, a pair-list note that
+ matches leftover-map post complete-case coverage, a pair-list note that
+ matches leftover-map item complete-case coverage, a pair-list note that
+ matches leftover-map incomplete post coverage, and a pair-list note that
+ matches leftover-map incomplete item coverage; click a post marker or a
+ pair button opens that post. A missing, non-finite, or negative singular
+ value omits that `σ` badge independently of leftover-map axis share.
+ Rank-0 unused axes still name `σ 0.00`. Do not invent `σ_k` from
+ leftover-map axis share, leftover-map distance, leftover-map rank, or
+ plotted coordinates. Never invent a leftover score. Never invent a theta.
+ No new columns.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71bf70876..62d09380f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,34 @@ All notable changes to this project are documented here. Format follows
### Added
+- 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
+ Gabriel biplot of already-named coordinates, leftover-map singular values
+ when finite, leftover-map axis share when finite, axis ticks that match
+ `ξ (x, y) ζ (x, y)` on the pair row, pair segments that match `d` on
+ the pair row, pair segments that match `R̂` on the pair row, pair
+ segments that match `R̂²/R²` on the pair row, pair segments that match
+ `U²/R²` on the pair row, pair segments that match `2R̂U/R²` on the pair
+ row, pair segments that match `U` on the pair row, pair segments that
+ match `R` on the pair row, pair segments that match `Y` on the pair
+ row, pair segments that match `E` on the pair row, pair segments that
+ match leftover-map rank on the pair row, a plot caption that matches
+ leftover-map complete-case coverage above the pair list, a plot caption
+ that matches leftover-map item complete-case coverage, a plot caption
+ that matches leftover-map incomplete post coverage, a plot caption that
+ matches leftover-map incomplete item coverage, a pair-list note that
+ matches leftover-map post complete-case coverage, a pair-list note that
+ matches leftover-map item complete-case coverage, a pair-list note that
+ matches leftover-map incomplete post coverage, and a pair-list note
+ that matches leftover-map incomplete item coverage; click a post marker
+ or a pair button opens that post. A missing, non-finite, or negative
+ singular value omits that `σ` badge independently of leftover-map axis
+ share. Rank-0 unused axes still name `σ 0.00`. Do not invent `σ_k` from
+ leftover-map axis share, leftover-map distance, leftover-map rank, or
+ plotted coordinates. Never invent a leftover score or a theta. No new
+ columns.
+
- Period leftover pairs now fail-close leftover-map post complete-case
coverage on the leftover pair list through leftoverMapCoverageCounts
(ADR 0288 / v2.45.0). After `make seed`, closest and farthest leftover
diff --git a/CLAUDE.md b/CLAUDE.md
index 889f61d82..4ce0f23a3 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), 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), 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 a9d554176..4b4e4d485 100644
--- a/docs/adr/0148-leftover-map-axis-share.md
+++ b/docs/adr/0148-leftover-map-axis-share.md
@@ -3,7 +3,9 @@
**Decision status:** Accepted
**Date:** 2026-08-24
**Amended by:** [ADR 0269](0269-leftover-map-axis-share-plot.md)
-(leftover-map axis share on the graphic display)
+(leftover-map axis share on the graphic display);
+[ADR 0289](0289-leftover-map-plot-singular.md)
+(leftover-map singular values on the graphic display)
## Context
diff --git a/docs/adr/0269-leftover-map-axis-share-plot.md b/docs/adr/0269-leftover-map-axis-share-plot.md
index 9d5f1ea50..4add1cce8 100644
--- a/docs/adr/0269-leftover-map-axis-share-plot.md
+++ b/docs/adr/0269-leftover-map-axis-share-plot.md
@@ -2,6 +2,8 @@
**Decision status:** Accepted
**Date:** 2026-08-28
+**Amended by:** [ADR 0289](0289-leftover-map-plot-singular.md)
+(leftover-map singular values on the graphic display)
Amends [ADR 0148](0148-leftover-map-axis-share.md) and
[ADR 0268](0268-leftover-map-graphic-display.md). Independent of leftover-map
diff --git a/docs/adr/0289-leftover-map-plot-singular.md b/docs/adr/0289-leftover-map-plot-singular.md
new file mode 100644
index 000000000..b50d55d9d
--- /dev/null
+++ b/docs/adr/0289-leftover-map-plot-singular.md
@@ -0,0 +1,166 @@
+# ADR 0289 — Name leftover-map singular values on the graphic display
+
+**Decision status:** Proposed
+**Date:** 2026-08-30
+
+Amends leftover-map axis share on the graphic display
+([ADR 0269](0269-leftover-map-axis-share-plot.md)) and leftover-map axis share
+persistence ([ADR 0148](0148-leftover-map-axis-share.md)). Independent of
+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 residual disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)),
+leftover observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)),
+leftover-map explained leftover share persistence
+([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map
+unexplained leftover share persistence
+([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map
+reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)),
+leftover-map cross share persistence
+([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover
+persistence ([ADR 0182](0182-leftover-map-unexplained.md)), leftover-map rank
+persistence ([ADR 0164](0164-leftover-map-rank.md)), leftover-map complete-case
+coverage persistence ([ADR 0168](0168-leftover-map-complete-case-coverage.md)),
+and leftover-map graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)).
+
+## Context
+
+ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map
+axis share `σ_k² / Σ_j σ_j²` on each period report as two leftover-map axes.
+ADR 0269 already captions leftover-map graphic axes with persisted leftover-map
+axis share. Those plot axes still hide `σ_k` itself, so Gabriel (1971)
+singular values live in `report_leftover_map_axis` and never appear on the
+graphic display they reconstruct. Hiding `σ_k` 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 the leftover-map graphic display 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 **0289** so it does not collide with
+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 the leftover-map graphic display, 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. Axis 1 and axis 2 stay
+independently named: one missing singular value does not hide the other. Share
+and singular value omit independently:
+
+- no share and no `σ_k` keeps `leftover-map axis {k}`
+- share only stays `leftover-map axis {k} ({share}%)`
+- `σ_k` only is `leftover-map axis {k} σ {value}`
+- both is `leftover-map axis {k} σ {value} ({share}%)`
+
+Format `σ_k` with two decimal places. A missing, non-finite, or negative
+singular value omits that `σ` badge and does not invent `σ_k` from leftover-map
+axis share. Rank-0 unused axes still name `σ 0.00`. Click a post marker to
+open that post. Criterion markers are not post buttons. The grouping
+comparison strip (ADR 0149) stays on its reduced leftover payload and does not
+gain this caption.
+
+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`, closest and farthest leftover pairs sit above the member
+list with the leftover-map graphic display of persisted `ξ` and `ζ`, and each
+leftover-map axis names its persisted Gabriel singular value when finite
+together with persisted leftover-map axis share when finite; click a post
+marker or a pair button opens that post. Hidden posts stay hidden. Rank-0
+unused axes still plot at the origin and still name zero leftover-map
+singular value with zero leftover-map axis share. Report-level leftover-axis
+badges (ADR 0148) 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, and fail-close leftover-map post complete-case coverage on the
+pair list.
+
+## 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 6159c7240..5f4d1cb2a 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,12 @@
# Product & Technical Gap Baseline
+> #820 exact-current-parent reconstruction: persisted singular-value axis badges
+> are reconstructed from #819 `f37ca315e3c48fa37bbcafe96e46c5d7dab991b7`.
+> Preserve finite, non-negative persisted `σ_k`, including rank-0 `σ 0.00`; omit missing,
+> non-finite, or negative singular values independently of axis share. Parent share-only
+> regressions run with singular value unavailable so the contracts remain orthogonal.
+> ADR 0289 stays Proposed while Draft; package/frontend/runtime identity is 2.46.0.
+
> #819 current-parent reconstruction: v2.45 pair-list post coverage fail-close
> is being rebuilt from exact #818 `bc971a402ef9faecb2f0b4ffd743092f4779d76b`.
> Preserve only feature delta `a2c965511d26923bd878ec56d655fb8a0183c4d7`;
diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md
index f0aa61428..e3a7abf60 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. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, and `MissingAxisShare` cover two-pair maps, rank-0 origin with 0% share, a `0` tick, `d 0.00`, `R̂ 0.00`, `R̂²/R² 0.00`, `U²/R² 0.00`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, and missing share that keeps existing leftover-map axis text. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `--color-primary`, `--color-palette-blue-mid` |
-| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. | `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 when finite 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. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, and `MissingAxisSingular` cover two-pair maps, rank-0 origin with `σ 0.00` and 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 singular values, and missing singular values that keep leftover-map axis share. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `leftoverMapPlotAxisSingular`, `--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. | `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 a1c5ca160..32a794de7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.45.0",
+ "version": "2.46.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 70a4ad8c6..40d17fe8e 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -4144,8 +4144,8 @@ describe("App, authenticated", () => {
expect(screen.getByRole("button", { name: /open report post: public post/i })).toHaveTextContent("due 2026-01-12");
expect(await screen.findByLabelText("Leftover pairs")).toBeInTheDocument();
expect(screen.getByLabelText("Leftover-map graphic display")).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.getByRole("button", {
name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i,
diff --git a/frontend/src/components/LeftoverMapPlot.stories.tsx b/frontend/src/components/LeftoverMapPlot.stories.tsx
index 8a1d58015..339216101 100644
--- a/frontend/src/components/LeftoverMapPlot.stories.tsx
+++ b/frontend/src/components/LeftoverMapPlot.stories.tsx
@@ -137,3 +137,11 @@ export const MissingAxisShare: Story = {
],
},
};
+
+export const MissingAxisSingular: Story = {
+ args: {
+ leftoverMapAxes: [
+ { axis_index: 1, leftover_singular_value: Number.NaN, leftover_share: 0.82 },
+ ],
+ },
+};
diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx
index 1c6e11c22..9bd7352d0 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,56 +180,54 @@ describe("LeftoverMapPlot", () => {
expect(screen.getByLabelText("leftover-map rank rank 0")).toBeInTheDocument();
});
- it("renders every distinct persisted tick when rounded labels match", () => {
+ it("captions leftover-map axes with persisted leftover-map singular values and leftover-map axis share", () => {
render(
,
);
-
- expect(screen.getByLabelText("leftover-map axis 1 tick 0.00")).toBeInTheDocument();
- expect(screen.getAllByLabelText("leftover-map axis 1 tick +0.00")).toHaveLength(2);
+ expect(screen.getByText("leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument();
});
- it("captions leftover-map axes with persisted leftover-map axis share", () => {
+ it("captions leftover-map axes with persisted leftover-map singular values when share is missing or non-finite", () => {
render(
,
);
- 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")).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("keeps existing leftover-map axis text when share is missing or non-finite", () => {
+ it("keeps leftover-map axis share when singular value is missing, non-finite, or negative", () => {
render(
,
);
- expect(screen.getByText("leftover-map axis 1")).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();
+ expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument();
+ expect(screen.queryByText(/σ /)).not.toBeInTheDocument();
});
it("omits leftover-map distance on a pair segment when d is missing", () => {
@@ -840,4 +838,56 @@ describe("LeftoverMapPlot", () => {
"Leftover map used 2 of 2 scored criteria (complete-case)",
);
});
+ it("renders every distinct persisted tick when rounded labels match", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByLabelText("leftover-map axis 1 tick 0.00")).toBeInTheDocument();
+ expect(screen.getAllByLabelText("leftover-map axis 1 tick +0.00")).toHaveLength(2);
+ });
+
+ it("captions leftover-map axes with persisted leftover-map axis share", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument();
+ expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument();
+ });
+
+ it("keeps existing leftover-map axis text when share is missing or non-finite", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("leftover-map axis 1")).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();
+ });
+
});
diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx
index 1463019b0..e08972014 100644
--- a/frontend/src/components/LeftoverMapPlot.tsx
+++ b/frontend/src/components/LeftoverMapPlot.tsx
@@ -20,6 +20,12 @@ import {
leftoverShareForAxis,
LEFTOVER_MAP_PLOT_AXIS_SHARE,
} from "../leftoverMapPlotAxisShare";
+import {
+ formatLeftoverMapPlotAxisSingular,
+ leftoverSingularForAxis,
+ LEFTOVER_MAP_PLOT_AXIS_SINGULAR,
+ LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE,
+} from "../leftoverMapPlotAxisSingular";
import {
firstPlottablePairForPost,
layoutLeftoverMapPlot,
@@ -58,10 +64,23 @@ function leftoverMapPlotAxisText(
const percent = formatLeftoverMapPlotAxisShare(
leftoverShareForAxis(leftoverMapAxes, axisIndex),
);
+ const singular = formatLeftoverMapPlotAxisSingular(
+ leftoverSingularForAxis(leftoverMapAxes, axisIndex),
+ );
+ 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,
+ });
}
/**
@@ -69,8 +88,11 @@ 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.
- * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the
+ * Gabriel singular values when finite and non-negative, including rank-0
+ * zero singular values, and with persisted Gabriel inertia share when
+ * finite, including rank-0 zero-share axes. Do not invent ``σ_k`` from
+ * leftover-map axis share. 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
* explained leftover share ``e``, leftover-map unexplained leftover
@@ -98,8 +120,10 @@ function leftoverMapPlotAxisText(
* caption when incomplete post coverage is missing or not a usable integer.
* Omit that leftover-map incomplete item caption when incomplete item
* coverage is missing or not a usable integer.
- * Omit that axis badge when share is
- * missing or non-finite and keep the existing leftover-map axis text.
+ * Omit that axis singular-value badge when ``σ_k`` is missing, non-finite,
+ * or negative, independently of leftover-map axis share. Omit that axis
+ * share badge when share is missing or non-finite and keep the existing
+ * leftover-map axis text, including any leftover-map singular value.
* Omit the plot when no pair has four finite leftover-map coordinates.
* Never invent a leftover score.
*/
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/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx
index cdee52429..d99685cae 100644
--- a/frontend/src/components/LeftoverPairList.tsx
+++ b/frontend/src/components/LeftoverPairList.tsx
@@ -59,7 +59,10 @@ export type LeftoverPairListProps = {
* next action. When four finite coordinates exist, ADR 0268 draws the
* leftover-map graphic display above the pair buttons; click a post
* marker opens that post. ADR 0269 captions those leftover-map axes with
- * persisted leftover-map axis share when finite. ADR 0270 ticks those
+ * persisted leftover-map axis share when finite. ADR 0289 captions those
+ * leftover-map axes with persisted leftover-map singular values when
+ * finite and non-negative, independently of leftover-map axis share, and
+ * does not invent ``σ_k`` from leftover-map axis share. ADR 0270 ticks those
* leftover-map axes at persisted ``ξ`` / ``ζ`` so the pair-row badge matches
* the plot. ADR 0271 names persisted leftover-map distance ``d`` on those
* pair segments. ADR 0272 names persisted leftover-map reconstruction
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts
index 06fe63f7f..81db90274 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -65,6 +65,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}",
@@ -564,6 +566,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 value 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 value with 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 3ba3dcb79..43c2feca1 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -599,6 +599,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}",
@@ -1201,6 +1203,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}",
@@ -1807,6 +1811,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}",
@@ -2413,6 +2419,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
new file mode 100644
index 000000000..3c5f8900d
--- /dev/null
+++ b/frontend/src/leftoverMapPlotAxisSingular.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, it } from "vitest";
+import {
+ formatLeftoverMapPlotAxisSingular,
+ leftoverSingularForAxis,
+} from "./leftoverMapPlotAxisSingular";
+
+describe("leftoverSingularForAxis", () => {
+ const axes = [
+ { axis_index: 1, leftover_singular_value: 1.84 },
+ { axis_index: 2, leftover_singular_value: 0.86 },
+ ];
+
+ it("reads persisted leftover-map singular values without inventing a leftover score", () => {
+ expect(leftoverSingularForAxis(axes, 1)).toBe(1.84);
+ expect(leftoverSingularForAxis(axes, 2)).toBe(0.86);
+ });
+
+ it("names a rank-0 zero leftover-map singular value", () => {
+ expect(
+ leftoverSingularForAxis(
+ [
+ { axis_index: 1, leftover_singular_value: 0 },
+ { axis_index: 2, leftover_singular_value: 0 },
+ ],
+ 1,
+ ),
+ ).toBe(0);
+ });
+
+ it("omits a leftover-map axis when singular value is missing, non-finite, or negative", () => {
+ expect(leftoverSingularForAxis(undefined, 1)).toBeNull();
+ expect(leftoverSingularForAxis([], 1)).toBeNull();
+ expect(leftoverSingularForAxis([{ axis_index: 1, leftover_singular_value: null }], 1)).toBeNull();
+ expect(
+ leftoverSingularForAxis([{ axis_index: 1, leftover_singular_value: Number.NaN }], 1),
+ ).toBeNull();
+ expect(
+ leftoverSingularForAxis(
+ [{ axis_index: 1, leftover_singular_value: Number.POSITIVE_INFINITY }],
+ 1,
+ ),
+ ).toBeNull();
+ expect(leftoverSingularForAxis([{ axis_index: 1, leftover_singular_value: -0.01 }], 1)).toBeNull();
+ expect(leftoverSingularForAxis(axes, 3)).toBeNull();
+ });
+
+ it("does not invent leftover-map singular values from leftover-map axis share", () => {
+ expect(leftoverSingularForAxis([{ axis_index: 1 }], 1)).toBeNull();
+ });
+});
+
+describe("formatLeftoverMapPlotAxisSingular", () => {
+ it("formats persisted leftover-map singular values without inventing a leftover score", () => {
+ expect(formatLeftoverMapPlotAxisSingular(1.84)).toBe("1.84");
+ expect(formatLeftoverMapPlotAxisSingular(0.86)).toBe("0.86");
+ expect(formatLeftoverMapPlotAxisSingular(0)).toBe("0.00");
+ });
+
+ it("omits leftover-map singular values when the value is missing, non-finite, or negative", () => {
+ expect(formatLeftoverMapPlotAxisSingular(null)).toBeNull();
+ expect(formatLeftoverMapPlotAxisSingular(undefined)).toBeNull();
+ expect(formatLeftoverMapPlotAxisSingular(Number.NaN)).toBeNull();
+ expect(formatLeftoverMapPlotAxisSingular(Number.POSITIVE_INFINITY)).toBeNull();
+ expect(formatLeftoverMapPlotAxisSingular(Number.NEGATIVE_INFINITY)).toBeNull();
+ expect(formatLeftoverMapPlotAxisSingular(-0.01)).toBeNull();
+ });
+});
diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts
new file mode 100644
index 000000000..ac7271a74
--- /dev/null
+++ b/frontend/src/leftoverMapPlotAxisSingular.ts
@@ -0,0 +1,38 @@
+/** Caption leftover-map plot axes with persisted Gabriel singular values. */
+
+import type { LeftoverMapAxis } from "./api";
+
+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 type LeftoverMapPlotAxisSingular = {
+ axis_index: LeftoverMapAxis["axis_index"];
+ leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null;
+};
+
+export function leftoverSingularForAxis(
+ axes: ReadonlyArray | null | undefined,
+ axisIndex: number,
+): number | null {
+ const axis = axes?.find((candidate) => candidate.axis_index === axisIndex);
+ if (
+ axis == null ||
+ axis.leftover_singular_value == null ||
+ !Number.isFinite(axis.leftover_singular_value) ||
+ axis.leftover_singular_value < 0
+ ) {
+ return null;
+ }
+ return axis.leftover_singular_value;
+}
+
+export function formatLeftoverMapPlotAxisSingular(
+ leftoverSingular: number | null | undefined,
+): string | null {
+ if (leftoverSingular == null || !Number.isFinite(leftoverSingular) || leftoverSingular < 0) {
+ return null;
+ }
+ return leftoverSingular.toFixed(2);
+}
diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts
index 05360efc9..a34d91f3c 100644
--- a/frontend/src/leftoverMapPlotLayout.ts
+++ b/frontend/src/leftoverMapPlotLayout.ts
@@ -1,5 +1,8 @@
/** 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 axes 1 and 2 when finite and
+ * non-negative (ADR 0289). Do not invent ``σ_k`` from leftover-map axis
+ * share.
* 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
diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py
index 1fca0e0a5..df94aa1ae 100644
--- a/lineageweave/__init__.py
+++ b/lineageweave/__init__.py
@@ -130,4 +130,4 @@
"serialize_lineage_analysis_result",
]
-__version__ = "2.45.0"
+__version__ = "2.46.0"
diff --git a/pyproject.toml b/pyproject.toml
index df990f86f..1ae5db2cb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.45.0"
+version = "2.46.0"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
diff --git a/tests/test_adr_0289_governance.py b/tests/test_adr_0289_governance.py
new file mode 100644
index 000000000..956ab9149
--- /dev/null
+++ b/tests/test_adr_0289_governance.py
@@ -0,0 +1,11 @@
+"""Governance regression for draft ADR 0289."""
+
+from pathlib import Path
+
+ADR_PATH = Path(__file__).parents[1] / "docs/adr/0289-leftover-map-plot-singular.md"
+
+def test_draft_adr_0289_remains_proposed() -> None:
+ """Keep the unmerged decision proposed until independent acceptance exists."""
+ text = ADR_PATH.read_text(encoding="utf-8")
+ assert "**Decision status:** Proposed" in text
+ assert "**Decision status:** Accepted" not in text