diff --git a/AGENTS.md b/AGENTS.md index 8661a0ffa..0fd53fcb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,13 +75,14 @@ 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 / 0026) are computed in +Period leftover pairs (ADR 0017 / 0018 / 0026 / 0027) 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. The opened post names the leftover -criterion and marks that evaluation row as the next action. +criterion, marks that evaluation row as the next action, and +moves focus onto it (ADR 0027). `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 e2e91adc3..8870ecd5f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -525,7 +525,8 @@ 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, leftover-open copy -on that post (ADR 0026), and the +on that post (ADR 0026), leftover evaluation-row focus +(ADR 0027), 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/0.82.0-focus-leftover-eval-row.md b/CHANGELOG.d/0.82.0-focus-leftover-eval-row.md new file mode 100644 index 000000000..f2bcc8aab --- /dev/null +++ b/CHANGELOG.d/0.82.0-focus-leftover-eval-row.md @@ -0,0 +1,7 @@ +# 0.82.0 — Focus leftover evaluation row + +## Added + +- Opening a leftover pair focuses the matching evaluation row + (ADR 0027). After `make seed`, click Closest leftover: keyboard + focus lands on the sales-lead row. No invented leftover number. diff --git a/CHANGELOG.md b/CHANGELOG.md index c11301aaf..859aefbdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.82.0] - 2026-08-17 + +### Added + +- Opening a leftover pair focuses the matching IRT evaluation row + (ADR 0027). After `make seed`, click Closest leftover: the + sales-lead row is current and receives keyboard focus so “Read + that evaluation row next” is the next action, not a scroll hunt. + ## [0.78.0] - 2026-08-17 ### Added diff --git a/docs/adr/0027-focus-leftover-evaluation-row.md b/docs/adr/0027-focus-leftover-evaluation-row.md new file mode 100644 index 000000000..d42c4ebdd --- /dev/null +++ b/docs/adr/0027-focus-leftover-evaluation-row.md @@ -0,0 +1,46 @@ +# ADR 0027 — Focus the leftover evaluation row on the opened post + +**Decision status:** Accepted +**Date:** 2026-08-17 + +## Context + +ADR 0026 names leftover pair context on the opened post and marks the +matching evaluation row. The status still says “Read that evaluation +row next,” but the row stayed wherever it sat in the list. A long +rubric buries the leftover criterion under the fold. + +## Decision + +When leftover pairs for the open post match an evaluation row, that +row is the current evaluation item and receives focus. + +1. A leftover-matching row sets `aria-current="true"` and the + leftover badge from ADR 0026. +2. The first leftover pair in the loaded report payload is the + focus target. When the host provides layout, + `scrollIntoView({ block: "nearest" })` runs; keyboard + `tabIndex={-1}` focus always runs. Later leftover rows stay + marked, not invented. +3. A post with no leftover pair, or a row whose criterion does not + match, is not current and is not focused. + +Do not invent leftover numbers. Do not persist a second leftover +store. Do not mix this into #74 or #92. + +## Consequences + +After `make seed`, opening the closest leftover pair moves keyboard +focus to the sales-lead evaluation row. Mean θ stays on the report +panel. Rankings stay on ADR 0024. TEPP stays on #214. + +## Related + +Depends on [ADR 0026](0026-leftover-pair-open-context.md). + +## References + +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/frontend/package.json b/frontend/package.json index eef0c8735..5d3f2e2b8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.78.0", + "version": "0.82.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7202a1c80..812c3a56f 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1420,8 +1420,12 @@ describe("App, authenticated", () => { expect(screen.getByRole("status")).toHaveTextContent( "This post sat closest to sales-lead after main effects. Read that evaluation row next.", ); - expect(screen.getByText(/Sales-lead specificity: 3/)).toHaveTextContent("Closest leftover"); + const leftoverRow = screen.getByText(/Sales-lead specificity: 3/).closest("li"); + expect(leftoverRow).toHaveTextContent("Closest leftover"); + expect(leftoverRow).toHaveAttribute("aria-current", "true"); + expect(leftoverRow).toHaveFocus(); expect(screen.getByText(/Constructive stance: 2/)).not.toHaveTextContent("leftover"); + expect(screen.getByText(/Constructive stance: 2/).closest("li")).not.toHaveAttribute("aria-current"); }); it("opens Event Lineage, Keyman, and evaluation from a report member click", async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e9a47f701..c77816b10 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -770,12 +770,19 @@ function EvaluationPanel({ const [evaluating, setEvaluating] = useState(false); const [error, setError] = useState(null); const [orchestratorOff, setOrchestratorOff] = useState(false); + const leftoverRowRef = useRef(null); + const focusCriterion = leftoverPairs[0]?.criterion_code ?? null; useEffect(() => { setOrchestratorOff(false); setError(null); }, [postId]); + useEffect(() => { + leftoverRowRef.current?.scrollIntoView?.({ block: "nearest" }); + leftoverRowRef.current?.focus?.(); + }, [postId, focusCriterion, responses]); + async function handleEvaluate() { setEvaluating(true); setError(null); @@ -811,8 +818,14 @@ function EvaluationPanel({