IDKMesh

ADR-0024 — Serve Retained Run Evidence, and Read Product Spine Runs From a Mixed Store

Status: Accepted Date: 2026-10-02

Context

Issue #739 (API-4) lists GET /api/v1/runs/{run_id}/evidence and /decisions. ADR-0019 reserved both suffixes. Memory and the 2026-09-27 notes concluded that neither was buildable because “no store maps a digest back to content”. Reading the code shows that is only half true:

Decision

  1. GET /api/v1/runs/{run_id}/evidence serves the retained report, after verifying canonical_digest(report) == projection.evidence_report_digest and that the projection is a valid Product Spine run. A mismatch answers 500 evidence_integrity_error and the report is never served. Response: {api_version, schema_version, kind: "idkmesh-control-tower-run-evidence- response", ok, run_id, evidence_report_digest, evidence_report}.
  2. Errors are distinct. Unknown run: 404 run_not_found. A run that exists but has no retained evidence (a CLI-created run, or a run that has not reached evidence_ready): 404 evidence_not_available, so a client can tell “no such run” from “no evidence yet”.
  3. /decisions is not built. It stays a plain 404 not_found until #740 provides a decision store and accountable-principal authentication. A decision is a governance act; this ADR does not widen anyone’s authority.
  4. Product Spine run reads accept the idempotent offline result kind. _restore accepts product-spine-idempotency-result rows (checking their idempotency_request_digest against the atomic record) in addition to CLI rows. Such a run reports the atomic record’s request digest as create_request_digest and its stored idempotency key.
  5. GET /api/v1/runs lists only Product Spine runs. list_runs takes an explicit set of row kinds and returns only those; admission-only, execution-error and GitHub rows are excluded, so a mixed store no longer breaks the list or its keyset pagination. This is a stated filter, not a silent one: the specification names which kinds a run list contains.
  6. No fabrication. Nothing is synthesised for a run that has no evidence; the report is returned byte-for-byte as retained.

Implementation contract

Consequences

Positive

Costs

Alternatives considered

Keep requiring a separate evidence content store

Rejected. The content is already retained and digest-verified in the run row; a second store would duplicate it.

Return the report without re-verifying the digest

Rejected. The retained row is persisted state; fail-closed verification costs one canonical hash and prevents serving a tampered or truncated report.

Make the run list skip unrestorable rows silently

Rejected. A silent filter by exception hides corruption. Filtering by an explicit, documented kind set is deterministic; a row of a listed kind that fails to restore still fails loudly.

Build /decisions and the human-decision API now

Rejected. It needs a decision store, an Idempotency-Key contract and an authenticated human or governance principal (#670, enterprise identity). A local session token is not an accountable person, so this is a governance gate, not an implementation detail.

Revisit conditions

Revisit when #740 lands a decision store, when another writer starts retaining evidence, or when a run with a restorable kind other than the two listed appears.

Update 2026-10-02 (static review)

A read-only static review of the implementation, before any test had run, found that decision 4 (“Product Spine run reads accept the idempotent offline result kind”) had an unintended consequence, and tightened decision 1. The decisions are unchanged; reading is not controlling.