feat(aura-runner, aura-cli): a run reports the trace handle it recorded under

A single run persisted its taps and then said nothing about where: the only
way to learn the directory name was `ls runs/traces/`, and the chart intake's
not-found refusal pointed at "the handle a sweep/walk-forward/campaign run
printed" — two verbs retired with #319, and a single run printed no handle at
all. A caller holding a family id from a families listing was stuck: that id
is not a trace handle, and nothing said so.

The handle now rides BESIDE the report, not inside it. Both declared-tap entry
points return `RunOutcome { report, skipped, trace_name }` in place of the
`(report, skipped)` pair, and the CLI composes report + handle into one stdout
object through a `serde(flatten)` wrapper. `RunReport`, `MeasurementReport`,
`RunManifest` and the registry's compat mirror are untouched, so no stored
record shape moves and a tap-free run's line stays byte-identical.

Placement is the load-bearing decision, and it follows a precedent rather than
inventing one: the report is the durable C18 run record — its manifest states
what the run *was* — while a trace directory is where its output went. The
project settled this exact question one cycle earlier for the sibling value,
where the unbound-tap names ride beside the report so the CLI, not the
library, prints the note (C27/#297). An embedding host gets the handle as a
value, never as text to parse back.

The chart refusal gains a second arm. A family id
(`{campaign8}-{strategy_ordinal}-{instrument}-w..-r..-s..-{run}`) is
recognised syntactically — the trailing segment shape plus an 8-hex head, the
form `derive_trace_name` mints — and answered with the campaign's REAL
recorded handles, filtered to those `chart` would accept, suggesting one only
when exactly one matches. It never derives a handle by truncating the id: the
id's second segment counts strategies while the handle's counts runs, and one
campaign run mints family ids at several strategy ordinals whose traces all
live under that single run's handle. `TraceStore::names()` supplies the
enumeration, keeping trace file I/O in the store where C22 puts it.

Verification caught three errors worth recording. The first spec draft claimed
the handle was the id's leading pair — refuted by a green test where a run-0
campaign mints ordinal-1 ids. The second draft's replacement refusal said a
campaign run "prints one handle per family"; it prints one per run. Review
then found the `NotFound` filter untested — deleting it left the suite green —
so the case now has a test that fails without it.

closes #309
This commit is contained in:
2026-07-26 23:10:24 +02:00
parent 9636b00314
commit 9221bcd167
10 changed files with 448 additions and 31 deletions
+118 -13
View File
@@ -433,12 +433,42 @@ fn render_chart_by_kind(
std::process::exit(1);
}
NameResolution::None => {
eprintln!(
"aura: no recorded run or family '{name}' under runs/traces \
(check the handle a sweep/walk-forward/campaign run printed for a typo — \
a trace is produced by `aura exec --tap <NODE.FIELD>=<FOLD>` on a blueprint \
or a campaign's `presentation.persist_taps` section, not by naming a handle here)"
);
match family_id_campaign_prefix(name) {
Some(prefix) => {
// Say what was found, never that nothing exists: an
// unreadable store also yields an empty list, so
// "none" would be a claim this path cannot support.
let handles = recorded_handles_with_prefix(prefix, store);
if handles.is_empty() {
eprintln!(
"aura: no recorded run or family '{name}' under runs/traces — that \
looks like a family id, not a trace handle. The handle is the \
`trace_name` the campaign run printed; this id's second segment \
counts strategies while the handle's counts runs, so cutting the \
id down is not a way to derive it."
);
} else {
eprintln!(
"aura: no recorded run or family '{name}' under runs/traces — that \
looks like a family id, not a trace handle. Recorded trace handles \
for campaign '{prefix}': {}.",
handles.join(", ")
);
if handles.len() == 1 {
eprintln!("Did you mean: aura chart {}", handles[0]);
}
}
}
None => {
eprintln!(
"aura: no recorded run or family '{name}' under runs/traces \
(both a single run and a campaign run print their handle as `trace_name` \
on stdout — one per run — so check that value for a typo. A trace is \
produced by `aura exec --tap <NODE.FIELD>=<FOLD>` on a blueprint \
or a campaign's `presentation.persist_taps` section, not by naming a handle here)"
);
}
}
std::process::exit(1);
}
},
@@ -463,6 +493,55 @@ enum NameResolution {
None,
}
/// A family id (`{campaign8}-{strategy_ordinal}-{instrument}-w<k>-r<k>-s<k>-{run}`,
/// minted in `aura_campaign::exec` and printed under the `family_id` key) is
/// NOT a trace handle (`{campaign8}-{run}`): the second segment differs in
/// meaning, and one campaign run mints family ids at several strategy
/// ordinals whose traces all live under that one run's handle. Recognition is
/// purely syntactic: the trailing `-w<d>-r<d>-s<d>-<d>` suffix AND a leading
/// segment shaped like the campaign prefix `derive_trace_name` mints — the
/// 8-char head of a 64-hex campaign id. Requiring the hex head keeps an
/// ordinary run name that merely ends in those segments (root names may
/// contain hyphens) from being misread as a family id.
fn family_id_campaign_prefix(name: &str) -> Option<&str> {
let mut segs = name.rsplitn(5, '-');
let member = segs.next()?;
let s = segs.next()?;
let r = segs.next()?;
let w = segs.next()?;
let head = segs.next()?;
let numeric_after = |seg: &str, tag: char| {
seg.strip_prefix(tag).is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit()))
};
if member.is_empty()
|| !member.bytes().all(|b| b.is_ascii_digit())
|| !numeric_after(s, 's')
|| !numeric_after(r, 'r')
|| !numeric_after(w, 'w')
{
return None;
}
head.split('-')
.next()
.filter(|p| p.len() == 8 && p.bytes().all(|b| b.is_ascii_hexdigit()))
}
/// The recorded handles whose name starts with `prefix-`, sorted — filtered to
/// the ones `chart` would actually accept. Reads the store rather than the
/// filesystem: file I/O for traces belongs to `aura-registry::TraceStore`
/// (C22), not to this shell. The `name_kind` filter is what keeps a suggestion
/// honest: an interrupted run leaves a directory with no `index.json`, which
/// classifies as `NotFound`, and suggesting it would send the caller into the
/// refusal they just came from.
fn recorded_handles_with_prefix(prefix: &str, store: &aura_registry::TraceStore) -> Vec<String> {
store
.names()
.into_iter()
.filter(|n| n.strip_prefix(prefix).is_some_and(|rest| rest.starts_with('-')))
.filter(|n| !matches!(store.name_kind(n), aura_registry::NameKind::NotFound))
.collect()
}
/// Resolve a `chart` argument against `env.registry()`'s campaign-run records:
/// keep every record whose `trace_name` is `Some` (a family was actually
/// persisted for it) AND whose stored campaign document (`get_campaign`)
@@ -1198,6 +1277,26 @@ fn panic_refusal_prose(msg: &str) -> String {
}
}
/// One stdout line: the run record's own bytes plus the trace handle, when the
/// run recorded one. `flatten` is load-bearing — composing this through
/// `serde_json::Value` would re-sort every key (its map is key-ordered), so
/// the record's byte shape would change. With `flatten` the report serializes
/// exactly as `to_json` renders it and the handle is appended as one optional
/// trailing key.
#[derive(serde::Serialize)]
struct RunLine<'a, R: serde::Serialize> {
#[serde(flatten)]
report: &'a R,
#[serde(skip_serializing_if = "Option::is_none")]
trace_name: Option<&'a str>,
}
impl<R: serde::Serialize> RunLine<'_, R> {
fn to_json(&self) -> String {
serde_json::to_string(self).expect("a finite run line always serializes")
}
}
/// The `exec` blueprint leg (#319): a synthetic single run of a fully-bound
/// blueprint envelope — the sole surviving heir of the retired `dispatch_run`'s
/// `.json`-branch (and of the retired `run` verb it served, both gone with the
@@ -1344,7 +1443,7 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
// `SilencedPanic` + `catch_unwind`) and render it as a runtime-class
// refusal (exit 1, C14 partition) instead of an uncaught exit 101 —
// the input was well-formed argv; the value is what the node refuses.
let (report, skipped) = match aura_campaign::catch_member_panic(|| {
let outcome = match aura_campaign::catch_member_panic(|| {
run_signal_r(signal, &params, RunData::Synthetic, 0, env, tap_plan, Some(&base_topo))
})
.unwrap_or_else(|msg| {
@@ -1354,17 +1453,20 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
Ok(p) => p,
Err(e) => exit_on_runner_error(e),
};
for name in &skipped {
for name in &outcome.skipped {
crate::diag::note!("declared tap \"{name}\" unbound this run");
}
// #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 {
if outcome.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());
println!(
"{}",
RunLine { report: &outcome.report, trace_name: outcome.trace_name.as_deref() }.to_json()
);
} else if has_tap {
if !overrides.is_empty() {
eprintln!(
@@ -1388,15 +1490,18 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
std::process::exit(2);
}
let params: Vec<Scalar> = Vec::new();
let (report, skipped) =
let outcome =
match run_measurement(signal, &params, RunData::Synthetic, 0, env, tap_plan) {
Ok(p) => p,
Err(e) => exit_on_runner_error(e),
};
for name in &skipped {
for name in &outcome.skipped {
crate::diag::note!("declared tap \"{name}\" unbound this run");
}
println!("{}", report.to_json());
println!(
"{}",
RunLine { report: &outcome.report, trace_name: outcome.trace_name.as_deref() }.to_json()
);
} else {
eprintln!(
"aura: exec needs either a `bias` output (a strategy) or ≥1 \
+142
View File
@@ -4519,3 +4519,145 @@ fn show_resolves_a_unique_prefix_to_the_full_id_bytes() {
);
}
}
/// #309: the not-found remedy pointed at "the handle a sweep/walk-forward/
/// campaign run printed" — two of those verbs were retired with #319, and a
/// single run printed no handle at all. It must name the field a run actually
/// emits instead.
#[test]
fn chart_unknown_name_refusal_names_trace_name_not_retired_verbs() {
let cwd = temp_cwd("chart-unknown-no-retired-verbs");
let out = Command::new(BIN)
.args(["chart", "ghost"])
.current_dir(&cwd)
.output()
.expect("spawn chart ghost");
assert_eq!(out.status.code(), Some(1), "unknown name must still exit 1: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("trace_name"),
"the refusal must name the field a run emits its handle under: {stderr}"
);
assert!(
!stderr.contains("sweep") && !stderr.contains("walk-forward"),
"the refusal must not cite verbs retired by #319: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// #309: a caller who copied a family id out of a families listing must not
/// hit a dead end. The handle is NOT the id's leading pair — the id's second
/// segment is the strategy ordinal while the handle's is the campaign-run
/// counter — so the refusal lists the handles actually on disk.
#[test]
fn chart_family_id_lists_the_recorded_handles_of_that_campaign() {
let cwd = temp_cwd("chart-family-id");
// Two recorded handles of one campaign prefix, and deliberately NO
// `deadbeef-1`: a truncation rule would suggest exactly that.
for handle in ["deadbeef-0", "deadbeef-2"] {
let dir = cwd.join("runs/traces").join(handle).join("cell/member");
std::fs::create_dir_all(&dir).expect("fabricate a family fan-out");
std::fs::write(dir.join("index.json"), "{}").expect("write member index");
}
let out = Command::new(BIN)
.args(["chart", "deadbeef-1-GER40-w0-r0-s0-0"])
.current_dir(&cwd)
.output()
.expect("spawn chart on a family id");
assert_eq!(out.status.code(), Some(1), "must keep the intake's existing exit code: {:?}", out.status);
assert!(out.stdout.is_empty(), "nothing may be rendered: {:?}", String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("family id"), "the refusal must identify the shape: {stderr}");
assert!(
stderr.contains("deadbeef-0") && stderr.contains("deadbeef-2"),
"the recorded handles of that campaign must be listed: {stderr}"
);
assert!(
!stderr.contains("Did you mean"),
"with two candidates it must not suggest one: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// #309: with exactly one recorded handle under the prefix, the refusal
/// suggests it — the one-step fix the field test asked for.
#[test]
fn chart_family_id_suggests_the_single_recorded_handle() {
let cwd = temp_cwd("chart-family-id-single");
let dir = cwd.join("runs/traces/deadbeef-0/cell/member");
std::fs::create_dir_all(&dir).expect("fabricate a family fan-out");
std::fs::write(dir.join("index.json"), "{}").expect("write member index");
let out = Command::new(BIN)
.args(["chart", "deadbeef-0-GER40-w0-r0-s0-0"])
.current_dir(&cwd)
.output()
.expect("spawn chart on a family id");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Did you mean: aura chart deadbeef-0"),
"a single candidate must be suggested verbatim: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// #309: an index-less directory is NOT a chartable handle — an interrupted
/// run leaves one behind, and suggesting it would send the caller straight
/// back into the refusal they came from. The store lists it; the refusal must
/// filter it out. Without the `name_kind` filter this test fails by naming
/// `deadbeef-3` as the single candidate.
#[test]
fn chart_family_id_omits_an_index_less_directory_from_the_candidates() {
let cwd = temp_cwd("chart-family-id-indexless");
// A real, chartable family.
let good = cwd.join("runs/traces/deadbeef-0/cell/member");
std::fs::create_dir_all(&good).expect("fabricate a family fan-out");
std::fs::write(good.join("index.json"), "{}").expect("write member index");
// An interrupted run under the same campaign prefix: directory, no index.
std::fs::create_dir_all(cwd.join("runs/traces/deadbeef-3")).expect("fabricate a stub dir");
let out = Command::new(BIN)
.args(["chart", "deadbeef-1-GER40-w0-r0-s0-0"])
.current_dir(&cwd)
.output()
.expect("spawn chart on a family id");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("deadbeef-0"),
"the chartable handle must be listed: {stderr}"
);
assert!(
!stderr.contains("deadbeef-3"),
"an index-less directory must not be offered as a handle: {stderr}"
);
assert!(
stderr.contains("Did you mean: aura chart deadbeef-0"),
"with the stub filtered out exactly one candidate remains, so it is suggested: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// #309: an unmatched prefix invents nothing.
#[test]
fn chart_family_id_with_no_recorded_campaign_suggests_nothing() {
let cwd = temp_cwd("chart-family-id-none");
let out = Command::new(BIN)
.args(["chart", "cafebabe-0-GER40-w0-r0-s0-0"])
.current_dir(&cwd)
.output()
.expect("spawn chart on a family id");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("family id"), "the shape is still named: {stderr}");
assert!(!stderr.contains("Did you mean"), "nothing may be suggested: {stderr}");
let _ = std::fs::remove_dir_all(&cwd);
}
+72
View File
@@ -430,3 +430,75 @@ fn run_tap_selector_refuses_malformed_and_duplicate_pairs() {
assert!(!cwd.join("runs").exists());
}
}
/// #309: a run that records traces names the handle they landed under, on the
/// data plane — so a caller can chain `exec` into `chart` without knowing the
/// blueprint's render name. The tapped fixture's root composite is named
/// `sma_signal`, which is exactly the directory the existing
/// `single_run_persists_a_declared_tap_series` reads its trace back from.
#[test]
fn recording_run_reports_its_trace_handle_on_stdout() {
let cwd = temp_cwd("trace-handle-echo");
let bp_path = cwd.join("tapped_r_sma.json");
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
let out = Command::new(BIN)
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn exec");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let line = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(line.trim()).expect("stdout is one JSON object");
let handle = v["trace_name"].as_str().expect("trace_name is present and a string");
assert_eq!(handle, "sma_signal", "the handle is the run's own name");
assert!(
cwd.join("runs/traces").join(handle).join("fast_tap.json").exists(),
"the reported handle must name the directory the traces landed in; got {handle}"
);
}
/// #309: the handle is appended, never interleaved — the report's own key
/// order and bytes survive the flattening wrapper untouched. This is the first
/// pin of `serde(flatten)` byte behaviour in this workspace; assert bytes.
#[test]
fn trace_handle_is_appended_as_the_last_key() {
let cwd = temp_cwd("trace-handle-bytes");
let bp_path = cwd.join("tapped_r_sma.json");
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
let out = Command::new(BIN)
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn exec");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert!(
line.ends_with(",\"trace_name\":\"sma_signal\"}"),
"the handle must be the last key, appended before the closing brace: {line}"
);
}
/// #309: a tap-free run records nothing, so it reports no handle — the
/// wrapper must not emit an empty key. (The line's key order and grouping are
/// pinned separately, in `exec.rs`; this asserts only the absent key.)
#[test]
fn tap_free_run_line_carries_no_trace_handle() {
let cwd = temp_cwd("trace-handle-absent");
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["exec", &bp])
.current_dir(&cwd)
.output()
.expect("spawn exec");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert!(
!line.contains("trace_name"),
"a run that recorded nothing must emit no trace_name key at all: {line}"
);
}
+50
View File
@@ -158,6 +158,25 @@ impl TraceStore {
NameKind::NotFound
}
/// Every top-level directory entry in the store, sorted. This enumerates,
/// it does not classify: an interrupted run leaves a directory with no
/// `index.json`, which appears here but is `NotFound` to
/// [`TraceStore::name_kind`] — a caller that needs chartable names must
/// filter. A store directory that does not exist or cannot be read yields
/// none rather than an error: enumeration is a diagnostic aid, never a
/// run-blocking step, so callers must not read emptiness as "nothing
/// recorded".
pub fn names(&self) -> Vec<String> {
let Ok(entries) = fs::read_dir(&self.dir) else { return Vec::new() };
let mut out: Vec<String> = entries
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().into_string().ok())
.collect();
out.sort();
out
}
/// Read every member of a family. Two on-disk shapes are resolved: the
/// depth-1 campaign layout (`<name>/<key>/index.json`, `key` = the immediate
/// subdir name) and the depth-2 #224 sweep/walk-forward fan-out
@@ -575,6 +594,37 @@ mod tests {
let _ = fs::remove_dir_all(&root);
}
/// `names()` enumerates top-level directories, sorted, and nothing else:
/// a loose file in the store root is not a handle, and an index-less
/// directory (an interrupted run) IS listed even though `name_kind` calls
/// it absent — the split this method's callers must filter on.
#[test]
fn names_lists_sorted_directories_only() {
let root = temp_traces_root("names");
let store = TraceStore::open(&root);
store.write("zeta", &sample_manifest(), &sample_taps()).expect("write zeta");
store.write("alpha", &sample_manifest(), &sample_taps()).expect("write alpha");
// An interrupted run: directory created, no index written.
store.begin_run("halfway").expect("begin halfway");
// A loose file beside the handles must not be listed.
fs::write(root.join("traces").join("stray.json"), "{}").expect("write stray file");
assert_eq!(store.names(), vec!["alpha", "halfway", "zeta"]);
assert_eq!(store.name_kind("halfway"), NameKind::NotFound, "listed, but not chartable");
let _ = fs::remove_dir_all(&root);
}
/// An absent store root is not an error for enumeration — callers use it
/// on a diagnostic path where a refusal is already being printed.
#[test]
fn names_on_an_absent_store_is_empty() {
let root = temp_traces_root("names-absent");
let store = TraceStore::open(&root);
assert!(store.names().is_empty());
let _ = fs::remove_dir_all(&root);
}
#[test]
fn name_kind_classifies_run_family_and_absent() {
let root = temp_traces_root("namekind");
+19
View File
@@ -30,6 +30,25 @@ pub use tap_plan::{
};
pub use tap_recorder::TapRecorder;
/// What a declared-tap entry point returns: the run's record plus the two
/// values that ride *beside* it rather than inside it. The report is the
/// durable C18 record; these are per-invocation facts the shell renders
/// (C27/#297 — the library never prints).
///
/// `Debug` is load-bearing, not decoration: a caller asserting a refusal with
/// `Result::unwrap_err` needs the `Ok` side to be `Debug`, which the
/// `(report, skipped)` tuple this type replaced satisfied implicitly.
#[derive(Debug)]
pub struct RunOutcome<R> {
/// The run record itself.
pub report: R,
/// Declared taps the plan left unbound this run; the CLI prints the note.
pub skipped: Vec<String>,
/// The trace-store handle the run's taps landed under, when it recorded
/// any; `None` when the plan persisted nothing.
pub trace_name: Option<String>,
}
/// A refusal a library function reports instead of exiting the process
/// itself. The shell (`aura-cli`'s `exit_on_runner_error`, shared by the
/// dispatch arms) maps it back to the stderr bytes + exit code; the class
+11 -6
View File
@@ -86,7 +86,7 @@ fn compile_error_prose(e: &CompileError, role_names: &[String]) -> String {
pub fn run_measurement(
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &Env,
plan: TapPlan,
) -> Result<(MeasurementReport, Vec<String>), RunnerError> {
) -> Result<crate::RunOutcome<MeasurementReport>, RunnerError> {
// topology_hash's own two-line body, inlined (mirrors member::run_signal_r):
// `content_id_of` over the canonical (#164) blueprint JSON — the CLI shell's
// `topology_hash` helper is the same primitive, kept single-sourced at
@@ -123,6 +123,7 @@ pub fn run_measurement(
// C14 class 2: fault in argv-named content (#297)
let bound = bind_tap_plan(&mut flat, plan, env, &run_name)
.map_err(|e| RunnerError { exit_code: e.exit_class(), message: e.to_string() })?;
let trace_name = bound.trace_name().map(str::to_string);
let mut h = Harness::bootstrap(flat).expect("valid measurement harness");
h.run_bound(key_supply(&binding, sources))
@@ -140,7 +141,11 @@ pub fn run_measurement(
let skipped = bound.skipped().to_vec();
bound.finish(&manifest)
.map_err(|e| RunnerError { exit_code: e.exit_class(), message: e.to_string() })?;
Ok((MeasurementReport { manifest, taps: tap_names }, skipped))
Ok(crate::RunOutcome {
report: MeasurementReport { manifest, taps: tap_names },
skipped,
trace_name,
})
}
#[cfg(test)]
@@ -193,9 +198,9 @@ mod tests {
let (env, root) = temp_project_env("fold");
let mut plan = TapPlan::empty();
plan.subscribe("fast_tap", TapSubscription::named("mean"));
let (report, _skipped) =
let report =
run_measurement(tapped_r_sma(), &[], RunData::Synthetic, 0, &env, plan)
.expect("measurement run succeeds");
.expect("measurement run succeeds").report;
assert_eq!(report.taps, vec!["fast_tap".to_string()]);
let text = std::fs::read_to_string(
@@ -222,9 +227,9 @@ mod tests {
let (env, root) = temp_project_env("tapfree");
let doc = include_str!("../../aura-cli/examples/r_sma.json");
let signal = blueprint_from_json(doc, &|t| std_vocabulary(t)).expect("r_sma loads");
let (report, _skipped) =
let report =
run_measurement(signal, &[], RunData::Synthetic, 0, &env, TapPlan::record_all())
.expect("measurement run succeeds");
.expect("measurement run succeeds").report;
assert!(report.taps.is_empty(), "no declared taps");
assert!(!root.join("runs").exists(), "a tap-free run writes no runs/ entry");
}
+15 -10
View File
@@ -553,7 +553,7 @@ fn compile_error_prose(e: &CompileError, names: &[String], params: &[Scalar]) ->
pub fn run_signal_r(
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &Env,
plan: TapPlan, topo: Option<&str>,
) -> Result<(RunReport, Vec<String>), RunnerError> {
) -> Result<crate::RunOutcome<RunReport>, RunnerError> {
// #297 fieldtest finding B1 (milestone-safe-to-embed): mirror the CLI's
// own `exec_blueprint_leg` exposes-neither guard (`aura-cli/src/main.rs`,
// `has_bias`/`has_tap`) IN the library, before `wrap_r` below wires a
@@ -618,6 +618,7 @@ pub fn run_signal_r(
// C14 class 2: fault in argv-named content (#297)
let bound = bind_tap_plan(&mut flat, plan, env, &run_name)
.map_err(|e| RunnerError { exit_code: e.exit_class(), message: e.to_string() })?;
let trace_name = bound.trace_name().map(str::to_string);
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
@@ -645,7 +646,11 @@ pub fn run_signal_r(
// tap-free (or nothing-persisting) plan wrote nothing at all.
bound.finish(&manifest)
.map_err(|e| RunnerError { exit_code: e.exit_class(), message: e.to_string() })?;
Ok((RunReport { manifest, metrics }, skipped))
Ok(crate::RunOutcome {
report: RunReport { manifest, metrics },
skipped,
trace_name,
})
}
/// Run one bootstrapped member of a loaded-signal sweep: the reduce-mode path
@@ -1262,10 +1267,10 @@ mod tests {
let env = Env::std();
let loaded = blueprint_from_json(include_str!("../../aura-cli/examples/r_breakout.json"), &|t| std_vocabulary(t))
.expect("shipped r_breakout example loads");
let (via_file, _) = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None)
.expect("r_breakout example runs");
let (via_carve, _) = run_signal_r(r_breakout_signal(Some(R_BREAKOUT_CHANNEL)), &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None)
.expect("carved r_breakout signal runs");
let via_file = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None)
.expect("r_breakout example runs").report;
let via_carve = run_signal_r(r_breakout_signal(Some(R_BREAKOUT_CHANNEL)), &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None)
.expect("carved r_breakout signal runs").report;
assert_eq!(via_file.metrics, via_carve.metrics, "loaded example grades identically to the carve");
}
@@ -1417,9 +1422,9 @@ mod tests {
let env = Env::std();
let loaded = blueprint_from_json(include_str!("../../aura-cli/examples/r_meanrev.json"), &|t| std_vocabulary(t))
.expect("shipped r_meanrev example loads");
let (via_file, _) = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None)
.expect("r_meanrev example runs");
let (via_carve, _) = run_signal_r(
let via_file = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None)
.expect("r_meanrev example runs").report;
let via_carve = run_signal_r(
r_meanrev_signal(Some(R_MEANREV_WINDOW), Some(R_MEANREV_BAND_K)),
&[],
RunData::Synthetic,
@@ -1428,7 +1433,7 @@ mod tests {
TapPlan::record_all(),
None,
)
.expect("carved r_meanrev signal runs");
.expect("carved r_meanrev signal runs").report;
assert_eq!(via_file.metrics, via_carve.metrics, "loaded example grades identically to the carve");
}
+11
View File
@@ -372,6 +372,11 @@ pub struct BoundTaps {
/// caller-printed "unbound" note migrates to the CLI, this is the data
/// it prints from.
skipped: Vec<String>,
/// The trace-store handle this run's taps landed under — `Some` exactly
/// when the plan persisted something and `begin_run` was called. Rides
/// beside the report like `skipped` (#297): the CLI prints it, the
/// library hands it back as a value.
trace_name: Option<String>,
}
impl BoundTaps {
@@ -380,6 +385,11 @@ impl BoundTaps {
&self.declared
}
/// The recorded trace handle, or `None` when this run persisted nothing.
pub fn trace_name(&self) -> Option<&str> {
self.trace_name.as_deref()
}
/// Declared taps that resolved to no subscription this run (#297) — the
/// data the caller's "unbound" note prints from.
pub fn skipped(&self) -> &[String] {
@@ -508,6 +518,7 @@ pub fn bind_tap_plan(
rows: Vec::new(),
outcomes: Vec::new(),
skipped,
trace_name: if persists { Some(run_name.to_string()) } else { None },
};
for (name, kind, sub) in resolved {
let node: Box<dyn Node> = match sub {
+3 -2
View File
@@ -826,7 +826,7 @@ $ aura exec 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd
With two stop regimes every (instrument, window) cell runs twice — the four
cells above are the "4 cell(s)" the validate summary counted. The regime
ordinal is an INFIX segment, `-r{k}`, right after the window segment
(`{id8}-{ordinal}-{instrument}-w{k}-r{k}-s{seed}-{member}`) — the
(`{id8}-{ordinal}-{instrument}-w{k}-r{k}-s{stage}-{run}`) — the
default/first regime carries `-r0` explicitly, never left unmarked — each
cell record names its regime, and generalization is keyed per regime —
regimes are compared, never pooled.
@@ -843,7 +843,8 @@ name — chartable with `aura chart <trace_name>` (members keyed
`<cell>/<member>`); `aura chart` also resolves a bare campaign id/name
against the stored campaign documents when no literal trace directory
matches, refusing rather than guessing when the name is ambiguous across
recorded runs.
recorded runs. A single run reports its handle the same way: the `trace_name` value on
its own stdout line, absent when the run recorded nothing.
Exit codes: a clean run exits 0; a usage error exits 2; a refusal before any
cell runs (invalid document, missing project, unresolvable strategy) exits 1;
@@ -71,6 +71,13 @@ path's job, not a per-run flag (the repeatable `--tap TAP=FOLD` selector,
#310, only chooses which of the blueprint's own declared taps to subscribe,
never names the trace).
A run that persists anything reports the handle it wrote under as
`trace_name` on its stdout line (#309) — appended to the record's own bytes,
absent entirely when the run recorded nothing — so the handle is read, never
guessed. The library hands it to an embedding caller as a value beside the
report, like the unbound-tap names (C27/#297); it is not part of the stored
record.
**Families.** A campaign document's `presentation.persist_taps` (the closed
tap vocabulary, C18/C27) requests traces for each cell's nominee, run through
`aura exec <campaign.json|id>` (#319 — a document's `axes` replace the