diff --git a/CHANGELOG.d/0.98.0-analysis-run-open-report-grouping.md b/CHANGELOG.d/0.98.0-analysis-run-open-report-grouping.md new file mode 100644 index 000000000..26f164206 --- /dev/null +++ b/CHANGELOG.d/0.98.0-analysis-run-open-report-grouping.md @@ -0,0 +1,4 @@ +# 0.98.0 Open the corp grouping with the scored week + +Open period report 2026-W02 also switches Report grouping to Corporate +entity and marks Demo Corp current. Mean θ stays on the report panel. diff --git a/CHANGELOG.md b/CHANGELOG.md index ba848eea0..1f7a2a579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ 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.98.0] - 2026-08-17 + +### Added + +- Opening **Open period report 2026-W02** from a corporate-entity + analysis run also switches Report grouping to Corporate entity and + marks the Demo Corp grouping current. The opened report is named + Demo Corp, not a UUID. Mean θ stays on the report panel. No TEPP + theta is invented. + ## [0.97.0] - 2026-08-17 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index b80545927..5d246f6ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,4 +42,5 @@ then start. Do not invent a theta. Hover the Result prefix to read the parent-choice digest. After `make seed`, open **Period report · Succeeded · Demo Corp**, then **Open period report 2026-W02**. The report period field is -focused. Mean θ stays on the period-report panel. +focused. Report grouping is Corporate entity and Demo Corp is current. +Mean θ stays on the period-report panel. diff --git a/backend/app/analysis_run_ingestion.py b/backend/app/analysis_run_ingestion.py index f9108b9c6..b848c6ca6 100644 --- a/backend/app/analysis_run_ingestion.py +++ b/backend/app/analysis_run_ingestion.py @@ -90,6 +90,24 @@ """ +def scope_grouping_key(row: Any) -> str | None: + """Persist the reconstruct grouping key for the run's authorized scope. + + A corporate-entity report run stores the week on ``scope_key``. The + grouping that reconstruct and the period-report panel share is the + corporate entity (or process unit / thread group), never that week + label and never a theta. + """ + scope = row["scope_kind_code"] + if scope == "analysis_scope_corporate_entity" and row["corporate_entity_id"]: + return str(row["corporate_entity_id"]) + if scope == "analysis_scope_process_unit" and row["process_unit_id"]: + return str(row["process_unit_id"]) + if scope == "analysis_scope_thread_group" and row["scope_key"]: + return str(row["scope_key"]) + return None + + def _iso(value: Any) -> str: """Serialize a timestamptz the same way post payloads do.""" return value.isoformat() if hasattr(value, "isoformat") else str(value) @@ -253,6 +271,9 @@ async def _serialize_runs( item["scope_entity_name"] = row["scope_entity_name"] if row["scope_key"]: item["scope_key"] = row["scope_key"] + grouping_key = scope_grouping_key(row) + if grouping_key: + item["scope_grouping_key"] = grouping_key payload.append(item) return payload diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index 07c0bf56f..e5459b30c 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -490,10 +490,14 @@ async def fetch_period_reports( selected_by_group[row["grouping_key"]].append(row) payload: list[dict[str, Any]] = [] for header in headers: + grouping_key = header["grouping_key"] payload.append( { "grouping_kind": header["grouping_kind"], - "grouping_key": header["grouping_key"], + "grouping_key": grouping_key, + "grouping_label": await resolve_grouping_label( + conn, header["grouping_kind"], grouping_key + ), "period_code": header["period_code"], "rubric_version": header["rubric_version"], "selected_model": header["selected_model"], diff --git a/docs/adr/0024-seed-period-report-analysis-run.md b/docs/adr/0024-seed-period-report-analysis-run.md index de8bf3240..a392de46f 100644 --- a/docs/adr/0024-seed-period-report-analysis-run.md +++ b/docs/adr/0024-seed-period-report-analysis-run.md @@ -47,8 +47,10 @@ on a path that is not allowed to (ADR 0021 / ADR 0022 / ADR 0023). After `make seed`, Demo Analyst opens Analysis runs and sees **Period report · Succeeded · Demo Corp** next to the lineage and TEPP rows. Opening it shows the cutoff posts and **Open period report -2026-W02** (the week stored on `scope_key`). Mean θ remains on the -period-report panel. Re-seed is idempotent on +2026-W02** (the week stored on `scope_key`). That click also switches +Report grouping to Corporate entity and marks the Demo Corp grouping +current, using the persisted scope grouping key. Mean θ remains on +the period-report panel. Re-seed is idempotent on `demo-report-seed-2026-w02`. ## References — APA 7th diff --git a/frontend/package.json b/frontend/package.json index d62e46766..3e22b4931 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.97.0", + "version": "0.98.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.css b/frontend/src/App.css index 135c251f2..d43b5d75f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -48,6 +48,12 @@ font-size: 1rem; } +.post-list-item[aria-current="true"], +.ticket-list-item[aria-current="true"] { + border-color: #2563eb; + box-shadow: inset 0 0 0 1px #2563eb; +} + .post-badge { font-size: 0.75rem; opacity: 0.7; diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 3d020ad40..55d7b24b6 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -189,6 +189,7 @@ describe("App, authenticated", () => { scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", scope_key: "2026-W02", + scope_grouping_key: "corp-1", status_code: reportSucceeded ? "analysis_status_succeeded" : "analysis_status_failed", status_label: reportSucceeded ? "Succeeded" : "Failed", knowledge_cutoff: "2026-01-12T12:00:00Z", @@ -674,6 +675,7 @@ describe("App, authenticated", () => { scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", scope_key: "2026-W02", + scope_grouping_key: "corp-1", status_code: options?.failedReportRun ? ("analysis_status_failed" as const) : ("analysis_status_succeeded" as const), @@ -743,7 +745,7 @@ describe("App, authenticated", () => { { grouping_kind: "corporate_entity", grouping_key: "corp-1", - grouping_label: "Test Corp", + grouping_label: "Demo Corp", mean_theta: 0.01, post_count: 8, link_method: "fipc", @@ -793,6 +795,59 @@ describe("App, authenticated", () => { }), ); } + if (url.includes("/api/reports/corporate_entity/") && method === "GET") { + return Promise.resolve( + jsonResponse({ + grouping_kind: "corporate_entity", + period_code: url.includes("2026-W03") ? "2026-W03" : "2026-W02", + reports: [ + { + grouping_key: "corp-other", + grouping_label: "Other Corp", + selected_model: "grm", + mean_theta: -0.2, + mean_theta_sd: 0.1, + post_count: 2, + item_count: 3, + fit_converged: true, + link_method: "fipc", + anchor_period_code: "2026-W02", + delta_mean_theta: null, + selected_items: [], + members: [], + }, + { + grouping_key: "corp-1", + grouping_label: "Demo Corp", + selected_model: "grm", + mean_theta: 0.42, + mean_theta_sd: 0.1, + post_count: 8, + item_count: 3, + fit_converged: true, + link_method: "fipc", + anchor_period_code: "2026-W02", + delta_mean_theta: null, + selected_items: [ + { item_code: "sales_lead_specificity", rank: 1, information: 0.7 }, + ], + members: [ + { + post_id: "post-1", + post_title: "Public post", + theta_eap: 0.91, + theta_sd: 0.2, + ticket_due_date: "2026-01-12", + ticket_title: "Send Northridge Grid the revised quote", + ticket_status_code: "open", + ticket_status_label: "Open", + }, + ], + }, + ], + }), + ); + } if (url.includes("/api/reports/") && method === "GET") { return Promise.resolve( jsonResponse({ @@ -2119,9 +2174,20 @@ describe("App, authenticated", () => { await userEvent.clear(periodInput); await userEvent.type(periodInput, "2026-W03"); expect(periodInput).toHaveValue("2026-W03"); + const groupingSelect = screen.getByLabelText("Report grouping"); + expect(groupingSelect).toHaveValue("process_unit"); await userEvent.click(screen.getByRole("button", { name: "Open period report 2026-W02" })); expect(periodInput).toHaveValue("2026-W02"); + expect(groupingSelect).toHaveValue("corporate_entity"); expect(periodInput).toHaveFocus(); + expect( + screen.getByRole("button", { name: "Compare corporate_entity: Demo Corp" }), + ).toHaveAttribute("aria-current", "true"); + expect( + screen.getByRole("button", { name: "Compare process_unit: Demo Report High" }), + ).not.toHaveAttribute("aria-current"); + expect(await screen.findByText(/Demo Corp: mean θ 0\.42/)).toBeInTheDocument(); + expect(screen.queryByText(/corp-1: mean θ/)).not.toBeInTheDocument(); }); it("does not tell a failed period report to connect the measurement service", async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c6a3f570d..570355774 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1689,6 +1689,28 @@ const REPORT_PERIOD_KEY = /^\d{4}-W\d{2}$/; * That key is a week label, not a theta. Missing or malformed keys * stay closed so we do not invent a period. */ +/** + * Report grouping that matches the run's authorized scope. + * + * A corporate-entity run must not leave the panel on process unit. + */ +function analysisRunReportGrouping(run: AnalysisRun): string | null { + switch (run.scope_kind_code) { + case "analysis_scope_corporate_entity": + return "corporate_entity"; + case "analysis_scope_process_unit": + return "process_unit"; + case "analysis_scope_thread_group": + return "thread_group"; + default: + return null; + } +} + +function analysisRunReportGroupingKey(run: AnalysisRun): string | undefined { + return run.scope_grouping_key || undefined; +} + function analysisRunReportPeriod(run: AnalysisRun): string | null { if (run.run_kind_code !== "analysis_run_report") { return null; @@ -1725,7 +1747,12 @@ function AnalysisRunsPanel({ }: { accessToken: string; onSelectPost: (postId: string, options?: SelectPostOptions) => void; - onSelectReportPeriod?: (periodCode: string) => void; + onSelectReportPeriod?: ( + periodCode: string, + groupingKind?: string, + groupingKey?: string, + groupingLabel?: string, + ) => void; }) { const [runs, setRuns] = useState(null); const [selected, setSelected] = useState(null); @@ -1903,7 +1930,12 @@ function AnalysisRunsPanel({ onClick={() => { const periodCode = analysisRunReportPeriod(selected); if (periodCode) { - onSelectReportPeriod(periodCode); + onSelectReportPeriod( + periodCode, + analysisRunReportGrouping(selected) ?? undefined, + analysisRunReportGroupingKey(selected), + selected.scope_entity_name, + ); document.getElementById("report-period")?.focus(); } }} @@ -2061,14 +2093,23 @@ function ReportsPanel({ onSelectPost, period, onSelectPeriod, + grouping, + onSelectGrouping, + openedGroupingKey, + openedGroupingLabel, + onOpenGrouping, }: { accessToken: string; canRebuild: boolean; onSelectPost: (postId: string) => void; period: string; onSelectPeriod: (periodCode: string) => void; + grouping: string; + onSelectGrouping: (groupingKind: string) => void; + openedGroupingKey?: string | null; + openedGroupingLabel?: string | null; + onOpenGrouping?: (groupingKey: string, groupingLabel: string) => void; }) { - const [grouping, setGrouping] = useState("process_unit"); const [payload, setPayload] = useState(null); const [index, setIndex] = useState(null); const [comparison, setComparison] = useState(null); @@ -2081,6 +2122,16 @@ function ReportsPanel({ thread_group: "Thread group", }; + function groupingIsOpened(groupingKind: string, groupingKey: string, groupingLabel?: string) { + if (groupingKind !== grouping) { + return false; + } + if (openedGroupingKey && groupingKey === openedGroupingKey) { + return true; + } + return Boolean(openedGroupingLabel && groupingLabel && groupingLabel === openedGroupingLabel); + } + useEffect(() => { setError(null); Promise.all([ @@ -2129,7 +2180,7 @@ function ReportsPanel({