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-reconstruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 2.12.25 — Leftover-map reconstruction

- Persist two-axis leftover-map reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}`
on leftover post–criterion pairs (ADR 0181). After `make seed`,
closest and farthest leftover pairs sit above the member list with
`R̂` next to leftover-map distance `d`; click opens that post. Omit
the badge when reconstruction is missing. Never invent a leftover
score.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ All notable changes to this project are documented here. Format follows
Jeon et al., 2021, eq. 3; ADR 0181). A missing length omits the
badge. Never invent a leftover score.

## [2.12.25] - 2026-08-24

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Duplicate 2.12.25 changelog version

The new section reuses ## [2.12.25], already used above for the leftover-map lengths increment (CHANGELOG.md:252). Two distinct feature entries now share one version, breaking the sequential order that reaches 2.12.25 just above.

Suggested change
## [2.12.25] - 2026-08-24
## [2.12.26] - 2026-08-24
Open in Devin Review

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


### Added

- Period leftover pair rows now name two-axis leftover-map reconstruction
`R̂ = ξ_{1:2} · ζ_{1:2}` next to leftover-map distance `d`, then open
that post (Gabriel, 1971; Jeon et al., 2021, eq. 3; ADR 0181). A missing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Reconstruction changelog cites wrong ADR

The reconstruction entry cites ADR 0181, but this PR's ADR and migration are 0182; 0181 is the earlier lengths increment. The same wrong number appears in CHANGELOG.d/2.12.25-leftover-map-reconstruction.md:3.

Suggested change
that post (Gabriel, 1971; Jeon et al., 2021, eq. 3; ADR 0181). A missing
that post (Gabriel, 1971; Jeon et al., 2021, eq. 3; ADR 0182). A missing
Open in Devin Review

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

reconstruction omits the badge rather than inventing a leftover score.

## [2.12.6] - 2026-08-20

### Added
Expand Down
2 changes: 1 addition & 1 deletion docker/postgres-init/migrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ for migration in /opt/lineageweave/migrations/*.sql; do
migration_name=${migration##*/}
case "$migration_name" in
0012_*|0013_*|0014_*|0015_*|0016_*|0017_*|0018_*|0019_*|0020_*|0021_*|0022_*|0023_*|0024_*|0025_*|0026_*|0027_*|0028_*|0029_*|0030_*|0031_*|0032_*|0033_*|0034_*|0035_*|0036_*|0037_*|0038_*|0039_*|0040_*|0041_*|0042_*|0043_*|0044_*|0045_*|0046_*|0047_*|0048_*|0049_*|0050_*) ;;
0060_*|0100_*|0101_*|0102_*|0103_*|0104_*|0105_*|0106_*|0107_*|0108_*|0109_*|0110_*|0111_*|0112_*|0113_*|0114_*|0130_*|0133_*|0134_*|0136_*|0137_*|0138_*|0139_*|0172_*|0173_*|0176_*|0177_*) ;;
0060_*|0100_*|0101_*|0102_*|0103_*|0104_*|0105_*|0106_*|0107_*|0108_*|0109_*|0110_*|0111_*|0112_*|0113_*|0114_*|0130_*|0133_*|0134_*|0136_*|0137_*|0138_*|0139_*|0172_*|0173_*|0176_*|0177_*|0179_*|0180_*|0181_*|0182_*) ;;
*) continue ;;
esac
printf 'Applying %s\n' "$migration_name"
Expand Down
85 changes: 85 additions & 0 deletions docs/adr/0182-leftover-map-reconstruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# ADR 0182 — Name two-axis leftover-map reconstruction 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) reconstructs a matrix from the biplot
as the inner product of person and item coordinates. The leftover map
readers read is two-axis: unused axes pad with zero, and hidden SVD
axes after the second are dropped. Full-rank inner product `ξ·ζ`
therefore reconstructs the centered residual, not the cell the
two-axis map shows. Hiding two-axis reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}`
lets a reader read leftover residual `R` or leftover-map distance `d`
as the reconstructed leftover cell.

This increment does not persist leftover-map coordinates, does not
name leftover-map inner product as a separate full-rank column, does
not name leftover-map cosine, does not name observed `Y` / expected
`E`, 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. Leftover-map distance stays full-rank Euclidean.

The unprotected-stack reconstructions for neighbouring leftover facts
use 0162–0180. This protected-main increment uses **0182** 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_reconstruction` — the two-axis
Gabriel reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}` of the leftover-map
person and item coordinates. Migration `0182` is the single source of
the column on every install path, fresh or existing -- shipped
migrations (`0001` / `0012`) are never edited after the fact. The
column is nullable so older leftover rows keep distance and residual
without fabricating a reconstruction. Fallback pairs that have no
complete-case leftover map omit the value rather than inventing one.
A rank-0 origin map stores `0.0` (zeros · zeros), not a missing value.
A non-finite reconstruction stores null rather than inventing a
leftover score.

The pair button shows `R̂ {signed}` next to leftover-map distance `d`
when the value is finite. Next action: leftover map reconstructs `R̂`
after IRT main effects; open this post to read the named criterion.
A missing or non-finite reconstruction omits the badge 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_reconstruction`. After `make seed`, closest and farthest
leftover pairs sit above the member list with named `R̂` 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
5 changes: 5 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ import {
useLocale,
} from "./i18n";
import { rememberOidcReturnUrl, returnUrlFromLocation } from "./oidcReturnUrl";
import {
formatLeftoverMapReconstruction,
formatSignedLeftoverValue,
LEFTOVER_MAP_RECONSTRUCTION_ACTION,
} from "./leftoverMapReconstruction";
Comment on lines +152 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 R̂ badge feature never wired into the UI

The reconstruction helpers imported here are never used, and LeftoverPairButton (frontend/src/components/LeftoverPairButton.tsx) was not updated to render the value; the call at App.tsx passes no reconstruction prop. No backend or api.ts change supplies leftover_map_reconstruction. The promised badge never appears, and noUnusedLocals also fails the build on the unused imports.

Open in Devin Review

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

import "./App.css";

function orchestratorUnavailableMessage(err: unknown, action: string): string {
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/leftoverMapReconstruction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import {
formatLeftoverMapReconstruction,
formatSignedLeftoverValue,
} from "./leftoverMapReconstruction";

describe("formatLeftoverMapReconstruction", () => {
it("names two-axis leftover-map reconstruction without inventing a leftover score", () => {
expect(formatLeftoverMapReconstruction(0.4)).toBe("R\u0302 +0.40");
expect(formatLeftoverMapReconstruction(-1.1)).toBe("R\u0302 \u22121.10");
expect(formatLeftoverMapReconstruction(0)).toBe("R\u0302 0.00");
expect(formatSignedLeftoverValue(0.4)).toBe("+0.40");
expect(formatSignedLeftoverValue(-1.1)).toBe("\u22121.10");
});

it("omits the badge when reconstruction is missing or non-finite", () => {
expect(formatLeftoverMapReconstruction(null)).toBeNull();
expect(formatLeftoverMapReconstruction(undefined)).toBeNull();
expect(formatLeftoverMapReconstruction(Number.NaN)).toBeNull();
expect(formatLeftoverMapReconstruction(Number.POSITIVE_INFINITY)).toBeNull();
});
});
26 changes: 26 additions & 0 deletions frontend/src/leftoverMapReconstruction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** Two-axis leftover-map reconstruction ``R̂ = ξ_{1:2} · ζ_{1:2}``. */

export const LEFTOVER_MAP_RECONSTRUCTION_ACTION =
"Leftover map reconstructs R̂ {value} after IRT main effects. Open this post to read {criterion}.";

export function formatSignedLeftoverValue(value: number): string | null {
if (!Number.isFinite(value)) {
return null;
}
const magnitude = Math.abs(value).toFixed(2);
if (value > 0) {
return `+${magnitude}`;
}
if (value < 0) {
return `\u2212${magnitude}`;
}
return magnitude;
}

export function formatLeftoverMapReconstruction(value: number | null | undefined): string | null {
if (value == null) {
return null;
}
const signed = formatSignedLeftoverValue(value);
return signed === null ? null : `R\u0302 ${signed}`;
}
8 changes: 8 additions & 0 deletions migrations/0182_report_leftover_map_reconstruction.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- ADR 0182: persist two-axis leftover-map reconstruction R̂ = ξ_{1:2} · ζ_{1:2}
-- on leftover post–criterion pairs. Distance stays Euclidean leftover-map d.
-- Upgrade column is nullable so older leftover rows keep distance and
-- residual without fabricating a reconstruction. This migration is the
-- single source of the column on fresh and existing installations.

alter table report_leftover_pair
add column if not exists leftover_map_reconstruction numeric;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Reverse 0182. Leftover distance and residual stay on the pair row.

alter table report_leftover_pair
drop column if exists leftover_map_reconstruction;
Loading