Skip to content
Merged
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: 6 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,14 @@ process-unit scope is visible only to affiliated accounts; a
thread-group scope is visible only when the account can already see a
post in that group; `all_visible` is requester-only. Hidden runs 404.
The home list is clickable: `GET /api/analysis-runs/{id}` fills a
labeled detail (cutoff, requested date, counts) without exposing a
DSN or raw record. The payload is lookup labels plus non-negative aggregate counts -- never
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. 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 ·
Demo Corp" with "3 documents".
Demo Corp" with "3 documents" and Pending / Running / Succeeded times.

## Phase 6a: fast-mlsirm dependency + Rust toolchain (infra only)

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.d/0.81.0-analysis-run-status-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 0.81.0 analysis-run status history

Detail of `GET /api/analysis-runs/{id}` shows the labeled append-only
lifecycle. The list stays latest-status only. Hidden runs 404.
Synthetic Demo Corp seed only.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.81.0] - 2026-08-16

### Added

- Analysis-run detail shows the labeled lifecycle: Pending, Running,
then Succeeded, with occurrence times from `analysis_run_status_event`.
The list stays latest-status only. Hidden runs still 404 and never
leak events. Failure codes stay machine tokens -- no invented label.
Synthetic Demo Corp seed only.

## [0.80.0] - 2026-08-16

### Added
Expand Down
30 changes: 30 additions & 0 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,35 @@ async def _counts_by_run(
return grouped


async def _status_history(
conn: asyncpg.Connection,
analysis_run_id: str,
) -> list[dict[str, Any]]:
"""Labeled append-only lifecycle for one already-visible run."""
rows = await conn.fetch(
"""
select status_ordinal, status_code, occurred_at, failure_code
from analysis_run_status_event
where analysis_run_id = $1::uuid
order by status_ordinal
""",
analysis_run_id,
)
labels = await labels_for_codes(conn, [row["status_code"] for row in rows])
history: list[dict[str, Any]] = []
for row in rows:
item: dict[str, Any] = {
"status_ordinal": int(row["status_ordinal"]),
"status_code": row["status_code"],
"status_label": labels.get(row["status_code"], row["status_code"]),
"occurred_at": _iso(row["occurred_at"]),
}
if row["failure_code"]:
item["failure_code"] = row["failure_code"]
history.append(item)
return history


async def _serialize_runs(
conn: asyncpg.Connection,
rows: list[asyncpg.Record],
Expand Down Expand Up @@ -187,4 +216,5 @@ async def fetch_visible_analysis_run(
detail["code_revision_sha"] = row["code_revision_sha"]
if row["failure_code"]:
detail["failure_code"] = row["failure_code"]
detail["status_history"] = await _status_history(conn, analysis_run_id)
return detail
5 changes: 4 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,10 @@ async def read_analysis_run(
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""One authorized analysis-run projection, or 404 when hidden."""
"""One authorized analysis-run projection, or 404 when hidden.

Detail adds the labeled status history. Hidden runs never leak events.
"""
_require_post_read(account)
try:
UUID(analysis_run_id)
Expand Down
19 changes: 17 additions & 2 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,14 +453,29 @@ def test_analysis_runs_are_labeled_aggregates_and_hide_other_scopes(
dumped = str(visible)
assert "postgresql://" not in dumped
assert "select " not in dumped.lower()
assert "status_history" not in visible

detail = client.get(
f"/api/analysis-runs/{seeded_db['visible_run_id']}",
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert detail.status_code == 200
assert detail.json()["configuration_schema_version"] == "lineage-run-v1"
assert "snapshot_sha256" not in detail.json()
body = detail.json()
assert body["configuration_schema_version"] == "lineage-run-v1"
assert "snapshot_sha256" not in body
history = body["status_history"]
assert [event["status_label"] for event in history] == [
"Pending",
"Running",
"Succeeded",
]
assert [event["occurred_at"][:16] for event in history] == [
"2026-01-12T12:31",
"2026-01-12T12:32",
"2026-01-12T12:33",
]
assert all("failure_code" not in event for event in history)
assert "postgresql://" not in str(body)

hidden = client.get(
f"/api/analysis-runs/{seeded_db['hidden_run_id']}",
Expand Down
8 changes: 6 additions & 2 deletions docs/adr/0014-authorized-analysis-run-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ LineageWeave owns a fail-closed read projection of the #89 registry:
- The payload carries lookup labels and non-negative aggregate counts.
It does not carry source SQL, DSNs, raw records, image bytes, provider
payloads, credentials, or another service's table names.
- `GET /api/analysis-runs/{id}` also returns the append-only labeled
`status_history`. The list does not. A failed event may include the
stored machine `failure_code`; this slice does not invent a label.
- TEPP remains a versioned `AnalysisRunRequest` consumer
(`lineageweave.tepp_client`). This slice does not fork TEPP arithmetic.
- contextual-orchestrator remains the only LLM path. This slice does not
Expand All @@ -37,8 +40,9 @@ LineageWeave owns a fail-closed read projection of the #89 registry:

`make seed` writes one synthetic Demo Corp lineage run so the existing
React home page can show Analysis runs without a second application.
Write/rebuild APIs, TEPP submission, and an Analysis Run Console remain
later slices.
The detail now shows the legal lifecycle the registry already stored.
Write/rebuild APIs, TEPP submission, and a fuller Analysis Run Console
remain later slices.

## References

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.80.0",
"version": "0.81.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,26 @@ describe("App, authenticated", () => {
count_value: 3,
},
],
status_history: [
{
status_ordinal: 1,
status_code: "analysis_status_pending",
status_label: "Pending",
occurred_at: "2026-01-12T12:31:00Z",
},
{
status_ordinal: 2,
status_code: "analysis_status_running",
status_label: "Running",
occurred_at: "2026-01-12T12:32:00Z",
},
{
status_ordinal: 3,
status_code: "analysis_status_succeeded",
status_label: "Succeeded",
occurred_at: "2026-01-12T12:33:00Z",
},
],
}),
);
}
Expand Down Expand Up @@ -1365,6 +1385,10 @@ describe("App, authenticated", () => {
expect(await screen.findByRole("heading", { name: "Lineage reconstruction · Succeeded · Demo Corp" })).toBeInTheDocument();
expect(screen.getByText(/Cutoff 2026-01-12/)).toBeInTheDocument();
expect(screen.getByText(/Requested 2026-01-12/)).toBeInTheDocument();
const history = screen.getByRole("list", { name: "Analysis run status history" });
expect(history).toHaveTextContent("Pending 2026-01-12 12:31");
expect(history).toHaveTextContent("Running 2026-01-12 12:32");
expect(history).toHaveTextContent("Succeeded 2026-01-12 12:33");
expect(screen.queryByText(/postgresql:\/\//)).not.toBeInTheDocument();
});

Expand Down
10 changes: 10 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,16 @@ function AnalysisRunsPanel({ accessToken }: { accessToken: string }) {
</li>
))}
</ul>
{selected.status_history && selected.status_history.length > 0 && (
<ol aria-label="Analysis run status history">
{selected.status_history.map((event) => (
<li key={event.status_ordinal}>
{event.status_label} {event.occurred_at.slice(0, 16).replace("T", " ")}
{event.failure_code ? ` · ${event.failure_code}` : ""}
</li>
))}
</ol>
)}
</div>
)}
</section>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,14 @@ export interface AnalysisRunCount {
count_value: number;
}

export interface AnalysisRunStatusEvent {
status_ordinal: number;
status_code: string;
status_label: string;
occurred_at: string;
failure_code?: string;
}

export interface AnalysisRun {
analysis_run_id: string;
run_kind_code: string;
Expand All @@ -511,6 +519,7 @@ export interface AnalysisRun {
knowledge_cutoff: string;
requested_at: string;
source_counts: AnalysisRunCount[];
status_history?: AnalysisRunStatusEvent[];
}

export function fetchAnalysisRuns(accessToken: string): Promise<{ analysis_runs: AnalysisRun[] }> {
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.80.0"
__version__ = "0.81.0"
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.80.0"
version = "0.81.0"
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