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
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,18 +271,18 @@ stops startup instead of leaving a healthy-looking partial schema, and
application code must not compensate for a missing table.

Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0158 / 0162 /
0163 / 0164 / 0182) are computed in `lineageweave/leftover_pairs.py` from the
0163 / 0164 / 0182 / 0201) are computed in `lineageweave/leftover_pairs.py` from the
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
plus leftover-map rank so rank 0 is not read as structure,
unexplained leftover, and the ADR 0201 reconstruction evidence. ADR 0201
is the sole normative reconstruction formula, storage, and audit contract;
do not duplicate or reinterpret it here. The pairs sit above the member
list so a click opens that post with the leftover criterion current
in Post quality (ADR 0158). Two-axis reconstruction `R̂` is not
persisted. Leftover-map axis share (ADR 0148) is Gabriel inertia of
in Post quality (ADR 0158). 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
6 changes: 3 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,10 +603,10 @@ 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 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 /
0182 / 0185) persist to `report_leftover_pair` with signed residual `R`,
0182 / 0185 / 0201) persist to `report_leftover_pair` with signed residual `R`,
observed `Y`, expected `E[Y|θ, item]`, full leftover-map rank, unexplained
leftover `U = R − R̂` named on the pair row, and leftover-map cross share
`x = 2 R̂ U / R²` of raw residual. Leftover-map axis share
leftover, ADR 0201 reconstruction evidence, and ADR 0185 cross-share evidence.
Those ADRs are the normative mathematical and storage contracts. 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
5 changes: 5 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,5 @@
## 2.12.31 — Leftover-map reconstruction

- Period leftover pairs now expose reconstruction evidence governed by ADR
0201. After `make seed`, open a closest or farthest pair to inspect it on
the named post. Missing reconstruction omits the badge.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ cutoff.

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), the text-channel embedding swap and cosine
(ADR 0048–0164 / 0182 / 0201), the text-channel embedding swap and cosine
clamp (ADR 0190), per-edge channel-score persistence (ADR 0195),
migration replay (ADR 0166), docstring coverage, and the measurement
boundary are all stated in [AGENTS.md](AGENTS.md) -- read it before
Expand Down
20 changes: 17 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,9 @@ 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, leftover_map_cross_share
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
leftover_map_unexplained, leftover_map_cross_share,
leftover_map_reconstruction
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
""",
grouping_kind,
grouping_key,
Expand All @@ -462,6 +463,7 @@ async def persist_period_report(
pair.leftover_map_rank,
pair.leftover_map_unexplained,
pair.leftover_map_cross_share,
pair.leftover_map_reconstruction,
)
for axis in report.leftover_map_axes:
await conn.execute(
Expand Down Expand Up @@ -647,7 +649,8 @@ 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, lp.leftover_map_cross_share, p.post_title,
lp.leftover_map_unexplained, lp.leftover_map_cross_share,
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 @@ -796,6 +799,11 @@ async def fetch_period_reports(
if row["leftover_map_cross_share"] is None
else float(row["leftover_map_cross_share"])
),
"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 Expand Up @@ -989,6 +997,7 @@ async def fetch_period_comparison(
f"""
select lp.grouping_kind, lp.grouping_key, lp.pair_kind, lp.post_id,
lp.criterion_code, lp.leftover_distance, lp.leftover_residual,
lp.leftover_map_reconstruction,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Comparison strip does not use the reconstruction next-action

fetch_period_comparison adds leftover_map_reconstruction to the payload, but the comparison strip renders its own inline buttons (App.tsx) rather than LeftoverPairList, so the reconstruction next-action ordering never applies there. No messaging inconsistency results.

Open in Devin Review

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

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 @@ -1033,6 +1042,11 @@ async def fetch_period_comparison(
"criterion_code": str(pair["criterion_code"]),
"leftover_distance": float(pair["leftover_distance"]),
"leftover_residual": float(pair["leftover_residual"]),
"leftover_map_reconstruction": (
None
if pair["leftover_map_reconstruction"] is None
else float(pair["leftover_map_reconstruction"])
),
"visibility_code": pair["visibility_code"],
"corporate_entity_id": str(pair["corporate_entity_id"]),
"has_real_source_context": bool(pair["has_real_source_context"]),
Expand Down
25 changes: 23 additions & 2 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@
/ "migrations"
/ "0185_report_leftover_map_cross_share.sql"
)
_LEFTOVER_MAP_RECONSTRUCTION_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
/ "0206_report_leftover_map_reconstruction.sql"
)
_GLOBAL_ASK_JOB_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
Expand Down Expand Up @@ -342,6 +347,7 @@ def seeded_db(demo_analyst_token):
cur.execute(_LEFTOVER_MAP_AXIS_MIGRATION.read_text())
cur.execute(_LEFTOVER_MAP_UNEXPLAINED_MIGRATION.read_text())
cur.execute(_LEFTOVER_MAP_CROSS_SHARE_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 @@ -5337,11 +5343,20 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token,
assert leftover_kinds <= {"closest", "farthest"}
assert all(pair["post_title"] for pair in high_report.get("leftover_pairs", []))
assert all(pair["leftover_distance"] >= 0 for pair in high_report.get("leftover_pairs", []))
assert all(
"leftover_map_reconstruction" in pair
for pair in high_report.get("leftover_pairs", [])
)
assert any(
pair["leftover_map_reconstruction"] is not None
for pair in high_report.get("leftover_pairs", [])
)
for pair in high_report.get("leftover_pairs", []):
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["leftover_map_reconstruction"]
assert reconstruction is None or isinstance(reconstruction, (int, float))
observed = pair.get("observed_response")
expected = pair.get("expected_response")
if observed is not None and expected is not None:
Expand All @@ -5351,9 +5366,10 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token,
if share is not None:
assert not math.isnan(share)
assert not math.isinf(share)
if unexplained is not None and reconstruction is not None:
assert unexplained + reconstruction == pytest.approx(pair["leftover_residual"])
assert "leftover_map_explained_share" not in pair
assert "leftover_map_unexplained_share" not in pair
assert "leftover_map_reconstruction" not in pair
leftover_axes = high_report.get("leftover_map_axes", [])
assert [axis["axis_index"] for axis in leftover_axes] == [1, 2]
assert all(axis["leftover_singular_value"] >= 0 for axis in leftover_axes)
Expand Down Expand Up @@ -5407,6 +5423,11 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token,
assert leftover_kinds <= {"closest", "farthest"}
assert all(pair["post_title"] for pair in leftover_thread.get("leftover_pairs", []))
assert all(pair["leftover_distance"] >= 0 for pair in leftover_thread.get("leftover_pairs", []))
assert all(
pair.get("leftover_map_reconstruction") is None
or isinstance(pair["leftover_map_reconstruction"], (int, float))
for pair in leftover_thread.get("leftover_pairs", [])
)


def test_seed_period_report_includes_fixture_event_lineage_posts(
Expand Down
21 changes: 9 additions & 12 deletions docs/adr/0003-fast-mlsirm-report-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,15 @@ 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 / 0185): after IRT main effects, persist closest and farthest
post–criterion pairs from the residual leftover map, and name
unexplained leftover `U = R − R̂` and leftover-map cross share
`x = 2 R̂ U / R²` of raw residual when Gabriel coordinates
exist so the identity remainder after two-axis reconstruction is not
read as leftover residual, leftover-map distance, explained leftover
share, or unexplained leftover share. Do not persist leftover-map
explained leftover share `e`, unexplained leftover share `s`, or any other
unsupported share alias in this slice. 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
7. **Leftover-pair slice** (shipped in 0.71.2; ADR 0017 / 0018 / 0048 /
0049): after IRT main effects, persist closest and farthest
post–criterion pairs from the residual leftover map. Do not fork LSIRM or
invent a leftover-pair API inside `fast-mlsirm` in this slice.
8. **Leftover evidence extensions:** unexplained leftover shipped in 2.12.26
(ADR 0182), cross-share evidence shipped in 2.12.29 (ADR 0185), and
reconstruction evidence is Unreleased for 2.12.31 (ADR 0201). Do not
persist explained share, unexplained share, or another unsupported alias.
9. **Leftover-map axis-share slice** (ADR 0148): persist Gabriel inertia
`σ_k² / Σ_j σ_j²` of leftover-map axes 1 and 2 on the same residual
SVD. Rank-0 residuals emit two zero-share axes. Do not invent a
leftover score.
Expand Down
5 changes: 3 additions & 2 deletions docs/adr/0048-persist-lsirm-leftover-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ distance `d` (ADR 0182), and names leftover-map cross share
exist so the identity remainder after two-axis reconstruction is not
read as leftover residual `R`, leftover-map distance `d`, explained
leftover share `e`, or unexplained leftover share `s` (ADR 0185).
This ADR does not add a persisted reconstruction column; persisting that
separate field requires its own accepted decision record.
ADR 0201 now persists that same signed reconstruction on the pair row so
`U + R̂ = R` remains directly auditable; it does not change this selection or
distance contract.

Cascade the rows with `report_period_score`. A leftover post must
also be a `report_member_score` row, and the leftover criterion
Expand Down
10 changes: 6 additions & 4 deletions docs/adr/0049-leftover-pair-report-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
[ADR 0164](0164-leftover-map-rank.md) (full map rank);
[ADR 0182](0182-leftover-map-unexplained.md) (unexplained leftover U);
[ADR 0158](0158-leftover-criterion-evaluation-landing.md) (criterion evaluation landing);
[ADR 0185](0185-leftover-map-cross-share.md) (leftover-map cross share)
[ADR 0185](0185-leftover-map-cross-share.md) (leftover-map cross share);
[ADR 0201](0201-leftover-map-reconstruction.md) (signed reconstruction R̂)

## Context

Expand All @@ -25,16 +26,16 @@ 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,
unexplained leftover `U` when finite, and leftover-map cross share next
to distance when finite. The next action names every available
unexplained leftover `U`, signed reconstruction `R̂` when finite, and
leftover-map cross share next to distance 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. When leftover-map cross
share is also present, the next action instead names the identity
remainder `x` two leftover-map axes leave in raw residual after
IRT main effects. A missing or non-finite value falls back in order —
cross share, then unexplained leftover, then the existing
cross share, then reconstruction, then unexplained leftover, then the existing
closest/farthest next action. Clicking the button opens that post with
leftover focus so Post quality marks the named criterion current
(ADR 0158). Residual naming is
Expand All @@ -43,6 +44,7 @@ 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-map cross
share naming is [ADR 0185](0185-leftover-map-cross-share.md).
Reconstruction naming is [ADR 0201](0201-leftover-map-reconstruction.md).

After `make seed`, closest and farthest leftover pairs sit above the
member list. Click a pair to open that post with the leftover
Expand Down
15 changes: 10 additions & 5 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 0201](0201-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 All @@ -21,15 +24,17 @@ shows. Hiding unexplained leftover `U = R − R̂` lets a buyer read
leftover residual `R` or leftover-map distance `d` as the leftover
the two-axis map does not reconstruct.

This increment does not persist leftover-map reconstruction `R̂`, does
At ADR 0182's initial acceptance, this increment did not persist
leftover-map reconstruction `R̂`; ADR 0201 now persists that value so
`U + R̂ = R` remains directly auditable. It still 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 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.
Reconstruction `R̂` is computed internally so `U` is honest, then
discarded.
Reconstruction `R̂` is computed internally so `U` is honest and is now
retained under ADR 0201.

The unprotected-stack reconstructions for neighbouring leftover facts
use 0162–0181. This protected-main increment uses **0182** so it does
Expand All @@ -53,8 +58,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
Loading
Loading