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

12 KiB
Raw Blame History

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).
  • A single un-named Sma at root → sma.length; un-named Exposureexposure.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.
  • 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 Exposureexposure.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.lengthUnknownKnob("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.