From 02fb06f7c7ef9c67744492dce557f30deadac396 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:53:08 +0000 Subject: [PATCH] fix(ui): pin running, succeeded, and cancelled next actions (v0.87.3) Home analysis-run rows now tell the operator the next click for every registered status. A running TEPP row is still not a calibrated result. Co-authored-by: Seongho Bae --- ARCHITECTURE.md | 9 +- .../0.87.3-terminal-run-next-action.md | 8 + CHANGELOG.md | 12 ++ CLAUDE.md | 5 +- docs/adr/0014-authorized-analysis-run-read.md | 8 +- frontend/package.json | 2 +- frontend/src/App.test.tsx | 138 +++++++++++++++--- frontend/src/App.tsx | 48 +++++- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- 10 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 CHANGELOG.d/0.87.3-terminal-run-next-action.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bf19c5f77..2466da150 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -492,7 +492,14 @@ calibrated negative result. A failed lineage row tells the operator to retry reconstruction, not to connect TEPP. A failed period-report row tells the operator to rebuild the report. A pending TEPP row does not claim a calibrated measurement. A pending lineage row -says reconstruction has not started yet. The +says reconstruction has not started yet. A running row tells the +operator to refresh the list, then open the run when the status +changes — a running TEPP row still is not a calibrated result. A +succeeded lineage row tells the operator to open the run and compare +each live title with the cutoff. A succeeded TEPP row names the +titles as the measurement corpus, not a reconstruction. A cancelled +row tells the operator to request or rebuild from a current snapshot. +The payload is lookup labels plus non-negative aggregate counts -- never source SQL, a DSN, a raw record, or a provider body. After `make seed`, Demo Analyst and Demo Admin see "Lineage reconstruction · Succeeded · diff --git a/CHANGELOG.d/0.87.3-terminal-run-next-action.md b/CHANGELOG.d/0.87.3-terminal-run-next-action.md new file mode 100644 index 000000000..e0721e4d4 --- /dev/null +++ b/CHANGELOG.d/0.87.3-terminal-run-next-action.md @@ -0,0 +1,8 @@ +# 0.87.3 running, succeeded, and cancelled next actions + +Refresh a running analysis-run row, then open it when the status +changes. Open a succeeded lineage run and compare each live title +with the cutoff before treating the body as reconstructed evidence. +A succeeded TEPP title list is the measurement corpus, not a +reconstruction. After cancel, request or rebuild from a current +snapshot. A running TEPP row is not a calibrated result (ADR 0014). diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a19fe92..b6997392e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ 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.87.3] - 2026-08-16 + +### Fixed + +- Running, succeeded, and cancelled analysis-run rows now tell the + operator the next click. Refresh a running row; open a succeeded + lineage run and compare each live title with the cutoff; treat a + succeeded TEPP title list as the measurement corpus, not a + reconstruction; request or rebuild after cancel. Copy stays pinned + to registered kinds so a running TEPP row is not a calibrated + result (ADR 0014). + ## [0.87.0] - 2026-08-16 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 870c77f87..0c1b4a21f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,10 @@ theta or a local psychometric substitute. The home list caption stays transport. A failed lineage row retries reconstruction -- it does not mention TEPP. A failed period-report row rebuilds the report. A pending TEPP row does not claim a calibrated measurement. A pending -lineage row says reconstruction has not started yet. +lineage row says reconstruction has not started yet. A running row +says refresh the list; a running TEPP row is not a calibrated result. +A succeeded lineage row compares live titles with the cutoff. A +cancelled row requests or rebuilds from a current snapshot. Digest prefixes stay audible; hover a prefix to read the full digest. Opening a cutoff title shows the live post -- compare it with the cutoff before treating the body as reconstructed evidence (ADR 0016). diff --git a/docs/adr/0014-authorized-analysis-run-read.md b/docs/adr/0014-authorized-analysis-run-read.md index 500c2bc2a..64141d42c 100644 --- a/docs/adr/0014-authorized-analysis-run-read.md +++ b/docs/adr/0014-authorized-analysis-run-read.md @@ -49,7 +49,13 @@ reconstruction, not to connect TEPP. A failed period-report row tells the operator to rebuild the report from a current snapshot. A pending or running TEPP row must not claim a calibrated measurement. A pending lineage row says reconstruction has not -started yet. The detail now shows the legal +started yet. A running row tells the operator to refresh the list, +then open the run when the status changes. A succeeded lineage row +tells the operator to open the run and compare each live title with +the cutoff. A succeeded TEPP row names the titles as the measurement +corpus, not a reconstruction. A cancelled row tells the operator to +request or rebuild from a current snapshot, still kind-specific. +The detail now shows the legal lifecycle the registry already stored. `POST /api/analysis-runs` now records a Pending run on an authorized cutoff capture (ADR 0017). Reconstruction, a live TEPP transport, and a fuller Analysis Run diff --git a/frontend/package.json b/frontend/package.json index 0d43d9fa2..ad8163077 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.87.0", + "version": "0.87.3", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index fd8a15146..fd41fc2ff 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -60,9 +60,15 @@ describe("App, authenticated", () => { searchUnavailable?: boolean; verificationEvidenceUrl?: string | null; failedLineageRun?: boolean; + runningLineageRun?: boolean; + cancelledLineageRun?: boolean; failedReportRun?: boolean; + runningReportRun?: boolean; + cancelledReportRun?: boolean; succeededTeppRun?: boolean; pendingTeppRun?: boolean; + runningTeppRun?: boolean; + cancelledTeppRun?: boolean; postBody?: string; }) { const statusLabel: Record = { @@ -87,6 +93,30 @@ describe("App, authenticated", () => { let nextEventId = 1; let createdPendingLineage: Record | null = null; + const lineageListStatus = options?.failedLineageRun + ? { status_code: "analysis_status_failed", status_label: "Failed" } + : options?.runningLineageRun + ? { status_code: "analysis_status_running", status_label: "Running" } + : options?.cancelledLineageRun + ? { status_code: "analysis_status_cancelled", status_label: "Cancelled" } + : { status_code: "analysis_status_succeeded", status_label: "Succeeded" }; + const teppListStatus = options?.succeededTeppRun + ? { status_code: "analysis_status_succeeded", status_label: "Succeeded" } + : options?.pendingTeppRun + ? { status_code: "analysis_status_pending", status_label: "Pending" } + : options?.runningTeppRun + ? { status_code: "analysis_status_running", status_label: "Running" } + : options?.cancelledTeppRun + ? { status_code: "analysis_status_cancelled", status_label: "Cancelled" } + : { status_code: "analysis_status_failed", status_label: "Failed" }; + const reportListStatus = options?.runningReportRun + ? { status_code: "analysis_status_running" as const, status_label: "Running" } + : options?.cancelledReportRun + ? { status_code: "analysis_status_cancelled" as const, status_label: "Cancelled" } + : { status_code: "analysis_status_failed" as const, status_label: "Failed" }; + const includeReportRow = + options?.failedReportRun || options?.runningReportRun || options?.cancelledReportRun; + const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); const method = init?.method ?? "GET"; @@ -367,10 +397,8 @@ describe("App, authenticated", () => { scope_kind_code: "analysis_scope_corporate_entity", scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", - status_code: options?.failedLineageRun - ? "analysis_status_failed" - : "analysis_status_succeeded", - status_label: options?.failedLineageRun ? "Failed" : "Succeeded", + status_code: lineageListStatus.status_code, + status_label: lineageListStatus.status_label, knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:30:00Z", source_counts: [ @@ -391,16 +419,8 @@ describe("App, authenticated", () => { scope_kind_code: "analysis_scope_corporate_entity", scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", - status_code: options?.succeededTeppRun - ? "analysis_status_succeeded" - : options?.pendingTeppRun - ? "analysis_status_pending" - : "analysis_status_failed", - status_label: options?.succeededTeppRun - ? "Succeeded" - : options?.pendingTeppRun - ? "Pending" - : "Failed", + status_code: teppListStatus.status_code, + status_label: teppListStatus.status_label, knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:34:00Z", source_counts: [ @@ -411,7 +431,7 @@ describe("App, authenticated", () => { }, ], }, - ...(options?.failedReportRun + ...(includeReportRow ? [ { analysis_run_id: "run-demo-report", @@ -420,8 +440,8 @@ describe("App, authenticated", () => { scope_kind_code: "analysis_scope_corporate_entity", scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", - status_code: "analysis_status_failed" as const, - status_label: "Failed", + status_code: reportListStatus.status_code, + status_label: reportListStatus.status_label, knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:38:00Z", source_counts: [ @@ -1647,6 +1667,9 @@ describe("App, authenticated", () => { expect(await screen.findByRole("heading", { name: "Analysis runs" })).toBeInTheDocument(); const list = screen.getByRole("list", { name: "Analysis runs" }); expect(list).toHaveTextContent("Lineage reconstruction · Succeeded · Demo Corp"); + expect(list).toHaveTextContent( + "Open this run, then compare each live title with the cutoff before treating the body as reconstructed evidence.", + ); expect(list).toHaveTextContent("TEPP measurement · Failed · Demo Corp"); expect(list).toHaveTextContent( "Open this run to see why it failed, then connect the measurement service and re-run.", @@ -1667,6 +1690,11 @@ describe("App, authenticated", () => { }), ); expect(await screen.findByRole("heading", { name: "Lineage reconstruction · Succeeded · Demo Corp" })).toBeInTheDocument(); + expect( + screen.getAllByText( + "Open this run, then compare each live title with the cutoff before treating the body as reconstructed evidence.", + ).length, + ).toBeGreaterThanOrEqual(2); expect(screen.getByText(/Cutoff 2026-01-12/)).toBeInTheDocument(); expect(screen.getByText(/Requested 2026-01-12/)).toBeInTheDocument(); const digests = screen.getByLabelText("Analysis run reproducibility digests"); @@ -1761,6 +1789,69 @@ describe("App, authenticated", () => { ).toBeInTheDocument(); }); + it("does not tell a running lineage run to connect the measurement service", async () => { + stubBackend({ runningLineageRun: true, runningTeppRun: true, runningReportRun: true }); + render(); + + await screen.findByRole("list", { name: "Analysis runs" }); + const lineageButton = screen.getByRole("button", { + name: "Open analysis run: Lineage reconstruction · Running · Demo Corp", + }); + const teppButton = screen.getByRole("button", { + name: "Open analysis run: TEPP measurement · Running · Demo Corp", + }); + const reportButton = screen.getByRole("button", { + name: "Open analysis run: Period report · Running · Demo Corp", + }); + expect(lineageButton).toHaveTextContent( + "This reconstruction is still running. Refresh the list, then open the run when the status changes.", + ); + expect(lineageButton).not.toHaveTextContent("measurement"); + expect(lineageButton).not.toHaveTextContent("calibrated"); + expect(teppButton).toHaveTextContent( + "This measurement is still running. Refresh the list, then open the run when the status changes. This is not a calibrated result yet.", + ); + expect(teppButton).not.toHaveTextContent("reconstruction"); + expect(reportButton).toHaveTextContent( + "This period report is still building. Refresh the list, then open the run when the status changes.", + ); + expect(reportButton).not.toHaveTextContent("measurement service"); + expect(reportButton).not.toHaveTextContent("reconstruction"); + }); + + it("does not tell a cancelled lineage run to connect the measurement service", async () => { + stubBackend({ + cancelledLineageRun: true, + cancelledTeppRun: true, + cancelledReportRun: true, + }); + render(); + + await screen.findByRole("list", { name: "Analysis runs" }); + const lineageButton = screen.getByRole("button", { + name: "Open analysis run: Lineage reconstruction · Cancelled · Demo Corp", + }); + const teppButton = screen.getByRole("button", { + name: "Open analysis run: TEPP measurement · Cancelled · Demo Corp", + }); + const reportButton = screen.getByRole("button", { + name: "Open analysis run: Period report · Cancelled · Demo Corp", + }); + expect(lineageButton).toHaveTextContent( + "This reconstruction was cancelled. Request a new lineage reconstruction from a current snapshot.", + ); + expect(lineageButton).not.toHaveTextContent("measurement service"); + expect(teppButton).toHaveTextContent( + "This measurement was cancelled before a calibrated result. Connect the measurement service, then request a new run.", + ); + expect(teppButton).not.toHaveTextContent("reconstruction"); + expect(reportButton).toHaveTextContent( + "This period report was cancelled. Rebuild the report from a current snapshot.", + ); + expect(reportButton).not.toHaveTextContent("measurement service"); + expect(reportButton).not.toHaveTextContent("reconstruction"); + }); + it("does not tell a pending TEPP run that it already measured", async () => { stubBackend({ pendingTeppRun: true }); render(); @@ -1782,11 +1873,16 @@ describe("App, authenticated", () => { stubBackend({ succeededTeppRun: true }); render(); - await userEvent.click( - await screen.findByRole("button", { - name: "Open analysis run: TEPP measurement · Succeeded · Demo Corp", - }), + const teppButton = await screen.findByRole("button", { + name: "Open analysis run: TEPP measurement · Succeeded · Demo Corp", + }); + expect(teppButton).toHaveTextContent( + "Open this run to see which posts this measurement used. The titles are the corpus, not a reconstruction.", ); + expect(teppButton).not.toHaveTextContent("replace Failed"); + expect(teppButton).not.toHaveTextContent("Reconstruction has not started yet"); + + await userEvent.click(teppButton); expect( await screen.findByText("These posts are the cutoff corpus this TEPP run measured."), ).toBeInTheDocument(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 07088e9d4..199569c73 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1447,12 +1447,12 @@ function analysisRunCaption(run: AnalysisRun): string { } /** - * Next action for a pending or failed run on the home list and detail. + * Next action for every registered run status on the home list and detail. * * The machine `failure_code` stays on detail history (ADR 0014). Copy - * is pinned to registered kinds so a pending TEPP row is not mistaken - * for reconstruction, and a failed lineage row is not mistaken for a - * missing TEPP transport. + * is pinned to registered kinds so a pending or running TEPP row is not + * mistaken for reconstruction or a calibrated result, and a failed or + * cancelled lineage row is not mistaken for a missing TEPP transport. */ function analysisRunNextAction(run: AnalysisRun): string | null { switch (run.status_code) { @@ -1469,6 +1469,32 @@ function analysisRunNextAction(run: AnalysisRun): string | null { return unexpected; } } + case "analysis_status_running": + switch (run.run_kind_code) { + case "analysis_run_lineage": + return "This reconstruction is still running. Refresh the list, then open the run when the status changes."; + case "analysis_run_tepp": + return "This measurement is still running. Refresh the list, then open the run when the status changes. This is not a calibrated result yet."; + case "analysis_run_report": + return "This period report is still building. Refresh the list, then open the run when the status changes."; + default: { + const unexpected: never = run.run_kind_code; + return unexpected; + } + } + case "analysis_status_succeeded": + switch (run.run_kind_code) { + case "analysis_run_lineage": + return "Open this run, then compare each live title with the cutoff before treating the body as reconstructed evidence."; + case "analysis_run_tepp": + return "Open this run to see which posts this measurement used. The titles are the corpus, not a reconstruction."; + case "analysis_run_report": + return "Open this run to see which posts the period report used, then rebuild if you need a newer cutoff."; + default: { + const unexpected: never = run.run_kind_code; + return unexpected; + } + } case "analysis_status_failed": switch (run.run_kind_code) { case "analysis_run_tepp": @@ -1482,9 +1508,19 @@ function analysisRunNextAction(run: AnalysisRun): string | null { return unexpected; } } - case "analysis_status_running": - case "analysis_status_succeeded": case "analysis_status_cancelled": + switch (run.run_kind_code) { + case "analysis_run_lineage": + return "This reconstruction was cancelled. Request a new lineage reconstruction from a current snapshot."; + case "analysis_run_tepp": + return "This measurement was cancelled before a calibrated result. Connect the measurement service, then request a new run."; + case "analysis_run_report": + return "This period report was cancelled. Rebuild the report from a current snapshot."; + default: { + const unexpected: never = run.run_kind_code; + return unexpected; + } + } case null: return null; default: { diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index 1950c39f8..9d1b2667d 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "0.87.0" +__version__ = "0.87.3" diff --git a/pyproject.toml b/pyproject.toml index ecfe24877..7913e402c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "0.87.0" +version = "0.87.3" description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication." readme = "README.md" license = { text = "MIT" }