diff --git a/AGENTS.md b/AGENTS.md
index 3af4169bc..77f1c551d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -198,9 +198,13 @@ confidently-negative signal are different things. Keyman extraction,
entity-relationship classification, post summary, in-popup chat, and
commitment derivation go through contextual-orchestrator the same way
adjudication does -- never a raw LLM API. Demo TEPP seed goes through
-`tepp_client` the same way: a missing transport or an unused accepted
-envelope is Failed (`tepp_not_available` / `tepp_result_not_persisted`),
-never a fabricated theta or a local psychometric substitute.
+`tepp_client` the same way: a missing transport is Failed
+(`tepp_not_available`). A strict accepted v1 envelope persists to
+`analysis_run_tepp_receipt` and the local run stays Running (ADR 0219);
+an invalid or unpublished envelope is Failed
+(`tepp_result_not_persisted`). Never a fabricated theta or a local
+psychometric substitute. Automatic polling stays unavailable until TEPP
+publishes its status route.
The lineage `text` channel follows [ADR 0190](docs/adr/0190-lineage-text-channel-embedding-swap.md):
when an embedding provider is configured, `reconstruct()` precomputes
diff --git a/CHANGELOG.d/2.29.0-tepp-accepted-seed.md b/CHANGELOG.d/2.29.0-tepp-accepted-seed.md
new file mode 100644
index 000000000..a6a68616e
--- /dev/null
+++ b/CHANGELOG.d/2.29.0-tepp-accepted-seed.md
@@ -0,0 +1,8 @@
+# 2.29.0 — Seeded TEPP accepted receipt stays Running
+
+After `make seed`, Demo Corp includes a Running TEPP measurement whose
+strict accepted v1 receipt is transport evidence (ADR 0219). Open that
+run: measurement request accepted — refresh to check whether results are
+ready. The missing-transport TEPP row stays Failed / `tepp_not_available`.
+Acceptance is not a calibrated result. Never invent a theta. No polling
+until TEPP publishes its status route.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a2724eb8..62df4ad09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,15 @@ All notable changes to this project are documented here. Format follows
### Added
+- After `make seed`, Demo Corp now includes a Running TEPP measurement
+ whose strict accepted v1 receipt is transport evidence (ADR 0219 /
+ v2.29.0). Open that run: measurement request accepted — refresh to
+ check whether results are ready. The missing-transport TEPP row stays
+ Failed / `tepp_not_available`. Acceptance is not a calibrated result
+ and the UI does not expose the receipt's transport `run_id`. Never
+ invent a theta. No polling until TEPP publishes its status route.
+ Issue #277 stays open.
+
- Period leftover pairs now caption leftover-map graphic-display pair
segments with persisted leftover-map distance `d` (ADR 0271 /
v2.28.0). After `make seed`, closest and farthest leftover pairs sit
diff --git a/CLAUDE.md b/CLAUDE.md
index 1a36a87b3..dbb498cd8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,14 +21,19 @@ sits on a public HTTP route.
## Analysis-run seed and run states (ADR 0013 / 0014 / 0024)
-`make seed` writes a Demo Corp lineage run, a TEPP run, and a Succeeded
-period-report run on one snapshot. The TEPP path goes through
-`tepp_client`: a missing transport or an unused accepted envelope is
-Failed (`tepp_not_available` / `tepp_result_not_persisted`). Never
-invent a theta or a local psychometric substitute.
+`make seed` writes a Demo Corp lineage run, a Failed TEPP run
+(missing transport), a Running TEPP run with a persisted accepted
+receipt, and a Succeeded period-report run on one snapshot. The TEPP
+path goes through `tepp_client`: a missing transport is Failed
+(`tepp_not_available`); a strict accepted v1 envelope persists as
+transport evidence and stays Running (ADR 0219). An invalid envelope
+is Failed (`tepp_result_not_persisted`). Never invent a theta or a
+local psychometric substitute.
- Failed TEPP is terminal -- open that row and connect a live TEPP
transport from it.
+- A Running TEPP with an accepted receipt is not a calibrated result --
+ refresh that run to check whether results are ready.
- A failed lineage row retries reconstruction and does not mention TEPP;
a failed period-report row rebuilds the report.
- Pending rows claim nothing: pending TEPP is not a calibrated
diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py
index cf7c249c2..c20555007 100644
--- a/backend/app/analysis_run_start.py
+++ b/backend/app/analysis_run_start.py
@@ -257,11 +257,14 @@ def _tepp_submission(
client: TeppClient,
request: AnalysisRunRequest,
) -> tuple[str, str, dict[str, Any] | None]:
- """Submit through ``tepp_client`` and require a completed result envelope.
-
- TEPP's target HTTP contract is asynchronous. An ``accepted`` response is
- therefore not a measurement and remains ``tepp_result_not_persisted``.
- Only a provider-authoritative completed envelope can enter the database.
+ """Submit through ``tepp_client`` and classify the provider envelope.
+
+ TEPP's target HTTP contract is asynchronous. A strict accepted v1
+ response is transport evidence (ADR 0219), not a measurement: the
+ caller persists ``analysis_run_tepp_receipt`` and leaves the local
+ run Running. Only a provider-authoritative completed envelope can
+ enter ``analysis_run_tepp_result``. An invalid or unpublished shape
+ stays ``tepp_result_not_persisted``.
"""
try:
response = client.submit_analysis_run(request)
@@ -271,12 +274,17 @@ def _tepp_submission(
return _FAILED, "tepp_result_not_persisted", None
if not isinstance(response, dict):
return _FAILED, "tepp_result_not_persisted", None
- state = response.get("status") or response.get("run_state")
+ status = response.get("status")
+ run_state = response.get("run_state")
+ if status is not None and run_state is not None and status != run_state:
+ return _FAILED, "tepp_result_not_persisted", None
+ state = status or run_state
remote_run_id = response.get("analysis_run_id") or response.get("run_id")
if state == "accepted":
if (
set(response)
== {"contract_version", "run_id", "run_state", "idempotency_key"}
+ and type(response["contract_version"]) is int
and response["contract_version"] == 1
and response["idempotency_key"] == request.idempotency_key
and isinstance(remote_run_id, str)
diff --git a/docs/adr/0219-tepp-terminal-result-lifecycle.md b/docs/adr/0219-tepp-terminal-result-lifecycle.md
index e100ab858..0741ecc2b 100644
--- a/docs/adr/0219-tepp-terminal-result-lifecycle.md
+++ b/docs/adr/0219-tepp-terminal-result-lifecycle.md
@@ -1,6 +1,6 @@
# ADR 0219 — Persist TEPP acceptance and consume terminal results
-**Decision status:** Accepted on this active PR; not protected-main truth until merge
+**Decision status:** Accepted; seed visibility on this PR; polling remains blocked on TEPP #249
**Date:** 2026-08-26
**Depends on:** ADR 0022, ADR 0023, ADR 0204; TEPP PR #157
**Refs:** LineageWeave issue #277; TEPP issues #156 and #249
@@ -66,6 +66,10 @@ sequenceDiagram
LineageWeave owns transport and provenance persistence only. TEPP retains all
statistical, psychometric, CPU, and GPU arithmetic. Automatic polling remains
unavailable until the owning service publishes its route and retry policy.
+`make seed` writes one Demo Corp TEPP run that stays Failed /
+`tepp_not_available` (missing transport) and one that stays Running with a
+persisted accepted receipt so the buyer copy is visible without inventing a
+theta.
## References — APA 7th
diff --git a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
index 99dfff47b..46ec6ac08 100644
--- a/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
+++ b/docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
@@ -79,7 +79,7 @@ provenance, retention, and immutable evidence rather than blanket masking.
| Idempotency is actor-scoped | Permit identical opaque keys for two accounts and reject reuse by the same account. |
| Lifecycle is ordered | Require pending first, contiguous ordinals, monotonic time, legal transitions, terminal finality, and append-only rows. |
| Rollback does not erase audit data silently | Reject 0018 rollback with any registry rows. A run-bearing registry empties only through an unrevoked `analysis_run_retention_grant` plus `analysis_run_retention_admin`, then `purge_analysis_run_registry('approved-retention-purge')`; a wrong token, a raw `DELETE`, and a runtime role that only knows the public phrase stay rejected. Export then delete `analysis_run_retention_event` before 0020 rollback. |
-| Start reconstruction recovers the designed tree | Persist edges from `lineage_edge_specs` on the A-100 fixture bag via `records_from_source_posts`; the pricing follow-up must parent both the revised quote and the delivery question. A period-report start must 422 without a theta. TEPP start submits through `tepp_client` and stays Failed (`tepp_not_available` / `tepp_result_not_persisted`) without a theta. Snapshot members exclude a later backfill. A concurrent or Running start is 409. A Succeeded retry returns the stored digest. |
+| Start reconstruction recovers the designed tree | Persist edges from `lineage_edge_specs` on the A-100 fixture bag via `records_from_source_posts`; the pricing follow-up must parent both the revised quote and the delivery question. A period-report start must 422 without a theta. TEPP start submits through `tepp_client`: missing transport is Failed (`tepp_not_available`); a strict accepted v1 envelope persists to `analysis_run_tepp_receipt` and stays Running; an invalid envelope is Failed (`tepp_result_not_persisted`) without a theta. Snapshot members exclude a later backfill. A concurrent or Running start is 409. A Succeeded retry returns the stored digest. |
## APA 7th references
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index b5d31877b..c6ced7dc9 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,49 @@
# Product & Technical Gap Baseline
+> Exact-head observation overlay: 2026-09-02 KST. Protected `main` is
+> `3f61c8242b9c02dec307a7396e83e28f7cdd9f3d`. PR #897 is
+> implemented through behavior-changing head
+> `419d099af60df2f275fd058bf42149700709f628`, mergeable, and protected by
+> normal squash auto-merge; exact-head Checks are queued and no qualifying
+> independent APPROVE exists. The following documentation-only commit records
+> that observation; it does not claim its own recursively unknowable commit
+> identifier as the observed implementation head. All implementation review
+> threads were resolved at the observation.
+> Its accepted-receipt seed now also fails closed
+> when a reseed encounters an existing Running history and a conflicting
+> receipt: it appends `analysis_status_failed` /
+> `tepp_result_not_persisted` unless a terminal event already exists, then
+> appends the missing Delivered event to any existing claimed outbox so a
+> terminal run cannot be retried. Focused regression evidence is 55 passing
+> tests. This remains candidate evidence,
+> not a protected-main release or authenticated PostgreSQL/UI runtime claim.
+> The queue currently contains 107 open PRs and 15 open Issues; stacked PRs
+> retain their declared bases until each parent is protected-merged. Canonical
+> remote names are `ContextualWisdomLab/LineageWeave`, `RankWeave`,
+> `ThreadWeave`, lowercase `disksage`, `TEPP`, `contextual-orchestrator`, and
+> `fast-mlsirm`. The largest buyer increment in this slice remains an honest
+> visible Running state after a persisted accepted receipt; producer status
+> polling stays unavailable under issue #277. TEPP PR #266 protected-merged
+> the strict HTTPS GET exchange builder, but neither that library contract nor
+> a closed provider issue proves a configured live service, authenticated
+> PostgreSQL lifecycle, evidence-based polling cadence, or rendered UI here.
+
+> Exact-head loop overlay: 2026-09-01 KST. Protected `main` is
+> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` (v2.24.0 leftover-map
+> coordinates, #782). Package/pyproject versions on that head are 2.28.0.
+> Open ready PRs still lack independent APPROVE. Leftover-map stacked
+> heads through v2.106 are gold-plating and are not this cycle's buyer
+> increment. #96 stays closed as a weaker duplicate of #91. Copilot
+> review is not independent APPROVE. Do not self-approve. Do not merge
+> stacked leftover PRs onto an unprotected leftover base. Issues #79
+> and #87 stay OPEN. Issue #277 stays OPEN: consumer receipt persist is
+> on `main`; producer status HTTP remains unavailable.
+> Next buyer increment on this cycle: seed a Demo Corp Running TEPP
+> accepted receipt (ADR 0219 / v2.29.0) so `make seed` shows
+> "Measurement request accepted. Refresh this run to check whether
+> results are ready." Missing-transport Failed remains. Never invent a
+> GET URL, retry interval, leftover score, or theta.
+
> Exact-head loop overlay: 2026-08-29 13:20 KST. Protected `main` is
> `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map
> explained leftover share, #775). Open ready PRs still lack independent
diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md
index f426285a6..376d7e772 100644
--- a/docs/storybook-inventory.md
+++ b/docs/storybook-inventory.md
@@ -15,6 +15,7 @@ operator-facing control you can click before changing product CSS.
| `Evidence/OrganizationAliasChip` | Click a cataloged org; the parenthetical is the unique corroborated SKOS companion. | `--color-chip-border`, `--radius-chip`, `OrganizationAliasChip` |
| `Evidence/OntologyExplorer` | Distinguish Event Lineage from typed ontology facts, inspect Post/Person/Organization/Team/Project shapes, token-backed secondary cues, and truth labels, then open authorized evidence. The named exact-values region supports keyboard scrolling; `LongLabelsAndEvidenceTable` proves complete labels wrap without character-count truncation, while `CombinedVoiceEvidence` covers primary-plus-additional Voice assignments and focuses the evidence action distinct from the carrying-Post action. Desktop, narrow, drawers, legend/filter, empty, truncated, partial, denied, stale, and rejected scenes cover ADR 0184/0222/0251 states. | `OntologyExplorer`, `ontologyLayout`, `--ontology-node-*-fill`, `--color-table-border` |
| `Evidence/OntologyExplorer` | Distinguish Event Lineage from typed ontology facts, inspect Post/Person/Organization/Team/Project/Work-evidence shapes, token-backed secondary cues, and truth labels, then open authorized evidence. The populated scene includes one assertion-backed occupational construct without a person-trait promotion. The named exact-values region supports keyboard scrolling; `LongLabelsAndEvidenceTable` proves complete labels wrap without character-count truncation. Desktop, narrow, drawers, legend/filter, empty, truncated, partial, denied, stale, and rejected scenes cover ADR 0184/0222/0255 states. | `OntologyExplorer`, `ontologyLayout`, `--ontology-node-*-fill`, `--color-table-border` |
+| `Analysis/TeppAcceptedReceipt` | Read that TEPP accepted the measurement request, then refresh the run to check whether results are ready. Acceptance is not a calibrated result and does not name a remote run id, TEPP, or Succeeded. | `TeppAcceptedReceipt`, `post-meta` |
| `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` |
| `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` |
| `Admin/AdminPanel` | Change the tenant brand name, then verify the saved or failed state before leaving settings. | `--surface`, `--border`, `--space-panel-block`, `AdminPanel` |
diff --git a/frontend/package.json b/frontend/package.json
index cb6d1347a..f62380df7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
- "version": "2.28.0",
+ "version": "2.29.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 790c4da69..69566ea54 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -145,6 +145,7 @@ describe("App, authenticated", () => {
succeededReportRun?: boolean;
succeededTeppRun?: boolean;
pendingTeppRun?: boolean;
+ runningTeppRun?: boolean;
pluralAffiliations?: boolean;
deferMe?: boolean;
deferPostOne?: boolean;
@@ -408,12 +409,16 @@ describe("App, authenticated", () => {
? "analysis_status_succeeded"
: options?.pendingTeppRun
? "analysis_status_pending"
- : "analysis_status_failed";
+ : options?.runningTeppRun
+ ? "analysis_status_running"
+ : "analysis_status_failed";
const teppLabel = options?.succeededTeppRun
? "Succeeded"
: options?.pendingTeppRun
? "Pending"
- : "Failed";
+ : options?.runningTeppRun
+ ? "Running"
+ : "Failed";
return Promise.resolve(
jsonResponse({
analysis_run_id: "run-demo-tepp",
@@ -426,7 +431,7 @@ describe("App, authenticated", () => {
status_label: teppLabel,
knowledge_cutoff: "2026-01-12T12:00:00Z",
requested_at: "2026-01-12T12:34:00Z",
- ...(options?.succeededTeppRun
+ ...(options?.runningTeppRun
? {
tepp_accepted_receipt: {
remote_run_id: "tepp-remote-run-1",
@@ -452,6 +457,21 @@ describe("App, authenticated", () => {
occurred_at: "2026-01-12T12:35:00Z",
},
]
+ : options?.runningTeppRun
+ ? [
+ {
+ status_ordinal: 1,
+ status_code: "analysis_status_pending",
+ status_label: "Pending",
+ occurred_at: "2026-01-12T12:35:00Z",
+ },
+ {
+ status_ordinal: 2,
+ status_code: "analysis_status_running",
+ status_label: "Running",
+ occurred_at: "2026-01-12T12:36:00Z",
+ },
+ ]
: [
{
status_ordinal: 1,
@@ -779,12 +799,16 @@ describe("App, authenticated", () => {
? "analysis_status_succeeded"
: options?.pendingTeppRun
? "analysis_status_pending"
- : "analysis_status_failed",
+ : options?.runningTeppRun
+ ? "analysis_status_running"
+ : "analysis_status_failed",
status_label: options?.succeededTeppRun
? "Succeeded"
: options?.pendingTeppRun
? "Pending"
- : "Failed",
+ : options?.runningTeppRun
+ ? "Running"
+ : "Failed",
knowledge_cutoff: "2026-01-12T12:00:00Z",
requested_at: "2026-01-12T12:34:00Z",
source_counts: [
@@ -3971,11 +3995,31 @@ describe("App, authenticated", () => {
expect(
await screen.findByText("These posts are the cutoff corpus this TEPP run measured."),
).toBeInTheDocument();
+ expect(screen.queryByLabelText("Measurement request accepted")).not.toBeInTheDocument();
+ expect(screen.queryByText("tepp-remote-run-1")).not.toBeInTheDocument();
+ expect(screen.queryByText(/replace Failed/i)).not.toBeInTheDocument();
+ });
+
+ it("names a running TEPP accepted receipt as transport evidence, not a result", async () => {
+ stubBackend({ runningTeppRun: true });
+ render();
+
+ await userEvent.click(
+ await screen.findByRole("button", {
+ name: "Open analysis run: TEPP measurement · Running · Demo Corp",
+ }),
+ );
expect(screen.getByLabelText("Measurement request accepted")).toHaveTextContent(
"Refresh this run to check whether results are ready.",
);
+ expect(
+ screen.getAllByText(
+ "Refresh this run. Start already queued the work on the durable outbox.",
+ ).length,
+ ).toBeGreaterThan(0);
expect(screen.queryByText("tepp-remote-run-1")).not.toBeInTheDocument();
- expect(screen.queryByText(/replace Failed/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/theta/i)).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Start TEPP measurement" })).not.toBeInTheDocument();
});
it("records a pending lineage run and opens the authorized detail", async () => {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e3fb6c796..ebb9ead21 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3368,7 +3368,8 @@ function AnalysisRunsPanel({
{" · "}
Requested {selected.requested_at.slice(0, 10)}
- {selected.tepp_accepted_receipt && (
+ {selected.tepp_accepted_receipt &&
+ selected.status_code === "analysis_status_running" && (
)}
tuple[str, str | None
"""Ask TEPP through the published client. A missing transport is Failed.
Never invents a psychometric score. ``tepp_not_available`` means the
- channel was dropped, not a calibrated negative result. A live
- envelope is also not a persistable measurement in this seed, so the
- run is not stamped Succeeded.
+ channel was dropped, not a calibrated negative result. A strict
+ accepted v1 envelope is transport evidence (ADR 0219), not a
+ measurement. An invalid or unpublished envelope stays
+ ``tepp_result_not_persisted`` and is not Succeeded.
"""
- request = tepp_seed_request()
- try:
- (client or TeppClient()).submit_analysis_run(request)
- except TeppNotAvailable:
- return "analysis_status_failed", "tepp_not_available"
- return "analysis_status_failed", "tepp_result_not_persisted"
+ from backend.app.analysis_run_start import _tepp_submission
+
+ status, failure, _envelope = _tepp_submission(
+ client or TeppClient(), tepp_seed_request()
+ )
+ return status, failure or None
+
+
+def tepp_accepted_seed_request(
+ corporate_entity_id: str = "demo-workspace",
+) -> AnalysisRunRequest:
+ """Build the Demo Corp TEPP request whose seed transport accepts v1."""
+ from backend.app.analysis_run_start import tepp_run_request
+
+ return tepp_run_request(
+ idempotency_key=DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY,
+ snapshot_sha256=demo_source_snapshot_sha256(),
+ knowledge_cutoff=datetime.fromisoformat("2026-01-12T12:00:00+00:00"),
+ corporate_entity_id=corporate_entity_id,
+ )
+
+
+def tepp_accepted_seed_client() -> TeppClient:
+ """Return a fixture transport that yields TEPP's strict accepted v1 shape."""
+
+ def _transport(payload: dict) -> dict:
+ return {
+ "contract_version": 1,
+ "run_id": DEMO_TEPP_ACCEPTED_REMOTE_RUN_ID,
+ "run_state": "accepted",
+ "idempotency_key": payload["idempotency_key"],
+ }
+
+ return TeppClient(transport=_transport)
+
+
+def tepp_accepted_seed_outcome(
+ client: TeppClient | None = None,
+ *,
+ corporate_entity_id: str = "demo-workspace",
+) -> tuple[str, str | None, dict | None]:
+ """Classify the accepted-fixture envelope without inventing a theta."""
+ from backend.app.analysis_run_start import _tepp_submission
+
+ status, failure, envelope = _tepp_submission(
+ client or tepp_accepted_seed_client(),
+ tepp_accepted_seed_request(corporate_entity_id),
+ )
+ return status, failure or None, envelope
def _seed_demo_tepp_run(cur, requested_by_account_id, corporate_entity_id) -> None:
@@ -1937,6 +1990,158 @@ def _seed_demo_tepp_run(cur, requested_by_account_id, corporate_entity_id) -> No
_seed_demo_run_outbox(cur, run_id)
+def _seed_tepp_accepted_receipt(cur, analysis_run_id, request, envelope) -> bool:
+ """Persist TEPP acceptance as transport evidence, never a measurement."""
+ remote_run_id = envelope.get("run_id")
+ if envelope.get("run_state") != "accepted" or not isinstance(remote_run_id, str):
+ return False
+ request_json = json.dumps(request.to_json(), separators=(",", ":"), sort_keys=True)
+ receipt_json = json.dumps(envelope, separators=(",", ":"), sort_keys=True)
+ receipt_values = (
+ remote_run_id,
+ hashlib.sha256(request_json.encode()).hexdigest(),
+ hashlib.sha256(receipt_json.encode()).hexdigest(),
+ )
+ cur.execute(
+ """
+ select remote_run_id, request_sha256, receipt_sha256
+ from analysis_run_tepp_receipt
+ where analysis_run_id = %s
+ """,
+ (analysis_run_id,),
+ )
+ existing = cur.fetchone()
+ if existing is not None:
+ return tuple(existing) == receipt_values
+ cur.execute(
+ """
+ insert into analysis_run_tepp_receipt
+ (analysis_run_id, remote_run_id, request_sha256, receipt_sha256,
+ accepted_status_code, received_at)
+ values (%s, %s, %s, %s, 'accepted', '2026-01-12T12:36:00Z')
+ on conflict do nothing
+ returning analysis_run_id
+ """,
+ (
+ analysis_run_id,
+ *receipt_values,
+ ),
+ )
+ return cur.fetchone() is not None
+
+
+def _seed_demo_tepp_accepted_run(cur, requested_by_account_id, corporate_entity_id) -> None:
+ """Insert one Demo-Corp TEPP run that stays Running after accepted v1.
+
+ Uses :func:`tepp_accepted_seed_client` so ``make seed`` can show the
+ buyer receipt copy. Missing-transport Failed remains the other TEPP
+ fixture. Never invents a theta or a completed result.
+ """
+ snapshot_id = _ensure_demo_source_snapshot(cur)
+ _ensure_demo_source_counts(cur, snapshot_id)
+ _ensure_demo_source_snapshot_members(cur, snapshot_id, corporate_entity_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_TEPP_ACCEPTED_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_tepp', %s,
+ %s, '2026-01-12T12:00:00Z', 'tepp-run-v1', %s, %s,
+ '2026-01-12T12:34:00Z')
+ returning analysis_run_id
+ """,
+ (
+ snapshot_id,
+ DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY,
+ requested_by_account_id,
+ "d" * 64,
+ "e" * 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),
+ )
+ request = tepp_accepted_seed_request(str(corporate_entity_id))
+ status, failure, envelope = tepp_accepted_seed_outcome(
+ corporate_entity_id=str(corporate_entity_id)
+ )
+ persist_receipt = (
+ status == "analysis_status_running"
+ and envelope is not None
+ and envelope.get("run_state") == "accepted"
+ )
+ if persist_receipt:
+ persist_receipt = _seed_tepp_accepted_receipt(cur, run_id, request, envelope)
+ if persist_receipt:
+ events = [
+ (1, "analysis_status_pending", "2026-01-12T12:35:00Z", None),
+ (2, "analysis_status_running", "2026-01-12T12:36:00Z", None),
+ ]
+ else:
+ if status == "analysis_status_running":
+ status = "analysis_status_failed"
+ failure = "tepp_result_not_persisted"
+ events = [
+ (1, "analysis_status_pending", "2026-01-12T12:35:00Z", None),
+ (2, "analysis_status_running", "2026-01-12T12:36:00Z", None),
+ (3, status, "2026-01-12T12:37:00Z", failure),
+ ]
+ cur.execute(
+ """
+ select coalesce(max(status_ordinal), 0),
+ coalesce(bool_or(status_code in
+ ('analysis_status_succeeded', 'analysis_status_failed',
+ 'analysis_status_cancelled')), false)
+ from analysis_run_status_event
+ where analysis_run_id = %s
+ """,
+ (run_id,),
+ )
+ status_row = cur.fetchone()
+ max_ordinal, has_terminal = status_row or (0, False)
+ if max_ordinal == 0:
+ for ordinal, event_status, occurred, fail in events:
+ cur.execute(
+ """
+ insert into analysis_run_status_event
+ (analysis_run_id, status_ordinal, status_code, occurred_at, failure_code)
+ values (%s, %s, %s, %s, %s)
+ """,
+ (run_id, ordinal, event_status, occurred, fail),
+ )
+ elif not persist_receipt and not has_terminal:
+ cur.execute(
+ """
+ insert into analysis_run_status_event
+ (analysis_run_id, status_ordinal, status_code, occurred_at, failure_code)
+ values (%s, %s, %s, %s, %s)
+ """,
+ (run_id, max_ordinal + 1, status, "2026-01-12T12:37:00Z", failure),
+ )
+ _seed_demo_run_outbox(cur, run_id, delivered=not persist_receipt)
+
+
def topic_lineage_seed_request() -> AnalysisRunRequest:
"""Build the Demo Corp topic-lineage request against the shared snapshot digest.
@@ -2120,11 +2325,12 @@ def _seed_demo_report_run(cur, requested_by_account_id, corporate_entity_id) ->
)
-def _seed_demo_run_outbox(cur, analysis_run_id) -> None:
- """Record a delivered start-work item for the seeded run.
+def _seed_demo_run_outbox(cur, analysis_run_id, *, delivered: bool = True) -> None:
+ """Record the start-work outbox path used by live start.
- Seed already stamped the terminal status. The outbox row proves the
- same durable path start uses. No theta is stored.
+ A terminal seed run is claimed then delivered. A Running TEPP
+ accepted-receipt seed stays claimed so a later status read can
+ resume without resubmitting (ADR 0219). No theta is stored.
"""
from datetime import datetime, timezone
@@ -2135,6 +2341,34 @@ def _seed_demo_run_outbox(cur, analysis_run_id) -> None:
(analysis_run_id,),
)
if cur.fetchone() is not None:
+ if delivered:
+ cur.execute(
+ """
+ select coalesce(max(delivery_ordinal), 0),
+ coalesce(bool_or(delivery_status_code =
+ 'analysis_outbox_delivered'), false)
+ from analysis_run_outbox_delivery
+ where analysis_run_id = %s
+ """,
+ (analysis_run_id,),
+ )
+ delivery_row = cur.fetchone()
+ max_ordinal, already_delivered = delivery_row or (0, False)
+ if not already_delivered:
+ cur.execute(
+ """
+ insert into analysis_run_outbox_delivery
+ (analysis_run_id, delivery_ordinal,
+ delivery_status_code, occurred_at)
+ values (%s, %s, 'analysis_outbox_delivered', %s)
+ on conflict do nothing
+ """,
+ (
+ analysis_run_id,
+ max_ordinal + 1,
+ datetime(2026, 1, 12, 12, 37, tzinfo=timezone.utc),
+ ),
+ )
return
cur.execute(
"""
@@ -2158,10 +2392,10 @@ def _seed_demo_run_outbox(cur, analysis_run_id) -> None:
)
if work_kind_code in ("analysis_run_tepp", "analysis_run_topic_lineage"):
claimed = datetime(2026, 1, 12, 12, 36, tzinfo=timezone.utc)
- delivered = datetime(2026, 1, 12, 12, 37, tzinfo=timezone.utc)
+ delivered_at = datetime(2026, 1, 12, 12, 37, tzinfo=timezone.utc)
else:
claimed = datetime(2026, 1, 12, 12, 32, tzinfo=timezone.utc)
- delivered = datetime(2026, 1, 12, 12, 33, tzinfo=timezone.utc)
+ delivered_at = datetime(2026, 1, 12, 12, 33, tzinfo=timezone.utc)
cur.execute(
"""
insert into analysis_run_outbox
@@ -2171,10 +2405,10 @@ def _seed_demo_run_outbox(cur, analysis_run_id) -> None:
""",
(analysis_run_id, work_kind_code, digest, claimed),
)
- for ordinal, status, occurred in (
- (1, "analysis_outbox_claimed", claimed),
- (2, "analysis_outbox_delivered", delivered),
- ):
+ deliveries = [(1, "analysis_outbox_claimed", claimed)]
+ if delivered:
+ deliveries.append((2, "analysis_outbox_delivered", delivered_at))
+ for ordinal, status, occurred in deliveries:
cur.execute(
"""
insert into analysis_run_outbox_delivery
diff --git a/tests/test_analysis_run_start.py b/tests/test_analysis_run_start.py
index 4465ebd8c..21d710245 100644
--- a/tests/test_analysis_run_start.py
+++ b/tests/test_analysis_run_start.py
@@ -552,6 +552,25 @@ def __init__(self) -> None:
assert failure == "tepp_result_not_persisted"
+def test_tepp_submit_outcome_keeps_strict_acceptance_running() -> None:
+ """A strict accepted v1 envelope is Running transport evidence, not a result."""
+
+ class _Accepting(TeppClient):
+ def __init__(self) -> None:
+ super().__init__(
+ transport=lambda payload: {
+ "contract_version": 1,
+ "run_id": "remote-run-1",
+ "run_state": "accepted",
+ "idempotency_key": payload["idempotency_key"],
+ }
+ )
+
+ status, failure = tepp_submit_outcome(_Accepting(), _tepp_request())
+ assert status == "analysis_status_running"
+ assert failure == ""
+
+
def test_tepp_anchor_projection_accepts_only_the_published_result_contract() -> None:
"""The consumer persists TEPP's exact v1 artifact, not an ad hoc nested flag."""
diff --git a/tests/test_release_identity.py b/tests/test_release_identity.py
new file mode 100644
index 000000000..0d4be3193
--- /dev/null
+++ b/tests/test_release_identity.py
@@ -0,0 +1,22 @@
+"""Release identity must stay synchronized across public package surfaces."""
+
+from __future__ import annotations
+
+import json
+import tomllib
+from pathlib import Path
+
+import lineageweave
+
+
+def test_release_versions_match() -> None:
+ """Expose one version through Python, package metadata, and the frontend."""
+ root = Path(__file__).resolve().parents[1]
+ project_version = tomllib.loads(
+ (root / "pyproject.toml").read_text(encoding="utf-8")
+ )["project"]["version"]
+ frontend_version = json.loads(
+ (root / "frontend" / "package.json").read_text(encoding="utf-8")
+ )["version"]
+
+ assert lineageweave.__version__ == project_version == frontend_version
diff --git a/tests/test_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index b25908cbe..a186e28bf 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -1,13 +1,22 @@
"""Seeded TEPP analysis runs go through tepp_client, never a local model."""
+from datetime import datetime, timezone
+
from lineageweave.tepp_client import AnalysisRunRequest, TeppClient, TeppNotAvailable
from scripts.seed_demo_data import (
+ DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY,
+ DEMO_TEPP_ACCEPTED_REMOTE_RUN_ID,
_ensure_demo_source_counts,
+ _seed_demo_tepp_accepted_run,
_seed_demo_tepp_run,
demo_source_snapshot_sha256,
+ tepp_accepted_seed_client,
+ tepp_accepted_seed_outcome,
+ tepp_accepted_seed_request,
tepp_seed_outcome,
tepp_seed_request,
)
+from backend.app.analysis_run_start import _tepp_submission
class _RecordingUnavailableClient(TeppClient):
@@ -73,6 +82,80 @@ def test_tepp_seed_outcome_does_not_treat_an_empty_envelope_as_success() -> None
assert failure == "tepp_result_not_persisted"
+def test_tepp_seed_outcome_keeps_strict_acceptance_running() -> None:
+ status, failure = tepp_seed_outcome(
+ TeppClient(
+ transport=lambda payload: {
+ "contract_version": 1,
+ "run_id": "tepp-seed-accepted-1",
+ "run_state": "accepted",
+ "idempotency_key": payload["idempotency_key"],
+ }
+ )
+ )
+ assert status == "analysis_status_running"
+ assert failure is None
+
+
+def test_tepp_submission_rejects_boolean_contract_version() -> None:
+ request = tepp_seed_request()
+ status, failure, envelope = _tepp_submission(
+ TeppClient(
+ transport=lambda payload: {
+ "contract_version": True,
+ "run_id": "tepp-seed-accepted-1",
+ "run_state": "accepted",
+ "idempotency_key": payload["idempotency_key"],
+ }
+ ),
+ request,
+ )
+ assert status == "analysis_status_failed"
+ assert failure == "tepp_result_not_persisted"
+ assert envelope is None
+
+
+def test_tepp_submission_rejects_conflicting_state_aliases() -> None:
+ request = tepp_seed_request()
+ status, failure, envelope = _tepp_submission(
+ TeppClient(
+ transport=lambda payload: {
+ "contract_version": 1,
+ "run_id": "tepp-seed-accepted-1",
+ "status": "accepted",
+ "run_state": "completed",
+ "idempotency_key": payload["idempotency_key"],
+ }
+ ),
+ request,
+ )
+ assert status == "analysis_status_failed"
+ assert failure == "tepp_result_not_persisted"
+ assert envelope is None
+
+
+def test_tepp_accepted_seed_request_shares_the_demo_snapshot() -> None:
+ request = tepp_accepted_seed_request("corp-1")
+ assert request.snapshot_id == demo_source_snapshot_sha256()
+ assert request.idempotency_key == DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY
+ assert request.model_contract_version == "tepp-lineage-criterion-v1"
+ assert request.output_profile == "lineage_pair_criterion_anchor"
+ assert request.tenant_workspace_id == "corp-1"
+ assert "theta" not in str(request.to_json()).casefold()
+
+
+def test_tepp_accepted_seed_outcome_is_running_transport_evidence() -> None:
+ status, failure, envelope = tepp_accepted_seed_outcome(tepp_accepted_seed_client())
+ assert status == "analysis_status_running"
+ assert failure is None
+ assert envelope == {
+ "contract_version": 1,
+ "run_id": DEMO_TEPP_ACCEPTED_REMOTE_RUN_ID,
+ "run_state": "accepted",
+ "idempotency_key": DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY,
+ }
+
+
def test_ensure_demo_source_counts_skips_insert_when_counts_exist() -> None:
cursor = _CountCursor(existing_counts=True)
_ensure_demo_source_counts(cursor, "snapshot-1")
@@ -105,13 +188,50 @@ def fetchone(self):
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:
+ if last.lstrip().startswith("select") and "from analysis_run where" in last:
return None
if "insert into analysis_run" in last:
return ("run-demo-tepp",)
+ if "insert into analysis_run_tepp_receipt" in last:
+ return ("run-demo-tepp",)
+ if last.lstrip().startswith("select") and "from analysis_run_outbox" in last:
+ return None
+ if last.lstrip().startswith("select") and "run.run_kind_code" in last:
+ return (
+ "analysis_run_tepp",
+ datetime(2026, 1, 12, 12, 0, tzinfo=timezone.utc),
+ "ab" * 32,
+ )
return None
+class _ExistingRunningTeppSeedCursor(_TeppSeedCursor):
+ """Model a reseed whose accepted receipt conflicts after Running."""
+
+ def fetchone(self):
+ last = self.statements[-1]
+ row = (
+ 2,
+ False,
+ "different-run",
+ "different-request",
+ "different-receipt",
+ 1,
+ "run-demo-tepp",
+ )
+ if "max(status_ordinal)" in last:
+ return row[:2]
+ if "from analysis_run_tepp_receipt" in last:
+ return row[2:5]
+ if "max(delivery_ordinal)" in last:
+ return row[5:6] + row[1:2]
+ if last.lstrip().startswith("select") and "from analysis_run_outbox" in last:
+ return row[5:6]
+ if last.lstrip().startswith("select") and "from analysis_run where" in last:
+ return row[6:7]
+ return super().fetchone()
+
+
def test_seed_demo_tepp_run_inserts_failed_tepp_not_available() -> None:
cursor = _TeppSeedCursor()
_seed_demo_tepp_run(cursor, "account-1", "corp-1")
@@ -130,3 +250,109 @@ def test_seed_demo_tepp_run_inserts_failed_tepp_not_available() -> None:
assert not any(
params is not None and "analysis_status_succeeded" in params for params in status_params
)
+ assert not any("insert into analysis_run_tepp_receipt" in sql for sql in cursor.statements)
+
+
+def test_seed_demo_tepp_accepted_run_stays_running_with_receipt() -> None:
+ cursor = _TeppSeedCursor()
+ _seed_demo_tepp_accepted_run(cursor, "account-1", "corp-1")
+ run_params = [
+ params
+ for sql, params in zip(cursor.statements, cursor.params, strict=True)
+ if "insert into analysis_run" in sql and "analysis_run_tepp" in sql
+ ]
+ assert any(
+ params is not None and DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY in params
+ for params in run_params
+ )
+ receipt_params = [
+ params
+ for sql, params in zip(cursor.statements, cursor.params, strict=True)
+ if "insert into analysis_run_tepp_receipt" in sql
+ ]
+ assert receipt_params
+ assert DEMO_TEPP_ACCEPTED_REMOTE_RUN_ID in receipt_params[0]
+ 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(
+ params is not None and "analysis_status_running" in params for params in status_params
+ )
+ assert not any(
+ params is not None and "analysis_status_failed" in params for params in status_params
+ )
+ assert not any(
+ params is not None and "analysis_status_succeeded" in params for params in status_params
+ )
+ delivery_params = [
+ params
+ for sql, params in zip(cursor.statements, cursor.params, strict=True)
+ if "insert into analysis_run_outbox_delivery" in sql
+ ]
+ assert any(
+ params is not None and "analysis_outbox_claimed" in params for params in delivery_params
+ )
+ assert not any(
+ params is not None and "analysis_outbox_delivered" in params
+ for params in delivery_params
+ )
+ assert not any("theta" in str(params).casefold() for params in cursor.params)
+
+
+def test_seed_demo_tepp_accepted_run_appends_failed_after_receipt_conflict() -> None:
+ cursor = _ExistingRunningTeppSeedCursor()
+ _seed_demo_tepp_accepted_run(cursor, "account-1", "corp-1")
+ status_params = [
+ params
+ for sql, params in zip(cursor.statements, cursor.params, strict=True)
+ if "insert into analysis_run_status_event" in sql
+ ]
+ assert status_params == [
+ (
+ "run-demo-tepp",
+ 3,
+ "analysis_status_failed",
+ "2026-01-12T12:37:00Z",
+ "tepp_result_not_persisted",
+ )
+ ]
+ delivery_params = [
+ params
+ for sql, params in zip(cursor.statements, cursor.params, strict=True)
+ if "insert into analysis_run_outbox_delivery" in sql
+ ]
+ assert delivery_params == [
+ (
+ "run-demo-tepp",
+ 2,
+ datetime(2026, 1, 12, 12, 37, tzinfo=timezone.utc),
+ )
+ ]
+ assert not any("theta" in str(params).casefold() for params in cursor.params)
+
+
+class _ConflictingReceiptCursor(_TeppSeedCursor):
+ """Model a remote-run uniqueness conflict at receipt insertion."""
+
+ def fetchone(self):
+ if "insert into analysis_run_tepp_receipt" in self.statements[-1]:
+ return None
+ return super().fetchone()
+
+
+def test_seed_demo_tepp_receipt_conflict_fails_instead_of_remaining_running() -> None:
+ cursor = _ConflictingReceiptCursor()
+ _seed_demo_tepp_accepted_run(cursor, "account-1", "corp-1")
+ 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(
+ params is not None
+ and "analysis_status_failed" in params
+ and "tepp_result_not_persisted" in params
+ for params in status_params
+ )
diff --git a/uv.lock b/uv.lock
index f94e79cf4..5b5f91f43 100644
--- a/uv.lock
+++ b/uv.lock
@@ -685,7 +685,7 @@ wheels = [
[[package]]
name = "lineageweave"
-version = "2.28.0"
+version = "2.29.0"
source = { editable = "." }
dependencies = [
{ name = "certifi" },