977 Commits

Author SHA1 Message Date
Brummel abd76c9756 plan: 0030 named param binding — sweep axes (iteration 2)
Iteration-2 plan for spec 0030 (the sweep side): two tasks.
- Task 1 (aura-engine): resolve_axes (shares resolve's name->slot mapping; adds the
  EmptyAxis check and a per-element axis kind-check, a superset of GridSpace::new so
  the downstream .expect() is infallible), Composite::axis / SweepBinder /
  SweepBinder::sweep, the SweepBinder re-export, and the engine tests (round-trip,
  EmptyAxis, MissingKnob, per-element KindMismatch on a mixed axis, a builder
  no-panic wrong-kind test, and a GridSpace .points() parity test). RED-first via a
  todo!() resolve_axes stub. EmptyAxis is now constructed (was defined in iter 1).
- Task 2 (aura-cli): convert sweep_family() grid construction to .axis(...).sweep(...)
  (keeping the per-point closure and the local `space` it reads for manifest params);
  drop the now-orphaned GridSpace import (the clippy -D warnings trap this iteration).

The sweep JSON param goldens (main.rs + tests/cli_run.rs) are name/value/order-
preserving and stay green unchanged. Engine core untouched (C1/C12/C19/C23).

refs #35
2026-06-11 00:24:12 +02:00
Brummel 64b19ab3d5 feat(aura-engine,aura-cli): named param binding — single-run .with()/.bootstrap() (0030 iter 1)
Bind a blueprint's open knobs by name for a single run instead of by a positional,
Scalar-wrapped Vec in param_space() order:

    bp.with("sma_cross.fast", 2).with("sma_cross.slow", 4).with("scale", 0.5).bootstrap()

A pure authoring layer over the existing Composite::param_space / bootstrap_with_params
primitives; the engine run loop and the construction core are untouched (C1/C12/C19/C23
preserved). Raw literals lower via Into<Scalar> (the literal fixes the variant: 2->I64,
0.5->F64, pinned by e97906a); the match key is the exact param_space() name — path-qualified
for a composite-interior knob (sma_cross.fast), bare for a root-level knob (scale).

This iteration (single-run side):
- BindError vocabulary (name-qualified); the full enum incl. the sweep-only EmptyAxis is
  defined and re-exported now, so the unconstructed variant is reachable public API, not
  dead_code — EmptyAxis is constructed in iteration 2 (the sweep side).
- resolve(): the shared name-resolution core, a total error order — Phase 1 per binding in
  input order (a UnknownKnob / b AmbiguousKnob / d DuplicateBinding), Phase 2 slot walk in
  param_space() order (e MissingKnob / f KindMismatch); first failing check wins.
- Composite::with -> Binder -> Binder::bootstrap; .with() not .bind() (bind is reserved for
  #55's structural-constant overlay).
- The CLI sample single-run test converted to the named form (behaviour-preserving).

Verified (run by the orchestrator, not the agent's claim): 9 new aura-engine tests green —
resolve round-trip + one per single-run BindError variant + two precedence tests + the C1
named-equals-positional equivalence over the sample composite harness; the converted CLI
sample test green; full `cargo test --workspace` green and `cargo clippy --workspace
--all-targets -- -D warnings` clean.

Iteration 2 (the sweep side: axis / SweepBinder / resolve_axes / EmptyAxis + the sweep CLI
conversion) is next. refs #35
2026-06-11 00:15:13 +02:00
Brummel 7cd990f789 plan: 0030 named param binding — single-run (iteration 1)
Iteration-1 plan for spec 0030 (the single-run side): two tasks.
- Task 1 (aura-engine): BindError vocabulary, the shared `resolve` core
  (two-phase total error order), Composite::with / Binder / Binder::bootstrap,
  the lib.rs re-export, and the engine tests (resolve round-trip, one per
  single-run BindError variant, two precedence tests, the C1 named≡positional
  equivalence test). RED-first via a todo!() resolve stub, GREEN on impl.
- Task 2 (aura-cli): convert the sample single-run test to .with(...).bootstrap()
  using the exact param_space() names (sma_cross.fast/sma_cross.slow/scale).

The full BindError enum (incl. EmptyAxis) is defined now and re-exported, so the
unconstructed sweep-only variant is reachable API (not dead_code) — EmptyAxis is
constructed in iteration 2. Engine core untouched (C1/C12/C19/C23).

refs #35
2026-06-11 00:09:29 +02:00
Brummel 8fae58fdc4 spec: 0030 named param binding (boss-signed)
Bind a blueprint's open knobs by name via a fluent builder instead of a
positional, Scalar-wrapped Vec in param_space() order: a single run as
bp.with("sma_cross.fast", 2).with("scale", 0.5).bootstrap(), a sweep as
bp.axis("sma_cross.fast", [2,3]).axis("scale", [0.5]).sweep(run). A pure
authoring layer over the existing bootstrap_with_params / GridSpace / sweep
primitives; the engine core is untouched (C1/C7/C12/C19/C23 preserved).

Ratified design (brainstorm -> specify):
- Fluent builder (.with()/.axis()), not a macro — C10 builder-API idiom.
- Raw literals via Into<Scalar>; the literal fixes the variant (2->I64,
  0.5->F64); kind-check is pure equality, no coercion.
- Match key = the EXACT param_space() name (user's Option 1): path-qualified
  for composite-interior knobs (sma_cross.fast), bare for root-level knobs
  (scale). Short bare names are an authoring choice (promote to a root leaf),
  not a job of this layer; no engine change, no unqualified matching.
- .with() not .bind() — bind is reserved for #55's structural-constant overlay.
- Total error order (a-f) over a single BindError vocabulary: Phase 1 validates
  bindings (UnknownKnob/AmbiguousKnob/EmptyAxis/DuplicateBinding), Phase 2 walks
  slots (MissingKnob/KindMismatch); first failing check wins; sweep kind-check is
  per-element, making resolve_axes a superset of GridSpace::new so the downstream
  .expect() is infallible.
- Two iterations: single-run side, then sweep-axis side (shared name-resolution
  core). The CLI sample stays nested (user's call); the worked example shows both
  qualified and bare names honestly, no fixture change.

Auto-signed under /boss spec_auto_sign after the objective gates (precondition,
parse no-op, grounding-check PASS) and a unanimous five-lens spec-skeptic panel.
The panel hardened the spec across five rounds: it corrected the worked-example
names to the real param_space() output, pinned the exact-name match key, and made
the error precedence a total order closing a sweep per-element panic path. The
literal-inference grounding gap was closed separately by e97906a. Two design
points were taken by the user directly (Option 1; keep the sample nested).

refs #35
2026-06-11 00:00:55 +02:00
Brummel e97906a0e1 test(aura-core): pin bare-literal Into<Scalar> kind inference
Authoring layers that lower raw literals via `impl Into<Scalar>` (the
named-param-binding surface, spec 0030 / #35) rely on a bare integer
literal inferring as i64 and a bare float as f64 — because i64/f64/bool
are the only `From<…> for Scalar` impls, each literal class resolves to
exactly one. This was true but untested: `from_widenings` uses suffixed
literals (`7i64`), so a regression in the inference path would survive it.

Behaviour-preserving pin of existing aura-core behaviour. Closes the one
grounding gap the grounding-check flagged on spec 0030; also guards the
property independently — adding a second integer `From` impl would now
break loudly here rather than silently at a `.with("knob", 2)` call site.

refs #35
2026-06-10 23:23:27 +02:00
Brummel 654b23ad1f audit: cycle 0029 (#33) — run registry; one ledger drift fixed
Cycle-close architect review of 16e31fe..HEAD against docs/design/INDEX.md +
CLAUDE.md. Regression gate: none configured (commands.regression: []), so the
architect is the sole gate.

What holds:
- C18 honoured — aura-registry stores (manifest, metrics) RunReports append-only
  to runs/runs.jsonl, reproducible-from-manifest; no git/Gitea duplication, no
  multi-project manager. The depth deferrals (lineage, re-derive, run-diff,
  run_id, Aura.toml runs-dir) are named in the spec, not silently dropped.
- C1 preserved — the sweep stays disjoint/lock-free; "sweep == N independent
  runs" now compares full RunReports; serde_json output is deterministic.
- C12/C19/C20 fit — SweepPoint.report closes cycle C's deferred manifest-per-
  point gap; the manifest stays World-supplied (engine `sweep` is `Fn -> RunReport`,
  domain-free).
- Workspace green (cli_run 11, registry 5, engine 93, …); clippy clean.

Resolution:
- FIX (this commit): docs/design/INDEX.md "External components" (the data-server
  row) still asserted the struck "external-dependency firewall / engine crates
  stay external-dependency-free" framing — stale after the C16 amendment (aura-
  core/aura-engine now link serde). Rewritten to the amended per-case policy.
  The iter-1 commit's "no ledger text still asserts it" missed this mirror; it is
  now true (re-grep clean across the ledger and live source).
- FOLLOW-UP (#54, idea): RunReport has two divergent JSON encoders — serde for
  the on-disk store, hand-rolled to_json for stdout. This was a deliberate,
  spec-named deferral (keep stdout goldens stable this cycle); filed for the
  stdout→serde unification that retires the last hand-rolled writer.

Ratify: the C16 amendment (blanket zero-dependency -> considered per-case policy)
is the intentional contract change of this cycle, justified in the 0029 spec and
the iter-1 commit; this audit ratifies it as drift-clean (the codebase no longer
contradicts it anywhere).

closes #33
2026-06-10 20:41:44 +02:00
Brummel 5bded0ca83 feat(aura-cli): aura runs registry — persist on sweep, list + rank (cycle D / iter 3)
Final iteration of the run-registry cycle (#33): the read surface that makes a
sweep family — and runs across separate invocations — comparable over time
(C18's "compare experiments over time", which has no home in git or Gitea).

- `aura sweep` now persists each point's RunReport to runs/runs.jsonl (append-
  only, one serde_json line per run) in addition to printing it.
- `aura runs list` prints every stored record, in store order.
- `aura runs rank <metric>` prints the stored runs best-first by the metric
  (total_pips desc; max_drawdown / exposure_sign_flips asc); an unknown metric
  is a usage error (exit 2), like every other aura arg error.

sweep_report splits into a production sweep_family() (the pure run) and a
#[cfg(test)] renderer kept for the in-bin goldens; the dispatch gains run_sweep
/ runs_list / runs_rank over default_registry() (runs/runs.jsonl under cwd).
Process goldens run the binary in a temp cwd so persistence never dirties the
repo; /runs/ is git-ignored as a backstop.

Verified end-to-end: two `aura sweep` invocations accumulate 8 records;
`aura runs list` shows all 8; `aura runs rank total_pips` orders them best-first;
`aura runs rank bogus` exits 2 with the known-metrics hint. cargo test
--workspace green (cli_run 11, registry 5, engine 93, …); clippy
--workspace --all-targets -D warnings clean; no stray runs/ in the work tree.

refs #33
2026-06-10 20:37:51 +02:00
Brummel 2811c060dc plan: 0029 run-registry CLI persistence + read surface (iteration 3)
Final iteration of cycle D: aura sweep persists each point's RunReport to
runs/runs.jsonl; aura runs list prints every stored record; aura runs rank
<metric> prints them best-first. sweep_report splits into a production
sweep_family() + a #[cfg(test)] renderer; process goldens run in a temp cwd so
persistence never dirties the repo; /runs/ is git-ignored.

refs #33
2026-06-10 20:33:57 +02:00
Brummel fe11cfea8a feat(engine,registry): SweepPoint carries RunReport; add aura-registry (cycle D / iter 2)
Iteration 2 of the run-registry cycle (#33): make the sweep family
self-describing and add the registry store.

Engine. SweepPoint.metrics (RunMetrics) becomes SweepPoint.report (RunReport),
and the sweep closure bound is now Fn(&[Scalar]) -> RunReport. This closes the
manifest-per-point gap cycle C explicitly deferred to #33 — each swept point now
carries its full (manifest, metrics), the unit the registry indexes (C18). All
engine-internal callers, the engine test fixture run_point, and the three sweep
tests were rethreaded in the same iteration so the crate stays green; the CLI
caller followed in the same change so `cargo test --workspace` is green again.

CLI. The sweep_report closure builds a per-point RunReport (manifest params =
param_space names zipped onto the point via scalar_as_param_f64; commit/window/
seed/broker as run_sample builds them) and prints via RunReport::to_json. The
hand-rolled sweep_point_to_json/json_string/scalar_token are retired (the report
renders itself), and the orphaned ParamSpec/SweepPoint imports dropped. Net:
aura run, aura sweep, and (next iteration) aura runs all print the same
RunReport shape. The two aura sweep goldens now pin the per-point manifest
params, NOT the commit (it is the real git HEAD, volatile).

Registry. New crate aura-registry: Registry::{open, append, load} over an
append-only runs/runs.jsonl (one serde_json line per RunReport), free rank_by
(best-first per metric — total_pips desc, max_drawdown/exposure_sign_flips asc),
and RegistryError (Io / Parse{line} / UnknownMetric). Five unit tests cover
append->load round-trip, missing-file = empty, corrupt-line = Parse{line},
per-metric ranking, and unknown-metric error. Built and unit-tested; NOT yet
CLI-wired (aura sweep persistence + aura runs list/rank are iteration 3).

Verified: cargo test --workspace green (aura-registry 5, aura-engine 93, aura-cli
7+8, others unchanged); clippy --workspace --all-targets -D warnings clean; aura
run stdout shape unchanged; aura sweep now emits full RunReports.

refs #33
2026-06-10 20:28:31 +02:00
Brummel 5fa003da64 plan: 0029 run-registry engine + registry (iteration 2)
Iteration 2 of cycle D: SweepPoint carries a full RunReport (closure bound ->
RunReport), all engine + CLI callers rethreaded in lockstep so the workspace
returns green; the hand-rolled sweep_point_to_json retired (each point prints
via RunReport::to_json); new aura-registry crate (open/append/load/rank_by +
RegistryError), built and unit-tested but not yet CLI-wired. The aura sweep
goldens pin the per-point manifest params, not the volatile git-HEAD commit.

refs #33
2026-06-10 20:22:18 +02:00
Brummel eec876975c feat(engine): serde foundation + amend C16 dependency policy (cycle D / iter 1)
Iteration 1 of the run-registry cycle (#33): lay the dependency + serde
foundation the registry's typed read-path needs, and amend the contract that
forbade it.

Contract change (load-bearing — C18/C16). C16's blanket "zero-external-
dependency by commitment" + "aura-ingest is the sole external-dependency
firewall" framing is struck and replaced by a considered, per-case dependency
policy: dependencies are admitted by deliberate review (what a crate pulls in
vs. what it buys), with particular scrutiny for anything entering the frozen
deploy artifact (C13); standard vetted crates (serde, rayon, ...) pass that
review and are used wherever they do the job, including in the bot; hand-rolling
what a vetted crate does is the anti-pattern. C16's engine/project split +
three-tier node reuse are unchanged. The five source comments that asserted the
struck framing (aura-engine/aura-ingest Cargo.toml + aura-ingest/report.rs docs)
are rewritten to match; no live source or ledger text still asserts it.

Per-case justification for serde (the policy now requires one): it gives the
run-report types a typed (de)serialization path — exactly what ranking/compare
over stored runs needs, and what the writer-only hand-rolled JSON (C14) could
never provide; closes the typed-read-path gap (#17). Its closure is tiny,
ubiquitous, heavily audited, and its output deterministic (C1-safe). serde_json
is test-only this iteration (dev-dependency); no production serde_json yet.

Changes: [workspace.dependencies] serde (derive) + serde_json; serde derives on
Timestamp (aura-core) and RunMetrics/RunManifest/RunReport (aura-engine), with
serde_json round-trip tests (window renders as a [from,to] integer array via the
transparent Timestamp newtype). The hand-rolled to_json writers are untouched
(still drive stdout). No aura-registry crate, no SweepPoint change — those are
iterations 2 and 3.

Verified: cargo test --workspace green (incl. the two new round-trip tests);
clippy --workspace --all-targets -D warnings clean; no surviving assertion of
the struck zero-dep framing in live source or the ledger.

refs #33
2026-06-10 20:11:39 +02:00
Brummel 8159b785a8 plan: 0029 run-registry foundation (iteration 1)
Iteration 1 of cycle D: amend C16 (blanket zero-dep -> per-case policy) in the
ledger + the five source comments asserting the struck framing; add serde/
serde_json workspace deps; derive Serialize/Deserialize on Timestamp and the
RunMetrics/RunManifest/RunReport report types, with serde_json round-trip tests.
No CLI change, no aura-registry crate, no SweepPoint change (those are iter 2/3).

refs #33
2026-06-10 20:05:20 +02:00
Brummel d309336a72 spec: 0029 run-registry — index & compare a sweep family
Cycle D of the milestone "The World — parameter-space & sweep" (#33). A
persistent, append-only run registry: each run's (manifest, metrics) is a
record in runs/runs.jsonl; a read surface lists all records and ranks them
best-first by a named metric — C18's "compare experiments over time", which
has no home in git or Gitea. First cut = persist + list + rank; promotion/
status, lineage, run-diff, on-demand re-derivation, run_id, and Aura.toml
runs-dir wiring are deferred.

Contract-level change: C16's blanket "zero-external-dependency by commitment"
is amended to a considered, per-case dependency policy (scrutinize what enters
the frozen artifact; standard vetted crates like serde/rayon pass that review;
never hand-roll what they do). The engine/project split + three-tier node
reuse of C16 are unchanged. This admits serde/serde_json — the typed read-path
ranking needs, and which closes #17 (typed RunReport handle).

Shape: serde derives on RunManifest/RunMetrics/RunReport + Timestamp;
SweepPoint carries a full RunReport (closing the manifest-per-point gap cycle C
deferred to #33); new crate aura-registry (open/append/load/rank_by); CLI gains
aura runs list / aura runs rank <metric> and aura sweep persists each point.
Three iterations, first planner handoff = iteration 1 (foundation).

Sign-off: spec_auto_sign was enabled for this cycle; the five-lens spec-skeptic
panel returned 3 SOUND + 2 BLOCK (rank-ordering semantics; iteration-cut
compile-ordering + Scalar->f64 coercion). Both BLOCKs were genuine defects,
fixed before sign-off (rank is now best-first per-metric; the closure-type
change lands with its callers in one green iteration; the coercion is shown).
Non-unanimous panel routed the spec to human sign-off, granted by the user.

refs #33
2026-06-10 19:59:27 +02:00
Brummel 728ca1a2d2 chore(boss): enable spec auto-sign for this project
The orchestrator may now sign a spec in the user's place under /boss,
but only through specify's Step-6 auto-sign gate: all objective gates
green (precondition, parse, grounding-check PASS) AND a unanimous
five-lens spec-skeptic panel. The human signature stays mandatory on
any non-PASS path.
2026-06-10 19:45:23 +02:00
Brummel 16e31fe05d audit: cycle 0028 (#32) — drift-clean; param-sweep grid (the World, cycle C)
Architect drift review over 776bd54..HEAD: drift-clean on every load-bearing
contract.
- C1: sweep()'s disjointness is real — sweep_with_threads shares only an
  AtomicUsize cursor, tags results by point index, sorts post-join; the family
  order is the enumeration order, thread-count-independent (pinned by
  family_is_deterministic_across_thread_counts at 1 vs 8 vs derived-N). Each
  point builds fresh, shares nothing mutable. First realization of "parallelism
  across sims, never within one".
- C16: zero-external-dependency intact — crates/aura-engine/Cargo.toml unchanged,
  no dependency added; execution is std::thread::scope, never rayon. No serde.
- C12: scope discipline held — only the C12.1 grid axis landed; random /
  optimize / walk-forward / MC and per-point manifest assembly are absent and
  deferred per spec.
- C19/C20: untouched — sweep drives bootstrap_with_params per point, no topology
  mutation.
- C14: the CLI's sweep_point_to_json mirrors RunReport::to_json's hand-rolled
  token rules byte-for-byte; no second JSON dialect. C8/C18: metrics-drain stays
  author-side in the closure (the engine ↔ harness boundary the design intends).

Regression: no scripts configured (profile regression: []) — architect is the
sole gate, and it is green.

Debt (filed, blocks nothing):
- #53 — the SMA-cross sample harness topology is now duplicated across three
  sites (blueprint.rs test, sweep.rs test, cli main.rs production) with no test
  pinning them equal. Pure tidy, carry-on.

Deferred follow-up filed:
- #52 — random param-sweep enumeration (C12.1's other half: ranges +
  sample-count + seeded RNG), the deliberate scope cut from this cycle's design.

Cycle 0028 closes #32 (param-sweep grid, engine primitive + aura sweep CLI
demonstrator). Milestone "parameter-space & sweep" is NOT closed by this audit
(that needs the milestone fieldtest, a separate manual act); #33 (run registry,
cycle D) remains open in the milestone.

closes #32
2026-06-10 18:55:24 +02:00
Brummel e94ee4253a feat(aura-cli): aura sweep — grid-sweep demonstrator (the World, cycle C / iter 2)
Make the iteration-1 sweep primitive visible without writing Rust: an
`aura sweep` subcommand runs the built-in sample over a small built-in grid
(fast in {2,3}, slow in {4,5}, scale in {0.5} = 4 points) and prints one
canonical JSON line per point, in enumeration (odometer) order.

- sample_blueprint_with_sinks() -> (Composite, Receiver, Receiver) now holds
  the sample topology and returns the two Recorder receivers a per-point run
  drains; build_sample() (the `aura graph` entry, which never runs the graph)
  is re-expressed as `sample_blueprint_with_sinks().0`, so there is one
  topology definition and the graph render is unchanged (its golden stays
  green).
- sweep_report() declares the grid over the sample's param_space and calls the
  engine sweep() with a per-point closure (build fresh -> bootstrap_with_params
  -> run -> drain both sinks via f64_field -> summarize). Pure + deterministic
  (C1): the same build yields a bit-identical report.
- sweep_point_to_json + json_string + scalar_token hand-roll the JSON (C14, no
  serde — the engine's json_str is private), mirroring RunReport::to_json's
  token rules: a whole-valued f64 renders without a fractional part (12.0->12),
  an I64 as an integer token, an F64 as the shortest round-trippable form.
- The main() dispatch grows a ["sweep"] arm; USAGE advertises it; the strict
  trailing-token contract is inherited (aura sweep extra -> exit 2).

Tested: sweep_point_to_json byte-pinned on a hand-built point (the f64/i64
token rules); sweep_report pins 4 lines with per-line params byte-exact in
odometer order + determinism; process goldens in tests/cli_run.rs pin the
stdout line-count/exit-zero and the strict-arg exit-2 path. Full
cargo test --workspace green (graph golden, engine iter-1 sweep tests, run/macd
all unaffected); clippy --workspace --all-targets -- -D warnings clean.

refs #32
2026-06-10 18:51:23 +02:00
Brummel 7bfcc3b86e plan: 0028 sweep CLI demonstrator (iteration 2)
refs #32
2026-06-10 18:46:13 +02:00
Brummel a665a966a7 feat(aura-engine): param-sweep grid primitive (the World, cycle C / iter 1)
Turn one value-empty blueprint into a family of disjoint instances over a
declared grid, run them in parallel, and collect per-point metrics. This is
C12.1's grid axis — the first "N instead of 1": #30/#31 made a blueprint
param-generic and bindable; this reaps it.

New module crates/aura-engine/src/sweep.rs, three parts:
- GridSpace: a validated cartesian grid over a blueprint's param_space() —
  one discrete value-list per slot, kind/arity/non-empty checked at
  construction (SweepError::{Arity, KindMismatch, EmptyAxis}, the typed gate
  before any run). points() enumerates in odometer order (last axis fastest),
  deterministic.
- sweep(): closure-driven. The engine owns enumeration + disjoint execution
  + collection; the author owns run_one: Fn(&[Scalar]) -> RunMetrics (build
  fresh -> bootstrap_with_params -> run -> drain -> summarize). Metrics
  reduction is harness-specific sink glue the engine cannot generically own
  (C8/C18), and a fresh per-point build resolves #31's two structural facts:
  bootstrap_with_params consumes the blueprint, and a Recorder bakes its
  channel in — so each point gets its own drainable instance.
- SweepFamily / SweepPoint: ordered, self-describing (params carry the point
  coordinate), in enumeration order independent of thread completion.

Execution is std::thread::scope over available_parallelism() workers pulling
point indices from a shared atomic cursor (work-stealing balances uneven
per-point cost); results are tagged by index and sorted after join, so the
family is bit-identical across thread counts and repeated runs (C1: disjoint,
lock-free; order = enumeration, not completion). No external dependency —
std only, never rayon (C16: the engine workspace stays zero-dep for deploy
purity). A private sweep_with_threads(nthreads) lets the tests pin
determinism at 1 vs N workers while the public sweep() derives the count.

Out of scope this iteration: the `aura sweep` CLI demonstrator (cycle 0028
iter 2); random enumeration, per-point manifest assembly (#33), value-domain
validation (C20) are deferred per the spec.

One deviation from the plan's verbatim code: GridSpace derives Debug
(load-bearing — the three .unwrap_err() fault tests need the Ok type to be
Debug to compile).

Verified: 8 new sweep:: tests green (enumeration/odometer order, the three
typed faults, sweep == N independent runs, determinism across thread counts,
distinct-points-distinct-metrics); cargo test --workspace green (92 engine
lib tests, no regression); cargo clippy --workspace --all-targets
-- -D warnings clean.

refs #32
2026-06-10 18:36:35 +02:00
Brummel e2b6ab8e1b plan: 0028 param-sweep grid (iteration 1 — engine primitive)
refs #32
2026-06-10 18:30:47 +02:00
Brummel 0692637b60 spec: 0028 param-sweep grid
A grid-enumerated family of disjoint instances from one value-empty
blueprint (milestone The World, cycle C). Engine-side GridSpace +
sweep() (std::thread::scope, lock-free disjoint per C1) + SweepFamily;
closure-driven so harness-specific metrics glue stays author-side.

refs #32
2026-06-10 18:21:23 +02:00
Brummel 776bd5432a fix(aura-cli): emit valid DOT identifiers for inline-expanded composites
Observed bug in the graph viewer shipped in 66dff88: clicking `[+]` to
inline-expand a composite in `aura graph`'s HTML output renders a broken graph —
the interior nodes' edges collapse onto a single phantom node "0" with uncoloured
arrows, and the real pin docking is lost. The collapsed view and the body-drill
view both render correctly; only inline-expand breaks.

Root cause: in graph-viewer.js, genDot's `build` forms a node id by path-joining
the model keys (`cid = cpath.join("__")`). The aura model keys interior nodes by
numeric index (C23), so an inline-expanded composite yields ids like `0__0`. A
DOT identifier that begins with a digit but is not a pure numeral is invalid —
Graphviz parses `0__0` as the numeral `0` plus a leftover token, collapsing
`0__0`/`0__1`/`0__2` onto one phantom node `0`. The collapsed view survives only
because its ids are single-digit numerals (accidentally valid); the prototype
survived because it used name keys, never digit-prefixed ones. The bug surfaced
only against the real index-keyed model.

Fix: letter-prefix the id (`cid = "n" + cpath.join("__")`) so every id is a valid
identifier by construction. The fix lives in the viewer (DOT-syntax is its
responsibility), not in model_to_json — the model's index keys are correct, and
leaking the Graphviz identifier constraint into the model would be a leaky
abstraction.

RED-first guard (this establishes the project's first JS test infra, since the
viewer now carries real logic — normalizeModel + genDot — that the model golden
does not cover):
- tests/viewer_dot_ids.mjs: a headless node guard that loads the real viewer
  module, discovers the expandable composites from a collapsed pass, inline-
  expands them, and asserts every node id in the produced DOT is a valid Graphviz
  identifier (quoted | pure numeral | bare identifier). Version-independent — it
  checks the identifier grammar, not a render. The expand keys are DISCOVERED via
  genDot's `expandable`, never hard-coded, so the guard cannot go vacuously green
  when the fix reshapes the id (a hard-coded "0" key would stop matching "n0" and
  silently test only the collapsed view — a false-green this guard refuses).
- tests/viewer_dot.rs: a Rust integration test that shells out to `node`,
  wiring the guard into `cargo test --workspace`. `node` is required: if it is
  absent the test fails (a skipped guard is not a guard).
- tests/fixtures/sample-model.json: the triggering fixture (numeric index keys,
  a nested composite).
- graph-viewer.js: made node-loadable (typeof window/document guards + a
  module.exports) so the guard can exercise the pure generator headless. This is
  a test-enabler, behaviour-neutral in the browser — verified by the existing
  render_html / graph_emits self-containment tests staying green and by the
  expand re-rendering correctly through the vendored WASM-Graphviz.

Verified RED against the unfixed viewer (the guard trips on `0__0`/`0__1`/`0__2`
and the Rust test fails), GREEN after the prefix; full `cargo test --workspace`
green; clippy clean. Rendered the expanded state through the vendored
WASM-Graphviz to confirm the interior wires up correctly (price -> both SMA
series, SMA value -> Sub lhs/rhs, Sub -> signal, all f64-blue).
2026-06-10 17:48:49 +02:00
Brummel 53f3fcf198 audit: cycle 0026 (#51) — drift-clean; aura graph is a self-contained WASM-Graphviz viewer
Cycle-close audit for cycle 0026, iteration 2 (the graph render viewer). Scope
7569980..HEAD (plan c3109bd + feat 66dff88); iteration 1 (model serializer,
288f589) was already inside the 0027 audit close and was not re-reviewed.

Architect drift review: clean. Contracts verified against docs/design/INDEX.md:
- C9 — render_html(&Composite) does only model_to_json + string assembly; no
  eval / compile_with_params / bootstrap on the render path.
- C10 / C14 — the vendored viewer JS is a render asset (no node/strategy logic,
  no DSL); no serde added (model stays hand-rolled).
- C4 — graph-viewer.js TYPE_COLOR is exactly {i64, f64, bool, timestamp}; the
  unknown-kind fallbacks are not a fifth type colour.
- C23 — input-pin labels are the model's non-load-bearing names (cycle 0027);
  wiring stays positional.
- ascii-dag retirement complete: dep gone, graph.rs gone, Cargo.lock clean; the
  only surviving mention is the deliberate cli_run contract test documenting the
  retirement, and INDEX.md:668 (the cycle-0017 note's legitimate historical
  account of the renderer defect that motivated the flat layout). node.rs's two
  stale justifications are re-grounded.
- Vendoring hermetic: the ~1.4 MB WASM/JS blobs are checked in (not git-ignored),
  build.rs fetches nothing — no half-state contradicting the checked-in decision.

Regression: commands.regression is empty for this profile — Step 2 no-op; the
architect is the gate.

Resolution: carry-on (no drift items, no regression gate). Cycle 0026 is
drift-clean — the code matches the ledger. This is not a milestone close.

Next direction (architect): the milestone-adjacent render work — expose rendering
for a consumer's own graph (#28) and interactive blueprint navigation in the
playground (#37) — is the natural next render-area cycle, each a separate spec.
2026-06-10 17:13:30 +02:00
Brummel 66dff88528 feat(aura-cli): render the graph as a self-contained WASM-Graphviz viewer; retire ascii-dag
Iteration 2 of cycle 0026 (graph render redesign). `aura graph` no longer emits
ASCII: it now prints one self-contained `.html` to stdout — the ported prototype
pin-graph viewer (drill / inline-expand / styled tooltips / breadcrumb) driven by
the deterministic model serializer that shipped in iteration 1, with layout and
SVG done in-browser by Graphviz compiled to WebAssembly. Closes the redesign
opened by spec 0026; unblocked by cycle 0027 (input ports are now named, so the
viewer labels every input pin from the model's real names instead of inventing
"a"/"b").

What ships:
- crates/aura-cli/assets/: the ported graph-viewer.js (prototype genDot/chrome
  verbatim + a normalizeModel adapter mapping aura's real model tuple shape —
  ins = [kind, firing, name], index node keys, `comp` refs, `src_<role>` — into
  the viewer's native shape), plus the vendored Graphviz-WASM (@viz-js/viz@3.7.0)
  and svg-pan-zoom@3.6.1 blobs and a refresh-assets.sh provenance script.
- crates/aura-cli/src/render.rs: render_html(&Composite) -> String, a read-only
  (C9) assembly — model_to_json + include_str! of the inlined assets, no eval,
  no build, no serde (C14). The `["graph"]` arm calls it.
- Retired: the ascii-dag adapter (graph.rs), the `ascii-dag` dependency, the
  `--compiled`/`--macd` graph flag plumbing, render_compiled, the color/terminal
  plumbing, and the 12 ascii-dag-asserting tests + the now-orphaned helpers.
  The cli_run integration test now pins the HTML page envelope.
- crates/aura-core/src/node.rs: the two last ascii-dag prose justifications
  re-grounded (single-line label rule kept, re-justified generically; the
  param-generic rule re-justified on C23, not on a renderer limitation).
- docs/design/INDEX.md: the cycle-0026 C9 realization note (both iterations).

Vendoring decision (spec deferred it to the plan): the WASM/JS blobs are checked
in, not fetched at build time. include_str! needs them at compile time, and a
build-time unpkg fetch would make the build non-hermetic/non-offline and let the
shipped artifact drift with the registry — against C1 (determinism) and C8
(frozen artifacts). ~1.4 MB is the price of a hermetic, frozen render asset.

Three adaptations beyond the plan, each verified: (1) render.rs imports
`aura_engine::Composite` (the path model_to_json takes), not aura_core; (2)
macd_blueprint is now test-only — removing the `--macd` arm left it used solely
by the param-space test, so it took `#[cfg(test)]` rather than removal or an
`#[allow]` suppression (the production `aura run --macd` path is intact, verified
emitting JSON); (3) the `grep ascii-dag` over crates/ matches only the new
contract test, which names the term deliberately to document the retirement — no
stale justification prose remains.

Invariants held (verified independently, not on agent report): C9 (render path
read-only), C10 (viewer is a render asset, no logic/DSL), C4 (four-colour palette
= the four scalar base types), C14 (no serde). The iteration-1 model byte golden
is unchanged and green. cargo build --workspace: 0 errors. cargo test --workspace:
157 passed, 0 failed (incl. the two new HTML tests + the unchanged model_golden).
cargo clippy --workspace --all-targets -- -D warnings: clean. ascii-dag absent
from `cargo tree -p aura-cli`.

closes #51
2026-06-10 17:09:12 +02:00
Brummel c3109bdfdd plan: 0026 graph render viewer (iteration 2)
Iteration-2 plan for cycle 0026 (graph render redesign): port the prototype
viewer, vendor Graphviz-WASM (checked in, for hermetic offline builds), add a
read-only render_html, wire `aura graph` to emit the self-contained HTML, and
retire ascii-dag (adapter, dep, --compiled/--macd flags, 12 ascii-dag tests).
Plus a stale-prose tidy in aura-core and the cycle-0026 ledger note.

refs #51
2026-06-10 16:49:19 +02:00
Brummel 7569980dcd audit: cycle 0027 (#50) — drift-clean; input ports named
Architect drift review (288f589..e304dba): cycle 0027 introduced NO drift, no
contract weakened, no ledger edit beyond the cycle's own C8 realization note.

- C23 (identity positional, names non-load-bearing): held. PortSpec.name is read
  only by port_json / scope_json (render); derive_signature carries Role.name for
  display; wiring stays by index (Edge/Target), the bootstrap kind-check reads
  .kind, never .name. The compilat is untouched.
- C9 (read-only render): model_to_json still &Composite -> String, no
  eval/build/bootstrap/compile on the path.
- C14 (no serde): still hand-rolled format! JSON.
- C4 (four scalar base types): unchanged.
- Homogeneity goal met: the re-captured model golden names every input tuple at
  both leaf and composite level, matching outputs/params; nothing half-named.
- Verification (re-run by the orchestrator, not taken on the agent's word): cargo
  build --workspace clean; cargo test --workspace 168 green / 0 red; clippy
  --all-targets -D warnings clean.

Regression gate: profile configures no regression scripts (regression: []) — a
no-op this cycle; architect is the sole gate and it is clear.

Deferred (pre-existing debt, NOT 0027 drift): graph.rs:324 multi_output_field_name
carries a stale #43 comment ("leaf field names unavailable pre-build") — false
since cycle 0024; the leaf branch falls back to the field index. The file is the
ascii-dag renderer slated for retirement by #51 (0026 it2); the dead fallback
dies with the file there. Recorded as a comment on #51 so it is not lost — not a
tidy iteration, since fixing soon-to-be-deleted code is wasted effort.

#21 intentionally NOT closed (a swapped same-kind wiring is still only
kind-checked) — the cycle's stated scope boundary, not drift.
2026-06-10 16:25:59 +02:00
Brummel e304dbaae1 feat(aura-core): name input ports
PortSpec gains a non-load-bearing `name: String`, so an input port is named just
as FieldSpec.name (output) and ParamSpec.name (param) already are — input ports
were the lone unnamed member of the node signature. Identity stays positional by
slot (C23); the name is render/debug only, never read by bootstrap or the run
loop. PortSpec drops Copy (String is not Copy), exactly as ParamSpec already does.

- Every aura-std node names its input slots: SMA/EMA "series", Sub/Add "lhs"/"rhs",
  Exposure "signal", SimBroker "exposure"/"price" (the slots become
  self-documenting), LinComb "term[i]" and Recorder "col[i]" generated in their
  build loops (mirroring LinComb's existing weights[i] param loop).
- derive_signature carries a composite's Role.name into the derived input port
  (it was dropped before — the output side already carried FieldSpec.name), so the
  graph model is homogeneously named at both levels.
- model_to_json (port_json + the composite-header inputs in scope_json) emits the
  name as a third tuple element: ["f64","any","exposure"]. The byte golden was
  re-captured (machine bytes) and its substring twins updated; the model is now
  fully named across inputs/outputs/params.
- All 16 PortSpec construction sites threaded in one compile-gate change; test
  fixtures carry fixture names. C8 realization note added to the design ledger.

Why name-only, no validation: the name is a pure debug symbol. Wire-by-name was
rejected (it would be a C23 contract change). Bootstrap slot-wiring validation
(which would close #21's same-kind swap footgun) is deferred to its own cycle —
a name alone does not catch the swap; it makes the slots self-documenting and
gives a future validation something to check against.

Verified: cargo test --workspace 168 green; clippy --all-targets -D warnings
clean; cargo build --workspace clean. Read-only render path (C9), no serde (C14),
scalar kinds unchanged (C4).

closes #50
refs #21
refs #51
2026-06-10 16:19:08 +02:00
Brummel 5288249b44 plan: 0027 name input ports
Four tasks for cycle 0027 (spec docs/specs/0027-name-input-ports.md):
1. Add name: String to PortSpec (drop Copy) and thread all 16 construction
   sites in one compile-gate task — aura-std nodes get real slot names, fixtures
   get fixture names, derive_signature carries Role.name; port_json untouched so
   the byte golden stays green and the full suite passes.
2. port_json appends the name; re-capture the model byte golden and update its
   two substring twins (sink_has_empty_outs witnesses a leaf PortSpec.name in the
   model, multi_input_composite witnesses a composite Role.name carried through).
3. Lock the per-node input-slot names (8 aura-std pins).
4. C8 realization note in the design ledger (cross-links C23).

refs #50
2026-06-10 16:04:10 +02:00
Brummel 55cfb29c74 spec: 0027 name input ports
Give a node's input ports a name. PortSpec { kind, firing } is the lone unnamed
member of the node signature today (FieldSpec.name / ParamSpec.name already
exist); a leaf node's input slots have no declared semantics. Add a
non-load-bearing name: String to PortSpec (drops Copy, mirrors ParamSpec), name
every input slot across aura-std nodes + fixtures, carry Role.name into
derive_signature's derived composite port (it is dropped today), and make
model_to_json emit the name in each port tuple. Identity stays positional by
slot (C23); the name is render/debug only, never read by bootstrap or the run
loop.

Surfaced by the 0026 graph-render redesign: a homogeneous pin-graph needs the
name to label input pins instead of inventing "a"/"b". Single iteration.

Design ratified interactively (brainstorm); grounding-check PASS (every
load-bearing current-state assumption ratified by a named green test); parse
gate a no-op (no spec_validation parser configured). Forks resolved: name is a
pure debug symbol, not wire-by-name (C23); String not &'static str (variadic
nodes generate term[i]/col[i]); no bootstrap validation this cycle.

refs #21
refs #50
2026-06-10 15:53:46 +02:00
Brummel 288f58953e feat(aura-engine): graph model serializer (0026 it1)
Iteration 1 of the graph render redesign: a read-only model_to_json(&Composite)
that serializes the harness root + every distinct composite type into the
canonical, deterministic JSON model the browser viewer will consume. New
crates/aura-engine/src/graph_model.rs; lib.rs re-exports model_to_json.

- House style: hand-rolled JSON like RunReport::to_json, NO serde (C14).
- Model shape (faithful to the types, resolving the spec's prototype-flavoured
  example): node keys are indices (C23, the blueprint has no unique names);
  input ports carry kind+firing only, no name (PortSpec has none — acceptance
  criterion 3); a vec![] output is the C8 sink; bound root source-roles are
  minted as synthetic src_<role> source nodes; composite boundaries use @role
  (input) and #N (output) endpoints; distinct composites collected once.
- Improvement over the plan: a composite's input kind is read from the actually
  fed interior slot (matching blueprint::derive_signature) rather than a hardcoded
  f64 — invents nothing.
- Read-only (C9): &Composite -> String, no eval/compile/bootstrap on the path.
- 10 new tests green: byte golden on the sample harness, determinism, structural
  mis-wire differs (param swaps are invisible to the pre-compile model — the
  property is carried by a structural re-target), read-only witness, sink /
  two-input / distinct-once structural assertions. Workspace green, clippy clean.

aura graph still renders via ascii-dag (untouched); iteration 2 ports the viewer
JS, vendors the WASM-Graphviz asset, emits the self-contained HTML, and retires
ascii-dag.
2026-06-10 11:18:28 +02:00
Brummel bbe2266b9e plan: 0026 graph model serializer
Iteration 1 of the graph render redesign: a read-only model_to_json(&Composite)
in a new crates/aura-engine/src/graph_model.rs, hand-rolled deterministic JSON
(RunReport::to_json house style, no serde). Six tasks: helpers, primitive record,
scope (index keys + synthetic source nodes from bound roles), composite defs
(@role/#N endpoints, distinct-once walk), top-level + byte golden + determinism,
structural assertions + structural-miswire-differs + read-only. Decisions: home
in aura-engine; node keys are indices (C23); input ports carry kind+firing, no
name (per acceptance criterion 3); bound root source-roles minted as synthetic
source nodes.
2026-06-10 11:10:23 +02:00
Brummel cdb6313ee8 spec: 0026 graph render redesign
Replace the ascii-dag aura graph view with a homogeneous pin-graph: aura emits a
deterministic JSON model (hand-rolled, RunReport::to_json house style, no serde);
a vendored viewer JS (genDot, ported from the prototype) turns it into DOT;
Graphviz-WASM lays out + draws it in the browser. aura stays a read-only
serializer (C9), ships no layout engine. The golden-tested contract is the JSON
model. Iteration 1: the model serializer + golden/determinism/read-only tests.
Iteration 2: viewer + WASM vendoring + self-contained HTML, retire ascii-dag.

Grounding-check PASS; design ratified interactively against the 0026 prototype.
2026-06-10 10:58:47 +02:00
Brummel 7f485bbe72 docs(design): graph-render redesign prototype
Interactive, throwaway prototype of the aura graph render redesign — the
visual language ratified for cycle 0026, replacing the ascii-dag text view.
Homogeneous pin-nodes (body + n input pins + m output pins) rendered via
Graphviz-WASM in the browser; pin-to-pin edges, type-as-colour, drill-down +
inline-expand of composites, per-element styled tooltips, source/sink colouring,
top-rank input ordering. index.html is the artifact; the ~1.4 MB WASM dep is
git-ignored and restored by fetch-deps.sh. Visual companion to docs/specs/0026.
2026-06-10 10:58:47 +02:00
Brummel 63af2d1095 audit: cycle 0025 (#49) — drift-clean; root renders like any composite
Architect drift review (d6f59d7..12b1bce): NO code drift, no contract weakened,
no ledger edit needed.

- C9 (read-only render): the change touches only label/entry assembly
  (render_blueprint, leaf_label, render_graph signature); no eval, no run-path
  code. 150 tests green (same count), clippy clean, build green.
- C12 ("mis-wiring visible before a run"): realized at the root, not overstated —
  a top-level fan-in's wiring was invisible (bare [SimBroker]); it now surfaces
  which producer feeds which slot ([SimBroker(#E,#price)]). The C8 label-render
  refinement (INDEX.md:250, "label() surfaces a mis-wiring") is extended to the
  root, not contradicted.
- C23 (names dissolve post-inline): render_compilat byte-untouched (still emits
  source:{kind} from SourceSpec.kind); the [price] pre-inline / [source:F64]
  post-inline split now renders C23's boundary-dissolution visibly, with
  compiled_view_golden as the negative control.
- C3 (sources bind at ingestion only): root entries filtered to source.is_some()
  bound roles — the one semantic root-vs-interior distinction, correctly retained.
- #49 acceptance: the Option<&Composite> drop unifies both render_graph callers
  onto one stub path; no dead arm, no second/root-only stub function, no scope
  creep beyond graph.rs (production) + main.rs (test-only goldens/assertions).

No ledger prose names the root source:{kind} marker as canonical; the C9
cycle-0017 realization (where: definitions render) is not stale. No new invariant
introduced — the cycle realizes existing contracts more fully, so no realization
note is manufactured.

Regression: none configured in the profile (no-op) — the architect review is the
gate. Resolution: carry-on (drift-clean). NOT a milestone close.

Tracker reconciliation done this cycle (no code, orchestrator-applied):
- #36 closed — 0024 resolved it (params declared once on the recipe; the 8
  lockstep tests deleted), but Gitea's `closes #43 #36` only auto-closed #43.
- #25 closed — 0024 removed the dyn Node::schema() re-call + bootstrap-derivation
  duplication this issue named (signatures gathered once into FlatGraph).
- #24 retitled to derive_signature()/signature() — the panic-vs-typed-error
  divergence persists under 0024's renamed symbols; still open, low-priority.
2026-06-09 18:16:57 +02:00
Brummel 12b1bcec22 feat(aura-cli): render the root composite like any other composite
Close #49 and remove the last two render special-cases the blueprint root still
carried, both pre-0024 vestiges. Since cycle 0024 the root is an ordinary
Composite with input_roles, so the render no longer needs to treat it apart:

(A) Fan-in slot stubs at the root. A composite-interior multi-input leaf already
    renders its wired slots as #… stubs; a top-level leaf did not, because
    render_blueprint threaded stub_ctx: None. It now passes the root composite
    (it IS a &Composite, carrying roles + edges), and stub_ctx drops its Option
    — both render_graph callers pass &Composite. The existing slot_source/
    fan_in_identifiers/signature_of serve a top-level leaf verbatim; no second
    stub path (#49 acceptance). SimBroker now renders [SimBroker(#E,#price)]
    (#E = the Exposure producer's sibling-unique signature prefix, #price = the
    price source role name).

(B) Role-named root entries. render_blueprint built entry markers from
    format!("source:{kind}") while render_definition uses role.name — the
    marker/stub asymmetry. The root now builds entries from role.name (filtered
    to bound roles, source.is_some()), identical to the interior, so the source
    marker reads [price] and matches the #price slot stub it feeds.

render_compilat is deliberately untouched: post-inline the role name has
dissolved (C23), so the compiled view keeps [source:F64] from the flat
SourceSpec.kind — [price] pre-inline / [source:F64] post-inline is C23 made
visible, and compiled_view_golden is the byte-unchanged negative control.

Design call (orchestrator, with the user): the marker/stub asymmetry was fixed at
its source (the entry naming) rather than by special-casing the stub to match the
kind-named marker — the latter would have been a root-only stub behaviour,
violating the "no second stub path" acceptance. The user chose full symmetry
([price], dropping the kind annotation) over a name+kind marker, prioritising
structural cleanliness; the only remaining root-vs-interior distinction is the
bound-role filter (C3), which is semantic, not cosmetic.

Scope: crates/aura-cli/src/graph.rs only; read-only render (C9),
behaviour-preserving for the run path (C1), no engine change.
blueprint_view_golden re-captured from the live `aura graph` output (the
where: section stays byte-identical — only the main graph re-flows); the
SimBroker needle, a root-SimBroker-stub assertion on the macd view, and a [src]
role-name-passthrough assertion are added.

Verification (orchestrator-run, not agent-reported): cargo build --workspace
green; cargo test --workspace 150 passed / 0 failed (same count, no run-path
test moved); cargo clippy --workspace --all-targets -D warnings clean.
compiled_view_golden and render_compilat byte-untouched.

closes #49
2026-06-09 18:11:22 +02:00
Brummel cd569aa6d0 plan: 0025 render root like any composite
Two-task plan for spec 0025 (#49). Task 1 is one compile unit in
crates/aura-cli/src/graph.rs: thread the root composite as stub_ctx (drop the
Option, update both render_graph callers) and name root entries by role.name
instead of source:{kind}; gate is `cargo build -p aura-cli` (goldens stay red
until Task 2 — the golden-recapture exception). Task 2 re-captures
blueprint_view_golden from the live `aura graph` output, updates the SimBroker
needle, adds a root-SimBroker-stub assertion to the macd test and a [src]
role-name-passthrough assertion to reused_composite_defined_once; gate is
`cargo test --workspace` + clippy. compiled_view_golden is the byte-unchanged
negative control (render_compilat path, C23). plan-recon mapped exact line
numbers and flagged the source:{kind} golden twin (blueprint changes /
compiled must not) and the stale, out-of-scope render_clustered.txt fixture.
2026-06-09 17:56:51 +02:00
Brummel ed1d22bbfa spec: 0025 render root like any composite (slot stubs + role-name entries)
Settled source: issue #49, enabled by cycle 0024 (1b39093, root is now a
Composite with input_roles). The cycle removes the last two render special-cases
the root still carries, both pre-0024 vestiges:

(A) render_blueprint threads stub_ctx: None, so a top-level multi-input leaf
    (SimBroker) renders bare [SimBroker] instead of the #… slot stubs an interior
    leaf already gets. Fix: pass the root composite as stub_ctx (it IS a
    &Composite since 0024); stub_ctx drops its Option (both render_graph callers
    pass &Composite). The existing slot_source/fan_in_identifiers/signature_of
    serve a top-level leaf verbatim — no second stub path (#49 acceptance #3).
    -> [SimBroker(#E,#price)] (#E = exposure producer's signature prefix,
    #price = the source role name).

(B) render_blueprint names root entries source:{kind} while render_definition
    names interior entries by role.name — the marker/stub asymmetry. Fix: build
    root entries from role.name (bound-only filter retained), identical to the
    interior. -> [price] instead of [source:F64], byte-symmetric with how the
    same role renders in a where: definition.

render_compilat is left as [source:F64]: post-inline the role name has dissolved
(C23), only the kind survives — [price] pre-inline / [source:F64] post-inline is
C23 made visible, and compiled_view_golden is the negative control.

Scope: crates/aura-cli/src/graph.rs only. Read-only render (C9),
behaviour-preserving for the run path (C1); no engine change. Grounding-check
PASS (10 assumptions ratified against green tests). Render goldens are the
accepted value-asserted regression; closes #49 on land.
2026-06-09 17:47:18 +02:00
Brummel d6f59d7a52 audit: cycle 0024 (#43, #36) — drift-clean; ledger reconciled to the pre-build signature
Architect drift review (862882b..1b39093): NO code drift, no contract weakened.
C1 (behaviour-preserving: 150 tests green, render goldens byte-identical, no
behavioural assertion altered), C8 (sink output: vec![] preserved), C9/C23
(read-only render touches no eval/build; derive_signature's Box::leak is
cold-path-only, leaked names non-load-bearing) all hold. Regression scripts:
none configured (no-op) — the architect review is the gate.

The only drift was design-ledger prose, anticipated and deferred to this audit
by the plan. Resolved here (fix path, orchestrator-applied — docwriter is barred
from the design ledger):

- docs/design/INDEX.md C8 Guarantee: rewritten — a node's signature (NodeSchema)
  is declared pre-build on the value-empty recipe; the built node implements
  lookbacks() (the one param-dependent sizing quantity) + eval(); schema() is gone.
- INDEX.md C8 cycle-0015 note: Blueprint::param_space -> Composite::param_space;
  the #36 lockstep-debt clause marked closed by 0024.
- INDEX.md: added a C8 cycle-0024 realization (signature lives once on the recipe,
  the signature/sizing split, #43/#36 closed) and a C19 cycle-0024 realization
  (struct Blueprint collapsed into the root Composite; Role.source/runnable-iff-
  bound/UnboundRootRole; FlatGraph as the named compilat). Stale symbol names in
  the dated 0016/0017 notes annotated in place with their 0024 renames.
- aura-std/src/lincomb.rs module doc: Blueprint::param_space -> Composite::param_space.

Ledger-gap (four new load-bearing invariants previously unrecorded) closed by the
two new realization notes. Green baseline re-confirmed after the edits: cargo test
--workspace 150 passed / 0 failed; cargo clippy --all-targets -D warnings clean.

refs #43 #36
2026-06-09 13:02:42 +02:00
Brummel 1b3909316e feat(aura): node signature lives in the blueprint; collapse Blueprint into Composite
Consolidate the node data structure so every node's signature (NodeSchema:
inputs/output/params) is declared once and exists in the blueprint pre-build,
and dissolve the special "root graph" type. Behaviour-preserving (C1).

Signature vs sizing
- NodeSchema is now the static signature only: InputSpec -> PortSpec{kind,firing},
  with lookback removed. The signature is fully static per blueprint (input
  kinds/firing, output fields, params); LinComb's variable arity is a builder arg,
  not an injected param.
- The one param-dependent quantity, an input's buffer lookback (e.g. Sma's window =
  its injected length), moves out of the signature to Node::lookbacks() -> Vec<usize>,
  read only by bootstrap for sizing. Node::schema() is removed.
- LeafFactory -> PrimitiveBuilder, which carries the full NodeSchema. The built node
  no longer re-declares it: closes the params-declared-twice drift (#36, the 8
  per-node factory_params_match_built_node_schema lockstep tests are deleted — their
  subject is now structurally impossible) and a value-empty recipe exposes its full
  I/O interface pre-build (#43).

Root is just a bound composite
- struct Blueprint is deleted; its compile/bootstrap/param_space methods move onto
  Composite. Role gains source: Option<ScalarKind> (None = open interior port,
  Some = bound ingestion feed). A composite is runnable iff every root role is bound;
  the "main graph" is no longer a category, only the fully-source-bound composite.
  New error CompileError::UnboundRootRole for an open root role.
- BlueprintNode::signature() answers uniformly for both arms: Primitive returns the
  builder's declared schema, Composite derives it from the interior (role kinds in,
  OutField kinds out, aggregated params), pre-build, no build.

compile -> FlatGraph -> bootstrap
- compile validates structure pre-build via signature() (validate_wiring: range +
  kind, returning the same variants as before, so an edge kind fault is now caught
  before any build closure fires) and emits FlatGraph{nodes,signatures,sources,edges}.
- bootstrap consumes the FlatGraph: kinds/firing/output from the carried signatures,
  buffer depth from node.lookbacks(). SourceSpec survives as the flat descriptor.

Renames: BlueprintNode::Leaf -> Primitive, LeafFactory -> PrimitiveBuilder.

Render (aura-cli/src/graph.rs) is migrated compile-only: it takes &Composite, maps
bound roles to the same source-entry shape, so both render goldens reproduce
byte-identical output (no re-capture needed). Render-fidelity tuning is the next cycle.

Verification (orchestrator-run, not agent-reported): cargo build --workspace green;
cargo test --workspace 150 passed / 0 failed; cargo clippy --workspace --all-targets
-D warnings clean. All pinned determinism/run-output tests pass with values unchanged;
no behavioural assertion was altered to go green. 5 new tests assert the signature is
pre-build and uniform, that compile rejects a kind mismatch without building (via a
panicking builder), UnboundRootRole, and lookbacks()/signature arity agreement.

Deferred to cycle-close audit (per plan): docs/design/INDEX.md and some aura-std
module docs still name the old Node::schema()/LeafFactory/BlueprintNode::Leaf/
Blueprint::param_space contracts; prose reconciliation is the architect's at audit.

closes #43 #36
2026-06-09 12:49:22 +02:00
Brummel c7fc2f6a37 plan: 0024 node signature in blueprint
Five-task, compile-gate-sequenced plan for the behaviour-preserving refactor:
aura-core types (PortSpec/PrimitiveBuilder/NodeSchema/lookbacks) -> aura-std
8 nodes -> aura-engine lib (Composite absorbs Blueprint, signature/derive_
signature, FlatGraph, pre-build validate_wiring, bootstrap reshape) -> engine
tests -> aura-cli/ingest + compile-only render. Per-crate partial build gates;
workspace gate only in the final task. One working-tree unit (committed once
after Task 5 green).

refs #43 #36
2026-06-09 11:30:38 +02:00
Brummel 78d68fa8ad spec: 0024 node signature in blueprint
Consolidate the node data structure so every node's signature (NodeSchema:
inputs/output/params) exists in the blueprint pre-build, declared once.

- Signature vs sizing split: NodeSchema becomes the static signature
  (InputSpec -> PortSpec, lookback removed); the one param-dependent quantity
  (input buffer lookback) moves to a build-time Node::lookbacks() query.
  Node::schema() is removed.
- PrimitiveBuilder (ex-LeafFactory) carries the schema; the built node no
  longer re-declares it -> closes the params-declared-twice drift.
- Blueprint collapses into Composite: Role gains source: Option<ScalarKind>;
  the root is the fully-bound composite (no "main graph"); new error
  UnboundRootRole.
- compile -> FlatGraph -> bootstrap: FlatGraph carries node + signature in
  parallel; bootstrap sizes from lookbacks(), kind-checks from the signatures.
- Renames: BlueprintNode::Leaf -> Primitive, LeafFactory -> PrimitiveBuilder.

Behaviour-preserving (C1). Render is out of scope beyond compiling (next cycle).

refs #43 #36
2026-06-09 11:09:06 +02:00
Brummel 862882bc04 audit: cycle 0023 (#48) — drift reconciled; render paths unified
Architect drift review (bb90c42..481172a): no contract drift. C9 (render
reads structure + label()/params()/output() only, never eval), C8 (sink =
zero-output leaf, no special case), C12 (blueprint = root composite is
render-structural only; types stay distinct), C23 (consumer-prefix mirrors
the := binding), #38 (composites stay opaque) — all hold. Three fidelity
items, resolved:

- [high, fixed] spec acceptance over-claimed: the SimBroker slot-stub box read
  as in-scope though the work is deferred. Spec amended — the stub acceptance
  is annotated against #49, the met boxes checked.
- [medium, recorded] no docs/plans/0023: this cycle ran spike→refactor under
  user steering instead of planner→implement, because the edge-label-vs-
  consumer-side fork needed an empirical probe (ascii-dag silently drops edge
  labels on collision) before a placeholder-free plan was possible. A plan
  RECORD (not a task plan) now holds the 0023 slot so the counter pairing
  stays intact and the next cycle takes 0024.
- [low, carry-on] multi_output_field_name's leaf-producer fallback is latent
  and untested (no multi-output leaf in the corpus). Carried as debt.

Regression scripts: none configured (no-op). Build/test/clippy green.
Deferred stub work is tracked in #49 (user-signed).

closes #48
2026-06-09 00:15:50 +02:00
Brummel 481172ae2e feat(aura-cli): unify blueprint main-graph render through one shared core
render_blueprint and render_definition were two divergent paths: the main
graph drew bare factory.label() leaves and bare edges, the composite interior
drew enriched leaves (params, slot stubs, output bindings). They now delegate
to one shared render_graph over borrowed slices — the blueprint is the root
composite, differing only at the borders (ParamNames::Factory vs Aliases,
Entry unifying SourceSpec and Role, empty OutField list = no bindings).

LeafFactory holds a Box<dyn Fn>, so it is not Clone — an owned root-composite
adapter is impossible; the shared core takes borrowed slices instead.

The main graph now shows what the strategy does: a multi-output producer's
selected field surfaces as a consumer-side prefix ([histogram -> Exposure(scale)]),
mirroring the := output-binding form. Edge labels are deliberately avoided —
ascii-dag 0.9.1 silently drops an edge label it cannot place without collision
(arena_render.rs can_place_label), unreliable in a dense graph.

Deferred: top-level blueprint multi-input leaves render without #-slot stubs
(stub_ctx None) — fan_in_identifiers is still composite-coupled via signature_of.

refs #48
2026-06-09 00:09:17 +02:00
Brummel 5dd9503e40 spec: 0023 unify blueprint main-graph render
The CLI blueprint main-graph render is a separate, poorer path than the
composite-definition render: bare leaves, bare edges, dropped from_field.
Unify both through the composite machinery — the blueprint is the root
composite, differing only at the borders (sources, factory params, sinks
as zero-output terminal leaves). Harness signature header deferred.

refs #48
2026-06-08 23:04:38 +02:00
Brummel bb90c42028 audit: cycle 0022 (#46) — drift reconciled; output bindings shipped
Architect drift review (range 69d2094..HEAD), regression gate empty
(no scripts configured). Status: drift_found, all resolved here or queued.

What holds: C8/C23 intact — output_binding folds OutField.name onto the
producer label as a pure render symbol; compiled_view_golden byte-identical,
no name reaches the compilat; the drawn where: graph is now exactly the
computation DAG (false terminals + node-count inflation gone).

Resolved (doc reconciliation, this commit):
- docs/design/INDEX.md: two stale render descriptions brought current —
  the C9/0018 realization said output names render as `[out:<name>]`
  markers; the C19/0017 realization said `[in:k]`/`[out]` port markers.
  Both superseded by the `name := producer` binding form (the `[out]`
  staleness is cycle-0022's; the adjacent `[in:k]` was pre-existing drift
  from 0019/0020 and is reconciled in the same clause while here).
- docs/specs/0022: post-ship note added — its Components/Testing strategy
  under-counted the blast radius, missing the full-render golden
  blueprint_view_golden (forced re-capture, caught at implement).

Queued (not fixed): output_binding's tuple arm `(a, b) := …` is spec'd +
shipped but unexercised (no fixture re-exports >1 field from one node) —
filed as #47 (idea).

cycle 0022 drift-clean after this reconciliation (not a milestone close).
2026-06-08 17:20:18 +02:00
Brummel 3b496883e6 feat(aura): render composite output re-exports as producer bindings
In the blueprint view's `where:` section, a composite's output re-exports
now fold onto their producing node's label as a binding
(`[macd := Sub(#Ef,#Es)]`) instead of each becoming a standalone terminal
node wired from its producer. An OutField re-exports an existing interior
node's value — it is never a new node — so the old form drew false
terminals (MACD's macd line feeds the signal EMA and histogram internally
yet rendered as a dead-end leaf) and inflated the node count by the output
arity (MACD where: 8 -> 6 nodes; sma_cross drops its [cross] stub + edge).
The drawn graph is now exactly the computation DAG; the `:=` prefix is the
visual signal that a node's value escapes the boundary. Input roles stay
standalone entry nodes (no producer to annotate) — the asymmetry is
deliberate.

render_definition drops its per-OutField node+edge loop; a new
output_binding helper yields the `name := ` / tuple `(a, b) := ` prefix.
Pure render layer: C8/C23 untouched, OutField.name never reaches the
compilat, compiled_view_golden byte-identical, MACD run deterministic and
unchanged in metrics.

Test blast radius was wider than spec 0022 §Components / the plan's Scope
note counted: besides the MACD pinning test, four more sites assert a
producer that carries an OutField and so gain the `:=` prefix —
blueprint_view_defines_each_composite_once, the two fan-in identifier
tests, the cli_run integration test, and (missed by the plan, caught at
implement) the full-render blueprint_view_golden. All re-pins are forced
by the design, zero judgement. The output_binding tuple arm is unexercised
by current fixtures (no composite re-exports >1 field from one node).

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

closes #46
2026-06-08 17:16:02 +02:00
Brummel b2f27f8590 plan: 0022 composite output binding render
RED-first: re-pin all five render assertions whose producer carries an
OutField (macd, sma_cross cross, two fan-in `o` outputs — across main.rs
+ cli_run.rs) to the `:= ` binding form, then fold output_binding onto
the producer label in render_definition. Blast radius wider than spec
§Components named (flagged for audit).

refs #46
2026-06-08 17:10:54 +02:00
Brummel 7d4740e8b2 spec: 0022 composite output binding render
Render composite output re-exports as producer-label bindings
(`name := <producer-label>`) instead of standalone terminal nodes:
folds the output name onto its producing node, dropping the false
terminals and the node-count inflation (MACD where: 8 -> 6). Pure
render layer; C8/C23 untouched, compiled view byte-identical.

refs #46
2026-06-08 17:04:16 +02:00
Brummel 69d20949c0 tidy(aura-engine): single-own alias-validity + shared alias-count anchor
Collapse the two correctness-neutral debt items the 0021 fan-in pre-pass
left behind (architect drift review at cycle close).

Alias-validity: the `BadInteriorIndex` check was raised in two places —
the structural pre-pass `check_composite_fan_in` and `inline_composite`.
Since `compile_with_params` runs the pre-pass over every composite before
any lowering, `inline_composite`'s copy was dead for that error path. The
check now lives in one helper `check_alias_indices`, called solely by the
pre-pass; `inline_composite` drops its copy (and the now-unused
`param_aliases` binding — the overlay is a pure naming layer, unused in
lowering, which is driven by the injected scalar vector).

Alias-count: the per-node `a.node == node` predicate was re-derived in
three spots (signature base, the unaliased-param test, the CLI render
base-length). All three now route through one exported anchor
`aliases_on`, so an alias-semantics change touches one site.

Behaviour-preserving: the full workspace suite is the guard (15+6 cli,
25 core, 69 engine, 6+1+1 ingest/misc, 29 std — 0 failed),
`out_of_range_param_alias_rejected` still green (now raised by the
pre-pass), clippy -D warnings clean. No ledger change (the C9 refinement
landed in 0021).

closes #45
2026-06-08 16:38:09 +02:00
Brummel 04c2280dd8 audit: cycle 0021 (#44) — drift-noted; fan-in distinguishability shipped
Architect drift review over efbe0f4..3f4d756 (the two un-audited interim
render commits 32ac8a3/b73d707 plus the 0021 fan-in cycle).

Holds:
- C23 intact — the IndistinguishableFanIn check is a construction-phase
  pre-pass; the compilat stays name-free (compiled_view_golden byte-stable).
- C9 boundary clean — signature distinguishability lives at the blueprint
  layer and dissolves at inline; the C9 ledger refinement is accurate and
  in-format.
- Pre-pass placement (before the arity gate, not inside inline_composite) is
  the spec-aligned home, not drift: a no-param compile() of a param-bearing
  composite would hit ParamArity first.

Regression: commands.regression is empty (no gate) — architect is the sole
gate; clean of contradictions.

Debt (correctness-neutral, not blocking) tracked in #45 for a focused tidy
iteration rather than rushed at cycle tail:
- inline_composite's alias-validity loop is now dead for BadInteriorIndex (the
  pre-pass raises it first for every composite).
- per-node alias count is re-derived in three sites (signature_of base,
  leaf_has_unaliased_param, signature_base_len).

refs #45
2026-06-08 15:53:42 +02:00
Brummel 3f4d756ed2 feat(aura): fan-in input distinguishability — construction constraint + source-derived render
A fan-in node (>1 input) whose colliding sources hide an unnamed configuration
axis is now illegal at construction, and the definition view renders each fan-in
input as a source-derived recursive-signature identifier instead of the
positional, meaningless #A/#B.

The root defect was sma_cross: two Sma into a Sub with unaliased length params,
rendering sma_cross() -> (cross) with two indistinguishable [SMA] and
[Sub(#A,#B)] — the author meant fast vs slow but the identity hid it. Now it
renders sma_cross(fast:i64, slow:i64) with [SMA(fast)]/[SMA(slow)] and
[Sub(#Sf,#Ss)].

Shape (single source of truth for "signature" shared by engine + CLI):
- aura-engine signature_of(): a node's recursive authoring identity — type
  initial + alias initials + each wired input's signature (recursing into
  interior leaves, stopping at named roles/composites at their initial).
- aura-cli leaf_label/fan_in_identifiers: shortest sibling-unique prefix of a
  source's signature, never below its base (type + alias initials); a role-fed
  slot renders its name verbatim (#price); equal-signature interchangeable
  inputs keep the positional letter.
- aura-engine IndistinguishableFanIn: a signature collision is a fault only when
  a colliding source carries an unaliased param (the unnamed axis); param-less
  interchangeable sources (fan_composite's Pass/Pass) stay legal.

Param-aware criterion (refined during design): keeps the blast radius to the
param-bearing alias-less fixtures (sma_cross CLI + engine, the nested fast_slow);
fan_composite and the hand-wired flat fixtures stay green.

Placement decision (deviates from the plan): the constraint runs as a structural
pre-pass (check_fan_in_distinguishability) at the head of compile_with_params,
BEFORE the param-arity gate — not inside inline_composite as the plan drafted.
Reason: the no-param compile() of a param-bearing composite would hit ParamArity
first and preempt the fault; the spec mandates a structural check needing no
param values, so the pre-pass is the spec-aligned home. Alias-validity ordering
(BadInteriorIndex before the fan-in fault) is preserved at the pre-pass head.

C23 untouched: the check is construction-phase; the compilat stays name-free
(compiled_view_golden byte-stable). Ledger C9 carries the refinement.

Known debt (non-gating): the alias-index validity check now exists in both
inline_composite and the pre-pass (the pre-pass reaches every composite first,
making inline_composite's copy effectively dead). Left for a separate tidy — a
5-line, correctness-neutral removal with its own test surface.

closes #44
2026-06-08 15:50:19 +02:00