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:
2026-06-30 18:13:38 +02:00
parent df228986c4
commit 29a519dbf6
+215
View File
@@ -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).