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:
@@ -315,8 +315,8 @@ fn present_campaign(
|
|||||||
for cell in &outcome.record.cells {
|
for cell in &outcome.record.cells {
|
||||||
for (stage_ix, st) in cell.stages.iter().enumerate() {
|
for (stage_ix, st) in cell.stages.iter().enumerate() {
|
||||||
if matches!(&st.survivor_ordinals, Some(v) if v.is_empty()) {
|
if matches!(&st.survivor_ordinals, Some(v) if v.is_empty()) {
|
||||||
eprintln!(
|
crate::diag::note!(
|
||||||
"aura: cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
|
"cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
|
||||||
survivors; cell realization truncated",
|
survivors; cell realization truncated",
|
||||||
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1
|
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 {
|
if let Some(f) = &cell.fault {
|
||||||
failed += 1;
|
failed += 1;
|
||||||
fail_labels.push(format!("{}: {}", cell.instrument, cell_fault_kind_label(f.kind)));
|
fail_labels.push(format!("{}: {}", cell.instrument, cell_fault_kind_label(f.kind)));
|
||||||
eprintln!(
|
crate::diag::warning!(
|
||||||
"aura: cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
|
"cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
|
||||||
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1, f.stage, f.detail
|
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")
|
.expect("campaign run record serializes")
|
||||||
);
|
);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"campaign run {} recorded: {} cells{}",
|
"aura: campaign run {} recorded: {} cells{}",
|
||||||
outcome.record.run,
|
outcome.record.run,
|
||||||
outcome.record.cells.len(),
|
outcome.record.cells.len(),
|
||||||
if failed == 0 {
|
if failed == 0 {
|
||||||
|
|||||||
@@ -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};
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
//! (or the `--strategy r-sma` sugar) and print canonical JSON metrics/manifests;
|
//! (or the `--strategy r-sma` sugar) and print canonical JSON metrics/manifests;
|
||||||
//! this binary authors no built-in harness.
|
//! this binary authors no built-in harness.
|
||||||
|
|
||||||
|
mod diag;
|
||||||
mod render;
|
mod render;
|
||||||
mod graph_construct;
|
mod graph_construct;
|
||||||
mod campaign_run;
|
mod campaign_run;
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
|
|||||||
Ok(o) if o.status.success() => {
|
Ok(o) if o.status.success() => {
|
||||||
commit_all(&spec.target_dir, "aura new scaffold");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ pub fn scaffold_node_crate(spec: &ScaffoldSpec) -> Result<(), String> {
|
|||||||
Ok(o) if o.status.success() => {
|
Ok(o) if o.status.success() => {
|
||||||
commit_all(&spec.target_dir, "aura nodes new scaffold");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,7 @@ fn commit_all(dir: &std::path::Path, message: &str) {
|
|||||||
.current_dir(dir)
|
.current_dir(dir)
|
||||||
.output();
|
.output();
|
||||||
if !matches!(add, Ok(o) if o.status.success()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
let commit = std::process::Command::new("git")
|
let commit = std::process::Command::new("git")
|
||||||
@@ -374,8 +374,8 @@ fn commit_all(dir: &std::path::Path, message: &str) {
|
|||||||
.current_dir(dir)
|
.current_dir(dir)
|
||||||
.output();
|
.output();
|
||||||
if !matches!(commit, Ok(o) if o.status.success()) {
|
if !matches!(commit, Ok(o) if o.status.success()) {
|
||||||
eprintln!(
|
crate::diag::warning!(
|
||||||
"aura: warning: git commit failed (scaffold created without an initial commit)"
|
"git commit failed (scaffold created without an initial commit)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
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);
|
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()) {
|
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);
|
return Ok(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1452,6 +1452,19 @@ const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
|||||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
"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
|
/// The full annotated shape for the gated e2e: sweep -> gate -> walk_forward
|
||||||
/// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so
|
/// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so
|
||||||
/// walk-forward always has survivors; the mc annotator bootstraps the pooled
|
/// 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_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[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!(!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
|
/// 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,
|
modules,
|
||||||
[
|
[
|
||||||
"campaign_run.rs",
|
"campaign_run.rs",
|
||||||
|
"diag.rs",
|
||||||
"graph_construct.rs",
|
"graph_construct.rs",
|
||||||
"main.rs",
|
"main.rs",
|
||||||
"render.rs",
|
"render.rs",
|
||||||
|
|||||||
Reference in New Issue
Block a user