# Request evidence ledger implementation plan

**Date:** 2026-08-18  
**Plan slug:** `request-evidence-ledger`

## Owner-visible outcome

Opening a request on `/requests` shows the original ask and a truthful, ordered account of who did what, when, where, how, and under which recorded account. It can show the linked session, run, phase, gate, tool call, diff, landing, deployment, and installed proof. Every shown relationship comes from an explicit identifier written by the machinery that knew the fact. Missing history is named as a coverage gap; it is never reconstructed from titles, timestamps, paths, branch names, SHAs embedded in prose, or later silence.

The same records drive request lineage on `/reports`. The drawer and Reports must not maintain competing interpretations of delivery history.

## Evidence before implementation

Each slice ends with an installed owner flow, not a schema-only milestone. The required evidence is named at the start of each slice below. A slice is not complete until its exact source is on `origin/main`, deployed through the immutable release path, and exercised through the installed sidebar entrypoint. If browser infrastructure is unavailable, land the verified slice and name browser acceptance as unverified; never substitute an HTTP response for owner-flow proof.

## Measured gaps in the current implementation

1. `requests.title` is presented as “Your original request.” The complete original ask is not stored immutably.
2. `requests.detail` is both task description and current status reason. Lifecycle transitions clear or replace it, destroying prior meaning.
3. `receipt_trail` stores one short line plus a small untyped metadata object. It cannot faithfully represent gates, tools, diffs, logs, accounts, commits, deploys, proofs, or structured failures.
4. `request_transitions` records state changes, but not the complete evidence that caused them.
5. Branch, landing, commit, deployment, and proof do not have dedicated persisted relationships.
6. Repeated asks do not have an explicit relationship contract.
7. Land and deploy machinery do not currently stamp request evidence.
8. The task-board hook drops writes when the collector is unavailable.
9. Arbitrary manual receipts are attributed to `worker: "owner"`, even when that identity was not established.
10. `RequestsStore.findWorkKey()` scans prose and links for branch/SHA substrings. This is inferred correlation and must be deleted after explicit linkage is available.
11. The drawer prints permanent missing-data sentences because no source-coverage contract exists.
12. Rich factory phase, attempt, gate, tool, diff, token, and cost records exist, but request-to-run linkage is incomplete.

Existing rows are historical partial records. Migration must preserve their exact content and mark their unavailable fields; it must not invent original asks, actors, links, recovery, recurrence, account identity, cost, or proof.

## Truth rules

These rules are data-contract requirements and test assertions:

- Request/run/session/task/gate/change/land/deploy/proof relationships use explicit IDs propagated by the writer that owns both sides.
- A similar title, nearby timestamp, common path, branch name, SHA substring, or matching prose never creates a relationship.
- Recovery remains the existing authoritative `in_flight -> orphaned` transition: a classified session is `FINISHED`, its finish reason is `process exited`, rescued paths are greater than zero, and the exact recorded session ID matches an in-flight request. Silence, an error, or a later successful event never implies recovery.
- A repeated ask exists only after intake or an owner action records a relationship between two exact request IDs. Text similarity may be offered as a private search aid later, but never persisted or shown as fact without confirmation.
- Account identity is a recorded identity from the process/account journal. Credential paths and environment-file names are not identities.
- Provider cost is shown only when the provider record reports it. It is never estimated from tokens.
- Missing facts are `partial` or `unavailable`, never zero, false, success, or “not needed.”
- Event time is the producer's recorded occurrence time. Ingest time is stored separately and is never substituted for it.
- No UI action mutates a request unless the same transaction appends its journal event.

## Authoritative data model

### 1. Immutable request intake

Extend `requests` with nullable compatibility fields:

- `original_body`: the exact complete ask captured at intake;
- `original_body_format`: `plain_text` or `markdown`;
- `intake_source`: the source that supplied the ask;
- `intake_source_event_id`: the producer's idempotency key.

New requests require these fields. Existing rows retain `NULL` and project `Original ask unavailable; this request predates complete intake recording.` The title remains a short owner-facing summary and is never used as a fallback body.

### 2. Append-only typed events

Add `request_events` with:

- `id`;
- `request_id` foreign key;
- globally unique `(source_id, source_event_id)` idempotency key;
- `schema_version`;
- `kind`;
- `occurred_at` and `recorded_at`;
- `summary` in owner language;
- structured `payload` validated against the discriminated event contract;
- actor fields: `actor_type`, `actor_id`, `actor_display_name`;
- execution fields when explicitly known: `session_id`, `host_id`, `account_id`;
- source fields: `source_id`, `source_record_id`.

Event kinds cover intake, claim, progress, question, owner answer, priority change, correction, cancellation, recovery required, recovery resumed, phase start/end, retry, gate result, tool result, change recorded, land submitted/result, commit recorded, deploy start/result, proof recorded, and failure. A kind may omit fields it cannot know; validation must reject fabricated defaults.

Events are immutable. Corrections append a correction event that identifies the superseded event. Current request state is a transactional projection, not mutable history.

### 3. Explicit links and relationships

Add `request_evidence_links` for exact cross-domain identity:

- request ID;
- link kind: `task`, `session`, `factory_run`, `phase`, `gate`, `change`, `branch`, `land_submission`, `commit`, `deployment`, `proof`, `spec`, or `log`;
- target source and target ID;
- optional safe owner URL;
- occurrence time and actor/source event identity;
- structured metadata appropriate to the kind.

Uniqueness is `(request_id, link_kind, target_source, target_id)`. A link is inserted only by a writer that receives the request ID and owns the target ID. URLs are presentation metadata, not join keys.

Add `request_relationships` for request-to-request relationships:

- exact source and target request IDs;
- kind: `repeated_ask`, `supersedes`, `corrects`, or `split_from`;
- occurrence time;
- recorded actor/source event identity;
- optional owner-readable reason.

No automatic prose similarity writer is permitted.

### 4. Evidence attachments

Large diffs and log excerpts do not live in SQLite rows. Add a content-addressed evidence store under collector state with:

- digest and byte count;
- media type and encoding;
- creation source;
- redaction status;
- truncation status and original byte count when known;
- retention status.

Event payloads reference the digest. Writes use a temporary file, fsync, digest verification, and atomic rename. Readers enforce bounded responses and render an explicit truncated/expired/corrupt state. Secret redaction runs before persistence; a redaction failure rejects the attachment rather than storing raw material.

A Git change records exact base/head object IDs and repository identity. The attachment is a convenience rendering, not the correlation key.

### 5. Source commitments and coverage

Add a request-evidence source registry. Each source declares which facts it promises, its last successful delivery, and its retention. The request story projection returns coverage per source/fact as `complete`, `partial`, `unavailable`, `stale`, or `pending_delivery`, with an owner-readable reason.

The drawer stops printing hard-coded excuses. It renders only gaps returned by this projection. “No event occurred” and “the source did not record whether it occurred” remain distinct.

## Durable producer path

Create one installed `request-evidence append` client used by hooks and machinery. Producers do not call the collector and hope for success.

1. The client validates a versioned envelope and requires request ID, source ID, source event ID, and occurrence time.
2. It writes the envelope first to a host-local spool under Overdeck state using write/fsync/atomic rename.
3. A bounded delivery attempt sends it to the authenticated collector endpoint.
4. The collector validates the request exists, validates the discriminated payload, and commits the event, links, attachments, and state projection in one SQLite transaction.
5. The collector acknowledges the idempotency key. Only then may the local spool entry be removed.
6. A standing drainer retries with bounded backoff. Invalid envelopes move to a visible quarantine with their validation reason; they are never silently dropped.
7. Producer/source health reports the oldest queued item, queue count, last acknowledgement, and quarantine count. A disconnected remote host eventually appears stale; the UI does not pretend to know the contents of an unreachable spool.

The append route is object-authorized against the request and bounded by event, attachment, and batch limits. It rejects unknown kinds, unsafe links, future schema versions, mismatched attachment digests, and actor/account fields unsupported by that source. Logs and API errors must not expose collector bearer tokens or secret attachment content.

## Ingestion seams

Every writer records only facts it owns:

| Seam | Facts written | Explicit identity path |
|---|---|---|
| `/request`, task creation, and accepted owner intake | full immutable ask, summary, project, intake actor/source | generated request ID returned to caller and persisted in the task mapping |
| Task lifecycle hook | task link, claim/progress/completion/cancellation | exact task-to-request mapping file; no title fingerprint lookup after creation |
| Dispatcher/worktree claim | worker, host, session, branch/worktree reference | request ID carried in dispatch envelope and environment |
| Session ledger | session state and authoritative worker-loss/recovery event | exact recorded session ID already attached to the request |
| Factory launcher | factory-run link | request ID supplied to launcher; generated run ID returned and linked before phases execute |
| Factory trace | phases, attempts, retries, gates, tools, diffs, model/account/host, usage/cost when reported | persisted request ID on the run; no scan of prompt, branch, or SHA |
| Native tool hooks | tool name, bounded redacted input summary, result status/duration, log evidence reference | exact current request and session IDs installed in session context |
| `local-gate` | command identity, revision, start/end, exit, receipt/log reference | explicit request ID argument/environment; absent ID remains unattributed rather than guessed |
| Ship submit/conductor | submission, branch, exact head, gate verdict, landed commit | request IDs carried in the submission manifest; conductor emits one idempotent result per request |
| Deployment | release revision, target, start/end, readiness/rollback verdict | landed manifest's explicit request IDs carried into the immutable release receipt |
| Installed proof helper | proof kind, result, owner URL/evidence | required request ID plus exact deployed revision |
| Drawer owner actions | answer, priority, cancel, correction, recovery resume | authenticated request route appends event and projection atomically |

Where one commit or deployment serves several requests, the manifest contains the exact set of request IDs. The machinery emits one link per member; it never discovers membership by searching commit messages.

Factory schema/phase changes must run `python3 -m pytest modules/harness/factory/tests/ -q` in addition to focused tests. The richer factory records should be referenced instead of duplicated wherever retention and object identity are durable; the request ledger stores the explicit link and owner-relevant lifecycle event.

## Collector contracts

Add shared validated contracts under `packages/report-contract` or a dedicated shared request-contract module used by collector, writers, and web:

- `RequestEvidenceEnvelopeV1` for one idempotent append;
- discriminated `RequestEventV1` payloads;
- `RequestEvidenceLinkV1`;
- `RequestRelationshipV1`;
- `RequestStoryV1` with summary, immutable ask, ordered events, grouped lineage, relationships, actions, and source coverage;
- cursor pagination for events and evidence details.

Routes:

- `POST /requests/:id/evidence` appends one envelope transactionally and returns its acknowledgement/idempotent replay result.
- `GET /requests/:id/story` returns the bounded owner-facing projection. Event and evidence cursors fetch older or larger detail.
- Existing `GET /requests` remains the bounded board summary and does not embed every log or diff.
- Existing mutation routes become thin typed event writers and return the updated projection. They cannot update state without an event.

The story projection orders by occurrence time with deterministic ID tie-breaking, while visibly retaining ingest delay. It groups lineage only through evidence links. Unsupported or unavailable source detail produces a coverage item, not a fabricated timeline entry.

## Requests drawer

`RequestDrawer` fetches `GET /requests/:id/story` only when opened and renders these owner-language sections:

1. **Your original request** — complete immutable ask and intake time/source, or the explicit legacy coverage gap.
2. **What is happening** — current projected state and latest authoritative reason.
3. **Activity** — paginated typed events with actor, occurrence time, host/account/session when recorded, ingest delay when material, and links to exact source evidence.
4. **Work evidence** — phase/gate/tool outcomes, structured diff metadata and safe bounded diff view, log references, failures, and retries.
5. **Delivery** — spec, branch, landing, commit, deployment, and proof from dedicated links.
6. **Related requests** — explicit repeated/superseded/corrected/split relationships only.
7. **Coverage** — named source commitments that are partial, unavailable, stale, quarantined, or pending delivery.
8. **Actions** — answer, change priority, cancel, mark “not what I meant,” and resume recovery when valid for the current state. Every success appears immediately as a journal event; failed mutations remain visible and do not optimistically alter state.

The drawer remains non-modal on desktop so the board is not blacked out. It retains keyboard focus behavior, Escape/close controls, safe links, and a contained narrow-viewport layout. It does not expose internal IDs on card faces; IDs may appear in technical evidence detail where needed for verification.

## Reports projection

The `/reports` lineage reader consumes `RequestStoryV1`/the same repository projection and maps exact links to:

`request -> run -> gate -> change -> land -> deploy -> proof`

A missing stage is `missing` with the source coverage reason. It is never bridged to a later stage. Reports drill-down opens the same request evidence or exact target record; it does not independently infer lineage.

## Compatibility and migration

1. Add new nullable columns/tables without rewriting existing rows.
2. Project existing `request_transitions` and `receipt_trail` as read-only `legacy` events with their exact timestamps/text/metadata and `partial` coverage. Do not assign actors/accounts not present in the row.
3. New writes use the typed ledger. During one compatibility release, board summary projection can read both sources with deterministic de-duplication by explicit source event ID only.
4. Backfill explicit links only from durable exact columns such as an existing non-null `factory_run_id` or `session_id`. Do not parse `proof_url`, `plan_ref`, receipt prose, titles, SHAs, or branches.
5. Remove `findWorkKey()` and every substring-based association after callers pass exact request IDs. A test must fail if a SHA/branch appearing only in prose links a check.
6. Keep legacy tables until an installed report confirms all active writers use the ledger and the compatibility reader has no unexplained new legacy writes. Historical legacy rows remain available as partial history; they are never rewritten as complete events.

## Incremental delivery slices

### Slice 0 — Viewport and drawer repair

**Installed evidence:** `/requests` uses nearly the full viewport; summary controls do not overlap at the reported widths; opening a request leaves the board visible and usable behind the drawer.

- Ship the existing wide-shell opt-in, small gutters, contained board scrolling, wrapped header/filter layout, and `modal={false}` drawer behavior.
- Focused component assertion proves no `aria-modal`; layout acceptance checks the reported desktop widths and a narrow viewport in both themes.
- No recording claims are added in this slice.

### Slice 1 — The real original ask and first typed timeline

**Installed evidence:** create one request through the supported owner intake, then open it and see the complete ask plus intake and owner-answer events. Open a legacy request and see an explicit historical coverage gap rather than its title masquerading as the ask.

- Add immutable intake fields, typed event table/contracts, append transaction, compatibility reader, story endpoint, and lazy drawer projection.
- Convert request creation and owner answer to atomic typed events.
- Keep future evidence sections hidden until they have records or computed coverage.
- Prove title-only legacy rows never become full original asks.

### Slice 2 — Durable lifecycle delivery

**Installed evidence:** stop the collector, create/claim/update a fixture request, restore the collector, and see every event delivered once in the drawer. The Requests coverage section visibly reports queued/quarantined/stale producer health.

- Install the append client, host-local spool, drainer, idempotent acknowledgement, quarantine, and source-health projection.
- Move task create/update and session claim/loss writers to the durable client.
- Preserve the existing strict orphan/recovery predicate and prove unrelated dead sessions create no request event.
- Do not add a second enabling-only slice: source health and delayed events are visible here.

### Slice 3 — Run, gate, tool, and change evidence

**Installed evidence:** an explicitly linked request shows its factory run, phases, retry, gate verdict, tool result, actor/session/host/account where actually recorded, and one structured diff. A deliberately similar unlinked request shows no run evidence and a named coverage gap.

- Propagate request ID through dispatch/factory/session context.
- Persist exact factory-run/request linkage and connect existing trace records.
- Add native tool and `local-gate` durable writers.
- Add bounded content-addressed diff/log evidence and drawer views using existing Astryx/deck-ui components.
- Delete inference from request/run linkage in the touched path.
- Run the mandatory full factory suite because factory trace/phase machinery changes.

### Slice 4 — Landing, deployment, and proof

**Installed evidence:** submit and deploy one request-bearing change; its drawer and Reports show exact branch, submitted head, gate result, landed commit, release, readiness result, and installed proof. A shipped legacy row without these records remains partial.

- Carry request IDs in ship manifests, conductor outcomes, deployment receipts, and proof helper calls.
- Emit idempotent land/deploy/proof events and evidence links at the owning seams.
- Support one-to-many request sets without commit-message inference.
- Render the delivery section and shared Reports lineage from the same projection.

### Slice 5 — Related asks and complete owner actions

**Installed evidence:** from the drawer, change priority, answer blocked work, mark a request “not what I meant,” link a confirmed repeated ask, cancel a valid request, and resume a recoverable request. Every successful action appears once in Activity and related requests; invalid actions explain why and make no state change.

- Add typed relationships and state-aware action contracts.
- Require explicit target request IDs and actor confirmation for repeated/superseded/correction relationships.
- Journal action and projection atomically with optimistic-concurrency protection.
- Never offer recovery resume unless the request is actually `orphaned`.

### Slice 6 — Complete shared evidence and history proof

**Installed evidence:** an end-to-end fixture created through real supported seams shows request, run, gate, change, land, deploy, and proof in both `/requests` and `/reports`; source outage, expired attachment, unknown account, missing cost, and legacy history each render their distinct honest coverage state.

- Remove remaining legacy write paths and `findWorkKey()` inference.
- Add retention/corruption behavior and durable historical report buckets for the new evidence contract where needed by Reports.
- Verify restart persistence, idempotent replay, pagination, safe export/print equality, attachment bounds/redaction, and exact cross-page evidence links.
- Keep legacy data visibly partial forever unless a real exact source record supplies the missing fact.

## Verification matrix

Run cheapest checks first for every slice, then the broad suite once for the exact unchanged tree:

1. Contract and store tests: schema validation, transactional projection, idempotency, event ordering, compatibility, pagination, corrupt attachment, and unsupported versions.
2. Forbidden-inference tests: same title/time/path/prose/branch/SHA never links; silence never recovers; path never identifies account; absent provider cost stays unavailable.
3. Durable-delivery integration: collector unavailable, crash after spool write, retry, duplicate acknowledgement, invalid envelope quarantine, and remote source staleness.
4. Writer seam tests: task, session loss, factory, local gate/tool, ship, deploy, proof, and owner actions each emit the exact request/source IDs they own.
5. Security tests: object authorization, unsafe URL rejection, attachment size/digest/media checks, redaction failure, token non-disclosure, and bounded pagination/batches.
6. Web component tests: complete/partial/legacy stories, non-modal drawer, keyboard close/focus, both themes, narrow viewport, no body overflow, safe links, action error behavior, and no hard-coded missing-data excuse.
7. Reports tests: drawer and lineage agree; missing stages remain gaps; CSV/JSON/print include the same visible records and coverage.
8. Mandatory module checks: collector tests; web focused tests/build/typecheck; deck-ui tests/typecheck if deck-ui changes; slopgate; factory tests for any factory machinery change.
9. Installed owner flow: enter via sidebar, create/select authoritative fixture, exercise interactions, inspect evidence, and assert forbidden links are absent. HTTP readiness is diagnostic only.

## Completion conditions

This plan is complete only when:

- new requests preserve the exact original ask;
- every supported lifecycle writer is durable and idempotent;
- the drawer can show actor, time, source, session, host, account, run, gates, tools, diffs, logs, land, deploy, proof, and related asks when those facts were recorded;
- every unavailable fact has source-specific coverage instead of a stock excuse;
- `/requests` and `/reports` agree because they use one projection;
- all correlation inference paths are removed and guarded by tests;
- old incomplete history remains honestly incomplete;
- the final installed owner flow proves one complete lineage and one deliberately incomplete lineage without fabrication.
