diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index eba7dd90b..e00b8c8bd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -463,7 +463,9 @@ in SQL: the requester always sees their own run; a corporate-entity or 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 payload is lookup labels plus non-negative aggregate counts -- never +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 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". diff --git a/CHANGELOG.d/0.80.0-analysis-run-detail-click.md b/CHANGELOG.d/0.80.0-analysis-run-detail-click.md new file mode 100644 index 000000000..090ebb824 --- /dev/null +++ b/CHANGELOG.d/0.80.0-analysis-run-detail-click.md @@ -0,0 +1,5 @@ +# 0.80.0 analysis-run detail click + +Home Analysis runs rows open `GET /api/analysis-runs/{id}`. The +detail shows labeled aggregates and dates only. Hidden runs stay +404 / "not visible". Synthetic Demo Corp seed only. diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a74a4ba..27f7b3ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.80.0] - 2026-08-16 + +### Added + +- Home Analysis runs rows are buttons. Clicking the seeded Demo Corp + lineage run opens `GET /api/analysis-runs/{id}` and shows cutoff, + requested date, and document count. A hidden run is "This analysis + run is not visible." -- never a raw 404 or a DSN. Still synthetic + aggregates only. + ## [0.79.0] - 2026-08-16 ### Added diff --git a/frontend/package.json b/frontend/package.json index cde22610e..ca3a1810e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.79.0", + "version": "0.80.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index f7e89a3d3..ba1285c40 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -169,6 +169,29 @@ describe("App, authenticated", () => { jsonResponse({ post_id: "post-1", has_commitment: true, ticket }), ); } + if (url.endsWith("/api/analysis-runs/run-demo-lineage")) { + return Promise.resolve( + jsonResponse({ + analysis_run_id: "run-demo-lineage", + run_kind_code: "analysis_run_lineage", + run_kind_label: "Lineage reconstruction", + 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", + knowledge_cutoff: "2026-01-12T12:00:00Z", + requested_at: "2026-01-12T12:30:00Z", + source_counts: [ + { + count_type_code: "analysis_count_document", + count_type_label: "Documents", + count_value: 3, + }, + ], + }), + ); + } if (url.endsWith("/api/analysis-runs")) { return Promise.resolve( jsonResponse({ @@ -1333,6 +1356,16 @@ describe("App, authenticated", () => { expect(list).toHaveTextContent("3 documents"); expect(list).not.toHaveTextContent("postgresql://"); expect(list).not.toHaveTextContent("select "); + + await userEvent.click( + screen.getByRole("button", { + name: "Open analysis run: Lineage reconstruction · Succeeded · Demo Corp", + }), + ); + 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(); + expect(screen.queryByText(/postgresql:\/\//)).not.toBeInTheDocument(); }); it("shows the calibrated period-report mean theta on the home page", async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 67cd99a9d..8568e9acf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import { deriveCommitment, evaluatePost, extractPostKeymen, + fetchAnalysisRun, fetchAnalysisRuns, fetchCalendar, fetchLineageGraph, @@ -1346,8 +1347,15 @@ function PostDetailPopup({ ); } +function analysisRunCaption(run: AnalysisRun): string { + return [run.run_kind_label, run.status_label, run.scope_entity_name ?? run.scope_kind_label] + .filter(Boolean) + .join(" · "); +} + function AnalysisRunsPanel({ accessToken }: { accessToken: string }) { const [runs, setRuns] = useState(null); + const [selected, setSelected] = useState(null); const [error, setError] = useState(null); useEffect(() => { @@ -1356,7 +1364,21 @@ function AnalysisRunsPanel({ accessToken }: { accessToken: string }) { .catch((err) => setError(String(err))); }, [accessToken]); - if (error) return

{error}

; + async function handleOpen(runId: string) { + setError(null); + try { + setSelected(await fetchAnalysisRun(accessToken, runId)); + } catch (err) { + setSelected(null); + if (err instanceof BackendError && err.status === 404) { + setError("This analysis run is not visible."); + return; + } + setError(String(err)); + } + } + + if (error && runs === null) return

{error}

; if (runs === null) return

Loading analysis runs...

; return ( @@ -1364,6 +1386,7 @@ function AnalysisRunsPanel({ accessToken }: { accessToken: string }) {

Analysis runs

+ {error &&

{error}

} {runs.length === 0 ? (

No analysis runs visible to this account yet -- try `make seed`. @@ -1374,26 +1397,43 @@ function AnalysisRunsPanel({ accessToken }: { accessToken: string }) { const documentCount = run.source_counts.find( (count) => count.count_type_code === "analysis_count_document", ); - const caption = [ - run.run_kind_label, - run.status_label, - run.scope_entity_name ?? run.scope_kind_label, - ] - .filter(Boolean) - .join(" · "); + const caption = analysisRunCaption(run); return (

  • - {caption} - {documentCount && ( - - {documentCount.count_value} {documentCount.count_type_label.toLowerCase()} - - )} +
  • ); })} )} + {selected && ( +
    +

    {analysisRunCaption(selected)}

    +

    + Cutoff {selected.knowledge_cutoff.slice(0, 10)} + {" · "} + Requested {selected.requested_at.slice(0, 10)} +

    +
      + {selected.source_counts.map((count) => ( +
    • + {count.count_value} {count.count_type_label.toLowerCase()} +
    • + ))} +
    +
    + )} ); } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 91e9f65e4..bc1c39e6d 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -516,3 +516,7 @@ export interface AnalysisRun { export function fetchAnalysisRuns(accessToken: string): Promise<{ analysis_runs: AnalysisRun[] }> { return backendFetch("/api/analysis-runs", accessToken); } + +export function fetchAnalysisRun(accessToken: string, analysisRunId: string): Promise { + return backendFetch(`/api/analysis-runs/${analysisRunId}`, accessToken); +} diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index a8f40a3cc..7b561a3ab 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "0.79.0" +__version__ = "0.80.0" diff --git a/pyproject.toml b/pyproject.toml index 9f9ed8537..57a3973ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "0.79.0" +version = "0.80.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" } diff --git a/uv.lock b/uv.lock index 6d9094f6a..2c009d7e1 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "0.79.0" +version = "0.80.0" source = { virtual = "." } dependencies = [ { name = "certifi" },