feat(cli): stderr class markers — diag module, campaign/verb-sugar retags

Iteration stderr-markers-1, tasks 1-2 of the stderr-class-markers plan
(spec signed via grounding-check PASS, decisions logged on #278).

The two stderr diagnostic classes become machine-separable (refs #278):
a new crate-internal diag module owns the grammar as note!/warning!
macros (`aura: note: <text>` benign continuing-run diagnostic,
`aura: warning: <text>` recorded fault the run survives); the four
undifferentiated sites are retagged — gate-emptied cell -> note,
failed cell / walkforward cell / mc cell -> warning — and the four
scaffold literals migrate onto the shared macro (output bytes
unchanged). The always-on record summary line gains the bare `aura: `
namespace it was missing; bare-prefix stays the vocabulary for error
lines accompanying a non-zero exit (C14 partition) and plain info
lines. Exit codes unchanged throughout.

Verification: RED->GREEN warning pin on the per-cell-fault e2e; new
gate-emptied e2e (impossible n_trades threshold) pins the note marker,
exit 0, and the summary-line prefix; scaffold e2es and the full
workspace suite green; clippy -D warnings clean on aura-cli. The C28
shell-module roster test gained the diag.rs entry.

Known residue for the next slices: aura-runner literals (stale-dylib
warning, three tap/no-nominee notes) and the #313 zero-trade notice
follow in tasks 3-5. The verb_sugar warning call sites remain e2e-
uncovered (a hostless walkforward fault fixture is structurally
unreachable — single-instrument validation refuses before the cell
loop); their grammar is pinned via the shared macro and the campaign
warning e2e.

refs #278
This commit is contained in:
2026-07-23 22:52:46 +02:00
parent cfc2d9f5b1
commit 34987be389
7 changed files with 98 additions and 12 deletions
+5 -5
View File
@@ -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 {
+20
View File
@@ -0,0 +1,20 @@
//! Stable stderr class markers (C14): `aura: note: <text>` for benign
//! diagnostics on a continuing run (exit code unaffected),
//! `aura: warning: <text>` 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};
+1
View File
@@ -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;
+5 -5
View File
@@ -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)"
);
}
}
+2 -2
View File
@@ -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);
}
+64
View File
@@ -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
@@ -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",