feat(research,campaign,cli): chunked instrument-major cell walk + duplicate-instrument refusal

Two pieces of the parallel-cell-loop iteration (spec: parallel-cell-loop),
still sequential — behaviour-identical results, suite green unchanged:

- validate_campaign refuses a campaign document listing the same instrument
  twice (DocFault::DuplicateInstrument + CLI prose). A duplicate enumerates
  two identical cells and is the one input that could make two cells share a
  registry family name; with distinct instruments the per-cell family name
  (all four cell axes + stage ordinal) is unique by construction, so the
  upcoming parallel appends can never race one name's run-index assignment.
- aura_campaign::execute flattens the 4-nested cell loop into a document-
  order-indexed cell list, groups it by instrument ordinal, and walks
  sequential chunks of K instrument groups; results land in index-addressed
  slots and the ordered outputs (outcomes, realizations, nominee groups) are
  rebuilt in document order. K arrives as a new NonZeroUsize parameter
  (default DEFAULT_PARALLEL_INSTRUMENTS = 4), exposed as
  --parallel-instruments on aura campaign run and threaded through the
  run_campaign chain; the dissolved-verb sugar paths pass the default.

The chunk walk is the structural RAM bound for the parallel flip that
follows: no cell of an instrument outside the current chunk can be in
flight at all (chosen over a semaphore gate — blocking inside pool tasks
risks worker starvation; decision log on #277). Verified: workspace suite
green unchanged, clippy -D warnings clean, E2E fixtures pin the CLI prose
and the document-order persistence across K.

refs #277
This commit is contained in:
2026-07-16 14:19:31 +02:00
parent 98ddcd3595
commit 7fa3ef4e26
8 changed files with 325 additions and 67 deletions
+23
View File
@@ -800,6 +800,7 @@ pub enum DocFault {
ZeroWalkForwardLength { stage: usize, field: &'static str },
// campaign side
EmptyInstruments,
DuplicateInstrument { index: usize, instrument: String },
NoWindow,
BadWindow { index: usize },
BadRegime { index: usize },
@@ -888,6 +889,15 @@ pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
if doc.data.instruments.is_empty() {
faults.push(DocFault::EmptyInstruments);
}
let mut seen_instruments = std::collections::BTreeSet::new();
for (index, instrument) in doc.data.instruments.iter().enumerate() {
if !seen_instruments.insert(instrument.as_str()) {
faults.push(DocFault::DuplicateInstrument {
index,
instrument: instrument.clone(),
});
}
}
if doc.data.windows.is_empty() {
faults.push(DocFault::NoWindow);
}
@@ -1989,6 +1999,19 @@ mod tests {
assert!(validate_campaign(&emit).contains(&DocFault::UnknownEmitKind("pie_chart".into())));
}
#[test]
fn a_campaign_with_duplicate_instruments_is_refused() {
let ok = parse_campaign(CAMPAIGN_FIXTURE).unwrap();
assert_eq!(validate_campaign(&ok), Vec::new());
let mut dup = ok.clone();
let first = dup.data.instruments[0].clone();
dup.data.instruments.push(first.clone());
let index = dup.data.instruments.len() - 1;
assert!(validate_campaign(&dup)
.contains(&DocFault::DuplicateInstrument { index, instrument: first }));
}
/// #201 decision 1 (cycle 0109): the tap namespace is a CLOSED vocabulary —
/// the wrap convention's four persisted sink names. A genuinely new
/// observable is a new vocabulary entry (or an authored blueprint sink),