11dfff860c
Per-cycle fieldtest of node-instance naming (#56), driven as a downstream consumer from the public interface only (ledger + spec + rustdoc + CLI; no crates/*/src read), three fixtures built and run from HEAD. Verdict: the core 0031 promise holds first-try. A consumer authors Sma::builder().named("fast"), inspects param_space() (sma_cross.fast.length), binds by name and runs; the default-name case (sma.length / exposure.scale, verbatim lowercase) and paramless-interchangeable-stays-legal both hold. 0 bugs, 3 working, 2 friction, 1 spec_gap. The one real gap (verified by the orchestrator against the fixture output): the spec's headline forcing function IndistinguishableFanIn does NOT reach an author on the canonical .with(...).bootstrap() flow. An un-named 2-SMA cross emits a literal DUPLICATE knob (sma_cross.sma.length x2); the binder resolves names before the compile fan-in check, so the author hits UnknownKnob / AmbiguousKnob("sma_cross.sma.length") — which point at the knob, not at the cure "name your nodes". IndistinguishableFanIn only surfaces via the positional compile_with_params path. Rejection still happens (no invalid blueprint runs), so it is an ergonomic/signposting gap, not a correctness bug. Routed to the backlog (relates to #58); 0031 stays audit-closed. Minor: FlatGraph/Harness lack Debug, so a bootstrap Result can't be {:?}-printed.
195 lines
12 KiB
Markdown
195 lines
12 KiB
Markdown
# Fieldtest — cycle-0031 — 2026-06-11
|
||
|
||
**Status:** Draft — awaiting orchestrator triage
|
||
**Author:** fieldtester (dispatched by fieldtest skill)
|
||
|
||
## Scope
|
||
|
||
Cycle 0031 (node-instance naming, #56) gives every blueprint node a name. A
|
||
primitive builder gains an optional instance name (`Sma::builder().named("fast")`);
|
||
omitted, it defaults to the node's type label ASCII-lowercased verbatim
|
||
(`"SMA"`→`"sma"`, no snake_case). `param_space()` is now uniformly
|
||
`<node>.<param>` at every level including the root (`sma_cross.fast.length`,
|
||
`exposure.scale`). Fan-in distinguishability (C9) and `signature_of` re-key onto
|
||
the node name: two same-type siblings both defaulting to `"sma"` collide and
|
||
`IndistinguishableFanIn` fires; naming them `"fast"`/`"slow"` fixes both the
|
||
naming collision and the fan-in check in one act. The index-addressed
|
||
`ParamAlias` overlay (and `Composite::new`'s 5th argument) was retired.
|
||
|
||
Tested from the public interface only: the design ledger (C9/C23 amendments),
|
||
spec 0031, and `cargo doc --workspace` rustdoc; never `crates/*/src`. Build
|
||
exercised: `cargo build --workspace` (green from HEAD `0e411f1`), then each
|
||
fixture via `cargo run --manifest-path
|
||
fieldtests/cycle-0031-node-naming/Cargo.toml --bin <name>` so HEAD source is
|
||
always what runs. The fixture crate is a standalone non-member crate
|
||
path-depending on `aura-core/-engine/-std`, as a C16 research project would.
|
||
|
||
## Per-task roll-up
|
||
|
||
| Task | Axis | Built? | Ran? | Matched expected? |
|
||
|---|---|---|---|---|
|
||
| 1 named single-run | a | yes | yes | yes — `sma_cross.fast.length` etc., bound & ran, deterministic |
|
||
| 2 forcing function | b | yes | yes | partially — fan-in is rejected, but **not via the diagnostic the spec headlines** (see F4) |
|
||
| 3 default names | c | yes | yes | yes — `sma.length`/`exposure.scale`, verbatim lowercase, paramless legal |
|
||
|
||
## Examples
|
||
|
||
### fieldtests/cycle-0031-node-naming/c0031_1_named_single_run.rs — the named single-run authoring path (axis a)
|
||
- Builds the canonical harness `source → sma_cross{ Sma("fast"), Sma("slow"),
|
||
Sub } → Exposure → SimBroker → Recorder`, inspects `param_space()`, binds
|
||
`sma_cross.fast.length` / `sma_cross.slow.length` / `exposure.scale` by name
|
||
(no `ParamAlias`, no index counting), runs, and re-runs disjoint for C1.
|
||
- Why it fits: this is exactly the friction the 0030 milestone fixture (`mw_1`)
|
||
had to work around with two hand-counted `ParamAlias` overlays — acceptance
|
||
#1/#2/#4 of the spec.
|
||
- Outcome: built and ran first try. `param_space()` = `[sma_cross.fast.length:I64,
|
||
sma_cross.slow.length:I64, exposure.scale:F64]`; named bind bootstrapped and
|
||
recorded 12 exposure rows; two disjoint runs bit-identical.
|
||
|
||
### fieldtests/cycle-0031-node-naming/c0031_2_forcing_function.rs — the forcing function (axis b)
|
||
- Authors the SAME 2-SMA cross WITHOUT names, then tries to bind/bootstrap it
|
||
the way a downstream author naturally would (`.with(...).bootstrap()`), then
|
||
isolates the compile wall via `compile_with_params`, then recovers by naming
|
||
the legs and runs.
|
||
- Why it fits: acceptance #3 — the deliberate forcing function, and the carrier's
|
||
explicit question "is the diagnostic discoverable/helpful from the public
|
||
surface alone?"
|
||
- Outcome: built and ran. The un-named cross IS rejected (good), but the
|
||
diagnostic an author reaches depends entirely on the path (see F4/F5). Naming
|
||
the legs resolves it and the named cross runs (12 rows).
|
||
|
||
### fieldtests/cycle-0031-node-naming/c0031_3_default_names.rs — default names + paramless-legal (axis c)
|
||
- A single un-named `Sma` at root → `sma.length`; un-named `Exposure` →
|
||
`exposure.scale`; checks no `sim_broker`-style snake_case leaks; binds the
|
||
default names and runs; then a two-`Add` paramless same-name fan-in compiles
|
||
(stays legal).
|
||
- Why it fits: the spec's "Default name" and "Paramless interchangeable stays
|
||
legal" clauses, plus the carrier's optional 3rd axis.
|
||
- Outcome: built and ran. `sma.length` / `exposure.scale` emitted at root and
|
||
bindable; paramless same-name fan-in compiled (`ok: true`).
|
||
|
||
## Findings
|
||
|
||
### [working] Named single-run path: `.named()` → uniform `<node>.<param>` → bind by name → run, deterministic
|
||
- Example: c0031_1.
|
||
- What happened: `Sma::builder().named("fast")` made `param_space()` emit
|
||
`sma_cross.fast.length` / `sma_cross.slow.length`, and the root `Exposure`
|
||
emitted `exposure.scale` — exactly the spec's uniform shape. Binding all three
|
||
by name with `.with(...).bootstrap()` succeeded with no `ParamAlias` and no
|
||
index counting; the run recorded a populated 12-row trace; a disjoint second
|
||
run was bit-identical (C1).
|
||
- Why working: the headline acceptance (#1/#2/#4) is reachable, correct, and
|
||
unambiguous from rustdoc + ledger alone. The exact pain the 0030 milestone
|
||
fixture documented (`mw_1`'s two hand-counted overlays) is gone.
|
||
- Recommended action: carry-on.
|
||
|
||
### [working] Naming the colliding legs is the single recovering act; the named cross runs
|
||
- Example: c0031_2 (block [2]).
|
||
- What happened: the same un-named topology that is rejected becomes legal the
|
||
moment both SMAs are `.named("fast")`/`.named("slow")` — one act clears both
|
||
the param-path collision and the fan-in check, and the harness runs (12 rows).
|
||
- Why working: confirms the spec's central claim that one author act fixes both
|
||
walls.
|
||
- Recommended action: carry-on.
|
||
|
||
### [working] Default names = verbatim-lowercased type label, root-level qualified, paramless fan-in stays legal
|
||
- Example: c0031_3.
|
||
- What happened: an un-named `Sma` at root emitted `sma.length` (root segment now
|
||
present, as the spec inverts the old bare-root rule); un-named `Exposure` →
|
||
`exposure.scale`; no `sim_broker` snake_case appeared; both default names bound
|
||
and ran. Two un-named **paramless** same-type (`Add`) legs under a fan-in
|
||
compiled cleanly — the spec's "paramless interchangeable stays legal" clause
|
||
holds.
|
||
- Why working: the default-name mechanism and the param-bearing-vs-paramless
|
||
distinction behave exactly as specified.
|
||
- Note (not a defect): `SimBroker` exposes **no** tunable knob (its spread is a
|
||
constructor value), so the `"simbroker"` vs `"sim_broker"` verbatim-lowercase
|
||
rule could not be exercised on an actually-emitted knob; `sma`/`exposure`
|
||
confirm the no-snake-case rule on real knobs.
|
||
- Recommended action: carry-on.
|
||
|
||
### [spec_gap] The headline `IndistinguishableFanIn` diagnostic is unreachable from the canonical by-name authoring flow
|
||
- Example: c0031_2 (blocks [0], [1], [1a], [1b]).
|
||
- What happened: the spec headlines `IndistinguishableFanIn { node }` as the
|
||
forcing-function diagnostic that tells the author "name the colliding legs"
|
||
(acceptance #3, ledger C9 amendment). But a downstream author using the
|
||
**canonical single-run flow** (`.with(...).bootstrap()`, the spec-0030 surface
|
||
this cycle builds on) never sees it:
|
||
- guessing the old collision name `sma_cross.length` →
|
||
`UnknownKnob("sma_cross.length")`;
|
||
- binding the name `param_space()` **actually** emits — `sma_cross.sma.length`
|
||
— → `AmbiguousKnob("sma_cross.sma.length")`;
|
||
- `IndistinguishableFanIn { node: 2 }` only appears via the **positional**
|
||
`compile_with_params(&[...])`, which a by-name author has no reason to call.
|
||
- Why spec_gap: acceptance #3 says "an unnamed same-type param-bearing fan-in is
|
||
rejected with `IndistinguishableFanIn`, and the rejection names the node;
|
||
naming the legs resolves it." It IS rejected — correctly — but through
|
||
`.bootstrap()` the rejection is a **binder** error (`UnknownKnob` /
|
||
`AmbiguousKnob`) that points at a knob *name*, not at "give your nodes distinct
|
||
names." The author whose mental model is "I bound a name that doesn't resolve"
|
||
is led to fiddle with the bind string, not to `.named()` the legs — the exact
|
||
recovery the cycle is built to teach. Two readings are equally plausible from
|
||
the surface: (i) "`IndistinguishableFanIn` is the contract; the binder errors
|
||
are an incidental earlier wall" vs (ii) "the by-name flow should surface the
|
||
fan-in/naming guidance too." I asserted neither in the fixture — I recorded
|
||
both walls verbatim.
|
||
- Recommended action: ratify-or-tighten. Either (a) document on
|
||
`param_space()` / `Composite::with` that a duplicate-named (default-collision)
|
||
knob is the *symptom* and node-naming is the *cure* — i.e. make `AmbiguousKnob`
|
||
/ `UnknownKnob` text (or rustdoc) point at `.named()`; or (b) have the
|
||
by-name `.bootstrap()` path surface `IndistinguishableFanIn` (or fold its
|
||
guidance into the binder error) so the canonical flow reaches the diagnostic
|
||
the spec promises. No engine behaviour is wrong; the guidance is mis-routed.
|
||
|
||
### [friction] `param_space()` advertises a literal duplicate knob name for an un-named same-type fan-in
|
||
- Example: c0031_2 (block [0]).
|
||
- What happened: the un-named cross's `param_space()` emits
|
||
`sma_cross.sma.length` **twice** — two `ParamSpec` entries with byte-identical
|
||
names, neither individually bindable (`AmbiguousKnob`). A value-empty blueprint
|
||
(C11) thus publicly advertises two knobs the consumer cannot address.
|
||
- Why friction: the task (bind the cross) still completes once the author names
|
||
the legs, but the public `param_space()` projection contains an un-actionable
|
||
duplicate — a consumer enumerating it (e.g. to build a sweep grid or a UI) sees
|
||
a degenerate entry with no surface signal that the cure is `.named()`. The old
|
||
pre-0031 collision was `sma_cross.length` (×2); 0031 changes the string but not
|
||
the duplicate-ness in the default case.
|
||
- Recommended action: plan (tidy) — consider whether `param_space()` should
|
||
de-dup, error, or annotate a collision-named entry, so the flat projection is
|
||
never silently degenerate. At minimum a rustdoc note on `param_space()` that a
|
||
duplicate entry signals an un-named same-type sibling.
|
||
|
||
### [friction] `FlatGraph` / `Harness` lack `Debug`, so a `compile`/`bootstrap` `Result` cannot be `{:?}`-printed
|
||
- Example: c0031_2, c0031_3 (compile-time, worked around).
|
||
- What happened: `compile_with_params` returns `Result<FlatGraph, CompileError>`
|
||
and `bootstrap` returns `Result<Harness, CompileError>`; printing either with
|
||
`{:?}` fails to compile (`FlatGraph`/`Harness` don't implement `Debug`). A
|
||
consumer probing diagnostics must `.err()`/`.is_ok()` the Result rather than
|
||
print it whole.
|
||
- Why friction: not new to 0031 and not wrong, but it surfaced immediately when
|
||
field-testing the very errors this cycle re-keys — the natural first reflex
|
||
(`println!("{:?}", result)`) does not compile, forcing a `.err()` dance.
|
||
- Recommended action: carry-on (or plan, low priority) — deriving `Debug` on the
|
||
success types would let consumers print a compile/bootstrap result directly;
|
||
orthogonal to 0031's contract.
|
||
|
||
## Recommendation summary
|
||
|
||
| Finding | Class | Action |
|
||
|---|---|---|
|
||
| Named single-run path uniform `<node>.<param>`, bind, run, deterministic | working | carry-on |
|
||
| Naming the colliding legs is the single recovering act | working | carry-on |
|
||
| Default names verbatim-lowercase, root-qualified, paramless legal | working | carry-on |
|
||
| `IndistinguishableFanIn` unreachable from the canonical by-name flow | spec_gap | ratify / tighten (route the naming guidance into the binder error or rustdoc) |
|
||
| `param_space()` advertises a literal duplicate knob in the default case | friction | plan (tidy) |
|
||
| `FlatGraph`/`Harness` not `Debug` → can't print a compile/bootstrap Result | friction | carry-on / plan (low) |
|
||
|
||
## Verdict
|
||
|
||
Cycle 0031 delivers its core promise cleanly — `.named()`, uniform
|
||
`<node>.<param>` paths, and the one-act fan-in/param-path fix all work first try
|
||
from the public surface; the only real gap is that the spec's headline
|
||
`IndistinguishableFanIn` diagnostic never reaches an author using the canonical
|
||
`.with(...).bootstrap()` flow (they hit `UnknownKnob`/`AmbiguousKnob` instead),
|
||
so the cure (`.named()`) is under-signposted exactly where the cycle most wants
|
||
to teach it.
|