ADR-05: Repetition-aware CV is a safety invariant (not a binding convenience)

Status: accepted (2026-05-28) Blocks: workstream B (dag-ml-data validators), workstream E (bridge), workstream F (parity).

Context

nirs4all groups multiple spectra per physical sample using set_repetition(column). Cross-validators must keep all repetitions of a sample in the same fold — otherwise OOF predictions for sample X consume training predictions of sample X (leakage).

Roadmap v1 punted this to the binding (“Python constructs the folds”). Codex pushed back: this is a safety invariant. A binding bug that splits repetitions across folds silently corrupts OOF; dag-ml-data must validate.

Decision

The invariant is enforced inside dag-ml-data, not at the binding boundary.

  1. DatasetSchema declares the repetition contract — a GroupSpec.kind = "repetition_group" block names the column that groups observations and asserts uniqueness of (group_id, sample_id) pairs. (Workstream B task 7.)

  2. The data plan validator refuses fold sets that violate the contract. After a fold split lands in dag-ml-core’s FoldSet, dag-ml-data’s coordinator envelope validator joins the fold assignments against the repetition GroupSpec and refuses the materialization with RepetitionLeakageError if any group has its observations split across folds.

  3. Bindings construct folds; data layer audits them. Splitter controllers (KennardStone, SPXY, KFold, …) live in the binding. They produce FoldSets. The Rust runtime hands the FoldSet to dag-ml-data, which validates and either accepts or refuses. Refusal is a hard error (no warning) because leakage is silent in metrics.

  4. Augmentation origin inherits group membership — augmented rows derived from sample X carry X’s group_id and are constrained to X’s fold (ADR-04 materialization records the lineage).

Consequences

  • dag-ml-data gains a RepetitionLeakageValidator in relation.rs that joins the FoldSet against SampleRelationTable.repetition_group_id.

  • The bridge’s controller-manifest exporter (workstream E task 3) declares each splitter’s respects_repetition capability so the planner can pre-warn on incompatible combinations.

  • The parity oracle’s repetition cases (rep_to_sources_basic, rep_to_pp_basic, aggregation_rep_*) become the executable proof of this invariant.

  • dag-ml’s OofEdge validation already refuses train predictions as meta-model training features. With ADR-05 enforced upstream, both layers refuse leakage independently — defense in depth.

Risk

  • A naïve splitter that ignores repetition will be rejected at materialize time rather than silently producing biased metrics. Users see a clear error pointing at the offending step. This is intentional.

Open follow-ups

  • The synthetic-builder fixture for parity cases needs a repetition column. The parser-fixture aggregate_mean (E04) already has one (sample_id); confirm column names on first end-to-end run.