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.
12 KiB
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, inspectsparam_space(), bindssma_cross.fast.length/sma_cross.slow.length/exposure.scaleby name (noParamAlias, 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-countedParamAliasoverlays — 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 viacompile_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
Smaat root →sma.length; un-namedExposure→exposure.scale; checks nosim_broker-style snake_case leaks; binds the default names and runs; then a two-Addparamless 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.scaleemitted 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")madeparam_space()emitsma_cross.fast.length/sma_cross.slow.length, and the rootExposureemittedexposure.scale— exactly the spec's uniform shape. Binding all three by name with.with(...).bootstrap()succeeded with noParamAliasand 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
Smaat root emittedsma.length(root segment now present, as the spec inverts the old bare-root rule); un-namedExposure→exposure.scale; nosim_brokersnake_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):
SimBrokerexposes 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/exposureconfirm 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 positionalcompile_with_params(&[...]), which a by-name author has no reason to call.
- guessing the old collision name
- 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) "IndistinguishableFanInis 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::withthat a duplicate-named (default-collision) knob is the symptom and node-naming is the cure — i.e. makeAmbiguousKnob/UnknownKnobtext (or rustdoc) point at.named(); or (b) have the by-name.bootstrap()path surfaceIndistinguishableFanIn(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()emitssma_cross.sma.lengthtwice — twoParamSpecentries 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 wassma_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 onparam_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_paramsreturnsResult<FlatGraph, CompileError>andbootstrapreturnsResult<Harness, CompileError>; printing either with{:?}fails to compile (FlatGraph/Harnessdon't implementDebug). 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
Debugon 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.