From 4be003eba54962ed2f15b8ccb62f136697a70746 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Mon, 31 Aug 2026 23:34:58 +0000
Subject: [PATCH 01/14] feat(analysis): seed Running TEPP accepted receipts
(v2.29.0)
Persist a Demo Corp strict accepted v1 receipt as transport evidence and
leave that run Running after make seed. Missing-transport TEPP stays
Failed. Do not treat acceptance as a calibrated result or invent a theta.
---
AGENTS.md | 10 +-
CHANGELOG.d/2.29.0-tepp-accepted-seed.md | 8 +
CHANGELOG.md | 8 +
CLAUDE.md | 15 +-
backend/app/analysis_run_start.py | 13 +-
.../0219-tepp-terminal-result-lifecycle.md | 6 +-
.../ANALYSIS_RUN_REGISTRY_REFERENCES.md | 2 +-
docs/product-technical-gap-baseline.md | 17 ++
docs/storybook-inventory.md | 1 +
frontend/package.json | 2 +-
frontend/src/App.test.tsx | 56 ++++-
frontend/src/App.tsx | 3 +-
pyproject.toml | 2 +-
scripts/seed_demo_data.py | 196 ++++++++++++++++--
tests/test_analysis_run_start.py | 19 ++
tests/test_seed_tepp_run.py | 102 ++++++++-
uv.lock | 2 +-
17 files changed, 418 insertions(+), 44 deletions(-)
create mode 100644 CHANGELOG.d/2.29.0-tepp-accepted-seed.md
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..3a3869e2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,14 @@ 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 does not name a remote 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..d97e6a064 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)
diff --git a/docs/adr/0219-tepp-terminal-result-lifecycle.md b/docs/adr/0219-tepp-terminal-result-lifecycle.md
index e100ab858..a0c94bce9 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..7dea49b45 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,22 @@
# Product & Technical Gap Baseline
+> 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.
"""
+ 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() -> AnalysisRunRequest:
+ """Build the Demo Corp TEPP request whose seed transport accepts v1."""
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"
+ return AnalysisRunRequest(
+ idempotency_key=DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY,
+ tenant_workspace_id=request.tenant_workspace_id,
+ snapshot_id=request.snapshot_id,
+ knowledge_cutoff=request.knowledge_cutoff,
+ model_contract_version=request.model_contract_version,
+ output_profile=request.output_profile,
+ )
+
+
+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,
+) -> 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()
+ )
+ return status, failure or None, envelope
def _seed_demo_tepp_run(cur, requested_by_account_id, corporate_entity_id) -> None:
@@ -1937,6 +1985,117 @@ 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) -> None:
+ """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
+ request_json = json.dumps(request.to_json(), separators=(",", ":"), sort_keys=True)
+ receipt_json = json.dumps(envelope, separators=(",", ":"), sort_keys=True)
+ 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
+ """,
+ (
+ analysis_run_id,
+ remote_run_id,
+ hashlib.sha256(request_json.encode()).hexdigest(),
+ hashlib.sha256(receipt_json.encode()).hexdigest(),
+ ),
+ )
+
+
+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),
+ )
+ status, failure, envelope = tepp_accepted_seed_outcome()
+ persist_receipt = (
+ status == "analysis_status_running"
+ and envelope is not None
+ and envelope.get("run_state") == "accepted"
+ )
+ if persist_receipt:
+ _seed_tepp_accepted_receipt(cur, run_id, tepp_accepted_seed_request(), envelope)
+ events = [
+ (1, "analysis_status_pending", "2026-01-12T12:35:00Z", None),
+ (2, "analysis_status_running", "2026-01-12T12:36:00Z", None),
+ ]
+ else:
+ 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 1 from analysis_run_status_event where analysis_run_id = %s limit 1",
+ (run_id,),
+ )
+ if cur.fetchone() is None:
+ 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),
+ )
+ _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 +2279,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
@@ -2158,10 +2318,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 +2331,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_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index b25908cbe..1874ae06e 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -1,10 +1,18 @@
"""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,
)
@@ -73,6 +81,41 @@ 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_accepted_seed_request_shares_the_demo_snapshot() -> None:
+ request = tepp_accepted_seed_request()
+ assert request.snapshot_id == demo_source_snapshot_sha256()
+ assert request.idempotency_key == DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY
+ assert request.model_contract_version == "tepp-analysis-run-v1"
+ 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,10 +148,18 @@ 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 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
@@ -130,3 +181,52 @@ 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)
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" },
From 0afd4db2d47ea8dba86ff129aca0377592899c40 Mon Sep 17 00:00:00 2001
From: Codex
Date: Tue, 1 Sep 2026 08:44:18 +0900
Subject: [PATCH 02/14] fix(analysis): fail closed on invalid TEPP receipts
Signed-off-by: Codex
---
backend/app/analysis_run_start.py | 1 +
.../0219-tepp-terminal-result-lifecycle.md | 2 +-
scripts/seed_demo_data.py | 60 ++++++++++++++-----
tests/test_seed_tepp_run.py | 52 +++++++++++++++-
4 files changed, 96 insertions(+), 19 deletions(-)
diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py
index d97e6a064..eb2d01754 100644
--- a/backend/app/analysis_run_start.py
+++ b/backend/app/analysis_run_start.py
@@ -280,6 +280,7 @@ def _tepp_submission(
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 a0c94bce9..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; seed visibility on this PR; polling remains blocked on TEPP #249
+**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
diff --git a/scripts/seed_demo_data.py b/scripts/seed_demo_data.py
index b421e9a8e..fb226ee39 100644
--- a/scripts/seed_demo_data.py
+++ b/scripts/seed_demo_data.py
@@ -24,6 +24,7 @@
import os
import time
import sys
+from datetime import datetime
from pathlib import Path
from urllib.parse import urlencode
@@ -1871,16 +1872,17 @@ def tepp_seed_outcome(client: TeppClient | None = None) -> tuple[str, str | None
return status, failure or None
-def tepp_accepted_seed_request() -> AnalysisRunRequest:
+def tepp_accepted_seed_request(
+ corporate_entity_id: str = "demo-workspace",
+) -> AnalysisRunRequest:
"""Build the Demo Corp TEPP request whose seed transport accepts v1."""
- request = tepp_seed_request()
- return AnalysisRunRequest(
+ from backend.app.analysis_run_start import tepp_run_request
+
+ return tepp_run_request(
idempotency_key=DEMO_TEPP_ACCEPTED_IDEMPOTENCY_KEY,
- tenant_workspace_id=request.tenant_workspace_id,
- snapshot_id=request.snapshot_id,
- knowledge_cutoff=request.knowledge_cutoff,
- model_contract_version=request.model_contract_version,
- output_profile=request.output_profile,
+ snapshot_sha256=demo_source_snapshot_sha256(),
+ knowledge_cutoff=datetime.fromisoformat("2026-01-12T12:00:00+00:00"),
+ corporate_entity_id=corporate_entity_id,
)
@@ -1900,12 +1902,15 @@ def _transport(payload: dict) -> dict:
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()
+ client or tepp_accepted_seed_client(),
+ tepp_accepted_seed_request(corporate_entity_id),
)
return status, failure or None, envelope
@@ -1985,13 +1990,29 @@ 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) -> None:
+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
+ 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
@@ -1999,14 +2020,14 @@ def _seed_tepp_accepted_receipt(cur, analysis_run_id, request, envelope) -> None
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,
- remote_run_id,
- hashlib.sha256(request_json.encode()).hexdigest(),
- hashlib.sha256(receipt_json.encode()).hexdigest(),
+ *receipt_values,
),
)
+ return cur.fetchone() is not None
def _seed_demo_tepp_accepted_run(cur, requested_by_account_id, corporate_entity_id) -> None:
@@ -2061,19 +2082,26 @@ def _seed_demo_tepp_accepted_run(cur, requested_by_account_id, corporate_entity_
""",
(run_id, corporate_entity_id),
)
- status, failure, envelope = tepp_accepted_seed_outcome()
+ 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:
- _seed_tepp_accepted_receipt(cur, run_id, tepp_accepted_seed_request(), envelope)
+ 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),
diff --git a/tests/test_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index 1874ae06e..94ea1563b 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -16,6 +16,7 @@
tepp_seed_outcome,
tepp_seed_request,
)
+from backend.app.analysis_run_start import _tepp_submission
class _RecordingUnavailableClient(TeppClient):
@@ -96,11 +97,31 @@ def test_tepp_seed_outcome_keeps_strict_acceptance_running() -> None:
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_accepted_seed_request_shares_the_demo_snapshot() -> None:
- request = tepp_accepted_seed_request()
+ 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-analysis-run-v1"
+ 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()
@@ -152,6 +173,8 @@ def fetchone(self):
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:
@@ -230,3 +253,28 @@ def test_seed_demo_tepp_accepted_run_stays_running_with_receipt() -> None:
for params in delivery_params
)
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
+ )
From 43b82cba0122e5ceb130f4596f6292d167e57984 Mon Sep 17 00:00:00 2001
From: Codex
Date: Tue, 1 Sep 2026 08:47:39 +0900
Subject: [PATCH 03/14] fix(analysis): reject conflicting TEPP states
Signed-off-by: Codex
---
CHANGELOG.md | 5 +++--
backend/app/analysis_run_start.py | 6 +++++-
docs/product-technical-gap-baseline.md | 1 -
tests/test_seed_tepp_run.py | 19 +++++++++++++++++++
4 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a3869e2c..62df4ad09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,8 +13,9 @@ All notable changes to this project are documented here. Format follows
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 does not name a remote run id. Never invent a theta. No polling
- until TEPP publishes its status route. Issue #277 stays open.
+ 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 /
diff --git a/backend/app/analysis_run_start.py b/backend/app/analysis_run_start.py
index eb2d01754..c20555007 100644
--- a/backend/app/analysis_run_start.py
+++ b/backend/app/analysis_run_start.py
@@ -274,7 +274,11 @@ 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 (
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 7dea49b45..0f1d819d1 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -10,7 +10,6 @@
> 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
diff --git a/tests/test_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index 94ea1563b..68cc78f4e 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -115,6 +115,25 @@ def test_tepp_submission_rejects_boolean_contract_version() -> None:
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()
From 8c8112246f70eaf4ac250f957891d8438f34d470 Mon Sep 17 00:00:00 2001
From: Codex
Date: Tue, 1 Sep 2026 12:15:58 +0900
Subject: [PATCH 04/14] fix(release): synchronize public version identity
Align the Python public version with the 2.29.0 package and frontend metadata and lock the three release surfaces with a regression test.
Signed-off-by: Codex
---
lineageweave/__init__.py | 2 +-
tests/test_release_identity.py | 22 ++++++++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
create mode 100644 tests/test_release_identity.py
diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py
index 45c371fa7..5c16e2571 100644
--- a/lineageweave/__init__.py
+++ b/lineageweave/__init__.py
@@ -130,4 +130,4 @@
"serialize_lineage_analysis_result",
]
-__version__ = "2.20.0"
+__version__ = "2.29.0"
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
From ec139215a96242e8e26f49c28ee42dead68d8c37 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 2 Sep 2026 02:53:24 +0900
Subject: [PATCH 05/14] fix(analysis): persist terminal seed conflict state
Append a Failed event when reseeding finds an existing Running history but cannot persist the accepted TEPP receipt.
Keep already-terminal histories idempotent and cover the receipt-conflict reseed path.
Signed-off-by: Codex
---
scripts/seed_demo_data.py | 22 ++++++++++++++++++++--
tests/test_seed_tepp_run.py | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/scripts/seed_demo_data.py b/scripts/seed_demo_data.py
index fb226ee39..6b1cd6fee 100644
--- a/scripts/seed_demo_data.py
+++ b/scripts/seed_demo_data.py
@@ -2108,10 +2108,19 @@ def _seed_demo_tepp_accepted_run(cur, requested_by_account_id, corporate_entity_
(3, status, "2026-01-12T12:37:00Z", failure),
]
cur.execute(
- "select 1 from analysis_run_status_event where analysis_run_id = %s limit 1",
+ """
+ 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,),
)
- if cur.fetchone() is None:
+ 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(
"""
@@ -2121,6 +2130,15 @@ def _seed_demo_tepp_accepted_run(cur, requested_by_account_id, corporate_entity_
""",
(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)
diff --git a/tests/test_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index 68cc78f4e..8038fb86a 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -205,6 +205,20 @@ def fetchone(self):
return None
+class _ExistingRunningTeppSeedCursor(_TeppSeedCursor):
+ """Model a reseed whose accepted receipt conflicts after Running."""
+
+ def fetchone(self):
+ last = self.statements[-1]
+ if "max(status_ordinal)" in last:
+ return (2, False)
+ if "from analysis_run_tepp_receipt" in last:
+ return ("different-run", "different-request", "different-receipt")
+ if last.lstrip().startswith("select") and "from analysis_run where" in last:
+ return ("run-demo-tepp",)
+ 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")
@@ -274,6 +288,26 @@ def test_seed_demo_tepp_accepted_run_stays_running_with_receipt() -> None:
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",
+ )
+ ]
+ 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."""
From edf1122ee303314c1959d9959a52bdeeaae631b7 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 2 Sep 2026 02:54:40 +0900
Subject: [PATCH 06/14] docs(gaps): refresh exact-head delivery evidence
Separate the current protected main, PR candidate, focused verification, and unverified authenticated runtime state.
Signed-off-by: Codex
---
docs/product-technical-gap-baseline.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 0f1d819d1..18f351ea4 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,5 +1,23 @@
# Product & Technical Gap Baseline
+> Exact-head loop overlay: 2026-09-02 KST. Protected `main` is
+> `3f61c8242b9c02dec307a7396e83e28f7cdd9f3d`. PR #897 is
+> `ec139215a96242e8e26f49c28ee42dead68d8c37`, mergeable, and protected by
+> normal squash auto-merge; exact-head Checks are queued and no qualifying
+> independent APPROVE exists. 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. Focused
+> regression evidence is 51 passing tests. This remains candidate evidence,
+> not a protected-main release or authenticated PostgreSQL/UI runtime claim.
+> The queue currently contains 106 open PRs and 14 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 until TEPP publishes that route.
+
> 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.
From 419d099af60df2f275fd058bf42149700709f628 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 2 Sep 2026 03:57:58 +0900
Subject: [PATCH 07/14] fix(seed): finish outbox after receipt conflict
Append the delivered event when a reseed fails a previously claimed accepted-receipt run, preventing retry of terminal work.
Signed-off-by: Codex
Commit-Message-Assisted-by: Claude (via Claude Code)
---
scripts/seed_demo_data.py | 28 ++++++++++++++++++++++++++++
tests/test_seed_tepp_run.py | 16 ++++++++++++++++
2 files changed, 44 insertions(+)
diff --git a/scripts/seed_demo_data.py b/scripts/seed_demo_data.py
index 6b1cd6fee..9f4faede4 100644
--- a/scripts/seed_demo_data.py
+++ b/scripts/seed_demo_data.py
@@ -2341,6 +2341,34 @@ def _seed_demo_run_outbox(cur, analysis_run_id, *, delivered: bool = True) -> No
(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(
"""
diff --git a/tests/test_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index 8038fb86a..e2830e015 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -214,6 +214,10 @@ def fetchone(self):
return (2, False)
if "from analysis_run_tepp_receipt" in last:
return ("different-run", "different-request", "different-receipt")
+ if "max(delivery_ordinal)" in last:
+ return (1, False)
+ if last.lstrip().startswith("select") and "from analysis_run_outbox" in last:
+ return (1,)
if last.lstrip().startswith("select") and "from analysis_run where" in last:
return ("run-demo-tepp",)
return super().fetchone()
@@ -305,6 +309,18 @@ def test_seed_demo_tepp_accepted_run_appends_failed_after_receipt_conflict() ->
"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)
From 722f709b8cd1223b99e97d59b06488ff3468f788 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 2 Sep 2026 04:00:14 +0900
Subject: [PATCH 08/14] docs(gaps): refresh accepted receipt evidence
Record the current protected main, PR head, queue, review, outbox regression, and TEPP status-exchange boundary without claiming live runtime proof.
Signed-off-by: Codex
Commit-Message-Assisted-by: Claude (via Claude Code)
---
docs/product-technical-gap-baseline.md | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index 18f351ea4..d027fbec7 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -2,21 +2,27 @@
> Exact-head loop overlay: 2026-09-02 KST. Protected `main` is
> `3f61c8242b9c02dec307a7396e83e28f7cdd9f3d`. PR #897 is
-> `ec139215a96242e8e26f49c28ee42dead68d8c37`, mergeable, and protected by
+> `419d099af60df2f275fd058bf42149700709f628`, mergeable, and protected by
> normal squash auto-merge; exact-head Checks are queued and no qualifying
-> independent APPROVE exists. Its accepted-receipt seed now also fails closed
+> independent APPROVE exists. All review threads are resolved on this head.
+> 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. Focused
-> regression evidence is 51 passing tests. This remains candidate evidence,
+> `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 106 open PRs and 14 open Issues; stacked PRs
+> 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 until TEPP publishes that route.
+> 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
From 7180068eedf40689e267e85478a9ad46981bcb82 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 2 Sep 2026 04:03:23 +0900
Subject: [PATCH 09/14] docs(gaps): separate implementation head observation
Avoid presenting a documentation commit's ancestor as the recursively unknowable current documentation SHA while retaining the exact behavior-changing head.
Signed-off-by: Codex
Commit-Message-Assisted-by: Claude (via Claude Code)
---
docs/product-technical-gap-baseline.md | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
index d027fbec7..c6ced7dc9 100644
--- a/docs/product-technical-gap-baseline.md
+++ b/docs/product-technical-gap-baseline.md
@@ -1,10 +1,14 @@
# Product & Technical Gap Baseline
-> Exact-head loop overlay: 2026-09-02 KST. Protected `main` is
+> 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. All review threads are resolved on this head.
+> 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` /
From 12caa9264b394c3b2ebd995943f3c8e42d01c0bd Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 2 Sep 2026 18:18:54 +0900
Subject: [PATCH 10/14] test(tepp): normalize reseed cursor tuple shape
Signed-off-by: Codex
---
tests/test_seed_tepp_run.py | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/tests/test_seed_tepp_run.py b/tests/test_seed_tepp_run.py
index e2830e015..a186e28bf 100644
--- a/tests/test_seed_tepp_run.py
+++ b/tests/test_seed_tepp_run.py
@@ -210,16 +210,25 @@ class _ExistingRunningTeppSeedCursor(_TeppSeedCursor):
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 (2, False)
+ return row[:2]
if "from analysis_run_tepp_receipt" in last:
- return ("different-run", "different-request", "different-receipt")
+ return row[2:5]
if "max(delivery_ordinal)" in last:
- return (1, False)
+ return row[5:6] + row[1:2]
if last.lstrip().startswith("select") and "from analysis_run_outbox" in last:
- return (1,)
+ return row[5:6]
if last.lstrip().startswith("select") and "from analysis_run where" in last:
- return ("run-demo-tepp",)
+ return row[6:7]
return super().fetchone()
From ba95f2246244dc56a30d20256f4d8834da3f0998 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 5 Sep 2026 05:52:59 +0900
Subject: [PATCH 11/14] chore(ci): adopt protected Draft admission for TEPP
lane
---
.github/workflows/tests.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 648b5e090..9a73249f6 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
- types: [opened, synchronize, reopened, closed]
+ types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
permissions:
contents: read
@@ -13,13 +13,13 @@ concurrency:
# A merged pull request can report the base ref on ``closed``. Keying PR
# events by number lets that close run cancel an older queued synchronize
# run for the same PR instead of consuming runners after the PR is closed.
- group: tests-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
+ group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
pytest:
name: Full test suite
- if: github.event_name != 'pull_request' || github.event.action != 'closed'
+ if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
services:
postgres:
@@ -65,7 +65,7 @@ jobs:
frontend:
name: Frontend lint, test, build
- if: github.event_name != 'pull_request' || github.event.action != 'closed'
+ if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
From a99499a5916ad4a0269d0ca653432bedd0330e6f Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 5 Sep 2026 05:53:14 +0900
Subject: [PATCH 12/14] chore(ci): adopt protected PROV-O Draft admission
---
.github/workflows/prov-o-contract.yml | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/prov-o-contract.yml b/.github/workflows/prov-o-contract.yml
index eae453153..a289748c9 100644
--- a/.github/workflows/prov-o-contract.yml
+++ b/.github/workflows/prov-o-contract.yml
@@ -2,6 +2,7 @@ name: PROV-O contract
on:
pull_request:
+ types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
branches: [main]
paths:
- "lineageweave/prov_o.py"
@@ -28,12 +29,13 @@ permissions:
contents: read
concurrency:
- group: prov-o-contract-${{ github.ref }}
- cancel-in-progress: true
+ group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
standards-contract:
name: Registry, inference, coverage, PostgreSQL
+ if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
services:
postgres:
From 4a9c7ee3c25a27b80f3d66c6dfe4efde017759b7 Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 5 Sep 2026 05:53:34 +0900
Subject: [PATCH 13/14] chore(ci): adopt protected ontology Draft admission
---
.github/workflows/ontology-pages.yml | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/ontology-pages.yml b/.github/workflows/ontology-pages.yml
index 05e7000bb..8cc734e27 100644
--- a/.github/workflows/ontology-pages.yml
+++ b/.github/workflows/ontology-pages.yml
@@ -2,6 +2,7 @@ name: Ontology Pages
on:
pull_request:
+ types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
branches: [main]
paths:
- "docs/ontology/**"
@@ -32,13 +33,14 @@ on:
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
jobs:
validate:
name: Validate ontology publication
- if: github.event_name == 'pull_request'
- concurrency:
- group: ontology-pages-validation-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
+ if: github.event_name == 'pull_request' && github.event.action != 'closed' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -82,9 +84,6 @@ jobs:
publish:
name: Publish ontology to GitHub Pages
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
- concurrency:
- group: ontology-pages-publication
- cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: read
From 08972d074f83a436b4837d3ea71f068c156c363c Mon Sep 17 00:00:00 2001
From: Seongho Bae
Date: Sat, 5 Sep 2026 05:53:45 +0900
Subject: [PATCH 14/14] test(ci): inherit protected Draft admission contract
---
tests/test_tests_workflow_contract.py | 58 ++++++++++++++++++++++++---
1 file changed, 52 insertions(+), 6 deletions(-)
diff --git a/tests/test_tests_workflow_contract.py b/tests/test_tests_workflow_contract.py
index e6b48461a..b0953f6f9 100644
--- a/tests/test_tests_workflow_contract.py
+++ b/tests/test_tests_workflow_contract.py
@@ -1,17 +1,63 @@
-"""Regression contracts for the repository-local test workflow."""
+"""Regression contracts for repository-local workflow concurrency."""
from pathlib import Path
-_WORKFLOW = Path(__file__).parents[1] / ".github" / "workflows" / "tests.yml"
+_WORKFLOW_DIRECTORY = Path(__file__).parents[1] / ".github" / "workflows"
+_PULL_REQUEST_GROUP = (
+ "group: ${{ github.workflow }}-${{ github.repository }}-"
+ "${{ github.event_name == 'pull_request' && "
+ "github.event.pull_request.number || github.run_id }}"
+)
+_PULL_REQUEST_CANCELLATION = (
+ "cancel-in-progress: ${{ github.event_name == 'pull_request' }}"
+)
+_PULL_REQUEST_TYPES = (
+ "types: [opened, synchronize, reopened, ready_for_review, "
+ "converted_to_draft, closed]"
+)
+_DRAFT_ADMISSION = "github.event.pull_request.draft == false"
def test_pull_request_concurrency_survives_closed_ref_change() -> None:
"""Key synchronize and closed events by PR number so close cancels stale work."""
- workflow = _WORKFLOW.read_text(encoding="utf-8")
+ workflow = (_WORKFLOW_DIRECTORY / "tests.yml").read_text(encoding="utf-8")
- assert "types: [opened, synchronize, reopened, closed]" in workflow
- assert "group: tests-${{ github.event.pull_request.number || github.ref }}" in workflow
- assert "cancel-in-progress: true" in workflow
+ assert _PULL_REQUEST_TYPES in workflow
assert workflow.count("github.event.action != 'closed'") == 2
+
+
+def test_pull_request_workflows_cancel_only_superseded_same_pr_runs() -> None:
+ """Scope PR cancellation by workflow, repository, and pull request number."""
+
+ for workflow_path in sorted(_WORKFLOW_DIRECTORY.glob("*.yml")):
+ workflow = workflow_path.read_text(encoding="utf-8")
+ if "pull_request:" not in workflow:
+ continue
+ assert _PULL_REQUEST_GROUP in workflow, workflow_path.name
+ assert _PULL_REQUEST_CANCELLATION in workflow, workflow_path.name
+
+
+def test_draft_pull_requests_do_not_consume_repository_local_runners() -> None:
+ """Cancel stale draft runs while deferring expensive jobs until review readiness."""
+
+ expected_draft_guards = {
+ "tests.yml": 2,
+ "prov-o-contract.yml": 1,
+ "ontology-pages.yml": 1,
+ }
+ for workflow_name, expected_guard_count in expected_draft_guards.items():
+ workflow = (_WORKFLOW_DIRECTORY / workflow_name).read_text(encoding="utf-8")
+ assert _PULL_REQUEST_TYPES in workflow, workflow_name
+ assert workflow.count(_DRAFT_ADMISSION) == expected_guard_count, workflow_name
+
+
+def test_ontology_publication_runs_are_not_cancelled() -> None:
+ """Keep publication runs isolated and non-cancelling outside pull requests."""
+
+ workflow = (_WORKFLOW_DIRECTORY / "ontology-pages.yml").read_text(
+ encoding="utf-8"
+ )
+ assert "github.run_id" in workflow
+ assert _PULL_REQUEST_CANCELLATION in workflow