feat(runner, cli): runner-side marker literals + the zero-trade walk-forward notice
Iteration stderr-markers-2, tasks 3-4 of the stderr-class-markers plan. aura-runner's four continuing-run diagnostics join the class grammar as hand-written literals (the #295 boundary keeps the macro owner in the presentation crate): the stale-dylib warning gains the `aura: ` namespace, and the three tap/no-nominee notices become `aura: note: ` — the latter two extracted into pure note-builder fns with unit pins. The two e2e pins on the cost-model tap string moved with the retag (research_docs.rs). A new project_nodes e2e exercises the real stale- dylib load path: fresh build emits nothing, a source newer than the dylib emits exactly one class-marked warning and still succeeds. The #313 notice ships on both walk-forward paths: `aura: note: all N walk-forward windows recorded zero trades` before the summary JSON, exit 0 untouched (a null result is a valid research result, #198). Wording and condition live in one shared diag helper taking the per-window trade counts (orchestrator tidy of the reviewer's duplicated-predicate residue); the call sites keep only the path-specific field extraction. Three e2es pin it: the equal-length degenerate on the synthetic path (exactly one note, no warnings, exit 0, summary unchanged), a traded negative twin, and the real/sugar-path degenerate gated on the GER40 archive. refs #278, refs #313
This commit is contained in:
@@ -18,3 +18,15 @@ macro_rules! warning {
|
||||
}
|
||||
|
||||
pub(crate) use {note, warning};
|
||||
|
||||
/// The #313 zero-trade note, shared by both walk-forward paths (the
|
||||
/// synthetic-blueprint path in `main.rs` and the `--real` sugar path in
|
||||
/// `verb_sugar.rs`) so the wording AND the condition live in exactly one
|
||||
/// place. Takes the per-window trade counts; a no-op unless there is at
|
||||
/// least one window and every one of them traded zero times.
|
||||
pub(crate) fn note_zero_trade_windows(mut window_trades: impl ExactSizeIterator<Item = u64>) {
|
||||
let n_windows = window_trades.len();
|
||||
if n_windows > 0 && window_trades.all(|n| n == 0) {
|
||||
note!("all {n_windows} walk-forward windows recorded zero trades");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,6 +917,12 @@ fn run_blueprint_walkforward(
|
||||
for w in &result.windows {
|
||||
println!("{}", family_member_line(&id, &w.run.oos_report));
|
||||
}
|
||||
crate::diag::note_zero_trade_windows(
|
||||
result
|
||||
.windows
|
||||
.iter()
|
||||
.map(|w| w.run.oos_report.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
|
||||
);
|
||||
println!("{}", walkforward_summary_json(&result));
|
||||
}
|
||||
|
||||
|
||||
@@ -549,6 +549,9 @@ pub(crate) fn run_walkforward_sugar(
|
||||
summary_axes.push("stop_length".to_string());
|
||||
summary_axes.push("stop_k".to_string());
|
||||
}
|
||||
crate::diag::note_zero_trade_windows(
|
||||
wf.reports.iter().map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
|
||||
);
|
||||
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports, &summary_axes));
|
||||
|
||||
// Trace persistence runs LAST (mirroring `present_campaign`'s ordering,
|
||||
|
||||
@@ -4782,6 +4782,101 @@ fn aura_walkforward_over_a_blueprint_persists_a_family() {
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#313, Path 2 — synthetic): a walk-forward whose every window records
|
||||
/// zero trades emits exactly one `aura: note: all N walk-forward windows
|
||||
/// recorded zero trades` on stderr; exit stays 0 and the summary JSON is
|
||||
/// unchanged (a null result is a valid research result, #198). Equal
|
||||
/// fast/slow lengths make the SMA difference constantly zero, so the bias
|
||||
/// never leaves 0 and no window trades.
|
||||
#[test]
|
||||
fn aura_walkforward_all_zero_trade_windows_emit_one_note() {
|
||||
let cwd = temp_cwd("blueprint-walkforward-zero-trade-note");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=8",
|
||||
"--axis", "sma_signal.slow.length=8", "--name", "wfzero"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (degenerate)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
assert_eq!(out.status.code(), Some(0), "exit stays 0: {stderr}");
|
||||
assert!(stdout.contains("\"walkforward\""), "summary line unchanged: {stdout}");
|
||||
assert_eq!(
|
||||
stderr.matches("walk-forward windows recorded zero trades").count(),
|
||||
1,
|
||||
"exactly one zero-trade note: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("aura: note: all "),
|
||||
"the note carries the class marker: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("aura: warning: "),
|
||||
"a benign degenerate run emits no warning lines: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#313 negative twin): a walk-forward with at least one traded window
|
||||
/// emits no zero-trade note.
|
||||
#[test]
|
||||
fn aura_walkforward_with_trades_emits_no_zero_trade_note() {
|
||||
let cwd = temp_cwd("blueprint-walkforward-traded-no-note");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3",
|
||||
"--axis", "sma_signal.slow.length=4,6", "--name", "wftrades"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (trading)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert_eq!(out.status.code(), Some(0), "exit 0: {stderr}");
|
||||
assert!(
|
||||
!stderr.contains("recorded zero trades"),
|
||||
"a traded run emits no zero-trade note: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#313, Path 1 — the real/sugar walk-forward): the degenerate
|
||||
/// equal-length config over the shared GER40 archive emits the same
|
||||
/// zero-trade note. Gated on the archive; skips cleanly on a data refusal.
|
||||
#[test]
|
||||
fn walkforward_real_all_zero_trade_windows_emit_the_note() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let (cwd, _g) = fresh_project();
|
||||
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=5", "--axis", "sma_signal.slow.length=5",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (real degenerate)");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert_eq!(
|
||||
stderr.matches("walk-forward windows recorded zero trades").count(),
|
||||
1,
|
||||
"exactly one zero-trade note on the real path: {stderr}"
|
||||
);
|
||||
assert!(stderr.contains("aura: note: all "), "class marker: {stderr}");
|
||||
}
|
||||
|
||||
/// E2E (#173): a CLOSED blueprint with no --axis (nothing to re-fit) is a usage
|
||||
/// error (exit 2), naming that >= 1 --axis is required.
|
||||
#[test]
|
||||
|
||||
@@ -212,6 +212,58 @@ fn attached_crate_namespace_resolves_after_build() {
|
||||
);
|
||||
}
|
||||
|
||||
/// E2E (#278): loading an attached node crate whose `src/` is newer than its
|
||||
/// built dylib emits exactly one `aura: warning: ` class-marked line naming
|
||||
/// the stale dylib, and still succeeds (the run proceeds with the existing
|
||||
/// dylib rather than refusing) — staleness is a continuing-run warning, never
|
||||
/// a fault. A freshly built dylib (no touched source since) emits none. Real
|
||||
/// `cargo build` + a real `libloading::Library::new` load, exercising
|
||||
/// `aura_runner::project::load_crate`'s actual call site rather than the pure
|
||||
/// `stale_warning` helper alone.
|
||||
#[test]
|
||||
fn attached_crate_load_warns_when_source_outruns_the_built_dylib() {
|
||||
let cwd = temp_cwd("stale-warn");
|
||||
assert!(aura(&["new", "lab"], &cwd).status.success());
|
||||
let proj = cwd.join("lab");
|
||||
let engine = env!("CARGO_MANIFEST_DIR").to_string() + "/../..";
|
||||
assert!(aura(&["nodes", "new", "lab-nodes", "--engine-path", &engine], &proj).status.success());
|
||||
let crate_root = cwd.join("lab-nodes");
|
||||
let build = Command::new("cargo").arg("build").current_dir(&crate_root).output().expect("cargo build");
|
||||
assert!(build.status.success(), "{}", String::from_utf8_lossy(&build.stderr));
|
||||
|
||||
// Fresh build, untouched source: no staleness warning.
|
||||
let fresh = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(fresh.status.success(), "stderr: {}", String::from_utf8_lossy(&fresh.stderr));
|
||||
assert!(
|
||||
!String::from_utf8_lossy(&fresh.stderr).contains("may be stale"),
|
||||
"a freshly built dylib is not stale: {}",
|
||||
String::from_utf8_lossy(&fresh.stderr)
|
||||
);
|
||||
|
||||
// Bump `src/lib.rs`'s mtime a full minute past "now" (comfortably past the
|
||||
// dylib's just-completed build time) without rebuilding — the exact
|
||||
// "source outran the dylib, no rebuild yet" shape `stale_warning` guards.
|
||||
let lib_rs = crate_root.join("src/lib.rs");
|
||||
let f = std::fs::File::open(&lib_rs).expect("open lib.rs");
|
||||
f.set_modified(std::time::SystemTime::now() + std::time::Duration::from_secs(60))
|
||||
.expect("bump lib.rs mtime past the dylib's build time");
|
||||
|
||||
let stale = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(
|
||||
stale.status.success(),
|
||||
"staleness is a warning, not a refusal: {}",
|
||||
String::from_utf8_lossy(&stale.stderr)
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&stale.stderr);
|
||||
assert_eq!(
|
||||
stderr.matches("may be stale").count(),
|
||||
1,
|
||||
"exactly one staleness warning: {stderr}"
|
||||
);
|
||||
assert!(stderr.contains("aura: warning: "), "carries the warning class marker (#278): {stderr}");
|
||||
assert!(stderr.contains("run `cargo build` to refresh"), "names the remedy: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (#316 self-description): the scaffold's own seed node ships a
|
||||
/// `doc:` line that itself passes the C29 shape gate (`aura_core::doc_gate`)
|
||||
/// — non-empty and not a bare restatement of the type name. This exact text
|
||||
|
||||
@@ -3747,7 +3747,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
|
||||
|
||||
assert!(
|
||||
out.contains(
|
||||
"aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
"aura: note: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
),
|
||||
"the unproducible tap gets a named, one-time note with the remedy: {out}"
|
||||
);
|
||||
@@ -3850,7 +3850,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||
assert_eq!(code_gross, Some(0), "cost-less baseline run: {out_gross}");
|
||||
assert!(
|
||||
out_gross.contains(
|
||||
"aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
"aura: note: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
),
|
||||
"the cost-less run skips the net tap with the remedy: {out_gross}"
|
||||
);
|
||||
|
||||
@@ -462,7 +462,7 @@ fn stale_warning(
|
||||
) -> Option<String> {
|
||||
if source_mtime > dylib_mtime {
|
||||
Some(format!(
|
||||
"warning: {} may be stale — dylib built {} but a project source \
|
||||
"aura: warning: {} may be stale — dylib built {} but a project source \
|
||||
file was modified {} (run `cargo build` to refresh); proceeding \
|
||||
with the existing dylib",
|
||||
dylib_path.display(),
|
||||
@@ -916,6 +916,7 @@ mod tests {
|
||||
let source_mtime = UNIX_EPOCH + Duration::from_secs(2_003_702_400); // 2033-06-30
|
||||
let path = PathBuf::from("/tmp/x.so");
|
||||
let msg = stale_warning(&path, dylib_mtime, source_mtime).expect("source is newer");
|
||||
assert!(msg.starts_with("aura: warning: "), "carries the warning class marker (#278): {msg}");
|
||||
assert!(msg.contains("2001"), "names the dylib's mtime: {msg}");
|
||||
assert!(msg.contains("2033"), "names the source's newer mtime: {msg}");
|
||||
}
|
||||
|
||||
@@ -259,6 +259,28 @@ pub fn tap_channel(tap: &str) -> Option<TapChannel> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The note text for a requested tap outside the closed vocabulary (#278):
|
||||
/// pure over just the tap name, so the exact marker-bearing text
|
||||
/// `persist_campaign_traces` writes to stderr is unit-testable without
|
||||
/// booting a real archive run (the `tap_channel` `None` arm that reaches
|
||||
/// this is defensive — `validate_campaign` already refuses an unknown tap
|
||||
/// name before this function ever runs).
|
||||
fn tap_not_produced_note(tap: &str) -> String {
|
||||
format!("aura: note: tap \"{tap}\" is not produced by this run; skipped")
|
||||
}
|
||||
|
||||
/// The note text for a cell with neither a nominee nor a non-empty terminal
|
||||
/// family (#278): pure over the record's own cell fields, so the exact
|
||||
/// marker-bearing text `persist_campaign_traces` writes to stderr is
|
||||
/// unit-testable without booting a real archive run — the `cell_member_fanout`
|
||||
/// twin above already covers the emptiness logic itself.
|
||||
fn no_nominee_note(strategy: &str, instrument: &str, window_ms: (i64, i64)) -> String {
|
||||
format!(
|
||||
"aura: note: cell {strategy}/{instrument}/[{}, {}]: no nominee; no traces persisted",
|
||||
window_ms.0, window_ms.1
|
||||
)
|
||||
}
|
||||
|
||||
/// The content-derived on-disk key of one campaign cell under
|
||||
/// `traces/<trace_name>/` — the `member_key` discipline (content, never a
|
||||
/// runtime ordinal): strategy content-id prefix + instrument +
|
||||
@@ -368,10 +390,10 @@ pub fn persist_campaign_traces(
|
||||
for tap in taps {
|
||||
match tap_channel(tap) {
|
||||
Some(TapChannel::Net) if campaign.cost.is_empty() => eprintln!(
|
||||
"aura: tap \"{tap}\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
"aura: note: tap \"{tap}\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
),
|
||||
Some(ch) => routed.push((tap.as_str(), ch)),
|
||||
None => eprintln!("aura: tap \"{tap}\" is not produced by this run; skipped"),
|
||||
None => eprintln!("{}", tap_not_produced_note(tap)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,8 +410,8 @@ pub fn persist_campaign_traces(
|
||||
let members = cell_member_fanout(cell_out);
|
||||
if members.is_empty() {
|
||||
eprintln!(
|
||||
"aura: cell {}/{}/[{}, {}]: no nominee; no traces persisted",
|
||||
cell_rec.strategy, cell_rec.instrument, cell_rec.window_ms.0, cell_rec.window_ms.1
|
||||
"{}",
|
||||
no_nominee_note(&cell_rec.strategy, &cell_rec.instrument, cell_rec.window_ms)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -753,4 +775,27 @@ mod tests {
|
||||
assert_eq!(tap_channel("net_r_equity"), Some(TapChannel::Net));
|
||||
assert_eq!(tap_channel("bogus"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #278: the unrecognized-tap note carries the `note` class marker AND
|
||||
/// names the offending tap — the exact text `persist_campaign_traces`
|
||||
/// writes to stderr on `tap_channel`'s defensive `None` arm.
|
||||
fn tap_not_produced_note_carries_the_note_marker_and_names_the_tap() {
|
||||
let msg = tap_not_produced_note("bogus");
|
||||
assert!(msg.starts_with("aura: note: "), "carries the note class marker: {msg}");
|
||||
assert!(msg.contains("\"bogus\""), "names the offending tap: {msg}");
|
||||
assert!(msg.contains("is not produced by this run"), "states the reason: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #278: the no-candidate-cell note carries the `note` class marker AND
|
||||
/// names the cell (strategy/instrument/window) — the exact text
|
||||
/// `persist_campaign_traces` writes to stderr when `cell_member_fanout`
|
||||
/// (unit-tested above) reports no nominee and no non-empty family.
|
||||
fn no_nominee_note_carries_the_note_marker_and_names_the_cell() {
|
||||
let msg = no_nominee_note("bb34aa55", "GER40", (1_000, 2_000));
|
||||
assert!(msg.starts_with("aura: note: "), "carries the note class marker: {msg}");
|
||||
assert!(msg.contains("bb34aa55/GER40/[1000, 2000]"), "names the cell: {msg}");
|
||||
assert!(msg.contains("no nominee; no traces persisted"), "states the reason: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user