refactor(aura-runner, aura-cli): retire the family builders and the CLI topology_hash duplicate
Slice 7 of the #319 retirement. aura-runner's blueprint_sweep_family / blueprint_walkforward_family / blueprint_mc_family lost their only production callers with the quintet and are gone (family.rs 889 -> 199 lines) together with their zero-caller helpers; reproduce's live inputs (DataSource, showcase/walkforward sources) stay. Coverage was ported first: member-computation identity + family-shared topology_hash now pin through a campaign-document twin in tests/exec.rs, reproduce's multi-member and MC-seed-label branches re-mint their fixtures directly. The CLI-side topology_hash helper goes with them — its keep-rationale was disproven (the record line's hash is computed in the runner's member machinery; minuted on the issue) — and c28_layering's file list drops the stale verb_sugar entry. The synthetic per-seed MC family remains an intentional retirement casualty, recorded in the test dispositions. refs #319
This commit is contained in:
@@ -663,8 +663,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
// The content id (#158) is the SHA256 of the same `blueprint_to_json`
|
||||
// bytes `graph build` emits; the identity id (#171) the SHA256 of the
|
||||
// debug-name-blind `blueprint_identity_json` form — both via the one
|
||||
// shared `crate::content_id` primitive `topology_hash` also uses, so all
|
||||
// surfaces agree by construction.
|
||||
// shared `crate::content_id` primitive, the same hash the run
|
||||
// manifest's `topology_hash` uses (computed inline in
|
||||
// `aura_runner::member`'s `run_signal_r`/`run_blueprint_member` — #319
|
||||
// Task 9 retired the CLI-side `topology_hash` wrapper this comment
|
||||
// used to name), so all surfaces agree by construction.
|
||||
let composite = match cmd.content_id.as_ref() {
|
||||
Some(Some(file)) => {
|
||||
let text = match std::fs::read_to_string(file) {
|
||||
|
||||
+157
-397
@@ -15,26 +15,33 @@ use aura_core::{Scalar, Timestamp};
|
||||
#[cfg(test)]
|
||||
use aura_core::ScalarKind;
|
||||
#[cfg(test)]
|
||||
use aura_core::Cell;
|
||||
#[cfg(test)]
|
||||
use aura_composites::StopRule;
|
||||
use aura_engine::{
|
||||
blueprint_from_json, blueprint_to_json, f64_field, join_on_ts,
|
||||
Composite, JoinedRow,
|
||||
blueprint_from_json, f64_field, join_on_ts,
|
||||
JoinedRow,
|
||||
SelectionMode,
|
||||
};
|
||||
// `blueprint_to_json`/`Composite` lost their last production caller with
|
||||
// `topology_hash` (#319 Task 9 — the CLI-side wrapper retired, its survival
|
||||
// rationale disproven); reached only from the test module now, mirroring
|
||||
// `Bias` below.
|
||||
#[cfg(test)]
|
||||
use aura_engine::{blueprint_to_json, Composite};
|
||||
#[cfg(test)]
|
||||
use aura_engine::{window_of, ColumnarTrace, Harness, VecSource};
|
||||
use aura_registry::{group_families, rank_by, FamilyMember, NameKind, RunTraces};
|
||||
// `FamilyKind`/`mc_member_reports`/`sweep_member_reports`/`PlateauMode` are only
|
||||
// reached from the test module's family-builder reproduce-fixture tests (#319
|
||||
// Task 8 — the quintet's own production callers of these are retired); the
|
||||
// imports are test-only, mirroring `Bias` below.
|
||||
// `FamilyKind` is only reached from the test module's reproduce-fixture tests
|
||||
// (#319 Task 8 — the quintet's own production callers of these are retired);
|
||||
// the import is test-only, mirroring `Bias` below.
|
||||
#[cfg(test)]
|
||||
use aura_registry::{mc_member_reports, sweep_member_reports, FamilyKind, PlateauMode};
|
||||
use aura_registry::FamilyKind;
|
||||
#[cfg(test)]
|
||||
use aura_registry::Registry;
|
||||
use aura_backtest::RunReport;
|
||||
#[cfg(test)]
|
||||
use aura_backtest::{SweepFamily, WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
|
||||
use aura_backtest::{WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
|
||||
use aura_measurement::information_coefficient;
|
||||
// The C28 assembly position (#295): the member-run recipe, the axis/translate
|
||||
// helpers, and the harness-assembly constants all live in `aura-runner` now;
|
||||
@@ -46,18 +53,16 @@ use aura_runner::member::{
|
||||
use aura_runner::{TapPlan, TapSubscription};
|
||||
#[cfg(test)]
|
||||
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
|
||||
// The family builders (blueprint sweep / walk-forward / MC) and the shared
|
||||
// `DataSource` data provider / `--select` objective `Selection` live in
|
||||
// `aura_runner::family` (#295 Task 8); with the quintet's own production
|
||||
// callers retired (#319 Task 8), they are only reached from the test
|
||||
// module's family-builder tests now (Task 9 owns their eventual removal) —
|
||||
// the imports are test-only, mirroring `Bias` below.
|
||||
// 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
|
||||
// primitives, reached here only from the test module's reproduce-fixture
|
||||
// tests (#319 Task 9 — the family builders that used to call them in this
|
||||
// crate are retired). The imports are test-only, mirroring `Bias` below.
|
||||
#[cfg(test)]
|
||||
use aura_runner::family::{
|
||||
blueprint_mc_family, blueprint_sweep_family, blueprint_walkforward_family, DataSource, Selection,
|
||||
};
|
||||
use aura_runner::family::{synthetic_walk_sources, DataSource};
|
||||
#[cfg(test)]
|
||||
use aura_runner::family::{render_bind_error, select_winner, showcase_prices};
|
||||
use aura_runner::family::showcase_prices;
|
||||
// `reproduce_family_in` (#295) lives in `aura_runner::reproduce`, returning
|
||||
// `Result<_, aura_runner::RunnerError>`; the test module's own call sites
|
||||
// unwrap it (the production dispatch arm is `dispatch_reproduce`).
|
||||
@@ -94,19 +99,18 @@ use aura_runner::member::CostLeg;
|
||||
use aura_backtest::{summarize, summarize_r};
|
||||
#[cfg(test)]
|
||||
use aura_engine::GraphBuilder;
|
||||
#[cfg(test)]
|
||||
use aura_std::Sub;
|
||||
// `std_vocabulary` is now only reached through `aura_runner::project::Env::resolve` in production
|
||||
// code; the test module still builds reference blueprints against it directly, so
|
||||
// the import is test-only.
|
||||
#[cfg(test)]
|
||||
use aura_vocabulary::std_vocabulary;
|
||||
// `Bias`/`Sma` are only reached from the test module; the imports are test-only.
|
||||
// `Bias` is only reached from the test module; the import is test-only.
|
||||
// `Sub`/`Sma` lost their last test users with #319 Task 9's family-builder
|
||||
// unit-test retirement (they built the deleted tests' probe graphs) — no
|
||||
// longer imported here.
|
||||
#[cfg(test)]
|
||||
use aura_strategy::Bias;
|
||||
#[cfg(test)]
|
||||
use aura_std::Sma;
|
||||
#[cfg(test)]
|
||||
use std::sync::mpsc;
|
||||
#[cfg(test)]
|
||||
use std::collections::HashSet;
|
||||
@@ -574,31 +578,20 @@ fn runs_family(id: &str, rank: Option<&str>, env: &aura_runner::project::Env) {
|
||||
// --- r-sma harness (the SMA-cross signal scored in R) --------------------
|
||||
|
||||
/// SHA256 (hex) of a canonical (#164) blueprint JSON string — the content id (#158).
|
||||
/// The single hashing primitive, shared by [`topology_hash`] (from a live `Composite`)
|
||||
/// and the op-script `graph introspect --content-id` path (`crate::content_id`), so the
|
||||
/// two surfaces agree by construction over the same canonical bytes. Research-side
|
||||
/// The single hashing primitive: fed a live `Composite`'s canonical bytes
|
||||
/// (`blueprint_to_json`) this is the run manifest's `topology_hash` — the
|
||||
/// same computation `aura_runner::member`'s `run_signal_r`/
|
||||
/// `run_blueprint_member` inline on the record-line path (#319 Task 9: the
|
||||
/// CLI-side `topology_hash` wrapper that used to duplicate this call is
|
||||
/// retired, its survival rationale disproven — its only callers were the
|
||||
/// family-builder-range unit tests, now gone with the builders) — and fed the
|
||||
/// op-script `graph introspect --content-id` path's bytes (`crate::content_id`)
|
||||
/// it is the SAME hash, so the two surfaces agree by construction. Research-side
|
||||
/// (aura-cli), off the frozen engine (invariant 8).
|
||||
fn content_id(canonical_json: &str) -> String {
|
||||
aura_research::content_id_of(canonical_json)
|
||||
}
|
||||
|
||||
/// SHA256 (hex) of the canonical (#164, no-trailing-newline) serialization of a
|
||||
/// signal blueprint — the run's `topology_hash` (#158).
|
||||
///
|
||||
/// #319 Task 8 finding: this primitive's only production caller was
|
||||
/// `validate_and_register_axes` (retired with the quintet); `exec`'s own
|
||||
/// record line gets its `manifest.topology_hash` from `aura_runner::member`'s
|
||||
/// own internal computation, never through this fn. It is kept alive
|
||||
/// (production-visible, not test-gated) per the retirement plan's explicit
|
||||
/// instruction, and still exercised directly by the family-builder-range
|
||||
/// unit tests below (Task 9 territory) — hence `#[allow(dead_code)]` rather
|
||||
/// than deletion. Orchestrator: confirm whether `exec` should route through
|
||||
/// this fn after all, or whether the KEEP was based on a stale assumption.
|
||||
#[allow(dead_code)]
|
||||
fn topology_hash(signal: &Composite) -> String {
|
||||
content_id(&blueprint_to_json(signal).expect("a buildable signal serializes"))
|
||||
}
|
||||
|
||||
/// One measurement run's IC scalar + its permutation-null significance, for stdout.
|
||||
#[derive(serde::Serialize)]
|
||||
struct IcReport {
|
||||
@@ -1099,10 +1092,10 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
|
||||
};
|
||||
if has_bias {
|
||||
// #246 override set (Task 3): validated against the un-reopened
|
||||
// wrapped OPEN space via `override_paths` (the strict, erroring
|
||||
// variant `blueprint_sweep_family` also resolves its `--axis` grid
|
||||
// through), so an unmatched path refuses with that same shared
|
||||
// prose (`member.rs:768-771`) rather than a second wording.
|
||||
// wrapped OPEN space via `override_paths` (the single strict,
|
||||
// erroring axis-resolution path this shell drives), so an
|
||||
// unmatched path refuses with that same shared prose
|
||||
// (`member.rs:768-771`) rather than a second wording.
|
||||
let raw_space = blueprint_axis_probe(&doc, env).param_space();
|
||||
let axes: Vec<(String, Vec<Scalar>)> = ov.iter().map(|(p, v)| (p.clone(), vec![*v])).collect();
|
||||
let paths = override_paths(&axes, &raw_space, &signal).unwrap_or_else(|e| {
|
||||
@@ -1541,35 +1534,6 @@ fn main() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Audit follow-up to #247/#269: `render_bind_error` is exhaustive — the
|
||||
/// axis-usage variants the old catch-all Debug-framed render as prose
|
||||
/// naming the axis, with no Rust identifier on the user's stderr.
|
||||
#[test]
|
||||
fn render_bind_error_prose_covers_the_axis_usage_variants() {
|
||||
let dup = render_bind_error(&aura_engine::BindError::DuplicateBinding("a.b".into()));
|
||||
assert!(dup.contains("a.b") && dup.contains("bound twice"), "{dup}");
|
||||
assert!(!dup.contains("DuplicateBinding"), "Debug leak: {dup}");
|
||||
let empty = render_bind_error(&aura_engine::BindError::EmptyAxis("a.b".into()));
|
||||
assert!(empty.contains("a.b") && empty.contains("no values"), "{empty}");
|
||||
assert!(!empty.contains("EmptyAxis"), "Debug leak: {empty}");
|
||||
let range = render_bind_error(&aura_engine::BindError::EmptyRange("a.b".into()));
|
||||
assert!(range.contains("a.b") && !range.contains("EmptyRange"), "Debug leak: {range}");
|
||||
}
|
||||
|
||||
/// Audit follow-up to #247/#269: a `Compile` fault at the sweep boundary —
|
||||
/// a blueprint defect, not an axis usage error — renders as a prose frame
|
||||
/// that names the situation and labels the embedded compile detail as
|
||||
/// internal, instead of leaking the bare Debug struct as the whole message.
|
||||
#[test]
|
||||
fn render_bind_error_frames_a_compile_fault_as_prose_with_labeled_detail() {
|
||||
let msg = render_bind_error(&aura_engine::BindError::Compile(
|
||||
aura_engine::CompileError::BadInteriorIndex,
|
||||
));
|
||||
assert!(msg.contains("failed to bootstrap"), "{msg}");
|
||||
assert!(msg.contains("internal detail"), "{msg}");
|
||||
assert!(!msg.starts_with("Compile"), "the frame must lead with prose: {msg}");
|
||||
}
|
||||
|
||||
/// An empty (or absent) archive is informational absence, not a fault
|
||||
/// (#264), but under the all-JSON namespace reshape (#273) stdout must
|
||||
/// stay pure NDJSON: `data_list_report` returns an empty `Vec` rather
|
||||
@@ -1718,17 +1682,6 @@ mod tests {
|
||||
.expect("loads")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_winner_refuses_plateau_without_a_lattice() {
|
||||
// A plateau request with no lattice (a random sweep would yield None) is
|
||||
// refused, never silently argmaxed. The refuse short-circuits before the
|
||||
// family is read, so an empty family is fine here.
|
||||
let fam = SweepFamily { space: vec![], points: vec![] };
|
||||
let err = select_winner(&fam, "total_pips", Selection::Plateau(PlateauMode::Mean), None)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("requires a grid sweep"), "refuse message: {err}");
|
||||
}
|
||||
|
||||
fn cmp_member(key: &str, ts: &[i64], vals: &[f64]) -> FamilyMember {
|
||||
cmp_member_win(key, ts, vals, (0, 0))
|
||||
}
|
||||
@@ -1981,45 +1934,6 @@ mod tests {
|
||||
assert_eq!(d.full_window(&env), window_of(&s).unwrap());
|
||||
}
|
||||
|
||||
/// A multi-column blueprint over synthetic data (a single close walk)
|
||||
/// refuses honestly through the builders' exit-free Err contract, naming
|
||||
/// the beyond-close columns and the --real remedy — never a panic from a
|
||||
/// source-count mismatch. High/low-consuming, closed (mc requires it),
|
||||
/// with the mandatory `bias` output.
|
||||
const OHLC_REFUSAL_BLUEPRINT: &str = r#"{
|
||||
"format_version": 1,
|
||||
"blueprint": {
|
||||
"name": "hl_range",
|
||||
"nodes": [ {"primitive":{"type":"Sub"}} ],
|
||||
"edges": [],
|
||||
"input_roles": [
|
||||
{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},
|
||||
{"name":"low","targets":[{"node":0,"slot":1}],"source":"F64"}
|
||||
],
|
||||
"output": [{"node":0,"field":0,"name":"bias"}]
|
||||
}
|
||||
}"#;
|
||||
|
||||
#[test]
|
||||
fn synthetic_data_refuses_a_multi_column_blueprint() {
|
||||
let env = aura_runner::project::Env::std();
|
||||
let err = blueprint_mc_family(OHLC_REFUSAL_BLUEPRINT, 2, &DataSource::Synthetic, &env)
|
||||
.expect_err("a high/low blueprint cannot run over the synthetic close walk");
|
||||
assert_eq!(
|
||||
err,
|
||||
"strategy \"hl_range\" consumes columns beyond close (high, low) — synthetic \
|
||||
data generates a close series only; run with --real <SYMBOL>"
|
||||
);
|
||||
let err = blueprint_sweep_family(
|
||||
OHLC_REFUSAL_BLUEPRINT,
|
||||
&[("x".to_string(), vec![Scalar::i64(1)])],
|
||||
&DataSource::Synthetic,
|
||||
&env,
|
||||
)
|
||||
.expect_err("the synthetic sweep path refuses the same shape");
|
||||
assert!(err.contains("consumes columns beyond close"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wf_real_roller_sizes_are_90_30_30_days_in_ns() {
|
||||
// Independent expected value: a day reconstructed from its time units
|
||||
@@ -2043,50 +1957,6 @@ mod tests {
|
||||
// below is dropped for the same reason (`member_key` no longer exists); the
|
||||
// doc comment is trimmed to match.
|
||||
|
||||
/// Property: a `blueprint_sweep_family` member built from a serialized signal is
|
||||
/// the SAME trading result as the cycle-1 single run of that signal at the same
|
||||
/// params — the loaded-blueprint sweep reuses the identical `wrap_r` run path
|
||||
/// (the keystone). Every member of one family carries the SAME `topology_hash` (the
|
||||
/// loaded signal's, the deviation from the Rust-built mirror).
|
||||
#[test]
|
||||
fn blueprint_sweep_member_equals_single_run_and_shares_topology_hash() {
|
||||
// An OPEN signal (both SMA knobs free) so the sweep can bind them by name; the
|
||||
// serialized doc round-trips to the topology the single run hashes.
|
||||
let env = aura_runner::project::Env::std();
|
||||
let open = load_open_r_sma();
|
||||
let doc = blueprint_to_json(&open).expect("serializes");
|
||||
let data = DataSource::Synthetic;
|
||||
// fast pinned at 2, slow varied over {4, 6}: a 2x1 grid, slow the varying axis.
|
||||
let axes = vec![
|
||||
("sma_signal.fast.length".to_string(), vec![Scalar::i64(2)]),
|
||||
("sma_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
|
||||
];
|
||||
let family = blueprint_sweep_family(&doc, &axes, &data, &env).expect("named axes resolve");
|
||||
assert_eq!(family.points.len(), 2, "2x1 grid -> 2 members");
|
||||
|
||||
// (b) every member carries the shared topology_hash of the loaded signal.
|
||||
let topo = topology_hash(&open);
|
||||
assert_eq!(topo.len(), 64, "topology_hash is a 64-hex SHA256");
|
||||
for pt in &family.points {
|
||||
assert_eq!(pt.report.manifest.topology_hash.as_deref(), Some(topo.as_str()));
|
||||
}
|
||||
|
||||
// (a) the slow=4 member reproduces the cycle-1 single run at fast=2, slow=4 —
|
||||
// same equity/exposure stream (total_pips/max_drawdown/bias_sign_flips) and the
|
||||
// same topology_hash, proving the loaded blueprint runs through the identical path.
|
||||
let single = run_signal_r(
|
||||
load_open_r_sma(),
|
||||
&[Scalar::i64(2), Scalar::i64(4)],
|
||||
RunData::Synthetic,
|
||||
0,
|
||||
&env,
|
||||
TapPlan::record_all(),
|
||||
);
|
||||
let member4 = &family.points[0].report; // slow=4 is the first odometer point
|
||||
assert_eq!(member4.metrics, single.metrics, "loaded sweep member == single run");
|
||||
assert_eq!(member4.manifest.topology_hash, single.manifest.topology_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blueprint_axis_probe_lists_prefixed_open_knobs() {
|
||||
// The open fixture's two SMA lengths are the sweepable knobs; the probe
|
||||
@@ -2104,97 +1974,15 @@ mod tests {
|
||||
assert!(blueprint_axis_probe(closed, &env).param_space().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blueprint_walkforward_family_refits_each_window() {
|
||||
// The closed blueprint's two bound SMA lengths are re-fit per IS window over
|
||||
// a 2x2 grid via the #246 bound-override reopen path (bound = overridable default).
|
||||
let env = aura_runner::project::Env::std();
|
||||
let doc = include_str!("../examples/r_sma.json");
|
||||
let axes = vec![
|
||||
("sma_signal.fast.length".to_string(), vec![Scalar::i64(2), Scalar::i64(3)]),
|
||||
("sma_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
|
||||
];
|
||||
let result = blueprint_walkforward_family(doc, &axes, &DataSource::Synthetic, Selection::Argmax, &env);
|
||||
// 24/12/12 over the 60-bar synthetic span -> 3 rolling windows.
|
||||
assert_eq!(result.windows.len(), 3, "three rolling IS/OOS windows");
|
||||
for w in &result.windows {
|
||||
assert_eq!(w.run.chosen_params.len(), 2, "both axes re-fit each window");
|
||||
assert!(w.run.oos_report.metrics.r.is_some(), "OOS record is R-metrics");
|
||||
}
|
||||
// reduce-mode retains no raw pip curve -> the stitched pip-equity is empty.
|
||||
assert!(result.stitched_oos_equity.is_empty(), "no raw pip curve in reduce-mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blueprint_mc_family_seeds_differ() {
|
||||
// MC over a CLOSED signal (both SMA knobs bound): 3 seeds -> 3 draws, one shared
|
||||
// topology_hash, and DIFFERING metrics — the seed reaches the DATA (a distinct
|
||||
// synthetic walk per draw), not just the manifest label. The anti-degenerate guard:
|
||||
// a regression to seed-as-label-only would make the three draws identical.
|
||||
let env = aura_runner::project::Env::std();
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let family = blueprint_mc_family(&doc, 3, &DataSource::Synthetic, &env).expect("closed blueprint");
|
||||
|
||||
assert_eq!(family.draws.len(), 3, "one draw per seed");
|
||||
assert_eq!(family.draws.iter().map(|d| d.seed).collect::<Vec<_>>(), vec![1, 2, 3]);
|
||||
let topo = family.draws[0].report.manifest.topology_hash.clone();
|
||||
assert!(topo.is_some(), "members carry a topology_hash");
|
||||
assert!(
|
||||
family.draws.iter().all(|d| d.report.manifest.topology_hash == topo),
|
||||
"all members share one topology_hash"
|
||||
);
|
||||
let m: Vec<_> = family.draws.iter().map(|d| &d.report.metrics).collect();
|
||||
assert!(m[0] != m[1] || m[1] != m[2], "seeds must yield differing realizations");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blueprint_mc_family_rejects_vacuous_deep_lookback() {
|
||||
// Property: the mc family builder REFUSES a silent-vacuous Monte-Carlo — one where
|
||||
// every per-seed draw collapses to a bit-identical realization — by RETURNING a named
|
||||
// error rather than an `Ok` family that looks like a real (but indistinguishable)
|
||||
// distribution. A CLOSED deep-lookback signal whose slow SMA length (60) equals the
|
||||
// fixed 60-bar synthetic walk never warms, so every seed yields zero trades and thus
|
||||
// identical metrics; that is a wrong result with no error (C10 refuse-don't-guess).
|
||||
let env = aura_runner::project::Env::std();
|
||||
// slow len (60) == walk len -> never warms
|
||||
let mut g = GraphBuilder::new("deep_probe");
|
||||
let fast = g.add(Sma::builder().named("fast").bind("length", Scalar::i64(2)));
|
||||
let slow = g.add(Sma::builder().named("slow").bind("length", Scalar::i64(60)));
|
||||
let spread = g.add(Sub::builder());
|
||||
let exposure = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(0.5)));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, vec![fast.input("series"), slow.input("series")]);
|
||||
g.connect(fast.output("value"), spread.input("lhs"));
|
||||
g.connect(slow.output("value"), spread.input("rhs"));
|
||||
g.connect(spread.output("value"), exposure.input("signal"));
|
||||
g.expose(exposure.output("bias"), "bias");
|
||||
let deep = g.build().expect("deep probe wiring resolves");
|
||||
let doc = blueprint_to_json(&deep).expect("serializes");
|
||||
let err = blueprint_mc_family(&doc, 3, &DataSource::Synthetic, &env)
|
||||
.expect_err("a vacuous (all-identical) Monte-Carlo is rejected, not returned");
|
||||
assert!(
|
||||
err.contains("vacuous") || err.contains("identical"),
|
||||
"names the vacuous/degenerate condition: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blueprint_mc_family_rejects_an_open_blueprint() {
|
||||
// Property: the mc family builder REFUSES an open blueprint (free knobs) by RETURNING
|
||||
// a named error — never a hidden process exit — so the closed-blueprint precondition
|
||||
// is unit-testable (the IO wrapper renders it to stderr + exit 2, mirroring the sibling
|
||||
// blueprint_sweep_family). MC binds no axis, so a free knob would have no binder; the
|
||||
// rejection pre-empts the downstream compile_with_params arity panic.
|
||||
let env = aura_runner::project::Env::std();
|
||||
// both SMA knobs free -> non-empty param_space
|
||||
let open = load_open_r_sma();
|
||||
let doc = blueprint_to_json(&open).expect("serializes");
|
||||
let err = blueprint_mc_family(&doc, 4, &DataSource::Synthetic, &env)
|
||||
.expect_err("an open blueprint is rejected, not run");
|
||||
assert!(err.contains("closed blueprint"), "names the closed-blueprint requirement: {err}");
|
||||
}
|
||||
|
||||
/// Property, ported (#319 — the family builders retired, this test's
|
||||
/// original minted its 2-member family via the now-deleted
|
||||
/// `blueprint_sweep_family`): `reproduce_family_in` re-derives EVERY
|
||||
/// member of a persisted multi-member family bit-identically (index
|
||||
/// alignment across members, not just the single-member case its
|
||||
/// stop-regime/costed-member siblings pin) — including the open-at-end
|
||||
/// member. The family is minted directly via `run_blueprint_member`
|
||||
/// (the same reduce-mode member path any minting mechanism drives),
|
||||
/// mirroring the sibling reproduce tests below.
|
||||
#[test]
|
||||
fn reproduce_family_re_derives_every_member_bit_identically() {
|
||||
// a unique temp runs store so the on-disk family + blueprint store do not collide.
|
||||
@@ -2208,20 +1996,44 @@ mod tests {
|
||||
let open = load_open_r_sma();
|
||||
let doc = blueprint_to_json(&open).expect("serializes");
|
||||
let data = DataSource::Synthetic;
|
||||
// 2x grid over slow.length {4,6} at fast=2 — slow=4 is the open-at-end member.
|
||||
let axes = vec![
|
||||
("sma_signal.fast.length".to_string(), vec![Scalar::i64(2)]),
|
||||
("sma_signal.slow.length".to_string(), vec![Scalar::i64(4), Scalar::i64(6)]),
|
||||
];
|
||||
let family = blueprint_sweep_family(&doc, &axes, &data, &env).expect("axes resolve");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
// topology_hash's own two-line body, inlined (mirrors
|
||||
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
|
||||
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
|
||||
// wrapper is retired, its only production/test callers gone).
|
||||
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
|
||||
let binding = aura_runner::binding::resolve_binding("everymember", reload().input_roles(), &BTreeMap::new())
|
||||
.expect("the price role resolves");
|
||||
let window = data.full_window(&env);
|
||||
let pip = data.pip_size();
|
||||
// fast pinned at 2, slow varied over {4, 6}: two members, slow=6 the open-at-end one.
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
let reports: Vec<RunReport> = [4_i64, 6]
|
||||
.iter()
|
||||
.map(|&slow| {
|
||||
run_blueprint_member(
|
||||
reload(),
|
||||
&[Cell::from_i64(2), Cell::from_i64(slow)],
|
||||
&space,
|
||||
data.run_sources(&env, &binding.columns()),
|
||||
window,
|
||||
0,
|
||||
pip,
|
||||
&topo,
|
||||
&env,
|
||||
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
|
||||
&binding,
|
||||
&[],
|
||||
"",
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// persist exactly as run_blueprint_sweep does: store the blueprint, append the family.
|
||||
let topo = family.points[0].report.manifest.topology_hash.clone().expect("topo");
|
||||
let canonical =
|
||||
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
|
||||
// persist exactly as any family-minting mechanism does: store the blueprint, append the family.
|
||||
let canonical = blueprint_to_json(&reload()).unwrap();
|
||||
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
|
||||
let id = reg
|
||||
.append_family("repro", FamilyKind::Sweep, &sweep_member_reports(&family))
|
||||
.append_family("repro", FamilyKind::Sweep, &reports)
|
||||
.expect("append");
|
||||
|
||||
// reproduce: every member re-derives bit-identically (incl the open-at-end member).
|
||||
@@ -2258,7 +2070,11 @@ mod tests {
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
let topo = topology_hash(&reload());
|
||||
// topology_hash's own two-line body, inlined (mirrors
|
||||
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
|
||||
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
|
||||
// wrapper is retired, its only production/test callers gone).
|
||||
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
let pip = data.pip_size();
|
||||
let window = data.full_window(&env);
|
||||
@@ -2324,7 +2140,11 @@ mod tests {
|
||||
let data = DataSource::Synthetic;
|
||||
let doc = blueprint_to_json(&load_closed_r_sma()).expect("serializes");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
let topo = topology_hash(&reload());
|
||||
// topology_hash's own two-line body, inlined (mirrors
|
||||
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
|
||||
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
|
||||
// wrapper is retired, its only production/test callers gone).
|
||||
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
let pip = data.pip_size();
|
||||
let window = data.full_window(&env);
|
||||
@@ -2375,6 +2195,14 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property, ported (#319 — the family builders retired, this test's
|
||||
/// original minted its 3-draw MC family via the now-deleted
|
||||
/// `blueprint_mc_family`): `reproduce_family_in` re-derives every MC
|
||||
/// member bit-identically, each seed's synthetic walk reconstructed from
|
||||
/// `manifest.seed` (the realization branch) — minted directly via
|
||||
/// `run_blueprint_member` over `synthetic_walk_sources(seed)`, the same
|
||||
/// per-seed source `aura_runner::reproduce`'s own synthetic-MC branch
|
||||
/// reconstructs.
|
||||
#[test]
|
||||
fn reproduce_family_re_derives_every_mc_member_bit_identically() {
|
||||
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
|
||||
@@ -2387,16 +2215,34 @@ mod tests {
|
||||
let env = aura_runner::project::Env::std();
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
let data = DataSource::Synthetic;
|
||||
let family = blueprint_mc_family(&doc, 3, &data, &env).expect("closed blueprint");
|
||||
// topology_hash's own two-line body, inlined (mirrors
|
||||
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
|
||||
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
|
||||
// wrapper is retired, its only production/test callers gone).
|
||||
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
|
||||
let binding = aura_runner::binding::resolve_binding("everymc", reload().input_roles(), &BTreeMap::new())
|
||||
.expect("the price role resolves");
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
assert!(space.is_empty(), "MC binds no axis — the fixture must be closed");
|
||||
|
||||
// persist exactly as run_blueprint_mc does: store the blueprint, append the MC family.
|
||||
let topo = family.draws[0].report.manifest.topology_hash.clone().expect("topo");
|
||||
let canonical =
|
||||
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
|
||||
let reports: Vec<RunReport> = (1_u64..=3)
|
||||
.map(|seed| {
|
||||
let sources = synthetic_walk_sources(seed);
|
||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||
run_blueprint_member(
|
||||
reload(), &[], &space, sources, window, seed, data.pip_size(), &topo, &env,
|
||||
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding, &[], "",
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// persist exactly as any MC-minting mechanism does: store the blueprint, append the family.
|
||||
let canonical = blueprint_to_json(&reload()).unwrap();
|
||||
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
|
||||
let id = reg
|
||||
.append_family("mcrepro", FamilyKind::MonteCarlo, &mc_member_reports(&family))
|
||||
.append_family("mcrepro", FamilyKind::MonteCarlo, &reports)
|
||||
.expect("append");
|
||||
|
||||
// reproduce: every MC member re-derives bit-identically (its seed-driven walk is
|
||||
@@ -2411,73 +2257,13 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// #246/#328: a fully-bound (closed) blueprint IS sweepable — a RAW axis
|
||||
/// naming a bound param re-opens it (the bound value is the default), so
|
||||
/// the retired "fully bound; nothing to sweep" refusal must not resurface.
|
||||
/// Inverse guard: an axis naming NEITHER an open nor a bound param gets
|
||||
/// the one clear boundary message (not a terse `UnknownKnob` debug leak).
|
||||
#[test]
|
||||
fn blueprint_sweep_family_overrides_a_bound_param_and_names_unknown_axes() {
|
||||
let env = aura_runner::project::Env::std();
|
||||
// same fixture the retired test swept: both SMA knobs bound.
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
|
||||
// (a) override axis (RAW name, #328): two members, no refusal
|
||||
let axes = vec![(
|
||||
"fast.length".to_string(),
|
||||
vec![Scalar::i64(2), Scalar::i64(4)],
|
||||
)];
|
||||
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
|
||||
.expect("a bound param is a default — the axis overrides it");
|
||||
assert_eq!(fam.points.len(), 2);
|
||||
|
||||
// (b) unknown axis: the boundary message, no UnknownKnob leak
|
||||
let bad = vec![("nope".to_string(), vec![Scalar::i64(1)])];
|
||||
let err = blueprint_sweep_family(&doc, &bad, &DataSource::Synthetic, &env)
|
||||
.expect_err("an axis matching neither space is refused");
|
||||
assert!(err.contains("names no param"), "boundary message, got: {err}");
|
||||
assert!(err.contains("--list-axes"), "must point at --list-axes: {err}");
|
||||
assert!(!err.contains("UnknownKnob"), "must not leak the debug render: {err}");
|
||||
}
|
||||
|
||||
/// #249/#328: an axis-reopened bound param flows through `params` ("what
|
||||
/// varied") and must NOT also appear in `defaults` ("what was held") — the
|
||||
/// two are disjoint by construction. Sweeping `fast.length` (RAW, #328) on
|
||||
/// the fully bound r_sma example leaves `slow.length`/`bias.scale`
|
||||
/// untouched (they stay in `defaults`), while `fast.length` moves to
|
||||
/// `params` and drops out of `defaults` entirely. Both `manifest.params`
|
||||
/// and `manifest.defaults` keys are RAW (#328, batch 2 flips defaults too).
|
||||
#[test]
|
||||
fn sweep_override_excludes_the_reopened_default_from_the_manifest() {
|
||||
let env = aura_runner::project::Env::std();
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let axes = vec![("fast.length".to_string(), vec![Scalar::i64(2)])];
|
||||
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
|
||||
.expect("a bound param is a default — the axis overrides it");
|
||||
assert_eq!(fam.points.len(), 1);
|
||||
let manifest = &fam.points[0].report.manifest;
|
||||
|
||||
let param_names: Vec<&str> = manifest.params.iter().map(|(n, _)| n.as_str()).collect();
|
||||
assert!(param_names.contains(&"fast.length"), "swept axis is a raw param: {param_names:?}");
|
||||
|
||||
let default_names: Vec<&str> = manifest.defaults.iter().map(|(n, _)| n.as_str()).collect();
|
||||
assert!(
|
||||
!default_names.contains(&"fast.length"),
|
||||
"the reopened default must not also appear in defaults: {default_names:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
default_names,
|
||||
["slow.length", "bias.scale"],
|
||||
"the untouched bound params stay in defaults, RAW: {default_names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: an MC family's `aura reproduce` lines carry the member's own `seed=<N>`
|
||||
/// label. MC members hold no tuning params (the params-join is empty), so the line would
|
||||
/// otherwise print a BLANK member label; the seed is each draw's realization identity and
|
||||
/// must show. Sweep / walk-forward labels still echo their params (covered elsewhere).
|
||||
/// Property, ported (#319 — the family builders retired, same minting
|
||||
/// swap as the sibling MC-reproduce test above): an MC family's `aura
|
||||
/// reproduce` lines carry the member's own `seed=<N>` label. MC members
|
||||
/// hold no tuning params (the params-join is empty), so the line would
|
||||
/// otherwise print a BLANK member label; the seed is each draw's
|
||||
/// realization identity and must show. Sweep / walk-forward labels
|
||||
/// still echo their params (covered elsewhere).
|
||||
#[test]
|
||||
fn reproduce_mc_member_labels_carry_the_seed() {
|
||||
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
|
||||
@@ -2490,15 +2276,32 @@ mod tests {
|
||||
// MC binds no axis -> closed blueprint
|
||||
let closed = load_closed_r_sma();
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
|
||||
let data = DataSource::Synthetic;
|
||||
let family = blueprint_mc_family(&doc, 3, &data, &env).expect("closed blueprint");
|
||||
// topology_hash's own two-line body, inlined (mirrors
|
||||
// `aura_runner::member::run_signal_r`'s and `run_blueprint_member`'s
|
||||
// own copy — the #319 Task 9 finding: the CLI-side `topology_hash`
|
||||
// wrapper is retired, its only production/test callers gone).
|
||||
let topo = content_id(&blueprint_to_json(&reload()).expect("a buildable signal serializes"));
|
||||
let binding = aura_runner::binding::resolve_binding("mcseedlabel", reload().input_roles(), &BTreeMap::new())
|
||||
.expect("the price role resolves");
|
||||
let space = blueprint_axis_probe(&doc, &env).param_space();
|
||||
|
||||
let topo = family.draws[0].report.manifest.topology_hash.clone().expect("topo");
|
||||
let canonical =
|
||||
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
|
||||
let reports: Vec<RunReport> = (1_u64..=3)
|
||||
.map(|seed| {
|
||||
let sources = synthetic_walk_sources(seed);
|
||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||
run_blueprint_member(
|
||||
reload(), &[], &space, sources, window, seed, data.pip_size(), &topo, &env,
|
||||
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding, &[], "",
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let canonical = blueprint_to_json(&reload()).unwrap();
|
||||
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
|
||||
let id = reg
|
||||
.append_family("mcseed", FamilyKind::MonteCarlo, &mc_member_reports(&family))
|
||||
.append_family("mcseed", FamilyKind::MonteCarlo, &reports)
|
||||
.expect("append");
|
||||
|
||||
let rep = reproduce_family_in(®, &id, &data, &env).expect("reproduce_family_in succeeds over a well-formed persisted family");
|
||||
@@ -2552,43 +2355,10 @@ mod tests {
|
||||
assert_eq!(doc, doc2, "f64 blueprint param survives the store round-trip bit-identically");
|
||||
}
|
||||
|
||||
/// `topology_hash` is deterministic per signal and distinguishes topologies —
|
||||
/// the #158 reproducibility-anchor property.
|
||||
#[test]
|
||||
fn topology_hash_is_stable_and_distinguishes() {
|
||||
let closed = load_closed_r_sma();
|
||||
let open = load_open_r_sma();
|
||||
let h = topology_hash(&closed);
|
||||
assert_eq!(h, topology_hash(&closed), "same signal -> same hash");
|
||||
assert_ne!(h, topology_hash(&open), "distinct blueprints -> distinct hash");
|
||||
|
||||
// Same shape as the closed example, differing only in the BOUND fast-SMA
|
||||
// length (3 vs 2): the hash must still discriminate on bound param values,
|
||||
// not merely on topology shape — distinct sweep/mc members must key to
|
||||
// distinct store entries.
|
||||
let mut g = GraphBuilder::new("sma_signal");
|
||||
let fast = g.add(Sma::builder().named("fast").bind("length", Scalar::i64(3)));
|
||||
let slow = g.add(Sma::builder().named("slow").bind("length", Scalar::i64(4)));
|
||||
let spread = g.add(Sub::builder());
|
||||
let exposure = g.add(Bias::builder().named("bias").bind("scale", Scalar::f64(0.5)));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, vec![fast.input("series"), slow.input("series")]);
|
||||
g.connect(fast.output("value"), spread.input("lhs"));
|
||||
g.connect(slow.output("value"), spread.input("rhs"));
|
||||
g.connect(spread.output("value"), exposure.input("signal"));
|
||||
g.expose(exposure.output("bias"), "bias");
|
||||
let bound_variant = g.build().expect("bound-value probe wiring resolves");
|
||||
assert_ne!(
|
||||
h,
|
||||
topology_hash(&bound_variant),
|
||||
"same shape, different bound value -> different hash"
|
||||
);
|
||||
}
|
||||
|
||||
/// #158 acc 1 (content-id stability across the store round-trip): a blueprint's content
|
||||
/// id survives serialize -> reload -> re-serialize (#164 idempotence) — the property
|
||||
/// content-addressed reproduction rests on (the stored bytes re-hash to the members'
|
||||
/// topology_hash). `content_id` is the shared primitive `topology_hash` uses.
|
||||
/// manifest `topology_hash`, `content_id`'s own composite-facing computation).
|
||||
#[test]
|
||||
fn content_id_is_stable_across_the_store_round_trip() {
|
||||
let closed = load_closed_r_sma();
|
||||
@@ -2624,16 +2394,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #158 acc 1 (cross-surface agreement): `topology_hash` (from a live `Composite`, the
|
||||
/// run/sweep path) and the op-script `graph introspect --content-id` surface are the
|
||||
/// SAME hash of the SAME canonical bytes — both go through the one `content_id`
|
||||
/// primitive. Pins that the two command paths cannot silently drift apart.
|
||||
#[test]
|
||||
fn topology_hash_is_the_content_id_of_the_canonical_form() {
|
||||
let sig = load_closed_r_sma();
|
||||
assert_eq!(topology_hash(&sig), content_id(&blueprint_to_json(&sig).expect("serializes")));
|
||||
}
|
||||
|
||||
/// The op-script twin of the closed r-sma example (`load_closed_r_sma`): same
|
||||
/// topology — SMA(2)/SMA(4) over one `price` role, spread, Bias with `scale`
|
||||
/// BOUND to 0.5 (boundness is identity-bearing) — differing only in debug names
|
||||
@@ -2663,7 +2423,7 @@ mod tests {
|
||||
.expect("op-script twin builds");
|
||||
assert_ne!(
|
||||
content_id(&json),
|
||||
topology_hash(&rust_built),
|
||||
content_id(&blueprint_to_json(&rust_built).expect("a buildable signal serializes")),
|
||||
"authoring paths keep distinct content ids"
|
||||
);
|
||||
let loaded = blueprint_from_json(&json, &|t| std_vocabulary(t)).expect("twin reloads");
|
||||
|
||||
Reference in New Issue
Block a user