From 089a15713736cbecd4141d460c43d985d3973a92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 11:21:33 +0000 Subject: [PATCH 01/23] feat(reports): leftover-map unexplained leftover share on pair segments (v2.31.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 unexplained leftover share s = U²/R² (ADR 0274). 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, and pair segments that match U²/R² on the pair row; click a post marker or a pair button opens that post. A missing or non-finite s omits that unexplained leftover share caption. A share greater than 1 is shown, never clamped. Do not invent s from U and R or from plotted coordinates. Never invent a leftover score or a theta. No new columns. --- AGENTS.md | 8 +- ARCHITECTURE.md | 5 +- ...-leftover-map-segment-unexplained-share.md | 15 ++ CHANGELOG.md | 15 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 7 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- .../0233-leftover-map-unexplained-share.md | 4 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- ...73-leftover-map-segment-explained-share.md | 6 +- ...-leftover-map-segment-unexplained-share.md | 132 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 33 +++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../components/LeftoverMapPlot.stories.tsx | 1 + .../src/components/LeftoverMapPlot.test.tsx | 37 ++++- frontend/src/components/LeftoverMapPlot.tsx | 22 ++- frontend/src/components/LeftoverPairList.tsx | 4 +- frontend/src/i18n.test.ts | 25 +++- frontend/src/i18n.ts | 20 +-- frontend/src/leftoverMapPlotLayout.test.ts | 93 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 27 +++- pyproject.toml | 2 +- uv.lock | 2 +- 24 files changed, 441 insertions(+), 40 deletions(-) create mode 100644 CHANGELOG.d/2.31.0-leftover-map-segment-unexplained-share.md create mode 100644 docs/adr/0274-leftover-map-segment-unexplained-share.md diff --git a/AGENTS.md b/AGENTS.md index 266618122..ca152fe7c 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) are computed in `lineageweave/leftover_pairs.py` from the +0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274) 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 @@ -309,7 +309,11 @@ coordinates. ADR 0273 captions leftover-map pair segments with persisted leftover-map explained leftover share `e` so the pair-row `R̂²/R²` badge matches the graphic; a missing or non-finite `e` omits that explained leftover share caption and does not invent `e` from `R̂` and -`R` or from plotted coordinates. When `R`, `R̂`, `U`, `x`, +`R` or from plotted coordinates. ADR 0274 captions leftover-map pair +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`, 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 a071f7a8b..4569b175e 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) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274) 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 @@ -650,7 +650,8 @@ badge matches the plot. ADR 0271 captions leftover-map pair segments 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`. Click a post marker or a pair button +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 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.31.0-leftover-map-segment-unexplained-share.md b/CHANGELOG.d/2.31.0-leftover-map-segment-unexplained-share.md new file mode 100644 index 000000000..9560197a4 --- /dev/null +++ b/CHANGELOG.d/2.31.0-leftover-map-segment-unexplained-share.md @@ -0,0 +1,15 @@ +## 2.31.0 — Leftover-map unexplained leftover share on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover-map unexplained leftover share `s = U² / R²` (ADR 0274). 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, and pair segments that match `U²/R²` on the + pair row; click a post marker or a pair button opens that post. A + missing or non-finite `s` omits that unexplained leftover share + caption. A share greater than 1 is shown, never clamped. Do not invent + `s` from `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 271c89353..8c96e1431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption leftover-map graphic-display pair + segments with persisted leftover-map unexplained leftover share + `s = U² / R²` (ADR 0274 / v2.31.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, and pair segments that match `U²/R²` on the pair + row; click a post marker or a pair button opens that post. A + missing or non-finite `s` omits that unexplained leftover share + caption. A share greater than 1 is shown, never clamped. Do not + invent `s` from `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 explained leftover share `e = R̂² / R²` (ADR 0273 / v2.30.0). After `make seed`, closest and diff --git a/CLAUDE.md b/CLAUDE.md index c4dad11ee..1579f3ab5 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), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274), 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 3c0adc8a5..6451f3ef6 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -16,7 +16,8 @@ [ADR 0270](0270-leftover-map-coordinate-ticks.md) (leftover-map coordinate ticks); [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 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) ## Context @@ -79,7 +80,9 @@ pair segments with persisted leftover-map distance `d`; it adds no columns. ADR 0272 captions leftover-map pair segments with persisted 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. +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. 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 39cca48bd..e60d13201 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -17,7 +17,8 @@ [ADR 0270](0270-leftover-map-coordinate-ticks.md) (leftover-map coordinate ticks); [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 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) ## Context @@ -47,7 +48,8 @@ when finite (ADR 0269). Leftover-map axis ticks name 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). The pair renders every available finite measurement. +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. 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 @@ -84,6 +86,8 @@ Leftover-map reconstruction on pair segments is [ADR 0272](0272-leftover-map-segment-reconstruction.md). 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). 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/0233-leftover-map-unexplained-share.md b/docs/adr/0233-leftover-map-unexplained-share.md index ebbfe41b3..7d0a5efe8 100644 --- a/docs/adr/0233-leftover-map-unexplained-share.md +++ b/docs/adr/0233-leftover-map-unexplained-share.md @@ -4,7 +4,9 @@ **Date:** 2026-08-27 **Amended by:** [ADR 0266](0266-leftover-map-explained-share.md) -(explained leftover share e) +(explained leftover share e); +[ADR 0274](0274-leftover-map-segment-unexplained-share.md) +(leftover-map unexplained leftover share on graphic-display pair segments) Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and [ADR 0049](0049-leftover-pair-report-ui.md). Independent of leftover-map diff --git a/docs/adr/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index 9182aab35..e13aad80a 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -12,7 +12,9 @@ [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) +(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) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -89,7 +91,8 @@ leftover-map axes when finite (ADR 0269). Leftover-map axis ticks name 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). When coordinates, reconstruction, and +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 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 3330e3104..5fe4e4810 100644 --- a/docs/adr/0273-leftover-map-segment-explained-share.md +++ b/docs/adr/0273-leftover-map-segment-explained-share.md @@ -3,6 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-29 +**Amended by:** [ADR 0274](0274-leftover-map-segment-unexplained-share.md) +(leftover-map unexplained leftover 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 reconstruction on pair segments @@ -84,7 +87,8 @@ 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̂`, and persisted leftover-map -explained leftover share `e`; click a post marker or a pair button +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 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 new file mode 100644 index 000000000..bd26fd3d1 --- /dev/null +++ b/docs/adr/0274-leftover-map-segment-unexplained-share.md @@ -0,0 +1,132 @@ +# ADR 0274 — Name leftover-map unexplained leftover share on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-29 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md) and +[ADR 0049](0049-leftover-pair-report-ui.md). Independent of 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 0233 already persists leftover-map unexplained leftover share +`s = U² / R²` of raw residual after two-axis Gabriel reconstruction. +ADR 0182 already persists unexplained leftover `U`. ADR 0273 already +names persisted leftover-map explained leftover share `e` on the +leftover-map graphic-display pair segment. The segment still has no +unexplained leftover share label, so a buyer who reads `U²/R² 0.02` +on the pair row cannot match that square share to the graphic pair. +Hiding `s` on the segment lets leftover-map reconstruction `R̂`, +leftover residual `R`, unexplained leftover `U`, or leftover-map +explained leftover share `e` be read as the leftover the truncated map +cannot reconstruct even after `e` is 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 `s` the pair row already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover-map unexplained leftover share. It does not add +columns. It does not recompute `s` from `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 **0274** so it does not +collide with leftover-map explained leftover share on pair segments +(0273), leftover-map reconstruction on pair segments (0272), leftover-map +distance on pair segments (0271), leftover-map coordinate ticks (0270), +leftover-map axis share on the graphic display (0269), leftover-map +graphic display (0268), leftover-map coordinates (0267 / migration 0245), +leftover-map explained leftover share persistence (0266 / migration 0244), +leftover-map unexplained leftover share persistence (0233 / migration +0233), leftover-map reconstruction persistence (0201 / migration 0206), +leftover-map cross share (0185), leftover residual disclosure, leftover +observed `Y` / expected `E`, leftover-map rank, two-axis leftover-map +distance 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 unexplained leftover +share formatter as the pair-row `U²/R²` badge. A missing or +non-finite `s` omits that unexplained leftover share caption and keeps +the connecting line and any leftover-map distance, reconstruction, or +explained leftover share caption. Rank-0 origin cells still name +`U²/R² 0.00` when that persisted share is finite. A finite share +greater than 1 is shown, never clamped. Do not invent `s` from `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 +unexplained leftover 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`, and persisted leftover-map unexplained +leftover share `s`; 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`, +`x`, `s`, and `e` are all finite, `e + s + x = 1` remains the same +identity already persisted by 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, 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 cross 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}`. Unexplained leftover share is the square share +`s = U² / R²` of the leftover the truncated map cannot reconstruct.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ae32121c1..5dd2e2204 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,38 @@ # Product & Technical Gap Baseline +> 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 +> 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), replaying v2.29 reconstruction captions, is stacked on +> leftover-coordinates at `79ec22f0a4b7236ab06500c7bf8f392da6a9419b`. +> Leave #802 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 stays closed as a weaker duplicate of #91 +> (`state_reason` still 403). 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 unexplained leftover +> share `s` on graphic-display pair segments (ADR 0274 / v2.31.0) +> delivered locally on +> `feat/leftover-map-segment-unexplained-share-v2310`. Caption each +> closest/farthest segment with persisted leftover-map unexplained +> leftover share so the pair-row `U²/R²` badge matches the graphic. +> UI-only; no new columns. Missing/non-finite `s` omits that unexplained +> leftover share caption. A share greater than 1 is shown, never +> clamped. Do not invent `s` from `U` and `R` or from plotted +> coordinates. Do not invent leftover scores. Stack onto leftover branch +> `feat/leftover-map-segment-explained-share-v2300`; leave the PR open +> for independent review. Do not squash-merge onto the leftover base. +> The following buyer increment is leftover-map cross share `x` on pair +> segments so `e + s + x = 1` is graphic-auditable. + > Exact-head loop overlay: 2026-08-29 20:00 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 e26bd5c3b..13d926fd6 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, and pair-segment `R̂²/R²` to leftover-map explained 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`, and `R̂²/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, and pair segments name persisted leftover-map explained 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, 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` | | `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 8a01b19f8..c0fbd16f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.30.0", + "version": "2.31.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.stories.tsx b/frontend/src/components/LeftoverMapPlot.stories.tsx index aad15812e..91ccf8bc7 100644 --- a/frontend/src/components/LeftoverMapPlot.stories.tsx +++ b/frontend/src/components/LeftoverMapPlot.stories.tsx @@ -82,6 +82,7 @@ export const RankZeroOrigin: Story = { leftover_map_rank: 0, leftover_map_reconstruction: 0, leftover_map_explained_share: 0, + leftover_map_unexplained_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 8b2067652..156942cb3 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -14,6 +14,7 @@ const PAIRS: LeftoverPair[] = [ leftover_residual: 0.4, leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, + leftover_map_unexplained_share: 0.02, leftover_map_person_axis_1: 0.5, leftover_map_person_axis_2: 0.1, leftover_map_item_axis_1: 0.5, @@ -28,6 +29,7 @@ const PAIRS: LeftoverPair[] = [ leftover_residual: -1.1, leftover_map_reconstruction: -0.95, leftover_map_explained_share: 0.6, + leftover_map_unexplained_share: 0.05, leftover_map_person_axis_1: 0.9, leftover_map_person_axis_2: 0.8, leftover_map_item_axis_1: -0.7, @@ -49,7 +51,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̂, and leftover-map explained leftover share e. 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, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -65,6 +67,8 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map reconstruction R̂ −0.95")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map explained leftover share R̂²/R² 0.76")).toBeInTheDocument(); 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(); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -113,6 +117,7 @@ describe("LeftoverMapPlot", () => { leftover_map_rank: 0, leftover_map_reconstruction: 0, leftover_map_explained_share: 0, + leftover_map_unexplained_share: 0, leftover_map_person_axis_1: 0, leftover_map_person_axis_2: 0, leftover_map_item_axis_1: 0, @@ -137,6 +142,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map distance d 0.00")).toBeInTheDocument(); 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(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -250,4 +256,33 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map reconstruction R̂ +0.25")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map distance d 0.12")).toBeInTheDocument(); }); + + it("omits leftover-map unexplained leftover share on a pair segment when s is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/leftover-map unexplained leftover share/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map explained leftover share R̂²/R² 0.76")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map reconstruction R̂ +0.25")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 29ffed490..45919e4c6 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -14,6 +14,7 @@ import { LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, + LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED_SHARE, LEFTOVER_MAP_PLOT_TICK, } from "../leftoverMapPlotLayout"; import "./LeftoverMapPlot.css"; @@ -50,12 +51,14 @@ function leftoverMapPlotAxisText( * Gabriel inertia share when finite, including rank-0 zero-share axes. * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the * pair row match the plot. Pair segments name persisted leftover-map - * distance ``d``, leftover-map reconstruction ``R̂``, and leftover-map - * explained leftover share ``e`` so the pair-row badges match the graphic. + * 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. * 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 axis badge when share is missing or non-finite + * 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. @@ -183,6 +186,19 @@ export function LeftoverMapPlot({ {segment.explainedShareLabel} ) : null} + {segment.unexplainedShareLabel !== null ? ( + + {segment.unexplainedShareLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index 75e500821..b0fd2c910 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -63,7 +63,9 @@ export type LeftoverPairListProps = { * the plot. ADR 0271 names persisted leftover-map distance ``d`` on those * pair segments. ADR 0272 names persisted leftover-map reconstruction * ``R̂`` on those pair segments. ADR 0273 names persisted leftover-map - * explained leftover share ``e`` on those pair segments. Every badge still + * 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 * 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 6feba1e48..e9b579126 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -69,7 +69,8 @@ describe("i18n", () => { "leftover-map distance {label}", "leftover-map reconstruction {label}", "leftover-map explained 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̂, and leftover-map explained leftover share e. Click a post marker to open that post. The plot does not invent a leftover score.", + "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.", "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.", @@ -333,22 +334,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂ 与残差图已解释残差份额 e。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e 与残差图未解释残差份额 s。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "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̂ và phần dư giải thích e. 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 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ư.", ], ] 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̂, and leftover-map explained leftover share e. 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, and leftover-map unexplained leftover share s. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -395,6 +396,18 @@ describe("i18n", () => { ); }); + it.each([ + ["ko", "잔여 지도 미설명 잔여 점유율 U²/R² 0.02"], + ["zh", "残差图未解释残差份额 U²/R² 0.02"], + ["ja", "残差マップ未説明残差割合 U²/R² 0.02"], + ["vi", "phần dư chưa giải thích bản đồ phần dư U²/R² 0.02"], + ] as const)("formats leftover-map segment unexplained leftover share in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover-map unexplained leftover share {label}", { label: "U²/R² 0.02" })).toBe( + expected, + ); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 52f64363c..5c8f0aa9c 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -594,8 +594,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map distance {label}": "잔여 지도 거리 {label}", "leftover-map reconstruction {label}": "잔여 지도 재구성 {label}", "leftover-map explained 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̂, and leftover-map explained leftover share e. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "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입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "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.": @@ -1176,8 +1177,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map distance {label}": "残差图距离 {label}", "leftover-map reconstruction {label}": "残差图重建 {label}", "leftover-map explained 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̂, and leftover-map explained leftover share e. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂ 与残差图已解释残差份额 e。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "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。点击帖子标记打开该帖子。此图不会虚构残差分数。", "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.": @@ -1762,8 +1764,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map distance {label}": "残差マップ距離 {label}", "leftover-map reconstruction {label}": "残差マップ再構成 {label}", "leftover-map explained 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̂, and leftover-map explained leftover share e. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "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 です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "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.": @@ -2348,8 +2351,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map distance {label}": "khoảng cách bản đồ phần dư {label}", "leftover-map reconstruction {label}": "tái dựng bản đồ phần dư {label}", "leftover-map explained leftover share {label}": "phần dư 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̂, and leftover-map explained leftover share e. 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̂ và phần dư giải thích e. 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 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ư.", "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 ac6fe5ed8..52d1c2317 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -25,6 +25,7 @@ function pair( leftover_distance: 0.12, leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, + leftover_map_unexplained_share: 0.02, ...overrides, }; } @@ -434,6 +435,98 @@ 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 unexplained leftover 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, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.unexplainedShareLabel)).toEqual([ + "U\u00b2/R\u00b2 0.02", + "U\u00b2/R\u00b2 0.05", + ]); + expect(layout?.segments[0]?.unexplainedShareX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.unexplainedShareY).toBeGreaterThan( + layout?.segments[0]?.explainedShareY ?? 0, + ); + }); + + it("omits a leftover-map unexplained leftover share caption when s is missing or non-finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ leftover_map_unexplained_share: null }), + pair({ + pair_kind: "farthest", + leftover_map_unexplained_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.unexplainedShareLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.explainedShareLabel)).toEqual([ + "R\u0302\u00b2/R\u00b2 0.76", + "R\u0302\u00b2/R\u00b2 0.76", + ]); + }); + + it("does not invent leftover-map unexplained leftover share from unexplained leftover 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, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.unexplainedShareLabel).toBe("U\u00b2/R\u00b2 0.02"); + expect(layout?.segments[0]?.unexplainedShareLabel).not.toBe("U\u00b2/R\u00b2 1.00"); + }); + + it("names rank-0 origin unexplained leftover share s 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, + }), + ], + criterionLabel, + ); + 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 7cc26c807..8ab992677 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -2,18 +2,20 @@ * Leftover-map axis share captions axes 1 and 2 when finite (ADR 0269). * 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), and persisted - * leftover-map explained leftover share ``e`` (ADR 0273). + * 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). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; import { formatLeftoverMapExplainedShare } from "./leftoverMapExplainedShare"; import { formatLeftoverMapReconstruction } from "./leftoverMapReconstruction"; +import { formatLeftoverMapUnexplainedShare } from "./leftoverMapUnexplainedShare"; 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̂, and leftover-map explained leftover share e. 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, and leftover-map unexplained leftover share s. 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}"; @@ -30,6 +32,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION = export const LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE = "leftover-map explained leftover share {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED_SHARE = + "leftover-map unexplained leftover share {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -47,6 +52,7 @@ export type LeftoverMapPlottablePair = { leftover_distance?: number | null; leftover_map_reconstruction?: number | null; leftover_map_explained_share?: number | null; + leftover_map_unexplained_share?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -74,12 +80,15 @@ export type LeftoverMapPlotSegment = { distanceLabel: string | null; reconstructionLabel: string | null; explainedShareLabel: string | null; + unexplainedShareLabel: string | null; labelX: number; labelY: number; reconstructionX: number; reconstructionY: number; explainedShareX: number; explainedShareY: number; + unexplainedShareX: number; + unexplainedShareY: number; }; export type LeftoverMapPlotTick = { @@ -303,6 +312,9 @@ export function layoutLeftoverMapPlot( const explainedShareLabel = formatLeftoverMapExplainedShare( pair.leftover_map_explained_share, ); + const unexplainedShareLabel = formatLeftoverMapUnexplainedShare( + pair.leftover_map_unexplained_share, + ); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -317,6 +329,12 @@ export function layoutLeftoverMapPlot( labelPosition.labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0), ); + const unexplainedShareY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -328,10 +346,13 @@ export function layoutLeftoverMapPlot( distanceLabel, reconstructionLabel, explainedShareLabel, + unexplainedShareLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, explainedShareY, + unexplainedShareX: labelPosition.labelX, + unexplainedShareY, ...labelPosition, }); } diff --git a/pyproject.toml b/pyproject.toml index 839a61c8d..98f938768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.30.0" +version = "2.31.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 b21381b50..3b22a4111 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.30.0" +version = "2.31.0" source = { editable = "." } dependencies = [ { name = "certifi" }, From c513002bea9e0ee3e6ccd0931c5256e52dbbd878 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 14:18:45 +0000 Subject: [PATCH 02/23] 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" }, From e3fdfd741a5b4a063843120d01243febb3d304f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 17:23:36 +0000 Subject: [PATCH 03/23] feat(reports): leftover-map unexplained leftover on pair segments (v2.33.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 unexplained leftover U = R − R̂ (ADR 0276). 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, pair segments that match 2R̂U/R² on the pair row, and pair segments that match U on the pair row; click a post marker or a pair button opens that post. A missing or non-finite U omits that unexplained leftover caption. A finite negative leftover is shown; do not clamp to nonnegative. Do not invent U from R and R̂ or from plotted coordinates. Never invent a leftover score or a theta. No new columns. --- AGENTS.md | 8 +- ARCHITECTURE.md | 5 +- ...ftover-map-segment-unexplained-leftover.md | 17 ++ CHANGELOG.md | 17 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 7 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- docs/adr/0182-leftover-map-unexplained.md | 8 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- .../0275-leftover-map-segment-cross-share.md | 2 + ...ftover-map-segment-unexplained-leftover.md | 147 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 33 ++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../components/LeftoverMapPlot.stories.tsx | 1 + .../src/components/LeftoverMapPlot.test.tsx | 39 ++++- frontend/src/components/LeftoverMapPlot.tsx | 21 ++- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 23 ++- frontend/src/i18n.ts | 20 ++- frontend/src/leftoverMapPlotLayout.test.ts | 103 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 33 +++- pyproject.toml | 2 +- uv.lock | 2 +- 24 files changed, 473 insertions(+), 41 deletions(-) create mode 100644 CHANGELOG.d/2.33.0-leftover-map-segment-unexplained-leftover.md create mode 100644 docs/adr/0276-leftover-map-segment-unexplained-leftover.md diff --git a/AGENTS.md b/AGENTS.md index 019f0e89c..306c433b3 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) 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) 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 @@ -317,7 +317,11 @@ the pair-row `U²/R²` badge matches the graphic; a missing or non-finite 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`, +`x` from `R̂`, `U`, and `R` or from plotted coordinates. ADR 0276 captions leftover-map pair +segments with persisted leftover-map unexplained leftover `U` so +the pair-row `U` badge matches the graphic; a missing or non-finite +`U` omits that unexplained leftover caption and does not invent +`U` from `R` 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 e1e86008d..cfd68b96d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276) 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 @@ -653,7 +653,8 @@ 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`. ADR 0275 captions leftover-map pair segments with persisted leftover-map -cross share `x`. Click a post marker or a pair button +cross share `x`. ADR 0276 captions leftover-map pair segments with persisted leftover-map +unexplained leftover `U`. 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.33.0-leftover-map-segment-unexplained-leftover.md b/CHANGELOG.d/2.33.0-leftover-map-segment-unexplained-leftover.md new file mode 100644 index 000000000..8d0bc4a91 --- /dev/null +++ b/CHANGELOG.d/2.33.0-leftover-map-segment-unexplained-leftover.md @@ -0,0 +1,17 @@ +## 2.33.0 — Leftover-map unexplained leftover on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover-map unexplained leftover `U = R − R̂` (ADR 0276). 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, pair segments that match `2R̂U/R²` on the pair row, and + pair segments that match `U` on the pair row; click a post marker or + a pair button opens that post. A missing or non-finite `U` omits that + unexplained leftover caption. A finite negative leftover is shown; + do not clamp to nonnegative. Do not invent `U` from `R` 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 944e3a14c..1afc6d53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ 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 unexplained leftover + `U = R − R̂` (ADR 0276 / v2.33.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, + pair segments that match `2R̂U/R²` on the pair row, and pair + segments that match `U` on the pair row; click a post marker or a + pair button opens that post. A missing or non-finite `U` omits that + unexplained leftover caption. A finite negative leftover is shown; + do not clamp to nonnegative. Do not invent `U` from `R` 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 cross share `x = 2 R̂ U / R²` (ADR 0275 / v2.32.0). After `make seed`, closest and diff --git a/CLAUDE.md b/CLAUDE.md index 107079ed7..56cc3bcda 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), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276), 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 f94e7d097..74e871ea3 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -18,7 +18,8 @@ [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 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments) +[ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments) ## Context @@ -84,7 +85,9 @@ 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. ADR 0275 captions leftover-map pair segments with -persisted leftover-map cross share `x`; it adds no columns. +persisted leftover-map cross share `x`; it adds no columns. ADR 0276 +captions leftover-map pair segments with persisted leftover-map +unexplained leftover `U`; 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 b3ae6fea1..fd856219c 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -19,7 +19,8 @@ [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 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments) +[ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments) ## Context @@ -51,7 +52,8 @@ 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). Pair segments name persisted leftover-map -cross share `x` (ADR 0275). The pair renders every available finite measurement. +cross share `x` (ADR 0275). Pair segments name persisted leftover-map +unexplained leftover `U` (ADR 0276). 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 @@ -92,6 +94,8 @@ 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). +Leftover-map unexplained leftover on pair segments is +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.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/0182-leftover-map-unexplained.md b/docs/adr/0182-leftover-map-unexplained.md index 13ab5a573..1e96d8f81 100644 --- a/docs/adr/0182-leftover-map-unexplained.md +++ b/docs/adr/0182-leftover-map-unexplained.md @@ -4,7 +4,9 @@ **Date:** 2026-08-24 **Amended by:** [ADR 0201](0201-leftover-map-reconstruction.md) -(two-axis reconstruction R̂) +(two-axis reconstruction R̂); +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) +(leftover-map unexplained leftover on graphic-display pair segments) Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and [ADR 0049](0049-leftover-pair-report-ui.md). @@ -73,7 +75,9 @@ score. Do not invent a theta. `GET /api/reports/{grouping}/{period}` returns `leftover_map_unexplained`. After `make seed`, closest and farthest leftover pairs sit above the member list with named `U` next to `d`; -click opens that post. Hidden posts stay hidden. +click opens that post. Hidden posts stay hidden. ADR 0276 +captions leftover-map graphic-display pair segments with the same +persisted leftover-map unexplained leftover; 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 c11c16c7d..674f1de06 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -16,7 +16,9 @@ [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) +(leftover-map cross share on pair segments); +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) +(leftover-map unexplained leftover on pair segments) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -95,7 +97,8 @@ 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). Pair segments name -persisted leftover-map cross share `x` (ADR 0275). When coordinates, reconstruction, and +persisted leftover-map cross share `x` (ADR 0275). Pair segments name +persisted leftover-map unexplained leftover `U` (ADR 0276). 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/0275-leftover-map-segment-cross-share.md b/docs/adr/0275-leftover-map-segment-cross-share.md index c5e062c43..c80b19fb6 100644 --- a/docs/adr/0275-leftover-map-segment-cross-share.md +++ b/docs/adr/0275-leftover-map-segment-cross-share.md @@ -2,6 +2,8 @@ **Decision status:** Accepted **Date:** 2026-08-29 +**Amended by:** [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) +(leftover-map unexplained leftover on pair segments) Amends [ADR 0268](0268-leftover-map-graphic-display.md), [ADR 0049](0049-leftover-pair-report-ui.md), and leftover-map cross share diff --git a/docs/adr/0276-leftover-map-segment-unexplained-leftover.md b/docs/adr/0276-leftover-map-segment-unexplained-leftover.md new file mode 100644 index 000000000..79ee88b01 --- /dev/null +++ b/docs/adr/0276-leftover-map-segment-unexplained-leftover.md @@ -0,0 +1,147 @@ +# ADR 0276 — Name leftover-map unexplained leftover on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover-map unexplained +leftover persistence ([ADR 0182](0182-leftover-map-unexplained.md)). Independent of +leftover-map cross share on pair segments +([ADR 0275](0275-leftover-map-segment-cross-share.md)), leftover-map +unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover-map +explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), and leftover-map axis share +persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0182 already persists unexplained leftover `U = R − R̂` after +two-axis Gabriel reconstruction. ADR 0275 already names persisted +leftover-map cross share `x` on the leftover-map graphic-display pair +segment. The segment still has no unexplained leftover label, so a +buyer who reads `U +0.05` on the pair row cannot match that signed +leftover to the graphic pair. Hiding `U` on the segment lets leftover +residual `R`, leftover-map reconstruction `R̂`, leftover-map explained +leftover share `e`, leftover-map unexplained leftover share `s`, or +leftover-map cross share `x` be read as the leftover the truncated map +cannot reconstruct even after `e`, `s`, and `x` are named on the +segment. When `R`, `R̂`, and `U` are finite, `U + R̂ = R`; the graphic +must name the same persisted `U` the pair row already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover-map unexplained leftover. It does not add columns. +It does not recompute `U` from `R` 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 **0276** so it does not +collide with leftover-map cross share on pair segments (0275), leftover-map +unexplained leftover share on pair segments (0274), leftover-map explained +leftover share on pair segments (0273), leftover-map reconstruction on pair +segments (0272), leftover-map distance on pair segments (0271), leftover-map +coordinate ticks (0270), leftover-map axis share on the graphic display +(0269), leftover-map graphic display (0268), leftover-map coordinates +(0267 / migration 0245), leftover-map explained leftover share persistence +(0266 / migration 0244), leftover-map unexplained leftover share +persistence (0233 / migration 0233), leftover-map reconstruction +persistence (0201 / migration 0206), leftover-map cross share persistence +(0185), leftover-map unexplained leftover persistence (0182), 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 unexplained leftover +formatter as the pair-row `U` badge. A missing or non-finite `U` omits +that unexplained leftover caption and keeps the connecting line and any +leftover-map distance, reconstruction, explained leftover share, +unexplained leftover share, or leftover-map cross share caption. Rank-0 +origin cells still name `U 0.00` when that persisted leftover is finite. +A finite negative leftover is shown; do not clamp to nonnegative. Do not +invent `U` from `R` 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 unexplained leftover 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`, persisted leftover-map cross share `x`, and persisted leftover-map +unexplained leftover `U`; 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, still name +`2R̂U/R² 0.00` when that leftover-map cross share is persisted, and +still name `U 0.00` when that unexplained leftover is persisted. When +`R`, `R̂`, and `U` are all finite, `U + R̂ = R` remains the same identity +already persisted by ADR 0182 and ADR 0201. 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 +persistence, 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, leftover-map unexplained leftover share on pair +segments, and leftover-map cross 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}`. Unexplained leftover is the signed remainder +`U = R − R̂` after that two-axis reconstruction.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c6b291c2e..10ffd80ea 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,38 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-30 02:09 KST. Protected `main` is +> `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map +> explained leftover share). GitHub writes work (comment/close/create-PR). +> Token still has empty `X-OAuth-Scopes`; repo permission is ADMIN. +> Open leftover stack still lacks independent APPROVE. Do not self-approve. +> #782 leftover-map coordinates through pair-segment distance +> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained +> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29 +> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair +> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross +> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. Leave +> #782/#802/#803/#804 open for independent review. Do not squash-merge +> stacked leftover PRs onto an unprotected leftover base. Strix on #782 +> failed closed at `Run Strix (quick)`; do not weaken fail-closed on +> `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError` classifier is +> already on ContextualWisdomLab/.github. Copilot review is not +> independent APPROVE. Issues #79 and #87 stay open. +> +> Next buyer increment on this cycle: leftover-map unexplained leftover +> `U` on graphic-display pair segments (ADR 0276 / v2.33.0) delivered +> locally on `feat/leftover-map-segment-unexplained-leftover-v2330`. +> Caption each closest/farthest segment with persisted leftover-map +> unexplained leftover so the pair-row `U` badge matches the graphic +> and `U + R̂ = R` is graphic-auditable. UI-only; no new columns. +> Missing/non-finite `U` omits that unexplained leftover caption. A +> finite negative leftover is shown; do not clamp to nonnegative. Do not +> invent `U` from `R` and `R̂` or from plotted coordinates. Do not invent +> leftover scores. Stack onto leftover branch +> `feat/leftover-map-segment-cross-share-v2320` / #804; 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 23:10 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 d63417657..35671b57d 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, 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` | +| `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, and pair-segment `U` to leftover-map unexplained leftover, 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`, `2R̂U/R² 0.00`, and `U 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, pair segments name persisted leftover-map cross share, and pair segments name persisted leftover-map unexplained leftover. | `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 fbd3aeb86..465f0dda4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.32.0", + "version": "2.33.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.stories.tsx b/frontend/src/components/LeftoverMapPlot.stories.tsx index 915ff4223..d79587245 100644 --- a/frontend/src/components/LeftoverMapPlot.stories.tsx +++ b/frontend/src/components/LeftoverMapPlot.stories.tsx @@ -84,6 +84,7 @@ export const RankZeroOrigin: Story = { leftover_map_explained_share: 0, leftover_map_unexplained_share: 0, leftover_map_cross_share: 0, + leftover_map_unexplained: 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 82efd959f..931ee1ed1 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -16,6 +16,7 @@ const PAIRS: LeftoverPair[] = [ leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, leftover_map_cross_share: 0.12, + leftover_map_unexplained: 0.05, leftover_map_person_axis_1: 0.5, leftover_map_person_axis_2: 0.1, leftover_map_item_axis_1: 0.5, @@ -32,6 +33,7 @@ const PAIRS: LeftoverPair[] = [ leftover_map_explained_share: 0.6, leftover_map_unexplained_share: 0.05, leftover_map_cross_share: -0.24, + leftover_map_unexplained: -0.25, leftover_map_person_axis_1: 0.9, leftover_map_person_axis_2: 0.8, leftover_map_item_axis_1: -0.7, @@ -53,7 +55,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, 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.", + "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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -73,6 +75,8 @@ describe("LeftoverMapPlot", () => { 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(); + expect(screen.getByLabelText("leftover-map unexplained leftover U +0.05")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map unexplained leftover U −0.25")).toBeInTheDocument(); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -123,6 +127,7 @@ describe("LeftoverMapPlot", () => { leftover_map_explained_share: 0, leftover_map_unexplained_share: 0, leftover_map_cross_share: 0, + leftover_map_unexplained: 0, leftover_map_person_axis_1: 0, leftover_map_person_axis_2: 0, leftover_map_item_axis_1: 0, @@ -149,6 +154,7 @@ describe("LeftoverMapPlot", () => { 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(); + expect(screen.getByLabelText("leftover-map unexplained leftover U 0.00")).toBeInTheDocument(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -321,4 +327,35 @@ describe("LeftoverMapPlot", () => { 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(); }); + + it("omits leftover-map unexplained leftover on a pair segment when U is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/^leftover-map unexplained leftover U/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.12")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map unexplained leftover share U²/R² 0.02")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 0f4de1959..8593e56ec 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -15,6 +15,7 @@ import { LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, + LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED, LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED_SHARE, LEFTOVER_MAP_PLOT_TICK, } from "../leftoverMapPlotLayout"; @@ -54,14 +55,15 @@ function leftoverMapPlotAxisText( * pair row match the plot. Pair segments name persisted leftover-map * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map * explained leftover share ``e``, leftover-map unexplained leftover - * share ``s``, and leftover-map cross share ``x`` so the pair-row - * badges match the graphic. + * share ``s``, leftover-map cross share ``x``, and leftover-map + * unexplained leftover ``U`` 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 leftover-map cross share caption when - * ``x`` is missing or non-finite. Omit that axis badge when share is + * ``x`` is missing or non-finite. Omit that unexplained leftover caption + * when ``U`` 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. @@ -215,6 +217,19 @@ export function LeftoverMapPlot({ {segment.crossShareLabel} ) : null} + {segment.unexplainedLeftoverLabel !== null ? ( + + {segment.unexplainedLeftoverLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index ae7fce7c8..327c00e9a 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -66,7 +66,8 @@ export type LeftoverPairListProps = { * explained leftover share ``e`` on those pair segments. ADR 0274 names * persisted leftover-map unexplained leftover share ``s`` on those pair * segments. ADR 0275 names persisted leftover-map cross share ``x`` on - * those pair segments. Every badge still + * those pair segments. ADR 0276 names persisted leftover-map unexplained + * leftover ``U`` 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 eade0358e..c64df0090 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -71,7 +71,8 @@ describe("i18n", () => { "leftover-map explained leftover share {label}", "leftover-map unexplained leftover share {label}", "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.", + "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. 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.", @@ -335,22 +336,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s 与残差图交叉份额 x。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x 与残差图未解释残差 U。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "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, 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ư.", + "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, phần giao x và phần dư chưa giải thích U. 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, 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.", + "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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -419,6 +420,16 @@ describe("i18n", () => { expect(tf("leftover-map cross share {label}", { label: "2R̂U/R² 0.12" })).toBe(expected); }); + it.each([ + ["ko", "잔여 지도 미설명 잔여 U +0.05"], + ["zh", "残差图未解释残差 U +0.05"], + ["ja", "残差マップ未説明残差 U +0.05"], + ["vi", "phần dư chưa giải thích U +0.05"], + ] as const)("formats leftover-map segment unexplained leftover in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover-map unexplained leftover {label}", { label: "U +0.05" })).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index a16a3b7a7..50d6aad4f 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -596,8 +596,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map explained leftover share {label}": "잔여 지도 설명 잔여 점유율 {label}", "leftover-map unexplained leftover share {label}": "잔여 지도 미설명 잔여 점유율 {label}", "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입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "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.": @@ -1180,8 +1181,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map explained leftover share {label}": "残差图已解释残差份额 {label}", "leftover-map unexplained leftover share {label}": "残差图未解释残差份额 {label}", "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。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x 与残差图未解释残差 U。点击帖子标记打开该帖子。此图不会虚构残差分数。", "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.": @@ -1768,8 +1770,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map explained leftover share {label}": "残差マップ説明済み残差割合 {label}", "leftover-map unexplained leftover share {label}": "残差マップ未説明残差割合 {label}", "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 です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "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.": @@ -2356,8 +2359,9 @@ const TRANSLATIONS: Partial>> = { "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 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ư.", + "leftover-map unexplained leftover {label}": "phần dư chưa giải thích {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, leftover-map cross share x, and leftover-map unexplained leftover U. 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, phần giao x và phần dư chưa giải thích U. 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 8d6e79224..eda3b0d88 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -27,6 +27,7 @@ function pair( leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, leftover_map_cross_share: 0.12, + leftover_map_unexplained: 0.05, ...overrides, }; } @@ -616,11 +617,113 @@ describe("layoutLeftoverMapPlot", () => { leftover_map_explained_share: 0, leftover_map_unexplained_share: 0, leftover_map_cross_share: 0, + leftover_map_unexplained: 0, }), ], criterionLabel, ); expect(layout?.segments[0]?.crossShareLabel).toBe("2R\u0302U/R\u00b2 0.00"); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U 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"); + }); + + it("names persisted leftover-map unexplained leftover 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, + leftover_map_unexplained: -0.25, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.unexplainedLeftoverLabel)).toEqual([ + "U +0.05", + "U \u22120.25", + ]); + expect(layout?.segments[0]?.unexplainedLeftoverX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.unexplainedLeftoverY).toBeGreaterThan( + layout?.segments[0]?.crossShareY ?? 0, + ); + }); + + it("omits a leftover-map unexplained leftover caption when U is missing or non-finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ leftover_map_unexplained: null }), + pair({ + pair_kind: "farthest", + leftover_map_unexplained: 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.unexplainedLeftoverLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.crossShareLabel)).toEqual([ + "2R\u0302U/R\u00b2 0.12", + "2R\u0302U/R\u00b2 0.12", + ]); + }); + + it("does not invent leftover-map unexplained leftover 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, + leftover_map_unexplained: 0.05, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U +0.05"); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).not.toBe("U +0.15"); + }); + + it("names rank-0 origin unexplained leftover U 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, + leftover_map_unexplained: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U 0.00"); + 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"); diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 215d3a7dc..94162258d 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -4,20 +4,24 @@ * 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), persisted - * leftover-map unexplained leftover share ``s`` (ADR 0274), and persisted - * leftover-map cross share ``x`` (ADR 0275). + * leftover-map unexplained leftover share ``s`` (ADR 0274), persisted + * leftover-map cross share ``x`` (ADR 0275), and persisted leftover-map + * unexplained leftover ``U`` (ADR 0276). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; import { formatLeftoverMapExplainedShare } from "./leftoverMapExplainedShare"; import { formatLeftoverMapReconstruction } from "./leftoverMapReconstruction"; +import { + formatLeftoverMapUnexplained, + formatSignedLeftoverValue, +} from "./leftoverMapUnexplained"; import { formatLeftoverMapUnexplainedShare } from "./leftoverMapUnexplainedShare"; -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, 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."; + "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, leftover-map cross share x, and leftover-map unexplained leftover U. 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}"; @@ -40,6 +44,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED_SHARE = export const LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE = "leftover-map cross share {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED = + "leftover-map unexplained leftover {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -59,6 +66,7 @@ export type LeftoverMapPlottablePair = { leftover_map_explained_share?: number | null; leftover_map_unexplained_share?: number | null; leftover_map_cross_share?: number | null; + leftover_map_unexplained?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -88,6 +96,7 @@ export type LeftoverMapPlotSegment = { explainedShareLabel: string | null; unexplainedShareLabel: string | null; crossShareLabel: string | null; + unexplainedLeftoverLabel: string | null; labelX: number; labelY: number; reconstructionX: number; @@ -98,6 +107,8 @@ export type LeftoverMapPlotSegment = { unexplainedShareY: number; crossShareX: number; crossShareY: number; + unexplainedLeftoverX: number; + unexplainedLeftoverY: number; }; export type LeftoverMapPlotTick = { @@ -325,6 +336,9 @@ export function layoutLeftoverMapPlot( pair.leftover_map_unexplained_share, ); const crossShareLabel = formatLeftoverMapCrossShare(pair.leftover_map_cross_share); + const unexplainedLeftoverLabel = formatLeftoverMapUnexplained( + pair.leftover_map_unexplained, + ); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -352,6 +366,14 @@ export function layoutLeftoverMapPlot( (explainedShareLabel !== null ? 1 : 0) + (unexplainedShareLabel !== null ? 1 : 0), ); + const unexplainedLeftoverY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0) + + (unexplainedShareLabel !== null ? 1 : 0) + + (crossShareLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -365,6 +387,7 @@ export function layoutLeftoverMapPlot( explainedShareLabel, unexplainedShareLabel, crossShareLabel, + unexplainedLeftoverLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, @@ -373,6 +396,8 @@ export function layoutLeftoverMapPlot( unexplainedShareY, crossShareX: labelPosition.labelX, crossShareY, + unexplainedLeftoverX: labelPosition.labelX, + unexplainedLeftoverY, ...labelPosition, }); } diff --git a/pyproject.toml b/pyproject.toml index e9de742c7..dfc9a04e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.32.0" +version = "2.33.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 24dddc5f9..ee8b86fda 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.32.0" +version = "2.33.0" source = { editable = "." } dependencies = [ { name = "certifi" }, From 33f6c3dda7f80b1bcf7b244d5e6d65068d582ba7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 21:38:55 +0000 Subject: [PATCH 04/23] feat(reports): leftover residual on pair segments (v2.34.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 residual R so U + R̂ = R is graphic-auditable. UI-only; no new columns. Do not invent R from Y and E, from U and R̂, or from plotted coordinates. --- AGENTS.md | 11 +- ARCHITECTURE.md | 5 +- .../2.34.0-leftover-map-segment-residual.md | 18 +++ CHANGELOG.md | 17 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 6 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- .../adr/0277-leftover-map-segment-residual.md | 152 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 35 ++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../src/components/LeftoverMapPlot.test.tsx | 36 ++++- frontend/src/components/LeftoverMapPlot.tsx | 22 ++- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 23 ++- frontend/src/i18n.ts | 20 ++- frontend/src/leftoverMapPlotLayout.test.ts | 105 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 28 +++- frontend/src/leftoverResidual.test.ts | 17 +- frontend/src/leftoverResidual.ts | 7 + pyproject.toml | 2 +- 22 files changed, 491 insertions(+), 39 deletions(-) create mode 100644 CHANGELOG.d/2.34.0-leftover-map-segment-residual.md create mode 100644 docs/adr/0277-leftover-map-segment-residual.md diff --git a/AGENTS.md b/AGENTS.md index 306c433b3..08fe2f9d3 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) 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) 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 @@ -321,8 +321,13 @@ the pair-row `2R̂U/R²` badge matches the graphic; a missing or non-finite segments with persisted leftover-map unexplained leftover `U` so the pair-row `U` badge matches the graphic; a missing or non-finite `U` omits that unexplained leftover caption and does not invent -`U` from `R` and `R̂` or from plotted coordinates. When `R`, `R̂`, `U`, `x`, -`s`, and `e` are finite, `e + s + x = 1`. When coordinates, +`U` from `R` and `R̂` or from plotted coordinates. ADR 0277 captions leftover-map pair +segments with persisted leftover residual `R` so the pair-row residual +badge matches the graphic; a missing or non-finite `R` omits that leftover +residual caption and does not invent `R` from `Y` and `E`, from `U` and +`R̂`, or from plotted coordinates. When `R`, `R̂`, `U`, `x`, +`s`, and `e` are finite, `e + s + x = 1`. When `R`, `R̂`, and `U` are +finite, `U + R̂ = R`. When coordinates, reconstruction, and distance are finite, `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖`. The pairs sit above the member list so a click opens that post with the leftover criterion current diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cfd68b96d..72e315b8a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277) 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 @@ -654,7 +654,8 @@ explained leftover share `e`. ADR 0274 captions leftover-map pair segments with persisted leftover-map unexplained leftover share `s`. ADR 0275 captions leftover-map pair segments with persisted leftover-map cross share `x`. ADR 0276 captions leftover-map pair segments with persisted leftover-map -unexplained leftover `U`. Click a post marker or a pair button +unexplained leftover `U`. ADR 0277 captions leftover-map pair segments with persisted leftover +residual `R`. 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.34.0-leftover-map-segment-residual.md b/CHANGELOG.d/2.34.0-leftover-map-segment-residual.md new file mode 100644 index 000000000..eae4ccf0c --- /dev/null +++ b/CHANGELOG.d/2.34.0-leftover-map-segment-residual.md @@ -0,0 +1,18 @@ +## 2.34.0 — Leftover residual on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover residual `R = Y − E[Y|θ, item]` (ADR 0277). 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, pair segments that match `2R̂U/R²` on the pair row, pair + segments that match `U` on the pair row, and pair segments that match + `R` on the pair row; click a post marker or a pair button opens that + post. A missing or non-finite `R` omits that leftover residual caption. + A finite negative leftover is shown; do not clamp to nonnegative. Do + not invent `R` from `Y` and `E`, from `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 1afc6d53c..dc317cea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ 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 residual `R = Y − E[Y|θ, item]` + (ADR 0277 / v2.34.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, pair segments that match + `2R̂U/R²` on the pair row, pair segments that match `U` on the pair + row, and pair segments that match `R` on the pair row; click a post + marker or a pair button opens that post. A missing or non-finite `R` + omits that leftover residual caption. A finite negative leftover is + shown; do not clamp to nonnegative. Do not invent `R` from `Y` and + `E`, from `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 `U = R − R̂` (ADR 0276 / v2.33.0). After `make seed`, closest and diff --git a/CLAUDE.md b/CLAUDE.md index 56cc3bcda..68bebb178 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), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277), 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 74e871ea3..fb4938955 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -19,7 +19,8 @@ [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 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); -[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments) +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); +[ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments) ## Context @@ -87,7 +88,8 @@ segments with persisted leftover-map unexplained leftover share `s`; it adds no columns. ADR 0275 captions leftover-map pair segments with persisted leftover-map cross share `x`; it adds no columns. ADR 0276 captions leftover-map pair segments with persisted leftover-map -unexplained leftover `U`; it adds no columns. +unexplained leftover `U`; it adds no columns. ADR 0277 captions leftover-map +pair segments with persisted leftover residual `R`; 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 fd856219c..f45a7d462 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -20,7 +20,8 @@ [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 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); -[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments) +[ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); +[ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments) ## Context @@ -53,7 +54,8 @@ 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). Pair segments name persisted leftover-map cross share `x` (ADR 0275). Pair segments name persisted leftover-map -unexplained leftover `U` (ADR 0276). The pair renders every available finite measurement. +unexplained leftover `U` (ADR 0276). Pair segments name persisted leftover +residual `R` (ADR 0277). 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 @@ -96,6 +98,8 @@ Leftover-map cross share on pair segments is [ADR 0275](0275-leftover-map-segment-cross-share.md). Leftover-map unexplained leftover on pair segments is [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md). +Leftover residual on pair segments is +[ADR 0277](0277-leftover-map-segment-residual.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/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index 674f1de06..42e7f8154 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -18,7 +18,9 @@ [ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) -(leftover-map unexplained leftover on pair segments) +(leftover-map unexplained leftover on pair segments); +[ADR 0277](0277-leftover-map-segment-residual.md) +(leftover residual on pair segments) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -98,7 +100,8 @@ 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). Pair segments name persisted leftover-map cross share `x` (ADR 0275). Pair segments name -persisted leftover-map unexplained leftover `U` (ADR 0276). When coordinates, reconstruction, and +persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name +persisted leftover residual `R` (ADR 0277). 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/0277-leftover-map-segment-residual.md b/docs/adr/0277-leftover-map-segment-residual.md new file mode 100644 index 000000000..0f43a4227 --- /dev/null +++ b/docs/adr/0277-leftover-map-segment-residual.md @@ -0,0 +1,152 @@ +# ADR 0277 — Name leftover residual on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)). Independent of +leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments +([ADR 0275](0275-leftover-map-segment-cross-share.md)), leftover-map +unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover-map +explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover +persistence ([ADR 0182](0182-leftover-map-unexplained.md)), leftover observed +`Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)), and leftover-map axis share +persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0162 already discloses leftover residual `R = Y − E[Y|θ, item]` after +IRT main effects on leftover pair rows. ADR 0276 already names persisted +leftover-map unexplained leftover `U` on the leftover-map graphic-display +pair segment. The segment still has no residual label, so a buyer who +reads `+0.40` on the pair row cannot match that signed leftover to the +graphic pair. Hiding `R` on the segment lets leftover-map reconstruction +`R̂` or unexplained leftover `U` be read as the leftover that entered the +truncated map even after `U` is named on the segment. When `R`, `R̂`, and +`U` are finite, `U + R̂ = R`; the graphic must name the same persisted `R` +the pair row already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover residual. It does not add columns. It does not +recompute `R` from `Y` and `E`, from `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 **0277** so it does not +collide with leftover-map unexplained leftover on pair segments (0276), leftover-map +cross share on pair segments (0275), leftover-map unexplained leftover share +on pair segments (0274), leftover-map explained leftover share on pair +segments (0273), leftover-map reconstruction on pair segments (0272), leftover-map +distance on pair segments (0271), leftover-map coordinate ticks (0270), leftover-map +axis share on the graphic display (0269), leftover-map graphic display (0268), leftover-map +coordinates (0267 / migration 0245), leftover-map explained leftover share persistence +(0266 / migration 0244), leftover-map unexplained leftover share +persistence (0233 / migration 0233), leftover-map reconstruction +persistence (0201 / migration 0206), leftover-map cross share persistence +(0185), leftover-map unexplained leftover persistence (0182), leftover +residual disclosure (0162), 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 persisted leftover residual `R` as `R ±n.nn`, using +the same signed leftover formatter as the pair-row residual badge. A +missing or non-finite `R` omits that leftover residual caption and keeps +the connecting line and any leftover-map distance, reconstruction, +explained leftover share, unexplained leftover share, leftover-map cross +share, or unexplained leftover caption. Rank-0 origin cells still name +`R 0.00` when that persisted leftover is finite. A finite negative +leftover is shown; do not clamp to nonnegative. Do not invent `R` from +`Y` and `E`, from `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 residual 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`, persisted leftover-map cross share `x`, persisted leftover-map +unexplained leftover `U`, and persisted leftover residual `R`; 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, +still name `2R̂U/R² 0.00` when that leftover-map cross share is persisted, +still name `U 0.00` when that unexplained leftover is persisted, and still +name `R 0.00` when that leftover residual is persisted. When `R`, `R̂`, +and `U` are all finite, `U + R̂ = R` remains the same identity already +persisted by ADR 0162, ADR 0182, and ADR 0201. 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 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 persistence, 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, leftover-map +unexplained leftover share on pair segments, leftover-map cross share on +pair segments, and leftover-map unexplained leftover 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 residual is the signed remainder +`R = Y − E[Y|θ, item]` after IRT main effects. Unexplained leftover is +the signed remainder `U = R − R̂` after that two-axis reconstruction.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 10ffd80ea..4c78b9803 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-30 06:20 KST. Protected `main` is +> `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map +> explained leftover share). GitHub writes work (comment/close/create-PR). +> Token still has empty `X-OAuth-Scopes`; repo permission is ADMIN. +> Open leftover stack still lacks independent APPROVE. Do not self-approve. +> #782 leftover-map coordinates through pair-segment distance +> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained +> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29 +> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair +> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross +> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map +> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is +> `e3fdfd74`. Leave #782/#802/#803/#804/#805 open for independent review. +> Do not squash-merge stacked leftover PRs onto an unprotected leftover +> base. Strix on #782 failed closed at `Run Strix (quick)`; do not weaken +> fail-closed on `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError` +> classifier is already on ContextualWisdomLab/.github. Copilot review is +> not independent APPROVE. Issues #79 and #87 stay open. Only collaborator +> is `seonghobae`; no independent reviewer can be requested from this token. +> +> Next buyer increment on this cycle: leftover residual `R` on +> graphic-display pair segments (ADR 0277 / v2.34.0) delivered locally on +> `feat/leftover-map-segment-residual-v2340`. Caption each +> closest/farthest segment with persisted leftover residual so the +> pair-row `R` badge matches the graphic and `U + R̂ = R` is +> graphic-auditable. UI-only; no new columns. Missing/non-finite `R` +> omits that leftover residual caption. A finite negative leftover is +> shown; do not clamp to nonnegative. Do not invent `R` from `Y` and `E`, +> from `U` and `R̂`, or from plotted coordinates. Do not invent leftover +> scores. Stack onto leftover branch +> `feat/leftover-map-segment-unexplained-leftover-v2330` / #805; 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-30 02:09 KST. Protected `main` is > `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map > explained leftover share). GitHub writes work (comment/close/create-PR). diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 35671b57d..c0a9afac8 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, and pair-segment `U` to leftover-map unexplained leftover, 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`, `2R̂U/R² 0.00`, and `U 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, pair segments name persisted leftover-map cross share, and pair segments name persisted leftover-map unexplained leftover. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, and pair-segment `R` to leftover residual, 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`, `2R̂U/R² 0.00`, `U 0.00`, and `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, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, and pair segments name persisted leftover residual. | `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 465f0dda4..201eb9e31 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.33.0", + "version": "2.34.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 931ee1ed1..f67bb8fb3 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -55,7 +55,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, leftover-map unexplained leftover share s, leftover-map cross share x, and leftover-map unexplained leftover U. 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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -77,6 +77,8 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² -0.24")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover U +0.05")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover U −0.25")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover residual R +0.40")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover residual R −1.10")).toBeInTheDocument(); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -155,6 +157,7 @@ describe("LeftoverMapPlot", () => { 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(); expect(screen.getByLabelText("leftover-map unexplained leftover U 0.00")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover residual R 0.00")).toBeInTheDocument(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -358,4 +361,35 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.12")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover share U²/R² 0.02")).toBeInTheDocument(); }); + + it("omits leftover residual on a pair segment when R is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/^leftover residual R/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map unexplained leftover U +0.05")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.12")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 8593e56ec..0cbaa702b 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -15,6 +15,7 @@ import { LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, + LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL, LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED, LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED_SHARE, LEFTOVER_MAP_PLOT_TICK, @@ -55,15 +56,17 @@ function leftoverMapPlotAxisText( * pair row match the plot. Pair segments name persisted leftover-map * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map * explained leftover share ``e``, leftover-map unexplained leftover - * share ``s``, leftover-map cross share ``x``, and leftover-map - * unexplained leftover ``U`` so the pair-row badges match the graphic. + * share ``s``, leftover-map cross share ``x``, leftover-map + * unexplained leftover ``U``, and leftover residual ``R`` 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 leftover-map cross share caption when * ``x`` is missing or non-finite. Omit that unexplained leftover caption - * when ``U`` is missing or non-finite. Omit that axis badge when share is + * when ``U`` is missing or non-finite. Omit that leftover residual + * caption when ``R`` 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. @@ -230,6 +233,19 @@ export function LeftoverMapPlot({ {segment.unexplainedLeftoverLabel} ) : null} + {segment.residualLabel !== null ? ( + + {segment.residualLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index 327c00e9a..5217c3583 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -67,7 +67,8 @@ export type LeftoverPairListProps = { * persisted leftover-map unexplained leftover share ``s`` on those pair * segments. ADR 0275 names persisted leftover-map cross share ``x`` on * those pair segments. ADR 0276 names persisted leftover-map unexplained - * leftover ``U`` on those pair segments. Every badge still + * leftover ``U`` on those pair segments. ADR 0277 names persisted leftover + * residual ``R`` 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 c64df0090..bc6c5f99a 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -72,7 +72,8 @@ describe("i18n", () => { "leftover-map unexplained leftover share {label}", "leftover-map cross share {label}", "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.", + "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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.", @@ -336,22 +337,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x 与残差图未解释残差 U。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U 与残差 R。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "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, phần dư chưa giải thích s, phần giao x và phần dư chưa giải thích U. 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, phần giao x, phần dư chưa giải thích U và phần dư R. 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, leftover-map unexplained leftover share s, leftover-map cross share x, and leftover-map unexplained leftover U. 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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -430,6 +431,16 @@ describe("i18n", () => { expect(tf("leftover-map unexplained leftover {label}", { label: "U +0.05" })).toBe(expected); }); + it.each([ + ["ko", "잔여 R +0.40"], + ["zh", "残差 R +0.40"], + ["ja", "残差 R +0.40"], + ["vi", "phần dư R +0.40"], + ] as const)("formats leftover-map segment leftover residual in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover residual {label}", { label: "R +0.40" })).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 50d6aad4f..13ee6327b 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -597,8 +597,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover share {label}": "잔여 지도 미설명 잔여 점유율 {label}", "leftover-map cross share {label}": "잔여 지도 교차 점유율 {label}", "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "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.": @@ -1182,8 +1183,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover share {label}": "残差图未解释残差份额 {label}", "leftover-map cross share {label}": "残差图交叉份额 {label}", "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x 与残差图未解释残差 U。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U 与残差 R。点击帖子标记打开该帖子。此图不会虚构残差分数。", "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.": @@ -1771,8 +1773,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover share {label}": "残差マップ未説明残差割合 {label}", "leftover-map cross share {label}": "残差マップ交差割合 {label}", "leftover-map unexplained leftover {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, leftover-map cross share x, and leftover-map unexplained leftover U. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "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.": @@ -2360,8 +2363,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover share {label}": "phần dư chưa giải thích bản đồ phần dư {label}", "leftover-map cross share {label}": "phần giao bản đồ phần dư {label}", "leftover-map unexplained leftover {label}": "phần dư chưa giải thích {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, leftover-map cross share x, and leftover-map unexplained leftover U. 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, phần giao x và phần dư chưa giải thích U. 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 residual {label}": "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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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, phần giao x, phần dư chưa giải thích U và phần dư R. 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 eda3b0d88..53b9a1acd 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -28,6 +28,7 @@ function pair( leftover_map_unexplained_share: 0.02, leftover_map_cross_share: 0.12, leftover_map_unexplained: 0.05, + leftover_residual: 0.4, ...overrides, }; } @@ -729,6 +730,110 @@ 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 residual 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, + leftover_map_unexplained: -0.25, + leftover_residual: -1.1, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.residualLabel)).toEqual([ + "R +0.40", + "R \u22121.10", + ]); + expect(layout?.segments[0]?.residualX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.residualY).toBeGreaterThan( + layout?.segments[0]?.unexplainedLeftoverY ?? 0, + ); + }); + + it("omits a leftover residual caption when R is missing or non-finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ leftover_residual: null }), + pair({ + pair_kind: "farthest", + leftover_residual: 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.residualLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.unexplainedLeftoverLabel)).toEqual([ + "U +0.05", + "U +0.05", + ]); + }); + + it("does not invent leftover residual from unexplained leftover or reconstruction", () => { + 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, + leftover_map_unexplained: 0.05, + leftover_residual: 0.4, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.residualLabel).toBe("R +0.40"); + expect(layout?.segments[0]?.residualLabel).not.toBe("R +1.05"); + }); + + it("names rank-0 origin leftover residual R 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, + leftover_map_unexplained: 0, + leftover_residual: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.residualLabel).toBe("R 0.00"); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U 0.00"); + 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 94162258d..067eaf1d6 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -5,8 +5,9 @@ * persisted leftover-map reconstruction ``R̂`` (ADR 0272), persisted * leftover-map explained leftover share ``e`` (ADR 0273), persisted * leftover-map unexplained leftover share ``s`` (ADR 0274), persisted - * leftover-map cross share ``x`` (ADR 0275), and persisted leftover-map - * unexplained leftover ``U`` (ADR 0276). + * leftover-map cross share ``x`` (ADR 0275), persisted leftover-map + * unexplained leftover ``U`` (ADR 0276), and persisted leftover residual + * ``R`` (ADR 0277). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; @@ -18,10 +19,11 @@ import { formatSignedLeftoverValue, } from "./leftoverMapUnexplained"; import { formatLeftoverMapUnexplainedShare } from "./leftoverMapUnexplainedShare"; +import { formatLeftoverMapResidual } from "./leftoverResidual"; 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, leftover-map unexplained leftover share s, leftover-map cross share x, and leftover-map unexplained leftover U. 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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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}"; @@ -47,6 +49,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE = export const LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED = "leftover-map unexplained leftover {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL = + "leftover residual {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -67,6 +72,7 @@ export type LeftoverMapPlottablePair = { leftover_map_unexplained_share?: number | null; leftover_map_cross_share?: number | null; leftover_map_unexplained?: number | null; + leftover_residual?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -97,6 +103,7 @@ export type LeftoverMapPlotSegment = { unexplainedShareLabel: string | null; crossShareLabel: string | null; unexplainedLeftoverLabel: string | null; + residualLabel: string | null; labelX: number; labelY: number; reconstructionX: number; @@ -109,6 +116,8 @@ export type LeftoverMapPlotSegment = { crossShareY: number; unexplainedLeftoverX: number; unexplainedLeftoverY: number; + residualX: number; + residualY: number; }; export type LeftoverMapPlotTick = { @@ -339,6 +348,7 @@ export function layoutLeftoverMapPlot( const unexplainedLeftoverLabel = formatLeftoverMapUnexplained( pair.leftover_map_unexplained, ); + const residualLabel = formatLeftoverMapResidual(pair.leftover_residual); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -374,6 +384,15 @@ export function layoutLeftoverMapPlot( (unexplainedShareLabel !== null ? 1 : 0) + (crossShareLabel !== null ? 1 : 0), ); + const residualY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0) + + (unexplainedShareLabel !== null ? 1 : 0) + + (crossShareLabel !== null ? 1 : 0) + + (unexplainedLeftoverLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -388,6 +407,7 @@ export function layoutLeftoverMapPlot( unexplainedShareLabel, crossShareLabel, unexplainedLeftoverLabel, + residualLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, @@ -398,6 +418,8 @@ export function layoutLeftoverMapPlot( crossShareY, unexplainedLeftoverX: labelPosition.labelX, unexplainedLeftoverY, + residualX: labelPosition.labelX, + residualY, ...labelPosition, }); } diff --git a/frontend/src/leftoverResidual.test.ts b/frontend/src/leftoverResidual.test.ts index bcdb831c3..342d3ad37 100644 --- a/frontend/src/leftoverResidual.test.ts +++ b/frontend/src/leftoverResidual.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { formatLeftoverResidual } from "./leftoverResidual"; +import { formatLeftoverMapResidual, formatLeftoverResidual } from "./leftoverResidual"; describe("formatLeftoverResidual", () => { it("keeps a signed residual without inventing a leftover score", () => { @@ -9,3 +9,18 @@ describe("formatLeftoverResidual", () => { expect(formatLeftoverResidual(Number.NaN)).toBe("—"); }); }); + +describe("formatLeftoverMapResidual", () => { + it("names leftover residual without inventing a leftover score", () => { + expect(formatLeftoverMapResidual(0.4)).toBe("R +0.40"); + expect(formatLeftoverMapResidual(-1.1)).toBe("R \u22121.10"); + expect(formatLeftoverMapResidual(0)).toBe("R 0.00"); + }); + + it("omits the badge when leftover residual is missing or non-finite", () => { + expect(formatLeftoverMapResidual(null)).toBeNull(); + expect(formatLeftoverMapResidual(undefined)).toBeNull(); + expect(formatLeftoverMapResidual(Number.NaN)).toBeNull(); + expect(formatLeftoverMapResidual(Number.POSITIVE_INFINITY)).toBeNull(); + }); +}); diff --git a/frontend/src/leftoverResidual.ts b/frontend/src/leftoverResidual.ts index e21fe98d7..33886e308 100644 --- a/frontend/src/leftoverResidual.ts +++ b/frontend/src/leftoverResidual.ts @@ -12,3 +12,10 @@ export function formatLeftoverResidual(value: number): string { } return magnitude; } + +export function formatLeftoverMapResidual(value: number | null | undefined): string | null { + if (value == null || !Number.isFinite(value)) { + return null; + } + return `R ${formatLeftoverResidual(value)}`; +} diff --git a/pyproject.toml b/pyproject.toml index dfc9a04e1..798db8ac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.33.0" +version = "2.34.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 b334b00eec9dc1196a6bf8a7e33a35e44a6b6c9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:23:24 +0000 Subject: [PATCH 05/23] feat(reports): leftover observed on pair segments (v2.35.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 observed Y so Y − E = R is graphic-auditable. UI-only; no new columns. Do not invent Y from R and E or from plotted coordinates. --- AGENTS.md | 11 +- ARCHITECTURE.md | 5 +- .../2.35.0-leftover-map-segment-observed.md | 18 ++ CHANGELOG.md | 17 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 5 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- .../adr/0278-leftover-map-segment-observed.md | 160 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 40 +++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../src/components/LeftoverMapPlot.test.tsx | 39 ++++- frontend/src/components/LeftoverMapPlot.tsx | 21 ++- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 23 ++- frontend/src/i18n.ts | 20 ++- frontend/src/leftoverMapPlotLayout.test.ts | 108 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 28 ++- frontend/src/leftoverObservedExpected.test.ts | 18 +- frontend/src/leftoverObservedExpected.ts | 7 + pyproject.toml | 2 +- 22 files changed, 510 insertions(+), 38 deletions(-) create mode 100644 CHANGELOG.d/2.35.0-leftover-map-segment-observed.md create mode 100644 docs/adr/0278-leftover-map-segment-observed.md diff --git a/AGENTS.md b/AGENTS.md index 08fe2f9d3..dfd852c61 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) 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) 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 @@ -325,8 +325,13 @@ the pair-row `U` badge matches the graphic; a missing or non-finite segments with persisted leftover residual `R` so the pair-row residual badge matches the graphic; a missing or non-finite `R` omits that leftover residual caption and does not invent `R` from `Y` and `E`, from `U` and -`R̂`, or from plotted coordinates. When `R`, `R̂`, `U`, `x`, -`s`, and `e` are finite, `e + s + x = 1`. When `R`, `R̂`, and `U` are +`R̂`, or from plotted coordinates. ADR 0278 captions leftover-map pair +segments with persisted leftover observed `Y` so the pair-row `Y` badge +matches the graphic; a missing or non-finite `Y` omits that leftover +observed caption and does not invent `Y` from `R` and `E` or from plotted +coordinates. 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 finite, `U + R̂ = R`. 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 72e315b8a..8b6968f6b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278) 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 @@ -655,7 +655,8 @@ segments with persisted leftover-map unexplained leftover share `s`. ADR 0275 captions leftover-map pair segments with persisted leftover-map cross share `x`. ADR 0276 captions leftover-map pair segments with persisted leftover-map unexplained leftover `U`. ADR 0277 captions leftover-map pair segments with persisted leftover -residual `R`. Click a post marker or a pair button +residual `R`. ADR 0278 captions leftover-map pair segments with persisted leftover +observed `Y`. 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.35.0-leftover-map-segment-observed.md b/CHANGELOG.d/2.35.0-leftover-map-segment-observed.md new file mode 100644 index 000000000..c65d06db4 --- /dev/null +++ b/CHANGELOG.d/2.35.0-leftover-map-segment-observed.md @@ -0,0 +1,18 @@ +## 2.35.0 — Leftover observed Y on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover observed `Y` (ADR 0278). 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, + pair segments that match `2R̂U/R²` on the pair row, pair segments + that match `U` on the pair row, pair segments that match `R` on the + pair row, and pair segments that match `Y` on the pair row; click a + post marker or a pair button opens that post. A missing or + non-finite `Y` omits that leftover observed caption. A finite + negative observed is shown; do not clamp to nonnegative. Do not + invent `Y` from `R` and `E` 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 dc317cea2..7915ecc4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ 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 observed `Y` (ADR 0278 / v2.35.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, pair segments that match `2R̂U/R²` on + the pair row, pair segments that match `U` on the pair row, pair + segments that match `R` on the pair row, and pair segments that match + `Y` on the pair row; click a post marker or a pair button opens that + post. A missing or non-finite `Y` omits that leftover observed + caption. A finite negative observed is shown; do not clamp to + nonnegative. Do not invent `Y` from `R` and `E` 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 residual `R = Y − E[Y|θ, item]` (ADR 0277 / v2.34.0). After `make seed`, closest and farthest leftover diff --git a/CLAUDE.md b/CLAUDE.md index 68bebb178..81b568490 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), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278), 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 fb4938955..45d41417a 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -20,7 +20,8 @@ [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); [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); -[ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments) +[ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); +[ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments) ## Context @@ -90,6 +91,8 @@ persisted leftover-map cross share `x`; it adds no columns. ADR 0276 captions leftover-map pair segments with persisted leftover-map unexplained leftover `U`; it adds no columns. ADR 0277 captions leftover-map pair segments with persisted leftover residual `R`; it adds no columns. +ADR 0278 captions leftover-map pair segments with persisted leftover +observed `Y`; 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 f45a7d462..34320cb62 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -21,7 +21,8 @@ [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); [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); -[ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments) +[ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); +[ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments) ## Context @@ -55,7 +56,8 @@ explained leftover share `e` (ADR 0273). Pair segments name persisted leftover-m unexplained leftover share `s` (ADR 0274). Pair segments name persisted leftover-map cross share `x` (ADR 0275). Pair segments name persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name persisted leftover -residual `R` (ADR 0277). The pair renders every available finite measurement. +residual `R` (ADR 0277). Pair segments name persisted leftover observed +`Y` (ADR 0278). 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 @@ -100,6 +102,8 @@ Leftover-map unexplained leftover on pair segments is [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md). Leftover residual on pair segments is [ADR 0277](0277-leftover-map-segment-residual.md). +Leftover observed Y on pair segments is +[ADR 0278](0278-leftover-map-segment-observed.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/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index 42e7f8154..fce18867f 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -20,7 +20,9 @@ [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); [ADR 0277](0277-leftover-map-segment-residual.md) -(leftover residual on pair segments) +(leftover residual on pair segments); +[ADR 0278](0278-leftover-map-segment-observed.md) +(leftover observed Y on pair segments) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -101,7 +103,8 @@ persisted leftover-map explained leftover share `e` (ADR 0273). Pair segments na persisted leftover-map unexplained leftover share `s` (ADR 0274). Pair segments name persisted leftover-map cross share `x` (ADR 0275). Pair segments name persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name -persisted leftover residual `R` (ADR 0277). When coordinates, reconstruction, and +persisted leftover residual `R` (ADR 0277). Pair segments name +persisted leftover observed `Y` (ADR 0278). 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/0278-leftover-map-segment-observed.md b/docs/adr/0278-leftover-map-segment-observed.md new file mode 100644 index 000000000..c15d02b84 --- /dev/null +++ b/docs/adr/0278-leftover-map-segment-observed.md @@ -0,0 +1,160 @@ +# ADR 0278 — Name leftover observed Y on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover observed `Y` / +expected `E` ([ADR 0163](0163-leftover-observed-expected.md)). Independent of +leftover residual on pair segments +([ADR 0277](0277-leftover-map-segment-residual.md)), leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments +([ADR 0275](0275-leftover-map-segment-cross-share.md)), leftover-map +unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover-map +explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover +persistence ([ADR 0182](0182-leftover-map-unexplained.md)), and leftover-map axis share +persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0163 already persists observed category `Y` and expected +`E[Y|θ, item]` on leftover pair rows. ADR 0277 already names persisted +leftover residual `R` on the leftover-map graphic-display pair segment. +The segment still has no observed-category label, so a buyer who reads +`Y 2.40` on the pair row cannot match that observed response to the +graphic pair. Hiding `Y` on the segment lets leftover residual `R` be +read as the category that entered the truncated map even after `R` is +named on the segment. When `Y`, `E`, and `R` are finite, `Y − E = R`; +the graphic must name the same persisted `Y` the pair row already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover observed `Y`. It does not add columns. It does not +recompute `Y` from `R` and `E` 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. Expected `E` on pair segments stays a later +increment. + +The dashboard stack already used neighbouring leftover facts under +other numbers. This protected increment uses **0278** so it does not +collide with leftover residual on pair segments (0277), leftover-map +unexplained leftover on pair segments (0276), leftover-map cross share +on pair segments (0275), leftover-map unexplained leftover share +on pair segments (0274), leftover-map explained leftover share on pair +segments (0273), leftover-map reconstruction on pair segments (0272), leftover-map +distance on pair segments (0271), leftover-map coordinate ticks (0270), leftover-map +axis share on the graphic display (0269), leftover-map graphic display (0268), leftover-map +coordinates (0267 / migration 0245), leftover-map explained leftover share persistence +(0266 / migration 0244), leftover-map unexplained leftover share +persistence (0233 / migration 0233), leftover-map reconstruction +persistence (0201 / migration 0206), leftover-map cross share persistence +(0185), leftover-map unexplained leftover persistence (0182), leftover +residual disclosure (0162), leftover observed `Y` / expected `E` persistence +(0163), 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 persisted leftover observed `Y` as `Y n.nn`, using +the same observed formatter as the pair-row `Y` badge. A missing or +non-finite `Y` omits that leftover observed caption and keeps the +connecting line and any leftover-map distance, reconstruction, +explained leftover share, unexplained leftover share, leftover-map cross +share, unexplained leftover, or leftover residual caption. Rank-0 origin +cells still name `Y 0.00` when that persisted observed is finite. A +finite negative observed is shown; do not clamp to nonnegative. Do not +invent `Y` from `R` and `E` 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 observed 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`, persisted leftover-map cross share `x`, persisted leftover-map +unexplained leftover `U`, persisted leftover residual `R`, and persisted +leftover observed `Y`; 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, still name `2R̂U/R² 0.00` when +that leftover-map cross share is persisted, still name `U 0.00` when that +unexplained leftover is persisted, still name `R 0.00` when that leftover +residual is persisted, and still name `Y 0.00` when that leftover +observed is persisted. When `Y`, `E`, and `R` are all finite, +`Y − E = R` remains the same identity already persisted by ADR 0162 and +ADR 0163. When `R`, `R̂`, and `U` are all finite, `U + R̂ = R` remains +the same identity already persisted by ADR 0162, ADR 0182, and ADR 0201. +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 expected `E` +on pair segments, 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 +persistence, 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, leftover-map unexplained leftover share on pair +segments, leftover-map cross share on pair segments, leftover-map +unexplained leftover on pair segments, and leftover residual 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 residual is the signed remainder +`R = Y − E[Y|θ, item]` after IRT main effects. Observed `Y` is the +category that entered that residual.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 4c78b9803..b2667a86e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,45 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-30 13:06 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash +> of leftover-map coordinates (v2.24.0 / #782). Revert #808 +> (`revert-pr782-unauthorized` @ `a10c2101`) restores `main` to +> `fc13acaa` (v2.23.0) and still needs independent APPROVE. GitHub +> writes work (comment/close/create-PR/push). Token still has empty +> `X-OAuth-Scopes`; repo permission is ADMIN. Open leftover stack +> still lacks independent APPROVE. Do not self-approve. +> #782 leftover-map coordinates through pair-segment distance +> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained +> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29 +> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair +> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross +> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map +> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is +> `e3fdfd74`. #806 leftover residual `R` on pair segments (v2.34.0 / +> ADR 0277) is `33f6c3dd`. Leave #782/#802/#803/#804/#805/#806/#808 +> open for independent review. Do not squash-merge stacked leftover PRs +> onto an unprotected leftover base. Do not merge #808 without +> independent APPROVE. Strix on #782 failed closed at `Run Strix (quick)`; +> do not weaken fail-closed on `Vulnerabilities [1-9]`. Org Strix +> `ModelBehaviorError` classifier is already on ContextualWisdomLab/.github. +> Copilot review is not independent APPROVE. Issues #79 and #87 stay open. +> #96 closed as weaker duplicate of #91. Only collaborator is +> `seonghobae`; no independent reviewer can be requested from this token. +> +> Next buyer increment on this cycle: leftover observed `Y` on +> graphic-display pair segments (ADR 0278 / v2.35.0) delivered locally on +> `feat/leftover-map-segment-observed-v2350`. Caption each +> closest/farthest segment with persisted leftover observed so the +> pair-row `Y` badge matches the graphic and `Y − E = R` is +> graphic-auditable. UI-only; no new columns. Missing/non-finite `Y` +> omits that leftover observed caption. A finite negative observed is +> shown; do not clamp to nonnegative. Do not invent `Y` from `R` and +> `E` or from plotted coordinates. Do not invent leftover scores. +> Stack onto leftover branch `feat/leftover-map-segment-residual-v2340` +> / #806; 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-30 06:20 KST. Protected `main` is > `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map > explained leftover share). GitHub writes work (comment/close/create-PR). diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index c0a9afac8..f7b4558b3 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, and pair-segment `R` to leftover residual, 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`, `2R̂U/R² 0.00`, `U 0.00`, and `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, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, and pair segments name persisted leftover residual. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, and pair-segment `Y` to leftover observed, 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`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, and `Y 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, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, and pair segments name persisted leftover observed. | `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 201eb9e31..0f972aef9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.34.0", + "version": "2.35.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index f67bb8fb3..7da06f2b4 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -12,6 +12,7 @@ const PAIRS: LeftoverPair[] = [ criterion_code: "sales_lead_quality", leftover_distance: 0.12, leftover_residual: 0.4, + observed_response: 2.4, leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, @@ -29,6 +30,7 @@ const PAIRS: LeftoverPair[] = [ criterion_code: "negative_sentiment", leftover_distance: 1.84, leftover_residual: -1.1, + observed_response: 0.9, leftover_map_reconstruction: -0.95, leftover_map_explained_share: 0.6, leftover_map_unexplained_share: 0.05, @@ -55,7 +57,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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -79,6 +81,8 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map unexplained leftover U −0.25")).toBeInTheDocument(); expect(screen.getByLabelText("leftover residual R +0.40")).toBeInTheDocument(); expect(screen.getByLabelText("leftover residual R −1.10")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover observed Y 0.90")).toBeInTheDocument(); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -124,6 +128,7 @@ describe("LeftoverMapPlot", () => { criterion_code: "sales_lead_quality", leftover_distance: 0, leftover_residual: 0, + observed_response: 0, leftover_map_rank: 0, leftover_map_reconstruction: 0, leftover_map_explained_share: 0, @@ -158,6 +163,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover U 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover residual R 0.00")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover observed Y 0.00")).toBeInTheDocument(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -392,4 +398,35 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map unexplained leftover U +0.05")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map cross share 2R̂U/R² 0.12")).toBeInTheDocument(); }); + + it("omits leftover observed on a pair segment when Y is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/^leftover observed Y/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover residual R +0.40")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map unexplained leftover U +0.05")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 0cbaa702b..f518efb91 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -14,6 +14,7 @@ import { LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, + LEFTOVER_MAP_PLOT_SEGMENT_OBSERVED, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL, LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED, @@ -57,8 +58,8 @@ function leftoverMapPlotAxisText( * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map * explained leftover share ``e``, leftover-map unexplained leftover * share ``s``, leftover-map cross share ``x``, leftover-map - * unexplained leftover ``U``, and leftover residual ``R`` so the - * pair-row badges match the graphic. + * unexplained leftover ``U``, leftover residual ``R``, and leftover + * observed ``Y`` 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 @@ -66,7 +67,8 @@ function leftoverMapPlotAxisText( * missing or non-finite. Omit that leftover-map cross share caption when * ``x`` is missing or non-finite. Omit that unexplained leftover caption * when ``U`` is missing or non-finite. Omit that leftover residual - * caption when ``R`` is missing or non-finite. Omit that axis badge when share is + * caption when ``R`` is missing or non-finite. Omit that leftover observed + * caption when ``Y`` 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. @@ -246,6 +248,19 @@ export function LeftoverMapPlot({ {segment.residualLabel} ) : null} + {segment.observedLabel !== null ? ( + + {segment.observedLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index 5217c3583..8f3bec764 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -68,7 +68,8 @@ export type LeftoverPairListProps = { * segments. ADR 0275 names persisted leftover-map cross share ``x`` on * those pair segments. ADR 0276 names persisted leftover-map unexplained * leftover ``U`` on those pair segments. ADR 0277 names persisted leftover - * residual ``R`` on those pair segments. Every badge still + * residual ``R`` on those pair segments. ADR 0278 names persisted leftover + * observed ``Y`` 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 bc6c5f99a..748f644f9 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -73,7 +73,8 @@ describe("i18n", () => { "leftover-map cross share {label}", "leftover-map unexplained leftover {label}", "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.", + "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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.", @@ -337,22 +338,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U 与残差 R。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R 与观测 Y。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "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, phần dư chưa giải thích s, phần giao x, phần dư chưa giải thích U và phần dư R. 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, phần giao x, phần dư chưa giải thích U, phần dư R và Y quan sát. 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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -441,6 +442,16 @@ describe("i18n", () => { expect(tf("leftover residual {label}", { label: "R +0.40" })).toBe(expected); }); + it.each([ + ["ko", "관측 Y 2.40"], + ["zh", "观测 Y 2.40"], + ["ja", "観測 Y 2.40"], + ["vi", "quan sát Y 2.40"], + ] as const)("formats leftover-map segment leftover observed in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover observed {label}", { label: "Y 2.40" })).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 13ee6327b..6b8ef0a54 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -598,8 +598,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map cross share {label}": "잔여 지도 교차 점유율 {label}", "leftover-map unexplained leftover {label}": "잔여 지도 미설명 잔여 {label}", "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "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.": @@ -1184,8 +1185,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map cross share {label}": "残差图交叉份额 {label}", "leftover-map unexplained leftover {label}": "残差图未解释残差 {label}", "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U 与残差 R。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R 与观测 Y。点击帖子标记打开该帖子。此图不会虚构残差分数。", "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.": @@ -1774,8 +1776,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map cross share {label}": "残差マップ交差割合 {label}", "leftover-map unexplained leftover {label}": "残差マップ未説明残差 {label}", "leftover residual {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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "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.": @@ -2364,8 +2367,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map cross share {label}": "phần giao bản đồ phần dư {label}", "leftover-map unexplained leftover {label}": "phần dư chưa giải thích {label}", "leftover residual {label}": "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, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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, phần giao x, phần dư chưa giải thích U và phần dư R. 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 observed {label}": "quan sát {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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, phần giao x, phần dư chưa giải thích U, phần dư R và Y quan sát. 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 53b9a1acd..574fd4115 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -834,6 +834,114 @@ 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 observed on pair segments without inventing a leftover score", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ observed_response: 2.4 }), + 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, + leftover_map_unexplained: -0.25, + leftover_residual: -1.1, + observed_response: 0.9, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.observedLabel)).toEqual([ + "Y 2.40", + "Y 0.90", + ]); + expect(layout?.segments[0]?.observedX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.observedY).toBeGreaterThan( + layout?.segments[0]?.residualY ?? 0, + ); + }); + + it("omits a leftover observed caption when Y is missing or non-finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ observed_response: null }), + pair({ + pair_kind: "farthest", + observed_response: 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.observedLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.residualLabel)).toEqual([ + "R +0.40", + "R +0.40", + ]); + }); + + it("does not invent leftover observed from residual and expected", () => { + 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, + leftover_map_unexplained: 0.05, + leftover_residual: 0.4, + observed_response: 9.9, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.observedLabel).toBe("Y 9.90"); + expect(layout?.segments[0]?.observedLabel).not.toBe("Y 2.40"); + }); + + it("names rank-0 origin leftover observed Y 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, + leftover_map_unexplained: 0, + leftover_residual: 0, + observed_response: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.observedLabel).toBe("Y 0.00"); + expect(layout?.segments[0]?.residualLabel).toBe("R 0.00"); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U 0.00"); + 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 067eaf1d6..6ed932965 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -6,8 +6,8 @@ * leftover-map explained leftover share ``e`` (ADR 0273), persisted * leftover-map unexplained leftover share ``s`` (ADR 0274), persisted * leftover-map cross share ``x`` (ADR 0275), persisted leftover-map - * unexplained leftover ``U`` (ADR 0276), and persisted leftover residual - * ``R`` (ADR 0277). + * unexplained leftover ``U`` (ADR 0276), persisted leftover residual + * ``R`` (ADR 0277), and persisted leftover observed ``Y`` (ADR 0278). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; @@ -19,11 +19,12 @@ import { formatSignedLeftoverValue, } from "./leftoverMapUnexplained"; import { formatLeftoverMapUnexplainedShare } from "./leftoverMapUnexplainedShare"; +import { formatLeftoverMapObserved } from "./leftoverObservedExpected"; import { formatLeftoverMapResidual } from "./leftoverResidual"; 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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, and leftover residual R. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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}"; @@ -52,6 +53,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED = export const LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL = "leftover residual {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_OBSERVED = + "leftover observed {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -73,6 +77,7 @@ export type LeftoverMapPlottablePair = { leftover_map_cross_share?: number | null; leftover_map_unexplained?: number | null; leftover_residual?: number | null; + observed_response?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -104,6 +109,7 @@ export type LeftoverMapPlotSegment = { crossShareLabel: string | null; unexplainedLeftoverLabel: string | null; residualLabel: string | null; + observedLabel: string | null; labelX: number; labelY: number; reconstructionX: number; @@ -118,6 +124,8 @@ export type LeftoverMapPlotSegment = { unexplainedLeftoverY: number; residualX: number; residualY: number; + observedX: number; + observedY: number; }; export type LeftoverMapPlotTick = { @@ -349,6 +357,7 @@ export function layoutLeftoverMapPlot( pair.leftover_map_unexplained, ); const residualLabel = formatLeftoverMapResidual(pair.leftover_residual); + const observedLabel = formatLeftoverMapObserved(pair.observed_response); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -393,6 +402,16 @@ export function layoutLeftoverMapPlot( (crossShareLabel !== null ? 1 : 0) + (unexplainedLeftoverLabel !== null ? 1 : 0), ); + const observedY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0) + + (unexplainedShareLabel !== null ? 1 : 0) + + (crossShareLabel !== null ? 1 : 0) + + (unexplainedLeftoverLabel !== null ? 1 : 0) + + (residualLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -408,6 +427,7 @@ export function layoutLeftoverMapPlot( crossShareLabel, unexplainedLeftoverLabel, residualLabel, + observedLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, @@ -420,6 +440,8 @@ export function layoutLeftoverMapPlot( unexplainedLeftoverY, residualX: labelPosition.labelX, residualY, + observedX: labelPosition.labelX, + observedY, ...labelPosition, }); } diff --git a/frontend/src/leftoverObservedExpected.test.ts b/frontend/src/leftoverObservedExpected.test.ts index 56008273a..58f9c0e60 100644 --- a/frontend/src/leftoverObservedExpected.test.ts +++ b/frontend/src/leftoverObservedExpected.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { formatLeftoverObservedExpected } from "./leftoverObservedExpected"; +import { formatLeftoverMapObserved, formatLeftoverObservedExpected } from "./leftoverObservedExpected"; describe("formatLeftoverObservedExpected", () => { it("names observed Y and expected E without inventing a leftover score", () => { @@ -15,3 +15,19 @@ describe("formatLeftoverObservedExpected", () => { expect(formatLeftoverObservedExpected(2.4, Number.POSITIVE_INFINITY)).toBeNull(); }); }); + +describe("formatLeftoverMapObserved", () => { + it("names leftover observed without inventing a leftover score", () => { + expect(formatLeftoverMapObserved(2.4)).toBe("Y 2.40"); + expect(formatLeftoverMapObserved(0.9)).toBe("Y 0.90"); + expect(formatLeftoverMapObserved(0)).toBe("Y 0.00"); + expect(formatLeftoverMapObserved(-1.1)).toBe("Y -1.10"); + }); + + it("omits the badge when leftover observed is missing or non-finite", () => { + expect(formatLeftoverMapObserved(null)).toBeNull(); + expect(formatLeftoverMapObserved(undefined)).toBeNull(); + expect(formatLeftoverMapObserved(Number.NaN)).toBeNull(); + expect(formatLeftoverMapObserved(Number.POSITIVE_INFINITY)).toBeNull(); + }); +}); diff --git a/frontend/src/leftoverObservedExpected.ts b/frontend/src/leftoverObservedExpected.ts index 50e225280..89d0a5132 100644 --- a/frontend/src/leftoverObservedExpected.ts +++ b/frontend/src/leftoverObservedExpected.ts @@ -14,3 +14,10 @@ export function formatLeftoverObservedExpected( } return `Y ${observed.toFixed(2)} · E ${expected.toFixed(2)}`; } + +export function formatLeftoverMapObserved(value: number | null | undefined): string | null { + if (value == null || !Number.isFinite(value)) { + return null; + } + return `Y ${value.toFixed(2)}`; +} diff --git a/pyproject.toml b/pyproject.toml index 798db8ac5..1a8eaabc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.34.0" +version = "2.35.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 6e37757aae8b37b729cf16f076ffcec6c94cd445 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 07:17:47 +0000 Subject: [PATCH 06/23] feat(reports): leftover expected on pair segments (v2.36.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 expected E[Y|θ, item] (ADR 0279). After make seed, closest and farthest leftover pairs sit above the member list with pair segments that match E on the pair row; click a post marker or a pair button opens that post. A missing or non-finite E omits that leftover expected caption. A finite negative expected is shown; do not clamp to nonnegative. Do not invent E from Y and R or from plotted coordinates. Never invent a leftover score or a theta. UI-only; no new columns. --- AGENTS.md | 6 +- ARCHITECTURE.md | 5 +- .../2.36.0-leftover-map-segment-expected.md | 19 ++ CHANGELOG.md | 18 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 6 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- .../adr/0279-leftover-map-segment-expected.md | 164 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 42 +++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../src/components/LeftoverMapPlot.test.tsx | 39 ++++- frontend/src/components/LeftoverMapPlot.tsx | 22 ++- frontend/src/i18n.test.ts | 23 ++- frontend/src/i18n.ts | 20 ++- frontend/src/leftoverMapPlotLayout.test.ts | 113 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 29 +++- frontend/src/leftoverObservedExpected.test.ts | 22 ++- frontend/src/leftoverObservedExpected.ts | 7 + pyproject.toml | 2 +- uv.lock | 2 +- 22 files changed, 525 insertions(+), 37 deletions(-) create mode 100644 CHANGELOG.d/2.36.0-leftover-map-segment-expected.md create mode 100644 docs/adr/0279-leftover-map-segment-expected.md diff --git a/AGENTS.md b/AGENTS.md index dfd852c61..d9c6035ac 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) 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) 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 @@ -329,6 +329,10 @@ residual caption and does not invent `R` from `Y` and `E`, from `U` and segments with persisted leftover observed `Y` so the pair-row `Y` badge matches the graphic; a missing or non-finite `Y` omits that leftover observed caption and does not invent `Y` from `R` and `E` or from plotted +coordinates. ADR 0279 captions leftover-map pair +segments with persisted leftover expected `E` so the pair-row `E` badge +matches the graphic; a missing or non-finite `E` omits that leftover +expected caption and does not invent `E` from `Y` and `R` or from plotted coordinates. 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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8b6968f6b..e3eb9a843 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279) 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 @@ -656,7 +656,8 @@ ADR 0275 captions leftover-map pair segments with persisted leftover-map cross share `x`. ADR 0276 captions leftover-map pair segments with persisted leftover-map unexplained leftover `U`. ADR 0277 captions leftover-map pair segments with persisted leftover residual `R`. ADR 0278 captions leftover-map pair segments with persisted leftover -observed `Y`. Click a post marker or a pair button +observed `Y`. ADR 0279 captions leftover-map pair segments with persisted leftover +expected `E`. 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.36.0-leftover-map-segment-expected.md b/CHANGELOG.d/2.36.0-leftover-map-segment-expected.md new file mode 100644 index 000000000..cf5231f02 --- /dev/null +++ b/CHANGELOG.d/2.36.0-leftover-map-segment-expected.md @@ -0,0 +1,19 @@ +## 2.36.0 — Leftover expected E on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover expected `E[Y|θ, item]` (ADR 0279). 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, + pair segments that match `2R̂U/R²` on the pair row, pair segments + that match `U` on the pair row, pair segments that match `R` on the + pair row, pair segments that match `Y` on the pair row, and pair + segments that match `E` on the pair row; click a post marker or a + pair button opens that post. A missing or non-finite `E` omits that + leftover expected caption. A finite negative expected is shown; do + not clamp to nonnegative. Do not invent `E` from `Y` 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 7915ecc4a..61197f9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ 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 expected `E[Y|θ, item]` (ADR 0279 / + v2.36.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, pair segments that match `2R̂U/R²` on + the pair row, pair segments that match `U` on the pair row, pair + segments that match `R` on the pair row, pair segments that match `Y` + on the pair row, and pair segments that match `E` on the pair row; + click a post marker or a pair button opens that post. A missing or + non-finite `E` omits that leftover expected caption. A finite + negative expected is shown; do not clamp to nonnegative. Do not + invent `E` from `Y` 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 observed `Y` (ADR 0278 / v2.35.0). After `make seed`, closest and farthest leftover pairs sit above the diff --git a/CLAUDE.md b/CLAUDE.md index 81b568490..8f45800f9 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), 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), 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 45d41417a..4788e7302 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -21,7 +21,8 @@ [ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); [ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); -[ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments) +[ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments); +[ADR 0279](0279-leftover-map-segment-expected.md) (leftover expected E on pair segments) ## Context @@ -92,7 +93,8 @@ captions leftover-map pair segments with persisted leftover-map unexplained leftover `U`; it adds no columns. ADR 0277 captions leftover-map pair segments with persisted leftover residual `R`; it adds no columns. ADR 0278 captions leftover-map pair segments with persisted leftover -observed `Y`; it adds no columns. +observed `Y`; it adds no columns. ADR 0279 captions leftover-map pair +segments with persisted leftover expected `E`; 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 34320cb62..d8e5147a6 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -22,7 +22,8 @@ [ADR 0275](0275-leftover-map-segment-cross-share.md) (leftover-map cross share on pair segments); [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); [ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); -[ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments) +[ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments); +[ADR 0279](0279-leftover-map-segment-expected.md) (leftover expected E on pair segments) ## Context @@ -57,7 +58,8 @@ unexplained leftover share `s` (ADR 0274). Pair segments name persisted leftover cross share `x` (ADR 0275). Pair segments name persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name persisted leftover residual `R` (ADR 0277). Pair segments name persisted leftover observed -`Y` (ADR 0278). The pair renders every available finite measurement. +`Y` (ADR 0278). Pair segments name persisted leftover expected +`E` (ADR 0279). 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 @@ -104,6 +106,8 @@ Leftover residual on pair segments is [ADR 0277](0277-leftover-map-segment-residual.md). Leftover observed Y on pair segments is [ADR 0278](0278-leftover-map-segment-observed.md). +Leftover expected E on pair segments is +[ADR 0279](0279-leftover-map-segment-expected.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/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index fce18867f..48a6de6aa 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -22,7 +22,9 @@ [ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); [ADR 0278](0278-leftover-map-segment-observed.md) -(leftover observed Y on pair segments) +(leftover observed Y on pair segments); +[ADR 0279](0279-leftover-map-segment-expected.md) +(leftover expected E on pair segments) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -104,7 +106,8 @@ persisted leftover-map unexplained leftover share `s` (ADR 0274). Pair segments persisted leftover-map cross share `x` (ADR 0275). Pair segments name persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name persisted leftover residual `R` (ADR 0277). Pair segments name -persisted leftover observed `Y` (ADR 0278). When coordinates, reconstruction, and +persisted leftover observed `Y` (ADR 0278). Pair segments name +persisted leftover expected `E` (ADR 0279). 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/0279-leftover-map-segment-expected.md b/docs/adr/0279-leftover-map-segment-expected.md new file mode 100644 index 000000000..f01adbd8d --- /dev/null +++ b/docs/adr/0279-leftover-map-segment-expected.md @@ -0,0 +1,164 @@ +# ADR 0279 — Name leftover expected E on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover observed `Y` / +expected `E` ([ADR 0163](0163-leftover-observed-expected.md)). Independent of +leftover observed on pair segments +([ADR 0278](0278-leftover-map-segment-observed.md)), leftover residual on pair segments +([ADR 0277](0277-leftover-map-segment-residual.md)), leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments +([ADR 0275](0275-leftover-map-segment-cross-share.md)), leftover-map +unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover-map +explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover +persistence ([ADR 0182](0182-leftover-map-unexplained.md)), and leftover-map axis share +persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0163 already persists observed category `Y` and expected +`E[Y|θ, item]` on leftover pair rows. ADR 0278 already names persisted +leftover observed `Y` on the leftover-map graphic-display pair segment. +The segment still has no expected-category label, so a buyer who reads +`E 2.00` on the pair row cannot match that expected response to the +graphic pair. Hiding `E` on the segment lets leftover observed `Y` or +leftover residual `R` be read as the IRT main-effect expectation even +after `Y` is named on the segment. When `Y`, `E`, and `R` are finite, +`Y − E = R`; the graphic must name the same persisted `E` the pair row +already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover expected `E[Y|θ, item]`. It does not add columns. It +does not recompute `E` from `Y` 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 **0279** so it does not +collide with leftover observed on pair segments (0278), leftover residual +on pair segments (0277), leftover-map unexplained leftover on pair +segments (0276), leftover-map cross share on pair segments (0275), +leftover-map unexplained leftover share on pair segments (0274), +leftover-map explained leftover share on pair segments (0273), +leftover-map reconstruction on pair segments (0272), leftover-map +distance on pair segments (0271), leftover-map coordinate ticks (0270), +leftover-map axis share on the graphic display (0269), leftover-map +graphic display (0268), leftover-map coordinates (0267 / migration 0245), +leftover-map explained leftover share persistence (0266 / migration 0244), +leftover-map unexplained leftover share persistence (0233 / migration 0233), +leftover-map reconstruction persistence (0201 / migration 0206), leftover-map +cross share persistence (0185), leftover-map unexplained leftover +persistence (0182), leftover residual disclosure (0162), leftover observed +`Y` / expected `E` persistence (0163), 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 persisted leftover expected `E[Y|θ, item]` as `E n.nn`, +using the same expected formatter as the pair-row `E` badge. A missing or +non-finite `E` omits that leftover expected caption and keeps the +connecting line and any leftover-map distance, reconstruction, +explained leftover share, unexplained leftover share, leftover-map cross +share, unexplained leftover, leftover residual, or leftover observed +caption. Rank-0 origin cells still name `E 0.00` when that persisted +expected is finite. A finite negative expected is shown; do not clamp to +nonnegative. Do not invent `E` from `Y` 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 expected +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`, persisted leftover-map cross share `x`, persisted leftover-map +unexplained leftover `U`, persisted leftover residual `R`, persisted +leftover observed `Y`, and persisted leftover expected `E`; 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, +still name `2R̂U/R² 0.00` when that leftover-map cross share is persisted, +still name `U 0.00` when that unexplained leftover is persisted, still +name `R 0.00` when that leftover residual is persisted, still name +`Y 0.00` when that leftover observed is persisted, and still name +`E 0.00` when that leftover expected is persisted. When `Y`, `E`, and +`R` are all finite, `Y − E = R` remains the same identity already +persisted by ADR 0162 and ADR 0163. When `R`, `R̂`, and `U` are all +finite, `U + R̂ = R` remains the same identity already persisted by +ADR 0162, ADR 0182, and ADR 0201. 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-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 persistence, 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, leftover-map +unexplained leftover share on pair segments, leftover-map cross share on +pair segments, leftover-map unexplained leftover on pair segments, +leftover residual on pair segments, and leftover observed 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 residual is the signed remainder +`R = Y − E[Y|θ, item]` after IRT main effects. Expected `E` is the +already-fitted category that entered that residual.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b2667a86e..b7fc0f4bf 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,47 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-30 16:09 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash +> of leftover-map coordinates (v2.24.0 / #782). Revert #808 +> (`revert-pr782-unauthorized` @ `1af3e53e`) restores `main` toward +> `fc13acaa` (v2.23.0) and still needs independent APPROVE. GitHub +> writes work (comment/close/create-PR/push). Token still has empty +> `X-OAuth-Scopes`; repo permission is ADMIN. Open leftover stack +> still lacks independent APPROVE. Do not self-approve. +> #782 leftover-map coordinates through pair-segment distance +> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained +> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29 +> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair +> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross +> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map +> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is +> `e3fdfd74`. #806 leftover residual `R` on pair segments (v2.34.0 / +> ADR 0277) is `33f6c3dd`. #809 leftover observed `Y` on pair segments +> (v2.35.0 / ADR 0278) is `b334b00e`. Leave +> #782/#802/#803/#804/#805/#806/#808/#809 open for independent review. +> Do not squash-merge stacked leftover PRs onto an unprotected leftover +> base. Do not merge #808 without independent APPROVE. Strix on #782 +> failed closed at `Run Strix (quick)`; do not weaken fail-closed on +> `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError` classifier is +> already on ContextualWisdomLab/.github. Copilot review is not +> independent APPROVE. Issues #79 and #87 stay open. #96 does not exist +> on this repository. Only collaborator is `seonghobae`; no independent +> reviewer can be requested from this token. +> +> Next buyer increment on this cycle: leftover expected `E` on +> graphic-display pair segments (ADR 0279 / v2.36.0) delivered locally on +> `feat/leftover-map-segment-expected-v2360`. Caption each +> closest/farthest segment with persisted leftover expected so the +> pair-row `E` badge matches the graphic and `Y − E = R` is +> graphic-auditable. UI-only; no new columns. Missing/non-finite `E` +> omits that leftover expected caption. A finite negative expected is +> shown; do not clamp to nonnegative. Do not invent `E` from `Y` and +> `R` or from plotted coordinates. Do not invent leftover scores. +> Stack onto leftover branch `feat/leftover-map-segment-observed-v2350` +> / #809; 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-30 13:06 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash > of leftover-map coordinates (v2.24.0 / #782). Revert #808 diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index f7b4558b3..e229d1341 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, and pair-segment `Y` to leftover observed, 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`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, and `Y 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, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, and pair segments name persisted leftover observed. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, and pair-segment `E` to leftover expected, 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`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, and `E 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, 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, and pair segments name persisted leftover expected. | `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 0f972aef9..533638914 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.35.0", + "version": "2.36.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 7da06f2b4..0ccaca566 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -13,6 +13,7 @@ const PAIRS: LeftoverPair[] = [ leftover_distance: 0.12, leftover_residual: 0.4, observed_response: 2.4, + expected_response: 2.0, leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, @@ -31,6 +32,7 @@ const PAIRS: LeftoverPair[] = [ leftover_distance: 1.84, leftover_residual: -1.1, observed_response: 0.9, + expected_response: 2.0, leftover_map_reconstruction: -0.95, leftover_map_explained_share: 0.6, leftover_map_unexplained_share: 0.05, @@ -57,7 +59,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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -83,6 +85,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover residual R −1.10")).toBeInTheDocument(); expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); expect(screen.getByLabelText("leftover observed Y 0.90")).toBeInTheDocument(); + expect(screen.getAllByLabelText("leftover expected E 2.00")).toHaveLength(2); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -129,6 +132,7 @@ describe("LeftoverMapPlot", () => { leftover_distance: 0, leftover_residual: 0, observed_response: 0, + expected_response: 0, leftover_map_rank: 0, leftover_map_reconstruction: 0, leftover_map_explained_share: 0, @@ -164,6 +168,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover-map unexplained leftover U 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover residual R 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover observed Y 0.00")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover expected E 0.00")).toBeInTheDocument(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -429,4 +434,36 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover residual R +0.40")).toBeInTheDocument(); expect(screen.getByLabelText("leftover-map unexplained leftover U +0.05")).toBeInTheDocument(); }); + + it("omits leftover expected on a pair segment when E is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/^leftover expected E/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover residual R +0.40")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index f518efb91..1b339f51a 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -14,6 +14,7 @@ import { LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, + LEFTOVER_MAP_PLOT_SEGMENT_EXPECTED, LEFTOVER_MAP_PLOT_SEGMENT_OBSERVED, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL, @@ -58,8 +59,9 @@ function leftoverMapPlotAxisText( * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map * explained leftover share ``e``, leftover-map unexplained leftover * share ``s``, leftover-map cross share ``x``, leftover-map - * unexplained leftover ``U``, leftover residual ``R``, and leftover - * observed ``Y`` so the pair-row badges match the graphic. + * unexplained leftover ``U``, leftover residual ``R``, leftover + * observed ``Y``, and leftover expected ``E`` 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 @@ -68,7 +70,8 @@ function leftoverMapPlotAxisText( * ``x`` is missing or non-finite. Omit that unexplained leftover caption * when ``U`` is missing or non-finite. Omit that leftover residual * caption when ``R`` is missing or non-finite. Omit that leftover observed - * caption when ``Y`` is missing or non-finite. Omit that axis badge when share is + * caption when ``Y`` is missing or non-finite. Omit that leftover expected + * caption when ``E`` 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. @@ -261,6 +264,19 @@ export function LeftoverMapPlot({ {segment.observedLabel} ) : null} + {segment.expectedLabel !== null ? ( + + {segment.expectedLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index 748f644f9..04ad3a73a 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -74,7 +74,8 @@ describe("i18n", () => { "leftover-map unexplained leftover {label}", "leftover residual {label}", "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.", + "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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.", @@ -338,22 +339,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y, 기대 E입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R 与观测 Y。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R、观测 Y 与期望 E。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y、期待 E です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "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, phần dư chưa giải thích s, phần giao x, phần dư chưa giải thích U, phần dư R và Y quan sát. 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, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát và E kỳ vọng. 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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -452,6 +453,16 @@ describe("i18n", () => { expect(tf("leftover observed {label}", { label: "Y 2.40" })).toBe(expected); }); + it.each([ + ["ko", "기대 E 2.00"], + ["zh", "期望 E 2.00"], + ["ja", "期待 E 2.00"], + ["vi", "kỳ vọng E 2.00"], + ] as const)("formats leftover-map segment leftover expected in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover expected {label}", { label: "E 2.00" })).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 6b8ef0a54..68e4ddaeb 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -599,8 +599,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover {label}": "잔여 지도 미설명 잔여 {label}", "leftover residual {label}": "잔여 {label}", "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y, 기대 E입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "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.": @@ -1186,8 +1187,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover {label}": "残差图未解释残差 {label}", "leftover residual {label}": "残差 {label}", "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R 与观测 Y。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R、观测 Y 与期望 E。点击帖子标记打开该帖子。此图不会虚构残差分数。", "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.": @@ -1777,8 +1779,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover {label}": "残差マップ未説明残差 {label}", "leftover residual {label}": "残差 {label}", "leftover observed {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y、期待 E です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "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.": @@ -2368,8 +2371,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map unexplained leftover {label}": "phần dư chưa giải thích {label}", "leftover residual {label}": "phần dư {label}", "leftover observed {label}": "quan sát {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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, phần giao x, phần dư chưa giải thích U, phần dư R và Y quan sát. 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 expected {label}": "kỳ vọng {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát và E kỳ vọng. 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 574fd4115..08a72d742 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -942,6 +942,119 @@ 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 expected on pair segments without inventing a leftover score", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ observed_response: 2.4, expected_response: 2.0 }), + 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, + leftover_map_unexplained: -0.25, + leftover_residual: -1.1, + observed_response: 0.9, + expected_response: 2.0, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.expectedLabel)).toEqual([ + "E 2.00", + "E 2.00", + ]); + expect(layout?.segments[0]?.expectedX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.expectedY).toBeGreaterThan( + layout?.segments[0]?.observedY ?? 0, + ); + }); + + it("omits a leftover expected caption when E is missing or non-finite", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ observed_response: 2.4, expected_response: null }), + pair({ + pair_kind: "farthest", + observed_response: 2.4, + expected_response: 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.expectedLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.observedLabel)).toEqual([ + "Y 2.40", + "Y 2.40", + ]); + }); + + it("does not invent leftover expected from observed and 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, + leftover_map_unexplained: 0.05, + leftover_residual: 0.4, + observed_response: 2.4, + expected_response: 9.9, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.expectedLabel).toBe("E 9.90"); + expect(layout?.segments[0]?.expectedLabel).not.toBe("E 2.00"); + }); + + it("names rank-0 origin leftover expected E 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, + leftover_map_unexplained: 0, + leftover_residual: 0, + observed_response: 0, + expected_response: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.expectedLabel).toBe("E 0.00"); + expect(layout?.segments[0]?.observedLabel).toBe("Y 0.00"); + expect(layout?.segments[0]?.residualLabel).toBe("R 0.00"); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U 0.00"); + 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 6ed932965..8db21295f 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -7,7 +7,8 @@ * leftover-map unexplained leftover share ``s`` (ADR 0274), persisted * leftover-map cross share ``x`` (ADR 0275), persisted leftover-map * unexplained leftover ``U`` (ADR 0276), persisted leftover residual - * ``R`` (ADR 0277), and persisted leftover observed ``Y`` (ADR 0278). + * ``R`` (ADR 0277), persisted leftover observed ``Y`` (ADR 0278), and + * persisted leftover expected ``E`` (ADR 0279). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; @@ -19,12 +20,12 @@ import { formatSignedLeftoverValue, } from "./leftoverMapUnexplained"; import { formatLeftoverMapUnexplainedShare } from "./leftoverMapUnexplainedShare"; -import { formatLeftoverMapObserved } from "./leftoverObservedExpected"; +import { formatLeftoverMapExpected, formatLeftoverMapObserved } from "./leftoverObservedExpected"; import { formatLeftoverMapResidual } from "./leftoverResidual"; 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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, and leftover observed Y. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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}"; @@ -56,6 +57,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL = export const LEFTOVER_MAP_PLOT_SEGMENT_OBSERVED = "leftover observed {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_EXPECTED = + "leftover expected {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -78,6 +82,7 @@ export type LeftoverMapPlottablePair = { leftover_map_unexplained?: number | null; leftover_residual?: number | null; observed_response?: number | null; + expected_response?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -110,6 +115,7 @@ export type LeftoverMapPlotSegment = { unexplainedLeftoverLabel: string | null; residualLabel: string | null; observedLabel: string | null; + expectedLabel: string | null; labelX: number; labelY: number; reconstructionX: number; @@ -126,6 +132,8 @@ export type LeftoverMapPlotSegment = { residualY: number; observedX: number; observedY: number; + expectedX: number; + expectedY: number; }; export type LeftoverMapPlotTick = { @@ -358,6 +366,7 @@ export function layoutLeftoverMapPlot( ); const residualLabel = formatLeftoverMapResidual(pair.leftover_residual); const observedLabel = formatLeftoverMapObserved(pair.observed_response); + const expectedLabel = formatLeftoverMapExpected(pair.expected_response); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -412,6 +421,17 @@ export function layoutLeftoverMapPlot( (unexplainedLeftoverLabel !== null ? 1 : 0) + (residualLabel !== null ? 1 : 0), ); + const expectedY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0) + + (unexplainedShareLabel !== null ? 1 : 0) + + (crossShareLabel !== null ? 1 : 0) + + (unexplainedLeftoverLabel !== null ? 1 : 0) + + (residualLabel !== null ? 1 : 0) + + (observedLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -428,6 +448,7 @@ export function layoutLeftoverMapPlot( unexplainedLeftoverLabel, residualLabel, observedLabel, + expectedLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, @@ -442,6 +463,8 @@ export function layoutLeftoverMapPlot( residualY, observedX: labelPosition.labelX, observedY, + expectedX: labelPosition.labelX, + expectedY, ...labelPosition, }); } diff --git a/frontend/src/leftoverObservedExpected.test.ts b/frontend/src/leftoverObservedExpected.test.ts index 58f9c0e60..52ead7f6d 100644 --- a/frontend/src/leftoverObservedExpected.test.ts +++ b/frontend/src/leftoverObservedExpected.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { formatLeftoverMapObserved, formatLeftoverObservedExpected } from "./leftoverObservedExpected"; +import { + formatLeftoverMapExpected, + formatLeftoverMapObserved, + formatLeftoverObservedExpected, +} from "./leftoverObservedExpected"; describe("formatLeftoverObservedExpected", () => { it("names observed Y and expected E without inventing a leftover score", () => { @@ -31,3 +35,19 @@ describe("formatLeftoverMapObserved", () => { expect(formatLeftoverMapObserved(Number.POSITIVE_INFINITY)).toBeNull(); }); }); + +describe("formatLeftoverMapExpected", () => { + it("names leftover expected without inventing a leftover score", () => { + expect(formatLeftoverMapExpected(2.0)).toBe("E 2.00"); + expect(formatLeftoverMapExpected(0.9)).toBe("E 0.90"); + expect(formatLeftoverMapExpected(0)).toBe("E 0.00"); + expect(formatLeftoverMapExpected(-1.1)).toBe("E -1.10"); + }); + + it("omits the badge when leftover expected is missing or non-finite", () => { + expect(formatLeftoverMapExpected(null)).toBeNull(); + expect(formatLeftoverMapExpected(undefined)).toBeNull(); + expect(formatLeftoverMapExpected(Number.NaN)).toBeNull(); + expect(formatLeftoverMapExpected(Number.POSITIVE_INFINITY)).toBeNull(); + }); +}); diff --git a/frontend/src/leftoverObservedExpected.ts b/frontend/src/leftoverObservedExpected.ts index 89d0a5132..86612b681 100644 --- a/frontend/src/leftoverObservedExpected.ts +++ b/frontend/src/leftoverObservedExpected.ts @@ -21,3 +21,10 @@ export function formatLeftoverMapObserved(value: number | null | undefined): str } return `Y ${value.toFixed(2)}`; } + +export function formatLeftoverMapExpected(value: number | null | undefined): string | null { + if (value == null || !Number.isFinite(value)) { + return null; + } + return `E ${value.toFixed(2)}`; +} diff --git a/pyproject.toml b/pyproject.toml index 1a8eaabc0..65363463c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.35.0" +version = "2.36.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 ee8b86fda..1777a12ef 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.33.0" +version = "2.36.0" source = { editable = "." } dependencies = [ { name = "certifi" }, From e626a1d0770208d6f821e06542091aa2ead87f25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:23:22 +0000 Subject: [PATCH 07/23] feat(reports): leftover-map rank on pair segments (v2.37.0) Caption leftover-map graphic-display pair segments with persisted leftover-map rank (ADR 0280). After make seed, closest and farthest leftover pairs sit above the member list with pair segments that match rank on the pair row; click a post marker or a pair button opens that post. A missing, negative, or non-integer rank omits that leftover-map rank caption. Rank 0 is shown when that persisted rank is a non-negative integer; do not hide a rank-0 collapse. Do not invent rank from plotted coordinates, leftover-map distance, or the count of unused axes. Never invent a leftover score or a theta. UI-only; no new columns. --- AGENTS.md | 8 +- ARCHITECTURE.md | 5 +- .../2.37.0-leftover-map-segment-rank.md | 20 ++ CHANGELOG.md | 19 ++ CLAUDE.md | 2 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 7 +- docs/adr/0049-leftover-pair-report-ui.md | 8 +- docs/adr/0268-leftover-map-graphic-display.md | 7 +- docs/adr/0280-leftover-map-segment-rank.md | 172 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 43 +++++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- .../src/components/LeftoverMapPlot.test.tsx | 39 +++- frontend/src/components/LeftoverMapPlot.tsx | 21 ++- frontend/src/i18n.test.ts | 23 ++- frontend/src/i18n.ts | 20 +- frontend/src/leftoverMapPlotLayout.test.ts | 118 ++++++++++++ frontend/src/leftoverMapPlotLayout.ts | 31 +++- pyproject.toml | 2 +- uv.lock | 2 +- 20 files changed, 516 insertions(+), 37 deletions(-) create mode 100644 CHANGELOG.d/2.37.0-leftover-map-segment-rank.md create mode 100644 docs/adr/0280-leftover-map-segment-rank.md diff --git a/AGENTS.md b/AGENTS.md index d9c6035ac..7ae4a5426 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) 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) 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 @@ -333,7 +333,11 @@ coordinates. ADR 0279 captions leftover-map pair segments with persisted leftover expected `E` so the pair-row `E` badge matches the graphic; a missing or non-finite `E` omits that leftover expected caption and does not invent `E` from `Y` and `R` or from plotted -coordinates. When `R`, `R̂`, `U`, `x`, +coordinates. ADR 0280 captions leftover-map pair +segments with persisted leftover-map rank so the pair-row `rank` badge +matches the graphic; a missing, negative, or non-integer rank omits that +leftover-map rank caption and does not invent rank from plotted +coordinates, leftover-map distance, or the count of unused axes. 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 finite, `U + R̂ = R`. When coordinates, diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e3eb9a843..4827ec7df 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280) 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 @@ -657,7 +657,8 @@ cross share `x`. ADR 0276 captions leftover-map pair segments with persisted lef unexplained leftover `U`. ADR 0277 captions leftover-map pair segments with persisted leftover residual `R`. ADR 0278 captions leftover-map pair segments with persisted leftover observed `Y`. ADR 0279 captions leftover-map pair segments with persisted leftover -expected `E`. Click a post marker or a pair button +expected `E`. ADR 0280 captions leftover-map pair segments with persisted leftover-map +rank. 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.37.0-leftover-map-segment-rank.md b/CHANGELOG.d/2.37.0-leftover-map-segment-rank.md new file mode 100644 index 000000000..ca9d3351a --- /dev/null +++ b/CHANGELOG.d/2.37.0-leftover-map-segment-rank.md @@ -0,0 +1,20 @@ +## 2.37.0 — Leftover-map rank on graphic-display pair segments + +- Caption leftover-map graphic-display pair segments with persisted + leftover-map rank (ADR 0280). 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, pair segments that match + `2R̂U/R²` on the pair row, pair segments that match `U` on the pair + row, pair segments that match `R` on the pair row, pair segments that + match `Y` on the pair row, pair segments that match `E` on the pair + row, and pair segments that match leftover-map rank on the pair row; + click a post marker or a pair button opens that post. A missing, + negative, or non-integer rank omits that leftover-map rank caption. + Rank `0` is shown when that persisted rank is a non-negative integer; + do not hide a rank-0 collapse. Do not invent rank from plotted + coordinates, leftover-map distance, or the count of unused axes. + Never invent a leftover score. Never invent a theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index 61197f9e1..e35505006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ 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 rank (ADR 0280 / v2.37.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, pair segments that match `2R̂U/R²` on the pair row, pair segments + that match `U` on the pair row, pair segments that match `R` on the + pair row, pair segments that match `Y` on the pair row, pair segments + that match `E` on the pair row, and pair segments that match leftover-map + rank on the pair row; click a post marker or a pair button opens that + post. A missing, negative, or non-integer rank omits that leftover-map + rank caption. Rank `0` is shown when that persisted rank is a + non-negative integer; do not hide a rank-0 collapse. Do not invent rank + from plotted coordinates, leftover-map distance, or the count of unused + axes. 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 expected `E[Y|θ, item]` (ADR 0279 / v2.36.0). After `make seed`, closest and farthest leftover pairs sit diff --git a/CLAUDE.md b/CLAUDE.md index 8f45800f9..75cc2391a 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), 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), 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 4788e7302..032fe8e0e 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -22,7 +22,8 @@ [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); [ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); [ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments); -[ADR 0279](0279-leftover-map-segment-expected.md) (leftover expected E on pair segments) +[ADR 0279](0279-leftover-map-segment-expected.md) (leftover expected E on pair segments); +[ADR 0280](0280-leftover-map-segment-rank.md) (leftover-map rank on pair segments) ## Context @@ -94,7 +95,9 @@ unexplained leftover `U`; it adds no columns. ADR 0277 captions leftover-map pair segments with persisted leftover residual `R`; it adds no columns. ADR 0278 captions leftover-map pair segments with persisted leftover observed `Y`; it adds no columns. ADR 0279 captions leftover-map pair -segments with persisted leftover expected `E`; it adds no columns. +segments with persisted leftover expected `E`; it adds no columns. ADR 0280 +captions leftover-map pair segments with persisted leftover-map rank; 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 d8e5147a6..bc59a5026 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -23,7 +23,8 @@ [ADR 0276](0276-leftover-map-segment-unexplained-leftover.md) (leftover-map unexplained leftover on pair segments); [ADR 0277](0277-leftover-map-segment-residual.md) (leftover residual on pair segments); [ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments); -[ADR 0279](0279-leftover-map-segment-expected.md) (leftover expected E on pair segments) +[ADR 0279](0279-leftover-map-segment-expected.md) (leftover expected E on pair segments); +[ADR 0280](0280-leftover-map-segment-rank.md) (leftover-map rank on pair segments) ## Context @@ -59,7 +60,8 @@ cross share `x` (ADR 0275). Pair segments name persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name persisted leftover residual `R` (ADR 0277). Pair segments name persisted leftover observed `Y` (ADR 0278). Pair segments name persisted leftover expected -`E` (ADR 0279). The pair renders every available finite measurement. +`E` (ADR 0279). Pair segments name persisted leftover-map rank +(ADR 0280). 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 @@ -108,6 +110,8 @@ Leftover observed Y on pair segments is [ADR 0278](0278-leftover-map-segment-observed.md). Leftover expected E on pair segments is [ADR 0279](0279-leftover-map-segment-expected.md). +Leftover-map rank on pair segments is +[ADR 0280](0280-leftover-map-segment-rank.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/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index 48a6de6aa..c91dc5b9c 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -24,7 +24,9 @@ [ADR 0278](0278-leftover-map-segment-observed.md) (leftover observed Y on pair segments); [ADR 0279](0279-leftover-map-segment-expected.md) -(leftover expected E on pair segments) +(leftover expected E on pair segments); +[ADR 0280](0280-leftover-map-segment-rank.md) +(leftover-map rank on pair segments) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map @@ -107,7 +109,8 @@ persisted leftover-map cross share `x` (ADR 0275). Pair segments name persisted leftover-map unexplained leftover `U` (ADR 0276). Pair segments name persisted leftover residual `R` (ADR 0277). Pair segments name persisted leftover observed `Y` (ADR 0278). Pair segments name -persisted leftover expected `E` (ADR 0279). When coordinates, reconstruction, and +persisted leftover expected `E` (ADR 0279). Pair segments name +persisted leftover-map rank (ADR 0280). 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/0280-leftover-map-segment-rank.md b/docs/adr/0280-leftover-map-segment-rank.md new file mode 100644 index 000000000..0781bb0ff --- /dev/null +++ b/docs/adr/0280-leftover-map-segment-rank.md @@ -0,0 +1,172 @@ +# ADR 0280 — Name leftover-map rank on graphic-display pair segments + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover-map rank +([ADR 0164](0164-leftover-map-rank.md)). Independent of leftover expected on +pair segments ([ADR 0279](0279-leftover-map-segment-expected.md)), leftover +observed on pair segments +([ADR 0278](0278-leftover-map-segment-observed.md)), leftover residual on pair +segments ([ADR 0277](0277-leftover-map-segment-residual.md)), leftover-map +unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments +([ADR 0275](0275-leftover-map-segment-cross-share.md)), leftover-map +unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover +observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)), +leftover-map explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover +persistence ([ADR 0182](0182-leftover-map-unexplained.md)), and leftover-map axis +share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0164 already persists leftover-map rank — the number of Gabriel +singular values above the leftover singular floor — on leftover pair +rows. A rank-0 residual still emits a stable closest/farthest pair so +`make seed` is not empty; the stored distance is then zero, not a +fabricated interaction. ADR 0279 already names persisted leftover +expected `E` on the leftover-map graphic-display pair segment. The +segment still has no rank label, so a buyer who reads `rank 0` on the +pair row cannot match that collapse to the graphic pair. Hiding rank on +the segment lets a rank-0 origin cell with `d 0.00` and `E 0.00` be +read as a two-axis leftover map even after the pair-row badge names no +leftover structure. When leftover-map rank is a finite non-negative +integer, the graphic must name the same persisted rank the pair row +already shows. + +This increment names leftover-map graphic-display pair segments with +persisted leftover-map rank. It does not add columns. It does not +recompute rank from plotted coordinates, from leftover-map distance, or +from the count of unused axes. 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 **0280** so it does not +collide with leftover expected on pair segments (0279), leftover observed +on pair segments (0278), leftover residual on pair segments (0277), +leftover-map unexplained leftover on pair segments (0276), leftover-map +cross share on pair segments (0275), leftover-map unexplained leftover +share on pair segments (0274), leftover-map explained leftover share on +pair segments (0273), leftover-map reconstruction on pair segments +(0272), leftover-map distance on pair segments (0271), leftover-map +coordinate ticks (0270), leftover-map axis share on the graphic display +(0269), leftover-map graphic display (0268), leftover-map coordinates +(0267 / migration 0245), leftover-map explained leftover share +persistence (0266 / migration 0244), leftover-map unexplained leftover +share persistence (0233 / migration 0233), leftover-map reconstruction +persistence (0201 / migration 0206), leftover-map cross share persistence +(0185), leftover-map unexplained leftover persistence (0182), leftover +residual disclosure (0162), leftover observed `Y` / expected `E` +persistence (0163), leftover-map rank persistence (0164), 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 persisted leftover-map rank as `rank n`, using the +same rank formatter as the pair-row `rank` badge. A missing rank, a +negative rank, or a non-integer rank omits that leftover-map rank +caption and keeps the connecting line and any leftover-map distance, +reconstruction, explained leftover share, unexplained leftover share, +leftover-map cross share, unexplained leftover, leftover residual, +leftover observed, or leftover expected caption. Rank-0 origin cells +still name `rank 0` when that persisted rank is a non-negative integer. +Do not invent rank from plotted coordinates, from leftover-map distance, +or from the count of unused axes. 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 rank 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`, persisted leftover-map cross share `x`, persisted leftover-map +unexplained leftover `U`, persisted leftover residual `R`, persisted +leftover observed `Y`, persisted leftover expected `E`, and persisted +leftover-map rank; 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, still name `2R̂U/R² 0.00` when that leftover-map +cross share is persisted, still name `U 0.00` when that unexplained leftover +is persisted, still name `R 0.00` when that leftover residual is persisted, +still name `Y 0.00` when that leftover observed is persisted, still name +`E 0.00` when that leftover expected is persisted, and still name `rank 0` +when that leftover-map rank is persisted. When `Y`, `E`, and `R` are all +finite, `Y − E = R` remains the same identity already persisted by ADR +0162 and ADR 0163. When `R`, `R̂`, and `U` are all finite, `U + R̂ = R` +remains the same identity already persisted by ADR 0162, ADR 0182, and +ADR 0201. 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-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 persistence, leftover-map inner product, +leftover-map cosine, leftover-map length, leftover-map reconstruction +persistence, leftover-map unexplained leftover persistence, 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, +leftover-map unexplained leftover share on pair segments, leftover-map +cross share on pair segments, leftover-map unexplained leftover on pair +segments, leftover residual on pair segments, leftover observed on pair +segments, and leftover expected 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 rank is the number of Gabriel +singular values above the leftover singular floor on the complete-case +residual rectangle. Rank `0` is an origin collapse, not a two-axis map.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b7fc0f4bf..7b83aae3f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,48 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-30 19:08 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash +> of leftover-map coordinates (v2.24.0 / #782). Revert #808 +> (`revert-pr782-unauthorized` @ `1af3e53e`) restores `main` toward +> `fc13acaa` (v2.23.0) and still needs independent APPROVE. GitHub +> writes work (comment/close/create-PR/push). Token still has empty +> `X-OAuth-Scopes`; repo permission is ADMIN. Open leftover stack +> still lacks independent APPROVE. Do not self-approve. +> #782 leftover-map coordinates through pair-segment distance +> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained +> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29 +> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair +> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross +> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map +> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is +> `e3fdfd74`. #806 leftover residual `R` on pair segments (v2.34.0 / +> ADR 0277) is `33f6c3dd`. #809 leftover observed `Y` on pair segments +> (v2.35.0 / ADR 0278) is `b334b00e`. #810 leftover expected `E` on pair +> segments (v2.36.0 / ADR 0279) is `6e37757a`. Leave +> #782/#802/#803/#804/#805/#806/#808/#809/#810 open for independent review. +> Do not squash-merge stacked leftover PRs onto an unprotected leftover +> base. Do not merge #808 without independent APPROVE. Strix on #782 +> failed closed at `Run Strix (quick)`; do not weaken fail-closed on +> `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError` classifier is +> already on ContextualWisdomLab/.github. Copilot review is not +> independent APPROVE. Issues #79 and #87 stay open. #96 is already +> closed. Only collaborator is `seonghobae`; no independent reviewer can +> be requested from this token. +> +> Next buyer increment on this cycle: leftover-map rank on +> graphic-display pair segments (ADR 0280 / v2.37.0) delivered locally on +> `feat/leftover-map-segment-rank-v2370`. Caption each closest/farthest +> segment with persisted leftover-map rank so the pair-row `rank` badge +> matches the graphic and a rank-0 origin is not read as a two-axis map. +> UI-only; no new columns. Missing, negative, or non-integer rank omits +> that leftover-map rank caption. Rank `0` is shown when persisted. Do +> not invent rank from plotted coordinates, leftover-map distance, or +> the count of unused axes. Do not invent leftover scores. Stack onto +> leftover branch `feat/leftover-map-segment-expected-v2360` / #810; +> 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-30 16:09 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash > of leftover-map coordinates (v2.24.0 / #782). Revert #808 diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index e229d1341..2c1d14bfd 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, and pair-segment `E` to leftover expected, 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`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, and `E 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, 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, and pair segments name persisted leftover expected. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map 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`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, and missing share that keeps existing leftover-map axis text. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `--color-primary`, `--color-palette-blue-mid` | +| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. | `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 533638914..efb8af9b2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.36.0", + "version": "2.37.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 0ccaca566..798f582a6 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -19,6 +19,7 @@ const PAIRS: LeftoverPair[] = [ leftover_map_unexplained_share: 0.02, leftover_map_cross_share: 0.12, leftover_map_unexplained: 0.05, + leftover_map_rank: 1, leftover_map_person_axis_1: 0.5, leftover_map_person_axis_2: 0.1, leftover_map_item_axis_1: 0.5, @@ -38,6 +39,7 @@ const PAIRS: LeftoverPair[] = [ leftover_map_unexplained_share: 0.05, leftover_map_cross_share: -0.24, leftover_map_unexplained: -0.25, + leftover_map_rank: 1, leftover_map_person_axis_1: 0.9, leftover_map_person_axis_2: 0.8, leftover_map_item_axis_1: -0.7, @@ -59,7 +61,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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -86,6 +88,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); expect(screen.getByLabelText("leftover observed Y 0.90")).toBeInTheDocument(); expect(screen.getAllByLabelText("leftover expected E 2.00")).toHaveLength(2); + expect(screen.getAllByLabelText("leftover-map rank rank 1")).toHaveLength(2); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -169,6 +172,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover residual R 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover observed Y 0.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover expected E 0.00")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover-map rank rank 0")).toBeInTheDocument(); }); it("captions leftover-map axes with persisted leftover-map axis share", () => { @@ -466,4 +470,37 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); expect(screen.getByLabelText("leftover residual R +0.40")).toBeInTheDocument(); }); + + it("omits leftover-map rank on a pair segment when rank is missing", () => { + render( + , + ); + expect(screen.queryByLabelText(/^leftover-map rank/)).not.toBeInTheDocument(); + expect(screen.getByLabelText("leftover expected E 2.00")).toBeInTheDocument(); + expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 1b339f51a..e5f238211 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -16,6 +16,7 @@ import { LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_EXPECTED, LEFTOVER_MAP_PLOT_SEGMENT_OBSERVED, + LEFTOVER_MAP_PLOT_SEGMENT_RANK, LEFTOVER_MAP_PLOT_SEGMENT_RECONSTRUCTION, LEFTOVER_MAP_PLOT_SEGMENT_RESIDUAL, LEFTOVER_MAP_PLOT_SEGMENT_UNEXPLAINED, @@ -60,8 +61,8 @@ function leftoverMapPlotAxisText( * explained leftover share ``e``, leftover-map unexplained leftover * share ``s``, leftover-map cross share ``x``, leftover-map * unexplained leftover ``U``, leftover residual ``R``, leftover - * observed ``Y``, and leftover expected ``E`` so the pair-row badges - * match the graphic. + * observed ``Y``, leftover expected ``E``, and leftover-map rank 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 @@ -71,7 +72,8 @@ function leftoverMapPlotAxisText( * when ``U`` is missing or non-finite. Omit that leftover residual * caption when ``R`` is missing or non-finite. Omit that leftover observed * caption when ``Y`` is missing or non-finite. Omit that leftover expected - * caption when ``E`` is missing or non-finite. Omit that axis badge when share is + * caption when ``E`` is missing or non-finite. Omit that leftover-map rank + * caption when rank is missing, negative, or not an integer. 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. @@ -277,6 +279,19 @@ export function LeftoverMapPlot({ {segment.expectedLabel} ) : null} + {segment.rankLabel !== null ? ( + + {segment.rankLabel} + + ) : null} ))} {layout.items.map((marker) => ( diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index 04ad3a73a..e1b4b7806 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -75,7 +75,8 @@ describe("i18n", () => { "leftover residual {label}", "leftover observed {label}", "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.", + "leftover-map rank {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. 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.", @@ -339,22 +340,22 @@ describe("i18n", () => { it.each([ [ "ko", - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y, 기대 E입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y, 기대 E, 잔여 지도 순위입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", ], - ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R、观测 Y 与期望 E。点击帖子标记打开该帖子。此图不会虚构残差分数。"], + ["zh", "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R、观测 Y、期望 E 与残差图秩。点击帖子标记打开该帖子。此图不会虚构残差分数。"], [ "ja", - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y、期待 E です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y、期待 E、残差マップ階数です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", ], [ "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, phần dư chưa giải thích s, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát và E kỳ vọng. 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, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát, E kỳ vọng và hạng bản đồ phần dư. 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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBe(expected); }); @@ -463,6 +464,16 @@ describe("i18n", () => { expect(tf("leftover expected {label}", { label: "E 2.00" })).toBe(expected); }); + it.each([ + ["ko", "잔여 지도 순위 rank 1"], + ["zh", "残差图秩 rank 1"], + ["ja", "残差マップ階数 rank 1"], + ["vi", "hạng bản đồ phần dư rank 1"], + ] as const)("formats leftover-map segment leftover-map rank in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover-map rank {label}", { label: "rank 1" })).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 축 1 (82%)"], ["zh", "残差图轴 1 (82%)"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 68e4ddaeb..b2ae5d7f9 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -600,8 +600,9 @@ const TRANSLATIONS: Partial>> = { "leftover residual {label}": "잔여 {label}", "leftover observed {label}": "관측 {label}", "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y, 기대 E입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", + "leftover-map rank {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 주효과 이후 잔여 지도입니다. 축 눈금은 저장된 잔여 지도 좌표입니다. 쌍 선분은 잔여 지도 거리 d, 잔여 지도 재구성 R̂, 잔여 지도 설명 잔여 점유율 e, 잔여 지도 미설명 잔여 점유율 s, 잔여 지도 교차 점유율 x, 잔여 지도 미설명 잔여 U, 잔여 R, 관측 Y, 기대 E, 잔여 지도 순위입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "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.": @@ -1188,8 +1189,9 @@ const TRANSLATIONS: Partial>> = { "leftover residual {label}": "残差 {label}", "leftover observed {label}": "观测 {label}", "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R、观测 Y 与期望 E。点击帖子标记打开该帖子。此图不会虚构残差分数。", + "leftover-map rank {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT 主效应后的残差图。轴刻度标出已保存的残差图坐标。配对线段标出残差图距离 d、残差图重建 R̂、残差图已解释残差份额 e、残差图未解释残差份额 s、残差图交叉份额 x、残差图未解释残差 U、残差 R、观测 Y、期望 E 与残差图秩。点击帖子标记打开该帖子。此图不会虚构残差分数。", "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.": @@ -1780,8 +1782,9 @@ const TRANSLATIONS: Partial>> = { "leftover residual {label}": "残差 {label}", "leftover observed {label}": "観測 {label}", "leftover expected {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. Click a post marker to open that post. The plot does not invent a leftover score.": - "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y、期待 E です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", + "leftover-map rank {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score.": + "IRT主効果後の残差マップです。軸目盛は保存済みの残差マップ座標です。ペア線分は残差マップ距離 d、残差マップ再構成 R̂、残差マップ説明済み残差割合 e、残差マップ未説明残差割合 s、残差マップ交差割合 x、残差マップ未説明残差 U、残差 R、観測 Y、期待 E、残差マップ階数です。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "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.": @@ -2372,8 +2375,9 @@ const TRANSLATIONS: Partial>> = { "leftover residual {label}": "phần dư {label}", "leftover observed {label}": "quan sát {label}", "leftover expected {label}": "kỳ vọng {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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát và E kỳ vọng. 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 rank {label}": "hạng 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. 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, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát, E kỳ vọng và hạng bản đồ phần dư. 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 08a72d742..9421a6e5f 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -1055,6 +1055,124 @@ 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 rank on pair segments without inventing leftover structure", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ leftover_map_rank: 1, observed_response: 2.4, expected_response: 2.0 }), + 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, + leftover_map_unexplained: -0.25, + leftover_residual: -1.1, + observed_response: 0.9, + expected_response: 2.0, + leftover_map_rank: 1, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.rankLabel)).toEqual([ + "rank 1", + "rank 1", + ]); + expect(layout?.segments[0]?.rankX).toBeCloseTo(layout?.segments[0]?.labelX ?? 0, 5); + expect(layout?.segments[0]?.rankY).toBeGreaterThan( + layout?.segments[0]?.expectedY ?? 0, + ); + }); + + it("omits a leftover-map rank caption when rank is missing or not a non-negative integer", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ leftover_map_rank: null, expected_response: 2.0 }), + pair({ + pair_kind: "farthest", + leftover_map_rank: Number.NaN, + leftover_map_item_axis_1: -0.7, + leftover_map_item_axis_2: -0.4, + criterion_code: "negative_sentiment", + expected_response: 2.0, + }), + ], + criterionLabel, + ); + expect(layout?.segments.map((segment) => segment.rankLabel)).toEqual([null, null]); + expect(layout?.segments.map((segment) => segment.expectedLabel)).toEqual([ + "E 2.00", + "E 2.00", + ]); + }); + + it("does not invent leftover-map rank from plotted coordinates", () => { + 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, + leftover_map_unexplained: 0.05, + leftover_residual: 0.4, + observed_response: 2.4, + expected_response: 2.0, + leftover_map_rank: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.rankLabel).toBe("rank 0"); + expect(layout?.segments[0]?.rankLabel).not.toBe("rank 2"); + expect(layout?.segments[0]?.rankLabel).not.toBe("rank 1"); + }); + + it("names rank-0 origin leftover-map rank 0 when that persisted value is a non-negative integer", () => { + 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, + leftover_map_unexplained: 0, + leftover_residual: 0, + observed_response: 0, + expected_response: 0, + leftover_map_rank: 0, + }), + ], + criterionLabel, + ); + expect(layout?.segments[0]?.rankLabel).toBe("rank 0"); + expect(layout?.segments[0]?.expectedLabel).toBe("E 0.00"); + expect(layout?.segments[0]?.observedLabel).toBe("Y 0.00"); + expect(layout?.segments[0]?.residualLabel).toBe("R 0.00"); + expect(layout?.segments[0]?.unexplainedLeftoverLabel).toBe("U 0.00"); + 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 8db21295f..2699bc74a 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -7,13 +7,15 @@ * leftover-map unexplained leftover share ``s`` (ADR 0274), persisted * leftover-map cross share ``x`` (ADR 0275), persisted leftover-map * unexplained leftover ``U`` (ADR 0276), persisted leftover residual - * ``R`` (ADR 0277), persisted leftover observed ``Y`` (ADR 0278), and - * persisted leftover expected ``E`` (ADR 0279). + * ``R`` (ADR 0277), persisted leftover observed ``Y`` (ADR 0278), + * persisted leftover expected ``E`` (ADR 0279), and persisted leftover-map + * rank (ADR 0280). */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; import { formatLeftoverMapExplainedShare } from "./leftoverMapExplainedShare"; +import { formatLeftoverMapRank } from "./leftoverMapRank"; import { formatLeftoverMapReconstruction } from "./leftoverMapReconstruction"; import { formatLeftoverMapUnexplained, @@ -25,7 +27,7 @@ import { formatLeftoverMapResidual } from "./leftoverResidual"; 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, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, and leftover expected E. 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. 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}"; @@ -60,6 +62,9 @@ export const LEFTOVER_MAP_PLOT_SEGMENT_OBSERVED = export const LEFTOVER_MAP_PLOT_SEGMENT_EXPECTED = "leftover expected {label}"; +export const LEFTOVER_MAP_PLOT_SEGMENT_RANK = + "leftover-map rank {label}"; + export const PLOT_WIDTH = 480; export const PLOT_HEIGHT = 320; export const PLOT_PADDING = 40; @@ -83,6 +88,7 @@ export type LeftoverMapPlottablePair = { leftover_residual?: number | null; observed_response?: number | null; expected_response?: number | null; + leftover_map_rank?: number | null; leftover_map_person_axis_1?: number | null; leftover_map_person_axis_2?: number | null; leftover_map_item_axis_1?: number | null; @@ -116,6 +122,7 @@ export type LeftoverMapPlotSegment = { residualLabel: string | null; observedLabel: string | null; expectedLabel: string | null; + rankLabel: string | null; labelX: number; labelY: number; reconstructionX: number; @@ -134,6 +141,8 @@ export type LeftoverMapPlotSegment = { observedY: number; expectedX: number; expectedY: number; + rankX: number; + rankY: number; }; export type LeftoverMapPlotTick = { @@ -367,6 +376,7 @@ export function layoutLeftoverMapPlot( const residualLabel = formatLeftoverMapResidual(pair.leftover_residual); const observedLabel = formatLeftoverMapObserved(pair.observed_response); const expectedLabel = formatLeftoverMapExpected(pair.expected_response); + const rankLabel = formatLeftoverMapRank(pair.leftover_map_rank); const labelPosition = leftoverMapSegmentLabelPosition( personPos.x, personPos.y, @@ -432,6 +442,18 @@ export function layoutLeftoverMapPlot( (residualLabel !== null ? 1 : 0) + (observedLabel !== null ? 1 : 0), ); + const rankY = leftoverMapStackedCaptionY( + labelPosition.labelY, + (distanceLabel !== null ? 1 : 0) + + (reconstructionLabel !== null ? 1 : 0) + + (explainedShareLabel !== null ? 1 : 0) + + (unexplainedShareLabel !== null ? 1 : 0) + + (crossShareLabel !== null ? 1 : 0) + + (unexplainedLeftoverLabel !== null ? 1 : 0) + + (residualLabel !== null ? 1 : 0) + + (observedLabel !== null ? 1 : 0) + + (expectedLabel !== null ? 1 : 0), + ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", postId: pair.post_id, @@ -449,6 +471,7 @@ export function layoutLeftoverMapPlot( residualLabel, observedLabel, expectedLabel, + rankLabel, reconstructionX: labelPosition.labelX, reconstructionY, explainedShareX: labelPosition.labelX, @@ -465,6 +488,8 @@ export function layoutLeftoverMapPlot( observedY, expectedX: labelPosition.labelX, expectedY, + rankX: labelPosition.labelX, + rankY, ...labelPosition, }); } diff --git a/pyproject.toml b/pyproject.toml index 65363463c..cd9d2f8f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.36.0" +version = "2.37.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 1777a12ef..0e7d2ce9d 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.36.0" +version = "2.37.0" source = { editable = "." } dependencies = [ { name = "certifi" }, From 8efa8e3e083141bfab4d32634ab9b5eeb62770fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:30:01 +0900 Subject: [PATCH 08/23] test(reports): reproduce descendant layout regressions --- .../leftoverMapPlotDescendantLayout.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 frontend/src/leftoverMapPlotDescendantLayout.test.ts diff --git a/frontend/src/leftoverMapPlotDescendantLayout.test.ts b/frontend/src/leftoverMapPlotDescendantLayout.test.ts new file mode 100644 index 000000000..a965db6e0 --- /dev/null +++ b/frontend/src/leftoverMapPlotDescendantLayout.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { layoutLeftoverMapPlot, type LeftoverMapPlottablePair } from "./leftoverMapPlotLayout"; + +function extendedPair( + overrides: Partial = {}, +): LeftoverMapPlottablePair { + return { + pair_kind: "closest", + post_id: "post-descendant-layout", + post_title: "Descendant layout post", + criterion_code: "sales_lead_quality", + leftover_distance: 0.12, + leftover_map_reconstruction: 0.25, + leftover_map_explained_share: 0.76, + leftover_map_unexplained_share: 0.24, + leftover_map_cross_share: -0.10, + leftover_map_unexplained: 0.50, + leftover_residual: 0.75, + observed_response: 2.40, + expected_response: 2.00, + leftover_map_rank: 0, + leftover_map_person_axis_1: -1, + leftover_map_person_axis_2: -1, + leftover_map_item_axis_1: 1, + leftover_map_item_axis_2: -1, + ...overrides, + }; +} + +describe("leftover-map descendant layout inherits #802 bounds", () => { + it("preserves distinct persisted ticks even when their formatted labels round identically", () => { + const layout = layoutLeftoverMapPlot( + [ + extendedPair({ + leftover_map_person_axis_1: 0.001, + leftover_map_item_axis_1: 0.004, + }), + ], + (criterionCode) => criterionCode, + ); + + const axis1 = layout?.ticks.filter((tick) => tick.axis === 1); + expect(axis1?.map((tick) => tick.value)).toEqual([0, 0.001, 0.004]); + expect(axis1?.map((tick) => tick.label)).toEqual(["0.00", "+0.00", "+0.00"]); + }); + + it("keeps all ten persisted captions inside an 80px canvas", () => { + const layout = layoutLeftoverMapPlot( + [extendedPair()], + (criterionCode) => criterionCode, + { height: 80 }, + ); + + expect(layout).not.toBeNull(); + expect(layout?.height).toBe(80); + const segment = layout?.segments[0]; + expect(segment).toBeDefined(); + expect(segment?.labelY).toBeGreaterThanOrEqual(12); + expect(segment?.reconstructionY).toBeGreaterThanOrEqual(segment?.labelY ?? 0); + expect(segment?.explainedShareY).toBeGreaterThanOrEqual(segment?.reconstructionY ?? 0); + expect(segment?.unexplainedShareY).toBeGreaterThanOrEqual(segment?.explainedShareY ?? 0); + expect(segment?.crossShareY).toBeGreaterThanOrEqual(segment?.unexplainedShareY ?? 0); + expect(segment?.unexplainedLeftoverY).toBeGreaterThanOrEqual(segment?.crossShareY ?? 0); + expect(segment?.residualY).toBeGreaterThanOrEqual(segment?.unexplainedLeftoverY ?? 0); + expect(segment?.observedY).toBeGreaterThanOrEqual(segment?.residualY ?? 0); + expect(segment?.expectedY).toBeGreaterThanOrEqual(segment?.observedY ?? 0); + expect(segment?.rankY).toBeGreaterThanOrEqual(segment?.expectedY ?? 0); + expect(segment?.rankY).toBeLessThanOrEqual(76); + }); +}); From 66bccfbc670188ce46e22b0f0ec345942a5e50b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:30:56 +0900 Subject: [PATCH 09/23] fix(reports): carry parent plot bounds into rank descendant --- frontend/src/leftoverMapPlotLayout.ts | 103 +++++++++++++++++++++----- 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 2699bc74a..cb1116c16 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -72,7 +72,10 @@ export const PLOT_TICK_LENGTH = 6; const UNIT_DISPLAY_SPAN = 2; const COLLAPSED_SPAN = 1e-12; const COINCIDENT_LABEL_OFFSET = 14; +const SEGMENT_LABEL_OFFSET = 24; const RECONSTRUCTION_LABEL_OFFSET = 12; +const SEGMENT_LABEL_TOP_INSET = 12; +const SEGMENT_LABEL_BOTTOM_INSET = 4; export type LeftoverMapPlottablePair = { pair_kind: LeftoverPair["pair_kind"]; @@ -211,17 +214,17 @@ function toSvg( } function uniqueCoordinateTicks(values: number[]): { value: number; label: string }[] { - const byLabel = new Map(); + const seen = new Set(); + const ticks: { value: number; label: string }[] = []; for (const value of values) { const label = formatSignedLeftoverValue(value); - if (label === null) { + if (label === null || seen.has(value)) { continue; } - if (!byLabel.has(label)) { - byLabel.set(label, value); - } + seen.add(value); + ticks.push({ value, label }); } - return [...byLabel.entries()].map(([label, value]) => ({ value, label })); + return ticks; } function leftoverMapCoordinateTicks( @@ -268,14 +271,43 @@ function leftoverMapSegmentLabelPosition( y2: number, ): { labelX: number; labelY: number } { const coincident = Math.abs(x1 - x2) < 0.01 && Math.abs(y1 - y2) < 0.01; + const midpointX = (x1 + x2) / 2; + const midpointY = (y1 + y2) / 2; + if (coincident) { + return { + labelX: midpointX, + labelY: midpointY - COINCIDENT_LABEL_OFFSET, + }; + } + const deltaX = x2 - x1; + const deltaY = y2 - y1; + const length = Math.hypot(deltaX, deltaY); return { - labelX: (x1 + x2) / 2, - labelY: coincident ? (y1 + y2) / 2 - COINCIDENT_LABEL_OFFSET : (y1 + y2) / 2, + labelX: midpointX - (deltaY / length) * SEGMENT_LABEL_OFFSET, + labelY: midpointY + (deltaX / length) * SEGMENT_LABEL_OFFSET, }; } -function leftoverMapStackedCaptionY(labelY: number, stackedAbove: number): number { - return stackedAbove > 0 ? labelY + stackedAbove * RECONSTRUCTION_LABEL_OFFSET : labelY; +function leftoverMapStackedCaptionY( + labelY: number, + stackedAbove: number, + captionSpacing = RECONSTRUCTION_LABEL_OFFSET, +): number { + return stackedAbove > 0 ? labelY + stackedAbove * captionSpacing : labelY; +} + +function leftoverMapCaptionSpacing(height: number, captionCount: number): number { + if (captionCount <= 1) { + return 0; + } + const availableStackHeight = Math.max( + 0, + height - SEGMENT_LABEL_TOP_INSET - SEGMENT_LABEL_BOTTOM_INSET, + ); + return Math.min( + RECONSTRUCTION_LABEL_OFFSET, + availableStackHeight / (captionCount - 1), + ); } export function layoutLeftoverMapPlot( @@ -383,46 +415,75 @@ export function layoutLeftoverMapPlot( itemPos.x, itemPos.y, ); + const captionCount = [ + distanceLabel, + reconstructionLabel, + explainedShareLabel, + unexplainedShareLabel, + crossShareLabel, + unexplainedLeftoverLabel, + residualLabel, + observedLabel, + expectedLabel, + rankLabel, + ].filter((label) => label !== null).length; + const captionSpacing = leftoverMapCaptionSpacing(height, captionCount); + const maximumLabelY = Math.max( + SEGMENT_LABEL_TOP_INSET, + height - + SEGMENT_LABEL_BOTTOM_INSET - + Math.max(0, captionCount - 1) * captionSpacing, + ); + const labelY = Math.min( + Math.max(labelPosition.labelY, SEGMENT_LABEL_TOP_INSET), + maximumLabelY, + ); const reconstructionY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, distanceLabel !== null && reconstructionLabel !== null ? 1 : 0, + captionSpacing, ); const explainedShareY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0), + captionSpacing, ); const unexplainedShareY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0), + captionSpacing, ); const crossShareY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0) + (unexplainedShareLabel !== null ? 1 : 0), + captionSpacing, ); const unexplainedLeftoverY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0) + (unexplainedShareLabel !== null ? 1 : 0) + (crossShareLabel !== null ? 1 : 0), + captionSpacing, ); const residualY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0) + (unexplainedShareLabel !== null ? 1 : 0) + (crossShareLabel !== null ? 1 : 0) + (unexplainedLeftoverLabel !== null ? 1 : 0), + captionSpacing, ); const observedY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0) + @@ -430,9 +491,10 @@ export function layoutLeftoverMapPlot( (crossShareLabel !== null ? 1 : 0) + (unexplainedLeftoverLabel !== null ? 1 : 0) + (residualLabel !== null ? 1 : 0), + captionSpacing, ); const expectedY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0) + @@ -441,9 +503,10 @@ export function layoutLeftoverMapPlot( (unexplainedLeftoverLabel !== null ? 1 : 0) + (residualLabel !== null ? 1 : 0) + (observedLabel !== null ? 1 : 0), + captionSpacing, ); const rankY = leftoverMapStackedCaptionY( - labelPosition.labelY, + labelY, (distanceLabel !== null ? 1 : 0) + (reconstructionLabel !== null ? 1 : 0) + (explainedShareLabel !== null ? 1 : 0) + @@ -453,6 +516,7 @@ export function layoutLeftoverMapPlot( (residualLabel !== null ? 1 : 0) + (observedLabel !== null ? 1 : 0) + (expectedLabel !== null ? 1 : 0), + captionSpacing, ); segments.push({ pairKind: pair.pair_kind === "farthest" ? "farthest" : "closest", @@ -491,6 +555,7 @@ export function layoutLeftoverMapPlot( rankX: labelPosition.labelX, rankY, ...labelPosition, + labelY, }); } From 02ae2004d46e8d86dc0db6dc71df12b1563785d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:31:43 +0900 Subject: [PATCH 10/23] fix(reports): preserve distinct tick keys in rank descendant --- frontend/src/components/LeftoverMapPlot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index e5f238211..72ea5aae8 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -138,7 +138,7 @@ export function LeftoverMapPlot({ {layout.ticks.map((tick) => ( From 0f6d074ce39de0ce140e4ea20479c0a5ec21ea38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:37:26 +0900 Subject: [PATCH 11/23] fix(reports): carry parent caption readability into rank descendant --- frontend/src/components/LeftoverMapPlot.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/LeftoverMapPlot.css b/frontend/src/components/LeftoverMapPlot.css index c96a691ae..619fddaee 100644 --- a/frontend/src/components/LeftoverMapPlot.css +++ b/frontend/src/components/LeftoverMapPlot.css @@ -97,6 +97,10 @@ .leftover-map-plot-segment-label { fill: var(--color-text); font-size: 0.65rem; + paint-order: stroke; + stroke: var(--color-background); + stroke-width: 3px; + stroke-linejoin: round; } .leftover-map-plot-item { From 90f7e19431620edc80cd25a2352b3f9aac0f0d21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:38:11 +0900 Subject: [PATCH 12/23] fix(stack): carry protected Draft admission into rank descendant --- .github/workflows/tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 648b5e090..9a73249f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - types: [opened, synchronize, reopened, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] permissions: contents: read @@ -13,13 +13,13 @@ concurrency: # A merged pull request can report the base ref on ``closed``. Keying PR # events by number lets that close run cancel an older queued synchronize # run for the same PR instead of consuming runners after the PR is closed. - group: tests-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: pytest: name: Full test suite - if: github.event_name != 'pull_request' || github.event.action != 'closed' + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest services: postgres: @@ -65,7 +65,7 @@ jobs: frontend: name: Frontend lint, test, build - if: github.event_name != 'pull_request' || github.event.action != 'closed' + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest steps: - name: Checkout repository From 2dee25d70d813a3b19e3c7b548c78d959145acbd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:38:32 +0900 Subject: [PATCH 13/23] fix(stack): carry protected ontology admission into rank descendant --- .github/workflows/ontology-pages.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ontology-pages.yml b/.github/workflows/ontology-pages.yml index 05e7000bb..8cc734e27 100644 --- a/.github/workflows/ontology-pages.yml +++ b/.github/workflows/ontology-pages.yml @@ -2,6 +2,7 @@ name: Ontology Pages on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] branches: [main] paths: - "docs/ontology/**" @@ -32,13 +33,14 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: validate: name: Validate ontology publication - if: github.event_name == 'pull_request' - concurrency: - group: ontology-pages-validation-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + if: github.event_name == 'pull_request' && github.event.action != 'closed' && github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - name: Checkout repository @@ -82,9 +84,6 @@ jobs: publish: name: Publish ontology to GitHub Pages if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - concurrency: - group: ontology-pages-publication - cancel-in-progress: false runs-on: ubuntu-latest permissions: contents: read From 85f1289766b53c3fa258a453ac7589231f5a0dac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:38:49 +0900 Subject: [PATCH 14/23] fix(stack): carry protected PROV-O admission into rank descendant --- .github/workflows/prov-o-contract.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prov-o-contract.yml b/.github/workflows/prov-o-contract.yml index eae453153..a289748c9 100644 --- a/.github/workflows/prov-o-contract.yml +++ b/.github/workflows/prov-o-contract.yml @@ -2,6 +2,7 @@ name: PROV-O contract on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] branches: [main] paths: - "lineageweave/prov_o.py" @@ -28,12 +29,13 @@ permissions: contents: read concurrency: - group: prov-o-contract-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: standards-contract: name: Registry, inference, coverage, PostgreSQL + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest services: postgres: From cae01d82cb99ff9a5c110da938c3b3ab7bdcd66c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:39:02 +0900 Subject: [PATCH 15/23] test(stack): carry protected workflow contract into rank descendant --- tests/test_tests_workflow_contract.py | 58 ++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/tests/test_tests_workflow_contract.py b/tests/test_tests_workflow_contract.py index e6b48461a..b0953f6f9 100644 --- a/tests/test_tests_workflow_contract.py +++ b/tests/test_tests_workflow_contract.py @@ -1,17 +1,63 @@ -"""Regression contracts for the repository-local test workflow.""" +"""Regression contracts for repository-local workflow concurrency.""" from pathlib import Path -_WORKFLOW = Path(__file__).parents[1] / ".github" / "workflows" / "tests.yml" +_WORKFLOW_DIRECTORY = Path(__file__).parents[1] / ".github" / "workflows" +_PULL_REQUEST_GROUP = ( + "group: ${{ github.workflow }}-${{ github.repository }}-" + "${{ github.event_name == 'pull_request' && " + "github.event.pull_request.number || github.run_id }}" +) +_PULL_REQUEST_CANCELLATION = ( + "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" +) +_PULL_REQUEST_TYPES = ( + "types: [opened, synchronize, reopened, ready_for_review, " + "converted_to_draft, closed]" +) +_DRAFT_ADMISSION = "github.event.pull_request.draft == false" def test_pull_request_concurrency_survives_closed_ref_change() -> None: """Key synchronize and closed events by PR number so close cancels stale work.""" - workflow = _WORKFLOW.read_text(encoding="utf-8") + workflow = (_WORKFLOW_DIRECTORY / "tests.yml").read_text(encoding="utf-8") - assert "types: [opened, synchronize, reopened, closed]" in workflow - assert "group: tests-${{ github.event.pull_request.number || github.ref }}" in workflow - assert "cancel-in-progress: true" in workflow + assert _PULL_REQUEST_TYPES in workflow assert workflow.count("github.event.action != 'closed'") == 2 + + +def test_pull_request_workflows_cancel_only_superseded_same_pr_runs() -> None: + """Scope PR cancellation by workflow, repository, and pull request number.""" + + for workflow_path in sorted(_WORKFLOW_DIRECTORY.glob("*.yml")): + workflow = workflow_path.read_text(encoding="utf-8") + if "pull_request:" not in workflow: + continue + assert _PULL_REQUEST_GROUP in workflow, workflow_path.name + assert _PULL_REQUEST_CANCELLATION in workflow, workflow_path.name + + +def test_draft_pull_requests_do_not_consume_repository_local_runners() -> None: + """Cancel stale draft runs while deferring expensive jobs until review readiness.""" + + expected_draft_guards = { + "tests.yml": 2, + "prov-o-contract.yml": 1, + "ontology-pages.yml": 1, + } + for workflow_name, expected_guard_count in expected_draft_guards.items(): + workflow = (_WORKFLOW_DIRECTORY / workflow_name).read_text(encoding="utf-8") + assert _PULL_REQUEST_TYPES in workflow, workflow_name + assert workflow.count(_DRAFT_ADMISSION) == expected_guard_count, workflow_name + + +def test_ontology_publication_runs_are_not_cancelled() -> None: + """Keep publication runs isolated and non-cancelling outside pull requests.""" + + workflow = (_WORKFLOW_DIRECTORY / "ontology-pages.yml").read_text( + encoding="utf-8" + ) + assert "github.run_id" in workflow + assert _PULL_REQUEST_CANCELLATION in workflow From 1887be0de1133fd9bccc0aa169f71015d035a303 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:40:50 +0900 Subject: [PATCH 16/23] test(release): reproduce descendant version identity drift --- tests/test_package_version.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/test_package_version.py diff --git a/tests/test_package_version.py b/tests/test_package_version.py new file mode 100644 index 000000000..cb85f136e --- /dev/null +++ b/tests/test_package_version.py @@ -0,0 +1,20 @@ +"""Release identity stays consistent across Python and frontend artifacts.""" + +from __future__ import annotations + +import json +import tomllib +from pathlib import Path + +from lineageweave import __version__ + + +def test_release_versions_are_synchronized() -> None: + """Runtime provenance must use the same version as shipped package metadata.""" + repository_root = Path(__file__).resolve().parents[1] + project = tomllib.loads((repository_root / "pyproject.toml").read_text(encoding="utf-8")) + frontend = json.loads( + (repository_root / "frontend" / "package.json").read_text(encoding="utf-8") + ) + + assert __version__ == project["project"]["version"] == frontend["version"] From 3f02dd688ba0de5d5f93b9867a52d2cc1d30b5b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 17:55:24 +0900 Subject: [PATCH 17/23] fix(release): align runtime version with v2.37 descendant --- lineageweave/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index 45c371fa7..217361388 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -130,4 +130,4 @@ "serialize_lineage_analysis_result", ] -__version__ = "2.20.0" +__version__ = "2.37.0" From d50cc61a1917a15f03a01b097c2020ecae2e5682 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 17:55:40 +0900 Subject: [PATCH 18/23] docs(stack): carry Pages landing into rank descendant --- docs/index.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..3b2deb73d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ +# LineageWeave + +LineageWeave reconstructs evidence-bound lineage, project journeys, and relationship context from scattered enterprise records so people can inspect how events, claims, posts, and decisions relate over time. + +## Start here + +Use LineageWeave when source systems contain related records without reliable explicit cross-record links and you need a browsable, evidence-preserving view of branching histories rather than a flat search result. + +The product combines deterministic lineage assembly with bounded retrieval/fusion and optional adjudication through shared ContextualWisdomLab components. It keeps source authority, authentication, tenant scope, measurement, and model-provider routing in their owning products and contracts. + +## Product surfaces + +- lineage reconstruction and project journeys; +- operations/dashboard views over governed records; +- evidence-aware Global Ask and related context navigation; +- authenticated API and web client over durable PostgreSQL state; +- integration with ThreadWeave, RankWeave, TEPP, contextual-orchestrator, and shared context contracts through explicit boundaries. + +## Documentation + +- [Repository overview and local stack](../README.md) +- [Architecture](../ARCHITECTURE.md) +- [Product requirements](product-requirements.md) +- [Product/technical gap baseline](product-technical-gap-baseline.md) +- [Architecture decisions](adr/) +- [Research notes](lineage-bi-research-notes.md) + +## Status and evidence + +The repository has progressed beyond its original synthetic reconstruction demo into a broader authenticated product stack, while synthetic fixtures remain important for safe examples and tests. Treat protected-main behavior, current release evidence, and repository verification as authoritative; do not infer production readiness from a documentation page or an open pull request alone. From e27d29f8221378840849953c9e78604d3fadc6ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 17:55:50 +0900 Subject: [PATCH 19/23] test(stack): carry short-canvas caption bound into rank descendant --- .../src/leftoverMapPlotSmallCanvas.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 frontend/src/leftoverMapPlotSmallCanvas.test.ts diff --git a/frontend/src/leftoverMapPlotSmallCanvas.test.ts b/frontend/src/leftoverMapPlotSmallCanvas.test.ts new file mode 100644 index 000000000..75d18ab13 --- /dev/null +++ b/frontend/src/leftoverMapPlotSmallCanvas.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { layoutLeftoverMapPlot, type LeftoverMapPlottablePair } from "./leftoverMapPlotLayout"; + +const completeCaptionPair: LeftoverMapPlottablePair = { + pair_kind: "closest", + post_id: "post-small-canvas", + post_title: "Small canvas post", + criterion_code: "sales_lead_quality", + leftover_distance: 0.12, + leftover_map_reconstruction: 0.25, + leftover_map_explained_share: 0.76, + leftover_map_person_axis_1: -1, + leftover_map_person_axis_2: -1, + leftover_map_item_axis_1: 1, + leftover_map_item_axis_2: -1, +}; + +describe("layoutLeftoverMapPlot small-canvas caption bounds", () => { + it("keeps a complete three-caption stack inside the requested 20px height", () => { + const layout = layoutLeftoverMapPlot( + [completeCaptionPair], + (criterionCode) => criterionCode, + { height: 20 }, + ); + + expect(layout).not.toBeNull(); + expect(layout?.height).toBe(20); + const segment = layout?.segments[0]; + expect(segment).toBeDefined(); + expect(segment?.labelY).toBeGreaterThanOrEqual(12); + expect(segment?.reconstructionY).toBeGreaterThanOrEqual(segment?.labelY ?? 0); + expect(segment?.explainedShareY).toBeGreaterThanOrEqual(segment?.reconstructionY ?? 0); + expect(segment?.explainedShareY).toBeLessThanOrEqual(16); + }); +}); From 98c794fe1f94b8c5a49a8ecbeced0141e3900ffc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 18:01:00 +0900 Subject: [PATCH 20/23] test(i18n): reproduce lost Vietnamese share terminology --- .../src/leftoverMapPlotVietnameseCopy.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 frontend/src/leftoverMapPlotVietnameseCopy.test.ts diff --git a/frontend/src/leftoverMapPlotVietnameseCopy.test.ts b/frontend/src/leftoverMapPlotVietnameseCopy.test.ts new file mode 100644 index 000000000..16d29d5db --- /dev/null +++ b/frontend/src/leftoverMapPlotVietnameseCopy.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { setLocale, tf } from "./i18n"; + +const GRAPHIC_DISPLAY_COPY = + "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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score."; + +afterEach(() => { + setLocale("en"); +}); + +describe("leftover-map Vietnamese copy inheritance", () => { + it("keeps explained leftover share terminology as an explicit share", () => { + setLocale("vi"); + + expect( + tf("leftover-map explained leftover share {label}", { label: "R̂²/R² 0.76" }), + ).toBe("tỷ phần phần dư được giải thích trên bản đồ phần dư R̂²/R² 0.76"); + }); + + it("keeps the expanded graphic description explicit about explained share e", () => { + setLocale("vi"); + + expect(tf(GRAPHIC_DISPLAY_COPY, {})).toContain( + "tỷ phần phần dư được giải thích e", + ); + }); +}); From 430077e24aca9a3643e66fa52bb8b7e21d0fb3b4 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 5 Sep 2026 22:09:46 +0900 Subject: [PATCH 21/23] test(reports): carry parent caption offset expectation into rank stack --- frontend/src/leftoverMapPlotLayout.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index 9421a6e5f..b3799454f 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -232,10 +232,11 @@ describe("layoutLeftoverMapPlot", () => { criterionLabel, ); expect(layout?.segments.map((segment) => segment.distanceLabel)).toEqual(["d 0.12", "d 1.84"]); - expect(layout?.segments[0]?.labelX).toBeCloseTo( - ((layout?.segments[0]?.x1 ?? 0) + (layout?.segments[0]?.x2 ?? 0)) / 2, - 5, - ); + const segment = layout?.segments[0]; + const midpointX = ((segment?.x1 ?? 0) + (segment?.x2 ?? 0)) / 2; + const midpointY = ((segment?.y1 ?? 0) + (segment?.y2 ?? 0)) / 2; + expect(Math.hypot((segment?.labelX ?? 0) - midpointX, (segment?.labelY ?? 0) - midpointY)) + .toBeCloseTo(24, 5); }); it("omits a leftover-map distance caption when d is missing or non-finite", () => { From 8c16fa26ee3e97381a547a5b2e2eca8eed691ff2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 06:47:57 +0900 Subject: [PATCH 22/23] fix(i18n): restore Vietnamese explained-share terminology --- frontend/src/i18n.test.ts | 4 ++-- frontend/src/i18n.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index e1b4b7806..d5b56cf09 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -349,7 +349,7 @@ describe("i18n", () => { ], [ "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, phần dư chưa giải thích s, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát, E kỳ vọng và hạng bản đồ phần dư. 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̂, tỷ phần phần dư được giải thích e, phần dư chưa giải thích s, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát, E kỳ vọng và hạng bản đồ phần dư. 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); @@ -394,7 +394,7 @@ describe("i18n", () => { ["ko", "잔여 지도 설명 잔여 점유율 R̂²/R² 0.76"], ["zh", "残差图已解释残差份额 R̂²/R² 0.76"], ["ja", "残差マップ説明済み残差割合 R̂²/R² 0.76"], - ["vi", "phần dư giải thích bản đồ phần dư R̂²/R² 0.76"], + ["vi", "tỷ phần phần dư được giải thích trên bản đồ phần dư R̂²/R² 0.76"], ] as const)("formats leftover-map segment explained leftover share in %s", (locale, expected) => { setLocale(locale); expect(tf("leftover-map explained leftover share {label}", { label: "R̂²/R² 0.76" })).toBe( diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index b2ae5d7f9..84b2d10a6 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -2368,7 +2368,7 @@ const TRANSLATIONS: Partial>> = { "leftover-map axis {axis} tick {value}": "vạch trục bản đồ phần dư {axis} {value}", "leftover-map distance {label}": "khoảng cách bản đồ phần dư {label}", "leftover-map reconstruction {label}": "tái dựng bản đồ phần dư {label}", - "leftover-map explained leftover share {label}": "phần dư giải thích bản đồ phần dư {label}", + "leftover-map explained leftover share {label}": "tỷ phần phần dư được giải thích trên 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 cross share {label}": "phần giao bản đồ phần dư {label}", "leftover-map unexplained leftover {label}": "phần dư chưa giải thích {label}", @@ -2377,7 +2377,7 @@ const TRANSLATIONS: Partial>> = { "leftover expected {label}": "kỳ vọng {label}", "leftover-map rank {label}": "hạng 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, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. 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, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát, E kỳ vọng và hạng bản đồ phần dư. 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̂, tỷ phần phần dư được giải thích e, phần dư chưa giải thích s, phần giao x, phần dư chưa giải thích U, phần dư R, Y quan sát, E kỳ vọng và hạng bản đồ phần dư. 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.": From 4dcd789385f67c76a4e479194f566f35bb5d2d76 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 6 Sep 2026 22:23:11 +0900 Subject: [PATCH 23/23] test(reports): cover final rank caption at plot boundaries --- frontend/src/leftoverMapPlotLayout.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index b80585c66..cc2b161d0 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -405,6 +405,9 @@ describe("layoutLeftoverMapPlot", () => { leftover_map_person_axis_2: -1, leftover_map_item_axis_1: 1, leftover_map_item_axis_2: -1, + observed_response: 2.4, + expected_response: 2.0, + leftover_map_rank: 1, }), pair({ pair_kind: "farthest", @@ -414,14 +417,20 @@ describe("layoutLeftoverMapPlot", () => { leftover_map_person_axis_2: 1, leftover_map_item_axis_1: -1, leftover_map_item_axis_2: 1, + observed_response: 2.4, + expected_response: 2.0, + leftover_map_rank: 1, }), ], criterionLabel, ); - for (const segment of layout?.segments ?? []) { + expect(layout?.segments).toHaveLength(2); + for (const segment of layout!.segments) { expect(segment.labelY).toBeGreaterThanOrEqual(12); - expect(segment.explainedShareY).toBeLessThanOrEqual((layout?.height ?? 0) - 4); + expect(segment.rankLabel).toBe("rank 1"); + expect(segment.rankY).toBeGreaterThan(segment.expectedY); + expect(segment.rankY).toBeLessThanOrEqual(layout!.height - 4); } });