Files
Aura/docs/specs/fieldtest-0031-node-naming.md
T
Brummel 11dfff860c fieldtest: cycle 0031 node-naming — naming cure under-signposted on the by-name flow
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.
2026-06-11 12:18:29 +02:00

195 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.