# RFC 0001: Minimal payload receipt provenance

**Status:** Implemented in `lifeloop.v0.2`.

**Date:** 2026-05-15.

**Scope:** `lifeloop.v0.2` lifecycle-contract schema bump before v1 freeze.

## Summary

Add the minimum payload provenance needed for a lifecycle receipt to identify
which client-owned artifact Lifeloop placed:

- `payload_kind: String` on each `PayloadReceipt`;
- `content_digest: Option<String>` on each `PayloadReceipt`, echoed only when
  the corresponding `PayloadEnvelope` carried one.

The source of truth is the payload envelope used during negotiation, not any
payload echoed by the client response.

Do not add client-specific baseline semantics, handoff semantics, durable
ledger APIs, body storage, metadata echoing, digest normalization, or repair
policy.

## Motivation

Lifeloop already accepts payload identity and integrity metadata on
`PayloadEnvelope`: `payload_id`, `payload_kind`, `byte_size`, and optional
`content_digest`. Receipt synthesis currently collapses that to
`payload_id`, `placement`, `status`, and `byte_size`.

That makes the receipt weaker than the placement decision it summarizes. A
later reader can prove that a payload label was delivered, skipped, degraded,
or failed, but cannot prove from the receipt alone which payload class or
declared artifact fingerprint was involved.

This is a generic lifecycle provenance gap. It is not specific to CCD handoffs.

## Non-Goals

- No CCD-specific fields such as `handoff_processed_at_startup`,
  `baseline_role`, or `processed_at_startup`.
- No inherited-vs-session drift classification.
- No stale-path repair policy.
- No durable receipt-ledger query surface.
- No `body` or `body_ref` persistence in receipts.
- No automatic digest calculation or verification beyond the existing payload
  validation contract.
- No digest-format registry or cross-client digest normalization.
- No blanket `metadata` echo into payload receipts.

## Proposed Wire Shape

`PayloadReceipt` becomes:

```rust
pub struct PayloadReceipt {
    pub payload_id: String,
    pub payload_kind: String,
    pub placement: PlacementClass,
    pub status: PlacementOutcome,
    pub byte_size: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_digest: Option<String>,
}
```

`payload_kind` is required because `PayloadEnvelope::payload_kind` is already
required on the payloads Lifeloop negotiates. It is not a global taxonomy. It
is meaningful inside the receipt's existing `client_id` scope.

`content_digest` remains optional because some valid payloads do not carry a
digest. It is omitted from serialized receipts when absent, matching existing
optional payload fields. When present, it is copied from the payload envelope
into the receipt. Lifeloop must not invent a digest during receipt synthesis.

## Provenance Source

Receipt synthesis must copy provenance from the payload envelope that Lifeloop
used during placement negotiation.

Do not source these fields from `CallbackResponse::client_payloads`. Client
responses may echo no payloads, may echo only derived payloads, or may echo a
different payload set than the dispatch delivered. Response payloads are output
payloads, not authoritative evidence for what Lifeloop attempted to place.

Do not merge these fields from `CallbackRequest::payload_refs` when a full
`PayloadEnvelope` was negotiated. `PayloadRef` is a request-level reference and
can describe out-of-band artifacts; this RFC does not add cross-correlation or
conflict detection between refs and envelopes.

The implementation may carry this data by adding internal provenance fields to
`PayloadPlacementDecision` or by adding an internal provenance map to
`NegotiatedPlan`. That internal carrier is not a wire-contract addition. It
exists only so `PayloadReceipt` is synthesized from the same payload facts that
placement negotiation observed.

If a future path creates payload placement decisions from `PayloadRef` values
without full `PayloadEnvelope` values, that path must define its own
provenance source before emitting the new receipt fields. This RFC does not
require payload receipts for ref-only requests.

## Receipt Synthesis Rule

When synthesizing a payload receipt from a `PayloadEnvelope` and placement
decision, Lifeloop copies:

- `payload_id` from `PayloadEnvelope::payload_id`;
- `payload_kind` from `PayloadEnvelope::payload_kind`;
- `byte_size` from the effective transported byte size already used for
  placement;
- `content_digest` from `PayloadEnvelope::content_digest`;
- `placement` and `status` from the placement decision.

Failed, skipped, and degraded placement outcomes still include the same
identity fields. They describe the artifact the adapter attempted or declined
to place.

## Why This Is Minimal

`payload_kind` and `content_digest` are already part of the inbound payload
contract. Echoing them does not give Lifeloop new semantic authority; it only
prevents receipt synthesis from throwing away correlation data that Lifeloop
already used while routing.

`body_ref` is intentionally excluded. References may encode local paths, URLs,
or storage topology that receipts should not preserve by default. A digest is
the narrower provenance handle.

`metadata` is intentionally excluded. Client metadata can carry arbitrary
product semantics and sensitive context. Echoing all metadata would make
receipts a client-state mirror instead of a lifecycle receipt.

The useful comparison key is client-scoped, for example
`(receipt.client_id, payload_kind, content_digest)` when a digest exists. This
RFC does not make `payload_kind` or `content_digest` globally comparable across
unrelated clients.

## Client Utility

- CCD can correlate one of its startup handoff payloads by kind and digest
  without Lifeloop knowing what a handoff means.
- Fixity-style clients can correlate their own repeated-signal batches by
  digest instead of relying only on caller-chosen IDs.
- Thread-sync publishers can prove which thread artifact class and fingerprint
  was handled without embedding the artifact body.
- RLM-style clients can trace recursive frame payloads through
  `parent_receipt_id` chains while keeping payload contents opaque.
- Memory clients can receipt-only candidate snapshots while preserving
  provenance without copying private candidate bodies into receipts.

## Compatibility

This is a wire-contract change and must ride the normal schema-bump discipline.
Because `PayloadReceipt` uses strict deserialization, adding a required field
has no rolling wire-compatibility window: old consumers reject new receipts and
new consumers reject old receipts. The schema bump must therefore be atomic for
each receipt path, or mediated by explicit schema negotiation outside this RFC.

The implementation change must update:

- `PayloadReceipt` in the public wire type;
- `PayloadReceipt::validate()` so `payload_kind` is non-empty;
- the internal placement-decision/provenance carrier used by receipt
  synthesis;
- receipt synthesis;
- wire-contract serialization tests;
- lifecycle-contract docs and fixtures;
- downstream conformance fixtures that pin payload receipts.

Durable idempotency stores must be schema-scoped or migrated when this change
lands. Replaying a pre-bump receipt against a post-bump receipt under the same
idempotency key is not required to compare equal, because the receipt wire
schema changed. Lifeloop should document that migration requirement rather than
adding a compatibility shim that preserves old receipt bodies inside new ones.

Until that schema bump lands, clients that need startup-baseline provenance
should store their own payload digest beside the Lifeloop receipt rather than
expecting Lifeloop to infer client semantics.

## Acceptance Criteria

- Every generated `payload_receipts[]` entry includes `payload_kind`.
- `PayloadReceipt::validate()` rejects an empty `payload_kind`.
- A generated `payload_receipts[]` entry includes `content_digest` exactly when
  the negotiated source payload included one.
- Receipt synthesis does not use `CallbackResponse::client_payloads` as the
  source for payload receipt provenance.
- Lifeloop does not compute, dereference, or persist payload bodies to satisfy
  this RFC.
- No CCD, handoff, memory, or repair vocabulary is added to Lifeloop core.
- Existing tests prove receipt-only payloads retain the same minimal provenance
  as prompt-delivered payloads.
