diff --git a/AGENTS.md b/AGENTS.md index dba1c4b42..f81781eaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,9 @@ Cross-agent conventions for `LineageWeave`, readable by any coding agent A demo BI prototype that reconstructs git-branch-style lineage between scattered short records. See [ARCHITECTURE.md](ARCHITECTURE.md) for the design and [`docs/lineage-bi-research-notes.md`](docs/lineage-bi-research-notes.md) -for the literature it is grounded in. +for the literature it is grounded in. After `make seed`, Analysis runs +lists lineage, TEPP, and the built period-report registry row on one +Demo Corp snapshot (ADR 0022). Mean θ stays on the period-report panel. ## Hard rule: no real data, ever diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b66c7cde7..b658474ce 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -491,13 +491,17 @@ measurement service) so `tepp_not_available` is not mistaken for a 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. The +does not claim a calibrated measurement. A pending lineage row +says reconstruction has not started yet. 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" and Pending / Running / Succeeded times, and "TEPP measurement · Failed · Demo Corp" whose detail history ends -in Failed / `tepp_not_available`. +in Failed / `tepp_not_available`. Seed also records +"Period report · Succeeded · Demo Corp" on that same snapshot after +the calibrated report tables are written (ADR 0022). Open that row +to confirm the cutoff posts; mean θ stays on the period-report panel. ## Phase 6a: fast-mlsirm dependency + Rust toolchain (infra only) diff --git a/CHANGELOG.d/0.86.0-related-nodes-team-org-walk.md b/CHANGELOG.d/0.86.0-related-nodes-team-org-walk.md index 4989a054f..6e4605d5a 100644 --- a/CHANGELOG.d/0.86.0-related-nodes-team-org-walk.md +++ b/CHANGELOG.d/0.86.0-related-nodes-team-org-walk.md @@ -1,4 +1,5 @@ Related-node walks include team and organization mention edges. Click an R&R team to open sibling posts. Thread-group run lists honor knowledge_cutoff. Failed period-report rows rebuild the report; a pending TEPP corpus -does not claim a calibrated measurement. +does not claim a calibrated measurement. A pending lineage row says +reconstruction has not started yet. diff --git a/CHANGELOG.d/0.86.4-seed-period-report-run.md b/CHANGELOG.d/0.86.4-seed-period-report-run.md new file mode 100644 index 000000000..4b1066525 --- /dev/null +++ b/CHANGELOG.d/0.86.4-seed-period-report-run.md @@ -0,0 +1,3 @@ +Seed records the built Demo Corp period report as a Succeeded +analysis-run registry row on the shared snapshot (ADR 0022). Mean θ +stays on the period-report panel. diff --git a/CHANGELOG.md b/CHANGELOG.md index 13aeb02f9..8ede97be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ 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.86.4] - 2026-08-16 + +### Added + +- `make seed` now records the already-built Demo Corp period report as + **Period report · Succeeded · Demo Corp** on the shared analysis-run + snapshot (ADR 0022). Open that row to confirm the cutoff posts. Mean + θ stays on the period-report panel — the registry row does not copy + a score. + +### Fixed + +- Opening a Pending lineage run repeats that reconstruction has not + started. Pending next-action copy is pinned to the registered run + kinds, so a Pending TEPP row does not say reconstruction. + ## [0.86.2] - 2026-08-16 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index a11127584..69bfbf34e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,8 @@ ADRs under `docs/adr/`. Do not fork those rules here. ## Analysis-run seed (v0.85.0) -`make seed` writes a Demo Corp lineage run and a TEPP run on the same -snapshot (ADR 0013). The TEPP path goes through `tepp_client`. A missing +`make seed` writes a Demo Corp lineage run, a TEPP run, and a Succeeded +period-report run on the same snapshot (ADR 0013 / ADR 0022). The TEPP path goes through `tepp_client`. A missing 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 @@ -14,9 +14,12 @@ theta or a local psychometric substitute. The home list caption stays (ADR 0014). Open a Failed TEPP row, then connect a live TEPP 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. +pending TEPP row does not claim a calibrated measurement. A pending +lineage row says reconstruction has not started yet. 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). `POST /api/analysis-runs` records Pending on an authorized cutoff capture (ADR 0017) and does not reconstruct lineage. +After `make seed`, open **Period report · Succeeded · Demo Corp** +to confirm the cutoff posts; mean θ stays on the period-report panel. diff --git a/docs/adr/0013-normalized-analysis-run-registry.md b/docs/adr/0013-normalized-analysis-run-registry.md index b60164dc6..c1c61776d 100644 --- a/docs/adr/0013-normalized-analysis-run-registry.md +++ b/docs/adr/0013-normalized-analysis-run-registry.md @@ -249,7 +249,9 @@ Acceptance requires: a live transport remains a later slice. A missing or unused TEPP envelope must stay Failed (`tepp_not_available` / `tepp_result_not_persisted`) and must not write a local psychometric - substitute. + substitute. Seed also records a Succeeded `analysis_run_report` on + that snapshot after the period-report tables are written (ADR 0022); + the registry row does not copy a theta. 5. Execute private actual-data analysis and store only signed aggregate and reproducibility manifests outside public source control. 6. Run browser E2E through real OIDC, product navigation, and evidence drill-down. diff --git a/docs/adr/0014-authorized-analysis-run-read.md b/docs/adr/0014-authorized-analysis-run-read.md index 29c074c8f..cd737e80d 100644 --- a/docs/adr/0014-authorized-analysis-run-read.md +++ b/docs/adr/0014-authorized-analysis-run-read.md @@ -38,9 +38,10 @@ LineageWeave owns a fail-closed read projection of the #89 registry: ## Consequences -`make seed` writes one synthetic Demo Corp lineage run and one TEPP -run on the same snapshot so the existing React home page can show both -kinds without a second application. The TEPP run is Failed / +`make seed` writes one synthetic Demo Corp lineage run, one TEPP +run, and one Succeeded period-report run on the same snapshot so the +existing React home page can show all three kinds without a second +application (ADR 0022). 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 TEPP run, then connect the measurement @@ -48,7 +49,8 @@ service. 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 from a current snapshot. A pending or running TEPP row must not claim a calibrated -measurement. The detail now shows the legal +measurement. A pending lineage row says reconstruction has not +started yet. 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/docs/adr/0022-seed-period-report-analysis-run.md b/docs/adr/0022-seed-period-report-analysis-run.md new file mode 100644 index 000000000..52438860d --- /dev/null +++ b/docs/adr/0022-seed-period-report-analysis-run.md @@ -0,0 +1,62 @@ +# ADR 0022 — Seed records the built period report on the shared snapshot + +**Decision status:** Accepted on this active PR; not protected-main truth until merge +**Date:** 2026-08-16 +**Depends on:** ADR 0013 normalized analysis-run registry; ADR 0003 +fast-mlsirm report integration; ADR 0014 authorized analysis-run read +**Refs:** After `make seed`, lineage and TEPP registry rows were visible +on home Analysis runs, but the calibrated period report lived only on +the separate report panel. ADR 0021 is the person-catalog bind on #153. + +## Context + +Seed already scores Demo Corp week-2/week-3 reports through +`fast-mlsirm` and persists them on the report tables. The analysis-run +registry already has `analysis_run_report`. Operators who opened +Analysis runs after `make seed` could retry a Failed TEPP transport or +inspect a Succeeded lineage tree, then had no registry row for the +report they could already see on the period-report panel. + +A fake Failed report row would contradict the built report. Copying +mean θ onto `analysis_run` would invent a psychometric field the +registry is not allowed to store (ADR 0013). + +## Decision + +- `_seed_demo_period_report` still builds the calibrated report first. +- `_seed_demo_report_run` then inserts `analysis_run_report` on the + same Demo Corp snapshot, scoped to the same corporate entity. +- The lifecycle is Pending → Running → Succeeded because the report + tables already hold the scored period. The run row stores only + registry digests and counts — never a theta, item bank, or provider + body. +- Home next-action copy for a Succeeded report stays empty. Failed + report fixtures still say rebuild the period report. +- `POST /api/analysis-runs` stays lineage-or-TEPP as implemented + (ADR 0017). This slice does not add a Request period-report button + and does not call TEPP. + +## Consequences + +After `make seed`, Demo Analyst opens Analysis runs and sees +**Period report · Succeeded · Demo Corp** next to the lineage and TEPP +rows. Opening it shows the cutoff posts. Mean θ remains on the +period-report panel. Re-seed is idempotent on +`demo-report-seed-2026-w02`. + +## References — APA 7th + +American Educational Research Association, American Psychological +Association, & National Council on Measurement in Education. (2014). +*Standards for educational and psychological testing*. American +Educational Research Association. + +International Organization for Standardization. (2019). *ISO 8601-1:2019: +Date and time—Representations for information interchange—Part 1: Basic +rules* (confirmed 2024; Amendment 1:2022). + +Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*. +World Wide Web Consortium. https://www.w3.org/TR/prov-dm/ + +World Wide Web Consortium. (2022). *Time ontology in OWL* (W3C +Recommendation). https://www.w3.org/TR/owl-time/ diff --git a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md index b41b31c17..15ce517c7 100644 --- a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md +++ b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md @@ -8,7 +8,7 @@ | Source | Product implication | Implemented evidence | |---|---|---| | W3C PROV-DM and PROV-O | Preserve identifiable entities, activities, agents, generation/use, and derivation without flattening provenance into display-only edges. | `analysis_source_snapshot`, `analysis_run`, authenticated requester, append-only status events, immutable digests; later product bindings continue to use the separate `provenance_*` layer from ADR 0011. | -| W3C Time Ontology in OWL | Keep temporal concepts explicit and avoid collapsing distinct clocks. | Evidence availability and snapshot capture remain on `analysis_source_snapshot`; analysis knowledge cutoff and request time remain on `analysis_run`; status occurrence and database record time remain distinct. `GET /api/analysis-runs/{id}` visible posts apply `created_at <= knowledge_cutoff` (ADR 0016). Opening a listed title warns that the live body may have changed after that cutoff. | +| W3C Time Ontology in OWL | Keep temporal concepts explicit and avoid collapsing distinct clocks. | Evidence availability and snapshot capture remain on `analysis_source_snapshot`; analysis knowledge cutoff and request time remain on `analysis_run`; status occurrence and database record time remain distinct. `GET /api/analysis-runs/{id}` visible posts apply `created_at <= knowledge_cutoff` (ADR 0016). Opening a listed title warns that the live body may have changed after that cutoff. Seed records the built period report as a later Succeeded run on that same snapshot (ADR 0022) without copying a theta onto the registry row. | | W3C Accessible Name and Description Computation 1.1 | Do not let `aria-label` replace visible text the operator must hear. | Analysis-run digest prefixes live in a labeled group; the prefixes remain the accessible contents and the full digest is on `title` for hover verification. | | ISO 8601-1:2019 | Use unambiguous timestamp representation and timezone-aware persistence. | PostgreSQL `timestamptz` for availability, capture, cutoff, request, occurrence, and record clocks; tests use explicit `Z` offsets. | | PostgreSQL 18 constraints and trigger contracts | Put integrity close to durable truth and use constraints for row shape while triggers enforce cross-row state and serialization. | Digest/check constraints, category allowlists, account-scoped uniqueness, shape constraints, immutable-row triggers, shared snapshot-row locking, and serialized status transitions. | diff --git a/frontend/package.json b/frontend/package.json index fb52f7948..05f877ba2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.86.2", + "version": "0.86.4", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 65d25259b..6e514292f 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -61,6 +61,7 @@ describe("App, authenticated", () => { verificationEvidenceUrl?: string | null; failedLineageRun?: boolean; failedReportRun?: boolean; + succeededReportRun?: boolean; succeededTeppRun?: boolean; pendingTeppRun?: boolean; postBody?: string; @@ -175,6 +176,7 @@ describe("App, authenticated", () => { ); } if (url.endsWith("/api/analysis-runs/run-demo-report")) { + const reportSucceeded = Boolean(options?.succeededReportRun); return Promise.resolve( jsonResponse({ analysis_run_id: "run-demo-report", @@ -183,8 +185,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", - status_label: "Failed", + status_code: reportSucceeded ? "analysis_status_succeeded" : "analysis_status_failed", + status_label: reportSucceeded ? "Succeeded" : "Failed", knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:38:00Z", source_counts: [ @@ -194,22 +196,45 @@ describe("App, authenticated", () => { count_value: 3, }, ], - visible_posts: [], - status_history: [ - { - status_ordinal: 1, - status_code: "analysis_status_pending", - status_label: "Pending", - occurred_at: "2026-01-12T12:39:00Z", - }, - { - status_ordinal: 2, - status_code: "analysis_status_failed", - status_label: "Failed", - occurred_at: "2026-01-12T12:40:00Z", - failure_code: "period_report_rebuild_failed", - }, - ], + visible_posts: reportSucceeded + ? [{ post_id: "post-1", post_title: "Public post" }] + : [], + status_history: reportSucceeded + ? [ + { + status_ordinal: 1, + status_code: "analysis_status_pending", + status_label: "Pending", + occurred_at: "2026-01-12T12:39:00Z", + }, + { + status_ordinal: 2, + status_code: "analysis_status_running", + status_label: "Running", + occurred_at: "2026-01-12T12:40:00Z", + }, + { + status_ordinal: 3, + status_code: "analysis_status_succeeded", + status_label: "Succeeded", + occurred_at: "2026-01-12T12:41:00Z", + }, + ] + : [ + { + status_ordinal: 1, + status_code: "analysis_status_pending", + status_label: "Pending", + occurred_at: "2026-01-12T12:39:00Z", + }, + { + status_ordinal: 2, + status_code: "analysis_status_failed", + status_label: "Failed", + occurred_at: "2026-01-12T12:40:00Z", + failure_code: "period_report_rebuild_failed", + }, + ], }), ); } @@ -408,7 +433,7 @@ describe("App, authenticated", () => { }, ], }, - ...(options?.failedReportRun + ...(options?.failedReportRun || options?.succeededReportRun ? [ { analysis_run_id: "run-demo-report", @@ -417,8 +442,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" as const, - status_label: "Failed", + status_code: options?.succeededReportRun + ? ("analysis_status_succeeded" as const) + : ("analysis_status_failed" as const), + status_label: options?.succeededReportRun ? "Succeeded" : "Failed", knowledge_cutoff: "2026-01-12T12:00:00Z", requested_at: "2026-01-12T12:38:00Z", source_counts: [ @@ -1737,6 +1764,31 @@ describe("App, authenticated", () => { expect(teppButton).not.toHaveTextContent("reconstruction"); }); + it("does not tell a succeeded period report to rebuild, reconstruct, or measure", async () => { + stubBackend({ succeededReportRun: true }); + render(); + + const reportButton = await screen.findByRole("button", { + name: "Open analysis run: Period report · Succeeded · Demo Corp", + }); + expect(reportButton).not.toHaveTextContent("rebuild the period report"); + expect(reportButton).not.toHaveTextContent("Reconstruction has not started yet"); + expect(reportButton).not.toHaveTextContent("measurement service"); + expect(reportButton).not.toHaveTextContent("θ"); + + await userEvent.click(reportButton); + expect( + await screen.findByRole("heading", { name: "Period report · Succeeded · Demo Corp" }), + ).toBeInTheDocument(); + expect(screen.queryByText(/rebuild the period report/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Reconstruction has not started yet/)).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { + name: "Open live post (may have changed after cutoff): Public post", + }), + ).toBeInTheDocument(); + }); + it("does not tell a failed period report to connect the measurement service", async () => { stubBackend({ failedReportRun: true }); render(); @@ -1772,6 +1824,7 @@ describe("App, authenticated", () => { ).toBeInTheDocument(); expect(screen.queryByText(/replace Failed/i)).not.toBeInTheDocument(); expect(screen.queryByText(/this TEPP run measured/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Reconstruction has not started yet/)).not.toBeInTheDocument(); }); it("does not tell a succeeded TEPP run to replace Failed", async () => { @@ -1799,7 +1852,11 @@ describe("App, authenticated", () => { expect( await screen.findByRole("heading", { name: "Lineage reconstruction · Pending · Demo Corp" }), ).toBeInTheDocument(); - expect(screen.getByText(/has not started yet/)).toBeInTheDocument(); + expect( + screen.getByText( + "Open this run to confirm which posts it will use. Reconstruction has not started yet.", + ), + ).toBeInTheDocument(); const postCall = fetchMock.mock.calls.find( (call) => String(call[0]).endsWith("/api/analysis-runs") && call[1]?.method === "POST", ); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a511f713d..d589a3644 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1451,28 +1451,48 @@ function analysisRunCaption(run: AnalysisRun): string { } /** - * Next action for a failed run on the home list. + * Next action for a pending or failed run on the home list and detail. * * 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. + * 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. */ function analysisRunNextAction(run: AnalysisRun): string | null { - if (run.status_code === "analysis_status_pending") { - return "Open this run to confirm which posts it will use. Reconstruction has not started yet."; - } - 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."; - case "analysis_run_report": - return "Open this run to see why it failed, then rebuild the period report from a current snapshot."; + switch (run.status_code) { + case "analysis_status_pending": + switch (run.run_kind_code) { + case "analysis_run_lineage": + return "Open this run to confirm which posts it will use. Reconstruction has not started yet."; + case "analysis_run_tepp": + return "Open this run to confirm which posts TEPP will measure. Measurement has not started yet — this is not a calibrated result."; + case "analysis_run_report": + return "Open this run to confirm which posts the period report will use. The report has not been built yet."; + default: { + const unexpected: never = run.run_kind_code; + return unexpected; + } + } + case "analysis_status_failed": + 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."; + case "analysis_run_report": + return "Open this run to see why it failed, then rebuild the period report from a current snapshot."; + default: { + const unexpected: never = run.run_kind_code; + return unexpected; + } + } + case "analysis_status_running": + case "analysis_status_succeeded": + case "analysis_status_cancelled": + case null: + return null; default: { - const unexpected: never = run.run_kind_code; + const unexpected: never = run.status_code; return unexpected; } } @@ -1648,6 +1668,7 @@ function AnalysisRunsPanel({ if (runs === null) return

Loading analysis runs...

; const corpusHint = selected ? analysisRunCorpusHint(selected) : null; + const selectedNextAction = selected ? analysisRunNextAction(selected) : null; return (
@@ -1699,6 +1720,7 @@ function AnalysisRunsPanel({ {selected && (

{analysisRunCaption(selected)}

+ {selectedNextAction &&

{selectedNextAction}

}

Cutoff {selected.knowledge_cutoff.slice(0, 10)} {" · "} diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index efe84890b..93c53ac54 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "0.86.2" +__version__ = "0.86.4" diff --git a/pyproject.toml b/pyproject.toml index 6e41c7ca7..7254de99e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "0.86.2" +version = "0.86.4" 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/scripts/seed_demo_data.py b/scripts/seed_demo_data.py index 9d246445d..8584f1877 100644 --- a/scripts/seed_demo_data.py +++ b/scripts/seed_demo_data.py @@ -39,11 +39,12 @@ DEFAULT_KEYCLOAK_ADMIN_USER = os.environ.get("KEYCLOAK_ADMIN", "admin") DEFAULT_VALKEY_URL = "redis://localhost:16379/0" -# ADR 0013: one Demo Corp capture, many runs (lineage + TEPP). +# ADR 0013: one Demo Corp capture, many runs (lineage + TEPP + report). DEMO_SOURCE_SNAPSHOT_MATERIAL = b"lineageweave-synthetic-demo-snapshot-v1" DEMO_SOURCE_CONTRACT_VERSION = "demo-source-contract-v1" DEMO_LINEAGE_IDEMPOTENCY_KEY = "demo-lineage-seed-2026-w02" DEMO_TEPP_IDEMPOTENCY_KEY = "demo-tepp-seed-2026-w02" +DEMO_REPORT_IDEMPOTENCY_KEY = "demo-report-seed-2026-w02" # (post_title, ticket_title, due_date) -- Event Lineage fixtures a report # member click opens. Activity seed uses the same titles so Valkey matches. @@ -347,6 +348,11 @@ def seed( account_ids["demo.analyst"], corporate_entity_id, ) + _seed_demo_report_run( + cur, + account_ids["demo.analyst"], + corporate_entity_id, + ) conn.commit() finally: @@ -1225,9 +1231,9 @@ def demo_source_snapshot_sha256() -> str: def _ensure_demo_source_snapshot(cur): """Return the shared Demo Corp capture, inserting it on first seed. - Lineage and TEPP runs share this snapshot (ADR 0013: one capture, - many runs). The digest is a hash of a fixed demo contract string -- - never a source row or DSN. + Lineage, TEPP, and period-report runs share this snapshot + (ADR 0013: one capture, many runs). The digest is a hash of a + fixed demo contract string -- never a source row or DSN. """ digest = demo_source_snapshot_sha256() cur.execute( @@ -1446,6 +1452,74 @@ def _seed_demo_tepp_run(cur, requested_by_account_id, corporate_entity_id) -> No ) +def _seed_demo_report_run(cur, requested_by_account_id, corporate_entity_id) -> None: + """Record the already-built Demo Corp period report on the shared snapshot. + + ``_seed_demo_period_report`` persists calibrated report tables first. + This registry row is Succeeded because that write already happened. + It does not copy a theta onto ``analysis_run`` and does not invent + a local psychometric substitute (ADR 0022). + """ + snapshot_id = _ensure_demo_source_snapshot(cur) + _ensure_demo_source_counts(cur, snapshot_id) + cur.execute( + """ + select analysis_run_id from analysis_run + where requested_by_account_id = %s + and idempotency_key = %s + """, + (requested_by_account_id, DEMO_REPORT_IDEMPOTENCY_KEY), + ) + run_row = cur.fetchone() + if run_row is None: + cur.execute( + """ + insert into analysis_run + (analysis_source_snapshot_id, run_kind_code, idempotency_key, + requested_by_account_id, knowledge_cutoff, + configuration_schema_version, configuration_sha256, + code_revision_sha, requested_at) + values (%s, 'analysis_run_report', %s, + %s, '2026-01-12T12:00:00Z', 'report-run-v1', %s, %s, + '2026-01-12T12:38:00Z') + returning analysis_run_id + """, + ( + snapshot_id, + DEMO_REPORT_IDEMPOTENCY_KEY, + requested_by_account_id, + "f" * 64, + "a" * 40, + ), + ) + run_id = cur.fetchone()[0] + else: + run_id = run_row[0] + cur.execute( + """ + insert into analysis_run_scope + (analysis_run_id, scope_kind_code, corporate_entity_id) + values (%s, 'analysis_scope_corporate_entity', %s) + on conflict (analysis_run_id) do nothing + """, + (run_id, corporate_entity_id), + ) + for ordinal, status, occurred in ( + (1, "analysis_status_pending", "2026-01-12T12:39:00Z"), + (2, "analysis_status_running", "2026-01-12T12:40:00Z"), + (3, "analysis_status_succeeded", "2026-01-12T12:41:00Z"), + ): + cur.execute( + """ + insert into analysis_run_status_event + (analysis_run_id, status_ordinal, status_code, occurred_at) + values (%s, %s, %s, %s) + on conflict do nothing + """, + (run_id, ordinal, status, occurred), + ) + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--postgres-dsn", default=DEFAULT_POSTGRES_DSN) diff --git a/tests/test_seed_report_run.py b/tests/test_seed_report_run.py new file mode 100644 index 000000000..12ea6efc3 --- /dev/null +++ b/tests/test_seed_report_run.py @@ -0,0 +1,73 @@ +"""Seeded period-report analysis runs record the built report, never a theta.""" + +from scripts.seed_demo_data import ( + DEMO_REPORT_IDEMPOTENCY_KEY, + _seed_demo_report_run, +) + + +class _ReportSeedCursor: + """Drive ``_seed_demo_report_run`` without a live database.""" + + def __init__(self) -> None: + self.statements: list[str] = [] + self.params: list[object] = [] + + def execute(self, sql: str, params=None) -> None: + self.statements.append(" ".join(sql.split())) + self.params.append(params) + + def fetchone(self): + last = self.statements[-1] + if last.lstrip().startswith("select") and "from analysis_source_snapshot" in last: + return None + if "insert into analysis_source_snapshot" in last: + return ("snapshot-demo",) + if last.lstrip().startswith("select") and "from analysis_source_count" in last: + return None + if last.lstrip().startswith("select") and "from analysis_run" in last: + return None + if "insert into analysis_run" in last: + return ("run-demo-report",) + return None + + +def test_seed_demo_report_run_inserts_succeeded_report_without_a_theta() -> None: + cursor = _ReportSeedCursor() + _seed_demo_report_run(cursor, "account-1", "corp-1") + run_inserts = [ + (sql, params) + for sql, params in zip(cursor.statements, cursor.params, strict=True) + if "insert into analysis_run" in sql + ] + assert run_inserts, "seed must insert the period-report analysis_run row" + sql, params = run_inserts[0] + assert "analysis_run_report" in sql + assert params is not None + assert DEMO_REPORT_IDEMPOTENCY_KEY in params + assert "report-run-v1" in sql + assert not any( + isinstance(value, str) and ("theta" in value.lower() or "θ" in value) + for value in params + ) + status_params = [ + params + for sql, params in zip(cursor.statements, cursor.params, strict=True) + if "insert into analysis_run_status_event" in sql + ] + assert any( + event_params is not None and "analysis_status_succeeded" in event_params + for event_params in status_params + ) + assert not any( + event_params is not None and "analysis_status_failed" in event_params + for event_params in status_params + ) + assert not any( + event_params is not None + and any( + isinstance(value, str) and "theta" in value.lower() + for value in event_params + ) + for event_params in status_params + ) diff --git a/uv.lock b/uv.lock index e1d2860cf..0f0f81957 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "0.86.2" +version = "0.86.4" source = { virtual = "." } dependencies = [ { name = "certifi" },