From b8ea6151d3b51518d26071f4d842f1ea78edffb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 07:22:55 +0900 Subject: [PATCH 1/2] feat: open the scored week from a period-report analysis run (v0.97.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After make seed, open Period report · Succeeded · Demo Corp and click Open period report 2026-W02. Mean θ stays on the report panel. No TEPP theta is invented. --- .../0.97.0-analysis-run-open-period-report.md | 4 ++ CHANGELOG.md | 9 +++ backend/app/analysis_run_ingestion.py | 2 + .../0024-seed-period-report-analysis-run.md | 6 +- frontend/package.json | 2 +- frontend/src/App.test.tsx | 59 ++++++++++-------- frontend/src/App.tsx | 62 +++++++++++++++++-- frontend/src/api.ts | 1 + lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- scripts/seed_demo_data.py | 9 +-- uv.lock | 2 +- 12 files changed, 119 insertions(+), 41 deletions(-) create mode 100644 CHANGELOG.d/0.97.0-analysis-run-open-period-report.md diff --git a/CHANGELOG.d/0.97.0-analysis-run-open-period-report.md b/CHANGELOG.d/0.97.0-analysis-run-open-period-report.md new file mode 100644 index 000000000..78f685dcb --- /dev/null +++ b/CHANGELOG.d/0.97.0-analysis-run-open-period-report.md @@ -0,0 +1,4 @@ +# 0.97.0 Open the seeded period report from its analysis run + +Open Period report · Succeeded · Demo Corp, then Open period report +2026-W02. Mean θ stays on the report panel. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4134cc94d..e351731e0 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.97.0] - 2026-08-17 + +### Added + +- A Succeeded period-report analysis run now opens the scored week. + After `make seed`, open **Period report · Succeeded · Demo Corp** + and click **Open period report 2026-W02**. Mean θ stays on the + report panel. No TEPP theta is invented. + ## [0.96.0] - 2026-08-17 ### Added diff --git a/backend/app/analysis_run_ingestion.py b/backend/app/analysis_run_ingestion.py index fe27c9e75..f9108b9c6 100644 --- a/backend/app/analysis_run_ingestion.py +++ b/backend/app/analysis_run_ingestion.py @@ -251,6 +251,8 @@ async def _serialize_runs( } if row["scope_entity_name"]: item["scope_entity_name"] = row["scope_entity_name"] + if row["scope_key"]: + item["scope_key"] = row["scope_key"] payload.append(item) return payload diff --git a/docs/adr/0024-seed-period-report-analysis-run.md b/docs/adr/0024-seed-period-report-analysis-run.md index 9ee8f90b7..de8bf3240 100644 --- a/docs/adr/0024-seed-period-report-analysis-run.md +++ b/docs/adr/0024-seed-period-report-analysis-run.md @@ -29,7 +29,8 @@ on a path that is not allowed to (ADR 0021 / ADR 0022 / ADR 0023). - `_seed_demo_period_report` still builds the calibrated report first. - `_seed_demo_report_run` then inserts `analysis_run_report` on the - same Demo Corp snapshot, scoped to the same corporate entity. + same Demo Corp snapshot, scoped to the same corporate entity, with + `scope_key` `2026-W02` so the home detail can open that week. - The lifecycle is Pending → Running → Succeeded because the report tables already hold the scored period. The run row stores only registry digests and counts — never a theta, item bank, or provider @@ -45,7 +46,8 @@ 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. Mean θ remains on the +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 `demo-report-seed-2026-w02`. diff --git a/frontend/package.json b/frontend/package.json index 85873e815..d62e46766 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.96.0", + "version": "0.97.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 49c9e7869..5842360af 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -179,7 +179,7 @@ describe("App, authenticated", () => { ); } if (url.endsWith("/api/analysis-runs/run-demo-report")) { - const reportSucceeded = Boolean(options?.succeededReportRun); + const reportSucceeded = !options?.failedReportRun; return Promise.resolve( jsonResponse({ analysis_run_id: "run-demo-report", @@ -188,6 +188,7 @@ describe("App, authenticated", () => { scope_kind_code: "analysis_scope_corporate_entity", scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", + scope_key: "2026-W02", status_code: reportSucceeded ? "analysis_status_succeeded" : "analysis_status_failed", status_label: reportSucceeded ? "Succeeded" : "Failed", knowledge_cutoff: "2026-01-12T12:00:00Z", @@ -665,31 +666,28 @@ describe("App, authenticated", () => { }, ], }, - ...(options?.failedReportRun || options?.succeededReportRun - ? [ - { - analysis_run_id: "run-demo-report", - run_kind_code: "analysis_run_report" as const, - run_kind_label: "Period report", - scope_kind_code: "analysis_scope_corporate_entity", - scope_kind_label: "Corporate entity", - scope_entity_name: "Demo Corp", - status_code: options?.succeededReportRun - ? ("analysis_status_succeeded" as const) - : ("analysis_status_failed" as const), - status_label: options?.succeededReportRun ? "Succeeded" : "Failed", - knowledge_cutoff: "2026-01-12T12:00:00Z", - requested_at: "2026-01-12T12:38:00Z", - source_counts: [ - { - count_type_code: "analysis_count_document", - count_type_label: "Documents", - count_value: 3, - }, - ], - }, - ] - : []), + { + analysis_run_id: "run-demo-report", + run_kind_code: "analysis_run_report" as const, + run_kind_label: "Period report", + scope_kind_code: "analysis_scope_corporate_entity", + scope_kind_label: "Corporate entity", + scope_entity_name: "Demo Corp", + scope_key: "2026-W02", + status_code: options?.failedReportRun + ? ("analysis_status_failed" as const) + : ("analysis_status_succeeded" as const), + status_label: options?.failedReportRun ? "Failed" : "Succeeded", + knowledge_cutoff: "2026-01-12T12:00:00Z", + requested_at: "2026-01-12T12:38:00Z", + source_counts: [ + { + count_type_code: "analysis_count_document", + count_type_label: "Documents", + count_value: 3, + }, + ], + }, ], }), ); @@ -1904,6 +1902,7 @@ describe("App, authenticated", () => { const list = screen.getByRole("list", { name: "Analysis runs" }); expect(list).toHaveTextContent("Lineage reconstruction · Succeeded · Demo Corp"); expect(list).toHaveTextContent("TEPP measurement · Failed · Demo Corp"); + expect(list).toHaveTextContent("Period report · Succeeded · Demo Corp"); expect(list).toHaveTextContent( "Open this run to see why it failed, then connect the measurement service and re-run.", ); @@ -2114,6 +2113,14 @@ describe("App, authenticated", () => { name: "Open live post: Public post", }), ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Open period report 2026-W02" })).toBeInTheDocument(); + + const periodInput = screen.getByLabelText("Report period"); + await userEvent.clear(periodInput); + await userEvent.type(periodInput, "2026-W03"); + expect(periodInput).toHaveValue("2026-W03"); + await userEvent.click(screen.getByRole("button", { name: "Open period report 2026-W02" })); + expect(periodInput).toHaveValue("2026-W02"); }); 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 6bf05c4b0..59c2d500f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1681,6 +1681,28 @@ function analysisRunCanRequestTeppRetry(run: AnalysisRun): boolean { return run.run_kind_code === "analysis_run_tepp" && run.status_code === "analysis_status_failed"; } +const REPORT_PERIOD_KEY = /^\d{4}-W\d{2}$/; + +/** + * Period code stored on a succeeded report run's scope key. + * + * That key is a week label, not a theta. Missing or malformed keys + * stay closed so we do not invent a period. + */ +function analysisRunReportPeriod(run: AnalysisRun): string | null { + if (run.run_kind_code !== "analysis_run_report") { + return null; + } + if (run.status_code !== "analysis_status_succeeded") { + return null; + } + const key = run.scope_key; + if (!key || !REPORT_PERIOD_KEY.test(key)) { + return null; + } + return key; +} + /** * Open options for a reconstructed parent or child. * @@ -1699,9 +1721,11 @@ function analysisRunPostOpenOptions(run: AnalysisRun, postId: string): SelectPos function AnalysisRunsPanel({ accessToken, onSelectPost, + onSelectReportPeriod, }: { accessToken: string; onSelectPost: (postId: string, options?: SelectPostOptions) => void; + onSelectReportPeriod?: (periodCode: string) => void; }) { const [runs, setRuns] = useState(null); const [selected, setSelected] = useState(null); @@ -1872,6 +1896,20 @@ function AnalysisRunsPanel({ {requesting ? "Recording the run..." : "Request a new TEPP measurement"} )} + {analysisRunReportPeriod(selected) && onSelectReportPeriod && ( + + )} {selected.reconstructed_edges && selected.reconstructed_edges.length > 0 && (
    {selected.reconstructed_edges.map((edge) => ( @@ -2020,13 +2058,16 @@ function ReportsPanel({ accessToken, canRebuild, onSelectPost, + period, + onSelectPeriod, }: { accessToken: string; canRebuild: boolean; onSelectPost: (postId: string) => void; + period: string; + onSelectPeriod: (periodCode: string) => void; }) { const [grouping, setGrouping] = useState("process_unit"); - const [period, setPeriod] = useState("2026-W02"); const [payload, setPayload] = useState(null); const [index, setIndex] = useState(null); const [comparison, setComparison] = useState(null); @@ -2098,7 +2139,7 @@ function ReportsPanel({ setPeriod(event.target.value)} + onChange={(event) => onSelectPeriod(event.target.value)} /> @@ -2128,7 +2169,7 @@ function ReportsPanel({