feat(campaign,registry,engine): record widenings + preflight v2 fault set (0108 tasks 1-2)
Task 1: RBootstrap and Generalization gain serde derives; lineage.rs gains StageBootstrap (per_survivor | pooled_oos) and CampaignGeneralization; StageRealization.bootstrap and CampaignRunRecord.generalizations land serde-default sparse (a pre-0108 campaign_runs.jsonl line still parses — pinned), threaded None/empty through every literal site. No behaviour change. Task 2: ExecFault::UnsupportedStage is gone; the preflight now admits the v2 shape sweep [gate]* [wf]? [mc]? [generalize]? via a monotone rank walk (precise per-pair PipelineShape prose) and statically refuses: single-instrument generalize (GeneralizeNeedsInstruments), non-R generalize metrics via check_r_metric (GeneralizeNonRMetric — no fourth roster site), zero mc params (ZeroBootstrapParam). CLI prose swapped accordingly. The loop's E2E phase added a tier-boundary pin: process validate accepts [sweep, mc, walk_forward] while campaign run refuses it data-free. KNOWN RED (documented in the plan, flips in task 5): crates/aura-cli/tests/research_docs.rs::campaign_run_v1_boundary_refuses_mc_process still pins the retired 'not executable in v1' prose. Interim gates: registry 57/0, campaign 33/0, cli --bin 111/0, workspace build clean. refs #200
This commit is contained in:
@@ -30,8 +30,8 @@ mod compat;
|
||||
mod lineage;
|
||||
pub use lineage::{
|
||||
group_families, mc_member_reports, sweep_member_reports, walkforward_member_reports,
|
||||
CampaignRunRecord, CellRealization, Family, FamilyKind, FamilyRunRecord, StageRealization,
|
||||
StageSelection,
|
||||
CampaignGeneralization, CampaignRunRecord, CellRealization, Family, FamilyKind,
|
||||
FamilyRunRecord, StageBootstrap, StageRealization, StageSelection,
|
||||
};
|
||||
|
||||
mod trace_store;
|
||||
@@ -661,7 +661,7 @@ pub fn optimize_deflated(
|
||||
/// order. `worst_case` is `min_i metric_i`; since every R-metric is
|
||||
/// higher-is-better, the min is unconditionally the conservative floor (no
|
||||
/// direction branch, unlike `PlateauMode::Worst`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Generalization {
|
||||
pub selection_metric: String,
|
||||
pub n_instruments: usize,
|
||||
@@ -1710,6 +1710,7 @@ mod tests {
|
||||
run: 99,
|
||||
seed: 7,
|
||||
cells: vec![],
|
||||
generalizations: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1777,18 +1778,116 @@ mod tests {
|
||||
n_neighbours: None,
|
||||
},
|
||||
}),
|
||||
bootstrap: None,
|
||||
},
|
||||
StageRealization {
|
||||
block: "std::gate".to_string(),
|
||||
family_id: None,
|
||||
survivor_ordinals: Some(vec![0, 3, 4, 7]),
|
||||
selection: None,
|
||||
bootstrap: None,
|
||||
},
|
||||
],
|
||||
}],
|
||||
generalizations: vec![],
|
||||
};
|
||||
let run = reg.append_campaign_run(&record).expect("append");
|
||||
assert_eq!(run, 0);
|
||||
assert_eq!(reg.load_campaign_runs().expect("load"), vec![record]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_run_record_roundtrips_with_annotator_fields() {
|
||||
use aura_engine::r_bootstrap;
|
||||
let path = temp_family_dir("campaign_run_annotator_roundtrip");
|
||||
let reg = Registry::open(&path);
|
||||
// r_bootstrap is pure given its seed (C1), and serde_json round-trips
|
||||
// finite f64 exactly, so whole-record PartialEq holds across the store.
|
||||
let per_a = r_bootstrap(&[0.5, -0.2, 0.3, 1.1], 8, 2, 7);
|
||||
let per_b = r_bootstrap(&[-0.4, 0.9], 8, 1, 7);
|
||||
let pooled = r_bootstrap(&[0.2, 0.2, -0.1, 0.6, 0.4], 8, 2, 7);
|
||||
let record = CampaignRunRecord {
|
||||
campaign: "feed".to_string(),
|
||||
process: "beef".to_string(),
|
||||
run: 0, // matches the counter's first assignment, so whole-record PartialEq holds
|
||||
seed: 7,
|
||||
cells: vec![
|
||||
CellRealization {
|
||||
strategy: "3f9c".to_string(),
|
||||
instrument: "EURUSD".to_string(),
|
||||
window_ms: (0, 1000),
|
||||
stages: vec![StageRealization {
|
||||
block: "std::monte_carlo".to_string(),
|
||||
family_id: None,
|
||||
survivor_ordinals: None,
|
||||
selection: None,
|
||||
bootstrap: Some(StageBootstrap::PerSurvivor(vec![
|
||||
(0, per_a),
|
||||
(2, per_b),
|
||||
])),
|
||||
}],
|
||||
},
|
||||
CellRealization {
|
||||
strategy: "3f9c".to_string(),
|
||||
instrument: "GER40".to_string(),
|
||||
window_ms: (0, 1000),
|
||||
stages: vec![StageRealization {
|
||||
block: "std::monte_carlo".to_string(),
|
||||
family_id: None,
|
||||
survivor_ordinals: None,
|
||||
selection: None,
|
||||
bootstrap: Some(StageBootstrap::PooledOos(pooled)),
|
||||
}],
|
||||
},
|
||||
],
|
||||
generalizations: vec![CampaignGeneralization {
|
||||
strategy_ordinal: 0,
|
||||
window_ordinal: 0,
|
||||
generalization: Some(Generalization {
|
||||
selection_metric: "net_expectancy_r".to_string(),
|
||||
n_instruments: 2,
|
||||
worst_case: 0.04,
|
||||
sign_agreement: 2,
|
||||
per_instrument: vec![
|
||||
("EURUSD".to_string(), 0.04),
|
||||
("GER40".to_string(), 0.11),
|
||||
],
|
||||
}),
|
||||
winners: vec![
|
||||
(
|
||||
"EURUSD".to_string(),
|
||||
vec![("sma_cross.fast.length".to_string(), Scalar::i64(3))],
|
||||
),
|
||||
(
|
||||
"GER40".to_string(),
|
||||
vec![("sma_cross.fast.length".to_string(), Scalar::i64(5))],
|
||||
),
|
||||
],
|
||||
missing: vec!["US500".to_string()],
|
||||
}],
|
||||
};
|
||||
let run = reg.append_campaign_run(&record).expect("append annotator record");
|
||||
assert_eq!(run, 0);
|
||||
assert_eq!(
|
||||
reg.load_campaign_runs().expect("load annotator record"),
|
||||
vec![record],
|
||||
"bootstrap (both variants) and generalizations survive the round-trip",
|
||||
);
|
||||
}
|
||||
|
||||
/// A pre-0108 stored line — no `bootstrap`, no `generalizations` — still
|
||||
/// parses (the C14/C23 serde-default widening convention): existing
|
||||
/// `campaign_runs.jsonl` stores survive the record widening unchanged.
|
||||
#[test]
|
||||
fn campaign_run_line_without_new_fields_still_parses() {
|
||||
let path = temp_family_dir("campaign_run_pre_widening");
|
||||
let store = path.with_file_name("campaign_runs.jsonl");
|
||||
let line = r#"{"campaign":"cafe","process":"beef","run":0,"seed":7,"cells":[{"strategy":"3f9c","instrument":"EURUSD","window_ms":[0,1000],"stages":[{"block":"std::gate","survivor_ordinals":[0,2]}]}]}"#;
|
||||
fs::write(&store, format!("{line}\n")).expect("write pre-widening line");
|
||||
let reg = Registry::open(&path);
|
||||
let loaded = reg.load_campaign_runs().expect("pre-widening line parses");
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0].cells[0].stages[0].bootstrap, None);
|
||||
assert!(loaded[0].generalizations.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
use aura_core::Scalar;
|
||||
use aura_engine::{FamilySelection, McFamily, RunReport, SweepFamily, WalkForwardResult};
|
||||
use aura_engine::{
|
||||
FamilySelection, McFamily, RBootstrap, RunReport, SweepFamily, WalkForwardResult,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Registry, RegistryError};
|
||||
use crate::{Generalization, Registry, RegistryError};
|
||||
|
||||
/// Which C12 orchestration axis produced a family.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -80,6 +82,11 @@ pub struct CampaignRunRecord {
|
||||
pub run: usize,
|
||||
pub seed: u64,
|
||||
pub cells: Vec<CellRealization>,
|
||||
/// Campaign-scope generalize annotations (cycle 0108), one per
|
||||
/// (strategy, window) across instruments. Absent pre-0108 — the
|
||||
/// serde-default widening keeps existing stored lines parseable.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub generalizations: Vec<CampaignGeneralization>,
|
||||
}
|
||||
|
||||
/// One realized (strategy, instrument, window) cell: the pipeline prefix that
|
||||
@@ -108,6 +115,10 @@ pub struct StageRealization {
|
||||
pub survivor_ordinals: Option<Vec<usize>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub selection: Option<StageSelection>,
|
||||
/// std::monte_carlo stages only (cycle 0108). Absent pre-0108 — the
|
||||
/// serde-default widening keeps existing stored lines parseable.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bootstrap: Option<StageBootstrap>,
|
||||
}
|
||||
|
||||
/// The recorded winner of a selection-bearing stage: its ordinal in the stage's
|
||||
@@ -119,6 +130,38 @@ pub struct StageSelection {
|
||||
pub selection: FamilySelection,
|
||||
}
|
||||
|
||||
/// The recorded result of a `std::monte_carlo` stage (cycle 0108, #200 d1/d4):
|
||||
/// the payload mirrors the stage's input shape.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StageBootstrap {
|
||||
/// After sweep/gates: one bootstrap per surviving member,
|
||||
/// `(ordinal into the population family, result)`.
|
||||
PerSurvivor(Vec<(usize, RBootstrap)>),
|
||||
/// After a walk_forward: one bootstrap over the pooled per-window
|
||||
/// OOS trade-R series (roll order).
|
||||
PooledOos(RBootstrap),
|
||||
}
|
||||
|
||||
/// The campaign-scope generalize annotation (#200 d2): recorded at the scope
|
||||
/// it is computed, keyed (strategy, window) across instruments.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CampaignGeneralization {
|
||||
pub strategy_ordinal: usize,
|
||||
pub window_ordinal: usize,
|
||||
/// None when fewer than 2 instrument cells produced a winner (gate
|
||||
/// truncation) — the shortfall is recorded, never computed around.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub generalization: Option<Generalization>,
|
||||
/// Per-instrument winning params — divergent winners are exposed,
|
||||
/// never averaged away. Present whenever >= 1 winner exists.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub winners: Vec<(String, Vec<(String, Scalar)>)>,
|
||||
/// Instrument cells that contributed no winner (gate-truncated), by name.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub missing: Vec<String>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
/// Assign a fresh per-name `run` index (one past the highest `run` already
|
||||
/// stored for `name`, or `0` if unseen), stamp `reports` as ordinal-ordered
|
||||
|
||||
@@ -73,8 +73,10 @@ fn campaign_run(campaign: &str) -> CampaignRunRecord {
|
||||
n_neighbours: None,
|
||||
},
|
||||
}),
|
||||
bootstrap: None,
|
||||
}],
|
||||
}],
|
||||
generalizations: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user