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:
2026-06-30 22:45:01 +02:00
parent 2b1c24668d
commit 327d71b1d4
4 changed files with 21 additions and 803 deletions
-215
View File
@@ -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).