Chain of Custody

Composition partially resolved 7 invariants TLA+ model buggy twin ×2 regulated composes 1 atom + 1 substrate
Derived card — generated from the corpus (status line, invariant headers, sibling formal-model files, Composes edges). The specification below is canonical. composition graph →
Table of contents
  1. Chain of Custody
    1. Summary
    2. Intent
    3. Composes
    4. Composition logic
      1. Composition state
      2. Configuration
      3. Primitive policies
      4. Action wiring
        1. originate_custody
        2. transfer_custody
        3. transform_custody
        4. disclose_custody
        5. archive_custody
        6. verify_custody
        7. read (passthrough)
      5. The load-bearing wiring decision — custody-event ⇒ audit-event binding
    5. Composition-level invariants
    6. Examples
      1. Walkthrough — pharmaceutical chain of custody under 21 CFR Part 211
      2. Legal evidence — physical exhibit at trial under FRE 901(b)(9)
      3. Rejection path — transfer by non-current custodian
      4. Rejection path — custody action on an Archived chain
      5. Failure path — custody step whose audit write fails, recovered
      6. DEA controlled substance — 21 CFR Part 1304 inventory custody
      7. Regulated adversarial scenarios
    7. Generation acceptance
      1. Record checks
      2. External checks
    8. Non-goals and edge cases
    9. Terms
      1. Originate Custody
      2. Transfer Custody
      3. Transform Custody
      4. Disclose Custody
      5. Archive Custody
      6. Verify Custody
      7. Custody Proof
      8. Continuity Check
      9. Overall Verdict
      10. Custody Proof Complete
      11. Custody Proof Incomplete
    10. Standards references
    11. Status
    12. Ledger
    13. Decisions

Summary

Chain of Custody is a regulated composition (a spec that wires two or more atoms — freestanding, self-contained pattern specs — together) that solves the chain-of-custody problem no single atom solves alone: proving, from the records alone, that an artifact moved through an unbroken sequence of verified, non-repudiable custodians, that no entry was tampered with, and that the chain was kept for the required period. It wires two constituents: Provenance (the append-only custody chain with structural continuity — one current custodian at all times, hand-to-hand transfers) and the Audit Trail substrate (the tamper-evident — designed so unauthorized changes are detectable — regulated-audit substrate that attribution-stamps and seals every custody event via the Event Log, Actor Identity, Retention Window, and Tamper Evidence atoms it contains).

The composition’s defining emergent guarantee is records-alone custody proof: a single query, verify_custody(chain_id, original_event_payloads), returns the full ordered chain with, per entry, the verified attribution of the acting custodian (who authorized this step), the tamper-evident seal status (has the entry been altered), and the retention state (is the entry still held lawfully) — plus a continuity check confirming that every transfer’s outgoing custodian equals the prior holder. This is the structural answer to the regulator’s, judge’s, or auditor’s four questions — who held it, was the transfer unbroken, was the record altered, was it kept long enough — from the records alone, without developer narration. Neither Provenance alone nor Audit Trail alone produces this answer; the composition is what wires them into a single custody-proof surface.

Its most common uses are pharmaceutical chain of custody under FDA (US Food and Drug Administration) 21 CFR (Code of Federal Regulations) Part 211 and DEA (US Drug Enforcement Administration) 21 CFR Part 1304 (controlled substance inventory), physical-evidence authentication under FRE (Federal Rules of Evidence) 901(b)(9), financial instrument custody records under SEC (US Securities and Exchange Commission) Rule 17a-4, and digital-file chain of custody under ISO (International Organization for Standardization) 23081 (records-management metadata). Any system that must prove, from records alone, that every step in an artifact’s journey was attributed, sealed, and retention-governed is a candidate for this composition.


Intent

Every domain that must account for an artifact’s journey faces the same four-part requirement: the chain must be unbroken (no gap in custody, hand-to-hand transfers with no unattributed interval), the custodians must be verified (each hand-off attributed to a real actor, not just an opaque reference), the chain must be tamper-evident (any after-the-fact rewrite detectable from the records alone), and the chain must be retention-governed (retained for its regulatory lifetime and lawfully destroyable with a defensible record of destruction). No single atom satisfies all four. Provenance supplies the first. Audit Trail supplies the third and fourth, and transitively the second via Actor Identity. But neither provides the full custody-proof surface until they are wired together: Provenance does not verify its opaque custodian_ref values and does not seal its chain; Audit Trail does not know what a custody chain is or that its events need to be tied to custody entries. The wiring is this composition.

The cross-domain structural identity is the composition’s thesis. Under FDA 21 CFR Part 211 (Current Good Manufacturing Practice for Finished Pharmaceuticals), a pharmaceutical manufacturer must demonstrate an unbroken, attributed, tamper-protected, retention-compliant chain of custody from manufacture through distribution to dispensing. Under FRE 901(b)(9) (Authentication by Process or System), an officer presenting physical evidence at trial must authenticate the exhibit via an unbroken custody chain in which every handler is identified. Under DEA 21 CFR Part 1304 (Controlled Substance Inventory Records), a complete, accurate record of every change of custody for a controlled substance is a legal obligation. The structural form is identical across all three domains: one artifact, one current custodian at all times, hand-to-hand transfers, each hand-off attributed and verifiable, the record sealed against tampering, and the record retained for its regulatory lifetime. One grounded composition satisfies all three.

This is a composition, not a new primitive. Provenance and Audit Trail (with its constituent atoms, Event Log, Actor Identity, Retention Window, and Tamper Evidence — reached transitively) are unchanged. The composition is the wiring that makes them coherent as a single chain-of-custody surface. It introduces emergent actions — Originate Custody, Transfer Custody, Transform Custody, Disclose Custody, Archive Custody, Verify Custody — that belong to no single constituent and exist only because the two are wired together. Verify Custody in particular belongs to neither constituent alone: Provenance can verify structural continuity but cannot verify attribution or tamper-evidence; Audit Trail can verify attribution and tamper-evidence but does not know which events constitute a custody chain. The composition is the layer that answers: is this custody chain complete, attributed, sealed, and within its retention horizon?

What the composition is not: it is not a DAG-model (DAG — directed acyclic graph) provenance tracker (the linear single-artifact constraint is inherited from Provenance); it is not a multi-party simultaneous custody surface; it is not the legal-hold suspension layer over custody chains (that is Legal Hold / Defensible Retention); it is not the artifact-registry layer (validating that artifact_ref names a real artifact in the host system is the host’s obligation); and it is not the clock-authority layer (inherited from Audit Trail). Each is named explicitly in Edge cases.


Composes

  • Provenance — the structural custody chain. Supplies the core guarantee: custody continuity (Invariant 4 — one current custodian at all times, every transfer is hand-to-hand with from_custodian_ref read from chain state). The composition calls originate, transfer, transform, disclose, archive, and read. Provenance is the subject-scoped chain that Event Log cannot replicate; the composition does not re-derive its custody-continuity guarantee but adds attribution verification and tamper-evidence coverage to each entry via the Audit Trail substrate wiring.

  • Audit Trail — the regulated-audit substrate. Every custody action the composition exposes records here as two AuditTrail.record_action calls — an intent record written before the Provenance write and an outcome record written after it (Authentication precedes commitment) — each producing an Event Log entry (append-only, totally ordered), an Actor Identity attestation (binding the acting custodian’s custodian_ref to a verified credential), a Retention Window record (for the audit event), and a Tamper Evidence seal per the cadence. The doubling of substrate writes per custody action is a real deployment cost, stated here where the substrate wiring is introduced rather than left to be discovered. The composition maintains exactly one Audit Trail instance configured with the host’s regulatory retention policy for the custody audit events. Actor Identity, Tamper Evidence, Retention Window, and Event Log are reached transitively through Audit Trail — the composition does not maintain separate instances of those four atoms at this layer. The Audit Trail’s record_action is the mechanism by which each custody entry receives an attributed, sealed, retention-governed audit event, closing the gap Provenance’s custodian_ref Edge case named: “Non-repudiable custodian identity → Actor Identity.”

The Event Log, Actor Identity, Retention Window (for audit events), and Tamper Evidence atoms are reached transitively through Audit Trail; the composition does not maintain separate instances of those atoms at this layer. This follows the Compositions of compositions convention (see spec-format.md §Compositions of compositions): naming Audit Trail as the substrate is what satisfies the Actor Identity, Tamper Evidence, and Retention Window requirement — they are reached transitively, not maintained as separate instances at this composition layer.


Composition logic

Composition state

The composition owns one piece of emergent state that wires the two constituents into a queryable, records-alone-defensible custody-proof surface:

  • entry_to_event — map from Provenance entry_id to the Audit Trail event_id that attributes, seals, and retention-governs that custody entry. This is the traversal backbone: given any custody entry, the composition can locate its corresponding attributed audit event, verify its attestation (who authorized this step), verify its seal (was the entry tampered with), and confirm its retention state (is it still lawfully held). Contract classification: derived index (execution-contract.md §Composition state; reclassified 2026-06-11, closing part of the Invariant-4 coverage-gap finding). Every fact in the map is reconstructible from constituent stores — the named rebuild procedure enumerates the Audit Trail substrate’s events whose data.entry_id names an entry of the chain (every custody outcome record_action carries entry_id in its data payload; intent records carry the invocation’s parameters and no entry_id, so the rebuild’s filter excludes them by construction — which is what keeps them from corrupting the derived index) and re-keys them by that field. The Contract’s obligations follow: the map sits outside the per-action atomicity surface — the truth-bearing atomicity obligation (Invariant 4) binds the Provenance entry and the audit event, and the map’s population, occurring only after both truth-bearing writes succeed (the Provenance entry and the outcome audit event — the intent record is a third audit write, deliberately outside this surface), is evidence that obligation was met, never a peer write whose failure the compensation protocol must handle; a missing or lost map entry is a rebuild trigger, not data loss — reads consult the index with rebuild-on-miss semantics, and an entry that still lacks an audit event after rebuild is the real orphan (Invariant 4’s surfaced finding); and the map claims no cross-constituent transactional consistency. Never modified after insertion. Collision rule for the rebuild: where the enumeration finds two outcome events carrying one entry_id, the binding is the first by sequence_number and the second is a binding-duplicate(entry_id) finding — reachable only by a second writer landing an outcome for one entry, which the one-writer rule of the partial-failure edge case forecloses in a conforming deployment; the rebuild reports it and never silently re-keys. An entry_id whose rebuild yields no audit event is the partial-failure orphan surfaced per the Cross-store consistency under partial failure edge case; an outcome event_id carrying a data.entry_id not present in Provenance is a structural finding (Invariant 4’s inverse orphan — unreachable through this composition’s wiring); an intent record carries no data.entry_id and is never an inverse orphan. The purged subset is the classification’s one exception, and it is truth-bearing: the substrate’s cascade destroys a purged event’s data entirely, and its destruction record preserves (event_id, attestation_id) only — so for a Purged event, the entry_id → event_id join survives in no constituent store and the derived-index rebuild reconstructs nothing. Map entries whose events are Purged therefore carry their own durability obligation: the binding for an event entering the purge cascade must survive — either the map’s own entry is durable (a named host obligation), or the deployment persists the entry_id into a surviving record at purge time (alongside the destruction record). A lost purged-subset entry is data loss, not a rebuild trigger, and Verify Custody’s binding determination is retention-aware because of exactly this (its step 3a).

The Provenance chain state (entries, current_custodian, chain lifecycle) and the Audit Trail substrate state (event_to_attestation, event_to_retention, seal_coverage, sealed_through) are owned by their respective constituent instances. The composition does not duplicate them; it indexes into them via entry_to_event.

Configuration

  • audit_trail_retention_policy — the policy reference (or content-derived policy selector) configured on this composition’s single Audit Trail instance, governing the lifetime of the custody audit events (the record_action entries in the Event Log, not the Provenance chain entries themselves). This is set once on the Audit Trail instance; Audit Trail’s record_action takes no per-call retention argument — every custody-audit event this composition writes inherits this configured policy. The custody audit record should persist at least as long as the Provenance chain it describes, and typically longer for regulatory defensibility after the artifact’s chain has been archived. For pharmaceutical deployments under 21 CFR Part 211 the policy must encode at least the predicate-rule retention minimum; for legal-evidence deployments under FRE 901(b)(9) the policy must outlast the matter’s litigation horizon. Multi-jurisdiction policy reconciliation (when FDA, DEA, and court-rule retention obligations overlap) is a Policy Reconciliation composing pattern concept; this composition takes the reconciled policy_ref as input.

  • seal_cadence — inherited from the Audit Trail substrate’s configuration (per-event, interval-based, or on-demand). For regulated chain-of-custody deployments, per-event or tight interval-based cadences are recommended: every custody event should be covered by a seal as promptly as possible because the unsealed tail is the window during which tampering is structurally undetectable at verification time. Pharmaceutical 21 CFR Part 11 ALCOA (Attributable, Contemporaneous, Original, Accurate) and legal-evidence FRE 901(b)(9) authentication both benefit from a narrow unsealed-tail window. The deployer configures seal_cadence on the Audit Trail instance; this composition does not impose an override, but the custody-proof verification outcome (Invariant 2 — tamper-evident coverage) names the unsealed-tail policy consequence explicitly. The cadence also sets the shape of every verification presentation. Verify Custody presents, per entry, the payloads of the entry’s whole covering range (the substrate’s rule — Audit Trail Invariant 7), so under an interval cadence one custody entry’s check re-presents every seal-mate, intent records and unrelated actions included, and a lawful purge of any seal-mate leaves the survivors unverifiable(partially-purged-coverage) permanently absent a composed Seal Lifecycle pattern. Per-event cadence is therefore the recommended wiring for a chain-of-custody instance: each presentation is a singleton and each purge isolates to its own event. An interval cadence remains admissible where the deployment accepts the range-shaped presentation and the standing partial-coverage answer, or composes Seal Lifecycle; the presentation is range-shaped under every cadence, and the recommendation chooses the cadence under which the range is one event.

  • custody_completion_bound — the deployment-declared maximum duration of a custody action between its intent record and its outcome record, read against the substrate’s clock at the intent’s recorded_at. It is the lower edge of the reconciliation scan: an intent younger than it may belong to an invocation still between its Provenance write and its outcome append, and a compensation fired at it would append a second outcome for one entry — the duplicate Invariant 4’s exactly-one forbids. The upper edge is audit_trail_retention_policy’s horizon: past it the intent and outcome payloads are destroyed, the entry’s entry_to_event binding is the purged subset (Composition state), and the scan reads that subset and re-emits nothing — a Provenance entry whose events aged out is binding-unknown(purged-horizon) at Verify Custody step 3a, never an orphan. Default: none.
  • compensation_window and reconciliation_cadence — the deployment-declared duration within which an orphan entry must have its outcome event landed or be escalated as an unresolved finding, and the interval at which the reconciliation scan runs in addition to its mandatory run at restart. The inequality that makes the window meetable is custody_completion_bound + reconciliation_cadence + outcome_write_latency < compensation_window, strictly — an orphan created at t is invisible to the scan until t + custody_completion_bound, the next run is at most a cadence later, and the compensating write lands a latency after that; cadence no longer than the window is satisfied by a deployment that breaches on every orphan. outcome_write_latency is the deployment’s disclosed bound on one record_action landing. The three terms are checked at instance start, and a deployment whose knobs fail the inequality refuses to start (§Liveness is arithmetic). Invariant 4’s liveness arm spends the window.
  • index_durability — the durability the deployment owes the entry_to_event store, as an ordering: at least as durable as the Provenance chain store it keys, because the purged subset — bindings whose events have entered the purge cascade — is the only carrier of those bindings (Composition state), and a lost entry there is data loss. This is the “named host obligation” that bullet already spends, given its entry.
  • recovery_identity — a deployment-provisioned actor_ref + credential pair under which every write the reconciliation scan makes is attested — the custody.recovery_intended record it writes before compensating and the compensating outcome event itself — and under which an in-invocation compensation re-attests when the outcome record’s write is deterministically rejected. The in-invocation residual is narrow and should be read as narrow: the credential was validated at the intent record before anything committed, so the routine bad-credential case never reaches here — what remains there is exactly one case: a credential valid at the intent record and revoked before the outcome record lands — a mid-flight revocation. The scan-side use is not narrow: a crash between the two truth-bearing writes leaves an orphan whose custodian is gone and whose credential was never persisted, so the scan can attest only as this identity. invalid-request is deliberately not part of this residual: the step-1 caps foreclose it by construction, and re-attestation could not clear it anyway (it changes the attesting actor, never the payload the substrate refused), so an observed invalid-request at the outcome record is a mis-derived deployment cap and a pageable fault rather than a compensation case. Both leave a committed Provenance entry whose audit is owed and unlandable under the custodian’s own credential (see the rejection-mapping rule and the Cross-store consistency under partial failure edge case). Retrying the original call cannot land a deterministic rejection, so the compensation re-attests under this identity; events so attested — and only events so attested — carry cascade_recovery = true and name the original custodian_ref in data, keeping attribution honest — the record shows which operational identity attested the compensation and which custodian performed the custody step. Mirrors the composition-actor convention established in Multi-Party Approval’s Configuration. The intent record — not an upstream pre-check — is now the control against unauthenticated custody changes, so a deployment may exercise this identity only in the narrowed mid-flight case; it must still be declared wherever that arm is reachable. (Added 2026-06-11 with the Invariant 4 safety + liveness restatement — the liveness arm’s deterministic-rejection leg rests on this declared capability.)
  • chain_serialization — an instance capability requirement: the deployment-supplied mutual exclusion keyed by chain_id under which every custody-state-changing invocation runs its whole critical section — from Transfer Custody’s resolution read (or the intent record, for the other four actions) through its outcome record and its in-invocation retries — and which the reconciliation scan takes for every chain it examines, across its pre-check and its compensating write. No constituent grants it: Provenance serializes its own writes per chain and sends a read-then-write section to the host (Concurrent transfer attempts), and Audit Trail serializes nothing across calls; a section spanning several constituent calls is the deployment’s, and this entry is where it is declared (§Capability provenance, the multi-call-section tell). Its semantics are part of the declaration: the section is released when its holder returns or dies; where the host implements it as a lease, the lease is exactly custody_completion_bound long — no shorter, so a conforming invocation is never evicted mid-section; no longer, so a stalled-but-alive holder blocks the scan for at most the bound, which is already the liveness inequality’s first term — and its expiry is the invocation’s terminus: an invocation whose lease expired has yielded, makes no further write until it re-takes the section, re-runs its outcome pre-check under it, and then only adopts an outcome the scan landed (proceed as landed) or, finding none and being past the bound, returns rejected(recording-failure(outcome)) and leaves the write to the scan. Every write after an invocation’s first is made only while the section is held. Default: none — a deployment that supplies no section has no conforming reconciliation.
  • outcome_retry_attempts — the number of times an invocation re-attempts its outcome record_action on a transient recording-failure(step-2 | step-3) before it returns rejected(recording-failure(outcome)) and yields the orphan to the scan. This composition samples no clock, so its retry terminus is counted rather than timed; it is the invocation’s half of the one-writer-per-entry rule (partial-failure edge case). Default: none.
  • genesis_lookup — an instance capability requirement: a deployment-supplied lookup from an artifact_ref to the chains originated for it. Provenance’s read surface is keyed by chain_id alone and declares no lookup by artifact — its own edge case sends that to a host-side derived index — so the reconciliation scan’s genesis leg, which must find a chain whose intent record could name no chain_id, rests on this capability and on nothing the constituent grants. A deployment declares it or does not; where it does not, the genesis leg is downgraded to surfaced (partial-failure edge case) and Invariant 4’s liveness arm says so. Default: not supplied.
  • intent_candidates_cap — the most intent_event_candidates a compensating outcome event may name. The step-1 caps size the outcome payload against the substrate’s payload_cap; the compensation payload is the outcome’s plus the candidate set, and this cap is what keeps the compensation inside the same envelope (§An outcome is sized before the intent). An orphan whose candidate set exceeds the cap is escalated as an unresolved finding rather than compensated. Default: none.

Primitive policies

The composition takes string-typed inputs at its action boundaries; each is validated either at this layer or by a constituent.

  • chain_id — opaque, system-generated by Provenance’s originate (wrapped by Originate Custody). Supplied by callers to address an existing chain. The composition does not normalize, case-fold, or trim; chain_id equality is opaque byte-identity. An unknown chain_id yields not-known from the relevant action; the composition propagates Provenance’s not-known at this boundary.
  • entry_id — opaque, system-generated by Provenance per entry. Returned in action results; used as the key in entry_to_event. Byte-identity equality as a map key; never normalized.
  • event_id — opaque, system-generated by Audit Trail’s Event Log. Returned in action results; stored as the value in entry_to_event. Byte-identity equality; never normalized.
  • artifact_ref — opaque reference to the artifact whose custody this chain tracks. Must contain at least one non-whitespace character (Provenance’s requirement); validated by the constituent at originate. The composition propagates Provenance’s invalid-ref. This composition does not validate artifact_ref against an external artifact registry; the host system is responsible for ensuring the reference is meaningful.
  • custodian_ref — opaque reference to the custodian performing or receiving a custody action. Must contain at least one non-whitespace character; validated by Provenance at each action — archive included, whose invalid-ref arm the constituent added in its Final Critique 6 closure, discharging this composition’s previously-routed overstatement. The composition maps this to the actor_ref passed to AuditTrail.record_action — the two are the same identity: the custodian is the actor whose credential the Audit Trail binds. The distinction between the opaque custodian_ref (as recorded in the Provenance chain) and the actor_ref / credential pair (as consumed by Audit Trail’s Actor Identity) is the acting-custodian attribution rule: see The load-bearing wiring decision below for the per-action custodian-to-actor mapping.
  • credential — opaque credential material consumed only by AuditTrail.record_action (specifically by the Actor Identity inside the substrate). Boundary shape-validation: non-null and non-empty — the one check step 1 makes; the content is never inspected here. The substrate’s Actor Identity validates the material at the intent record, before any constituent call that commits, and surfaces invalid-credential, which the composition returns as rejected(invalid-credential) with nothing committed and no custody moved, transformed, disclosed, or closed (Invariant 7). At the outcome record the same arm can only report, not refuse — see the rejection-mapping rule below.
  • Length caps (payload-safety). Every caller-supplied string that rides an audit payload — artifact_ref, custodian_ref / to_custodian_ref, recipient_ref, transformation_descriptor, metadata — is additionally capped at step 1 of each action, with the composition-layer caps set (deployment configuration) so the maximal audit payload assembled from capped inputs fits the wired substrate’s configured payload_cap. An over-cap input returns the field’s format rejection (invalid-ref for references, invalid-descriptor for the descriptor) before any write at this layer, the intent record included — which matters, because the intent record is now the first write and an uncapped field would otherwise reach it and draw a substrate invalid-request where this layer promises a typed rejection. This cap is load-bearing for Invariant 4’s liveness, and its mechanism is now doubled rather than single: the cap gives the cheap, correctly-typed rejection at step 1, and the intent record is a second, structural net — an oversized descriptor that somehow passed the cap would fail at the intent record before Provenance commits at all. The permanent uncompensatable orphan the cap was written to foreclose (an oversized descriptor committing irreversibly, then every audit attempt including the recovery_identity compensation rejecting deterministically against the substrate’s cap) is therefore now foreclosed twice over, and the old single-net causal chain no longer describes a reachable path.
  • Boundary priority note. Step 1’s format checks run before any constituent call — the intent record and Transfer Custody’s resolution read included, both of which are constituent calls that now precede the Provenance write — so a call carrying both a malformed input and an unknown chain_id reports the format rejection first — the composition’s boundary deliberately inverts Provenance’s own existence-first priority for boundary checks only (no constituent is consulted for malformed input); within the constituent, the atom’s declared priority stands unchanged. The inversion is declared and stable.
  • genesis_type — must be exactly originated or received (Provenance’s rule). Propagated from Provenance’s invalid-genesis-type if violated.
  • transformation_descriptor — non-empty, non-whitespace-only string describing a transformation applied by the current custodian. Validated by Provenance; invalid-descriptor propagated to the composition boundary.
  • recipient_ref — opaque reference to the party receiving a disclosure. Must contain at least one non-whitespace character; validated by Provenance. Propagated as invalid-ref if violated.
  • metadata — optional opaque payload at Originate Custody. Passed through to Provenance; no composition-layer validation.

record_action rejection-mapping rule — by call position. Every custody-state-changing action makes two AuditTrail.record_action calls: an intent record before the Provenance write and an outcome record after it (Authentication precedes commitment, Action wiring). The substrate’s rejection taxonomy (invalid-credential | invalid-request | recording-failure(step) — the failure arm carries the failed substrate step as its payload) therefore maps by the call’s position relative to the constituent write, not uniformly.

  • At the intent record — nothing has committed, so every arm is a clean pre-state rejection. invalid-credentialrejected(invalid-credential): the caller’s credential did not validate against the actor registry for the acting custodian, and the composition refuses the custody change with nothing written anywhere. invalid-requestrejected(invalid-request): a deployment fault (a misconfigured substrate retention policy, or a constructed payload over the substrate’s cap — nearly unreachable here, since this layer’s own length caps refuse over-size inputs earlier with a better-typed invalid-ref / invalid-descriptor), pageable and never retried, because a retry re-sends the identical payload. recording-failure(step)rejected(recording-failure(intent)): the one genuinely retryable arm — nothing committed, so the caller may retry the whole action.

  • At the outcome record — the Provenance entry is immutable once committed and cannot be rolled back, so no arm can refuse the act, only report it — and one arm reports that it succeeded. recording-failure(step-4) means the outcome event is appended and attested, only its retention placement failed: the invocation reads the event’s id back through the substrate’s declared enumeration selecting action_ref and this intent_event_id, binds entry_to_event, and returns success with a hard alert on the unretained event — never a retry, which would mint a duplicate custody.* event. invalid-request has two sources the token does not distinguish — the payload cap, foreclosed by the step-1 caps and so a mis-derived deployment cap if observed (nothing appended), and the substrate’s own retention-configuration fault (the event appended) — so the same read-back decides: found → as step-4; absent → a pageable deployment fault that cannot recur-and-clear, the orphan below. recording-failure(step-2 | step-3)rejected(recording-failure(outcome)) and the orphan — a Provenance custody entry with no corresponding outcome event and no entry_to_event binding — surfaced per the Cross-store consistency under partial failure edge case; the in-invocation retry loops on this arm only, and for at most outcome_retry_attempts (Configuration) before the invocation yields the orphan to the scan. The position rides the exported code, recording-failure(intent | outcome) on every signature: intent tells the caller nothing committed and the whole action may be retried; outcome tells the caller the custody entry exists and a retry of the action would commit a second one — the caller waits for the scan, or reads the chain (§A composition’s own rejection arm carries the retry bit). invalid-credential — reachable now only as a mid-flight revocation, since the same credential validated at the intent record moments earlier — is answered by re-attestation under the declared recovery_identity.

This composition enforces the seam at its own boundary. Because the intent record precedes every Provenance call that commits, the acting custodian’s credential is validated before custody moves, transforms, is disclosed, or is terminally closed; invalid-credential is a clean pre-state refusal and is a declared returnable code on all five signatures below. An upstream Actor Identity pre-check is therefore defence in depth, not a control this composition depends on. Authentication is not inheritable across a composition boundary and this surface is independently callable, so its own verification is the direct-call path’s only defence. recovery_identity remains declared for the narrowed residual described above, where a compensating attestation is the backstop for a committed entry’s owed audit — never, as it once was, the backstop for an unauthenticated custody move.

No primitive is case-sensitivity-normalized at the composition layer; deployments wanting normalization wire it at the calling layer before invoking composition actions.

Action wiring

The composition exposes six orchestrating actions and one read passthrough. Every custody-state-changing action records in Audit Trail; read passes through to Provenance.read without recording. All entry_to_event insertions occur only after both the Provenance write and the outcome Audit Trail write succeed — the population of the map is the evidence that the atomicity obligation was met.

Authentication precedes commitment — the intent record. Every custody-state-changing action opens with an intent record: an AuditTrail.record_action naming what the invocation is about to do, written before the Provenance call that commits. It is one call doing two jobs. First, it is where the acting custodian’s credential is verified — the substrate validates it against the actor registry’s public material for the supplied actor_ref inside record_action (Actor Identity’s own guard) — so custody is never moved, transformed, disclosed, or terminally closed before that identity is checked; invalid-credential is a clean pre-state refusal with nothing written anywhere. Second, it is a durable record of the attempt: an intent record with no outcome record names an invocation that committed nothing, committed and failed to record, or died in between — or, for a transfer alone, one whose committed entry attributes a different releasing custodian than the intent authenticated (a principal divergence) — which the reconciliation scan resolves against the Provenance chain. The intent record carries the invocation’s parameters, never a constituent-minted id (none exists yet) and no caller-composed timestamp (this composition samples no clock); the outcome record carries the entry_id, the result, and intent_event_id back to its intent record, and the pair is what the audit reads.

The intent-to-outcome relation, declared. Every custody outcome event names exactly one intent event through intent_event_id; every intent event is named by at most one outcome event. The zero case on the second side is not a defect and is the relation’s whole point: an unmatched intent record is an authenticated attempt the chain refused, an invocation that committed and has not yet recorded, one that died between, or — for a transfer alone — one whose entry committed under a different releasing custodian than the intent authenticated (a principal divergence, Transfer Custody step 5); the reconciliation scan resolves which against the Provenance chain, and only the committed-and-unrecorded case owes compensation — the divergence is escalated, never compensated. The relation is composition-introduced and lives in the outcome event’s payload; it is deliberately not part of Invariant 4’s entry-to-event bijection, which is over custody entries and outcome events alone.

Two asymmetries a reader would otherwise take for errors. Transfer Custody alone performs a read-only resolution step before its intent record, because the principal its guarantee names — the outgoing custodian — is not known until the chain is read; consequently its not-known and archived arms are refused before any record is written when the chain was already in that state at step 2 — an archive_custody that lands between step 2 and step 4 is outside the transfer-versus-transfer serialization obligation, so it leaves an intent record written and draws archived from step 4’s constituent guard, which is the ordinary post-intent shape. The other three chain-addressed actions’ constituent guards (not-known, archived / already-archived, not-current-custodian) fire after their intent record. That is deliberate: an authenticated attempt the chain then refuses is evidence a custody trail should keep — a former custodian trying to alter a chain they no longer hold is exactly the event an investigator wants — and duplicating the constituent’s guards at this layer to avoid it would paraphrase the substrate rather than read its contract. And Originate Custody’s intent record can name no chain_id, since none exists before the commit, so its intent-to-outcome join is one-directional where the other four are symmetric.

The full rejection taxonomy for each action enumerates every reason the action can fail; constituent rejections are either propagated (with the same name), renamed (with the mapping noted), or surfaced as a new code at the composition’s boundary. Silent rejection-code drift is a Pass 1 reference-graph finding.


originate_custody

originate_custody(
  artifact_ref,
  custodian_ref,
  genesis_type,
  credential,
  metadata?
) →
    {chain_id, entry_id, event_id}
  | rejected(
      invalid-ref
    | invalid-genesis-type
    | invalid-credential
    | invalid-request
    | recording-failure(intent | outcome)
    )

Opens a new Provenance chain for artifact_ref under the custody of custodian_ref, and immediately binds the genesis entry to an attributed Audit Trail event.

Steps:

  1. Validate artifact_ref, custodian_ref, credential per Primitive policies. Any failure → rejected(invalid-ref) for empty/whitespace artifact_ref or custodian_ref; invalid-genesis-type if genesis_type ∉ {originated, received}. Stop.
  2. Intent record. AuditTrail.record_action(action_ref = custody.originate_intended, actor_ref = custodian_ref, credential, data = {artifact_ref, custodian_ref, genesis_type, metadata?})event_id_intent. Nothing has committed, so every arm is a clean pre-state rejection (the rejection-mapping rule): invalid-credentialrejected(invalid-credential); invalid-requestrejected(invalid-request); recording-failure(step)rejected(recording-failure(intent)), retryable as a whole action. The payload carries the invocation’s parameters and no constituent-minted id — neither chain_id nor entry_id exists yet — and no timestamp: this composition samples no clock, and the substrate stamps the event at its own seam exactly as it does the outcome record (Clock source). One consequence stated rather than implied: because the intent record cannot name chain_id, the intent-to-outcome join for genesis is one-directional — the outcome event’s intent_event_id points back, nothing points forward — so a crash between the commit and the outcome record leaves the intent joinable to the orphan chain only by (actor_ref, artifact_ref, genesis_type) plus Event Log order. The other four actions carry chain_id in both records and have no such asymmetry.
  3. Provenance.originate(artifact_ref, custodian_ref, genesis_type, [metadata])chain_id. The constituent’s contract returns chain_id only — the genesis entry_id is obtained by the declared read surface: call Provenance.read(chain_id) and take the genesis entry at sequence_number = 1 (its entry_id is what the following steps bind and return; the read is exact under the per-chain serialization obligation — Edge cases, Concurrent transfer attempts). Map constituent rejections: invalid-refrejected(invalid-ref); invalid-genesis-typerejected(invalid-genesis-type); storage-failurerejected(recording-failure(intent)) — the constituent persisted nothing, so the whole action may be retried. Stop on any.
  4. AuditTrail.record_action(action_ref = custody.originated for genesis_type = originated, custody.received for genesis_type = received, actor_ref = custodian_ref, credential, data = {chain_id, entry_id, intent_event_id: event_id_intent, custodian_ref, artifact_ref, genesis_type})event_id. The action reference mirrors the genesis entry’s actual event_type, so Invariant 1’s action_refevent_type match holds for both genesis kinds. The event’s own timestamp is stamped by the substrate at its seam (Event Log’s recorded_at) — the composition puts no timestamp into the payload; the entry-side stamp is the Provenance entry’s own recorded_at (see the Clock source edge case). The genesis custodian is the actor — they are attesting that the artifact entered custody under their hand. Arms per the rejection-mapping rule’s outcome position: step-4 and the retention-source invalid-request → the event is appended; read it back by intent_event_id and proceed. Otherwise (the Provenance entry landed in step 3 but the audit write did not): return rejected(recording-failure(outcome)); the orphan (Provenance chain with no Audit Trail outcome event, no entry_to_event binding) is surfaced per the Cross-store consistency under partial failure edge case. Stop on any arm that rejects.
  5. Record entry_to_event[entry_id] = event_id.
  6. Return {chain_id, entry_id, event_id}.

transfer_custody

transfer_custody(
  chain_id,
  to_custodian_ref,
  credential
) →
    {entry_id, event_id}
  | rejected(
      not-known
    | archived
    | invalid-ref
    | invalid-credential
    | invalid-request
    | principal-divergence
    | recording-failure(intent | outcome)
    )

Records a hand-to-hand custody transfer on the chain. The acting custodian is the outgoing (current) custodian — they are releasing the artifact; their credential attests the release. The incoming custodian (to_custodian_ref) is named in the Provenance entry’s to_custodian_ref field but does not attest the receiving step (they will attest their own subsequent actions under their custody).

Steps:

  1. Validate to_custodian_ref, credential per Primitive policies. to_custodian_ref must contain at least one non-whitespace character; credential is opaque (consumed by Actor Identity inside Audit Trail). Any failure → rejected(invalid-ref). Stop.
  2. Resolve the acting custodian — read-only. Provenance.read(chain_id) → the chain’s ordered entries; map not-knownrejected(not-known). Derive acting_custodian by the same cursor rule Continuity Check already defines: the to_custodian_ref of the most recent transferred entry, or the genesis custodian_ref if none. Derive the chain state the same way — a terminal archived entry → rejected(archived). This step writes nothing, and it exists because of a problem no re-ordering alone can solve: the principal this composition’s guarantee names is the outgoing custodian, and until the chain is read the only actor available is to_custodian_ref, the incoming one, who by the acting-custodian attribution rule is explicitly not the attesting party. Authenticating that actor would satisfy the step numbering while binding the wrong principal — an ordering fix producing a binding defect. Reading the holder from chain state first is what makes the intent record able to name the right actor. (The derivation replays the entry chain rather than trusting a projection, per Provenance’s own rule that on any disagreement the replayed chain is authoritative; this is the same derivation Verify Custody performs.)
  3. Intent record. AuditTrail.record_action(action_ref = custody.transfer_intended, actor_ref = acting_custodian, credential, data = {chain_id, from_custodian_ref: acting_custodian, to_custodian_ref})event_id_intent. Arms as in Originate Custody step 2 — nothing has committed, so all three are clean pre-state rejections. Note the asymmetry this action alone has: because step 2 resolved existence and archival state read-only, not-known and archived are refused before the intent record here, where in the other three chain-addressed actions the constituent’s own guards fire after it.
  4. Provenance.transfer(chain_id, to_custodian_ref)entry_id. The atom reads from_custodian_ref = current_custodian from chain state — the outgoing custodian’s identity is structural, not caller-supplied (Provenance Invariant 4). Map: not-knownrejected(not-known); archivedrejected(archived); invalid-refrejected(invalid-ref); storage-failurerejected(recording-failure(intent)) — the constituent persisted nothing, so the whole action may be retried. Stop on any.
  5. Read from_custodian_ref from the just-written entry via the declared read surface, and confirm it equals the acting_custodian step 3 authenticated. A divergence means the per-chain serialization obligation was violated — another transfer interleaved between the resolution read and this commit — and it lands as rejected(principal-divergence): no outcome record is written, no entry_to_event binding is made, and the committed entry is surfaced in the same outcome as a finding of its own class. Nobody authenticated the release the entry records — the intent record authenticated the resolved holder, the entry attributes another — so the composition cannot attest it now and the scan must not attest it later (partial-failure edge case: a principal-divergence entry is outside Invariant 4’s bijection, never an orphan owing compensation). Under the declared serialization the two are the same actor by construction; the confirmation is what makes that guarantee checkable rather than assumed, and the landing is what makes a breach visible. Concretely: call Provenance.read(chain_id) and take the entry whose entry_id step 4 returned (equivalently the highest sequence_number, exact under the per-chain serialization obligation); its from_custodian_ref field is the pre-transfer holder, structural per Provenance Invariant 4. This is the actor who is releasing custody and whose credential this action binds.
  6. AuditTrail.record_action(action_ref = custody.transferred, actor_ref = from_custodian_ref, credential, data = {chain_id, entry_id, intent_event_id: event_id_intent, custodian_ref: from_custodian_ref, from_custodian_ref, to_custodian_ref})event_id. (No timestamp rides the payload — the substrate stamps the event at its own seam; see Clock source.) The outgoing custodian’s credential is the one bound here — they are releasing; the composition attests that custodian’s release. Arms per the rejection-mapping rule’s outcome position: step-4 and the retention-source invalid-request → appended, read back by intent_event_id and proceed; otherwise rejected(recording-failure(outcome)); orphan per the Cross-store consistency under partial failure edge case. Stop on any arm that rejects.
  7. Record entry_to_event[entry_id] = event_id.
  8. Return {entry_id, event_id}.

Acting custodian for transfer — design note (see The load-bearing wiring decision for the full defense). credential is the outgoing custodian’s credential, not the incoming custodian’s. This is the structurally correct choice: the transfer records a release by the current holder; the current holder is who the chain shows as responsible; their credential attests the hand-off. The incoming custodian does not attest the receipt in this action — their subsequent Transform Custody, Disclose Custody, or Archive Custody calls will bind their own credential, confirming they took real custody. This matches the real-world chain-of-custody discipline in both pharmaceutical and legal-evidence handling.


transform_custody

transform_custody(
  chain_id,
  custodian_ref,
  transformation_descriptor,
  credential
) →
    {entry_id, event_id}
  | rejected(
      not-known
    | archived
    | not-current-custodian
    | invalid-ref
    | invalid-descriptor
    | invalid-credential
    | invalid-request
    | recording-failure(intent | outcome)
    )

Records a transformation of the artifact by the current custodian. Only the current holder may record a transformation.

Steps:

  1. Validate custodian_ref, transformation_descriptor, credential per Primitive policies. custodian_ref must contain at least one non-whitespace character; transformation_descriptor must contain at least one non-whitespace character. Any failure → rejected(invalid-ref) for custodian_ref, rejected(invalid-descriptor) for transformation_descriptor. Stop.
  2. Intent record. AuditTrail.record_action(action_ref = custody.transform_intended, actor_ref = custodian_ref, credential, data = {chain_id, custodian_ref, transformation_descriptor})event_id_intent. Arms as in Originate Custody step 2 — nothing has committed, so all three are clean pre-state rejections. The constituent’s own guards (not-known, archived / already-archived, not-current-custodian) fire at step 3, after this record: an authenticated attempt that the chain then refuses is retained deliberately, not by oversight — see the Action wiring preamble.
  3. Provenance.transform(chain_id, custodian_ref, transformation_descriptor)entry_id. Map: not-knownrejected(not-known); archivedrejected(archived); not-current-custodianrejected(not-current-custodian); invalid-refrejected(invalid-ref); invalid-descriptorrejected(invalid-descriptor); storage-failurerejected(recording-failure(intent)) — the constituent persisted nothing, so the whole action may be retried. Stop on any.
  4. AuditTrail.record_action(action_ref = custody.transformed, actor_ref = custodian_ref, credential, data = {chain_id, entry_id, intent_event_id: event_id_intent, custodian_ref, transformation_descriptor})event_id. (No timestamp rides the payload — the substrate stamps the event at its own seam; see Clock source.) The current custodian’s credential is bound — only the holder can transform. Arms per the rejection-mapping rule’s outcome position: step-4 and the retention-source invalid-request → appended, read back by intent_event_id and proceed; otherwise rejected(recording-failure(outcome)); orphan per the Cross-store consistency under partial failure edge case. Stop on any arm that rejects.
  5. Record entry_to_event[entry_id] = event_id.
  6. Return {entry_id, event_id}.

disclose_custody

disclose_custody(
  chain_id,
  custodian_ref,
  recipient_ref,
  credential
) →
    {entry_id, event_id}
  | rejected(
      not-known
    | archived
    | not-current-custodian
    | invalid-ref
    | invalid-credential
    | invalid-request
    | recording-failure(intent | outcome)
    )

Records a disclosure by the current custodian to a named recipient. Custody is NOT transferred — the current custodian retains custody after a disclosure. Used to record that the artifact was shown or a copy provided to a party, for the purpose of placing that fact on the custody timeline.

Steps:

  1. Validate custodian_ref, recipient_ref, credential per Primitive policies. Each must contain at least one non-whitespace character. Any failure → rejected(invalid-ref). Stop.
  2. Intent record. AuditTrail.record_action(action_ref = custody.disclose_intended, actor_ref = custodian_ref, credential, data = {chain_id, custodian_ref, recipient_ref})event_id_intent. Arms as in Originate Custody step 2 — nothing has committed, so all three are clean pre-state rejections. The constituent’s own guards (not-known, archived / already-archived, not-current-custodian) fire at step 3, after this record: an authenticated attempt that the chain then refuses is retained deliberately, not by oversight — see the Action wiring preamble.
  3. Provenance.disclose(chain_id, custodian_ref, recipient_ref)entry_id. Map: not-knownrejected(not-known); archivedrejected(archived); not-current-custodianrejected(not-current-custodian); invalid-refrejected(invalid-ref); storage-failurerejected(recording-failure(intent)) — the constituent persisted nothing, so the whole action may be retried. Stop on any.
  4. AuditTrail.record_action(action_ref = custody.disclosed, actor_ref = custodian_ref, credential, data = {chain_id, entry_id, intent_event_id: event_id_intent, custodian_ref, recipient_ref})event_id. (No timestamp rides the payload — the substrate stamps the event at its own seam; see Clock source.) Arms per the rejection-mapping rule’s outcome position: step-4 and the retention-source invalid-request → appended, read back by intent_event_id and proceed; otherwise rejected(recording-failure(outcome)); orphan per the partial-failure edge case. Stop on any arm that rejects.
  5. Record entry_to_event[entry_id] = event_id.
  6. Return {entry_id, event_id}.

archive_custody

archive_custody(
  chain_id,
  custodian_ref,
  credential
) →
    {entry_id, event_id}
  | rejected(
      not-known
    | already-archived
    | not-current-custodian
    | invalid-ref
    | invalid-credential
    | invalid-request
    | recording-failure(intent | outcome)
    )

Records the terminal disposition of the artifact by the current custodian and transitions the chain to Archived. No further custody entries are accepted after this action. The archiving custodian’s credential attests the terminal disposition.

Steps:

  1. Validate custodian_ref, credential per Primitive policies. Any failure → rejected(invalid-ref). Stop.
  2. Intent record. AuditTrail.record_action(action_ref = custody.archive_intended, actor_ref = custodian_ref, credential, data = {chain_id, custodian_ref})event_id_intent. Arms as in Originate Custody step 2 — nothing has committed, so all three are clean pre-state rejections. The constituent’s own guards (not-known, archived / already-archived, not-current-custodian) fire at step 3, after this record: an authenticated attempt that the chain then refuses is retained deliberately, not by oversight — see the Action wiring preamble.
  3. Provenance.archive(chain_id, custodian_ref)entry_id. Map: not-knownrejected(not-known); already-archivedrejected(already-archived); invalid-refrejected(invalid-ref) (the constituent’s arm, added in Provenance’s Final Critique 6 closure — this signature already declared it); not-current-custodianrejected(not-current-custodian); storage-failurerejected(recording-failure(intent)) — the constituent persisted nothing, so the whole action may be retried. Stop on any.
  4. AuditTrail.record_action(action_ref = custody.archived, actor_ref = custodian_ref, credential, data = {chain_id, entry_id, intent_event_id: event_id_intent, custodian_ref})event_id. (No timestamp rides the payload — the substrate stamps the event at its own seam; see Clock source.) Arms per the rejection-mapping rule’s outcome position: step-4 and the retention-source invalid-request → appended, read back by intent_event_id and proceed; otherwise rejected(recording-failure(outcome)); the Provenance chain is in Archived state but the terminal audit event is missing — a high-priority orphan per the partial-failure edge case (an archived chain without an attributed terminal record is a deficiency in the custody-proof surface, though not a custody-gap finding because the Provenance archived entry still anchors the terminal disposition). Stop on failure.
  5. Record entry_to_event[entry_id] = event_id.
  6. Return {entry_id, event_id}.

verify_custody

verify_custody(chain_id, original_event_payloads) →
    custody-proof
  | rejected(not-known)

The emergent verification action. original_event_payloads is a map keyed by the audit log’s sequence_number exactly (byte-identity, one keying, no alternate form) to the byte-exact payload Event Log holds at that position — because the record set a seal commits to is a sequence range, not one entry’s event. The substrate’s rule does not vary: AuditTrail.verify_record takes the payloads of every event in the covering seal’s coverage, in ascending sequence_number order, and read_record names that range for each event (Audit Trail Invariant 7). A range spans this composition’s intent records and whatever other actions the deployment records on the same instance — events that carry no entry_id — so a map keyed by entry_id could never present what a seal demands under any cadence but per-event; under an interval cadence a one-payload presentation returns failed-verification(seal-record-set-mismatch) for every entry. Keying by sequence_number is what lets the composition assemble, per entry, exactly the presentation its covering range names (step 3c); under per-event cadence each range is a singleton and the map holds one payload per event — the per-entry shape is the special case, not the rule. It is a required argument because tamper-evidence verification is self-contained only given the originating records (Audit Trail Invariant 7 — verification asymmetry preserved). Surfacing the payloads at the signature, rather than having the composition fetch them internally, is intentional: the verifier must present the record set, not trust the composition to supply it. For a Retained or compensation-window entry whose covering range is not wholly present in the map (a Purged entry is answered by the substrate before the map is consulted — step 3b), the per-entry attestation_verification is reported as unverifiable(payload-not-supplied(missing)), naming the absent sequence numbers, rather than failing — partial verification (continuity + attribution + retention for the entries whose ranges are supplied) is still useful, and the affected entries are named in the verdict. Constructs and returns the records-alone custody proof for the chain identified by chain_id. This action is the composition’s defining emergent contribution: neither Provenance (structural continuity only, no attribution or seal) nor Audit Trail (attribution, seal, and retention, but no custody continuity) can answer this query alone. Only the composition can, because it holds entry_to_event — the binding that lets the verifier traverse from each custody entry to its attributed, sealed, retention-governed audit event.

The returned Custody Proof is a structured artifact containing:

  • chain_id — the identifier of the chain being verified.
  • chain_state — the Provenance chain’s current state (Open or Archived).
  • Continuity Check — the result of replaying the Provenance entries in sequence_number ascending order, maintaining a running current-custodian cursor, and verifying Provenance Invariant 4’s custody-continuity clauses plus Provenance Invariant 5’s density, labeled here (i)–(iv): (i) for every transferred entry, from_custodian_ref equals the current custodian in effect immediately before that transfer — equivalently, the to_custodian_ref of the most recent preceding transferred entry, or the genesis custodian_ref if no prior transfer exists (intervening transformed and disclosed entries do not change custody, so the cursor carries the holder through them; comparing a transfer’s from against the immediately adjacent entry would be wrong); (ii) every transformed, disclosed, and archived entry’s custodian_ref equals the cursor at that point — the then-current-holder attribution clause; (iii) the cursor is non-null and single-valued at every point from genesis to archive — never custody-by-nobody or by two parties; and (iv) the replayed sequence is dense, exactly 1..n — this clause is Provenance Invariant 5’s claim, not a clause of Invariant 4, verified in the same pass since the replay walks the numbers. Result: continuous or gap-detected(entry_id, clause, expected, actual). This is Provenance Invariant 4 (clauses i–iii) with Invariant 5’s density (clause iv) replayed on demand, in full.
  • entries — an ordered sequence (by sequence_number) of per-entry verification records. Each record contains:
    • entry_id, sequence_number, event_type, custodian_ref (or from_custodian_ref/to_custodian_ref for transfers), recorded_at — from Provenance.
    • event_id — from entry_to_event[entry_id]. absent if the entry has no binding — flagged in the proof as binding-gap (an Invariant 4 violation), or as binding-unknown(purged-horizon) where the retention-aware determination of step 3a applies (a once-bound entry whose event was lawfully purged and whose binding was lost — a durability-obligation finding against Composition state’s purged subset, not a bijection violation).
    • attestation_verification — the result of AuditTrail.verify_record(event_id, <presentation>) for the bound event_id, where the presentation is the covering range’s payloads assembled from original_event_payloads in ascending sequence_number order (step 3c). Returns verified, failed-verification(reason), not-known (the audit event is unknown — a binding-gap), or the constituent’s own distinct unverifiable(reason) arm — never a flavor of failed-verification. That arm covers payload-not-supplied(missing) (the map lacks one or more members of this entry’s covering range — the composition-added case, naming the absent sequence numbers), the availability-class reasons (attestation-registry-unavailable and kin, relayed unchanged as unverifiable(availability): the record could not be checked now, a different fact from failing its check, resolved by re-verification when the dependency returns), and partially-purged-coverage — a standing condition, not a transient: under the interval seal cadence this spec recommends, once one seal-mate in a multi-event seal range is lawfully purged, the surviving events’ seal check can never again be re-run in full — the entry reports unverifiable(partially-purged-coverage) permanently, resolved only by a composed Seal Lifecycle pattern (forthcoming) that re-seals surviving ranges (no amount of waiting or dependency recovery clears it). The attestation verification answers: was the attesting identity’s credential valid, and is the audit event covered by a verifying seal?
    • entry_payload_match — the per-entry field-match clause: the entry’s stored fields (event_type, custodian_ref or the from_custodian_ref/to_custodian_ref pair, and whichever event-type-specific fields the bound event’s payload carries) are compared against the sealed payload’s copies in original_event_payloads[n], where n is the bound event’s own sequence_number as read_record reports it. match, or entry-payload-mismatch(field) — the sealed payload is the anchored copy, so an in-place rewrite of the Provenance entry under a surviving binding is caught here (a rewritten transformation_descriptor, a consistently rewritten from/to pair), where binding-gap catches only a missing event. unverifiable(payload-not-supplied) where no payload was supplied. Direct cryptographic sealing of the chain store itself remains the composing Tamper-Evidence-on-Provenance concept the constituent’s own edge case names; this clause is the audit-layer detection of chain rewrites.
    • actor_match — the per-entry attribution clause of Invariant 1, which no other field verifies: attestation_verification = verified says the attesting credential was valid, not whose it was. The bound event’s attesting actor_ref — read from the attestation record through read_record, where it survives the purge cascade — is compared to the custodian the Provenance entry names (custodian_ref; from_custodian_ref for a transfer). match where they are equal; match(recovery) where the actor is the deployment’s recovery_identity, the event carries cascade_recovery = true, and data.custodian_ref equals the entry’s custodian; attribution-mismatch(actor_ref) otherwise — a valid credential attesting a custody step its holder did not perform, which is an insider’s forgery and a Custody Proof Incomplete class of its own. For a Purged event the canonical comparison is still answerable (the attestation’s actor_ref survives); the recovery comparison needs data and reports unknown(purged).
    • recovery_attestedtrue when the bound event carries cascade_recovery = true (Invariant 1’s recovery-attestation arm): the step’s audit record was compensated under the deployment’s recovery_identity rather than attested by the acting custodian. Carrier, stated: cascade_recovery rides the event’s data payload, so for a Purged event the marker is unreadable with the destroyed data — recovery_attested is then reported unknown(purged), never false, and the auditor’s surviving signal is the attestation side: the destruction record preserves (event_id, attestation_id), and an attestation whose actor is the deployment’s recovery_identity rather than a custodian identifies the compensated step. Reported per entry so the auditor sees exactly which custody steps lack the custodian’s own attestation.
    • retention_state — the retention state of the audit event, read through the substrate’s declared consolidated read surface, AuditTrail.read_record(event_id), and relayed with the substrate’s full status vocabulary: Retained, Purged (with the substrate’s (compensation-window) qualifier relayed as-is where it carries one), unresolved (compensation window) (the substrate’s own status for an event whose retention placement sits inside its compensation window), or unknown (if the event_id is not bound). No substrate-internal map is read — the substrate’s emergent state is reached only through the surfaces it declares.
  • Overall Verdict — a summary verdict: Custody Proof Complete (continuity continuous, every entry has a binding, every attestation_verification returns verified or failed-verification(purged) — the latter is lawfully destroyed, with entry_payload_match = not-applicable(purged) counted as consistent — every actor_match is match or unknown(purged) — the canonical comparison ran on the attestation’s surviving actor; only the recovery comparison is unanswerable past the purge — and no entry is recovery_attested), custody-proof-complete-with-recovered-attestations(entry_ids) (the same, except the named entries carry recovery attestations in place of the custodian’s own — their actor_match is match(recovery) — structurally sound under Invariant 1’s recovery arm, surfaced for the auditor’s judgment rather than silently folded into completeness), or Custody Proof Incomplete enumerating the specific failure classes (gap-detected, binding-gap, binding-duplicate, entry-payload-mismatch, attribution-mismatch, attestation-failed, seal-failed), the incomplete-input classes (payload-not-supplied, availability), and the standing coverage class (partially-purged-coverage — a lawful purge of a seal-mate, not a defect and not resolvable by waiting; the Seal Lifecycle composing remedy is named at the attestation_verification field, and the verdict enumerates the affected entries so the auditor weighs them). The incomplete-input and standing classes are distinguished from the genuine-failure classes: they signal an incomplete verification input, environment, or lawfully bounded coverage, not a defect in the custody record.

Steps:

  1. Call Provenance.read(chain_id) → the chain’s entries in sequence_number ascending order, or rejected(not-known) propagated. This one declared read is both the existence check and the entry read — the composition consults no undeclared store-lookup surface.
  2. Execute the continuity check: replay entries, verifying clauses (i)–(iii) (Provenance Invariant 4) plus clause (iv) density (Provenance Invariant 5) as defined under Continuity Check. Record the result.
  3. For each entry e in the ordered sequence: a. Look up event_id = entry_to_event[e.entry_id] (rebuild-on-miss per Composition state). If absent after rebuild, the determination is retention-aware: where the chain’s audit horizon shows lawful purges (Purged-state retention records exist for the chain’s events) and the entry predates that horizon, record binding-unknown(purged-horizon) — a lawfully purged, once-bound entry is reconstructible from no store (Composition state’s truth-bearing purged subset), and conflating it with a never-bound entry would manufacture a false binding-gap; otherwise record binding-gap for this entry. b. If event_id present, call AuditTrail.read_record(event_id) — the substrate’s declared consolidated read surface — and take from the returned audit record the event’s own sequence_number n, its covering seal’s range [lo .. hi] (or unsealed tail), its retention state, the attestation’s surviving actor_ref, and the cascade_recovery marker (for recovery_attested). Branch on the retention state before anything the caller supplied is examined. If it is Purged, the substrate’s own verdict comes first and is the only one: call AuditTrail.verify_record(event_id, <whatever members of the range the map holds, possibly none>) — the substrate reads retention state at its step 1 and answers failed-verification(purged) before it reads a presentation — record that, record entry_payload_match = not-applicable(purged), evaluate actor_match from the attestation record’s surviving actor_ref (below), and skip step 3c: a lawfully destroyed payload is not a payload the caller failed to supply, and a membership check run ahead of the constituent’s purged answer would make Custody Proof Complete unreachable for any chain past its first purge (§Lawful destruction is answered before absence). The range is otherwise the instruction for the next step: the composition tells the caller nothing about cadence, because the substrate has already told the composition what to present. c. (Retained and compensation-window events only.) Assemble the presentation original_event_payloads[lo], …, original_event_payloads[hi] in ascending order (for an unsealed-tail event, the singleton at n). If any member is absent from the map, record attestation_verification = unverifiable(payload-not-supplied(missing = <the absent sequence numbers>)) and continue. Otherwise call AuditTrail.verify_record(event_id, <presentation>) — Audit Trail Invariant 7 requires the covering record set re-presented byte-exact; the caller supplies it via the argument, and the composition never fetches, reorders or normalizes it. Record the verification result, relaying the constituent’s unverifiable(reason) arm as-is — availability-class reasons and the standing partially-purged-coverage alike. Then execute the entry–payload field match (entry_payload_match) against original_event_payloads[n], the payload at the bound event’s own position, and evaluate actor_match from the attestation’s actor_ref and the payload’s data.custodian_ref.
  4. Compute overall_verdict from the collected results, with the recovery-attested entries surfaced per the Overall Verdict definition.
  5. Return custody-proof with all fields populated.

Note on original_event_payloads at verify_custody. The asymmetry is surfaced at the signature: original_event_payloads is a required argument, not an internally-fetched value (Audit Trail Invariant 7 — verification asymmetry preserved: the verifier must present the original record set, not trust the composition to fetch it). For interactive audits, the auditor retrieves payloads from cold storage — through Event Log’s declared sequence-range read where the log is still online, keyed by the positions read_record names — and supplies the full map. For operational monitoring queries, the system supplies the payloads from its own store. A caller who supplies a partial map (or none) receives a partial proof: Retained and compensation-window entries whose covering range is incomplete report unverifiable(payload-not-supplied(missing)) and the verdict names them, while continuity, attribution, and retention for the supplied entries are still verified. This mirrors Audit Trail’s own verify_record(event_id, original_event_payload) design, where the payload is an explicit second argument rather than a hidden fetch.


read (passthrough)

read(chain_id, query?) →
    ordered_sequence_of_entries
  | rejected(not-known | invalid-query)

Passes directly to Provenance.read(chain_id, [query]) without modification. No Audit Trail event is recorded — this is a pure read that changes no state. Returns entries in sequence_number ascending order. The composition does not enrich the entries with Audit Trail data at the read layer; enrichment occurs in Verify Custody.


The load-bearing wiring decision — custody-event ⇒ audit-event binding

The composition’s structural reason to exist: every Provenance custody action emits a pair. First, before the Provenance write, an intent record_action naming the invocation’s parameters, which is where the acting custodian’s credential is validated against the actor registry — so no custody entry commits on an unverified identity. Then, ordered after the Provenance write — never inside a transaction with it, since neither the entry nor the append can be withdrawn — an outcome record_action that (a) names the custody entry_id in its data payload, (b) attributes the acting custodian via credential (Actor Identity through Audit Trail), (c) is thereby sealed and placed under retention, and (d) carries intent_event_id back to the intent record that authenticated it. The binding is recorded in entry_to_event.

Principle. A court-admissible or regulator-acceptable chain of custody requires four properties simultaneously: continuity (no gap, hand-to-hand), verified attribution (each custodian non-repudiably identified), tamper-evidence (any rewrite detectable from the records alone), and retention governance (retained for the regulatory lifetime, lawfully destroyable with a defensible record). No single atom satisfies all four. Provenance supplies continuity and a structural custody record. Audit Trail supplies attribution, tamper-evidence, and retention governance. The gap is the binding: without a per-entry link between the Provenance custody chain and the Audit Trail’s attributed, sealed, retained event, the two stores are parallel but unjoined — an auditor can verify continuity separately and attribution separately, but cannot prove continuity-and-attribution-and-seal-and-retention together for the same entry. The entry_to_event binding closes that gap.

Likely objection. Why not fold attribution, tamper-evidence, and retention directly into Provenance, so the chain itself carries all four properties?

Mechanism that resolves it. Those are exactly the concepts Provenance deliberately extracted during its EOS (Essence of Software — Daniel Jackson’s framework for specifying software concepts as freestanding, composable units) Pass 2 to remain freestanding. Provenance’s Edge cases name all three: “Non-repudiable custodian identity → Actor Identity”, “Cryptographic tamper-evidence on the chain → Tamper Evidence”, “Retention and defensible disposal → Retention Window / Defensible Retention.” The composition is precisely where those concepts re-converge. And the re-convergence is efficient: Audit Trail already wires all three (Actor Identity + Tamper Evidence + Retention Window + Event Log) into a single regulated-audit substrate. The composition needs only one binding — custody entry to audit event — and Audit Trail provides the full attribution+seal+retention surface transitively. Absorbing those three concepts into Provenance would destroy Provenance’s freestanding status and force every non-regulated use of a custody chain to carry the regulated overhead.

Result. A records-alone-defensible custody proof that neither Provenance nor Audit Trail provides alone. An auditor holding entry_to_event plus the Provenance chain plus the Audit Trail substrate can answer the four regulator/court questions for any custody entry without developer narration, without source code, without runbooks.

The acting-custodian attribution rule (per action):

This is a Pass-3-prone detail that the wiring decision must state explicitly. The credential passed to each composition action binds a specific actor via Audit Trail’s Actor Identity. The rule is:

  • Originate Custody → the genesis custodian attests. They are creating the chain and taking initial custody; their credential is bound at genesis.
  • Transfer Custody → the outgoing (current) custodian attests. They are releasing custody; from_custodian_ref is read from Provenance’s chain state (Provenance Invariant 4 — it cannot be forged), and the composition passes that reference as actor_ref to AuditTrail.record_action. The reference is read twice, and both reads are load-bearing: resolved read-only before the intent record, so the credential authenticated is the outgoing custodian’s rather than the incoming caller’s; and confirmed from the committed entry after the transfer, so the attributed principal is the structural one. A single post-commit read would attribute correctly while authenticating either nobody or the wrong party — the distinction Invariant 7 turns on. The incoming custodian (to_custodian_ref) does not attest the receipt in this action — their subsequent actions on the chain (transform, disclose, archive, or transfer) will bind their own credential, evidencing that they took active custody.
  • Transform Custody → the current custodian attests. They are performing the transformation under their custody; Provenance enforces that only the current holder can call transform (not-current-custodian guard), so the actor and the current holder are the same.
  • Disclose Custody → the current custodian attests. Same discipline as transform.
  • Archive Custody → the current custodian attests. They are recording terminal disposition from their hand; the archiving custodian is the chain’s last-recorded holder.

This rule closes the gap Provenance’s Edge case named: “Non-repudiable custodian identity → Actor Identity.” By passing custodian_ref (or from_custodian_ref for transfers) as the actor_ref to AuditTrail.record_action, the composition binds each Provenance opaque custodian reference to a verified actor credential — transforming structural continuity into attributed, non-repudiable continuity.


Composition-level invariants

These invariants (conditions that must always hold) emerge from the composition. None belongs to a single constituent; each requires both Provenance and the Audit Trail substrate working together to hold.

  • Invariant 1 — Attributed custody. Every custody entry in the Provenance chain has exactly one corresponding outcome Audit Trail event (via entry_to_event) whose action_ref matches the entry’s event_type (the custody.* vocabulary covers every entry kind, custody.received for a received genesis included) and whose verified attestation binds an authorized identity — in the canonical arm, the acting custodian’s custodian_ref; in the recovery-attestation arm (the deterministic-rejection leg of Invariant 4’s liveness — narrowly, the custodian’s credential validated at the intent record and was revoked before the outcome record landed), the deployment’s declared recovery_identity, with the acting custodian named in data.custodian_ref and the event carrying cascade_recovery = true, so attribution stays honest: the record shows which operational identity attested the compensation and which custodian performed the step. A recovery-attested entry is not custodian-attested, and Verify Custody reports it as such (recovery_attested) rather than folding it into the canonical arm. No custody entry exists in the Provenance chain without an attributable, attribution-verified audit record under one of the two arms. And every outcome event is itself preceded, in the Audit Trail’s own sequence, by an intent event of the matching kind — named by the outcome’s intent_event_id, carrying the same chain_id, and attested under the acting custodian’s own actor_ref (including on the recovery arm: the compensation changes who attests the outcome, never who attested the intent, which is what keeps the pair’s actor match meaningful for compensated steps). The action_ref-to-event_type match above is scoped to outcome events; the custody.*_intended vocabulary matches no event_type and does not falsify it. Rests on: Provenance Invariant 1 (entry immutability), Provenance Invariant 7 (custodian presence — every entry has a non-empty custodian_ref), and Audit Trail Invariant 1 (attribution coverage — every Audit Trail event has a verified attestation). Established by the entry_to_event population step in each action wiring.

  • Invariant 2 — Tamper-evident custody chain. Every custody entry in the Provenance chain is covered by the Audit Trail seal via its entry_to_event binding: any rewrite of any custody entry’s corresponding audit event is detectable from the records alone, and Verify Custody’s per-entry attestation_verification will return failed-verification(seal-...) for the affected entry. Rests on: Audit Trail Invariant 3 (integrity coverage modulo unsealed tail) and Audit Trail Invariant 7 (verification asymmetry preserved). The unsealed tail is observable and bounded by the configured seal_cadence; a tighter cadence narrows the tamper-detection window.

  • Invariant 3 — Retention-governed custody and honest disposal. Every custody audit event — intent and outcome alike — is placed under retention by the Audit Trail substrate at record_action time, and the pair can be split by the purge horizon, so the authentication-precedence determination (Generation acceptance) is answerable from the records only within that horizon, exactly as Verify Custody’s binding determination is retention-aware; no custody event is purged before its retention obligation is honored; when retention expires, the cascade runs per the Audit Trail substrate’s actual cascade-on-purge rule — the readability of the event’s data is destroyed in its entirety, the attestation’s proof is destroyed while its record survives with attestation_id, action_ref, actor_ref and attested_at still readable — nothing is scrubbed or rewritten; the substrate’s mechanism is shredding-class and Actor Identity’s records are immutable — the destruction record preserves (event_id, attestation_id), and seal-coverage records are never purged. That surviving actor is what Verify Custody’s purged fallback for recovery_attested reads, so the rule is stated as the constituent states it rather than paraphrased into a stronger destruction the composition cannot then rely on; and Verify Custody’s per-entry attestation_verification returns failed-verification(purged) for lawfully destroyed entries — distinguishing lawfully destroyed from missing. Rests on: Audit Trail Invariant 2 (retention coverage), Audit Trail Invariant 4 (cascade-on-purge), and Audit Trail Invariant 8 (honest representation of destruction).

  • Invariant 4 — Binding bijection / no dangling partial (safety + liveness). A one-to-one binding between Provenance custody entries created through this composition and Audit Trail custody events: every such entry has exactly one corresponding record_action event whose data.entry_id points back to it, and vice versa. The two truth-bearing writes (the Provenance entry and the outcome audit event — the intent record is a third audit write, deliberately outside this bijection surface, and is neither side of it; the entry_to_event map is a derived index outside this surface too for events still readable, its purged subset truth-bearing under a durability obligation — see Composition state) are ordered, never atomic — the Provenance entry first, the outcome append second; neither can be withdrawn, so no transaction spans them and none is claimed, and the failure path of the Cross-store consistency under partial failure edge case closes the one partial the ordering leaves. Because Provenance entries are immutable once committed and synchronous rollback is unavailable, that orphan state — a custody entry with no audit event — is reachable under the prescribed design, durably, until compensation lands. The honest claim therefore splits:

    • Safety — no unsurfaced orphan. At all times, every orphan is detectable from the records alone (enumerate the Provenance chain against the Audit Trail events through the substrate’s read surface). Surfacing has two mandatory legs: a partial failure that returns surfaces the orphan as a high-priority compliance finding in the same outcome that returns rejected(recording-failure(outcome)); a partial failure that cannot return — a process crash between the two truth-bearing writes — is caught by the mandated reconciliation scan, which the intent record sharpens: the crashed invocation is no longer inferred solely from a state-versus-trail difference, because it left a record of its own naming the chain, the acting custodian whose credential was verified, and the custody step it was about to take, so the scan reads what was attempted from the trail rather than reconstructing it (weaker for genesis alone, whose intent record can name no chain_id) (the same orphan enumeration, run at restart and on reconciliation_cadence, between custody_completion_bound and the audit horizon; see the partial-failure edge case), so no orphan survives unsurfaced past the scan bound. Never a quiet inconsistency. A custody audit event naming an entry_id absent from the Provenance chain (the inverse orphan) is unreachable through this composition’s wiring. Recovered bindings on the recovery-attestation arm remain distinguishable from clean ones via the cascade_recovery marker on the compensating event. The other two recovery arms leave no marker, by design and not by omission: a custodian-attested retry is the custodian’s own attestation, landed late, and is indistinguishable from a clean write because it is one; the bind-to-existing arm writes no event at all — its only trace is on the substrate side, the retention placed by the substrate’s own reconciliation rather than at record time, visible as the (compensation-window) qualifier until it lands. The distinguishability claim is scoped to the arm that changes who attests.
    • Liveness — every orphan is bound within compensation_window. The mandated compensation (retry the failed AuditTrail.record_action — inside the invocation under the custodian’s re-presented credential for at most outcome_retry_attempts, by the scan under recovery_identity behind a custody.recovery_intended record thereafter, never both: one writer per entry, serialized on the chain, partial-failure edge case) restores the bijection: an orphan is a surfaced transient under compensation, never a steady state of a conforming implementation. Formally: Orphan(e) ↝ Bound(e) under weak fairness on the retry — the eventuality lives in the implementation’s retry obligation; the formal model carries its enabledness half (the retry action is enabled in exactly the orphan configuration, so no orphan state is a dead end). The retry discharges transient failures whose (step) payload shows the append did not land; on the retention arm the event already exists and the binding completes without a new append (the pre-checked recovery — rejection-mapping rule); a deterministic rejection of the outcome write cannot land by repetition. One cause only: a mid-flight credential revocation — the credential validated at the intent record moments earlier and was revoked before the outcome record landed. invalid-request is not on this arm, and the distinction matters because re-attestation could not clear it if it were: re-attesting under recovery_identity changes actor_ref and credential and leaves data untouched, so a size- or policy-driven refusal recurs identically (and a longer recovery actor_ref can only make a size refusal worse). It is instead foreclosed by construction — the step-1 caps are set so the maximal outcome payload assembled from capped inputs fits the wired substrate’s payload_cap — and if it is ever observed it is a mis-derived deployment cap: a pageable fault, never a compensation case, so there the compensation re-attests under the deployment’s declared recovery_identity (see Configuration and the partial-failure edge case) — and the step-1 payload-safety caps (Primitive policies) foreclose the one deterministic cause no re-attestation could cure, an input breaching the substrate’s payload_cap. The eventuality holds across the failure classes: by retry, by binding-to-existing, or by recovery-attestation. Two carve-outs, stated: a genesis orphan in a deployment that does not supply genesis_lookup (Configuration) is surfaced within the window and not bound — the scan cannot find a chain its intent could not name; and a Transfer Custody entry landed as principal-divergence is outside the bijection altogether — it is a serialization breach nobody authenticated, surfaced as its own finding class and never compensated.

    This is the load-bearing claim and the formal-model subject — the model covers both arms, the clean ordered pair and the compensated partial failure; it mirrors Audit Trail Invariant 4 (cascade-on-purge coordination across four stores) at the entry-creation boundary. Rests on: Provenance Invariant 1 (entry immutability — the orphan cannot be rolled back, which is why the design compensates rather than reverses), Audit Trail Invariant 1 and its record_action surface with the (step)-payloaded failure arm, the intent → entry → outcome ordering and the bounded reconciliation scan (partial-failure edge case), the step-1 payload-safety caps (Primitive policies), and the declared recovery_identity (Configuration). A violation — an unsurfaced custody-entry orphan, or an orphan outside compensation — is a high-priority finding surfaced per the partial-failure edge case.

  • Invariant 5 — Records-alone custody proof (forensic completability). verify_custody(chain_id, original_event_payloads) lets an auditor prove, from the records alone, that the chain has: (a) unbroken structural custody continuity (Provenance Invariant 4 replayed as continuity_check = continuous), (b) a verified attribution for every non-purged entry (attestation_verification = verified), (c) tamper-evident seal coverage for every non-purged entry (inherited from attestation_verification’s seal check), and (d) an honest retention state for every entry (Retained for non-expired entries, Purged with honest-destruction provenance for expired ones). The overall_verdict = custody-proof-complete is the emergent guarantee neither Provenance (no attribution or seal) nor Audit Trail (no custody continuity) provides alone. Rests on: Invariant 1 through 4 above, Provenance Invariants 1 and 4, and Audit Trail Invariants 1, 3, 6, 7, and 8.

  • Invariant 6 — Constituent invariants preserved. Provenance Invariant 1 through 10 hold over the Provenance chain instance; Audit Trail Invariant 1 through 8 hold over the Audit Trail substrate instance, and transitively all Event Log, Actor Identity, Retention Window, and Tamper Evidence invariants hold over their constituent instances within the substrate. The composition does not weaken or override any constituent invariant.

  • Invariant 7 — Authentication precedes commitment. No Provenance call that commits a custody entry is reached on any path before this composition’s caller has presented a credential that validates against the actor registry’s public material for the acting custodian’s actor_ref. The intent record is the mechanism: it is an AuditTrail.record_action call, the substrate validates the credential inside it (Actor Identity’s own attest guard), and it stands before originate, transfer, transform, disclose, and — the terminal case — archive. Custody is therefore never moved, transformed, disclosed, or permanently closed on an unverified actor’s asserted authority; invalid-credential is a pre-state refusal with nothing committed, not a fault discovered over an immutable entry.

    The bound principal is the principal the guarantee names. For Transfer Custody the intent record’s actor_ref is the outgoing custodian, resolved read-only from chain state at step 2 before the transfer commits — not the caller-supplied incoming custodian, who is the only actor available before that read and who by the acting-custodian attribution rule is explicitly not the attesting party. Authenticating the available actor rather than the correct one would satisfy the step ordering while binding the wrong principal, which is a binding defect no re-ordering fixes; the resolution phase exists for that reason and for no other. Step 5’s confirmation that the committed from_custodian_ref equals the resolved acting_custodian is what makes the pair checkable rather than assumed.

    What this does and does not establish: a successful validation establishes that material matching the actor’s registered verifier was presented at that instant. It does not establish that the presenter is that actor (a stolen credential validates), that the presentation is bound to a channel or session, or that it cannot be replayed — and it establishes nothing about authority: whether this custodian was the correct party to take this custody step remains externally clearable. One scope limit, stated rather than left to be discovered: the precedence property is provable through Generation acceptance check 6, not through Verify Custody — that action verifies the outcome event each custody entry is bound to; intent records are presented as seal-mates whenever an entry’s covering range includes them, but the action never verifies them individually, so its verdict does not speak to the precedence property. Rests on: Audit Trail’s record_action and the Actor Identity attestation reached through it; Provenance Invariant 4 (the outgoing custodian is read from chain state, so the resolved principal cannot be forged); and the per-chain serialization obligation (Concurrent transfer attempts), which is what makes the resolved principal and the committed from_custodian_ref the same actor. Defended in-line: the ordering is visible in every action’s step list, and Generation acceptance check 6 tests it from the records alone.


Examples

Walkthrough — pharmaceutical chain of custody under 21 CFR Part 211

A pharmaceutical manufacturer uses this composition to track batch X-91 from manufacturing through distribution to dispensing. Configuration: audit_trail_retention_policy = pharma_7yr_predicate_rule (encoding the applicable predicate-rule retention minimum), seal_cadence = per-event (ALCOA — Attributable, Contemporaneous, Original, Accurate — requires contemporaneous, tamper-evident records; per-event sealing satisfies this at the strongest cadence).

  1. Manufacturer originates the batch. The quality-release officer calls originate_custody(artifact_ref="batch-x91", custodian_ref="manuf-lab-7", genesis_type=originated, credential=<qro_credential>){chain_id="chain-0041", entry_id="e1", event_id="ev_1001"}. Provenance opens the chain; AuditTrail.record_action(custody.originated, actor_ref="manuf-lab-7", qro_credential, ...)ev_1001; entry_to_event["e1"] = "ev_1001". The genesis is attributed and sealed.

  2. Transfer to regional distributor. transfer_custody(chain_id="chain-0041", to_custodian_ref="dist-region-3", credential=<lab7_credential>){entry_id="e2", event_id="ev_1002"}. Provenance writes the transferred entry (from_custodian_ref="manuf-lab-7", to_custodian_ref="dist-region-3"); the composition reads from_custodian_ref = "manuf-lab-7" and passes it as actor_ref to Audit Trail. The outgoing custodian’s credential attests the release. entry_to_event["e2"] = "ev_1002".

  3. Distributor transfers to hospital pharmacy. transfer_custody("chain-0041", "pharm-hosp-9", <dist_credential>){entry_id="e3", event_id="ev_1003"}. entry_to_event["e3"] = "ev_1003".

  4. Pharmacist transforms (dispenses dose). transform_custody("chain-0041", "pharm-hosp-9", "dispensed 10mg dose into dispensing unit D44", <pharm_credential>){entry_id="e4", event_id="ev_1004"}. entry_to_event["e4"] = "ev_1004".

  5. Archive at terminal disposition. archive_custody("chain-0041", "pharm-hosp-9", <pharm_credential>){entry_id="e5", event_id="ev_1005"}. entry_to_event["e5"] = "ev_1005". The Provenance chain is now Archived.

  6. FDA inspection. The FDA inspector asks: “Prove unbroken, attributed, tamper-evident, retention-compliant custody of batch X-91 under 21 CFR Part 211.” The system calls verify_custody("chain-0041", original_event_payloads) — the payload map retrieved from cold storage, keyed by audit-log sequence_number and covering every seal range the chain’s events fall in; the two-argument form is the signature. The result:

    • continuity_check = continuous (every from_custodian_ref matches the prior holder).
    • For each of e1e5: attestation_verification = verified (each attributed, each seal valid, each in Retained retention state).
    • overall_verdict = custody-proof-complete. The inspector sees a single structural answer to all four questions. Invariant 1 through 5 are the structural guarantees behind each field. The inspector consults no source code, no runbooks, no developer narration.

A detective collects exhibit-A at a crime scene, handled through forensic lab, evidence room, and courtroom. Each step uses this composition with credential bound to the individual officer’s or lab’s verified identity via the Audit Trail’s Actor Identity. At trial, defense counsel challenges authentication under FRE 901(b)(9).

The prosecution calls verify_custody(chain_id_of_exhibit_A, original_event_payloads) — the payload map assembled from the evidence-room records, keyed by audit-log sequence_number over the covering ranges read_record names. The returned custody-proof:

  • continuity_check = continuous — every transfer’s from_custodian_ref matches the prior holder; no unattributed handler appears.
  • Every entry’s attestation_verification = verified — each custodian’s credential was valid at the time of their action.
  • overall_verdict = custody-proof-complete.

Defense counsel’s claim — that the exhibit passed through an unrecorded, unverified handler — has no structural basis. Invariant 1 (attributed custody) and Provenance Invariant 4 (custody continuity) are the structural rebuttal. The exhibit is authenticated under FRE 901(b)(9)’s process-or-system standard.

Rejection path — transfer by non-current custodian

A prior custodian, "manuf-lab-7", attempts to record a transformation after having transferred to "dist-region-3": transform_custody("chain-0041", "manuf-lab-7", "added label update", <lab7_credential>)rejected(not-current-custodian). Step 2 writes the intent record custody.transform_intendedlab7_credential validates, so the attempt is authenticated and durable — and Provenance’s not-current-custodian guard then catches the call at step 3: no Provenance entry is written, no outcome event is recorded, no entry_to_event binding is created, and the binding bijection (Invariant 4) is preserved, because it is over entries and outcome events and an intent record is neither. The chain’s integrity is unchanged. The intent record stands, and that is the point: a former custodian attempting to alter a chain they no longer hold is precisely what a chain-of-custody trail should retain, and before the intent record existed the attempt left no trace anywhere. It surfaces in Generation acceptance check 6’s unmatched-intent residue and is adjudicated against the Provenance chain, which shows the attempt was refused.

Rejection path — custody action on an Archived chain

After the pharmaceutical chain is Archived (example step 5), a downstream system attempts another transfer: transfer_custody("chain-0041", "disposal-unit-1", <credential>)rejected(archived). Derived at step 2’s read-only resolution — which reads the terminal archived entry from chain state — and confirmed by Provenance’s own guard had the call proceeded. No state is written anywhere, and here that includes no intent record — because the archive preceded the call: Transfer Custody alone resolves existence and archival state before its intent record, so its not-known and archived arms are refused earlier than the other three chain-addressed actions’ constituent guards, which fire after theirs.

Failure path — custody step whose audit write fails, recovered

A hospital pharmacist transfers a controlled-substance tote: transfer_custody("chain-0107", "pharm-hosp-9", credential=<evid_room>). Step 3’s intent record custody.transfer_intended lands first — the evidence-room custodian’s credential validates, so what follows is an authenticated attempt on the record. Step 4’s Provenance.transfer commits (entry_id = e5); step 6’s AuditTrail.record_action returns recording-failure (substrate store outage — the (step) payload shows the Event Log append did not land). The call returns rejected(recording-failure(outcome)) — the position telling the pharmacist the entry exists and the action must not be re-run — and in the same outcome the orphan — a committed custody entry with no audit event — surfaces to the compliance dashboard as a high-priority finding. Recovery, per the (step)-aware discipline, has two writers in sequence and never at once. First, inside the invocation, before it returns: the pre-check traverses the log for an event carrying data.entry_id = e5 and finds none (had the failure been the retention arm, the event would already be appended and the invocation would bind to it instead), so the record_action is re-attempted under the pharmacist’s re-presented credential, up to outcome_retry_attempts times with the chain’s section held; had one landed, entry_to_event[e5] would be populated and the event attested by the custodian with no cascade_recovery marker — the custodian’s own attestation, landed late — and the call would return success. In this run the outage outlasts the attempts, so the invocation returns rejected(recording-failure(outcome)) and releases the section. Second, the scan: at its next run past custody_completion_bound it takes the chain’s section, re-runs the same pre-check under it, pairs e5 to the transfer intent (chain, to_custodian_ref, the bound; the intent’s actor equals the entry’s from_custodian_ref, so this is the ordinary orphan), writes custody.recovery_intended, and lands the outcome under recovery_identity with cascade_recovery = true and the pharmacist named in data.custodian_ref. A later Verify Custody reports e5 with recovery_attested = true, actor_match = match(recovery), and an Overall Verdict of custody-proof-complete-with-recovered-attestations([e5]) — structurally sound, surfaced for the auditor’s judgment. Had the credential been bad, this path would never have been reached: it is refused at step 3’s intent record with nothing committed. The other case that reaches the recovery_identity re-attestation inside the invocation is a credential valid at step 3 and revoked before step 6 lands — a mid-flight revocation — where no re-attempt under the custodian’s credential can land and the compensation re-attests at once, with the same marker, payload, and verdict.

DEA controlled substance — 21 CFR Part 1304 inventory custody

A DEA-licensed pharmacy tracks each controlled substance dispensing as a separate artifact with its own chain under this composition. Each Originate Custody (pharmacy receives from supplier) → Transfer Custody (to dispensing cabinet) → Transform Custody (patient dispensing recorded) → Archive Custody sequence produces a complete attributed, sealed, retention-governed custody record per substance unit. A DEA (Drug Enforcement Administration) compliance inspection calls Verify Custody for each in-scope chain and verifies the returned custody-proof-complete verdict. Every change of custody is attribution-stamped; every record is sealed; every record is retained per the applicable predicate rule.

Regulated adversarial scenarios

Three scenarios the composition must survive in regulated contexts:

Regulator audit — pharma: prove unbroken, attributed, tamper-evident, retention-honored custody of batch X-91 under 21 CFR Part 211.

An FDA inspector runs verify_custody("chain-0041", original_event_payloads). The returned custody-proof:

  • continuity_check = continuous: by Provenance Invariant 4, every from_custodian_ref equals the prior to_custodian_ref (or genesis custodian_ref). The chain cannot have a gap because Provenance reads from_custodian_ref from chain state — no forged predecessor is possible.
  • Per-entry attestation_verification = verified: by Invariant 1 (attributed custody), each entry has an outcome Audit Trail event whose Actor Identity attestation binds the custodian_ref to a credential — and, by Invariant 7, an intent event preceding the entry’s commit under that same custodian, which is the stronger claim under Part 11’s Attributable limb: the identity was checked before the custody step, not merely recorded after it. The Tamper Evidence seal (via Audit Trail Invariant 3) confirms no entry was rewritten.
  • Per-entry retention_state = Retained: by Invariant 3, every entry is under active retention per the configured predicate-rule policy; none has elapsed.
  • overall_verdict = custody-proof-complete. The inspector’s four questions — unbroken custody, verified attribution, tamper-evidence, retention honored — are answered from the records alone. No developer narration is required. Invariant 1 through 5 are the structural basis for each answer.

Disputed transaction — legal evidence: defense claims tampering or an unattributed handler under FRE 901(b)(9).

Defense counsel claims: (a) custody was broken (an unrecorded handler existed), or (b) a custodian was unverified (their identity was not attributable), or (c) the chain was tampered with (an entry was altered after the fact). The prosecution’s Verify Custody result addresses each:

  • Claim (a): continuity_check = continuous, resting on Provenance Invariant 4. If there were a gap, a transfer would have been recorded with a from_custodian_ref that did not match the prior holder — structurally impossible because Provenance reads from_custodian_ref from chain state.
  • Claim (b): per-entry attestation_verification = verified, resting on Invariant 1 and Audit Trail Invariant 1. Each custodian’s credential was verified by Actor Identity before their custody step committed — the intent record is the records-alone proof, and the outcome record’s intent_event_id names it — so the claim is exact rather than approximate; the attestation is in the record.
  • Claim (c): per-entry attestation_verification seal check (Invariant 2, Audit Trail Invariant 3). Any rewrite of an audit event produces a failed-verification(seal-proof-invalid) result. A rewrite of the Provenance chain itself is caught by the per-entry field-match clause (entry_payload_match): the sealed audit payload’s copies are the anchored values, so a rewritten transformation_descriptor or a consistently rewritten from/to pair reports entry-payload-mismatch, while a removed binding reports binding-gap. Direct cryptographic sealing of the chain store itself remains the composing Tamper-Evidence-on-Provenance concept the constituent’s own edge case names.

Breach or incident investigation: reconstruct custody during an anomaly window and bound the time of tampering via the Audit Trail seal cadence.

An incident responder suspects a custody record was tampered with between two dates. The responder calls verify_custody(chain_id, original_event_payloads) with payloads from the suspect window. For each entry in the window:

  • attestation_verification = verified for entries whose covering seal predates the anomaly: the seal is intact.
  • attestation_verification = failed-verification(seal-proof-invalid) for entries whose seal was tampered with: the first such entry, and its seal’s sealed_at and anchored_at (from the Tamper Evidence atom via Audit Trail), bound the window. The most recent seal that returns verified end-to-end and the first that returns failed-verification(seal-proof-invalid) define the forensic window — a bound that holds when the deployment chains its seals (each seal covering its predecessor’s proof, the substrate’s chained-cadence option); without seal chaining the tampering bound is per-seal, not global. The seal_cadence governs the window’s resolution.
  • attestation_verification = failed-verification(unsealed) for entries in the unsealed tail: these are covered by Event Log per-event immutability but not yet seal-verified; a tighter seal_cadence would have narrowed this window. Where seals carry anchored_at from a TSA (Time-Stamp Authority — a trusted third party that signs proofs of when data existed) outside the adversary’s reach, the upper bound on tampering time is independently established. The forensic picture comes from the records alone.

Generation acceptance

A derived implementation of Chain of Custody is acceptable — in the regulator-acceptance sense — when an external auditor, given the composition’s emergent state (entry_to_event) plus the Provenance chain store and the Audit Trail substrate stores, can do all of the following without recourse to source code, runbooks, or developer narration.

Record checks

These checks are answerable by reading the composition’s records (including the Audit Trail substrate):

  1. Every custody entry has a verified attribution. For every entry_id in the Provenance chain, confirm entry_to_event[entry_id] is populated. For every populated binding, call AuditTrail.verify_record(event_id, <the covering range's payloads, as read_record names the range>) and confirm it returns verified (or failed-verification(purged) for entries whose retention has elapsed — lawful destruction distinguished from missing). An entry with no entry_to_event binding, or whose attestation_verification returns failed-verification(attestation-...), is a conformance failure. Then confirm the attestation is the custodian’s: the event’s attesting actor_ref (the attestation record’s, surviving the purge) equals the entry’s custodian_reffrom_custodian_ref for a transfer — or is recovery_identity on an event carrying cascade_recovery = true whose data.custodian_ref equals it. A valid attestation under any other actor is a conformance failure (attribution-mismatch): the credential was real and the custody step was not its holder’s. For an event carrying cascade_recovery = true, the verified attestation lawfully binds the recovery_identity rather than the acting custodian — Invariant 1’s recovery arm — and the auditor confirms the payload names the acting custodian in data.custodian_ref; such entries are conformant but enumerated (they are the steps lacking the custodian’s own attestation). The marker and the attesting actor must agree: an event carrying cascade_recovery = true whose attestation binds a custodian, or an event attested by recovery_identity without the marker, is a conformance failure — a custodian-attested retry carries no marker (partial-failure edge case, leg (c)). Invariant 1 is the contract.

  2. Every custody entry is tamper-evidently sealed. For every custody outcome event of the chain (enumerated as check 5 enumerates them — the log, not the map), confirm the event’s seal-coverage status through the substrate’s declared consolidated read surface — AuditTrail.read_record(event_id), whose returned audit record carries the covering seal’s evidence_id and range, or unsealed tail (permissible only where the deployment’s unsealed-tail policy permits a pending seal). For covered events, AuditTrail.verify_record’s seal check returns verified. A sealed event that returns failed-verification(seal-proof-invalid) is a conformance failure; an unsealed event is a seal-cadence-gap finding. Invariant 2 is the contract.

  3. Custody is continuous. Replay the Provenance chain entries in sequence_number ascending order and confirm Provenance Invariant 4’s continuity clauses plus Invariant 5’s density, exactly as Verify Custody’s Continuity Check defines them (clauses (i)–(iv)): transfer from matches the cursor, transform/disclose/archive entries carry the then-current holder, the cursor is never null or double-valued, and the sequence is dense 1..n. A discrepancy at any entry on any clause is a conformance failure. Invariant 5 (records-alone custody proof) rests on this continuity check.

  4. Retention is honored; honest disposal is provable. For every custody outcome event of the chain (enumerated as in check 5), read the retention record through AuditTrail.read_record(event_id) — the returned audit record carries the retention record’s state — and confirm it is Retained or Purged. For Retained entries: now < retention_until, so no early purge has occurred (Audit Trail Invariant 2 + Retention Window Invariant 7). For Purged entries: a retention record in Purged state with purged_at ≥ retention_until is present — lawful destruction is provable from the records alone; AuditTrail.verify_record returns failed-verification(purged), distinguishing lawfully destroyed from missing (Audit Trail Invariant 8). A custody entry whose event_id is purged without a Purged-state retention record is a conformance failure. Invariant 3 is the contract.

  5. Binding bijection is complete. For every entry_id in the Provenance chain, entry_to_event[entry_id] is populated (no dangling custody entry). Then quantify over the log, not the map: enumerate the chain’s custody outcome events (the substrate’s sequence-range read, filtered composition-side to the custody.* outcome action_refs carrying this chain_id) and confirm each event’s data.entry_id names a Provenance entry in the chain (no orphan audit event), that exactly one outcome event names each entry (two is binding-duplicate — a second writer, foreclosed by the one-writer rule), and that entry_to_event agrees with that set. An entry_id without a binding, an entry named by two outcome events, or an event_id whose data.entry_id does not match its key in entry_to_event, is a conformance failure; an entry the trail records as principal-divergence (a custody.transfer_intended whose resolved actor differs from the committed from_custodian_ref, with no outcome) is reported under that class and is neither a bijection failure nor an orphan. Invariant 4 is the contract.

  6. Authentication precedence. For every custody outcome event in the trail (custody.originated, custody.received, custody.transferred, custody.transformed, custody.disclosed, custody.archived), confirm an intent event of the matching kind (custody.originate_intended, custody.transfer_intended, custody.transform_intended, custody.disclose_intended, custody.archive_intended) precedes it in the Audit Trail’s own sequence, is named by the outcome’s intent_event_id, and carries the same chain_id (for the four non-genesis actions) and the same acting custodian. Because the substrate validates the caller’s credential against the actor registry inside every record_action, the named intent event is the records-alone proof that the acting custodian was authenticated before the custody entry committed — which is what makes Invariant 7 verifiable rather than asserted. For an outcome event carrying cascade_recovery = true, compare against the original custodian named in data.custodian_ref, never against the compensating event’s attesting recovery_identity — the intent record for a compensated step still stands under the custodian’s own actor, and a naive attesting-actor comparison would condemn every event the composition’s own compensation path produces. An outcome event with no preceding intent event under the same acting custodian is a conformance failure: the custody step either bypassed this composition’s surface or committed before its authentication.

An intent event with no outcome event is not a failure, and for a chain of custody it is evidence worth having. It names an invocation whose constituent guard refused it (not-current-custodian, archived, already-archived, not-known), or one that committed and has not yet recorded, or one that died in between; the reconciliation sweep resolves which by consulting the Provenance chain, and an intent whose outcome never lands while the chain shows the entry did commit is exactly the orphan that sweep compensates — unless the committed entry’s from_custodian_ref is not the actor the intent authenticated, which is a principal divergence the sweep escalates and never compensates. A former custodian attempting to alter a chain they no longer hold now leaves an authenticated, durable record of the attempt where previously it left no trace anywhere.

Horizon. This check is answerable only within the audit retention horizon: the substrate’s purge destroys a purged event’s data, taking intent_event_id and the payload’s actor copy with it, so for entries behind the horizon the determination is unverifiable(purged-horizon) — never a conformance failure. This is the same retention-aware discipline Verify Custody applies to its binding determination.

  1. Constituent Generation acceptance bars. Verify each constituent’s own Generation acceptance bar over its respective store: Provenance’s seven checks (entry attribution, single-origin, custody continuity, sequence-number order, archived-terminal, no mutation, and event-typing-with-id-uniqueness — the seventh added in the constituent’s Final Critique 6 closure), and Audit Trail’s eight checks, per that composition’s own Generation acceptance section. The composition’s invariants depend on the correctness of the constituents’ invariants.

External checks

These audit questions arise around this composition but cannot be answered from the composition’s records alone:

  • Whether the custodian_ref corresponds to a real, authorized actor. This composition records custodian_ref values in Provenance and binds each to a credential via Audit Trail’s Actor Identity. It does not verify that the custodian_ref is the correct individual for the custody step (e.g., that the quality-release officer was authorized under the deployment’s organizational policy to release batch X-91). That requires a Permissions instance scoped to custody-transfer authority — a composing peer, not a constituent of this composition.
  • Whether the artifact_ref corresponds to a real artifact in the host system. This composition records artifact_ref as an opaque reference. It does not validate that the reference names a real, active artifact in the deployment’s artifact registry. Verification requires the host system’s artifact registry; this composition records the reference but cannot attest to its real-world meaning.
  • Whether the chain satisfies jurisdiction-specific requirements beyond the structural form. This composition satisfies the structural form (attributed, continuous, sealed, retention-governed). Whether the specific actions taken, custodians involved, or retention periods meet a particular court’s evidentiary standard (FRE 901(b)(9) authentication beyond structural continuity) or a specific agency’s substantive requirements (FDA 21 CFR Part 211 predicate-rule content) requires court documentation, regulatory guidance, or inspector judgment. This composition provides the structural form; the substantive assessment is the external evaluator’s.

Non-goals and edge cases

  • DAG-style multi-artifact custody and artifact splitting. Inherited from Provenance as an explicit non-goal. W3C PROV (Provenance Data Model — W3C’s directed-acyclic-graph representation of provenance with wasDerivedFrom and wasGeneratedBy relationships) wasDerivedFrom relationships — one artifact derived from or split into others — are out of scope. This composition is a linear single-artifact chain; custody continuity (Invariant 4) depends on the linearity. A pharmaceutical sample aliquoted into sub-samples requires separate chains, each with its own originate_custody(genesis_type=received) call; this composition records each independently.

  • Multi-party simultaneous custody. Dual custody (two parties must jointly hold the artifact), escrow, and similar shared-custody arrangements are out of scope (inherited from Provenance). The composition’s state machine has exactly one current_custodian at all times; Provenance Invariant 4 enforces this. Composing patterns that require dual-control gates (e.g., dual-key access to a controlled substance vault) must model joint custody above this composition.

  • Legal-hold suspension of purge over the custody chain. When litigation or investigation requires suspension of normal purge over custody audit events, a Legal Hold pattern intercepts purge calls against the Audit Trail substrate’s retention records. Defensible Retention composes Legal Hold + Retention Window + Audit Trail into the hold-blocks-purge surface; this composition does not absorb this concept. A deployment composing this composition + Defensible Retention gets both the custody-proof surface and the hold-blocks-purge gate over the custody audit events.

  • Right-to-erasure vs. retention obligation over custody chains. A GDPR (EU General Data Protection Regulation — Europe’s data-privacy law) Article 17 erasure request can collide with a regulatory retention obligation (FDA, DEA, FRE). This composition does not adjudicate; the Erasure Coordination composing pattern (with legal counsel in the loop) decides whether to honor, defer, or document the override. Inherited from the Audit Trail substrate’s edge case.

  • Cross-store consistency under partial failure. Every custody action of this composition writes an intent record to Audit Trail first, then to Provenance, then the outcome record to Audit Trail, then updates entry_to_event (a derived index outside the atomicity surface — see Composition state). A failure after the Provenance write but before the Audit Trail write produces an orphan custody entry: a Provenance entry with no Audit Trail event. Provenance’s entries are immutable once committed, so synchronous rollback is not available. The implementation must (a) recover (step)-aware and pre-checked: first traverse the log for an existing event carrying this entry_id — the substrate’s recording-failure(step) step-4 arm (retention placement over the audit event) leaves the event already appended and attested, where a blind retry would mint a duplicate custody.* event against Invariant 4’s exactly-one; on that arm the recovery binds entry_to_event to the existing event and the substrate’s own reconciliation owns the retention gap. Only where no event landed is the AuditTrail.record_action retried — at most outcome_retry_attempts times inside the invocation (Configuration), the per-chain critical section held throughout; when the attempts are spent the invocation returns rejected(recording-failure(outcome)), releases the section, and the orphan is the scan’s. One writer per entry: the scan takes the same per-chain section (chain_serialization, Configuration) for every chain it examines and holds it across its pre-check and its compensating write, so an invocation still retrying and a scan run — or two scan runs, at restart and on cadence, on one node or two — never both land an outcome for one entry; the look-then-append pre-check is re-read under the section, never before it, and a scan that cannot take a chain’s section skips the chain until its next run (§A compensator is exclusive). A chain whose invocation died holds no section; a chain whose invocation is alive and slow does, and the scan waits on the invocation’s own terminus rather than racing it. Then (b) immediately surface the orphan to the compliance dashboard as a high-priority finding in the same outcome that returns rejected(recording-failure(outcome)), and (c) once the compensating Audit Trail event lands, populate entry_to_event. The cascade_recovery = true marker rides exactly one arm — the recovery-identity re-attestation below, where the attesting actor is the deployment’s recovery_identity rather than the custodian, which is the fact the marker exists to surface. A transient-failure retry inside the invocation lands under the custodian’s own re-presented credential and carries no marker: it is the custodian’s own attestation arriving late, and marking it would make Invariant 1’s canonical arm, recovery_attested, and check 1 all misread a conforming write. The bind-to-existing arm writes no event and marks nothing. A retry the scan performs — after a crash, with the custodian gone and their credential never persisted — is attested under recovery_identity, carries the marker, names the custodian in data, and is preceded by a custody.recovery_intended record naming the chain_id, the entry_id, and the intent event (or candidates) it pairs the entry to, so the trail shows the compensation was occasioned by the scan and not by a direct call; what the compensating event carries is re-derived from the Provenance entry and the intent’s payload, never remembered. Two legs complete the protocol (added 2026-06-11 with Invariant 4’s safety + liveness restatement): the reconciliation scan — the same orphan enumeration (Provenance entries against outcome events through the substrate’s read surface), run at restart and on reconciliation_cadence, between two edges: it examines no intent younger than custody_completion_bound (a younger one may belong to an invocation still between its Provenance write and its outcome append, and a compensation fired at it would mint a second outcome for one entry) and none past the audit horizon (there the payloads are destroyed and the binding’s purged subset is truth-bearing — read, never re-emitted). It pairs by the records, and says when it cannot. An orphan entry’s intent is the unmatched intent event for the same chain_id whose parameters the entry realizes — the event type, the to_custodian_ref of a transfer, the descriptor of a transformation, the recipient of a disclosure — from the custodian the entry names; under the per-chain serialization at most one such invocation was in flight, so one match is the ordinary case. Where more than one unmatched intent matches (an authenticated attempt the chain refused, followed by the same custodian’s identical successful one), the compensating event carries intent_event_candidates naming every match rather than choosing, and intent_event_id only where the match is unique. Where none matches, nothing is compensated: an orphan entry with no unmatched intent realizing it inside the bound is a write that bypassed this composition’s surface, or one whose intent aged out — it is escalated as an unresolved finding (orphan unpaired) within compensation_window, and no outcome is manufactured for it. The transfer leg pairs in two steps, because the field the pairing would otherwise read is the one a breach corrupts: an orphan transferred entry is paired to an unmatched custody.transfer_intended by chain_id, to_custodian_ref, and the bound — not by the custodian the entry names — and the intent’s actor_ref is then compared to the entry’s from_custodian_ref: equal → the ordinary orphan, compensated under recovery_identity; different → the principal divergence below, escalated and never compensated. A pairing rule that read from the custodian the entry names would find no intent for a divergent entry and file it under the zero-match case, and the fourth disposition would be unreachable. Genesis is the weak case — its intent can name no chain_id, and Provenance’s read surface is keyed by chain_id alone, with no lookup by artifact_ref (the constituent’s own edge case sends that to a host-side index). Finding the orphan chain at all therefore rests on genesis_lookup (Configuration), an instance capability the deployment declares or does not. Where it is declared, the scan resolves each unmatched custody.originate_intended older than the bound to the chains originated for its artifact_ref and pairs by custodian, genesis_type, and the bound. Where it is not, the genesis leg is downgraded to surfaced: the unmatched intent is escalated as genesis unresolved within compensation_window and never compensated, and Invariant 4’s liveness arm reads surfaced rather than bound for genesis orphans in that deployment — the honest answer, since a compensation the scan cannot pair to a chain would be a record with nothing behind it. A compensating event names at most intent_candidates_cap candidates; past the cap the orphan is escalated, not compensated, so the compensation payload stays inside the envelope the step-1 caps size. The scan catches the crash-orphan, a process death between the two truth-bearing writes that leaves no returning outcome to surface the finding. The scan gains a second, richer input: unmatched intent records — an intent event no outcome event names through intent_event_id. Its disposition rule must be read as stated, or the sweep manufactures a new class of false finding: an intent record with no outcome record is not a failure. It is an invocation that committed nothing (a constituent guard refused it), or committed and has not yet recorded, or died between the two; the scan resolves which by consulting the Provenance chain, and only the middle case is an orphan owing compensation. A fourth disposition belongs to Transfer Custody alone: a committed transferred entry whose from_custodian_ref differs from the actor its unmatched custody.transfer_intended authenticated is a principal divergence (step 5’s landing) — the serialization obligation was breached and nobody authenticated the release the entry records — and the scan must not compensate it: an outcome attested under recovery_identity would manufacture an attribution for a release no credential attested. It is escalated as an unresolved finding of its own class, outside Invariant 4’s bijection, and stays there. Intent records are invisible to the duplicate-suppression traversal below, since they carry no entry_id; and the deterministic-rejection arm — where the outcome write’s failure is deterministic rather than transient, which after the intent record leaves exactly one cause, a mid-flight credential revocation (an invalid-request there is foreclosed by the step-1 caps and, being a payload refusal, could not be cleared by re-attestation anyway — see the rejection-mapping rule’s two arms) — retrying the same call cannot land it, so the compensating record_action is re-attested under the deployment’s declared recovery_identity (see Configuration) — the one arm whose event carries cascade_recovery = true, naming the original custodian in data. The Archive Custody partial failure is the most consequential: an Archived chain without its terminal audit event is a deficiency in the custody-proof surface, though not a custody-gap finding (the Provenance archived entry still anchors the terminal disposition). Deployments under FDA 21 CFR Part 11 ALCOA and FRE 901(b)(9) exposure must treat any orphan custody entry as a hard alerting condition.

  • Pre-genesis external custody. When a chain opens with genesis_type = received, the artifact had custody history outside this system before intake. This composition records custody only from the genesis entry forward; it makes no claim about pre-intake custody. Provenance Invariant 4 (custody continuity) applies from genesis onward, not before. Pre-intake provenance, where required, is a separate chain or an external record the host links via metadata at Originate Custody.

  • Clock source. The recorded_at readings that appear in this composition’s records are sampled by others — this composition samples none of them. The intent record changes nothing here and deliberately so: it carries no caller-composed timestamp, and the substrate stamps it at its own seam exactly as it stamps the outcome record. A composition-supplied intended_at would re-open the clock question this spec settled by consuming no clock at all. The Provenance entry’s own recorded_at is stamped by Provenance from the wall-time reading injected at that atom’s I/O seam when the entry is written (Provenance’s Wall-time is best-effort rule and its own Logic-confinement note — the atom reads no clock inside its transition either). The audit event’s own recorded_at is stamped by the substrate at its seam when Event Log appends the event — record_action(action_ref, actor_ref, credential, data) accepts no timestamp and the composition writes none into the payload (a caller-composed recorded_at inside data would be a second, unverifiable clock claim the substrate has no mechanism to inject or police); an auditor reads the event’s timestamp back through the substrate’s declared read_record surface. Logic-confinement (execution-contract.md §Logic Confinement Principle, rule 3) is preserved by this composition consuming no clock at all — each constituent stamps at its own seam — and no action signature carries a now parameter (the seam, not a parameter, is the contract for clock entry). The two stamps are therefore two seams’ readings, ordinarily adjacent and never claimed equal. This follows Immutable Transaction Ledger’s Clock source and Resolve a Person’s Data Rights’ Clock semantics edge cases. Both readings are best-effort wall-time annotations; sequence_number is the authoritative order source (inherited from Provenance). For deployments where custodial timestamps have legal force (chain-of-custody timestamps in court proceedings, pharmaceutical distribution records under ALCOA), the implementation must source time from a trustworthy clock; a Trusted Timestamping composition (per RFC 3161 — the Internet standard for trusted time-stamping) provides the verifiable time anchor. Audit Trail’s Clock source for cadence and purge edge case governs the seal-cadence and retention-purge clock — and, transitively, the host-injected reading the substrate supplies as now; this composition inherits it.

  • Concurrent transfer attempts. Two callers simultaneously attempting Transfer Custody on the same chain must be serialized by the underlying implementation. The first write wins; the second observes an updated current_custodian and either succeeds (if the new holder is the intended recipient) or fails. The composition does not provide compare-and-swap semantics for conditional transfer; Provenance’s Concurrent transfer attempts edge case governs. Implementations serializing on chain_id satisfy the requirement — the section is the deployment’s declared chain_serialization (Configuration), with the lease semantics stated there — and the critical section must span the resolution read through the outcome record and every in-invocation retry, not merely the constituent’s state transition, and it covers every writer on the chain: the five custody actions and the reconciliation scan alike. This is load-bearing for Transfer Custody since the authentication-precedence touch: a narrower section lets another transfer interleave between step 2’s resolution and step 4’s commit, so the acting_custodian the intent record authenticated would differ from the from_custodian_ref the committed entry attributes — the intent record binding one principal while the outcome attributes another, which is a binding defect rather than a lost update. Step 5’s confirmation detects the divergence and lands it as rejected(principal-divergence) — a finding class of its own that the scan never compensates (partial-failure edge case); the widened serialization is what makes it unreachable in a conforming deployment, and the landing is what makes a non-conforming one visible.

  • Seal cadence vs. custody-proof completeness. Events in the Audit Trail’s unsealed tail (between sealed_through and the current append point) are not yet covered by a seal; attestation_verification for those events returns failed-verification(unsealed) under strict mode, or verified under lenient mode. Under lenient mode, per-event Event Log immutability is accepted as sufficient until the next seal cadence. The seal_cadence configuration governs the window; tighter cadence gives stronger real-time tamper-detection. Deployments under ALCOA (pharmaceutical) or FRE 901(b)(9) (legal evidence) should use per-event cadence (Configuration — the presentation-shape argument), or a tight interval cadence where the range-shaped presentation and its partial-coverage consequence are acceptable. Inherited from Audit Trail’s Verification of the unsealed tail edge case.

  • custodian_ref binding across rename. If a deployment renames a custodian’s reference (identity migration, staff change), Audit Trail’s Actor Identity continues to verify attestations against the historical public material as long as that material is retained. This composition records the opaque custodian_ref as it was at the time of the action; a rename does not retroactively alter historical entries (Provenance Invariant 1 — entry immutability). The binding between the old custodian_ref and the real-world actor is externally clearable (see Generation acceptance).

  • Artifact transform without custody change — disclosure-scope accounting. Provenance’s disclose records only the custody-timeline fact that a disclosure occurred — to whom, at what point in the chain. What subset of the artifact’s data was disclosed and under what authority is the Selective Disclosure concept. This composition wraps Provenance’s disclose faithfully; deployments requiring disclosure-scope accounting compose Selective Disclosure alongside this composition.

  • Retention of the Provenance chain vs. the custody audit events. This composition’s audit_trail_retention_policy governs the lifetime of the custody audit events (the attributed, sealed record_action entries), not the Provenance chain entries themselves. Provenance is append-only and retains its chain indefinitely from its own perspective — its spec names retention and defensible disposal as composing concepts. The consequence: when a custody audit event reaches its retention end and is lawfully purged via the Audit Trail cascade, the corresponding Provenance entry persists, and Verify Custody reports that entry’s attestation_verification = failed-verification(purged) — lawful destruction of the attribution and seal, honestly distinguished from missing (Invariant 3). Defensible disposal of the Provenance chain itself — destroying the custody entries, not just their audit attribution — is a separate composing concept: a Retention Window / Defensible Retention instance applied to the Provenance chain directly. This composition does not absorb full-chain disposal; it governs the attributed audit layer and names chain disposal as the composing concept.

  • Auditing the custody-proof query itself. Verify Custody is a pure read; it records no Audit Trail event. For high-assurance deployments that must also account for who requested a custody proof and when — an access-audit over the forensic-query surface — an access-logging composing pattern wraps this composition’s read surface, mirroring how Audit Trail names attempted-but-not-committed actions as a Failed-Attempt Log composing concept rather than absorbing them. This composition’s own audit surface is committed custody actions, not custody queries.


Terms

The canonical concepts this spec refers to. Each [Term] marker in the prose above links to its term entry here. A term entry states what the concept is, in plain English, plus its Kind — one of five: Type (a thing or category), Operation (a behavior), Member (a value of an enumerated Type), or, for a named datum, Field (a datum a Type carries — what does it carry?) or Parameter (a value an Operation needs — what does it need?). A term entry also names the Type it is a Member of / Field of, the Operation it is a Parameter of, and its Role where the domain assigns one. A term entry carries one Projection line — the concept’s single canonical lowering token, the one place the concrete name stays visible on the page — for every Field, Parameter, and pinned/wire Member. Everything else about casing (each target’s snake / camel / pascal / const / wire form) is derived from that one token by tools/harness/term-adapter.mjs, never hand-written. This is a composition, so its own concepts are: the six custody actions it exposes — the five custody-recording actions (Originate Custody, Transfer Custody, Transform Custody, Disclose Custody, Archive Custody) and the emergent verification (Verify Custody); the structure that verification returns (Custody Proof) with its custody-continuity replay (Continuity Check) and summary Overall Verdict; and the two verdict values (Custody Proof Complete, Custody Proof Incomplete). Its load-bearing guarantee — every Provenance custody entry binds to an attributed, sealed, retention-governed Audit Trail event, so records-alone custody proof (continuity + attribution + tamper-evidence + retention) holds from origin to disposition (the custody-event ⇒ audit-event binding, Invariant 4) — is a structural property, not a datum. Its emergent state (entry_to_event) is a single derived index over the Audit Trail substrate, left as a backticked token; the chain lifecycle states (Open / Archived) are the Provenance constituent’s, and its rejection taxonomy is entirely relayed (Provenance’s not-known / archived / already-archived / not-current-custodian / invalid-ref / invalid-genesis-type / invalid-descriptor; the Audit Trail substrate’s invalid-credential and invalid-request, both reachable cleanly at the intent record) or the recording-failure(intent | outcome) this layer returns, so none is carded. The custody audit event types — the outcome records (custody.originated, custody.received, custody.transferred, custody.transformed, custody.disclosed, custody.archived) and the intent records that precede them (custody.originate_intended, custody.transfer_intended, custody.transform_intended, custody.disclose_intended, custody.archive_intended), together with the intent_event_id payload token that joins each pair — and the per-entry verification values (verified, failed-verification(reason), unverifiable(reason) with its payload-not-supplied / availability / partially-purged-coverage reasons, and the continuity/failure classes continuous, gap-detected, binding-gap, binding-unknown(purged-horizon), entry-payload-mismatch, attestation-failed, seal-failed) stay backticked as wire values, as do the read passthrough (read), the constituent calls and their outcomes — Provenance’s originate / transfer / transform / disclose / archive / read, Audit Trail’s record_action / verify_record — the relayed constituent tokens (chain_id, entry_id, event_id, artifact_ref, custodian_ref, from_custodian_ref, to_custodian_ref, recipient_ref, genesis_type, transformation_descriptor, credential), the composition output fields left uncarded (entries, chain_state, attestation_verification, retention_state, sequence_number), the deployment configuration knobs (audit_trail_retention_policy, seal_cadence, recovery_identity), and concrete example ids. Constituent atom and substrate names remain the existing full links to ../atoms/* and ./audit-trail.md; constituent operations stay backticked qualified calls, not cross-page links (the decided convention). (annotation.md Terms registry; representational only — it changes no guarantee, invariant, or behavior of the composition above.)

Originate Custody

The composition action that opens a new Provenance custody chain for an artifact under a genesis custodian, binding the genesis entry to an attributed Audit Trail event (custody.originated). Returns {chain_id, entry_id, event_id}; the genesis custodian’s credential attests the artifact entered custody under their hand.

Kind: Operation

Transfer Custody

The composition action that records a hand-to-hand transfer to a new custodian. The outgoing custodian attests the release — from_custodian_ref is read from Provenance’s chain state (unforgeable, Provenance Invariant 4) and bound as the actor on the custody.transferred event; the incoming custodian attests their own later actions. A committed entry whose from_custodian_ref is not the custodian the intent record authenticated is refused as principal-divergence with no outcome written — a serialization breach surfaced as its own class, never compensated.

Kind: Operation

Transform Custody

The composition action recording a transformation of the artifact by the current custodian (Provenance enforces holder-only), bound to an attributed custody.transformed event.

Kind: Operation

Disclose Custody

The composition action recording a disclosure of the artifact by the current custodian to a named recipient — custody is not transferred — bound to an attributed custody.disclosed event.

Kind: Operation

Archive Custody

The composition action recording the terminal disposition by the current custodian and transitioning the chain to Archived — no further entries are accepted — bound to an attributed custody.archived event.

Kind: Operation

Verify Custody

The composition’s emergent verification: construct the records-alone Custody Proof for a chain by replaying Provenance continuity (Continuity Check) and, for each entry, verifying its bound Audit Trail event’s attestation, seal, and retention. Neither constituent can answer it alone; the caller presents the original event payloads (Audit Trail’s verification asymmetry).

Kind: Operation

Custody Proof

The structured artifact Verify Custody returns: the chain’s state, its Continuity Check result, an ordered per-entry sequence of attestation / seal / retention verifications, and a summary Overall Verdict. The records-alone proof of unbroken + attributed + tamper-evident + retention-governed custody that neither Provenance nor Audit Trail provides alone.

Kind: Type Role: the records-alone custody proof

Continuity Check

The Custody Proof field carrying the result of replaying the Provenance entries in order and confirming every transfer’s from_custodian_ref equals the custodian in effect immediately before it (Provenance Invariant 4, replayed on demand): continuous, or gap-detected naming the entry and the mismatch.

Kind: Field Field of: the custody proof Role: the custody-continuity result Projection: continuity_check

Overall Verdict

The Custody Proof’s summary field, with three values: Custody Proof Complete when continuity holds, every entry is bound, every supplied verification passes (or is lawful destruction), and no entry is recovery-attested; custody-proof-complete-with-recovered-attestations(entry_ids) — the same, except the named entries carry recovery attestations in place of the custodian’s own, surfaced for the auditor’s judgment; else Custody Proof Incomplete naming the specific failure, incomplete-input, and standing-coverage classes.

Kind: Field Field of: the custody proof Role: the summary verdict Projection: overall_verdict

Custody Proof Complete

The Overall Verdict value when the chain’s continuity is unbroken, every entry has its audit-event binding, and every supplied attestation verifies (or is a lawful failed-verification(purged)) — the full custody is proven from the records alone.

Kind: Member Member of: the overall verdict Role: Verdict Projection: custody-proof-complete

Custody Proof Incomplete

The Overall Verdict value when at least one failure, incomplete-input, or standing-coverage class applies (a continuity gap, a binding gap or duplicate, an entry–payload mismatch, an attribution mismatch, a failed attestation, a seal failure, a not-supplied payload, an availability condition, or partially-purged seal coverage) — the proof is not fully self-proving, and the verdict names why per class.

Kind: Member Member of: the overall verdict Role: Verdict Projection: custody-proof-incomplete


Standards references

This composition is the structural form of the chain-of-custody requirement across its canonical domains:

  • FDA 21 CFR Part 211 (Current Good Manufacturing Practice — Finished Pharmaceuticals) — requires an unbroken, attributed, tamper-protected, retention-compliant chain of custody from manufacture through distribution for drug substances and products. The composition’s Originate CustodyTransfer CustodyArchive Custody sequence, with per-entry attribution and seal via the Audit Trail substrate, is the operational form of Part 211’s custodial recording requirement. The ALCOA / ALCOA+ (Attributable, Contemporaneous, Original, Accurate, plus Complete, Consistent, Enduring, Available) principles are all satisfied simultaneously by the four-atom stack inside the Audit Trail substrate, applied to every custody entry.

  • DEA 21 CFR Part 1304 (Controlled Substance Inventory Records) — requires complete, accurate records of every change of custody of a controlled substance. Invariant 1 (attributed custody) and Invariant 5 (records-alone custody proof) are the structural implementation of this requirement at every transfer step.

  • Federal Rules of Evidence 901(b)(9) (Authenticating or Identifying Evidence — Process or System) — the US evidentiary rule for authenticating physical or electronic evidence via chain-of-custody records. The composition’s Verify Custody result — continuity_check = continuous plus per-entry attestation_verification = verified — is the structural form of the unbroken, attributed chain courts require for authentication. Invariant 4 (custody continuity) and Invariant 1 (attributed custody) are the rebuttal to authentication challenges.

  • ISO 23081 (Information and documentation — Managing metadata for records) — the ISO (International Organization for Standardization) standard on records-management metadata. The composition’s custody entries map directly to the origin, transfer, transformation, and disclosure metadata elements ISO 23081 specifies; the Audit Trail attribution and retention satisfy ISO 23081’s authenticity and retention obligations.

  • W3C PROV (Provenance Data Model — W3C’s RDF (Resource Description Framework)-based standard for representing provenance as a directed acyclic graph) — this composition implements the linear single-entity spine of a W3C PROV graph: the wasGeneratedBy, used, and wasAttributedTo relationships along a single entity’s chain. The deliberate non-goal of wasDerivedFrom (DAG derivation) and artifact splitting are explicitly out of scope relative to the full PROV model.

  • SEC Rule 17a-4 (Records to be preserved by certain exchange members, brokers, and dealers) — requires broker-dealer records (including custody records for financial instruments) to be preserved in a non-rewriteable, non-erasable format. The Audit Trail substrate’s Tamper Evidence (Invariant 2) satisfies the integrity requirement; the configured audit_trail_retention_policy satisfies the lifetime requirement.

this composition inherits the broader standards compliance of its constituents:

  • Through Audit Trail (and its transitive atoms): SOX (Sarbanes-Oxley Act) §802 record retention, HIPAA (Health Insurance Portability and Accountability Act) §164.312(b) audit controls, PCI DSS (Payment Card Industry Data Security Standard) Requirement 10, 21 CFR Part 11 electronic records, ISO/IEC 27001 §A.12.4 logging and monitoring, GDPR Articles 30 and 32, and the full Audit Trail standards inheritance. Deployments composing this composition for pharmaceutical or legal-evidence purposes receive these as the substrate’s contribution; they are framed as inherited, not as this composition’s own primary standards anchors.

  • Through Provenance: ISO 23081, W3C PROV, FDA 21 CFR Part 211, DEA 21 CFR Part 1304, FRE 901(b)(9), and SEC Rule 17a-4 at the structural custody-chain layer. This composition lifts these to the full attributed+sealed+retention-governed form those standards actually require but that Provenance alone cannot satisfy.


Status

partially resolved — see the Ledger.

Ledger

status: partially resolved
formal: pending — re-derivation, 2026-08-30: the one-writer protocol (bounded in-invocation retry, scan under the per-chain section), the principal-divergence landing, and the genesis downgrade; was verified — chain-of-custody.tla + 2 twins; Invariant 7 not yet modelled, obligation open; 2026-06-11
last gate: 2026-08-30 — third gate, fresh reader, under the frozen rules — 5 foundational corrected in-round, 14 refining and 5 rhetorical routed (2 refining and the closure check's 3 residues and 7 consistency items also corrected in-round); 2026-08-26 authentication-precedence gate — 1 foundational routed (since closed), 2 foundational and 8 refining/rhetorical corrected in-round, 7 refining and 5 rhetorical routed (1 refining since closed); Final Critique 7's 2 other foundational (since closed), 7 refining and 4 rhetorical also routed

open:
- 2026-08-26-d · refining · [Originate Custody] step 3; [Transfer Custody] step 5 · `Provenance.read`'s fail-stop contract has no declared landing at the genesis and confirmation reads, so an unavailable read strands a committed entry with no outcome record → land the arm
- 2026-08-26-e · refining · step 1, all five actions · the `credential` shape check has no landing in two actions and a mis-typed one (`invalid-ref`) in three → land it as `invalid-credential`
- 2026-08-26-f · refining · `entry_to_event` rebuild; duplicate-suppression pre-check; Invariant 4 safety leg; check 6 · every enumeration-dependent procedure is a payload-field or `action_ref`-shaped lookup the substrate routes away from its pass-through, and neither the range-read capability nor Reverse Index is declared → declare the sequence-range read as an instance capability with selection composition-side
- 2026-08-26-h · refining · `attestation_verification` vocabulary; [Custody Proof Complete] · the `(compensation-window)` qualifier is never relayed → relay it
- 2026-08-26-i · refining · Generation acceptance check 5 · lacks the purged-horizon carve-out checks 1 and 6 carry → add it
- 2026-08-26-j · refining · Pass 2 boundary · the line between the intent record's retained attempts and the Failed-Attempt Log composing concept is never drawn → draw it
- 2026-08-26-k · rhetorical · Edge cases, concurrent transfer attempts · "either succeeds or fails" misstates the atom's both-succeed-under-serialization rule, and the reason the second call fails through this surface is unnamed → correct and name it
- 2026-08-26-m · rhetorical · [Originate Custody] step 3 · the serialization citation over-cites where Provenance Invariant 3 suffices → cite narrowly
- 2026-08-26-n · rhetorical · Standards references; Summary · IEC unexpanded; DEA re-glossed with a different expansion → gloss once
- 2026-08-26-o · rhetorical · normative prose · three changelog residues → remove
- 2026-08-26-p · refining · `retention_state` relay · misplaces the `(compensation-window)` qualifier, incompatible with `Purged`, and the actual channel is unstated → move it to `verify_record` outcomes
- 2026-08-26-q · refining · `unverifiable(availability)` relay · "relayed unchanged" is self-contradictory → pin whether the constituent's reasons survive or collapse
- 2026-08-26-r · refining · [Originate Custody] · the rejection arm for an empty credential is unspecified; the blanket `invalid-ref` mapping elsewhere is undeclared rationale → specify and declare
- 2026-08-26-s · refining · Examples, breach scenario · "chained-cadence option" cites a substrate knob that does not exist; chaining is a `seal_mechanism` property → correct
- 2026-08-26-t · refining · Invariant 2 · omits the standing `partially-purged-coverage` bound the spec elaborates → add it
- 2026-08-26-u · refining · Generation acceptance checks 1 and 5; step 3a · brand every missing binding a conformance failure, against step 3a's `binding-unknown(purged-horizon)` → reconcile
- 2026-08-26-v · rhetorical · length-cap scope predicate · lists `metadata`, which rides no audit payload → drop it
- 2026-08-26-w · rhetorical · cross-reference · "signatures above" should read below → fix
- 2026-08-26-x · rhetorical · Terms preamble · "two verdict values" against the carded three → fix
- 2026-08-26-y · rhetorical · `entry_payload_match` · "anchored copy" overreaches under an `unverifiable` seal outcome → soften
- 2026-08-29-a · refining · formal · the model's compensation action carries no identity, no recovery record, and no age bound → extend the model with the bounded scan under `recovery_identity`
- 2026-08-30-v · refining · formal · the model has one compensator; the protocol now has two writers over one entry serialized on the chain, a counted retry terminus, and a `principal-divergence` landing outside the bijection → extend it
- 2026-08-30-a · refining · [Verify Custody] step 3a · "predates that horizon" has no predicate; a never-bound entry older than the first purge is labelled `binding-unknown(purged-horizon)` and masks a `binding-gap` → define it via the durable purged-subset binding
- 2026-08-30-b · refining · check 5 vs step 3a · a lost purged-subset binding is a durability finding in one and a conformance failure in the other → pick one
- 2026-08-30-c · refining · Composition state, purged subset · the truth-bearing half names no atom, and "persist `entry_id` at purge time" rides a cascade the composition does not wrap → name Erasure Tombstone *(forthcoming)*, drop the alternative
- 2026-08-30-d · refining · outcome-position read-back · "declared enumeration selecting `action_ref` and `intent_event_id`" attributes payload-field selection to Audit Trail; not declared as an instance capability; a replica-lag miss has no fallback → declare the range read with composition-side filtering (with 2026-08-26-f), state the miss fallback
- 2026-08-30-e · refining · Configuration bounds · the scan's `now` is undeclared in a composition that consumes no clock → declare the scan's reading as host-injected at its seam
- 2026-08-30-f · refining · [Overall Verdict] · `failed-verification(unsealed)`, the `(compensation-window)` qualifier, and `binding-unknown(purged-horizon)` are assigned to no verdict class → enumerate the landings
- 2026-08-30-g · refining · Invariant 1 · stated unconditionally while Invariant 4 admits the orphan transient → qualify at quiescence modulo `compensation_window`
- 2026-08-30-i · refining · *Concurrent transfer attempts* · "the second either succeeds (if the new holder is the intended recipient)" misattributes Provenance's rule → restate (with 2026-08-26-k)
- 2026-08-30-j · refining · step 1, [Originate Custody] · an empty credential has no code → `rejected(invalid-credential)` (with 2026-08-26-e, -r)
- 2026-08-30-k · refining · `attestation_verification` bullet · "interval seal cadence this spec recommends" contradicts Configuration's per-event recommendation → fix
- 2026-08-30-l · refining · Configuration · `compensation_window` / `reconciliation_cadence` collide by name with the substrate's knobs; `custody_completion_bound` "Default: none" states no start-time refusal → distinguish, and state the refusal
- 2026-08-30-m · refining · Standards references; prose · W3C, RFC, IEC undefined; "non-repudiable" unglossed → define inline (with 2026-08-26-n)
- 2026-08-30-n · refining · genesis pairing key · step 2 says `(actor_ref, artifact_ref, genesis_type)`, the edge case says custodian, `genesis_type`, and the bound → align
- 2026-08-30-o · refining · outcome-position `invalid-request`, event absent · the orphan no liveness arm covers while Invariant 4 claims every orphan is bound → state its disposition (escalated, not bound)
- 2026-08-30-q · rhetorical · Terms · lists `record_action` / `verify_record` only; `read_record` and the range read are consumed → list them
- 2026-08-30-r · rhetorical · Examples · "chained-cadence option" — chaining is a `seal_mechanism` property → rename (with 2026-08-26-s)
- 2026-08-30-s · rhetorical · `recovery_identity` bullet · restates the narrow case three times → split
- 2026-08-30-t · rhetorical · Invariant 4 liveness · the `invalid-request` sentence reads as re-attest → rewrite
- 2026-08-30-u · rhetorical · Terms · `principal-divergence` absent from the "entirely relayed" sentence → add

Decisions

Directional changes only — the turns a future reader must know the pattern took, and why. Everything smaller lives in the commit that made it: git log -- compositions/chain-of-custody.md.

  • 2026-08-30 — One writer per entry, the purged answer first, the attestor named, the divergence landed, the genesis leg honest about what it can find. Chose: an in-invocation retry bounded by outcome_retry_attempts and a scan that takes the per-chain section for every chain it touches, so the invocation and the scan never both land an outcome for one entry, with a binding-duplicate collision rule on the rebuild and check 5 quantified over the log; Verify Custody branching on retention state before any composition-side membership check, so a purged entry lands failed-verification(purged) and Custody Proof Complete stays reachable after a purge; an actor_match field and check-1 clause comparing the attestation’s surviving actor_ref to the entry’s custodian; rejected(principal-divergence) at Transfer Custody step 5 with no outcome written and a scan that never compensates it; genesis_lookup declared as an instance capability requirement, the genesis leg downgraded to surfaced without it; recording-failure(intent | outcome) on every signature; the liveness inequality written out with outcome_write_latency; intent_candidates_cap sizing the compensation envelope; and, on the closure check, chain_serialization declared as the instance capability the one-writer rule rests on, with a lease exactly the bound long whose expiry is the invocation’s terminus, the transfer leg pairing by chain and recipient before it compares actors so a divergence is reachable at the scan, the zero-match landing (escalate, never compensate), and checks 2 and 4 quantified over the log. Over: “retry until it lands” beside a scan starting at the bound; a membership check ahead of the constituent’s purged short-circuit; a check that confirmed the credential was valid and not whose it was; a code in the signature with no landing; a genesis leg that presumed a lookup Provenance declines; a bare token on both sides of the commit. Because: two compensators over one act land two outcomes the seal then protects; a lawfully destroyed payload is not one the caller failed to supply; a valid credential attesting a step its holder did not perform is the forgery the proof exists to catch; a committed entry nobody authenticated cannot be attested later without inventing an attribution; and a caller who cannot tell intent from outcome re-runs a committed act (the frozen rules of 2026-08-30 — A compensator is exclusive, Lawful destruction is answered before absence, A composition’s own rejection arm carries the retry bit, Liveness is arithmetic, An outcome is sized before the intent, and Capability provenance frozen — with §Authentication precedence’s binding half and §A transcribed rejection arm’s fourth tell).
  • 2026-08-29 — The scan is bounded at both edges, pairs by parameters and names candidates, and writes as the recovery identity behind a recovery record; the outcome’s step decides its landing. Chose: custody_completion_bound below and the audit horizon above for the reconciliation scan; an orphan entry paired to its intent by chain, custodian, and the parameters the entry realizes, with intent_event_candidates where more than one unmatched intent matches; every scan write under recovery_identity behind custody.recovery_intended (the in-invocation retry keeping the custodian’s credential and no marker); recording-failure(step-4) and the retention-source invalid-request at an outcome record read back and treated as landed at all five actions; compensation_window, reconciliation_cadence and index_durability declared; Invariant 4’s “committed atomically or” restated as ordered. Over: an unbounded scan whose crash-side retry had no identity it could honestly use, and “all three arms surface as recording-failure” at the outcome. Because: an unbounded scan mints a second outcome beside an in-flight invocation’s and re-emits lawful destruction as orphans; the custodian’s credential is never persisted; and the substrate’s step-4 arm means the event exists (the frozen rules of 2026-08-29 — A reconciliation is bounded at both ends, Intents pair with outcomes, Recovery commits under a declared service identity, A transcribed rejection arm keeps its payload).

  • 2026-08-27 — The verification presentation is keyed by audit-log position, not by custody entry. Chose: original_event_payloads keyed by sequence_number, with Verify Custody assembling each entry’s presentation from the covering range read_record names. Over: a map keyed by entry_id holding one payload per entry, or a wiring rule requiring per-event cadence. Because: the substrate’s rule is that a seal commits to a sequence range whose members include intent records and unrelated actions carrying no entry_id; an entry-keyed map cannot present that set under any cadence but per-event, and pinning the cadence would trade a representational fix for a deployment constraint.
  • 2026-06-11 — Invariant 4’s compensated arm is in the model, not idealized away. Chose: re-derive the model over sequential sub-writes with compensation. Over: the original model, which committed the sub-writes as one atomic action. Because: the Provenance write is irreversible and the audit append cannot be withdrawn, so the compensated path is the design.
  • 2026-08-26 — Transfer Custody resolves the outgoing custodian before it authenticates, and authenticates before it commits. Chose: a read-only resolution step, then the intent record under the outgoing custodian’s credential, then the Provenance call; invalid-credential restored to all five signatures, reversing Final Critique 5’s F2 on new grounds. Over: declaring the pre-check “a deployment obligation” (Final Critique 5’s F-6 closure), or authenticating the incoming caller, the only principal available earlier. Because: a declaration discharges nothing the records can show, and the guarantee names the outgoing custodian, a principal produced by the commit.