diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36e24332b..e78d36254 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] permissions: contents: read diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b662b00b1..2492ee50d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -475,9 +475,10 @@ labeled detail (cutoff, requested date, counts, status history) without exposing a DSN or raw record. Status history is detail-only and uses lookup labels plus occurrence times; a failure event keeps its machine `failure_code` rather than an invented caption. Failed -list rows add a next-action line (open the run, then connect the +TEPP list rows add a next-action line (open the run, then connect the measurement service) so `tepp_not_available` is not mistaken for a -calibrated negative result. The +calibrated negative result. A failed lineage row tells the operator +to retry reconstruction, not to connect TEPP. 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.84.0-tepp-analysis-run.md b/CHANGELOG.d/0.84.0-tepp-analysis-run.md index 080cc8240..c96531899 100644 --- a/CHANGELOG.d/0.84.0-tepp-analysis-run.md +++ b/CHANGELOG.d/0.84.0-tepp-analysis-run.md @@ -1,6 +1,6 @@ # 0.84.0 TEPP analysis-run seed Seed writes `analysis_run_tepp` via `tepp_client` on the shared Demo -Corp snapshot. The home list shows Failed and the next action; detail -history keeps `tepp_not_available`. Missing transport is not a fake -measurement. +Corp snapshot. The home list shows Failed and a kind-specific next +action; detail history keeps `tepp_not_available`. Missing transport +is not a fake measurement. A failed lineage row does not mention TEPP. diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f4878b4..c36b2666d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ All notable changes to this project are documented here. Format follows keeps `tepp_not_available` -- never a fabricated theta. TEPP stays a wire client, not a local psychometric engine. `make seed` skips snapshot-count inserts once counts exist so a re-run does not hit - the freeze trigger. + the freeze trigger. A failed lineage row tells the operator to retry + reconstruction; only a failed TEPP row mentions the measurement + service. Stacked PRs now run the same GitHub Checks as PRs to main. ## [0.83.0] - 2026-08-16 diff --git a/CLAUDE.md b/CLAUDE.md index 3af72ad45..0a2950e91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,4 +11,6 @@ transport or an unused accepted envelope is Failed (`tepp_not_available` / `tepp_result_not_persisted`). Do not invent a theta or a local psychometric substitute. The home list caption stays `kind · status · entity`; the machine failure code is detail-only -(ADR 0014). Open the Failed row, then connect a live TEPP transport. +(ADR 0014). Open a Failed TEPP row, then connect a live TEPP +transport. A failed lineage row retries reconstruction -- it does not +mention TEPP. diff --git a/docs/adr/0014-authorized-analysis-run-read.md b/docs/adr/0014-authorized-analysis-run-read.md index dea201bfc..841dfb383 100644 --- a/docs/adr/0014-authorized-analysis-run-read.md +++ b/docs/adr/0014-authorized-analysis-run-read.md @@ -43,10 +43,11 @@ run on the same snapshot so the existing React home page can show both kinds without a second application. The TEPP run is Failed / `tepp_not_available` when the default transport is missing -- the list keeps that machine code off the caption (this decision) and instead -tells the operator to open the run, then connect the measurement -service. The detail now shows the legal lifecycle the registry already -stored. Write/rebuild APIs, a live TEPP transport, and a fuller -Analysis Run Console remain later slices. +tells the operator to open the TEPP run, then connect the measurement +service. A failed lineage row tells the operator to retry +reconstruction, not to connect TEPP. The detail now shows the legal +lifecycle the registry already stored. Write/rebuild APIs, a live TEPP +transport, and a fuller Analysis Run Console remain later slices. ## References diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index e2a30c684..c95abf160 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -59,6 +59,8 @@ describe("App, authenticated", () => { chatUnavailable?: boolean; searchUnavailable?: boolean; verificationEvidenceUrl?: string | null; + failedLineageRun?: boolean; + succeededTeppRun?: boolean; }) { const statusLabel: Record = { open: "Open", @@ -178,8 +180,10 @@ 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", - status_label: "Failed", + status_code: options?.succeededTeppRun + ? "analysis_status_succeeded" + : "analysis_status_failed", + status_label: options?.succeededTeppRun ? "Succeeded" : "Failed", knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:34:00Z", source_counts: [ @@ -205,10 +209,14 @@ describe("App, authenticated", () => { }, { status_ordinal: 3, - status_code: "analysis_status_failed", - status_label: "Failed", + status_code: options?.succeededTeppRun + ? "analysis_status_succeeded" + : "analysis_status_failed", + status_label: options?.succeededTeppRun ? "Succeeded" : "Failed", occurred_at: "2026-01-12T12:37:00Z", - failure_code: "tepp_not_available", + ...(options?.succeededTeppRun + ? {} + : { failure_code: "tepp_not_available" }), }, ], }), @@ -269,8 +277,10 @@ describe("App, authenticated", () => { scope_kind_code: "analysis_scope_corporate_entity", scope_kind_label: "Corporate entity", scope_entity_name: "Demo Corp", - status_code: "analysis_status_succeeded", - status_label: "Succeeded", + status_code: options?.failedLineageRun + ? "analysis_status_failed" + : "analysis_status_succeeded", + status_label: options?.failedLineageRun ? "Failed" : "Succeeded", knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:30:00Z", source_counts: [ @@ -288,8 +298,10 @@ 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", - status_label: "Failed", + status_code: options?.succeededTeppRun + ? "analysis_status_succeeded" + : "analysis_status_failed", + status_label: options?.succeededTeppRun ? "Succeeded" : "Failed", knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:34:00Z", source_counts: [ @@ -1479,6 +1491,39 @@ describe("App, authenticated", () => { expect(teppHistory).not.toHaveTextContent("Succeeded"); }); + it("does not tell a failed lineage run to connect the measurement service", async () => { + stubBackend({ failedLineageRun: true }); + render(); + + const list = await screen.findByRole("list", { name: "Analysis runs" }); + expect(list).toHaveTextContent("Lineage reconstruction · Failed · Demo Corp"); + expect(list).toHaveTextContent( + "Open this run to see why it failed, then retry reconstruction from a current snapshot.", + ); + expect(list).toHaveTextContent( + "Open this run to see why it failed, then connect the measurement service and re-run.", + ); + const lineageButton = screen.getByRole("button", { + name: "Open analysis run: Lineage reconstruction · Failed · Demo Corp", + }); + expect(lineageButton).not.toHaveTextContent("measurement service"); + }); + + it("does not tell a succeeded TEPP run to replace Failed", async () => { + stubBackend({ succeededTeppRun: true }); + render(); + + await userEvent.click( + await screen.findByRole("button", { + name: "Open analysis run: TEPP measurement · Succeeded · Demo Corp", + }), + ); + expect( + await screen.findByText("These posts are the cutoff corpus this TEPP run measured."), + ).toBeInTheDocument(); + expect(screen.queryByText(/replace Failed/i)).not.toBeInTheDocument(); + }); + it("shows the calibrated period-report mean theta on the home page", async () => { stubBackend(); render(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 50602c687..a5f66d7a9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1356,14 +1356,22 @@ function analysisRunCaption(run: AnalysisRun): string { /** * Next action for a failed run on the home list. * - * The machine `failure_code` stays on detail history (ADR 0014). The - * list tells the operator to open the run, then reconnect the service. + * The machine `failure_code` stays on detail history (ADR 0014). Copy + * is kind-specific so a failed lineage reconstruction is not mistaken + * for a missing TEPP transport. */ function analysisRunNextAction(run: AnalysisRun): string | null { - if (run.status_code === "analysis_status_failed") { - return "Open this run to see why it failed, then connect the measurement service and re-run."; + if (run.status_code !== "analysis_status_failed") { + return null; + } + switch (run.run_kind_code) { + case "analysis_run_tepp": + return "Open this run to see why it failed, then connect the measurement service and re-run."; + case "analysis_run_lineage": + return "Open this run to see why it failed, then retry reconstruction from a current snapshot."; + default: + return "Open this run to see why it failed, then retry after the blocking service is connected."; } - return null; } /** @@ -1389,10 +1397,13 @@ function analysisRunEmptyPostsHint(run: AnalysisRun): string { */ function analysisRunCorpusHint(run: AnalysisRun): string | null { if (run.run_kind_code !== "analysis_run_tepp") return null; - return ( - "These posts are the cutoff corpus TEPP would measure. Connect a TEPP " + - "transport, then re-run, to replace Failed with a calibrated result." - ); + if (run.status_code === "analysis_status_failed") { + return ( + "These posts are the cutoff corpus TEPP would measure. Connect a TEPP " + + "transport, then re-run, to replace Failed with a calibrated result." + ); + } + return "These posts are the cutoff corpus this TEPP run measured."; } function AnalysisRunsPanel({