Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

FlagWhat it asksSource 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

VersionBehaviour
0.6.x (current)Per-file row count check (parquet rows / CSV lines minus header)
0.7.0Above, 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

VersionBehaviour
0.6.x (current)SELECT COUNT(*) FROM (<base_query>) and compare to exported rows.
0.7.0Above, 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

VersionBehaviour
0.6.x (current)Reuse chunk_checkpoint rows in the local state DB to skip already-completed chunks; no destination-side awareness.
0.7.0Above, 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.

VersionUse
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 -c <config> -e <export> — already exists; partition-level reconciliation for chunked exports previously run with chunk_checkpoint: true (re-runs per-chunk COUNT(*) against stored chunk counts). For snapshot/incremental/keyset it bails and directs the operator to rivet run --reconcile, the whole-export COUNT(*)-vs-exported-rows audit (reconcile_source_count in src/pipeline/job.rs). The subcommand itself (src/pipeline/reconcile_cmd.rs) is a sibling partition-level check, not a standalone driver of the --reconcile flag semantics.
  • 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:

    ## 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.