Skip to content

feat: name leftover residual on period-report pair rows (v2.12.22) - #530

Merged
seonghobae merged 17 commits into
feat/oidc-return-remember-login-v21219from
feat/leftover-residual-disclosure-v21222
Aug 24, 2026
Merged

feat: name leftover residual on period-report pair rows (v2.12.22)#530
seonghobae merged 17 commits into
feat/oidc-return-remember-login-v21219from
feat/leftover-residual-disclosure-v21222

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Buyer next action

After make seed, leftover closest/farthest pairs still sit above the period-report member list. Each pair now names signed leftover residual R = Y − E[Y|θ, item] next to leftover-map distance d. Read R, 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.

  • Name already-persisted leftover_residual on the pair row. No schema change; ADR 0048 / migration 0012 already stores the column.
  • UI badge R {signed} next to leftover-map distance d.
  • Next action: leftover residual R after IRT main effects; open this post to read the named criterion.
  • ADR 0178 (not 0162 from the unprotected stack). Release 2.12.22.

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 #426 stack, not protected main.

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.
  • No Python leftover_pairs change; residual is already persisted.

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


Open in Devin Review

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.

Copy link
Copy Markdown
Contributor Author

Independent leftover residual disclosure onto protected main. Exact head 1834e7d. This identity will not self-approve. Squash-merge only after independent exact-head APPROVE and required checks on this 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 #426 only.

Issues #79 and #87 stay open.

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7263d8da-d1f9-46ca-9e13-2bfa3c3703a0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Independent exact-head APPROVE still required on 1834e7d. Author will not self-approve. Squash-merge only with independent APPROVE + exact-head required success. Frontend tsc on this head reports unused leftoverResidual import and App.tsx string|undefined at 4627 — follow-up if that is this slice; do not wait on Checks to start the next unique leftover honesty increment. Issues #79 and #87 stay open.

Copy link
Copy Markdown
Contributor Author

write-probe 2026-08-23T21:15:05Z cycle: retry GitHub writes first.

Copy link
Copy Markdown
Contributor Author

Next unique leftover honesty slice (not mixed into this residual stack): leftover-map inner product ξ·ζ as v2.12.23 / ADR 0179 — #531 exact head c9793316070625da95267228b0d0005460614f0d. This identity will not self-approve #530 or #531.

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.

Copy link
Copy Markdown
Contributor Author

Retry GitHub writes first this cycle: writes work as seonghobae.

Independent exact-head APPROVE still required on 1834e7df72a782d1d9f879a7c0426e07471b5c79. Author will not self-approve. Squash-merge only with independent APPROVE + exact-head required success. Do not wait on Checks.

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.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

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 02c4396d646fcff6912951313d1dc4a1f56d833d.

…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
@seonghobae
seonghobae changed the base branch from main to feat/leftover-map-rank-v21221 August 23, 2026 22:01

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +3 to +18
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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.
Open in Devin Review

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

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

Open in Devin Review

Comment thread scripts/seed_demo_data.py
Comment on lines +1228 to +1230
pair.observed_response,
pair.expected_response,
pair.leftover_map_rank,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.
Open in Devin Review

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

Comment on lines +138 to +139
return (
max(distance, 0.0),

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: 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.

Open in Devin Review

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

Comment thread scripts/seed_demo_data.py
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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 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.

Open in Devin Review

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

Comment on lines 100 to 101
for person, item in observed:
distance = abs(float(residual[person, item]) - center)

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: 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.

Open in Devin Review

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
@seonghobae
seonghobae merged commit a8e5e89 into feat/oidc-return-remember-login-v21219 Aug 24, 2026
2 of 4 checks passed
seonghobae added a commit that referenced this pull request Aug 24, 2026
…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.
seonghobae added a commit that referenced this pull request Aug 24, 2026
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.
seonghobae added a commit that referenced this pull request Aug 24, 2026
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.
seonghobae added a commit that referenced this pull request Aug 24, 2026
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant