diff --git a/crates/aura-cli/src/campaign_run.rs b/crates/aura-cli/src/campaign_run.rs index eb4040b..2a8aa99 100644 --- a/crates/aura-cli/src/campaign_run.rs +++ b/crates/aura-cli/src/campaign_run.rs @@ -315,8 +315,8 @@ fn present_campaign( for cell in &outcome.record.cells { for (stage_ix, st) in cell.stages.iter().enumerate() { if matches!(&st.survivor_ordinals, Some(v) if v.is_empty()) { - eprintln!( - "aura: cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \ + crate::diag::note!( + "cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \ survivors; cell realization truncated", cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1 ); @@ -333,8 +333,8 @@ fn present_campaign( if let Some(f) = &cell.fault { failed += 1; fail_labels.push(format!("{}: {}", cell.instrument, cell_fault_kind_label(f.kind))); - eprintln!( - "aura: cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues", + crate::diag::warning!( + "cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues", cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1, f.stage, f.detail ); } @@ -388,7 +388,7 @@ fn present_campaign( .expect("campaign run record serializes") ); eprintln!( - "campaign run {} recorded: {} cells{}", + "aura: campaign run {} recorded: {} cells{}", outcome.record.run, outcome.record.cells.len(), if failed == 0 { diff --git a/crates/aura-cli/src/diag.rs b/crates/aura-cli/src/diag.rs new file mode 100644 index 0000000..3f9a36e --- /dev/null +++ b/crates/aura-cli/src/diag.rs @@ -0,0 +1,20 @@ +//! Stable stderr class markers (C14): `aura: note: ` for benign +//! diagnostics on a continuing run (exit code unaffected), +//! `aura: warning: ` for recorded faults the run survives. +//! Error lines that accompany a non-zero exit — and plain info lines +//! such as the run-record summary — keep the bare `aura:` prefix; for +//! errors, the exit-code partition is the machine contract. + +macro_rules! note { + ($($arg:tt)*) => { + eprintln!("aura: note: {}", format_args!($($arg)*)) + }; +} + +macro_rules! warning { + ($($arg:tt)*) => { + eprintln!("aura: warning: {}", format_args!($($arg)*)) + }; +} + +pub(crate) use {note, warning}; diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 1d243c8..57010f4 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -3,6 +3,7 @@ //! (or the `--strategy r-sma` sugar) and print canonical JSON metrics/manifests; //! this binary authors no built-in harness. +mod diag; mod render; mod graph_construct; mod campaign_run; diff --git a/crates/aura-cli/src/scaffold.rs b/crates/aura-cli/src/scaffold.rs index 7cedbda..8d705de 100644 --- a/crates/aura-cli/src/scaffold.rs +++ b/crates/aura-cli/src/scaffold.rs @@ -283,7 +283,7 @@ pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> { Ok(o) if o.status.success() => { commit_all(&spec.target_dir, "aura new scaffold"); } - _ => eprintln!("aura: warning: git init failed (project created without a repo)"), + _ => crate::diag::warning!("git init failed (project created without a repo)"), } Ok(()) } @@ -324,7 +324,7 @@ pub fn scaffold_node_crate(spec: &ScaffoldSpec) -> Result<(), String> { Ok(o) if o.status.success() => { commit_all(&spec.target_dir, "aura nodes new scaffold"); } - _ => eprintln!("aura: warning: git init failed (node crate created without a repo)"), + _ => crate::diag::warning!("git init failed (node crate created without a repo)"), } Ok(()) } @@ -357,7 +357,7 @@ fn commit_all(dir: &std::path::Path, message: &str) { .current_dir(dir) .output(); if !matches!(add, Ok(o) if o.status.success()) { - eprintln!("aura: warning: git add failed (scaffold created without an initial commit)"); + crate::diag::warning!("git add failed (scaffold created without an initial commit)"); return; } let commit = std::process::Command::new("git") @@ -374,8 +374,8 @@ fn commit_all(dir: &std::path::Path, message: &str) { .current_dir(dir) .output(); if !matches!(commit, Ok(o) if o.status.success()) { - eprintln!( - "aura: warning: git commit failed (scaffold created without an initial commit)" + crate::diag::warning!( + "git commit failed (scaffold created without an initial commit)" ); } } diff --git a/crates/aura-cli/src/verb_sugar.rs b/crates/aura-cli/src/verb_sugar.rs index dc2b964..a270f58 100644 --- a/crates/aura-cli/src/verb_sugar.rs +++ b/crates/aura-cli/src/verb_sugar.rs @@ -526,7 +526,7 @@ pub(crate) fn run_walkforward_sugar( )?; if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) { - eprintln!("aura: walkforward cell failed at stage {}: {}", f.stage, f.detail); + crate::diag::warning!("walkforward cell failed at stage {}: {}", f.stage, f.detail); return Ok(1); } @@ -690,7 +690,7 @@ pub(crate) fn run_mc_sugar( )?; if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) { - eprintln!("aura: mc cell failed at stage {}: {}", f.stage, f.detail); + crate::diag::warning!("mc cell failed at stage {}: {}", f.stage, f.detail); return Ok(1); } diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index a251532..04c0b94 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -1452,6 +1452,19 @@ const SWEEP_ONLY_PROCESS_DOC: &str = r#"{ "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ] }"#; +/// A gate no realized sweep member can ever satisfy (`n_trades ge 999999`) — +/// deterministically empties `survivor_ordinals` regardless of the fixture's +/// actual trade counts, driving the #278 zero-survivor NOTE marker. +const GATE_EMPTIES_PROCESS_DOC: &str = r#"{ + "format_version": 1, + "kind": "process", + "name": "gate-empties", + "pipeline": [ + { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, + { "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 999999.0 } ] } + ] +}"#; + /// The full annotated shape for the gated e2e: sweep -> gate -> walk_forward /// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so /// walk-forward always has survivors; the mc annotator bootstraps the pooled @@ -1978,6 +1991,57 @@ fn campaign_run_synthetic_e2e_parallel_instruments_contains_a_real_per_cell_faul assert_eq!(cells[1]["instrument"].as_str(), Some("SYMB"), "doc order preserved: {line}"); assert!(cells[0]["fault"].is_null(), "SYMA has data in January, its cell succeeds: {line}"); assert!(!cells[1]["fault"].is_null(), "SYMB has no January data, its cell is faulted: {line}"); + assert!( + out.contains("aura: warning: cell ("), + "the failed-cell line carries the warning class marker (#278): {out}" + ); +} + +/// #278 sibling of the warning-class pin above: a gate that empties a cell's +/// survivors (never a fault, #198 decision 8) emits the NOTE class marker, +/// not the bare `aura:` prefix nor `warning:`. `GATE_EMPTIES_PROCESS_DOC`'s +/// impossible threshold guarantees zero survivors regardless of the +/// fixture's actual sweep-member trade counts. Second property pinned on the +/// same invocation: the run's terminal "recorded" summary line — previously +/// printed with NO `aura:` prefix at all — now carries it too, so every +/// stderr line this command emits (per-cell class markers AND the run-level +/// summary) shares the one `aura:`-rooted vocabulary; a regression that +/// reverted just this one un-tagged `eprintln!` back to a bare line would +/// not be caught by the note-marker assertion above. +#[test] +fn campaign_run_synthetic_e2e_gate_emptied_cell_carries_the_note_marker() { + let (dir, _fixture) = fresh_project_with_data(); + let runs_dir = dir.join("runs"); + std::fs::remove_dir_all(&runs_dir).ok(); + let _cleanup = ScratchGuard(vec![ + ScratchPath::Dir(runs_dir.clone()), + ScratchPath::File(dir.join("gateempty.process.json")), + ScratchPath::File(dir.join("gateempty.campaign.json")), + ]); + let bp_id = seed_blueprint(&dir, "campaign-run-gateempty-seed"); + let proc_id = + register_process_doc(&dir, "gateempty.process.json", GATE_EMPTIES_PROCESS_DOC); + let doc = campaign_doc_json_for( + "SYMA", + &bp_id, + &proc_id, + (1704844800000, 1705708800000), + "", + "", + ); + write_doc(&dir, "gateempty.campaign.json", &doc); + + let (out, code) = + run_code_in(&dir, &["campaign", "run", "gateempty.campaign.json"]); + assert_eq!(code, Some(0), "a zero-survivor cell is a valid result, not a fault: {out}"); + assert!( + out.contains("aura: note: cell "), + "the gate-emptied cell line carries the note class marker (#278): {out}" + ); + assert!( + out.contains("aura: campaign run "), + "the run-level recorded summary line also carries the aura: prefix (#278): {out}" + ); } /// The v2 boundary, campaign side: a generalize-bearing process is an diff --git a/crates/aura-vocabulary/tests/c28_layering.rs b/crates/aura-vocabulary/tests/c28_layering.rs index 8c12005..7da26e5 100644 --- a/crates/aura-vocabulary/tests/c28_layering.rs +++ b/crates/aura-vocabulary/tests/c28_layering.rs @@ -179,6 +179,7 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() { modules, [ "campaign_run.rs", + "diag.rs", "graph_construct.rs", "main.rs", "render.rs",