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
7 changes: 7 additions & 0 deletions CHANGELOG.d/2.12.24-leftover-map-cosine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 2.12.24 — Leftover-map cosine

- Persist leftover-map cosine on leftover post–criterion pairs
(ADR 0180). After `make seed`, closest and farthest leftover pairs
sit above the member list with cosine next to leftover-map distance
`d`; click opens that post. Omit the badge when cosine 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 @@ -253,6 +253,15 @@ All notable changes to this project are documented here. Format follows
(Gabriel, 1971; Jeon et al., 2021, eq. 3; ADR 0179). A missing
inner product omits the badge rather than inventing a leftover score.

## [2.12.24] - 2026-08-24

### Added

- Period leftover pair rows now name leftover-map cosine next to
leftover-map distance `d`, then open that post (Gabriel, 1971;
Jeon et al., 2021, eq. 3; ADR 0180). A missing cosine omits the
badge rather than inventing leftover-map alignment.

## [2.12.6] - 2026-08-20

### Added
Expand Down
3 changes: 2 additions & 1 deletion docs/adr/0048-persist-lsirm-leftover-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ the IRT matrix is unusable. A rank-0 residual still emits a
stable pair so `make seed` is not empty; the stored distance is
then zero, not a fabricated interaction.

The UI contract is ADR 0049.
The UI contract is ADR 0049. Leftover-map cosine on those pair rows
is [ADR 0180](0180-leftover-map-cosine.md).

## Consequences

Expand Down
78 changes: 78 additions & 0 deletions docs/adr/0180-leftover-map-cosine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# ADR 0180 — Name leftover-map cosine 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 alignment as
the cosine of the angle between `ξ_p` and `ζ_i`. Hiding that cosine
lets a buyer read a large reconstructed leftover cell, or a close
map pair, as leftover-map alignment without a scale-free value.

This increment does not persist leftover-map coordinates, does not
name leftover-map inner product, 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.

The unprotected-stack reconstructions for neighbouring leftover facts
use 0162–0179. This protected-main increment uses **0180** so it does
not collide with 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_cosine` — the cosine of the
angle between leftover-map person and item coordinates that produced
leftover-map distance `d`. Migration `0180` 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 alignment. Fallback pairs that have no
complete-case leftover map, and pairs whose person or item vector
sits at the origin, omit the value rather than inventing one.

The pair button shows `cos {signed}` next to leftover-map distance
`d` when the value is finite. Next action: leftover-map cosine names
leftover-map alignment independent of distance; open this post to
read the named criterion. A missing or non-finite cosine 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_cosine`.
After `make seed`, closest and farthest leftover pairs sit above the
member list with named cosine 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, and leftover-map inner
product.

## 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
27 changes: 27 additions & 0 deletions frontend/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,33 @@ describe("i18n", () => {
expect(document.documentElement.lang).toBe(locale);
});

it.each([
[
"ko",
"잔여 지도 코사인 +0.95이(가) 거리와 무관한 잔여 지도 정렬을 이름합니다. sales-lead 기준을 읽으려면 이 글을 여세요.",
],
[
"zh",
"残差图余弦 +0.95 命名与距离无关的残差图对齐。打开这篇帖子阅读 sales-lead。",
],
[
"ja",
"残差マップ余弦 +0.95 は距離に依存しない残差マップの向きを示します。この投稿を開いて sales-lead を読んでください。",
],
[
"vi",
"Cosin bản đồ phần dư +0.95 đặt tên sự thẳng hàng độc lập với khoảng cách. Mở bài viết này để đọc sales-lead.",
],
] as const)("formats leftover-map cosine next action in %s", (locale, expected) => {
setLocale(locale);
expect(
tf(
"Leftover-map cosine {value} names leftover-map alignment independent of distance. Open this post to read {criterion}.",
{ value: "+0.95", criterion: "sales-lead" },
),
).toBe(expected);
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

it.each([
["ko", "DEMO은(는) 이벤트 계보의 현재 항목입니다. 다음으로 Keyman과 평가를 읽으세요."],
["zh", "DEMO 是事件谱系中的当前记录。接下来查看关键联系人和评估。"],
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"잔여 맵 내적 ξ·ζ {value}가 IRT 주효과 이후 잔여를 재구성합니다. {criterion}을(를) 읽으려면 이 글을 여세요.",
"Open leftover {kind} pair: {title} · {criterion}":
"잔여 {kind} 쌍 열기: {title} · {criterion}",
"Leftover-map cosine {value} names leftover-map alignment independent of distance. Open this post to read {criterion}.":
"잔여 지도 코사인 {value}이(가) 거리와 무관한 잔여 지도 정렬을 이름합니다. {criterion} 기준을 읽으려면 이 글을 여세요.",
},
zh: {
"Unknown": "未知",
Expand Down Expand Up @@ -1198,6 +1200,8 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"残余图内积 ξ·ζ {value} 重建了 IRT 主效应后的残余。打开这篇帖子以阅读 {criterion}。",
"Open leftover {kind} pair: {title} · {criterion}":
"打开残余{kind}对:{title} · {criterion}",
"Leftover-map cosine {value} names leftover-map alignment independent of distance. Open this post to read {criterion}.":
"残差图余弦 {value} 命名与距离无关的残差图对齐。打开这篇帖子阅读 {criterion}。",
},
ja: {
"Unknown": "不明",
Expand Down Expand Up @@ -1752,6 +1756,8 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"残差マップ内積 ξ·ζ {value} が IRT 主効果後の残差を再構成します。{criterion} を読むにはこの投稿を開いてください。",
"Open leftover {kind} pair: {title} · {criterion}":
"残差{kind}ペアを開く: {title} · {criterion}",
"Leftover-map cosine {value} names leftover-map alignment independent of distance. Open this post to read {criterion}.":
"残差マップ余弦 {value} は距離に依存しない残差マップの向きを示します。この投稿を開いて {criterion} を読んでください。",
},
vi: {
"Unknown": "Không rõ",
Expand Down Expand Up @@ -2306,6 +2312,8 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"Tích trong bản đồ phần dư ξ·ζ {value} tái tạo phần dư sau hiệu ứng chính IRT. Mở bài viết này để đọc {criterion}.",
"Open leftover {kind} pair: {title} · {criterion}":
"Mở cặp phần dư {kind}: {title} · {criterion}",
"Leftover-map cosine {value} names leftover-map alignment independent of distance. Open this post to read {criterion}.":
"Cosin bản đồ phần dư {value} đặt tên sự thẳng hàng độc lập với khoảng cách. Mở bài viết này để đọc {criterion}.",
},
};

Expand Down
19 changes: 19 additions & 0 deletions frontend/src/leftoverMapCosine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { formatLeftoverMapCosine, formatSignedLeftoverValue } from "./leftoverMapCosine";

describe("formatLeftoverMapCosine", () => {
it("names leftover-map cosine without inventing a leftover score", () => {
expect(formatLeftoverMapCosine(0.95)).toBe("cos +0.95");
expect(formatLeftoverMapCosine(-1)).toBe("cos −1.00");
expect(formatLeftoverMapCosine(0)).toBe("cos 0.00");
expect(formatSignedLeftoverValue(0.95)).toBe("+0.95");
expect(formatSignedLeftoverValue(-1)).toBe("−1.00");
});

it("omits the badge when cosine is missing or non-finite", () => {
expect(formatLeftoverMapCosine(null)).toBeNull();
expect(formatLeftoverMapCosine(undefined)).toBeNull();
expect(formatLeftoverMapCosine(Number.NaN)).toBeNull();
expect(formatLeftoverMapCosine(Number.POSITIVE_INFINITY)).toBeNull();
});
});
26 changes: 26 additions & 0 deletions frontend/src/leftoverMapCosine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** Gabriel leftover-map cosine after IRT main effects. */

export const LEFTOVER_MAP_COSINE_ACTION =
"Leftover-map cosine {value} names leftover-map alignment independent of distance. 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 `−${magnitude}`;
}
return magnitude;
}

export function formatLeftoverMapCosine(value: number | null | undefined): string | null {
if (value == null) {
return null;
}
const signed = formatSignedLeftoverValue(value);
return signed === null ? null : `cos ${signed}`;
}
2 changes: 1 addition & 1 deletion lineageweave/period_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def leftover_pairs_for_fit(
theta: np.ndarray,
fit: PolytomousFit,
) -> tuple[LeftoverPair, ...]:
"""Leftover pairs from the already-fitted GRM/GPCM main effects."""
"""Leftover pairs from the already-fitted GRM/GPCM main effects, with cosine."""
probs = _category_probabilities(model, theta, fit)
expected = expected_category_matrix(matrix, probs)
return leftover_pairs_from_residual(post_ids, item_codes, matrix, expected)
Comment on lines +255 to 258

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Cosine feature is unimplemented beyond the migration and a util

The PR claims to persist, fetch, and seed leftover_map_cosine, but the backend never computes or stores it. leftover_pairs.py has no cosine field on LeftoverPair (it even computes an inner_product at leftover_pairs.py that is discarded), period_report.py only gains a docstring, and no api.ts/App.tsx change wires the badge. The frontend formatLeftoverMapCosine util and the GET contract in ADR 0180 therefore have no data source, so the cos badge never renders. Sibling leftover PRs (e.g. commit e05d63b) touched all of these files; this one does not.

Open in Devin Review

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

Expand Down
8 changes: 8 additions & 0 deletions migrations/0180_report_leftover_map_cosine.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- ADR 0180: persist leftover-map cosine 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 alignment. 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_cosine numeric;
Comment on lines +7 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Cosine migration is never applied on any install path

The new migration is not added to the allowlist in docker/postgres-init/migrate.sh:20 and is not executed in scripts/seed_demo_data.py (which runs each migration file by name). The leftover_map_cosine column is therefore never created on the Compose stack or by make seed.

Prompt for agents
migrations/0180_report_leftover_map_cosine.sql is a new migration, but neither install path applies it. docker/postgres-init/migrate.sh applies only migrations whose filename prefix matches its case allowlist (line 20-21); 0180_* is absent, so the migration is skipped via the default `continue`. scripts/seed_demo_data.py applies migrations by executing each file by name (e.g. line 121 runs 0177_report_leftover_observed_expected.sql) and does not run 0180. As a result the leftover_map_cosine column is never created. Follow the sibling leftover-migration pattern: add 0180_* to the migrate.sh allowlist and add a cur.execute() for migrations/0180_report_leftover_map_cosine.sql in seed_demo_data.py alongside the other leftover migrations. Also check tests/test_migration_replay.py, which asserts prefixes such as 0177_* are present in the script.
Open in Devin Review

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

4 changes: 4 additions & 0 deletions migrations/rollback/0180_report_leftover_map_cosine.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Reverse 0180. Leftover distance and residual stay on the pair row.

alter table report_leftover_pair
drop column if exists leftover_map_cosine;