fix(train): leak-free subject-disjoint split + synthetic-val disclosure (ADR-155 §Tier-1.2)

MM-Fi windows are stride-1 (~99% overlap), so an index-level split leaks; and
bin/train.rs validated real training against a SYNTHETIC val set, making any
printed PCK meaningless on two counts.

- MmFiDataset::subject_disjoint_split partitions whole subjects -> the two views
  share no subject and no window (leak-free by construction, deterministic per
  seed). assert_split_leak_free verifies subject- AND window-disjointness and is
  called inside the split so a leaky split is never handed out.
- bin/train.rs now prefers the real split; the synthetic path is a labelled
  run_smoke_test ("[SMOKE-TEST] DO NOT REPORT") reachable only as a fallback.
- New DatasetError::InvalidSplit.

Tests prove disjointness, determinism, single-subject/bad-fraction rejection,
and that the validator catches an injected subject leak.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-06-11 19:56:57 -04:00
parent 50b657459f
commit 2a2a2c5b06
3 changed files with 415 additions and 18 deletions
@@ -280,6 +280,12 @@ pub enum DatasetError {
/// An I/O error that carries no path context.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// A train/test split is invalid — it leaks information across the boundary
/// (a subject appears in both partitions, or a window is shared) or is
/// degenerate (an empty partition). ADR-155 §Tier-1.2.
#[error("Invalid split: {0}")]
InvalidSplit(String),
}
impl DatasetError {