Compare commits
11 Commits
cfc2d9f5b1
...
6fb7caf929
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fb7caf929 | |||
| 48979193b9 | |||
| a20b6f6003 | |||
| 59f547e313 | |||
| 51096a3212 | |||
| bd6ddf9efe | |||
| 7e1dd25a76 | |||
| cc683fb065 | |||
| 913d18755e | |||
| 1102d776df | |||
| 34987be389 |
@@ -830,6 +830,19 @@ fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
|
|||||||
.unwrap_or_else(|| "member panicked (non-string payload)".to_string())
|
.unwrap_or_else(|| "member panicked (non-string payload)".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `SilencedPanic` + `catch_unwind` + `panic_message` recipe above
|
||||||
|
/// (this module's three call sites), exposed for reuse by a member-run caller
|
||||||
|
/// OUTSIDE this crate that drives a bare member run without going through
|
||||||
|
/// `execute` (#278: the synthetic blueprint sweep/walk-forward family
|
||||||
|
/// builders in `aura-runner`, which share this exact bare-member seam but
|
||||||
|
/// carry no #272 fault boundary of their own). Returns the panic payload's
|
||||||
|
/// best-effort message on containment; `AssertUnwindSafe` is the same
|
||||||
|
/// judgement documented on `panic_message` above.
|
||||||
|
pub fn catch_member_panic<R>(f: impl FnOnce() -> R) -> Result<R, String> {
|
||||||
|
let _silence = SilencedPanic::enter();
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_err(panic_message)
|
||||||
|
}
|
||||||
|
|
||||||
/// Assemble the (CellOutcome, CellRealization) pair for a cell that failed at a
|
/// Assemble the (CellOutcome, CellRealization) pair for a cell that failed at a
|
||||||
/// stage (#272): the realized stage prefix survives in `stages`, `families`/
|
/// stage (#272): the realized stage prefix survives in `stages`, `families`/
|
||||||
/// `selections` carry whatever already-persisted stages produced (empty for a
|
/// `selections` carry whatever already-persisted stages produced (empty for a
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
mod exec;
|
mod exec;
|
||||||
pub use exec::{
|
pub use exec::{
|
||||||
execute, member_fault_prose, CampaignOutcome, CellOutcome, StageFamily, StageSelectionOut,
|
catch_member_panic, execute, member_fault_prose, CampaignOutcome, CellOutcome, StageFamily,
|
||||||
|
StageSelectionOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|||||||
@@ -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,65 @@
|
|||||||
|
//! 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};
|
||||||
|
|
||||||
|
/// The zero-trade note's message text (#313), pluralization-aware — the
|
||||||
|
/// milestone fieldtest tripped over "all 1 walk-forward windows"
|
||||||
|
/// (captured: `fieldtests/milestone-stderr-honesty/captured/
|
||||||
|
/// example2_singular.err`). Pure over the count so the wording is
|
||||||
|
/// unit-pinned.
|
||||||
|
fn zero_trade_note_text(n_windows: usize) -> String {
|
||||||
|
if n_windows == 1 {
|
||||||
|
"the single walk-forward window recorded zero trades".to_string()
|
||||||
|
} else {
|
||||||
|
format!("all {n_windows} walk-forward windows recorded zero trades")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!("{}", zero_trade_note_text(n_windows));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::zero_trade_note_text;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// #278 fieldtest friction: the single-window form must read as
|
||||||
|
/// grammatical English while the plural form keeps the exact phrase
|
||||||
|
/// the e2e pins grep for.
|
||||||
|
fn zero_trade_note_pluralizes() {
|
||||||
|
assert_eq!(
|
||||||
|
zero_trade_note_text(1),
|
||||||
|
"the single walk-forward window recorded zero trades"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
zero_trade_note_text(3),
|
||||||
|
"all 3 walk-forward windows recorded zero trades"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -916,6 +917,12 @@ fn run_blueprint_walkforward(
|
|||||||
for w in &result.windows {
|
for w in &result.windows {
|
||||||
println!("{}", family_member_line(&id, &w.run.oos_report));
|
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));
|
println!("{}", walkforward_summary_json(&result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +549,9 @@ pub(crate) fn run_walkforward_sugar(
|
|||||||
summary_axes.push("stop_length".to_string());
|
summary_axes.push("stop_length".to_string());
|
||||||
summary_axes.push("stop_k".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));
|
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports, &summary_axes));
|
||||||
|
|
||||||
// Trace persistence runs LAST (mirroring `present_campaign`'s ordering,
|
// Trace persistence runs LAST (mirroring `present_campaign`'s ordering,
|
||||||
@@ -690,7 +693,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4782,6 +4782,234 @@ fn aura_walkforward_over_a_blueprint_persists_a_family() {
|
|||||||
let _ = std::fs::remove_dir_all(&cwd);
|
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 (#278 decision: same fault class, same verb, same surface => same
|
||||||
|
/// treatment): a member fault on the SYNTHETIC walk-forward path is CONTAINED
|
||||||
|
/// exactly like the real/campaign path contains it (#272) — it surfaces as an
|
||||||
|
/// `aura: warning: ` class-marked stderr line naming the member fault and the
|
||||||
|
/// process takes the deliberate failed-cells exit 3 (C14 partition,
|
||||||
|
/// `exit_on_campaign_result` precedent). It never escapes as an uncaught Rust
|
||||||
|
/// panic: no exit 101, no `panicked at`, no internal `crates/` source path on
|
||||||
|
/// the consumer's stderr.
|
||||||
|
#[test]
|
||||||
|
fn aura_walkforward_synthetic_member_panic_is_contained() {
|
||||||
|
let cwd = temp_cwd("blueprint-walkforward-member-panic-contained");
|
||||||
|
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
// length=0 poisons the member: `Sma::new` asserts "SMA length must be >= 1"
|
||||||
|
// during member compile — the same fault class the real path records as
|
||||||
|
// "a member panicked: …" and survives.
|
||||||
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=0", "--name", "wfpanic"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura walkforward (poison member)");
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||||
|
assert!(
|
||||||
|
stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||||||
|
"the member fault surfaces as one class-marked warning naming the fault: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("panicked at"),
|
||||||
|
"no raw panic report reaches the consumer: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("crates/"),
|
||||||
|
"no internal source path leaks to the consumer: {stderr}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(),
|
||||||
|
Some(3),
|
||||||
|
"a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// E2E (#278 decision, sweep twin): the synthetic `aura sweep` shares the
|
||||||
|
/// same bare-member seam (`run_blueprint_member` inside the family builders'
|
||||||
|
/// sweep closures, no #272 fault boundary), so the same poison must be
|
||||||
|
/// contained the same way — warning + exit 3, no panic escape.
|
||||||
|
#[test]
|
||||||
|
fn aura_sweep_synthetic_member_panic_is_contained() {
|
||||||
|
let cwd = temp_cwd("blueprint-sweep-member-panic-contained");
|
||||||
|
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args(["sweep", &bp, "--axis", "sma_signal.fast.length=0", "--name", "swpanic"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura sweep (poison member)");
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||||
|
assert!(
|
||||||
|
stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||||||
|
"the member fault surfaces as one class-marked warning naming the fault: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("panicked at") && !stderr.contains("crates/"),
|
||||||
|
"no raw panic report or internal source path leaks: {stderr}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(),
|
||||||
|
Some(3),
|
||||||
|
"a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// E2E (#278 decision, mc twin): the synthetic `aura mc` shares the same
|
||||||
|
/// bare-member seam as its sweep/walkforward siblings (`run_blueprint_member`
|
||||||
|
/// inside `blueprint_mc_family`'s per-seed draw closure, no #272 fault
|
||||||
|
/// boundary), so the same member-compile fault class must be contained the
|
||||||
|
/// same way — one `aura: warning: ` line naming the fault, no raw panic text
|
||||||
|
/// or internal `crates/` path on stderr, deliberate exit 3 (C14), never 101.
|
||||||
|
/// mc takes only CLOSED blueprints, so the poison is BOUND (`length=0` on the
|
||||||
|
/// SMA add-op) rather than a poison axis: the test builds its minimal 4-op
|
||||||
|
/// poison blueprint inline via `aura graph build`. Two seeds so the parallel
|
||||||
|
/// draws join before the lowest-seed fault resolves (the shipped sweep
|
||||||
|
/// contract's thread-order-independent shape, C1).
|
||||||
|
#[test]
|
||||||
|
fn aura_mc_synthetic_member_panic_is_contained() {
|
||||||
|
let cwd = temp_cwd("blueprint-mc-member-panic-contained");
|
||||||
|
// Minimal poison: each op is load-bearing (source: the close role; add+bind:
|
||||||
|
// the length=0 that trips `Sma::new`'s "SMA length must be >= 1" assert at
|
||||||
|
// member compile; feed: wiring so build validation passes; expose: the bias
|
||||||
|
// contract). Bound length keeps the blueprint CLOSED, as `aura mc` requires.
|
||||||
|
let ops = r#"[
|
||||||
|
{"op":"source","role":"close","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":0}}},
|
||||||
|
{"op":"feed","role":"close","into":["sma.series"]},
|
||||||
|
{"op":"expose","from":"sma.value","as":"bias"}
|
||||||
|
]"#;
|
||||||
|
let ops_path = cwd.join("poison.ops.json");
|
||||||
|
std::fs::write(&ops_path, ops).expect("write poison op-list");
|
||||||
|
let bp_path = cwd.join("poison.bp.json");
|
||||||
|
let build = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args(["graph", "build"])
|
||||||
|
.stdin(std::fs::File::open(&ops_path).expect("open poison op-list"))
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura graph build (poison blueprint)");
|
||||||
|
assert_eq!(
|
||||||
|
build.status.code(),
|
||||||
|
Some(0),
|
||||||
|
"poison blueprint builds (the fault is a member-COMPILE fault, not a build refusal): {}",
|
||||||
|
String::from_utf8_lossy(&build.stderr)
|
||||||
|
);
|
||||||
|
std::fs::write(&bp_path, &build.stdout).expect("write poison blueprint");
|
||||||
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||||
|
.args(["mc", bp_path.to_str().expect("utf-8 path"), "--seeds", "2"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura mc (poison member)");
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||||
|
assert!(
|
||||||
|
stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||||||
|
"the member fault surfaces as one class-marked warning naming the fault: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("panicked at") && !stderr.contains("crates/"),
|
||||||
|
"no raw panic report or internal source path leaks: {stderr}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(),
|
||||||
|
Some(3),
|
||||||
|
"a contained member fault is the deliberate failed-cells exit 3, never 101: {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
|
/// E2E (#173): a CLOSED blueprint with no --axis (nothing to re-fit) is a usage
|
||||||
/// error (exit 2), naming that >= 1 --axis is required.
|
/// error (exit 2), naming that >= 1 --axis is required.
|
||||||
#[test]
|
#[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
|
/// 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`)
|
/// `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
|
/// — non-empty and not a bare restatement of the type name. This exact text
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -3683,7 +3747,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
out.contains(
|
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}"
|
"the unproducible tap gets a named, one-time note with the remedy: {out}"
|
||||||
);
|
);
|
||||||
@@ -3786,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_eq!(code_gross, Some(0), "cost-less baseline run: {out_gross}");
|
||||||
assert!(
|
assert!(
|
||||||
out_gross.contains(
|
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}"
|
"the cost-less run skips the net tap with the remedy: {out_gross}"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
//! CLI shell still needs its own for call sites outside any family builder).
|
//! CLI shell still needs its own for call sites outside any family builder).
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use aura_campaign::{catch_member_panic, member_fault_prose, MemberFault};
|
||||||
use aura_composites::StopRule;
|
use aura_composites::StopRule;
|
||||||
use aura_core::{Cell, ParamSpec, Scalar, Timestamp};
|
use aura_core::{Cell, ParamSpec, Scalar, Timestamp};
|
||||||
use aura_engine::{
|
use aura_engine::{
|
||||||
@@ -315,7 +316,10 @@ pub fn select_winner(
|
|||||||
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
|
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
|
||||||
/// run BEFORE this closure is invoked per grid point, so its body never influences the
|
/// run BEFORE this closure is invoked per grid point, so its body never influences the
|
||||||
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
|
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
|
||||||
/// satisfy the terminal, at O(1) cost per point instead of a full member run.
|
/// satisfy the terminal, at O(1) cost per point instead of a full member run. Reused
|
||||||
|
/// (#278) as the discarded slot value for a member run [`catch_member_panic`]
|
||||||
|
/// contains: the caller always resolves the captured fault and exits before this
|
||||||
|
/// value is ever rendered or persisted, so its content is equally irrelevant there.
|
||||||
fn axis_grid_probe_report() -> RunReport {
|
fn axis_grid_probe_report() -> RunReport {
|
||||||
RunReport {
|
RunReport {
|
||||||
manifest: RunManifest {
|
manifest: RunManifest {
|
||||||
@@ -334,6 +338,48 @@ fn axis_grid_probe_report() -> RunReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The LOWEST enumeration index's captured member-panic message, found by
|
||||||
|
/// walking `family.points` — guaranteed by `assemble_sweep`'s own
|
||||||
|
/// `collect()` to be in grid-enumeration order regardless of which worker
|
||||||
|
/// finished first (C1) — for the first point whose params match a captured
|
||||||
|
/// fault. Thread-order-independent by construction (mirrors the lowest-
|
||||||
|
/// index-fault convention `aura-campaign::exec::run_members` establishes for
|
||||||
|
/// the identical #272 seam), without needing a separate numeric-index
|
||||||
|
/// side-channel: the returned family already carries the points in order.
|
||||||
|
fn lowest_point_fault(family: &SweepFamily, faults: Mutex<Vec<(Vec<Cell>, String)>>) -> Option<String> {
|
||||||
|
let captured = faults.into_inner().expect("fault capture lock");
|
||||||
|
family
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.find_map(|pt| captured.iter().find(|(p, _)| *p == pt.params).map(|(_, m)| m.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The LOWEST seed's captured member-panic message (#278), found by walking
|
||||||
|
/// `family.draws` — `monte_carlo`'s own contract guarantees seed-**input**
|
||||||
|
/// order regardless of thread completion (C1) — for the first draw whose
|
||||||
|
/// seed matches a captured fault. Thread-order-independent by construction,
|
||||||
|
/// mirroring [`lowest_point_fault`]'s and `lowest_window_fault`'s identical
|
||||||
|
/// convention one level over (per-point sweep / per-window walk-forward).
|
||||||
|
fn lowest_seed_fault(family: &McFamily, faults: Mutex<Vec<(u64, String)>>) -> Option<String> {
|
||||||
|
let captured = faults.into_inner().expect("fault capture lock");
|
||||||
|
family
|
||||||
|
.draws
|
||||||
|
.iter()
|
||||||
|
.find_map(|d| captured.iter().find(|(s, _)| *s == d.seed).map(|(_, m)| m.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render + exit(3) on a contained member panic (#278: the deliberate
|
||||||
|
/// failed-cells exit, C14, never the raw 101 an uncontained panic would
|
||||||
|
/// yield) — reuses `aura_campaign::member_fault_prose`'s established
|
||||||
|
/// "a member panicked: …" wording rather than re-wording it, and the
|
||||||
|
/// `aura: warning: ` class marker (#278 decision) the CLI's own `diag`
|
||||||
|
/// macro would emit, hand-written here since that macro is presentation-
|
||||||
|
/// crate-private (#295 boundary) and this fires from `aura-runner`.
|
||||||
|
fn exit_on_member_panic(msg: &str) -> ! {
|
||||||
|
eprintln!("aura: warning: {}", member_fault_prose(&MemberFault::Panic(msg.to_string())));
|
||||||
|
std::process::exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
|
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
|
||||||
/// member (#253). Reuses the SAME strict, erroring axis-name check
|
/// member (#253). Reuses the SAME strict, erroring axis-name check
|
||||||
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
|
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
|
||||||
@@ -426,15 +472,35 @@ pub fn blueprint_sweep_family(
|
|||||||
for (n, vals) in iter {
|
for (n, vals) in iter {
|
||||||
binder = binder.axis(n, vals.clone());
|
binder = binder.axis(n, vals.clone());
|
||||||
}
|
}
|
||||||
binder
|
// #278: `run_blueprint_member` runs bare here (no #272 fault boundary of its
|
||||||
|
// own), so a member-compile panic (e.g. an `Sma::new` length assert) would
|
||||||
|
// otherwise unwind straight through this sweep to an uncaught exit 101 —
|
||||||
|
// contained the same way the real/campaign path contains it, via
|
||||||
|
// `catch_member_panic` + a per-point capture, resolved to the lowest
|
||||||
|
// enumeration index's message after the sweep joins (thread-order-
|
||||||
|
// independent, C1).
|
||||||
|
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
|
||||||
|
let family = binder
|
||||||
.sweep(|point| {
|
.sweep(|point| {
|
||||||
// fresh per-member graph (Composite is !Clone, reload per member) run through
|
// fresh per-member graph (Composite is !Clone, reload per member) run through
|
||||||
// the shared reduce-mode member path — the same fn reproduction re-runs.
|
// the shared reduce-mode member path — the same fn reproduction re-runs.
|
||||||
run_blueprint_member(reload(doc), point, &space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
match catch_member_panic(|| {
|
||||||
|
run_blueprint_member(reload(doc), point, &space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||||
|
}) {
|
||||||
|
Ok(report) => report,
|
||||||
|
Err(msg) => {
|
||||||
|
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
|
||||||
|
axis_grid_probe_report()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// render the sweep terminal's BindError to prose (#247), the fn's String error
|
// render the sweep terminal's BindError to prose (#247), the fn's String error
|
||||||
// contract — never the raw Debug struct.
|
// contract — never the raw Debug struct.
|
||||||
.map_err(|e| render_bind_error(&e))
|
.map_err(|e| render_bind_error(&e))?;
|
||||||
|
if let Some(msg) = lowest_point_fault(&family, faults) {
|
||||||
|
exit_on_member_panic(&msg);
|
||||||
|
}
|
||||||
|
Ok(family)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sweep the LOADED blueprint over the user `--axis` grid on an in-sample window
|
/// Sweep the LOADED blueprint over the user `--axis` grid on an in-sample window
|
||||||
@@ -447,10 +513,19 @@ pub fn blueprint_sweep_family(
|
|||||||
/// its walk-forward in-sample twin); an axis matching neither space is refused
|
/// its walk-forward in-sample twin); an axis matching neither space is refused
|
||||||
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
|
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
|
||||||
/// "fully bound; nothing to sweep" refusal) before any member runs.
|
/// "fully bound; nothing to sweep" refusal) before any member runs.
|
||||||
|
///
|
||||||
|
/// The returned `Option<String>` (#278) is this fn's OWN resolved member-panic
|
||||||
|
/// capture: this fn's nested sweep runs inside `blueprint_walkforward_family`'s
|
||||||
|
/// per-window parallel closure, so a contained member panic here must NOT
|
||||||
|
/// `eprintln!`+`exit` on the spot (that would race across windows, the exact
|
||||||
|
/// `#177` duplicated-rejection class the dispatch-boundary axis pre-flight
|
||||||
|
/// above already avoids) — captured internally, resolved to the lowest-index
|
||||||
|
/// message via [`lowest_point_fault`] before returning, for the caller to push
|
||||||
|
/// onto its own per-window fault list only after every window has joined.
|
||||||
pub fn blueprint_sweep_over(
|
pub fn blueprint_sweep_over(
|
||||||
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
|
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
|
||||||
env: &Env, binding: &ResolvedBinding,
|
env: &Env, binding: &ResolvedBinding,
|
||||||
) -> Result<(SweepFamily, Vec<usize>), BindError> {
|
) -> Result<(SweepFamily, Vec<usize>, Option<String>), BindError> {
|
||||||
let reload = |d: &str| {
|
let reload = |d: &str| {
|
||||||
blueprint_from_json(d, &|t| env.resolve(t))
|
blueprint_from_json(d, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||||
@@ -476,11 +551,22 @@ pub fn blueprint_sweep_over(
|
|||||||
for (n, vals) in iter {
|
for (n, vals) in iter {
|
||||||
binder = binder.axis(n, vals.clone());
|
binder = binder.axis(n, vals.clone());
|
||||||
}
|
}
|
||||||
binder.sweep_with_lattice(|point| {
|
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
|
||||||
|
let (family, lattice) = binder.sweep_with_lattice(|point| {
|
||||||
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
||||||
let window = window_of(&sources).expect("non-empty in-sample window");
|
let window = window_of(&sources).expect("non-empty in-sample window");
|
||||||
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
|
match catch_member_panic(|| {
|
||||||
})
|
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||||
|
}) {
|
||||||
|
Ok(report) => report,
|
||||||
|
Err(msg) => {
|
||||||
|
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
|
||||||
|
axis_grid_probe_report()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let fault = lowest_point_fault(&family, faults);
|
||||||
|
Ok((family, lattice, fault))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
|
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
|
||||||
@@ -509,6 +595,37 @@ pub fn run_oos_blueprint(
|
|||||||
(Vec::new(), report)
|
(Vec::new(), report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A discarded placeholder [`WindowRun`] for a window whose IS refit or OOS
|
||||||
|
/// run was contained after a member panic (#278): `chosen_params` matches
|
||||||
|
/// `space`'s arity with inert zero cells (the `assemble_walk_forward` arity
|
||||||
|
/// invariant), `oos_equity` is empty (an empty segment leaves the stitched
|
||||||
|
/// curve unbroken, same convention as a genuinely equity-less window), and
|
||||||
|
/// the report reuses [`axis_grid_probe_report`]'s zero-cost placeholder.
|
||||||
|
/// Never rendered: the caller always resolves the captured window fault and
|
||||||
|
/// exits before touching a faulted window's run.
|
||||||
|
fn placeholder_window_run(space: &[ParamSpec]) -> WindowRun {
|
||||||
|
WindowRun {
|
||||||
|
chosen_params: space.iter().map(|_| Cell::from_i64(0)).collect(),
|
||||||
|
oos_equity: Vec::new(),
|
||||||
|
oos_report: axis_grid_probe_report(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The LOWEST roll-order window's captured member-panic message (#278),
|
||||||
|
/// found by walking `result.windows` — guaranteed by `assemble_walk_forward`
|
||||||
|
/// to be in roll order regardless of thread completion (C1) — for the first
|
||||||
|
/// window whose bounds match a captured fault. Thread-order-independent by
|
||||||
|
/// construction, mirroring [`lowest_point_fault`]'s identical convention one
|
||||||
|
/// level down (per-point within one window's IS sweep) and
|
||||||
|
/// `aura-campaign::exec`'s own lowest-index walk-forward fault attribution.
|
||||||
|
fn lowest_window_fault(result: &WalkForwardResult, faults: Mutex<Vec<(WindowBounds, String)>>) -> Option<String> {
|
||||||
|
let captured = faults.into_inner().expect("fault capture lock");
|
||||||
|
result
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.find_map(|w| captured.iter().find(|(b, _)| *b == w.bounds).map(|(_, m)| m.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
|
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
|
||||||
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
|
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
|
||||||
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
|
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
|
||||||
@@ -568,18 +685,45 @@ pub fn blueprint_walkforward_family(
|
|||||||
eprintln!("aura: {}", render_bind_error(&e));
|
eprintln!("aura: {}", render_bind_error(&e));
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
walk_forward(roller, space.clone(), |w: WindowBounds| {
|
// #278: `blueprint_sweep_over`'s inner sweep and `run_oos_blueprint`'s single
|
||||||
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
|
// member run both drive `run_blueprint_member` bare, so either could
|
||||||
|
// otherwise unwind an uncaught member-compile panic straight through this
|
||||||
|
// window closure. Both are contained (`blueprint_sweep_over` resolves its
|
||||||
|
// own capture and returns it; `run_oos_blueprint` via `catch_member_panic`
|
||||||
|
// directly) and captured PER WINDOW here rather than printed on the spot —
|
||||||
|
// this closure runs across windows in parallel (`walk_forward`), so an
|
||||||
|
// inline `eprintln!`+`exit` would race the same way the axis pre-flight
|
||||||
|
// above was written to avoid (#177). The lowest roll-order window's fault
|
||||||
|
// is resolved once, after every window has joined.
|
||||||
|
let window_faults: Mutex<Vec<(WindowBounds, String)>> = Mutex::new(Vec::new());
|
||||||
|
let result = walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||||
|
let (is_family, lattice, is_fault) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
|
||||||
.expect("axes validated in the dispatch-boundary pre-flight");
|
.expect("axes validated in the dispatch-boundary pre-flight");
|
||||||
|
if let Some(msg) = is_fault {
|
||||||
|
window_faults.lock().expect("fault capture lock").push((w, msg));
|
||||||
|
return placeholder_window_run(&space);
|
||||||
|
}
|
||||||
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||||
};
|
};
|
||||||
let (oos_equity, mut oos_report) =
|
match catch_member_panic(|| {
|
||||||
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides);
|
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides)
|
||||||
oos_report.manifest.selection = Some(selection);
|
}) {
|
||||||
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
Ok((oos_equity, mut oos_report)) => {
|
||||||
})
|
oos_report.manifest.selection = Some(selection);
|
||||||
|
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
||||||
|
}
|
||||||
|
Err(msg) => {
|
||||||
|
window_faults.lock().expect("fault capture lock").push((w, msg));
|
||||||
|
placeholder_window_run(&space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(msg) = lowest_window_fault(&result, window_faults) {
|
||||||
|
exit_on_member_panic(&msg);
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — `blueprint_mc_family`'s
|
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — `blueprint_mc_family`'s
|
||||||
@@ -642,11 +786,35 @@ pub fn blueprint_mc_family(
|
|||||||
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
|
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
|
||||||
let seeds: Vec<u64> = (1..=n_seeds).collect();
|
let seeds: Vec<u64> = (1..=n_seeds).collect();
|
||||||
let base_point: Vec<Scalar> = Vec::new();
|
let base_point: Vec<Scalar> = Vec::new();
|
||||||
|
// #278: `run_blueprint_member` ran bare here, the one family builder without
|
||||||
|
// the #272 fault boundary its sweep/walk-forward siblings gained in 51096a3
|
||||||
|
// — a member-compile panic (e.g. an `Sma::new` length assert) would otherwise
|
||||||
|
// unwind straight through `monte_carlo`'s `run_indexed` to an uncaught exit
|
||||||
|
// 101. Contained the same way: `catch_member_panic` + a per-seed capture,
|
||||||
|
// resolved to the LOWEST seed's message after the join (thread-order-
|
||||||
|
// independent, C1) via `lowest_seed_fault`.
|
||||||
|
let faults: Mutex<Vec<(u64, String)>> = Mutex::new(Vec::new());
|
||||||
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
|
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
|
||||||
let sources = synthetic_walk_sources(seed);
|
let sources = synthetic_walk_sources(seed);
|
||||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||||
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
match catch_member_panic(|| {
|
||||||
|
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||||
|
}) {
|
||||||
|
Ok(report) => report,
|
||||||
|
Err(msg) => {
|
||||||
|
faults.lock().expect("fault capture lock").push((seed, msg));
|
||||||
|
axis_grid_probe_report()
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// The captured fault must win BEFORE the vacuous-mc guard below: a faulted
|
||||||
|
// draw's placeholder report is metrics-identical across every faulted seed,
|
||||||
|
// so a run with >= 2 faulted seeds (or one faulted + one real draw sharing
|
||||||
|
// its placeholder's zero metrics) could otherwise trip the vacuous refusal
|
||||||
|
// instead of surfacing the real member fault.
|
||||||
|
if let Some(msg) = lowest_seed_fault(&family, faults) {
|
||||||
|
exit_on_member_panic(&msg);
|
||||||
|
}
|
||||||
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
|
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
|
||||||
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
|
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
|
||||||
// the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the
|
// the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ fn stale_warning(
|
|||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
if source_mtime > dylib_mtime {
|
if source_mtime > dylib_mtime {
|
||||||
Some(format!(
|
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 \
|
file was modified {} (run `cargo build` to refresh); proceeding \
|
||||||
with the existing dylib",
|
with the existing dylib",
|
||||||
dylib_path.display(),
|
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 source_mtime = UNIX_EPOCH + Duration::from_secs(2_003_702_400); // 2033-06-30
|
||||||
let path = PathBuf::from("/tmp/x.so");
|
let path = PathBuf::from("/tmp/x.so");
|
||||||
let msg = stale_warning(&path, dylib_mtime, source_mtime).expect("source is newer");
|
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("2001"), "names the dylib's mtime: {msg}");
|
||||||
assert!(msg.contains("2033"), "names the source's newer 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
|
/// The content-derived on-disk key of one campaign cell under
|
||||||
/// `traces/<trace_name>/` — the `member_key` discipline (content, never a
|
/// `traces/<trace_name>/` — the `member_key` discipline (content, never a
|
||||||
/// runtime ordinal): strategy content-id prefix + instrument +
|
/// runtime ordinal): strategy content-id prefix + instrument +
|
||||||
@@ -368,10 +390,10 @@ pub fn persist_campaign_traces(
|
|||||||
for tap in taps {
|
for tap in taps {
|
||||||
match tap_channel(tap) {
|
match tap_channel(tap) {
|
||||||
Some(TapChannel::Net) if campaign.cost.is_empty() => eprintln!(
|
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)),
|
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);
|
let members = cell_member_fanout(cell_out);
|
||||||
if members.is_empty() {
|
if members.is_empty() {
|
||||||
eprintln!(
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -753,4 +775,27 @@ mod tests {
|
|||||||
assert_eq!(tap_channel("net_r_equity"), Some(TapChannel::Net));
|
assert_eq!(tap_channel("net_r_equity"), Some(TapChannel::Net));
|
||||||
assert_eq!(tap_channel("bogus"), None);
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ frozen deploy artifact ([invariant 8](c13-hot-reload-frozen-deploy.md)) cannot
|
|||||||
pick up. Usage lines follow the clap house style (`Usage: aura <verb> …`);
|
pick up. Usage lines follow the clap house style (`Usage: aura <verb> …`);
|
||||||
refusal diagnostics stay unprefixed, since a diagnostic is not a usage line.
|
refusal diagnostics stay unprefixed, since a diagnostic is not a usage line.
|
||||||
|
|
||||||
|
**The stderr class markers (#278/#313).** Diagnostics on a *continuing* run
|
||||||
|
carry a stable class marker the exit code cannot supply: `aura: note: <text>`
|
||||||
|
for a benign, expected diagnostic — a gate-emptied cell, a skipped tap, an
|
||||||
|
all-zero-trade walk-forward — where the run stays valid and the exit code is
|
||||||
|
unaffected (a null result is a valid research result, #198); and
|
||||||
|
`aura: warning: <text>` for a recorded fault or suspect condition the run
|
||||||
|
survives — a failed cell feeding exit 3, a failed scaffold git step, a stale
|
||||||
|
hot-reload dylib. 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 below is the machine contract.
|
||||||
|
The markers are grep-stable: `grep '^aura: warning: '` isolates faults,
|
||||||
|
`grep '^aura: note: '` benign notices. The grammar's single source is
|
||||||
|
`aura-cli/src/diag.rs`; the few runner-side literals repeat it verbatim.
|
||||||
|
|
||||||
**The exit-code partition** is a durable part of the automation contract: a
|
**The exit-code partition** is a durable part of the automation contract: a
|
||||||
caller branches on the failure class without parsing stderr.
|
caller branches on the failure class without parsing stderr.
|
||||||
|
|
||||||
|
|||||||
@@ -305,6 +305,10 @@ System Quality Number — `√n · mean_R / stdev_R`: the dispersion- and trade-
|
|||||||
**Avoid:** — (superseded)
|
**Avoid:** — (superseded)
|
||||||
**Superseded (2026-06-28 C10 reframe): there is no Stage-2 currency/compounding gate.** Research is one pure feed-forward R loop — gross R → net R via the `cost model` — with money, compounding, and a real broker living only at a separate later **live / deploy edge** (compounding is a post-hoc money-management transform of the net-R sequence, not an in-loop axis). The pre-reframe cleave gated a Stage-2 currency / fixed-fractional / realistic-broker layer behind `E[R] > 0`; **"Stage-1"** now survives only as a historical cycle name — the shipped feed-forward R chain's identifier family was renamed to the r-family (`r-sma` / `r-breakout` / `r-meanrev`, #174) — not one half of a two-stage gate.
|
**Superseded (2026-06-28 C10 reframe): there is no Stage-2 currency/compounding gate.** Research is one pure feed-forward R loop — gross R → net R via the `cost model` — with money, compounding, and a real broker living only at a separate later **live / deploy edge** (compounding is a post-hoc money-management transform of the net-R sequence, not an in-loop axis). The pre-reframe cleave gated a Stage-2 currency / fixed-fractional / realistic-broker layer behind `E[R] > 0`; **"Stage-1"** now survives only as a historical cycle name — the shipped feed-forward R chain's identifier family was renamed to the r-family (`r-sma` / `r-breakout` / `r-meanrev`, #174) — not one half of a two-stage gate.
|
||||||
|
|
||||||
|
### stderr class marker
|
||||||
|
**Avoid:** log level, severity tag
|
||||||
|
The stable prefix pair separating diagnostics on a continuing run: `aura: note: ` marks a benign, expected outcome (exit code unaffected), `aura: warning: ` a recorded fault or suspect condition the run survives. Error lines that accompany a non-zero exit and plain info lines keep the bare `aura: ` prefix — for errors, the exit-code partition (C14) is the machine contract.
|
||||||
|
|
||||||
### strategy
|
### strategy
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
A reusable composite-node blueprint — broker-, data-, and viz-independent, inputs declared as named roles — whose output is the **bias** stream (unsized direction + conviction; not an equity curve, not a size; sizing and the position table are derived downstream layers). Frozen with a broker into a bot for deploy.
|
A reusable composite-node blueprint — broker-, data-, and viz-independent, inputs declared as named roles — whose output is the **bias** stream (unsized direction + conviction; not an equity curve, not a size; sizing and the position table are derived downstream layers). Frozen with a broker into a bot for deploy.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
### run A: zero-trade walkforward (synthetic) — expect note, exit 0
|
||||||
|
aura: note: all 3 walk-forward windows recorded zero trades
|
||||||
|
# exitA=0
|
||||||
|
### run B: real sweep with a poison axis member — expect warning, exit 3
|
||||||
|
# exitB=3
|
||||||
|
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||||
|
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||||
|
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: note: all 3 walk-forward windows recorded zero trades
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: note: the single walk-forward window recorded zero trades
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
### real sweep, poison member (CAUGHT -> warning?, survives?)
|
||||||
|
# exit=3
|
||||||
|
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||||
|
|
||||||
|
### synthetic walkforward, SAME poison (uncaught panic?)
|
||||||
|
# exit=3
|
||||||
|
aura: warning: a member panicked: SMA length must be >= 1
|
||||||
|
|
||||||
|
### sweep, every member zero trades (any notice?)
|
||||||
|
# exit=0
|
||||||
|
|
||||||
|
### mc, vacuous / all seeds identical (marker? exit?)
|
||||||
|
# exit=2
|
||||||
|
aura: mc is vacuous: every seed produced an identical result — the strategy never warmed over the synthetic walk, so no seed reached a distinguishable realization; use a shallower-lookback blueprint or a longer walk
|
||||||
|
|
||||||
|
### generalize, every instrument zero trades (any notice?)
|
||||||
|
# exit=0
|
||||||
|
|
||||||
|
### generalize, one instrument has no data in window (marker? exit?)
|
||||||
|
# exit=1
|
||||||
|
aura: generalize produced no cross-instrument grade
|
||||||
|
|
||||||
|
### walkforward yielding a single OOS window (plural grammar)
|
||||||
|
# exit=0
|
||||||
|
aura: note: the single walk-forward window recorded zero trades
|
||||||
|
|
||||||
|
### walkforward without a project (real mode)
|
||||||
|
# exit=1
|
||||||
|
aura: walkforward needs a project: strategies resolve against the project store and vocabulary (no Aura.toml found up from /tmp/tmp.MQAgiiVtay)
|
||||||
|
|
||||||
|
### degenerate axis on a NON-campaign call (graph build finalize)
|
||||||
|
# exit=1
|
||||||
|
aura: finalize: slot const.clock is unconnected
|
||||||
|
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Example 1 — Mission (1): a consumer separates benign notices from real faults
|
||||||
|
# by FORM alone (grep / CI log scan), and checks exit codes against the binary's
|
||||||
|
# own conventions.
|
||||||
|
#
|
||||||
|
# Finding proven here: the two markers cannot co-occur in ONE invocation (the
|
||||||
|
# only note-emitting verb, walkforward, is single-cell all-or-nothing: the note
|
||||||
|
# fires only when every window SUCCEEDS with zero trades, i.e. exit 0; any
|
||||||
|
# warning implies a cell/fold failure, i.e. exit 3). So separability is shown
|
||||||
|
# the way the promise's "CI log scan" actually consumes it: over the accumulated
|
||||||
|
# stderr of several runs.
|
||||||
|
set -u
|
||||||
|
AURA="${AURA:?set AURA to the release binary path}"
|
||||||
|
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||||
|
LOG="$HERE/captured/example1_combined.log"
|
||||||
|
|
||||||
|
# Build the synthetic zero-trade strategy from its canonical op-list.
|
||||||
|
"$AURA" graph build < "$HERE/ops/flat.ops.json" > "$WORK/flat.json"
|
||||||
|
|
||||||
|
# A real project is required to reach the `aura: warning:` class at all
|
||||||
|
# (synthetic member panics are uncaught — see example 3). Scaffold one.
|
||||||
|
( cd "$WORK" && "$AURA" new proj >/dev/null )
|
||||||
|
|
||||||
|
: > "$LOG"
|
||||||
|
echo "### run A: zero-trade walkforward (synthetic) — expect note, exit 0" >> "$LOG"
|
||||||
|
"$AURA" walkforward "$WORK/flat.json" --axis graph.sma.length=2,3,5 >/dev/null 2>>"$LOG"
|
||||||
|
echo "# exitA=$?" >> "$LOG"
|
||||||
|
echo "### run B: real sweep with a poison axis member — expect warning, exit 3" >> "$LOG"
|
||||||
|
( cd "$WORK/proj" && "$AURA" sweep blueprints/signal.json --real EURUSD \
|
||||||
|
--from 1577836800000 --to 1609459200000 --axis proj_signal.fast.length=0,2 \
|
||||||
|
>/dev/null 2>>"$HERE/captured/example1_runB.err" )
|
||||||
|
echo "# exitB=$?" >> "$LOG"
|
||||||
|
cat "$HERE/captured/example1_runB.err" >> "$LOG"
|
||||||
|
|
||||||
|
echo "=== combined log ==="; cat "$LOG"
|
||||||
|
echo "=== benign notices (grep '^aura: note: ') ==="; grep '^aura: note: ' "$LOG" || true
|
||||||
|
echo "=== real faults (grep '^aura: warning: ') ==="; grep '^aura: warning: ' "$LOG" || true
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Example 2 — Mission (2): a walk-forward whose every window records zero trades
|
||||||
|
# must announce itself with an explicit notice; one that trades must not; exit
|
||||||
|
# code stays success either way. Blueprints authored via the binary's own
|
||||||
|
# op-script surface (`aura graph build`).
|
||||||
|
set -u
|
||||||
|
AURA="${AURA:?set AURA to the release binary path}"
|
||||||
|
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
"$AURA" graph build < "$HERE/ops/flat.ops.json" > "$WORK/flat.json"
|
||||||
|
"$AURA" graph build < "$HERE/ops/momentum.ops.json" > "$WORK/momentum.json"
|
||||||
|
cd "$WORK" # synthetic families record to ./runs — keep them in the temp dir
|
||||||
|
|
||||||
|
echo "=== A: flat (zero-trade) walkforward — expect note + exit 0 ==="
|
||||||
|
"$AURA" walkforward "$WORK/flat.json" --axis graph.sma.length=2,3,5 \
|
||||||
|
>/dev/null 2>"$HERE/captured/example2_flat.err"
|
||||||
|
echo "# exit=$?"; cat "$HERE/captured/example2_flat.err"
|
||||||
|
|
||||||
|
echo "=== B: momentum (trades) walkforward — expect NO note + exit 0 ==="
|
||||||
|
"$AURA" walkforward "$WORK/momentum.json" --axis graph.sma.length=2,3,5 \
|
||||||
|
>/dev/null 2>"$HERE/captured/example2_momentum.err"
|
||||||
|
echo "# exit=$?"; cat "$HERE/captured/example2_momentum.err"; echo "(stderr empty above = no note)"
|
||||||
|
|
||||||
|
echo "=== C: single-window edge — expect the 'all 1 windows' plural nit ==="
|
||||||
|
# The synthetic roller always yields 3 windows; a sub-120-day REAL window yields
|
||||||
|
# exactly one OOS window, which exposes the ungrammatical "all 1 ... windows".
|
||||||
|
( cd "$WORK" && "$AURA" new proj >/dev/null )
|
||||||
|
( cd "$WORK/proj" && "$AURA" walkforward blueprints/signal.json --real EURUSD \
|
||||||
|
--from 1577836800000 --to 1583020800000 --axis proj_signal.fast.length=4 \
|
||||||
|
>/dev/null 2>"$HERE/captured/example2_singular.err" )
|
||||||
|
echo "# exit=$?"; cat "$HERE/captured/example2_singular.err"
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Example 3 — Mission (3): from the consumer's seat, do any OTHER diagnostics
|
||||||
|
# encountered en route fall OUTSIDE both stderr classes (note/warning) when they
|
||||||
|
# plausibly belong to one? Survey the diagnostics a driver actually hits and tag
|
||||||
|
# each with (marker?, exit code, verdict). Reads only the binary's own output.
|
||||||
|
set -u
|
||||||
|
AURA="${AURA:?set AURA to the release binary path}"
|
||||||
|
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||||
|
OUT="$HERE/captured/example3_survey.log"
|
||||||
|
: > "$OUT"
|
||||||
|
|
||||||
|
run() { # label ; then the command via "$@"
|
||||||
|
local label="$1"; shift
|
||||||
|
"$@" >/dev/null 2>"$WORK/e"; local e=$?
|
||||||
|
{ echo "### $label"; echo "# exit=$e"; sed 's/^/ /' "$WORK/e"; echo; } >> "$OUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
"$AURA" graph build < "$HERE/ops/flat.ops.json" > "$WORK/flat.json"
|
||||||
|
"$AURA" graph build < "$HERE/ops/momentum.ops.json" > "$WORK/momentum.json"
|
||||||
|
"$AURA" graph build < "$HERE/ops/flat_closed.ops.json" > "$WORK/flat_closed.json"
|
||||||
|
( cd "$WORK" && "$AURA" new proj >/dev/null )
|
||||||
|
|
||||||
|
# (A) SAME fault class, two surfaces: real campaign vs synthetic.
|
||||||
|
run "real sweep, poison member (CAUGHT -> warning?, survives?)" \
|
||||||
|
bash -c "cd '$WORK/proj' && '$AURA' sweep blueprints/signal.json --real EURUSD --from 1577836800000 --to 1609459200000 --axis proj_signal.fast.length=0,2"
|
||||||
|
run "synthetic walkforward, SAME poison (uncaught panic?)" \
|
||||||
|
"$AURA" walkforward "$WORK/momentum.json" --axis graph.sma.length=0,3
|
||||||
|
|
||||||
|
# (B) null-result siblings of the zero-trade note that get NO note.
|
||||||
|
run "sweep, every member zero trades (any notice?)" \
|
||||||
|
"$AURA" sweep "$WORK/flat.json" --axis graph.sma.length=2,3,5
|
||||||
|
run "mc, vacuous / all seeds identical (marker? exit?)" \
|
||||||
|
"$AURA" mc "$WORK/flat_closed.json" --seeds 5
|
||||||
|
run "generalize, every instrument zero trades (any notice?)" \
|
||||||
|
bash -c "cd '$WORK/proj' && '$AURA' generalize blueprints/signal.json --real EURUSD,GBPUSD --from 1577836800000 --to 1609459200000 --axis proj_signal.fast.length=4"
|
||||||
|
|
||||||
|
# (C) partial-failure surfaced without a marker.
|
||||||
|
run "generalize, one instrument has no data in window (marker? exit?)" \
|
||||||
|
bash -c "cd '$WORK/proj' && '$AURA' generalize blueprints/signal.json --real EURUSD,Copper --from 1514764800000 --to 1530403200000 --axis proj_signal.fast.length=4"
|
||||||
|
|
||||||
|
# (D) plural-agreement nit on the note itself.
|
||||||
|
run "walkforward yielding a single OOS window (plural grammar)" \
|
||||||
|
bash -c "cd '$WORK/proj' && '$AURA' walkforward blueprints/signal.json --real EURUSD --from 1577836800000 --to 1583020800000 --axis proj_signal.fast.length=4"
|
||||||
|
|
||||||
|
# (E) hard errors that (correctly) sit outside both classes.
|
||||||
|
run "walkforward without a project (real mode)" \
|
||||||
|
bash -c "cd '$WORK' && '$AURA' walkforward proj/blueprints/signal.json --real EURUSD --axis proj_signal.fast.length=2"
|
||||||
|
run "degenerate axis on a NON-campaign call (graph build finalize)" \
|
||||||
|
bash -c "echo '[{\"op\":\"add\",\"type\":\"Const\"}]' | '$AURA' graph build"
|
||||||
|
|
||||||
|
cat "$OUT"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{"op":"doc","text":"flat: bias = 0 * sign(close - SMA(close,length)); Scale factor bound to 0 so no window ever trades, while sma.length stays an open axis"},
|
||||||
|
{"op":"source","role":"close","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"sma"},
|
||||||
|
{"op":"add","type":"Sub","name":"d"},
|
||||||
|
{"op":"add","type":"Sign","name":"s"},
|
||||||
|
{"op":"add","type":"Scale","name":"z","bind":{"factor":{"F64":0.0}}},
|
||||||
|
{"op":"feed","role":"close","into":["sma.series","d.lhs"]},
|
||||||
|
{"op":"connect","from":"sma.value","to":"d.rhs"},
|
||||||
|
{"op":"connect","from":"d.value","to":"s.value"},
|
||||||
|
{"op":"connect","from":"s.value","to":"z.signal"},
|
||||||
|
{"op":"expose","from":"z.value","as":"bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[
|
||||||
|
{"op":"doc","text":"flat-closed: bias = 0, all params bound (mc requires a closed blueprint)"},
|
||||||
|
{"op":"source","role":"close","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":3}}},
|
||||||
|
{"op":"add","type":"Sub","name":"d"},
|
||||||
|
{"op":"add","type":"Sign","name":"s"},
|
||||||
|
{"op":"add","type":"Scale","name":"z","bind":{"factor":{"F64":0.0}}},
|
||||||
|
{"op":"feed","role":"close","into":["sma.series","d.lhs"]},
|
||||||
|
{"op":"connect","from":"sma.value","to":"d.rhs"},
|
||||||
|
{"op":"connect","from":"d.value","to":"s.value"},
|
||||||
|
{"op":"connect","from":"s.value","to":"z.signal"},
|
||||||
|
{"op":"expose","from":"z.value","as":"bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{"op":"doc","text":"momentum: bias = sign(close - SMA(close,length)); length is the open IS-refit axis"},
|
||||||
|
{"op":"source","role":"close","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"sma"},
|
||||||
|
{"op":"add","type":"Sub","name":"d"},
|
||||||
|
{"op":"add","type":"Sign","name":"s"},
|
||||||
|
{"op":"feed","role":"close","into":["sma.series","d.lhs"]},
|
||||||
|
{"op":"connect","from":"sma.value","to":"d.rhs"},
|
||||||
|
{"op":"connect","from":"d.value","to":"s.value"},
|
||||||
|
{"op":"expose","from":"s.value","as":"bias"}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user