Files
Aura/docs/plans/0092b-run-from-blueprint-cli.md
T
Brummel 43a25547ce plan: 0092b run-from-blueprint CLI arm + tests (continuation)
The remaining Tasks 4-5 of #165 after the ff4a1b3 infrastructure (restructure +
topology_hash + the shared run_signal_stage1r seam): the `aura run
<blueprint.json>` CLI arm (wiring the committed seam, retiring its transient
dead_code allow) + the loaded-signal bit-identical / determinism / E2E tests +
the in-repo demo blueprint. Resolves the Task-4 block by dropping the
consumer-less --pip-size flag (pip is data-derived).

refs #165
2026-06-30 20:25:49 +02:00

8.8 KiB

Run-from-blueprint — CLI arm + tests (continuation) — Implementation Plan

Parent spec: docs/specs/0092-run-from-blueprint.md (#165) Continues: docs/plans/0092-run-from-blueprint.md Tasks 1-3, shipped in commit ff4a1b3 (the restructure + RunManifest.topology_hash + the shared run_signal_stage1r seam). This plan is the remaining Tasks 4-5.

For agentic workers: REQUIRED SUB-SKILL: use the implement skill to run this plan. Steps use - [ ] checkboxes.

Goal: wire the committed run_signal_stage1r seam to a CLI aura run <blueprint.json> arm and prove it end-to-end (a loaded signal runs bit-identical to its Rust-built twin, manifest carries topology_hash).

Already in the tree (commit ff4a1b3, do NOT re-create):

  • run_signal_stage1r(signal: Composite, params: &[Scalar], data: RunData, seed: u64) -> RunReport (crates/aura-cli/src/main.rs:2940) — currently #[allow(dead_code)] (no production caller yet).
  • topology_hash(signal: &Composite) -> String (main.rs:2700), stage1_signal(...) (:2670), wrap_stage1r(...) (:2759).
  • RunManifest.topology_hash: Option<String> + the aura-registry read-mirror.
  • sha2 dep; blueprint_to_json, BlueprintNode imported.
  • Inline RED seam tests the Task-3 implementer added (a topology_hash assertion on the existing run_stage1_r test + run_signal_stage1r_folds_the_same_r_block_as_run_stage1_r). Do NOT duplicate these; Task 2 below adds only the LOADED-signal path tests.

Design decision (resolves the Task-4 block): the speculative --pip-size flag is DROPPED — pip_size is data-derived inside the run path (synthetic → SYNTHETIC_PIP_SIZE, real → the instrument sidecar via open_real_source/resolve_run_data), exactly as run_stage1_r does. The user-ratified surface is positional entry + --params + the hash, not a pip override.


Task 1: The aura run <blueprint.json> CLI arm

Files:

  • Modify: crates/aura-cli/src/main.rs (the run subcommand dispatch + run_signal_stage1r's #[allow(dead_code)])

  • Step 1: Add a blueprint dispatch arm BEFORE the existing harness-kind parse, so a .json file path routes to the data-driven path and every other aura run <kind> argument keeps the existing HarnessKind behaviour untouched (#159 stays). Read the current run dispatch + parse_run_args / resolve_run_data (the --real/--from/--to/--seed flags already exist) to thread data + seed. The arm:

// in the `["run", rest @ ..]` dispatch arm, before the HarnessKind parse:
if let Some(path) = rest.first().filter(|a| a.ends_with(".json") && std::path::Path::new(a).is_file()) {
    let doc = std::fs::read_to_string(path).unwrap_or_else(|e| { eprintln!("aura: {path}: {e}"); std::process::exit(2); });
    let signal = blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap_or_else(|e| { eprintln!("aura: {path}: {e:?}"); std::process::exit(2); });
    let params = parse_param_cells(rest);  // --params '[{"I64":2},...]' -> Vec<Scalar>, [] if absent
    let data = resolve_run_data(rest);     // existing helper: --real <SYM> [--from --to] else Synthetic
    let seed = parse_seed(rest);           // existing --seed parse, 0 default
    let report = run_signal_stage1r(signal, &params, data, seed);
    println!("{}", report.to_json());
    return;
}

Use the EXISTING data/seed parsing helpers (resolve_run_data and the --seed parse the implementer referenced in the block report) rather than new ones; add use aura_std::std_vocabulary; and blueprint_from_json to the imports.

  • Step 2: Add parse_param_cells — parse the --params JSON cell array into Vec<Scalar> (Scalar already derives Deserialize; the {"I64":2} tagged form is the #155 representation). Absent --params → empty vec:
/// Parse `--params '[{"I64":2},{"F64":0.5}]'` (the #155 tagged-Scalar cell array)
/// into the `Vec<Scalar>` `compile_with_params` wants. Absent flag -> empty.
fn parse_param_cells(args: &[&str]) -> Vec<Scalar> {
    match flag_value(args, "--params") {   // reuse the existing flag-extraction helper
        Some(json) => serde_json::from_str(json).unwrap_or_else(|e| { eprintln!("aura: --params: {e}"); std::process::exit(2); }),
        None => Vec::new(),
    }
}

(Confirm the existing flag-extraction helper's name — the recon/Task-1 implementer used one for --real/--seed; match it. If none is reusable, a minimal args.iter().position(|a| *a == "--params").and_then(...) is fine.)

  • Step 3: Retire the transient #[allow(dead_code)] on run_signal_stage1r (main.rs:2940 area) now that the CLI arm is its production caller.

  • Step 4: Verify the build + the existing dispatch is intact

Run: cargo build -p aura-cli Expected: 0 errors, 0 warnings (the dead_code allow is gone and the fn is now used, so clippy -D warnings is satisfied).

Run: cargo test -p aura-cli parse_run_args Expected: PASS — the existing harness-kind arg parsing unchanged.


Task 2: Demo blueprint + the loaded-signal E2E + bit-identical test

Files:

  • Create: crates/aura-cli/tests/fixtures/stage1_signal.json — the demo signal blueprint

  • Create: crates/aura-cli/tests/fixtures/unknown_node.json — a blueprint referencing a non-vocabulary type

  • Test: crates/aura-cli/src/main.rs test module (the loaded-signal bit-identical + determinism)

  • Test: crates/aura-cli/tests/cli_run.rs (binary E2E)

  • Step 1: Generate + commit the demo signal blueprint. It must equal blueprint_to_json(&stage1_signal(Some(2), Some(4))) so it loads cleanly. Add a one-off emitter and capture its output into the fixture:

Run: cargo test -p aura-cli emit_demo_signal_fixture -- --ignored --nocapture where the emitter is:

#[test]
#[ignore = "regenerates the committed demo fixture"]
fn emit_demo_signal_fixture() {
    let json = blueprint_to_json(&stage1_signal(Some(2), Some(4))).expect("serializes");
    std::fs::write("tests/fixtures/stage1_signal.json", &json).expect("write fixture");
}

Then verify the fixture is the format_version:1 signal (4 nodes: fast/slow SMA, Sub, Bias; one price role; one bias output). Also hand-write tests/fixtures/unknown_node.json = the same shape but one node "type":"Nope" (or copy the fixture and edit one type to a non-vocabulary name) for the failure E2E.

  • Step 2: The loaded-signal bit-identical test (unit, main.rs test module — the KEYSTONE property, distinct from the Task-3 Rust-twin seam test):
#[test]
fn loaded_signal_runs_bit_identical_to_rust_built() {
    let rust = stage1_signal(Some(2), Some(4));
    let json = blueprint_to_json(&rust).expect("serializes");
    let loaded = blueprint_from_json(&json, &|t| std_vocabulary(t)).expect("loads");
    let a = run_signal_stage1r(stage1_signal(Some(2), Some(4)), &[], RunData::Synthetic, 0);
    let b = run_signal_stage1r(loaded, &[], RunData::Synthetic, 0);
    assert_eq!(a.to_json(), b.to_json(), "loaded run is bit-identical incl. topology_hash");
    assert_eq!(a.manifest.topology_hash.as_ref().expect("hash").len(), 64, "64-hex sha256");
}

#[test]
fn topology_hash_is_stable_and_distinguishes() {
    let h = topology_hash(&stage1_signal(Some(2), Some(4)));
    assert_eq!(h, topology_hash(&stage1_signal(Some(2), Some(4))), "same signal -> same hash");
    assert_ne!(h, topology_hash(&stage1_signal(Some(3), Some(4))), "different topology -> different hash");
}
  • Step 3: The binary E2E in crates/aura-cli/tests/cli_run.rs (use the file's existing binary-spawn helper; if it spawns from the crate root, the fixture path is crates/aura-cli/tests/fixtures/stage1_signal.json relative to the workspace — match the existing fixture-loading tests in that file):
#[test]
fn aura_run_loads_and_runs_a_blueprint_file() {
    let (stdout, _stderr, ok) = run(&["run", "tests/fixtures/stage1_signal.json"]); // match the file's run() signature/cwd
    assert!(ok, "exit 0");
    assert!(stdout.contains("\"topology_hash\":\""), "manifest carries the topology hash: {stdout}");
}

#[test]
fn aura_run_rejects_an_unknown_node_blueprint() {
    let (_stdout, stderr, ok) = run(&["run", "tests/fixtures/unknown_node.json"]);
    assert!(!ok, "non-zero exit");
    assert!(stderr.contains("UnknownNodeType") || stderr.to_lowercase().contains("unknown node"), "names the cause: {stderr}");
}

(Confirm the cli_run.rs run(...) helper's exact signature + working directory — the existing tests there set the precedent; match them, including whether stdin is needed.)

  • Step 4: Full verification

Run: cargo test -p aura-cli loaded_signal_runs_bit_identical_to_rust_built Expected: PASS.

Run: cargo test --workspace Expected: PASS — full suite green.

Run: cargo clippy --workspace --all-targets -- -D warnings Expected: clean.