feat(cli): record declared taps on the single-run path (#282)
The payoff: a hand-authored blueprint's declared taps become recorded output from one `aura run`. run_signal_r binds each tap (hoisted into flat.taps after wrap_r nests the signal) to a fresh Recorder before bootstrap, drains the series after the run, and persists each as a ColumnarTrace via the existing env.trace_store() — the same trace surface the campaign path feeds, so the existing chart tooling reads them. Duplicate tap names are refused (the caller-owned DuplicateBind dedup) before anything persists. A tap-free run writes no trace store — byte-identical to today. Sweep/reduce leaves taps unbound (run_blueprint_member untouched). Covered end-to-end: a persisted tap series, the tap-free no-write guard, and the duplicate-name refusal. refs #282
This commit is contained in:
@@ -54,17 +54,18 @@ use aura_std::Scale;
|
||||
// `Delay`/`Scale` above.
|
||||
#[cfg(test)]
|
||||
use aura_std::{Add, Gt, Latch, Mul, Sqrt};
|
||||
// `Bias`/`Ema`/`Sma`/`ColumnarTrace` are only reached from the test module; the
|
||||
// imports are test-only, mirroring `Delay`/`Scale` above.
|
||||
#[cfg(test)]
|
||||
// `ColumnarTrace` is now also reached from `run_signal_r`'s tap-persistence
|
||||
// path, so it is a production import (no longer test-only).
|
||||
use aura_engine::ColumnarTrace;
|
||||
// `Bias`/`Ema`/`Sma` are only reached from the test module; the imports are
|
||||
// test-only, mirroring `Delay`/`Scale` above.
|
||||
#[cfg(test)]
|
||||
use aura_std::{Bias, Ema, Sma};
|
||||
#[cfg(test)]
|
||||
use aura_std::{ConstantCost, VolSlippageCost};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
/// The pip size the built-in *synthetic* families (sweep/walkforward/mc over a
|
||||
@@ -1716,10 +1717,12 @@ fn resolve_run_data(
|
||||
/// run over `data`, and build the RunReport (manifest carries topology_hash).
|
||||
/// The single construction+run path shared by the `aura run <blueprint.json>` CLI
|
||||
/// arm and its bit-identical test.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn run_signal_r(
|
||||
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &project::Env,
|
||||
) -> RunReport {
|
||||
let topo = topology_hash(&signal); // before signal is consumed
|
||||
let run_name = signal.name().to_string(); // before signal is consumed by `wrap_r`
|
||||
// The default binding (name defaults; `aura run` carries no campaign
|
||||
// overrides). Refusals are the established `aura: ` + exit-1 register.
|
||||
let binding = binding::resolve_binding(signal.name(), signal.input_roles(), &BTreeMap::new())
|
||||
@@ -1745,9 +1748,28 @@ fn run_signal_r(
|
||||
let (tx_req, _rx_req) = mpsc::channel();
|
||||
let (sources, window, pip_size) = resolve_run_data(&data, env, &binding);
|
||||
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding, None);
|
||||
let flat = wrapped
|
||||
let mut flat = wrapped
|
||||
.compile_with_params(params)
|
||||
.expect("signal binds + wraps to a valid harness");
|
||||
// Bind each declared tap to a fresh `Recorder` + channel, before bootstrap
|
||||
// — `flat.taps` already carries the signal's declared taps hoisted to the
|
||||
// root. Dedup is the caller's per `TapBindError::DuplicateBind`'s doc (the
|
||||
// engine keeps no cross-call state).
|
||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
||||
let mut tap_drains: Vec<(String, ScalarKind, mpsc::Receiver<(Timestamp, Vec<Scalar>)>)> = Vec::new();
|
||||
let declared: Vec<aura_engine::FlatTap> = flat.taps.clone();
|
||||
for tap in &declared {
|
||||
if !seen.insert(tap.name.clone()) {
|
||||
eprintln!("aura: {}", aura_engine::TapBindError::DuplicateBind { name: tap.name.clone() });
|
||||
std::process::exit(1);
|
||||
}
|
||||
let kind = flat.signatures[tap.node].output[tap.field].kind;
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let sig = Recorder::builder(vec![kind], Firing::Any, tx.clone()).schema().clone();
|
||||
flat.bind_tap(&tap.name, Box::new(Recorder::new(&[kind], Firing::Any, tx)), sig)
|
||||
.expect("declared tap binds (name from flat.taps, kind from its own signature)");
|
||||
tap_drains.push((tap.name.clone(), kind, rx));
|
||||
}
|
||||
let mut h = Harness::bootstrap(flat).expect("valid r-sma harness");
|
||||
// `sources` were opened via `resolve_run_data(&data, env, &binding)` against
|
||||
// this SAME `binding`, and `key_supply` keys them by that binding's own role
|
||||
@@ -1769,6 +1791,23 @@ fn run_signal_r(
|
||||
manifest.project = env.provenance();
|
||||
let mut metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
metrics.r = Some(summarize_r(&r_rows, &[]));
|
||||
// Drain + persist each declared tap's series, guarded so a tap-free
|
||||
// `aura run` stays byte-identical to today (no `runs/` write at all).
|
||||
// `manifest` is built above so the persisted `index.json` carries this
|
||||
// run's own provenance.
|
||||
if !tap_drains.is_empty() {
|
||||
let tap_traces: Vec<ColumnarTrace> = tap_drains
|
||||
.into_iter()
|
||||
.map(|(name, kind, rx)| {
|
||||
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.try_iter().collect();
|
||||
ColumnarTrace::from_rows(&name, &[kind], &rows)
|
||||
})
|
||||
.collect();
|
||||
env.trace_store().write(&run_name, &manifest, &tap_traces).unwrap_or_else(|e| {
|
||||
eprintln!("aura: writing tap traces failed: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
}
|
||||
RunReport { manifest, metrics }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user