Skip to content
Closed
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
30 changes: 17 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,21 +259,25 @@ 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 / 0185) 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
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,
plus unexplained leftover `U = R − R̂` after two-axis Gabriel
reconstruction, plus explained leftover share `e = R̂_c² / R̃²` of
centered leftover, next to leftover-map distance `d`. Two-axis
reconstruction `R̂` / `R̂_c` is computed internally and is not
persisted. They 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
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
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
`report_leftover_map_coverage` and captions the pair list with how
many scored posts entered the map.

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 / 0049 / 0119 / 0158 / 0162 / 0163 / 0164 / 0182) persist to
ADR 0017 / 0048 / 0049 / 0119 / 0158 / 0162 / 0163 / 0164 / 0182 / 0185) 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̂` named on the pair row, and explained leftover share
`e = R̂_c² / R̃²` of centered leftover. 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
9 changes: 9 additions & 0 deletions CHANGELOG.d/2.12.28-leftover-map-explained-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 2.12.28 — Leftover-map explained leftover share

- Persist explained leftover share `e = R̂_c² / R̃²` of centered
leftover on leftover post–criterion pairs (ADR 0185). After
`make seed`, closest and farthest leftover pairs sit above the
member list with `R̂²/R̃²` next to leftover-map distance `d`; click
opens that post. Omit the badge when the share is missing. Never
Comment thread
coderabbitai[bot] marked this conversation as resolved.
invent a leftover score. Do not persist leftover-map unexplained
leftover share `s`, unexplained leftover `U`, or reconstruction `R̂`.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ All notable changes to this project are documented here. Format follows
is unwired, with `이 범위의 일정을 아직 받을 수 없습니다`. Weekly VOC and
newspaper stay on the board.

## [2.12.28] - 2026-08-24

### Added

- Period leftover pair rows now name explained leftover share
`e = R̂_c² / R̃²` of centered leftover next to leftover-map distance
`d`, then open that post (Gabriel, 1971; Jeon et al., 2021, eq. 3;
ADR 0185). A missing share omits the badge rather than inventing a
leftover score. Two-axis reconstruction `R̂_c` stays internal and is
not persisted. Unexplained leftover share `s` is not persisted here.

## [2.12.26] - 2026-08-24

### Added
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), the text-channel embedding swap and cosine
(ADR 0048–0164 / 0182 / 0185), 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
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_explained_share
) 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_explained_share,
)
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_explained_share, 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_explained_share": (
None
if row["leftover_map_explained_share"] is None
else float(row["leftover_map_explained_share"])
),
"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
17 changes: 14 additions & 3 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@
/ "migrations"
/ "0182_report_leftover_map_unexplained.sql"
)
_LEFTOVER_MAP_EXPLAINED_SHARE_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
/ "0185_report_leftover_map_explained_share.sql"
)


def _postgres_available() -> bool:
Expand Down Expand Up @@ -305,6 +310,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_EXPLAINED_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 @@ -5049,9 +5055,14 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token,
assert "leftover_map_reconstruction" not in pair
observed = pair.get("observed_response")
expected = pair.get("expected_response")
if observed is None or expected is None:
continue
assert abs(pair["leftover_residual"] - (observed - expected)) < 1e-6
if observed is not None and expected is not None:
assert abs(pair["leftover_residual"] - (observed - expected)) < 1e-6
share = pair.get("leftover_map_explained_share")
assert share is None or isinstance(share, (int, float))
if share is not None:
assert share >= 0
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
10 changes: 6 additions & 4 deletions docs/adr/0003-fast-mlsirm-report-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ 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 / 0185): 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
`U = R − R̂` and explained leftover share `e = R̂_c² / R̃²` of
centered leftover when Gabriel coordinates exist so the leftover
cell the two-axis map does and does not reconstruct is not read as
leftover residual or leftover-map distance. Do not persist
leftover-map unexplained leftover share `s` or two-axis reconstruction
`R̂`. 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
Expand Down
10 changes: 7 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 0185](0185-leftover-map-explained-share.md) (explained leftover share)

## Context

Expand Down Expand Up @@ -41,8 +42,11 @@ 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), and names explained leftover share
`e = R̂_c² / R̃²` of centered leftover so the leftover cell the
two-axis map reconstructs is likewise not read as leftover residual
`R` or leftover-map distance `d` (ADR 0185). Two-axis reconstruction
`R̂` / `R̂_c` is computed internally and is not persisted.

Cascade the rows with `report_period_score`. A leftover post must
also be a `report_member_score` row, and the leftover criterion
Expand Down
43 changes: 26 additions & 17 deletions docs/adr/0049-leftover-pair-report-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
[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-explained-share.md) (explained leftover share);
[ADR 0158](0158-leftover-criterion-evaluation-landing.md) (criterion evaluation landing)

## Context

ADR 0048 persists closest and farthest leftover post–criterion pairs.
Those pairs only help if a buyer can see them on the Period reports
Those pairs only help if a reader can see them on the Period reports
panel and open the named post without hunting through the member list.

The member list is already the click-through to Event Lineage, Keyman,
Expand All @@ -23,20 +24,25 @@ second navigation surface.
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.
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.
distance, full map rank, observed `Y`, expected `E` when finite,
unexplained leftover `U` when finite, and explained leftover share `e`
when Gabriel coordinates exist. Every available measurement renders as
its own badge before opening the post; no amendment hides another, a
missing share or a missing unexplained leftover keeps its badge off
without removing the others, and rank 0 explicitly names no leftover
structure. The single next-action sentence follows a priority order
among the amendments: explained share (the newest, most specific
measurement) leads when present, then unexplained leftover names
"leftover map leaves unexplained `U` after IRT main effects; open this
post to read the named criterion", then the residual/observed-expected/
rank fallbacks. A missing share or missing unexplained leftover falls
through to the next entry in that order.
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
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).
quality marks the named criterion current (ADR 0158). 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),
explained-share naming is [ADR 0185](0185-leftover-map-explained-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 All @@ -49,9 +55,12 @@ A hidden post never appears as a leftover pair.

The authorized report payload carries `leftover_pairs` next to
`members` and `selected_items`. Screen-reader names are
`Open leftover closest pair: {title}` and
`Open leftover farthest pair: {title}` so the control announces the
next action, not only the distance.
The visible pair label `{kind}: {title} · {criterion}` (for example
`Closest leftover: Public post · sales-lead`) doubles as the
screen-reader name so the announced text matches what the reader sees
and names the criterion, not only the distance. Earlier increments
documented an `Open leftover closest pair: {title}` shape; the
criterion-bearing label supersedes it.

## Related

Expand Down
Loading
Loading