From ef5fcf63b6bcc2f6b451cb22b277116fb10fe138 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 15:40:10 +0000 Subject: [PATCH 01/11] feat(reports): leftover-map singular values on grouping comparison strip (v2.51.0) Caption persisted leftover-map singular values on GET /api/reports/compare/{period} through leftoverMapCompareAxisSingular (ADR 0294). Distinct aria-label Leftover map comparison axis singular. Omit missing, non-finite, or negative leftover-map singular value independently of leftover-map comparison axis share. Do not invent leftover-map singular value from leftover-map axis share. No SQL. No leftover score or theta. --- AGENTS.md | 14 ++- ARCHITECTURE.md | 2 + ...51.0-leftover-map-compare-axis-singular.md | 18 +++ CHANGELOG.md | 39 +++++++ CLAUDE.md | 2 +- backend/tests/test_api.py | 8 ++ ...0294-leftover-map-compare-axis-singular.md | 108 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 32 ++++++ docs/storybook-inventory.md | 2 +- frontend/package.json | 2 +- frontend/src/App.test.tsx | 25 +++- frontend/src/App.tsx | 33 ++++-- frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 2 + frontend/src/i18n.ts | 8 ++ frontend/src/leftoverMapCompareAxis.test.ts | 66 +++++++++++ frontend/src/leftoverMapCompareAxis.ts | 37 +++++- frontend/src/leftoverMapPlotLayout.ts | 5 +- pyproject.toml | 2 +- 19 files changed, 389 insertions(+), 19 deletions(-) create mode 100644 CHANGELOG.d/2.51.0-leftover-map-compare-axis-singular.md create mode 100644 docs/adr/0294-leftover-map-compare-axis-singular.md diff --git a/AGENTS.md b/AGENTS.md index 9f55e640a..9dbe6db7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -422,8 +422,16 @@ pairs can read Gabriel inertia of residual SVD axes 1 and 2 for that grouping; a missing or non-finite share omits that leftover-map comparison axis share badge and does not invent leftover-map axis share from leftover-map singular value, leftover pair count, or plotted marker count. Share `0` is shown when -persisted, including rank-0 unused axes. The strip does not gain leftover-map +persisted, including rank-0 unused axes. ADR 0293 does not caption leftover-map singular values or the leftover-map graphic. +ADR 0294 captions the grouping comparison strip with persisted leftover-map +singular values through leftoverMapCompareAxisSingular so a buyer who compares leftover +pairs can read the Gabriel scale of residual SVD axes 1 and 2 for that grouping; +a missing, non-finite, or negative leftover-map singular value omits that leftover-map +comparison axis singular badge independently of leftover-map comparison axis share +and does not invent leftover-map singular value from leftover-map axis share, leftover +pair count, or plotted marker count. Rank-0 leftover-map singular value `0` is shown +when persisted. The strip does not gain the leftover-map graphic. 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 @@ -446,6 +454,10 @@ Grouping comparison incomplete post coverage (ADR 0291) captions the grouping comparison strip with how many scored posts stayed out of each grouping's map. Grouping comparison incomplete item coverage (ADR 0292) captions the grouping comparison strip with how many scored criteria stayed out of each grouping's map. +Grouping comparison leftover-map axis share (ADR 0293) captions the grouping +comparison strip with Gabriel inertia of residual SVD axes. +Grouping comparison leftover-map singular values (ADR 0294) captions the grouping +comparison strip with the Gabriel scale of those axes. Item complete-case coverage (ADR 0282) captions the leftover-map graphic with how many scored criteria entered the map. Item complete-case coverage on the pair list (ADR 0285) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index dd6bc8a4e..35f815e56 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -677,6 +677,8 @@ ADR 0292 captions the grouping comparison strip with persisted leftover-map inco item coverage through leftoverMapIncompleteItemCount. ADR 0293 captions the grouping comparison strip with persisted leftover-map axis share through leftoverMapCompareAxisShare. +ADR 0294 captions the grouping comparison strip with persisted leftover-map singular +values through leftoverMapCompareAxisSingular. 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 diff --git a/CHANGELOG.d/2.51.0-leftover-map-compare-axis-singular.md b/CHANGELOG.d/2.51.0-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..f73c7aa67 --- /dev/null +++ b/CHANGELOG.d/2.51.0-leftover-map-compare-axis-singular.md @@ -0,0 +1,18 @@ +## 2.51.0 — Leftover-map singular values on the grouping comparison strip + +- Caption leftover-map singular values on the grouping comparison strip through + leftoverMapCompareAxisSingular (ADR 0294). 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, + leftover-map singular values when finite and nonnegative, axis ticks that + match `ξ (x, y) ζ (x, y)` on the pair row, and leftover pairs with leftover-map + distance `d`; click a post marker or a pair button opens that post. A missing, + non-finite, or negative leftover-map singular value omits that leftover-map + comparison axis singular badge independently of leftover-map comparison axis + share. Rank-0 leftover-map singular value `0` is shown when persisted. Do not + invent leftover-map singular value from leftover-map axis share, 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 the leftover-map graphic. Never invent a leftover + score. Never invent a theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2706f7c31..4cfd2e6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,45 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption the grouping comparison strip with + persisted leftover-map singular values through leftoverMapCompareAxisSingular + (ADR 0294 / v2.51.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, leftover-map singular + values when finite and nonnegative, 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, a grouping comparison leftover-map axis share badge that + matches leftover-axis report badges, and a grouping comparison leftover-map + singular-value badge; click a post marker or a pair button opens that post. + A missing, non-finite, or negative leftover-map singular value omits that + leftover-map comparison axis singular badge independently of leftover-map + comparison axis share. Rank-0 leftover-map singular value `0` is shown when + that persisted value is a finite nonnegative number. Do not invent leftover-map + singular value from leftover-map axis share, 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 the leftover-map graphic. Never invent a leftover score. Never + invent a theta. No new columns. + - Period leftover pairs now caption the grouping comparison strip with persisted leftover-map axis share through leftoverMapCompareAxisShare (ADR 0293 / v2.50.0). After `make seed`, closest and farthest leftover diff --git a/CLAUDE.md b/CLAUDE.md index 1963b8114..6993e4098 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ cutoff. Global Ask optional `knowledge_cutoff` uses the same cover Create/start endpoint rules (ADR 0017 / 0021), tie-vs-miss similarity (ADR 0026), R&R catalog ids (ADR 0019 / 0027), leftover pairs -(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294), 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/backend/tests/test_api.py b/backend/tests/test_api.py index 95431c425..c20a110fc 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -5845,6 +5845,14 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, 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 all( + isinstance(axis["leftover_singular_value"], (int, float)) for axis in leftover_compare_axes + ) + assert all( + axis["leftover_singular_value"] == axis["leftover_singular_value"] + and axis["leftover_singular_value"] >= 0 + for axis in leftover_compare_axes + ) 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/0294-leftover-map-compare-axis-singular.md b/docs/adr/0294-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..07e52fa5a --- /dev/null +++ b/docs/adr/0294-leftover-map-compare-axis-singular.md @@ -0,0 +1,108 @@ +# ADR 0294 — Name leftover-map singular values on the grouping comparison strip + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends leftover-map axis share on the grouping comparison strip +([ADR 0293](0293-leftover-map-compare-axis-share.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)) +and leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)). The dashboard plot stack +already named leftover-map singular values on graphic axes and leftover-axis +badges under other numbers; this protected increment uses **0294**. + +## Context + +ADR 0148 already persists leftover-map singular values `σ_k` and leftover-map +axis share `σ_k² / Σ_j σ_j²` on `report_leftover_map_axis`. ADR 0293 already +includes those persisted axes on `GET /api/reports/compare/{period}` and +captions leftover-map axis share on the grouping comparison strip. The strip +still names share without naming leftover-map singular value for that +grouping, so a buyer who compares PU / corp / thread leftover pairs can treat +an 82% first axis as if it had the same Gabriel scale as a rank-0 unused axis. + +This increment captions each grouping row through leftoverMapCompareAxisSingular. +It does not add columns. It does not invent leftover-map singular value from +leftover-map axis share, 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. + +## Decision + +On the grouping comparison strip, caption leftover-map axis `k` as +`leftover map comparison axis {k} σ {value}` only when leftoverMapCompareAxisSingular +returns a usable leftover-map singular value from persisted +`leftover_singular_value`. Use the distinct accessible name +`Leftover map comparison axis singular` so the strip caption is not the +leftover-axis report badge (`leftover axis {k} σ {value} {share}%`) and is not +the graphic caption (`leftover-map axis {k} σ {value}`). A missing, non-finite, +or negative leftover-map singular value omits that leftover-map comparison axis +singular badge independently of leftover-map comparison axis share, leftover-map +post coverage notes, leftover-map item coverage notes, leftover-map incomplete +post notes, leftover-map incomplete item notes, leftover pairs, leftover-map +distance `d`, and any leftover-map captions on the pair list and graphic. +Rank-0 leftover-map singular value `0` is shown when that persisted value is a +finite nonnegative number. Do not invent leftover-map singular value from +leftover-map axis share, 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 add the leftover-map graphic to +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, persisted leftover-map axis share, and +persisted leftover-map singular values on each grouping row when +leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / +leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / +leftoverMapCompareAxisShare / leftoverMapCompareAxisSingular 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 axis share on the +grouping comparison strip, leftover-map inner product, leftover-map cosine, +leftover-map length, leftover-map graphic display, leftover-map axis share +on the graphic display, leftover-map singular values on graphic axes, and +leftover-map singular values on leftover-axis badges. + +## 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 +singular values are the Gabriel scale `σ_k` of residual SVD axes; +grouping comparison leftover-map singular values name that persisted +`σ_k` for that grouping only when leftoverMapCompareAxisSingular returns +a usable value.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6958ec3d1..f7ee5868a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,37 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-31 00:35 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. +> #825 leftover-map incomplete item coverage on the grouping comparison +> strip (v2.49.0 / ADR 0292) is `bca6baa`. #828 leftover-map axis share +> on the grouping comparison strip (v2.50.0 / ADR 0293) is `e791376`. +> Leave #782/#802/#803/#804/#805/#806/#808/#809/#810/#811/#812/#813/#814/#815/#816/#817/#818/#819/#821/#822/#823/#824/#825/#828 open for +> independent review. Do not squash-merge stacked leftover PRs onto an +> unprotected leftover base. Do not merge #808 without independent APPROVE. +> Copilot review is not independent APPROVE. Issues #79 and #87 stay open. +> #96 is already closed. Only collaborator is `seonghobae`. +> +> Next buyer increment on this cycle: leftover-map singular values on the +> grouping comparison strip (ADR 0294 / v2.51.0) on +> `feat/leftover-map-compare-axis-singular-v2510`. Caption persisted +> leftover_singular_value already on GET /api/reports/compare/{period} +> through leftoverMapCompareAxisSingular with distinct aria-label +> `Leftover map comparison axis singular` and copy +> `leftover map comparison axis {axis} σ {value}`. UI-only; no new columns. +> Missing, non-finite, or negative leftover-map singular value omits that +> badge independently of leftover-map comparison axis share. Rank-0 `0` +> is shown when persisted. Do not invent leftover-map singular value from +> leftover-map axis share. Do not add the leftover-map graphic. Stack onto +> leftover branch `feat/leftover-map-compare-axis-share-v2500` / #828; +> leave the PR open for independent review. Do not squash-merge onto the +> leftover base. Do not invent leftover scores. +> > Exact-head loop overlay: 2026-08-30 23:45 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 6852fca40..5141ecec1 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,7 +6,7 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| | `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match axis ticks to those coordinates, pair-segment `d` to leftover-map distance, pair-segment `R̂` to leftover-map reconstruction, pair-segment `R̂²/R²` to leftover-map explained leftover share, pair-segment `U²/R²` to leftover-map unexplained leftover share, pair-segment `2R̂U/R²` to leftover-map cross share, pair-segment `U` to leftover-map unexplained leftover, pair-segment `R` to leftover residual, pair-segment `Y` to leftover observed, pair-segment `E` to leftover expected, and pair-segment leftover-map rank, then click a post marker to open that post. Leftover-map axes name persisted leftover-map axis share when finite. The plot names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. `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. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The grouping comparison strip names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, leftover-map incomplete item coverage, and leftover-map axis share with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisShare return usable values. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. The leftover-map graphic display sits above the pair buttons when coordinates are finite, leftover-map axes name persisted leftover-map axis share, leftover-map axis ticks name persisted coordinates, pair segments name persisted leftover-map distance, pair segments name persisted leftover-map reconstruction, pair segments name persisted leftover-map explained leftover share, pair segments name persisted leftover-map unexplained leftover share, pair segments name persisted leftover-map cross share, pair segments name persisted leftover-map unexplained leftover, pair segments name persisted leftover residual, pair segments name persisted leftover observed, pair segments name persisted leftover expected, and pair segments name persisted leftover-map rank. The pair-list note names leftover-map complete-case coverage only when leftoverMapCoverageCounts returns usable complete-case integers, leftover-map item complete-case coverage, leftover-map incomplete post coverage, and leftover-map incomplete item coverage when persisted. The grouping comparison strip names leftover-map complete-case coverage, leftover-map item complete-case coverage, leftover-map incomplete post coverage, leftover-map incomplete item coverage, leftover-map axis share, and leftover-map singular values with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisShare / leftoverMapCompareAxisSingular return usable values. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | | `Workspace/OperationsDashboard` | Compare Event and post counts, inspect external-information coverage, then open the cited source behind a claim, handover, or repeat-issue fact. `EvidenceReady`, `NarrowViewport`, `AnalysisPendingAndMissingEvidence`, `AnalysisFailed`, and `LoadError` cover populated, mobile, unavailable-evidence, analysis-pending, retryable failure, and transport-error states. | `--color-dashboard-*`, `OperationsDashboard` | | `Post/SimilarVocPanel` | Compare ontology/semantic similar VOC and prior action evidence, then open the source; unavailable states show no fabricated TEPP theta or weight. | `SimilarVocPanel.css`, `SimilarVocPanel` | | `Post/Recorded perspectives` | Read the imported primary and every evidence-connected additional Voice with its recorded truth state instead of flattening them into one compound category. `CombinedEvidence`, `RejectedEvidence`, and `NarrowViewport` cover desktop, rejected-evidence, and narrow layouts. | `VoicePerspectiveList`, `ticket-list`, `post-meta` | diff --git a/frontend/package.json b/frontend/package.json index 497940a94..2238b2b96 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.50.0", + "version": "2.51.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 129526d0f..a5e7bd6c3 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4357,7 +4357,30 @@ describe("App, authenticated", () => { within(screen.getByLabelText("Grouping comparison")).queryByText(/leftover-map axis 1/), ).not.toBeInTheDocument(); expect( - within(screen.getByLabelText("Grouping comparison")).queryByText(/σ 1\.84/), + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison axis singular", + ), + ).toHaveLength(4); + expect( + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison axis singular", + )[0], + ).toHaveTextContent("leftover map comparison axis 1 σ 0.00"); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText( + "leftover map comparison axis 1 σ 1.84", + ), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByText( + "leftover map comparison axis 2 σ 0.86", + ), + ).toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText("leftover axis 1 σ 1.84 82%"), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByText("leftover-map axis 1 σ 1.84"), ).not.toBeInTheDocument(); expect( screen.getByRole("button", { name: "Compare Business unit (PU): Demo Report High, mean θ 0.81" }), diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7d77c77eb..3db20507f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { focusedGraphMustReset } from "./focusedGraphSelection"; import { canAuthorVoice, postPrimaryVoiceLabel } from "./voicePerspective"; -import { Component, lazy, Suspense, useCallback, useEffect, useEffectEvent, useRef, useState, type ReactNode } from "react"; +import { Component, Fragment, lazy, Suspense, useCallback, useEffect, useEffectEvent, useRef, useState, type ReactNode } from "react"; import { useAuth } from "react-oidc-context"; import { askPostChat, @@ -139,8 +139,11 @@ import { } from "./leftoverMapCoverage"; import { leftoverMapCompareAxisShare, + leftoverMapCompareAxisSingular, LEFTOVER_MAP_COMPARE_AXIS_SHARE, LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_LABEL, } from "./leftoverMapCompareAxis"; import "./App.css"; @@ -4056,17 +4059,29 @@ function ReportsPanel({ ) : null} {row.leftover_map_axes?.map((axis) => { const comparisonAxisShare = leftoverMapCompareAxisShare(axis); - if (comparisonAxisShare === null) { + const comparisonAxisSingular = leftoverMapCompareAxisSingular(axis); + if (comparisonAxisShare === null && comparisonAxisSingular === null) { return null; } return ( - - {tf(LEFTOVER_MAP_COMPARE_AXIS_SHARE, comparisonAxisShare)} - + + {comparisonAxisShare !== null ? ( + + {tf(LEFTOVER_MAP_COMPARE_AXIS_SHARE, comparisonAxisShare)} + + ) : null} + {comparisonAxisSingular !== null ? ( + + {tf(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, comparisonAxisSingular)} + + ) : null} + ); })} {row.leftover_pairs && row.leftover_pairs.length > 0 && ( diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index fed808ca9..5f0c4a23d 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -89,7 +89,8 @@ export type LeftoverPairListProps = { * grouping comparison strip, not this pair list. ADR 0292 names persisted leftover-map * incomplete item coverage on the grouping comparison strip, not this pair list. ADR 0293 * names persisted leftover-map axis share on the grouping comparison strip, not this pair - * list. Every badge still + * list. ADR 0294 names persisted leftover-map singular values on the grouping comparison + * strip, not this pair list. 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 c63d1b5b2..62e6e9fcc 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -44,6 +44,7 @@ describe("i18n", () => { "Answer", "leftover axis {axis} {share}%", "leftover map comparison axis {axis} {share}%", + "leftover map comparison axis {axis} σ {value}", "Leftover-map axis share", "Leftover-map axis share is Gabriel inertia of residual SVD axes 1 and 2. Open a leftover pair to read the post–criterion cell. The shares do not invent a leftover score.", "Leftover pairs", @@ -83,6 +84,7 @@ describe("i18n", () => { "Leftover map comparison incomplete posts", "Leftover map comparison incomplete items", "Leftover map comparison axis share", + "Leftover map comparison axis singular", "Leftover-map graphic item coverage", "Leftover map item coverage", "Leftover map incomplete posts", diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 3f186c2c2..dfa41d366 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -274,6 +274,8 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison incomplete items": "잔여 지도 비교 불완전 기준", "Leftover map comparison axis share": "잔여 지도 비교 축 비율", "leftover map comparison axis {axis} {share}%": "잔여 지도 비교 축 {axis} {share}%", + "Leftover map comparison axis singular": "잔여 지도 비교 축 특이값", + "leftover map comparison axis {axis} σ {value}": "잔여 지도 비교 축 {axis} σ {value}", "Leftover map item coverage": "잔여 지도 기준 포함 범위", "Leftover map incomplete posts": "잔여 지도 불완전 글", "Leftover map incomplete items": "잔여 지도 불완전 기준", @@ -886,6 +888,8 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison incomplete items": "残差地图比较不完整准则", "Leftover map comparison axis share": "残差地图比较轴占比", "leftover map comparison axis {axis} {share}%": "残差地图比较轴 {axis} {share}%", + "Leftover map comparison axis singular": "残差地图比较轴奇异值", + "leftover map comparison axis {axis} σ {value}": "残差地图比较轴 {axis} σ {value}", "Leftover map item coverage": "残差地图准则覆盖范围", "Leftover map incomplete posts": "残差地图不完整帖文", "Leftover map incomplete items": "残差地图不完整准则", @@ -1519,6 +1523,8 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison incomplete items": "残差マップの比較不完全基準", "Leftover map comparison axis share": "残差マップの比較軸の比率", "leftover map comparison axis {axis} {share}%": "残差マップの比較軸 {axis} {share}%", + "Leftover map comparison axis singular": "残差マップの比較軸の特異値", + "leftover map comparison axis {axis} σ {value}": "残差マップの比較軸 {axis} σ {value}", "Leftover map item coverage": "残差マップの基準対象範囲", "Leftover map incomplete posts": "残差マップの不完全投稿", "Leftover map incomplete items": "残差マップの不完全基準", @@ -2131,6 +2137,8 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison incomplete items": "Tiêu chí không đầy đủ so sánh trên bản đồ phần dư", "Leftover map comparison axis share": "Tỷ trọng trục so sánh bản đồ phần dư", "leftover map comparison axis {axis} {share}%": "trục so sánh bản đồ phần dư {axis} {share}%", + "Leftover map comparison axis singular": "Giá trị singular trục so sánh bản đồ phần dư", + "leftover map comparison axis {axis} σ {value}": "trục so sánh bản đồ phần dư {axis} σ {value}", "Leftover map item coverage": "Phạm vi tiêu chí bản đồ phần dư", "Leftover map incomplete posts": "Bài không đầy đủ trên bản đồ phần dư", "Leftover map incomplete items": "Tiêu chí không đầy đủ trên bản đồ phần dư", diff --git a/frontend/src/leftoverMapCompareAxis.test.ts b/frontend/src/leftoverMapCompareAxis.test.ts index 1a9ac333d..858438d69 100644 --- a/frontend/src/leftoverMapCompareAxis.test.ts +++ b/frontend/src/leftoverMapCompareAxis.test.ts @@ -1,10 +1,15 @@ import { describe, expect, it } from "vitest"; import { leftoverMapCompareAxisShare, + leftoverMapCompareAxisSingular, LEFTOVER_MAP_COMPARE_AXIS_SHARE, LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_LABEL, LEFTOVER_MAP_LIST_AXIS_SHARE, + LEFTOVER_MAP_LIST_AXIS_SINGULAR, LEFTOVER_MAP_PLOT_AXIS_SHARE, + LEFTOVER_MAP_PLOT_AXIS_SINGULAR, } from "./leftoverMapCompareAxis"; describe("leftoverMapCompareAxisShare", () => { @@ -63,3 +68,64 @@ describe("leftoverMapCompareAxisShare", () => { expect(LEFTOVER_MAP_COMPARE_AXIS_SHARE).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE); }); }); + +describe("leftoverMapCompareAxisSingular", () => { + it("names persisted leftover-map singular values without inventing a leftover score", () => { + expect( + leftoverMapCompareAxisSingular({ axis_index: 1, leftover_singular_value: 1.84 }), + ).toEqual({ axis: 1, value: "1.84" }); + expect( + leftoverMapCompareAxisSingular({ axis_index: 2, leftover_singular_value: 0.86 }), + ).toEqual({ axis: 2, value: "0.86" }); + }); + + it("names a rank-0 zero leftover-map singular value", () => { + expect( + leftoverMapCompareAxisSingular({ axis_index: 1, leftover_singular_value: 0 }), + ).toEqual({ axis: 1, value: "0.00" }); + }); + + it("omits leftover-map singular values that are missing, non-finite, or negative", () => { + expect(leftoverMapCompareAxisSingular(null)).toBeNull(); + expect(leftoverMapCompareAxisSingular(undefined)).toBeNull(); + expect(leftoverMapCompareAxisSingular({ axis_index: 1 })).toBeNull(); + expect( + leftoverMapCompareAxisSingular({ axis_index: 1, leftover_singular_value: Number.NaN }), + ).toBeNull(); + expect( + leftoverMapCompareAxisSingular({ + axis_index: 1, + leftover_singular_value: Number.POSITIVE_INFINITY, + }), + ).toBeNull(); + expect( + leftoverMapCompareAxisSingular({ axis_index: 1, leftover_singular_value: -0.01 }), + ).toBeNull(); + }); + + it("does not invent leftover-map singular value from leftover-map axis share", () => { + expect( + leftoverMapCompareAxisSingular({ + axis_index: 1, + leftover_singular_value: Number.NaN, + }), + ).toBeNull(); + expect( + leftoverMapCompareAxisSingular({ axis_index: 1, leftover_singular_value: 1.84 }), + ).not.toEqual({ axis: 1, value: "82" }); + }); + + it("keeps the grouping comparison leftover-map singular caption distinct from leftover-axis badges and graphic axes", () => { + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_LABEL).toBe( + "Leftover map comparison axis singular", + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_LABEL).not.toBe( + LEFTOVER_MAP_COMPARE_AXIS_SHARE_LABEL, + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).toBe( + "leftover map comparison axis {axis} σ {value}", + ); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_LIST_AXIS_SINGULAR); + expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR); + }); +}); diff --git a/frontend/src/leftoverMapCompareAxis.ts b/frontend/src/leftoverMapCompareAxis.ts index 0ce1c330e..f7d574c90 100644 --- a/frontend/src/leftoverMapCompareAxis.ts +++ b/frontend/src/leftoverMapCompareAxis.ts @@ -1,4 +1,4 @@ -/** Caption leftover-map axis share on the grouping comparison strip (ADR 0293). */ +/** Caption leftover-map axis share and singular values on the grouping comparison strip (ADR 0293 / ADR 0294). */ import type { LeftoverMapAxis } from "./api"; @@ -10,11 +10,24 @@ export const LEFTOVER_MAP_LIST_AXIS_SHARE = "leftover axis {axis} {share}%"; export const LEFTOVER_MAP_PLOT_AXIS_SHARE = "leftover-map axis {axis} ({share}%)"; +export const LEFTOVER_MAP_COMPARE_AXIS_SINGULAR_LABEL = "Leftover map comparison axis singular"; + +export const LEFTOVER_MAP_COMPARE_AXIS_SINGULAR = "leftover map comparison axis {axis} σ {value}"; + +export const LEFTOVER_MAP_LIST_AXIS_SINGULAR = "leftover axis {axis} σ {value} {share}%"; + +export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR = "leftover-map axis {axis} σ {value}"; + export type LeftoverMapCompareAxisShare = { axis: number; share: string; }; +export type LeftoverMapCompareAxisSingular = { + axis: number; + value: string; +}; + export function leftoverMapCompareAxisShare( axis: Pick | null | undefined, ): LeftoverMapCompareAxisShare | null { @@ -32,3 +45,25 @@ export function leftoverMapCompareAxisShare( share: (axis.leftover_share * 100).toFixed(0), }; } + +export function leftoverMapCompareAxisSingular( + axis: Pick | null | undefined, +): LeftoverMapCompareAxisSingular | null { + if (axis == null) { + return null; + } + if (!Number.isInteger(axis.axis_index) || axis.axis_index < 1) { + return null; + } + if ( + axis.leftover_singular_value == null || + !Number.isFinite(axis.leftover_singular_value) || + axis.leftover_singular_value < 0 + ) { + return null; + } + return { + axis: axis.axis_index, + value: axis.leftover_singular_value.toFixed(2), + }; +} diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 082764847..4da4f0cb5 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -20,8 +20,9 @@ * (ADR 0289), grouping comparison item complete-case coverage * (ADR 0290), grouping comparison incomplete post coverage * (ADR 0291), grouping comparison incomplete item coverage - * (ADR 0292), and grouping comparison leftover-map axis share - * (ADR 0293) caption the pair list or the grouping comparison strip, + * (ADR 0292), grouping comparison leftover-map axis share + * (ADR 0293), and grouping comparison leftover-map singular values + * (ADR 0294) caption the pair list or the grouping comparison strip, * not this graphic layout. */ diff --git a/pyproject.toml b/pyproject.toml index 1ed881f7d..33c2ed641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.50.0" +version = "2.51.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 aa54b5e473ac133ea8a3327aae9366dbeb0f096a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:29:07 +0900 Subject: [PATCH 02/11] test(reports): require current axis-singular release authority Add the persistent governance contract before allocating ADR 0370 / v2.56.0 so the reconstructed #830 delta cannot inherit the predecessor release identity. --- ...compare_axis_singular_release_authority.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_leftover_map_compare_axis_singular_release_authority.py diff --git a/tests/test_leftover_map_compare_axis_singular_release_authority.py b/tests/test_leftover_map_compare_axis_singular_release_authority.py new file mode 100644 index 000000000..2d8eb440e --- /dev/null +++ b/tests/test_leftover_map_compare_axis_singular_release_authority.py @@ -0,0 +1,36 @@ +"""ADR and release-identity regression for grouping-comparison axis singular values.""" + +from __future__ import annotations + +import json +import re +import tomllib +from pathlib import Path + +_ROOT = Path(__file__).resolve().parents[1] +_ADR_DIRECTORY = _ROOT / "docs" / "adr" + + +def test_axis_singular_decision_uses_unclaimed_proposed_adr_0370() -> None: + """Keep the reconstructed axis-singular decision off historical ADR 0294.""" + canonical = _ADR_DIRECTORY / "0370-leftover-map-compare-axis-singular.md" + + assert canonical.exists(), "axis-singular decision must use unclaimed ADR 0370" + content = canonical.read_text(encoding="utf-8") + assert content.startswith("# ADR 0370 —") + assert "**Decision status:** Proposed" in content + assert "**Decision status:** Accepted" not in content + assert "v2.56.0" in content + + +def test_axis_singular_release_identity_is_2560_everywhere() -> None: + """Keep the reconstructed feature on one unreleased product identity.""" + project = tomllib.loads((_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + frontend = json.loads((_ROOT / "frontend" / "package.json").read_text(encoding="utf-8")) + runtime = (_ROOT / "lineageweave" / "__init__.py").read_text(encoding="utf-8") + match = re.search(r'^__version__ = "([^"]+)"$', runtime, flags=re.MULTILINE) + + assert match is not None + assert project["project"]["version"] == "2.56.0" + assert frontend["version"] == "2.56.0" + assert match.group(1) == "2.56.0" From 8f6e17c7ca85de34b54e590025b2d72808109b16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:31:22 +0900 Subject: [PATCH 03/11] docs(reports): allocate ADR 0370 and v2.56.0 Repair the reconstructed #830 release identity without reusing historical ADR 0294 / v2.51.0. Keep the decision Proposed, synchronize Python/frontend package versions, and record the persisted axis-singular product contract. --- ...56.0-leftover-map-compare-axis-singular.md | 3 +++ ...0370-leftover-map-compare-axis-singular.md | 27 +++++++++++++++++++ frontend/package.json | 2 +- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.d/2.56.0-leftover-map-compare-axis-singular.md create mode 100644 docs/adr/0370-leftover-map-compare-axis-singular.md diff --git a/CHANGELOG.d/2.56.0-leftover-map-compare-axis-singular.md b/CHANGELOG.d/2.56.0-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..82b7f1d12 --- /dev/null +++ b/CHANGELOG.d/2.56.0-leftover-map-compare-axis-singular.md @@ -0,0 +1,3 @@ +# v2.56.0 — grouping-comparison leftover-map axis singular values + +Grouping-comparison rows now surface persisted finite, non-negative leftover-map singular values `σ_k` for Gabriel axes under ADR 0370. Persisted zero remains visible as `0.00`; missing, non-finite, or negative values omit only the singular caption. Axis share and singular availability are independent, and LineageWeave never derives `σ_k` from any UI-visible or psychometric proxy. diff --git a/docs/adr/0370-leftover-map-compare-axis-singular.md b/docs/adr/0370-leftover-map-compare-axis-singular.md new file mode 100644 index 000000000..8e7c9799f --- /dev/null +++ b/docs/adr/0370-leftover-map-compare-axis-singular.md @@ -0,0 +1,27 @@ +# ADR 0370 — Persisted leftover-map singular values in grouping comparison axes + +**Decision status:** Proposed + +## Problem +The grouping-comparison read model persists `leftover_singular_value` for each Gabriel leftover-map axis, but exact predecessor #829 (`0e4fd5815686120ec66203cb6848834b56bdf289`, ADR 0369 / v2.55.0) does not expose that scale on the comparison strip. Historical #830 demonstrated the buyer-visible delta on stale ancestry and historical ADR 0294 / v2.51.0, which are evidence only because those identities now belong to different serialized decisions. + +## Constraints +- Consume only persisted `leftover_singular_value`; LineageWeave must not derive `σ_k` from axis share, pair or marker counts, coordinates, distance, rank, coverage, incomplete counts, reconstruction, or any other psychometric value. +- Accept only finite, non-negative persisted values. Persisted zero is meaningful and must remain visible as `0.00`; missing, non-finite, or negative values omit only the singular-value caption. +- Singular-value availability and axis-share availability are independent. An invalid or missing value on one axis metric must not suppress a valid value on the other. +- Preserve the existing full-visible-grouping authorization boundary for persisted grouping aggregates; do not reconstruct a hidden population from caller-visible members. +- The comparison caption remains a LineageWeave read-model/UI concern. Psychometric estimation and true-parameter validation remain owned by the released `fast-mlsirm` boundary. +- The canonical eight-locale translation resource remains the versioned database ledger serialized through #922/#929/#932. This decision must not create a competing inline ES/DE/FR store. + +## Alternatives and decision +Omitting `σ_k` hides persisted Gabriel scale that is already available in the authorized read model. Recomputing it in the client would duplicate psychometric authority and can diverge from the persisted factorization. Coupling singular display to axis-share validity would also discard valid evidence. Therefore the grouping-comparison strip consumes the persisted scalar directly, formats finite non-negative values independently from axis share, preserves zero, and fail-closes only the invalid singular caption. + +This is the serialized successor of exact #829 and allocates v2.56.0. Historical #830 ADR 0294 / v2.51.0 remains provenance only; current ADR 0294 continues to govern grouping-comparison incomplete-item coverage. + +## Verification +A current-parent test must first RED because exact #829 lacks the grouping-comparison singular formatter. The causal implementation then requires focused formatter/render/API regressions, the full PostgreSQL-backed backend suite, frontend lint/test/build/Storybook, release-version parity, and a clean exact-head tree. Material UI completion additionally requires current-head responsive, keyboard, focus, and accessibility evidence. Hosted security gates and qualifying independent review remain separate merge gates; this ADR stays Proposed until the protected-main release is actually accepted. + +## Evidence +Gabriel, K. R. (1971). The biplot graphic display of matrices with application to principal component analysis. *Biometrika, 58*(3), 453–467. + +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. diff --git a/frontend/package.json b/frontend/package.json index 228735341..4f0158ad8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.55.0", + "version": "2.56.0", "type": "module", "scripts": { "dev": "vite", diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index 15a8d1d08..de9e32170 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -130,4 +130,4 @@ "serialize_lineage_analysis_result", ] -__version__ = "2.55.0" +__version__ = "2.56.0" diff --git a/pyproject.toml b/pyproject.toml index 61be353ce..09a2849c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.55.0" +version = "2.56.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 5e7756b224ea0101a84c0ab917218582811105b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:42:13 +0900 Subject: [PATCH 04/11] test(reports): bind axis-singular release to universal lock --- ..._leftover_map_compare_axis_singular_release_authority.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_leftover_map_compare_axis_singular_release_authority.py b/tests/test_leftover_map_compare_axis_singular_release_authority.py index 2d8eb440e..b77eec7ec 100644 --- a/tests/test_leftover_map_compare_axis_singular_release_authority.py +++ b/tests/test_leftover_map_compare_axis_singular_release_authority.py @@ -28,9 +28,15 @@ def test_axis_singular_release_identity_is_2560_everywhere() -> None: project = tomllib.loads((_ROOT / "pyproject.toml").read_text(encoding="utf-8")) frontend = json.loads((_ROOT / "frontend" / "package.json").read_text(encoding="utf-8")) runtime = (_ROOT / "lineageweave" / "__init__.py").read_text(encoding="utf-8") + lock = tomllib.loads((_ROOT / "uv.lock").read_text(encoding="utf-8")) + locked_lineageweave = [ + package for package in lock["package"] if package.get("name") == "lineageweave" + ] match = re.search(r'^__version__ = "([^"]+)"$', runtime, flags=re.MULTILINE) assert match is not None assert project["project"]["version"] == "2.56.0" assert frontend["version"] == "2.56.0" assert match.group(1) == "2.56.0" + assert len(locked_lineageweave) == 1 + assert locked_lineageweave[0]["version"] == "2.56.0" From 704bd36763cb796037052f715bbe8e5b5fa2269d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:44:29 +0900 Subject: [PATCH 05/11] ci(release): regenerate v2.56 universal lock once --- .github/workflows/temporary-uv-lock-830.yml | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/temporary-uv-lock-830.yml diff --git a/.github/workflows/temporary-uv-lock-830.yml b/.github/workflows/temporary-uv-lock-830.yml new file mode 100644 index 000000000..577437332 --- /dev/null +++ b/.github/workflows/temporary-uv-lock-830.yml @@ -0,0 +1,65 @@ +name: Temporary v2.56 lock regeneration + +on: + push: + branches: + - feat/leftover-map-compare-axis-singular-v2510 + +permissions: + contents: write + +jobs: + regenerate: + if: ${{ !contains(github.event.head_commit.message, '[lock-regenerated]') }} + runs-on: ubuntu-latest + steps: + - name: Checkout exact branch head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up pinned uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Regenerate and constrain lock delta + shell: bash + run: | + set -euo pipefail + uv lock + git diff --exit-code -- . ':(exclude)uv.lock' + python - <<'PY' + import subprocess + + diff = subprocess.check_output( + ["git", "diff", "--unified=0", "--", "uv.lock"], text=True + ) + added = [ + line for line in diff.splitlines() + if line.startswith("+") and not line.startswith("+++") + ] + removed = [ + line for line in diff.splitlines() + if line.startswith("-") and not line.startswith("---") + ] + assert removed == ['-version = "2.37.0"'], diff + assert added == ['+version = "2.56.0"'], diff + PY + git diff --check + + - name: Commit generated universal lock non-force + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add uv.lock + git commit -m "chore(release): regenerate v2.56 universal lock [lock-regenerated]" + git push origin "HEAD:${GITHUB_REF_NAME}" From 9b06dce1b39a5337d669f96130a7a1d9d4d0b17d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:44:50 +0000 Subject: [PATCH 06/11] chore(release): regenerate v2.56 universal lock [lock-regenerated] --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 0e7d2ce9d..879d38ff5 100644 --- a/uv.lock +++ b/uv.lock @@ -685,7 +685,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.37.0" +version = "2.56.0" source = { editable = "." } dependencies = [ { name = "certifi" }, From fe480375fb12ae7f58d3d39fbc1bd980f38bd62f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:45:20 +0900 Subject: [PATCH 07/11] ci(release): remove completed v2.56 lock regeneration workflow --- .github/workflows/temporary-uv-lock-830.yml | 65 --------------------- 1 file changed, 65 deletions(-) delete mode 100644 .github/workflows/temporary-uv-lock-830.yml diff --git a/.github/workflows/temporary-uv-lock-830.yml b/.github/workflows/temporary-uv-lock-830.yml deleted file mode 100644 index 577437332..000000000 --- a/.github/workflows/temporary-uv-lock-830.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Temporary v2.56 lock regeneration - -on: - push: - branches: - - feat/leftover-map-compare-axis-singular-v2510 - -permissions: - contents: write - -jobs: - regenerate: - if: ${{ !contains(github.event.head_commit.message, '[lock-regenerated]') }} - runs-on: ubuntu-latest - steps: - - name: Checkout exact branch head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Set up pinned uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - with: - version: "0.11.28" - enable-cache: false - - - name: Regenerate and constrain lock delta - shell: bash - run: | - set -euo pipefail - uv lock - git diff --exit-code -- . ':(exclude)uv.lock' - python - <<'PY' - import subprocess - - diff = subprocess.check_output( - ["git", "diff", "--unified=0", "--", "uv.lock"], text=True - ) - added = [ - line for line in diff.splitlines() - if line.startswith("+") and not line.startswith("+++") - ] - removed = [ - line for line in diff.splitlines() - if line.startswith("-") and not line.startswith("---") - ] - assert removed == ['-version = "2.37.0"'], diff - assert added == ['+version = "2.56.0"'], diff - PY - git diff --check - - - name: Commit generated universal lock non-force - shell: bash - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add uv.lock - git commit -m "chore(release): regenerate v2.56 universal lock [lock-regenerated]" - git push origin "HEAD:${GITHUB_REF_NAME}" From 63a7f959e756beacc5f2ffb116f7f4fdd78ac402 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:53:26 +0900 Subject: [PATCH 08/11] test(reports): cover large finite axis singular formatting --- frontend/src/leftoverMapCompareAxis.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/leftoverMapCompareAxis.test.ts b/frontend/src/leftoverMapCompareAxis.test.ts index 858438d69..933f0cb23 100644 --- a/frontend/src/leftoverMapCompareAxis.test.ts +++ b/frontend/src/leftoverMapCompareAxis.test.ts @@ -85,6 +85,12 @@ describe("leftoverMapCompareAxisSingular", () => { ).toEqual({ axis: 1, value: "0.00" }); }); + it("formats a finite 1e21 singular value to two decimals without exponential notation", () => { + expect( + leftoverMapCompareAxisSingular({ axis_index: 1, leftover_singular_value: 1e21 }), + ).toEqual({ axis: 1, value: "1000000000000000000000.00" }); + }); + it("omits leftover-map singular values that are missing, non-finite, or negative", () => { expect(leftoverMapCompareAxisSingular(null)).toBeNull(); expect(leftoverMapCompareAxisSingular(undefined)).toBeNull(); @@ -128,4 +134,4 @@ describe("leftoverMapCompareAxisSingular", () => { expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_LIST_AXIS_SINGULAR); expect(LEFTOVER_MAP_COMPARE_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR); }); -}); +}); \ No newline at end of file From 09a3a8fdf0f66821808c8859e5a0e76ec6d31eea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 23:56:02 +0900 Subject: [PATCH 09/11] fix(reports): keep large axis singular values fixed-decimal --- frontend/src/leftoverMapCompareAxis.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/leftoverMapCompareAxis.ts b/frontend/src/leftoverMapCompareAxis.ts index 7e671110b..065653805 100644 --- a/frontend/src/leftoverMapCompareAxis.ts +++ b/frontend/src/leftoverMapCompareAxis.ts @@ -32,6 +32,12 @@ type LeftoverMapCompareAxisSingularInput = Pick & Partial>; +const LARGE_FIXED_TWO_DECIMAL = new Intl.NumberFormat("en-US", { + useGrouping: false, + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + export function leftoverMapCompareAxisShare( axis: Pick | null | undefined, ): LeftoverMapCompareAxisShare | null { @@ -74,8 +80,10 @@ export function leftoverMapCompareAxisSingular( ) { return null; } + const value = axis.leftover_singular_value; return { axis: axis.axis_index, - value: axis.leftover_singular_value.toFixed(2), + // ECMAScript toFixed switches to exponential notation at 1e21. + value: value >= 1e21 ? LARGE_FIXED_TWO_DECIMAL.format(value) : value.toFixed(2), }; -} +} \ No newline at end of file From d4aa190a2608c3f12118f6c6a58e6f85550fb07b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 00:00:23 +0900 Subject: [PATCH 10/11] ci(reports): repair finite-axis assertion once --- .../workflows/temporary-axis-finite-830.yml | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .github/workflows/temporary-axis-finite-830.yml diff --git a/.github/workflows/temporary-axis-finite-830.yml b/.github/workflows/temporary-axis-finite-830.yml new file mode 100644 index 000000000..e2c1d61f6 --- /dev/null +++ b/.github/workflows/temporary-axis-finite-830.yml @@ -0,0 +1,126 @@ +name: Temporary #830 finite-axis assertion repair + +on: + push: + branches: + - feat/leftover-map-compare-axis-singular-v2510 + +permissions: + contents: write + pull-requests: read + +concurrency: + group: temporary-axis-finite-830 + cancel-in-progress: false + +jobs: + repair: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + services: + postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + LINEAGEWEAVE_TEST_POSTGRES_ADMIN_DSN: postgresql://postgres:postgres@localhost:5432/postgres + EXPECTED_PRODUCT_PARENT: 09a3a8fdf0f66821808c8859e5a0e76ec6d31eea + PRODUCT_BRANCH: feat/leftover-map-compare-axis-singular-v2510 + steps: + - name: Checkout exact workflow head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Verify exact authority + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + test "$(git rev-parse HEAD^)" = "$EXPECTED_PRODUCT_PARENT" + live_head="$(gh api repos/${GITHUB_REPOSITORY}/pulls/830 --jq .head.sha)" + test "$live_head" = "$GITHUB_SHA" + test "$(git status --porcelain)" = "" + + - name: Apply finite-value assertion repair + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + path = Path("backend/tests/test_api.py") + text = path.read_text() + old_general = ' assert all(axis["leftover_singular_value"] >= 0 for axis in leftover_axes)\n' + new_general = ( + ' assert all(\n' + ' math.isfinite(axis["leftover_singular_value"])\n' + ' and axis["leftover_singular_value"] >= 0\n' + ' for axis in leftover_axes\n' + ' )\n' + ) + old_compare = ( + ' assert all(\n' + ' axis["leftover_singular_value"] == axis["leftover_singular_value"]\n' + ' and axis["leftover_singular_value"] >= 0\n' + ' for axis in leftover_compare_axes\n' + ' )\n' + ) + new_compare = ( + ' assert all(\n' + ' math.isfinite(axis["leftover_singular_value"])\n' + ' and axis["leftover_singular_value"] >= 0\n' + ' for axis in leftover_compare_axes\n' + ' )\n' + ) + if text.count(old_general) != 1: + raise SystemExit("expected general singular assertion exactly once") + if text.count(old_compare) != 1: + raise SystemExit("expected comparison singular assertion exactly once") + path.write_text(text.replace(old_general, new_general, 1).replace(old_compare, new_compare, 1)) + PY + git diff --check + git diff -- backend/tests/test_api.py + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up locked dependency manager + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Select pinned Rust toolchain + run: | + rustup toolchain install 1.97.1 --profile minimal + rustup default 1.97.1 + + - name: Install committed universal lock + run: uv sync --frozen --extra dev --extra backend + + - name: Full PostgreSQL-backed GREEN + run: uv run --frozen python -m pytest -q + + - name: Remove temporary workflow and promote repair non-force + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + live_head="$(gh api repos/${GITHUB_REPOSITORY}/pulls/830 --jq .head.sha)" + test "$live_head" = "$GITHUB_SHA" + git rm .github/workflows/temporary-axis-finite-830.yml + git add backend/tests/test_api.py + git diff --cached --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "test(reports): require finite persisted axis singular values" + git push origin HEAD:"$PRODUCT_BRANCH" From b36b2534c9b3a63b66c56ae22ff3dcad40ae5c67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:54:05 +0000 Subject: [PATCH 11/11] test(reports): require finite persisted axis singular values --- .../workflows/temporary-axis-finite-830.yml | 126 ------------------ backend/tests/test_api.py | 8 +- 2 files changed, 6 insertions(+), 128 deletions(-) delete mode 100644 .github/workflows/temporary-axis-finite-830.yml diff --git a/.github/workflows/temporary-axis-finite-830.yml b/.github/workflows/temporary-axis-finite-830.yml deleted file mode 100644 index e2c1d61f6..000000000 --- a/.github/workflows/temporary-axis-finite-830.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: Temporary #830 finite-axis assertion repair - -on: - push: - branches: - - feat/leftover-map-compare-axis-singular-v2510 - -permissions: - contents: write - pull-requests: read - -concurrency: - group: temporary-axis-finite-830 - cancel-in-progress: false - -jobs: - repair: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - services: - postgres: - image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 - env: - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U postgres" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - LINEAGEWEAVE_TEST_POSTGRES_ADMIN_DSN: postgresql://postgres:postgres@localhost:5432/postgres - EXPECTED_PRODUCT_PARENT: 09a3a8fdf0f66821808c8859e5a0e76ec6d31eea - PRODUCT_BRANCH: feat/leftover-map-compare-axis-singular-v2510 - steps: - - name: Checkout exact workflow head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Verify exact authority - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - test "$(git rev-parse HEAD^)" = "$EXPECTED_PRODUCT_PARENT" - live_head="$(gh api repos/${GITHUB_REPOSITORY}/pulls/830 --jq .head.sha)" - test "$live_head" = "$GITHUB_SHA" - test "$(git status --porcelain)" = "" - - - name: Apply finite-value assertion repair - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - path = Path("backend/tests/test_api.py") - text = path.read_text() - old_general = ' assert all(axis["leftover_singular_value"] >= 0 for axis in leftover_axes)\n' - new_general = ( - ' assert all(\n' - ' math.isfinite(axis["leftover_singular_value"])\n' - ' and axis["leftover_singular_value"] >= 0\n' - ' for axis in leftover_axes\n' - ' )\n' - ) - old_compare = ( - ' assert all(\n' - ' axis["leftover_singular_value"] == axis["leftover_singular_value"]\n' - ' and axis["leftover_singular_value"] >= 0\n' - ' for axis in leftover_compare_axes\n' - ' )\n' - ) - new_compare = ( - ' assert all(\n' - ' math.isfinite(axis["leftover_singular_value"])\n' - ' and axis["leftover_singular_value"] >= 0\n' - ' for axis in leftover_compare_axes\n' - ' )\n' - ) - if text.count(old_general) != 1: - raise SystemExit("expected general singular assertion exactly once") - if text.count(old_compare) != 1: - raise SystemExit("expected comparison singular assertion exactly once") - path.write_text(text.replace(old_general, new_general, 1).replace(old_compare, new_compare, 1)) - PY - git diff --check - git diff -- backend/tests/test_api.py - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Set up locked dependency manager - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - with: - version: "0.11.28" - enable-cache: false - - - name: Select pinned Rust toolchain - run: | - rustup toolchain install 1.97.1 --profile minimal - rustup default 1.97.1 - - - name: Install committed universal lock - run: uv sync --frozen --extra dev --extra backend - - - name: Full PostgreSQL-backed GREEN - run: uv run --frozen python -m pytest -q - - - name: Remove temporary workflow and promote repair non-force - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - live_head="$(gh api repos/${GITHUB_REPOSITORY}/pulls/830 --jq .head.sha)" - test "$live_head" = "$GITHUB_SHA" - git rm .github/workflows/temporary-axis-finite-830.yml - git add backend/tests/test_api.py - git diff --cached --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "test(reports): require finite persisted axis singular values" - git push origin HEAD:"$PRODUCT_BRANCH" diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index c20a110fc..3fe3ee935 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -5778,7 +5778,11 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, assert explained_share + unexplained_share + share == pytest.approx(1.0) leftover_axes = high_report.get("leftover_map_axes", []) assert [axis["axis_index"] for axis in leftover_axes] == [1, 2] - assert all(axis["leftover_singular_value"] >= 0 for axis in leftover_axes) + assert all( + math.isfinite(axis["leftover_singular_value"]) + and axis["leftover_singular_value"] >= 0 + for axis in leftover_axes + ) assert all(0.0 <= axis["leftover_share"] <= 1.0 for axis in leftover_axes) leftover_coverage = high_report.get("leftover_map_coverage") assert leftover_coverage is not None @@ -5849,7 +5853,7 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, isinstance(axis["leftover_singular_value"], (int, float)) for axis in leftover_compare_axes ) assert all( - axis["leftover_singular_value"] == axis["leftover_singular_value"] + math.isfinite(axis["leftover_singular_value"]) and axis["leftover_singular_value"] >= 0 for axis in leftover_compare_axes )