Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,17 @@ against a live local stack (`make up`) and self-skip without one -- see
[README.md](README.md#local-product-stack-docker-compose).

Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0162 / 0163 /
0164 / 0182) are computed in `lineageweave/leftover_pairs.py` from the
0164 / 0182 / 0183) are computed in `lineageweave/leftover_pairs.py` from the
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
residual after a real GRM/GPCM score, never invented. Distances are
Euclidean on the two-dimensional Gabriel leftover map; missing cells stay
out of the factorization. Closest and farthest post–criterion pairs
persist to `report_leftover_pair` with signed residual `R`, observed
`Y`, and expected `E[Y|θ, item]` so `R = Y − E` remains auditable,
plus leftover-map rank so rank 0 is not read as structure, and
unexplained leftover `U = R − R̂` next to leftover-map distance `d`
after two-axis Gabriel reconstruction. They sit above the member
list so a click opens that post. Two-axis reconstruction `R̂` is not
persisted. Leftover-map axis share (ADR 0148) is Gabriel inertia of
plus leftover-map rank so rank 0 is not read as structure,
unexplained leftover `U = R − R̂`, and leftover-map reconstruction
`R̂ = ξ_{1:2} · ζ_{1:2}` next to leftover-map distance `d` so
`U + R̂ = R` stays auditable. They sit above the member
list so a click opens that post. Leftover-map axis share (ADR 0148) is Gabriel inertia of
residual SVD axes 1 and 2 and persists to `report_leftover_map_axis`.
Rank-0 residuals emit two zero-share axes; the shares are report-level
and are not a leftover score. Complete-case coverage (ADR 0168) persists to
Expand Down
7 changes: 4 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,10 +596,11 @@ on those same fixed parameters (Kim, 2006 FIPC). After scoring,
information at the group's mean θ (Lord, 1980 max-info CAT). Rankings
persist to `report_item_information`. After those IRT main effects,
residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021;
ADR 0017 / 0048 / 0119 / 0162 / 0163 / 0164 / 0182) persist to
ADR 0017 / 0048 / 0119 / 0162 / 0163 / 0164 / 0182 / 0183) persist to
`report_leftover_pair` with signed residual `R`, observed `Y`, expected
`E[Y|θ, item]`, full leftover-map rank, and unexplained leftover
`U = R − R̂` named on the pair row. Leftover-map axis share (Gabriel
`E[Y|θ, item]`, full leftover-map rank, unexplained leftover
`U = R − R̂`, and two-axis reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}` so
`U + R̂ = R` stays auditable. Leftover-map axis share (Gabriel
inertia of residual SVD axes 1 and 2; ADR 0148) persists to
`report_leftover_map_axis`. Complete-case leftover-map coverage (ADR
0168) persists to `report_leftover_map_coverage` so readers see how
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.d/2.12.31-leftover-map-reconstruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## 2.12.31 — Leftover-map reconstruction

- Persist leftover-map reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}` on leftover
post–criterion pairs (ADR 0183). 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. Signed reconstruction is
stored, never clamped. Identity `U + R̂ = R` stays auditable.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project are documented here. Format follows

### Added

- Period leftover pair rows now name 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 0183). A missing
reconstruction omits the badge rather than inventing a leftover score.
Signed reconstruction is stored, never clamped. Identity `U + R̂ = R`
stays auditable.
- Opening a post with persisted image-region evidence now shows each region's
bounding range beside its caption, OCR, and tags (ADR 0155). After
`make seed`, a synthetic process-diagram region reads **Region location:
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ and to read its mean θ and member posts, then open a post. Those
members land immediately under that next action, ahead of Other Corp
and the week strip. After `make seed`, leftover closest/farthest pairs
sit above the member list with leftover-map rank (rank 0 names no
leftover structure) and unexplained leftover `U` next to leftover-map
leftover structure), unexplained leftover `U`, and leftover-map
reconstruction `R̂` next to leftover-map
distance `d`. Leftover-map axis share badges name Gabriel inertia
of axes 1 and 2; open a leftover pair to read the post–criterion cell.
The shares do not invent a leftover score. Opening Public post names the next action: read
Expand Down
12 changes: 9 additions & 3 deletions backend/app/report_ingestion.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ async def persist_period_report(
grouping_kind, grouping_key, period_code, rubric_version,
pair_kind, post_id, criterion_code, leftover_distance, leftover_residual,
observed_response, expected_response, leftover_map_rank,
leftover_map_unexplained
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
leftover_map_unexplained, leftover_map_reconstruction
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
""",
grouping_kind,
grouping_key,
Expand All @@ -461,6 +461,7 @@ async def persist_period_report(
pair.expected_response,
pair.leftover_map_rank,
pair.leftover_map_unexplained,
pair.leftover_map_reconstruction,
Comment thread
seonghobae marked this conversation as resolved.
)
for axis in report.leftover_map_axes:
await conn.execute(
Expand Down Expand Up @@ -646,7 +647,7 @@ async def fetch_period_reports(
select lp.grouping_key, lp.pair_kind, lp.post_id, lp.criterion_code,
lp.leftover_distance, lp.leftover_residual,
lp.observed_response, lp.expected_response, lp.leftover_map_rank,
lp.leftover_map_unexplained, p.post_title,
lp.leftover_map_unexplained, lp.leftover_map_reconstruction, p.post_title,
p.visibility_code, p.corporate_entity_id,
({_SOURCE_CONTEXT_PRESENT_SQL}) as has_real_source_context
from report_leftover_pair lp
Expand Down Expand Up @@ -790,6 +791,11 @@ async def fetch_period_reports(
if row["leftover_map_unexplained"] is None
else float(row["leftover_map_unexplained"])
),
"leftover_map_reconstruction": (
None
if row["leftover_map_reconstruction"] is None
else float(row["leftover_map_reconstruction"])
),
"visibility_code": row["visibility_code"],
"corporate_entity_id": str(row["corporate_entity_id"]),
"has_real_source_context": bool(row["has_real_source_context"]),
Expand Down
11 changes: 10 additions & 1 deletion backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
/ "migrations"
/ "0182_report_leftover_map_unexplained.sql"
)
_LEFTOVER_MAP_RECONSTRUCTION_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
/ "0183_report_leftover_map_reconstruction.sql"
)


def _postgres_available() -> bool:
Expand Down Expand Up @@ -273,6 +278,7 @@ def seeded_db(demo_analyst_token):
cur.execute(_GLOBAL_ASK_JOB_MIGRATION.read_text())
cur.execute(_LEFTOVER_MAP_AXIS_MIGRATION.read_text())
cur.execute(_LEFTOVER_MAP_UNEXPLAINED_MIGRATION.read_text())
cur.execute(_LEFTOVER_MAP_RECONSTRUCTION_MIGRATION.read_text())
cur.execute(
"insert into common_lookup_value (lookup_category, lookup_code, lookup_label) values "
"('corporate_entity_level', 'group', 'Group'), "
Expand Down Expand Up @@ -5014,7 +5020,10 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token,
assert pair["leftover_map_rank"] >= 0
unexplained = pair.get("leftover_map_unexplained")
assert unexplained is None or isinstance(unexplained, (int, float))
assert "leftover_map_reconstruction" not in pair
reconstruction = pair.get("leftover_map_reconstruction")
assert reconstruction is None or isinstance(reconstruction, (int, float))
if unexplained is not None and reconstruction is not None:
assert abs(unexplained + reconstruction - pair["leftover_residual"]) < 1e-6
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
observed = pair.get("observed_response")
expected = pair.get("expected_response")
if observed is None or expected is None:
Expand Down
11 changes: 5 additions & 6 deletions docs/adr/0003-fast-mlsirm-report-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,12 @@ than one large PR:
`information_polytomous` (Lord, 1980 max-info). Persist the ranking
(`report_item_information`) and show the rank-1 item on the Period
reports panel. Do not reimplement an information function here.
7. **Leftover-pair slice** (shipped in 0.71.2; ADR 0017 / 0018 / 0048 / 0049 / 0182): after
7. **Leftover-pair slice** (shipped in 0.71.2; ADR 0017 / 0018 / 0048 / 0049 / 0182 / 0183): after
IRT main effects, persist closest and farthest post–criterion pairs
from the residual leftover map, and name unexplained leftover
`U = R − R̂` when Gabriel coordinates exist so the leftover cell the
two-axis map does not reconstruct is not read as leftover residual
or leftover-map distance. Do not persist two-axis reconstruction
`R̂`. Do not fork LSIRM; do not invent a
from the residual leftover map, name unexplained leftover
`U = R − R̂` when Gabriel coordinates exist, and persist two-axis
reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}` so `U + R̂ = R` stays
auditable. Do not fork LSIRM; do not invent a
leftover-pair API inside `fast-mlsirm` in this slice.
8. **Leftover-map axis-share slice** (ADR 0148): persist Gabriel inertia
`σ_k² / Σ_j σ_j²` of leftover-map axes 1 and 2 on the same residual
Expand Down
8 changes: 5 additions & 3 deletions docs/adr/0048-persist-lsirm-leftover-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
**Amended by:** [ADR 0119](0119-leftover-map-two-dimensional-distance.md) (two leftover-map axes);
[ADR 0163](0163-leftover-observed-expected.md) (observed Y and expected E);
[ADR 0164](0164-leftover-map-rank.md) (full map rank);
[ADR 0182](0182-leftover-map-unexplained.md) (unexplained leftover U)
[ADR 0182](0182-leftover-map-unexplained.md) (unexplained leftover U);
[ADR 0183](0183-leftover-map-reconstruction.md) (two-axis reconstruction R̂)

## Context

Expand Down Expand Up @@ -41,8 +42,9 @@ rank while distance remains on the first two axes (ADR 0164). Each
leftover row also names unexplained leftover `U = R − R̂` when
Gabriel coordinates exist so the leftover cell the two-axis map does
not reconstruct is not read as leftover residual `R` or leftover-map
distance `d` (ADR 0182). Two-axis reconstruction `R̂` is computed
internally and is not persisted.
distance `d` (ADR 0182). Two-axis reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}`
is persisted so `U + R̂ = R` stays auditable (ADR 0183). Do not
substitute a separately centered reconstruction.

Cascade the rows with `report_period_score`. A leftover post must
also be a `report_member_score` row, and the leftover criterion
Expand Down
18 changes: 11 additions & 7 deletions docs/adr/0049-leftover-pair-report-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
**Amended by:** [ADR 0162](0162-leftover-residual-disclosure.md) (signed residual R);
[ADR 0163](0163-leftover-observed-expected.md) (observed Y and expected E);
[ADR 0164](0164-leftover-map-rank.md) (full map rank);
[ADR 0182](0182-leftover-map-unexplained.md) (unexplained leftover U)
[ADR 0182](0182-leftover-map-unexplained.md) (unexplained leftover U);
[ADR 0183](0183-leftover-map-reconstruction.md) (two-axis reconstruction R̂)

## Context

Expand All @@ -23,18 +24,21 @@ On each period-report group, render leftover pairs **above** the
member list. Each pair is a button: closest or farthest label, post
title, criterion short label, signed residual `R`, two-axis leftover-map
distance, full map rank, observed `Y`, expected `E` when finite, and
unexplained leftover `U` when finite.
unexplained leftover `U` when finite, and reconstruction `R̂` when finite.
The next action names every available measurement before opening the
post; no amendment hides another, rank 0 explicitly names no
leftover structure, and unexplained leftover names "leftover map leaves
unexplained `U` after IRT main effects; open this post to read the
named criterion" when present. A missing unexplained leftover keeps
the existing next action.
leftover structure, reconstruction names "leftover map reconstructs
`R̂` after IRT main effects; open this post to read the named
criterion" when present, and unexplained leftover names "leftover map
leaves unexplained `U` after IRT main effects; open this post to read
the named criterion" when reconstruction is missing. A missing
reconstruction keeps the unexplained leftover next action.
Clicking the button opens that post with the same handler as a member
row. Residual naming is [ADR 0162](0162-leftover-residual-disclosure.md),
observed/expected naming is [ADR 0163](0163-leftover-observed-expected.md),
rank naming is [ADR 0164](0164-leftover-map-rank.md), unexplained
leftover naming is [ADR 0182](0182-leftover-map-unexplained.md).
leftover naming is [ADR 0182](0182-leftover-map-unexplained.md),
reconstruction naming is [ADR 0183](0183-leftover-map-reconstruction.md).

After `make seed`, closest and farthest leftover pairs sit above the
member list. Click a pair to open that post.
Expand Down
7 changes: 5 additions & 2 deletions docs/adr/0182-leftover-map-unexplained.md
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
**Decision status:** Accepted
**Date:** 2026-08-24

**Amended by:** [ADR 0183](0183-leftover-map-reconstruction.md)
(two-axis reconstruction R̂)

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

Expand Down Expand Up @@ -53,8 +56,8 @@ without fabricating unexplained leftover. Fallback pairs that have no
complete-case leftover map omit the value rather than inventing one.
A rank-0 origin map stores `0.0` (`R = 0` and `R̂ = 0`), not a
missing value. A non-finite unexplained leftover stores null rather
than inventing a leftover score. Do not persist
`leftover_map_reconstruction`.
than inventing a leftover score. Persist
`leftover_map_reconstruction` so `U + R̂ = R` stays auditable.

The pair button shows `U {signed}` next to leftover-map distance `d`
when the value is finite. Next action: leftover map leaves unexplained
Expand Down
93 changes: 93 additions & 0 deletions docs/adr/0183-leftover-map-reconstruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# ADR 0183 — Name leftover-map reconstruction on period-report pair rows

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

Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md),
[ADR 0049](0049-leftover-pair-report-ui.md), and
[ADR 0182](0182-leftover-map-unexplained.md). Independent of landed
complete-case coverage ([ADR 0168](0168-leftover-map-complete-case-coverage.md)).

## Context

ADR 0182 already persists unexplained leftover `U = R − R̂` after
two-axis Gabriel reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}`. That
reconstruction is computed internally so `U` is honest, then discarded.
A buyer who reads `U` next to leftover residual `R` cannot check
`U + R̂ = R` without the reconstruction the two-axis map actually
uses. Hiding `R̂` lets leftover residual `R` or leftover-map distance
`d` be read as the leftover the map reconstructs.

This increment persists leftover-map reconstruction `R̂`. It 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 leftover-map length, does not name leftover-map
explained share, unexplained share, or cross share, and does not land
Post quality on the leftover criterion. Leftover-map distance stays
two-axis Euclidean. Reconstruction is the same internal two-axis inner
product already used for `U`, so `U + R̂ = R` remains true. Do not
substitute a separately centered reconstruction `R̃` that would break
that identity.

The unprotected-stack reconstructions for neighbouring leftover facts
use 0162–0181 and 0184–0186. This protected-main increment uses
**0183** so it does not collide with leftover-map unexplained leftover
(0182), leftover-map reconstruction on the centered stack (0186),
leftover-map unexplained share (0184 on that stack), leftover-map
explained share, leftover-map cross share, leftover residual
disclosure, leftover observed `Y` / expected `E`, leftover-map rank,
two-axis leftover-map distance, leftover coverage, leftover-map axis
share (0148), or leftover interaction-map persistence.

## Decision

Each leftover pair names `leftover_map_reconstruction` — two-axis
Gabriel reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}`. Unused axes pad with
zero. Hidden SVD axes after the second are dropped. Migration `0183`
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,
residual, and unexplained leftover without fabricating 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` (`R = 0`
and `R̂ = 0`), not a missing value. A non-finite reconstruction stores
null rather than inventing a leftover score. A signed reconstruction
is stored, never clamped. Do not add a nonnegative CHECK.

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 unexplained-leftover 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. When both
reconstruction and unexplained leftover are finite,
`U + R̂ = R`.

## 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,
leftover-map cosine, leftover-map length, leftover-map unexplained
share, leftover-map explained share, leftover-map cross share, and
the centered leftover-map reconstruction stack.

## 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
Loading
Loading