//! Verb sugar: the dissolved orchestration verbs' argv → generated campaign //! documents (docs/specs/sweep-dissolution.md; #210 fork decisions). //! //! A dissolved verb invocation is translated into a selection-free process //! document plus a campaign document expressing exactly the invocation's //! intent, both auto-registered content-addressed (every ad-hoc invocation //! becomes durable, diffable, reproducible intent), then run through the one //! campaign executor in sugar presentation mode (member lines only). use std::collections::BTreeMap; use aura_core::{Scalar, ScalarKind}; use aura_research::{ campaign_to_json, content_id_of, process_to_json, tap_vocabulary, Axis, CampaignDoc, DataSection, DocKind, DocRef, Presentation, ProcessDoc, ProcessRef, RiskRegime, SelectRule, StageBlock, StrategyEntry, SweepSelection, WfMode, Window, FORMAT_VERSION, }; /// The two generated documents of one dissolved-verb invocation. #[derive(Debug)] pub(crate) struct GeneratedSweep { pub process: ProcessDoc, pub campaign: CampaignDoc, } /// One dissolved-verb invocation's shared shape (#214/#220): arbitrary RAW /// axes (post `wrapped_to_raw_axis` strip), the family name, the instrument /// list, the shared window, the canonical blueprint, and the optional single /// Vol stop regime (`None` binds no regime — sweep's contract). All four /// dissolved verbs (sweep, generalize, walkforward, mc) route their dispatch /// args through this shape. pub(crate) struct SugarInvocation<'a> { pub axes: &'a [(String, Vec)], pub name: String, /// Instruments: sweep/walkforward/mc carry exactly one; generalize >=2. pub symbols: Vec, pub from_ms: i64, pub to_ms: i64, pub blueprint_canonical: &'a str, pub stop: Option, /// Whether the invocation carried `--trace` (sweep/walkforward, #224): /// when true, the generated campaign's presentation requests the full /// tap vocabulary so `persist_campaign_traces` actually writes each /// member's tap series, instead of the #168 no-op `persist_taps: vec![]`. pub trace: bool, } /// The generated campaign's `persist_taps` section for one invocation: /// when `--trace` was requested, the tap vocabulary MINUS the net channel /// (`net_r_equity`), else empty (the #168 no-op — this is the one seam /// `translate_sweep`/`translate_walkforward` share for the decision). Every /// generated sugar campaign carries an empty cost section (`cost: vec![]`) /// by construction, so the net channel is never producible there; requesting /// it anyway would make `campaign_run`'s executor print an unreachable "add a /// cost block to the campaign document" remedy (#240). The exclusion reuses /// `campaign_run`'s own tap/channel classification (`tap_channel`/ /// `TapChannel::Net`) rather than duplicating the net-tap name here. fn persist_taps_from(trace: bool) -> Vec { if trace { tap_vocabulary() .iter() .filter(|t| crate::campaign_run::tap_channel(t) != Some(crate::campaign_run::TapChannel::Net)) .map(|t| t.to_string()) .collect() } else { vec![] } } /// The single protective-stop regime a dissolved verb binds (`RiskRegime::Vol`). #[derive(Clone, Copy)] pub(crate) struct VolStop { pub length: i64, pub k: f64, } /// The walk-forward roller sizes (ms) a wf/mc invocation carries. #[derive(Clone, Copy)] pub(crate) struct WfWindows { pub in_sample_ms: u64, pub out_of_sample_ms: u64, pub step_ms: u64, } /// The mc-specific bootstrap knobs. #[derive(Clone, Copy)] pub(crate) struct McKnobs { pub resamples: u32, pub block_len: u32, pub seed: u64, } /// The generic doc-axes map: every invocation axis under its arbitrary raw /// bind-key (the sweep shape, adopted by all four verbs — #220). fn doc_axes_from(axes: &[(String, Vec)]) -> Result, String> { let mut doc_axes: BTreeMap = BTreeMap::new(); for (axis_name, values) in axes { doc_axes.insert(axis_name.clone(), axis_from_values(axis_name, values)?); } Ok(doc_axes) } /// The optional single Vol stop regime -> the campaign risk vector. fn risk_from(stop: Option) -> Vec { stop.map(|s| vec![RiskRegime::Vol { length: s.length, k: s.k }]).unwrap_or_default() } /// One probe param per axis (any one grid value — `bind_axes` checks NAME /// coverage/uniqueness, never the bound value). fn probe_params_from(axes: &[(String, Vec)]) -> Vec<(String, Scalar)> { axes.iter().filter_map(|(n, vals)| vals.first().map(|v| (n.clone(), *v))).collect() } /// Map one verb axis (`--axis name=csv`, already scalar-parsed) to a /// document `Axis`: the kind is the first value's kind; a mixed-kind CSV is /// refused (the document axis declares its kind ONCE — #189). fn axis_from_values(name: &str, values: &[Scalar]) -> Result { let kind = match values.first() { Some(Scalar::I64(_)) => ScalarKind::I64, Some(Scalar::F64(_)) => ScalarKind::F64, Some(Scalar::Bool(_)) => ScalarKind::Bool, Some(Scalar::Timestamp(_)) => ScalarKind::Timestamp, None => return Err(format!("axis {name}: empty value list")), }; let homogeneous = values.iter().all(|v| { matches!( (v, kind), (Scalar::I64(_), ScalarKind::I64) | (Scalar::F64(_), ScalarKind::F64) | (Scalar::Bool(_), ScalarKind::Bool) | (Scalar::Timestamp(_), ScalarKind::Timestamp) ) }); if !homogeneous { return Err(format!( "axis {name}: mixed value kinds in one axis (an axis declares its kind once)" )); } Ok(Axis { kind, values: values.to_vec() }) } /// Translate a real-data blueprint sweep invocation into its two generated /// documents. `inv.blueprint_canonical` is the canonical blueprint JSON already /// stored by topology hash; its content id is the strategy ref. pub(crate) fn translate_sweep(inv: &SugarInvocation) -> Result { let process = ProcessDoc { format_version: FORMAT_VERSION, kind: DocKind::Process, name: "sweep".to_string(), description: None, pipeline: vec![StageBlock::Sweep { selection: None }], }; let campaign = CampaignDoc { format_version: FORMAT_VERSION, kind: DocKind::Campaign, name: inv.name.clone(), description: None, data: DataSection { instruments: inv.symbols.clone(), windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }], // The verb sugar passes no bindings: name defaults rule (#231); // authored campaign documents may set them. bindings: BTreeMap::new(), }, strategies: vec![StrategyEntry { r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)), axes: doc_axes_from(inv.axes)?, }], process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, // No stop regime is bound for a dissolved single-sweep member (the // dispatch passes `stop: None`; absent risk == empty risk, per the // established parity). The other verbs bind the single Vol regime // through this same seam. risk: risk_from(inv.stop), cost: vec![], // No stage of a selection-free single-sweep pipeline consumes the // seed; a fixed zero keeps generated bytes deterministic so identical // invocations dedupe onto identical content ids. seed: 0, presentation: Presentation { persist_taps: persist_taps_from(inv.trace), emit: vec!["family_table".to_string()], }, }; Ok(GeneratedSweep { process, campaign }) } /// Auto-register both generated documents (content-addressed, idempotent). /// Returns `(process_id, campaign_id)`. pub(crate) fn register_generated( reg: &aura_registry::Registry, generated: &GeneratedSweep, ) -> Result<(String, String), String> { let process_id = reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?; let campaign_id = reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?; Ok((process_id, campaign_id)) } /// The two generated documents of one dissolved `generalize` invocation. #[derive(Debug)] pub(crate) struct GeneratedGeneralize { pub process: ProcessDoc, pub campaign: CampaignDoc, } /// Translate one `aura generalize` invocation into its two generated documents: /// a **selection-bearing** process (`[std::sweep(argmax), std::generalize]` — /// generalize needs a nominee, so the sweep stage is selection-bearing, unlike /// the selection-free single-sweep translator) and a campaign running the one /// fixed candidate (single-value raw axes) across all instruments under the /// invocation's single risk regime. `inv.blueprint_canonical` is the user's /// blueprint already stored by topology hash; its content id is the strategy ref. pub(crate) fn translate_generalize( inv: &SugarInvocation, metric: &str, ) -> Result { let process = ProcessDoc { format_version: FORMAT_VERSION, kind: DocKind::Process, name: "generalize".to_string(), description: None, pipeline: vec![ StageBlock::Sweep { selection: Some(SweepSelection { metric: metric.to_string(), select: SelectRule::Argmax, deflate: false, }), }, StageBlock::Generalize { metric: metric.to_string() }, ], }; let campaign = CampaignDoc { format_version: FORMAT_VERSION, kind: DocKind::Campaign, name: inv.name.clone(), description: None, data: DataSection { instruments: inv.symbols.clone(), windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }], // The verb sugar passes no bindings: name defaults rule (#231); // authored campaign documents may set them. bindings: BTreeMap::new(), }, strategies: vec![StrategyEntry { r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)), axes: doc_axes_from(inv.axes)?, }], process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, // The stop is a single structural risk regime; the member runner maps // it back to StopRule::Vol at run time. risk: risk_from(inv.stop), cost: vec![], seed: 0, presentation: Presentation { persist_taps: vec![], emit: vec!["family_table".to_string()], }, }; Ok(GeneratedGeneralize { process, campaign }) } /// Auto-register both generated generalize documents (content-addressed, /// idempotent). Returns `(process_id, campaign_id)`. pub(crate) fn register_generated_g( reg: &aura_registry::Registry, generated: &GeneratedGeneralize, ) -> Result<(String, String), String> { let process_id = reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?; let campaign_id = reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?; Ok((process_id, campaign_id)) } /// Validate a generated `(process, campaign)` pair BEFORE anything is /// registered (#210 c0110 fieldtest, "store litter" finding): a referential /// refusal must never leave a dead generated document in the content- /// addressed store. Defensive doc-shape checks first (generated docs should /// always pass this — the translators build them by construction), then the /// executable-shape preflight, then the axis-binding check against the /// blueprint's own (wrapped) open param space via the pure `bind_axes` seam /// (campaign_run.rs) — the referential shape the dispatch-boundary name /// preflight (main.rs) does not catch: a subset of axes that leaves an open /// param unbound (probe P3). Probe P3's space is the REOPENED one (#246): the /// same `raw_bound_overrides_of` derivation `CliMemberRunner::run_member` uses /// per member (campaign_run.rs) — a bound-param axis passes this /// executable-shape preflight exactly like an already-open one; an axis /// matching neither space still fails `bind_axes`'s own named check below. /// `probe_params` is any one grid value per axis (checks NAME /// coverage/uniqueness, never the bound value). Shared by both dissolved-verb /// sugars: one seam enforcing the store-litter invariant, rather than two /// drift-prone copies. fn validate_before_register( process: &ProcessDoc, campaign: &CampaignDoc, blueprint_canonical: &str, probe_params: &[(String, Scalar)], env: &crate::project::Env, ) -> Result<(), String> { let doc_faults = aura_research::validate_campaign(campaign); if !doc_faults.is_empty() { return Err(crate::research_docs::fault_block( "generated campaign document invalid:", doc_faults.iter().map(crate::research_docs::doc_fault_prose).collect(), )); } let process_faults = aura_research::validate_process(process); if !process_faults.is_empty() { return Err(crate::research_docs::fault_block( "generated process document invalid:", process_faults.iter().map(crate::research_docs::doc_fault_prose).collect(), )); } aura_campaign::preflight(process, campaign).map_err(|f| crate::campaign_run::exec_fault_prose(&f))?; // The un-reopened wrapped OPEN space (#246): derive which of the probe's // RAW axis names re-open a bound param of the strategy, against a raw // (un-reopened) reload of the same canonical bytes — infallible, the // strategy ref's own content. let raw_space = crate::blueprint_axis_probe(blueprint_canonical, env).param_space(); let raw_signal = aura_engine::blueprint_from_json(blueprint_canonical, &|t| env.resolve(t)) .expect("a generated sugar campaign's strategy ref reloads its own canonical bytes"); let param_names: Vec = probe_params.iter().map(|(n, _)| n.clone()).collect(); let overrides = crate::campaign_run::raw_bound_overrides_of(¶m_names, &raw_space, &raw_signal); let space = crate::blueprint_axis_probe_reopened(blueprint_canonical, env, &overrides).param_space(); let strategy_id = content_id_of(blueprint_canonical); crate::campaign_run::bind_axes(&space, &strategy_id, probe_params) .map(|_| ()) .map_err(|f| crate::campaign_run::exec_fault_prose(&aura_campaign::ExecFault::Member(f))) } /// Run one dissolved sweep invocation end-to-end: register the generated /// documents, then execute through the one campaign path in sugar mode. pub(crate) fn run_sweep_sugar( inv: &SugarInvocation, env: &crate::project::Env, ) -> Result<(), String> { let generated = translate_sweep(inv)?; let probe_params = probe_params_from(inv.axes); validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?; let reg = env.registry(); let (_process_id, campaign_id) = register_generated(®, &generated)?; crate::campaign_run::run_campaign_by_id(&campaign_id, env, crate::campaign_run::RunPresentation::MemberLinesOnly) } /// Build the `CrossInstrument` family members from an executed generalize /// campaign: each cell's nominee `RunReport`, in cell (instrument doc) order — /// the same order + shape the inline `run_generalize` built its `members` in, /// each already carrying `manifest.instrument`. Debug-asserts one member per /// cell: generalize's pipeline has no gate stage, so every cell nominates — /// a cell silently missing its nominee here would be a campaign-executor /// regression, not a legitimate generalize outcome, and the filter_map below /// must not swallow it unnoticed. fn cross_instrument_members( outcome: &aura_campaign::CampaignOutcome, ) -> Vec { let members: Vec = outcome .cells .iter() .filter_map(|c| c.nominee.as_ref().map(|(_, report)| report.clone())) .collect(); debug_assert_eq!( members.len(), outcome.cells.len(), "generalize has no gate stage — every cell must nominate" ); members } /// Run one dissolved `generalize` invocation end-to-end: register the generated /// documents, run through the one campaign path, then reprint today's exact /// `{"generalize":{…}}` + `{"family_id":…}` lines from the recorded outcome and /// persist the `CrossInstrument` family. The stdout is byte-identical to the /// retired welded path (the committed exact-grade anchor is the gate). pub(crate) fn run_generalize_sugar( inv: &SugarInvocation, metric: &str, env: &crate::project::Env, ) -> Result<(), String> { let generated = translate_generalize(inv, metric)?; let probe_params = probe_params_from(inv.axes); validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?; let reg = env.registry(); let (_process_id, campaign_id) = register_generated_g(®, &generated)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; // Reprint the verb's aggregate line from the recorded cross-instrument // grade — byte-identical to the retired welded path's `generalize_json(&agg)`. let cg = run .outcome .record .generalizations .iter() .find_map(|g| g.generalization.as_ref()) .ok_or("generalize produced no cross-instrument grade")?; println!("{}", crate::generalize_json(cg)); let members = cross_instrument_members(&run.outcome); let family_id = reg .append_family(&inv.name, aura_registry::FamilyKind::CrossInstrument, &members) .map_err(|e| e.to_string())?; println!("{{\"family_id\":\"{family_id}\"}}"); Ok(()) } /// The two generated documents of one dissolved `walkforward` invocation. #[derive(Debug)] pub(crate) struct GeneratedWalkforward { pub process: ProcessDoc, pub campaign: CampaignDoc, } /// Translate one `aura walkforward --real` invocation into its two generated /// documents: a `[std::grid, std::walk_forward]` process (the leading /// `std::grid` stage only enumerates the IS-refit survivor grid for the wf /// stage — it executes nothing and persists no family (#256)) and a campaign /// running the invocation's axis grid over one instrument under its single /// risk regime. The roller sizes come from `w` (ms). `inv.blueprint_canonical` /// is the user's blueprint already stored by topology hash; its content id is /// the strategy ref. pub(crate) fn translate_walkforward( inv: &SugarInvocation, metric: &str, w: WfWindows, select: SelectRule, ) -> Result { let process = ProcessDoc { format_version: FORMAT_VERSION, kind: DocKind::Process, name: "walkforward".to_string(), description: None, pipeline: vec![ StageBlock::Grid, StageBlock::WalkForward { in_sample_ms: w.in_sample_ms, out_of_sample_ms: w.out_of_sample_ms, step_ms: w.step_ms, mode: WfMode::Rolling, metric: metric.to_string(), select, }, ], }; let campaign = CampaignDoc { format_version: FORMAT_VERSION, kind: DocKind::Campaign, name: inv.name.clone(), description: None, data: DataSection { instruments: inv.symbols.clone(), windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }], // The verb sugar passes no bindings: name defaults rule (#231); // authored campaign documents may set them. bindings: BTreeMap::new(), }, strategies: vec![StrategyEntry { r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)), axes: doc_axes_from(inv.axes)?, }], process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, risk: risk_from(inv.stop), cost: vec![], seed: 0, presentation: Presentation { persist_taps: persist_taps_from(inv.trace), emit: vec!["family_table".to_string()], }, }; Ok(GeneratedWalkforward { process, campaign }) } /// Auto-register both generated walkforward documents (content-addressed, /// idempotent). Returns `(process_id, campaign_id)`. pub(crate) fn register_generated_wf( reg: &aura_registry::Registry, generated: &GeneratedWalkforward, ) -> Result<(String, String), String> { let process_id = reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?; let campaign_id = reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?; Ok((process_id, campaign_id)) } /// Run one dissolved `walkforward` invocation end-to-end: register the generated /// documents, run through the one campaign path, then reprint the per-window member /// lines + the summary line from the recorded `WalkForward` family. The stdout /// summary line is byte-identical to the retired welded path (the committed /// exact-grade anchor is the gate). pub(crate) fn run_walkforward_sugar( inv: &SugarInvocation, metric: &str, w: WfWindows, select: SelectRule, env: &crate::project::Env, ) -> Result<(), String> { let generated = translate_walkforward(inv, metric, w, select)?; let probe_params = probe_params_from(inv.axes); validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?; let reg = env.registry(); let (_process_id, campaign_id) = register_generated_wf(®, &generated)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; // The single cell's walk_forward StageFamily. let wf = run .outcome .cells .iter() .flat_map(|c| c.families.iter()) .find(|f| f.block == "std::walk_forward") .ok_or("walkforward produced no walk_forward family")?; for report in &wf.reports { println!("{}", crate::family_member_line(&wf.family_id, report)); } // Summary axes: the invocation's raw axis names in argv order, then the // stop columns when a regime is bound (#220 — the renderer hardcodes none). let mut summary_axes: Vec = inv.axes.iter().map(|(n, _)| n.clone()).collect(); if inv.stop.is_some() { summary_axes.push("stop_length".to_string()); summary_axes.push("stop_k".to_string()); } println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports, &summary_axes)); // Trace persistence runs LAST (mirroring `present_campaign`'s ordering, // 0109): a no-op when `--trace` was not requested (`persist_taps` empty // => `trace_name` is `None`), so this is always safe to call. Unlike // `run_sweep_sugar` (routed through `run_campaign_by_id` -> the shared // `present_campaign` tail), this walkforward sugar calls // `run_campaign_returning` directly and prints its own bespoke lines, so // it must run this tail itself. Its generalize/mc siblings call // `run_campaign_returning` the same way but do NOT run this tail: neither // has a `--trace` grammar (generalize never did; `mc --real` refuses // `--name`/`--trace` up front, #224) and `campaign.presentation.persist_taps` // is always empty for them, so their `run.outcome.record.trace_name` is // always `None`. `run`/`mc --trace` stay refused by design (#224), not by // this tail being skipped. if let Some(trace_name) = &run.outcome.record.trace_name { crate::campaign_run::persist_campaign_traces( trace_name, &run.campaign.presentation.persist_taps, &run.outcome, &run.campaign, &run.strategies, &run.server, env, )?; } Ok(()) } /// The two generated documents of one dissolved `mc` invocation. #[derive(Debug)] pub(crate) struct GeneratedMc { pub process: ProcessDoc, pub campaign: CampaignDoc, } /// Translate one `aura mc --real` invocation into its two generated documents: /// a `[std::grid, std::walk_forward, std::monte_carlo]` process (the leading /// `std::grid` stage only enumerates the IS-refit survivor grid for the wf /// stage — it executes nothing and persists no family (#256); the terminal /// monte_carlo stage pools the per-window OOS trade-R series and r-bootstraps /// `E[R]`) and a campaign running the invocation's axis grid over one instrument /// under its single risk regime. Unlike `translate_walkforward` (which hardcodes /// `seed: 0`), `translate_mc` sets `campaign.seed = mc.seed` (the mc `--seed`): /// the wf winners are argmax hence deflation-seed-independent (the shipped /// walkforward anchor proved this), so remapping the campaign seed leaves the /// pooled series unchanged while making the terminal `r_bootstrap` at that seed /// reproduce the retired inline bootstrap. pub(crate) fn translate_mc( inv: &SugarInvocation, metric: &str, w: WfWindows, mc: McKnobs, ) -> Result { let process = ProcessDoc { format_version: FORMAT_VERSION, kind: DocKind::Process, name: "mc".to_string(), description: None, pipeline: vec![ StageBlock::Grid, StageBlock::WalkForward { in_sample_ms: w.in_sample_ms, out_of_sample_ms: w.out_of_sample_ms, step_ms: w.step_ms, mode: WfMode::Rolling, metric: metric.to_string(), select: SelectRule::Argmax, }, StageBlock::MonteCarlo { resamples: mc.resamples, block_len: mc.block_len }, ], }; let campaign = CampaignDoc { format_version: FORMAT_VERSION, kind: DocKind::Campaign, name: inv.name.clone(), description: None, data: DataSection { instruments: inv.symbols.clone(), windows: vec![Window { from_ms: inv.from_ms, to_ms: inv.to_ms }], // The verb sugar passes no bindings: name defaults rule (#231); // authored campaign documents may set them. bindings: BTreeMap::new(), }, strategies: vec![StrategyEntry { r#ref: DocRef::ContentId(content_id_of(inv.blueprint_canonical)), axes: doc_axes_from(inv.axes)?, }], process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) }, risk: risk_from(inv.stop), cost: vec![], seed: mc.seed, presentation: Presentation { persist_taps: vec![], emit: vec!["family_table".to_string()], }, }; Ok(GeneratedMc { process, campaign }) } /// Auto-register both generated mc documents (content-addressed, idempotent). /// Returns `(process_id, campaign_id)`. pub(crate) fn register_generated_mc( reg: &aura_registry::Registry, generated: &GeneratedMc, ) -> Result<(String, String), String> { let process_id = reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?; let campaign_id = reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?; Ok((process_id, campaign_id)) } /// Run one dissolved `mc` invocation end-to-end: register the generated documents, /// run through the one campaign path, then reprint the single `mc_r_bootstrap` grade /// line from the recorded terminal `PooledOos` bootstrap. The stdout line is /// byte-identical to the retired welded path (the committed exact-grade anchor is /// the gate). mc prints ONLY the one summary line — no per-window member lines /// (unlike walkforward): the monte_carlo stage is a terminal annotator, not a family. pub(crate) fn run_mc_sugar( inv: &SugarInvocation, metric: &str, w: WfWindows, mc: McKnobs, env: &crate::project::Env, ) -> Result<(), String> { let generated = translate_mc(inv, metric, w, mc)?; let probe_params = probe_params_from(inv.axes); validate_before_register(&generated.process, &generated.campaign, inv.blueprint_canonical, &probe_params, env)?; let reg = env.registry(); let (_process_id, campaign_id) = register_generated_mc(®, &generated)?; let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?; // The single cell's terminal monte_carlo StageRealization carries the pooled-OOS // bootstrap (record path: outcome.record.cells[].stages[].bootstrap). let boot = run .outcome .record .cells .iter() .flat_map(|c| c.stages.iter()) .find_map(|s| match &s.bootstrap { Some(aura_registry::StageBootstrap::PooledOos(b)) => Some(b), _ => None, }) .ok_or("mc produced no pooled-OOS bootstrap")?; println!("{}", crate::mc_r_bootstrap_json(boot)); Ok(()) } #[cfg(test)] mod tests { use super::*; fn axes() -> Vec<(String, Vec)> { vec![ ("fast.length".to_string(), vec![Scalar::I64(2), Scalar::I64(4)]), ("slow.length".to_string(), vec![Scalar::I64(8), Scalar::I64(16)]), ] } fn inv<'a>( axes: &'a [(String, Vec)], name: &str, symbols: &[&str], stop: Option, bp: &'a str, ) -> SugarInvocation<'a> { SugarInvocation { axes, name: name.to_string(), symbols: symbols.iter().map(|s| s.to_string()).collect(), from_ms: 100, to_ms: 200, blueprint_canonical: bp, stop, trace: false, } } fn g_axes() -> Vec<(String, Vec)> { vec![ ("fast.length".to_string(), vec![Scalar::i64(3)]), ("slow.length".to_string(), vec![Scalar::i64(12)]), ] } fn wf_axes() -> Vec<(String, Vec)> { vec![ ("fast.length".to_string(), vec![Scalar::i64(3), Scalar::i64(5)]), ("slow.length".to_string(), vec![Scalar::i64(12), Scalar::i64(20)]), ] } const WF_W: WfWindows = WfWindows { in_sample_ms: 7_776_000_000, out_of_sample_ms: 2_592_000_000, step_ms: 2_592_000_000, }; #[test] fn translate_sweep_is_deterministic_and_names_flow() { let ax = axes(); let i = inv(&ax, "probe", &["GER40"], None, "{\"bp\":1}"); let a = translate_sweep(&i).unwrap(); let b = translate_sweep(&i).unwrap(); assert_eq!( content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&b.campaign)), "identical invocations must generate identical campaign ids" ); assert_eq!(a.campaign.name, "probe"); assert_eq!(a.campaign.seed, 0); assert_eq!(a.process.name, "sweep"); assert_eq!(a.process.pipeline, vec![StageBlock::Sweep { selection: None }]); assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]); assert_eq!(a.campaign.data.windows, vec![Window { from_ms: 100, to_ms: 200 }]); assert_eq!(a.campaign.presentation.emit, vec!["family_table".to_string()]); assert!(a.campaign.presentation.persist_taps.is_empty()); // the process ref is the content id of the generated process bytes assert_eq!( a.campaign.process.r#ref, DocRef::ContentId(content_id_of(&process_to_json(&a.process))) ); } #[test] fn translate_sweep_refuses_a_mixed_kind_axis() { let mixed = vec![("k".to_string(), vec![Scalar::I64(1), Scalar::F64(2.5)])]; let err = translate_sweep(&inv(&mixed, "n", &["GER40"], None, "{}")).unwrap_err(); assert!(err.contains("mixed value kinds"), "{err}"); } /// Property (#224, pin moved to #240): `SugarInvocation.trace = true` /// requests the tap vocabulary MINUS the net channel on the generated /// campaign's `presentation.persist_taps` — the honest inverse of the /// #168 no-op (where `persist_taps` stayed `vec![]` regardless of /// `--trace`), narrowed to what the generated campaign's own empty cost /// section can actually produce (#240: `net_r_equity` is unproducible /// there and must be excluded). `trace = false` (the default, pinned by /// the sibling `translate_sweep_is_deterministic_and_names_flow`) keeps /// the vocabulary empty. Kept as its own translator-level pin alongside /// the cross-translator `dissolved_trace_requests_only_producible_taps_ /// excluding_the_net_channel` property test (#240): that test pins the /// shared `persist_taps_from` seam once for both translators together, /// this one keeps `translate_sweep`'s own regression independent of the /// walkforward sibling so a translator-specific wiring break is not /// masked by the other translator still passing. #[test] fn translate_sweep_trace_requests_the_producible_tap_subset() { let ax = axes(); let mut i = inv(&ax, "probe", &["GER40"], None, "{\"bp\":1}"); i.trace = true; let g = translate_sweep(&i).unwrap(); let want: Vec = vec!["equity".to_string(), "exposure".to_string(), "r_equity".to_string()]; assert_eq!(g.campaign.presentation.persist_taps, want); } #[test] fn generated_campaign_validates_intrinsically_and_preflights() { let ax = axes(); let g = translate_sweep(&inv(&ax, "probe", &["GER40"], None, "{\"bp\":1}")).unwrap(); assert!(aura_research::validate_campaign(&g.campaign).is_empty()); assert!(aura_research::validate_process(&g.process).is_empty()); assert!(aura_campaign::preflight(&g.process, &g.campaign).is_ok()); } /// The one seam all four verbs share (#220): `stop: Some(VolStop)` binds the /// single Vol regime; `stop: None` binds no regime (sweep's contract). #[test] fn sugar_invocation_stop_maps_to_the_vol_regime() { let ax = axes(); let mut i = inv(&ax, "n", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"); let with = translate_sweep(&i).unwrap(); assert_eq!(with.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]); i.stop = None; let without = translate_sweep(&i).unwrap(); assert!(without.campaign.risk.is_empty()); } #[test] fn translate_generalize_is_deterministic_and_carries_the_regime() { let ax = g_axes(); let i = inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"); let a = translate_generalize(&i, "expectancy_r").unwrap(); let b = translate_generalize(&i, "expectancy_r").unwrap(); assert_eq!( content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&b.campaign)), "identical invocations must generate identical campaign ids" ); // A selection-bearing pipeline: sweep(argmax) then generalize. assert_eq!( a.process.pipeline, vec![ StageBlock::Sweep { selection: Some(SweepSelection { metric: "expectancy_r".to_string(), select: SelectRule::Argmax, deflate: false, }) }, StageBlock::Generalize { metric: "expectancy_r".to_string() }, ] ); // The stop rides a single risk regime (the structural axis). assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]); // All instruments under one campaign; the candidate as single-value axes. assert_eq!( a.campaign.data.instruments, vec!["GER40".to_string(), "USDJPY".to_string()] ); assert_eq!(a.campaign.strategies.len(), 1); assert!(a.campaign.strategies[0].axes.contains_key("fast.length")); assert!(a.campaign.strategies[0].axes.contains_key("slow.length")); assert_eq!(a.process.name, "generalize"); assert_eq!(a.campaign.name, "generalize"); // Intrinsically valid + preflights (like the sweep sibling). assert!(aura_research::validate_campaign(&a.campaign).is_empty()); assert!(aura_research::validate_process(&a.process).is_empty()); assert!(aura_campaign::preflight(&a.process, &a.campaign).is_ok()); } /// Content-addressing correctness for the risk-regime axis (#210 T1-T2): /// two invocations that differ ONLY in the protective stop (`stop_length`/ /// `stop_k`) must generate campaigns with DIFFERENT content ids. The stop /// is now carried through `CampaignDoc.risk` rather than as an opaque /// runtime constant, so a hashing/serialization bug that dropped or /// normalized-away the regime would silently collide two distinct /// invocations onto one cached campaign — the wrong grade would be served /// for one of them without any error. This pins that a regime change is /// visible in the generated bytes, both against a same-length, /// different-k pair and a different-length, same-k pair. #[test] fn translate_generalize_diverging_regimes_do_not_collide_content_ids() { let ax = g_axes(); let base = translate_generalize(&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "expectancy_r").unwrap(); let different_k = translate_generalize(&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "expectancy_r").unwrap(); let different_length = translate_generalize(&inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "expectancy_r").unwrap(); let base_id = content_id_of(&campaign_to_json(&base.campaign)); let k_id = content_id_of(&campaign_to_json(&different_k.campaign)); let length_id = content_id_of(&campaign_to_json(&different_length.campaign)); assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id"); assert_ne!( base_id, length_id, "a different stop_length must not collide onto the same campaign id" ); assert_ne!(k_id, length_id, "the two diverging regimes must not collide with each other"); } /// The translator's output is not merely intrinsically valid in isolation /// (preflight/validate never touch a store) — `register_generated_g` must /// actually persist documents the campaign can resolve through a real /// `Registry`: the campaign's `process.ref` content id must fetch back the /// EXACT process the translator produced, and the returned `campaign_id` /// must fetch back the exact campaign. A drift between what gets hashed /// into the ref and what gets stored (e.g. a canonicalization mismatch) /// would pass every static check here yet leave the campaign permanently /// unresolvable at real run time — this is the property that would catch /// that silently, before the dispatch rewire ever exercises the path live. #[test] fn register_generated_g_stores_documents_the_campaign_can_actually_resolve() { let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp")) .join("aura-verb-sugar-generalize-registry-test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let reg = aura_registry::Registry::open(dir.join("runs.jsonl")); let ax = g_axes(); let generated = translate_generalize( &inv(&ax, "generalize", &["GER40", "USDJPY"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "expectancy_r", ) .unwrap(); let (process_id, campaign_id) = register_generated_g(®, &generated).unwrap(); let stored_campaign = reg.get_campaign(&campaign_id).unwrap().expect("campaign stored"); assert_eq!(stored_campaign, campaign_to_json(&generated.campaign)); let DocRef::ContentId(ref_id) = &generated.campaign.process.r#ref else { panic!("generalize's process ref is a content id"); }; assert_eq!(ref_id, &process_id, "the stored process id must be the campaign's own ref"); let stored_process = reg.get_process(ref_id).unwrap().expect("process stored"); assert_eq!(stored_process, process_to_json(&generated.process)); let _ = std::fs::remove_dir_all(&dir); } #[test] fn translate_walkforward_is_deterministic_and_carries_the_regime() { let ax = wf_axes(); let i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"); let a = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); let b = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); assert_eq!( content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&b.campaign)), "identical invocations must generate identical campaign ids" ); // pipeline: std::grid (enumerate-only) then walk_forward(argmax, rolling) assert_eq!( a.process.pipeline, vec![ StageBlock::Grid, StageBlock::WalkForward { in_sample_ms: 7_776_000_000, out_of_sample_ms: 2_592_000_000, step_ms: 2_592_000_000, mode: WfMode::Rolling, metric: "sqn_normalized".to_string(), select: SelectRule::Argmax, }, ] ); assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]); assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]); assert_eq!(a.campaign.strategies.len(), 1); // multi-value IS-refit grid axes assert_eq!( a.campaign.strategies[0].axes["fast.length"].values, vec![Scalar::i64(3), Scalar::i64(5)] ); assert_eq!( a.campaign.strategies[0].axes["slow.length"].values, vec![Scalar::i64(12), Scalar::i64(20)] ); assert_eq!(a.process.name, "walkforward"); assert!(aura_research::validate_campaign(&a.campaign).is_empty()); assert!(aura_research::validate_process(&a.process).is_empty()); assert!(aura_campaign::preflight(&a.process, &a.campaign).is_ok()); } /// Property (#224, pin moved to #240): mirrors /// `translate_sweep_trace_requests_the_producible_tap_subset` for /// walkforward's translator — the walkforward sibling refusal lifts /// identically, riding the same `persist_taps_from` seam, and the same /// net-channel exclusion applies (#240: the generated campaign's empty /// cost section cannot produce `net_r_equity`). Kept as its own /// translator-level pin alongside the cross-translator /// `dissolved_trace_requests_only_producible_taps_excluding_the_net_ /// channel` property test for the same reason as the sweep sibling: a /// walkforward-specific wiring break must not be masked by sweep alone /// still passing. #[test] fn translate_walkforward_trace_requests_the_producible_tap_subset() { let ax = wf_axes(); let mut i = inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"); i.trace = true; let g = translate_walkforward(&i, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); let want: Vec = vec!["equity".to_string(), "exposure".to_string(), "r_equity".to_string()]; assert_eq!(g.campaign.presentation.persist_taps, want); } /// Property (#240): a dissolved `--trace` invocation's generated campaign /// requests only the taps its own document can produce. Every generated /// sugar campaign carries an empty cost section (`cost: vec![]`) by /// construction, so the net-channel tap `net_r_equity` is unproducible and /// must be excluded — the requested set is the tap vocabulary MINUS the net /// channel, concretely `["equity", "exposure", "r_equity"]`. Pinned for /// both trace-bearing translators (sweep + walkforward), which share the /// one `persist_taps_from` seam; without this the executor later prints an /// unreachable "add a cost block to the campaign document" remedy for a tap /// the generator knowingly ordered but its own document cannot produce. #[test] fn dissolved_trace_requests_only_producible_taps_excluding_the_net_channel() { let want = vec!["equity".to_string(), "exposure".to_string(), "r_equity".to_string()]; let sax = axes(); let mut si = inv(&sax, "probe", &["GER40"], None, "{\"bp\":1}"); si.trace = true; let s = translate_sweep(&si).unwrap(); assert_eq!( s.campaign.presentation.persist_taps, want, "sweep --trace must request only producible taps: the generated cost section is empty, so net_r_equity is unproducible and must be dropped" ); let wax = wf_axes(); let mut wi = inv(&wax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"); wi.trace = true; let w = translate_walkforward(&wi, "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); assert_eq!( w.campaign.presentation.persist_taps, want, "walkforward --trace must request only producible taps: the generated cost section is empty, so net_r_equity is unproducible and must be dropped" ); } #[test] fn translate_walkforward_diverging_regimes_do_not_collide_content_ids() { let ax = wf_axes(); let base = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); let different_k = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); let different_length = translate_walkforward(&inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 20, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax).unwrap(); let base_id = content_id_of(&campaign_to_json(&base.campaign)); let k_id = content_id_of(&campaign_to_json(&different_k.campaign)); let length_id = content_id_of(&campaign_to_json(&different_length.campaign)); assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id"); assert_ne!( base_id, length_id, "a different stop_length must not collide onto the same campaign id" ); assert_ne!(k_id, length_id, "the two diverging regimes must not collide with each other"); } #[test] fn register_generated_wf_stores_documents_the_campaign_can_actually_resolve() { let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp")) .join("aura-verb-sugar-walkforward-registry-test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let reg = aura_registry::Registry::open(dir.join("runs.jsonl")); let ax = wf_axes(); let generated = translate_walkforward( &inv(&ax, "walkforward", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, SelectRule::Argmax, ) .unwrap(); let (process_id, campaign_id) = register_generated_wf(®, &generated).unwrap(); let stored_campaign = reg.get_campaign(&campaign_id).unwrap().expect("campaign stored"); assert_eq!(stored_campaign, campaign_to_json(&generated.campaign)); let DocRef::ContentId(ref_id) = &generated.campaign.process.r#ref else { panic!("walkforward's process ref is a content id"); }; assert_eq!(ref_id, &process_id, "the stored process id must be the campaign's own ref"); let stored_process = reg.get_process(ref_id).unwrap().expect("process stored"); assert_eq!(stored_process, process_to_json(&generated.process)); let _ = std::fs::remove_dir_all(&dir); } #[test] fn translate_mc_is_deterministic_and_carries_the_regime_and_seed() { let ax = wf_axes(); let i = inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"); let a = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap(); let b = translate_mc(&i, "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap(); assert_eq!( content_id_of(&campaign_to_json(&a.campaign)), content_id_of(&campaign_to_json(&b.campaign)), "identical invocations must generate identical campaign ids" ); // pipeline: std::grid (enumerate-only) then walk_forward(argmax, rolling) then monte_carlo assert_eq!( a.process.pipeline, vec![ StageBlock::Grid, StageBlock::WalkForward { in_sample_ms: 7_776_000_000, out_of_sample_ms: 2_592_000_000, step_ms: 2_592_000_000, mode: WfMode::Rolling, metric: "sqn_normalized".to_string(), select: SelectRule::Argmax, }, StageBlock::MonteCarlo { resamples: 1000, block_len: 5 }, ] ); assert_eq!(a.campaign.seed, 42, "the campaign seed carries the mc --seed"); assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]); assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]); assert_eq!(a.campaign.strategies.len(), 1); assert_eq!( a.campaign.strategies[0].axes["fast.length"].values, vec![Scalar::i64(3), Scalar::i64(5)] ); assert_eq!( a.campaign.strategies[0].axes["slow.length"].values, vec![Scalar::i64(12), Scalar::i64(20)] ); assert_eq!(a.process.name, "mc"); assert!(aura_research::validate_campaign(&a.campaign).is_empty()); assert!(aura_research::validate_process(&a.process).is_empty()); assert!(aura_campaign::preflight(&a.process, &a.campaign).is_ok()); } #[test] fn translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids() { let ax = wf_axes(); let base = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap(); let different_seed = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 7 }).unwrap(); let different_k = translate_mc(&inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 3.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }).unwrap(); let base_id = content_id_of(&campaign_to_json(&base.campaign)); let seed_id = content_id_of(&campaign_to_json(&different_seed.campaign)); let k_id = content_id_of(&campaign_to_json(&different_k.campaign)); assert_ne!(base_id, seed_id, "a different mc seed must not collide onto the same campaign id"); assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id"); assert_ne!(seed_id, k_id, "the two diverging documents must not collide with each other"); } #[test] fn register_generated_mc_stores_documents_the_campaign_can_actually_resolve() { let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp")) .join("aura-verb-sugar-mc-registry-test"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let reg = aura_registry::Registry::open(dir.join("runs.jsonl")); let ax = wf_axes(); let generated = translate_mc( &inv(&ax, "mc", &["GER40"], Some(VolStop { length: 14, k: 2.0 }), "{\"bp\":1}"), "sqn_normalized", WF_W, McKnobs { resamples: 1000, block_len: 5, seed: 42 }, ) .unwrap(); let (process_id, campaign_id) = register_generated_mc(®, &generated).unwrap(); let stored_campaign = reg.get_campaign(&campaign_id).unwrap().expect("campaign stored"); assert_eq!(stored_campaign, campaign_to_json(&generated.campaign)); let DocRef::ContentId(ref_id) = &generated.campaign.process.r#ref else { panic!("mc's process ref is a content id"); }; assert_eq!(ref_id, &process_id, "the stored process id must be the campaign's own ref"); let stored_process = reg.get_process(ref_id).unwrap().expect("process stored"); assert_eq!(stored_process, process_to_json(&generated.process)); let _ = std::fs::remove_dir_all(&dir); } }