From c4f1b8b14f2e147e2c4f6a46219cda66ac8a4027 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 15:35:24 +0000 Subject: [PATCH 1/3] feat: name leftover residual on period-report pair rows (v2.12.12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Period leftover pairs already persisted R = Y − E[Y|θ, item] beside leftover-map distance. The pair button only showed d, so a large leftover response looked the same as a merely distant map pair. Show signed residual R with the next action to read it after IRT main effects, then open that post. Independent of leftover-map persistence (#481) and leftover-criterion landing (#485). Jeon et al. (2021, eq. 3); Gabriel (1971). ADR 0162. --- AGENTS.md | 7 +- ARCHITECTURE.md | 3 +- .../2.12.12-leftover-residual-disclosure.md | 6 ++ CHANGELOG.md | 6 ++ docs/adr/0049-leftover-pair-report-ui.md | 9 +-- docs/adr/0162-leftover-residual-disclosure.md | 63 ++++++++++++++++ docs/storybook-inventory.md | 1 + frontend/package.json | 2 +- frontend/src/App.test.tsx | 6 +- frontend/src/App.tsx | 35 ++------- .../components/LeftoverPairList.stories.tsx | 42 +++++++++++ .../src/components/LeftoverPairList.test.tsx | 71 +++++++++++++++++++ frontend/src/components/LeftoverPairList.tsx | 64 +++++++++++++++++ frontend/src/i18n.test.ts | 20 ++++++ frontend/src/i18n.ts | 28 ++++++++ frontend/src/leftoverResidual.test.ts | 11 +++ frontend/src/leftoverResidual.ts | 14 ++++ pyproject.toml | 2 +- 18 files changed, 349 insertions(+), 41 deletions(-) create mode 100644 CHANGELOG.d/2.12.12-leftover-residual-disclosure.md create mode 100644 docs/adr/0162-leftover-residual-disclosure.md create mode 100644 frontend/src/components/LeftoverPairList.stories.tsx create mode 100644 frontend/src/components/LeftoverPairList.test.tsx create mode 100644 frontend/src/components/LeftoverPairList.tsx create mode 100644 frontend/src/leftoverResidual.test.ts create mode 100644 frontend/src/leftoverResidual.ts diff --git a/AGENTS.md b/AGENTS.md index 1728f9e61..a2cd37662 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,12 +186,13 @@ in the same spirit) -- never against real data, per the hard rule above. against a live local stack (`make up`) and self-skip without one -- see [README.md](README.md#local-product-stack-docker-compose). -Period leftover pairs (ADR 0017 / 0018) are computed in +Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0162) are computed in `lineageweave/leftover_pairs.py` from the residual after a real GRM/GPCM score, never invented. Missing cells stay out of the Gabriel factorization. Closest and farthest post–criterion pairs -persist to `report_leftover_pair` and sit above the member list so -a click opens that post. +persist to `report_leftover_pair` and sit above the member list with +leftover residual `R` and leftover-map distance `d` so a click opens +that post. `frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`, pnpm via Corepack -- do not add a second Node package manager or a diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d0280ff97..3704dba02 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -601,7 +601,8 @@ bank as the dummy high/low band rows, so comparison-strip click through opens those DAG posts. Report members include the earliest open ticket title, status lookup label, and due date when one exists. The home page renders the actual mean θ, the FIPC delta, the CAT-selected item, leftover -closest/farthest pairs above the member list, and the +closest/farthest pairs (residual `R` after IRT main effects plus +leftover-map distance `d`) above the member list, and the PU / corp / thread comparison -- never a placeholder. TEPP is unchanged. ## Phase 6b: Knowledge Graph as a real Ontology + Semantic Layer diff --git a/CHANGELOG.d/2.12.12-leftover-residual-disclosure.md b/CHANGELOG.d/2.12.12-leftover-residual-disclosure.md new file mode 100644 index 000000000..efadde4d9 --- /dev/null +++ b/CHANGELOG.d/2.12.12-leftover-residual-disclosure.md @@ -0,0 +1,6 @@ +# 2.12.12 leftover residual disclosure + +After `make seed`, closest and farthest leftover pairs sit above the +period-report member list with leftover residual `R` and leftover-map +distance `d`. Read the residual after IRT main effects, then click the +pair to open that post. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ed1a099..a10d89d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to this project are documented here. Format follows ## [Unreleased] +### Added + +- Period leftover pair rows now name leftover residual `R` after IRT + main effects next to leftover-map distance `d`, then open that post + (Jeon et al., 2021, eq. 3; ADR 0162). + ### Fixed - `make smoke` and `make seed` now run through the locked project `uv` diff --git a/docs/adr/0049-leftover-pair-report-ui.md b/docs/adr/0049-leftover-pair-report-ui.md index a93985b40..3d0ac7dc5 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -17,10 +17,11 @@ 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, leftover-map distance, and the next -action (“Open this post to read the criterion it sat closest to / -farthest from after main effects.”). Clicking the button opens that -post with the same handler as a member row. +title, criterion short label, leftover residual `R`, leftover-map +distance, and the next action (“Leftover residual R after IRT main +effects. Open this post to read {criterion}.”). Clicking the button +opens that post with the same handler as a member row. Residual +disclosure is [ADR 0162](0162-leftover-residual-disclosure.md). After `make seed`, closest and farthest leftover pairs sit above the member list. Click a pair to open that post. diff --git a/docs/adr/0162-leftover-residual-disclosure.md b/docs/adr/0162-leftover-residual-disclosure.md new file mode 100644 index 000000000..1f274acc6 --- /dev/null +++ b/docs/adr/0162-leftover-residual-disclosure.md @@ -0,0 +1,63 @@ +# ADR 0162 — Disclose leftover residual on period-report pair rows + +**Decision status:** Accepted +**Date:** 2026-08-23 + +## Context + +ADR 0048 already persists `leftover_distance` (Euclidean leftover-map +gap from the Gabriel biplot of `R`) and `leftover_residual` +(`R = Y − E[Y|θ, item]`) on `report_leftover_pair`. ADR 0049 already +renders closest and farthest pairs above the member list and opens the +named post. The pair button showed only `d`, so a buyer could not tell +a large leftover response from a merely distant map pair. + +Jeon et al. (2021, eq. 3) leftover interaction is +`−γ‖ξ_p − ζ_i‖`. Distance is that map gap. Residual is the observed +leftover *after IRT main effects* that entered the biplot. They are +different quantities. Hiding residual would keep the persisted column +as an unpublished measurement. + +This increment does not persist leftover-map coordinates (ADR 0121 / +PR #481) and does not land Post quality on the leftover criterion +(ADR 0125 / PR #485). + +## Decision + +Each leftover pair button shows: + +1. closest or farthest label, post title, and criterion short label; +2. leftover residual `R` with an explicit sign, two decimal places; +3. leftover-map distance `d`; +4. the next action: read residual `R` after IRT main effects, then + open this post to read the named criterion. + +Missing leftover rows still render nothing. A non-finite residual +renders an em dash rather than a fabricated leftover score. Click +still uses the same post-open handler as ADR 0049. + +## Consequences + +`GET /api/reports/{grouping}/{period}` already returns +`leftover_residual`. The frontend now names that value. After +`make seed`, closest and farthest leftover pairs sit above the member +list with `R` and `d`; click opens that post. + +## Related + +Amends [ADR 0049](0049-leftover-pair-report-ui.md). Depends on +[ADR 0048](0048-persist-lsirm-leftover-pairs.md) and +[ADR 0003](0003-fast-mlsirm-report-integration.md). Independent of +leftover interaction-map persistence and leftover-criterion evaluation +landing. + +## 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 diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 28c59bd48..1d1c1dd93 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -9,6 +9,7 @@ buyer-facing control you can click before changing product CSS. | `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` | | `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` | | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | +| `Reports/LeftoverPairList` | Read leftover residual R after IRT main effects, then open the named post. | `--color-chip-border`, `LeftoverPairList` | Repeated web objects must use `frontend/src/styles/tokens.css` and a module under `frontend/src/components/`. Do not add a second Node package manager; diff --git a/frontend/package.json b/frontend/package.json index e2e996bbe..e100d4f32 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.12.6", + "version": "2.12.12", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..51b594ca5 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -3357,13 +3357,15 @@ describe("App, authenticated", () => { }); expect(closestPair).toHaveTextContent("Closest leftover: Public post · sales-lead"); expect(closestPair).toHaveTextContent( - "Open this post to read the criterion it sat closest to after main effects.", + "Leftover residual R +0.40 after IRT main effects. Open this post to read sales-lead.", ); + expect(closestPair).toHaveTextContent("R +0.40"); expect(closestPair).toHaveTextContent("d 0.12"); expect(farthestPair).toHaveTextContent("Farthest leftover: Specification revision requested · negative"); expect(farthestPair).toHaveTextContent( - "Open this post to read the criterion it sat farthest from after main effects.", + "Leftover residual R −1.10 after IRT main effects. Open this post to read negative.", ); + expect(farthestPair).toHaveTextContent("R −1.10"); expect(farthestPair).toHaveTextContent("d 1.84"); const memberButton = screen.getByRole("button", { name: /open report post: public post/i }); expect(closestPair.compareDocumentPosition(memberButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..2ee82152d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { AdminPanel } from "./components/AdminPanel"; +import { LeftoverPairList } from "./components/LeftoverPairList"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useAuth } from "react-oidc-context"; @@ -3390,35 +3391,11 @@ function ReportsPanel({ )} {report.leftover_pairs && report.leftover_pairs.length > 0 && ( - + onSelectPost(postId)} + /> )} {report.members.length > 0 && (