Files
Aura/crates/aura-runner/src/runner.rs
T
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

802 lines
38 KiB
Rust

//! [`DefaultMemberRunner`]: the shipped `aura_campaign::MemberRunner`
//! implementation over the loaded-blueprint machinery, plus the disk-layout
//! writers that persist a campaign run's traces (#295).
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::{mpsc, Arc};
use aura_backtest::{point_from_params, summarize, summarize_r, RunReport};
use aura_campaign::{
CampaignOutcome, CellOutcome, CellSpec, MemberFault, MemberRunner,
};
use aura_core::{Scalar, ScalarKind, Timestamp};
use aura_engine::{blueprint_from_json, f64_field, ColumnarTrace};
use aura_ingest::{instrument_geometry, open_columns_window, unix_ms_to_epoch_ns};
use aura_registry::WriteKind;
use aura_research::{CampaignDoc, CostSpec};
use crate::axes::{bind_axes, raw_bound_overrides_of};
use crate::coverage::interior_gap_months;
use crate::member::{
blueprint_axis_probe, blueprint_axis_probe_reopened, key_supply, reopen_all,
run_blueprint_member, wrap_r, wrapped_bound_overrides_of, CostLeg,
};
use crate::project::Env;
use crate::translate::{cost_nodes_for, stop_rule_for_regime};
/// Map any byte outside the portable directory-name charset `[A-Za-z0-9._-]`
/// to `_`. The single source of filesystem-portability for an on-disk path
/// component (valid on Linux / Windows / macOS, also URL-path- and
/// cloud-sync-safe). Used by the campaign trace layout's per-cell/per-member
/// keys ([`campaign_cell_key`], [`member_trace_key`]) — a durable disk-layout
/// contract, not stdout presentation.
pub fn sanitize_component(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { c } else { '_' })
.collect()
}
/// Render a scalar value case-lessly: integers/timestamps as decimal digits,
/// bool as `true`/`false`, f64 via Rust's `Display` (decimal, shortest
/// round-trip, NO scientific notation). Case-less rendering is what keeps two
/// members of one family from ever differing only by letter case
/// (case-insensitive-FS safety). The single source for the trace layout's
/// member-key label AND the shell's `aura reproduce` rendering — the two must
/// stay byte-identical (#224).
pub fn render_value(v: &Scalar) -> String {
match v {
Scalar::I64(n) => n.to_string(),
Scalar::F64(x) => x.to_string(),
Scalar::Bool(b) => b.to_string(),
Scalar::Timestamp(t) => t.0.to_string(),
}
}
/// The shipped harness/data binding seam for `aura_campaign::execute`: members
/// run through the shipped loaded-blueprint machinery (`wrap_r` reduce-mode
/// via `run_blueprint_member`) over windowed real M1 close bars
/// (`M1FieldSource::open_window` — the ms→ns crossing happens at exactly this
/// seam, via `unix_ms_to_epoch_ns`). All refusals are member faults for the
/// library to surface; never a process exit inside a sweep worker.
pub struct DefaultMemberRunner<'a> {
env: &'a Env,
server: Arc<data_server::DataServer>,
/// The campaign's `data.bindings` overrides (role -> column), threaded
/// into per-member binding resolution (#231: rebind per campaign without
/// touching the blueprint's content id).
bindings: BTreeMap<String, String>,
/// The campaign's cost model (#234), threaded into every member run via
/// `cost_nodes_for` (empty = zero costs, today's graph).
cost: Vec<CostSpec>,
}
impl<'a> DefaultMemberRunner<'a> {
pub fn new(
env: &'a Env,
server: Arc<data_server::DataServer>,
bindings: BTreeMap<String, String>,
cost: Vec<CostSpec>,
) -> Self {
Self { env, server, bindings, cost }
}
/// The runner's own data server, shared with the trace-persistence tail
/// (`persist_campaign_traces` re-runs members over the same archive).
pub fn server(&self) -> Arc<data_server::DataServer> {
Arc::clone(&self.server)
}
}
impl MemberRunner for DefaultMemberRunner<'_> {
fn run_member(
&self,
cell: &CellSpec,
params: &[(String, Scalar)],
window_ms: (i64, i64),
) -> Result<RunReport, MemberFault> {
// The member's blueprint, loaded RAW (no re-open yet) — the SAME
// reload the probe below wraps, so `bound_param_space()` here and
// the probe's `param_space()` after `blueprint_axis_probe_reopened`
// resolve against one topology.
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible");
// The un-reopened wrapped OPEN space (#246) — the SAME probe the
// sweep verbs resolve against (single-sourced in main.rs; identical
// `false, true, None` wrap) — derives which of this cell's RAW axis
// names re-open a bound param before the real, reopened probe/reload
// are built: probe and member reload must re-open identically so
// `params` resolves against one space.
let raw_space = blueprint_axis_probe(&cell.blueprint_json, self.env).param_space();
let param_names: Vec<String> = params.iter().map(|(n, _)| n.clone()).collect();
let overrides = raw_bound_overrides_of(&param_names, &raw_space, &signal);
let space =
blueprint_axis_probe_reopened(&cell.blueprint_json, self.env, &overrides)
.param_space();
let point = bind_axes(&space, &cell.strategy_id, params)?;
let signal = reopen_all(signal, &overrides);
// The member's resolved input binding (campaign data.bindings
// overrides win over name defaults). A refusal is a member fault,
// never a process exit.
let binding =
crate::binding::resolve_binding(&cell.strategy_id, signal.input_roles(), &self.bindings)
.map_err(MemberFault::Bind)?;
// Real windowed data — geometry BEFORE bar data (the shipped pre-data
// refusal order of `open_real_source`), both as member faults.
let geo = instrument_geometry(&self.server, &cell.instrument).ok_or_else(|| {
MemberFault::Run(format!(
"no recorded geometry for symbol '{}' at {} — refusing to run a \
real instrument with a guessed pip",
cell.instrument,
self.env.data_path()
))
})?;
let from = unix_ms_to_epoch_ns(window_ms.0);
let to = unix_ms_to_epoch_ns(window_ms.1);
// One source per resolved binding column, canonical order (the same
// order `wrap_r` declares the roles in — the shared plan).
let sources = match open_columns_window(
&self.server,
&cell.instrument,
Some(from),
Some(to),
&binding.columns(),
) {
Some(s) => s,
// No archived file overlaps the window at all.
None => {
return Err(MemberFault::NoData {
instrument: cell.instrument.clone(),
window_ms,
});
}
};
// A window that overlaps a file but holds zero matching bars yields
// sources whose first peek is None (open_window's documented contract)
// — the same no-data condition (all columns decode the same bars, so
// probing the first source covers the set).
if aura_engine::Source::peek(sources[0].as_ref()).is_none() {
return Err(MemberFault::NoData {
instrument: cell.instrument.clone(),
window_ms,
});
}
// The shipped member recipe (wrap, bind, run, summarize):
// `run_blueprint_member` verbatim. Seed 0 (seed-free real-data runs),
// topology_hash = the strategy's content id, project provenance
// stamped inside the helper.
let stop = stop_rule_for_regime(cell.regime);
let mut report = run_blueprint_member(
signal,
&point,
&space,
sources,
(from, to),
0,
geo.pip_size,
&cell.strategy_id,
self.env,
stop,
&binding,
&self.cost,
&cell.instrument,
);
report.manifest.instrument = Some(cell.instrument.clone());
Ok(report)
}
/// #272: derive the cell's archive coverage from the #264 primitives —
/// `None` when the archive has no file for the instrument at all (the
/// `NoData` member fault's job, not coverage) or when the effective
/// evaluated span covers the requested window exactly with no interior
/// gap month. One call per cell (not per member): coverage is a property
/// of the cell's (instrument, window), never a swept param point.
fn window_coverage(&self, cell: &CellSpec) -> Option<aura_registry::CellCoverage> {
let data_path = PathBuf::from(self.env.data_path());
let months = aura_ingest::list_m1_months(&data_path, &cell.instrument);
if months.is_empty() {
return None; // no archive at all is the NoData fault's job, not coverage
}
let (eff_from_ts, eff_to_ts) = aura_ingest::archive_extent(
&self.server,
&data_path,
&cell.instrument,
Some(cell.window_ms.0),
Some(cell.window_ms.1),
)?;
let eff_from = aura_ingest::epoch_ns_to_unix_ms(eff_from_ts);
let eff_to = aura_ingest::epoch_ns_to_unix_ms(eff_to_ts);
let gap_months = interior_gap_months(&months, cell.window_ms);
if eff_from == cell.window_ms.0 && eff_to == cell.window_ms.1 && gap_months.is_empty() {
return None; // fully covered — nothing to annotate
}
Some(aura_registry::CellCoverage {
effective_from_ms: eff_from,
effective_to_ms: eff_to,
gap_months,
})
}
}
/// Which drained wrap-convention channel a requested tap persists from on a
/// campaign trace re-run. The routing follows the wrap-convention channels:
/// equity <- the SimBroker equity recorder (tx_eq), exposure <- the Bias
/// recorder (tx_ex), r_equity <- the cum_realized_r + unrealized_r recorder
/// (tx_req), net_r_equity <- the cost leg's LinComb(4) net-curve recorder
/// (tx_net, #234) — produced only when the campaign document carries a cost
/// block (the caller's producibility check).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TapChannel {
Equity,
Exposure,
REquity,
Net,
}
/// Route one requested tap of the closed vocabulary to its drained channel;
/// `None` marks a name outside the vocabulary (unreachable — `validate_campaign`
/// refuses it — and mapped defensively rather than guessed at).
pub fn tap_channel(tap: &str) -> Option<TapChannel> {
// The emit_vocabulary debug_assert's twin: every vocabulary tap must be
// routable here — a future fifth vocabulary entry fails loudly instead of
// silently skipping.
debug_assert!(
aura_research::tap_vocabulary()
.iter()
.all(|t| matches!(t.id, "equity" | "exposure" | "r_equity" | "net_r_equity")),
"tap_vocabulary drifted from the channels persist_campaign_traces routes"
);
match tap {
"equity" => Some(TapChannel::Equity),
"exposure" => Some(TapChannel::Exposure),
"r_equity" => Some(TapChannel::REquity),
"net_r_equity" => Some(TapChannel::Net),
_ => None,
}
}
/// 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 +
/// doc-positional window ordinal, sanitized to one portable path component.
/// `regime_ordinal` (#219/#212) discriminates two risk regimes over the same
/// (strategy, instrument, window) cell so their traces never collide: the
/// default regime (ordinal 0) keeps the pre-#219 key unchanged (no stored
/// trace dir is renamed by this change), a non-default regime appends
/// `-r{ordinal}`.
pub fn campaign_cell_key(
strategy: &str,
instrument: &str,
window_ordinal: usize,
regime_ordinal: usize,
) -> String {
let strategy8 = strategy.get(..8).unwrap_or(strategy);
let base = format!("{strategy8}-{instrument}-w{window_ordinal}");
let base =
if regime_ordinal == 0 { base } else { format!("{base}-r{regime_ordinal}") };
sanitize_component(&base)
}
/// The per-member subdirectory key under a swept cell's trace dir (#224):
/// the member's own manifest params label — the exact `"name=value, ..."`
/// join `aura reproduce` prints — sanitized for filesystem use (a raw label
/// carries `=`/`, ` which `sanitize_component` maps to `_`). The member's
/// ordinal into the family is the fallback when the label is empty (a closed
/// blueprint / a monte-carlo seed member carries no tuning params).
pub fn member_trace_key(report: &RunReport, ordinal: usize) -> String {
let label = report
.manifest
.params
.iter()
.map(|(n, v)| format!("{n}={}", render_value(v)))
.collect::<Vec<_>>()
.join(", ");
let key = if label.is_empty() { ordinal.to_string() } else { label };
sanitize_component(&key)
}
/// The per-cell member fan-out (#224): a nominee cell writes its one trace
/// directly at `<cell_key>/` (`None` subdir key, unchanged since 0109); a
/// no-nominee cell with a completed terminal family (the selection-free sweep
/// shape) writes every one of that family's members under its own
/// `<cell_key>/<member_key>/` subdirectory instead of silently dropping every
/// member but a (non-existent) nominee. An empty return (no nominee AND no
/// non-empty terminal family) is the caller's cue to skip the cell loudly.
/// Pure over the executed outcome's own `CellOutcome`, so it is unit-testable
/// without booting a real archive run.
pub fn cell_member_fanout(cell_out: &CellOutcome) -> Vec<(Option<String>, &RunReport)> {
match &cell_out.nominee {
Some((_, nominee_report)) => vec![(None, nominee_report)],
None => match cell_out.families.last() {
Some(fam) if !fam.reports.is_empty() => fam
.reports
.iter()
.enumerate()
.map(|(i, r)| (Some(member_trace_key(r, i)), r))
.collect(),
_ => Vec::new(),
},
}
}
/// Persist the requested `persist_taps` for every cell under
/// `traces/<trace_name>/<cell_key>/` (0109, #201 d5; #224): a cell with a
/// nominee (walkforward/generalize/mc — selection-bearing pipelines) writes
/// its single trace directly at `<cell_key>/`, unchanged since 0109. A cell
/// with NO nominee but a completed terminal family (a selection-free sweep,
/// #224's headline: "each swept member's tap series") writes EVERY member of
/// that family under its own `<cell_key>/<member_key>/` subdirectory — never
/// narrowing to one nominated member, which would silently drop the others
/// the sweep actually produced. Each written member is independently re-run
/// once in non-reduce trace mode over its own recorded `manifest.window`,
/// asserting the re-run METRICS equal the recorded member metrics (the C1
/// drift alarm — manifest fields are fresh-context and not compared), and
/// writes the requested-AND-producible taps through the sweep verbs'
/// member-dir mechanism: a slash-joined `"{name}/{key}"` handed to
/// `TraceStore::write` (the slash-joined member-dir layout the sweep verbs
/// established). The written index manifest is the RECORDED
/// member's — the trace documents that run's provenance, not a re-derived
/// fresh-context one. Loud stderr per no-candidate cell (neither a nominee
/// nor a non-empty terminal family) and ONCE per unproducible requested tap
/// (producibility is run-configuration-level, not per-cell); one summary
/// line at the end. Every `Err` is a refusal `campaign_cmd` renders as
/// `aura: {msg}` + exit 1.
pub fn persist_campaign_traces(
trace_name: &str,
taps: &[String],
outcome: &CampaignOutcome,
campaign: &CampaignDoc,
strategies: &[(String, String)],
server: &Arc<data_server::DataServer>,
env: &Env,
) -> Result<(), String> {
let store = env.trace_store();
store
.ensure_name_free(trace_name, WriteKind::Family)
.map_err(|e| e.to_string())?;
// Requested ∩ producible, in request order. `net_r_equity` is producible
// exactly when the document carries a cost block (#234); the skip notice
// names the remedy — the tap is optional presentation, not a refusal.
// When nothing producible was requested, no member dir is written and the
// summary honestly reports 0 tap(s).
let mut routed: Vec<(&str, TapChannel)> = Vec::new();
for tap in taps {
match tap_channel(tap) {
Some(TapChannel::Net) if campaign.cost.is_empty() => eprintln!(
"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!("{}", tap_not_produced_note(tap)),
}
}
// `outcome.cells` is index-aligned with `outcome.record.cells` by
// construction: `execute` pushes both in the same cell-loop iteration.
let mut persisted_cells = 0usize;
for (cell_rec, cell_out) in outcome.record.cells.iter().zip(&outcome.cells) {
// A nominee cell writes its one trace directly at `<cell_key>/`
// (`None` subdir key, unchanged since 0109); a no-nominee cell with a
// completed terminal family (the selection-free sweep shape, #224)
// writes every one of that family's members under its own
// `<cell_key>/<member_key>/` subdirectory instead of silently
// dropping every member but a (non-existent) nominee.
let members = cell_member_fanout(cell_out);
if members.is_empty() {
eprintln!(
"{}",
no_nominee_note(&cell_rec.strategy, &cell_rec.instrument, cell_rec.window_ms)
);
continue;
}
if routed.is_empty() {
continue;
}
// The cell key is doc-derived: the window ordinal is the POSITION
// of the cell's window in campaign.data.windows (mirroring the
// family-name convention), never a loop counter re-derived here.
let window_ordinal = campaign
.data
.windows
.iter()
.position(|w| (w.from_ms, w.to_ms) == cell_rec.window_ms)
.ok_or_else(|| {
format!(
"cell window [{}, {}] is not one of campaign.data.windows",
cell_rec.window_ms.0, cell_rec.window_ms.1
)
})?;
let cell_key = campaign_cell_key(
&cell_rec.strategy,
&cell_rec.instrument,
window_ordinal,
cell_rec.regime_ordinal,
);
// The cell's blueprint: the run's own index-aligned resolution, by
// content id (exactly the bytes the executor's members ran).
let (_, blueprint_json) = strategies
.iter()
.find(|(id, _)| *id == cell_rec.strategy)
.ok_or_else(|| {
format!(
"cell strategy {} is not among the run's resolved strategies",
cell_rec.strategy
)
})?;
// The #246 override set (silent variant, `reproduce_family_in`'s
// recipe): re-derived from the cell's own recorded manifest param
// names, against a raw probe space + raw strategy load — the members
// of one cell share the same knob NAMES (only values differ), so this
// is safe to compute once, cell-invariant, from the first member.
let raw_space = blueprint_axis_probe(blueprint_json, env).param_space();
let raw_signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible");
let recorded: Vec<String> = members
.first()
.map(|(_, r)| r.manifest.params.iter().map(|(n, _)| n.clone()).collect())
.unwrap_or_default();
let overrides = wrapped_bound_overrides_of(&recorded, &raw_space, &raw_signal);
// Cell-invariant across every member of this cell (the wrapped param
// space depends only on the cell's blueprint; the resolved geometry
// only on the cell's instrument) — computed once here rather than
// redundantly inside the member loop below.
let space =
blueprint_axis_probe_reopened(blueprint_json, env, &overrides).param_space();
// Same resolved pip the member ran at (`DefaultMemberRunner::run_member`'s
// `geo.pip_size`), else the C1 drift-alarm equality below would trip
// spuriously on a re-run computed at a different (default) pip.
let geo = instrument_geometry(server, &cell_rec.instrument).ok_or_else(|| {
format!(
"no recorded geometry for symbol '{}' at {} — refusing to re-run a \
real instrument with a guessed pip",
cell_rec.instrument,
env.data_path()
)
})?;
for (member_subdir, member_report) in members.iter().cloned() {
// Re-run the member, non-reduce: the SAME member the executor ran
// (same wrapped space, same params, same window, seed-free real
// data), mirroring `DefaultMemberRunner::run_member` with the
// reduce fold off so the per-cycle tap streams exist. The window
// is the member report's own `manifest.window` — already
// epoch-ns (`run_blueprint_member` stamped the post-seam
// bounds), so no second ms->ns crossing here.
let point = point_from_params(&space, &member_report.manifest.params)?;
let (from, to) = member_report.manifest.window;
let no_data = || {
format!(
"no data for instrument {} in the member window [{}, {}] (epoch-ns)",
cell_rec.instrument, from.0, to.0
)
};
let signal = reopen_all(
blueprint_from_json(blueprint_json, &|t| env.resolve(t))
.expect("stored blueprint passed the referential gate; reload is infallible"),
&overrides,
);
// Campaign data.bindings overrides win over name defaults — the
// SAME resolution the member ran under, so the C1 drift alarm
// compares like with like.
let binding = crate::binding::resolve_binding(
&cell_rec.strategy,
signal.input_roles(),
&campaign.data.bindings,
)?;
let sources = open_columns_window(
server,
&cell_rec.instrument,
Some(from),
Some(to),
&binding.columns(),
)
.ok_or_else(no_data)?;
if aura_engine::Source::peek(sources[0].as_ref()).is_none() {
return Err(no_data());
}
// The cell's OWN regime (#219/#212), not the hardcoded default: the
// recorded member ran under `cell_rec.regime`, bound through the same
// `stop_rule_for_regime` helper `DefaultMemberRunner::run_member`
// uses, so the two sites cannot drift apart again (the #219
// divergence class) and the re-run binds the same stop the C1
// drift alarm below relies on.
let stop = stop_rule_for_regime(cell_rec.regime);
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
let (tx_req, rx_req) = mpsc::channel();
// The campaign's OWN cost model (#234), bound the same way
// `DefaultMemberRunner::run_member` binds it (via
// `cost_nodes_for`): empty = no leg, so a cost-less campaign's
// re-run is unchanged. A non-empty model MUST be threaded here
// too — the recorded member ran net (#295's
// `run_blueprint_member`); leaving this re-run gross would trip
// the C1 drift alarm below on every legitimate costed campaign,
// not just on a real divergence.
let (tx_cost, rx_cost) = mpsc::channel();
let (tx_net, rx_net) = mpsc::channel();
let cost_leg = (!campaign.cost.is_empty()).then(|| CostLeg {
nodes: cost_nodes_for(&campaign.cost, &cell_rec.instrument),
tx_cost,
tx_net,
});
let mut h =
wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, geo.pip_size, &binding, cost_leg)
.bootstrap_with_cells(&point)
.expect("the member's point re-bootstraps (it already ran this realization)");
// The recorded member already ran this realization once (see the
// `.expect` immediately above); `sources` are re-opened against the
// SAME `binding` this trace re-run resolved, so a `SourceBindError`
// here can only be an internal wiring inconsistency, not a fresh
// user input mistake. Panic like the sibling bootstrap invariant
// instead of a process-exit dressed as a refusal message.
h.run_bound(key_supply(&binding, sources))
.expect("sources re-opened against `binding` key-match that binding's own roles by construction");
// Drain ALL SIX channels (`run_signal_r` leaves req undrained; the
// trace path must not): eq/ex/req/net feed the taps, r+cost feed
// the metrics.
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
let req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
let cost_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_cost.try_iter().collect();
let net_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_net.try_iter().collect();
// The C1 drift alarm: metrics equality against the recorded
// member. The reduce-mode fold shares its arithmetic with this
// non-reduce reduction (SeriesFold via `summarize`; GatedRecorder
// emits exactly the rows `summarize_r`'s ledger reads), so equality
// is bit-exact.
let mut rerun_metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
rerun_metrics.r = Some(summarize_r(&r_rows, &cost_rows));
if rerun_metrics != member_report.metrics {
return Err(format!(
"trace re-run diverged from the recorded member (C1 violation): cell \
{cell_key} of trace {trace_name} does not reproduce its recorded \
metrics; refusing to persist a silently-wrong trace"
));
}
let traces: Vec<ColumnarTrace> = routed
.iter()
.map(|&(tap, ch)| {
let rows: &[(Timestamp, Vec<Scalar>)] = match ch {
TapChannel::Equity => &eq_rows,
TapChannel::Exposure => &ex_rows,
TapChannel::REquity => &req_rows,
TapChannel::Net => &net_rows,
};
ColumnarTrace::from_rows(tap, &[ScalarKind::F64], rows)
})
.collect();
let write_key = match &member_subdir {
Some(sub) => format!("{trace_name}/{cell_key}/{sub}"),
None => format!("{trace_name}/{cell_key}"),
};
store
.write(&write_key, &member_report.manifest, &traces)
.map_err(|e| e.to_string())?;
}
persisted_cells += 1;
}
eprintln!(
"aura: traces persisted: {trace_name} ({} tap(s) x {persisted_cells} cell(s))",
routed.len()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use aura_campaign::StageFamily;
use aura_engine::RunManifest;
/// A minimal `RunReport` fixture carrying exactly the manifest params
/// `member_trace_key`/`cell_member_fanout` read; the metrics are an empty
/// `summarize`, irrelevant to either function under test.
fn report_with_params(params: Vec<(String, Scalar)>) -> RunReport {
RunReport {
manifest: RunManifest {
commit: "test".to_string(),
params,
defaults: Vec::new(),
window: (Timestamp(0), Timestamp(0)),
seed: 0,
broker: "test".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: summarize(&[], &[]),
}
}
#[test]
/// 0109: the campaign cell key is content-derived (the `member_key`
/// discipline — never a runtime ordinal): strategy content-id prefix +
/// instrument + doc-positional window ordinal, one portable component.
fn campaign_cell_key_is_content_derived() {
let strategy = "bb34aa55".repeat(8); // a 64-hex content id
assert_eq!(campaign_cell_key(&strategy, "GER40", 0, 0), "bb34aa55-GER40-w0");
}
#[test]
/// A non-portable instrument byte maps to `_` (the shared
/// `sanitize_component` charset) — one path component, never a nested
/// path or an invalid directory name.
fn campaign_cell_key_sanitizes_non_portable_bytes() {
let strategy = "0123abcd".repeat(8);
assert_eq!(campaign_cell_key(&strategy, "GER/40", 3, 0), "0123abcd-GER_40-w3");
}
#[test]
/// A non-default regime_ordinal (#219/#212) appends `-r{ordinal}` so two
/// regimes over the same (strategy, instrument, window) cell land in
/// distinct trace dirs; ordinal 0 (the default regime) is covered above
/// and stays unsuffixed for pre-#219 key stability.
fn campaign_cell_key_appends_regime_suffix_for_non_default_ordinal() {
let strategy = "bb34aa55".repeat(8);
assert_eq!(campaign_cell_key(&strategy, "GER40", 0, 2), "bb34aa55-GER40-w0-r2");
}
#[test]
/// #224: `member_trace_key` renders the SAME `"name=value, ..."` join
/// `aura reproduce` prints for a member's params (single-sourced label
/// format), then sanitizes it for filesystem use — a raw label's `=` and
/// `, ` separators are hostile to a path component and must map to `_`
/// (the shared `sanitize_component` charset), never survive verbatim.
fn member_trace_key_mirrors_reproduce_label_and_sanitizes_it() {
let report = report_with_params(vec![
("fast".to_string(), Scalar::i64(2)),
("slow".to_string(), Scalar::i64(4)),
]);
// The raw reproduce-format label is "fast=2, slow=4"; sanitizing maps
// each of `=`, `,`, ` ` individually to `_` (so the ", " separator
// becomes two underscores, not one).
assert_eq!(member_trace_key(&report, 0), "fast_2__slow_4");
}
#[test]
/// #224: a member with no tuning params (a closed blueprint, or a
/// monte-carlo seed member) renders an empty reproduce label, so
/// `member_trace_key` falls back to the member's own ordinal into the
/// family — never an empty (invalid) path component.
fn member_trace_key_falls_back_to_the_ordinal_when_params_are_empty() {
let report = report_with_params(Vec::new());
assert_eq!(member_trace_key(&report, 3), "3");
}
#[test]
/// #224: two members with different params must land at distinct keys —
/// the whole point of per-member subdirectories is that the sweep's
/// members never collide into one on-disk slot.
fn member_trace_key_differs_across_members_with_different_params() {
let a = report_with_params(vec![("length".to_string(), Scalar::i64(10))]);
let b = report_with_params(vec![("length".to_string(), Scalar::i64(20))]);
assert_ne!(member_trace_key(&a, 0), member_trace_key(&b, 1));
}
/// A no-nominee `CellOutcome` whose terminal family holds two members
/// (a plain selection-free sweep, #224's headline shape).
fn two_member_family_cell() -> CellOutcome {
CellOutcome {
families: vec![StageFamily {
stage: 0,
block: "std::sweep",
family_id: "fam".to_string(),
reports: vec![
report_with_params(vec![("length".to_string(), Scalar::i64(10))]),
report_with_params(vec![("length".to_string(), Scalar::i64(20))]),
],
}],
selections: Vec::new(),
nominee: None,
}
}
#[test]
/// #224: a no-nominee cell with a non-empty terminal family fans out to
/// ONE (subdir, report) pair PER member — never narrowing to a single
/// nominated member, which would silently drop every other member the
/// sweep actually produced. Each pair carries a distinct `Some(key)`
/// subdir (never `None`, which is reserved for the nominee case).
fn cell_member_fanout_yields_one_entry_per_family_member() {
let cell = two_member_family_cell();
let members = cell_member_fanout(&cell);
assert_eq!(members.len(), 2, "one dir per member, not one nominee");
let Some(key0) = &members[0].0 else { panic!("member 0 must carry a subdir key") };
let Some(key1) = &members[1].0 else { panic!("member 1 must carry a subdir key") };
assert_ne!(key0, key1, "distinct members must land at distinct subdirs");
}
#[test]
/// #224: a nominee cell (walkforward/generalize/mc) still writes its one
/// trace directly at `<cell_key>/` — a `None` subdir key, unchanged since
/// 0109 — regardless of how many stage families it also carries.
fn cell_member_fanout_prefers_the_nominee_with_no_subdir() {
let mut cell = two_member_family_cell();
let nominee_report = report_with_params(vec![("length".to_string(), Scalar::i64(30))]);
cell.nominee = Some((vec![("length".to_string(), Scalar::i64(30))], nominee_report));
let members = cell_member_fanout(&cell);
assert_eq!(members.len(), 1, "the nominee is the cell's single trace");
assert!(members[0].0.is_none(), "the nominee writes directly at <cell_key>/, no subdir");
}
#[test]
/// #224: a cell with neither a nominee nor a non-empty terminal family
/// (every family is empty, or there are none) fans out to nothing — the
/// caller's cue to skip the cell loudly rather than write an empty dir.
fn cell_member_fanout_is_empty_with_no_nominee_and_no_members() {
let cell = CellOutcome { families: Vec::new(), selections: Vec::new(), nominee: None };
assert!(cell_member_fanout(&cell).is_empty());
}
#[test]
/// #234 — a DELIBERATE pin move (was: `net_r_equity` routes to `None` on
/// the cost-free wrap): the full closed tap vocabulary now routes,
/// `net_r_equity` to the cost leg's net-curve channel. Producibility
/// (does THIS run carry a cost model?) is the caller's check, not the
/// routing's.
fn tap_channel_routes_the_full_vocabulary() {
assert_eq!(tap_channel("equity"), Some(TapChannel::Equity));
assert_eq!(tap_channel("exposure"), Some(TapChannel::Exposure));
assert_eq!(tap_channel("r_equity"), Some(TapChannel::REquity));
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}");
}
}