spec: 0092 run-from-blueprint (boss-signed)
Cycle 1 of the World/C21 milestone (#165): `aura run <blueprint.json>` loads a serialized C24 signal blueprint, wraps it in the existing Stage-1-R run scaffolding via a shared seam extracted from run_stage1_r, compiles + bootstraps + runs, and emits a RunReport whose RunManifest carries a new Tier-1-optional topology_hash (SHA256 of the canonical blueprint form, #158 anchor). A serialized blueprint is a SIGNAL (sinks/brokers/data/clock out of the round-trippable set, C24), so aura run WRAPS the loaded signal in a runnable harness — the same wrap the Rust path does, shared as one function so "bit-identical to the Rust path" is the same seam over the same signal. The round-trip bit-identity is already proven (blueprint_serde_e2e.rs); cycle 1 lifts it to the CLI + adds topology_hash. Boss-signed on a grounding-check PASS: all six load-bearing current-behaviour assumptions ratified by named green tests (blueprint_from_json, the bit-identical round-trip, the run_stage1_r compile/bootstrap/run path, RunManifest's Tier-1 optional-field pattern, canonical blueprint_to_json, window_of). Derived wrapping decision recorded on #165. sha2 enters under the C16 per-case dependency review (SHA256 user-settled). refs #165
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# 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