# Fieldtest — cycle-0049 (random param-sweep) — 2026-06-17 **Status:** Draft — awaiting orchestrator triage **Author:** fieldtester (dispatched by fieldtest skill) ## Scope Cycle 0049 shipped the **random** half of the C12.1 param-sweep axis in `aura_engine`. New public surface: `RandomSpace` (draws `N` seeded uniform points over per-slot continuous ranges, validated against a blueprint's param-space before any run), a typed `ParamRange` (`{lo, hi}` with `ParamRange::i64` / `ParamRange::f64` constructors and `kind()`), a `Space` trait (`points()` / `param_specs()`) that both `GridSpace` and `RandomSpace` implement so `sweep` / `sweep_with_threads` are generic over `impl Space`, and three new `SweepError` variants (`NonNumericRange`, `RangeKindMismatch`, `EmptyRange`). The per-point run closure is unchanged (`Fn(&[Cell]) -> RunReport`). This field test exercised the surface from a standalone downstream consumer crate (path-deps only; public interface = ledger + glossary + spec 0049 + `cargo doc` rustdoc; no `crates/*/src` read). Binary exercised: each bin rebuilt from HEAD via `cargo run --manifest-path …` after a clean `cargo build --workspace`. ## Examples ### fieldtests/cycle-0049-random-sweep/c0049_1_continuous_tune.rs — headline continuous tuning - Builds a 3-param SMA-cross harness (`sma_cross.fast.length: I64`, `sma_cross.slow.length: I64`, `exposure.scale: F64`), declares one `ParamRange` per slot, draws 200 seeded points via `RandomSpace::new`, runs `sweep`, and picks the best point by `total_pips`. - Fits the cycle scope: this is the named "audience reaches for it" task — a grid over continuous ranges explodes; random draw is the standard tool. - Outcome: built (first try after a self-inflicted port-name slip), ran, matched expected. 200 in-range points, 92 distinct metrics, best #121 = 6.3216 pips at fast=8/slow=12/scale=1.3182. ### fieldtests/cycle-0049-random-sweep/c0049_2_validation_gate.rs — typed validation gate - Declares invalid ranges and records the typed `SweepError` from `RandomSpace::new` before any run: `Arity`, `RangeKindMismatch` (F64 range on I64 slot), `EmptyRange` (I64 `lo>hi`), `EmptyRange` (F64 `lo>=hi`), the valid I64 `lo==hi` single point (accepted), and `NonNumericRange` (range on a Bool slot). The Bool slot required authoring a tiny custom node (`GateNode`), because no shipped `aura-std` node declares a Bool/Timestamp param. - Fits the cycle scope: directly probes axis-hint 2 (the typed gate). - Outcome: built, ran, matched expected. Every fault caught pre-run with exact slot + kind; `NonNumericRange` wins over `RangeKindMismatch` on the Bool slot. ### fieldtests/cycle-0049-random-sweep/c0049_3_reproducibility.rs — reproducibility + seed-sensitivity + wide-range edge - Compares whole `SweepFamily`s (`derive(PartialEq)`): same `(ranges, count, seed)` reproduces an identical family; a different seed changes it. Then probes the full `ParamRange::i64(i64::MIN, i64::MAX)` range through `Space::points()` to check the commit-body hardening claim (the literal sampler would `% 0`). - Fits the cycle scope: axis-hint 3 (the C1 promise) plus the documented sampler deviation. - Outcome: built, ran, matched expected. Same-seed family identical; different-seed family differs; the full i64-domain range was accepted and `points()` produced 8 values spanning the signed domain with no panic. ### fieldtests/cycle-0049-random-sweep/c0049_4_space_interchange.rs — Space-trait interchangeability - One downstream `tune_and_rank(space: &S)` consumer, written once and called with both a `GridSpace` and a `RandomSpace` over the same param-space, same `run_one` closure, asserting both produce comparable, non-degenerate families carrying the same schema. - Fits the cycle scope: axis-hint 4 (the abstraction's payoff). - Outcome: built, ran, matched expected. Both 12-point families ranked through the identical generic consumer; shared `family.space`; named view identical. ## Findings ### [working] Headline continuous-range tuning is the code a researcher would write - Example: c0049_1. - The whole flow — `param_space()` → one `ParamRange` per slot → `RandomSpace::new(&space, ranges, count, seed)?` → `sweep(&rand_space, run_one)` → rank by metric → `family.named_params(best)` for a readable coordinate — read naturally and compiled/ran on the first real attempt. All 200 draws landed inside their declared ranges (I64 inclusive, F64 half-open), and `bootstrap_with_cells(point)` paired cleanly with the `Fn(&[Cell]) -> RunReport` closure (no `Scalar`/`Cell` conversion dance in the hot loop). The family spread (92 distinct metrics over 200 points) and a single argmax pass picked the best. This is the cycle's primary acceptance criterion met empirically. - Recommended downstream action: carry-on. ### [working] The typed validation gate is precise and fires before any run - Example: c0049_2. - Every reachable fault surfaced from `RandomSpace::new` with an exact, self-describing variant: `Arity { expected: 3, got: 2 }`, `RangeKindMismatch { slot: 0, expected: I64, got: F64 }`, `EmptyRange { slot: 0 }` (I64 lo>hi) and `{ slot: 2 }` (F64 lo>=hi), and `NonNumericRange { slot: 0, kind: Bool }`. The valid I64 `lo==hi` single point was accepted (7 points). The ordering is sensible: a non-numeric slot is reported as `NonNumericRange` even when the supplied range's kind also mismatches, so the author sees the structural cause (wrong slot kind) rather than a downstream symptom. - Recommended downstream action: carry-on. ### [working] Reproducibility, seed-sensitivity, and the wide-range i64 hardening all hold - Example: c0049_3. - `SweepFamily`'s `derive(PartialEq)` made the C1 promise checkable in one line: `family(seed) == family(seed)` held end to end (manifest + metrics included), and `family(seed_a) != family(seed_b)`. The full `[i64::MIN, i64::MAX]` range — the case the commit body (e17d78f) flags as a spec deviation hardened against `% 0` / signed-overflow — was accepted by `new` and produced 8 finite values across the signed domain through the public `Space::points()`, no panic. The documented deviation is observably upheld at the public surface. - Recommended downstream action: carry-on. ### [working] The Space trait delivers its payoff: one consumer, both enumerations - Example: c0049_4. - A single generic `tune_and_rank` drove a `GridSpace` and a `RandomSpace` with zero per-enumeration branching, the same `run_one`, and a shared `family.space` schema for the named view. The grid path stayed behaviour-preserving (the existing `GridSpace::new` / `points()` / `param_specs()` inherent methods are still there alongside the trait impl). - Recommended downstream action: carry-on. ### [spec_gap] `NonNumericRange` / Bool-slot ranges are unreachable with the shipped node roster - Example: c0049_2 (the GateNode workaround). - Every shipped `aura-std` node declares only `I64` (`length`) or `F64` (`scale`) params; none has a `Bool` or `Timestamp` knob. So a downstream consumer using only the standard roster can never hit the `NonNumericRange` gate (nor a Bool `RangeKindMismatch`) — I had to author a custom `PrimitiveBuilder`-based node with a Bool param purely to construct a Bool slot. The variant is correct and the C16 "author your own node" path makes it reachable, but the cycle ships a validation arm that no shipped surface can trigger. The ledger/spec do not say whether a Bool knob is an expected near-term authoring case or a purely defensive guard. Plausible alternate reading: `NonNumericRange` is dead-until-a-Bool-knob-node-exists and could have been deferred with the rest of the random axis. I picked the reading "it is a defensive structural guard for future Bool/Timestamp knobs" and the example demonstrates it works under that reading. - Recommended downstream action: ratify (record in the ledger that the Bool/Timestamp param-slot is a forward-looking authoring case the gate guards, so the otherwise-untriggerable variant is intentional, not drift). ### [friction] `RandomSpace` has no named-axis builder; positional `ranges` must line up with `param_space()` by hand - Examples: c0049_1, c0049_3, c0049_4. - `GridSpace` has a fluent, by-name sibling: `Composite::axis("sma_cross.fast", …).sweep(run)` (the `SweepBinder`), which resolves axes against `param_space()` by name so a mis-ordered or mis-counted axis is impossible. `RandomSpace` has no counterpart: the author must build `Vec` in exactly `param_space()` slot order and pass it positionally to `RandomSpace::new(&space, ranges, …)`. The `Arity` / `RangeKindMismatch` checks catch a wrong count or a kind-swapped slot, but a same-kind transposition (e.g. swapping the two I64 ranges for `fast.length` and `slow.length`) passes validation silently and tunes the wrong knob over the wrong interval — the exact failure class the named `SweepBinder` was built to remove for the grid axis (cf. node memory: the mw_1 ParamAlias pain, fixed by naming). The task completes, but the author re-takes on the positional-alignment burden that the grid path already retired. - Recommended downstream action: plan (a `RandomSpace` named-axis builder — a `RandomBinder` sibling to `SweepBinder` mapping `name -> ParamRange` against `param_space()`, so the by-name resolution that protects the grid sweep also protects the random sweep). ### [spec_gap] `SweepError`'s rustdoc summary names only GridSpace though it now gates both spaces - Examples: c0049_2 (the error type the consumer reads). - The `SweepError` enum's top-level rustdoc (the public doc a consumer reads to understand the type) still reads: "A structural fault constructing a **`GridSpace`** — the typed gate before any run." The enum now also carries the three RandomSpace-only variants (`NonNumericRange`, `RangeKindMismatch`, `EmptyRange`), each documented individually as RandomSpace faults, but the type-level summary is stale: a consumer scanning the type doc would not learn that this same error type gates `RandomSpace::new`. The audit commit (3fca781) noted refreshing the *module* doc to name `RandomSpace` + the `Space` trait, but the `SweepError` type doc itself was not updated. Another equally-plausible reading is that `SweepError` is deliberately the single shared sweep-error type and the summary should say so; I cannot tell which from the public surface. - Recommended downstream action: tighten the design ledger / docs (one-line doc-debt fix: broaden the `SweepError` summary to "constructing a `GridSpace` *or* `RandomSpace`" — doc-only, behaviour-preserving, the same low-grade doc-debt class the audit already fixed inline for the module doc). ## Recommendation summary | Finding | Class | Action | |---|---|---| | Headline continuous tuning works | working | carry-on | | Typed validation gate is precise | working | carry-on | | Reproducibility + seed-sensitivity + wide-i64 hold | working | carry-on | | Space-trait interchangeability payoff | working | carry-on | | NonNumericRange unreachable with shipped roster | spec_gap | ratify | | No named-axis builder for RandomSpace | friction | plan | | SweepError doc summary names only GridSpace | spec_gap | tighten the design ledger / docs |