# Destination-backend correctness matrix — SCENARIO × object-store BACKEND.
# Captures the destination dimension the export-strategy ledgers do not: the
# shared OpenDAL CloudDestination path (S3 / GCS / Azure) vs the local filesystem
# destination, and the dogfood findings on it. Verified on fake-gcs / MinIO
# emulators AND (for the prefix + round-trip) real GCS via the DuckDB independent
# oracle — the same discipline as source_parity_sweep, extended to the cloud leg.
#
# `engines:` is the guard's generic column axis — here the destination backends.
# S3/GCS/Azure share ONE CloudDestination impl (src/destination/cloud.rs), so a
# scenario proven by a shared-seam UNIT test (e.g. the prefix normalization) is
# the SAME test in all three cloud columns; `local` is a separate impl (fs joins,
# no OpenDAL) and is `na` where the scenario is a cloud-object-key concern.
#
# Cell = { test: <fn> } | { na: "<why n/a for this backend>" } | { gap: "<why,
# ratcheted>" }. Same drift-guard as the sibling ledgers
# (tests/offline/chunking_matrix_guard.rs).
engines: [local, gcs, s3, azure]
scenarios:
- id: prefix_no_trailing_slash_normalized
what: "dogfood B (MEDIUM): a cloud prefix WITHOUT a trailing slash must not jam the part name onto it (`exports/mydata` + `orders.parquet` → `exports/mydataorders.parquet`) — every op builds the key as `format!(\"{}{}\", prefix, key)` and list_prefix appends `/`, so an un-normalized prefix produces a FALSE PART_MISSING on 100%-present, DuckDB-readable data. normalize_prefix adds the slash at construction; the three cloud backends share the one CloudDestination path, so one unit test covers all. Live-verified on real GCS."
local: { na: "the local filesystem destination joins with Path::join, not string concat — no trailing-slash key-jamming; the prefix is a directory." }
gcs: { test: normalize_prefix_appends_a_trailing_slash_only_when_needed }
s3: { test: normalize_prefix_appends_a_trailing_slash_only_when_needed }
azure: { test: normalize_prefix_appends_a_trailing_slash_only_when_needed }
- id: full_export_round_trips_and_reads_back
what: "an export LANDS at the destination and reads back COMPLETE (content, not just object presence) — the emulator round-trip. Representative-engine cells (the stand runs one engine per backend; the read-back is via the store's own list/get, an independent oracle, not rivet's counter)."
local: { na: "local round-trip is the implicit default of every batch stand/live test (count_parquet_rows re-reads the written files)." }
gcs: { test: stand_dest_gcs_mysql }
s3: { test: stand_dest_s3_mysql }
azure: { gap: "no azurite full-round-trip stand test yet — the CloudDestination path is shared with S3/GCS (proven above), but an azure-specific emulator round-trip is unwritten." }
- id: row_count_parity_across_backends
what: "the SAME export to local vs S3 vs GCS yields IDENTICAL row counts — an independent cross-backend oracle (dogfood confirmed the fake-gcs count == real-GCS count == source via a DuckDB re-read, so the emulator does not lie about the real backend)."
local: { test: destination_parity_row_counts_match_across_local_s3_gcs }
gcs: { test: destination_parity_row_counts_match_across_local_s3_gcs }
s3: { test: destination_parity_row_counts_match_across_local_s3_gcs }
azure: { na: "the parity harness compares local/s3/gcs; azure joins the same CloudDestination seam, so its parity is covered by the shared round-trip + prefix cells, not a fourth column here." }
- id: validate_verdict_is_advisory_not_a_run_gate
what: "dogfood A (design, ADR-0001 §I7 MFN): `run --validate` populates the report/metrics verdict but is NON-fatal — the run exits 0 even on a verified-wrong verdict; the HARD gate is the standalone `rivet validate` command (exit 3) and `--reconcile` (COUNT mismatch). A dogfood verify-agent mis-flagged this as a bug not knowing the ADR; the roast test pins the documented contract. Destination-agnostic (the verdict path is the same for every backend)."
local: { test: roast_metric_validated_matches_final_summary_verdict }
gcs: { na: "the --validate-is-advisory contract is destination-agnostic; the roast test exercises it on local, which is sufficient (the verdict path does not branch on backend)." }
s3: { na: "destination-agnostic — see the local cell." }
azure: { na: "destination-agnostic — see the local cell." }