The engine's ingestion input was the only type encoding an eager-dataflow
assumption: `Harness::run(Vec<Vec<(Timestamp, Scalar)>>)` — one fully
materialized stream per source. At realistic research scale (20y, 3 tick
streams, ~7.9e9 ticks) that layout is ~190 GB resident, non-residable. Yet
the merge loop never needs the whole stream: it only ever peeks the head
timestamp of each live source and pops one record. So the eager Vec is
gratuitous.
This lands the first half of the Source seam (plan Tasks 1-2):
- A `Source` trait (`peek(&self) -> Option<Timestamp>`,
`next(&mut) -> Option<(Timestamp, Scalar)>`), object-safe so the merge
holds `Vec<Box<dyn Source>>`. The peek/next split is the producer contract
the k-way merge drives.
- `VecSource`, an adapter over the old `Vec<(Timestamp, Scalar)>` — the
cycle-0011 eager shape, named. It IS the old behaviour.
- `Harness::run` re-typed to `Vec<Box<dyn Source>>`; the merge loop's index
arithmetic becomes peek/next. C4 tie-by-source-index is preserved (the
strictly-`<` replace + source-order scan is byte-for-byte the old
semantics). The forward+eval body is unchanged.
- All 53 call sites threaded VecSource-wrapped in the same change, so the
workspace compiles atomically and run output is byte-identical (C1).
Behaviour-preserving is the load-bearing guarantee: the full suite is green
and unchanged (the two-run C1 determinism pairs — real_bars r1/r2, blueprint
sweep-equality, harness h/h2 — stay byte-identical); aura-engine unit tests
129 -> 132 (+3 VecSource unit tests). No residency change yet: VecSource holds
the same Vec as before. The lazy data-server-backed streaming source and its
end-to-end residency proof are the second half (plan Tasks 3-4).
refs #71
Decomposes the boss-signed spec into four tasks: (1) the `Source` trait +
`VecSource` adapter in aura-engine; (2) the atomic `Harness::run` re-type to
`Vec<Box<dyn Source>>` with all 53 call sites threaded behaviour-preserving;
(3) the streaming `M1FieldSource` over a data-server window; (4) a gated
streaming e2e + the measured residency predicate.
refs #71
Re-types the engine's ingestion input from a materialized
Vec<Vec<(Timestamp, Scalar)>> into a `Source` producer trait the k-way
merge drives by peek/next, and proves it against a real, lazily-streamed
data-server window in the same cycle — closing the cycle-0011 deliberate
eager-materialization gap (C12 Arc<[T]> cross-sim sharing). A VecSource
adapter keeps every existing run call site byte-identical
(behaviour-preserving); an M1FieldSource streams data-server chunks with
O(one-chunk) residency, independent of window length.
Foundational cycle of the World-II milestone; #66/#68/#69 inherit the
seam. Signed under the boss auto-sign gate: precondition clean, grounding
PASS on the final bytes, unanimous five-lens spec-skeptic panel (two
editorial ambiguity rounds on the residency predicate repaired to a
measured per-pull CHUNK_SIZE ceiling).
refs #71
Ships C12 orchestration axis 2 (optimize): pick the single best point of
a sweep family by a named RunMetrics field. The GREEN half of the
RED-first iteration whose executable-spec landed in the previous commit.
optimize(&SweepFamily, metric) -> Result<SweepPoint, RegistryError>
returns the whole winning SweepPoint — its params coordinate AND its
RunReport — because the caller needs the params that won, not just the
metric. "Best" is fixed per metric (total_pips higher-is-better;
max_drawdown / exposure_sign_flips lower-is-better); ties resolve to the
earliest enumeration-order point (the family is in odometer order, and
only a strictly-better later point displaces the incumbent).
Single source of truth for "best", as #67 required ("reuse rank_by; do
not fork"): rank_by's per-metric direction is extracted into a private
`metric_cmp` helper that resolves the boundary metric *name* to a closed
`enum Metric` once and returns one comparator closure. Both rank_by
(sort by it) and optimize (argmax via `reduce`, strictly-better-displaces)
now call it, so the per-metric direction lives in exactly one match.
rank_by is behaviour-preserving — its existing tests pass unchanged.
The sweep executor is untouched.
No caller-supplied `direction`: one definition of best per metric,
consistent with the shipped rank_by invariant (a caller direction would
admit incoherent asks like the worst max_drawdown). An unknown metric is
UnknownMetric. A SweepFamily is non-empty by construction (EmptyAxis is
rejected upstream), so the argmax always yields a winner.
CLI surface: the "pick the best" view is already served by
`aura runs rank <metric>`, whose first line is the optimum over the
persisted registry; optimize() is the new *programmatic* axis-2 entry,
for World programs that hold a live SweepFamily and need the winning
params back in-process — which the registry/RunReport path cannot give.
No redundant CLI command added.
closes#67
The RED half of a RED-first iteration for the optimize/argmax
orchestration axis (C12 axis 2). Pins the headline behaviour before
the feature exists: optimize(&SweepFamily, metric) returns the single
winning SweepPoint — its params coordinate AND its RunReport — chosen
by the metric's fixed per-metric sense of best (reusing rank_by's
direction semantics), with ties resolving to the earliest
enumeration-order point.
No caller-supplied direction: that would contradict the shipped
rank_by invariant that "best" is fixed by each metric's meaning, and
would admit incoherent asks (e.g. the worst max_drawdown). One
definition of best per metric.
RED for the right reason — optimize is absent (E0425), the sole
error; cargo build --workspace stays green (test-only module). GREEN
lands in the following commit, which closes#67.
Three RunReport builders hand-rolled the same RunManifest with identical
commit/seed/broker fields (only params/window varied), drifting
independently. Extract sim_optimal_manifest(params, window) — one source for
the broker label, so #22's per-asset pip work has a single edit point.
kind_str and collect_distinct_composites no longer exist in aura-cli (it
consumes model_to_json directly), so the "mirrors aura-cli's ..." notes
pointed at nothing. Keep the substantive rationale (Debug is PascalCase).
resolve (single Scalar per slot) and resolve_axes (a Vec<Scalar> axis per
slot) were the same two-phase named-binding resolution against param_space(),
duplicated almost verbatim — the live instance of the "two raise-sites for
one invariant" hazard (cf. #45/#53). A change to binding precedence or error
ordering had to touch both in lockstep.
Collapse the shared skeleton into resolve_into<T>, parameterized by two
closures: claim_ok (phase-1 claim-time gate, axis-only EmptyAxis, before the
duplicate check) and kind_ok (phase-2 per-slot kind gate). The error total
order now lives in exactly one place. Behaviour-preserving: same error for
same input, all 231 tests green (the resolve/bind precedence tests pin it).
FieldSpec.name was &'static str, forcing derive_signature to Box::leak
every re-exported composite OutField name. Because compile_with_params
reruns derive_signature per sweep point, an N-point sweep over an
M-output-field composite leaked N×M boundary names unbounded — a scaling
wall the moment the World/sweep layer compiles families per point.
Pull FieldSpec.name to String, mirroring the already-owned PortSpec.name
and ParamSpec.name: a composite re-exports interior fields under runtime
boundary names, so ownership is the correct model, not a workaround.
FieldSpec loses Copy; the only fallout is one borrow at the render site.
leak_name/Box::leak deleted.
Behaviour-preserving: signature contents unchanged, all 231 tests green.
"Compilat" (German "Kompilat") was a coined noun for the product of the
bootstrap compilation — neither English nor a natural fit. The runtime
artifact already has a code identifier for exactly this thing: the
`FlatGraph` struct (harness.rs). Replace the coinage with that identifier:
- prose mentions -> "flat graph" (mirrors the type, reads plainly)
- definitional anchors -> `FlatGraph` (C11, C23, the running-graph line)
- `render_compilat` -> `render_flat_graph` (historical render symbol;
keeps the `render_blueprint` / `render_flat_graph`
source-vs-product pairing)
- `intra-compilat` -> `intra-graph`
- `from_compilat` (test) -> `from_flat`
"compilation" / "re-compilation" / "bootstrap-as-compilation" (the process,
ordinary English) are deliberately left untouched. Behaviour-preserving:
only comments, design ledger, specs/plans, one test-local variable and its
assert messages change. Full workspace test suite green; clippy clean.
Close-audit for cycle 0040 (commit range b6174cf..227d004). Architect drift review
against the design ledger + CLAUDE.md domain invariants: no contract drift, no debt.
- C23 intact: check_ports_connected is index-based and pre-build, reads only
signature().inputs.len() + the existing raw-index Edge/Target, and emits nothing
into FlatGraph. Lowering pushes leaf-recipe signatures (not derive_signature's
output); composites inline away; the run loop is untouched.
- derive_signature/interior_slot_kind bounds-totality weakens no validation: the
placeholder kind for a structurally-invalid composite is consumed only by the
parent's speculative kind-check, while the recursion independently re-rejects the
same out-of-range index via the guarded BadInteriorIndex/OutputPortOutOfRange
loops. The change converts a panic into a deferred-but-still-reported error.
- The C8 cycle-0040 realization note accurately records the new invariant, scopes it
to interior slots, names open roles as providers, and does not re-claim the dropped
swap-closure goal or overstate name load-bearingness.
- No legal graph is newly rejected (open source:None roles excluded as providers;
root-open stays governed by UnboundRootRole).
Regression gate: no regression script declared in project facts -> architect is the
sole gate (drift-clean). cargo test --workspace 231 passed / 0 failed; cargo clippy
--workspace --all-targets -- -D warnings clean (independently re-run by the
orchestrator).
refs #65
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
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
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
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
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
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.
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.
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.
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
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
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
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.
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
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
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.
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
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
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
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.
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
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
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
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.
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
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
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.
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.
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.
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
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
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
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
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
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
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
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).
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