feat: name leftover residual on period-report pair rows (v2.12.22) - #530
Conversation
After seed, leftover closest/farthest pairs sit above the member list with observed Y and expected E[Y|θ, item] next to leftover-map distance d. Click opens that post. Residual stays R = Y − E (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0170). Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), and two-axis distance (#522). Issues #79 and #87 stay open.
After seed, leftover closest/farthest pairs sit above the member list with leftover-map rank next to leftover-map distance d. Click opens that post. Rank is the count of Gabriel singular values above the floor (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0171). Rank 0 names no leftover structure. Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), two-axis distance (#522), and observed Y / expected E (#527). Issues #79 and #87 stay open.
ADR 0171 is already used by analysis-run status same clock (#524). Keep leftover-map rank independent: ADR 0172 and migration 0172. Shipped 0001 / 0012 stay untouched.
After seed, leftover closest/farthest pairs sit above the member list with signed leftover residual R next to leftover-map distance d. Click opens that post. Residual is R = Y − E[Y|θ, item] (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0178). A non-finite residual is an em dash. Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), two-axis distance (#522), observed Y / expected E (#527), and leftover-map rank (#529). Issues #79 and #87 stay open.
|
Independent leftover residual disclosure onto protected main. Exact head Does not mix leftover-map rank (#529), Y/E (#527 / colliding #526), two-axis (#522), coverage (#518), axis share (#519), comparison strip (#521), coords (#481), or criterion landing (#485). Closed #502 shipped this buyer fact onto unprotected |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Independent exact-head APPROVE still required on |
|
write-probe 2026-08-23T21:15:05Z cycle: retry GitHub writes first. |
Drop unused OIDC import and unauthenticated AdminPanel that fail tsc (TS6192 / TS2322). Login matches the workspace branch: AdminPanel is authenticated-only.
Drop unused OIDC import and unauthenticated AdminPanel that fail tsc (TS6192 / TS2322). AdminPanel stays authenticated-only.
|
Retry GitHub writes first this cycle: writes work as seonghobae. Independent exact-head Requested reviewers stay empty: the only org collaborator is the author, so review cannot be requested from this account. Copilot/human who is not the author must APPROVE independently. Next unique leftover honesty slice (not mixed into this residual stack): two-axis leftover-map reconstruction R̂ = ξ_{1:2} · ζ_{1:2} as v2.12.25 / ADR 0181 — distinct from #531 full-rank inner product and from #522 two-axis distance. |
|
Next unique leftover honesty slice (not mixed into this residual stack) is now open: leftover-map reconstruction R̂ = ξ_{1:2} · ζ_{1:2} as v2.12.25 / ADR 0181 — #534 exact head |
…ilter' into feat/leftover-pair-observed-expected-v21220
Merge #490 without force-push. Keep ADR 0049 next-action copy and name observed Y and expected E on leftover pair buttons. Replay migration 0177 on existing volumes.
…cted-v21220' into feat/leftover-map-rank-v21221
Merge stacked #527 without force-push. Persist leftover_map_rank with observed Y and expected E, and name rank on leftover pair buttons.
…nto feat/leftover-residual-disclosure-v21222
| export const LEFTOVER_RESIDUAL_ACTION = | ||
| "Leftover residual R {residual} after IRT main effects. Open this post to read {criterion}."; | ||
|
|
||
| export function formatLeftoverResidual(value: number): string { | ||
| if (!Number.isFinite(value)) { | ||
| return "—"; | ||
| } | ||
| const magnitude = Math.abs(value).toFixed(2); | ||
| if (value > 0) { | ||
| return `+${magnitude}`; | ||
| } | ||
| if (value < 0) { | ||
| return `\u2212${magnitude}`; | ||
| } | ||
| return magnitude; | ||
| } |
There was a problem hiding this comment.
🔴 Signed residual R never rendered on pair rows
formatLeftoverResidual and LEFTOVER_RESIDUAL_ACTION are never imported or called. LeftoverPairButton still renders only observed/expected and rank, and App.tsx passes no residual prop, so the promised R badge next to d never appears.
Prompt for agents
The new frontend/src/leftoverResidual.ts exports formatLeftoverResidual and LEFTOVER_RESIDUAL_ACTION, but neither is wired into the UI. frontend/src/components/LeftoverPairButton.tsx renders observed/expected (formatLeftoverObservedExpected) and rank (formatLeftoverMapRank) badges next to the distance badge, but never renders the residual. frontend/src/App.tsx (around line 4197) passes observedResponse, expectedResponse, and leftoverMapRank props to LeftoverPairButton but no residual. To deliver the feature described in ADR 0178 and the CHANGELOG, add a leftoverResidual prop to LeftoverPairButton, format it with formatLeftoverResidual, and render it as a post-badge next to the d badge; pass pair.leftover_residual from App.tsx.
Was this helpful? React with 👍 or 👎 to provide feedback.
…v21219' into feat/leftover-pair-observed-expected-v21220
…cted-v21220' into feat/leftover-map-rank-v21221
…nto feat/leftover-residual-disclosure-v21222
| pair.observed_response, | ||
| pair.expected_response, | ||
| pair.leftover_map_rank, |
There was a problem hiding this comment.
🟡 Seed insert names leftover columns absent from the test database
_persist_seed_period_report now inserts observed_response, expected_response, and leftover_map_rank into report_leftover_pair. The backend seeded_db fixture (test_api.py:258) creates that table only from 0001_initial_schema.sql and never applies migrations 0012/0172/0177, so those columns do not exist. test_seed_period_report_surfaces_on_get_reports and test_seed_period_report_includes_fixture_event_lineage_posts call this insert and raise an undefined-column error against a live stack.
Prompt for agents
The insert in _persist_seed_period_report (scripts/seed_demo_data.py) now writes observed_response, expected_response, and leftover_map_rank into report_leftover_pair. These columns are added only by migrations 0177 (observed/expected) and 0172 (leftover_map_rank). The backend integration fixture seeded_db in backend/tests/test_api.py creates report_leftover_pair from 0001_initial_schema.sql and applies a curated list of later migrations that does NOT include 0012, 0172, or 0177, so the three new columns are missing there. Tests that call _seed_demo_period_report against seeded_db will fail with an undefined-column error on a live stack. Update the seeded_db fixture migration list to apply migrations/0172_report_leftover_map_rank.sql and migrations/0177_report_leftover_observed_expected.sql (after report_leftover_pair exists) so the schema matches the new insert. Also note scripts/seed_demo_data.py's own seed() migration list applies 0177 but not 0172, which is the same inconsistency; add 0172 there too for self-consistency even though the make-seed path currently gets 0172 via migrate.sh.
Was this helpful? React with 👍 or 👎 to provide feedback.
| return ( | ||
| max(distance, 0.0), |
There was a problem hiding this comment.
📝 Info: Residual tolerance matches DB check boundary
_candidate_row raises when abs(residual - (Y-E)) >= 1e-6 and the 0177 check accepts < 1e-6; these are exact complements, so any Python-built row satisfies the DB constraint and the exact-boundary case is rejected by both. Consistent.
Was this helpful? React with 👍 or 👎 to provide feedback.
| cur.execute((migrations / "0010_report_item_information.sql").read_text()) | ||
| cur.execute((migrations / "0011_post_chat_result.sql").read_text()) | ||
| cur.execute((migrations / "0012_report_leftover_pair.sql").read_text()) | ||
| cur.execute((migrations / "0177_report_leftover_observed_expected.sql").read_text()) |
There was a problem hiding this comment.
🔍 seed() adds 0177 migration but omits 0172
seed() applies 0177_report_leftover_observed_expected.sql but not 0172_report_leftover_map_rank.sql, though _persist_seed_period_report inserts leftover_map_rank. The make-seed path is saved only because migrate.sh applies 0172; the asymmetry is fragile and worth aligning.
Was this helpful? React with 👍 or 👎 to provide feedback.
| for person, item in observed: | ||
| distance = abs(float(residual[person, item]) - center) |
There was a problem hiding this comment.
📝 Info: Rank forced to 0 on the distance fallback
When the biplot yields positions but every candidate distance is non-finite, the fallback branch resets leftover_map_rank to 0 and rebuilds candidates from |residual - center|. A report with a real SVD rank > 0 then persists rank 0. Defensible since the map was unusable, but the stored rank stops reflecting the singular-value count.
Was this helpful? React with 👍 or 👎 to provide feedback.
…v21219' into feat/leftover-residual-disclosure-v21222 # Conflicts: # CHANGELOG.md # docker/postgres-init/migrate.sh # tests/test_migration_replay.py # tests/test_schema.py
a8e5e89
into
feat/oidc-return-remember-login-v21219
…econstruction-v21225 Resolves the migrate.sh allowlist conflict between this PR's own new migrations (0179-0182: inner product, cosine, length, reconstruction) and the 0173 fix that landed further up the leftover-map stack (PRs #530-#533, already merged) -- union of both sides' migration numbers. CHANGELOG.md merged cleanly this time (no reordering needed). Verified: 33 passed in tests/test_schema.py + tests/test_migration_replay.py, 6 passed in tests/test_leftover_pairs.py, against a fresh Postgres 16 instance with the full migration chain applied.
This branch was behind main (like #547/#405 earlier this session). Resolved 2 conflicts: - CHANGELOG.md: both sides added independent [Unreleased] Added/Fixed entries; unioned them and merged the resulting duplicate "### Fixed" headers into one section. - docs/product-technical-gap-baseline.md: took main's current restructured snapshot wholesale, same reasoning as #405. frontend/src/App.tsx merged with no conflict and already carries the accessToken-guarded AdminPanel render (`destination === "admin" && accessToken`), confirming this branch doesn't need the recurring AdminPanel/oidcReturnUrl fix applied elsewhere this session. Verified: frontend build/lint clean. This PR's own new logic (33 tests across test_skos_organization_alias_binding.py, test_corporate_hierarchy_resolution.py, test_tied_organization_no_create.py -- pure-Python, no DB fixture) passes in 1.36s. Could not complete a full-suite run this cycle: the shared local Postgres is under the same sustained ~20-concurrent-session contention documented on #530/#547/#405 earlier this session (one run exceeded 30 minutes with no sign of finishing). Deferring full-suite confirmation to this PR's isolated GitHub Actions run, unaffected by local contention.
Downstream of #480's SKOS org-alias catalog work; also behind main. Resolved 7 conflicts: - pyproject.toml, frontend/package.json, uv.lock: this PR's own version (2.14.0) doesn't collide with main's current tip (2.12.18), kept it. - CHANGELOG.md: HEAD's Added entry sat under [Unreleased] even though the version files claimed 2.14.0 -- promoted it to its own dated "## [2.14.0]" section (matching the convention every other version bump in this file already follows) and placed main's [Unreleased] additions/fixes above it. - docs/product-technical-gap-baseline.md: took main's current snapshot wholesale, same reasoning as #405/#480. - docs/storybook-inventory.md, frontend/src/App.test.tsx: independent additions on each side (new story row; new stub-config flags); unioned. frontend/src/App.tsx merged with no conflict and already carries the accessToken-guarded AdminPanel render. Verified: frontend build/lint clean. This PR's own new logic (37 tests across test_organization_alias.py, test_affiliate_tree.py, test_knowledge_graph.py, test_organization_name_resolution_ingestion.py) passes in 4.94s. Full-suite run hit the same sustained local-Postgres contention documented on #530/#547/#405/#480 this session (15+ min with no sign of finishing); deferring to this PR's isolated GitHub Actions run as the authoritative check, same precedent as those four.
… reconciled with ADR-0145 weight estimation Genuine dual-feature conflict, not a mechanical one: this PR's own rebuild_lineage() rewrite (adjudication/embedding client wiring, session advisory lock, bounded snapshot-changed retry, ADR 0195 channel_scores persistence) landed at the same call site main independently modified for ADR 0145 persisted channel-weight loading. Resolved by threading all three concerns through one function: - lineage_edge_specs() / reconstruct() already had all three keyword params (llm, embedding, weights) merge cleanly with no conflict -- confirming the two features are compositionally compatible. - Rebuilt rebuild_lineage() to: compute the active channel set (adding "llm" only when adjudication_client is real, matching how load_estimated_channel_weights decides whether a persisted vector applies), load weights once, then run this PR's lock+retry+recheck structure with weights threaded through the lineage_edge_specs call. - backend/app/main.py, docker/postgres-init/migrate.sh (took main's ADR 0166 generalized wildcard replay pattern -- already covers this PR's own migration 0195), CHANGELOG.md (promoted this PR's [Unreleased] entry to a dated ## [2.24.0] section, matching the file's own convention, ahead of main's [2.12.18]), pyproject.toml/uv.lock (kept this PR's 2.24.0, no collision with main's 2.12.18 tip), frontend/src/App.test.tsx (recurring OIDC-return-url pattern, took main's superset assertion): mechanical, low-risk. tests/test_lineage_ingestion.py needed real fixes, not just marker removal, to keep passing against the merged rebuild_lineage(): - _RebuildConnection (this PR's own fake asyncpg connection, used by 7 of its own tests) predates ADR 0145 and had no fetchval -- added one returning False (no persisted weight table), matching every other fake connection's default in this file. - 5 monkeypatched lineage_edge_specs stand-ins across this PR's own tests only accepted (records, *, llm, embedding) -- rebuild_lineage now always passes weights= too. Added weights=None to each stub; without this every adjudication/embedding-wiring test would have failed with "unexpected keyword argument 'weights'" the moment this PR's own branch met main. - Concatenated main's two lineage_graphs_for_posts tests after this PR's ten rebuild_lineage tests -- different function, no overlap. Verified: this module's own 26 tests pass, 32 more in directly-adjacent modules (reconstruct, embedding_client, adjudication_client, channel weight estimation) pass, `python -c "import backend.app.main"` succeeds, frontend build/lint clean, the one touched App.test.tsx assertion passes standalone. Could not complete the full ~800-test suite locally -- severe sustained Postgres contention from ~20 concurrent peer sessions, same as #530/#547/#405/#480/#482 this session (20+ min, no sign of finishing). Deferring full-suite confirmation to this PR's isolated GitHub Actions run.
Buyer next action
After
make seed, leftover closest/farthest pairs still sit above the period-report member list. Each pair now names signed leftover residualR = Y − E[Y|θ, item]next to leftover-map distanced. ReadR, then click the pair to open that post to read the named criterion. A non-finite residual is an em dash. Never invent a leftover score. Never invent a theta.Exact unique scope
Independent leftover honesty slice on protected
main@ef6f5a5(v2.12.6). Exact head:1834e7d.leftover_residualon the pair row. No schema change; ADR 0048 / migration0012already stores the column.R {signed}next to leftover-map distanced.Rafter IRT main effects; open this post to read the named criterion.Does not mix into leftover persist-map (#481), leftover criterion landing (#485), leftover complete-case coverage (#518), leftover-map axis share (#519), leftover comparison-strip (#521), two-axis leftover-map distance (#522), colliding Y/E (#526), observed Y / expected E (#527), or leftover-map rank (#529). Closed #502 shipped residual disclosure onto the unprotected
#426stack, not protectedmain.Issues #79 and #87 stay open.
Merge contract
Independent exact-head APPROVE required. Do not self-approve. Squash-merge only with independent APPROVE + exact-head required success. Do not merge colliding #526.
Tests
pnpm exec vitest run leftoverResidual i18n App: leftoverResidual + i18n + App 101 passed.Papers
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