# Fieldtest — milestone "The World — parameter-space & sweep" — 2026-06-11

**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill, milestone-scope variant)
**Scope mode:** MILESTONE (closing functional gate), not per-cycle.

## Milestone delivery roll-up

The milestone promises a native-Rust strategy author can **declare** typed knobs,
**bind** a value-empty blueprint into one runnable instance **by name**, **spread**
it across a **family** over named axes, and **index/compare** the family
(issues #30–#36). Per the three curated end-to-end scenarios:

| Scenario | Promise clause | Delivered? |
|---|---|---|
| 1 — single run, bound **by name** | declare + bind by name (`.with(...).bootstrap()`) | 🟢 GREEN — **with one caveat**: the promised names `sma_cross.fast/.slow` are not free; they require author-added C23 `ParamAlias` overlays. Once aliased, `.with("sma_cross.fast", 2).bootstrap()` binds and runs; diagnostics are clean. |
| 2 — **sweep** over named axes + **compare** the family | spread + index/compare (`.axis(...).sweep(run)`, #32/#33) | 🟢 GREEN — `.axis("sma_cross.fast", [2,3]).sweep(run)` yields a 4-member, ordered, disjoint, non-constant `SweepFamily`; comparison/ranking assembled from the public `SweepPoint`/`RunReport`/`RunMetrics` fields. The `aura sweep` + `aura runs rank` CLI delivers the same payoff with no Rust. |
| 3 — structural-constant negative space | (deliberately **out of milestone scope**; grounds #55) | 🔴 RED **by design** — the definitional `2` of SMA2-entry can only be expressed as an open `param_space()` knob (avenue a) or via an escape hatch reachable only for `SimBroker`, not indicators (avenue b). This is the forward-pointer to #55, **not** a milestone-blocking defect. |

**Verdict (one line):** the milestone's promise is **empirically delivered** —
scenarios 1 and 2 are green end-to-end (the by-name caveat is a friction, not a
gap in the capability) — with scenario 3 logged as the empirical grounding for
#55 (structural-constant binding), which the milestone never promised.

## Scope

The milestone spans cycles 0015→0030 (param declaration #30, binding #31, sweep
#32, run registry #33, named binding #35). It promises that an author can declare
a node's tunable knobs (typed), aggregate them into a flat, path-qualified
`param_space()`, bind one runnable instance **by name** (`.with()/.bootstrap()`),
spread a family over **named axes** (`.axis()/.sweep()`), and index/compare the
resulting `SweepFamily`. The deeper promise (C11/C12/C19): a blueprint is
param-generic; binding sizes nodes but never changes topology; family members are
C1-disjoint; `param_space()` is the single ground truth both single-run binding
and sweep enumeration address.

This fieldtest derives three end-to-end scenarios top-down from that promise and
drives them from the **public interface only**: the design ledger
(`docs/design/INDEX.md`), the specs (0015/0028/0030), and the crates' published
rustdoc (`cargo doc --workspace --no-deps`). **No `crates/*/src` was read** — the
current API (incl. the 0024 deletion of `struct Blueprint`, the root-is-a-Composite
model, the `Sma::factory` → `Sma::builder` rename, and the `Role.source` field)
was discovered entirely from the ledger + rustdoc + the compiler's own
diagnostics. The artefact under test was built from HEAD (`cargo build
--workspace`; the consumer crate path-deps the engine crates; fixtures run via
`cargo run`, so HEAD is always what executed).

## Examples

### fieldtests/milestone-the-world-param-sweep/mw_1_single_named.rs — single concrete run, bound by name
- Authors the SMA-cross harness as a root `Composite` (0024 model: a Composite
  all of whose roles are source-bound), inspects `param_space()`, and tries to
  bind by the milestone-promise names `sma_cross.fast/.slow`. Records the
  diagnostic, then the actual-emitted name, then the aliased fix, then five
  diagnostic probes.
- Fits the milestone: this is the headline by-name binding story (#30/#31/#35) —
  declare, inspect, bind one instance by name, run it.
- Outcome: built? **yes.** ran? yes. matched expected? **partially** — the
  promise-form bind **fails** (`UnknownKnob("sma_cross.fast")`); the real names
  collide (`AmbiguousKnob("sma_cross.length")`); after adding `ParamAlias`
  overlays the promise-form bind **works** and runs (12 non-degenerate equity
  rows). Diagnostics clean.

### fieldtests/milestone-the-world-param-sweep/mw_2_sweep_compare.rs — sweep over named axes + compare the family
- Takes the (aliased) harness, spreads it over `.axis("sma_cross.fast", [2,3])
  .axis("sma_cross.slow", [4,5]).axis("scale", [0.5]).sweep(run)`, then builds a
  comparison table + best-by-`total_pips` ranking from the public `SweepFamily`
  fields, asserts the family is ordered (odometer), disjoint, and non-constant.
- Fits the milestone: the "N instead of 1" core (#32) + the index/compare surface
  (#33).
- Outcome: built? yes. ran? yes. matched expected? **yes** — 4 members, 4
  distinct `total_pips`, odometer order `[2,4]→[2,5]→[3,4]→[3,5]`, ranking works.

### fieldtests/milestone-the-world-param-sweep/mw_3_structural_constant.rs — negative-space probe (grounds #55)
- Authors the "SMA2-entry" strategy and tries to express the definitional `2`
  honestly via both avenues the carrier names: (a) leave it as an open
  `param_space()` knob fixed to 2; (b) imitate `SimBroker::builder(constant)`.
  Records both as wrong, plus the wished-for `.bind()`/`builder_with()` consumer
  code (commented out — it does not compile).
- Fits the milestone: the deliberate negative space — what the param-space layer
  **cannot** express, the acceptance evidence for #55.
- Outcome: built? yes. ran? yes (avenue a runs; avenue b is a documented compile
  wall). matched expected? **yes** — both avenues are recordably wrong; the `2`
  is fungible (a) or the escape hatch is unreachable for an indicator (b).

### Run artefacts
- `out_mw_1.txt` / `out_mw_2.txt` / `out_mw_3.txt` — verbatim stdout of each
  fixture. Plus the `aura sweep` + `aura runs rank total_pips` CLI output
  (recorded inline in findings), the no-Rust face of the same family payoff.

## Findings

### [working] The named-axis sweep delivers a comparable, ordered, disjoint family from the public surface
- Example: mw_2; corroborated by `aura sweep` / `aura runs rank`.
- What happened: `.axis(name, vals)*.sweep(run)` resolved the named axes to a
  4-point grid, ran each disjointly, and returned a `SweepFamily` whose
  `points[i].params` (slot-order coordinate) and `points[i].report.metrics`
  (`total_pips`/`max_drawdown`/`exposure_sign_flips`) let a consumer build a
  comparison table and a best-by-metric ranking with **no scaffolding beyond a
  for-loop**. Order is odometer (last axis fastest), the 4 metrics are distinct
  (not a constant family), every `total_pips` finite. The `aura sweep` CLI emits
  the same family as JSON lines and `aura runs rank total_pips` orders them
  best-first across invocations.
- Why working: this is the milestone's core "N instead of 1" + index/compare
  promise (#32/#33), delivered end-to-end from the public interface.
- Recommended action: carry-on.

### [working] Single-run binding by name + clean, knob-named diagnostics (once knobs have distinct names)
- Example: mw_1 (aliased path) + probes 3/4/5/6.
- What happened: `.with("sma_cross.fast", 2).with("sma_cross.slow", 4)
  .with("scale", 0.5).bootstrap()` binds and runs (12-row trace). The diagnostics
  are precise and name the offending knob: a typo →
  `UnknownKnob("sma_cross.slo")`; an unqualified short name →
  `UnknownKnob("fast")`; a wrong kind → `KindMismatch { knob: "scale", expected:
  F64, got: I64 }`; the un-aliased collision → `AmbiguousKnob("sma_cross.length")`.
  Raw literals (`2`, `0.5`) lower without suffixes.
- Why working: spec 0030's by-name binding + the total error order land exactly
  as designed; the error vocabulary is a strict win over a positional vector.
- Recommended action: carry-on.

### [friction] The promised knob names `sma_cross.fast/.slow` do NOT exist out of the box — they require hand-added `ParamAlias` overlays
- Examples: mw_1 [2a]/[2b]/[2c]; the engine's own `aura sweep` sample (whose
  manifest shows `sma_cross.fast`/`.slow`, so even the shipped sample had to add
  the aliases).
- What happened: the milestone promise and the spec 0030 worked example both
  bind `.with("sma_cross.fast", 2)`. Run verbatim against a plain SMA-cross
  composite, `param_space()` emits **`sma_cross.length` twice** (the raw `Sma`
  factory param name, path-qualified only by the composite name), so
  `.with("sma_cross.fast", 2)` → `UnknownKnob("sma_cross.fast")` and binding the
  real name `.with("sma_cross.length", 2)` → `AmbiguousKnob("sma_cross.length")`
  (two identical names). To get the promised distinct names the author must
  author two `ParamAlias { name: "fast"/"slow", node, slot }` overlays on the
  composite. Only then does `param_space()` emit `sma_cross.fast`/`sma_cross.slow`
  and the promise-form bind succeed.
- Why friction (not bug): the binding layer is correct — it faithfully matches
  the exact `param_space()` name, and spec 0030 §Architecture explicitly says
  short/distinct names are an authoring choice resolved by `ParamAlias`. But the
  milestone's headline example reads as if `fast`/`slow` are free, and a new
  author following it verbatim hits two consecutive errors before discovering the
  required alias step. The acceptance evidence (the code a user writes) is
  longer than advertised. The task **completes**, but only after the author
  supplies the naming overlay the example silently assumed.
- Recommended action: plan (tidy / docs) — either (i) make the worked example in
  the ledger/spec show the `ParamAlias` step explicitly (so `sma_cross.fast` is
  visibly *earned*), or (ii) give `Sma::builder()` a name argument / a
  per-instance recipe label so two sibling SMAs carry distinct factory names
  without a separate alias. Low-to-medium; it is the first thing a by-name author
  trips on.

### [friction] A fan-in of two same-signature un-aliased knobs cannot bootstrap — aliasing is mandatory, not optional, for the canonical SMA-cross
- Examples: mw_3 (initial bootstrap → `IndistinguishableFanIn { node: 2 }`);
  mw_1's plain harness is only ever *inspected*, never bootstrapped, for the same
  reason.
- What happened: `Sub` fanning in two `Sma`s with equal recursive signatures —
  each carrying an unaliased param slot — is a compile-time
  `CompileError`/`BootstrapError::...IndistinguishableFanIn` (the C9 fan-in
  distinguishability refinement). So the **most canonical** strategy in the whole
  project (a 2-leg SMA cross) does not bootstrap until the author aliases its two
  length knobs. The same `ParamAlias` overlays demanded by the friction above are
  thus doubly mandatory: once to bind by distinct names, once to clear the fan-in
  check.
- Why friction (not bug): the `IndistinguishableFanIn` rejection is a designed,
  documented C9 guarantee (it catches a hidden unnamed configuration axis), and
  the diagnostic names the node. But it converts "author a 2-SMA cross and bind
  it" — the project's running example — into a four-step dance (build, alias to
  satisfy fan-in, alias to get distinct names — the same two aliases, then bind).
  The friction is that the milestone's own canonical example cannot be run in its
  minimal form.
- Recommended action: plan — pair with the friction above; both are dissolved by
  per-instance SMA naming at construction (a recipe label), which would satisfy
  fan-in distinguishability AND yield distinct param-space names in one step.

### [friction] The sweep closure must re-bind positionally and re-zip param names the author already declared
- Example: mw_2 (the `.sweep(|point| { ... bootstrap_with_params(point.to_vec())
  ... })` closure; the empty `manifest.params`).
- What happened: `.axis(...).sweep(run)` resolves named axes to a positional
  point, but the per-point closure receives a bare `&[Scalar]` and must call
  `bootstrap_with_params(point.to_vec())` (positional) and rebuild the blueprint
  by hand. To put readable knob names into the per-point `RunManifest.params`,
  the author must re-zip `param_space()` names onto the point themselves — the
  named layer that resolved the axes does not thread the names through to the
  closure. (The `aura sweep` CLI does this zip by hand internally.)
- Why friction: the named-binding ergonomics stop at the grid boundary; inside
  the sweep the author is back to positional vectors and manual name-zipping,
  re-deriving information `param_space()` already holds. The comparison in mw_2
  works, but only because the consumer re-zips names onto `pt.params` for display.
- Recommended action: plan — consider passing the closure a named view of the
  point (e.g. `&[(/* name */ &str, Scalar)]` or a small `Point` with `get(name)`),
  or have `SweepPoint` carry the param names so a consumer need not re-zip from
  `param_space()`. Low-to-medium.

### [spec_gap] The definitional structural constant (SMA2-entry's `2`) has no honest expression — grounds #55
- Example: mw_3 avenues (a) and (b).
- What happened: the SMA2-entry strategy's `2` is bound to the two-candle
  construction (sweeping it deforms the strategy). Today only two avenues exist,
  both wrong:
  - **(a)** leave the length open: `param_space()` emits `sma2_entry.bias : I64`
    (forced to alias it just to clear `IndistinguishableFanIn`), **indistinguishable
    from the genuinely-tunable `sma2_entry.slow`**. A sweep
    `.axis("sma2_entry.bias", [2,5,10])` would enumerate **deformed** strategies
    as valid family members. `param_space()` declares a degree of freedom the
    strategy's definition forbids (C20 says structural axes are not sweep params —
    but nothing here distinguishes the two).
  - **(b)** imitate `SimBroker::builder(1e-4)` (a constant captured **outside**
    `param_space`). Verbatim wall: `Sma::builder()` takes **no** length arg
    (length is always an injected param), and `Sma::new(2)` returns a **built**
    `Sma`, not a `PrimitiveBuilder`/`BlueprintNode` — and the only value-conversion
    is `From<PrimitiveBuilder> for BlueprintNode` (confirmed from rustdoc; there
    is **no** `From<Sma>`). A blueprint holds value-empty recipes, never built
    nodes. The construction-constant escape hatch exists for `SimBroker` only
    because its `builder(pip_size)` **takes** the constant; there is no
    `Sma::builder_with(2)`. So the escape hatch is **structurally unreachable**
    for a stock indicator node.
- Why spec_gap (not a milestone bug): the milestone **never promised**
  structural-constant binding — the spec 0030 §Goal explicitly puts it out of
  scope ("structural blueprint constants … is the opposite operation and lives in
  #55"). This finding is the **empirical grounding** for #55: the wished-for
  consumer code (a `Sma::builder().bind("length", 2)` that removes the knob from
  `param_space`, or a `Sma::builder_with(2)` recipe constructor) is recorded
  verbatim in mw_3 as the acceptance evidence a future #55 spec needs.
- Recommended action: ratify the deferral as correct (the milestone gate is not
  blocked) **and** route the recorded wished-for code to #55 as its acceptance
  evidence. The `.bind()` name is already reserved for exactly this (spec 0030).

## Recommendation summary

| Finding | Class | Action | Routing |
|---|---|---|---|
| Named-axis sweep yields a comparable/ordered/disjoint family | working | carry-on | — |
| Single-run by-name binding + clean knob-named diagnostics | working | carry-on | — |
| Promised `sma_cross.fast/.slow` need hand-added `ParamAlias` | friction | plan (tidy/docs) | #30/#35 follow-up or doc-fix |
| Canonical SMA-cross can't bootstrap without alias (`IndistinguishableFanIn`) | friction | plan | pairs with the alias-naming friction |
| Sweep closure re-binds positionally + re-zips names | friction | plan | #32/#33 ergonomics follow-up |
| SMA2-entry's definitional `2` has no honest expression | spec_gap | ratify deferral + route evidence | **#55** |

## Milestone roll-up

**The milestone "The World — parameter-space & sweep" DELIVERS its promise.** A
native-Rust author can declare typed knobs, inspect them via a flat
path-qualified `param_space()`, bind one runnable instance **by name**
(`.with(...).bootstrap()`) with a precise, knob-named error vocabulary, spread the
blueprint across a **named-axis family** (`.axis(...).sweep(run)`), and
index/compare the disjoint, ordered, non-constant `SweepFamily` — all from the
public interface, with the `aura sweep` + `aura runs rank` CLI as the no-Rust
face of the same payoff. Scenarios 1 and 2 (the actual close-gate) are green
end-to-end.

The frictions are about **the naming ergonomics on the way in**, not the
capability: the milestone's headline by-name example (`sma_cross.fast/.slow`) is
not free — those distinct names require author-added `ParamAlias` overlays, which
are **doubly mandatory** because the canonical 2-SMA cross also will not bootstrap
without them (`IndistinguishableFanIn`). A by-name author following the worked
example verbatim hits two consecutive errors before discovering the alias step.
All are tidy follow-ups, none block the gate.

Scenario 3 is the deliberate negative space: the definitional structural constant
of SMA2-entry has no honest expression today (it leaks into `param_space()` as a
fungible knob, and the `SimBroker` construction-constant escape hatch is
unreachable for an indicator). This is **exactly** the gap #55 is filed for — the
milestone never promised it — and the wished-for consumer code is now recorded as
#55's empirical acceptance evidence. **No bugs found.** Two `working`, three
`friction`, one `spec_gap` (→ #55) — none blocking the milestone gate.
