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.
DatasetSchemadeclares the repetition contract — aGroupSpec.kind = "repetition_group"block names the column that groups observations and asserts uniqueness of(group_id, sample_id)pairs. (Workstream B task 7.)The data plan validator refuses fold sets that violate the contract. After a fold split lands in
dag-ml-core’sFoldSet, dag-ml-data’s coordinator envelope validator joins the fold assignments against the repetitionGroupSpecand refuses the materialization withRepetitionLeakageErrorif any group has its observations split across folds.Bindings construct folds; data layer audits them. Splitter controllers (KennardStone, SPXY, KFold, …) live in the binding. They produce
FoldSets. The Rust runtime hands theFoldSetto dag-ml-data, which validates and either accepts or refuses. Refusal is a hard error (no warning) because leakage is silent in metrics.Augmentation origin inherits group membership — augmented rows derived from sample X carry X’s
group_idand are constrained to X’s fold (ADR-04 materialization records the lineage).
Consequences¶
dag-ml-datagains aRepetitionLeakageValidatorinrelation.rsthat joins theFoldSetagainstSampleRelationTable.repetition_group_id.The bridge’s controller-manifest exporter (workstream E task 3) declares each splitter’s
respects_repetitioncapability 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’sOofEdgevalidation 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.