plan: 0046 sweep named-binding
Three tasks for spec 0046. T1: zip_params (aura-core), RED-first. T2: GridSpace retains its ParamSpec list + SweepFamily carries it + named_params (aura-engine), RED-first, with the aura-registry optimize-test SweepFamily literal threaded in the same task. T3: sim_optimal_manifest typed-Scalar migration + all eight call sites (aura-cli), behaviour-preserving. Each signature/field change threads every one of its sites within its own task, before that task's workspace compile gate. refs #57
This commit is contained in:
@@ -0,0 +1,534 @@
|
||||
# Sweep named-binding — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0046-sweep-named-binding.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Replace the three open-coded `param_space()`-name re-zips in the CLI
|
||||
with one derived projection (`zip_params`) and make a returned `SweepFamily`
|
||||
self-describing (`named_params`), without touching the run-closure signature.
|
||||
|
||||
**Architecture:** A free function `zip_params(space, point) -> Vec<(String,
|
||||
Scalar)>` in aura-core pairs names with a positional point. `GridSpace` retains
|
||||
the `ParamSpec` list it already receives in `new()`; `SweepFamily` carries it
|
||||
(stamped once in `sweep_with_threads`) and exposes `named_params(i)`. The CLI's
|
||||
`sim_optimal_manifest` takes typed `Scalar` pairs and collapses to f64
|
||||
internally; all eight of its call sites migrate. Behaviour-preserving:
|
||||
`SweepPoint`, enumeration, and the closure bound are untouched.
|
||||
|
||||
**Tech Stack:** aura-core (`node.rs`, `lib.rs`), aura-engine (`sweep.rs`),
|
||||
aura-registry (`lib.rs` test), aura-cli (`main.rs`).
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-core/src/node.rs` — add free fn `zip_params` + a unit-test module
|
||||
- Modify: `crates/aura-core/src/lib.rs:42` — add `zip_params` to the `node` re-export
|
||||
- Modify: `crates/aura-engine/src/sweep.rs` — `GridSpace.space` + `param_specs()`; `SweepFamily.space` + `named_params()`; stamp in `sweep_with_threads`; import `zip_params`; 2 tests
|
||||
- Modify: `crates/aura-registry/src/lib.rs:278` — thread `space: vec![]` into the `optimize` test's `SweepFamily` literal
|
||||
- Modify: `crates/aura-cli/src/main.rs` — `sim_optimal_manifest` input type + 8 call sites; import `zip_params`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `zip_params` (aura-core)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-core/src/node.rs` (ParamSpec at :67-71; module imports at :13)
|
||||
- Modify: `crates/aura-core/src/lib.rs:42`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Append to `crates/aura-core/src/node.rs` (end of file):
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod zip_params_tests {
|
||||
use super::{zip_params, ParamSpec};
|
||||
use crate::{Scalar, ScalarKind};
|
||||
|
||||
#[test]
|
||||
fn zip_params_pairs_names_with_values_in_slot_order() {
|
||||
let space = vec![
|
||||
ParamSpec { name: "fast".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "scale".into(), kind: ScalarKind::F64 },
|
||||
];
|
||||
let point = vec![Scalar::I64(2), Scalar::F64(0.5)];
|
||||
assert_eq!(
|
||||
zip_params(&space, &point),
|
||||
vec![
|
||||
("fast".to_string(), Scalar::I64(2)),
|
||||
("scale".to_string(), Scalar::F64(0.5)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_params_matches_hand_zip() {
|
||||
let space = vec![
|
||||
ParamSpec { name: "a".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
|
||||
];
|
||||
let point = vec![Scalar::I64(7), Scalar::I64(9)];
|
||||
let hand: Vec<(String, Scalar)> =
|
||||
space.iter().zip(&point).map(|(ps, v)| (ps.name.clone(), *v)).collect();
|
||||
assert_eq!(zip_params(&space, &point), hand);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p aura-core zip_params`
|
||||
Expected: FAIL — compile error `cannot find function `zip_params` in this scope` /
|
||||
unresolved import `super::zip_params` (E0432/E0425); `zip_params` does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Add the function and re-export it**
|
||||
|
||||
`Scalar` is already in scope in `node.rs` (its `use crate::{Ctx, Scalar, ScalarKind};`
|
||||
at line 13). Add this free function to `crates/aura-core/src/node.rs` (immediately
|
||||
after the `ParamSpec` struct, ~line 71):
|
||||
|
||||
```rust
|
||||
/// Pair each param-space name with the co-indexed positional value — the
|
||||
/// inverse of positional binding (C23): names are a derived view, not identity.
|
||||
/// `space` and `point` are co-indexed in `param_space()` slot order; a length
|
||||
/// mismatch (contract violation) truncates to the shorter, never panics.
|
||||
pub fn zip_params(space: &[ParamSpec], point: &[Scalar]) -> Vec<(String, Scalar)> {
|
||||
space.iter().zip(point).map(|(ps, v)| (ps.name.clone(), *v)).collect()
|
||||
}
|
||||
```
|
||||
|
||||
Then add `zip_params` to the node re-export in `crates/aura-core/src/lib.rs:42`.
|
||||
Change:
|
||||
|
||||
```rust
|
||||
pub use node::{FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder};
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
pub use node::{
|
||||
zip_params, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `cargo test -p aura-core zip_params`
|
||||
Expected: PASS — 2 tests (`zip_params_pairs_names_with_values_in_slot_order`,
|
||||
`zip_params_matches_hand_zip`).
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `GridSpace` retains specs; `SweepFamily` carries them + `named_params` (aura-engine), with the aura-registry literal threaded
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-engine/src/sweep.rs` (imports :7; `GridSpace` :14-17; `GridSpace::new` :24-44; `SweepFamily` :106-109; `sweep_with_threads` :169-182; test module :184+)
|
||||
- Modify: `crates/aura-registry/src/lib.rs:278` (the `optimize` test's `SweepFamily` literal)
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Add to the `#[cfg(test)] mod tests` in `crates/aura-engine/src/sweep.rs` (after
|
||||
`sweep_equals_n_independent_runs`, ~line 326). These reuse the existing
|
||||
`composite_sma_cross_harness`, `sma_cross_grid`, and `run_point` helpers:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn sweep_family_carries_param_space() {
|
||||
let space = composite_sma_cross_harness().0.param_space();
|
||||
let family = sweep(&sma_cross_grid(), run_point);
|
||||
assert_eq!(family.space, space);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_named_params_round_trips() {
|
||||
let space = composite_sma_cross_harness().0.param_space();
|
||||
let family = sweep(&sma_cross_grid(), run_point);
|
||||
// odometer-first point is [I64(2), I64(4), F64(0.5)]
|
||||
let expected: Vec<(String, Scalar)> = space
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(family.points[0].params.clone())
|
||||
.map(|(ps, v)| (ps.name, v))
|
||||
.collect();
|
||||
assert_eq!(family.named_params(0), expected);
|
||||
assert_eq!(family.named_params(0)[0].1, Scalar::I64(2));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test -p aura-engine sweep_family_carries_param_space`
|
||||
Expected: FAIL — compile error `no field `space` on type `&SweepFamily`` (E0609);
|
||||
the field does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Retain the specs on `GridSpace`**
|
||||
|
||||
Import `zip_params` in `crates/aura-engine/src/sweep.rs:7`. Change:
|
||||
|
||||
```rust
|
||||
use aura_core::{ParamSpec, Scalar, ScalarKind};
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
use aura_core::{zip_params, ParamSpec, Scalar, ScalarKind};
|
||||
```
|
||||
|
||||
Change the `GridSpace` struct (`sweep.rs:14-17`):
|
||||
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct GridSpace {
|
||||
axes: Vec<Vec<Scalar>>,
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub struct GridSpace {
|
||||
space: Vec<ParamSpec>,
|
||||
axes: Vec<Vec<Scalar>>,
|
||||
}
|
||||
```
|
||||
|
||||
In `GridSpace::new`, change the final constructor (`sweep.rs:43`) from
|
||||
`Ok(Self { axes })` to:
|
||||
|
||||
```rust
|
||||
Ok(Self { space: space.to_vec(), axes })
|
||||
```
|
||||
|
||||
Add a `param_specs` accessor inside `impl GridSpace` (next to `points`):
|
||||
|
||||
```rust
|
||||
/// The param-space (names + kinds) this grid was validated against, retained
|
||||
/// for the family to carry — the derived-name source (C23: names, not identity).
|
||||
pub fn param_specs(&self) -> &[ParamSpec] {
|
||||
&self.space
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Carry the space on `SweepFamily` + add `named_params`; stamp it**
|
||||
|
||||
Change the `SweepFamily` struct (`sweep.rs:106-109`):
|
||||
|
||||
```rust
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SweepFamily {
|
||||
pub points: Vec<SweepPoint>,
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SweepFamily {
|
||||
pub space: Vec<ParamSpec>,
|
||||
pub points: Vec<SweepPoint>,
|
||||
}
|
||||
|
||||
impl SweepFamily {
|
||||
/// The i-th point's params paired with their names — a derived view over the
|
||||
/// carried param-space (reuses [`zip_params`]); no new per-point state.
|
||||
pub fn named_params(&self, i: usize) -> Vec<(String, Scalar)> {
|
||||
zip_params(&self.space, &self.points[i].params)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Change the family assembly in `sweep_with_threads` (`sweep.rs:175-181`) from:
|
||||
|
||||
```rust
|
||||
SweepFamily {
|
||||
points: points
|
||||
.into_iter()
|
||||
.zip(reports)
|
||||
.map(|(params, report)| SweepPoint { params, report })
|
||||
.collect(),
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
SweepFamily {
|
||||
space: space.param_specs().to_vec(),
|
||||
points: points
|
||||
.into_iter()
|
||||
.zip(reports)
|
||||
.map(|(params, report)| SweepPoint { params, report })
|
||||
.collect(),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Thread the new field into the aura-registry test literal**
|
||||
|
||||
The field addition breaks the `SweepFamily { points: vec![..] }` literal in the
|
||||
`optimize` test (`crates/aura-registry/src/lib.rs:278`). `optimize`/`rank_by`
|
||||
read only `.points[].report`, so an empty space suffices. Change:
|
||||
|
||||
```rust
|
||||
let family = SweepFamily {
|
||||
points: vec![
|
||||
point(0.0, 1.0), // 0: also-ran
|
||||
point(10.0, 3.0), // 1: tied max, earliest -> the winner
|
||||
point(20.0, 2.0), // 2: also-ran
|
||||
point(30.0, 3.0), // 3: tied max, but later -> must lose the tie
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
let family = SweepFamily {
|
||||
space: vec![],
|
||||
points: vec![
|
||||
point(0.0, 1.0), // 0: also-ran
|
||||
point(10.0, 3.0), // 1: tied max, earliest -> the winner
|
||||
point(20.0, 2.0), // 2: also-ran
|
||||
point(30.0, 3.0), // 3: tied max, but later -> must lose the tie
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Compile the whole workspace (incl. tests) to confirm the field is fully threaded**
|
||||
|
||||
Run: `cargo test --workspace --no-run`
|
||||
Expected: builds with 0 errors (aura-engine assembly + aura-registry test literal
|
||||
both threaded; the aura-cli still compiles — it constructs no `SweepFamily`
|
||||
literal).
|
||||
|
||||
- [ ] **Step 7: Run the new tests to verify they pass**
|
||||
|
||||
Run: `cargo test -p aura-engine sweep_family_carries_param_space`
|
||||
Expected: PASS — 1 test.
|
||||
|
||||
Run: `cargo test -p aura-engine family_named_params_round_trips`
|
||||
Expected: PASS — 1 test.
|
||||
|
||||
- [ ] **Step 8: Verify the determinism guard still passes**
|
||||
|
||||
Run: `cargo test -p aura-engine family_is_deterministic_across_thread_counts`
|
||||
Expected: PASS — 1 test (enumeration untouched; the new `space` field is equal
|
||||
across thread counts for the same grid).
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Migrate `sim_optimal_manifest` to typed `Scalar` pairs + all 8 call sites (aura-cli)
|
||||
|
||||
Behaviour-preserving migration (no new RED test): the manifest's stored
|
||||
`params: Vec<(String, f64)>` is byte-identical before and after; the existing
|
||||
CLI E2E suite is the regression guard.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (imports :16; `sim_optimal_manifest` :130-142; closures :386-392, :503-509, :530-536; literal callers :161, :224, :613, :869, :970)
|
||||
|
||||
- [ ] **Step 1: Import `zip_params`**
|
||||
|
||||
Change `crates/aura-cli/src/main.rs:16`:
|
||||
|
||||
```rust
|
||||
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
use aura_core::{zip_params, Firing, Scalar, ScalarKind, Timestamp};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Change `sim_optimal_manifest` to take typed pairs, collapsing f64 internally**
|
||||
|
||||
Change `crates/aura-cli/src/main.rs:130-142`:
|
||||
|
||||
```rust
|
||||
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(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
fn sim_optimal_manifest(
|
||||
params: Vec<(String, Scalar)>,
|
||||
window: (Timestamp, Timestamp),
|
||||
seed: u64,
|
||||
) -> RunManifest {
|
||||
// The lossy f64 collapse lives here — the manifest field (the deferred typed
|
||||
// param-space precursor) owns its own lossiness; callers pass typed Scalars.
|
||||
let params = params.into_iter().map(|(n, s)| (n, scalar_as_param_f64(&s))).collect();
|
||||
RunManifest {
|
||||
commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(),
|
||||
params,
|
||||
window,
|
||||
seed,
|
||||
broker: "sim-optimal(pip_size=0.0001)".to_string(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Migrate the three sweep closures to `zip_params`**
|
||||
|
||||
In `sweep_family` (`main.rs:386-392`), delete the hand-zip and pass `zip_params`.
|
||||
Change:
|
||||
|
||||
```rust
|
||||
let params = space
|
||||
.iter()
|
||||
.zip(point)
|
||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
||||
.collect();
|
||||
RunReport {
|
||||
manifest: sim_optimal_manifest(params, window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
RunReport {
|
||||
manifest: sim_optimal_manifest(zip_params(&space, point), window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
```
|
||||
|
||||
In `sweep_over` (`main.rs:503-509`), the block is byte-identical to
|
||||
`sweep_family`'s. Change:
|
||||
|
||||
```rust
|
||||
let params = space
|
||||
.iter()
|
||||
.zip(point)
|
||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
||||
.collect();
|
||||
RunReport {
|
||||
manifest: sim_optimal_manifest(params, window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
RunReport {
|
||||
manifest: sim_optimal_manifest(zip_params(&space, point), window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
```
|
||||
|
||||
In `run_oos` (`main.rs:530-536`), the zip is over the `params` argument into
|
||||
`named`. Change:
|
||||
|
||||
```rust
|
||||
let named = space
|
||||
.iter()
|
||||
.zip(params)
|
||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
||||
.collect();
|
||||
let report = RunReport {
|
||||
manifest: sim_optimal_manifest(named, window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
};
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
let report = RunReport {
|
||||
manifest: sim_optimal_manifest(zip_params(&space, params), window, 0),
|
||||
metrics: summarize(&equity, &exposure),
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Migrate the five literal callers to `Scalar` values**
|
||||
|
||||
In `run_sample` (`main.rs:161`), `run_sample_real` (`main.rs:224`), `mc_family`
|
||||
(`main.rs:613`), and `run_sample_seeded` (`main.rs:970`) — each passes the same
|
||||
3-tuple. Change every occurrence of:
|
||||
|
||||
```rust
|
||||
vec![
|
||||
("sma_fast".to_string(), 2.0),
|
||||
("sma_slow".to_string(), 4.0),
|
||||
("exposure_scale".to_string(), 0.5),
|
||||
],
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
vec![
|
||||
("sma_fast".to_string(), Scalar::F64(2.0)),
|
||||
("sma_slow".to_string(), Scalar::F64(4.0)),
|
||||
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||
],
|
||||
```
|
||||
|
||||
In `run_macd` (`main.rs:869`), the 4-tuple incl. `ema_signal`. Change:
|
||||
|
||||
```rust
|
||||
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),
|
||||
],
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
vec![
|
||||
("ema_fast".to_string(), Scalar::F64(2.0)),
|
||||
("ema_slow".to_string(), Scalar::F64(4.0)),
|
||||
("ema_signal".to_string(), Scalar::F64(3.0)),
|
||||
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||
],
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Compile the whole workspace (incl. tests)**
|
||||
|
||||
Run: `cargo test --workspace --no-run`
|
||||
Expected: builds with 0 errors (all 8 `sim_optimal_manifest` call sites migrated,
|
||||
incl. the `#[cfg(test)]` helper `run_sample_seeded`).
|
||||
|
||||
- [ ] **Step 6: Verify behaviour-preservation via the existing E2E suite**
|
||||
|
||||
Run: `cargo test -p aura-cli run_sample_is_deterministic_and_non_trivial`
|
||||
Expected: PASS (the manifest params for the sample run are unchanged).
|
||||
|
||||
Run: `cargo test -p aura-cli sweep_report_renders_four_points_in_odometer_order`
|
||||
Expected: PASS (the `aura sweep` output is byte-identical).
|
||||
|
||||
Run: `cargo test -p aura-cli run_macd_compiles_from_nested_composite_and_is_deterministic`
|
||||
Expected: PASS (the 4-tuple `run_macd` manifest is unchanged).
|
||||
|
||||
- [ ] **Step 7: Full workspace suite + lint**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — all tests green (no regressions; the two new aura-engine tests
|
||||
and two new aura-core tests included).
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: clean — 0 warnings.
|
||||
Reference in New Issue
Block a user