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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user