Compare commits

...

11 Commits

Author SHA1 Message Date
claude 6fb7caf929 fieldtest: milestone-stderr-honesty re-verify — panic containment + plural fix live
All three mission drivers re-run against the rebuilt release binary
after 51096a3/a20b6f6/4897919: the synthetic poison row now shows
aura: warning: + exit 3 (zero raw panic lines in the survey), the
single-window case reads "the single walk-forward window recorded
zero trades", and the marker-separability log still greps into two
clean classes. Captures refreshed in place; the milestone fieldtest
roll-up is green. refs #278, refs #313
2026-07-24 00:59:22 +02:00
claude 48979193b9 tidy(cli): pluralization-aware zero-trade note wording
Milestone-fieldtest friction: a single-OOS-window walk-forward printed
"all 1 walk-forward windows recorded zero trades" (captured RED
evidence: fieldtests/milestone-stderr-honesty/captured/
example2_singular.err). The message text moves into a pure
zero-trade_note_text builder — singular "the single walk-forward
window recorded zero trades", plural unchanged so the existing e2e
phrase pins hold — with a unit pin on both forms. refs #278
2026-07-24 00:58:20 +02:00
claude a20b6f6003 fix(runner): contain mc synthetic member panics (GREEN)
blueprint_mc_family joins its sweep/walkforward siblings on the
51096a3 machinery: catch_member_panic around the bare member run in
the monte_carlo draw closure, per-seed capture, lowest-seed
resolution after the join (seed-input order, thread-order-independent,
C1), exit via the shared aura: warning: + exit 3 path. Ordering is
load-bearing and documented at the site: the captured fault resolves
BEFORE the vacuous-mc guard, so metrics-identical placeholder reports
from faulted draws cannot masquerade as a vacuous run. All three
containment e2es green; clippy clean. refs #278
2026-07-24 00:57:33 +02:00
claude 59f547e313 test(cli): RED -- mc synthetic member panic is contained like its siblings
Orchestrator probe after 51096a3: blueprint_mc_family is the one
remaining family builder running members bare — a closed poison
blueprint (SMA length bound 0) through aura mc --seeds 2 exits 101
with a raw panic. RED e2e pins the shared contract: no 101, an
aura: warning: fault line, no panic text or crates/ path, exit 3.
Single-seed escape confirms the bare seam (not the parallelism) is
the cause. refs #278
2026-07-24 00:47:05 +02:00
claude 51096a3212 fix(runner, campaign): contain synthetic member panics like the real path (GREEN)
The #272 containment recipe (SilencedPanic + catch_unwind +
panic_message) is exposed from aura-campaign as catch_member_panic and
applied at the bare-member seam in the synthetic family builders:
blueprint_sweep_family contains per-point, resolves the lowest
enumeration index's fault after the sweep joins (thread-order-
independent, C1), and exits deliberately with the aura: warning:
class marker + exit 3 (C14) instead of the raw panic/exit-101 escape;
blueprint_sweep_over captures instead of exiting (it runs inside the
per-window walkforward parallel closure — an on-the-spot exit would
race across windows) and hands the resolved fault up. Fault prose
reuses member_fault_prose; the real path's containment is untouched.

Deliberate residue, both tracked: blueprint_mc_family's member call
site keeps the bare run (empirical probe of the reachable surface
follows this commit; a reachable escape goes the same RED-first way),
and the new eprintln+exit(3) site in aura-runner joins the #297
process::exit inventory (noted there) rather than fighting M4's
future RunnerError propagation now.

Both RED e2es green; workspace suite green via the mini-verify;
clippy -D warnings clean on the touched crates. refs #278
2026-07-24 00:43:06 +02:00
claude bd6ddf9efe test(cli): RED -- synthetic member panics are contained like the real path
Milestone-fieldtest bug (source-blind run, 2026-07-24): a member fault
on the synthetic walkforward/sweep path escapes as an uncaught panic
(exit 101, no class marker, internal source path leaked) while the
real path contains the identical fault via the #272 boundary as
aura: warning: + exit 3. Cause: the family builders in
aura-runner/src/family.rs run members bare, without the catch_unwind
containment that lives only in aura-campaign/src/exec.rs. Two RED
e2es pin the derived contract (decision log on #278): no exit 101,
a warning-marked fault line, no panic text or crates/ path on stderr,
exit 3 per the C14 partition. refs #278
2026-07-24 00:08:56 +02:00
claude 7e1dd25a76 fieldtest: milestone-stderr-honesty — 3 examples, 1 bug / 2 friction / 2 spec-gap
Milestone fieldtest for "Surface honesty on stderr", run source-blind
against the release binary only (binary-only knowledge floor, user
direction 2026-07-23): op-lists authored via the binary's own graph
build/introspect surfaces, project + real archive reached through help
alone. Markers grep-separable, zero-trade notice exact, abort paths
correctly unmarked (4x working). Findings routed: synthetic member-fault
panic (exit 101, source-path leak) -> debug; null-result and
partial-failure widening -> tracker; op-script discoverability ->
#323 evidence; silent unknown-op-field drop -> tracker; plural nit ->
tidy. refs #278, refs #313
2026-07-24 00:04:53 +02:00
claude cc683fb065 audit(cycle stderr-class-markers): drift adjudication — clean, no baseline moved
Cycle #278+#313 close (commits 34987be, 1102d77, 913d187 on cfc2d9f).

Architect (What holds): C14 exit-code partition untouched at every retag
site (exit-carrying refusal sites verified bare `aura: `, both class
markers grep-exact by codebase sweep); spec site table 9/9 shipped with
both moving pins; the #313 note on both walk-forward paths through the
one shared diag helper, pinned by three e2es; commit bodies truthful,
c28_layering roster updated, clippy green on both touched crates.

Architect drift items, adjudicated:
- [medium, dissolved on verification] Claimed contradiction between
  C14's warning-class text ("a failed cell feeding exit 3") and the
  verb_sugar sites' `return Ok(1)`. Verified against the tree: the
  sugar paths return a failed-cell COUNT into
  `exit_on_campaign_result` (main.rs:1684), whose mapping is
  `Ok(0) => {}` / `Ok(_) => exit(3)` — the sugar-path failed-cell
  warning feeds exit 3 exactly as the campaign path does. Ledger and
  code agree; no edit. Carry-on.
- [low] The cycle spec's Components line scoped diag.rs to "the two
  marker macros" while the shipped module also owns the #313 helper.
  The spec is a git-ignored working file deleted at this close; the
  durable records (C14 paragraph, commit 1102d77 body) describe the
  shipped shape correctly. Carry-on.

Regression (aura-bench, exit 0, all fingerprints OK, raw):
engine_throughput    bars_per_s 13638785   wall_s 0.733s   Δ bars_per_s -2.6% wall_s +2.7%   fingerprint OK
ingest_throughput    bars_per_s 13030599   wall_s 0.385s   Δ bars_per_s +0.2% wall_s -0.2%   fingerprint OK
campaign_sweep       cpu_percent 2194.0   peak_rss_mb 93.3   wall_s 1.402s   Δ cpu_percent +0.2% peak_rss_mb +1.2% wall_s +0.5%   fingerprint OK
campaign_heavy       cpu_percent 2146.0   peak_rss_mb 99.6   wall_s 5.620s   Δ cpu_percent -0.3% peak_rss_mb -1.7% wall_s +0.8%   fingerprint OK
cli_fixed_cost       help_ms 1.5ms   run_ms 3.6ms   Δ help_ms -1.7% run_ms -0.9%   fingerprint OK
(loadavg warning during the run: the workspace suite ran in parallel;
all deltas within tolerance regardless.) No baseline update, so no
ratify statement is due.

Cycle stderr-class-markers tidy (drift-clean). Not a milestone close:
the "Surface honesty on stderr" milestone additionally needs its green
milestone fieldtest (binary-only knowledge floor per the user direction
of 2026-07-23), which runs next; the formal close stays the user's act.

refs #278, refs #313
2026-07-23 23:41:38 +02:00
claude 913d18755e docs(ledger, glossary): the stderr class-marker grammar joins C14
Iteration stderr-markers-2 close, task 5 of the stderr-class-markers
plan. C14's Current state gains the marker paragraph — note/warning
classes for continuing-run diagnostics, bare `aura: ` for exit-carrying
error lines and plain info lines, grep-stable grammar, single source in
aura-cli/src/diag.rs — and the glossary gains the `stderr class marker`
entry. No new C-number: C14 already owns the CLI stderr conventions and
the exit-code partition this grammar completes (derivation logged on
the #278 decision thread).

With this the milestone promise is shipped: the two diagnostic classes
are machine-separable on stderr (#278), and the all-zero-trade
walk-forward announces itself instead of completing silently (#313).
Exit-code semantics unchanged throughout.

closes #278
closes #313
2026-07-23 23:36:08 +02:00
claude 1102d776df 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
2026-07-23 23:35:23 +02:00
claude 34987be389 feat(cli): stderr class markers — diag module, campaign/verb-sugar retags
Iteration stderr-markers-1, tasks 1-2 of the stderr-class-markers plan
(spec signed via grounding-check PASS, decisions logged on #278).

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

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

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

refs #278
2026-07-23 22:52:46 +02:00
28 changed files with 907 additions and 37 deletions
+13
View File
@@ -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())
}
/// 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
/// stage (#272): the realized stage prefix survives in `stages`, `families`/
/// `selections` carry whatever already-persisted stages produced (empty for a
+2 -1
View File
@@ -13,7 +13,8 @@
mod 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;
+5 -5
View File
@@ -315,8 +315,8 @@ fn present_campaign(
for cell in &outcome.record.cells {
for (stage_ix, st) in cell.stages.iter().enumerate() {
if matches!(&st.survivor_ordinals, Some(v) if v.is_empty()) {
eprintln!(
"aura: cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
crate::diag::note!(
"cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
survivors; cell realization truncated",
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1
);
@@ -333,8 +333,8 @@ fn present_campaign(
if let Some(f) = &cell.fault {
failed += 1;
fail_labels.push(format!("{}: {}", cell.instrument, cell_fault_kind_label(f.kind)));
eprintln!(
"aura: cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
crate::diag::warning!(
"cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1, f.stage, f.detail
);
}
@@ -388,7 +388,7 @@ fn present_campaign(
.expect("campaign run record serializes")
);
eprintln!(
"campaign run {} recorded: {} cells{}",
"aura: campaign run {} recorded: {} cells{}",
outcome.record.run,
outcome.record.cells.len(),
if failed == 0 {
+65
View File
@@ -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"
);
}
}
+7
View File
@@ -3,6 +3,7 @@
//! (or the `--strategy r-sma` sugar) and print canonical JSON metrics/manifests;
//! this binary authors no built-in harness.
mod diag;
mod render;
mod graph_construct;
mod campaign_run;
@@ -916,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));
}
+5 -5
View File
@@ -283,7 +283,7 @@ pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
Ok(o) if o.status.success() => {
commit_all(&spec.target_dir, "aura new scaffold");
}
_ => eprintln!("aura: warning: git init failed (project created without a repo)"),
_ => crate::diag::warning!("git init failed (project created without a repo)"),
}
Ok(())
}
@@ -324,7 +324,7 @@ pub fn scaffold_node_crate(spec: &ScaffoldSpec) -> Result<(), String> {
Ok(o) if o.status.success() => {
commit_all(&spec.target_dir, "aura nodes new scaffold");
}
_ => eprintln!("aura: warning: git init failed (node crate created without a repo)"),
_ => crate::diag::warning!("git init failed (node crate created without a repo)"),
}
Ok(())
}
@@ -357,7 +357,7 @@ fn commit_all(dir: &std::path::Path, message: &str) {
.current_dir(dir)
.output();
if !matches!(add, Ok(o) if o.status.success()) {
eprintln!("aura: warning: git add failed (scaffold created without an initial commit)");
crate::diag::warning!("git add failed (scaffold created without an initial commit)");
return;
}
let commit = std::process::Command::new("git")
@@ -374,8 +374,8 @@ fn commit_all(dir: &std::path::Path, message: &str) {
.current_dir(dir)
.output();
if !matches!(commit, Ok(o) if o.status.success()) {
eprintln!(
"aura: warning: git commit failed (scaffold created without an initial commit)"
crate::diag::warning!(
"git commit failed (scaffold created without an initial commit)"
);
}
}
+5 -2
View File
@@ -526,7 +526,7 @@ pub(crate) fn run_walkforward_sugar(
)?;
if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) {
eprintln!("aura: walkforward cell failed at stage {}: {}", f.stage, f.detail);
crate::diag::warning!("walkforward cell failed at stage {}: {}", f.stage, f.detail);
return Ok(1);
}
@@ -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,
@@ -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()) {
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);
}
+228
View File
@@ -4782,6 +4782,234 @@ 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 (#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
/// error (exit 2), naming that >= 1 --axis is required.
#[test]
+52
View File
@@ -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
+66 -2
View File
@@ -1452,6 +1452,19 @@ const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
}"#;
/// A gate no realized sweep member can ever satisfy (`n_trades ge 999999`) —
/// deterministically empties `survivor_ordinals` regardless of the fixture's
/// actual trade counts, driving the #278 zero-survivor NOTE marker.
const GATE_EMPTIES_PROCESS_DOC: &str = r#"{
"format_version": 1,
"kind": "process",
"name": "gate-empties",
"pipeline": [
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
{ "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 999999.0 } ] }
]
}"#;
/// The full annotated shape for the gated e2e: sweep -> gate -> walk_forward
/// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so
/// walk-forward always has survivors; the mc annotator bootstraps the pooled
@@ -1978,6 +1991,57 @@ fn campaign_run_synthetic_e2e_parallel_instruments_contains_a_real_per_cell_faul
assert_eq!(cells[1]["instrument"].as_str(), Some("SYMB"), "doc order preserved: {line}");
assert!(cells[0]["fault"].is_null(), "SYMA has data in January, its cell succeeds: {line}");
assert!(!cells[1]["fault"].is_null(), "SYMB has no January data, its cell is faulted: {line}");
assert!(
out.contains("aura: warning: cell ("),
"the failed-cell line carries the warning class marker (#278): {out}"
);
}
/// #278 sibling of the warning-class pin above: a gate that empties a cell's
/// survivors (never a fault, #198 decision 8) emits the NOTE class marker,
/// not the bare `aura:` prefix nor `warning:`. `GATE_EMPTIES_PROCESS_DOC`'s
/// impossible threshold guarantees zero survivors regardless of the
/// fixture's actual sweep-member trade counts. Second property pinned on the
/// same invocation: the run's terminal "recorded" summary line — previously
/// printed with NO `aura:` prefix at all — now carries it too, so every
/// stderr line this command emits (per-cell class markers AND the run-level
/// summary) shares the one `aura:`-rooted vocabulary; a regression that
/// reverted just this one un-tagged `eprintln!` back to a bare line would
/// not be caught by the note-marker assertion above.
#[test]
fn campaign_run_synthetic_e2e_gate_emptied_cell_carries_the_note_marker() {
let (dir, _fixture) = fresh_project_with_data();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("gateempty.process.json")),
ScratchPath::File(dir.join("gateempty.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "campaign-run-gateempty-seed");
let proc_id =
register_process_doc(&dir, "gateempty.process.json", GATE_EMPTIES_PROCESS_DOC);
let doc = campaign_doc_json_for(
"SYMA",
&bp_id,
&proc_id,
(1704844800000, 1705708800000),
"",
"",
);
write_doc(&dir, "gateempty.campaign.json", &doc);
let (out, code) =
run_code_in(&dir, &["campaign", "run", "gateempty.campaign.json"]);
assert_eq!(code, Some(0), "a zero-survivor cell is a valid result, not a fault: {out}");
assert!(
out.contains("aura: note: cell "),
"the gate-emptied cell line carries the note class marker (#278): {out}"
);
assert!(
out.contains("aura: campaign run "),
"the run-level recorded summary line also carries the aura: prefix (#278): {out}"
);
}
/// The v2 boundary, campaign side: a generalize-bearing process is an
@@ -3683,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}"
);
@@ -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!(
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}"
);
+185 -17
View File
@@ -15,8 +15,9 @@
//! CLI shell still needs its own for call sites outside any family builder).
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_core::{Cell, ParamSpec, Scalar, Timestamp};
use aura_engine::{
@@ -315,7 +316,10 @@ pub fn select_winner(
/// 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
/// 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 {
RunReport {
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
/// member (#253). Reuses the SAME strict, erroring axis-name check
/// `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 {
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| {
// fresh per-member graph (Composite is !Clone, reload per member) run through
// 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
// 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
@@ -447,10 +513,19 @@ pub fn blueprint_sweep_family(
/// its walk-forward in-sample twin); an axis matching neither space is refused
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
/// "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(
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
env: &Env, binding: &ResolvedBinding,
) -> Result<(SweepFamily, Vec<usize>), BindError> {
) -> Result<(SweepFamily, Vec<usize>, Option<String>), BindError> {
let reload = |d: &str| {
blueprint_from_json(d, &|t| env.resolve(t))
.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 {
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 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
@@ -509,6 +595,37 @@ pub fn run_oos_blueprint(
(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
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
/// 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));
std::process::exit(2);
}
walk_forward(roller, space.clone(), |w: WindowBounds| {
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
// #278: `blueprint_sweep_over`'s inner sweep and `run_oos_blueprint`'s single
// 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");
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)) {
Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
};
let (oos_equity, mut oos_report) =
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 }
})
match catch_member_panic(|| {
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides)
}) {
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
@@ -642,11 +786,35 @@ pub fn blueprint_mc_family(
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
let seeds: Vec<u64> = (1..=n_seeds).collect();
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 sources = synthetic_walk_sources(seed);
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
// 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
+2 -1
View File
@@ -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}");
}
+49 -4
View File
@@ -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}");
}
}
@@ -179,6 +179,7 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() {
modules,
[
"campaign_run.rs",
"diag.rs",
"graph_construct.rs",
"main.rs",
"render.rs",
@@ -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> …`);
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
caller branches on the failure class without parsing stderr.
+4
View File
@@ -305,6 +305,10 @@ System Quality Number — `√n · mean_R / stdev_R`: the dispersion- and trade-
**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.
### 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
**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.
@@ -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
@@ -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
@@ -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"
@@ -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"}
]