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
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,19 @@ 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 / 0201) are computed in `lineageweave/leftover_pairs.py` from the
0163 / 0164 / 0182 / 0185 / 0201 / 0233) 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,
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
unexplained leftover, the ADR 0201 reconstruction evidence, ADR 0185
cross-share evidence, and ADR 0233 unexplained leftover share
`s = U² / R²`. ADR 0201 is the sole normative reconstruction formula,
storage, and audit contract; do not duplicate or reinterpret it here.
ADR 0233 is the sole unexplained leftover share contract; do not persist
leftover-map explained share `e` here. The pairs sit above the member
list so a click opens that post with the leftover criterion current
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`.
Expand Down
5 changes: 3 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,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 / 0201) persist to `report_leftover_pair` with signed residual `R`,
0182 / 0185 / 0201 / 0233) persist to `report_leftover_pair` with signed residual `R`,
observed `Y`, expected `E[Y|θ, item]`, full leftover-map rank, unexplained
leftover, ADR 0201 reconstruction evidence, and ADR 0185 cross-share evidence.
leftover, ADR 0201 reconstruction evidence, ADR 0185 cross-share evidence,
and ADR 0233 unexplained leftover share `s`.
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
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.d/2.22.0-leftover-map-unexplained-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 2.22.0 — Leftover-map unexplained leftover share

- Persist leftover-map unexplained leftover share `s = U² / R²` of raw
residual on leftover post–criterion pairs (ADR 0233). After
`make seed`, closest and farthest leftover pairs sit above the
member list with `U²/R²` next to leftover-map distance `d`; click
opens that post. Omit the badge when the share is missing. A share
greater than 1 is shown, never clamped. Never invent a leftover
score. Do not introduce leftover-map explained share `e`.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ All notable changes to this project are documented here. Format follows
leftover remains the ADR 0182 value `U = R − R̂`. Explained leftover share
`e` and unexplained leftover share `s` are not persisted here.

- Period leftover pair rows now name leftover-map unexplained leftover share
`s = U² / R²` of raw residual next to leftover-map distance `d`, then
open that post (Gabriel, 1971; Jeon et al., 2021, eq. 3; ADR 0233). After
`make seed`, closest and farthest leftover pairs sit above the member
list with `U²/R²` next to `d`. A missing share omits the badge rather
than inventing a leftover score. A share greater than 1 is shown, never
clamped. Two-axis reconstruction `R̂` and leftover-map cross share `x`
stay as already persisted. Explained leftover share `e` is not persisted
here.

- The grouping comparison strip now names leftover post–criterion
pairs on each visible row (ADR 0149). After `make seed`, open a
leftover pair on A-100 from the strip to read that post. A leftover
Expand Down
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 / 0201), the text-channel embedding swap and cosine
(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233), 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
12 changes: 9 additions & 3 deletions backend/app/report_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,8 @@ async def persist_period_report(
pair_kind, post_id, criterion_code, leftover_distance, leftover_residual,
observed_response, expected_response, leftover_map_rank,
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)
leftover_map_reconstruction, leftover_map_unexplained_share
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
""",
grouping_kind,
grouping_key,
Expand All @@ -465,6 +465,7 @@ async def persist_period_report(
pair.leftover_map_unexplained,
pair.leftover_map_cross_share,
pair.leftover_map_reconstruction,
pair.leftover_map_unexplained_share,
)
for axis in report.leftover_map_axes:
await conn.execute(
Expand Down Expand Up @@ -652,7 +653,7 @@ async def fetch_period_reports(
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,
lp.leftover_map_reconstruction, p.post_title,
lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, p.post_title,
p.visibility_code, p.corporate_entity_id, p.process_unit_id,
({_SOURCE_CONTEXT_PRESENT_SQL}) as has_real_source_context
from report_leftover_pair lp
Expand Down Expand Up @@ -809,6 +810,11 @@ async def fetch_period_reports(
if row["leftover_map_reconstruction"] is None
else float(row["leftover_map_reconstruction"])
),
"leftover_map_unexplained_share": (
None
if row["leftover_map_unexplained_share"] is None
else float(row["leftover_map_unexplained_share"])
),
"visibility_code": row["visibility_code"],
"corporate_entity_id": str(row["corporate_entity_id"]),
"process_unit_id": (
Expand Down
18 changes: 17 additions & 1 deletion backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@
/ "migrations"
/ "0206_report_leftover_map_reconstruction.sql"
)
_LEFTOVER_MAP_UNEXPLAINED_SHARE_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
/ "0233_report_leftover_map_unexplained_share.sql"
)
_GLOBAL_ASK_JOB_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
Expand Down Expand Up @@ -410,6 +415,7 @@ def seeded_db(demo_analyst_token):
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(_LEFTOVER_MAP_UNEXPLAINED_SHARE_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 @@ -5812,10 +5818,15 @@ 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)
unexplained_share = pair.get("leftover_map_unexplained_share")
assert unexplained_share is None or isinstance(unexplained_share, (int, float))
if unexplained_share is not None:
assert not math.isnan(unexplained_share)
assert not math.isinf(unexplained_share)
assert unexplained_share >= 0.0
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
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 @@ -5878,6 +5889,11 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token,
or isinstance(pair["leftover_map_reconstruction"], (int, float))
for pair in leftover_thread.get("leftover_pairs", [])
)
assert all(
"leftover_map_unexplained_share" not in pair
and "leftover_map_explained_share" not in pair
for pair in leftover_thread.get("leftover_pairs", [])
)


def test_seed_period_report_includes_fixture_event_lineage_posts(
Expand Down
10 changes: 8 additions & 2 deletions docs/adr/0048-persist-lsirm-leftover-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
[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 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̂);
[ADR 0233](0233-leftover-map-unexplained-share.md) (unexplained leftover share s)

## Context

Expand Down Expand Up @@ -49,7 +51,11 @@ read as leftover residual `R`, leftover-map distance `d`, explained
leftover share `e`, or unexplained leftover share `s` (ADR 0185).
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.
distance contract. ADR 0233 persists unexplained leftover share
`s = U² / R²` of raw residual so the leftover the truncated map cannot
reconstruct is not read as leftover residual `R`, leftover-map distance
`d`, unexplained leftover `U`, or leftover-map cross share `x`. This
increment does not persist leftover-map explained leftover share `e`.

Cascade the rows with `report_period_score`. A leftover post must
also be a `report_member_score` row, and the leftover criterion
Expand Down
19 changes: 12 additions & 7 deletions docs/adr/0049-leftover-pair-report-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
[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 0201](0201-leftover-map-reconstruction.md) (signed reconstruction R̂)
[ADR 0201](0201-leftover-map-reconstruction.md) (signed reconstruction R̂);
[ADR 0233](0233-leftover-map-unexplained-share.md) (unexplained leftover share s)

## Context

Expand All @@ -26,17 +27,19 @@ 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`, signed reconstruction `R̂` when finite, and
unexplained leftover `U`, signed reconstruction `R̂` when finite,
leftover-map unexplained leftover share `s = U² / 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
post to read the named criterion" when present. When leftover-map
unexplained leftover share is also present, the next action instead names
the square share `s` of raw residual two leftover-map axes leave after
IRT main effects. A missing or non-finite value falls back in order —
cross share, then reconstruction, then unexplained leftover, then the existing
closest/farthest next action. Clicking the button opens that post with
unexplained leftover share, then cross share, then reconstruction, then
unexplained leftover, then rank / observed `Y` / expected `E`, then the
existing residual next action. Clicking the button opens that post with
leftover focus so Post quality marks the named criterion current
(ADR 0158). Residual naming is
[ADR 0162](0162-leftover-residual-disclosure.md), observed/expected
Expand All @@ -45,6 +48,8 @@ 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).
Unexplained leftover share naming is
[ADR 0233](0233-leftover-map-unexplained-share.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
106 changes: 106 additions & 0 deletions docs/adr/0233-leftover-map-unexplained-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ADR 0233 — Name leftover-map unexplained leftover share on period-report pair rows

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

Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and
[ADR 0049](0049-leftover-pair-report-ui.md). Independent of leftover-map
cross share ([ADR 0185](0185-leftover-map-cross-share.md)) and leftover-map
reconstruction ([ADR 0201](0201-leftover-map-reconstruction.md)).

## Context

ADR 0182 already persists unexplained leftover `U = R − R̂` after
two-axis Gabriel reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}`. ADR 0185
already persists leftover-map cross share `x = 2 R̂ U / R²`. The
raw-residual cell identity `R² = R̂² + U² + 2 R̂ U` therefore yields
`e + s + x = 1` with explained leftover share `e = R̂² / R²` and
unexplained leftover share `s = U² / R²`. Hiding `s` lets a buyer
read leftover residual `R`, leftover-map distance `d`, or unexplained
leftover `U` as the leftover the truncated map cannot reconstruct,
even though `s` is the square share of that leftover.

This increment persists leftover-map unexplained leftover share `s`.
It does not persist leftover-map explained leftover share `e`, does
not persist leftover-map coordinates, does not name leftover-map inner
product, cosine, or length, and does not land Post quality on the
leftover criterion. Leftover-map distance stays two-axis Euclidean.
Reconstruction `R̂` and unexplained leftover `U` remain the same
internal two-axis terms already used for `x`, so `e + s + x = 1`
stays auditable from persisted `R`, `R̂`, `U`, `x`, and `s`.

The unprotected-stack reconstructions for neighbouring leftover facts
use 0183 for unexplained leftover share. The dashboard stack already
uses **0232** for leftover-map explained leftover share (PR #728) and
**0222** for operations-case analysis input. This protected-main
increment uses **0233** (migration **0233**) so it does not collide with
GNB chrome (0183), ontology explorer (0184), leftover-map cross share
(0185), leftover-map reconstruction (0201 / migration 0206), leftover
residual disclosure, leftover observed `Y` / expected `E`, leftover-map
rank, two-axis leftover-map distance, leftover coverage, leftover-map
axis share (0148), leftover interaction-map persistence, leftover-map
explained leftover share (0232 on the dashboard stack), or
operations-case analysis input (0222 on that stack).

## Decision

Each leftover pair names `leftover_map_unexplained_share` — leftover-map
unexplained leftover share `s = U² / R²` of raw residual after
two-axis Gabriel reconstruction `R̂ = ξ_{1:2} · ζ_{1:2}` and
unexplained leftover `U = R − R̂`. Migration `0233` 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, unexplained leftover, reconstruction, and cross share
without fabricating a share. Fallback pairs that have no
complete-case leftover map omit the value rather than inventing one.
A rank-0 origin cell stores `0.0` when `R = R̂ = U = 0`, not a missing
value. A rank-0 constant residual with `R̂ = 0` stores `1.0` (`s = U² / R²`
with `U = R`). A non-finite share stores null rather than inventing a
leftover score. `s` is nonnegative because it is a square share; a
finite share greater than 1 is stored when `|U| > |R|`. Do not add an
upper-bound CHECK. This increment does not introduce
`leftover_map_explained_share`.

The pair button shows `U²/R² {share}` next to leftover-map
distance `d` when the value is a finite number. Next action: leftover
map leaves unexplained leftover share `s` of raw residual after IRT
main effects; open this post to read the named criterion. A missing
or non-finite share omits the badge and keeps the existing
cross-share / reconstruction / unexplained-leftover next action. Do
not invent a leftover score. Do not invent a theta.

## Consequences

`GET /api/reports/{grouping}/{period}` returns
`leftover_map_unexplained_share`. After `make seed`, closest and farthest
leftover pairs sit above the member list with named `U²/R²` next
to `d`; click opens that post. Hidden posts stay hidden. When `R`,
`R̂`, `U`, `x`, and `s` are all finite, `e + s + x = 1` with
`e = R̂² / R²` computed internally.

The grouping comparison strip (ADR 0149) stays on its reduced leftover
payload (distance, residual, reconstruction). Unexplained leftover
share is a period-report pair fact, not a comparison-strip badge.

## 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 reconstruction,
leftover-map unexplained leftover, leftover-map cross share, and
leftover-map explained leftover share.

## 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
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.17.0",
"version": "2.22.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Loading
Loading