Two tasks: (1) aura-engine SplitMix64 PRNG + pub SyntheticSpec::source(seed) -> impl Source beside VecSource, with producer RED tests; (2) aura-cli sim_optimal_manifest gains a seed param (four callers pass 0), plus a test-only run_sample_seeded + SeededTrace and three e2e RED tests pinning the #66 acceptance. RED-first throughout. refs #66
14 KiB
Seed-as-input — Implementation Plan
Parent spec:
docs/specs/0042-seed-as-input.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill to run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Make RunManifest.seed a live captured input — ship a seeded source
(Fn(u64) -> impl Source) whose stream is fully seed-determined, and thread the
seed into the manifest at the construction site (Fork B).
Architecture: Two crates. (1) aura-engine gains a private SplitMix64
PRNG and a pub SyntheticSpec with fn source(&self, seed: u64) -> impl Source,
placed beside the existing Source producer VecSource in harness.rs (same
category, same module as the Source trait it implements). (2) aura-cli's
sim_optimal_manifest gains a seed: u64 parameter; its four existing callers
pass 0 (unchanged behaviour), and a test-only run_sample_seeded exercises a
live seed and returns the drained sink trace for a trace-level determinism
assertion. The engine event loop is untouched — it sees only a Source (C3).
Tech Stack: Rust, aura-engine (harness.rs, lib.rs), aura-cli
(main.rs), the existing Source/VecSource seam, sample_harness +
f64_field/summarize fold.
Files this plan creates or modifies:
- Modify:
crates/aura-engine/src/harness.rs— addSplitMix64(private) +SyntheticSpec(pub) +impl SyntheticSpec::sourceafter theimpl Source for VecSourceblock (insert after line 91, beforeFlatGraphat line 98); add two producer RED tests in the#[cfg(test)] mod testsblock. - Modify:
crates/aura-engine/src/lib.rs:46— addSyntheticSpecto thepub use harness::{…}re-export list. - Modify:
crates/aura-cli/src/main.rs—sim_optimal_manifestdef (125-133) gainsseed: u64; thread0into its four callers (154, 216, 385, 564); addSyntheticSpecto theuse aura_engine::{…}import (17-20); addSeededTracerun_sample_seeded+ three e2e RED tests in the#[cfg(test)] mod testsblock (opens at 608).
Task 1: Seeded source producer (aura-engine)
Files:
-
Modify:
crates/aura-engine/src/harness.rs(insert after line 91; tests in themod testsblock) -
Modify:
crates/aura-engine/src/lib.rs:46 -
Step 1: Write the failing producer tests
In crates/aura-engine/src/harness.rs, inside the existing #[cfg(test)] mod tests
block (after the f64_stream helper, ~line 396), add:
#[test]
fn seeded_source_same_seed_same_stream() {
// Same seed -> bit-identical stream (C1/C12 determinism).
let spec = SyntheticSpec { start: 1.0, len: 32, step: 1 };
let drain = |seed| {
let mut s = spec.source(seed);
let mut out: Vec<(Timestamp, Scalar)> = Vec::new();
while let Some(item) = Source::next(&mut s) {
out.push(item);
}
out
};
assert_eq!(drain(7), drain(7));
}
#[test]
fn seeded_source_different_seed_differs() {
// Different seeds -> different streams.
let spec = SyntheticSpec { start: 1.0, len: 32, step: 1 };
let drain = |seed| {
let mut s = spec.source(seed);
let mut out: Vec<(Timestamp, Scalar)> = Vec::new();
while let Some(item) = Source::next(&mut s) {
out.push(item);
}
out
};
assert_ne!(drain(1), drain(2));
}
- Step 2: Run the tests to verify they fail
Run: cargo test -p aura-engine seeded_source
Expected: FAIL — does not compile: cannot find struct, variant or union type SyntheticSpec in this scope (the producer does not exist yet).
- Step 3: Write the PRNG + seeded source
In crates/aura-engine/src/harness.rs, immediately after the
impl Source for VecSource { … } block (which ends at line 91) and before the
FlatGraph doc comment (line 93), insert:
/// A tiny, fully-deterministic, dependency-free PRNG (SplitMix64). The seed
/// completely determines the sequence; no external entropy, no global state.
/// Bit-stable across toolchains and crate versions — the property C1 needs for
/// seed-as-input reproducibility (C12).
struct SplitMix64 {
state: u64,
}
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
/// A uniform `f64` in `[0, 1)` from the top 53 bits.
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
}
}
/// Config for a seeded synthetic price walk — everything *except* the seed.
/// Partially applying it (`|seed| spec.source(seed)`) yields the
/// `Fn(u64) -> impl Source` contract the Monte-Carlo family (#68) consumes
/// (C12 seed-as-input). The produced stream is an ordinary ingestion-boundary
/// `Source` (C3), generated at the data-generation edge outside the engine graph.
#[derive(Clone, Copy, Debug)]
pub struct SyntheticSpec {
/// Opening price.
pub start: f64,
/// Number of M1 steps.
pub len: usize,
/// Timestamp increment per step.
pub step: i64,
}
impl SyntheticSpec {
/// Produce a synthetic price stream **fully determined by `seed`** (C1/C12):
/// a multiplicative random walk, each step a small seeded perturbation.
/// Returns a `Source` (the C3 ingestion producer), NOT a `Vec` — so a
/// Monte-Carlo family can re-seed without materializing N streams (#68).
/// (First cut collects to a `VecSource` internally; the signature does not
/// name `Vec`, so a later lazy generator is a drop-in replacement.)
pub fn source(&self, seed: u64) -> impl Source {
let mut rng = SplitMix64::new(seed);
let mut price = self.start;
let stream: Vec<(Timestamp, Scalar)> = (0..self.len)
.map(|i| {
let shock = (rng.next_f64() - 0.5) * 0.004; // ±0.2% per step
price *= 1.0 + shock;
(Timestamp(i as i64 * self.step + 1), Scalar::F64(price))
})
.collect();
VecSource::new(stream)
}
}
Then in crates/aura-engine/src/lib.rs:46, add SyntheticSpec to the harness
re-export (alphabetically, after SourceSpec, before Target). Change:
pub use harness::{BootstrapError, Edge, FlatGraph, Harness, Source, SourceSpec, Target, VecSource};
to:
pub use harness::{
BootstrapError, Edge, FlatGraph, Harness, Source, SourceSpec, SyntheticSpec, Target, VecSource,
};
- Step 4: Run the tests to verify they pass
Run: cargo test -p aura-engine seeded_source
Expected: PASS — test result: ok. 2 passed.
Task 2: Thread the seed into the manifest + seeded run path (aura-cli)
Files:
-
Modify:
crates/aura-cli/src/main.rs(def 125-133; callers 154, 216, 385, 564; imports 17-20; tests block at 608) -
Step 1: Write the failing end-to-end tests + the seeded run vehicle
In crates/aura-cli/src/main.rs, inside the #[cfg(test)] mod tests block
(after use super::*; at ~line 609), add SeededTrace, run_sample_seeded, and
the three tests. run_sample_seeded lives here under #[cfg(test)] deliberately:
this cycle ships no production caller for it (no --seed flag — spec Non-goals),
so a non-test definition would be dead_code and fail clippy -D warnings. It is
this cycle's test vehicle; #68 writes its own engine-side consumer.
/// The drained sink trace of a seeded run — the recorded rows of the equity
/// and exposure sinks. Compared row-for-row so the C1 seed-determinism
/// property is tested at the trace level (strictly stronger than the folded
/// 3-field metrics). `PartialEq` not `Eq`: `Scalar` carries `f64`.
#[derive(Debug, PartialEq)]
struct SeededTrace {
equity: Vec<(Timestamp, Vec<Scalar>)>,
exposure: Vec<(Timestamp, Vec<Scalar>)>,
}
/// A seeded run of the sample harness: the synthetic stream is generated
/// from `seed`, that same seed is recorded into the manifest, and the
/// drained sink trace is returned alongside the report. Every byte of both
/// is a function of `seed`.
fn run_sample_seeded(seed: u64) -> (RunReport, SeededTrace) {
let (mut h, rx_eq, rx_ex) = sample_harness();
let spec = SyntheticSpec { start: 1.0, len: 64, step: 1 };
let window = (Timestamp(1), Timestamp((spec.len as i64 - 1) * spec.step + 1));
h.run(vec![Box::new(spec.source(seed))]);
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 metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0));
let report = RunReport {
manifest: sim_optimal_manifest(
vec![
("sma_fast".to_string(), 2.0),
("sma_slow".to_string(), 4.0),
("exposure_scale".to_string(), 0.5),
],
window,
seed,
),
metrics,
};
(report, SeededTrace { equity: eq_rows, exposure: ex_rows })
}
#[test]
fn same_seed_bit_identical_trace() {
// Bit-identical sink trace for a fixed seed (acceptance bullet 1, C1).
let (_, trace_a) = run_sample_seeded(42);
let (_, trace_b) = run_sample_seeded(42);
assert_eq!(trace_a, trace_b);
}
#[test]
fn different_seed_different_trace() {
// Different seeds perturb the trace (acceptance bullet 2).
let (a, _) = run_sample_seeded(1);
let (b, _) = run_sample_seeded(2);
assert_ne!(a.metrics, b.metrics);
}
#[test]
fn seed_recorded_in_manifest() {
// The seed that drove the run is recorded (acceptance bullet 3).
let (report, _) = run_sample_seeded(7);
assert_eq!(report.manifest.seed, 7);
}
- Step 2: Run the tests to verify they fail
Run: cargo test -p aura-cli seed
Expected: FAIL — does not compile: cannot find … SyntheticSpec (not yet
imported) and this function takes 2 arguments but 3 were supplied
(sim_optimal_manifest still has the old signature).
- Step 3: Add the seed parameter, thread it, and import
SyntheticSpec
3a. In crates/aura-cli/src/main.rs:17-20, add SyntheticSpec to the
aura_engine import (alphabetically). Change:
use aura_engine::{
f64_field, summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness,
RunManifest, RunReport, SourceSpec, SweepFamily, Target, VecSource,
};
to:
use aura_engine::{
f64_field, summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness,
RunManifest, RunReport, SourceSpec, SweepFamily, SyntheticSpec, Target, VecSource,
};
3b. Change the sim_optimal_manifest definition (lines 125-133). Replace:
fn sim_optimal_manifest(params: Vec<(String, f64)>, window: (Timestamp, Timestamp)) -> RunManifest {
RunManifest {
commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(),
params,
window,
seed: 0,
broker: "sim-optimal(pip_size=0.0001)".to_string(),
}
}
with (add the seed: u64 parameter; the body field becomes seed):
fn sim_optimal_manifest(
params: Vec<(String, f64)>,
window: (Timestamp, Timestamp),
seed: u64,
) -> RunManifest {
RunManifest {
commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(),
params,
window,
seed,
broker: "sim-optimal(pip_size=0.0001)".to_string(),
}
}
3c. Thread 0 into all four existing callers (every site must be updated in this
step — the signature change is a hard compile error until all four are threaded).
Caller in run_sample (lines 154-161) — replace the window, line that closes
the argument list with window, followed by 0,:
manifest: sim_optimal_manifest(
vec![
("sma_fast".to_string(), 2.0),
("sma_slow".to_string(), 4.0),
("exposure_scale".to_string(), 0.5),
],
window,
0,
),
Caller in run_sample_real (lines 216-223):
manifest: sim_optimal_manifest(
vec![
("sma_fast".to_string(), 2.0),
("sma_slow".to_string(), 4.0),
("exposure_scale".to_string(), 0.5),
],
window,
0,
),
Caller in the sweep grid-report closure (line 385) — replace:
manifest: sim_optimal_manifest(params, window),
with:
manifest: sim_optimal_manifest(params, window, 0),
Caller in run_macd (lines 564-572):
manifest: sim_optimal_manifest(
vec![
("ema_fast".to_string(), 2.0),
("ema_slow".to_string(), 4.0),
("ema_signal".to_string(), 3.0),
("exposure_scale".to_string(), 0.5),
],
window,
0,
),
- Step 4: Run the tests to verify they pass
Run: cargo test -p aura-cli seed
Expected: PASS — test result: ok. 3 passed (the filter seed matches exactly
same_seed_bit_identical_trace, different_seed_different_trace,
seed_recorded_in_manifest; no pre-existing aura-cli test name contains seed).
- Step 5: Full-workspace regression + lint gate
Run: cargo test --workspace
Expected: PASS — the whole suite is green, including the four seed-free
determinism tests that pin the unchanged callers
(run_sample_is_deterministic_and_non_trivial, sweep_report_is_deterministic,
run_macd_compiles_from_nested_composite_and_is_deterministic,
run_sample_real_streams_real_close_bars_deterministically) and the
report.rs serde tests that pin "seed":0.
Run: cargo clippy --workspace --all-targets -- -D warnings
Expected: 0 warnings (no dead_code: run_sample_seeded/SeededTrace are under
#[cfg(test)]; the seed parameter is used by all four production callers).