diff --git a/AGENTS.md b/AGENTS.md
index 67960eefe..d3d2c7e3e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -792,6 +792,17 @@ person coordinates `(0.00, 0.00)`. Do not invent leftover-map person 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. Leftover-map comparison graphic leftover-map
post markers stay `Open leftover map comparison graphic leftover-map post {title} at ξ {person}` this increment.
+ADR 0340 captions leftover-map pair leftover-map criterion with persisted leftover-map
+item coordinates when leftoverMapListCriterionBadge returns a usable leftover-map pair leftover-map criterion leftover-map
+item coordinate caption. Distinct accessible name
+`leftover pair leftover-map criterion {label} at ζ {item}`.
+A missing or non-finite leftover-map item coordinate pair omits that leftover-map pair leftover-map criterion leftover-map
+item coordinate caption. Rank-0 unused axes still name leftover-map
+item coordinates `(0.00, 0.00)`. Do not invent leftover-map item coordinates from leftover-map person coordinates
+`ξ`. Do not invent leftover-map person coordinates `ξ` from leftover-map item coordinates `ζ`. Leftover-map pair leftover-map
+post leftover-map person coordinates stay `leftover pair leftover-map post {title} at ξ {person}` this increment. Leftover-map graphic leftover-map
+criterion markers stay `leftover-map criterion {label} at ζ {item}` this increment. Leftover-map comparison graphic leftover-map
+criterion markers stay `leftover map comparison graphic leftover-map criterion {label} at ζ {item}` 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
@@ -953,6 +964,10 @@ Leftover-map pair leftover-map post leftover-map person coordinates independentl
(ADR 0339) captions leftover-map pair leftover-map post with persisted leftover-map
person coordinates when leftoverMapListPostBadge returns a usable leftover-map pair leftover-map post leftover-map
person coordinate caption.
+Leftover-map pair leftover-map criterion leftover-map item coordinates independently of leftover-map pair leftover-map post leftover-map person coordinates
+(ADR 0340) captions leftover-map pair leftover-map criterion with persisted leftover-map
+item coordinates when leftoverMapListCriterionBadge returns a usable leftover-map pair leftover-map criterion leftover-map
+item 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 b97921a66..946cee918 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -800,6 +800,9 @@ person coordinate caption independently of leftover-map comparison graphic lefto
ADR 0339 captions leftover-map pair leftover-map post with persisted leftover-map
person coordinates when leftoverMapListPostBadge returns a usable leftover-map pair leftover-map post leftover-map
person coordinate caption independently of leftover-map pair leftover-map criterion leftover-map item coordinates.
+ADR 0340 captions leftover-map pair leftover-map criterion with persisted leftover-map
+item coordinates when leftoverMapListCriterionBadge returns a usable leftover-map pair leftover-map criterion leftover-map
+item coordinate caption independently of leftover-map pair leftover-map post leftover-map person 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
@@ -869,7 +872,8 @@ comparison graphic leftover-map axis leftover-map singular values as leftoverMap
(ADR 0336), leftover-map comparison graphic leftover-map post leftover-map person coordinates independently of leftover-map criterion leftover-map item coordinates
(ADR 0337), leftover-map graphic leftover-map post leftover-map person coordinates independently of leftover-map comparison graphic leftover-map post markers
(ADR 0338), leftover-map pair leftover-map post leftover-map person coordinates independently of leftover-map pair leftover-map criterion leftover-map item coordinates
-(ADR 0339);
+(ADR 0339), leftover-map pair leftover-map criterion leftover-map item coordinates independently of leftover-map pair leftover-map post leftover-map person coordinates
+(ADR 0340);
`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.97.0-leftover-map-list-criterion-coordinates.md b/CHANGELOG.d/2.97.0-leftover-map-list-criterion-coordinates.md
new file mode 100644
index 000000000..6a2fd3a94
--- /dev/null
+++ b/CHANGELOG.d/2.97.0-leftover-map-list-criterion-coordinates.md
@@ -0,0 +1,24 @@
+## 2.97.0 — Leftover-map item coordinates on leftover-map pair leftover-map criterion independently of leftover-map pair leftover-map post leftover-map person coordinates
+
+- Caption leftover-map pair leftover-map criterion with persisted leftover-map
+ item coordinates through leftoverMapListCriterionBadge independently of leftover-map
+ pair leftover-map post leftover-map person coordinates (ADR 0340). After `make seed`, leftover-map pair leftover-map
+ criterion name persisted leftover-map item coordinates when leftoverMapListCriterionBadge
+ returns a usable leftover-map pair leftover-map criterion leftover-map item coordinate caption; click a leftover pair
+ or a leftover-map post marker opens that post. A missing or
+ non-finite leftover-map item coordinate pair omits that leftover-map pair leftover-map criterion leftover-map
+ item coordinate caption. Rank-0 unused
+ axes still name leftover-map item coordinates `(0.00, 0.00)`. Leftover-map pair leftover-map
+ criterion stay
+ `leftover pair leftover-map criterion {label} at ζ {item}`
+ when leftover-map item coordinates are finite so they stay distinct from leftover-map graphic leftover-map criterion markers
+ `leftover-map criterion {label} at ζ {item}`, leftover-map comparison graphic leftover-map criterion markers
+ `leftover map comparison graphic leftover-map criterion {label} at ζ {item}`, and leftover-map pair leftover-map post leftover-map person coordinates
+ `leftover pair leftover-map post {title} at ξ {person}`. Do not invent leftover-map
+ item coordinates from leftover-map person coordinates `ξ`. Do not invent leftover-map person coordinates `ξ` from leftover-map
+ item coordinates `ζ`. This increment does not change leftover-map pair leftover-map
+ post leftover-map person coordinates, leftover-map graphic leftover-map
+ post markers, leftover-map comparison graphic leftover-map post markers, leftover-map graphic leftover-map criterion markers, leftover-map comparison 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 b0605d26b..08b791b84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,28 @@ All notable changes to this project are documented here. Format follows
### Added
+- Period leftover pairs now caption leftover-map pair leftover-map
+ criterion with persisted leftover-map item coordinates through leftoverMapListCriterionBadge
+ independently of leftover-map pair leftover-map post leftover-map person coordinates (ADR 0340 / v2.97.0). After `make seed`,
+ leftover-map pair leftover-map criterion name persisted leftover-map
+ item coordinates when leftoverMapListCriterionBadge returns a usable leftover-map pair leftover-map criterion
+ leftover-map item coordinate caption; click a leftover pair or a leftover-map post marker
+ opens that post. A missing or non-finite leftover-map item coordinate pair omits that leftover-map
+ pair leftover-map criterion leftover-map item coordinate caption.
+ Rank-0 unused axes still name leftover-map item coordinates `(0.00, 0.00)`. Leftover-map pair leftover-map
+ criterion stay
+ `leftover pair leftover-map criterion {label} at ζ {item}`
+ when leftover-map item coordinates are finite so they stay distinct from leftover-map graphic leftover-map criterion markers
+ `leftover-map criterion {label} at ζ {item}`, leftover-map comparison graphic leftover-map criterion markers
+ `leftover map comparison graphic leftover-map criterion {label} at ζ {item}`, and leftover-map pair leftover-map post leftover-map person coordinates
+ `leftover pair leftover-map post {title} at ξ {person}`. Do not invent leftover-map
+ item coordinates from leftover-map person coordinates `ξ`. Do not invent leftover-map person coordinates `ξ` from leftover-map
+ item coordinates `ζ`. This increment does not change leftover-map pair leftover-map
+ post leftover-map person coordinates, leftover-map graphic leftover-map
+ post markers, leftover-map comparison graphic leftover-map post markers, leftover-map graphic leftover-map criterion markers, leftover-map comparison 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 pair leftover-map
post with persisted leftover-map person coordinates through leftoverMapListPostBadge
independently of leftover-map pair leftover-map criterion leftover-map item coordinates (ADR 0339 / v2.96.0). After `make seed`,
diff --git a/CLAUDE.md b/CLAUDE.md
index 998ba4b7a..e01c0d2eb 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 / 0337 / 0338 / 0339), 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 / 0338 / 0339 / 0340), 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 fc76fc96a..e316c2dfb 100644
--- a/docs/adr/0267-leftover-map-coordinates.md
+++ b/docs/adr/0267-leftover-map-coordinates.md
@@ -16,7 +16,9 @@
[ADR 0338](0338-leftover-map-plot-post-coordinates.md)
(leftover-map person coordinates on leftover-map graphic leftover-map post markers);
[ADR 0339](0339-leftover-map-list-post-coordinates.md)
-(leftover-map person coordinates on leftover-map pair leftover-map post independently of leftover-map pair leftover-map criterion leftover-map item coordinates)
+(leftover-map person coordinates on leftover-map pair leftover-map post independently of leftover-map pair leftover-map criterion leftover-map item coordinates);
+[ADR 0340](0340-leftover-map-list-criterion-coordinates.md)
+(leftover-map item coordinates on leftover-map pair leftover-map criterion independently of leftover-map pair leftover-map post leftover-map person coordinates)
Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md),
[ADR 0049](0049-leftover-pair-report-ui.md), and
diff --git a/docs/adr/0339-leftover-map-list-post-coordinates.md b/docs/adr/0339-leftover-map-list-post-coordinates.md
index 44ce49027..020598bbf 100644
--- a/docs/adr/0339-leftover-map-list-post-coordinates.md
+++ b/docs/adr/0339-leftover-map-list-post-coordinates.md
@@ -3,6 +3,9 @@
**Decision status:** Accepted
**Date:** 2026-08-31
+**Amended by:** [ADR 0340](0340-leftover-map-list-criterion-coordinates.md)
+(leftover-map item coordinates on leftover-map pair leftover-map criterion independently of leftover-map pair leftover-map post leftover-map person coordinates)
+
Amends leftover-map coordinates
([ADR 0267](0267-leftover-map-coordinates.md)). Independent of leftover-map
graphic leftover-map post leftover-map person coordinates independently of leftover-map comparison graphic leftover-map post markers
diff --git a/docs/adr/0340-leftover-map-list-criterion-coordinates.md b/docs/adr/0340-leftover-map-list-criterion-coordinates.md
new file mode 100644
index 000000000..aec9907e4
--- /dev/null
+++ b/docs/adr/0340-leftover-map-list-criterion-coordinates.md
@@ -0,0 +1,142 @@
+# ADR 0340 — Name leftover-map item coordinates on leftover-map pair leftover-map criterion independently of leftover-map pair leftover-map post leftover-map person coordinates
+
+**Decision status:** Accepted
+**Date:** 2026-08-31
+
+Amends leftover-map coordinates
+([ADR 0267](0267-leftover-map-coordinates.md)). Independent of leftover-map
+pair leftover-map post leftover-map person coordinates independently of leftover-map pair leftover-map criterion leftover-map item coordinates
+([ADR 0339](0339-leftover-map-list-post-coordinates.md)). Independent of leftover-map
+graphic leftover-map post leftover-map person coordinates independently of leftover-map comparison graphic leftover-map post markers
+([ADR 0338](0338-leftover-map-plot-post-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)). 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-map
+graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)). Independent of leftover-map
+comparison graphic ([ADR 0304](0304-leftover-map-compare-graphic.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 0339 already names leftover-map pair leftover-map
+post leftover-map person coordinates
+`leftover pair leftover-map post {title} at ξ {person}`
+independently of leftover-map pair leftover-map criterion leftover-map
+item coordinates. Pair-list leftover-map criterion leftover-map item
+coordinates still interpolate leftover-map item coordinates only through
+`formatLeftoverMapCoordinates`, which requires both leftover-map
+person coordinates `ξ` and leftover-map item coordinates `ζ`. A
+missing or non-finite leftover-map pair leftover-map post leftover-map
+person coordinate pair hides leftover-map pair leftover-map criterion leftover-map
+item coordinates. Rank-0 unused axes still persist leftover-map
+item coordinates `(0.00, 0.00)`. A finite negative leftover is shown, never
+clamped. Do not invent leftover-map item coordinates from leftover-map person
+coordinates `ξ`. Do not invent leftover-map person coordinates `ξ` from leftover-map
+item coordinates `ζ`. Leftover-map pair leftover-map post leftover-map person coordinates stay
+`leftover pair leftover-map post {title} at ξ {person}`
+this increment. Leftover-map graphic leftover-map criterion markers stay
+`leftover-map criterion {label} at ζ {item}`
+this increment. Leftover-map comparison graphic leftover-map criterion markers stay
+`leftover map comparison graphic leftover-map criterion {label} at ζ {item}`
+this increment.
+
+This increment names leftover-map pair leftover-map criterion leftover-map
+item coordinates as leftoverMapListCriterionBadge, matching leftover-map
+graphic leftover-map criterion leftover-map item coordinates independently of leftover-map
+pair leftover-map post leftover-map person coordinates. Leftover-map pair leftover-map
+criterion leftover-map item coordinates stay
+`leftover pair leftover-map criterion {label} at ζ {item}`
+when leftover-map item coordinates are finite, so they stay distinct from leftover-map
+graphic leftover-map criterion markers
+`leftover-map criterion {label} at ζ {item}`,
+leftover-map comparison graphic leftover-map criterion markers
+`leftover map comparison graphic leftover-map criterion {label} at ζ {item}`,
+and leftover-map pair leftover-map post leftover-map person coordinates
+`leftover pair leftover-map post {title} at ξ {person}`.
+It does not add columns. Do not invent a leftover score. Do not invent a theta.
+
+This protected increment uses **0340** so it does not collide with leftover-map
+pair leftover-map post leftover-map person coordinates independently of leftover-map pair leftover-map criterion leftover-map item coordinates
+(0339), leftover-map graphic leftover-map post leftover-map person coordinates independently of leftover-map comparison graphic leftover-map post markers
+(0338), leftover-map comparison graphic leftover-map post leftover-map person coordinates independently of leftover-map criterion leftover-map item coordinates
+(0337), 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-map graphic display (0268), leftover-map coordinates (0267),
+or the dashboard stacks.
+
+## Decision
+
+On leftover-map pair leftover-map criterion, caption leftover-map
+item coordinates `ζ_{1:2}` when leftoverMapListCriterionBadge
+returns a usable leftover-map pair leftover-map criterion leftover-map item coordinate caption.
+A missing or non-finite leftover-map item coordinate pair omits that leftover-map
+pair leftover-map criterion leftover-map item coordinate caption. Rank-0 unused axes still name leftover-map
+item 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 pair leftover-map criterion leftover-map item coordinates omit
+independently of leftover-map pair leftover-map post leftover-map person
+coordinates. This increment does not change leftover-map pair leftover-map
+post leftover-map person coordinates, leftover-map graphic leftover-map
+post markers, leftover-map comparison graphic leftover-map
+post markers, leftover-map graphic leftover-map criterion markers,
+leftover-map comparison 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 pair leftover-map criterion name persisted
+leftover-map item coordinates when leftoverMapListCriterionBadge returns a usable
+leftover-map pair leftover-map criterion leftover-map item 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 pair leftover-map criterion leftover-map item coordinates
+`(0.00, 0.00)`. Leftover-map pair leftover-map post leftover-map person coordinates stay
+`leftover pair leftover-map post {title} at ξ {person}`. Leftover-map graphic leftover-map criterion markers stay
+`leftover-map criterion {label} at ζ {item}`. Leftover-map comparison graphic leftover-map criterion markers stay
+`leftover map comparison graphic leftover-map criterion {label} at ζ {item}`. Leftover-map pair leftover-map post leftover-map person coordinates independently of leftover-map
+pair leftover-map criterion leftover-map item coordinates (ADR 0339), leftover-map graphic leftover-map post leftover-map person coordinates independently of leftover-map
+comparison graphic leftover-map post markers (ADR 0338), leftover-map comparison graphic leftover-map post leftover-map person coordinates independently of leftover-map
+criterion leftover-map item coordinates (ADR 0337), 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-map graphic display (ADR 0268), and leftover-map
+coordinates (ADR 0267) remain.
+
+## Related
+
+Independent of leftover-map pair leftover-map post leftover-map person coordinates independently of leftover-map
+pair leftover-map criterion leftover-map item coordinates
+([ADR 0339](0339-leftover-map-list-post-coordinates.md)). Independent of leftover-map graphic leftover-map post leftover-map person coordinates independently of leftover-map
+comparison graphic leftover-map post markers
+([ADR 0338](0338-leftover-map-plot-post-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)). 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-map
+graphic display ([ADR 0268](0268-leftover-map-graphic-display.md)). Independent of leftover-map
+comparison graphic ([ADR 0304](0304-leftover-map-compare-graphic.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 8cab28c94..792c230c2 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,27 @@
# Product & Technical Gap Baseline
+> Exact-head loop overlay: 2026-08-31 18:20 KST. Protected `main` is
+> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not
+> self-approve. #881 leftover-map person coordinates on leftover-map pair leftover-map
+> post independently of leftover-map pair leftover-map criterion leftover-map item coordinates (v2.96.0 / ADR 0339) is
+> `167888d`, stacked on #880 @ `c69459a`. Independent APPROVE is missing (Devin COMMENTED,
+> not APPROVE). Next buyer increment on this cycle: leftover-map pair leftover-map
+> criterion naming persisted leftover-map item coordinates independently of leftover-map
+> pair leftover-map post leftover-map person coordinates (ADR 0340 / v2.97.0) delivered locally on
+> `feat/leftover-map-list-criterion-coordinates-v2970`. Caption persisted leftover-map
+> item coordinates on leftover-map pair leftover-map criterion when leftoverMapListCriterionBadge
+> returns a usable leftover-map pair leftover-map criterion leftover-map item coordinate caption. Distinct accessible name
+> `leftover pair leftover-map criterion {label} at ζ {item}`.
+> Rank-0 unused axes still name leftover-map item coordinates `(0.00, 0.00)`. A missing or
+> non-finite leftover-map item coordinate pair omits independently of leftover-map pair leftover-map post leftover-map person `ξ`. Leftover-map pair leftover-map post leftover-map person coordinates stay
+> `leftover pair leftover-map post {title} at ξ {person}` this increment. Leftover-map graphic leftover-map criterion markers stay
+> `leftover-map criterion {label} at ζ {item}` this increment. Leftover-map comparison graphic leftover-map criterion markers stay
+> `leftover map comparison graphic leftover-map criterion {label} at ζ {item}` this increment. Do not invent leftover-map
+> item coordinates from leftover-map person coordinates `ξ`. Do not invent leftover-map
+> person coordinates `ξ` from leftover-map item coordinates `ζ`. Do not persist leftover-map inner product, cosine, or length. Stack onto #881.
+> 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 17:40 KST. Protected `main` is
> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not
> self-approve. #880 leftover-map person coordinates on leftover-map graphic leftover-map
diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md
index acf5d348b..f6f6dae0a 100644
--- a/docs/storybook-inventory.md
+++ b/docs/storybook-inventory.md
@@ -6,7 +6,7 @@ operator-facing control you can click before changing product CSS.
| Story | Operator next action | Token / module |
|---|---|---|
| `Reports/LeftoverMapPlot` | Read the leftover-map graphic display of persisted `ξ` (posts) and `ζ` (criteria), match leftover-map graphic leftover-map post markers to `Open leftover-map post {title} at ξ {person}`, 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. Leftover-map pair leftover-map post name persisted leftover-map person coordinates under `leftover pair leftover-map post {title} at ξ {person}` independently of leftover-map pair leftover-map criterion leftover-map item coordinates when leftoverMapListPostBadge returns a usable leftover-map pair leftover-map post leftover-map person coordinate caption. 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 post markers name persisted leftover-map person coordinates under `Open leftover-map post {title} at ξ {person}`, 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`, `leftoverMapCoordinates`, `ticket-list`, `post-badge` |
+| `Reports/LeftoverPairList` | Read closest/farthest leftover pairs with named `R`, `Y`/`E`, rank, `U`, `s`, `e`, `x`, `R̂`, `ξ`/`ζ`, and `d`, then open that post. Leftover-map pair leftover-map post name persisted leftover-map person coordinates under `leftover pair leftover-map post {title} at ξ {person}` independently of leftover-map pair leftover-map criterion leftover-map item coordinates when leftoverMapListPostBadge returns a usable leftover-map pair leftover-map post leftover-map person coordinate caption. Leftover-map pair leftover-map criterion name persisted leftover-map item coordinates under `leftover pair leftover-map criterion {label} at ζ {item}` independently of leftover-map pair leftover-map post leftover-map person coordinates when leftoverMapListCriterionBadge returns a usable leftover-map pair leftover-map criterion leftover-map item coordinate caption. 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 post markers name persisted leftover-map person coordinates under `Open leftover-map post {title} at ξ {person}`, 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`, `leftoverMapCoordinates`, `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 47e199441..9d7265c62 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.96.0",
+ "version": "2.97.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index a86c7f17d..0044c5fa4 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -4300,6 +4300,9 @@ describe("App, authenticated", () => {
expect(closestPair).toHaveTextContent("2R̂U/R² 0.12");
expect(closestPair).toHaveTextContent("R̂ +0.25");
expect(closestPair).toHaveTextContent("ξ (+0.50, +0.10) ζ (+0.50, −0.02)");
+ expect(closestPair).toHaveTextContent(
+ "leftover pair leftover-map criterion sales-lead at ζ (+0.50, −0.02)",
+ );
expect(closestPair).toHaveTextContent("d 0.12");
expect(closestPair).toHaveAccessibleName(
"leftover pair leftover-map post Public post at ξ (+0.50, +0.10)",
@@ -4317,7 +4320,16 @@ describe("App, authenticated", () => {
expect(farthestPair).toHaveTextContent("2R̂U/R² -0.24");
expect(farthestPair).toHaveTextContent("R̂ −0.95");
expect(farthestPair).toHaveTextContent("ξ (+0.90, +0.80) ζ (−0.70, −0.40)");
+ expect(farthestPair).toHaveTextContent(
+ "leftover pair leftover-map criterion negative at ζ (−0.70, −0.40)",
+ );
expect(farthestPair).toHaveTextContent("d 2.00");
+ expect(
+ screen.getByLabelText("leftover pair leftover-map criterion sales-lead at ζ (+0.50, −0.02)"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText("leftover pair leftover-map criterion negative at ζ (−0.70, −0.40)"),
+ ).toBeInTheDocument();
expect(0.5 * 0.5 + 0.1 * -0.02).toBeCloseTo(0.248);
expect(Math.hypot(0.5 - 0.5, 0.1 - -0.02)).toBeCloseTo(0.12);
expect(0.9 * -0.7 + 0.8 * -0.4).toBeCloseTo(-0.95);
diff --git a/frontend/src/components/LeftoverPairList.test.tsx b/frontend/src/components/LeftoverPairList.test.tsx
index 0ffd611f2..36de3d5cb 100644
--- a/frontend/src/components/LeftoverPairList.test.tsx
+++ b/frontend/src/components/LeftoverPairList.test.tsx
@@ -154,6 +154,7 @@ describe("LeftoverPairList", () => {
"Leftover map places this post at ξ (+0.50, +0.10) and the criterion at ζ (+0.50, −0.02) after IRT main effects. Open this post to read sales-lead.",
);
expect(closest).toHaveTextContent("ξ (+0.50, +0.10) ζ (+0.50, −0.02)");
+ expect(closest).toHaveTextContent("leftover pair leftover-map criterion sales-lead at ζ (+0.50, −0.02)");
expect(closest).toHaveTextContent("R̂²/R² 0.76");
expect(closest).toHaveTextContent("U²/R² 0.02");
expect(closest).toHaveTextContent("2R̂U/R² 0.12");
@@ -168,6 +169,9 @@ describe("LeftoverPairList", () => {
}),
).toBeInTheDocument();
expect(screen.getByLabelText("leftover-map criterion sales-lead at ζ (+0.50, −0.02)")).toBeInTheDocument();
+ expect(
+ screen.getByLabelText("leftover pair leftover-map criterion sales-lead at ζ (+0.50, −0.02)"),
+ ).toBeInTheDocument();
expect(screen.queryByLabelText("Criterion ζ sales-lead")).not.toBeInTheDocument();
expect(
screen.queryByRole("button", {
@@ -198,6 +202,9 @@ describe("LeftoverPairList", () => {
});
expect(closest).toHaveTextContent("Closest leftover: Public post · sales-lead");
expect(closest).not.toHaveTextContent("ξ (+0.50, +0.10) ζ");
+ expect(
+ screen.queryByLabelText("leftover pair leftover-map criterion sales-lead at ζ"),
+ ).not.toBeInTheDocument();
expect(
screen.queryByRole("button", {
name: "Open leftover closest pair: Public post · sales-lead",
@@ -229,6 +236,9 @@ describe("LeftoverPairList", () => {
name: "leftover pair leftover-map post Public post at ξ (0.00, 0.00)",
}),
).toBeInTheDocument();
+ expect(
+ screen.getByLabelText("leftover pair leftover-map criterion sales-lead at ζ (0.00, 0.00)"),
+ ).toBeInTheDocument();
});
it("keeps leftover-map pair leftover-map post leftover-map person coordinates omitted when ξ is missing", () => {
@@ -258,6 +268,9 @@ describe("LeftoverPairList", () => {
name: /leftover pair leftover-map post Public post at ξ/,
}),
).not.toBeInTheDocument();
+ expect(
+ screen.getByLabelText("leftover pair leftover-map criterion sales-lead at ζ (+0.50, −0.02)"),
+ ).toBeInTheDocument();
});
it("opens the named post from a leftover-map graphic display marker", async () => {
diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx
index d0814099a..4fc9af145 100644
--- a/frontend/src/components/LeftoverPairList.tsx
+++ b/frontend/src/components/LeftoverPairList.tsx
@@ -7,6 +7,7 @@ import {
import {
formatLeftoverMapCoordinatePair,
formatLeftoverMapCoordinates,
+ leftoverMapListCriterionBadge,
leftoverMapListPostBadge,
LEFTOVER_MAP_COORDINATES_ACTION,
} from "../leftoverMapCoordinates";
@@ -141,6 +142,8 @@ export type LeftoverPairListProps = {
* coordinates and leftover-map post leftover-map person coordinates, not this pair list.
* ADR 0339 captions leftover-map pair leftover-map post leftover-map person coordinates through leftoverMapListPostBadge
* independently of leftover-map pair leftover-map criterion leftover-map item coordinates.
+ * ADR 0340 captions leftover-map pair leftover-map criterion leftover-map item coordinates through leftoverMapListCriterionBadge
+ * independently of leftover-map pair leftover-map post leftover-map person coordinates.
* Every badge still
* renders together before opening the named post.
*/
@@ -192,6 +195,11 @@ export function LeftoverPairList({
pair.leftover_map_person_axis_1,
pair.leftover_map_person_axis_2,
);
+ const listCriterionBadge = leftoverMapListCriterionBadge(
+ criterion,
+ pair.leftover_map_item_axis_1,
+ pair.leftover_map_item_axis_2,
+ );
const crossShareBadge = formatLeftoverMapCrossShare(pair.leftover_map_cross_share);
const reconstruction = formatLeftoverMapReconstruction(
pair.leftover_map_reconstruction,
@@ -330,6 +338,14 @@ export function LeftoverPairList({
{crossShareBadge ? {crossShareBadge} : null}
{reconstruction ? {reconstruction} : null}
{coordinatesBadge ? {coordinatesBadge} : null}
+ {listCriterionBadge ? (
+
+ {tf(listCriterionBadge.key, listCriterionBadge.values)}
+
+ ) : null}
d {pair.leftover_distance.toFixed(2)}
diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts
index 9f1de7dc5..56f87a964 100644
--- a/frontend/src/i18n.test.ts
+++ b/frontend/src/i18n.test.ts
@@ -57,6 +57,7 @@ describe("i18n", () => {
"Leftover residual R {residual} after IRT main effects. Open this post to read {criterion}.",
"Open leftover {kind} pair: {title} · {criterion}",
"leftover pair leftover-map post {title} at ξ {person}",
+ "leftover pair leftover-map criterion {label} at ζ {item}",
"Open this post to read the criterion it sat closest to after main effects.",
"Open this post to read the criterion it sat farthest from after main effects.",
"Leftover map leaves unexplained U {value} after IRT main effects. Open this post to read {criterion}.",
diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts
index ec33e5efc..7fdc3d332 100644
--- a/frontend/src/i18n.ts
+++ b/frontend/src/i18n.ts
@@ -645,6 +645,8 @@ const TRANSLATIONS: Partial>> = {
"잔여 {kind} 쌍 열기: {title} · {criterion}",
"leftover pair leftover-map post {title} at ξ {person}":
"잔여 쌍 잔여 지도 글 {title} (ξ {person})",
+ "leftover pair leftover-map criterion {label} at ζ {item}":
+ "잔여 쌍 잔여 지도 기준 {label} (ζ {item})",
"Open this post to read the criterion it sat closest to after main effects.":
"주효과 이후 가장 가깝게 앉은 기준을 읽으려면 이 글을 여세요.",
"Open this post to read the criterion it sat farthest from after main effects.":
@@ -1328,6 +1330,8 @@ const TRANSLATIONS: Partial>> = {
"打开残余 {kind} 配对:{title} · {criterion}",
"leftover pair leftover-map post {title} at ξ {person}":
"残差配对残差图帖子 {title}(ξ {person})",
+ "leftover pair leftover-map criterion {label} at ζ {item}":
+ "残差配对残差图准则 {label}(ζ {item})",
"Open this post to read the criterion it sat closest to after main effects.":
"打开这篇帖子,阅读主效应后距离最近的准则。",
"Open this post to read the criterion it sat farthest from after main effects.":
@@ -2015,6 +2019,8 @@ const TRANSLATIONS: Partial>> = {
"残差 {kind} ペアを開く: {title} · {criterion}",
"leftover pair leftover-map post {title} at ξ {person}":
"残差ペアの残差マップ投稿 {title}(ξ {person})",
+ "leftover pair leftover-map criterion {label} at ζ {item}":
+ "残差ペアの残差マップ基準 {label}(ζ {item})",
"Open this post to read the criterion it sat closest to after main effects.":
"主効果後に最も近くなった基準を読むには、この投稿を開いてください。",
"Open this post to read the criterion it sat farthest from after main effects.":
@@ -2706,6 +2712,8 @@ const TRANSLATIONS: Partial>> = {
"Mở cặp phần dư {kind}: {title} · {criterion}",
"leftover pair leftover-map post {title} at ξ {person}":
"Cặp phần dư bài viết bản đồ phần dư {title} tại ξ {person}",
+ "leftover pair leftover-map criterion {label} at ζ {item}":
+ "Cặp phần dư tiêu chí bản đồ phần dư {label} tại ζ {item}",
"Open this post to read the criterion it sat closest to after main effects.":
"Mở bài viết này để đọc tiêu chí nằm gần nhất sau hiệu ứng chính.",
"Open this post to read the criterion it sat farthest from after main effects.":
diff --git a/frontend/src/leftoverMapCoordinates.test.ts b/frontend/src/leftoverMapCoordinates.test.ts
index 310da3238..9a243efea 100644
--- a/frontend/src/leftoverMapCoordinates.test.ts
+++ b/frontend/src/leftoverMapCoordinates.test.ts
@@ -1,15 +1,21 @@
import { describe, expect, it } from "vitest";
import {
+ leftoverMapComparePlotCriterionBadge,
leftoverMapComparePlotPostBadge,
+ leftoverMapPlotCriterionBadge,
leftoverMapPlotPostBadge,
+ LEFTOVER_MAP_COMPARE_PLOT_CRITERION,
LEFTOVER_MAP_COMPARE_PLOT_POST_ACTION,
+ LEFTOVER_MAP_PLOT_CRITERION,
LEFTOVER_MAP_PLOT_POST_ACTION,
} from "./leftoverMapPlotLayout";
import {
formatLeftoverMapCoordinatePair,
formatLeftoverMapCoordinates,
+ leftoverMapListCriterionBadge,
leftoverMapListPostBadge,
LEFTOVER_MAP_COMPARE_COORDINATES_LABEL,
+ LEFTOVER_MAP_LIST_CRITERION,
LEFTOVER_MAP_LIST_POST_ACTION,
} from "./leftoverMapCoordinates";
@@ -98,3 +104,56 @@ describe("leftoverMapListPostBadge", () => {
);
});
});
+
+describe("leftoverMapListCriterionBadge", () => {
+ it("names persisted leftover-map item coordinates without inventing a leftover score", () => {
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, -0.02)).toEqual({
+ key: LEFTOVER_MAP_LIST_CRITERION,
+ values: { label: "sales-lead", item: "(+0.50, \u22120.02)" },
+ });
+ expect(leftoverMapListCriterionBadge("negative", -0.7, -0.4)).toEqual({
+ key: LEFTOVER_MAP_LIST_CRITERION,
+ values: { label: "negative", item: "(\u22120.70, \u22120.40)" },
+ });
+ });
+
+ it("names rank-0 origin leftover-map item coordinates as ζ (0.00, 0.00)", () => {
+ expect(leftoverMapListCriterionBadge("sales-lead", 0, 0)).toEqual({
+ key: LEFTOVER_MAP_LIST_CRITERION,
+ values: { label: "sales-lead", item: "(0.00, 0.00)" },
+ });
+ });
+
+ it("omits leftover-map item coordinates when ζ is missing or non-finite", () => {
+ expect(leftoverMapListCriterionBadge("sales-lead", null, -0.02)).toBeNull();
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, undefined)).toBeNull();
+ expect(leftoverMapListCriterionBadge("sales-lead", Number.NaN, -0.02)).toBeNull();
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, Number.POSITIVE_INFINITY)).toBeNull();
+ });
+
+ it("names leftover-map pair leftover-map criterion leftover-map item coordinates independently of leftover-map pair leftover-map post leftover-map person coordinates", () => {
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, -0.02)).not.toBeNull();
+ expect(formatLeftoverMapCoordinates(null, 0.1, 0.5, -0.02)).toBeNull();
+ expect(formatLeftoverMapCoordinates(Number.NaN, 0.1, 0.5, -0.02)).toBeNull();
+ expect(leftoverMapListPostBadge("Public post", null, 0.1)).toBeNull();
+ });
+
+ it("stays distinct from leftover-map graphic leftover-map criterion markers, leftover-map comparison graphic leftover-map criterion markers, and leftover-map pair leftover-map post leftover-map person coordinates", () => {
+ expect(LEFTOVER_MAP_LIST_CRITERION).toBe(
+ "leftover pair leftover-map criterion {label} at ζ {item}",
+ );
+ expect(LEFTOVER_MAP_LIST_CRITERION).not.toBe(LEFTOVER_MAP_PLOT_CRITERION);
+ expect(LEFTOVER_MAP_LIST_CRITERION).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_CRITERION);
+ expect(LEFTOVER_MAP_LIST_CRITERION).not.toBe(LEFTOVER_MAP_LIST_POST_ACTION);
+ expect(LEFTOVER_MAP_LIST_CRITERION).not.toBe("Criterion ζ {label}");
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe(
+ leftoverMapPlotCriterionBadge("sales-lead", 0.5, -0.02)?.key ?? "",
+ );
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe(
+ leftoverMapComparePlotCriterionBadge("sales-lead", 0.5, -0.02)?.key ?? "",
+ );
+ expect(leftoverMapListCriterionBadge("sales-lead", 0.5, -0.02)?.key).not.toBe(
+ leftoverMapListPostBadge("Public post", 0.5, 0.1)?.key ?? "",
+ );
+ });
+});
diff --git a/frontend/src/leftoverMapCoordinates.ts b/frontend/src/leftoverMapCoordinates.ts
index bccb06faa..7c7e45514 100644
--- a/frontend/src/leftoverMapCoordinates.ts
+++ b/frontend/src/leftoverMapCoordinates.ts
@@ -11,6 +11,9 @@ export const LEFTOVER_MAP_COMPARE_COORDINATES_LABEL =
export const LEFTOVER_MAP_LIST_POST_ACTION =
"leftover pair leftover-map post {title} at ξ {person}";
+export const LEFTOVER_MAP_LIST_CRITERION =
+ "leftover pair leftover-map criterion {label} at ζ {item}";
+
const PERSON_BADGE = "\u03BE";
const ITEM_BADGE = "\u03B6";
@@ -19,6 +22,11 @@ export type LeftoverMapListPostBadge = {
values: { title: string; person: string };
};
+export type LeftoverMapListCriterionBadge = {
+ key: string;
+ values: { label: string; item: string };
+};
+
export function formatLeftoverMapCoordinatePair(
axis1: number | null | undefined,
axis2: number | null | undefined,
@@ -60,3 +68,16 @@ export function leftoverMapListPostBadge(
}
return { key: LEFTOVER_MAP_LIST_POST_ACTION, values: { title, person } };
}
+
+/** ADR 0340 leftover-map pair leftover-map criterion leftover-map item coordinates ζ. */
+export function leftoverMapListCriterionBadge(
+ label: string,
+ axis1: number | null | undefined,
+ axis2: number | null | undefined,
+): LeftoverMapListCriterionBadge | null {
+ const item = formatLeftoverMapCoordinatePair(axis1, axis2);
+ if (item === null) {
+ return null;
+ }
+ return { key: LEFTOVER_MAP_LIST_CRITERION, values: { label, item } };
+}
diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts
index 6fee93a37..e08e175d6 100644
--- a/frontend/src/leftoverMapPlotLayout.ts
+++ b/frontend/src/leftoverMapPlotLayout.ts
@@ -135,6 +135,10 @@
* person coordinates ξ through leftoverMapListPostBadge independently of leftover-map
* pair-list leftover-map criterion leftover-map item coordinates ζ, not leftover-map
* graphic leftover-map post markers.
+ * ADR 0340 fail-closes leftover-map pair-list leftover-map criterion leftover-map
+ * item coordinates ζ through leftoverMapListCriterionBadge independently of leftover-map
+ * pair-list leftover-map post leftover-map person coordinates ξ, not leftover-map
+ * graphic leftover-map criterion markers.
*/
import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates";
diff --git a/pyproject.toml b/pyproject.toml
index f680cb99a..14e9c7069 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
-version = "2.96.0"
+version = "2.97.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" }