# ADR-0013: Trust Flag Contract

**Status**: Proposed
**Date**: 2026-05-21
**Context**: ADR-0012 introduced cloud manifest invariants (M1–M9). Implementing
M5 (manifest-aware verification), M6 (legacy fallback), M8 (resume decision
matrix), M9 (quarantine), and the future encryption-aware verify path could
each plausibly grow its own CLI flag (`--validate-manifest`, `--validate-deep`,
`--verify`, `--decrypt`, `--check-success`, …).  Without an explicit contract
the surface area drifts; six months from now operators face a flag soup that
contradicts the project's existing "predictable, minimal CLI" stance.

This ADR locks the trust-flag surface for `rivet run` at exactly three flags
(`--validate`, `--reconcile`, `--resume`) and the safety-override flag
`--force`, and pins the rule that ADR-0012 work and beyond extends *semantics*,
never adds new flags.

---

## Goals

1. Operators have **one** mental model for "how do I ask Rivet to prove the run
   was correct?" — and that model fits in a handful of words.
2. New trust invariants (M5–M9 today, encryption later) are absorbed under the
   existing flags transparently, with the operator's existing CI / Airflow
   wiring continuing to work unchanged.
3. The cheapest useful check is reachable without a source query
   (`--validate`); the full audit is reachable with one flag (`--reconcile`).
4. Deprecation pressure on the CLI shape is explicit, not accidental.

## Non-goals

1. A unified `rivet verify` subcommand.  Rejected — the verdict is surfaced
   via the existing run report (`.rivet/runs/<run_id>/summary.{md,json}`)
   and the existing flags.  See ADR-0012 §"Non-goals" item 3.
2. Per-invariant flags (`--check-m5`, `--validate-manifest`, etc.).  Same
   rejection: the operator should not have to know which ADR-0012 letter
   their check maps to.
3. Renaming `--validate` to `--check`.  See "Naming" below.

---

## The contract

`rivet run` exposes three mutually composable trust flags and one
safety-override flag:

| Flag | What it asks | Source query? | Implies |
|---|---|---|---|
| `--validate` | "Is the output internally consistent?" | No | — |
| `--reconcile` | "Does the output match the source right now?" | Yes | `--validate` |
| `--resume` | "Pick up where the prior run left off." | Sometimes | — |
| `--force` | "Override a refusal that would otherwise abort the run." | — | — |

Trust flags are composable: `--validate --reconcile` is the same as just
`--reconcile`; `--resume --reconcile` runs resume and then reconciles.

`--force` is a category apart — it overrides safety gates, not check
semantics.  Calling it a trust flag would be a misnomer; it's the inverse.

---

## Semantics that grow under each flag

ADR-0012 invariants land under existing flags as follows.  The flag itself
does not change between versions — only what it does internally.

### `--validate`

| Version | Behaviour |
|---|---|
| 0.6.x (current) | Per-file row count check (parquet rows / CSV lines minus header) |
| 0.7.0 | Above, **plus** ADR-0012 M5: read `manifest.json` from the destination, verify every listed part exists at the recorded `size_bytes`, verify `_SUCCESS` body matches the manifest fingerprint. |
| 0.7.0 (M6) | When the destination prefix has no manifest (legacy run), the new checks degrade to the 0.6.x file-row check and the report carries `legacy_run: true` so the reduction is explicit, not silent. |
| 0.7.x (shipped) | `--validate` also confirms each part's **content** via the MD5 the store surfaces in its listing (GCS `md5Hash`, S3/Azure single-PUT) — **no download**. The earlier "`--validate --deep` re-fingerprints every part" projection was **rejected** (re-downloading a whole dataset to recompute a hash we already verified pre-upload is wasteful and partial). Verification depth is instead a per-export config — `verify: size` (default) / `verify: content` (content MD5 required; size-only parts fail) — not a CLI flag, which keeps the no-new-trust-flag contract. |
| 0.7.2+ (encryption track) | When parts are encrypted, metadata-only verify (no key needed) is the default; `--validate --identity ./key.txt` adds the decrypting verify. |

The flag stays `--validate`.  No `--validate-manifest`, no `--check-success`,
no `--verify-output`.

### `--reconcile`

| Version | Behaviour |
|---|---|
| 0.6.x (current) | `SELECT COUNT(*) FROM (<base_query>)` and compare to exported rows. |
| 0.7.0 | Above, **plus** the full `--validate` chain (M5/M6 included).  In effect, `--reconcile` becomes "everything `--validate` does + source comparison".  No new flag is needed for "full audit" — `--reconcile` already is that. |
| 0.7.x (future) | Source schema fingerprint compared to manifest's `schema_fingerprint` so silent type drift surfaces in the verdict. |

The implication direction is fixed: `--reconcile` implies `--validate`,
never the other way around.  Operators who only want the cheap check use
`--validate`; operators who want the full audit use `--reconcile` and
get everything for free.

### `--resume`

| Version | Behaviour |
|---|---|
| 0.6.x (current) | Reuse `chunk_checkpoint` rows in the local state DB to skip already-completed chunks; no destination-side awareness. |
| 0.7.0 | Above, **plus** ADR-0012 M8: read the manifest at the destination, apply the decision matrix per part (skip / rewrite / lost / quarantine), refuse to start when `_SUCCESS` is present unless `--force` is given. |
| 0.7.0 (M9) | Untracked / fingerprint-mismatch parts are moved to `_quarantine/<run_id>/<original-name>` best-effort during resume.  This is automatic, not a flag. |

Quarantine (M9) is intentionally not a flag.  An operator who said "resume"
already accepted that the destination would be touched; refusing to move a
corrupt artifact in that mode would be worse than a best-effort relocation
with an audit warning.

### `--force`

A safety-override, not a check.

| Version | Use |
|---|---|
| 0.7.0 | `--resume --force`: proceed even when `_SUCCESS` is present (M8 gate).  Without it, resume against a complete run refuses and exits non-zero so an operator can't accidentally re-export over a verified dataset. |

`--force` is scoped to the specific safety gate it overrides.  Future gates
(if they appear) reuse the same flag rather than adding `--force-resume`,
`--force-overwrite`, etc.

---

## Naming

Why keep `--validate` rather than the conceptually cleaner `--check`:

1. The word is already in the codebase (`pipeline::validate`, `validate_output`,
   `RunReport.validation`).  Renaming it now ripples into Airflow operator
   code and CI scripts that grep for `--validate`.  The cost outweighs the
   gain.
2. The roadmap (`rivet_roadmap_0_6_1_to_0_8_0_encryption.md`) reserved `check`
   for the **pre-run** preflight subcommand.  Mixing pre-run and post-run
   checks under one word would re-introduce exactly the ambiguity this ADR
   is trying to prevent.
3. `--validate` is the same word every other widely-used data tool spells
   (dbt, Airflow, Great Expectations).  Operators don't have to learn a
   Rivet-specific dialect.

---

## What this rules out

These are explicitly **not** going to ship, in 0.7.0 or later, unless this
ADR is superseded:

- `rivet verify` subcommand as a **higher-level umbrella** that subsumes
  validate + reconcile + manifest + schema under one new noun.  See the
  carveout below for the narrower allowance.
- `--verify`, `--audit`, `--check-output`, `--full` flags on `rivet run`.
- Per-invariant flags (`--check-m5`, `--validate-manifest`, `--require-success`).
- Behaviour where `--validate` triggers a source query.
- Behaviour where `--reconcile` does *not* imply `--validate`.
- Silent fallback when manifest is missing — see M6: the report must say
  `legacy_run: true` so the operator knows the surface they're looking at.

If a use case appears that these rule out, the right move is to reopen this
ADR and amend it, not to slip a new flag in under the radar.

## Subcommand carveouts (amendment 2026-05-21)

The contract above pins the **flag surface** of `rivet run`.  It does not
forbid subcommands whose only job is to **re-drive existing flag semantics
standalone**, without introducing new trust nouns.  Two examples:

- `rivet reconcile <export>` — already exists; standalone driver for the
  `--reconcile` semantics that `rivet run --reconcile` performs at end-of-run.
- `rivet validate [--export <name>]` — added 2026-05-21; standalone driver
  for the M5/M6 semantics that `rivet run --validate` performs at end-of-run.
  Runs the same `pipeline::validate_manifest::verify_at_destination` code
  path against an existing destination prefix, no source query, no
  extraction, no state writes.

Allowed subcommand patterns:

- The subcommand's verdict **must** be expressible by an existing flag.
  `rivet validate` produces the same `ManifestVerification` shape that
  `validation.manifest` carries in `summary.json`; an Airflow consumer
  reads it identically from either source.
- The subcommand **must not** introduce a new trust noun in the
  operator-facing language.  "Validate" maps to `--validate`; "reconcile"
  maps to `--reconcile`.  A `rivet verify` subcommand was rejected above
  precisely because "verify" is *not* an existing flag.
- The subcommand **must not** depend on having run an extraction.  These
  are between-run inspection tools, not retroactive run mutators.

Rationale: between-run polling (Airflow sensors, CI gating, operator
triage) is a real workflow that the existing `--validate` flag cannot
serve — it only fires at end-of-run.  Refusing to ship a standalone
driver would force operators to either re-run the entire export to
re-verify, or to reimplement M5 in shell against the manifest schema.
Both are worse than a thin subcommand.

---

## Acceptance criteria

- `rivet run --help` lists exactly the flags above for trust/resume/safety.
- `--reconcile` produces a verdict that subsumes everything `--validate`
  produces (i.e. an operator running `--reconcile` never needs to also
  pass `--validate` to get the full picture).
- Run report renders a single "Verdicts" section that names the strongest
  check the operator asked for, not a column per ADR letter.
- Adding M5, M6, M8, M9 to the codebase causes **zero** changes to
  `rivet run`'s `clap` derive struct beyond `--force` (the safety override).
- Subcommand carveouts (see amendment below) are limited to standalone
  drivers that re-run an existing flag's semantics.  `rivet validate` is
  the first such carveout; future carveouts must clear the same bar.

### Status (2026-05-21)

- `--validate` extended with M5/M6 semantics: ✅ `feat(0.7.0): manifest-aware --validate (1ef2fbb)`
- Standalone `rivet validate` subcommand: ✅ `feat(0.7.0): rivet validate subcommand (20b849a)`
- `--force` safety override + `_SUCCESS` gate: ✅ `feat(0.7.0): _SUCCESS gate + pure resume decision matrix (9b510c7)`
- M8 chunked-resume executor wiring (no new flag): ⚠️ Phase C-γ
- M9 quarantine on resume (no new flag): ⚠️ Phase C-δ
- Integration anchor test (`§24` in `trust_artifacts_integration`)
  pins the `rivet run` flag set; refuses any flag outside the contract.
- `--reconcile` implies `--validate`: ✅ enforced at plan build
  (`plan.validate = validate || reconcile` in `plan/build.rs`). Previously the
  two flags were gated independently downstream, so `run --reconcile` ran only
  the source-count check and skipped the M5/M6 manifest verdict — a drift from
  the acceptance criterion above. Pinned by
  `run_reconcile_implies_validate_produces_manifest_verdict` (a reconcile-only
  run must produce the `validated:` verdict; a plain run must not).

---

## Open items

1. How `--reconcile` reports its three sub-checks (file rows, manifest M5,
   source COUNT) is a render decision, not a CLI surface decision.  The
   suggestion is one block in `summary.md`:

   ```text
   ## Verdicts

   - Validation:    PASSED (manifest M5 + 13 parts verified)
   - Reconciliation: MATCHED (2,500 rows source ↔ 2,500 rows in manifest)
   - Schema:        unchanged (xxh3:cad2…)
   ```

   Bikeshed-friendly; not part of this ADR's contract.

2. Verification *depth* turned out **not** to be a `--validate` modifier at
   all: it's the per-export `verify: size | content` config (shipped 0.7.x).
   The `--validate --deep` re-download idea was rejected — content is verified
   pre-upload and via the free listing MD5, never by pulling bytes back. The
   encryption-aware `--validate --identity ...` modifier remains a future
   decision. Either way the contract holds: no new top-level trust flag.

---

## References

- ADR-0012: Cloud Manifest Contract — defines M1–M9.
- `rivet_roadmap_0_6_1_to_0_8_0_encryption.md` — release scoping.
- `pipeline::report::RunReport.validation` / `.reconciliation` — the
  on-wire shape these flags drive.
