From d9972c260b1864e0b6391cbf050686e0bdd73359 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 08:17:31 +0000 Subject: [PATCH 01/11] feat(reports): leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers independently of leftover-map criterion leftover-map item coordinates (v2.94.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caption leftover-map comparison graphic leftover-map post markers with persisted leftover-map person coordinates through leftoverMapComparePlotPostBadge independently of leftover-map criterion leftover-map item coordinates (ADR 0337). Rank-0 unused axes still name leftover-map person coordinates (0.00, 0.00). A missing or non-finite leftover-map person coordinate pair omits independently of leftover-map criterion ζ. Do not invent leftover-map person coordinates from leftover-map item coordinates ζ. UI-only; no SQL; never invent a leftover score or a theta. --- AGENTS.md | 13 ++ ARCHITECTURE.md | 6 +- ...tover-map-compare-plot-post-coordinates.md | 22 +++ CHANGELOG.md | 21 +++ CLAUDE.md | 2 +- docs/adr/0267-leftover-map-coordinates.md | 4 +- docs/adr/0268-leftover-map-graphic-display.md | 4 +- docs/adr/0304-leftover-map-compare-graphic.md | 4 +- ...0334-leftover-map-axis-tick-share-badge.md | 4 +- ...-map-compare-plot-criterion-coordinates.md | 3 + ...tover-map-compare-plot-post-coordinates.md | 136 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 21 +++ docs/storybook-inventory.md | 4 +- frontend/package.json | 2 +- frontend/src/App.test.tsx | 13 +- .../src/components/LeftoverMapPlot.test.tsx | 20 +++ frontend/src/components/LeftoverMapPlot.tsx | 31 +++- frontend/src/i18n.test.ts | 17 +++ frontend/src/i18n.ts | 12 ++ frontend/src/leftoverMapPlotLayout.test.ts | 52 +++++++ frontend/src/leftoverMapPlotLayout.ts | 27 ++++ pyproject.toml | 2 +- 22 files changed, 400 insertions(+), 20 deletions(-) create mode 100644 CHANGELOG.d/2.94.0-leftover-map-compare-plot-post-coordinates.md create mode 100644 docs/adr/0337-leftover-map-compare-plot-post-coordinates.md diff --git a/AGENTS.md b/AGENTS.md index bcd435292..c1ec0adbc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -764,6 +764,15 @@ item coordinate caption and keeps `Criterion ζ {label}`. Rank-0 unused axes sti item coordinates `(0.00, 0.00)`. Do not invent leftover-map item coordinates from leftover-map post `ξ`. Do not invent leftover-map post `ξ` from leftover-map item coordinates. Leftover-map graphic leftover-map criterion markers stay `leftover-map criterion {label} at ζ {item}` this increment. +ADR 0337 captions leftover-map comparison graphic leftover-map post markers with persisted leftover-map +person coordinates when leftoverMapComparePlotPostBadge returns a usable leftover-map comparison graphic leftover-map post leftover-map +person coordinate caption. Distinct accessible name +`Open leftover map comparison graphic leftover-map post {title} at ξ {person}`. +A missing or non-finite leftover-map person coordinate pair omits that leftover-map comparison graphic leftover-map post leftover-map +person coordinate caption and keeps `Open leftover-map post {title}`. Rank-0 unused axes still name leftover-map +person coordinates `(0.00, 0.00)`. Do not invent leftover-map person coordinates from leftover-map item coordinates +`ζ`. Do not invent leftover-map item coordinates `ζ` from leftover-map person coordinates `ξ`. Leftover-map graphic leftover-map +post markers stay `Open leftover-map post {title} at ξ {person}` this increment. 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 @@ -913,6 +922,10 @@ Leftover-map comparison graphic leftover-map criterion leftover-map item coordin (ADR 0336) captions leftover-map comparison graphic leftover-map criterion markers with persisted leftover-map item coordinates when leftoverMapComparePlotCriterionBadge returns a usable leftover-map comparison graphic leftover-map criterion leftover-map item coordinate caption. +Leftover-map comparison graphic leftover-map post leftover-map person coordinates independently of leftover-map criterion leftover-map item coordinates +(ADR 0337) captions leftover-map comparison graphic leftover-map post markers with persisted leftover-map +person coordinates when leftoverMapComparePlotPostBadge returns a usable leftover-map comparison graphic leftover-map post leftover-map +person coordinate caption. 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 8d2f0ff80..81e84bcc8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -791,6 +791,9 @@ item coordinate caption independently of leftover-map post ξ markers. ADR 0336 captions leftover-map comparison graphic leftover-map criterion markers with persisted leftover-map item coordinates when leftoverMapComparePlotCriterionBadge returns a usable leftover-map comparison graphic leftover-map criterion leftover-map item coordinate caption independently of leftover-map post ξ markers. +ADR 0337 captions leftover-map comparison graphic leftover-map post markers with persisted leftover-map +person coordinates when leftoverMapComparePlotPostBadge returns a usable leftover-map comparison graphic leftover-map post leftover-map +person coordinate caption independently of leftover-map criterion leftover-map item coordinates. 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 @@ -857,7 +860,8 @@ comparison graphic leftover-map axis leftover-map singular values as leftoverMap (ADR 0333), leftover-axis tick leftover-map axis share independently of leftover-map singular values (ADR 0334), leftover-map graphic leftover-map criterion leftover-map item coordinates independently of leftover-map post ξ markers (ADR 0335), leftover-map comparison graphic leftover-map criterion leftover-map item coordinates independently of leftover-map post ξ markers -(ADR 0336); +(ADR 0336), leftover-map comparison graphic leftover-map post leftover-map person coordinates independently of leftover-map criterion leftover-map item coordinates +(ADR 0337); `POST .../rebuild` scores every grouping kind (post_admin). `make seed` folds A-100/B-200 Event Lineage fixtures (and the Riverbend calendar post) that already have constructed IRT cells into the same shared diff --git a/CHANGELOG.d/2.94.0-leftover-map-compare-plot-post-coordinates.md b/CHANGELOG.d/2.94.0-leftover-map-compare-plot-post-coordinates.md new file mode 100644 index 000000000..f7d1d64b0 --- /dev/null +++ b/CHANGELOG.d/2.94.0-leftover-map-compare-plot-post-coordinates.md @@ -0,0 +1,22 @@ +## 2.94.0 — Leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers independently of leftover-map criterion leftover-map item coordinates + +- Caption leftover-map comparison graphic leftover-map post markers with persisted leftover-map + person coordinates through leftoverMapComparePlotPostBadge independently of leftover-map + criterion leftover-map item coordinates (ADR 0337). After `make seed`, leftover-map comparison graphic leftover-map + post markers name persisted leftover-map person coordinates when leftoverMapComparePlotPostBadge + returns a usable leftover-map comparison graphic leftover-map post leftover-map person coordinate caption; click a leftover pair + or a leftover-map post marker opens that post. A missing or + non-finite leftover-map person coordinate pair omits that leftover-map comparison graphic leftover-map post leftover-map + person coordinate caption and keeps `Open leftover-map post {title}`. Rank-0 unused + axes still name leftover-map person coordinates `(0.00, 0.00)`. Leftover-map comparison graphic leftover-map + post markers stay + `Open leftover map comparison graphic leftover-map post {title} at ξ {person}` + when leftover-map person coordinates are finite so they stay distinct from leftover-map graphic leftover-map post markers + `Open leftover-map post {title} at ξ {person}` and from leftover-map comparison graphic leftover-map criterion markers + `leftover map comparison graphic leftover-map criterion {label} at ζ {item}`. Do not invent leftover-map + person coordinates from leftover-map item coordinates `ζ`. Do not invent leftover-map item coordinates `ζ` from leftover-map + person coordinates `ξ`. This increment does not change leftover-map graphic leftover-map + post markers, leftover-map graphic leftover-map criterion markers, leftover-axis ticks, leftover-map graphic leftover-map axis ticks, + leftover-map comparison leftover-axis ticks, or leftover-map comparison graphic leftover-map axis ticks. + A finite negative leftover is shown, never clamped. Never invent a leftover score. + Never invent a theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f473e3c..c67e10873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,27 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption leftover-map comparison graphic leftover-map + post markers with persisted leftover-map person coordinates through leftoverMapComparePlotPostBadge + independently of leftover-map criterion leftover-map item coordinates (ADR 0337 / v2.94.0). After `make seed`, + leftover-map comparison graphic leftover-map post markers name persisted leftover-map + person coordinates when leftoverMapComparePlotPostBadge returns a usable leftover-map comparison graphic leftover-map post + leftover-map person coordinate caption; click a leftover pair or a leftover-map post marker + opens that post. A missing or non-finite leftover-map person coordinate pair omits that leftover-map + comparison graphic leftover-map post leftover-map person coordinate caption and keeps + `Open leftover-map post {title}`. + Rank-0 unused axes still name leftover-map person coordinates `(0.00, 0.00)`. Leftover-map comparison graphic leftover-map + post markers stay + `Open leftover map comparison graphic leftover-map post {title} at ξ {person}` + when leftover-map person coordinates are finite so they stay distinct from leftover-map graphic leftover-map post markers + `Open leftover-map post {title} at ξ {person}` and from leftover-map comparison graphic leftover-map criterion markers + `leftover map comparison graphic leftover-map criterion {label} at ζ {item}`. Do not invent leftover-map + person coordinates from leftover-map item coordinates `ζ`. Do not invent leftover-map item coordinates `ζ` from leftover-map + person coordinates `ξ`. This increment does not change leftover-map graphic leftover-map + post markers, leftover-map graphic leftover-map criterion markers, leftover-axis ticks, leftover-map graphic leftover-map axis ticks, + leftover-map comparison leftover-axis ticks, or leftover-map comparison graphic leftover-map axis ticks. A finite negative leftover + is shown, never clamped. Never invent a leftover score or a theta. No new columns. + - Period leftover pairs now caption leftover-map comparison graphic leftover-map criterion markers with persisted leftover-map item coordinates through leftoverMapComparePlotCriterionBadge independently of leftover-map post ξ markers (ADR 0336 / v2.93.0). After `make seed`, diff --git a/CLAUDE.md b/CLAUDE.md index 28204800e..ec125a6ca 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 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324 / 0325 / 0326 / 0327 / 0328 / 0329 / 0330 / 0331 / 0332 / 0333 / 0334 / 0335 / 0336), 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 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321 / 0322 / 0323 / 0324 / 0325 / 0326 / 0327 / 0328 / 0329 / 0330 / 0331 / 0332 / 0333 / 0334 / 0335 / 0336 / 0337), occupational construct catalog search (ADR 0257), the text-channel embedding swap and cosine clamp (ADR 0190), per-edge channel-score persistence (ADR 0195), token-backed status notices (ADR 0220), diff --git a/docs/adr/0267-leftover-map-coordinates.md b/docs/adr/0267-leftover-map-coordinates.md index 31da6fb7b..a0bc006a3 100644 --- a/docs/adr/0267-leftover-map-coordinates.md +++ b/docs/adr/0267-leftover-map-coordinates.md @@ -10,7 +10,9 @@ [ADR 0335](0335-leftover-map-plot-criterion-coordinates.md) (leftover-map item coordinates on leftover-map graphic leftover-map criterion markers); [ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md) -(leftover-map item coordinates on leftover-map comparison graphic leftover-map criterion markers) +(leftover-map item coordinates on leftover-map comparison graphic leftover-map criterion markers); +[ADR 0337](0337-leftover-map-compare-plot-post-coordinates.md) +(leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers) Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md), [ADR 0049](0049-leftover-pair-report-ui.md), and diff --git a/docs/adr/0268-leftover-map-graphic-display.md b/docs/adr/0268-leftover-map-graphic-display.md index 2cdc0cf78..121e888a2 100644 --- a/docs/adr/0268-leftover-map-graphic-display.md +++ b/docs/adr/0268-leftover-map-graphic-display.md @@ -32,7 +32,9 @@ [ADR 0335](0335-leftover-map-plot-criterion-coordinates.md) (leftover-map item coordinates on leftover-map graphic leftover-map criterion markers); [ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md) -(leftover-map item coordinates on leftover-map comparison graphic leftover-map criterion markers) +(leftover-map item coordinates on leftover-map comparison graphic leftover-map criterion markers); +[ADR 0337](0337-leftover-map-compare-plot-post-coordinates.md) +(leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers) Amends [ADR 0049](0049-leftover-pair-report-ui.md) and [ADR 0267](0267-leftover-map-coordinates.md). Independent of leftover-map diff --git a/docs/adr/0304-leftover-map-compare-graphic.md b/docs/adr/0304-leftover-map-compare-graphic.md index 1396227fb..8d4dd35ed 100644 --- a/docs/adr/0304-leftover-map-compare-graphic.md +++ b/docs/adr/0304-leftover-map-compare-graphic.md @@ -14,7 +14,9 @@ [ADR 0309](0309-leftover-map-compare-plot-incomplete-item.md) (leftover-map incomplete item coverage on the grouping comparison leftover-map graphic), [ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md) -(leftover-map item coordinates on leftover-map comparison graphic leftover-map criterion markers) +(leftover-map item coordinates on leftover-map comparison graphic leftover-map criterion markers); +[ADR 0337](0337-leftover-map-compare-plot-post-coordinates.md) +(leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers) Amends leftover pairs on the grouping comparison strip ([ADR 0149](0149-leftover-pairs-on-comparison-strip.md)), leftover-map diff --git a/docs/adr/0334-leftover-map-axis-tick-share-badge.md b/docs/adr/0334-leftover-map-axis-tick-share-badge.md index 1eeffb5fb..7b3e90be4 100644 --- a/docs/adr/0334-leftover-map-axis-tick-share-badge.md +++ b/docs/adr/0334-leftover-map-axis-tick-share-badge.md @@ -121,7 +121,9 @@ axis share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). Independen graphic leftover-map criterion leftover-map item coordinates independently of leftover-map post ξ markers ([ADR 0335](0335-leftover-map-plot-criterion-coordinates.md)). Independent of leftover-map comparison graphic leftover-map criterion leftover-map item coordinates independently of leftover-map post ξ markers -([ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md)). +([ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md)). Independent of leftover-map +comparison graphic leftover-map post leftover-map person coordinates independently of leftover-map criterion leftover-map item coordinates +([ADR 0337](0337-leftover-map-compare-plot-post-coordinates.md)). ## References diff --git a/docs/adr/0336-leftover-map-compare-plot-criterion-coordinates.md b/docs/adr/0336-leftover-map-compare-plot-criterion-coordinates.md index 218d236c6..007bbce63 100644 --- a/docs/adr/0336-leftover-map-compare-plot-criterion-coordinates.md +++ b/docs/adr/0336-leftover-map-compare-plot-criterion-coordinates.md @@ -3,6 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-31 +**Amended by:** [ADR 0337](0337-leftover-map-compare-plot-post-coordinates.md) +(leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers) + Amends leftover-map graphic display of persisted coordinates on the grouping comparison strip ([ADR 0304](0304-leftover-map-compare-graphic.md)), leftover-map graphic leftover-map criterion leftover-map item coordinates ([ADR 0335](0335-leftover-map-plot-criterion-coordinates.md)), leftover-map diff --git a/docs/adr/0337-leftover-map-compare-plot-post-coordinates.md b/docs/adr/0337-leftover-map-compare-plot-post-coordinates.md new file mode 100644 index 000000000..e204acb0c --- /dev/null +++ b/docs/adr/0337-leftover-map-compare-plot-post-coordinates.md @@ -0,0 +1,136 @@ +# ADR 0337 — Name leftover-map person coordinates on leftover-map comparison graphic leftover-map post markers independently of leftover-map criterion leftover-map item coordinates + +**Decision status:** Accepted +**Date:** 2026-08-31 + +Amends leftover-map graphic display of persisted coordinates on the grouping comparison strip +([ADR 0304](0304-leftover-map-compare-graphic.md)), leftover-map comparison graphic leftover-map criterion leftover-map item coordinates +([ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md)), leftover-map +graphic leftover-map criterion leftover-map item coordinates +([ADR 0335](0335-leftover-map-plot-criterion-coordinates.md)), leftover-map +graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)), leftover-map +coordinates ([ADR 0267](0267-leftover-map-coordinates.md)). Independent of leftover-axis +tick leftover-map axis share independently of leftover-map singular values +([ADR 0334](0334-leftover-map-axis-tick-share-badge.md)). Independent of leftover-map +comparison leftover-axis tick leftover-map axis share independently of leftover-map +singular values ([ADR 0333](0333-leftover-map-compare-axis-tick-share-badge.md)). Independent of leftover-map +graphic leftover-map axis tick leftover-map axis share independently of leftover-map +singular values ([ADR 0332](0332-leftover-map-plot-tick-share-badge.md)). Independent of leftover-map +comparison graphic leftover-map axis tick leftover-map axis share independently of leftover-map +singular values ([ADR 0331](0331-leftover-map-compare-plot-tick-share-badge.md)). + +## Context + +ADR 0267 already persists two-axis Gabriel person coordinates +`ξ_{1:2}` and item coordinates `ζ_{1:2}` on leftover pair rows so +`R̂ = ξ_{1:2} · ζ_{1:2}` and `d = ‖ξ_{1:2} − ζ_{1:2}‖` stay +auditable. ADR 0268 already draws leftover-map graphic leftover-map +post markers at persisted `ξ` and leftover-map graphic leftover-map +criterion markers at persisted `ζ`. ADR 0304 reuses that graphic on the grouping +comparison strip. ADR 0335 already names leftover-map graphic leftover-map +criterion markers `leftover-map criterion {label} at ζ {item}` independently of leftover-map +post `ξ` markers. ADR 0336 already names leftover-map comparison graphic leftover-map +criterion markers `leftover map comparison graphic leftover-map criterion {label} at ζ {item}` +independently of leftover-map post `ξ` markers. Those leftover-map comparison graphic leftover-map +post markers still read `Open leftover-map post {title} at ξ {person}`, so a buyer who +reads leftover-map comparison graphic leftover-map post markers can treat leftover-map location as leftover-map graphic leftover-map +post `ξ` without a next action on leftover-map comparison graphic leftover-map post leftover-map person coordinates. A missing or +non-finite leftover-map person coordinate pair is not a leftover score and +must omit independently of leftover-map criterion leftover-map item coordinates. Rank-0 unused +axes still persist leftover-map person coordinates `(0.00, 0.00)`. A finite +negative leftover is shown, never clamped. Do not invent leftover-map +person coordinates from leftover-map item coordinates `ζ`. Do not invent leftover-map +item coordinates `ζ` from leftover-map person coordinates `ξ`. Leftover-map graphic leftover-map +post markers stay `Open leftover-map post {title} at ξ {person}` this increment. + +This increment names leftover-map comparison graphic leftover-map post markers leftover-map +person coordinates as leftoverMapComparePlotPostBadge, matching leftover-map +comparison graphic leftover-map criterion leftover-map item coordinates. Leftover-map comparison graphic leftover-map +post markers stay +`Open leftover map comparison graphic leftover-map post {title} at ξ {person}` +when leftover-map person coordinates are finite, so they stay distinct from leftover-map +graphic leftover-map post markers `Open leftover-map post {title} at ξ {person}` and from leftover-map +comparison graphic leftover-map criterion markers `leftover map comparison graphic leftover-map criterion {label} at ζ {item}`. +It does not add columns. Do not invent a leftover score. Do not invent a theta. + +This protected increment uses **0337** so it does not collide with leftover-map +comparison graphic leftover-map criterion leftover-map item coordinates independently of leftover-map post ξ markers +(0336), leftover-map graphic leftover-map criterion leftover-map item coordinates independently of leftover-map post ξ markers +(0335), leftover-axis tick leftover-map axis share independently of leftover-map singular values +(0334), leftover-map comparison leftover-axis tick leftover-map axis share independently of leftover-map singular values +(0333), leftover-map graphic leftover-map axis tick leftover-map axis share independently of leftover-map singular values +(0332), leftover-map comparison graphic leftover-map axis tick leftover-map axis share independently of leftover-map singular values +(0331), leftover-map graphic display on the grouping comparison strip (0304), leftover-map graphic display (0268), leftover-map coordinates (0267), +or the dashboard stacks. + +## Decision + +On leftover-map comparison graphic leftover-map post markers, caption leftover-map +person coordinates `ξ_{1:2}` when leftoverMapComparePlotPostBadge +returns a usable leftover-map comparison graphic leftover-map post leftover-map person coordinate caption. +A missing or non-finite leftover-map person coordinate pair omits that leftover-map +comparison graphic leftover-map post leftover-map person coordinate caption and keeps +`Open leftover-map post {title}`. Rank-0 unused axes still name leftover-map +person coordinates `(0.00, 0.00)`. Click a leftover pair or a leftover-map +post marker to open that post. Criterion markers are not post buttons. + +Leftover-map comparison graphic leftover-map post leftover-map person coordinates omit +independently of leftover-map criterion leftover-map item coordinates. This increment does not +change leftover-map graphic leftover-map post markers, +leftover-map graphic leftover-map criterion markers, +leftover-axis ticks, leftover-map graphic leftover-map axis ticks, leftover-map +comparison leftover-axis ticks, or leftover-map comparison graphic leftover-map +axis ticks. This increment does not persist leftover-map inner product, cosine, or length. + +Do not add SQL. Do not edit shipped migrations. Do not invent a leftover +score. Do not invent a theta. + +## Consequences + +After `make seed`, leftover-map comparison graphic leftover-map post markers name persisted +leftover-map person coordinates when leftoverMapComparePlotPostBadge returns a usable +leftover-map comparison graphic leftover-map post leftover-map person coordinate caption; click a leftover pair +or a leftover-map post marker opens that post. Hidden posts stay hidden. Rank-0 unused +axes still name leftover-map comparison graphic leftover-map post leftover-map person coordinates +`(0.00, 0.00)`. Leftover-map graphic leftover-map post markers stay +`Open leftover-map post {title} at ξ {person}`. Leftover-map comparison graphic leftover-map criterion leftover-map item coordinates independently of leftover-map +post ξ markers (ADR 0336), leftover-map graphic leftover-map criterion leftover-map item coordinates independently of leftover-map +post ξ markers (ADR 0335), leftover-axis tick leftover-map axis share independently of leftover-map +singular values (ADR 0334), leftover-map comparison leftover-axis tick leftover-map axis share independently of leftover-map +singular values (ADR 0333), leftover-map graphic leftover-map axis tick leftover-map axis share independently of leftover-map +singular values (ADR 0332), leftover-map comparison graphic leftover-map axis tick leftover-map axis share independently of leftover-map +singular values (ADR 0331), leftover-map graphic display on the grouping comparison strip (ADR 0304), leftover-map graphic display (ADR 0268), and leftover-map +coordinates (ADR 0267) remain. + +## Related + +Independent of leftover-map comparison graphic leftover-map criterion leftover-map item coordinates independently of leftover-map +post ξ markers +([ADR 0336](0336-leftover-map-compare-plot-criterion-coordinates.md)). Independent of leftover-map graphic leftover-map criterion leftover-map item coordinates independently of leftover-map +post ξ markers +([ADR 0335](0335-leftover-map-plot-criterion-coordinates.md)). Independent of leftover-axis tick leftover-map axis share independently of leftover-map +singular values +([ADR 0334](0334-leftover-map-axis-tick-share-badge.md)). Independent of leftover-map +comparison leftover-axis tick leftover-map axis share independently of leftover-map +singular values +([ADR 0333](0333-leftover-map-compare-axis-tick-share-badge.md)). Independent of leftover-map +graphic leftover-map axis tick leftover-map axis share independently of leftover-map singular values +([ADR 0332](0332-leftover-map-plot-tick-share-badge.md)). Independent of leftover-map +comparison graphic leftover-map axis tick leftover-map axis share independently of leftover-map singular values +([ADR 0331](0331-leftover-map-compare-plot-tick-share-badge.md)). Independent of leftover-map +graphic display on the grouping comparison strip ([ADR 0304](0304-leftover-map-compare-graphic.md)). Independent of leftover-map +graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)). Independent of leftover-map +coordinates ([ADR 0267](0267-leftover-map-coordinates.md)). + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 +(LSIRM interaction `−γ‖ξ_j − ζ_i‖` after main effects +`α_j − β_i`; typically `p = 2` for the interaction map.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 9b14f4aa2..99a454dbb 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,26 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-31 17:03 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not +> self-approve. #878 leftover-map item coordinates on leftover-map comparison graphic leftover-map +> criterion markers independently of leftover-map post ξ markers (v2.93.0 / ADR 0336) is +> `617b971`, stacked on #876 @ `89ed56b`. Independent APPROVE is missing (Devin COMMENTED, +> not APPROVE). Next buyer increment on this cycle: leftover-map comparison graphic leftover-map +> post markers naming persisted leftover-map person coordinates independently of leftover-map +> criterion leftover-map item coordinates (ADR 0337 / v2.94.0) delivered locally on +> `feat/leftover-map-compare-plot-post-coordinates-v2940`. Caption persisted leftover-map +> person coordinates on leftover-map comparison graphic leftover-map post markers when leftoverMapComparePlotPostBadge +> returns a usable leftover-map comparison graphic leftover-map post leftover-map person coordinate caption. Distinct accessible name +> `Open leftover map comparison graphic leftover-map post {title} at ξ {person}`. +> Rank-0 unused axes still name leftover-map person coordinates `(0.00, 0.00)`. A missing or +> non-finite leftover-map person coordinate pair omits independently of leftover-map criterion `ζ` and keeps +> `Open leftover-map post {title}`. Leftover-map graphic leftover-map post markers stay +> `Open leftover-map post {title} at ξ {person}` this increment. Do not invent leftover-map +> person coordinates from leftover-map item coordinates `ζ`. Do not invent leftover-map +> item coordinates `ζ` from leftover-map person coordinates `ξ`. Do not persist leftover-map inner product, cosine, or length. Stack onto #878. +> Leave open for independent review. Do not squash-merge. Do not self-approve. Issues #79 +> and #87 stay open. Do not merge #808 without independent APPROVE. Never self-approve. Do not mix #877. + > Exact-head loop overlay: 2026-08-31 16:30 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not > self-approve. #876 leftover-map item coordinates on leftover-map graphic leftover-map diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index c7bba994f..33885ead8 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -5,8 +5,8 @@ operator-facing control you can click before changing product CSS. | Story | Operator next action | Token / module | |---|---|---| -| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match leftover-map graphic leftover-map criterion markers to `leftover-map criterion {label} at ζ {item}`, 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 `σ_k` 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. The comparison variant names leftover-map complete-case coverage under `Leftover map comparison graphic coverage` and leftover-map item complete-case coverage under `Leftover map comparison graphic item coverage` and leftover-map incomplete post coverage under `Leftover map comparison graphic incomplete posts` and leftover-map incomplete item coverage under `Leftover map comparison graphic incomplete items` and leftover-map comparison graphic leftover-map criterion markers under `leftover map comparison graphic leftover-map criterion {label} at ζ {item}`. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, `Comparison`, `ComparisonMissingCoverage`, `ComparisonMissingItemCoverage`, `ComparisonMissingIncompletePosts`, and `ComparisonMissingIncompleteItems` 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, missing share that keeps leftover-map axis `σ` without share, grouping comparison leftover-map graphic coverage that matches leftover map used N of M scored posts, omitted comparison graphic coverage when leftoverMapCoverageCounts fail-closes, grouping comparison leftover-map graphic item coverage that matches leftover map used N of M scored criteria, omitted comparison graphic item coverage when leftoverMapItemCoverageCounts fail-closes, grouping comparison leftover-map graphic incomplete posts that matches leftover map dropped N incomplete posts, omitted comparison graphic incomplete posts when leftoverMapIncompletePostCount fail-closes, grouping comparison leftover-map graphic incomplete items that matches leftover map dropped N incomplete criteria, and omitted comparison graphic incomplete items when leftoverMapIncompleteItemCount fail-closes. 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 singular values and leftover-map axis share, leftover-map axis ticks name persisted coordinates, leftover-map graphic leftover-map criterion markers name persisted leftover-map item coordinates under `leftover-map criterion {label} at ζ {item}`, leftover-axis ticks name persisted leftover-map singular values and leftover-map axis share independently under `leftover axis {axis} tick {value} σ {singular} {share}%`, 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-axis report badges with persisted leftover-map singular values under `leftover map comparison leftover axis {axis} σ {value}`, leftover-axis ticks with persisted leftover-map singular values and leftover-map axis share under `leftover map comparison leftover axis {axis} tick {value} σ {singular} {share}%` independently of leftover-map singular values, and leftover-map comparison graphic leftover-map criterion markers under `leftover map comparison graphic leftover-map criterion {label} at ζ {item}` with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisBadge / leftoverMapCompareAxisTickBadge / leftoverMapComparePlotCriterionBadge return usable values. | `LeftoverPairList`, `LeftoverMapPlot`, `ticket-list`, `post-badge` | +| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match leftover-map graphic leftover-map criterion markers to `leftover-map criterion {label} at ζ {item}`, 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 `σ_k` 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. The comparison variant names leftover-map complete-case coverage under `Leftover map comparison graphic coverage` and leftover-map item complete-case coverage under `Leftover map comparison graphic item coverage` and leftover-map incomplete post coverage under `Leftover map comparison graphic incomplete posts` and leftover-map incomplete item coverage under `Leftover map comparison graphic incomplete items` and leftover-map comparison graphic leftover-map criterion markers under `leftover map comparison graphic leftover-map criterion {label} at ζ {item}` and leftover-map comparison graphic leftover-map post markers under `Open leftover map comparison graphic leftover-map post {title} at ξ {person}`. `ClosestAndFarthest`, `RankZeroOrigin`, `MissingCoordinates`, `MissingAxisShare`, `Comparison`, `ComparisonMissingCoverage`, `ComparisonMissingItemCoverage`, `ComparisonMissingIncompletePosts`, and `ComparisonMissingIncompleteItems` 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, missing share that keeps leftover-map axis `σ` without share, grouping comparison leftover-map graphic coverage that matches leftover map used N of M scored posts, omitted comparison graphic coverage when leftoverMapCoverageCounts fail-closes, grouping comparison leftover-map graphic item coverage that matches leftover map used N of M scored criteria, omitted comparison graphic item coverage when leftoverMapItemCoverageCounts fail-closes, grouping comparison leftover-map graphic incomplete posts that matches leftover map dropped N incomplete posts, omitted comparison graphic incomplete posts when leftoverMapIncompletePostCount fail-closes, grouping comparison leftover-map graphic incomplete items that matches leftover map dropped N incomplete criteria, and omitted comparison graphic incomplete items when leftoverMapIncompleteItemCount fail-closes. 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 singular values and leftover-map axis share, leftover-map axis ticks name persisted coordinates, leftover-map graphic leftover-map criterion markers name persisted leftover-map item coordinates under `leftover-map criterion {label} at ζ {item}`, leftover-axis ticks name persisted leftover-map singular values and leftover-map axis share independently under `leftover axis {axis} tick {value} σ {singular} {share}%`, 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-axis report badges with persisted leftover-map singular values under `leftover map comparison leftover axis {axis} σ {value}`, leftover-axis ticks with persisted leftover-map singular values and leftover-map axis share under `leftover map comparison leftover axis {axis} tick {value} σ {singular} {share}%` independently of leftover-map singular values, and leftover-map comparison graphic leftover-map criterion markers under `leftover map comparison graphic leftover-map criterion {label} at ζ {item}` and leftover-map comparison graphic leftover-map post markers under `Open leftover map comparison graphic leftover-map post {title} at ξ {person}` with distinct comparison labels when leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount / leftoverMapCompareAxisBadge / leftoverMapCompareAxisTickBadge / leftoverMapComparePlotCriterionBadge / leftoverMapComparePlotPostBadge 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 2134be6d7..fe1eeb4fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.93.0", + "version": "2.94.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index fda9a50ba..35cc011f3 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4234,9 +4234,14 @@ describe("App, authenticated", () => { expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument(); expect( screen.getAllByRole("button", { - name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, + name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", }), - ).toHaveLength(2); + ).toHaveLength(1); + expect( + within(screen.getByLabelText("Grouping comparison")).getByRole("button", { + name: "Open leftover map comparison graphic leftover-map post Public post at ξ (+0.50, +0.10)", + }), + ).toBeInTheDocument(); expect(screen.getByLabelText("Leftover map coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); @@ -4438,7 +4443,7 @@ describe("App, authenticated", () => { ).toHaveTextContent("Open a leftover pair to read the post–criterion cell"); expect( within(screen.getByLabelText("Grouping comparison")).getByRole("button", { - name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", + name: "Open leftover map comparison graphic leftover-map post Public post at ξ (+0.50, +0.10)", }), ).toBeInTheDocument(); expect( @@ -4958,7 +4963,7 @@ describe("App, authenticated", () => { const comparison = await screen.findByLabelText("Grouping comparison"); await userEvent.click( await within(comparison).findByRole("button", { - name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", + name: "Open leftover map comparison graphic leftover-map post Public post at ξ (+0.50, +0.10)", }), ); await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument()); diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 80df55436..2ac835fb5 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -928,6 +928,16 @@ describe("LeftoverMapPlot", () => { expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument(); expect(screen.getByLabelText("leftover map comparison graphic leftover-map criterion sales-lead at ζ (+0.50, −0.02)")).toBeInTheDocument(); expect(screen.getByLabelText("leftover map comparison graphic leftover-map criterion negative at ζ (−0.70, −0.40)")).toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: "Open leftover map comparison graphic leftover-map post Public post at ξ (+0.50, +0.10)", + }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { + name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", + }), + ).not.toBeInTheDocument(); expect(screen.queryByLabelText("Criterion ζ sales-lead")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Criterion ζ negative")).not.toBeInTheDocument(); expect( @@ -979,6 +989,16 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover-map criterion sales-lead at ζ (0.00, 0.00)"), ).toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: "Open leftover map comparison graphic leftover-map post Public post at ξ (0.00, 0.00)", + }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { + name: "Open leftover-map post Public post at ξ (0.00, 0.00)", + }), + ).not.toBeInTheDocument(); expect(screen.queryByLabelText("Criterion ζ sales-lead")).not.toBeInTheDocument(); expect(screen.queryByLabelText("leftover-map criterion sales-lead at ζ (0.00, 0.00)")).not.toBeInTheDocument(); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 9c155332e..5471529b5 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -36,6 +36,7 @@ import { layoutLeftoverMapPlot, leftoverMapPlotCriterionBadge, leftoverMapComparePlotCriterionBadge, + leftoverMapComparePlotPostBadge, LEFTOVER_MAP_COMPARE_PLOT_CAPTION, LEFTOVER_MAP_COMPARE_PLOT_LABEL, LEFTOVER_MAP_COMPARE_PLOT_SVG, @@ -51,6 +52,7 @@ import { LEFTOVER_MAP_COMPARE_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_CAPTION, LEFTOVER_MAP_PLOT_POST_ACTION, + LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, @@ -139,6 +141,24 @@ function leftoverMapPlotCriterionText( return tf(badge.key, badge.values); } +function leftoverMapPlotPostText( + marker: { label: string; axis1: number; axis2: number }, + variant: LeftoverMapPlotVariant, +): string { + if (variant === "comparison") { + const badge = leftoverMapComparePlotPostBadge(marker.label, marker.axis1, marker.axis2); + if (badge === null) { + return tf(LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, { title: marker.label }); + } + return tf(badge.key, badge.values); + } + const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? ""; + return tf(LEFTOVER_MAP_PLOT_POST_ACTION, { + title: marker.label, + person, + }); +} + /** * Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. * @@ -149,7 +169,10 @@ function leftoverMapPlotCriterionText( * leftover-map item coordinate caption. Caption leftover-map comparison graphic leftover-map * criterion markers with persisted leftover-map item coordinates ``ζ_{1:2}`` * when leftoverMapComparePlotCriterionBadge returns a usable leftover-map comparison - * graphic leftover-map criterion leftover-map item coordinate caption. Caption leftover-map axes with persisted + * graphic leftover-map criterion leftover-map item coordinate caption. Caption leftover-map + * comparison graphic leftover-map post markers with persisted leftover-map person coordinates + * ``ξ_{1:2}`` when leftoverMapComparePlotPostBadge returns a usable leftover-map comparison + * graphic leftover-map post leftover-map person coordinate caption. Caption leftover-map axes with persisted * leftover-map singular values ``σ_k`` and Gabriel inertia share when finite, * including rank-0 zero-share axes. * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the @@ -581,17 +604,13 @@ export function LeftoverMapPlot({ ))} {layout.persons.map((marker) => { - const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? ""; return ( openPost(marker.id)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index 46afadebf..d060d0205 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -150,6 +150,8 @@ describe("i18n", () => { "Leftover map dropped {dropped} incomplete criteria", "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. 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. Click a post marker to open that post. The plot does not invent a leftover score.", "Open leftover-map post {title} at ξ {person}", + "Open leftover-map post {title}", + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}", "Read observed Y {observed} and expected E {expected} after IRT main effects, then open this post.", "Leftover map has no leftover structure after IRT main effects. Open this post.", "Leftover map rank {rank} after IRT main effects. Open this post.", @@ -462,6 +464,21 @@ describe("i18n", () => { ).toBe(expected); }); + it.each([ + ["ko", "잔여 지도 비교 그림 글 Public post 열기 (ξ (+0.50, +0.10))"], + ["zh", "打开残差地图比较图形帖子 Public post(ξ (+0.50, +0.10))"], + ["ja", "残差マップの比較図の投稿 Public post を開く(ξ (+0.50, +0.10))"], + ["vi", "Mở bài đồ họa so sánh bản đồ phần dư Public post tại ξ (+0.50, +0.10)"], + ] as const)("formats leftover map comparison graphic leftover-map post ξ in %s", (locale, expected) => { + setLocale(locale); + expect( + tf("Open leftover map comparison graphic leftover-map post {title} at ξ {person}", { + title: "Public post", + person: "(+0.50, +0.10)", + }), + ).toBe(expected); + }); + it.each([ ["ko", "잔여 지도 그림 포함 범위"], ["zh", "残差图图形覆盖范围"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 69a060f79..dee53c062 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -666,6 +666,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map criterion {label} at ζ {item}": "잔여 지도 기준 {label} (ζ {item})", "leftover map comparison graphic leftover-map criterion {label} at ζ {item}": "잔여 지도 비교 그림 기준 {label} (ζ {item})", + "Open leftover-map post {title}": "잔여 지도 글 {title} 열기", + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}": + "잔여 지도 비교 그림 글 {title} 열기 (ξ {person})", "leftover-map axis 1": "잔여 지도 축 1", "leftover-map axis 2": "잔여 지도 축 2", "leftover-map axis {axis} ({share}%)": "잔여 지도 축 {axis} ({share}%)", @@ -1344,6 +1347,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map criterion {label} at ζ {item}": "残差图准则 {label}(ζ {item})", "leftover map comparison graphic leftover-map criterion {label} at ζ {item}": "残差地图比较图形准则 {label}(ζ {item})", + "Open leftover-map post {title}": "打开残差图帖子 {title}", + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}": + "打开残差地图比较图形帖子 {title}(ξ {person})", "leftover-map axis 1": "残差图轴 1", "leftover-map axis 2": "残差图轴 2", "leftover-map axis {axis} ({share}%)": "残差图轴 {axis} ({share}%)", @@ -2026,6 +2032,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map criterion {label} at ζ {item}": "残差マップの基準 {label}(ζ {item})", "leftover map comparison graphic leftover-map criterion {label} at ζ {item}": "残差マップの比較図基準 {label}(ζ {item})", + "Open leftover-map post {title}": "残差マップの投稿 {title} を開く", + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}": + "残差マップの比較図の投稿 {title} を開く(ξ {person})", "leftover-map axis 1": "残差マップ軸 1", "leftover-map axis 2": "残差マップ軸 2", "leftover-map axis {axis} ({share}%)": "残差マップ軸 {axis} ({share}%)", @@ -2712,6 +2721,9 @@ const TRANSLATIONS: Partial>> = { "leftover-map criterion {label} at ζ {item}": "tiêu chí bản đồ phần dư {label} tại ζ {item}", "leftover map comparison graphic leftover-map criterion {label} at ζ {item}": "tiêu chí đồ họa so sánh bản đồ phần dư {label} tại ζ {item}", + "Open leftover-map post {title}": "Mở bài viết bản đồ phần dư {title}", + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}": + "Mở bài đồ họa so sánh bản đồ phần dư {title} tại ξ {person}", "leftover-map axis 1": "trục bản đồ phần dư 1", "leftover-map axis 2": "trục bản đồ phần dư 2", "leftover-map axis {axis} ({share}%)": "trục bản đồ phần dư {axis} ({share}%)", diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index a1b781abf..6783470db 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -7,6 +7,7 @@ import { layoutLeftoverMapPlot, leftoverMapPlotCriterionBadge, leftoverMapComparePlotCriterionBadge, + leftoverMapComparePlotPostBadge, LEFTOVER_MAP_COMPARE_PLOT_CAPTION, LEFTOVER_MAP_COMPARE_PLOT_LABEL, LEFTOVER_MAP_COMPARE_PLOT_SEGMENT_RECONSTRUCTION, @@ -24,6 +25,8 @@ import { LEFTOVER_MAP_PLOT_CRITERION, LEFTOVER_MAP_COMPARE_PLOT_CRITERION, LEFTOVER_MAP_PLOT_POST_ACTION, + LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, + LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, LEFTOVER_MAP_PLOT_TICK, PLOT_HEIGHT, PLOT_PADDING, @@ -1245,6 +1248,9 @@ describe("leftoverMapPlotCriterionBadge", () => { expect(leftoverMapPlotCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe( LEFTOVER_MAP_COMPARE_PLOT_CRITERION, ); + expect(leftoverMapPlotCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe( + LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, + ); }); }); @@ -1288,6 +1294,52 @@ describe("leftoverMapComparePlotCriterionBadge", () => { expect(leftoverMapComparePlotCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe( LEFTOVER_MAP_PLOT_POST_ACTION, ); + expect(leftoverMapComparePlotCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe( + LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, + ); + }); +}); + +describe("leftoverMapComparePlotPostBadge", () => { + it("names persisted leftover-map person coordinates without inventing a leftover score", () => { + expect(leftoverMapComparePlotPostBadge("Public post", 0.5, 0.1)).toEqual({ + key: LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, + values: { title: "Public post", person: "(+0.50, +0.10)" }, + }); + expect(leftoverMapComparePlotPostBadge("negative", -0.7, -0.4)).toEqual({ + key: LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, + values: { title: "negative", person: "(\u22120.70, \u22120.40)" }, + }); + }); + + it("names rank-0 origin leftover-map person coordinates as ξ (0.00, 0.00)", () => { + expect(leftoverMapComparePlotPostBadge("Public post", 0, 0)).toEqual({ + key: LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, + values: { title: "Public post", person: "(0.00, 0.00)" }, + }); + }); + + it("omits leftover-map person coordinates when ξ is missing or non-finite", () => { + expect(leftoverMapComparePlotPostBadge("Public post", null, 0.1)).toBeNull(); + expect(leftoverMapComparePlotPostBadge("Public post", 0.5, undefined)).toBeNull(); + expect(leftoverMapComparePlotPostBadge("Public post", Number.NaN, 0.1)).toBeNull(); + expect(leftoverMapComparePlotPostBadge("Public post", 0.5, Number.POSITIVE_INFINITY)).toBeNull(); + }); + + it("stays distinct from leftover-map graphic leftover-map post markers and leftover-map comparison graphic leftover-map criterion markers", () => { + expect(LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION).toBe( + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}", + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION).not.toBe(LEFTOVER_MAP_PLOT_POST_ACTION); + expect(LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION).not.toBe(LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED); + expect(LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_CRITERION); + expect(LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION).not.toBe(LEFTOVER_MAP_PLOT_CRITERION); + expect(leftoverMapComparePlotPostBadge("Public post", 0.5, 0.1)?.key).not.toBe( + LEFTOVER_MAP_PLOT_POST_ACTION, + ); + expect(leftoverMapComparePlotPostBadge("Public post", 0.5, 0.1)?.key).not.toBe( + LEFTOVER_MAP_COMPARE_PLOT_CRITERION, + ); }); }); diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index cc5fefda6..d24fc196f 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -123,6 +123,10 @@ * markers leftover-map item coordinates ζ through leftoverMapComparePlotCriterionBadge * independently of leftover-map post ξ markers, not leftover-map graphic leftover-map * criterion markers. + * ADR 0337 fail-closes leftover-map comparison graphic leftover-map post + * markers leftover-map person coordinates ξ through leftoverMapComparePlotPostBadge + * independently of leftover-map criterion leftover-map item coordinates ζ, not leftover-map + * graphic leftover-map post markers. */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; @@ -152,6 +156,12 @@ export const LEFTOVER_MAP_COMPARE_PLOT_SVG = "Leftover map comparison"; export const LEFTOVER_MAP_PLOT_POST_ACTION = "Open leftover-map post {title} at ξ {person}"; +export const LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED = + "Open leftover-map post {title}"; + +export const LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION = + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}"; + export const LEFTOVER_MAP_PLOT_CRITERION = "leftover-map criterion {label} at ζ {item}"; @@ -163,6 +173,11 @@ export type LeftoverMapPlotCriterionBadge = { values: { label: string; item: string }; }; +export type LeftoverMapPlotPostBadge = { + key: string; + values: { title: string; person: string }; +}; + export function leftoverMapPlotCriterionBadge( label: string, axis1: number | null | undefined, @@ -187,6 +202,18 @@ export function leftoverMapComparePlotCriterionBadge( return { key: LEFTOVER_MAP_COMPARE_PLOT_CRITERION, values: { label, item } }; } +export function leftoverMapComparePlotPostBadge( + title: string, + axis1: number | null | undefined, + axis2: number | null | undefined, +): LeftoverMapPlotPostBadge | null { + const person = formatLeftoverMapCoordinatePair(axis1, axis2); + if (person === null) { + return null; + } + return { key: LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, values: { title, person } }; +} + export const LEFTOVER_MAP_PLOT_TICK = "leftover-map axis {axis} tick {value}"; diff --git a/pyproject.toml b/pyproject.toml index 580016ad8..b15260a9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.93.0" +version = "2.94.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 a169e684557dbaf1ce80e07e4ff21f851b9e4a77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 12:53:02 +0900 Subject: [PATCH 02/11] fix(reports): restore comparison post coordinate badge --- frontend/src/leftoverMapPlotLayout.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 5689e4a36..a3c285328 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -30,6 +30,30 @@ export const LEFTOVER_MAP_COMPARE_PLOT_SVG = "Leftover map comparison"; export const LEFTOVER_MAP_PLOT_POST_ACTION = "Open leftover-map post {title} at ξ {person}"; +export const LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED = + "Open leftover-map post {title}"; + +export const LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION = + "Open leftover map comparison graphic leftover-map post {title} at ξ {person}"; + +export type LeftoverMapPlotPostBadge = { + key: string; + values: { title: string; person: string }; +}; + +/** Name only finite persisted comparison-post person coordinates; never infer ξ from item coordinates. */ +export function leftoverMapComparePlotPostBadge( + title: string, + axis1: number | null | undefined, + axis2: number | null | undefined, +): LeftoverMapPlotPostBadge | null { + const person = formatLeftoverMapCoordinatePair(axis1, axis2); + if (person === null) { + return null; + } + return { key: LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION, values: { title, person } }; +} + export const LEFTOVER_MAP_PLOT_TICK = "leftover-map axis {axis} tick {value}"; From 49d02bfc232a2979a57cdadcc7dc379042348c29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 12:53:50 +0900 Subject: [PATCH 03/11] fix(reports): wire comparison post coordinate action --- frontend/src/components/LeftoverMapPlot.tsx | 72 +++++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index aa5440d58..70e8987b5 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -30,6 +30,7 @@ import { import { firstPlottablePairForPost, layoutLeftoverMapPlot, + leftoverMapComparePlotPostBadge, LEFTOVER_MAP_COMPARE_PLOT_CAPTION, LEFTOVER_MAP_COMPARE_PLOT_LABEL, LEFTOVER_MAP_COMPARE_PLOT_SVG, @@ -43,6 +44,7 @@ import { LEFTOVER_MAP_COMPARE_PLOT_SEGMENT_EXPECTED, LEFTOVER_MAP_PLOT_CAPTION, LEFTOVER_MAP_PLOT_POST_ACTION, + LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, @@ -92,6 +94,24 @@ function leftoverMapPlotAxisText( return tf(LEFTOVER_MAP_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); } +function leftoverMapPlotPostText( + marker: { label: string; axis1: number; axis2: number }, + variant: LeftoverMapPlotVariant, +): string { + if (variant === "comparison") { + const badge = leftoverMapComparePlotPostBadge(marker.label, marker.axis1, marker.axis2); + if (badge === null) { + return tf(LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, { title: marker.label }); + } + return tf(badge.key, badge.values); + } + const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? ""; + return tf(LEFTOVER_MAP_PLOT_POST_ACTION, { + title: marker.label, + person, + }); +} + /** * Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. * @@ -175,6 +195,8 @@ function leftoverMapPlotAxisText( * ADR 0319 captions leftover-map distance on that comparison graphic using * the same localized composition boundary rather than adding another static * comparison-only translation key. + * Comparison post-marker action names use only persisted finite person ξ coordinates and remain + * distinct from report action names; criterion ζ coordinates are never used to infer ξ. * Never invent a leftover score. */ export function LeftoverMapPlot({ @@ -496,34 +518,28 @@ export function LeftoverMapPlot({ ))} - {layout.persons.map((marker) => { - const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? ""; - return ( - openPost(marker.id)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - openPost(marker.id); - } - }} - > - - - - {marker.label} - - - ); - })} + {layout.persons.map((marker) => ( + openPost(marker.id)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + openPost(marker.id); + } + }} + > + + + + {marker.label} + + + ))} From 48705cc99b3174b311f82a2cb9c9e10e3a32530a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 13:56:25 +0900 Subject: [PATCH 04/11] chore(stack): adopt report criterion coordinate boundary --- frontend/src/leftoverMapPlotLayout.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index a3c285328..3f05043f2 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -30,6 +30,27 @@ export const LEFTOVER_MAP_COMPARE_PLOT_SVG = "Leftover map comparison"; export const LEFTOVER_MAP_PLOT_POST_ACTION = "Open leftover-map post {title} at ξ {person}"; +export const LEFTOVER_MAP_PLOT_CRITERION = + "leftover-map criterion {label} at ζ {item}"; + +export type LeftoverMapPlotCriterionBadge = { + key: string; + values: { label: string; item: string }; +}; + +/** Builds report-graphic criterion copy only from the persisted item-axis pair. */ +export function leftoverMapPlotCriterionBadge( + label: string, + axis1: number | null | undefined, + axis2: number | null | undefined, +): LeftoverMapPlotCriterionBadge | null { + const item = formatLeftoverMapCoordinatePair(axis1, axis2); + if (item === null) { + return null; + } + return { key: LEFTOVER_MAP_PLOT_CRITERION, values: { label, item } }; +} + export const LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED = "Open leftover-map post {title}"; From 1a7598331b97c82842b30afd535f4b4f020c25a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 13:57:13 +0900 Subject: [PATCH 05/11] chore(stack): preserve report criterion boundary in post lane --- frontend/src/components/LeftoverMapPlot.tsx | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 70e8987b5..9f30b2b18 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -31,6 +31,7 @@ import { firstPlottablePairForPost, layoutLeftoverMapPlot, leftoverMapComparePlotPostBadge, + leftoverMapPlotCriterionBadge, LEFTOVER_MAP_COMPARE_PLOT_CAPTION, LEFTOVER_MAP_COMPARE_PLOT_LABEL, LEFTOVER_MAP_COMPARE_PLOT_SVG, @@ -94,6 +95,20 @@ function leftoverMapPlotAxisText( return tf(LEFTOVER_MAP_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); } +function leftoverMapPlotCriterionText( + marker: { label: string; axis1: number; axis2: number }, + variant: LeftoverMapPlotVariant, +): string { + if (variant === "comparison") { + return `${t("Criterion ζ")} ${marker.label}`; + } + const badge = leftoverMapPlotCriterionBadge(marker.label, marker.axis1, marker.axis2); + if (badge === null) { + return `${t("Criterion ζ")} ${marker.label}`; + } + return tf(badge.key, badge.values); +} + function leftoverMapPlotPostText( marker: { label: string; axis1: number; axis2: number }, variant: LeftoverMapPlotVariant, @@ -115,8 +130,10 @@ function leftoverMapPlotPostText( /** * Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. * - * Person markers are posts; item markers are leftover criteria. Click a - * post marker to open that post. Caption leftover-map axes with persisted + * Person markers are posts; item markers are leftover criteria. Report criterion + * markers name persisted item ζ through the report criterion badge while the + * comparison criterion contract remains owned by the parent lane. Click a post + * marker to open that post. Caption leftover-map axes with persisted * Gabriel inertia share when finite, including rank-0 zero-share axes. * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the * pair row match the plot. Pair segments name persisted leftover-map @@ -511,7 +528,7 @@ export function LeftoverMapPlot({ ))} {layout.items.map((marker) => ( - + {marker.label} From 9cb44bb43187cff081b0e6a439243d1a8da1e86a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 14:00:02 +0900 Subject: [PATCH 06/11] chore(stack): adopt comparison criterion boundary --- frontend/src/leftoverMapPlotLayout.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 3f05043f2..f0a3bd148 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -33,6 +33,9 @@ export const LEFTOVER_MAP_PLOT_POST_ACTION = export const LEFTOVER_MAP_PLOT_CRITERION = "leftover-map criterion {label} at ζ {item}"; +export const LEFTOVER_MAP_COMPARE_PLOT_CRITERION = + "leftover map comparison graphic leftover-map criterion {label} at ζ {item}"; + export type LeftoverMapPlotCriterionBadge = { key: string; values: { label: string; item: string }; @@ -51,6 +54,19 @@ export function leftoverMapPlotCriterionBadge( return { key: LEFTOVER_MAP_PLOT_CRITERION, values: { label, item } }; } +/** Builds comparison-graphic criterion copy only from the persisted item-axis pair. */ +export function leftoverMapComparePlotCriterionBadge( + label: string, + axis1: number | null | undefined, + axis2: number | null | undefined, +): LeftoverMapPlotCriterionBadge | null { + const item = formatLeftoverMapCoordinatePair(axis1, axis2); + if (item === null) { + return null; + } + return { key: LEFTOVER_MAP_COMPARE_PLOT_CRITERION, values: { label, item } }; +} + export const LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED = "Open leftover-map post {title}"; From 765302336626f44b12c2d42156de131325d36b66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 14:00:55 +0900 Subject: [PATCH 07/11] chore(stack): adopt comparison criterion action --- frontend/src/components/LeftoverMapPlot.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 9f30b2b18..5a30abc4e 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -30,6 +30,7 @@ import { import { firstPlottablePairForPost, layoutLeftoverMapPlot, + leftoverMapComparePlotCriterionBadge, leftoverMapComparePlotPostBadge, leftoverMapPlotCriterionBadge, LEFTOVER_MAP_COMPARE_PLOT_CAPTION, @@ -100,7 +101,11 @@ function leftoverMapPlotCriterionText( variant: LeftoverMapPlotVariant, ): string { if (variant === "comparison") { - return `${t("Criterion ζ")} ${marker.label}`; + const badge = leftoverMapComparePlotCriterionBadge(marker.label, marker.axis1, marker.axis2); + if (badge === null) { + return `${t("Criterion ζ")} ${marker.label}`; + } + return tf(badge.key, badge.values); } const badge = leftoverMapPlotCriterionBadge(marker.label, marker.axis1, marker.axis2); if (badge === null) { @@ -130,11 +135,10 @@ function leftoverMapPlotPostText( /** * Gabriel leftover-map graphic display of persisted ``ξ_{1:2}`` / ``ζ_{1:2}``. * - * Person markers are posts; item markers are leftover criteria. Report criterion - * markers name persisted item ζ through the report criterion badge while the - * comparison criterion contract remains owned by the parent lane. Click a post - * marker to open that post. Caption leftover-map axes with persisted - * Gabriel inertia share when finite, including rank-0 zero-share axes. + * Person markers are posts; item markers are leftover criteria. Report and + * comparison criterion markers preserve their parent-owned persisted-item + * coordinate names. Click a post marker to open that post. Caption leftover-map + * axes with persisted Gabriel inertia share when finite, including rank-0 zero-share axes. * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the * pair row match the plot. Pair segments name persisted leftover-map * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map From 9ea8e9ee8f773e7af9bf4ddded7c3265eb0581d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 14:11:33 +0900 Subject: [PATCH 08/11] fix(i18n): compose localized comparison marker names --- frontend/src/components/LeftoverMapPlot.tsx | 163 +++++++++----------- 1 file changed, 77 insertions(+), 86 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 5a30abc4e..c3559fd78 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -46,7 +46,6 @@ import { LEFTOVER_MAP_COMPARE_PLOT_SEGMENT_EXPECTED, LEFTOVER_MAP_PLOT_CAPTION, LEFTOVER_MAP_PLOT_POST_ACTION, - LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, LEFTOVER_MAP_PLOT_SEGMENT_CROSS_SHARE, LEFTOVER_MAP_PLOT_SEGMENT_DISTANCE, LEFTOVER_MAP_PLOT_SEGMENT_EXPLAINED_SHARE, @@ -103,15 +102,15 @@ function leftoverMapPlotCriterionText( if (variant === "comparison") { const badge = leftoverMapComparePlotCriterionBadge(marker.label, marker.axis1, marker.axis2); if (badge === null) { - return `${t("Criterion ζ")} ${marker.label}`; + return `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${t("Criterion ζ")} ${marker.label}`; } - return tf(badge.key, badge.values); + return `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${t("Criterion ζ")} ${badge.values.label} · ζ ${badge.values.item}`; } const badge = leftoverMapPlotCriterionBadge(marker.label, marker.axis1, marker.axis2); if (badge === null) { return `${t("Criterion ζ")} ${marker.label}`; } - return tf(badge.key, badge.values); + return `${t("Criterion ζ")} ${badge.values.label} · ζ ${badge.values.item}`; } function leftoverMapPlotPostText( @@ -121,9 +120,9 @@ function leftoverMapPlotPostText( if (variant === "comparison") { const badge = leftoverMapComparePlotPostBadge(marker.label, marker.axis1, marker.axis2); if (badge === null) { - return tf(LEFTOVER_MAP_PLOT_POST_ACTION_OMITTED, { title: marker.label }); + return `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${t("Post ξ")} ${marker.label}`; } - return tf(badge.key, badge.values); + return `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${tf(LEFTOVER_MAP_PLOT_POST_ACTION, badge.values)}`; } const person = formatLeftoverMapCoordinatePair(marker.axis1, marker.axis2) ?? ""; return tf(LEFTOVER_MAP_PLOT_POST_ACTION, { @@ -137,88 +136,80 @@ function leftoverMapPlotPostText( * * Person markers are posts; item markers are leftover criteria. Report and * comparison criterion markers preserve their parent-owned persisted-item - * coordinate names. Click a post marker to open that post. Caption leftover-map - * axes with persisted Gabriel inertia share when finite, including rank-0 zero-share axes. - * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the - * pair row match the plot. Pair segments name persisted leftover-map - * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map - * explained leftover share ``e``, leftover-map unexplained leftover - * share ``s``, leftover-map cross share ``x``, leftover-map - * unexplained leftover ``U``, leftover residual ``R``, leftover - * observed ``Y``, leftover expected ``E``, and leftover-map rank so the - * pair-row badges match the graphic. Name leftover-map complete-case - * coverage, leftover-map item complete-case coverage, leftover-map - * incomplete post coverage, and leftover-map incomplete item coverage - * on the figure when those persisted post and criterion counts are usable. - * Omit that distance caption when ``d`` is missing or non-finite. Omit - * that reconstruction caption when ``R̂`` is missing or non-finite. Omit - * that explained leftover share caption when ``e`` is missing or - * non-finite. Omit that unexplained leftover share caption when ``s`` is - * missing or non-finite. Omit that leftover-map cross share caption when - * ``x`` is missing or non-finite. Omit that unexplained leftover caption - * when ``U`` is missing or non-finite. Omit that leftover residual - * caption when ``R`` is missing or non-finite. Omit that leftover observed - * caption when ``Y`` is missing or non-finite. Omit that leftover expected - * caption when ``E`` is missing or non-finite. Omit that leftover-map rank + * coordinate names by composing already-localized labels with ζ; comparison + * post markers similarly compose the localized comparison label with the existing + * localized report post action rather than creating another SPA translation + * authority. Click a post marker to open that post. Caption leftover-map axes + * with persisted Gabriel inertia share when finite, including rank-0 zero-share + * axes. Axis ticks name persisted leftover-map coordinates so ξ / ζ on the pair + * row match the plot. Pair segments name persisted leftover-map distance ``d``, + * leftover-map reconstruction ``R̂``, leftover-map explained leftover share ``e``, + * leftover-map unexplained leftover share ``s``, leftover-map cross share ``x``, + * leftover-map unexplained leftover ``U``, leftover residual ``R``, leftover + * observed ``Y``, leftover expected ``E``, and leftover-map rank so the pair-row + * badges match the graphic. Name leftover-map complete-case coverage, leftover-map + * item complete-case coverage, leftover-map incomplete post coverage, and leftover- + * map incomplete item coverage on the figure when those persisted post and + * criterion counts are usable. Omit that distance caption when ``d`` is missing or + * non-finite. Omit that reconstruction caption when ``R̂`` is missing or non-finite. + * Omit that explained leftover share caption when ``e`` is missing or non-finite. + * Omit that unexplained leftover share caption when ``s`` is missing or non-finite. + * Omit that leftover-map cross share caption when ``x`` is missing or non-finite. + * Omit that unexplained leftover caption when ``U`` is missing or non-finite. Omit + * that leftover residual caption when ``R`` is missing or non-finite. Omit that + * leftover observed caption when ``Y`` is missing or non-finite. Omit that leftover + * expected caption when ``E`` is missing or non-finite. Omit that leftover-map rank * caption when rank is missing, negative, or not an integer. Omit that leftover-map * coverage caption when coverage is missing or not usable complete-case integers. - * Omit that leftover-map item coverage caption when item coverage is missing or - * not usable complete-case integers. Omit that leftover-map incomplete post - * caption when incomplete post coverage is missing or not a usable integer. - * Omit that leftover-map incomplete item caption when incomplete item - * coverage is missing or not a usable integer. - * Omit that axis badge when share is - * missing or non-finite and keep the existing leftover-map axis text. - * Omit the plot when no pair has four finite leftover-map coordinates. - * ADR 0304 reuses this graphic on the grouping comparison strip from - * already-named leftover-map coordinates. ADR 0305 captions leftover-map axis - * share on that comparison graphic from already-named leftover-map axes - * with distinct leftover map comparison axis labels. ADR 0306 captions leftover-map - * complete-case coverage on that comparison graphic from already-named - * leftover-map coverage with distinct leftover map comparison graphic coverage - * labels and does not caption leftover-map incomplete coverage on that - * comparison plot. ADR 0307 captions leftover-map item complete-case - * coverage on that comparison graphic from already-named leftover-map - * coverage with distinct leftover map comparison graphic item coverage - * labels and does not caption leftover-map incomplete item coverage on that - * comparison plot. ADR 0308 captions leftover-map incomplete post coverage - * on that comparison graphic from already-named leftover-map coverage with - * distinct leftover map comparison graphic incomplete posts labels. ADR 0309 - * captions leftover-map incomplete item coverage on that comparison graphic - * from already-named leftover-map coverage with distinct leftover map - * comparison graphic incomplete items labels. ADR 0310 captions leftover-map - * reconstruction on that comparison graphic from already-named leftover-map - * reconstruction with distinct leftover map comparison graphic reconstruction - * labels. ADR 0311 captions leftover-map explained leftover share on that - * comparison graphic from already-named leftover-map explained leftover share - * with distinct leftover map comparison graphic explained leftover share - * labels. ADR 0312 captions leftover-map unexplained leftover share on that - * comparison graphic from already-named leftover-map unexplained leftover share - * with distinct leftover map comparison graphic unexplained leftover share - * labels. ADR 0313 captions leftover-map cross share on that - * comparison graphic from already-named leftover-map cross share - * with distinct leftover map comparison graphic cross share - * labels. ADR 0314 captions leftover-map unexplained leftover on that - * comparison graphic from already-named leftover-map unexplained leftover - * with distinct leftover map comparison graphic unexplained leftover - * labels. ADR 0315 captions leftover residual on that - * comparison graphic from already-named leftover residual - * with distinct leftover map comparison graphic leftover residual - * labels. ADR 0316 captions leftover observed on that - * comparison graphic from already-named leftover observed - * with distinct leftover map comparison graphic leftover observed - * labels. ADR 0317 captions leftover expected on that - * comparison graphic from already-named leftover expected - * with distinct leftover map comparison graphic leftover expected - * labels. ADR 0318 captions leftover-map rank on that comparison graphic - * from already-named leftover-map rank while composing existing localized - * comparison-graphic and rank labels rather than adding SPA translation debt. - * ADR 0319 captions leftover-map distance on that comparison graphic using - * the same localized composition boundary rather than adding another static - * comparison-only translation key. - * Comparison post-marker action names use only persisted finite person ξ coordinates and remain - * distinct from report action names; criterion ζ coordinates are never used to infer ξ. - * Never invent a leftover score. + * Omit that leftover-map item coverage caption when item coverage is missing or not + * usable complete-case integers. Omit that leftover-map incomplete post caption when + * incomplete post coverage is missing or not a usable integer. Omit that leftover- + * map incomplete item caption when incomplete item coverage is missing or not a + * usable integer. Omit that axis badge when share is missing or non-finite and keep + * the existing leftover-map axis text. Omit the plot when no pair has four finite + * leftover-map coordinates. ADR 0304 reuses this graphic on the grouping comparison + * strip from already-named leftover-map coordinates. ADR 0305 captions leftover-map + * axis share on that comparison graphic from already-named leftover-map axes with + * distinct leftover map comparison axis labels. ADR 0306 captions leftover-map + * complete-case coverage on that comparison graphic from already-named leftover-map + * coverage with distinct leftover map comparison graphic coverage labels and does + * not caption leftover-map incomplete coverage on that comparison plot. ADR 0307 + * captions leftover-map item complete-case coverage on that comparison graphic from + * already-named leftover-map coverage with distinct leftover map comparison graphic + * item coverage labels and does not caption leftover-map incomplete item coverage on + * that comparison plot. ADR 0308 captions leftover-map incomplete post coverage on + * that comparison graphic from already-named leftover-map coverage with distinct + * leftover map comparison graphic incomplete posts labels. ADR 0309 captions + * leftover-map incomplete item coverage on that comparison graphic from already- + * named leftover-map coverage with distinct leftover map comparison graphic + * incomplete items labels. ADR 0310 captions leftover-map reconstruction on that + * comparison graphic from already-named leftover-map reconstruction with distinct + * leftover map comparison graphic reconstruction labels. ADR 0311 captions leftover- + * map explained leftover share on that comparison graphic from already-named + * leftover-map explained leftover share with distinct leftover map comparison + * graphic explained leftover share labels. ADR 0312 captions leftover-map + * unexplained leftover share on that comparison graphic from already-named leftover- + * map unexplained leftover share with distinct leftover map comparison graphic + * unexplained leftover share labels. ADR 0313 captions leftover-map cross share on + * that comparison graphic from already-named leftover-map cross share with distinct + * leftover map comparison graphic cross share labels. ADR 0314 captions leftover-map + * unexplained leftover on that comparison graphic from already-named leftover-map + * unexplained leftover with distinct leftover map comparison graphic unexplained + * leftover labels. ADR 0315 captions leftover residual on that comparison graphic + * from already-named leftover residual with distinct leftover map comparison graphic + * leftover residual labels. ADR 0316 captions leftover observed on that comparison + * graphic from already-named leftover observed with distinct leftover map comparison + * graphic leftover observed labels. ADR 0317 captions leftover expected on that + * comparison graphic from already-named leftover expected with distinct leftover map + * comparison graphic leftover expected labels. ADR 0318 captions leftover-map rank on + * that comparison graphic from already-named leftover-map rank while composing + * existing localized comparison-graphic and rank labels rather than adding SPA + * translation debt. ADR 0319 captions leftover-map distance on that comparison + * graphic using the same localized composition boundary rather than adding another + * static comparison-only translation key. Comparison post-marker action names use + * only persisted finite person ξ coordinates and remain distinct from report action + * names; criterion ζ coordinates are never used to infer ξ. Never invent a leftover + * score. */ export function LeftoverMapPlot({ pairs, From 449a0aa48e2ffef75d10bb3f123e3b68cd8fa597 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 06:45:11 +0900 Subject: [PATCH 09/11] test(reports): execute comparison post accessibility contract --- ...erMapPlot.comparisonAccessibility.test.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx diff --git a/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx b/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx new file mode 100644 index 000000000..da9daeda8 --- /dev/null +++ b/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx @@ -0,0 +1,58 @@ +import { render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { LeftoverPair } from "../api"; +import { setLocale } from "../i18n"; +import { LeftoverMapPlot } from "./LeftoverMapPlot"; + +const PAIR: LeftoverPair = { + pair_kind: "closest", + post_id: "post-demo-public", + post_title: "Public post", + criterion_code: "sales_lead_quality", + leftover_distance: 0.12, + leftover_residual: 0.4, + leftover_map_person_axis_1: 0.5, + leftover_map_person_axis_2: 0.1, + leftover_map_item_axis_1: 0.5, + leftover_map_item_axis_2: -0.02, +}; + +describe("LeftoverMapPlot comparison post accessibility", () => { + afterEach(() => setLocale("en")); + + it("composes the comparison marker name from existing localized copy", () => { + setLocale("ko"); + render( + "sales-lead"} + onSelectPost={vi.fn()} + variant="comparison" + />, + ); + + expect( + screen.getByRole("button", { + name: "잔여 지도 비교 그림: 잔여 지도 글 Public post 열기 (ξ (+0.50, +0.10))", + }), + ).toBeInTheDocument(); + }); + + it("does not invent a post marker when persisted person coordinates are unavailable", () => { + const { container } = render( + "sales-lead"} + onSelectPost={vi.fn()} + variant="comparison" + />, + ); + + expect(container).toBeEmptyDOMElement(); + }); +}); From e2b6724bd35cab36137d21ab76874fb4a06bccb6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 06:45:54 +0900 Subject: [PATCH 10/11] test(reports): preserve non-plottable post open action --- ...erMapPlot.comparisonAccessibility.test.tsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx b/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx index da9daeda8..941829f0c 100644 --- a/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.comparisonAccessibility.test.tsx @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { LeftoverPair } from "../api"; import { setLocale } from "../i18n"; import { LeftoverMapPlot } from "./LeftoverMapPlot"; +import { LeftoverPairList } from "./LeftoverPairList"; const PAIR: LeftoverPair = { pair_kind: "closest", @@ -17,6 +18,11 @@ const PAIR: LeftoverPair = { leftover_map_item_axis_2: -0.02, }; +const PAIR_WITHOUT_PERSON_COORDINATES: LeftoverPair = { + ...PAIR, + leftover_map_person_axis_1: null, +}; + describe("LeftoverMapPlot comparison post accessibility", () => { afterEach(() => setLocale("en")); @@ -41,12 +47,7 @@ describe("LeftoverMapPlot comparison post accessibility", () => { it("does not invent a post marker when persisted person coordinates are unavailable", () => { const { container } = render( "sales-lead"} onSelectPost={vi.fn()} variant="comparison" @@ -55,4 +56,21 @@ describe("LeftoverMapPlot comparison post accessibility", () => { expect(container).toBeEmptyDOMElement(); }); + + it("keeps the pair-button open action when missing coordinates make the plot non-plottable", () => { + render( + "sales-lead"} + onSelectPost={vi.fn()} + />, + ); + + expect(screen.queryByLabelText("Leftover-map graphic display")).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: "Open leftover closest pair: Public post · sales-lead", + }), + ).toBeInTheDocument(); + }); }); From 0c1a97266f74b6963a765b7b78830146687861e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 21 Sep 2026 08:24:42 +0900 Subject: [PATCH 11/11] fix(stack): preserve current #1033 tree in #1034 convergence --- ...ftover-map-comparison-singular-desktop.png | Bin 0 -> 64460 bytes ...eftover-map-comparison-singular-mobile.png | Bin 0 -> 3810 bytes docs/storybook-inventory.md | 2 +- frontend/src/App.test.tsx | 8 +- .../src/components/LeftoverMapPlot.test.tsx | 98 ++++++++++-------- frontend/src/components/LeftoverMapPlot.tsx | 9 +- frontend/src/leftoverMapPlotLayout.test.ts | 17 +++ frontend/src/leftoverMapPlotLayout.ts | 10 +- ...ng_comparison_graphic_singular_contract.py | 45 +++++--- 9 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 docs/screenshots/leftover-map-comparison-singular-desktop.png create mode 100644 docs/screenshots/leftover-map-comparison-singular-mobile.png diff --git a/docs/screenshots/leftover-map-comparison-singular-desktop.png b/docs/screenshots/leftover-map-comparison-singular-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1fac9c4276f0d6c62729373ea975ed56192cf4 GIT binary patch literal 64460 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/leftover-map-comparison-singular-mobile.png b/docs/screenshots/leftover-map-comparison-singular-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9bac1552e994c4d9e96b659a214479af489017 GIT binary patch literal 3810 zcmeHK|5MU;7{{_%R+zZUYmvD8+;zJptka!eQn57g<0f`}xoiozx*)a$wWb*=eqkDS z8K+hzq-?9rwN{!GiXEv7|2oDhLP&?Bo83-R;J2@8|V?KkxTFujlnV z_kP%W@woMo??u93u=QYm?mieStOW*J`RVEv5Tb$ifG`-m3(Vbh;LKIal!${o9@p?< zaEE1bvS9~4m31x^5ud|4C*_`pMOTI+qvOL7gCd!nJAa1BMHt%krzYhCm~oy-2#NY-x1ErM{L*%W}gcgArAfx9cJ=0yJI_I;t%_)IVN?@MEK$i^9J>6fF>5wOJGF_7^#;eROt8T6ST2YqgCFHU6 zrjLK2b~=HRVYg3Uu)!tAT4Xa&L6-!)Gu4KgSK}yVNzkpO@at@%S7?&t{ymBWuhZ$0 z2;P8i7VEPJFp|Zibz1o)BEx#Gk6(YbF$YA9-xG^{Ht}rz(){>@;jQJ;A!5youVdqC z=VUpCN{3ldg!M1@-Oovy(~!Gqre9ibmUpj$$Aid;O2Ctb%k2_;*>>{3}o=rZO z9$@qppz0PL-8wQvZhq62Vn_`zJbmrJ1))ITkVT;GyA6z}%!P+My2SIcvk2{`ZLFY6 z{Vq9=2+^S>q*p5+MQ+YV;KWtdKjo)ylcZU?JWiJPb*0qIvN=>(ris0ctf2~rxY8~(flIhP&64j;GWN)p;p`Neeh~_p^1E>3%Wy?Ql4V^$N0%lJ zaDa+$NKF4%TJe(iK|jC^qfB}@hb7A!ceFN}4LQlrWVl|mqKJ6ex}D7`oIE0NE&@C% zIJ{3mwcH>+NrkXt@f|*NTuw62s@3YsFVr#$j@pPFZn~_XO^tlaj0&#N@$bSV^&5T$ z*a~%GZ(8gTydr%V_{UADRNA`$sz1IMMDRS{M+aksLs|=__~5N({@dL1(k;+y((NEc zb3prPuE_j|yt; z_IgzXbMxaUO#*x?sBd=e=$tU93ekj-WZ$k<><6V1g2S6eIx;Is*6kClD>0UKJQ(eE zYFKZSVR}!dVB=0PMazq6b2jEz+iqrJt|$_GS|r#pj1olhM5+p#q{^ljbrxu4nno? rfA0sLC}= { expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.86, 18%)")).toBeInTheDocument(); expect( screen.getAllByRole("button", { name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, @@ -4322,12 +4322,12 @@ describe("App, authenticated", () => { ).not.toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 1 (82%)", + "leftover map comparison axis 1 (σ 1.84, 82%)", ), ).toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 2 (18%)", + "leftover map comparison axis 2 (σ 0.86, 18%)", ), ).toBeInTheDocument(); expect( diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 56258c883..42e6bf0f6 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -843,8 +843,16 @@ describe("LeftoverMapPlot", () => { "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + const axis1 = screen.getByText( + "leftover map comparison axis 1 (σ 1.84, 82%)", + ); + const axis2 = screen.getByText( + "leftover map comparison axis 2 (σ 0.86, 18%)", + ); + expect(axis1).toHaveAttribute("x", "472"); + expect(axis1).toHaveAttribute("text-anchor", "end"); + expect(axis2).toHaveAttribute("x", "472"); + expect(axis2).toHaveAttribute("text-anchor", "end"); expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2 (18%)")).not.toBeInTheDocument(); }); @@ -861,9 +869,11 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 1 (σ 1.84)"), + ).toBeInTheDocument(); expect(screen.getByText("leftover map comparison axis 2")).toBeInTheDocument(); - expect(screen.queryByText(/leftover map comparison axis 1 \(/)).not.toBeInTheDocument(); + expect(screen.queryByText(/leftover map comparison axis 1 \(σ 1\.84,/)).not.toBeInTheDocument(); expect(screen.queryByText(/leftover map comparison axis 2 \(/)).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2")).not.toBeInTheDocument(); @@ -890,8 +900,12 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)"), + ).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 2 (σ 0.00, 0%)"), + ).toBeInTheDocument(); }); it("names leftover-map coverage on the comparison graphic with a distinct accessible name", () => { @@ -935,8 +949,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover map comparison incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover map comparison incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.86, 18%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic coverage when coverage is missing or not usable", () => { @@ -954,7 +968,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic coverage")).not.toBeInTheDocument(); expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { "Leftover map used 2 of 2 scored criteria (complete-case)", ); expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic item coverage when item coverage is missing or not usable", () => { @@ -1009,7 +1023,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete posts when incomplete post coverage is missing or not usable", () => { @@ -1067,7 +1081,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete items when incomplete item coverage is missing or not usable", () => { @@ -1128,7 +1142,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names leftover-map comparison graphic coverage 0 of M on a rank-0 origin when that persisted used count is a non-negative integer", () => { @@ -1209,8 +1223,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map reconstruction on the comparison graphic with a distinct accessible name", () => { @@ -1246,7 +1260,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic incomplete items")).toHaveTextContent( "Leftover map dropped 0 incomplete criteria", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic reconstruction when R̂ is missing or not usable", () => { @@ -1280,7 +1294,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin reconstruction on the comparison graphic when that persisted value is finite", () => { @@ -1323,7 +1337,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map explained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1359,7 +1373,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic explained leftover share when e is missing or not usable", () => { @@ -1396,7 +1410,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin explained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1443,7 +1457,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1482,7 +1496,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover share when s is missing or not usable", () => { @@ -1522,7 +1536,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin unexplained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1573,7 +1587,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map cross share on the comparison graphic with a distinct accessible name", () => { @@ -1615,7 +1629,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic cross share when x is missing or not usable", () => { @@ -1658,7 +1672,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map cross share on the comparison graphic when that persisted value is finite", () => { @@ -1713,7 +1727,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover on the comparison graphic with a distinct accessible name", () => { @@ -1758,7 +1772,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover when U is missing or not usable", () => { @@ -1804,7 +1818,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map unexplained leftover on the comparison graphic when that persisted value is finite", () => { @@ -1863,7 +1877,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover residual on the comparison graphic with a distinct accessible name", () => { @@ -1911,7 +1925,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover residual when R is missing or not usable", () => { @@ -1960,7 +1974,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover residual on the comparison graphic when that persisted value is finite", () => { @@ -2022,7 +2036,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover observed on the comparison graphic with a distinct accessible name", () => { @@ -2073,7 +2087,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover observed when Y is missing or not usable", () => { @@ -2125,7 +2139,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover observed on the comparison graphic when that persisted value is finite", () => { @@ -2191,7 +2205,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover expected on the comparison graphic with a distinct accessible name", () => { @@ -2241,7 +2255,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover expected when E is missing or not usable", () => { @@ -2296,7 +2310,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover expected on the comparison graphic when that persisted value is finite", () => { @@ -2366,6 +2380,6 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 8b5855be0..fc3d9b06e 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -396,7 +396,12 @@ export function LeftoverMapPlot({ {leftoverMapPlotAxisText(1, leftoverMapAxes, variant)} - + {leftoverMapPlotAxisText(2, leftoverMapAxes, variant)} {layout.ticks.map((tick) => { @@ -423,7 +428,7 @@ export function LeftoverMapPlot({ : tf(LEFTOVER_MAP_PLOT_TICK, { axis: tick.axis, value: tick.label }); return ( diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index 0f2704da5..dedfc5801 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -206,6 +206,23 @@ describe("layoutLeftoverMapPlot", () => { expect(originAxis1).toMatchObject({ x: layout?.originX, y: layout?.originY, label: "0.00" }); }); + it("keeps distinct persisted coordinate ticks when their displayed labels match", () => { + const layout = layoutLeftoverMapPlot( + [ + pair({ + leftover_map_person_axis_1: 0.501, + leftover_map_item_axis_1: 0.504, + }), + ], + criterionLabel, + ); + const matchingTicks = layout?.ticks.filter( + (tick) => tick.axis === 1 && tick.label === "+0.50", + ); + expect(matchingTicks?.map((tick) => tick.value)).toEqual([0.501, 0.504]); + expect(matchingTicks?.[0]?.x).not.toBe(matchingTicks?.[1]?.x); + }); + it("does not invent drawing-scale leftover-map ticks on a rank-0 origin cell", () => { const layout = layoutLeftoverMapPlot( [ diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index f0a3bd148..f70bb98d1 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -288,17 +288,19 @@ function boundedCaptionY(y: number, height: number): number { } function uniqueCoordinateTicks(values: number[]): { value: number; label: string }[] { - const byLabel = new Map(); + const uniqueValues = new Set(); + const ticks: { value: number; label: string }[] = []; for (const value of values) { const label = formatSignedLeftoverValue(value); if (label === null) { continue; } - if (!byLabel.has(label)) { - byLabel.set(label, value); + if (!uniqueValues.has(value)) { + uniqueValues.add(value); + ticks.push({ value, label }); } } - return [...byLabel.entries()].map(([label, value]) => ({ value, label })); + return ticks; } function leftoverMapCoordinateTicks( diff --git a/tests/test_grouping_comparison_graphic_singular_contract.py b/tests/test_grouping_comparison_graphic_singular_contract.py index f2eafc0e9..2815f7204 100644 --- a/tests/test_grouping_comparison_graphic_singular_contract.py +++ b/tests/test_grouping_comparison_graphic_singular_contract.py @@ -9,6 +9,15 @@ APP_TEST_SOURCE = ROOT / "frontend" / "src" / "App.test.tsx" +def _exported_function_source(source: str, signature: str) -> str: + """Return one exported function so independence checks stay scoped to that function.""" + start = source.index(signature) + next_export = source.find("\nexport function ", start + len(signature)) + if next_export == -1: + return source[start:] + return source[start:next_export] + + def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None: """Comparison-axis σ copy must remain distinct from report, strip, share, and tick copy.""" assert SINGULAR_SOURCE.exists(), ( @@ -32,24 +41,28 @@ def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None def test_singular_value_is_read_from_axis_evidence_and_fails_closed() -> None: - """Persisted σ and share are formatted independently; neither is derived from the other.""" + """Persisted σ=0 stays explicit; missing, non-finite, or negative σ omits independently.""" assert SINGULAR_SOURCE.exists(), "persisted singular-value projection helper is missing" singular_source = SINGULAR_SOURCE.read_text(encoding="utf-8") - badge_source = singular_source.split( - "export function leftoverMapComparePlotAxisBadge", 1 - )[-1].split("export function leftoverMapCompareAxisBadge", 1)[0] - - assert "axis.leftover_singular_value" in badge_source - assert "axis.leftover_share" in badge_source - assert "formatLeftoverMapPlotAxisSingular" in badge_source - assert "formatLeftoverMapPlotAxisShare" in badge_source - assert "Number.isFinite" in singular_source - assert "< 0" in singular_source - assert "return null" in singular_source - assert ".toFixed(2)" in singular_source - assert "Math.sqrt" not in badge_source - assert "Math.max" not in badge_source - assert "Math.min" not in badge_source + formatter_source = _exported_function_source( + singular_source, + "export function formatLeftoverMapPlotAxisSingular", + ) + + assert "Number.isFinite" in formatter_source + assert "< 0" in formatter_source + assert "return null" in formatter_source + assert ".toFixed(2)" in formatter_source + assert "leftover_share" not in formatter_source + assert "Math.sqrt" not in formatter_source + assert "Math.max" not in formatter_source + assert "Math.min" not in formatter_source + + assert ( + "const singular = formatLeftoverMapPlotAxisSingular(axis.leftover_singular_value);" + in singular_source + ) + assert "const share = formatLeftoverMapPlotAxisShare(axis.leftover_share);" in singular_source def test_app_acceptance_requires_exact_comparison_sigma_and_share_copy() -> None: