feat(aura-cli, aura-engine): zero-trade cell + synthetic-smoke notes; pending-builder serialize fence

Harvest sweep, batch 4 of 6.

- present_campaign's cell loop gains the non-walk-forward sibling of
  the #313 note: a cell whose families are all non-wf and whose every
  report traded zero times gets 'aura: note: ... metrics are vacuous,
  not a break-even result' on stderr (emit-independent; wf cells keep
  their window note, gate-truncated cells stay silent).
- exec's single-run bias leg (always RunData::Synthetic — a direct
  blueprint exec never binds real data) notes on a zero-trade run that
  the built-in 18-cycle stream is a smoke fixture and points at a
  campaign document's data section for a meaningful run.
- blueprint_serde refuses to serialize a still-pending arg-bearing
  builder (SerializeError::PendingBuilder naming node type + missing
  args + the try_args remedy) instead of emitting an args-free v1
  document whose fault only surfaces at load as MissingArg — the
  write-side twin of the load-path fence; only the Rust GraphBuilder
  path can reach this state.

closes #324
refs #341
This commit is contained in:
2026-07-26 12:26:27 +02:00
parent 2b692a71e3
commit d2b0cdf64c
5 changed files with 283 additions and 14 deletions
+15
View File
@@ -380,6 +380,21 @@ fn present_campaign(run: CampaignRun, env: &Env) -> Result<usize, String> {
crate::diag::note_zero_trade_windows(
wf.reports.iter().map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
);
} else if !cell_out.families.is_empty() {
// #324 (re-scoped): the non-walk-forward sibling — a cell whose
// families are ALL non-`std::walk_forward` (grid sweep, MC
// bootstrap, …) has no OOS-window vocabulary, so without a note
// here a cell that never traded is indistinguishable from a
// break-even one. Over every report across every family this
// cell ran (a cell with zero families, e.g. gate-truncated,
// emits nothing new here).
let trades: Vec<u64> = cell_out
.families
.iter()
.flat_map(|f| f.reports.iter())
.map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades))
.collect();
crate::diag::note_zero_trade_cell(trades.into_iter());
}
if emit_family {
for fam in &cell_out.families {
+61 -1
View File
@@ -44,9 +44,53 @@ pub(crate) fn note_zero_trade_windows(mut window_trades: impl ExactSizeIterator<
}
}
/// #324 (re-scoped): the non-walk-forward sibling of `note_zero_trade_windows`
/// — a campaign cell whose families are ALL non-walk-forward (grid sweep, MC
/// bootstrap, …) has no per-window vocabulary to note through, yet a
/// consumer still cannot tell "0.0 = break-even" from "0.0 = never traded"
/// without this. Takes the per-report trade counts across every family the
/// cell ran; a no-op unless there is at least one report and every one of
/// them traded zero times.
fn zero_trade_cell_note_text(n_reports: usize) -> String {
if n_reports == 1 {
"the cell's one report recorded zero trades; its metrics are vacuous, not a break-even \
result"
.to_string()
} else {
format!(
"the cell's {n_reports} reports all recorded zero trades; its metrics are vacuous, \
not a break-even result"
)
}
}
/// See `zero_trade_cell_note_text`. Called from the campaign cell loop's
/// non-walk-forward branch (`campaign_run.rs`), independent of `emit`.
pub(crate) fn note_zero_trade_cell(mut report_trades: impl ExactSizeIterator<Item = u64>) {
let n_reports = report_trades.len();
if n_reports > 0 && report_trades.all(|n| n == 0) {
note!("{}", zero_trade_cell_note_text(n_reports));
}
}
/// #324 (comment 4501): `exec <blueprint.json>`'s single-run signal/bias leg
/// always drives the built-in synthetic stream (`RunData::Synthetic`, no
/// direct-blueprint exec ever binds real data — only a campaign document's
/// `data` section does) — a tiny `n_cycles`-bar smoke fixture, not market
/// data. Zero trades on it is expected, not a broken strategy; a caller that
/// validates against it needs telling so. Unconditional — the caller gates
/// on the zero-trade condition before calling.
pub(crate) fn note_synthetic_smoke_zero_trades(n_cycles: usize) {
note!(
"the built-in synthetic stream ({n_cycles} cycles) is a small smoke fixture; zero \
trades is expected here — bind real data via a campaign document's `data` section \
(`aura exec <campaign.json>`) for a meaningful run"
);
}
#[cfg(test)]
mod tests {
use super::zero_trade_note_text;
use super::{zero_trade_cell_note_text, zero_trade_note_text};
#[test]
/// #278 fieldtest friction: the single-window form must read as
@@ -62,4 +106,20 @@ mod tests {
"all 3 walk-forward windows recorded zero trades"
);
}
#[test]
/// #324: the cell-level sibling pluralizes the same way and names the
/// vacuous-metrics property, not just the raw zero count.
fn zero_trade_cell_note_pluralizes() {
assert_eq!(
zero_trade_cell_note_text(1),
"the cell's one report recorded zero trades; its metrics are vacuous, not a \
break-even result"
);
assert_eq!(
zero_trade_cell_note_text(4),
"the cell's 4 reports all recorded zero trades; its metrics are vacuous, not a \
break-even result"
);
}
}
+12
View File
@@ -53,6 +53,11 @@ use aura_runner::member::{
use aura_runner::{TapPlan, TapSubscription};
#[cfg(test)]
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
// `r_sma_prices` sizes the #324 synthetic-smoke-fixture note (`exec`'s
// single-run bias leg names the built-in stream's actual cycle count) —
// production-reached now, not test-only (contrast the `#[cfg(test)]`
// imports around it, which lost their production callers to #295).
use aura_runner::member::r_sma_prices;
// The shared `DataSource` data provider lives in `aura_runner::family`
// (#295 Task 8) — its production caller is `aura_runner::reproduce`;
// `showcase_prices`/`synthetic_walk_sources` are its synthetic stream
@@ -1300,6 +1305,13 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
eprintln!("aura: {}", panic_refusal_prose(&msg));
std::process::exit(1);
});
// #324 (comment 4501): this leg always runs `RunData::Synthetic` (no
// direct-blueprint exec ever binds real data), so a validating
// caller reading zero trades here as "broken strategy" needs telling
// it never touched real data.
if report.metrics.r.as_ref().map_or(0, |m| m.n_trades) == 0 {
crate::diag::note_synthetic_smoke_zero_trades(r_sma_prices().len());
}
println!("{}", report.to_json());
} else if has_tap {
if !overrides.is_empty() {