Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ·
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.d/0.87.3-terminal-run-next-action.md
Original file line number Diff line number Diff line change
@@ -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).
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 7 additions & 1 deletion docs/adr/0014-authorized-analysis-run-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.87.0",
"version": "0.87.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
138 changes: 117 additions & 21 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
Expand All @@ -87,6 +93,30 @@ describe("App, authenticated", () => {
let nextEventId = 1;
let createdPendingLineage: Record<string, unknown> | 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";
Expand Down Expand Up @@ -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: [
Expand All @@ -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: [
Expand All @@ -411,7 +431,7 @@ describe("App, authenticated", () => {
},
],
},
...(options?.failedReportRun
...(includeReportRow
? [
{
analysis_run_id: "run-demo-report",
Expand All @@ -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: [
Expand Down Expand Up @@ -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.",
Expand All @@ -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");
Expand Down Expand Up @@ -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(<App />);

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(<App />);

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(<App />);
Expand All @@ -1782,11 +1873,16 @@ describe("App, authenticated", () => {
stubBackend({ succeededTeppRun: true });
render(<App />);

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();
Expand Down
48 changes: 42 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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":
Expand All @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
"sentence_excerpts",
]

__version__ = "0.87.0"
__version__ = "0.87.3"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down