From c513002bea9e0ee3e6ccd0931c5256e52dbbd878 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:18:45 +0000 Subject: [PATCH] feat(reports): leftover-map cross share on pair segments (v2.32.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caption leftover-map graphic-display pair segments with persisted leftover-map cross share x = 2 R̂ U / R² (ADR 0275). After make seed, closest and farthest leftover pairs sit above the member list with the Gabriel biplot of already-named coordinates, leftover-map axis share when finite, axis ticks that match ξ (x, y) ζ (x, y) on the pair row, pair segments that match d on the pair row, pair segments that match R̂ on the pair row, pair segments that match R̂²/R² on the pair row, pair segments that match U²/R² on the pair row, and pair segments that match 2R̂U/R² on the pair row; click a post marker or a pair button opens that post. A missing or non-finite x omits that leftover-map cross share caption. A finite negative share is shown; do not clamp to nonnegative. Do not invent x from R̂, U, and R or from plotted coordinates. Never invent a leftover score or a theta. No new columns. --- AGENTS.md | 8 +- ARCHITECTURE.md | 6 +- ...2.32.0-leftover-map-segment-cross-share.md | 16 ++ CHANGELOG.md | 16 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 6 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- docs/adr/0185-leftover-map-cross-share.md | 6 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- ...73-leftover-map-segment-explained-share.md | 7 +- ...-leftover-map-segment-unexplained-share.md | 8 +- .../0275-leftover-map-segment-cross-share.md | 141 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 35 +++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../components/LeftoverMapPlot.stories.tsx | 1 + .../src/components/LeftoverMapPlot.test.tsx | 38 ++++- frontend/src/components/LeftoverMapPlot.tsx | 28 +++- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 23 ++- frontend/src/i18n.ts | 20 ++- frontend/src/leftoverMapPlotLayout.test.ts | 99 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 26 +++- pyproject.toml | 2 +- uv.lock | 2 +- 25 files changed, 468 insertions(+), 46 deletions(-) create mode 100644 CHANGELOG.d/2.32.0-leftover-map-segment-cross-share.md create mode 100644 docs/adr/0275-leftover-map-segment-cross-share.md diff --git a/AGENTS.md b/AGENTS.md index ca152fe7c..019f0e89c 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) 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) 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 @@ -313,7 +313,11 @@ explained leftover share caption and does not invent `e` from `R̂` and segments with persisted leftover-map unexplained leftover share `s` so the pair-row `U²/R²` badge matches the graphic; a missing or non-finite `s` omits that unexplained leftover share caption and does not invent -`s` from `U` and `R` or from plotted coordinates. When `R`, `R̂`, `U`, `x`, +`s` from `U` and `R` or from plotted coordinates. ADR 0275 captions leftover-map pair +segments with persisted leftover-map cross share `x` so +the pair-row `2R̂U/R²` badge matches the graphic; a missing or non-finite +`x` omits that leftover-map cross share caption and does not invent +`x` from `R̂`, `U`, and `R` or from plotted coordinates. When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, `e + s + x = 1`. When coordinates, reconstruction, and distance are finite, `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖`. The pairs sit above the member diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4569b175e..e1e86008d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275) 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 @@ -651,7 +651,9 @@ with persisted leftover-map distance `d`. ADR 0272 captions leftover-map pair segments with persisted leftover-map reconstruction `R̂`. ADR 0273 captions leftover-map pair segments with persisted leftover-map explained leftover share `e`. ADR 0274 captions leftover-map pair -segments with persisted leftover-map unexplained leftover share `s`. Click a post marker or a pair button +segments with persisted leftover-map unexplained leftover share `s`. +ADR 0275 captions leftover-map pair segments with persisted leftover-map +cross share `x`. 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.32.0-leftover-map-segment-cross-share.md b/CHANGELOG.d/2.32.0-leftover-map-segment-cross-share.md new file mode 100644 index 000000000..6a661293a --- /dev/null +++ b/CHANGELOG.d/2.32.0-leftover-map-segment-cross-share.md @@ -0,0 +1,16 @@ +## 2.32.0 — Leftover-map cross share on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover-map cross share `x = 2 R̂ U / R²` (ADR 0275). After + `make seed`, closest and farthest leftover pairs sit above the member + list with the Gabriel biplot of already-named coordinates, leftover-map + axis share when finite, axis ticks that match `ξ (x, y) ζ (x, y)` on + the pair row, pair segments that match `d` on the pair row, pair + segments that match `R̂` on the pair row, pair segments that match + `R̂²/R²` on the pair row, pair segments that match `U²/R²` on the + pair row, and pair segments that match `2R̂U/R²` on the pair row; + click a post marker or a pair button opens that post. A missing or + non-finite `x` omits that leftover-map cross share caption. A finite + negative share is shown; do not clamp to nonnegative. Do not invent + `x` from `R̂`, `U`, and `R` or from plotted coordinates. Never invent + a leftover score. Never invent a theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c96e1431..944e3a14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,22 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption leftover-map graphic-display pair + segments with persisted leftover-map cross share + `x = 2 R̂ U / R²` (ADR 0275 / v2.32.0). After `make seed`, closest and + farthest leftover pairs sit above the member list with the Gabriel + biplot of already-named coordinates, leftover-map axis share when + finite, axis ticks that match `ξ (x, y) ζ (x, y)` on the pair row, + pair segments that match `d` on the pair row, pair segments that + match `R̂` on the pair row, pair segments that match `R̂²/R²` on + the pair row, pair segments that match `U²/R²` on the pair row, and + pair segments that match `2R̂U/R²` on the pair row; click a post + marker or a pair button opens that post. A missing or non-finite `x` + omits that leftover-map cross share caption. A finite negative share + is shown; do not clamp to nonnegative. Do not invent `x` from `R̂`, + `U`, and `R` or from plotted coordinates. Never invent a leftover + score or a theta. No new columns. + - Period leftover pairs now caption leftover-map graphic-display pair segments with persisted leftover-map unexplained leftover share `s = U² / R²` (ADR 0274 / v2.31.0). After `make seed`, closest and diff --git a/CLAUDE.md b/CLAUDE.md index 1579f3ab5..107079ed7 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), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275), 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/0048-persist-lsirm-leftover-pairs.md b/docs/adr/0048-persist-lsirm-leftover-pairs.md index 6451f3ef6..f94e7d097 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -17,7 +17,8 @@ [ADR 0271](0271-leftover-map-segment-distance.md) (leftover-map distance on pair segments); [ADR 0272](0272-leftover-map-segment-reconstruction.md) (leftover-map reconstruction on pair segments); [ADR 0273](0273-leftover-map-segment-explained-share.md) (leftover-map explained leftover share on pair segments); -[ADR 0274](0274-leftover-map-segment-unexplained-share.md) (leftover-map unexplained leftover share on pair segments) +[ADR 0274](0274-leftover-map-segment-unexplained-share.md) (leftover-map unexplained leftover share on pair segments); +[ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments) ## Context @@ -82,7 +83,8 @@ leftover-map reconstruction `R̂`; it adds no columns. ADR 0273 captions leftover-map pair segments with persisted leftover-map explained leftover share `e`; it adds no columns. ADR 0274 captions leftover-map pair segments with persisted leftover-map unexplained leftover share `s`; it -adds no columns. +adds no columns. ADR 0275 captions leftover-map pair segments with +persisted leftover-map cross share `x`; it adds no columns. Cascade the rows with `report_period_score`. A leftover post must also be a `report_member_score` row, and the leftover criterion diff --git a/docs/adr/0049-leftover-pair-report-ui.md b/docs/adr/0049-leftover-pair-report-ui.md index e60d13201..b3ae6fea1 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -18,7 +18,8 @@ [ADR 0271](0271-leftover-map-segment-distance.md) (leftover-map distance on pair segments); [ADR 0272](0272-leftover-map-segment-reconstruction.md) (leftover-map reconstruction on pair segments); [ADR 0273](0273-leftover-map-segment-explained-share.md) (leftover-map explained leftover share on pair segments); -[ADR 0274](0274-leftover-map-segment-unexplained-share.md) (leftover-map unexplained leftover share on pair segments) +[ADR 0274](0274-leftover-map-segment-unexplained-share.md) (leftover-map unexplained leftover share on pair segments); +[ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments) ## Context @@ -49,7 +50,8 @@ when finite (ADR 0269). Leftover-map axis ticks name persisted `ξ` / distance `d` (ADR 0271). Pair segments name persisted leftover-map reconstruction `R̂` (ADR 0272). Pair segments name persisted leftover-map explained leftover share `e` (ADR 0273). Pair segments name persisted leftover-map -unexplained leftover share `s` (ADR 0274). The pair renders every available finite measurement. +unexplained leftover share `s` (ADR 0274). Pair segments name persisted leftover-map +cross share `x` (ADR 0275). The pair renders every available finite measurement. The next action uses the first available value in the priority below; no amendment hides another badge, rank 0 explicitly names no leftover structure, and unexplained leftover names @@ -88,6 +90,8 @@ Leftover-map explained leftover share on pair segments is [ADR 0273](0273-leftover-map-segment-explained-share.md). Leftover-map unexplained leftover share on pair segments is [ADR 0274](0274-leftover-map-segment-unexplained-share.md). +Leftover-map cross share on pair segments is +[ADR 0275](0275-leftover-map-segment-cross-share.md). After `make seed`, closest and farthest leftover pairs sit above the member list. Click a pair to open that post with the leftover diff --git a/docs/adr/0185-leftover-map-cross-share.md b/docs/adr/0185-leftover-map-cross-share.md index 755523b43..56d030df7 100644 --- a/docs/adr/0185-leftover-map-cross-share.md +++ b/docs/adr/0185-leftover-map-cross-share.md @@ -2,6 +2,8 @@ **Decision status:** Draft **Date:** 2026-08-24 +**Amended by:** [ADR 0275](0275-leftover-map-segment-cross-share.md) +(leftover-map cross share on graphic-display pair segments) Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and [ADR 0049](0049-leftover-pair-report-ui.md). @@ -83,7 +85,9 @@ invent a leftover score. Do not invent a theta. `GET /api/reports/{grouping}/{period}` returns `leftover_map_cross_share`. After `make seed`, closest and farthest leftover pairs sit above the member list with named `2R̂U/R²` next -to `d`; click opens that post. Hidden posts stay hidden. +to `d`; click opens that post. Hidden posts stay hidden. ADR 0275 +captions leftover-map graphic-display pair segments with that same +persisted leftover-map cross share; it adds no columns. ## Related diff --git a/docs/adr/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index e13aad80a..c11c16c7d 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -14,7 +14,9 @@ [ADR 0273](0273-leftover-map-segment-explained-share.md) (leftover-map explained leftover share on pair segments); [ADR 0274](0274-leftover-map-segment-unexplained-share.md) -(leftover-map unexplained leftover share on pair segments) +(leftover-map unexplained leftover share on pair segments); +[ADR 0275](0275-leftover-map-segment-cross-share.md) +(leftover-map cross share on pair segments) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -92,7 +94,8 @@ persisted `ξ` / `ζ` coordinates (ADR 0270). Pair segments name persisted leftover-map distance `d` (ADR 0271). Pair segments name persisted leftover-map reconstruction `R̂` (ADR 0272). Pair segments name persisted leftover-map explained leftover share `e` (ADR 0273). Pair segments name -persisted leftover-map unexplained leftover share `s` (ADR 0274). When coordinates, reconstruction, and +persisted leftover-map unexplained leftover share `s` (ADR 0274). Pair segments name +persisted leftover-map cross share `x` (ADR 0275). When coordinates, reconstruction, and distance are all finite, `R̂ = ξ_{1:2} · ζ_{1:2}` and `d = ‖ξ_{1:2} − ζ_{1:2}‖` remain the same identities already persisted by ADR 0267. diff --git a/docs/adr/0273-leftover-map-segment-explained-share.md b/docs/adr/0273-leftover-map-segment-explained-share.md index 5fe4e4810..030f6d85e 100644 --- a/docs/adr/0273-leftover-map-segment-explained-share.md +++ b/docs/adr/0273-leftover-map-segment-explained-share.md @@ -4,7 +4,9 @@ **Date:** 2026-08-29 **Amended by:** [ADR 0274](0274-leftover-map-segment-unexplained-share.md) -(leftover-map unexplained leftover share on pair segments) +(leftover-map unexplained leftover share on pair segments); +[ADR 0275](0275-leftover-map-segment-cross-share.md) +(leftover-map cross share on pair segments) Amends [ADR 0268](0268-leftover-map-graphic-display.md) and [ADR 0049](0049-leftover-pair-report-ui.md). Independent of leftover-map @@ -88,7 +90,8 @@ finite, leftover-map axis ticks name the same coordinates shown on the pair row, pair segments name persisted leftover-map distance `d`, persisted leftover-map reconstruction `R̂`, and persisted leftover-map explained leftover share `e`, and pair segments name persisted leftover-map -unexplained leftover share `s` (ADR 0274); click a post marker or a pair button +unexplained leftover share `s` (ADR 0274) and leftover-map cross share `x` +(ADR 0275); click a post marker or a pair button opens that post. Hidden posts stay hidden. Rank-0 unused axes still plot at the origin, still name zero leftover-map axis share with a `0` tick, still name `d 0.00` when that distance is persisted, still name diff --git a/docs/adr/0274-leftover-map-segment-unexplained-share.md b/docs/adr/0274-leftover-map-segment-unexplained-share.md index bd26fd3d1..53ce99e85 100644 --- a/docs/adr/0274-leftover-map-segment-unexplained-share.md +++ b/docs/adr/0274-leftover-map-segment-unexplained-share.md @@ -2,6 +2,8 @@ **Decision status:** Accepted **Date:** 2026-08-29 +**Amended by:** [ADR 0275](0275-leftover-map-segment-cross-share.md) +(leftover-map cross share on pair segments) Amends [ADR 0268](0268-leftover-map-graphic-display.md) and [ADR 0049](0049-leftover-pair-report-ui.md). Independent of leftover-map @@ -88,13 +90,15 @@ finite, leftover-map axis ticks name the same coordinates shown on the pair row, pair segments name persisted leftover-map distance `d`, persisted leftover-map reconstruction `R̂`, persisted leftover-map explained leftover share `e`, and persisted leftover-map unexplained -leftover share `s`; click a post marker or a pair button opens that +leftover share `s`, and pair segments name persisted leftover-map +cross share `x` (ADR 0275); click a post marker or a pair button opens that post. Hidden posts stay hidden. Rank-0 unused axes still plot at the origin, still name zero leftover-map axis share with a `0` tick, still name `d 0.00` when that distance is persisted, still name `R̂ 0.00` when that reconstruction is persisted, still name `R̂²/R² 0.00` when that explained leftover share is persisted, and still name `U²/R² 0.00` -when that unexplained leftover share is persisted. When `R`, `R̂`, `U`, +when that unexplained leftover share is persisted, and still name `2R̂U/R² 0.00` +when that leftover-map cross share is persisted. When `R`, `R̂`, `U`, `x`, `s`, and `e` are all finite, `e + s + x = 1` remains the same identity already persisted by ADR 0233 and ADR 0266. diff --git a/docs/adr/0275-leftover-map-segment-cross-share.md b/docs/adr/0275-leftover-map-segment-cross-share.md new file mode 100644 index 000000000..c5e062c43 --- /dev/null +++ b/docs/adr/0275-leftover-map-segment-cross-share.md @@ -0,0 +1,141 @@ +# ADR 0275 — Name leftover-map cross share on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-29 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover-map cross share +persistence ([ADR 0185](0185-leftover-map-cross-share.md)). Independent of +leftover-map unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover-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)), +and leftover-map axis share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0185 already persists leftover-map cross share +`x = 2 R̂ U / R²` of raw residual after two-axis Gabriel reconstruction. +ADR 0274 already names persisted leftover-map unexplained leftover share +`s` on the leftover-map graphic-display pair segment. The segment still +has no leftover-map cross share label, so a buyer who reads `2R̂U/R² 0.12` +on the pair row cannot match that identity remainder to the graphic pair. +Hiding `x` on the segment lets leftover-map reconstruction `R̂`, leftover +residual `R`, unexplained leftover `U`, leftover-map explained leftover +share `e`, or leftover-map unexplained leftover share `s` be read as a +complete leftover partition even after `e` and `s` are named on the +segment. When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, +`e + s + x = 1`; the graphic must name the same persisted `x` the pair +row already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover-map cross share. It does not add columns. It does not +recompute `x` from `R̂`, `U`, and `R` or from plotted coordinates. It +does not persist leftover-map inner product, cosine, or length as +separate columns. 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 **0275** so it does not +collide with 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 persistence (0185), leftover residual disclosure, +leftover observed `Y` / expected `E`, leftover-map rank, two-axis leftover-map +distance persistence, 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 each closest or farthest +pair segment with the same persisted leftover-map cross share formatter +as the pair-row `2R̂U/R²` badge. A missing or non-finite `x` omits that +leftover-map cross share caption and keeps the connecting line and any +leftover-map distance, reconstruction, explained leftover share, or +unexplained leftover share caption. Rank-0 origin cells still name +`2R̂U/R² 0.00` when that persisted share is finite. A finite share +greater than 1 is shown, never clamped. A finite negative share is +shown; do not clamp to nonnegative. Do not invent `x` from `R̂`, `U`, +and `R` or from plotted coordinates. 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 +these leftover-map cross share captions. + +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 `ζ`, leftover-map axes name persisted Gabriel inertia share when +finite, leftover-map axis ticks name the same coordinates shown on +the pair row, pair segments name persisted leftover-map distance `d`, +persisted leftover-map reconstruction `R̂`, persisted leftover-map +explained leftover share `e`, persisted leftover-map unexplained +leftover share `s`, and persisted leftover-map cross share `x`; click +a post marker or a pair button opens that post. Hidden posts stay +hidden. Rank-0 unused axes still plot at the origin, still name zero +leftover-map axis share with a `0` tick, still name `d 0.00` when that +distance is persisted, still name `R̂ 0.00` when that reconstruction is +persisted, still name `R̂²/R² 0.00` when that explained leftover share +is persisted, still name `U²/R² 0.00` when that unexplained leftover +share is persisted, and still name `2R̂U/R² 0.00` when that leftover-map +cross share is persisted. When `R`, `R̂`, `U`, `x`, `s`, and `e` are +all finite, `e + s + x = 1` remains the same identity already persisted +by ADR 0185, ADR 0233, and ADR 0266. + +## 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 persistence, leftover-map rank, +leftover-map inner product, leftover-map cosine, leftover-map length, +leftover-map reconstruction persistence, leftover-map unexplained +leftover, leftover-map cross share persistence, leftover-map unexplained +leftover share persistence, leftover-map explained leftover share +persistence, leftover-map coordinate persistence, leftover-map graphic +display, leftover-map axis share on the graphic display, leftover-map +coordinate ticks, leftover-map distance on pair segments, leftover-map +reconstruction on pair segments, leftover-map explained leftover share +on pair segments, and leftover-map unexplained leftover share on pair +segments. + +## 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. Gabriel +reconstruction of the leftover cell is the two-axis inner product +`R̂ = ξ_{1:2} · ζ_{1:2}`. Leftover-map cross share is the identity +remainder `x = 2 R̂ U / R²` of raw residual after two-axis +reconstruction, with `U = R − R̂`.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5dd2e2204..c6b291c2e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,40 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-29 23:10 KST. Protected `main` is +> `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map +> explained leftover share, #775). Open ready PRs still lack independent +> APPROVE. #782 leftover-map coordinates + graphic + axis share + ticks +> + distance (v2.24.0–v2.28.0 / ADR 0267–0271) is on +> `9bdd3e4d6e47a34ac22fca228b8d2a23d8cecca9` with auto-merge SQUASH +> armed. Independent APPROVE is still required for protected main. +> #802 leftover-map explained leftover share on pair segments (v2.30.0 +> / ADR 0273) is stacked on leftover-coordinates at +> `79ec22f0a4b7236ab06500c7bf8f392da6a9419b`. #803 leftover-map +> unexplained leftover share on pair segments (v2.31.0 / ADR 0274) is +> stacked on #802 at `089a15713736cbecd4141d460c43d985d3973a92`. Leave +> #782/#802/#803 open for independent review. Do not squash-merge stacked +> leftover PRs onto an unprotected leftover base. Auto-merge squash +> remains armed on #782/#780/#774/#772/#771/#770. Drafts remain dirty +> against `main`. #96 is already gone (`Could not resolve to an Issue +> with the number of 96`). GitHub writes through `gh`/MCP succeed +> (comment/create-PR/auto-merge). Copilot review is not independent +> APPROVE. Do not self-approve. +> +> Next buyer increment on this cycle: leftover-map cross share `x` on +> graphic-display pair segments (ADR 0275 / v2.32.0) delivered locally on +> `feat/leftover-map-segment-cross-share-v2320`. Caption each +> closest/farthest segment with persisted leftover-map cross share so +> the pair-row `2R̂U/R²` badge matches the graphic and +> `e + s + x = 1` is graphic-auditable. UI-only; no new columns. +> Missing/non-finite `x` omits that leftover-map cross share caption. A +> finite negative share is shown; do not clamp to nonnegative. Do not +> invent `x` from `R̂`, `U`, and `R` or from plotted coordinates. Do not +> invent leftover scores. Stack onto leftover branch +> `feat/leftover-map-segment-unexplained-share-v2310`; leave the PR open +> for independent review. Do not squash-merge onto the leftover base. +> Do not persist leftover-map inner product, cosine, or length as +> separate columns. + > Exact-head loop overlay: 2026-08-29 20:20 KST. Protected `main` is > `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map > explained leftover share, #775). Open ready PRs still lack independent diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 13d926fd6..d63417657 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, and pair-segment `U²/R²` to leftover-map unexplained leftover share, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. `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`, and `U²/R² 0.00`, 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, and pair segments name persisted leftover-map unexplained leftover share. | `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, and pair-segment `2R̂U/R²` to leftover-map cross share, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. `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`, and `2R̂U/R² 0.00`, 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, and pair segments name persisted leftover-map cross share. | `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 c0fbd16f6..fbd3aeb86 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.31.0", + "version": "2.32.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.stories.tsx b/frontend/src/components/LeftoverMapPlot.stories.tsx index 91ccf8bc7..915ff4223 100644 --- a/frontend/src/components/LeftoverMapPlot.stories.tsx +++ b/frontend/src/components/LeftoverMapPlot.stories.tsx @@ -83,6 +83,7 @@ export const RankZeroOrigin: Story = { leftover_map_reconstruction: 0, leftover_map_explained_share: 0, leftover_map_unexplained_share: 0, + leftover_map_cross_share: 0, leftover_map_person_axis_1: 0, leftover_map_person_axis_2: 0, leftover_map_item_axis_1: 0, diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 156942cb3..82efd959f 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -15,6 +15,7 @@ const PAIRS: LeftoverPair[] = [ leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, + leftover_map_cross_share: 0.12, leftover_map_person_axis_1: 0.5, leftover_map_person_axis_2: 0.1, leftover_map_item_axis_1: 0.5, @@ -30,6 +31,7 @@ const PAIRS: LeftoverPair[] = [ leftover_map_reconstruction: -0.95, leftover_map_explained_share: 0.6, leftover_map_unexplained_share: 0.05, + leftover_map_cross_share: -0.24, leftover_map_person_axis_1: 0.9, leftover_map_person_axis_2: 0.8, leftover_map_item_axis_1: -0.7, @@ -51,7 +53,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover-map graphic display")).toBeInTheDocument(); expect( screen.getByText( - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -69,6 +71,8 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map explained leftover share R̂²/R² 0.60")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover share U²/R² 0.02")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover share U²/R² 0.05")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.12")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² -0.24")).toBeInTheDocument(); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -118,6 +122,7 @@ describe("LeftoverMapPlot", () => { leftover_map_reconstruction: 0, leftover_map_explained_share: 0, leftover_map_unexplained_share: 0, + leftover_map_cross_share: 0, leftover_map_person_axis_1: 0, leftover_map_person_axis_2: 0, leftover_map_item_axis_1: 0, @@ -143,6 +148,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map reconstruction R̂ 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map explained leftover share R̂²/R² 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover share U²/R² 0.00")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.00")).toBeInTheDocument(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -285,4 +291,34 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map explained leftover share R̂²/R² 0.76")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map reconstruction R̂ +0.25")).toBeInTheDocument(); }); + + it("omits leftover-map cross share on a pair segment when x is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/leftover-map cross share/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map unexplained leftover share U²/R² 0.02")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map explained leftover share R̂²/R² 0.76")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 45919e4c6..0f4de1959 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -11,6 +11,7 @@ import { layoutLeftoverMapPlot, LEFTOVER_MAP_PLOT_CAPTION, LEFTOVER_MAP_PLOT_POST_ACTION, + LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, @@ -52,16 +53,18 @@ function leftoverMapPlotAxisText( * 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``, and leftover-map unexplained leftover - * share ``s`` so the pair-row badges match the graphic. + * explained leftover share ``e``, leftover-map unexplained leftover + * share ``s``, and leftover-map cross share ``x`` so the pair-row + * badges match the graphic. * Omit that distance caption when ``d`` is missing or non-finite. Omit * that reconstruction caption when ``R̂`` is missing or non-finite. Omit * that explained leftover share caption when ``e`` is missing or * non-finite. Omit that unexplained leftover share caption when ``s`` is - * missing or non-finite. Omit that axis badge when share is missing or non-finite - * and keep the existing leftover-map axis text. Omit the plot when no - * pair has four finite leftover-map coordinates. Never invent a leftover - * score. + * missing or non-finite. Omit that leftover-map cross share caption when + * ``x`` is missing or non-finite. Omit that axis badge when share is + * missing or non-finite and keep the existing leftover-map axis text. + * Omit the plot when no pair has four finite leftover-map coordinates. + * Never invent a leftover score. */ export function LeftoverMapPlot({ pairs, @@ -199,6 +202,19 @@ export function LeftoverMapPlot({ {segment.unexplainedShareLabel} ) : null} + {segment.crossShareLabel !== null ? ( + + {segment.crossShareLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index b0fd2c910..ae7fce7c8 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -65,7 +65,8 @@ export type LeftoverPairListProps = { * ``R̂`` on those pair segments. ADR 0273 names persisted leftover-map * explained leftover share ``e`` on those pair segments. ADR 0274 names * persisted leftover-map unexplained leftover share ``s`` on those pair - * segments. Every badge still + * segments. ADR 0275 names persisted leftover-map cross share ``x`` on + * those pair segments. Every badge still * renders together before opening the named post. */ export function LeftoverPairList({ diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index e9b579126..eade0358e 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -70,7 +70,8 @@ describe("i18n", () => { "leftover-map reconstruction {label}", "leftover-map explained leftover share {label}", "leftover-map unexplained leftover share {label}", - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.", + "leftover-map cross share {label}", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.", "Open leftover-map post {title} at ξ {person}", "Read observed Y {observed} and expected E {expected} after IRT main effects, then open this post.", "Leftover map has no leftover structure after IRT main effects. Open this post.", @@ -334,22 +335,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e 与残差图未解释残差份额 s。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s 与残差图交叉份额 x。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "vi", - "Bản đồ phần dư sau hiệu ứng chính IRT. Vạch trục ghi tọa độ bản đồ phần dư đã lưu. Đoạn cặp ghi khoảng cách bản đồ phần dư d, tái dựng bản đồ phần dư R̂, phần dư giải thích e và phần dư chưa giải thích s. Nhấn dấu bài viết để mở bài đó. Hình này không tạo ra điểm phần dư.", + "Bản đồ phần dư sau hiệu ứng chính IRT. Vạch trục ghi tọa độ bản đồ phần dư đã lưu. Đoạn cặp ghi khoảng cách bản đồ phần dư d, tái dựng bản đồ phần dư R̂, phần dư giải thích e, phần dư chưa giải thích s và phần giao x. Nhấn dấu bài viết để mở bài đó. Hình này không tạo ra điểm phần dư.", ], ] as const)("formats leftover-map graphic display caption in %s", (locale, expected) => { setLocale(locale); expect( t( - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -408,6 +409,16 @@ describe("i18n", () => { ); }); + it.each([ + ["ko", "잔여 지도 교차 점유율 2R̂U/R² 0.12"], + ["zh", "残差图交叉份额 2R̂U/R² 0.12"], + ["ja", "残差マップ交差割合 2R̂U/R² 0.12"], + ["vi", "phần giao bản đồ phần dư 2R̂U/R² 0.12"], + ] as const)("formats leftover-map segment cross share in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover-map cross share {label}", { label: "2R̂U/R² 0.12" })).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 5c8f0aa9c..a16a3b7a7 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -595,8 +595,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map reconstruction {label}": "잔여 지도 재구성 {label}", "leftover-map explained leftover share {label}": "잔여 지도 설명 잔여 점유율 {label}", "leftover-map unexplained leftover share {label}": "잔여 지도 미설명 잔여 점유율 {label}", - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "leftover-map cross share {label}": "잔여 지도 교차 점유율 {label}", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "Open leftover-map post {title} at ξ {person}": "잔여 지도 글 {title} 열기 (ξ {person})", "Read observed Y {observed} and expected E {expected} after IRT main effects, then open this post.": @@ -1178,8 +1179,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map reconstruction {label}": "残差图重建 {label}", "leftover-map explained leftover share {label}": "残差图已解释残差份额 {label}", "leftover-map unexplained leftover share {label}": "残差图未解释残差份额 {label}", - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e 与残差图未解释残差份额 s。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "leftover-map cross share {label}": "残差图交叉份额 {label}", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s 与残差图交叉份额 x。点击帖子标记打开该帖子。此图不会虚构残差分数。", "Open leftover-map post {title} at ξ {person}": "打开残差图帖子 {title}(ξ {person})", "Read observed Y {observed} and expected E {expected} after IRT main effects, then open this post.": @@ -1765,8 +1767,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map reconstruction {label}": "残差マップ再構成 {label}", "leftover-map explained leftover share {label}": "残差マップ説明済み残差割合 {label}", "leftover-map unexplained leftover share {label}": "残差マップ未説明残差割合 {label}", - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "leftover-map cross share {label}": "残差マップ交差割合 {label}", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "Open leftover-map post {title} at ξ {person}": "残差マップの投稿 {title} を開く(ξ {person})", "Read observed Y {observed} and expected E {expected} after IRT main effects, then open this post.": @@ -2352,8 +2355,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map reconstruction {label}": "tái dựng bản đồ phần dư {label}", "leftover-map explained leftover share {label}": "phần dư giải thích bản đồ phần dư {label}", "leftover-map unexplained leftover share {label}": "phần dư chưa giải thích bản đồ phần dư {label}", - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.": - "Bản đồ phần dư sau hiệu ứng chính IRT. Vạch trục ghi tọa độ bản đồ phần dư đã lưu. Đoạn cặp ghi khoảng cách bản đồ phần dư d, tái dựng bản đồ phần dư R̂, phần dư giải thích e và phần dư chưa giải thích s. Nhấn dấu bài viết để mở bài đó. Hình này không tạo ra điểm phần dư.", + "leftover-map cross share {label}": "phần giao bản đồ phần dư {label}", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score.": + "Bản đồ phần dư sau hiệu ứng chính IRT. Vạch trục ghi tọa độ bản đồ phần dư đã lưu. Đoạn cặp ghi khoảng cách bản đồ phần dư d, tái dựng bản đồ phần dư R̂, phần dư giải thích e, phần dư chưa giải thích s và phần giao x. Nhấn dấu bài viết để mở bài đó. Hình này không tạo ra điểm phần dư.", "Open leftover-map post {title} at ξ {person}": "Mở bài viết bản đồ phần dư {title} tại ξ {person}", "Read observed Y {observed} and expected E {expected} after IRT main effects, then open this post.": diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index 52d1c2317..8d6e79224 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -26,6 +26,7 @@ function pair( leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, + leftover_map_cross_share: 0.12, ...overrides, }; } @@ -452,6 +453,7 @@ describe("layoutLeftoverMapPlot", () => { leftover_map_reconstruction: -0.95, leftover_map_explained_share: 0.6, leftover_map_unexplained_share: 0.05, + leftover_map_cross_share: -0.24, }), ], criterionLabel, @@ -518,6 +520,7 @@ describe("layoutLeftoverMapPlot", () => { leftover_map_reconstruction: 0, leftover_map_explained_share: 0, leftover_map_unexplained_share: 0, + leftover_map_cross_share: 0, }), ], criterionLabel, @@ -527,6 +530,102 @@ describe("layoutLeftoverMapPlot", () => { expect(layout?.segments[0]?.reconstructionLabel).toBe("R\u0302 0.00"); expect(layout?.segments[0]?.distanceLabel).toBe("d 0.00"); }); + + it("names persisted leftover-map cross share on pair segments without inventing a leftover score", () => { + const layout = layoutLeftoverMapPlot( + [ + pair(), + pair({ + pair_kind: "farthest", + post_id: "post-demo-spec", + criterion_code: "negative_sentiment", + leftover_map_person_axis_1: 0.9, + leftover_map_person_axis_2: 0.8, + leftover_map_item_axis_1: -0.7, + leftover_map_item_axis_2: -0.4, + leftover_distance: 1.84, + leftover_map_reconstruction: -0.95, + leftover_map_explained_share: 0.6, + leftover_map_unexplained_share: 0.05, + leftover_map_cross_share: -0.24, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.crossShareLabel)).toEqual([ + "2R\u0302U/R\u00b2 0.12", + "2R\u0302U/R\u00b2 -0.24", + ]); + expect(layout?.segments[0]?.crossShareX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.crossShareY).toBeGreaterThan( + layout?.segments[0]?.unexplainedShareY ?? 0, + ); + }); + + it("omits a leftover-map cross share caption when x is missing or non-finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ leftover_map_cross_share: null }), + pair({ + pair_kind: "farthest", + leftover_map_cross_share: Number.NaN, + leftover_map_item_axis_1: -0.7, + leftover_map_item_axis_2: -0.4, + criterion_code: "negative_sentiment", + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.crossShareLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.unexplainedShareLabel)).toEqual([ + "U\u00b2/R\u00b2 0.02", + "U\u00b2/R\u00b2 0.02", + ]); + }); + + it("does not invent leftover-map cross share from reconstruction or residual", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ + leftover_map_person_axis_1: 1, + leftover_map_person_axis_2: 0, + leftover_map_item_axis_1: 1, + leftover_map_item_axis_2: 0, + leftover_map_reconstruction: 1, + leftover_map_explained_share: 0.76, + leftover_map_unexplained_share: 0.02, + leftover_map_cross_share: 0.12, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.crossShareLabel).toBe("2R\u0302U/R\u00b2 0.12"); + expect(layout?.segments[0]?.crossShareLabel).not.toBe("2R\u0302U/R\u00b2 1.00"); + }); + + it("names rank-0 origin leftover-map cross share x 0.00 when that persisted value is finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ + leftover_map_person_axis_1: 0, + leftover_map_person_axis_2: 0, + leftover_map_item_axis_1: 0, + leftover_map_item_axis_2: 0, + leftover_distance: 0, + leftover_map_reconstruction: 0, + leftover_map_explained_share: 0, + leftover_map_unexplained_share: 0, + leftover_map_cross_share: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.crossShareLabel).toBe("2R\u0302U/R\u00b2 0.00"); + expect(layout?.segments[0]?.unexplainedShareLabel).toBe("U\u00b2/R\u00b2 0.00"); + expect(layout?.segments[0]?.explainedShareLabel).toBe("R\u0302\u00b2/R\u00b2 0.00"); + expect(layout?.segments[0]?.reconstructionLabel).toBe("R\u0302 0.00"); + expect(layout?.segments[0]?.distanceLabel).toBe("d 0.00"); + }); }); describe("formatLeftoverMapDistance", () => { diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 8ab992677..215d3a7dc 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -3,11 +3,13 @@ * 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 - * leftover-map explained leftover share ``e`` (ADR 0273), and persisted - * leftover-map unexplained leftover share ``s`` (ADR 0274). + * leftover-map explained leftover share ``e`` (ADR 0273), persisted + * leftover-map unexplained leftover share ``s`` (ADR 0274), and persisted + * leftover-map cross share ``x`` (ADR 0275). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; +import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; import { formatLeftoverMapExplainedShare } from "./leftoverMapExplainedShare"; import { formatLeftoverMapReconstruction } from "./leftoverMapReconstruction"; import { formatLeftoverMapUnexplainedShare } from "./leftoverMapUnexplainedShare"; @@ -15,7 +17,7 @@ import { formatSignedLeftoverValue } from "./leftoverMapUnexplained"; import type { LeftoverPair } from "./api"; export const LEFTOVER_MAP_PLOT_CAPTION = - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score."; + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, and leftover-map cross share x. Click a post marker to open that post. The plot does not invent a leftover score."; export const LEFTOVER_MAP_PLOT_POST_ACTION = "Open leftover-map post {title} at ξ {person}"; @@ -35,6 +37,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE = export const LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED_SHARE = "leftover-map unexplained leftover share {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE = + "leftover-map cross share {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -53,6 +58,7 @@ export type LeftoverMapPlottablePair = { leftover_map_reconstruction?: number | null; leftover_map_explained_share?: number | null; leftover_map_unexplained_share?: number | null; + leftover_map_cross_share?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -81,6 +87,7 @@ export type LeftoverMapPlotSegment = { reconstructionLabel: string | null; explainedShareLabel: string | null; unexplainedShareLabel: string | null; + crossShareLabel: string | null; labelX: number; labelY: number; reconstructionX: number; @@ -89,6 +96,8 @@ export type LeftoverMapPlotSegment = { explainedShareY: number; unexplainedShareX: number; unexplainedShareY: number; + crossShareX: number; + crossShareY: number; }; export type LeftoverMapPlotTick = { @@ -315,6 +324,7 @@ export function layoutLeftoverMapPlot( const unexplainedShareLabel = formatLeftoverMapUnexplainedShare( pair.leftover_map_unexplained_share, ); + const crossShareLabel = formatLeftoverMapCrossShare(pair.leftover_map_cross_share); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -335,6 +345,13 @@ export function layoutLeftoverMapPlot( (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0), ); + const crossShareY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0) + + (unexplainedShareLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -347,12 +364,15 @@ export function layoutLeftoverMapPlot( reconstructionLabel, explainedShareLabel, unexplainedShareLabel, + crossShareLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, explainedShareY, unexplainedShareX: labelPosition.labelX, unexplainedShareY, + crossShareX: labelPosition.labelX, + crossShareY, ...labelPosition, }); } diff --git a/pyproject.toml b/pyproject.toml index 98f938768..e9de742c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.31.0" +version = "2.32.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/uv.lock b/uv.lock index 3b22a4111..24dddc5f9 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.31.0" +version = "2.32.0" source = { editable = "." } dependencies = [ { name = "certifi" },