From 43a25547ce24c8f6dfe565f874d49b586f1c4261 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 30 Jun 2026 20:25:49 +0200 Subject: [PATCH] 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 ` 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 --- docs/plans/0092b-run-from-blueprint-cli.md | 182 +++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/plans/0092b-run-from-blueprint-cli.md diff --git a/docs/plans/0092b-run-from-blueprint-cli.md b/docs/plans/0092b-run-from-blueprint-cli.md new file mode 100644 index 0000000..c3e2d9b --- /dev/null +++ b/docs/plans/0092b-run-from-blueprint-cli.md @@ -0,0 +1,182 @@ +# 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 +` 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` + 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 ` 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 ` 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: + +```rust +// 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, [] if absent + let data = resolve_run_data(rest); // existing helper: --real [--from --to] else Synthetic + let seed = parse_seed(rest); // existing --seed parse, 0 default + let report = run_signal_stage1r(signal, ¶ms, 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` already derives `Deserialize`; the `{"I64":2}` + tagged form is the #155 representation). Absent `--params` → empty vec: + +```rust +/// Parse `--params '[{"I64":2},{"F64":0.5}]'` (the #155 tagged-Scalar cell array) +/// into the `Vec` `compile_with_params` wants. Absent flag -> empty. +fn parse_param_cells(args: &[&str]) -> Vec { + 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: + +```rust +#[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): + +```rust +#[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): + +```rust +#[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.