audit(0095): cycle close — MC over a loaded blueprint; ledger refreshed (C18/C21); rm ephemeral spec/plan
Architect drift review (scope cec7811..HEAD, the sole gate — no regression script) returned drift-clean with positive What-holds: - invariants 8/9: the whole cycle is aura-cli (main.rs + cli_run.rs + ephemeral docs); nothing entered aura-engine/aura-registry; FamilyKind::MonteCarlo, monte_carlo, append_family, mc_member_reports are pre-existing seams reused. - invariant 1 / C1: draws run disjoint-parallel via the engine monte_carlo seam (seed-input order, no within-sim concurrency); bit-identity proven by the reproduce round-trip (unit + E2E). - the C1 lockstep is a single shared fn: synthetic_walk_sources (one fixed SyntheticSpec) is called by BOTH the persist path and the reproduce MonteCarlo branch; pip is lockstepped too (both pass DataSource::Synthetic). - the sweep-sibling pattern matches: Result<McFamily,String> + boundary render. - scope honesty: WF deferral (#173, dep #169) and synthetic-walk-DGP (#172) are named in the spec + feat body, not silent. Carry-on debt (does NOT block close, filed forward, NOT new-this-cycle drift): synthetic_walk_sources' len:60 is hand-calibrated to the shallow stage1 warm-up; a deep-lookback closed blueprint warms poorly -> silent-vacuous draws with no error, and the anti-degenerate test only exercises the shallow fixture. Folded into #172 (real-data DGP retires the length calibration) per the architect's recommendation, rather than a new thread. Ledger refreshed: the C21 realization block now records the MC verb shipped (cycle 0095) with WF deferred; the C18 reproduction block notes aura reproduce now spans FamilyKind::MonteCarlo. Verified green (orchestrator, this session): cargo build --workspace clean; cargo clippy --workspace --all-targets -- -D warnings clean; cargo test --workspace green. Cycle is drift-clean (not a milestone close — #170's WF half remains, milestone stays open). refs #170
This commit is contained in:
+19
-4
@@ -1313,7 +1313,10 @@ content-addressing, and whether the id should exclude the composite **debug-name
|
||||
non-load-bearing symbol, invariant 11 — an op-script and the Rust builder produce
|
||||
different canonical forms today), remain deferred (#171, #170). Reproduction is proven on
|
||||
synthetic (deterministic) data; recorded-dataset reproduction rides the DataServer seam
|
||||
(#124).
|
||||
(#124). **Cycle 0095 (#170):** `aura reproduce` now also spans `FamilyKind::MonteCarlo` —
|
||||
branching on the family kind, it reconstructs each member's seed-driven synthetic walk from
|
||||
the recorded `manifest.seed` (the `Sweep` arm unchanged), so an `aura mc` family re-derives
|
||||
bit-identically through the same `run_blueprint_member` path.
|
||||
|
||||
### C19 — Bootstrap: blueprint → instance (recursive)
|
||||
**Guarantee.** Construction is a distinct phase, recursive at every level. Each
|
||||
@@ -1886,9 +1889,21 @@ reproducible per C18. Two design facts: a sweep needs an **open** blueprint (a f
|
||||
has an empty `param_space`, distinct from cycle-1's bound run fixture), and the signal is
|
||||
**re-loaded per member** from its serialized doc (the `Composite` is `!Clone` — its
|
||||
`PrimitiveBuilder`s hold `Box<dyn Fn>` build closures, #164 — and `bootstrap_with_cells`
|
||||
consumes the graph). Monte-Carlo / walk-forward over a loaded blueprint are the same machinery
|
||||
per verb (a tracked follow-on); per-member trace-writing on `--trace` and an axis-name
|
||||
discovery surface are tracked debt.
|
||||
consumes the graph). **Monte-Carlo over a loaded blueprint shipped (cycle 0095, #170).** `aura mc
|
||||
<blueprint.json> --seeds N` runs a **closed** blueprint across N seeds — each seed a distinct
|
||||
synthetic price walk (`synthetic_walk_sources`, the `mc_family` `SyntheticSpec` pattern), the
|
||||
draws disjoint-parallel via the engine `monte_carlo` seam (invariant 1) — and aggregates to a
|
||||
`FamilyKind::MonteCarlo` family with one stored blueprint per family (the C18 hook), so it
|
||||
`aura reproduce`s bit-identically (C1). MC binds no axis, so it needs a **closed** blueprint
|
||||
(the sweep's open/closed distinction inverted); an open one returns a named `Err` (rendered
|
||||
exit-2 at the `run_blueprint_mc` boundary, the sweep sibling's contract — no hidden exit in the
|
||||
pure builder). **Walk-forward is deferred (#173, dep #169):** a loaded blueprint declares no
|
||||
axes to re-fit per window, so it could only be a non-IS-refit windowed eval — which would
|
||||
collide with the hard-wired `walkforward` verb's IS-optimizing guarantee. The synthetic-walk DGP
|
||||
is the machinery, not trader-grade MC statistics; a real-data block-bootstrap — and the
|
||||
`synthetic_walk_sources` `len:60`↔warm-up coupling it retires (a deep-lookback closed blueprint
|
||||
warms poorly → silent-vacuous draws today) — ride #172. Per-member trace-writing on `--trace` and
|
||||
an axis-name discovery surface are tracked debt.
|
||||
|
||||
**Content-addressed reproduction shipped (cycle 0094, #158, C18)**: `topology_hash`
|
||||
landed cycle 0092; re-deriving a member's FlatGraph from a stored manifest + the
|
||||
|
||||
@@ -1,565 +0,0 @@
|
||||
# Monte-Carlo over a Loaded Blueprint — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0095-mc-over-a-loaded-blueprint.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Add `aura mc <blueprint.json> --seeds N` — a Monte-Carlo family from a
|
||||
loaded CLOSED blueprint, each seed drawing a distinct synthetic walk — persisted as
|
||||
`FamilyKind::MonteCarlo` with the content-addressed store hook, `aura reproduce`
|
||||
bit-identical.
|
||||
|
||||
**Architecture:** All code lands in `crates/aura-cli/src/main.rs` (engine + registry
|
||||
untouched — invariants 8/9). A new `.json`-discriminator on the `["mc", ..]` dispatch
|
||||
arm (mirroring the 0093 sweep arm) routes to `blueprint_mc_family` (builds an
|
||||
`McFamily` by running the fixed closed blueprint over N seed-driven synthetic walks
|
||||
through the shared `run_blueprint_member` path) → `run_blueprint_mc` (the persist seam:
|
||||
`put_blueprint` + `append_family(MonteCarlo)` + print). `reproduce_family_in` gains a
|
||||
`MonteCarlo` realization branch that reconstructs each member's seed-driven walk.
|
||||
|
||||
**Tech Stack:** Rust; `SyntheticSpec::source(seed)` (aura-engine), `McFamily`/`McDraw`/
|
||||
`McAggregate` (aura-engine), `run_blueprint_member` (unchanged), `Registry::put_blueprint`
|
||||
+ `append_family` (aura-registry), `topology_hash`/`content_id` (local).
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-cli/src/main.rs` — new `parse_mc_blueprint_args` + `McBlueprintArgs`
|
||||
(beside `parse_sweep_blueprint_args` ~3797-3835); new `synthetic_walk_sources` +
|
||||
`blueprint_mc_family` + `run_blueprint_mc` (beside `blueprint_sweep_family`/`run_blueprint_sweep`
|
||||
~3154-3246); the `MonteCarlo` branch in `reproduce_family_in` (~2503-2574, the run at ~2561);
|
||||
the `["mc", rest @ ..]` dispatch arm (~4006-4015).
|
||||
- Test: `crates/aura-cli/src/main.rs` — unit tests beside the sweep units (~5144-5230):
|
||||
`parse_mc_blueprint_args_grammar`, `blueprint_mc_family_seeds_differ`,
|
||||
`reproduce_family_re_derives_every_mc_member_bit_identically`.
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` — E2E beside the sweep/reproduce E2Es (~3296-3489):
|
||||
`aura_mc_over_a_blueprint_reproduces_bit_identically`, `aura_mc_rejects_an_open_blueprint`.
|
||||
|
||||
Facts pinned by plan-recon (line refs are current; re-anchor by symbol if drifted):
|
||||
- `run_blueprint_member(signal: Composite, point: &[Cell], space: &[ParamSpec], sources:
|
||||
Vec<Box<dyn aura_engine::Source>>, window: (Timestamp,Timestamp), seed: u64, pip: f64,
|
||||
topo: &str) -> RunReport` — **unchanged**; stamps `manifest.seed`/`manifest.topology_hash`.
|
||||
- `McDraw { pub seed: u64, pub report: RunReport }`, `McFamily { pub draws, pub aggregate }`,
|
||||
`McAggregate::from_draws(&[McDraw]) -> McAggregate` (all pub; `from_draws` needs draws non-empty).
|
||||
- `SyntheticSpec { start: f64, len: usize, step: i64 }`; `.source(seed) -> impl Source`;
|
||||
`window_of(&[Box<dyn Source>]) -> Option<(Timestamp,Timestamp)>`.
|
||||
- `mc_member_line(id, seed, report) -> String`; `mc_aggregate_json(&McAggregate) -> String`;
|
||||
`mc_member_reports(&McFamily) -> Vec<RunReport>`.
|
||||
- `Family { pub id, pub kind: FamilyKind, pub members: Vec<FamilyRunRecord> }`;
|
||||
`FamilyKind::MonteCarlo` is a variant. `stage1_signal(fast: Option<i64>, slow: Option<i64>)
|
||||
-> Composite` (Some binds → closed). `STAGE1_R_STOP_LENGTH = 3`, fixture SMA slow=4 → the
|
||||
graph warms in ~4 bars, so a len-60 walk yields many trades that differ across seeds.
|
||||
- All needed imports (`Cell`, `ParamSpec`, `Composite`, `McFamily`, `McDraw`, `McAggregate`,
|
||||
`SyntheticSpec`, `window_of`, `blueprint_from_json`, `blueprint_to_json`, `FamilyKind`,
|
||||
`mc_member_reports`, `std_vocabulary`) are already in `main.rs` — no `use` edits.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `parse_mc_blueprint_args` + `McBlueprintArgs`
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (beside `parse_sweep_blueprint_args` ~3797-3835)
|
||||
- Test: `crates/aura-cli/src/main.rs` (beside the sweep-arg unit `parse_sweep_blueprint_args_grammar` ~5107)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add to the `#[cfg(test)] mod tests` block, beside `parse_sweep_blueprint_args_grammar`:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn parse_mc_blueprint_args_grammar() {
|
||||
// --seeds required and parsed; default name "mc"; seeds >= 1; unknown/real flags rejected.
|
||||
let ok = parse_mc_blueprint_args(&["--seeds", "8", "--name", "mc1"]).expect("valid");
|
||||
assert_eq!(ok.n_seeds, 8);
|
||||
assert_eq!(ok.name, "mc1");
|
||||
let def = parse_mc_blueprint_args(&["--seeds", "3"]).expect("default name");
|
||||
assert_eq!(def.name, "mc");
|
||||
assert!(parse_mc_blueprint_args(&[]).is_err(), "--seeds is required");
|
||||
assert!(parse_mc_blueprint_args(&["--seeds", "0"]).is_err(), "seeds must be >= 1");
|
||||
assert!(parse_mc_blueprint_args(&["--seeds", "x"]).is_err(), "seeds must be a number");
|
||||
assert!(
|
||||
parse_mc_blueprint_args(&["--seeds", "4", "--real", "EURUSD"]).is_err(),
|
||||
"synthetic-only this cycle: --real is rejected (real-data MC is #172)"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura parse_mc_blueprint_args_grammar`
|
||||
Expected: FAIL — compile error `cannot find function parse_mc_blueprint_args` (the fn/struct do not exist yet).
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Add beside `parse_sweep_blueprint_args` (~3797):
|
||||
|
||||
```rust
|
||||
/// Parsed tail of `aura mc <blueprint.json> …`: the seed count + the family name.
|
||||
/// Synthetic-only this cycle (real-data MC rides #172), so `--real`/`--from`/`--to`
|
||||
/// are rejected as unknown flags. Mirrors `parse_sweep_blueprint_args`' shape.
|
||||
struct McBlueprintArgs {
|
||||
n_seeds: u64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn parse_mc_blueprint_args(rest: &[&str]) -> Result<McBlueprintArgs, String> {
|
||||
let usage = || "usage: mc <blueprint.json> --seeds <n> [--name <n>]".to_string();
|
||||
let mut n_seeds: Option<u64> = None;
|
||||
let mut name: Option<String> = None;
|
||||
let mut tail = rest;
|
||||
while let Some((flag, t)) = tail.split_first() {
|
||||
let (value, t) = t.split_first().ok_or_else(usage)?;
|
||||
match *flag {
|
||||
"--seeds" => {
|
||||
let n: u64 = value.parse().map_err(|_| usage())?;
|
||||
if n == 0 {
|
||||
return Err(usage());
|
||||
}
|
||||
n_seeds = Some(n);
|
||||
}
|
||||
"--name" if name.is_none() => name = Some((*value).to_string()),
|
||||
_ => return Err(usage()),
|
||||
}
|
||||
tail = t;
|
||||
}
|
||||
let n_seeds = n_seeds.ok_or_else(usage)?; // --seeds is required
|
||||
Ok(McBlueprintArgs { n_seeds, name: name.unwrap_or_else(|| "mc".to_string()) })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura parse_mc_blueprint_args_grammar`
|
||||
Expected: PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: `synthetic_walk_sources` + `blueprint_mc_family` (closed-blueprint guard)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (beside `blueprint_sweep_family` ~3154; `mc_family` pattern ~2210)
|
||||
- Test: `crates/aura-cli/src/main.rs` (beside `blueprint_sweep_member_equals_single_run_and_shares_topology_hash` ~5151)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn blueprint_mc_family_seeds_differ() {
|
||||
// MC over a CLOSED signal (both SMA knobs bound): 3 seeds -> 3 draws, one shared
|
||||
// topology_hash, and DIFFERING metrics — the seed reaches the DATA (a distinct
|
||||
// synthetic walk per draw), not just the manifest label. The anti-degenerate guard:
|
||||
// a regression to seed-as-label-only would make the three draws identical.
|
||||
let closed = stage1_signal(Some(2), Some(4));
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let family = blueprint_mc_family(&doc, 3, &DataSource::Synthetic);
|
||||
|
||||
assert_eq!(family.draws.len(), 3, "one draw per seed");
|
||||
assert_eq!(family.draws.iter().map(|d| d.seed).collect::<Vec<_>>(), vec![1, 2, 3]);
|
||||
let topo = family.draws[0].report.manifest.topology_hash.clone();
|
||||
assert!(topo.is_some(), "members carry a topology_hash");
|
||||
assert!(
|
||||
family.draws.iter().all(|d| d.report.manifest.topology_hash == topo),
|
||||
"all members share one topology_hash"
|
||||
);
|
||||
let m: Vec<_> = family.draws.iter().map(|d| &d.report.metrics).collect();
|
||||
assert!(m[0] != m[1] || m[1] != m[2], "seeds must yield differing realizations");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura blueprint_mc_family_seeds_differ`
|
||||
Expected: FAIL — compile error `cannot find function blueprint_mc_family` (fn does not exist yet).
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
Add beside `blueprint_sweep_family` (~3154):
|
||||
|
||||
```rust
|
||||
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — the `mc_family`
|
||||
/// pattern (a distinct realization per seed). A FIXED `SyntheticSpec` shared by the
|
||||
/// `aura mc <blueprint.json>` persist path AND the reproduce MonteCarlo branch, so the
|
||||
/// seed->walk reconstruction is bit-exact (C1). Length 60 comfortably warms the loaded
|
||||
/// stage1-r graph (SMA slow=4 + the len-3 vol stop) so draws produce differing trades.
|
||||
fn synthetic_walk_sources(seed: u64) -> Vec<Box<dyn aura_engine::Source>> {
|
||||
let spec = SyntheticSpec { start: 1.0, len: 60, step: 1 };
|
||||
vec![Box::new(spec.source(seed))]
|
||||
}
|
||||
|
||||
/// Build a Monte-Carlo family from a loaded CLOSED signal blueprint: run the fixed
|
||||
/// blueprint across `n_seeds` seeds, each seed drawing a distinct synthetic walk. The
|
||||
/// blueprint must be CLOSED (empty wrapped `param_space`) — MC binds no axis, so a free
|
||||
/// knob has no binder; an OPEN blueprint is rejected with a named error (exit 2) before
|
||||
/// any run, pre-empting the `compile_with_params` arity panic. Each draw runs the shared
|
||||
/// reduce-mode member path (`run_blueprint_member`, the same fn reproduce re-runs), so
|
||||
/// reproduction is bit-identical (C1); every member carries the shared `topology_hash`.
|
||||
fn blueprint_mc_family(doc: &str, n_seeds: u64, data: &DataSource) -> McFamily {
|
||||
let reload = |d: &str| {
|
||||
blueprint_from_json(d, &|t| std_vocabulary(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||
};
|
||||
let topo = topology_hash(&reload(doc));
|
||||
let pip = data.pip_size();
|
||||
// probe the wrapped param_space (as blueprint_sweep_family does); MC needs it empty.
|
||||
let (tx_eq, _) = mpsc::channel();
|
||||
let (tx_ex, _) = mpsc::channel();
|
||||
let (tx_r, _) = mpsc::channel();
|
||||
let (tx_req, _) = mpsc::channel();
|
||||
let space =
|
||||
wrap_stage1r(reload(doc), tx_eq, tx_ex, tx_r, tx_req, false, true, None).param_space();
|
||||
if !space.is_empty() {
|
||||
eprintln!(
|
||||
"aura: mc requires a closed blueprint (no free parameters); {} free knob(s) — \
|
||||
bind them or use `aura sweep --axis`",
|
||||
space.len()
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
let point: Vec<Cell> = Vec::new(); // closed -> empty point (as `aura run <blueprint.json>`)
|
||||
let draws: Vec<McDraw> = (1..=n_seeds)
|
||||
.map(|seed| {
|
||||
let sources = synthetic_walk_sources(seed);
|
||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||
let report =
|
||||
run_blueprint_member(reload(doc), &point, &space, sources, window, seed, pip, &topo);
|
||||
McDraw { seed, report }
|
||||
})
|
||||
.collect();
|
||||
let aggregate = McAggregate::from_draws(&draws);
|
||||
McFamily { draws, aggregate }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura blueprint_mc_family_seeds_differ`
|
||||
Expected: PASS. (If the three draws come back identical, the walk did not warm the
|
||||
stop — increase `SyntheticSpec.len`; 60 should suffice for slow=4 + stop-len 3.)
|
||||
|
||||
---
|
||||
|
||||
### Task 3: `reproduce_family_in` MonteCarlo realization branch
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (`reproduce_family_in` ~2503-2574; the run at ~2561-2570)
|
||||
- Test: `crates/aura-cli/src/main.rs` (beside `reproduce_family_re_derives_every_member_bit_identically` ~5194)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn reproduce_family_re_derives_every_mc_member_bit_identically() {
|
||||
let dir = std::env::temp_dir().join(format!("aura-repro-mc-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let reg = Registry::open(dir.join("runs.jsonl"));
|
||||
|
||||
// a CLOSED signal (both SMA knobs bound) — MC binds no axis.
|
||||
let closed = stage1_signal(Some(2), Some(4));
|
||||
let doc = blueprint_to_json(&closed).expect("serializes");
|
||||
let data = DataSource::Synthetic;
|
||||
let family = blueprint_mc_family(&doc, 3, &data);
|
||||
|
||||
// persist exactly as run_blueprint_mc does: store the blueprint, append the MC family.
|
||||
let topo = family.draws[0].report.manifest.topology_hash.clone().expect("topo");
|
||||
let canonical =
|
||||
blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap()).unwrap();
|
||||
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
|
||||
let id = reg
|
||||
.append_family("mcrepro", FamilyKind::MonteCarlo, &mc_member_reports(&family))
|
||||
.expect("append");
|
||||
|
||||
// reproduce: every MC member re-derives bit-identically (its seed-driven walk is
|
||||
// reconstructed from manifest.seed — the realization branch).
|
||||
let rep = reproduce_family_in(®, &id, &data);
|
||||
assert_eq!(rep.outcomes.len(), 3, "three MC members reproduced");
|
||||
assert!(
|
||||
rep.outcomes.iter().all(|(_, ok)| *ok),
|
||||
"every MC member re-derives bit-identically: {:?}",
|
||||
rep.outcomes
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura reproduce_family_re_derives_every_mc_member_bit_identically`
|
||||
Expected: FAIL — the assertion `every MC member re-derives bit-identically` fails: without
|
||||
the branch, reproduce re-runs MC members over `data.run_sources()` (the showcase series),
|
||||
not the seed-driven walk, so `rerun.metrics != stored.metrics` for every member.
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
In `reproduce_family_in`, replace the single member run (currently ~2561-2570):
|
||||
|
||||
```rust
|
||||
let rerun = run_blueprint_member(
|
||||
reload(),
|
||||
&point,
|
||||
&space,
|
||||
data.run_sources(),
|
||||
window,
|
||||
stored.manifest.seed,
|
||||
pip,
|
||||
&hash,
|
||||
);
|
||||
```
|
||||
|
||||
with a realization-aware version that reconstructs the member's data from the facet its
|
||||
family kind recorded:
|
||||
|
||||
```rust
|
||||
// Realization-aware: a MonteCarlo member ran over a seed-driven synthetic walk,
|
||||
// not the showcase — reconstruct it from manifest.seed so the re-run matches (C1).
|
||||
let (sources, member_window, seed) = match family.kind {
|
||||
FamilyKind::MonteCarlo => {
|
||||
let s = synthetic_walk_sources(stored.manifest.seed);
|
||||
let w = window_of(&s).expect("non-empty synthetic walk");
|
||||
(s, w, stored.manifest.seed)
|
||||
}
|
||||
_ => (data.run_sources(), window, stored.manifest.seed),
|
||||
};
|
||||
let rerun = run_blueprint_member(
|
||||
reload(),
|
||||
&point,
|
||||
&space,
|
||||
sources,
|
||||
member_window,
|
||||
seed,
|
||||
pip,
|
||||
&hash,
|
||||
);
|
||||
```
|
||||
|
||||
(`window` is the pre-loop `data.full_window()` at ~2515, used unchanged for the `_` arm;
|
||||
`family.kind` is on the grouped `Family` the loop iterates.)
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura reproduce_family_re_derives_every_mc_member_bit_identically`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Verify the Sweep reproduce path stays green**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura reproduce_family_re_derives_every_member_bit_identically`
|
||||
Expected: PASS (the `_` arm is byte-unchanged for Sweep members).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `run_blueprint_mc` + the `["mc", ..]` dispatch arm + E2E
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (`run_blueprint_mc` beside `run_blueprint_sweep` ~3210; the
|
||||
`["mc", rest @ ..]` dispatch arm ~4006-4015)
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` (beside `aura_reproduce_re_derives_a_persisted_sweep_bit_identically` ~3456)
|
||||
|
||||
- [ ] **Step 1: Write the failing E2E tests**
|
||||
|
||||
Add to `crates/aura-cli/tests/cli_run.rs`:
|
||||
|
||||
```rust
|
||||
/// E2E (acc 1+3+4): `aura mc <closed blueprint.json> --seeds N` builds a MonteCarlo
|
||||
/// family (one member per seed), writes exactly one content-addressed blueprint, and
|
||||
/// `aura reproduce <id>` re-derives every member bit-identically (exit 0).
|
||||
#[test]
|
||||
fn aura_mc_over_a_blueprint_reproduces_bit_identically() {
|
||||
let cwd = temp_cwd("mc-blueprint-reproduce");
|
||||
let fixture = format!("{}/tests/fixtures/stage1_signal.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
let mc = Command::new(BIN)
|
||||
.args(["mc", &fixture, "--seeds", "4", "--name", "mcx"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura mc blueprint");
|
||||
assert!(mc.status.success(), "mc exit: {:?} stderr={}", mc.status, String::from_utf8_lossy(&mc.stderr));
|
||||
let mc_out = String::from_utf8(mc.stdout).expect("utf-8 stdout");
|
||||
// one member line per seed (each carries "seed":n) + one aggregate line.
|
||||
assert_eq!(
|
||||
mc_out.lines().filter(|l| l.contains("\"seed\":")).count(),
|
||||
4,
|
||||
"one member line per seed: {mc_out}"
|
||||
);
|
||||
assert!(mc_out.contains("mc_aggregate"), "prints the aggregate line: {mc_out}");
|
||||
|
||||
// exactly one blueprint stored for the whole family (one topology per family).
|
||||
let store = cwd.join("runs/blueprints");
|
||||
let entries: Vec<_> = std::fs::read_dir(&store)
|
||||
.unwrap_or_else(|e| panic!("mc must create the blueprint store {store:?}: {e}"))
|
||||
.map(|e| e.expect("dir entry").path())
|
||||
.collect();
|
||||
assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}");
|
||||
|
||||
let repro = Command::new(BIN)
|
||||
.args(["reproduce", "mcx-0"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura reproduce");
|
||||
assert!(repro.status.success(), "reproduce exit: {:?} stderr={}", repro.status, String::from_utf8_lossy(&repro.stderr));
|
||||
let repro_out = String::from_utf8(repro.stdout).expect("utf-8 stdout");
|
||||
assert!(
|
||||
repro_out.contains("reproduced 4/4 members bit-identically"),
|
||||
"every MC member re-derives: {repro_out}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (acc 2): `aura mc <open blueprint.json>` is rejected with a named error + exit 2
|
||||
/// (NOT a panic / exit 101) — MC binds no axis, so a free knob has no binder.
|
||||
#[test]
|
||||
fn aura_mc_rejects_an_open_blueprint() {
|
||||
let cwd = temp_cwd("mc-blueprint-open-reject");
|
||||
let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = Command::new(BIN)
|
||||
.args(["mc", &fixture, "--seeds", "4"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura mc open-blueprint");
|
||||
assert_eq!(out.status.code(), Some(2), "an open blueprint fails clean (exit 2, not a panic): stderr={}", String::from_utf8_lossy(&out.stderr));
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(stderr.contains("closed blueprint"), "names the closed-blueprint requirement: {stderr}");
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run aura_mc_over_a_blueprint_reproduces_bit_identically`
|
||||
Expected: FAIL — `aura mc <fixture>.json` currently falls into `parse_mc_args`, which does
|
||||
not accept a `.json` first positional, so the command errors (no `.json` discriminator yet).
|
||||
|
||||
- [ ] **Step 3: Write `run_blueprint_mc`**
|
||||
|
||||
Add beside `run_blueprint_sweep` (~3210):
|
||||
|
||||
```rust
|
||||
/// `aura mc <blueprint.json> --seeds N`: build a Monte-Carlo family from a loaded CLOSED
|
||||
/// blueprint (the World/C21 verb), store the canonical blueprint ONCE keyed by the shared
|
||||
/// `topology_hash` (the 0094 hook, so `aura reproduce` re-derives it), record it as a
|
||||
/// `FamilyKind::MonteCarlo` family (C18/C21 lineage), and print each draw's member line
|
||||
/// (carrying the seed) plus the aggregate — mirroring `run_mc` / `run_blueprint_sweep`.
|
||||
fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource) {
|
||||
let family = blueprint_mc_family(doc, n_seeds, &data);
|
||||
let reg = default_registry();
|
||||
// Store the canonical blueprint ONCE, keyed by the family's shared topology_hash.
|
||||
let topo = family.draws[0]
|
||||
.report
|
||||
.manifest
|
||||
.topology_hash
|
||||
.clone()
|
||||
.expect("a blueprint mc stamps every member's topology_hash");
|
||||
let canonical = blueprint_to_json(
|
||||
&blueprint_from_json(doc, &|t| std_vocabulary(t))
|
||||
.expect("doc parse-validated at the dispatch boundary"),
|
||||
)
|
||||
.expect("a loaded blueprint re-serializes");
|
||||
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
let id = match reg.append_family(name, FamilyKind::MonteCarlo, &mc_member_reports(&family)) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
for draw in &family.draws {
|
||||
println!("{}", mc_member_line(&id, draw.seed, &draw.report));
|
||||
}
|
||||
println!("{}", mc_aggregate_json(&family.aggregate));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Grow the `["mc", ..]` dispatch arm**
|
||||
|
||||
Replace the current arm (~4006-4015):
|
||||
|
||||
```rust
|
||||
["mc", rest @ ..] => match parse_mc_args(rest) {
|
||||
Ok(McArgs::Synthetic { name, persist }) => run_mc(&name, persist),
|
||||
Ok(McArgs::RealR { choice, grid, block_len, n_resamples, seed }) => {
|
||||
run_mc_r_bootstrap(DataSource::from_choice(choice), &grid, block_len, n_resamples, seed)
|
||||
}
|
||||
Err(msg) => {
|
||||
eprintln!("aura: {msg}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
with the `.json`-discriminator arm (mirroring the `["sweep", ..]` arm ~3952):
|
||||
|
||||
```rust
|
||||
["mc", rest @ ..] => {
|
||||
// A `.json`-file first argument selects the loaded-blueprint Monte-Carlo (the
|
||||
// World/C21 verb), mirroring the `["sweep", ..]` discriminator; a bare
|
||||
// `--strategy`/`--name` token never ends in `.json`, so the paths stay disjoint.
|
||||
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);
|
||||
});
|
||||
if let Err(e) = blueprint_from_json(&doc, &|t| std_vocabulary(t)) {
|
||||
eprintln!("aura: {path}: {e:?}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let McBlueprintArgs { n_seeds, name } =
|
||||
parse_mc_blueprint_args(&rest[1..]).unwrap_or_else(|msg| {
|
||||
eprintln!("aura: {msg}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
run_blueprint_mc(&doc, n_seeds, &name, DataSource::Synthetic);
|
||||
return;
|
||||
}
|
||||
match parse_mc_args(rest) {
|
||||
Ok(McArgs::Synthetic { name, persist }) => run_mc(&name, persist),
|
||||
Ok(McArgs::RealR { choice, grid, block_len, n_resamples, seed }) => {
|
||||
run_mc_r_bootstrap(DataSource::from_choice(choice), &grid, block_len, n_resamples, seed)
|
||||
}
|
||||
Err(msg) => {
|
||||
eprintln!("aura: {msg}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the E2E tests to verify they pass**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run aura_mc_over_a_blueprint_reproduces_bit_identically`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run aura_mc_rejects_an_open_blueprint`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Verify the hard-wired `aura mc` path is undisturbed**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run mc`
|
||||
Expected: PASS (the hard-wired-`mc` E2Es still green — the `.json` discriminator keeps the
|
||||
non-`.json` path falling through to `parse_mc_args`).
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Full-workspace gate
|
||||
|
||||
**Files:** none (verification only)
|
||||
|
||||
- [ ] **Step 1: Build the workspace**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: builds clean, 0 errors.
|
||||
|
||||
- [ ] **Step 2: Run the full test suite**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: all suites pass (the 3 new units + 2 new E2Es green; no regressions).
|
||||
|
||||
- [ ] **Step 3: Lint**
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: 0 warnings.
|
||||
@@ -1,305 +0,0 @@
|
||||
# Monte-Carlo over a Loaded Blueprint — Design Spec
|
||||
|
||||
**Date:** 2026-07-01
|
||||
**Status:** Draft — awaiting user spec review
|
||||
**Authors:** orchestrator + Claude
|
||||
|
||||
**Reference issue:** #170 (World/C21 milestone). This cycle ships the **Monte-Carlo
|
||||
half** of #170; the walk-forward half is deferred (see Scope below). Design
|
||||
decisions, the grounding correction, and the two-gate narrowing are recorded on
|
||||
#170. Follow-on tracks: #172 (real-data block-bootstrap MC, deps #124) and #173
|
||||
(walk-forward over a loaded blueprint, deps #169).
|
||||
|
||||
## Goal
|
||||
|
||||
Add a `.json`-discriminator dispatch arm — `aura mc <blueprint.json> --seeds N` —
|
||||
that builds a **Monte-Carlo family** from a loaded (serialized) **closed** blueprint,
|
||||
mirroring cycle 0093's `aura sweep <blueprint.json>`. It runs the fixed blueprint
|
||||
across N seeds, each seed drawing a distinct synthetic price walk (`SyntheticSpec::
|
||||
source(seed)`, the `mc_family` pattern), persists the family under
|
||||
`FamilyKind::MonteCarlo` with the members' shared `topology_hash`, and writes the
|
||||
canonical blueprint once to the content-addressed store — so a persisted
|
||||
`aura mc` family reproduces bit-identically via `aura reproduce` (the concrete
|
||||
proof the store hook fired).
|
||||
|
||||
## Scope — MC only this cycle; walk-forward deferred
|
||||
|
||||
#170 asked for both `mc` and `walkforward` over a loaded blueprint. Two independent
|
||||
review gates on the draft narrowed this cycle to **MC only**:
|
||||
|
||||
- **Verb-name collision (walk-forward).** The hard-wired `aura walkforward` does a
|
||||
true per-window **in-sample re-optimization** (`sweep_over(is) → select_winner →
|
||||
run_oos`, main.rs:2010-2016). A loaded blueprint declares no sweepable axes yet
|
||||
(that is #169), so a loaded-blueprint `walkforward` could only be a *fixed-blueprint
|
||||
windowed out-of-sample evaluation* — no IS refit. Shipping that under the identical
|
||||
verb name (split only by a `.json` suffix) would let one trading term name two
|
||||
categorically different guarantees. The honest walk-forward over a loaded blueprint
|
||||
is the IS-refit form, which needs axis-discovery (#169) first; it is tracked by
|
||||
#173, not forced into this cycle under a colliding name.
|
||||
- The MC half carries **no** such collision: `aura mc` over a fixed closed blueprint,
|
||||
varying the data realization by seed, is a Monte-Carlo in the plain sense.
|
||||
|
||||
## Architecture
|
||||
|
||||
### The grounding correction (foundation)
|
||||
|
||||
`run_blueprint_member(signal, point, space, sources, window, seed, pip, topo)`
|
||||
(main.rs:3109) runs the member over the **`sources` argument as given** and uses
|
||||
`seed` / `window` only to stamp the manifest (`sim_optimal_manifest`). The
|
||||
stage-1-R graph is deterministic (C1). Therefore Monte-Carlo variation cannot come
|
||||
from the `seed` label alone — it must come from **re-drawing the input**, exactly
|
||||
as the hard-wired `mc_family` does (`SyntheticSpec::source(seed)` → a distinct price
|
||||
walk per seed, main.rs:2212). `aura mc` over a loaded blueprint therefore supplies
|
||||
**seed-derived synthetic sources** to `run_blueprint_member` per member:
|
||||
|
||||
| verb | facet | per-member sources | window | seed |
|
||||
|------|-------|--------------------|--------|------|
|
||||
| sweep (0093) | param point | showcase (full), fixed | full | 0 |
|
||||
| **mc** (new) | seed | `SyntheticSpec::source(seed)` walk | that walk's window | the seed |
|
||||
|
||||
`run_blueprint_member`'s signature is **unchanged** — MC only passes a different
|
||||
`(sources, window, seed)` triple than the sweep's `(showcase, full, 0)`. The shared
|
||||
reduce-mode member path makes reproduction bit-identical by construction (C1) — the
|
||||
0094 keystone.
|
||||
|
||||
### Closed blueprint only (grounded constraint)
|
||||
|
||||
MC declares **no** parameter axis (only the seed varies), so it runs the blueprint at
|
||||
its **fixed, fully-bound** configuration — a **closed** blueprint (an empty wrapped
|
||||
`param_space`). This is exactly the case `aura run <blueprint.json>` already supports:
|
||||
it passes empty params and bootstraps only when the blueprint has zero free knobs (the
|
||||
closed `stage1_signal.json` fixture). Grounding confirmed there is no per-knob
|
||||
"declared default" mechanism — `ParamSpec { name, kind }` carries no default and
|
||||
`compile_with_params` requires `params.len() == space.len()`. So MC uses the **empty
|
||||
point** (`&[]`), never a synthesized default point.
|
||||
|
||||
An **open** blueprint (a non-empty wrapped `param_space` — free knobs) is **rejected**
|
||||
with a named error + exit 2 *before* any run: MC has no axis to bind those knobs. This
|
||||
also fixes the boundary honestly — the raw `bootstrap`/`compile_with_params` path
|
||||
*panics* on an arity mismatch (main.rs:3084), so MC guards against it up front rather
|
||||
than letting a panic escape. (To vary a knob, the user reaches for `aura sweep
|
||||
<blueprint.json> --axis`; to Monte-Carlo it, the blueprint must be closed.)
|
||||
|
||||
### The persist seam
|
||||
|
||||
`run_blueprint_mc` replicates the two things the 0093 sweep persist seam does
|
||||
(`run_blueprint_sweep`, main.rs:3210):
|
||||
|
||||
1. `reg.put_blueprint(&topo, &canonical)` — the content-addressed store write (the
|
||||
0094 hook), keyed by the family's shared `topology_hash`, canonical bytes
|
||||
`blueprint_to_json(blueprint_from_json(doc))`. **Load-bearing:** without it,
|
||||
`aura reproduce` reports the family's members as "blueprint `<hash>` missing from
|
||||
store" (exit 2). (#170 reconciliation comment.)
|
||||
2. `reg.append_family(name, FamilyKind::MonteCarlo, &member_reports)`.
|
||||
|
||||
Then print each member line (`mc_member_line`, carrying the seed) plus the aggregate
|
||||
line — matching the hard-wired `run_mc` output shape.
|
||||
|
||||
### Reproduce becomes realization-aware (the one change to 0094 code)
|
||||
|
||||
`reproduce_family_in` (main.rs:2503) currently reconstructs every member over
|
||||
`data.run_sources()` (showcase) at `data.full_window()` with the stored seed —
|
||||
correct for `Sweep`, wrong for MC (whose data is seed-derived). It gains a per-member
|
||||
branch on the **family kind** that computes the member's `(sources, window, seed)`:
|
||||
|
||||
- `MonteCarlo` → `synthetic_walk_sources(manifest.seed)`, that walk's window, the seed.
|
||||
- everything else (`Sweep`, `CrossInstrument`) → `data.run_sources()`,
|
||||
`data.full_window()`, `manifest.seed` (unchanged; keeps 0094 green).
|
||||
|
||||
The grouped `Family` struct exposes `.kind: FamilyKind` (aura-registry lineage.rs:56);
|
||||
the manifest stores `seed` recoverably (compat.rs:30); `synthetic_walk_sources` is a
|
||||
fixed constant shared by the persist and reproduce paths, so the seed→walk
|
||||
reconstruction is bit-exact (C1).
|
||||
|
||||
## Concrete code shapes
|
||||
|
||||
### User-facing program (the acceptance evidence)
|
||||
|
||||
Monte-Carlo over a loaded closed blueprint, then prove it reproduces:
|
||||
|
||||
```console
|
||||
$ aura mc crates/aura-cli/tests/fixtures/stage1_signal.json --seeds 8 --name mc1
|
||||
mc1 {"family_id":"mc1-0","seed":1,"report":{...topology_hash:"<H>"...}}
|
||||
mc1 {"family_id":"mc1-0","seed":2,"report":{...topology_hash:"<H>"...}}
|
||||
... (8 members, one per seed; same topology_hash <H>, differing metrics)
|
||||
{"mc_aggregate":{...}}
|
||||
|
||||
$ aura reproduce mc1-0
|
||||
mc1-0 member <params> reproduced: bit-identical
|
||||
... (8 members)
|
||||
reproduced 8/8 members bit-identically # exit 0
|
||||
```
|
||||
|
||||
Rejecting an open blueprint (free knobs) — a named error, not a panic:
|
||||
|
||||
```console
|
||||
$ aura mc crates/aura-cli/tests/fixtures/stage1_signal_open.json --seeds 4
|
||||
aura: mc requires a closed blueprint (no free parameters); stage1_signal_open.json
|
||||
has 2 free knobs — bind them or use `aura sweep --axis` # exit 2
|
||||
```
|
||||
|
||||
All members carry the shared `topology_hash <H>`; a single `runs/blueprints/<H>.json`
|
||||
is written per family (one blueprint per family, C11/C12). The synthetic-walk
|
||||
realization is the deliverable's *machinery*, not a trader-grade Monte-Carlo — a
|
||||
real-data block-bootstrap MC is #172.
|
||||
|
||||
### Dispatch arm (before → after)
|
||||
|
||||
Before (main.rs:4006) — the hard-wired verb only:
|
||||
|
||||
```rust
|
||||
["mc", rest @ ..] => match parse_mc_args(rest) { /* Synthetic | RealR */ },
|
||||
```
|
||||
|
||||
After — a `.json`-file first positional selects the loaded-blueprint path (mirror of
|
||||
the 0093 `["sweep", rest @ ..]` arm at 3952), else fall through unchanged:
|
||||
|
||||
```rust
|
||||
["mc", rest @ ..] => {
|
||||
if let Some(path) = rest.first()
|
||||
.filter(|a| a.ends_with(".json") && std::path::Path::new(a).is_file())
|
||||
{
|
||||
let doc = read_to_string(path)/* exit 2 on io err */;
|
||||
blueprint_from_json(&doc, &|t| std_vocabulary(t))/* exit 2 on parse err, file-path ctx */;
|
||||
let McBlueprintArgs { n_seeds, name, data } =
|
||||
parse_mc_blueprint_args(&rest[1..])/* exit 2 on usage */;
|
||||
run_blueprint_mc(&doc, n_seeds, &name, DataSource::from_choice(data));
|
||||
return;
|
||||
}
|
||||
match parse_mc_args(rest) { /* unchanged */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Family builder + closed-blueprint guard
|
||||
|
||||
```rust
|
||||
fn blueprint_mc_family(doc: &str, n_seeds: u64, data: &DataSource) -> SweepFamily {
|
||||
let reload = |d: &str| blueprint_from_json(d, &|t| std_vocabulary(t))
|
||||
.expect("doc parse-validated at the dispatch boundary");
|
||||
let topo = topology_hash(&reload(doc));
|
||||
let pip = data.pip_size();
|
||||
let space = /* wrapped param_space probe, as blueprint_sweep_family does (main.rs:3177) */;
|
||||
// closed-blueprint guard: MC binds no axis, so a free knob has no binder.
|
||||
if !space.is_empty() {
|
||||
eprintln!("aura: mc requires a closed blueprint (no free parameters); \
|
||||
{n} free knob(s) — bind them or use `aura sweep --axis`", n = space.len());
|
||||
std::process::exit(2);
|
||||
}
|
||||
let point: Vec<Cell> = Vec::new(); // closed → empty point (as `aura run`)
|
||||
let points = (1..=n_seeds).map(|seed| {
|
||||
let sources = synthetic_walk_sources(seed); // fixed SyntheticSpec, seeded
|
||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||
run_blueprint_member(reload(doc), &point, &space, sources, window, seed, pip, &topo)
|
||||
}).collect();
|
||||
SweepFamily { points /* SweepPoint { report } */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Reproduce realization branch (before → after)
|
||||
|
||||
Before (main.rs:2514-2570) — one path for all members:
|
||||
|
||||
```rust
|
||||
let window = data.full_window();
|
||||
for member in &family.members {
|
||||
// ... run_blueprint_member(reload(), &point, &space, data.run_sources(),
|
||||
// window, stored.manifest.seed, pip, &hash)
|
||||
}
|
||||
```
|
||||
|
||||
After — the `(sources, window, seed)` triple is chosen by the family kind:
|
||||
|
||||
```rust
|
||||
for member in &family.members {
|
||||
let (sources, window, seed) = match family.kind {
|
||||
FamilyKind::MonteCarlo => {
|
||||
let s = synthetic_walk_sources(stored.manifest.seed);
|
||||
let w = window_of(&s).expect("non-empty walk");
|
||||
(s, w, stored.manifest.seed)
|
||||
}
|
||||
_ /* Sweep, CrossInstrument, (WalkForward → #173) */ =>
|
||||
(data.run_sources(), data.full_window(), stored.manifest.seed),
|
||||
};
|
||||
let rerun = run_blueprint_member(reload(), &point, &space, sources, window, seed, pip, &hash);
|
||||
outcomes.push((label, rerun.metrics == stored.metrics));
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
All in aura-cli `main.rs` (invariant 8: the frozen engine is untouched; invariant 9:
|
||||
the registry stays a dumb bytes/records store):
|
||||
|
||||
- `parse_mc_blueprint_args(&[&str]) -> Result<McBlueprintArgs, String>` — pure arg
|
||||
parser (mirror `parse_sweep_blueprint_args`): `--seeds <n>` (required, ≥1),
|
||||
`--name <n>` (default `"mc"`); synthetic-only (real data is #172).
|
||||
- `blueprint_mc_family` — the builder above, with the closed-blueprint guard.
|
||||
- `run_blueprint_mc(doc, n_seeds, name, data)` — the persist seam (put_blueprint +
|
||||
append_family(MonteCarlo) + print member/aggregate lines), mirroring
|
||||
`run_blueprint_sweep` / `run_mc`.
|
||||
- `synthetic_walk_sources(seed) -> Vec<Box<dyn Source>>` — the fixed seeded
|
||||
`SyntheticSpec` walk (sized to warm the loaded graph — showcase-length regime),
|
||||
shared by the persist and reproduce paths.
|
||||
- `reproduce_family_in` — gains the `MonteCarlo` realization branch.
|
||||
- one new dispatch arm.
|
||||
|
||||
## Data flow
|
||||
|
||||
`aura mc <bp> --seeds N`: read + parse-validate doc → `blueprint_mc_family`
|
||||
(closed-guard; N members, each `run_blueprint_member` over its seed's synthetic walk)
|
||||
→ `put_blueprint(topo, canonical)` → `append_family(name, MonteCarlo, reports)` →
|
||||
print member lines + aggregate.
|
||||
|
||||
`aura reproduce <id>` (MC family): load families → find `id` → for each member,
|
||||
reconstruct `synthetic_walk_sources(manifest.seed)` + its window → `run_blueprint_member`
|
||||
→ compare `metrics` → print bit-identical/DIVERGED → exit 0 (all identical) / 1 (any
|
||||
diverged).
|
||||
|
||||
## Error handling
|
||||
|
||||
Mirrors the 0093/0094 boundary discipline (stderr `aura: <msg>` + exit 2, never a
|
||||
panic):
|
||||
|
||||
- io error reading the `.json` file → `aura: <path>: <e>`, exit 2.
|
||||
- blueprint parse error → `aura: <path>: <e>`, exit 2 (validated once at the boundary,
|
||||
so per-member reloads are infallible).
|
||||
- **open blueprint (non-empty wrapped `param_space`)** → the closed-blueprint guard's
|
||||
named error, exit 2 (before any run — pre-empts the arity panic at main.rs:3084).
|
||||
- arg usage error (missing/`0` `--seeds`, unknown flag) → the parser's usage string, exit 2.
|
||||
- `aura reproduce` on an MC family: unknown id / member without `topology_hash` /
|
||||
blueprint missing from store → exit 2 (unchanged 0094 behaviour); any member
|
||||
diverges → named `DIVERGED` + exit 1.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
- **Unit (`cargo test -p aura-cli --bin aura`)**:
|
||||
- `blueprint_mc_family` over the **closed** fixture with `--seeds 3`: 3 members, all
|
||||
sharing one `topology_hash`, with **differing** metrics — proves the seed reaches
|
||||
the data (the anti-degenerate guard; a regression to seed-as-label-only would make
|
||||
the members identical and fail this test).
|
||||
- `parse_mc_blueprint_args`: `--seeds` required/parsed/`≥1`, default name,
|
||||
unknown-flag rejection.
|
||||
- **E2E (`cargo test -p aura-cli --test cli_run`)** driving the built binary:
|
||||
- `aura mc <closed fixture> --seeds 4 --name mcX` then `aura reproduce mcX-0` →
|
||||
asserts `reproduced 4/4 members bit-identically` + exit 0, and exactly one
|
||||
`runs/blueprints/<hash>.json`. **This is acceptance criterion (d)** — the
|
||||
store-hook proof.
|
||||
- `aura mc <open fixture> --seeds 4` → asserts the closed-blueprint named error on
|
||||
stderr + exit 2 (NOT a panic / exit 101) — the boundary guard.
|
||||
- No new registry code (the `append_family` MonteCarlo round-trip is already green).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. `aura mc <closed blueprint.json> --seeds N` builds an N-member
|
||||
`FamilyKind::MonteCarlo` family from the loaded blueprint, members sharing one
|
||||
`topology_hash` and **differing** in metrics (seed → synthetic data), and writes
|
||||
one stored blueprint.
|
||||
2. An **open** blueprint is rejected with a named `aura:` error + exit 2 (never a
|
||||
panic); the hard-wired `aura mc` path stays unchanged (the `.json` discriminator
|
||||
keeps them disjoint).
|
||||
3. **Reproduce compatibility (the store-hook proof):** a persisted `aura mc` family
|
||||
`aura reproduce`s **bit-identically** (exit 0); the 0094 `Sweep` reproduce path
|
||||
stays green.
|
||||
4. Engine untouched (invariant 8); the registry gains no domain logic (invariant 9);
|
||||
`cargo clippy --workspace --all-targets -- -D warnings` clean; determinism (C1)
|
||||
holds — reproduction is bit-exact via the shared `run_blueprint_member` path.
|
||||
Reference in New Issue
Block a user