diff --git a/AGENTS.md b/AGENTS.md index 230ef3aad..2bd32b4dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -542,3 +542,5 @@ ADR 0294 keeps grouping-comparison incomplete-item coverage in the LineageWeave ADR 0295 keeps grouping-comparison reconstruction in the LineageWeave read-model/UI boundary: format only persisted `R̂`, expose the value in the pair button accessible name, and never derive psychometric reconstruction from UI-visible proxies. ADR 0296 adds persisted grouping-comparison explained-leftover share `e` in the LineageWeave read-model/UI boundary: expose it in the pair button accessible name, keep the duplicate visible badge presentation-only, and never derive or clamp psychometric values. + +ADR 0367 exposes persisted grouping-comparison leftover-map axis share only for a fully caller-visible persisted grouping. Partial visibility returns no axis aggregate; never recompute it from visible members. Missing/non-finite share omits only the badge; persisted zero and finite negative values remain explicit. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f16d44bed..2c555752b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1095,3 +1095,5 @@ ADR 0294 adds persisted incomplete-item coverage to grouping comparison presenta ADR 0295 adds persisted `R̂` to grouping-comparison pair buttons. The explicit button accessible name carries the reconstruction label/value; the visible duplicate badge is presentation-only. Psychometric computation remains owned by fast-mlsirm. ADR 0296 adds persisted explained-leftover share `e = R̂²/R²` to grouping-comparison pair actions; the button accessible name is authoritative for assistive technology and fast-mlsirm remains psychometric owner. + +ADR 0367 adds persisted `leftover_map_axes` to the grouping-comparison read model under the same whole-population authorization rule as other psychometric aggregates. The UI consumes persisted `leftover_share` through `leftoverMapCompareAxisShare`; no subset recomputation or singular-value derivation is permitted. diff --git a/CHANGELOG.d/2.54.0-leftover-map-compare-axis-share.md b/CHANGELOG.d/2.54.0-leftover-map-compare-axis-share.md new file mode 100644 index 000000000..6f00bd9b8 --- /dev/null +++ b/CHANGELOG.d/2.54.0-leftover-map-compare-axis-share.md @@ -0,0 +1,40 @@ +## 2.54.0 — Leftover-map axis share on the grouping comparison strip + +- Caption leftover-map axis share on the grouping comparison strip through + leftoverMapCompareAxisShare (ADR 0367). 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, pair segments that match + leftover-map rank on the pair row, a plot caption that matches leftover-map + complete-case coverage above the pair list, a plot caption that matches + leftover-map item complete-case coverage, a plot caption that matches + leftover-map incomplete post coverage, a plot caption that matches + leftover-map incomplete item coverage, a pair-list note that matches + leftover-map post complete-case coverage, a pair-list note that matches + leftover-map item complete-case coverage, a pair-list note that matches + leftover-map incomplete post coverage, a pair-list note that matches + leftover-map incomplete item coverage, a grouping comparison note that + matches leftover-map post complete-case coverage, a grouping comparison + note that matches leftover-map item complete-case coverage, a grouping + comparison note that matches leftover-map incomplete post coverage, a + grouping comparison note that matches leftover-map incomplete item + coverage, and a grouping comparison leftover-map axis share badge that + names the same persisted share as leftover-axis report badges but uses + distinct comparison copy and accessible naming; click a post marker or a pair button + opens that post. A missing or non-finite share omits that leftover-map + comparison axis share badge. Share `0` is shown when that persisted share + is a finite number, including rank-0 unused axes. A finite negative share + is shown; do not clamp to nonnegative. Do not invent leftover-map axis + share from leftover-map singular value, leftover pair count, plotted + marker count, leftover-map distance, leftover-map rank, leftover-map post + coverage, leftover-map item coverage, leftover-map incomplete post + coverage, leftover-map incomplete item coverage, or the count of unused + axes. The strip does not gain leftover-map singular values or the + leftover-map graphic. Never invent a leftover score. Never invent a + theta. No new columns. diff --git a/backend/app/main.py b/backend/app/main.py index 5318c6075..a93a5ed69 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3052,6 +3052,11 @@ async def compare_period_groupings( **row, "members": [], "leftover_pairs": leftover_pairs, + "leftover_map_axes": ( + row.get("leftover_map_axes", []) + if len(members) == len(row["members"]) + else [] + ), "leftover_map_coverage": ( row.get("leftover_map_coverage") if len(members) == len(row["members"]) diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index d675fe3ab..58c6c2dc4 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -1086,6 +1086,21 @@ async def fetch_period_comparison( leftover_coverage_by_key = { (row["grouping_kind"], row["grouping_key"]): row for row in leftover_coverage } + leftover_axes = await conn.fetch( + """ + select grouping_kind, grouping_key, axis_index, leftover_singular_value, leftover_share + from report_leftover_map_axis + where period_code = $1 and rubric_version = $2 + and grouping_kind = any($3::text[]) + order by grouping_kind, grouping_key, axis_index + """, + period_code, + RUBRIC_VERSION, + list(GROUPING_KINDS), + ) + leftover_axes_by_key: dict[tuple[str, str], list[asyncpg.Record]] = defaultdict(list) + for row in leftover_axes: + leftover_axes_by_key[(row["grouping_kind"], row["grouping_key"])].append(row) payload: list[dict[str, Any]] = [] for row in rows: label = await resolve_grouping_label(conn, row["grouping_kind"], row["grouping_key"]) @@ -1132,6 +1147,16 @@ async def fetch_period_comparison( "leftover_map_coverage": _leftover_map_coverage_payload( leftover_coverage_by_key.get((row["grouping_kind"], row["grouping_key"])) ), + "leftover_map_axes": [ + { + "axis_index": int(axis["axis_index"]), + "leftover_singular_value": float(axis["leftover_singular_value"]), + "leftover_share": float(axis["leftover_share"]), + } + for axis in leftover_axes_by_key.get( + (row["grouping_kind"], row["grouping_key"]), [] + ) + ], } ) return payload diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 273a7ff94..95431c425 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -5836,6 +5836,15 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, assert leftover_compare_coverage["incomplete_post_count"] == ( leftover_compare_coverage["scored_post_count"] - leftover_compare_coverage["map_post_count"] ) + leftover_compare_axes = leftover_thread.get("leftover_map_axes", []) + assert [axis["axis_index"] for axis in leftover_compare_axes] == [1, 2] + assert all(isinstance(axis["leftover_share"], (int, float)) for axis in leftover_compare_axes) + assert all( + axis["leftover_share"] == axis["leftover_share"] for axis in leftover_compare_axes + ) + assert leftover_compare_axes[0]["leftover_share"] != leftover_compare_axes[0][ + "leftover_singular_value" + ] or leftover_compare_axes[0]["leftover_share"] in {0, 1} assert leftover_compare_coverage["map_item_count"] <= leftover_compare_coverage["scored_item_count"] assert leftover_compare_coverage["map_post_count"] != len( leftover_thread.get("leftover_pairs", []) diff --git a/docs/adr/0367-leftover-map-compare-axis-share.md b/docs/adr/0367-leftover-map-compare-axis-share.md new file mode 100644 index 000000000..c69542e0c --- /dev/null +++ b/docs/adr/0367-leftover-map-compare-axis-share.md @@ -0,0 +1,180 @@ +# ADR 0367 — Name leftover-map axis share on the grouping comparison strip + +**Decision status:** Proposed +**Date:** 2026-08-30 + +Amends leftover pairs on the grouping comparison strip +([ADR 0149](0149-leftover-pairs-on-comparison-strip.md)) and leftover-map +axis share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). +Independent of leftover-map incomplete item coverage on the grouping +comparison strip ([ADR 0292](0292-leftover-map-compare-incomplete-item.md)), +leftover-map incomplete post coverage on the grouping comparison strip +([ADR 0291](0291-leftover-map-compare-incomplete-post.md)), leftover-map item +complete-case coverage on the grouping comparison strip +([ADR 0290](0290-leftover-map-compare-item-coverage.md)), leftover-map +complete-case coverage on the grouping comparison strip +([ADR 0289](0289-leftover-map-compare-coverage.md)), leftover-map post +complete-case coverage fail-closed on the pair list +([ADR 0288](0288-leftover-map-list-post-coverage-helper.md)), leftover-map +incomplete item coverage on the pair list +([ADR 0287](0287-leftover-map-list-incomplete-item.md)), leftover-map incomplete +post coverage on the pair list +([ADR 0286](0286-leftover-map-list-incomplete-post.md)), leftover-map item +complete-case coverage on the pair list +([ADR 0285](0285-leftover-map-list-item-coverage.md)), leftover-map incomplete +item coverage on the graphic display +([ADR 0284](0284-leftover-map-plot-incomplete-item.md)), leftover-map incomplete +post coverage on the graphic display +([ADR 0283](0283-leftover-map-plot-incomplete.md)), leftover-map item +complete-case coverage on the graphic display +([ADR 0282](0282-leftover-map-plot-item-coverage.md)), leftover-map complete-case +coverage on the graphic display +([ADR 0281](0281-leftover-map-plot-coverage.md)), leftover-map rank on pair +segments ([ADR 0280](0280-leftover-map-segment-rank.md)), leftover expected on pair +segments ([ADR 0279](0279-leftover-map-segment-expected.md)), leftover observed on +pair segments ([ADR 0278](0278-leftover-map-segment-observed.md)), leftover +residual on pair segments ([ADR 0277](0277-leftover-map-segment-residual.md)), +leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments ([ADR 0275](0275-leftover-map-segment-cross-share.md)), +leftover-map unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-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)), leftover-map rank +persistence ([ADR 0164](0164-leftover-map-rank.md)), leftover-map complete-case +coverage persistence ([ADR 0168](0168-leftover-map-complete-case-coverage.md)), +and leftover-map graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)). + +## Context + +ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map +axis share `σ_k² / Σ_j σ_j²` on `report_leftover_map_axis` and captions +leftover-axis report badges with share. ADR 0269 already captions leftover-map +graphic axes with that same persisted share. ADR 0149 already carries +ABAC-filtered leftover pairs on `GET /api/reports/compare/{period}`. ADR 0289 +already includes leftover-map complete-case coverage on that comparison +payload. The grouping comparison strip still names leftover pairs, `d`, and +leftover-map coverage without naming leftover-map axis share for that grouping, +so a buyer who compares PU / corp / thread leftover pairs can treat a +rank-0 unused axis as if it had the same Gabriel inertia as an 82% first axis. + +This increment includes persisted leftover-map axes on the comparison payload +and captions each grouping row through leftoverMapCompareAxisShare. It does +not add columns. It does not invent leftover-map axis share from leftover-map +singular value, leftover pair count, plotted marker count, leftover-map +distance, leftover-map rank, leftover-map post coverage, leftover-map item +coverage, leftover-map incomplete post coverage, leftover-map incomplete item +coverage, or the count of unused axes. It does not persist leftover-map inner +product, cosine, or length as separate columns. 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 **0367** so it does not collide with +leftover-map incomplete item coverage on the grouping comparison strip (0292), +leftover-map incomplete post coverage on the grouping comparison strip (0291), +leftover-map item complete-case coverage on the grouping comparison strip +(0290), leftover-map complete-case coverage on the grouping comparison strip +(0289), leftover-map post complete-case coverage fail-closed on the pair list +(0288), leftover-map incomplete item coverage on the pair list (0287), +leftover-map incomplete post coverage on the pair list (0286), leftover-map +item complete-case coverage on the pair list (0285), leftover-map incomplete +item coverage on the graphic display (0284), leftover-map incomplete post +coverage on the graphic display (0283), leftover-map item complete-case +coverage on the graphic display (0282), leftover-map complete-case coverage +on the graphic display (0281), leftover-map rank on pair segments (0280), +or the dashboard stacks. + +## Decision + +On the grouping comparison strip, include persisted `leftover_map_axes` on +each comparison row and caption leftover-map axis `k` as +`leftover map comparison axis {k} {share}%` only when leftoverMapCompareAxisShare +returns a usable share from persisted `leftover_share`. Use the distinct +accessible name `Leftover map comparison axis share` so the strip caption is +not the leftover-axis report badge (`leftover axis {k} {share}%`) and is not +the graphic caption (`leftover-map axis {k} ({share}%)`). A missing axis row +or a missing or non-finite share omits that leftover-map comparison axis +share badge and keeps the strip leftover-map post coverage note, leftover-map +item coverage note, leftover-map incomplete post note, leftover-map +incomplete item note, leftover pairs, leftover-map distance `d`, and any +leftover-map captions on the pair list and graphic. Share `0` is shown when +that persisted share is a finite number, including rank-0 unused axes. A +finite negative share is shown; do not clamp to nonnegative. Axis 1 and +axis 2 stay independently named. Do not invent leftover-map axis share from +leftover-map singular value, leftover pair count, plotted marker count, +leftover-map distance, leftover-map rank, leftover-map post coverage, +leftover-map item coverage, leftover-map incomplete post coverage, +leftover-map incomplete item coverage, or the count of unused axes. Do not +caption leftover-map singular values or the leftover-map graphic on the strip +in this increment. Click a leftover pair on the strip to open that post. + +Do not add SQL migrations. 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`, the grouping comparison strip names persisted leftover-map +post complete-case coverage, persisted leftover-map item complete-case +coverage, persisted leftover-map incomplete post coverage, persisted +leftover-map incomplete item coverage, and persisted leftover-map axis share +on each grouping row when leftoverMapCoverageCounts / +leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / +leftoverMapIncompleteItemCount / leftoverMapCompareAxisShare return usable +values, then names leftover pairs with leftover-map distance `d`. Closest +and farthest leftover pairs still sit above the member list with the +leftover-map graphic display; click a post marker or a pair button opens that +post. +Hidden posts stay hidden. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover-map +complete-case coverage persistence, leftover-map axis share persistence, +leftover pairs on the grouping comparison strip, leftover-map complete-case +coverage on the grouping comparison strip, leftover-map item complete-case +coverage on the grouping comparison strip, leftover-map incomplete post +coverage on the grouping comparison strip, leftover-map incomplete item +coverage on the grouping comparison strip, leftover-map inner product, +leftover-map cosine, leftover-map length, leftover-map graphic display, +leftover-map item complete-case coverage on the graphic display, leftover-map +item complete-case coverage on the pair list, leftover-map incomplete post +coverage on the graphic display, leftover-map incomplete post coverage on the +pair list, leftover-map incomplete item coverage on the graphic display, +leftover-map incomplete item coverage on the pair list, leftover-map post +complete-case coverage fail-closed on the pair list, leftover-map axis share +on the graphic display, and leftover-map singular values. + +## 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. Leftover-map +axis share is Gabriel inertia `σ_k² / Σ_j σ_j²` of residual SVD axes; +grouping comparison leftover-map axis share names that persisted share +for that grouping only when leftoverMapCompareAxisShare returns a usable +share.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 9b2d3acea..6a0bc0295 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,9 +1,10 @@ # Product & Technical Gap Baseline +> Current serialized repair (2026-09-07): #828 is reconstructed from exact #827 `0f9e9c8db37948041f39b4e68e4bbd808fa83752` as ADR 0367 / v2.54.0. The valid delta carries persisted grouping-comparison `leftover_map_axes` but exposes them only when the caller can see the entire persisted grouping population; partial visibility returns no aggregate and never recomputes psychometrics from the visible subset. Presentation consumes persisted `leftover_share` only, with zero and finite negative values explicit and missing/non-finite values omitted. Historical v2.50.0/ADR0293 identity is evidence only. Existing five-locale compatibility copy is preserved while the database-backed eight-locale translation-ledger authority remains #922/#929/#932; this Draft does not create a competing translation source. + + > 2026-09-07 #822 reconstruction: exact #821 `93eaa40f...` owns ADR 0290/v2.47.0 and the full-visible-grouping coverage boundary. Preserve historical item-coverage composition as ADR 0291/v2.48.0; partial-visibility rows omit the shared persisted coverage aggregate. #963 remains owner of broader current-vs-historical baseline governance. -> > #821 current-parent reconstruction: preserve grouping-comparison leftover-map post complete-case coverage from historical `11a78553...` on exact #820 `503d043e...`, but move the decision to ADR 0290 / v2.47.0 and fail closed persisted coverage whenever ABAC hides any grouping member. Review 5126637582 is the authorization RED authority; #963 still owns the broader current-vs-historical baseline cleanup. -> > #820 exact-current-parent reconstruction: persisted singular-value axis badges > are reconstructed from #819 `f37ca315e3c48fa37bbcafe96e46c5d7dab991b7`. > Preserve finite, non-negative persisted `σ_k`, including rank-0 `σ 0.00`; omit missing, @@ -54,7 +55,6 @@ > 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 incomplete item > coverage on the pair list (ADR 0287 / v2.44.0) delivered locally on > `feat/leftover-map-list-incomplete-item-v2440`. Caption the pair-list @@ -110,7 +110,6 @@ > 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 incomplete post > coverage on the pair list (ADR 0286 / v2.43.0) delivered locally on > `feat/leftover-map-list-incomplete-post-v2430`. Caption the pair-list @@ -164,7 +163,6 @@ > 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 item complete-case > coverage on the pair list (ADR 0285 / v2.42.0) delivered locally on > `feat/leftover-map-list-item-coverage-v2420`. Caption the pair-list @@ -215,7 +213,6 @@ > 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 incomplete item > coverage on the graphic display (ADR 0284 / v2.41.0) delivered locally on > `feat/leftover-map-plot-incomplete-item-v2410`. Caption the leftover-map @@ -268,7 +265,6 @@ > 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 incomplete post > coverage on the graphic display (ADR 0283 / v2.40.0) delivered locally on > `feat/leftover-map-plot-incomplete-v2400`. Caption the leftover-map @@ -318,7 +314,6 @@ > 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 item complete-case > coverage on the graphic display (ADR 0282 / v2.39.0) delivered locally on > `feat/leftover-map-plot-item-coverage-v2390`. Caption the leftover-map @@ -349,7 +344,6 @@ > temporary audit evidence and are not committed. This later documentation > overlay does not inherit those results; exact-head GitHub Checks and an > independent approval remain required before protected merge. -> > Exact-head release-contract overlay: 2026-08-31 11:46 KST. > Protected `main` remains `cb187cadee5fb6c46d8a944815ccc154a1e028d1`; > sixty-nine open PRs and ten open issues were enumerated in a fresh snapshot. @@ -372,7 +366,6 @@ > leftover-map UI continues to render only persisted owner-produced values and > leaves missing or non-finite evidence unavailable. No self-approval, bypass, > force push, arbitrary weighting, or hidden-evidence substitution is used. -> > Exact-head loop overlay: 2026-08-31 06:31 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1` (leftover-map coordinates, > graphic, axis share, ticks, and segment distance through v2.28.0, #782). @@ -388,7 +381,6 @@ > the separately authorized derivation-evidence action. This is candidate > evidence only: authenticated PostgreSQL API and rendered runtime proof are > still required before marking the Voice acceptance boundary complete. -> > Highest buyer-visible active gap in this slice: finish the protected parent- > first delivery of persisted leftover-map reconstruction `R̂` and explained > share `e` on graphic pair segments (#802), without recomputing either value in @@ -416,7 +408,6 @@ > 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 explained leftover > share `e` on graphic-display pair segments (ADR 0273 / v2.30.0) > delivered locally on @@ -446,7 +437,6 @@ > `gh`/MCP succeed (comment/create-branch/auto-merge). Copilot review is > not independent APPROVE. Do not self-approve. Do not `gh pr merge` > stacked leftover PRs onto an unprotected leftover base. -> > Next buyer increment on this cycle: leftover-map reconstruction `R̂` > on graphic-display pair segments (ADR 0272 / v2.29.0). Caption each > closest/farthest segment with persisted leftover-map reconstruction @@ -470,7 +460,6 @@ > `gh`/MCP succeed. Copilot review is not independent APPROVE. Do not > self-approve. Do not `gh pr merge` stacked leftover PRs onto an > unprotected leftover base. -> > Next buyer increment on this cycle: leftover-map distance on > graphic-display pair segments (ADR 0271 / v2.28.0). Caption each > closest/farthest segment with persisted leftover-map distance `d` so @@ -492,7 +481,6 @@ > `gh`/MCP succeed (comment/create-branch/auto-merge). `git push` HTTPS > still fails (empty `X-OAuth-Scopes`). Copilot review is not > independent APPROVE. Do not self-approve. -> > Next buyer increment on this cycle: leftover-map coordinate ticks > (ADR 0270 / v2.27.0). Tick leftover-map axes at the origin and at each > unique finite persisted `ξ` / `ζ` so pair-row `ξ (x, y) ζ (x, y)` @@ -511,7 +499,6 @@ > 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 @@ -527,7 +514,6 @@ > `e2d13019004a5d8c019fecf7a39ceeef4093b8dd`; 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. -> > Next buyer increment on this cycle: leftover-map graphic display > of already-persisted `ξ_{1:2}` / `ζ_{1:2}` (ADR 0268 / v2.25.0). > UI-only; no new columns. `R̂` and `d` already are inner product and @@ -539,7 +525,6 @@ > explained leftover share, #775). Open ready PRs still lack independent > APPROVE. Drafts remain dirty against `main`. #96 stays closed as a > weaker duplicate of #91. GitHub writes through `gh` succeed. -> > Next buyer increment on this cycle: leftover-map coordinates > `ξ_{1:2}` / `ζ_{1:2}` (ADR 0267 / migration 0245 / v2.24.0) so > `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖` are buyer-auditable. Do not name @@ -555,7 +540,6 @@ > the Grok GitHub App now succeed (comment/close/auto-merge/update-branch) > despite empty `X-OAuth-Scopes`; git push is the remaining probe this > cycle. This overlay supersedes every older queue count below. -> > Next buyer increment on this cycle: leftover-map explained leftover > share `e = R̂² / R²` (ADR 0266 / migration 0244 / v2.23.0) so > `e + s + x = 1` is buyer-auditable. Do not persist leftover-map @@ -571,7 +555,6 @@ > `main`. Central ruleset 18156473 and repository no-force-push ruleset > 21065108 remain active. This overlay supersedes every older queue count below. > Checks from older heads, stacked bases, or merged PRs are not transferred. -> > Current-runtime boundary: the official Compose project was healthy at the > HTTP health route, but its PostgreSQL schema did not yet contain > `source_post_voice`; therefore no current Voice-history aggregate, diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 6ec386fc3..75b82777b 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,7 +6,7 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| | `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map singular values when finite and leftover-map axis share when finite. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, and `MissingAxisSingular` cover two-pair maps, rank-0 origin with `σ 0.00` and 0% share, a `0` tick, `d 0.00`, `R̂ 0.00`, `R̂²/R² 0.00`, `U²/R² 0.00`, `2R̂U/R² 0.00`, `U 0.00`, `R 0.00`, `Y 0.00`, `E 0.00`, and `rank 0`, omitted plots, missing share that keeps leftover-map singular values, and missing singular values that keep leftover-map axis share. The plot does not invent a leftover score. | `LeftoverMapPlot`, `leftoverMapPlotLayout`, `leftoverMapPlotAxisShare`, `leftoverMapPlotAxisSingular`, `--color-primary`, `--color-palette-blue-mid` | -| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map singular values and leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The grouping comparison strip names persisted leftover-map post complete-case coverage only for fully visible grouping populations; partial-visibility rows omit that aggregate. The grouping comparison strip also names persisted item complete-case coverage only for fully visible grouping populations. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map singular values and leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The grouping comparison strip names persisted leftover-map post complete-case coverage only for fully visible grouping populations; partial-visibility rows omit that aggregate. The grouping comparison strip also names persisted item complete-case coverage only for fully visible grouping populations. The comparison axis-share label is independent of the coverage helpers: it appears only for a finite persisted `leftover_share`, including zero and finite negative values, and is omitted for missing/non-finite share. Partial-visibility grouping rows expose no persisted axis aggregate. | `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 ff2d6a69f..52298d5f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.53.0", + "version": "2.54.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index ff0154e28..6768f4a0e 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -898,6 +898,10 @@ describe("App, authenticated", () => { incomplete_post_count: 0, incomplete_item_count: 0, }, + leftover_map_axes: [ + { axis_index: 1, leftover_singular_value: 0, leftover_share: 0 }, + { axis_index: 2, leftover_singular_value: 0, leftover_share: 0 }, + ], }, { grouping_kind: "corporate_entity", @@ -934,6 +938,10 @@ describe("App, authenticated", () => { incomplete_post_count: 1, incomplete_item_count: 0, }, + leftover_map_axes: [ + { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, + { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, + ], }, ], }), @@ -4325,6 +4333,34 @@ describe("App, authenticated", () => { "Leftover-map graphic incomplete items", ), ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByLabelText("Leftover-map axis share"), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison axis share", + ), + ).toHaveLength(4); + expect( + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison axis share", + )[0], + ).toHaveTextContent("leftover map comparison axis 1 0%"); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText("leftover map comparison axis 1 82%"), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText("leftover map comparison axis 2 18%"), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText("leftover axis 1 82%"), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText(/leftover-map axis 1/), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText(/σ 1\.84/), + ).not.toBeInTheDocument(); expect( screen.getByRole("button", { name: "Compare Business unit (PU): Demo Report High, mean θ 0.81" }), ).toHaveTextContent("mean θ 0.81"); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d249f852..d291e1f55 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -151,6 +151,11 @@ import { formatLeftoverMapExplainedShare, LEFTOVER_MAP_COMPARE_EXPLAINED_SHARE_LABEL, } from "./leftoverMapExplainedShare"; +import { + leftoverMapCompareAxisShare, + LEFTOVER_MAP_COMPARE_AXIS_SHARE, + LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL, +} from "./leftoverMapCompareAxis"; import "./App.css"; const AdminPanel = lazy(() => import("./components/AdminPanel").then((module) => ({ default: module.AdminPanel }))); @@ -4064,6 +4069,21 @@ function ReportsPanel({ {tf(LEFTOVER_MAP_PLOT_INCOMPLETE_ITEM, comparisonIncompleteItemCount)}

) : null} + {row.leftover_map_axes?.map((axis) => { + const comparisonAxisShare = leftoverMapCompareAxisShare(axis); + if (comparisonAxisShare === null) { + return null; + } + return ( + + {tf(LEFTOVER_MAP_COMPARE_AXIS_SHARE, comparisonAxisShare)} + + ); + })} {row.leftover_pairs && row.leftover_pairs.length > 0 && (