audit(0092): cycle close — run-from-blueprint; ledger refreshed, drift filed; rm ephemeral spec/plan
Cycle-close drift review (architect) for cycle 0092 / #165 (aura run <blueprint.json>). Drift-found on ledger currency only; the code is clean and correct. What holds (architect, evidence of review): the stage1_signal/wrap_stage1r split is behaviour-preserving (golden metrics byte-unchanged; loaded_signal bit-identical green; the STAGE1_R_SMA_* constants keep the hashed signal == the executed signal); sha2 + the topology_hash helper live in aura-cli only, aura-engine stays sha2-free (invariant 8 / C16); the resolver resolves the closed std_vocabulary set, unknown type -> UnknownNodeType, no registry (invariant 9); topology_hash is a correct Tier-1 optional and the RunManifest <-> RunManifestRead read-mirror both carry it. Resolved: ledger refreshed (C24 Status — the cycle-0092 run-from-blueprint realization + the topology_hash #158 anchor landing; #158 reframed from "premature" to "field landed, full reproduction remains"). Drift filed forward: #167 (single-source the stage1-r suffix-remapping lockstep — debt-med), and a scope note on #158 (the hash currently covers the signal only; a structural-variant content-id is the #158/#166 concern). The spec's illustrative instrument:"GER40" example (note-low) is moot — the ephemeral spec is removed here. Ephemeral 0092 spec + 0092/0092b plans git-rm'd per the lifecycle convention. closes #165
This commit is contained in:
+21
-1
@@ -1830,7 +1830,27 @@ old reader refuses cleanly (`LoadError::UnsupportedVersion` / `UnknownNodeType`,
|
||||
already green). The per-section required-flag scheme is deferred (no current
|
||||
Tier-2 section to validate it; recorded on #156).
|
||||
**Milestone delivered — 2026-06-30, cycle 0090 — the serializable format + loader + construction service.** A green end-to-end milestone fieldtest (`fieldtests/milestone-topology-as-data/`) proves the full author → serialize → load → construct → introspect → reproduce story from the public surface alone, 0 behavioural bugs; the round-trippable format (#155), the construction service (#157), and the forward-compat two-tier discipline (#156) all shipped. Polish filed forward: op-script grammar docs + a stale example (#163), the canonical trailing-newline / `Composite` value ergonomics (#164), CLI discoverability of `build`/`introspect` (#159).
|
||||
**Deferred to the World / project-as-crate cycle** (moved out of this milestone — they address a problem that only arises once the World *generates* runs from blueprint-data, which does not exist yet: `commit` still fully identifies every hard-wired topology): content-addressed topology identity in the manifest (#158, C18) — premature until a run is built *from* a serializable blueprint; retiring the pre-C24
|
||||
**Realization (2026-06-30, cycle 0092 — runs are built FROM blueprint-data, #165).**
|
||||
`aura run <blueprint.json>` now loads a serialized **signal** blueprint
|
||||
(`blueprint_from_json` → the closed `std_vocabulary`; an unknown type fails clean as
|
||||
`UnknownNodeType`, the data-plane face of invariant 9), wraps it in the Stage-1-R run
|
||||
scaffolding (sinks / broker / data supplied **at run**, not serialized — C24's deferred
|
||||
set), runs it, and emits a `RunReport` **bit-identical** (C1) to its Rust-built twin —
|
||||
proven by `loaded_signal_runs_bit_identical_to_rust_built`. The `RunManifest` now carries
|
||||
a **`topology_hash`**: SHA256 of the canonical (#164) `blueprint_to_json`, the #158
|
||||
reproducibility anchor, a Tier-1 optional field (#156). The hash + helper live
|
||||
research-side (`aura-cli` + `sha2`), off the frozen engine (invariant 8). This is the
|
||||
**keystone of the World/C21 milestone**: topology-as-data is now *runnable*, not only
|
||||
serializable. The harness wrap was made shareable — `stage1_r_graph()` =
|
||||
`wrap_stage1r(stage1_signal(...))`, so the Rust path and the data path are the same seam
|
||||
over the same signal (a behaviour-preserving C19/C23 restructure). Scope note: the hash
|
||||
covers the **signal** only (the fixed scaffolding is identified by `commit`); a content-id
|
||||
distinguishing harness-structural variants is a #158/#166 concern once scaffolding varies.
|
||||
|
||||
**Deferred to the World / project-as-crate cycle**: **full** content-addressed
|
||||
reproduction (#158, C18) — the `topology_hash` field landed cycle 0092; what remains is
|
||||
re-deriving the FlatGraph from a stored manifest and a content-id that covers
|
||||
structural-axis variants; retiring the pre-C24
|
||||
hard-wired `aura-cli` harnesses (`HarnessKind`, `run_stage1_r`, `*_sweep_family`) once
|
||||
the project-as-crate layer lands (#159, paired with #157's data-authoring surface). **Out of the first cut's round-trippable set**
|
||||
(deliberate; fails clean as `UnknownNodeType`, never a silent wrong graph): recording
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
# Run a harness from a serialized blueprint — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0092-run-from-blueprint.md` (#165)
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** `aura run <blueprint.json> [--params <json>]` loads a serialized signal
|
||||
blueprint, wraps it in the Stage-1-R run scaffolding via a shared seam extracted
|
||||
from `run_stage1_r`, runs it, and emits a `RunReport` whose manifest carries a
|
||||
`topology_hash` — bit-identical to the Rust-builder path (C1).
|
||||
|
||||
**Architecture:** A serialized blueprint is a *signal* (sinks/broker/data out of
|
||||
the round-trippable set, C24), so `aura run` wraps the loaded signal in the
|
||||
existing scaffolding. The wrap is shared: `stage1_r_graph` is restructured into
|
||||
`stage1_signal()` + `wrap_stage1r(signal,…)`, and a shared `run_signal_stage1r`
|
||||
runs either the Rust-built or the loaded signal — so "bit-identical to the Rust
|
||||
path" is the same seam over the same signal.
|
||||
|
||||
**Tech Stack:** `aura-cli` (the seam + CLI + the `sha2` hash, research-side),
|
||||
`aura-engine` (`RunManifest.topology_hash` field only — no `sha2` in the frozen
|
||||
engine, invariant 8).
|
||||
|
||||
**Task order / focus:** Tasks 1–2 are the risky/mechanical infrastructure (the
|
||||
behaviour-preserving restructure + the workspace-wide field add); run them and
|
||||
verify behaviour-preservation BEFORE Tasks 3–5 (seam + CLI + demo/tests).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Restructure `stage1_r_graph` into `stage1_signal()` + `wrap_stage1r()` (behaviour-preserving)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs:2630-2790` (`stage1_r_graph`)
|
||||
|
||||
This is a behaviour-preserving restructure, gated by the existing tests staying
|
||||
green — NOT a behaviour change. The signal nodes (`fast`, `slow`, `spread`,
|
||||
`exposure`) move into a standalone `Composite` with a `price` input-role and a
|
||||
`bias` output; everything else (broker / sinks / `exec` / cost / `vol_proxy`)
|
||||
stays in `wrap_stage1r`, which nests the signal and wires price + bias across the
|
||||
boundary.
|
||||
|
||||
- [ ] **Step 1: Add `stage1_signal`** — extract the signal leg (current
|
||||
`main.rs:2644-2655` + the price feed into `fast`/`slow` from `:2703-2715` + the
|
||||
spread/exposure wiring) into:
|
||||
|
||||
```rust
|
||||
/// The Stage-1 signal leg as a standalone, serializable Composite: SMA-cross →
|
||||
/// Bias. Exposes a `price` input-role and a single `bias` output — the boundary
|
||||
/// shape a serialized blueprint round-trips (the `blueprint_serde_e2e.rs`
|
||||
/// template). The two SMA knobs are bound when `Some` (byte-identical to the old
|
||||
/// build-time bind) and left open as `fast.length` / `slow.length` when `None`.
|
||||
fn stage1_signal(fast_len: Option<i64>, slow_len: Option<i64>) -> Composite {
|
||||
let mut g = GraphBuilder::new("stage1_signal");
|
||||
let mut fast_b = Sma::builder().named("fast");
|
||||
if let Some(l) = fast_len {
|
||||
fast_b = fast_b.bind("length", Scalar::i64(l));
|
||||
}
|
||||
let fast = g.add(fast_b);
|
||||
let mut slow_b = Sma::builder().named("slow");
|
||||
if let Some(l) = slow_len {
|
||||
slow_b = slow_b.bind("length", Scalar::i64(l));
|
||||
}
|
||||
let slow = g.add(slow_b);
|
||||
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");
|
||||
g.build().expect("stage1 signal wiring resolves")
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: confirm the exact `expose` method name/signature against `GraphBuilder`
|
||||
(the recon names `g.expose(handle, "name")`; the `blueprint_serde_e2e.rs`
|
||||
`signal()` fixture at `:49-70` is the authoritative template — match its
|
||||
role + single-output-expose shape exactly).
|
||||
|
||||
- [ ] **Step 2: Rename `stage1_r_graph` → `wrap_stage1r` taking a `signal`** —
|
||||
the body keeps everything EXCEPT the signal nodes; it nests the passed `signal`
|
||||
and rewires the boundary. The signature drops `fast_len`/`slow_len`:
|
||||
|
||||
```rust
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
fn wrap_stage1r(
|
||||
signal: Composite,
|
||||
tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_r: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_req: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
stop_open: bool,
|
||||
reduce: bool,
|
||||
cost: Option<(CostConfig, mpsc::Sender<(Timestamp, Vec<Scalar>)>, mpsc::Sender<(Timestamp, Vec<Scalar>)>)>,
|
||||
) -> Composite {
|
||||
let mut g = GraphBuilder::new("stage1_r");
|
||||
let sig = g.add(BlueprintNode::Composite(signal)); // nest the signal
|
||||
// ... broker / eq / ex / exec / rrec / vol_proxy / cost — VERBATIM from the
|
||||
// current stage1_r_graph body (main.rs:2656-2788) ...
|
||||
}
|
||||
```
|
||||
|
||||
The boundary rewiring (replacing the old internal `fast`/`slow`/`exposure`
|
||||
references):
|
||||
- the price feed (`:2703-2713`) feeds the signal's role instead of `fast`/`slow`:
|
||||
`price_targets` becomes `vec![sig.input("price"), broker.input("price"), exec.input("price")]`
|
||||
(+ the `vol_proxy` `vhi`/`vlo` pushes, unchanged);
|
||||
- drop the now-internal `fast`/`slow`/`spread` `connect`s (`:2714-2716` — they
|
||||
live inside `stage1_signal` now);
|
||||
- every `exposure.output("bias")` (`:2717-2719`) becomes `sig.output("bias")`:
|
||||
`broker.input("exposure")`, `ex.input("col[0]")`, `exec.input("bias")`.
|
||||
- everything from `:2720` down (broker.equity, the PM record loop, the
|
||||
`!reduce` r_equity + cost block) is VERBATIM.
|
||||
|
||||
- [ ] **Step 3: Reinstate `stage1_r_graph` as a thin wrapper** so all callers and
|
||||
the cost/sweep paths are untouched:
|
||||
|
||||
```rust
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
fn stage1_r_graph(
|
||||
tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_r: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
tx_req: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
|
||||
fast_len: Option<i64>,
|
||||
slow_len: Option<i64>,
|
||||
stop_open: bool,
|
||||
reduce: bool,
|
||||
cost: Option<(CostConfig, mpsc::Sender<(Timestamp, Vec<Scalar>)>, mpsc::Sender<(Timestamp, Vec<Scalar>)>)>,
|
||||
) -> Composite {
|
||||
wrap_stage1r(stage1_signal(fast_len, slow_len), tx_eq, tx_ex, tx_r, tx_req, stop_open, reduce, cost)
|
||||
}
|
||||
```
|
||||
|
||||
(`BlueprintNode` must be imported in `main.rs`; the recon notes `Composite`,
|
||||
`GraphBuilder` are already imported.)
|
||||
|
||||
- [ ] **Step 4: Verify behaviour-preservation (THE GATE).**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run`
|
||||
Expected: PASS — every stage1-r / sweep cli test unchanged.
|
||||
|
||||
Run: `cargo test -p aura-cli run_stage1_r_synthetic_folds_an_r_block`
|
||||
Expected: PASS — the structural stage1-r metrics test unchanged.
|
||||
|
||||
Run: `cargo test -p aura-engine --test blueprint_serde_e2e`
|
||||
Expected: PASS — the bit-identical round-trip template green.
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — no behaviour change anywhere. If any stage1-r metric or trace
|
||||
test changes, the restructure altered the graph: STOP, this is not
|
||||
behaviour-preserving (route to `debug`).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add `RunManifest.topology_hash` (Tier-1 optional) + thread `None` into every literal
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/report.rs:53` (add the field)
|
||||
- Modify: 24 `RunManifest { … }` literal sites across the workspace (add `topology_hash: None`)
|
||||
|
||||
- [ ] **Step 1: Add the field** after `instrument` (`report.rs:53`):
|
||||
|
||||
```rust
|
||||
/// SHA256 (hex) of the canonical `blueprint_to_json` of the run's signal
|
||||
/// topology — the #158 reproducibility anchor. `None` for a run not built
|
||||
/// from a serialized blueprint and for every pre-0092 line. One-directional
|
||||
/// serde widening (Tier-1, #156), identical idiom to `selection` / `instrument`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub topology_hash: Option<String>,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build the workspace to enumerate the broken literal sites**
|
||||
|
||||
Run: `cargo build --workspace --all-targets 2>&1 | grep "missing field"`
|
||||
Expected: ~24 `missing field topology_hash in initializer of RunManifest` errors.
|
||||
This compiler enumeration is authoritative (the recon's 24-site list is advisory).
|
||||
|
||||
- [ ] **Step 3: Add `topology_hash: None` to every flagged literal.** The
|
||||
recon-enumerated sites (confirm against the build): `main.rs:172`
|
||||
(`sim_optimal_manifest`), `blueprint.rs:997`, `mc.rs:239,264`,
|
||||
`report.rs:273,290,299,316,446,550,580,602`, `sweep.rs:664`,
|
||||
`walkforward.rs:288`, `tests/random_sweep_e2e.rs:112`, the four `aura-ingest`
|
||||
examples/tests, `aura-registry` `compat.rs:71` / `lib.rs:546,886` /
|
||||
`trace_store.rs:250`. Each adds `topology_hash: None` alongside the existing
|
||||
`selection`/`instrument` defaults.
|
||||
|
||||
- [ ] **Step 4: Verify the field is Tier-1 (byte-identical when None)**
|
||||
|
||||
Run: `cargo test -p aura-engine --lib report`
|
||||
Expected: PASS — `to_json_renders_the_canonical_form`,
|
||||
`runmanifest_without_instrument_field_deserialises_to_none`,
|
||||
`runreport_serde_round_trips` all green: a `None` `topology_hash` omits its key,
|
||||
so every existing JSON pin is byte-unchanged (#156 Tier-1).
|
||||
|
||||
Run: `cargo build --workspace --all-targets`
|
||||
Expected: 0 errors.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: The shared run seam `run_signal_stage1r` + the `sha256_hex` helper + `sha2`
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/Cargo.toml` (add `sha2`)
|
||||
- Modify: `crates/aura-cli/src/main.rs` (imports + the helper + the seam; rewire `run_stage1_r`)
|
||||
|
||||
- [ ] **Step 1: Add `sha2`** to `crates/aura-cli/Cargo.toml` `[dependencies]`
|
||||
(per-case C16 review: a vetted RustCrypto crate, SHA256 user-settled; lives in
|
||||
the research-side CLI, not the frozen engine):
|
||||
|
||||
```toml
|
||||
sha2 = "0.10"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: The hash helper + imports.** Add to `main.rs`
|
||||
`use aura_engine::{… , blueprint_from_json, blueprint_to_json, BlueprintNode};`
|
||||
and `use aura_std::{… , std_vocabulary};`, then:
|
||||
|
||||
```rust
|
||||
/// SHA256 (hex) of the canonical (#164, no-trailing-newline) serialization of a
|
||||
/// signal blueprint — the run's `topology_hash` (#158). Research-side (aura-cli),
|
||||
/// off the frozen engine (invariant 8).
|
||||
fn topology_hash(signal: &Composite) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
let canonical = blueprint_to_json(signal).expect("a buildable signal serializes");
|
||||
let digest = Sha256::digest(canonical.as_bytes());
|
||||
digest.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: The shared run seam.** The basic (no-cost) run path that both the
|
||||
bit-identical test and `aura run` use. `manifest.params` derives from the loaded
|
||||
signal's open-param names (`param_space`) zipped with the bound values:
|
||||
|
||||
```rust
|
||||
/// Run a signal blueprint through the Stage-1-R scaffolding: hash the signal,
|
||||
/// wrap it (broker + equity/exposure/R sinks), compile with `params`, bootstrap,
|
||||
/// run over `data`, and build the RunReport (manifest carries topology_hash).
|
||||
/// The single construction+run path shared by `aura run` and its bit-identical
|
||||
/// test.
|
||||
fn run_signal_stage1r(signal: Composite, params: &[Scalar], data: RunData, seed: u64) -> RunReport {
|
||||
let topo = topology_hash(&signal); // before signal is consumed
|
||||
let names: Vec<String> = signal.param_space().iter().map(|p| p.name.clone()).collect(); // confirm ParamSpec shape
|
||||
let (tx_eq, rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, rx_r) = mpsc::channel();
|
||||
let (tx_req, rx_req) = mpsc::channel();
|
||||
let wrapped = wrap_stage1r(signal, tx_eq, tx_ex, tx_r, tx_req, false, false, None);
|
||||
let flat = wrapped.compile_with_params(params).expect("signal binds + wraps to a valid harness");
|
||||
let mut h = Harness::bootstrap(flat).expect("valid stage1-r harness");
|
||||
let (sources, window, pip_size): (Vec<Box<dyn aura_engine::Source>>, _, f64) = match &data {
|
||||
RunData::Synthetic => {
|
||||
let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(VecSource::new(stage1_r_prices()))];
|
||||
let window = window_of(&sources).expect("non-empty synthetic stream");
|
||||
(sources, window, SYNTHETIC_PIP_SIZE)
|
||||
}
|
||||
RunData::Real { symbol, from, to } => {
|
||||
let (source, window, pip_size) = open_real_source(symbol, *from, *to);
|
||||
(vec![source], window, pip_size)
|
||||
}
|
||||
};
|
||||
h.run(sources);
|
||||
let eq_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_eq.try_iter().collect();
|
||||
let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_ex.try_iter().collect();
|
||||
let r_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_r.try_iter().collect();
|
||||
let _req_rows: Vec<(Timestamp, Vec<Scalar>)> = rx_req.try_iter().collect();
|
||||
let named_params: Vec<(String, Scalar)> = names.into_iter().zip(params.iter().copied()).collect();
|
||||
let mut manifest = sim_optimal_manifest(named_params, window, seed, pip_size);
|
||||
manifest.broker = stage1_r_broker_label(pip_size);
|
||||
manifest.topology_hash = Some(topo);
|
||||
let mut metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
|
||||
metrics.r = Some(summarize_r(&r_rows, &[]));
|
||||
RunReport { manifest, metrics }
|
||||
}
|
||||
```
|
||||
|
||||
NOTE (confirm at implement time, all recon-flagged): `Composite::param_space()`
|
||||
return type + the field exposing the param name (`ParamSpec.name` or similar) —
|
||||
match the existing `compile_with_params` arity-check source (`blueprint.rs:258`);
|
||||
`Scalar` is `Copy` (the `.copied()`); `summarize` / `summarize_r` /
|
||||
`f64_field` / `sim_optimal_manifest` / `stage1_r_broker_label` signatures as in
|
||||
`run_stage1_r`.
|
||||
|
||||
- [ ] **Step 4: Give `run_stage1_r` its `topology_hash` via the shared signal.**
|
||||
In `run_stage1_r` (`main.rs:3035` + `:3076`), the manifest must gain the hash of
|
||||
its signal (behaviour-preserving on metrics/traces; the manifest GAINS the key by
|
||||
design, Q4). Minimal edit — after `manifest.broker = …` (`:3076`):
|
||||
|
||||
```rust
|
||||
manifest.topology_hash = Some(topology_hash(&stage1_signal(Some(2), Some(4))));
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify**
|
||||
|
||||
Run: `cargo test -p aura-cli`
|
||||
Expected: PASS — `run_stage1_r_synthetic_folds_an_r_block` green (metrics
|
||||
unchanged); the manifest now carries `topology_hash` (no exact-JSON pin on the
|
||||
stage1-r manifest breaks, recon-verified).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: The `aura run <blueprint.json>` CLI arm
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs:3117-3310` (`HarnessKind`, `parse_run_args`, `run_dispatch`, the `["run", …]` arm)
|
||||
|
||||
- [ ] **Step 1: Add the blueprint arm BEFORE the harness-kind parse.** In the
|
||||
`run` dispatch, if the first positional argument is an existing `.json` file,
|
||||
take the blueprint path; otherwise the existing `HarnessKind` path is untouched
|
||||
(#159 stays). Parse new flags `--params <json>`, `--seed <n>`, `--pip-size <n>`
|
||||
(the `--real`/`--from`/`--to` data flags already exist on the run path). `--params`
|
||||
parses a JSON array of typed `Scalar` cells (`[{"I64":2},{"F64":0.5}]`, the #155
|
||||
representation):
|
||||
|
||||
```rust
|
||||
// in the run-subcommand dispatch, before parse_run_args(rest):
|
||||
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(/* --params arg or [] */);
|
||||
let data = /* --real <SYM> [--from --to] → RunData::Real, else RunData::Synthetic */;
|
||||
let seed = /* --seed or 0 */;
|
||||
let report = run_signal_stage1r(signal, ¶ms, data, seed);
|
||||
println!("{}", report.to_json());
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Thread the data/seed parsing through the existing `parse_run_args` helpers where
|
||||
they already exist (`--real`/`--from`/`--to`); add a small `parse_param_cells(&str) ->
|
||||
Vec<Scalar>` parsing the JSON cell array via `serde_json` into `Vec<Scalar>`
|
||||
(Scalar already derives Deserialize — confirm).
|
||||
|
||||
- [ ] **Step 2: Verify it builds + the existing run dispatch is intact**
|
||||
|
||||
Run: `cargo build -p aura-cli`
|
||||
Expected: 0 errors.
|
||||
|
||||
Run: `cargo test -p aura-cli parse_run_args`
|
||||
Expected: PASS — the existing harness-kind arg parsing unchanged.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Demo blueprint + the bit-identical / E2E tests
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/aura-cli/tests/fixtures/stage1_signal.json` — the demo signal blueprint
|
||||
- Test: `crates/aura-cli/src/main.rs` (unit: bit-identical + topology_hash determinism)
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` (E2E: `aura run <demo>` + unknown-type)
|
||||
|
||||
- [ ] **Step 1: Generate the demo signal blueprint** from the canonical signal so
|
||||
it is guaranteed loadable:
|
||||
|
||||
Run: `cargo run -p aura-cli -- run --emit-signal > /dev/null` is NOT available;
|
||||
instead generate it in a one-off test or via a tiny helper that writes
|
||||
`blueprint_to_json(&stage1_signal(Some(2), Some(4)))` to the fixture path. Acceptable:
|
||||
add a `#[test] fn write_demo_signal_fixture()` (ignored by default, `#[ignore]`) that
|
||||
emits it, OR commit the JSON produced by `blueprint_to_json(&stage1_signal(Some(2),Some(4)))`.
|
||||
The fixture content is the `format_version:1` signal blueprint (4 nodes: fast/slow
|
||||
SMA, Sub, Bias; one `price` role; one `bias` output).
|
||||
|
||||
- [ ] **Step 2: The bit-identical test** (unit, in `main.rs` test module):
|
||||
|
||||
```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 report_a = run_signal_stage1r(stage1_signal(Some(2), Some(4)), &[], RunData::Synthetic, 0);
|
||||
let report_b = run_signal_stage1r(loaded, &[], RunData::Synthetic, 0);
|
||||
assert_eq!(report_a.to_json(), report_b.to_json(), "loaded run is bit-identical incl. topology_hash");
|
||||
assert!(report_a.manifest.topology_hash.as_ref().expect("hash present").len() == 64, "64-hex sha256");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: topology_hash determinism test:**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn topology_hash_is_stable_and_distinguishes() {
|
||||
let h1 = topology_hash(&stage1_signal(Some(2), Some(4)));
|
||||
let h2 = topology_hash(&stage1_signal(Some(2), Some(4)));
|
||||
assert_eq!(h1, h2, "same signal -> same hash");
|
||||
assert_ne!(h1, topology_hash(&stage1_signal(Some(3), Some(4))), "different topology -> different hash");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: E2E** in `crates/aura-cli/tests/cli_run.rs` (the binary-spawn
|
||||
pattern already in that file): `aura run tests/fixtures/stage1_signal.json` exits
|
||||
0 and prints a `RunReport` JSON containing `"topology_hash":"` + 64 hex chars; an
|
||||
unknown-type blueprint (a fixture referencing `"type":"Nope"`) exits non-zero with
|
||||
an `UnknownNodeType`-derived message.
|
||||
|
||||
- [ ] **Step 5: 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.
|
||||
@@ -1,182 +0,0 @@
|
||||
# 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:
|
||||
|
||||
```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<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, ¶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>` (`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<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:
|
||||
|
||||
```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.
|
||||
@@ -1,215 +0,0 @@
|
||||
# Run a harness from a serialized blueprint (`aura run <blueprint.json>`) — Design Spec
|
||||
|
||||
**Date:** 2026-06-30
|
||||
**Status:** Draft — awaiting grounding-check + sign-off
|
||||
**Authors:** orchestrator + Claude
|
||||
**Parent:** World/C21 milestone (run + orchestrate harness families from blueprint-data); reference issue #165
|
||||
|
||||
## Goal
|
||||
|
||||
Turn a serialized C24 signal blueprint into an actual run from the CLI:
|
||||
|
||||
```
|
||||
aura run <blueprint.json> [--params <json>] [--real <SYM> [--from <ms>] [--to <ms>]] [--seed <n>] [--pip-size <n>]
|
||||
```
|
||||
|
||||
loads the blueprint (`blueprint_from_json`), wraps the loaded **signal** in the
|
||||
existing Stage-1-R run scaffolding (broker + equity/exposure recording sinks +
|
||||
data binding), compiles, bootstraps, runs, and prints a `RunReport` whose
|
||||
manifest carries a new `topology_hash`. The run is **bit-identical** to the
|
||||
Rust-builder path for the same signal + params + seed + data (C1).
|
||||
|
||||
This closes the data → bootstrap → run loop the cycle-0090 milestone fieldtest
|
||||
flagged: the CLI could author and emit topology (`aura graph build`, #157) but
|
||||
could not run an emitted blueprint.
|
||||
|
||||
## Architecture
|
||||
|
||||
A serialized blueprint is a **signal** — a strategy `Composite` (≤1 output, C8).
|
||||
Recording sinks (`Recorder`, a runtime `mpsc::Sender`), brokers (`SimBroker`, a
|
||||
construction-arg builder), the data source, and the clock are deliberately **out**
|
||||
of the round-trippable set (C24 ledger, "Out of the first cut's round-trippable
|
||||
set"). So `aura run` does not "just run" a blueprint — it **wraps** the loaded
|
||||
signal in a runnable harness exactly as the Rust path does.
|
||||
|
||||
The load-bearing move is a **shared wrapping seam**: the signal → Stage-1-R
|
||||
harness → run → `RunReport` assembly currently inlined in `run_stage1_r`
|
||||
(`crates/aura-cli/src/main.rs`) is extracted into a reusable function that takes a
|
||||
**signal `Composite`** plus its run bindings (params, data, seed, pip). Both
|
||||
`run_stage1_r` (with its hard-wired `stage1_r_graph()` signal) and the new
|
||||
`aura run <blueprint.json>` (with the loaded signal) call it — so "bit-identical
|
||||
to the Rust path" is, by construction, *the same function over the same signal*,
|
||||
not two implementations that must be kept in step.
|
||||
|
||||
The round-trip property itself is already proven: `blueprint_serde_e2e.rs` builds
|
||||
a signal, serializes it, loads it, wraps it with a `Recorder`, runs both twins,
|
||||
and asserts bit-identical output (the C24 acceptance). Cycle 1 lifts that property
|
||||
to the CLI and adds the manifest `topology_hash`.
|
||||
|
||||
The existing harness-kind dispatch (`HarnessKind`, `run_sample`, `run_stage1_r`,
|
||||
the `*_sweep_family` verbs) stays untouched this cycle (its retirement is #159,
|
||||
gated on the project-as-crate layer). `aura run` distinguishes a blueprint path by
|
||||
its `.json` extension / file existence; a non-path argument keeps the existing
|
||||
harness-kind behaviour.
|
||||
|
||||
## Concrete code shapes
|
||||
|
||||
### The user-facing program (the criterion's evidence)
|
||||
|
||||
Author + serialize a signal once (via the #157 op-script or a Rust builder), then:
|
||||
|
||||
```console
|
||||
$ aura graph build < sma_cross_ops.json > sma_cross.json # an existing #157 path, or any serialized signal
|
||||
$ aura run sma_cross.json --params '[{"I64":2},{"I64":4},{"F64":0.5}]'
|
||||
{"manifest":{"commit":"<sha>","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["bias_scale",{"F64":0.5}]],
|
||||
"window":[<ts>,<ts>],"seed":0,"broker":"sim-optimal@pip=<p>","topology_hash":"<64-hex>"},
|
||||
"metrics":{...R metrics...}}
|
||||
|
||||
$ aura run sma_cross.json --real GER40 # same signal, real M1 data
|
||||
{"manifest":{...,"instrument":"GER40","topology_hash":"<same-64-hex>"},"metrics":{...}}
|
||||
```
|
||||
|
||||
The blueprint carries the **topology** (which nodes, how wired, bound params); the
|
||||
broker, sinks, data, and seed are supplied at run — node logic stays Rust (C17).
|
||||
|
||||
### The shared wrapping seam (before → after)
|
||||
|
||||
Before — the wrapping is inlined in `run_stage1_r` (`main.rs`):
|
||||
|
||||
```rust
|
||||
fn run_stage1_r(pip_size: f64, source: ...) {
|
||||
let graph = stage1_r_graph(/* hard-wired signal + scaffolding */);
|
||||
let flat = graph.compile_with_params(&[]).expect("compiles");
|
||||
let h = Harness::bootstrap(flat).expect("bootstraps");
|
||||
let report = /* run, drain sinks, build manifest */;
|
||||
println!("{}", report.to_json());
|
||||
}
|
||||
```
|
||||
|
||||
After — a reusable seam takes a **signal** + bindings; `run_stage1_r` and
|
||||
`aura run` both call it:
|
||||
|
||||
```rust
|
||||
/// Wrap a signal Composite in the Stage-1-R run scaffolding (broker +
|
||||
/// equity/exposure sinks), compile, bootstrap, run, and build the RunReport.
|
||||
/// The single construction path shared by the hard-wired and data-driven runs.
|
||||
fn run_signal_stage1r(signal: Composite, bind: RunBindings) -> RunReport {
|
||||
let topology_hash = sha256_hex(&blueprint_to_json(&signal).expect("serializes"));
|
||||
let harness_graph = wrap_stage1r(signal, bind.pip_size); // adds broker + sinks (run-time, not serialized)
|
||||
let flat = harness_graph.compile_with_params(&bind.params)?;
|
||||
let h = Harness::bootstrap(flat)?;
|
||||
// ... run over bind.sources, drain sinks, build R metrics ...
|
||||
let mut manifest = sim_optimal_manifest(named_params, window, bind.seed, bind.pip_size);
|
||||
manifest.broker = stage1_r_broker_label(bind.pip_size);
|
||||
manifest.topology_hash = Some(topology_hash); // NEW
|
||||
RunReport { manifest, metrics }
|
||||
}
|
||||
|
||||
// aura run <blueprint.json>:
|
||||
let signal = blueprint_from_json(&fs::read_to_string(path)?, &std_vocabulary)?; // existing public API
|
||||
let report = run_signal_stage1r(signal, RunBindings::from_cli(args));
|
||||
println!("{}", report.to_json());
|
||||
```
|
||||
|
||||
### The new manifest field (before → after, Tier-1 optional)
|
||||
|
||||
`crates/aura-engine/src/report.rs` — `RunManifest`:
|
||||
|
||||
```rust
|
||||
pub struct RunManifest {
|
||||
pub commit: String,
|
||||
pub params: Vec<(String, Scalar)>,
|
||||
pub window: (Timestamp, Timestamp),
|
||||
pub seed: u64,
|
||||
pub broker: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub selection: Option<FamilySelection>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub instrument: Option<String>,
|
||||
+ /// SHA256 (hex) of the canonical `blueprint_to_json` of the run's signal
|
||||
+ /// topology (#158 reproducibility anchor). Tier-1 optional: absent on old
|
||||
+ /// manifests, which deserialize unchanged (#156).
|
||||
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
+ pub topology_hash: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
- **CLI surface** (`crates/aura-cli/src/main.rs`): `aura run <arg>` — when `<arg>`
|
||||
is a `.json` file, blueprint mode; else the existing harness-kind path. New flags
|
||||
`--params`, `--seed`, `--pip-size` (the `--real`/`--from`/`--to` data flags already
|
||||
exist on the run path). `--params` parses a JSON array of typed `Scalar` cells
|
||||
(`[{"I64":2},{"F64":0.5}]`, the #155 representation) into `Vec<Scalar>`.
|
||||
- **The shared seam** (`run_signal_stage1r` + `wrap_stage1r` + `RunBindings`): the
|
||||
extracted signal → harness → run → report assembly. Behaviour-preserving for
|
||||
`run_stage1_r` (same output).
|
||||
- **Topology hash** (`sha256_hex(blueprint_to_json(&signal))`): a small helper;
|
||||
`sha2` is the dependency (per-case review — a vetted standard crate entering the
|
||||
bot, C16 dependency policy; SHA256 is the user-settled algorithm).
|
||||
- **Manifest field** (`RunManifest.topology_hash`): the Tier-1 optional addition.
|
||||
|
||||
## Data flow
|
||||
|
||||
`<blueprint.json>` → `blueprint_from_json` → `Composite` (signal) →
|
||||
`wrap_stage1r` → `Composite` (harness, broker + sinks) → `compile_with_params` →
|
||||
`FlatGraph` → `Harness::bootstrap` → `Harness::run(sources)` → drain sinks → R
|
||||
metrics + `RunManifest{…, topology_hash}` → `RunReport` → stdout JSON. Window is
|
||||
probed from the sources post-run (`window_of`), as today.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Missing / unreadable file → `aura: <path>: <io error>`, exit non-zero.
|
||||
- `blueprint_from_json` `LoadError` (malformed JSON / `UnknownNodeType` /
|
||||
`UnsupportedVersion`) → printed by-identifier, exit non-zero. An unknown type
|
||||
fails clean (the #160 closed-set guard at the data-plane face; invariant 9).
|
||||
- `--params` arity / kind mismatch surfaces from `compile_with_params` as the
|
||||
existing by-identifier `CompileError`.
|
||||
- Compile / bootstrap faults (`UnconnectedPort`, `WouldCycle`, …) reported as today.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
- **Bit-identical run (the keystone property, C1):** in a test, build a signal
|
||||
`Composite` in Rust; run it via `run_signal_stage1r(signal.clone(), b)` →
|
||||
`report_A`; serialize → `blueprint_to_json` → `blueprint_from_json` → `signal'`;
|
||||
run `run_signal_stage1r(signal', b)` → `report_B`; assert `report_A == report_B`
|
||||
(metrics, traces, AND `topology_hash`). This rides the existing
|
||||
`blueprint_serde_e2e.rs` round-trip discipline.
|
||||
- **topology_hash determinism:** the same signal yields the same hash across two
|
||||
serializations; two structurally-different signals yield different hashes.
|
||||
- **Tier-1 forward-compat:** a `RunManifest` JSON without `topology_hash`
|
||||
deserializes (field `None`); a manifest with it round-trips. Rides the existing
|
||||
`report.rs` serde tests + the `#156` discipline.
|
||||
- **E2E (binary):** `aura run <demo>.json` over synthetic data exits 0 and prints a
|
||||
`RunReport` whose manifest contains a 64-hex `topology_hash`; an unknown-type
|
||||
blueprint exits non-zero with an `UnknownNodeType` message. A demo signal
|
||||
blueprint is shipped in-repo (e.g. `crates/aura-cli/blueprints/sma_cross.json`).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A blueprint serialized from a Rust-built signal, then loaded and run via the
|
||||
shared seam, produces a **bit-identical** `RunReport` (metrics + traces +
|
||||
`topology_hash`) to the Rust-built twin run through the same seam (C1).
|
||||
- [ ] The manifest carries `topology_hash` = SHA256 (hex) of the canonical
|
||||
(`#164`, no-trailing-newline) `blueprint_to_json` of the loaded signal.
|
||||
- [ ] `topology_hash` is a Tier-1 optional `RunManifest` field: existing manifests
|
||||
without it deserialize unchanged (#156).
|
||||
- [ ] `aura run <demo.json>` runs an in-repo demo blueprint end-to-end (exit 0,
|
||||
`RunReport` on stdout); an unknown node type fails clean (`UnknownNodeType`).
|
||||
- [ ] `run_stage1_r` is behaviour-preserving: its output is unchanged after the
|
||||
wrapping seam is extracted (the existing stage1-R tests stay green).
|
||||
|
||||
## Feature-acceptance (project criterion, applied)
|
||||
|
||||
- **Audience reaches for it:** a researcher who serialized a strategy (via the
|
||||
#157 op-script or the builder) naturally wants to *run* it without re-authoring
|
||||
Rust — the worked `aura run sma_cross.json` above is exactly that, and the
|
||||
cycle-0090 fieldtest named its absence as friction.
|
||||
- **Improves correctness / removes redundancy:** it makes a serialized topology
|
||||
executable and self-identifying (`topology_hash`), the reproducibility anchor
|
||||
#158 builds on; the shared seam removes the would-be redundancy of a second run
|
||||
path.
|
||||
- **Reintroduces no failure class:** node logic stays Rust (C17); the resolver
|
||||
resolves only compiled-in types (invariant 9, fails clean as `UnknownNodeType`);
|
||||
the run is deterministic (C1) and bit-identical to the Rust path; the deploy
|
||||
artifact stays frozen (this is a research-plane load path, invariant 8).
|
||||
Reference in New Issue
Block a user