diff --git a/AGENTS.md b/AGENTS.md index 4b82dc788..eefa47f47 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) are computed in `lineageweave/leftover_pairs.py` from the +0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269) 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 @@ -288,10 +288,13 @@ ADR 0201 is the sole normative reconstruction formula, storage, and audit contract; do not duplicate or reinterpret it here. ADR 0233 is the sole unexplained leftover share contract. ADR 0266 is the sole explained leftover share contract. ADR 0267 is the sole -leftover-map coordinate contract. ADR 0268 is the sole leftover-map +leftover-map coordinate contract. ADR 0268 is the leftover-map graphic-display contract: draw persisted `ξ` and `ζ` above the pair buttons; omit the plot when coordinates are missing; click a post -marker to open that post. When `R`, `R̂`, `U`, `x`, +marker to open that post. ADR 0269 captions those leftover-map axes +with persisted leftover-map axis share `σ_k² / Σ_j σ_j²` when finite, +including rank-0 zero-share axes; a missing or non-finite share omits +that axis badge and keeps the existing leftover-map axis text. 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 c515fec1c..2c087fd1e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,13 +637,14 @@ 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) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269) persist to `report_leftover_pair` with signed residual `R`, observed `Y`, expected `E[Y|θ, item]`, full leftover-map rank, unexplained leftover, ADR 0201 reconstruction evidence, ADR 0185 cross-share evidence, ADR 0233 unexplained leftover share `s`, ADR 0266 explained leftover share `e`, and ADR 0267 leftover-map coordinates `ξ_{1:2}` / `ζ_{1:2}`. ADR 0268 draws those persisted coordinates as the leftover-map graphic -display above the pair buttons. Click a post marker or a pair button +display above the pair buttons. ADR 0269 captions leftover-map axes 1 +and 2 with persisted leftover-map axis share. 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.26.0-leftover-map-axis-share-plot.md b/CHANGELOG.d/2.26.0-leftover-map-axis-share-plot.md new file mode 100644 index 000000000..0340b6cd4 --- /dev/null +++ b/CHANGELOG.d/2.26.0-leftover-map-axis-share-plot.md @@ -0,0 +1,11 @@ +## 2.26.0 — Leftover-map axis share on the graphic display + +- Caption leftover-map graphic-display axes with persisted leftover-map + axis share `σ_k² / Σ_j σ_j²` (ADR 0269). After `make seed`, closest + and farthest leftover pairs sit above the member list with the + Gabriel biplot of already-named coordinates, and each leftover-map + axis names its inertia share; click a post marker or a pair button + opens that post. Rank-0 zero-share axes still name 0%. A missing or + non-finite share omits that axis badge and keeps the existing axis + text. Never invent a leftover score. Never invent a theta. No new + columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index e67f7e2ec..e4c98aa57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption leftover-map graphic-display axes + with persisted leftover-map axis share `σ_k² / Σ_j σ_j²` (ADR 0269 / + v2.26.0). After `make seed`, closest and farthest leftover pairs sit + above the member list with the Gabriel biplot of already-named + coordinates, and each leftover-map axis names its inertia share; + click a post marker or a pair button opens that post. Rank-0 + zero-share axes still name 0%. A missing or non-finite share omits + that axis badge and keeps the existing leftover-map axis text. Never + invent a leftover score or a theta. No new columns. + - Period leftover pairs now draw the leftover-map graphic display of persisted `ξ_{1:2}` and `ζ_{1:2}` (ADR 0268 / v2.25.0). After `make seed`, closest and farthest leftover pairs sit above the diff --git a/CLAUDE.md b/CLAUDE.md index f8c7fd19c..3073348c9 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), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269), 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 53e6053aa..248cba6d6 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -11,7 +11,8 @@ [ADR 0233](0233-leftover-map-unexplained-share.md) (unexplained leftover share s); [ADR 0266](0266-leftover-map-explained-share.md) (explained leftover share e); [ADR 0267](0267-leftover-map-coordinates.md) (leftover-map coordinates ξ, ζ); -[ADR 0268](0268-leftover-map-graphic-display.md) (leftover-map graphic display) +[ADR 0268](0268-leftover-map-graphic-display.md) (leftover-map graphic display); +[ADR 0269](0269-leftover-map-axis-share-plot.md) (leftover-map axis share on the graphic display) ## Context @@ -66,7 +67,9 @@ When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, `e + s + x = 1`. ADR 0267 persists leftover-map coordinates `ξ_{1:2}` and `ζ_{1:2}` so reconstruction `R̂ = ξ · ζ` and distance `d = ‖ξ − ζ‖` stay auditable from the pair row. ADR 0268 draws those persisted coordinates as the -leftover-map graphic display; it adds no columns. +leftover-map graphic display; it adds no columns. ADR 0269 captions +those leftover-map axes with persisted leftover-map axis share; 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 ee14cb697..2673dec32 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -12,7 +12,8 @@ [ADR 0233](0233-leftover-map-unexplained-share.md) (unexplained leftover share s); [ADR 0266](0266-leftover-map-explained-share.md) (explained leftover share e); [ADR 0267](0267-leftover-map-coordinates.md) (leftover-map coordinates ξ, ζ); -[ADR 0268](0268-leftover-map-graphic-display.md) (leftover-map graphic display) +[ADR 0268](0268-leftover-map-graphic-display.md) (leftover-map graphic display); +[ADR 0269](0269-leftover-map-axis-share-plot.md) (leftover-map axis share on the graphic display) ## Context @@ -37,7 +38,8 @@ leftover-map coordinates `ξ_{1:2}` and `ζ_{1:2}` when finite, and leftover-map cross share next to distance when finite. When four finite coordinates exist, the leftover-map graphic display of those positions sits above the pair buttons (ADR 0268); click a post marker to open -that post. The next action names every available +that post. Leftover-map axis share captions those leftover-map axes +when finite (ADR 0269). The next action names every available measurement before opening the post; no amendment hides another, rank 0 explicitly names no leftover structure, and unexplained leftover names "leftover map leaves unexplained `U` after IRT main effects; open this diff --git a/docs/adr/0148-leftover-map-axis-share.md b/docs/adr/0148-leftover-map-axis-share.md index 2540ddf91..a9d554176 100644 --- a/docs/adr/0148-leftover-map-axis-share.md +++ b/docs/adr/0148-leftover-map-axis-share.md @@ -2,6 +2,8 @@ **Decision status:** Accepted **Date:** 2026-08-24 +**Amended by:** [ADR 0269](0269-leftover-map-axis-share-plot.md) +(leftover-map axis share on the graphic display) ## Context @@ -45,7 +47,9 @@ Rebuild and seed write leftover-map axes in the same transaction as leftover pairs. `GET /api/reports/{grouping}/{period}` returns `leftover_map_axes` next to `leftover_pairs`. The Period reports panel shows leftover-axis share badges and a caption that tells the buyer to -open a leftover pair. Migration `0169_report_leftover_map_axis.sql` +open a leftover pair. The leftover-map graphic display captions those +same leftover-map axes with the persisted share when finite (ADR 0269). +Migration `0169_report_leftover_map_axis.sql` upgrades volumes that already applied `0001`. ## References diff --git a/docs/adr/0267-leftover-map-coordinates.md b/docs/adr/0267-leftover-map-coordinates.md index be4919955..99dce6dd6 100644 --- a/docs/adr/0267-leftover-map-coordinates.md +++ b/docs/adr/0267-leftover-map-coordinates.md @@ -103,7 +103,8 @@ leftover-map distance, leftover-map rank, leftover-map inner product, leftover-map cosine, leftover-map length, leftover-map reconstruction, leftover-map unexplained leftover, leftover-map cross share, leftover-map unexplained leftover share, leftover-map explained -leftover share, and leftover-map graphic display. +leftover share, leftover-map graphic display, and leftover-map axis +share on the graphic display. ## References diff --git a/docs/adr/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index 05dd751b5..a758d6532 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -3,6 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-28 +**Amended by:** [ADR 0269](0269-leftover-map-axis-share-plot.md) +(leftover-map axis share on the graphic display) + Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map explained leftover share ([ADR 0266](0266-leftover-map-explained-share.md)), @@ -73,7 +76,8 @@ product, cosine, or length as separate columns. After `make seed`, closest and farthest leftover pairs sit above the member list with the leftover-map graphic display of persisted `ξ` and `ζ`; click a post marker or a pair button opens that post. -Hidden posts stay hidden. When coordinates, reconstruction, and +Hidden posts stay hidden. Leftover-map axis share captions those +leftover-map axes when finite (ADR 0269). 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. @@ -88,7 +92,8 @@ leftover-map distance, leftover-map rank, leftover-map inner product, leftover-map cosine, leftover-map length, leftover-map reconstruction, leftover-map unexplained leftover, leftover-map cross share, leftover-map unexplained leftover share, leftover-map explained -leftover share, and leftover-map coordinate persistence. +leftover share, leftover-map coordinate persistence, and leftover-map +axis share on the graphic display. ## References diff --git a/docs/adr/0269-leftover-map-axis-share-plot.md b/docs/adr/0269-leftover-map-axis-share-plot.md new file mode 100644 index 000000000..5785d42c0 --- /dev/null +++ b/docs/adr/0269-leftover-map-axis-share-plot.md @@ -0,0 +1,95 @@ +# ADR 0269 — Name leftover-map axis share on the graphic display + +**Decision status:** Accepted +**Date:** 2026-08-28 + +Amends [ADR 0148](0148-leftover-map-axis-share.md) and +[ADR 0268](0268-leftover-map-graphic-display.md). Independent of leftover-map +explained leftover share ([ADR 0266](0266-leftover-map-explained-share.md)), +leftover-map unexplained leftover share ([ADR 0233](0233-leftover-map-unexplained-share.md)), +leftover-map reconstruction ([ADR 0201](0201-leftover-map-reconstruction.md)), +and leftover-map coordinates ([ADR 0267](0267-leftover-map-coordinates.md)). + +## Context + +ADR 0148 already persists leftover-map axis share +`σ_k² / Σ_j σ_j²` on each period report as two leftover-map axes. +ADR 0268 already draws the leftover-map graphic display of persisted +`ξ_{1:2}` and `ζ_{1:2}`. Those plot axes still read only +"leftover-map axis 1" and "leftover-map axis 2", so Gabriel inertia +lives on report badges and not on the graphic display Gabriel (1971) +captioned. Hiding share on the axes lets leftover residual `R`, +leftover-map distance `d`, or reconstruction `R̂` be read as leftover-map +structure even after axis share itself is persisted. + +This increment captions the leftover-map graphic display with already +persisted leftover-map axis share. It does not add columns. It does not +persist leftover-map inner product, cosine, or length. It does not land +Post quality on the leftover criterion. Leftover-map distance stays +two-axis Euclidean. Do not invent a leftover score. Do not invent a +theta. + +The dashboard stack already used neighbouring leftover facts under +other numbers. This protected increment uses **0269** so it does not +collide with leftover-map graphic display (0268), leftover-map +coordinates (0267 / migration 0245), leftover-map explained leftover +share (0266 / migration 0244), leftover-map unexplained leftover share +(0233 / migration 0233), leftover-map reconstruction (0201 / migration +0206), leftover-map cross share (0185), leftover residual disclosure, +leftover observed `Y` / expected `E`, leftover-map rank, two-axis +leftover-map distance, leftover coverage, leftover-map axis share +persistence (0148), leftover interaction-map persistence, occupational +construct catalog search (0265), or the dashboard stacks. + +## Decision + +On the leftover-map graphic display, caption leftover-map axis `k` +with persisted leftover-map axis share when that share is finite, +including rank-0 zero-share axes. The caption is +`leftover-map axis {k} ({share}%)` from `σ_k² / Σ_j σ_j²`. A missing +or non-finite share omits that axis badge and keeps the existing +leftover-map axis text. Axis 1 and axis 2 stay independently named: +one missing share does not hide the other. Click a post marker to +open that post. Criterion markers are not post buttons. The grouping +comparison strip (ADR 0149) stays on its reduced leftover payload and +does not gain this caption. + +Do not add SQL. Do not edit shipped migrations. Do not persist inner +product, cosine, or length as separate columns. Do not invent a +leftover score. Do not invent a theta. + +## Consequences + +After `make seed`, closest and farthest leftover pairs sit above the +member list with the leftover-map graphic display of persisted `ξ` +and `ζ`, and each leftover-map axis names its persisted Gabriel +inertia share; click a post marker or a pair button opens that post. +Hidden posts stay hidden. Rank-0 unused axes still plot at the origin +and still name zero leftover-map axis share. Report-level leftover-axis +badges (ADR 0148) remain. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover observed +`Y` / expected `E`, leftover-map complete-case coverage, leftover-map +axis share persistence, leftover pairs on the grouping comparison +strip, two-axis leftover-map distance, leftover-map rank, leftover-map +inner product, leftover-map cosine, leftover-map length, leftover-map +reconstruction, leftover-map unexplained leftover, leftover-map cross +share, leftover-map unexplained leftover share, leftover-map explained +leftover share, leftover-map coordinate persistence, and leftover-map +graphic display. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 +(LSIRM interaction `−γ‖ξ_j − ζ_i‖` after main effects +`α_j − β_i`; typically `p = 2` for the interaction map.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8ae2beddf..3b7a48b87 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,24 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-28 19:15 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 display (v2.24.0 / +> v2.25.0 / ADR 0267 / ADR 0268) is on +> `2f7e9c8df695f12d03964d5caa68fa3355bdd923`; Strix fail-closed and no +> independent APPROVE. Drafts remain dirty against `main`. #96 stays +> closed as a weaker duplicate of #91. GitHub writes through MCP succeed +> (comment/create-branch/git push/auto-merge). Copilot review is not +> independent APPROVE. Do not self-approve. +> +> Next buyer increment on this cycle: leftover-map axis share on the +> graphic display (ADR 0269 / v2.26.0). Caption plot axes with persisted +> ADR 0148 `leftover_map_axes` inertia `σ_k² / Σ_j σ_j²`. UI-only; no +> new columns. Rank-0 zero-share axes still named. Missing/non-finite +> share omits that axis badge and keeps existing leftover-map axis +> text. Do not invent leftover scores. Do not mix into dashboard stacks +> #640/#778/#781. + > Exact-head loop overlay: 2026-08-28 16:05 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 094452a31..712509da2 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), then click a post marker to open that post. `ClosestAndFarthest`, `RankZeroOrigin`, and `MissingCoordinates` cover two-pair maps, rank-0 origin, and omitted plots. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `--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. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), 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, 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, and leftover-map axes name persisted leftover-map axis 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 87b052607..8c35c576e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.25.0", + "version": "2.26.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index d7a847147..f4f6f51ed 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4142,8 +4142,10 @@ describe("App, authenticated", () => { ); expect(screen.getByRole("button", { name: /open report post: public post/i })).toHaveTextContent("Open"); expect(screen.getByRole("button", { name: /open report post: public post/i })).toHaveTextContent("due 2026-01-12"); - expect(screen.getByLabelText("Leftover pairs")).toBeInTheDocument(); + expect(await screen.findByLabelText("Leftover pairs")).toBeInTheDocument(); expect(screen.getByLabelText("Leftover-map graphic display")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); expect( screen.getByRole("button", { name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e3fd0d8d6..e3fb6c796 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3868,6 +3868,7 @@ function ReportsPanel({ { onSelectPost(pair.post_id, { diff --git a/frontend/src/components/LeftoverMapPlot.stories.tsx b/frontend/src/components/LeftoverMapPlot.stories.tsx index 8b397271f..4cf7de2b7 100644 --- a/frontend/src/components/LeftoverMapPlot.stories.tsx +++ b/frontend/src/components/LeftoverMapPlot.stories.tsx @@ -8,6 +8,10 @@ const meta = { criterionLabel: (code: string) => code === "sales_lead_quality" ? "sales-lead" : "negative", onSelectPost: () => undefined, + leftoverMapAxes: [ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, + { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, + ], pairs: [ { pair_kind: "closest", @@ -61,6 +65,10 @@ export const ClosestAndFarthest: Story = {}; export const RankZeroOrigin: Story = { args: { + leftoverMapAxes: [ + { axis_index: 1, leftover_singular_value: 0, leftover_share: 0 }, + { axis_index: 2, leftover_singular_value: 0, leftover_share: 0 }, + ], pairs: [ { pair_kind: "closest", @@ -83,6 +91,10 @@ export const RankZeroOrigin: Story = { export const MissingCoordinates: Story = { args: { + leftoverMapAxes: [ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, + { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, + ], pairs: [ { pair_kind: "closest", @@ -96,3 +108,11 @@ export const MissingCoordinates: Story = { ], }, }; + +export const MissingAxisShare: Story = { + args: { + leftoverMapAxes: [ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: Number.NaN }, + ], + }, +}; diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 9da61f53b..df6b65576 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -50,6 +50,8 @@ describe("LeftoverMapPlot", () => { ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); expect(screen.getByText("Criterion ζ")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 1")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 2")).toBeInTheDocument(); const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -102,6 +104,10 @@ describe("LeftoverMapPlot", () => { leftover_map_item_axis_2: 0, }, ]} + leftoverMapAxes={[ + { axis_index: 1, leftover_singular_value: 0, leftover_share: 0 }, + { axis_index: 2, leftover_singular_value: 0, leftover_share: 0 }, + ]} criterionLabel={criterionLabel} onSelectPost={vi.fn()} />, @@ -109,5 +115,40 @@ describe("LeftoverMapPlot", () => { expect( screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (0.00, 0.00)" }), ).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 2 (0%)")).toBeInTheDocument(); + }); + + it("captions leftover-map axes with persisted leftover-map axis share", () => { + render( + , + ); + expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); + }); + + it("keeps existing leftover-map axis text when share is missing or non-finite", () => { + render( + , + ); + expect(screen.getByText("leftover-map axis 1")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 2")).toBeInTheDocument(); + expect(screen.queryByText(/leftover-map axis 1 \(/)).not.toBeInTheDocument(); + expect(screen.queryByText(/leftover-map axis 2 \(/)).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 998e96ac1..2b58a0c82 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -1,6 +1,11 @@ -import type { LeftoverPair } from "../api"; +import type { LeftoverMapAxis, LeftoverPair } from "../api"; import { t, tf } from "../i18n"; import { formatLeftoverMapCoordinatePair } from "../leftoverMapCoordinates"; +import { + formatLeftoverMapPlotAxisShare, + leftoverShareForAxis, + LEFTOVER_MAP_PLOT_AXIS_SHARE, +} from "../leftoverMapPlotAxisShare"; import { firstPlottablePairForPost, layoutLeftoverMapPlot, @@ -11,6 +16,7 @@ import "./LeftoverMapPlot.css"; export type LeftoverMapPlotProps = { pairs: LeftoverPair[]; + leftoverMapAxes?: LeftoverMapAxis[]; criterionLabel: (criterionCode: string) => string; onSelectPost: (pair: LeftoverPair) => void; }; @@ -19,15 +25,32 @@ function diamondPoints(x: number, y: number, radius: number): string { return `${x},${y - radius} ${x + radius},${y} ${x},${y + radius} ${x - radius},${y}`; } +function leftoverMapPlotAxisText( + axisIndex: 1 | 2, + leftoverMapAxes: LeftoverMapAxis[] | undefined, +): string { + const percent = formatLeftoverMapPlotAxisShare( + leftoverShareForAxis(leftoverMapAxes, axisIndex), + ); + if (percent === null) { + return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2"); + } + return tf(LEFTOVER_MAP_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); +} + /** * Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. * * Person markers are posts; item markers are leftover criteria. Click a - * post marker to open that post. Omit the plot when no pair has four + * post marker to open that post. Caption leftover-map axes with persisted + * Gabriel inertia share when finite, including rank-0 zero-share axes. + * 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, + leftoverMapAxes, criterionLabel, onSelectPost, }: LeftoverMapPlotProps) { @@ -78,10 +101,10 @@ export function LeftoverMapPlot({ y2={layout.originY} /> - {t("leftover-map axis 1")} + {leftoverMapPlotAxisText(1, leftoverMapAxes)} - {t("leftover-map axis 2")} + {leftoverMapPlotAxisText(2, leftoverMapAxes)} {layout.segments.map((segment) => ( code === "sales_lead_quality" ? "sales-lead" : "negative", onSelectPost: () => undefined, + leftoverMapAxes: [ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, + { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, + ], pairs: [ { pair_kind: "closest", diff --git a/frontend/src/components/LeftoverPairList.test.tsx b/frontend/src/components/LeftoverPairList.test.tsx index 965188a1c..74f4e8a64 100644 --- a/frontend/src/components/LeftoverPairList.test.tsx +++ b/frontend/src/components/LeftoverPairList.test.tsx @@ -182,6 +182,10 @@ describe("LeftoverPairList", () => { leftover_map_item_axis_2: -0.02, }, ]} + leftoverMapAxes={[ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, + { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, + ]} criterionLabel={criterionLabel} onSelectPost={onSelectPost} />, @@ -192,6 +196,8 @@ describe("LeftoverPairList", () => { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", }), ); + expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); expect(onSelectPost).toHaveBeenCalledWith( expect.objectContaining({ post_id: "post-demo-public", diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index 5ff92e9e2..452bc9d65 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -1,4 +1,4 @@ -import type { LeftoverPair } from "../api"; +import type { LeftoverMapAxis, LeftoverPair } from "../api"; import { t, tf } from "../i18n"; import { formatLeftoverMapCrossShare, @@ -37,6 +37,7 @@ import { LeftoverMapPlot } from "./LeftoverMapPlot"; export type LeftoverPairListProps = { pairs: LeftoverPair[]; + leftoverMapAxes?: LeftoverMapAxis[]; criterionLabel: (criterionCode: string) => string; onSelectPost: (pair: LeftoverPair) => void; }; @@ -56,11 +57,13 @@ export type LeftoverPairListProps = { * unexplained leftover, then the existing residual/rank/observed-expected * next action. When four finite coordinates exist, ADR 0268 draws the * leftover-map graphic display above the pair buttons; click a post - * marker opens that post. Every badge still renders together before - * opening the named post. + * marker opens that post. ADR 0269 captions those leftover-map axes with + * persisted leftover-map axis share when finite. Every badge still + * renders together before opening the named post. */ export function LeftoverPairList({ pairs, + leftoverMapAxes, criterionLabel, onSelectPost, }: LeftoverPairListProps) { @@ -71,6 +74,7 @@ export function LeftoverPairList({
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index b3a319c0f..b311760f8 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -64,6 +64,7 @@ describe("i18n", () => { "Criterion ζ", "leftover-map axis 1", "leftover-map axis 2", + "leftover-map axis {axis} ({share}%)", "Leftover map after IRT main effects. 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.", @@ -348,6 +349,16 @@ describe("i18n", () => { ).toBe(expected); }); + it.each([ + ["ko", "잔여 지도 축 1 (82%)"], + ["zh", "残差图轴 1 (82%)"], + ["ja", "残差マップ軸 1 (82%)"], + ["vi", "trục bản đồ phần dư 1 (82%)"], + ] as const)("formats leftover-map plot axis share in %s", (locale, expected) => { + setLocale(locale); + expect(tf("leftover-map axis {axis} ({share}%)", { axis: 1, share: "82" })).toBe(expected); + }); + it.each([ ["ko", "IRT 주효과 이후 관측 Y 2.40와 기대 E 2.00를 읽은 다음, 이 글을 여세요."], ["zh", "阅读 IRT 主效应后的观测 Y 2.40 与期望 E 2.00,然后打开这篇帖子。"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index df34daa5a..32d194ce3 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -589,6 +589,7 @@ const TRANSLATIONS: Partial>> = { "Criterion ζ": "기준 ζ", "leftover-map axis 1": "잔여 지도 축 1", "leftover-map axis 2": "잔여 지도 축 2", + "leftover-map axis {axis} ({share}%)": "잔여 지도 축 {axis} ({share}%)", "Leftover map after IRT main effects. Click a post marker to open that post. The plot does not invent a leftover score.": "IRT 주효과 이후 잔여 지도입니다. 글 표식을 눌러 그 글을 여세요. 이 그림은 잔여 점수를 만들어내지 않습니다.", "Open leftover-map post {title} at ξ {person}": @@ -1166,6 +1167,7 @@ const TRANSLATIONS: Partial>> = { "Criterion ζ": "准则 ζ", "leftover-map axis 1": "残差图轴 1", "leftover-map axis 2": "残差图轴 2", + "leftover-map axis {axis} ({share}%)": "残差图轴 {axis} ({share}%)", "Leftover map after IRT main effects. Click a post marker to open that post. The plot does not invent a leftover score.": "IRT 主效应后的残差图。点击帖子标记打开该帖子。此图不会虚构残差分数。", "Open leftover-map post {title} at ξ {person}": @@ -1747,6 +1749,7 @@ const TRANSLATIONS: Partial>> = { "Criterion ζ": "基準 ζ", "leftover-map axis 1": "残差マップ軸 1", "leftover-map axis 2": "残差マップ軸 2", + "leftover-map axis {axis} ({share}%)": "残差マップ軸 {axis} ({share}%)", "Leftover map after IRT main effects. Click a post marker to open that post. The plot does not invent a leftover score.": "IRT主効果後の残差マップです。投稿マーカーをクリックしてその投稿を開いてください。この図は残差スコアを作りません。", "Open leftover-map post {title} at ξ {person}": @@ -2328,6 +2331,7 @@ const TRANSLATIONS: Partial>> = { "Criterion ζ": "Tiêu chí ζ", "leftover-map axis 1": "trục bản đồ phần dư 1", "leftover-map axis 2": "trục bản đồ phần dư 2", + "leftover-map axis {axis} ({share}%)": "trục bản đồ phần dư {axis} ({share}%)", "Leftover map after IRT main effects. 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. 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}": diff --git a/frontend/src/leftoverMapPlotAxisShare.test.ts b/frontend/src/leftoverMapPlotAxisShare.test.ts new file mode 100644 index 000000000..08611b707 --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisShare.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { + formatLeftoverMapPlotAxisShare, + leftoverShareForAxis, +} from "./leftoverMapPlotAxisShare"; + +describe("leftoverShareForAxis", () => { + const axes = [ + { axis_index: 1, leftover_share: 0.82 }, + { axis_index: 2, leftover_share: 0.18 }, + ]; + + it("reads persisted leftover-map axis share without inventing a leftover score", () => { + expect(leftoverShareForAxis(axes, 1)).toBe(0.82); + expect(leftoverShareForAxis(axes, 2)).toBe(0.18); + }); + + it("names a rank-0 zero-share leftover-map axis", () => { + expect(leftoverShareForAxis([{ axis_index: 1, leftover_share: 0 }, { axis_index: 2, leftover_share: 0 }], 1)).toBe( + 0, + ); + }); + + it("omits a leftover-map axis when share is missing or non-finite", () => { + expect(leftoverShareForAxis(undefined, 1)).toBeNull(); + expect(leftoverShareForAxis([], 1)).toBeNull(); + expect(leftoverShareForAxis([{ axis_index: 1, leftover_share: null }], 1)).toBeNull(); + expect(leftoverShareForAxis([{ axis_index: 1, leftover_share: Number.NaN }], 1)).toBeNull(); + expect( + leftoverShareForAxis([{ axis_index: 1, leftover_share: Number.POSITIVE_INFINITY }], 1), + ).toBeNull(); + expect(leftoverShareForAxis(axes, 3)).toBeNull(); + }); +}); + +describe("formatLeftoverMapPlotAxisShare", () => { + it("formats persisted leftover-map axis share as percent without inventing a leftover score", () => { + expect(formatLeftoverMapPlotAxisShare(0.82)).toBe("82"); + expect(formatLeftoverMapPlotAxisShare(0.18)).toBe("18"); + expect(formatLeftoverMapPlotAxisShare(0)).toBe("0"); + }); + + it("omits leftover-map axis share when the value is missing or non-finite", () => { + expect(formatLeftoverMapPlotAxisShare(null)).toBeNull(); + expect(formatLeftoverMapPlotAxisShare(undefined)).toBeNull(); + expect(formatLeftoverMapPlotAxisShare(Number.NaN)).toBeNull(); + expect(formatLeftoverMapPlotAxisShare(Number.POSITIVE_INFINITY)).toBeNull(); + expect(formatLeftoverMapPlotAxisShare(Number.NEGATIVE_INFINITY)).toBeNull(); + }); +}); diff --git a/frontend/src/leftoverMapPlotAxisShare.ts b/frontend/src/leftoverMapPlotAxisShare.ts new file mode 100644 index 000000000..57d45aef4 --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisShare.ts @@ -0,0 +1,30 @@ +/** Caption leftover-map plot axes with persisted Gabriel inertia share. */ + +import type { LeftoverMapAxis } from "./api"; + +export const LEFTOVER_MAP_PLOT_AXIS_SHARE = "leftover-map axis {axis} ({share}%)"; + +export type LeftoverMapPlotAxisShare = { + axis_index: LeftoverMapAxis["axis_index"]; + leftover_share?: LeftoverMapAxis["leftover_share"] | null; +}; + +export function leftoverShareForAxis( + axes: ReadonlyArray | null | undefined, + axisIndex: number, +): number | null { + const axis = axes?.find((candidate) => candidate.axis_index === axisIndex); + if (axis == null || axis.leftover_share == null || !Number.isFinite(axis.leftover_share)) { + return null; + } + return axis.leftover_share; +} + +export function formatLeftoverMapPlotAxisShare( + leftoverShare: number | null | undefined, +): string | null { + if (leftoverShare == null || !Number.isFinite(leftoverShare)) { + return null; + } + return (leftoverShare * 100).toFixed(0); +} diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index eef687d5e..80095b51e 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -1,4 +1,6 @@ -/** Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. */ +/** Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. + * Leftover-map axis share captions axes 1 and 2 when finite (ADR 0269). + */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; import type { LeftoverPair } from "./api"; diff --git a/pyproject.toml b/pyproject.toml index 54deb8566..0e37b92aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.25.0" +version = "2.26.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 79f7d12e5..73a4f913c 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.24.0" +version = "2.26.0" source = { editable = "." } dependencies = [ { name = "certifi" },