Commit Graph

18 Commits

Author SHA1 Message Date
Brummel ff4a1b3d4a feat(0092): run-from-blueprint infrastructure — restructure + topology_hash + shared seam (Tasks 1-3)
Cycle-1 infrastructure for #165 (World/C21). Behaviour-preserving + workspace
green; the CLI arm + tests (Tasks 4-5) follow.

Task 1 — restructured stage1_r_graph into stage1_signal() (the serializable
signal leg: SMA-cross -> Bias, a `price` input-role + a `bias` output) +
wrap_stage1r(signal, ...) (broker/sinks/exec/cost around a nested signal), with
stage1_r_graph() = wrap_stage1r(stage1_signal(...)). DEVIATION from the plan's
"callers untouched" claim, verified behaviour-preserving: nesting the signal
prefixes its param-space names (stage1_signal.fast.length), which broke the
sweep's bare .axis("fast.length"); fixed exactly as the #137 stop-knob machinery
does — FAST_LENGTH_SUFFIX/SLOW_LENGTH_SUFFIX suffix-resolution + stage1_r_friendly_name
arms mapping the prefixed names back to the bare manifest names. All observable
behaviour (manifest names, member keys, metrics, traces) byte-identical; the flat
graph and every metric unchanged (full suite green, incl. sweep/walkforward/MC).

Task 2 — RunManifest.topology_hash: Option<String> (Tier-1 optional, serde
default/skip — old manifests byte-identical, #156), threaded None into all 24
literal sites across 7 crates. Also fixed the aura-registry RunManifestRead
read-mirror to carry topology_hash (it was hardcoding None on load) — else a
stored hash would silently drop on the registry round-trip once Task 3 stores a
real one.

Task 3 — the shared run_signal_stage1r seam (hash the signal, wrap, compile with
params, bootstrap, run, manifest with params from param_space + topology_hash) +
the sha256_hex topology_hash helper (sha2 in aura-cli, off the frozen engine,
invariant 8). run_stage1_r now carries its topology_hash too (every run becomes
self-identifying, #158). The seam is unwired until Task 4 (transient
#[allow(dead_code)], retired there); RED-first seam tests added.

Verified: cargo test --workspace green (51 suites, 0 failures); cargo clippy
--workspace --all-targets -D warnings clean.

refs #165
2026-06-30 20:23:42 +02:00
Brummel 0a06afed29 feat(0078): cross-instrument generalization as a validation step
Last piece of the Inferential-validation milestone (#146): a new
`aura generalize` subcommand grades how consistently a single candidate
holds across a set of instruments — the across-instrument axis of the
same anti-false-discovery concern as trials-deflation (#144, within one
family) and plateau-over-peak (#145, within one instrument's surface).

Aggregator/validator, not a third selector (user-ratified): it runs a
brought candidate (a single-cell stage1-r grid — --fast/--slow/--stop-
length/--stop-k pinned to single values) across an instrument list
(--real SYM1,SYM2,...), collects per-instrument R-metrics, and reduces
them to a worst-case floor (min over instruments) + a sign-agreement
count + the per-instrument breakdown. R-only (C10 — R is the only
account/instrument-agnostic unit); the worst-case min is the cross-
instrument sibling of PlateauMode::Worst and satisfies the non-
domination constraint by construction (a strong instrument cannot lift
a min). Rejected: a cross-instrument t-stat mean/std*sqrt(M), which
reintroduces the pooled mean the milestone warns against.

- RunManifest gains a first-class `instrument` lineage field (serde-
  widened, C14/C18 — legacy lines load as None, existing manifest bytes
  unchanged); the compat read-mirror threads it in lockstep.
- FamilyKind::CrossInstrument (C12 comparison axis); the M per-instrument
  runs persist as a CrossInstrument family, each member self-identifying
  via its stamped instrument. The aggregate is recomputable from the
  members (McAggregate precedent), printed live.
- The generalization reduction lives registry-side beside optimize_
  deflated/optimize_plateau (C9), reusing resolve_metric/metric_value/
  is_r_metric; check_r_metric is the data-free pre-check the CLI runs
  before evaluating any instrument.
- Additive: existing sweep/walkforward/mc/standalone paths stay byte-
  identical (C23 — instrument omitted when None via skip_serializing_if).

Verified by the orchestrator: cargo test --workspace green (0 failed —
the new generalization_*, check_r_metric, parse_generalize_*, and the
gated cross-instrument E2E all pass; the E2E ran the real GER40/USDJPY
path on this host, exit 0); cargo clippy --workspace --all-targets
-D warnings clean.

closes #146
2026-06-26 19:04:53 +02:00
Brummel cb32658fd1 feat(0077): prefer a robust parameter plateau over the in-sample peak
Add an opt-in plateau selection objective for walk-forward, recorded on the
same RunManifest.selection carrier #144 introduced. Default argmax is
byte-identical (C23); plateau is reached only via a new --select flag.

What landed:
- FamilySelection / SelectionMode reshape (report.rs): the selection RULE
  (mode) and its ANNOTATION are orthogonal — deflation fields become Option
  (present iff Argmax), plus PlateauMean/PlateauWorst variants and
  neighbourhood_score/n_neighbours (present iff Plateau*). Legacy lines still
  load: the deflation scalars deserialize from bare values via serde default
  (C14/C18). compat.rs embeds FamilySelection by value — reshape flows through
  untouched.
- GridSpace::axis_lens() + SweepBinder::sweep_with_lattice (engine): the grid
  radixes in param_space()/odometer order. sweep() delegates to
  sweep_with_lattice with the lattice dropped, so every existing .sweep()
  caller is byte-unchanged.
- optimize_plateau + PlateauMode + closed_neighbourhood (aura-registry, C9):
  each member scores as the mean/worst of its closed mixed-radix grid
  neighbourhood's metric_value; the winner is the smoothed argmax by the
  metric's direction (earliest-odometer tie, as optimize). Pure, no RNG (C1);
  in-sample only (C2). A higher_is_better helper now sources the per-metric
  direction once, shared by metric_cmp and optimize_plateau.
- CLI --select <argmax|plateau:mean|plateau:worst> (default argmax; unknown
  token exits 2). walkforward_family dispatches via a select_winner helper;
  sweep_over/stage1_r_sweep_over carry the lattice as Option<Vec<usize>>.
  runs-family display gains a plateau(<mode>)=<score> over <n> cells line.

RandomSpace-refuse: walkforward has no --random producer, so the
plateau-without-lattice guard is structural — select_winner returns the
refuse on a None lattice (the caller prints the message and exits 2),
unit-tested at the helper, not via a --random E2E (decided + recorded on #145
comment 1974, with the lattice-seam fork).

Tests: plateau-picks-plateau-not-spike, mean-vs-worst, lower-is-better
direction-flip, closed-neighbourhood golden, single-member degeneracy, C1
determinism; CLI select-parse, select_winner refuse; E2E argmax-byte-identical
(C23), plateau:mean and plateau:worst provenance. Full workspace suite green;
clippy --all-targets -D warnings clean.

closes #145
2026-06-26 16:38:18 +02:00
Brummel a2959050a4 feat(0076): deflate the sweep winner's metric for the number of trials
Adds optimize_deflated beside optimize in aura-registry: the in-sample
walk-forward selection now records, on each OOS winner's manifest, how much
the winner's metric is inflated by the size of the search it won — a deflated
score and (R arm) an overfit probability — without changing which member wins
(additive, C23; a regression test pins optimize_deflated's winner == optimize's).

- R arm (sqn_normalized/expectancy_r/...): a centred moving-block reality-check.
  Each member's per-trade R series is mean-subtracted to impose the no-edge null,
  resampled via the r_bootstrap kernel (extracted to a shared resample_block,
  byte-identical), and the best-of-K null-max gives
  overfit_probability = (count(null >= raw)+1)/(n+1) and
  deflated_score = raw - p95(null). Deterministic given the seed (C1).
- total_pips arm: a closed-form expected-max-of-K dispersion floor
  (inv_norm_cdf + expected_max_of_normals), no probability.

The record (FamilySelection on RunManifest) rides the proven
serde(default, skip_serializing_if) widening + a compat.rs mirror, so legacy
runs.jsonl/families.jsonl lines load unchanged and unstamped sweep/mc/standalone
manifests stay byte-identical (C14/C18). The SelectionMode enum reserves a
Plateau slot for #145. CLI: walk-forward stamps both arms; runs family <id> rank
surfaces a human-readable deflated/overfit line.

Quality review (re-dispatched after the implement-loop's quality gate exhausted)
caught a real defect: on the R arm with positive resamples but no member carrying
trade_rs (a family loaded from disk — trade_rs is serde-skipped), the null was
all-NEG_INFINITY, turning deflated_score into +inf. Fixed by collapsing "no usable
null" (zero resamples OR no trade_rs) into one degenerate floor (deflated == raw,
overfit == 1.0); pinned by optimize_deflated_no_trade_rs_floors_instead_of_infinity.

Verified: cargo test --workspace and cargo clippy --workspace --all-targets
-D warnings both green. Frictionless Stage-1 R; n_eff advisory and reload-time
recompute deferred (spec 0076 Out of scope).

closes #144
2026-06-26 14:53:32 +02:00
Brummel d858caf67b chore: scrub dangling references to deleted specs/plans from sources
docs/specs and docs/plans were retired (prior commit); the source/test
comments that cited them ("spec 0050 §4.1", "spec §Testing N", "per spec",
"the spec's ...") now point at nothing. Strip every such pointer while
preserving the technical substance, the design-ledger contract refs
(C1/C11/C20/C34/C12.1/...), and the Gitea issue refs (#41).

Comment/doc edits only across 20 files — no logic change; full workspace
suite green, clippy clean.
2026-06-18 11:16:28 +02:00
Brummel 91ef69ed7b feat(aura-engine): RandomBinder — by-name random param-sweep
Add `RandomBinder`, the by-name sibling to `SweepBinder`, so a random
sweep (`RandomSpace`, C12.1) can be built by name against `param_space()`
instead of by positional `Vec<ParamRange>` slot order. By-name resolution
makes a same-kind transposition (e.g. swapping the I64 ranges for
`fast.length` and `slow.length`) structurally impossible — the failure
class `RandomSpace::new`'s positional validation passes silently.

Direct structural mirror of `SweepBinder`: `Composite::range` opens the
binder, `.range(name, ParamRange)` accumulates, `.sweep(count, seed, run)`
resolves the named ranges via the shared `resolve_into` (new `resolve_ranges`
caller) and runs the disjoint sweep. New `BindError::EmptyRange` mirrors
`EmptyAxis`; `ParamRange::is_empty` homes the non-empty invariant the named
layer pre-checks so `RandomSpace::new` cannot fail.

closes #79
2026-06-17 14:14:35 +02:00
Brummel be8d267019 docs(aura-engine): SweepError summary names RandomSpace::new too
The SweepError type-level rustdoc still read "A structural fault constructing a
`GridSpace`" though the enum now also gates `RandomSpace::new` (the three
random-only variants are each documented individually, but the type summary was
stale). Broaden it to name both spaces and group the grid vs. random faults.
Doc-only, behaviour-preserving — the same low-grade doc-debt class the 0049
audit fixed inline for the module doc. Surfaced by the cycle-0049 fieldtest.

refs #52
2026-06-17 13:42:50 +02:00
Brummel 3fca7810d0 audit: cycle 0049 — drift-clean (random param-sweep)
Architect drift review over 3de00e2..HEAD (the 0049 spec/plan/feat plus the
two intervening refactors that had not been audited: aura-ingest M1 transpose
6390093, aura-std SMA perf 67c1f51). Verdict: feature clean — C1 determinism
preserved (RandomSpace points seed-determined before any run; grid path
behaviour-preserving via trait-forwards-to-inherent), the #52/#71 source-seam
firewall honoured in code (sweep stays &S: Space + Fn(&[Cell]) -> RunReport, no
Source type enters the sweep layer; SplitMix64 a code-path-disjoint instance),
and both intervening refactors behaviour-preserving (pinned by
incremental_matches_full_resum_within_tolerance and
chunked_accumulation_equals_single_transpose).

Two low-grade doc-debt items found and fixed inline (doc-only, behaviour-
preserving):
- sweep.rs module doc named only the grid axis; refreshed to name RandomSpace
  + the Space trait the module now also owns.
- C12 ledger had no realization note for axis-1 (param-sweep); added one
  recording the grid (0028) + random (0049) landing and the Space-trait
  unification.

Regression gate: the project configures no regression scripts, so the test
suite + lint are the gate. Independently verified: cargo test --workspace green
(incl. the 17 new engine tests + 6 public-API E2E), cargo clippy --workspace
--all-targets -- -D warnings clean.

Drift-clean, not a milestone close (no milestone fieldtest run this cycle).
2026-06-17 13:28:07 +02:00
Brummel e17d78f24f feat(aura-engine): random param-sweep — RandomSpace + a Space trait + typed ParamRange
Ship the random half of the C12.1 param-sweep axis (grid landed in 0028).
A new `Space` trait (`points`/`param_specs`) generalizes `sweep`/`sweep_with_threads`
from `&GridSpace` to `&S: Space`; `GridSpace`'s trait impl forwards to its existing
inherent methods, so the grid path is behaviour-preserving (C1, every pre-existing
grid sweep test stays green). `RandomSpace` is the second `Space` producer: N seeded
uniform points over per-slot typed `ParamRange { lo, hi }` ranges (I64 inclusive
[lo,hi], F64 half-open [lo,hi)), validated in `new` against the param-space — a
non-numeric slot, a range-kind mismatch, and an empty range are the three new typed
`SweepError` variants, all caught before any run. Sampling reuses the existing
bit-stable `SplitMix64` (promoted to `pub(crate)`) as a code-path-disjoint instance
from the data-edge seed RNG — the #52/#71 World-II source-seam firewall: they share
only the `u64` type, never a path. The sweep-layer signature stays param-only
(`Fn(&[Cell]) -> RunReport`); no `Source`/stream type enters it.

Deviation from the spec's verbatim sampler, accepted as a correctness hardening:
the I64 draw guards the full-width span ([i64::MIN, i64::MAX] computes a span of
2^64 that wraps a u64 to 0) and uses `lo.wrapping_add(draw as i64)` instead of a
plain `+`. This avoids both the `% 0` divide-by-zero at the full domain and the
signed-overflow panic the literal form hits on wide ranges in debug builds, while
preserving uniform-over-[lo,hi] draws and determinism (an extra RED test,
random_points_full_range_i64_does_not_panic, pins it). Modulo bias for spans not
dividing 2^64 remains the spec's documented, accepted simplification (param search
needs no cryptographic uniformity).

Includes a public-API E2E suite (tests/random_sweep_e2e.rs) exercising the feature
exactly as a downstream researcher would — reproducibility at the report boundary,
seed-sensitivity, in-range draws, the typed NonNumericRange gate, a well-formed
empty (count==0) family, and Grid/Random interchangeability through `Space`.

Verified: cargo test --workspace green (incl. 17 new engine tests + 6 E2E);
cargo clippy --workspace --all-targets -- -D warnings clean.

closes #52
2026-06-17 13:24:06 +02:00
Brummel 43716be10e refactor(aura-engine): Cell into the construction path — param-plane base/frontend split
Cell becomes the carrier of the construction path; Scalar narrows to the author/render boundaries. The validated/enumerated param point carries no redundant kind (it lives once, in the declared param-space); at the author edge the kind is a checksum — two independent sources, the typed value vs. the slot — so the self-describing Scalar stays there. The name->slot binding is dynamic (C23), so the check is necessarily a runtime one and the value must self-describe for it.

Base/frontend split (the AnyColumn push/push_cell pattern one level up): compile_with_cells / bootstrap_with_cells are the kind-check-free base; compile_with_params / bootstrap_with_params are the frontend that adds only the per-value kind checksum, strips to cells (new Scalar::cell(), the partner of Scalar::from_cell), and delegates. lower_items loses its per-primitive kind-check; PrimitiveBuilder::build and the std node builders (sma/ema/exposure/lincomb) read cells.

Boundary: construction -> Cell (PrimitiveBuilder::build, lower_items, GridSpace.axes/points, SweepPoint.params, the sweep closure). Author edges stay Scalar (GridSpace::new, bind, compile_with_params/bootstrap_with_params). walkforward chosen_params stays a self-describing Scalar report record (Option A — WalkForwardResult carries no space); the cell winner is reconstructed once at the WindowRun site via from_cell.

injective-check moved ahead of the arity-check in the frontend to preserve the pre-split error order (DuplicateParamPath before ParamArity). The lossy i64->f64 param projection (scalar_as_f64 / scalar_as_param_f64) is deliberately untouched — a separate follow-up.

Behaviour-preserving (C1): build --all-targets / test / clippy -D warnings / doc all clean across the workspace.
2026-06-16 16:22:17 +02:00
Brummel cd3d1ca9ed refactor(aura-core): split Scalar into a tag-free Cell + ScalarKind
Motivation
----------
`Scalar` was a tagged enum (I64/F64/Bool/Ts), so every scalar value
physically carried its own kind tag. But the kind is already known from
the schema/port/column the value flows through (C7: the type is a
property of the column, not of the value — the hot path is already
columnar `Column<T>`, and `AnyColumn::get` *reconstructs* the tag from
the column on the way out). The per-value tag was therefore redundant
with the kind the surrounding context already holds.

That redundancy had three costs:

  * It baked an implicit `match` (a branch) into every function that read
    a Scalar payload — even where the caller statically knew the type.
    The tag could never be exploited away.
  * Size: a tagged enum is tag + payload = 16 bytes (f64/i64 alignment),
    twice the 8 bytes the value needs. A `Column<Scalar>` would be double
    the memory and half the cache utilisation.
  * It is the shared root of several downstream papercuts we keep hitting
    — the lossy f64 manifest field, the `unreachable!` panic on a
    non-numeric param, the serde-tag question — all symptoms of "the type
    is baked into the value".

Change
------
Introduce `Cell`: a type-erased 64-bit word (`struct Cell(u64)`) that is
not readable without external type context. It is constructed per base
type (`from_i64/from_f64/from_bool/from_ts`) and read only by naming the
type at the call site (`i64()/f64()/bool()/ts()`) — each a branch-free
bit-cast. The hot path resolves the kind once at the boundary (from the
schema) and then reads natively, with no per-value branch. `Cell` knows
nothing of `Scalar` or `ScalarKind`; the dependency is strictly one-way,
and it lives in its own `cell.rs` (more is planned on top of it).

`Scalar` becomes `struct { kind: ScalarKind, cell: Cell }` — the
self-describing form for the dynamic boundaries (builder binding,
serialization, rendering), built on top of `Cell`. Its `as_*` accessors
now `debug_assert` the kind and return the native value (free in
release); calling the wrong accessor is a caller bug, not a checked
`Option`. The variant constructors `Scalar::I64(..)` become associated
fns `Scalar::i64(..)`.

`PartialEq` is hand-written (not derived) to preserve the former enum's
value semantics: kinds must match, then native payloads compare, so f64
keeps IEEE-754 behaviour (`NaN != NaN`, `+0.0 == -0.0`) and a kind
mismatch is never equal even when the raw words coincide. A fixture
(`scalar_eq_is_value_not_bitwise`) pins exactly the cases where bit- and
value-equality diverge, so it can't silently regress. `Cell`'s own
`Eq`/`Hash` stay bitwise — correct for a raw word.

The change is behaviour-preserving: Scalar's observable behaviour is
identical to the pre-Cell enum (the value-equality fixture proves it);
only the internal representation changed. The ~440 call sites across the
workspace are a mechanical constructor rename plus ~12 destructuring
sites (match-arms / `let`-patterns) rewritten to `kind()` + `as_*`.

Verified: cargo build --workspace --all-targets, cargo clippy --workspace
--all-targets -- -D warnings, cargo test --workspace — all green.
2026-06-16 12:12:52 +02:00
Brummel fb8cabf13f feat(aura-engine): sweep named-binding — zip_params + SweepFamily.named_params (C12 #57)
Thread a derived named view of a sweep point so consumers stop hand-zipping
param_space() names onto the bare &[Scalar]. The free function
zip_params(space, point) -> Vec<(String, Scalar)> in aura-core is the one
shared projection; GridSpace retains the ParamSpec list it already receives in
new() (it was validated then discarded); SweepFamily carries it (stamped once
in sweep_with_threads) and exposes named_params(i). The three CLI hand-zip
sites collapse to one zip_params call each.

The run-closure signature `Fn(&[Scalar]) -> RunReport` is byte-unchanged — the
named view is a derived projection (C23: slot is identity, name is derived),
not a closure-currency change — so #52's RandomSpace plugs into the same
sweep() execution layer with zero signature reconciliation (#71 firewall held).

sim_optimal_manifest now takes typed Vec<(String, Scalar)> and does the lossy
f64 collapse internally (one place; the manifest's f64-precursor field owns its
own lossiness — the typed-manifest upgrade stays the deferred typed-param-space
item, a non-goal here). All eight call sites migrated: three sweep closures
pass zip_params, five hand-listing callers (run_sample, run_sample_real,
mc_family, run_macd, run_sample_seeded) pass Scalar::F64 literals.
Behaviour-preserving: the collapse output is identical to the old per-site zip,
so aura sweep / walkforward / run output is byte-identical; SweepFamily.space
threaded into the aura-registry optimize test literal.

Verified by the orchestrator: cargo test --workspace green (4 new tests:
zip_params x2, sweep_family_carries_param_space, family_named_params_round_trips),
cargo clippy --workspace --all-targets -D warnings clean. (rust-analyzer
emitted stale mid-edit diagnostics; the real cargo build/test is clean.)

refs #57
2026-06-15 23:28:04 +02:00
Brummel e2c550f2f9 feat(aura-engine): monte_carlo family — McFamily over a seed set (C12 axis 4)
`monte_carlo(base_point, seeds, run_one) -> McFamily`, the Monte-Carlo
orchestration family: a fixed base point run over a seed set, each seed a
disjoint C1 realization. C12 axis 4 — Monte-Carlo IS a sweep over seeds, so it
reuses the disjoint-parallel executor rather than forking a run loop.

The reuse is realized by extracting the index-parallel core out of
`sweep_with_threads` into a `pub(crate) run_indexed<T>` (generic over the
per-job result, over a 0..n job range); `sweep_with_threads` becomes a thin
adapter over it. Behaviour-preserving (C1/C11/C23): the three pre-existing sweep
tests stay green — they ARE the proof the refactor preserved behaviour. A small
incidental win: the adapter consumes `points` via zip, dropping the old
per-point clone.

mc.rs adds McDraw{seed,report} / McFamily{draws,aggregate} (draws in seed-INPUT
order, independent of thread completion) / McAggregate (mean + p5/p25/p50/p75/p95
of all three run metrics — the V1 decision: covers every metric, not a "chosen"
one) / MetricStats, a private type-7 linear-interpolation `quantile`, and
`McAggregate::from_draws` (a pure post-run reduction recomputable from the
retained raw draws). Eager-agnostic firewall (#71): the API takes seeds + a
per-draw closure `Fn(u64,&[Scalar])->RunReport`, never a materialized stream Vec;
seed->Source construction stays a closure-body concern. An empty seed set is a
debug-asserted precondition, preserving the no-Result `-> McFamily` signature.

7 RED-first mc::tests pin: one draw per seed in input order; determinism across
1 vs 8 threads (C1); draw == an independent seeded run; distinct seeds perturb
the family; aggregate == from_draws(draws); type-7 quantile/mean math on a known
fixture; quantile endpoints + singleton. Workspace green, clippy -D warnings
clean, cargo doc clean.

refs #68
2026-06-15 13:45:28 +02:00
Brummel eec129ee51 feat(aura-engine): Source ingestion seam — run() takes Vec<Box<dyn Source>>
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
2026-06-15 10:23:09 +02:00
Brummel 1f01badac7 refactor(aura-engine): share the SMA-cross harness test fixtures
Collapse the three verbatim-duplicated #[cfg(test)] fixtures —
synthetic_prices, sma_cross, composite_sma_cross_harness — that lived
in both blueprint.rs's and sweep.rs's test modules into one shared
crate-root `#[cfg(test)] mod test_fixtures` (pub(crate) free fns). The
two consumers now import from `crate::test_fixtures`; sweep.rs's
test-mod `use` block sheds the imports that were only needed by the
moved fns.

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

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

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

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

closes #53
2026-06-14 00:18:04 +02:00
Brummel ffed8cc612 feat(aura-core,aura-engine,aura-cli): node-instance naming retires ParamAlias
Every blueprint node now carries a name. A primitive builder gains an optional
instance name (Sma::builder().named("fast")); when omitted it defaults to the
node's type label, ASCII-lowercased verbatim ("SMA"->"sma", "SimBroker"->
"simbroker"). param_space() is now uniformly <node>.<param> at every level
including the root (sma_cross.fast.length, exposure.scale). Fan-in
distinguishability (C9) and signature_of re-key onto the node name: two same-type
siblings both defaulting to "sma" collide and IndistinguishableFanIn fires, so
one act -- naming the legs "fast"/"slow" -- fixes both the naming collision and
the fan-in check. The index-addressed ParamAlias overlay, Composite.params, the
Composite::new 5th argument, aliases_on, and check_alias_indices are removed
(Role.name and OutField.name untouched). Ledger C9 and C23 amended.

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

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

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

closes #56
2026-06-11 11:51:30 +02:00
Brummel 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 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