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(
Loading analysis runs...
; const corpusHint = selected ? analysisRunCorpusHint(selected) : null; + const selectedNextAction = selected ? analysisRunNextAction(selected) : null; return ({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" },