From 52b3597e5c6b825f988a9c8a7c68ae1653535982 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 01:47:18 +0000 Subject: [PATCH 01/11] feat(reports): leftover-map singular values on comparison leftover-axis badges (v2.80.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caption leftover-axis report badges on the grouping comparison strip with persisted leftover-map singular values σ_k (ADR 0323). Comparison copy stays leftover map comparison leftover axis {k} σ {value} {share}% so it stays distinct from period leftover-axis leftover axis {k} σ {value} {share}% (ADR 0322). Rank-0 unused axes still name σ 0.00. Share and singular value omit independently. Do not invent σ_k from leftover-map axis share. Fail-closes leftover-map comparison graphic TypeScript on omitted leftover-map singular values. Never invent a leftover score. Never invent a theta. --- AGENTS.md | 13 +- ARCHITECTURE.md | 5 +- ...80.0-leftover-map-compare-axis-singular.md | 22 ++++ CHANGELOG.md | 22 +++- CLAUDE.md | 2 +- docs/adr/0148-leftover-map-axis-share.md | 4 +- docs/adr/0322-leftover-map-axis-singular.md | 3 +- ...0323-leftover-map-compare-axis-singular.md | 116 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 17 +++ docs/storybook-inventory.md | 2 +- frontend/package.json | 2 +- frontend/src/App.test.tsx | 20 +++ frontend/src/App.tsx | 34 +++++ .../src/components/LeftoverMapPlot.test.tsx | 3 +- frontend/src/components/LeftoverMapPlot.tsx | 12 +- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 42 +++++++ frontend/src/i18n.ts | 30 +++++ frontend/src/leftoverMapAxisBadge.test.ts | 8 +- frontend/src/leftoverMapAxisBadge.ts | 5 +- .../src/leftoverMapPlotAxisSingular.test.ts | 74 +++++++++++ frontend/src/leftoverMapPlotAxisSingular.ts | 42 +++++++ frontend/src/leftoverMapPlotLayout.test.ts | 3 + frontend/src/leftoverMapPlotLayout.ts | 2 + pyproject.toml | 2 +- 25 files changed, 468 insertions(+), 20 deletions(-) create mode 100644 CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md create mode 100644 docs/adr/0323-leftover-map-compare-axis-singular.md diff --git a/AGENTS.md b/AGENTS.md index 3039c75b6..f7ebeb9d3 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) 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) 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 @@ -650,8 +650,12 @@ ADR 0322 captions leftover-axis report badges with persisted leftover-map singul values `σ_k` when leftoverMapAxisBadgeSingular returns a usable finite non-negative value; a missing, non-finite, or negative singular value omits that `σ` badge and keeps `leftover axis {k} {share}%`. Do not invent leftover-map singular values from -leftover-map axis share. The grouping comparison strip does not gain leftover-axis -report badges with a distinct comparison-strip name. +leftover-map axis share. 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. Distinct accessible +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. 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 @@ -753,6 +757,9 @@ values `σ_k` when leftoverSingularForAxis returns a usable finite non-negative Leftover-axis report badge leftover-map singular values (ADR 0322) captions leftover-axis report badges with persisted leftover-map singular values `σ_k` when leftoverMapAxisBadgeSingular 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. 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 7bfd22ea6..8321c5d25 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -752,6 +752,8 @@ ADR 0321 captions leftover-map singular values on that grouping comparison leftover-map graphic from already-named leftover-map axes. 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`. 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 @@ -803,7 +805,8 @@ graphic (ADR 0318), leftover-map distance on the grouping comparison leftover-ma graphic (ADR 0319), leftover-map coordinate ticks on the grouping comparison leftover-map 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); +(ADR 0322), leftover-map singular values on leftover-axis report badges on the +grouping comparison strip (ADR 0323); `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.80.0-leftover-map-compare-axis-singular.md b/CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..305984458 --- /dev/null +++ b/CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md @@ -0,0 +1,22 @@ +## 2.80.0 — Leftover-map singular values on leftover-axis report badges on the grouping comparison strip + +- Caption leftover-axis report badges on the grouping comparison strip with + persisted leftover-map singular values `σ_k` (ADR 0323). After `make seed`, + grouping comparison leftover-axis badges 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 `σ` badge and keeps + `leftover map comparison leftover axis {k} {share}%`. Rank-0 unused axes still + name `σ 0.00`. Comparison leftover-axis badges stay + `leftover map comparison leftover axis {k} σ {value} {share}%` so they stay + distinct from period leftover-axis `leftover axis {k} σ {value} {share}%`, + from hyphen `leftover-map 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 in the + period-report panel. A finite negative leftover is shown, never clamped. + Never invent a leftover score. Never invent a theta. No new columns. + Fail-closes leftover-map comparison graphic TypeScript on omitted leftover-map + singular values (`leftover_singular_value: Number.NaN` and percent/singular + narrowing) inherited from the grouping comparison leftover-map graphic σ + increment. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d84ce590..9a0d1cec3 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 on the grouping + comparison strip with persisted leftover-map singular values `σ_k` + (ADR 0323 / v2.80.0). After `make seed`, grouping comparison leftover-axis + badges 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 `σ` badge and keeps + `leftover map comparison leftover axis {k} {share}%`. Rank-0 unused axes still + name `σ 0.00`. Comparison leftover-axis badges stay + `leftover map comparison leftover axis {k} σ {value} {share}%` so they stay + distinct from period leftover-axis `leftover axis {k} σ {value} {share}%`, + from hyphen `leftover-map 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 in the + period-report panel. A finite negative leftover is shown, never clamped. + Never invent a leftover score or a theta. No new columns. + Fail-closes leftover-map comparison graphic TypeScript on omitted leftover-map + singular values inherited from the grouping comparison leftover-map graphic σ + increment. + - Period leftover pairs now caption leftover-axis report badges with persisted leftover-map singular values `σ_k` (ADR 0322 / v2.79.0). After `make seed`, leftover-axis badges name persisted leftover-map singular values when finite @@ -20,7 +40,7 @@ All notable changes to this project are documented here. Format follows `leftover map comparison graphic leftover-map axis {k} σ {value} ({share}%)`. Do not invent `σ_k` from leftover-map axis share. This increment does not caption leftover-map singular values on leftover-axis badges on the grouping - comparison strip with a distinct name. A finite negative leftover is shown, + comparison strip with a distinct name (ADR 0323). A finite negative leftover is shown, never clamped. Never invent a leftover score or a theta. No new columns. - Period leftover pairs now name leftover-map singular values on the grouping diff --git a/CLAUDE.md b/CLAUDE.md index d4cbddbe7..4546003e0 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), 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), 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 4eb1f32c2..74728c994 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 0322](0322-leftover-map-axis-singular.md) -(leftover-map singular values on leftover-axis report badges) +(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) ## Context diff --git a/docs/adr/0322-leftover-map-axis-singular.md b/docs/adr/0322-leftover-map-axis-singular.md index 9a783b770..bf1183f72 100644 --- a/docs/adr/0322-leftover-map-axis-singular.md +++ b/docs/adr/0322-leftover-map-axis-singular.md @@ -82,7 +82,8 @@ singular values (ADR 0321) remain. 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. +distinct name +([ADR 0323](0323-leftover-map-compare-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 new file mode 100644 index 000000000..6d294a869 --- /dev/null +++ b/docs/adr/0323-leftover-map-compare-axis-singular.md @@ -0,0 +1,116 @@ +# ADR 0323 — Name leftover-map singular values on leftover-axis report badges on the grouping comparison strip + +**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 singular +values on the grouping comparison leftover-map graphic +([ADR 0321](0321-leftover-map-compare-plot-singular.md)), leftover-map +axis share persistence ([ADR 0148](0148-leftover-map-axis-share.md)), leftover-map +graphic display on the grouping comparison strip +([ADR 0304](0304-leftover-map-compare-graphic.md)). Independent of leftover-map +singular values on leftover-axis report badges in the period-report panel +(ADR 0322). + +## Context + +ADR 0148 already persists leftover-map singular values `σ_k` on +`report_leftover_map_axis` together with leftover-map axis share +`σ_k² / Σ_j σ_j²`. ADR 0322 already captions period-report leftover-axis +badges with `leftover axis {axis} σ {value} {share}%` when that singular +value is finite and non-negative. ADR 0321 already captions the grouping +comparison leftover-map graphic with +`leftover map comparison graphic leftover-map axis {axis} σ {value}` +when that singular value is finite and non-negative. The grouping +comparison strip still omits leftover-axis report badges, so a buyer +who compares leftover pairs can treat leftover-map graphic `σ_k` as +the leftover-axis report badge without a next action. Hiding persisted +`σ_k` on leftover-axis report badges lets leftover-map axis share be +read as a leftover score. 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 singular values on leftover-axis +report badges on the grouping comparison strip from already-named +leftover-map axes. Comparison copy uses the accessible names +`leftover map comparison leftover axis {axis} σ {value}` and +`leftover map comparison leftover axis {axis} σ {value} {share}%` +so they stay distinct from hyphen `leftover-map axis {axis} σ {value}`, +from pair-list and period-report leftover-axis +`leftover axis {axis} σ {value} {share}%` (ADR 0322), from comparison +graphic `leftover map comparison graphic leftover-map axis {axis} σ {value}`, +and from comparison axis-share +`leftover map comparison axis {axis} ({share}%)`. It does not add +columns. Do not invent a leftover score. Do not invent a theta. + +This protected increment uses **0323** so it does not collide with leftover-map +singular values on leftover-axis report badges in the period-report panel +(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), or the +dashboard stacks. + +## Decision + +On the grouping comparison strip, caption leftover-map axis 1 and leftover-map +axis 2 leftover-axis report badges with persisted leftover-map singular +values `σ_k` when that singular value is finite and non-negative. Share +and singular value omit independently: no share and no `σ_k` omits that +leftover-axis report badge; share only is +`leftover map comparison leftover axis {k} {share}%`; `σ_k` only is +`leftover map comparison leftover axis {k} σ {value}`; both is +`leftover map comparison leftover axis {k} σ {value} {share}%`. +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 leftover pair to open that post. + +Leftover-map comparison leftover-axis singular values omit independently +of leftover-map comparison graphic leftover-map axis singular values and +leftover-map axis share. A missing singular value omits that leftover-map +comparison leftover-axis `σ` caption and keeps a usable leftover-map +comparison leftover-axis share caption, leftover-map comparison graphic +leftover-map axis `σ` caption, leftover-map comparison 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 comparison graphic coverage notes. + +This increment does not change leftover-map singular values on leftover-axis +report badges in the period-report panel. Those leftover-axis badges stay +`leftover axis {axis} σ {value} {share}%` (ADR 0322). This increment does +not persist leftover-map inner product, cosine, or length. + +Do not add SQL migrations. Do not edit shipped migrations. Do not invent a leftover +score. Do not invent a theta. + +## Consequences + +After `make seed`, grouping comparison leftover pairs that already show +the leftover-map graphic of persisted `ξ` / `ζ` also name leftover-map +singular values on leftover-axis report badges when those persisted `σ_k` +are finite and non-negative. Rank-0 unused axes still name leftover-map +comparison leftover axis `σ 0.00`. Click a post marker or a pair button +opens that post. When coordinates, reconstruction, and distance are finite, +`R̂ = ξ · ζ` and `d = ‖ξ − ζ‖`. When leftover-map singular values and +leftover-map axis share are finite, leftover-map axis share is +`σ_k² / Σ_j σ_j²`. + +## Related + +Independent of leftover-map singular values on leftover-axis report badges +in the period-report panel ([ADR 0322](0322-leftover-map-axis-singular.md)). + +## References + +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 + +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 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 9fef7a938..19515f547 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,22 @@ # Product & Technical Gap Baseline +> 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 +> (v2.79.0 / ADR 0322) is `f24b664`, stacked on #861 @ `3aac452`. Independent +> APPROVE is missing. Next buyer increment on this +> cycle: leftover-map singular values on leftover-axis report badges on the +> grouping comparison strip (ADR 0323 / v2.80.0) delivered locally on +> `feat/leftover-map-compare-axis-singular-v2800`. Caption persisted leftover-map +> singular values `σ_k` on leftover-axis report badges from already-named leftover-map +> axes. Distinct accessible name `leftover map comparison 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 change +> leftover-map singular values on leftover-axis report badges in the period-report +> panel this increment. Stack onto #862. 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:35 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not > self-approve. #861 leftover-map singular values on the grouping comparison leftover-map diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 26b00286f..6811f24df 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,7 +6,7 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| | `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. 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, and leftover-map incomplete item coverage with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount return usable integers. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The grouping comparison strip names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, leftover-map incomplete item coverage, and leftover-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 304a03d4d..9b99eca3c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.79.0", + "version": "2.80.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 38e065235..085626a09 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4179,6 +4179,11 @@ 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.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( + "Open a leftover pair to read the post–criterion cell", + ); expect(screen.getByLabelText("Leftover-map axis share")).toHaveTextContent( "Open a leftover pair to read the post–criterion cell", ); @@ -4336,12 +4341,27 @@ describe("App, authenticated", () => { expect( within(screen.getByLabelText("Grouping comparison")).queryByText("leftover-map axis 2 (18%)"), ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText( + "leftover map comparison leftover axis 1 σ 1.84 82%", + ), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText( + "leftover map comparison leftover axis 2 σ 0.86 18%", + ), + ).toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).queryByText("leftover axis 1 σ 1.84 82%"), ).not.toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).queryByText("leftover axis 2 σ 0.86 18%"), ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByLabelText( + "Leftover map comparison leftover axis", + ), + ).toHaveTextContent("Open a leftover pair to read the post–criterion cell"); expect( within(screen.getByLabelText("Grouping comparison")).getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ecd927785..8d36a1b5f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -143,6 +143,13 @@ import { LEFTOVER_MAP_AXIS_BADGE_SHARE, LEFTOVER_MAP_AXIS_BADGE_SINGULAR, } from "./leftoverMapAxisBadge"; +import { + leftoverMapCompareAxisBadge, + leftoverSingularForAxis, + LEFTOVER_MAP_COMPARE_AXIS_CAPTION, + LEFTOVER_MAP_COMPARE_AXIS_LABEL, +} from "./leftoverMapPlotAxisSingular"; +import { leftoverShareForAxis } from "./leftoverMapPlotAxisShare"; import { formatLeftoverMapReconstruction, LEFTOVER_MAP_COMPARE_RECONSTRUCTION_LABEL, @@ -4101,6 +4108,33 @@ function ReportsPanel({ {tf(LEFTOVER_MAP_PLOT_INCOMPLETE_ITEM, comparisonIncompleteItemCount)}

) : null} + {row.leftover_map_axes?.map((axis) => { + const badge = leftoverMapCompareAxisBadge( + axis.axis_index, + leftoverSingularForAxis([axis], axis.axis_index), + leftoverShareForAxis([axis], axis.axis_index), + ); + if (badge === null) { + return null; + } + return ( + + {tf(badge.key, badge.values)} + + ); + })} + {row.leftover_map_axes?.some( + (axis) => + leftoverMapCompareAxisBadge( + axis.axis_index, + leftoverSingularForAxis([axis], axis.axis_index), + leftoverShareForAxis([axis], axis.axis_index), + ) !== null, + ) ? ( +

+ {t(LEFTOVER_MAP_COMPARE_AXIS_CAPTION)} +

+ ) : null} {row.leftover_pairs && row.leftover_pairs.length > 0 && ( <> diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 40162a36b..ed9b89639 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -895,6 +895,7 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByText("leftover map comparison axis 1 (82%)")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1 σ 1.84 (82%)")).not.toBeInTheDocument(); expect(screen.queryByText("leftover axis 1 σ 1.84 82%")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover map comparison leftover axis 1 σ 1.84 82%")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); }); @@ -903,7 +904,7 @@ describe("LeftoverMapPlot", () => { { "leftover map comparison graphic leftover-map axis {axis} tick {value}", "leftover map comparison graphic leftover-map axis {axis} σ {value}", "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)", + "leftover map comparison leftover axis {axis} σ {value}", + "leftover map comparison leftover axis {axis} σ {value} {share}%", + "leftover map comparison leftover axis {axis} {share}%", + "Leftover map comparison leftover axis", + "Leftover map comparison leftover-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 comparison coordinates", "Leftover map comparison graphic", "Leftover map comparison", @@ -740,6 +745,43 @@ describe("i18n", () => { ).toBe(expected); }); + it.each([ + ["ko", "잔여 지도 비교 잔차 축 1 σ 1.84"], + ["zh", "残差地图比较残差轴 1 σ 1.84"], + ["ja", "残差マップの比較残差軸 1 σ 1.84"], + ["vi", "trục phần dư so sánh bản đồ phần dư 1 σ 1.84"], + ] as const)( + "formats leftover map comparison leftover-axis singular values in %s", + (locale, expected) => { + setLocale(locale); + expect( + tf("leftover map comparison leftover 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 phần dư so sánh bản đồ phần dư 1 σ 1.84 82%"], + ] as const)( + "formats leftover map comparison leftover-axis singular share in %s", + (locale, expected) => { + setLocale(locale); + expect( + tf("leftover map comparison leftover axis {axis} σ {value} {share}%", { + axis: 1, + value: "1.84", + share: "82", + }), + ).toBe(expected); + }, + ); + it.each([ ["ko", "잔여 지도 비교 설명 잔여 점유율"], ["zh", "残差地图比较已解释残差份额"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index de3e12910..52cf6a35e 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -300,6 +300,13 @@ const TRANSLATIONS: Partial>> = { "leftover map comparison graphic leftover-map axis {axis} σ {value}": "잔여 지도 비교 그림 축 {axis} σ {value}", "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": "잔여 지도 비교 그림 축 {axis} σ {value} ({share}%)", + "leftover map comparison leftover axis {axis} σ {value}": "잔여 지도 비교 잔차 축 {axis} σ {value}", + "leftover map comparison leftover axis {axis} σ {value} {share}%": + "잔여 지도 비교 잔차 축 {axis} σ {value} {share}%", + "leftover map comparison leftover axis {axis} {share}%": "잔여 지도 비교 잔차 축 {axis} {share}%", + "Leftover map comparison leftover axis": "잔여 지도 비교 잔차 축", + "Leftover map comparison leftover-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 comparison coordinates": "잔여 지도 비교 좌표", "Leftover map comparison graphic": "잔여 지도 비교 그림", "Leftover map comparison": "잔여 지도 비교", @@ -947,6 +954,13 @@ const TRANSLATIONS: Partial>> = { "leftover map comparison graphic leftover-map axis {axis} σ {value}": "残差地图比较图形轴 {axis} σ {value}", "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": "残差地图比较图形轴 {axis} σ {value} ({share}%)", + "leftover map comparison leftover axis {axis} σ {value}": "残差地图比较残差轴 {axis} σ {value}", + "leftover map comparison leftover axis {axis} σ {value} {share}%": + "残差地图比较残差轴 {axis} σ {value} {share}%", + "leftover map comparison leftover axis {axis} {share}%": "残差地图比较残差轴 {axis} {share}%", + "Leftover map comparison leftover axis": "残差地图比较残差轴", + "Leftover map comparison leftover-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 comparison coordinates": "残差地图比较坐标", "Leftover map comparison graphic": "残差地图比较图形", "Leftover map comparison": "残差地图比较", @@ -1615,6 +1629,13 @@ const TRANSLATIONS: Partial>> = { "leftover map comparison graphic leftover-map axis {axis} σ {value}": "残差マップの比較図軸 {axis} σ {value}", "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": "残差マップの比較図軸 {axis} σ {value} ({share}%)", + "leftover map comparison leftover axis {axis} σ {value}": "残差マップの比較残差軸 {axis} σ {value}", + "leftover map comparison leftover axis {axis} σ {value} {share}%": + "残差マップの比較残差軸 {axis} σ {value} {share}%", + "leftover map comparison leftover axis {axis} {share}%": "残差マップの比較残差軸 {axis} {share}%", + "Leftover map comparison leftover axis": "残差マップの比較残差軸", + "Leftover map comparison leftover-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 comparison coordinates": "残差マップの比較座標", "Leftover map comparison graphic": "残差マップの比較図", "Leftover map comparison": "残差マップの比較", @@ -2263,6 +2284,15 @@ const TRANSLATIONS: Partial>> = { "trục đồ họa so sánh bản đồ phần dư {axis} σ {value}", "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": "trục đồ họa so sánh bản đồ phần dư {axis} σ {value} ({share}%)", + "leftover map comparison leftover axis {axis} σ {value}": + "trục phần dư so sánh bản đồ phần dư {axis} σ {value}", + "leftover map comparison leftover axis {axis} σ {value} {share}%": + "trục phần dư so sánh bản đồ phần dư {axis} σ {value} {share}%", + "leftover map comparison leftover axis {axis} {share}%": + "trục phần dư so sánh bản đồ phần dư {axis} {share}%", + "Leftover map comparison leftover axis": "Trục phần dư so sánh bản đồ phần dư", + "Leftover map comparison leftover-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ỷ phần trục phần dư so sánh bản đồ phần dư là quán tính Gabriel của trục SVD phần dư 1 và 2. Mở cặp phần dư để đọc ô bài–tiêu chí. Các tỷ phần này không tạo ra điểm phần dư.", "Leftover map comparison coordinates": "Tọa độ so sánh bản đồ phần dư", "Leftover map comparison graphic": "Đồ họa so sánh bản đồ phần dư", "Leftover map comparison": "So sánh bản đồ phần dư", diff --git a/frontend/src/leftoverMapAxisBadge.test.ts b/frontend/src/leftoverMapAxisBadge.test.ts index 84ec57a39..7261489cb 100644 --- a/frontend/src/leftoverMapAxisBadge.test.ts +++ b/frontend/src/leftoverMapAxisBadge.test.ts @@ -6,6 +6,9 @@ import { LEFTOVER_MAP_AXIS_BADGE_SINGULAR, } from "./leftoverMapAxisBadge"; import { + LEFTOVER_MAP_COMPARE_AXIS_SHARE, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, LEFTOVER_MAP_PLOT_AXIS_SINGULAR, @@ -51,7 +54,7 @@ describe("leftoverMapAxisBadgeSingular", () => { expect(leftoverMapAxisBadgeSingular({ axis_index: 1 })).toBeNull(); }); - it("stays distinct from leftover-map graphic and comparison graphic leftover-map axis σ copy", () => { + 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).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR); @@ -60,6 +63,9 @@ describe("leftoverMapAxisBadgeSingular", () => { expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe( LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, ); + 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}"); }); }); diff --git a/frontend/src/leftoverMapAxisBadge.ts b/frontend/src/leftoverMapAxisBadge.ts index 7d2b377df..71a7d9dc4 100644 --- a/frontend/src/leftoverMapAxisBadge.ts +++ b/frontend/src/leftoverMapAxisBadge.ts @@ -1,4 +1,7 @@ -/** Caption leftover-axis report badges with persisted Gabriel singular values. */ +/** Caption leftover-axis report badges with persisted Gabriel singular values. + * ADR 0323 captions leftover-axis report badges on the grouping comparison strip + * with a distinct leftover map comparison leftover axis name, not this helper. + */ import type { LeftoverMapAxis } from "./api"; import { diff --git a/frontend/src/leftoverMapPlotAxisSingular.test.ts b/frontend/src/leftoverMapPlotAxisSingular.test.ts index ce08b36a0..bd36ce16a 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.test.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.test.ts @@ -1,7 +1,13 @@ import { describe, expect, it } from "vitest"; import { formatLeftoverMapPlotAxisSingular, + leftoverMapCompareAxisBadge, leftoverSingularForAxis, + LEFTOVER_MAP_COMPARE_AXIS_CAPTION, + LEFTOVER_MAP_COMPARE_AXIS_LABEL, + LEFTOVER_MAP_COMPARE_AXIS_SHARE, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, LEFTOVER_MAP_PLOT_AXIS_SINGULAR, @@ -9,6 +15,7 @@ import { } from "./leftoverMapPlotAxisSingular"; import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE } from "./leftoverMapPlotAxisShare"; import { LEFTOVER_MAP_COMPARE_PLOT_TICK } from "./leftoverMapPlotLayout"; +import { LEFTOVER_MAP_AXIS_BADGE_SINGULAR } from "./leftoverMapAxisBadge"; describe("leftoverSingularForAxis", () => { const axes = [ @@ -92,5 +99,72 @@ describe("leftover map comparison graphic leftover-map axis singular labels", () expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE).not.toBe( "leftover axis {axis} σ {value} {share}%", ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR); + }); +}); + +describe("leftover map comparison leftover-axis report badges", () => { + it("names persisted leftover-map singular values on leftover-axis report badges", () => { + expect(leftoverMapCompareAxisBadge(1, 1.84, 0.82)).toEqual({ + key: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, + values: { axis: 1, value: "1.84", share: "82" }, + }); + expect(leftoverMapCompareAxisBadge(2, 0.86, 0.18)).toEqual({ + key: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, + values: { axis: 2, value: "0.86", share: "18" }, + }); + }); + + it("names rank-0 zero leftover-map singular values on leftover-axis report badges", () => { + expect(leftoverMapCompareAxisBadge(1, 0, 0)).toEqual({ + key: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, + values: { axis: 1, value: "0.00", share: "0" }, + }); + }); + + it("omits leftover-map singular values independently of leftover-map axis share", () => { + expect(leftoverMapCompareAxisBadge(1, Number.NaN, 0.82)).toEqual({ + key: LEFTOVER_MAP_COMPARE_AXIS_SHARE, + values: { axis: 1, share: "82" }, + }); + expect(leftoverMapCompareAxisBadge(2, -0.01, 0.18)).toEqual({ + key: LEFTOVER_MAP_COMPARE_AXIS_SHARE, + values: { axis: 2, share: "18" }, + }); + expect(leftoverMapCompareAxisBadge(1, 1.84, Number.NaN)).toEqual({ + key: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + values: { axis: 1, value: "1.84" }, + }); + expect(leftoverMapCompareAxisBadge(1, null, null)).toBeNull(); + }); + + it("does not invent leftover-map singular values from leftover-map axis share", () => { + expect(leftoverMapCompareAxisBadge(1, undefined, 0.82)?.key).toBe( + LEFTOVER_MAP_COMPARE_AXIS_SHARE, + ); + expect(leftoverMapCompareAxisBadge(1, undefined, 0.82)?.values.value).toBeUndefined(); + }); + + it("stays distinct from leftover-axis, leftover-map graphic, and comparison graphic copy", () => { + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).toBe( + "leftover map comparison leftover axis {axis} σ {value}", + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE).toBe( + "leftover map comparison leftover axis {axis} σ {value} {share}%", + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).toBe( + "leftover map comparison leftover axis {axis} {share}%", + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE).not.toBe( + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe("leftover axis {axis} σ {value}"); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE).not.toBe(LEFTOVER_MAP_AXIS_BADGE_SINGULAR); + expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).not.toBe("leftover axis {axis} {share}%"); + expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE); + expect(LEFTOVER_MAP_COMPARE_AXIS_LABEL).not.toBe("Leftover-map axis share"); + expect(LEFTOVER_MAP_COMPARE_AXIS_CAPTION).toContain("Open a leftover pair"); }); }); diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts index a621fe351..70130898e 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -1,6 +1,7 @@ /** Caption leftover-map plot axes with persisted Gabriel singular values. */ import type { LeftoverMapAxis } from "./api"; +import { formatLeftoverMapPlotAxisShare } from "./leftoverMapPlotAxisShare"; export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR = "leftover-map axis {axis} σ {value}"; @@ -13,11 +14,30 @@ export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE = "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)"; +export const LEFTOVER_MAP_COMPARE_AXIS_SINGULAR = + "leftover map comparison leftover axis {axis} σ {value}"; + +export const LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE = + "leftover map comparison leftover axis {axis} σ {value} {share}%"; + +export const LEFTOVER_MAP_COMPARE_AXIS_SHARE = + "leftover map comparison leftover axis {axis} {share}%"; + +export const LEFTOVER_MAP_COMPARE_AXIS_LABEL = "Leftover map comparison leftover axis"; + +export const LEFTOVER_MAP_COMPARE_AXIS_CAPTION = + "Leftover map comparison leftover-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."; + export type LeftoverMapPlotAxisSingular = { axis_index: LeftoverMapAxis["axis_index"]; leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; }; +export type LeftoverMapCompareAxisBadge = { + key: string; + values: { axis: number; value?: string; share?: string }; +}; + export function leftoverSingularForAxis( axes: ReadonlyArray | null | undefined, axisIndex: number, @@ -42,3 +62,25 @@ export function formatLeftoverMapPlotAxisSingular( } return leftoverSingular.toFixed(2); } + +export function leftoverMapCompareAxisBadge( + 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_COMPARE_AXIS_SHARE, values: { axis: axisIndex, share: percent } }; + } + if (singular !== null && percent === null) { + return { key: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, values: { axis: axisIndex, value: singular } }; + } + return { + key: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, + values: { axis: axisIndex, value: singular as string, share: percent as string }, + }; +} diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index 6787cd828..88d118208 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -1369,6 +1369,9 @@ 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 comparison leftover axis {axis} σ {value}", + ); expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe( LEFTOVER_MAP_COMPARE_PLOT_SEGMENT_DISTANCE, ); diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index a6d4787f8..217a56bae 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -83,6 +83,8 @@ * already-named leftover-map axes. * 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. */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; diff --git a/pyproject.toml b/pyproject.toml index da173ad89..95202a36c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.79.0" +version = "2.80.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 596d25198c8f3d04e4af8660ae908d8d5c69cb11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 17:16:13 +0900 Subject: [PATCH 02/11] fix(ui): project comparison strip singular badges --- frontend/src/leftoverMapPlotAxisSingular.ts | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts index dad7c12c4..5d22bce41 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -1,6 +1,7 @@ /** Project persisted leftover-map singular-value evidence into buyer-visible axis captions. */ import type { LeftoverMapAxis } from "./api"; +import { formatLeftoverMapPlotAxisShare } from "./leftoverMapPlotAxisShare"; export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = "leftover map comparison graphic leftover-map axis {axis} σ {value}"; @@ -8,11 +9,29 @@ export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE = "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)"; +export const LEFTOVER_MAP_COMPARE_AXIS_SINGULAR = + "leftover map comparison leftover axis {axis} σ {value}"; + +export const LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE = + "leftover map comparison leftover axis {axis} σ {value} {share}%"; + +export const LEFTOVER_MAP_COMPARE_AXIS_SHARE = + "leftover map comparison leftover axis {axis} {share}%"; + export type LeftoverMapPlotAxisSingular = { axis_index: LeftoverMapAxis["axis_index"]; leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; }; +export type LeftoverMapCompareAxisBadge = { + template: string; + values: { + axis: LeftoverMapAxis["axis_index"]; + value?: string; + share?: string; + }; +}; + export function leftoverSingularForAxis( axes: ReadonlyArray | null | undefined, axisIndex: number, @@ -37,3 +56,38 @@ export function formatLeftoverMapPlotAxisSingular( } return leftoverSingular.toFixed(2); } + +/** + * Compose comparison-strip axis evidence without deriving one persisted + * measurement from the other. Missing or invalid evidence is omitted + * independently; when both are absent, no badge is rendered. + */ +export function leftoverMapCompareAxisBadge( + axis: Pick & { + leftover_share?: LeftoverMapAxis["leftover_share"] | null; + leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; + }, +): LeftoverMapCompareAxisBadge | null { + const singular = formatLeftoverMapPlotAxisSingular(axis.leftover_singular_value); + const share = formatLeftoverMapPlotAxisShare(axis.leftover_share); + + if (singular === null && share === null) { + return null; + } + if (singular === null) { + return { + template: LEFTOVER_MAP_COMPARE_AXIS_SHARE, + values: { axis: axis.axis_index, share: share ?? undefined }, + }; + } + if (share === null) { + return { + template: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + values: { axis: axis.axis_index, value: singular }, + }; + } + return { + template: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, + values: { axis: axis.axis_index, value: singular, share }, + }; +} From 6fa55ff6531eec2d44855004a0a6b6c236a032f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 17:16:56 +0900 Subject: [PATCH 03/11] test(ui): cover comparison strip singular badge states --- frontend/src/leftoverMapAxisBadge.test.ts | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/frontend/src/leftoverMapAxisBadge.test.ts b/frontend/src/leftoverMapAxisBadge.test.ts index 84ec57a39..579273f88 100644 --- a/frontend/src/leftoverMapAxisBadge.test.ts +++ b/frontend/src/leftoverMapAxisBadge.test.ts @@ -6,6 +6,10 @@ import { LEFTOVER_MAP_AXIS_BADGE_SINGULAR, } from "./leftoverMapAxisBadge"; import { + leftoverMapCompareAxisBadge, + LEFTOVER_MAP_COMPARE_AXIS_SHARE, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, LEFTOVER_MAP_PLOT_AXIS_SINGULAR, @@ -63,3 +67,48 @@ describe("leftoverMapAxisBadgeSingular", () => { expect(LEFTOVER_MAP_AXIS_BADGE_SINGULAR).not.toBe("leftover axis {axis} σ {value}"); }); }); + +describe("leftoverMapCompareAxisBadge", () => { + it("keeps persisted singular and share evidence independent", () => { + expect( + leftoverMapCompareAxisBadge({ + axis_index: 1, + leftover_singular_value: 1.24, + leftover_share: 0.42, + }), + ).toEqual({ + template: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_SHARE, + values: { axis: 1, value: "1.24", share: "42" }, + }); + expect( + leftoverMapCompareAxisBadge({ + axis_index: 1, + leftover_singular_value: 0, + leftover_share: null, + }), + ).toEqual({ + template: LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + values: { axis: 1, value: "0.00" }, + }); + expect( + leftoverMapCompareAxisBadge({ + axis_index: 2, + leftover_singular_value: null, + leftover_share: 0.18, + }), + ).toEqual({ + template: LEFTOVER_MAP_COMPARE_AXIS_SHARE, + values: { axis: 2, share: "18" }, + }); + }); + + it("omits the comparison-strip badge when neither persisted measure is usable", () => { + expect( + leftoverMapCompareAxisBadge({ + axis_index: 1, + leftover_singular_value: Number.NaN, + leftover_share: Number.POSITIVE_INFINITY, + }), + ).toBeNull(); + }); +}); From cbe6c44d14ee7403fa909570b59265baf6bb7ecf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 17:18:15 +0900 Subject: [PATCH 04/11] fix(ui): render comparison strip axis evidence --- frontend/src/components/LeftoverMapPlot.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index e68507c5b..5de0b8fb5 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -29,6 +29,7 @@ import { } from "../leftoverMapPlotAxisShare"; import { formatLeftoverMapPlotAxisSingular, + leftoverMapCompareAxisBadge, leftoverSingularForAxis, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, @@ -119,7 +120,9 @@ function leftoverMapPlotAxisText( * 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. + * 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 * pair row match the plot. Pair segments name persisted leftover-map * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map @@ -216,6 +219,12 @@ export function LeftoverMapPlot({ const itemCoverageCounts = leftoverMapItemCoverageCounts(leftoverMapCoverage); const incompletePostCount = leftoverMapIncompletePostCount(leftoverMapCoverage); const incompleteItemCount = leftoverMapIncompleteItemCount(leftoverMapCoverage); + const comparisonAxisBadges = + variant === "comparison" + ? (leftoverMapAxes ?? []) + .map((axis) => leftoverMapCompareAxisBadge(axis)) + .filter((badge): badge is NonNullable => badge !== null) + : []; const openPost = (postId: string) => { const pair = firstPlottablePairForPost(pairs, postId); @@ -232,6 +241,15 @@ export function LeftoverMapPlot({
{t(variant === "comparison" ? LEFTOVER_MAP_COMPARE_PLOT_CAPTION : LEFTOVER_MAP_PLOT_CAPTION)}
+ {comparisonAxisBadges.length > 0 ? ( +
+ {comparisonAxisBadges.map((badge) => ( + + {tf(badge.template, badge.values)} + + ))} +
+ ) : null} {coverageCounts !== null ? (

Date: Tue, 15 Sep 2026 17:18:49 +0900 Subject: [PATCH 05/11] docs(adr): record comparison strip singular badges --- ...0323-leftover-map-compare-axis-singular.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/adr/0323-leftover-map-compare-axis-singular.md diff --git a/docs/adr/0323-leftover-map-compare-axis-singular.md b/docs/adr/0323-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..71ec12b5f --- /dev/null +++ b/docs/adr/0323-leftover-map-compare-axis-singular.md @@ -0,0 +1,37 @@ +# ADR 0323 — Name persisted singular values on grouping comparison leftover-axis badges + +**Decision status:** Proposed +**Date:** 2026-08-31 + +Amends leftover-map axis share persistence ([ADR 0148](0148-leftover-map-axis-share.md)) and the grouping comparison strip ([ADR 0149](0149-period-grouping-comparison.md)). It follows comparison-graphic singular evidence ([ADR 0321](0321-leftover-map-compare-plot-singular.md)) and report-axis singular badges ([ADR 0322](0322-leftover-map-axis-singular.md)) without changing either contract. + +## Context + +LineageWeave already persists leftover-map axis share and singular value `σ_k` for each axis. The report surface and the grouping-comparison graphic can expose that evidence, but the grouping comparison strip still omits it. Buyers therefore cannot read the persisted axis scale from the comparison strip itself. + +The two measurements have different missingness. A valid share must remain visible when `σ_k` is absent or invalid; a valid finite, non-negative `σ_k` — including `0` — must remain visible when share is absent or invalid. Neither value may be reconstructed from the other. This increment does not add persistence, SQL, psychometric estimation, or a new canonical owner. + +## Decision + +For comparison-strip leftover axes, project already persisted values into four independent states: + +- combined: `leftover map comparison leftover axis {axis} σ {value} {share}%`; +- singular only: `leftover map comparison leftover axis {axis} σ {value}`; +- share only: `leftover map comparison leftover axis {axis} {share}%`; +- neither usable: omit the badge. + +`σ_k` is accepted only when finite and non-negative; `σ=0` remains explicit. Share is accepted only when finite. No `Math.sqrt`, inversion, normalization, or other derivation is allowed. The copy remains distinct from report-axis badges and from comparison-graphic axis captions. + +The comparison rendering reuses LineageWeave-owned persisted evidence and existing localization composition. It does not copy logic from fast-mlsirm, TEPP, or any other canonical owner. + +## Consequences + +The grouping comparison buyer path exposes axis evidence without conflating scale and share. Missing evidence fails closed by omission, while usable evidence survives independently. Existing comparison-graphic interaction, pointer/keyboard behavior, and post navigation remain unchanged. + +This ADR stays **Proposed** until the exact-head implementation, executable contract, frontend tests, accessibility/browser evidence, and independent review are 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 ea107482477552087ce40a719e475cc3257b68b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 17:18:57 +0900 Subject: [PATCH 06/11] docs(changelog): record comparison axis singular evidence --- CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md diff --git a/CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md b/CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..34b21d6e2 --- /dev/null +++ b/CHANGELOG.d/2.80.0-leftover-map-compare-axis-singular.md @@ -0,0 +1,3 @@ +### Added + +- Grouping comparison leftover-axis evidence now names persisted Gabriel singular values independently of persisted axis share. Combined, singular-only, share-only, and empty states remain distinct; finite `σ=0` is preserved and neither measure is derived from the other. From 554e3adef2565060e54891f17148ca3d07e951d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 17:23:02 +0900 Subject: [PATCH 07/11] fix(ui): type comparison axis 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 5d22bce41..14fa82bde 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -25,11 +25,7 @@ export type LeftoverMapPlotAxisSingular = { export type LeftoverMapCompareAxisBadge = { template: string; - values: { - axis: LeftoverMapAxis["axis_index"]; - value?: string; - share?: string; - }; + values: Record; }; export function leftoverSingularForAxis( @@ -74,13 +70,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 }, @@ -88,6 +84,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 afa979798d66d200e2d164c7ae11cb2d1ed8a895 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 01:01:16 +0900 Subject: [PATCH 08/11] chore(stack): adopt #862 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 6f43c2dba844dc79969d9625114df227216e32b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 01:02:33 +0900 Subject: [PATCH 09/11] chore(stack): adopt #862 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 5de0b8fb5..66306dc6b 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -199,8 +199,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({ @@ -343,7 +345,14 @@ export function LeftoverMapPlot({ Date: Sat, 19 Sep 2026 23:25:07 +0900 Subject: [PATCH 10/11] test(reporting): scope singular independence contract --- ...ng_comparison_graphic_singular_contract.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/test_grouping_comparison_graphic_singular_contract.py b/tests/test_grouping_comparison_graphic_singular_contract.py index 7de47f808..45eae0dc9 100644 --- a/tests/test_grouping_comparison_graphic_singular_contract.py +++ b/tests/test_grouping_comparison_graphic_singular_contract.py @@ -8,6 +8,15 @@ SINGULAR_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotAxisSingular.ts" +def _exported_function_source(source: str, signature: str) -> str: + """Return one exported function so independence checks do not police sibling composition.""" + start = source.index(signature) + next_export = source.find("\nexport function ", start + len(signature)) + if next_export == -1: + return source[start:] + return source[start:next_export] + + def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None: """Comparison-axis σ copy must remain distinct from report, strip, share, and tick copy.""" assert SINGULAR_SOURCE.exists(), ( @@ -34,12 +43,24 @@ def test_singular_value_is_read_from_axis_evidence_and_fails_closed() -> None: """Persisted σ=0 stays explicit; missing, non-finite, or negative σ omits independently.""" assert SINGULAR_SOURCE.exists(), "persisted singular-value projection helper is missing" singular_source = SINGULAR_SOURCE.read_text(encoding="utf-8") + formatter_source = _exported_function_source( + singular_source, + "export function formatLeftoverMapPlotAxisSingular", + ) + + assert "Number.isFinite" in formatter_source + assert "< 0" in formatter_source + assert "return null" in formatter_source + assert ".toFixed(2)" in formatter_source + assert "leftover_share" not in formatter_source + assert "Math.sqrt" not in formatter_source + assert "Math.max" not in formatter_source + assert "Math.min" not in formatter_source - assert "leftover_singular_value" in singular_source - assert "Number.isFinite" in singular_source - assert "< 0" in singular_source - assert "return null" in singular_source - assert ".toFixed(2)" in singular_source - assert "leftover_share" not in singular_source - assert "Math.max" not in singular_source - assert "Math.min" not in singular_source + # Comparison composition may display independently persisted σ and share together; + # it must delegate to each evidence formatter instead of deriving one from the other. + assert ( + "const singular = formatLeftoverMapPlotAxisSingular(axis.leftover_singular_value);" + in singular_source + ) + assert "const share = formatLeftoverMapPlotAxisShare(axis.leftover_share);" in singular_source From a9e10500fab013da7ba4ec17451afa7000a0d38f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 23:04:36 +0900 Subject: [PATCH 11/11] test(reports): preserve owner acceptance RED through #863 --- ...ng_comparison_graphic_singular_contract.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/test_grouping_comparison_graphic_singular_contract.py b/tests/test_grouping_comparison_graphic_singular_contract.py index bda9a58fa..78e5ad4fb 100644 --- a/tests/test_grouping_comparison_graphic_singular_contract.py +++ b/tests/test_grouping_comparison_graphic_singular_contract.py @@ -17,18 +17,14 @@ def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None singular_source = SINGULAR_SOURCE.read_text(encoding="utf-8") plot_source = PLOT_SOURCE.read_text(encoding="utf-8") - assert ( - 'export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR =\n' - ' "leftover map comparison graphic leftover-map axis {axis} σ {value}";' - in singular_source - ) - assert ( - 'export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE =\n' - ' "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)";' - in singular_source - ) - assert "LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR" in plot_source - assert "LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE" in plot_source + assert singular_source.count("function formatLeftoverSingularValue") == 1 + assert singular_source.count("function fillTemplate") == 1 + assert "export function formatLeftoverSingularValue" in singular_source + assert "export function renderLeftoverMapAxisSingular" in singular_source + assert "export function renderLeftoverMapAxisSingularShare" in singular_source + assert "return renderLeftoverMapAxisSingular(" in singular_source + assert "return renderLeftoverMapAxisSingularShare(" in singular_source + assert "formatLeftoverSingularValue(" not in plot_source def test_singular_value_is_read_from_axis_evidence_and_fails_closed() -> None: @@ -41,7 +37,6 @@ def test_singular_value_is_read_from_axis_evidence_and_fails_closed() -> None: assert "< 0" in singular_source assert "return null" in singular_source assert ".toFixed(2)" in singular_source - assert "leftover_share" not in singular_source assert "Math.max" not in singular_source assert "Math.min" not in singular_source