977 Commits

Author SHA1 Message Date
Brummel 227d004c9d feat(aura-engine): reject unwired or double-wired input ports
A graph that leaves an interior input slot unconnected, or wires one slot from more
than one producer, is now a compile-time error instead of a silently-wrong run.
Until now an unwired interior slot was accepted and bootstrapped to an empty column
(harness.rs:137-148), so a forgotten connection ran a deterministic but wrong
backtest; two producers into one slot — ill-formed, since a slot holds one column —
was likewise uncompiled-against.

A single name-free check, `check_ports_connected`, is added to `validate_wiring`
after the existing index/kind checks and before the nested-composite recursion. It
counts coverage of each (interior-node, slot) uniformly across interior edges and
role targets, and rejects zero coverage (new `CompileError::UnconnectedPort`) or >1
(new `CompileError::DoubleWiredPort`). Because `validate_wiring` is called once from
`compile_with_params` and recurses, both the raw `Composite::new` path and the
`GraphBuilder::build()` path inherit it at every nesting level with no builder-side
code (mirrors `check_param_namespace_injective`, the param-side sibling). A
composite's own open input roles (source: None) are coverage providers — the
wired-by-enclosing boundary, the root already guarded by UnboundRootRole — not
consumers. Index-based and name-free: nothing reaches the compilat, so C23 holds.

Supporting fix: the check computes `signature()` on every interior node before the
recursion validates that node's interior, so `derive_signature` and
`interior_slot_kind` are made bounds-total (a placeholder kind for an out-of-range
OutField/target, never a panic; the real fault is still reported by validate_wiring's
guarded OutputPortOutOfRange/BadInteriorIndex). This latent panic was exposed by the
new check, not introduced by it.

Test maintenance (blast radius enumerated empirically, not hand-traced): four
existing tests relied incidentally on under-wiring being accepted — three negative
tests get a covering root role so their intended deep fault still surfaces via the
recursion, and one param-order nesting test is fully wired (param order unchanged).
Six new tests: five raw-surface (unwired, double-wire via edge+role and edge+edge,
nested, open-role boundary) and one builder-surface forgotten-leg (proving the
GraphBuilder inherits the check).

Narrowed scope of #65: the original "promote names to load-bearing wiring keys /
close the exposure-price swap structurally" goal was dropped — #64 already moved
authoring to handles + visible port names, so the residual same-kind swap is a
valid-but-wrong name choice no structural check catches, and a structural fix would
push domain semantics into the domain-free engine (C10/C7). This ships the name-free
half that stands on its own: wiring completeness, which catches forgotten and doubled
connections.

Verified: cargo test --workspace 231 passed / 0 failed; cargo clippy --workspace
--all-targets -- -D warnings clean.

closes #65
2026-06-14 16:39:40 +02:00
Brummel 277f8714d4 plan: 0040 add derive_signature bounds-guard step
Implement-time discovery: the new check (and the existing edge/role checks) compute
signature() on every interior node before the recursion validates that node's
interior. For a structurally-invalid composite (an out-of-range OutField or role
target), derive_signature indexed unguarded and PANICKED — exposed by
output_port_out_of_range_rejected the moment its covering root role forces
c.signature(). Added Task 1 Step 2b: make derive_signature and interior_slot_kind
bounds-total (a placeholder kind for an invalid index; the real fault is still
reported by validate_wiring's guarded OutputPortOutOfRange / BadInteriorIndex). Fix
verified empirically before re-dispatch. Not a design change to the check itself.

refs #65
2026-06-14 16:33:00 +02:00
Brummel 669541c6f6 plan: 0040 wiring-totality-check
Seven-task plan for the wiring-totality check (parent spec
docs/specs/0040-wiring-totality-check.md, 07b1ae1 + blast-radius correction
69b16f7): add the two CompileError variants + check_ports_connected + its
validate_wiring call site (T1); re-wire the three under-wired negative tests with a
covering root role (T2); fully wire the param-order nesting test (T3); add five
raw-surface tests — unwired, double-wire via edge+role and edge+edge, nested,
open-role boundary (T4); add the builder-surface forgotten-leg test (T5); land the
C8 ledger realization note (T6); full-suite + clippy gate (T7).

The blast radius (T1's expected-4-failures gate, T2/T3's fixes) was enumerated
empirically by a throwaway probe, not a hand-trace, after the hand-trace under-counted
it by one (param_space_mirrors_..._under_nesting).

refs #65
2026-06-14 16:21:48 +02:00
Brummel 69b16f7d39 spec: 0040 correct blast radius to four (empirically probed)
The boss-signed 0040 spec's blast-radius section claimed "exactly three" flipping
tests, named from a hand-trace. A throwaway probe (the check implemented, full
workspace suite run, then discarded) found FOUR: the three named negative tests plus
`param_space_mirrors_compiled_flat_node_param_order_under_nesting`, a compile-success
param-order test that today compiles a deliberately under-wired nested graph (LinComb's
two term inputs never wired) just to read its flat param order. The new check rejects
it where it currently returns Ok.

Correction is editorial, not a design change: the check itself is unchanged. The
fourth test's fix differs from the three negative tests — it needs the contrived graph
fully wired (fan fast_slow's output into both LinComb terms + a covering root role),
not just a covering role — so the §Blast radius and §Acceptance criteria now enumerate
all four with their distinct fixes. Verified empirically (119 passed, 4 failed; every
other crate green), the strongest possible grounding for the count.

refs #65
2026-06-14 16:14:52 +02:00
Brummel 07b1ae1c63 spec: 0040 wiring-totality-check (boss-signed)
Reject graphs with an unwired or double-wired interior input port. A new
name-free structural check `check_ports_connected` in `validate_wiring`
(crates/aura-engine/src/blueprint.rs) requires every interior node's every
declared input slot to be covered by exactly one wiring act — one `Edge{to,slot}`
or one `Role` `Target{node,slot}`, counted uniformly. Zero coverage is the new
`CompileError::UnconnectedPort` (a forgotten connection that today bootstraps a
silent empty column, harness.rs:137-148); more than one is `DoubleWiredPort` (a
slot holds one column). Run once at the existing compile boundary and recursive
per nesting level, so both the raw `Composite::new` and the `GraphBuilder::build`
surfaces inherit it. Index-based, name-free — the compilat is untouched (C23).

Narrowed scope of #65: the original "promote names to load-bearing wiring keys /
close the exposure-price swap structurally" goal is dropped (see the reconciliation
comment on the issue) — #64 already moved authoring to handles + visible port
names, so the residual same-kind swap is a valid-but-wrong name choice no
structural check catches, and a structural fix would push domain semantics into
the domain-free engine (C10/C7). This cycle ships only the name-free half that
stands on its own.

Auto-signed under /boss: precondition gate clean (no fork silently picked),
self-review + parse-trace clean (no spec-validation parser declared -> documented
no-op), grounding-check PASS on the final bytes (one BLOCK on an incomplete
blast-radius survey, repaired forward by naming the three flipping negative tests),
and a unanimous five-lens spec-skeptic panel (criterion, grounding, scope-fork,
ambiguity, plan-readiness all SOUND).

refs #65
2026-06-14 16:02:09 +02:00
Brummel b6174cff80 refactor(aura-engine): rename NodeHandle::in_/out to input/output
The trailing underscore on in_() was a keyword-escape scar (`in` is reserved).
Rename the NodeHandle port/field accessors to input()/output(), which also
aligns the authoring surface with the schema's own nomenclature
(NodeSchema.inputs/output, "input port"/"output field"): node.input("lhs")
now reads in the same vocabulary the schema uses, instead of a second one.

Mechanical, behaviour-preserving: the two method definitions plus every call
site in builder.rs and the aura-cli sample blueprints. Full workspace green;
clippy --all-targets -D warnings clean.
2026-06-14 10:35:52 +02:00
Brummel 50105c1957 refactor(aura-cli): author the sample blueprints via GraphBuilder
Adopt the name-based GraphBuilder (cycle 0039) in the CLI sample blueprints,
which previously hand-wired raw positional Edge/Role/OutField via
Composite::new. sma_cross, macd, signals, sample_blueprint_with_sinks, and
macd_strategy_blueprint now read as typed-handle authoring: nodes are added
for NodeHandles, ports/fields are wired by name (series/lhs/rhs/value,
exposure/price, term[i], col[0], the macd histogram/signal/macd outputs), and
build() lowers to the same index-wired Composite. This is what #64 was for —
the builder was previously exercised only in its own tests; the showcase
sample now dogfoods it.

Behaviour-preserving: node order, instance names (.named), bound params
(blend.weights[2]), and wiring are byte-identical, so param_space, the graph
render, the single run, and the sweep are unchanged — guarded green by the
existing aura-cli sample/sweep/param_space/render tests and the engine suite
(full workspace 0 failed; clippy --all-targets -D warnings clean).

sample_harness is deliberately left as a direct FlatGraph construction (baked
Sma::new(2) nodes, the single-run path) — it builds the compilat directly, not
a value-empty Composite, so it is not a GraphBuilder target.
2026-06-14 03:28:32 +02:00
Brummel 7865030d33 audit: cycle 0039 — drift-clean (GraphBuilder name-based wiring)
Close-audit for cycle 0039 (commit range 3a1dceb..a6a314b). Architect drift
review against the design ledger + CLAUDE.md domain invariants: no contract
drift, no debt. No regression scripts are configured (the architect is the
gate); build + test + clippy verified green independently by the orchestrator
(aura-engine 123 passed, +9 new; full workspace green; clippy --all-targets
-D warnings exit 0).

cycle 0039 (clean):
- C23 / C11 preserved: the compilat is byte-unchanged this cycle. harness.rs
  (Edge / FlatGraph / Target / SourceSpec) is untouched; the new GraphBuilder
  resolves every port/field name in build() (resolve_slot / resolve_field) and
  lowers to the unchanged Composite::new with raw-index Edge / Role / OutField.
  No name-carrying field was added to any compilat type — names resolve at the
  authoring boundary and never reach FlatGraph.
- C10 / C17 preserved: GraphBuilder is a genuine fluent Rust builder over the
  existing structs, not a DSL. Node references are typed Copy handles; only
  port/field names are strings, resolved against declared NodeSchema data.
- C8 / C1 / C2 preserved: resolution is cold authoring-time; the run loop and
  the bootstrap kind-check remain the structural gate (name resolution is
  necessary, not sufficient).
- #21 deferral is coherent: the exposure/price swap is made legible (named
  slots) but deliberately NOT structurally closed; issue #65 carries the
  structural follow-up (promoting port/field names to load-bearing keys), and
  no ledger contract claims the swap is closed.

Awareness (not drift, spec-consistent): feed() indexes self.roles[role.0]
directly, so a RoleHandle minted by a different builder panics rather than
returning BuildError::BadHandle (which covers only NodeHandles, per the signed
spec). RoleHandle out-of-range robustness is unspecified by spec 0039.
2026-06-14 02:29:37 +02:00
Brummel a6a314bb98 feat(aura-engine): name-based blueprint wiring via GraphBuilder
Add an additive, fluent GraphBuilder authoring surface that wires a
blueprint's topology by typed node handles and port/field names instead of
raw positional indices. Node references are Copy NodeHandle values (a node
reference cannot be mistyped); only port/field names are strings, resolved
against each node's cached NodeSchema by exactly-one-match at a single
fallible build() -> Result<Composite, BuildError> — the Binder posture, one
level up. build() lowers to the unchanged Composite::new, so the compilat
stays index-wired (C23): names resolve at the authoring boundary and never
reach FlatGraph. A new From<Composite> for BlueprintNode lets add() accept
nested composites.

Verified: builder-authored sma_cross is structurally equal to the hand-wired
fixture, and the full builder-authored harness lowers to a byte-identical
FlatGraph (equal edges + sources). The five BuildError variants
(Unknown/Ambiguous In/Out, BadHandle) are covered, and the SimBroker
exposure/price legs are now addressable by name (the #21 legibility win).

The structural close of the exposure/price swap (#21) is deliberately out of
scope — this builder makes the swap legible, not impossible — tracked as #65.

Two plan-test corrections applied during implementation (both in-scope, no
behaviour change): the harness parity test uses compile_with_params with a
topology-invariant param vector on both sides (the harness declares three
params, so a no-param compile() would trip ParamArity), and the error asserts
use .err()/Some(...) rather than unwrap_err() (Composite is not Debug).

aura-engine 123 tests green (+9); full workspace green; clippy --all-targets
-D warnings clean. Existing index-form Composite::new sites and tests
untouched (coexistence).

closes #64
2026-06-14 02:27:03 +02:00
Brummel 031081bf62 plan: 0039 graphbuilder-name-based-wiring
Task-by-task plan for the typed-handle GraphBuilder: the From<Composite> lift,
the builder module (types + accumulators + the fallible build() resolver), the
lib.rs wiring, and the test suite (structural parity, harness FlatGraph parity,
the five BuildError variants, #21 legibility, coexistence + clippy gate).

refs #64
2026-06-14 02:16:54 +02:00
Brummel 292c95756f spec: 0039 graphbuilder-name-based-wiring (boss-signed)
Add a typed-handle GraphBuilder authoring surface for blueprint topology:
node references become Copy NodeHandle values, ports/fields resolve by name
against the existing PortSpec.name/FieldSpec.name at a single fallible build()
terminal, lowering to the unchanged index-wired Composite (C23 holds by
construction — names never reach the compilat, mirroring param-name resolution).

Spec auto-signed under /boss: all objective gates green (precondition,
self-review, grounding-check PASS) and a unanimous five-lens spec-skeptic panel
(criterion, grounding, scope-fork, ambiguity, plan-readiness all SOUND).

The structural close of the SimBroker exposure/price swap (#21) is explicitly
out of scope, tracked as #65.

refs #64
2026-06-14 02:07:09 +02:00
Brummel 3a1dceb83a audit: cycle 0038 — drift-clean (shared SMA-cross test fixtures)
Close-audit for cycle 0038 (commit range c939673..1f01bad). Architect drift review
against the design ledger + CLAUDE.md domain invariants: no contract drift, no debt.
No regression scripts are configured (the architect is the gate); build + test +
clippy verified green independently by the orchestrator (aura-engine 114 passed,
count unchanged; the two load-bearing consumers green; clippy --all-targets
-D warnings exit 0).

cycle 0038 (clean):
- C16 preserved: the new shared fixtures live in a #[cfg(test)] mod test_fixtures,
  which compiles into no shipped artifact — the engine still ships no sample.
- C1 / C11 / C12 strengthened, not weakened: the determinism witness
  (composite_sma_cross_runs_bit_identical_to_hand_wired) and the sweep-equivalence
  witness (sweep_equals_n_independent_runs) now consume one shared fixture instead
  of two hand-kept copies, so the harness topology can no longer silently diverge
  between the two test modules — the exact hazard #53 named.
- Test-organisation convention held: a crate-root #[cfg(test)] module of pub(crate)
  free fns, sibling to the existing reexport_tests precedent, declared in lib.rs.
- graph_model.rs's minimal golden copy (sma_cross / sample_root) was correctly left
  independent and out of scope, by design.
- Stale-cross-ref sweep clean: docs/design/INDEX.md and docs/glossary.md name no
  fixture site that the move invalidated; the fixture-name hits in docs/ are
  historical specs/plans pinned to their own cycles, not live contracts.

In-flight plan correction (recorded for history): the plan's literal import list
over-imported sma_cross, which neither consumer calls directly (reached only
transitively via composite_sma_cross_harness); the implementer trimmed it to satisfy
the plan's own clippy -D warnings gate — within the task's import-cleanup scope.
2026-06-14 00:20:23 +02:00
Brummel 1f01badac7 refactor(aura-engine): share the SMA-cross harness test fixtures
Collapse the three verbatim-duplicated #[cfg(test)] fixtures —
synthetic_prices, sma_cross, composite_sma_cross_harness — that lived
in both blueprint.rs's and sweep.rs's test modules into one shared
crate-root `#[cfg(test)] mod test_fixtures` (pub(crate) free fns). The
two consumers now import from `crate::test_fixtures`; sweep.rs's
test-mod `use` block sheds the imports that were only needed by the
moved fns.

Why: the topology had to move in lockstep across the two copies but no
test pinned them equal, so an edit to one silently diverged the other
(#53). One definition removes the hazard structurally — better than a
pin-them-equal test, which loses its premise once there is a single
definition. Test-only; no production code, public API, or runtime path
changes. C16 untouched (a #[cfg(test)] module ships in no artifact).

The CLI sample is no longer a third peer (cycle 0036 enriched it into a
trend+momentum blend), so this is an engine-internal two-copy dedup, not
the three-way the issue originally named. graph_model.rs keeps its own
intentionally-minimal golden copy, out of scope.

Plan glitch fixed in-flight: the plan's literal import list named
sma_cross, but neither consumer calls it directly (only transitively via
composite_sma_cross_harness), so importing it tripped the plan's own
Step-7 clippy -D warnings gate. Trimmed to {composite_sma_cross_harness,
synthetic_prices} — within the task's "drop now-unused imports" scope.

Verified: cargo test --workspace green (aura-engine 114 passed, count
unchanged — behaviour-preserving); the two load-bearing fixture
consumers composite_sma_cross_runs_bit_identical_to_hand_wired (C1) and
sweep_equals_n_independent_runs (C11/C12) both green; cargo clippy
--workspace --all-targets -- -D warnings clean.

closes #53
2026-06-14 00:18:04 +02:00
Brummel 1718ba0f98 plan: 0038 dedup-sma-cross-test-fixtures 2026-06-14 00:11:18 +02:00
Brummel 47179ce5a4 spec: 0038 dedup-sma-cross-test-fixtures (boss-signed)
Collapse the three verbatim-duplicated SMA-cross harness test fixtures
(synthetic_prices, sma_cross, composite_sma_cross_harness) in aura-engine's
blueprint.rs and sweep.rs #[cfg(test)] modules into one shared crate-root
#[cfg(test)] mod test_fixtures. Behaviour-preserving, test-only.

Auto-signed under /boss: all objective gates green (precondition, self-review,
grounding-check PASS) and a unanimous five-lens spec-skeptic panel returned
SOUND.

refs #53
2026-06-14 00:04:55 +02:00
Brummel c939673aa7 audit: cycle 0037 — drift-clean (bound params in the graph model + viewer)
Close-audit for cycle 0037 (commit range d069484..47e0e60). Architect drift review
against the design ledger + CLAUDE.md domain invariants: no contract drift, no debt.
No regression scripts are configured (the architect is the gate); build + test +
clippy verified green independently by the orchestrator.

cycle 0037 (clean):
- C23 preserved: bind still resolves the slot name to a fixed position and re-splices
  the value in the build closure; BoundParam/"bound" is recorded purely beside that
  capture, a render/debug symbol dropped at lowering — bootstrap and the run loop
  never read bound_params() (only the model serializer does). The compilat stays
  index-wired. A genuine twin of the cycle-0035 instance-name thread.
- C14 preserved: the inline no-bind model_golden (sample_root) is byte-unchanged;
  the "bound" field is conditional (mirrors "name"), so unbound nodes are
  byte-identical, and only the sample fixture's blend node gained the field.
  scalar_str is total over all four Scalar variants and deterministic (f64 via {:?}
  keeps a decimal point so a bound f64 never reads as an i64).
- C12/C19 preserved: the tunable surface is untouched — bind still removes the slot
  from schema.params; the eight-param sweep pins stay byte-identical with weights[2]
  absent. The render annotation reads from a separate bound vec, not param_space().

Test coverage: the original-position unit test (trailing + reverse-chained), the
prim_record conditional-field unit test (i64 + f64), and a headless render guard
pinning slot-order faithfulness for both a trailing and a middle bind (the middle
case catches an append-only regression).

Resolution: carry-on. Next iteration: pick the next backlog item.
2026-06-13 23:25:48 +02:00
Brummel 47e0e605e1 feat(aura): surface bind-bound params in the graph model + viewer signature
A node built with `.bind(slot, value)` fixes a declared param to a structural
constant: the slot correctly leaves the tunable surface (param_space / sweep axes),
but it also vanished from the RENDERED signature, because prim_record emitted only
schema.params. The cycle-0036 `blend` (a LinComb(3) with weights[2] bound to 0.5)
rendered `LinComb[weights[0], weights[1]]` — a 2-arity signature over a 3-port box,
with the fixed 0.5 invisible. The signature misrepresented the node.

bind now ANNOTATES instead of erasing. All slots are shown; the bound one renders
`name=value`, dimmed. The blend renders:
    blend: LinComb[weights[0], weights[1], weights[2]=0.5]

Structurally a twin of cycle 0035's instance-name thread (commit 0ae8320): a
conditional field threaded engine -> model -> viewer.
- aura-core: a `BoundParam { pos, name, kind, value }` recorded by bind alongside
  the unchanged closure capture, plus a `bound_params()` accessor (twin of
  instance_name()). `pos` is the slot's ORIGINAL pre-bind position (the compressed
  index is mapped back over earlier-bound positions), so the render is faithful for
  any bind pattern, not just a trailing one.
- aura-engine: a conditional `"bound"` field in prim_record (present only when the
  node has binds, mirroring the conditional "name"), each entry [pos,"name","kind",
  "value"] via a new scalar_str helper (f64 via {:?} — keeps a decimal point so a
  bound f64 never reads as an i64).
- graph-viewer.js: adaptNodes/genDot carry `bound`; cellLabel merges free + bound
  params back into slot order by position and renders the bound slot dimmed
  (reusing the 0035 separator grey #9399b2).

Render notation settled with the user (issue #63, reconciliation comment): keep the
[...] convention and name=value (not parens, not positional value-only).

Render/debug surface only: dropped at lowering (C23 — bind still resolves the name
to a fixed position and the compilat is unchanged), model stays deterministic (C14
— the inline no-bind golden is byte-unchanged; only the sample fixture's blend node
gains "bound"). The tunable surface is untouched: the eight-param sweep pins stay
byte-identical (C12/C19). New unit tests pin the recorded original position and the
conditional model field; a new headless render guard pins the dimmed name=value
render and slot-order faithfulness for both a trailing and a non-trailing bind.

Verified: cargo build/test (0 failed) + clippy -D warnings (0) all green.

closes #63
2026-06-13 23:23:29 +02:00
Brummel 4227073fce plan: 0037 bound-param-in-graph-model
Six bite-sized tasks for spec 0037: aura-core records a BoundParam (original slot
position) in bind + a bound_params() accessor; aura-engine emits a conditional
"bound" field via scalar_str; graph-viewer.js merges free+bound params into slot
order and renders the bound slot dimmed as name=value; re-capture the sample-model
fixture; a new headless render guard (trailing + middle bind); full-workspace gate.

refs #63
2026-06-13 23:14:49 +02:00
Brummel 42850c6f11 spec: 0037 bound-param-in-graph-model (boss-signed)
Surface a bind-bound param in the graph model and the `aura graph` viewer
signature instead of dropping it. A node built with `.bind(slot, value)` keeps
the slot off the tunable surface (param_space / sweep axes — unchanged, correct)
but now ANNOTATES it in the render: all slots shown, the bound one as `name=value`,
dimmed. The cycle-0036 `blend` renders `LinComb[weights[0], weights[1], weights[2]=0.5]`.

Structurally a twin of cycle 0035's instance-name thread: a conditional field
(here `"bound"`) threaded engine -> model -> viewer, plus goldens and a headless
render guard. Render/debug surface only — dropped at lowering (C23), model stays
deterministic (C14).

Auto-signed under the /boss spec auto-sign gate: precondition clean (the one open
notation decision was settled with the user in-session and recorded as a
provenance-bearing reconciliation comment on the issue), self-review + parse-trace
clean, grounding-check PASS, and a unanimous five-lens spec-skeptic panel.

refs #63
2026-06-13 23:05:28 +02:00
Brummel d069484559 audit: cycle 0036 — refresh stale sample cross-refs; drift-clean otherwise
Close-audit for cycle 0036 (commit range 560c2d0..94af4c7). Architect drift review
against the design ledger + CLAUDE.md: no contract drift. No regression scripts are
configured (the architect is the gate); build + test + clippy verified green.

cycle 0036 tidy (clean):
- C8/C9/C10 preserved: the enriched sample is multiply-nested composites over shipped
  primitives — multi-output is the macd composite re-exporting columns (not a >1-record
  primitive); brokers stay downstream nodes.
- C12/C19 preserved: the eight sweep axes are a bijection with the injective,
  path-qualified param_space under nesting; the bound blend.weights[2] is correctly
  absent from both the axes and the surface. The re-captured render golden, the two
  in-crate pins, and the cli_run E2E pin all carry the identical eight-tuple in lockstep.
- C14 preserved: the re-captured sample-model.json is the deterministic model of the
  enriched sample; the engine's own minimal model_golden (sample_root) is independent
  and byte-unchanged.
- C23 preserved: bind resolves the param name to a fixed position; the fixed blend
  weight is a structural constant (deform-not-tune), dropped from the tuning surface,
  and the compilat is unchanged.

Two stale cross-references found and refreshed (doc debt, no behaviour change):
- crates/aura-engine/src/graph_model.rs: sample_root's "Mirrors build_sample()
  (...:161-190)" was doubly stale — build_sample moved and the CLI sample is now the
  richer signals graph; re-grounded to state sample_root is a deliberately minimal,
  independent serializer fixture.
- crates/aura-cli/src/main.rs: sample_blueprint_with_sinks' "single source of the
  sample topology" + "SMA lengths + exposure scale" overclaimed post-enrichment;
  re-grounded to the root harness whose signal is the nested signals composite, with
  the eight free params.
2026-06-13 20:27:25 +02:00
Brummel 94af4c788c feat(aura-cli): enrich the graph sample into a trend+momentum blend showcase
The built-in sample that `aura graph` renders and `aura sweep` runs is now a
"trend + momentum, weighted blend" strategy that exercises four authoring/viewer
capabilities the single-level sample left dark:
- multiply-nested composites: root -> signals -> {trend = sma_cross, momentum = macd};
- a multi-param node inside a composite: blend = LinComb(3) (weights[0]/[1] shown);
- a multi-output node: momentum (the macd composite) re-exports macd/signal/histogram,
  two of which the blend consumes;
- bind(): blend.weights[2] is fixed as a structural constant, so it drops out of the
  sweepable param surface (and renders absent).

The new `signals` composite reuses the existing sma_cross/macd builders unchanged; the
sweep re-paths its axes to the eight free params (still a 4-point grid) and runs an
18-tick warm-up stream (showcase_prices) so the MACD EMA-of-EMA chain and the all-Any
LinComb join warm up. The render fixture (sample-model.json) is re-captured, and a new
headless guard (viewer_nested_depth) pins that the viewer renders the nesting to depth 2
with valid Graphviz ids. Pure authoring over already-shipped primitives — no engine or
graph-viewer.js change.

The flat run_sample, run_macd/macd(), the inline model_golden test, and the viewer are
untouched. A third sweep-param pin in tests/cli_run.rs (the aura sweep E2E) was re-pathed
in lockstep with its in-crate twin.

closes #62
2026-06-13 20:22:59 +02:00
Brummel 30c1ad5e2e plan: 0036 sample-showcase-blueprint
Five tasks for the sample-showcase cycle: (1) the enriched blueprint source —
LinComb import, the `signals` composite (trend = sma_cross, momentum = macd, blend
= LinComb(3) with weights[2] bound), the node-0 swap in sample_blueprint_with_sinks,
the re-pathed sweep_family axes + a showcase_prices warm-up stream; (2) re-path the
two in-crate tests (bootstrap-runs-drains, sweep odometer); (3) re-capture the
sample-model.json render fixture; (4) a new headless depth-2 nesting guard
(viewer_nested_depth.mjs + .rs); (5) the workspace build/test/clippy gate.

refs #62
2026-06-13 20:12:10 +02:00
Brummel e7393f6809 spec: 0036 sample-showcase-blueprint (boss-signed)
Enrich the `aura graph` / `aura sweep` sample blueprint into a "trend + momentum,
weighted blend" strategy that showcases four authoring/viewer capabilities the
single-level sample left dark: multiply-nested composites, a multi-param node
inside a composite, a multi-output (MACD-like) node, and bind() (a param fixed as
a structural constant, dropped from the sweep surface). Pure authoring over
already-shipped primitives — no engine semantics or viewer change; reuses the
existing sma_cross/macd builders.

Auto-signed under /boss (spec auto-sign enabled): objective gates green
(precondition clean, self-review clean, grounding-check PASS) and a unanimous
five-lens spec-skeptic panel (criterion, grounding, scope-fork, ambiguity,
plan-readiness) returned SOUND. Design provenance recorded on #62.

refs #62
2026-06-13 19:58:27 +02:00
Brummel 560c2d0824 audit: cycle 0035 — drift-clean (node instance name in the graph model)
Close-audit for cycle 0035 (commit range 20000dd..0ae8320). Architect drift
review against the design ledger + CLAUDE.md: clean. No regression scripts are
configured (the architect is the gate); build + test + clippy verified green.

cycle 0035 tidy (clean):
- C23 preserved: the instance name is resolved to Option at authoring
  (instance_name()), emitted only on the render/model surface, and dropped at
  lowering — the compilat wiring is byte-unchanged.
- C14 preserved: model bytes stay deterministic; both golden twins (inline
  model_golden + sample-model.json) moved in lockstep to the same two "name"
  additions.
- C12/C19 untouched: the cycle adds no param-space surface; the param-ganging
  alternative was deflected to idea #61 (not via name collision — param_space
  is injective by design).
- Engine<->viewer lockstep intact: the conditional "name" producer (prim_record)
  and its three consumers (adaptNodes, genDot leaf-emit, cellLabel) moved
  together, protected by a prim_record unit test + a headless render guard.

The blueprint-viewer `SMA[length]` (a051571) and the compiled `Node::label`
`SMA(2)` are legitimately distinct surfaces (name-bearing blueprint model vs
value-bearing compiled view) — the split the ledger already draws at
INDEX.md:681-684, not drift.
2026-06-13 19:08:54 +02:00
Brummel 0ae8320d14 feat(aura): surface explicit node instance name in the graph viewer
A leaf primitive built with `.named("fast")` now renders its `aura graph` viewer
box head as `fast: SMA[length]` — the instance name as a `:` declaration prefix.
An unnamed leaf keeps the bare `SMA[length]`.

Engine half: a new raw `PrimitiveBuilder::instance_name() -> Option<&str>` (the
explicit name, default not resolved) feeds a conditional leading `"name"` field
in prim_record, present only for an explicitly-named node; both golden twins
(inline model_golden + sample-model.json) re-captured. Viewer half: adaptNodes
carries `name`, the genDot leaf-emit forwards it, and cellLabel prepends the
`name: ` prefix when present (composites stay bare). `named()`'s non-empty
debug_assert is unchanged, its doc re-grounded to the now load-bearing Some/None
invariant (knob-address segment + prefix switch).

The name is a render/model-only debug symbol — dropped at lowering (C23), and the
model stays deterministic (C14). Parameter ganging (one knob, several nodes) was
considered and spun off as an explicit composite-shared-param idea (#61), not via
name collision (param_space is injective, C12/C19).

closes #58
2026-06-13 19:02:16 +02:00
Brummel 77ddbeb237 plan: 0035 node-name-in-graph-model
Six tasks for the node-name-in-graph-model cycle: (1) aura-core instance_name()
accessor + re-grounded named() doc; (2) aura-engine prim_record conditional
"name" field + unit test; (3) re-capture both golden twins (inline model_golden
+ sample-model.json); (4) viewer render guard (RED, headless node guard);
(5) viewer adaptNodes/genDot/cellLabel name prefix (GREEN); (6) workspace
build/test/clippy gate.

refs #58
2026-06-13 18:53:53 +02:00
Brummel a421b5a8e1 spec: 0035 node-name-in-graph-model (boss-signed)
Surface a node's explicit instance name in the `aura graph` viewer: a named leaf
renders its box head as `fast: SMA[length]` (the instance name as a `:`
declaration prefix), an unnamed leaf keeps the bare `SMA[length]`. The name
crosses two surfaces — a conditional `"name"` field in the JSON graph model
(engine half) and a `cellLabel` prefix in graph-viewer.js (viewer half).
Explicit-`.named()`-only; `node_name()`'s lowercased-type default is not
surfaced. `named()`'s non-empty rule is kept with its code unchanged, its doc
re-grounded to the load-bearing invariant (knob-address segment + Some/None
prefix switch).

Signed via the /boss auto-sign gate: all objective gates green (precondition,
self-review, grounding-check PASS) and a unanimous five-lens spec-skeptic panel
(criterion, grounding, scope-fork, ambiguity, plan-readiness). Two earlier blocks
were resolved before the unanimous round — a separator-ambiguity by an editorial
fix, and a scope-fork (which nodes carry the prefix) by an explicit user decision
recorded as a provenance-bearing reconciliation comment on the issue.

refs #58
2026-06-13 18:38:47 +02:00
Brummel a051571d79 feat(aura-cli): render leaf param signature as TYPE[...] in the graph viewer
The viewer box head built the param signature in round parens — SMA(length) —
which reads as a function call. Switch the brackets to square — SMA[length] — so
the head reads as parametrisation, not invocation. cellLabel render-only change
in graph-viewer.js; the JSON model surface and both goldens are untouched.
2026-06-13 17:03:28 +02:00
Brummel 3c698d4200 chore: move skills-plugin profile into CLAUDE.md project facts
The skills plugin dropped dev-cycle-profile.yml. Migrate this project's facts into CLAUDE.md under '## Skills plugin: project facts' and remove the profile file.
2026-06-13 16:30:11 +02:00
Brummel 20000ddeb3 audit: cycle 0034 (#55) — record structural-constant bind in the ledger; drift-clean otherwise
Architect drift review over 52e0214..HEAD. All hard invariants hold; the only
drift was a ledger-prose gap — the new .bind() structural-constant affordance had
no contract entry, so the code offered a capability the contracts did not name.

Contracts (all hold):
- C23: bind resolves a param name -> position at authoring time and stores the
  index; the compilat stays wired by raw index, the name never reaches it. The
  by-name authoring address space (0032 amendment) is exactly where bind sits.
- C19/C8: the shrink is schema.params.remove(pos) on the single source of truth
  that params()/schema()/collect_params/lower_items all read, so no desync is
  structurally possible; param_space() injectivity (0032) is untouched (bind only
  removes entries). The param-declared-once posture holds.
- C9/C10: a pure builder-level value op — no by-name runtime node lookup, no
  registry, no DSL-style dynamic resolution.
- C16: zero Cargo.toml / lockfile changes.
- Construction layer (collect_params/lower_items/param_space/compile_with_params)
  byte-unchanged — verified independently; the blueprint.rs diff is a test-only
  hunk at 1886, the sole production edit is bind in aura-core/src/node.rs.

Regression: none configured (profile regression: []) — no-op; architect is the gate.

Resolution -- fix (1 ledger note, added inline this commit):
- docs/design/INDEX.md C19 — added a cycle-0034 realization note recording
  PrimitiveBuilder::bind as the structural-constant path (knob REMOVED from
  param_space), distinct from the cycle-0016 value-pin (knob stays, fixed in the
  injected vector); the #55 deform-vs-tune discriminator; C23 unaffected; export of
  a named frozen strategy deferred to #60.

Cycle 0034 drift-clean after this note. Not a milestone close (no milestone
fieldtest run). Gates: doc-only edit, compile-inert; cargo build/clippy/test
--workspace verified green this session.
2026-06-12 16:30:43 +02:00
Brummel 7d587d0c4e feat(aura-core): PrimitiveBuilder::bind — fix a node param as a structural constant
Add `PrimitiveBuilder::bind(slot, value)`: bind a declared param to a structural
constant, removed from param_space entirely rather than pinned in the injected
vector. This closes the gap #55 names — param-bearing nodes (Sma/Exposure/LinComb)
had no constant path, only the no-sweep-mode SimBroker precedent. The motivating
case is "SMA2-entry": the 2 is bound to the two-candle construction, so sweeping
it would enumerate deformed strategies as valid family members.

Approach (Option B, builder-level — the originally-planned direction): bind shrinks
the builder's declared param surface (schema.params) and wraps its build closure to
re-splice the captured constant at its original positional slot. The construction
layer (collect_params / lower_items / param_space / compile_with_params) is
byte-unchanged: both dock sites already key off builder.params(), so the shrink
propagates for free. Addressing is by param name (the by-name authoring address
space, C23/0032); the compilat stays wired by raw index — names never reach it.

bind enforces "exactly one open param matches" itself (collect-all-then-reject,
panic on zero / on duplicate), mirroring the resolve binder's posture, because a
node's own schema.params is not covered by check_param_namespace_injective. Chained
binds reconstruct the correct positional vector because each layer computes its
slot index relative to the param list it sees (no global position table).

Considered and rejected: a per-node builder_const(N) constructor (explodes
combinatorially over which subset is constant, not dynamic); a ParamBinding overlay
struct on Composite (would need new bookkeeping the builder-level form avoids).

Verified: cargo build/clippy/test --workspace all green; new tests cover slot
removal, positional reconstruction (chained + partial, via eval), the three panic
paths, and the composite param_space projection; blueprint.rs construction layer
confirmed byte-unchanged.

Deferred (out of scope, follow-up issue): exporting a named frozen strategy as a
blueprint-as-values collection — the issue's third fork, needs its own brainstorm.

closes #55
2026-06-12 16:26:28 +02:00
Brummel 3413194809 plan: 0034 blueprint-constant-bind
Task decomposition for PrimitiveBuilder::bind (spec 0034): T1 the method +
aura-core unit tests (Probe node drives positional reconstruction RED→GREEN;
three should_panic), T2 node-vehicle coverage (Sma/LinComb via eval in aura-std),
T3 Composite param_space coverage + construction-layer zero-change guard +
workspace gate. Crate-topology aware: aura-core unit tests use a local Probe,
not Sma/LinComb (no upward dep).

refs #55
2026-06-12 16:20:59 +02:00
Brummel 9e57caebb8 spec: 0034 blueprint-constant-bind (boss-signed)
Add PrimitiveBuilder::bind(slot, value): fix a node param as a structural
constant, removed from param_space entirely (not pinned in the injected
vector). Closes the gap #55 names — param-bearing nodes (Sma/Exposure/LinComb)
had no constant path, only the no-sweep-mode SimBroker precedent.

Settled direction (Option B, builder-level): .bind() shrinks the builder's
declared param surface and wraps its build closure to re-splice the constant;
the construction layer (collect_params/lower_items/param_space) is unchanged
because both already key off builder.params(). Addressing is by-name
(authoring address space, C23/0032); compilat stays index-wired.

Auto-signed under /boss spec_auto_sign: objective gates green, grounding-check
PASS, unanimous five-lens spec-skeptic panel SOUND (one editorial repair round:
corrected the harness param_space assertion and pinned the exactly-one-match
bind contract).

refs #55
2026-06-12 16:12:09 +02:00
Brummel 52e0214adb audit: cycle 0033 (#54) — refresh stale JSON-writer cross-refs after to_json→serde
Architect drift review over 065cf1d..HEAD. Code + contracts hold; the only drift
was documentation cross-references made stale by retiring RunReport's hand-rolled
to_json.

Contracts (all hold):
- C14: to_json still emits canonical, flat, deterministic JSON — now serde-produced.
  model_to_json (the graph-model JSON) stays hand-rolled and was correctly untouched.
- C16: serde_json dev-dep → normal-dep in aura-engine is squarely sanctioned by the
  amended per-case policy (INDEX.md:576-583) — a vetted standard crate used where it
  does the job (incl. the frozen bot), removing a hand-rolled writer. No gap.
- C1: report types have no HashMap; serde is declaration-order/deterministic; the
  r1.to_json()==r2.to_json() asserts and the new to_json_equals_serde_disk_shape pin
  stay green.

Regression: none configured (profile regression: []) — no-op; architect is the gate.

Resolution — fix (3 prose cross-refs, fixed inline this commit):
- graph_model.rs:5 (module doc) — claimed the model JSON is in the "RunReport::to_json
  house style (no serde)"; to_json is serde now. Reframed: the model JSON is hand-rolled
  (no serde), and is the engine's last hand-rolled JSON writer since 0033.
- graph_model.rs:30 (json_str doc) — cross-referenced RunReport::json_str, deleted this
  cycle. Reworded to describe the escape set directly (graph_model's own json_str stays).
- docs/design/INDEX.md:729 — "golden-tested like RunReport::to_json" framed the hand-rolled
  model JSON by a now-serde to_json. Reframed to name model_to_json as the surviving
  hand-rolled writer.

Cycle 0033 drift-clean after this tidy. Not a milestone close (the param-sweep milestone
fieldtest is a separate deliberate act). Gates: clippy --workspace --all-targets -D
warnings clean (doc-only edits, compile-inert).

refs #54
2026-06-11 19:46:34 +02:00
Brummel f1d0bf00ec fix(aura-engine): unify RunReport stdout JSON with the on-disk serde shape
RunReport had two divergent JSON encoders: the registry serialized to disk via
serde_json (params as an array-of-pairs, floats as 2.0), while stdout rendered
through a hand-rolled to_json (params as an object, whole floats as 2). The same
record was a structurally different document on disk than on the wire, and no
test pinned their relationship — most visibly, `aura runs list` read a record
from disk via serde and reprinted it in a different shape.

Retire the hand-rolled writer: RunReport::to_json now delegates to
serde_json::to_string(self), so stdout is byte-identical to the runs.jsonl line
for the same record. The hand-rolled json_str helper is deleted (the
graph_model.rs json_str is a separate symbol, untouched — the graph-model JSON
is out of scope, #58/#28). A new pin test, to_json_equals_serde_disk_shape,
asserts to_json() == serde_json::to_string(&report) so the two encoders can
never silently diverge again.

Observable change: stdout `params` is now an array-of-pairs and finite f64
fields carry a fractional part (2 -> 2.0); the nested envelope, field order,
window [from,to], and integer-valued seed/exposure_sign_flips are unchanged. The
on-disk shape does NOT change — only stdout moves to match disk.

serde_json is promoted from a dev-dependency to a normal dependency of
aura-engine (to_json is non-test code). Under the amended C16 per-case policy
this passes: serde_json is a vetted standard crate already in the workspace
(registry, ingest), deterministic (C1-safe, already relied on for the disk
path), and is exactly "the vetted standard crate doing what the code would
otherwise hand-roll" — it removes the last hand-rolled RunReport JSON writer.

Goldens flipped to the serde shape: the engine canonical-form golden, the
aura-cli sweep golden (cli_run.rs), and the aura-cli odometer-order sweep golden
(main.rs) — the last was not in the plan's inventory; the cargo test --workspace
gate surfaced it and it took the identical transformation. Structural CLI asserts
and the r1.to_json()==r2.to_json() determinism asserts are shape-agnostic and
stay green.

Spec docs/specs/0033, plan docs/plans/0033, boss-signed (unanimous 5-lens panel).
Two stale prose cross-refs (graph_model.rs:30, docs/design/INDEX.md:729) deferred
to cycle-close audit. Gates: cargo build/test --workspace green, clippy
--all-targets -D warnings clean.

closes #54
2026-06-11 19:43:36 +02:00
Brummel e2056b6436 plan: 0033 unify-runreport-json-serde
Two-task RED→GREEN plan for spec 0033. Task 1 (RED, test-only): flip the engine
canonical-form golden + the CLI sweep golden to the serde array-of-pairs/.0-float
shape, add the to_json_equals_serde_disk_shape pin test — all fail against the
current hand-rolled to_json. Task 2 (GREEN, atomic): promote serde_json dev-dep ->
normal dep, swap to_json's body to serde_json::to_string, delete the now-unused
json_str helper, refresh the method + module-header doc comments — one build+clippy
gate flips every Task-1 assertion green. graph_model.rs:30 + INDEX.md:729 stale
cross-refs deferred to cycle-close audit.

refs #54
2026-06-11 19:38:38 +02:00
Brummel c8634d7fa9 spec: 0033 unify-runreport-json-serde (boss-signed)
Settled-source direct entry from issue #54 (no brainstorm). Retire the
hand-rolled RunReport::to_json writer and render stdout through serde, so a
record's stdout JSON is byte-identical to its runs.jsonl line, and pin that
identity with a test. Removes the last hand-rolled RunReport JSON writer
(amended C16). Disk shape unchanged; graph-model JSON (model_to_json) out of
scope (#58/#28).

Auto-signed under /boss spec_auto_sign: objective gates green (precondition
clean, self-review clean, grounding-check PASS) + unanimous 5-lens spec-skeptic
panel (criterion/grounding/scope-fork/ambiguity/plan-readiness all SOUND). The
grounding juror re-ran serde_json 1.0.150 against the canonical-form fixture and
confirmed the spec's "After" bytes. The one C16-scrutinized consequence
(serde_json dev-dep -> normal dep in aura-engine) is named and justified in the
spec; scope-fork juror confirmed it is settled by amended C16, not a silent pick.

refs #54
2026-06-11 19:33:33 +02:00
Brummel 065cf1df1e audit: cycle 0032 (#59) — drift-clean; param_space() injectivity is a compile invariant
Architect drift review over 11dfff8..HEAD: clean, no drift, no debt.

- C9/C12/C19/C23 ledger prose matches the landed code exactly. The C9
  refinement names the retired fan-in machinery (signature_of, leaf_has_param,
  check_fan_in_distinguishability, check_composite_fan_in) as retired, names the
  two intentionally-dropped render-identity cases (asymmetric param/paramless,
  role-vs-leg) as deliberate drops, and points at DuplicateParamPath/.named().
- C23 honoured: the check reads only the param_space() boundary name projection;
  the compilat stays index-wired, no name became load-bearing.
- C1 preserved: param_space() stays infallible; the gate is pure pre-build
  structural validation; injective blueprints compile bit-identically.
- Drift hazards all clear: no live reference to the retired machinery (only
  frozen historical specs/plans/fieldtests + the cycle's own test-doc comments);
  the stale "CLI render" signature_of doc line was removed with the function;
  both resolve/resolve_axes callers check injectivity before resolving so the
  unreachable! arms are genuinely dead; no live contract references the removed
  IndistinguishableFanIn / AmbiguousKnob variants; engine core untouched.

Regression: none configured (profile regression: []) — architect is the gate.
Resolution: carry-on (drift-clean). Gates re-run by the orchestrator: cargo
build/test --workspace green (0 failed), clippy --all-targets -D warnings clean.

Drift-clean is not a milestone close (the milestone fieldtest gate is separate).
2026-06-11 18:33:16 +02:00
Brummel 1b7e4ad169 feat(aura-engine): param_space() injectivity as a compile invariant
A non-injective param_space() — two slots under one path-qualified name — is
the by-name knob address space (C12/C19) being broken: no binding can select
one slot without the other. This cycle makes it a structural compile error.

One check, check_param_namespace_injective over the param_space() names,
replaces the whole fan-in machinery (signature_of, leaf_has_param,
check_fan_in_distinguishability, check_composite_fan_in) and runs from
compile_with_params AND from both binders before resolve/resolve_axes — so the
canonical by-name author sees the structural cure (CompileError::DuplicateParamPath,
carrying the path and pointing at .named(...)) instead of the AmbiguousKnob symptom
(#59). With an injective space guaranteed before resolve, no bound name can
multi-match, so BindError::AmbiguousKnob is dead and removed (both resolver arms ->
unreachable!). param_space() stays infallible; the invariant lives in the compile
step (C23).

The check is strictly the by-name-addressability property. The old fan-in predicate
(equal signature AND >=1 param) also rejected two collisions that are NOT
param_space duplicates — an asymmetric param/paramless collision and a role-vs-leg
collision — which guarded render identity, dead since the renderer was retired in
0026. Both are intentionally dropped; a future node-/wiring-name check, if wanted,
is decoupled from injectivity.

The new invariant correctly rejected a pre-existing fixture (multi_output_composite's
two unnamed Sma legs); cured in place with the .named(fast/slow) the invariant
mandates. A new E2E test drives the cured cross through the by-name binder
end-to-end (resolve + bootstrap + run to a populated exposure trace).

C9/C12/C19/C23 ledger notes amended. Verified: cargo build/test --workspace green
(0 failed), clippy --workspace --all-targets -D warnings clean.

closes #59
2026-06-11 18:30:26 +02:00
Brummel 770cf7177d plan: 0032 param-namespace-injectivity
Three tasks for the single-iteration cycle:
- Task 1 (RED): add CompileError::DuplicateParamPath + the three new tests
  (non-fan-in duplicate, asymmetric nested collision, by-name #59 regression),
  verified failing against current code.
- Task 2 (GREEN): add check_param_namespace_injective, wire it into
  compile_with_params + both binders (before resolve), remove the fan-in
  machinery (signature_of/leaf_has_param/the two check fns) + IndistinguishableFanIn
  + AmbiguousKnob (both resolver arms -> unreachable!) + the lib.rs re-export,
  migrate tests 1/2, remove tests 7/8 + macd fixture. One cargo build --workspace
  gate over the whole enum-coupled change.
- Task 3: amend the C9/C12/C19/C23 ledger notes.

refs #59
2026-06-11 18:20:44 +02:00
Brummel 7d04cb313a spec: 0032 correct test 5 fixture nesting (boss-signed)
Test 5's asymmetric-collision fixture asserted a root-level param_space
["sma.length"] while also being "test 2's fixture with one leg swapped" (nested)
— a contradiction: check_fan_in_distinguishability only descends into composites,
so a root-level collision is not rejected today (not RED-first). Caught at plan
time while writing the verbatim fixture body.

Corrected: test 5 nests the asymmetric collision in an inner composite `asym`
(param-bearing Sma "sma" + paramless Pass1 .named("sma") + Sub, both leaves on
role price fanning into the Sub) under a source-bound root, asserting
param_space() == ["asym.sma.length"] (the single injective entry — the paramless
leg contributes no path) and that compile_with_params(&[I64(2)]) is Ok after.

Re-grounded PASS, re-panel unanimous SOUND. Forward correction on top of 7f059d0
(main is forward-only — no rewind of the boss-signed spec).

refs #59
2026-06-11 18:15:37 +02:00
Brummel 7f059d0d94 spec: 0032 param-namespace-injectivity (boss-signed)
param_space() injectivity becomes a stated, enforced compile invariant. One
structural check (check_param_namespace_injective) replaces the fan-in machinery
(check_fan_in_distinguishability, check_composite_fan_in, signature_of,
leaf_has_param) and the AmbiguousKnob binder branch. The error is path-carrying
(DuplicateParamPath) and the binders run the check before name resolution, so the
canonical by-name author sees the structural cure (.named(...)) instead of the
AmbiguousKnob symptom. param_space() stays infallible; the invariant lives in the
compile step (C23).

Two render-identity-only fan-in rejections (asymmetric param/paramless collision,
role-vs-leg signature collision) are intentionally dropped — neither is a
param_space duplicate (every knob keeps a unique path), and the property they
guarded (render distinguishability) has had no live consumer since the renderer
was retired in 0026. The larger is pinned by a now-compiles fixture.

Boss-signed under spec_auto_sign: objective gates green (precondition clean,
self-review clean, grounding-check PASS without override) + a unanimous five-lens
spec-skeptic panel. The grounding lens first BLOCKed a false set-equality claim;
the user ratified dropping both render-identity cases, the spec was reframed onto
the injectivity scope, re-grounded PASS, and the re-panel returned unanimous SOUND.

refs #59
2026-06-11 18:06:28 +02:00
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
Brummel 0e411f1796 audit: cycle 0031 (#56) — drift fixed (stale C9 render prose); node-instance naming
Architect drift review of cycle 0031 (node-instance naming; spec 5818497, plan
d890090, impl ffed8cc). Regression gate: no scripts configured (commands.regression
is empty) — the architect is the gate.

What holds: the ParamAlias retirement is complete and surgical — ParamAlias,
aliases_on, check_alias_indices, leaf_has_unaliased_param, Composite.params, and
the Composite::new 5th arg are gone from all code with no orphaned references
(only historical specs/plans 0016-0024 mention them, correctly untouched);
lib.rs re-exports cleaned. C23 preserved in fact: node_name() flows only into
signature_of (fan-in) and collect_params (param path), never into the compilat —
it stays name-free, wired by raw index. C11 value-empty holds: a node name is
construction identity, injected nowhere into the value vector. param_space() is
uniformly <node>.<param>; all call sites migrated in lockstep. 198 tests green,
clippy clean.

Drift fixed (this commit): the C9 fan-in refinement prose still asserted "the
graph view renders each fan-in input as the shortest sibling-unique prefix of its
source signature" — that render died with ascii-dag in cycle 0026; signature_of
now has zero render consumers. Corrected to state signature_of is
construction-phase-only with no render consumer, and that surfacing node identity
in the WASM viewer is tracked follow-up.

Drift routed to backlog: the node instance name does not reach the graph model
(model_to_json emits the type label, so the viewer shows identical [SMA] boxes
for named fan-in legs) — a deliberate 0031 scope decision, filed as #58 (idea,
relates to #13/#37), not a regression.

Cycle 0031 drift-clean after the one fix.
2026-06-11 11:54:41 +02:00
Brummel ffed8cc612 feat(aura-core,aura-engine,aura-cli): node-instance naming retires ParamAlias
Every blueprint node now carries a name. A primitive builder gains an optional
instance name (Sma::builder().named("fast")); when omitted it defaults to the
node's type label, ASCII-lowercased verbatim ("SMA"->"sma", "SimBroker"->
"simbroker"). 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, so
one act -- naming the legs "fast"/"slow" -- fixes both the naming collision and
the fan-in check. The index-addressed ParamAlias overlay, Composite.params, the
Composite::new 5th argument, aliases_on, and check_alias_indices are removed
(Role.name and OutField.name untouched). Ledger C9 and C23 amended.

This is why the change is correct, not merely convenient: blueprints are
value-empty (C11), so the thing that would otherwise distinguish two SMAs --
their length -- is not present at construction; identity must come from a name,
not a deferred value. Closes the #56 friction surfaced by the param-space &
sweep milestone fieldtest (a freshly-authored 2-SMA cross was unbindable and
would not bootstrap without two hand-counted ParamAlias overlays).

Two plan defects were corrected during implementation and verified:
- the three new fan-in/path tests authored sma_cross at root level, which would
  qualify to "fast.length" (root prefix is empty) and is not the nested case the
  fan-in check inspects; nesting sma_cross under a root (a shared
  sma_cross_under_root helper) restores the asserted "sma_cross.fast.length" and
  IndistinguishableFanIn{node:2};
- three cycle-0030 named-binder tests bound the real harness by the old names
  (sma_cross.fast / scale) and were migrated to the new path strings, surfaced by
  the workspace test gate.

Verified by the orchestrator: cargo build/test --workspace green (198 tests,
0 red), clippy --all-targets -D warnings clean. model_to_json emits the type
label + factory param names (node-name-independent), so sample-model.json and the
graph_model golden are byte-identical (untouched). NodeSchema gained a Default
derive (the builder default-name test constructs an empty schema). fieldtests/
are frozen non-workspace records, not migrated.

closes #56
2026-06-11 11:51:30 +02:00
Brummel d8900900b5 plan: 0031 node-instance naming
Implementation plan for spec 0031 (#56). Three tasks:
1. aura-core PrimitiveBuilder instance-name mechanism (.named()/node_name(),
   default = lowercased type label) — additive.
2. The engine mechanism + the full compile-coupled migration, as one task by
   necessity: collect_params node segment, signature_of + fan-in re-keyed onto
   the node name, ParamAlias / Composite.params / the Composite::new 5th arg /
   aliases_on / check_alias_indices removed, every in-workspace Composite::new
   caller and ParamAlias reference migrated, the mirror / top_level (inverted) /
   signature / CLI-golden assertions updated — all under one cargo build/test
   --workspace gate that enumerates migration completeness.
3. Ledger C9 + C23 amendments.

plan-recon (DONE_WITH_CONCERNS) surfaced two sites the brief missed, both now in
the plan: the sweep-golden twin crates/aura-cli/tests/cli_run.rs:214 (lockstep
with main.rs), and the model_to_json goldens — resolved as OUT of scope (the JSON
model emits the type label + factory param names, both node-name-independent, so
sample-model.json and the graph_model.rs golden stay byte-identical; the fixtures
migrate structurally only). signature_of stays -> String, re-keyed onto the node
name. fieldtests/ are frozen non-workspace records, out of scope.

refs #56
2026-06-11 11:29:57 +02:00
Brummel 581849766f spec: 0031 node-instance naming (boss-signed)
Design spec for #56 — give every blueprint node a name so param-space
path-qualification and fan-in distinguishability both flow from one
author-controlled identity, and retire the index-addressed ParamAlias overlay.

A primitive builder gains an optional name (default = the node's type-label
string, ASCII-lowercased verbatim: "SMA"->"sma", "SimBroker"->"simbroker";
explicit names must be non-empty). param_space() becomes uniformly
<node>.<param> at every level including the root. signature_of and the C9
fan-in rule re-key onto the node name: two same-type siblings both defaulting
to "sma" collide and IndistinguishableFanIn fires correctly, so one act —
naming the legs "fast"/"slow" — fixes both the naming collision and the fan-in
check. ParamAlias / Composite.params / the alias index-check are removed
(Role.name and OutField.name untouched). Ledger C9 and C23 amended. One
cohesive iteration: the engine mechanism + in-workspace migration (CLI sample,
engine tests, sweep goldens, graph_model.rs, sweep.rs, lib.rs re-export) land
together since removing the struct is compile-coupled; fieldtests/ are frozen
non-workspace records, out of scope.

Boss-signed under spec_auto_sign: all objective gates green (precondition,
self-review, grounding-check PASS) and a unanimous five-lens spec-skeptic panel
(criterion / grounding / scope-fork / ambiguity / plan-readiness) returned
SOUND after one fix round. The panel's first round caught real defects — a
worked example written against a non-existent fluent API, a false [param:]
render-marker claim (the marker was removed in cycle 0020), and an
under-specified compound default name — all corrected and re-grounded before
the unanimous pass.

refs #56
2026-06-11 11:20:22 +02:00
Brummel 03285d1b47 fieldtest: milestone the-world param-space & sweep — green, delivers promise
Milestone-scope fieldtest (closing functional gate) for "The World —
parameter-space & sweep" (issues #30-#36). Three curated end-to-end scenarios
derived top-down from the milestone promise, driven from the public interface
only (ledger + specs + rustdoc; no crates/*/src read), built and run from HEAD
via a downstream consumer crate.

Delivery verdict: GREEN. The milestone delivers its promise.
- mw_1 single run bound by name: .with("sma_cross.fast", 2).bootstrap() binds
  and runs (12 equity rows); diagnostics precise and knob-named (UnknownKnob /
  AmbiguousKnob / KindMismatch).
- mw_2 named-axis sweep + compare: .axis(..).sweep(run) yields a 4-member,
  ordered (odometer), disjoint, non-constant SweepFamily; ranking from public
  fields. CLI (aura sweep / aura runs rank) delivers the same with no Rust.
- mw_3 structural-constant negative space (deliberate): SMA2-entry's
  definitional `2` has no honest expression today -> grounds #55. Out of
  milestone scope; not a blocker.

Findings: 0 bugs, 2 working, 3 friction, 1 spec_gap. Verified independently
(re-ran all three from HEAD; confirmed a plain SMA-cross emits sma_cross.length
twice and the shipped CLI sample carries the fast/slow ParamAlias overlays).

Friction (routed to the forward queue, none blocks the gate):
- the promised sma_cross.fast/.slow names are not free — they require
  author-added ParamAlias overlays, doubly mandatory because the canonical
  2-SMA cross also will not bootstrap without them (IndistinguishableFanIn);
- the sweep closure drops back to positional bind + manual name re-zip.

spec_gap -> #55: the wished-for structural-constant consumer code is recorded
in mw_3 as #55's acceptance evidence.

This commit is the milestone fieldtest itself; it closes no issue. Closing the
tracker milestone stays a deliberate manual act on this green gate.
2026-06-11 09:11:37 +02:00
Brummel acaa6b0df8 audit: cycle 0030 (#35) — drift-clean; named param binding
cycle 0030 tidy (clean). The architect drift review against docs/design/INDEX.md
and CLAUDE.md found the cycle drift-clean: every load-bearing contract the named
binding layer touches holds.

- C1 / C12 / C19 / C23: a pure authoring layer — sweep.rs, the lowering path,
  collect_params, and param_space() are unchanged; with/axis/Binder/SweepBinder/
  resolve/resolve_axes all terminate into the existing bootstrap_with_params /
  GridSpace::new / sweep. No new state, no concurrency, no path into the run loop.
  Equivalence pinned by named_binder_runs_bit_identical_to_positional and
  named_axes_grid_parity_with_positional. The match key is the exact param_space()
  name (p.name == name), read-only.
- C7: pure-equality kind check (v.kind() != p.kind), no coercion.
- C16 / C10: std-only (the e97906a pin added a test, not a dependency); builder
  idiom (.with()/.axis()), no DSL/macro.
- The full BindError enum (incl. EmptyAxis) is constructed (EmptyAxis in
  resolve_axes) — no dead variant. No .bind() ships — the #55 structural-constant
  reservation is intact. The resolve/resolve_axes two-phase duplication is the
  documented accepted debt (spec §Components), not silent drift.

Resolution: one Nit fixed inline — a redundant `use crate::GridSpace;` in the
blueprint.rs test module (GridSpace is already in scope via `use super::*`).
Removed; the parity test still compiles, full `cargo test --workspace` green and
`cargo clippy --workspace --all-targets -- -D warnings` clean.

Regression gate: none configured (profile regression: []); architect is the gate.

This closes the named-binding feature (single run + sweep). Note: this is a
cycle drift-clean close, not a milestone close — the milestone fieldtest for
"The World — parameter-space & sweep" remains a deliberate manual act.

closes #35
2026-06-11 00:33:08 +02:00
Brummel 937257e368 feat(aura-engine,aura-cli): named param binding — sweep axes .axis()/.sweep() (0030 iter 2)
Bind a sweep's axes by name instead of by a positional GridSpace:

    bp.axis("sma_cross.fast", [2, 3]).axis("sma_cross.slow", [4, 5]).axis("scale", [0.5]).sweep(run)

The sweep half of spec 0030, completing the named-binding feature (#35). A pure
authoring layer over the existing GridSpace::new / sweep primitives; engine core
untouched (C1/C12/C19/C23).

This iteration:
- resolve_axes(): shares resolve's name->slot mapping (iter 1), adds the EmptyAxis
  check (the variant defined in iter 1 is now first constructed here) and a
  per-element axis kind-check (every element of each axis, first offending element
  in axis order). Its validation is a strict superset of GridSpace::new's
  (arity / non-empty / per-element kind), so SweepBinder::sweep's downstream
  GridSpace::new(...).expect(...) is infallible by construction — it can never
  panic on author input.
- Composite::axis -> SweepBinder -> SweepBinder::sweep (Result<SweepFamily, BindError>).
- The CLI sample sweep converted to the named form (behaviour-preserving).

Plan correction (verified by the orchestrator): the iteration plan said only the
GridSpace import was orphaned by the CLI conversion, but replacing the free
sweep(&grid, ..) call with the .sweep(..) method also orphans the free `sweep`
import — both were dropped from aura-cli to satisfy clippy -D warnings. The Scalar
import (from aura_core) stays used and was untouched.

Verified (run by the orchestrator): 6 new aura-engine tests green —
resolve_axes round-trip, EmptyAxis, MissingKnob, the per-element KindMismatch on a
mixed axis (the superset/no-panic guarantee), a builder no-panic wrong-kind test,
and a GridSpace .points() parity test; the two sweep JSON goldens
(sweep_report_renders_four_points_in_odometer_order + the cli_run integration
golden) green unchanged (name/value/order-preserving conversion); full
`cargo test --workspace` green and `cargo clippy --workspace --all-targets
-- -D warnings` clean.

refs #35
2026-06-11 00:30:14 +02:00