Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.d/2.12.25-leftover-map-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 2.12.25 — Leftover-map length

- Persist leftover-map lengths `‖ξ‖` and `‖ζ‖` on leftover
post–criterion pairs (ADR 0181). After `make seed`, closest and
farthest leftover pairs sit above the member list with `‖ξ‖` and
`‖ζ‖` next to leftover-map distance `d`; click opens that post.
Omit the badges when lengths are missing. Never invent a leftover
score.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ All notable changes to this project are documented here. Format follows
Jeon et al., 2021, eq. 3; ADR 0180). A missing cosine omits the
badge rather than inventing leftover-map alignment.

## [2.12.25] - 2026-08-24

### Added

- Period leftover pairs now name leftover-map lengths `‖ξ‖` and `‖ζ‖`
next to leftover-map distance `d`. After `make seed`, closest and
farthest leftover pairs sit above the member list with named
leftover-map magnitude; click opens that post (Gabriel, 1971;
Jeon et al., 2021, eq. 3; ADR 0181). A missing length omits the
badge. Never invent a leftover score.

## [2.12.6] - 2026-08-20

### Added
Expand Down
3 changes: 3 additions & 0 deletions docs/adr/0049-leftover-pair-report-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ criterion.

Missing leftover rows render nothing — never a placeholder pair.
A hidden post never appears as a leftover pair.
A missing leftover-map length omits the `‖ξ‖` / `‖ζ‖` badges and
keeps the closest/farthest next action. See
[ADR 0181](0181-leftover-map-length.md).

## Consequences

Expand Down
85 changes: 85 additions & 0 deletions docs/adr/0181-leftover-map-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# ADR 0181 — Name leftover-map length on period-report pair rows

**Decision status:** Accepted
**Date:** 2026-08-24

Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and
[ADR 0049](0049-leftover-pair-report-ui.md).

## Context

ADR 0048 already persists leftover-map distance `d = ‖ξ_p − ζ_i‖` and
leftover residual `R = Y − E[Y|θ, item]` on `report_leftover_pair`.
ADR 0049 already renders closest and farthest pairs above the member
list and opens the named post. Distance is the Jeon et al. (2021,
eq. 3) map gap. Gabriel (1971) also names leftover-map vector lengths
`‖ξ‖` and `‖ζ‖`. Hiding those lengths lets a buyer read a close
leftover-map pair as leftover-map aligned magnitude, or a distant pair
as large leftover-map displacement, without the polar magnitude.

A close leftover-map pair can sit at the origin (`‖ξ‖ = ‖ζ‖ = 0`) or
share a non-origin position (`d = 0` with positive length). Those are
different leftover-map facts.

This increment does not persist leftover-map coordinates, does not name
observed `Y` / expected `E`, does not name leftover-map cosine, does
not name leftover-map inner product, does not name leftover-map rank,
does not split leftover-map distance onto two axes, and does not land
Post quality on the leftover criterion.

The unprotected-stack reconstructions for neighbouring leftover facts
use 0121–0180. This protected-main increment uses **0181** so it does
not collide with leftover-map cosine (0180), leftover-map inner product
(0179), leftover residual disclosure (0178), leftover observed `Y` /
expected `E` (0177), leftover-map rank (0172), two-axis leftover-map
distance (0166), leftover coverage (0168), leftover-map axis share
(0148), or leftover interaction-map persistence (0121).

## Decision

Each leftover pair names `leftover_map_person_length` and
`leftover_map_item_length` — the Euclidean lengths `‖ξ‖` and `‖ζ‖` of
the leftover-map person and item coordinates that produced leftover-map
distance `d`. Migration `0181` is the single source of the columns on
every install path, fresh or existing -- shipped migrations (`0001` /
`0012`) are never edited after the fact. The columns are nullable so
older leftover rows keep distance and residual without fabricating
lengths. Fallback pairs that have no complete-case leftover map omit
the values rather than inventing them. Origin coordinates persist
length `0` because that is the measured leftover-map magnitude.

The pair button shows `‖ξ‖ {person}` and `‖ζ‖ {item}` next to
leftover-map distance `d` when both values are finite. Next action:
leftover-map length names leftover-map magnitude independently of
leftover-map distance; open this post to read the named criterion. A
missing or non-finite length omits the badges and keeps the existing
closest/farthest next action. Do not invent a leftover score. Do not
invent a theta.

## Consequences

`GET /api/reports/{grouping}/{period}` returns
`leftover_map_person_length` and `leftover_map_item_length`. After
`make seed`, closest and farthest leftover pairs sit above the member
list with named `‖ξ‖` and `‖ζ‖` next to `d`; click opens that post.
Hidden posts stay hidden.

## Related

Independent of leftover interaction-map persistence, leftover-criterion
evaluation landing, leftover residual disclosure, leftover observed
`Y` / expected `E`, leftover-map complete-case coverage, leftover-map
axis share, leftover pairs on the grouping comparison strip, two-axis
leftover-map distance, leftover-map rank, leftover-map inner product,
and leftover-map cosine.

## 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
24 changes: 24 additions & 0 deletions frontend/src/leftoverMapLength.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import {
formatLeftoverMapItemLength,
formatLeftoverMapLength,
formatLeftoverMapPersonLength,
} from "./leftoverMapLength";

describe("formatLeftoverMapLength", () => {
it("names leftover-map length without inventing a leftover score", () => {
expect(formatLeftoverMapPersonLength(0)).toBe("‖ξ‖ 0.00");
expect(formatLeftoverMapItemLength(1.25)).toBe("‖ζ‖ 1.25");
expect(formatLeftoverMapLength(0.4)).toBe("0.40");
expect(formatLeftoverMapPersonLength(0.4)).toBe("‖ξ‖ 0.40");
expect(formatLeftoverMapItemLength(0.9)).toBe("‖ζ‖ 0.90");
});

it("omits the badge when length is missing, negative, or non-finite", () => {
expect(formatLeftoverMapPersonLength(null)).toBeNull();
expect(formatLeftoverMapItemLength(undefined)).toBeNull();
expect(formatLeftoverMapLength(Number.NaN)).toBeNull();
expect(formatLeftoverMapLength(Number.POSITIVE_INFINITY)).toBeNull();
expect(formatLeftoverMapLength(-0.01)).toBeNull();
});
});
21 changes: 21 additions & 0 deletions frontend/src/leftoverMapLength.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** Gabriel leftover-map lengths ``‖ξ‖`` and ``‖ζ‖`` after IRT main effects. */

export const LEFTOVER_MAP_LENGTH_ACTION =
"Leftover-map length ‖ξ‖ {person} and ‖ζ‖ {item} names leftover-map magnitude independently of leftover-map distance. Open this post to read {criterion}.";

export function formatLeftoverMapLength(value: number | null | undefined): string | null {
if (value == null || !Number.isFinite(value) || value < 0) {
return null;
}
return value.toFixed(2);
}

export function formatLeftoverMapPersonLength(value: number | null | undefined): string | null {
const formatted = formatLeftoverMapLength(value);
return formatted === null ? null : `‖ξ‖ ${formatted}`;
}

export function formatLeftoverMapItemLength(value: number | null | undefined): string | null {
const formatted = formatLeftoverMapLength(value);
return formatted === null ? null : `‖ζ‖ ${formatted}`;
}
Comment on lines +6 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Frontend formatter not wired into UI

formatLeftoverMapLength and its person/item variants are exported and unit-tested, but no component imports them. The badges and i18n the PR describes are absent, so the formatter renders nothing to a buyer.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

25 changes: 25 additions & 0 deletions migrations/0181_report_leftover_map_length.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- ADR 0181: persist leftover-map lengths ‖ξ‖ and ‖ζ‖ on leftover
-- post–criterion pairs. Distance stays Euclidean leftover-map d.
-- Upgrade columns are nullable so older leftover rows keep distance
-- and residual without fabricating lengths. This migration is the
-- single source of the columns on fresh and existing installations.

alter table report_leftover_pair
add column if not exists leftover_map_person_length numeric,
add column if not exists leftover_map_item_length numeric;

do $$
begin
if not exists (
select 1
from pg_constraint
where conname = 'report_leftover_pair_map_length_nonnegative'
) then
alter table report_leftover_pair
add constraint report_leftover_pair_map_length_nonnegative
check (
(leftover_map_person_length is null or leftover_map_person_length >= 0)
and (leftover_map_item_length is null or leftover_map_item_length >= 0)
);
end if;
end $$;
8 changes: 8 additions & 0 deletions migrations/rollback/0181_report_leftover_map_length.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Reverse 0181. Leftover distance and residual stay on the pair row.

alter table report_leftover_pair
drop constraint if exists report_leftover_pair_map_length_nonnegative;

alter table report_leftover_pair
drop column if exists leftover_map_person_length,
drop column if exists leftover_map_item_length;
4 changes: 4 additions & 0 deletions tests/test_period_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ def test_leftover_residual_biplot_separates_aligned_and_opposed_cells() -> None:
}
assert farthest.leftover_residual == pytest.approx(-2.0)
assert farthest.leftover_distance == pytest.approx(2.0 * np.sqrt(2.0), rel=1e-6)
assert closest.leftover_map_person_length == pytest.approx(0.0, abs=1e-9)
assert closest.leftover_map_item_length == pytest.approx(0.0, abs=1e-9)
assert farthest.leftover_map_person_length == pytest.approx(np.sqrt(2.0), rel=1e-6)
assert farthest.leftover_map_item_length == pytest.approx(np.sqrt(2.0), rel=1e-6)
Comment on lines +228 to +231

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Leftover-length test reads attributes that never exist

The new assertions read closest.leftover_map_person_length and leftover_map_item_length (plus the farthest pair), but LeftoverPair in leftover_pairs.py defines no such fields and this PR leaves that file untouched. Every run raises AttributeError and the test fails. The backend never computes or persists these lengths.

Prompt for agents
The added assertions in tests/test_period_report.py reference LeftoverPair.leftover_map_person_length and LeftoverPair.leftover_map_item_length, but these fields were never added to the LeftoverPair dataclass in lineageweave/leftover_pairs.py, and no code computes them. The PR (per ADR 0181 and its own description) is supposed to persist/compute the Euclidean lengths of the Gabriel person coordinate (norm of person_coord) and item coordinate (norm of item_coord) that produced the leftover-map distance, omitting them (None) when there is no complete-case leftover map. Implement this: add nullable length fields to LeftoverPair, compute them in leftover_pairs_from_residual (person_coord/item_coord are already available around leftover_pairs.py:90-91 where distance is computed; the fallback branch at leftover_pairs.py:99-114 has no coordinates so lengths should be None), thread them through _candidate_row/_pair_from_candidate, and update the report serialization/API and frontend wiring so the new columns and badges are actually populated. Otherwise the test suite fails with AttributeError.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.



def test_leftover_missing_cells_are_not_treated_as_zero() -> None:
Expand Down
Loading