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
- 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.
- 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.
- The cheapest useful check is reachable without a source query
(
--validate); the full audit is reachable with one flag (--reconcile). - Deprecation pressure on the CLI shape is explicit, not accidental.
Non-goals
- A unified
rivet verifysubcommand. 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. - 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. - Renaming
--validateto--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:
- 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. - The roadmap (
rivet_roadmap_0_6_1_to_0_8_0_encryption.md) reservedcheckfor 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. --validateis 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 verifysubcommand 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,--fullflags onrivet run.- Per-invariant flags (
--check-m5,--validate-manifest,--require-success). - Behaviour where
--validatetriggers a source query. - Behaviour where
--reconciledoes not imply--validate. - Silent fallback when manifest is missing — see M6: the report must say
legacy_run: trueso 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 withchunk_checkpoint: true(re-runs per-chunkCOUNT(*)against stored chunk counts). For snapshot/incremental/keyset it bails and directs the operator torivet run --reconcile, the whole-export COUNT(*)-vs-exported-rows audit (reconcile_source_countinsrc/pipeline/job.rs). The subcommand itself (src/pipeline/reconcile_cmd.rs) is a sibling partition-level check, not a standalone driver of the--reconcileflag semantics.rivet validate [--export <name>]— added 2026-05-21; standalone driver for the M5/M6 semantics thatrivet run --validateperforms at end-of-run. Runs the samepipeline::validate_manifest::verify_at_destinationcode 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 validateproduces the sameManifestVerificationshape thatvalidation.manifestcarries insummary.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. Arivet verifysubcommand 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 --helplists exactly the flags above for trust/resume/safety.--reconcileproduces a verdict that subsumes everything--validateproduces (i.e. an operator running--reconcilenever needs to also pass--validateto 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’sclapderive 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 validateis the first such carveout; future carveouts must clear the same bar.
Status (2026-05-21)
--validateextended with M5/M6 semantics: ✅feat(0.7.0): manifest-aware --validate (1ef2fbb)- Standalone
rivet validatesubcommand: ✅feat(0.7.0): rivet validate subcommand (20b849a) --forcesafety override +_SUCCESSgate: ✅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 (
§24intrust_artifacts_integration) pins therivet runflag set; refuses any flag outside the contract. --reconcileimplies--validate: ✅ enforced at plan build (plan.validate = validate || reconcileinplan/build.rs). Previously the two flags were gated independently downstream, sorun --reconcileran only the source-count check and skipped the M5/M6 manifest verdict — a drift from the acceptance criterion above. Pinned byrun_reconcile_implies_validate_produces_manifest_verdict(a reconcile-only run must produce thevalidated:verdict; a plain run must not).
Open items
-
How
--reconcilereports its three sub-checks (file rows, manifest M5, source COUNT) is a render decision, not a CLI surface decision. The suggestion is one block insummary.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.
-
Verification depth turned out not to be a
--validatemodifier at all: it’s the per-exportverify: size | contentconfig (shipped 0.7.x). The--validate --deepre-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.