Commit Graph

54 Commits

Author SHA1 Message Date
Brummel 14e27a978b feat(0095): monte-carlo over a loaded blueprint — aura mc <bp.json> --seeds N
The MC half of #170 (World/C21): `aura mc <closed blueprint.json> --seeds N`
builds a FamilyKind::MonteCarlo family from a loaded CLOSED blueprint. Each seed
draws a distinct synthetic price walk (synthetic_walk_sources, the mc_family
pattern), so members differ in the DATA (not just a manifest label) — the sim is
deterministic (C1), so MC variation must come from re-drawing the input. Draws
run disjointly in parallel via the shared monte_carlo helper (invariant 1). The
canonical blueprint is stored once keyed by the shared topology_hash (the 0094
hook), and reproduce_family_in gains a MonteCarlo realization branch that
reconstructs each member's seed-driven walk from manifest.seed — so a persisted
mc family 'aura reproduce's bit-identically (the store-hook proof).

Closed-blueprint only: MC binds no axis, so a free knob has no binder; an open
blueprint yields a named Err (rendered to stderr + exit 2 at the run_blueprint_mc
boundary, mirroring blueprint_sweep_family — no hidden process exit in the pure
builder), pre-empting the compile_with_params arity panic.

Two quality-gate-driven improvements over the ratified plan, both applied and
verified: (1) delegate the disjoint draws to the engine monte_carlo seam instead
of a hand-rolled loop (my plan's 'closure-shape mismatch' rationale was wrong —
the Fn(u64,&[Scalar]) closure ignores the unused base_point, exactly as mc_family
does); (2) the closed-blueprint guard returns Result rather than exiting inside
the builder, matching the sweep sibling's contract and making the rejection
unit-testable. Engine + registry untouched (invariants 8/9).

Verified: cargo build --workspace clean; cargo clippy --workspace --all-targets
-- -D warnings clean; cargo test --workspace green (5 new tests: the parser,
anti-degenerate family, MC reproduce round-trip, and two E2Es incl. the open
rejection; no regressions).

refs #170
2026-07-01 13:18:00 +02:00
Brummel cec7811c32 audit(0094): cycle close — content-addressed reproduction; ledger refreshed, coverage closed; rm ephemeral spec/plan
Architect drift review (4751d4b..HEAD): drift_found — ledger-currency only; the
code is clean.

What holds (architect, confirmed by diff + green tests):
- Inv 8 / C9: aura-engine byte-untouched; content_id + sha2 in aura-cli, the
  store in aura-registry.
- Inv 9: Registry::{put,get}_blueprint is a dumb bytes-by-key store (opaque
  String keyed by String hash; no sha2, no parse, no sha256==hash verify — the
  caller owns the hash). No domain knowledge crept into the engine or registry.
- C1: the run_blueprint_member extraction is behaviour-preserving (keystone
  blueprint_sweep_member_equals_single_run_and_shares_topology_hash unchanged +
  green); serde_json/float_roundtrip serves determinism (disk==recomputed), not a
  mask (1-ULP canary green).
- C11/C12: one blueprint stored per family, keyed by the shared topology_hash.

Resolutions:
- [ledger] Refreshed C18 + C24: added the cycle-0094 Realization (the
  content-addressed store runs/blueprints/<hash>.json + aura reproduce), and
  retired the now-false "content-addressed identity / full reproduction deferred"
  claims (INDEX.md:1261, C24 deferred block). Only the structural-axis /
  whole-harness content-id (and the debug-name-in-id question) genuinely remains,
  filed forward (#171, #170).
- [debt->closed] Added the two untested refuse-don't-guess E2E arms
  (aura_reproduce_rejects_a_non_generated_family — a hard-wired sweep member has
  no topology_hash; aura_reproduce_rejects_a_missing_stored_blueprint — the store
  was deleted), and a cross-surface unit pin
  (topology_hash_is_the_content_id_of_the_canonical_form) so the two acc-1 paths
  cannot silently drift.
- [scaling] MC/walk-forward-from-blueprint (#170) will need the same put_blueprint
  hook or reproduce won't cover them — noted on #170.

No regression scripts in this project (Test is the gate): full workspace suite
green (51 suites), clippy -D warnings clean, binary verified end-to-end.

Ephemeral spec/plan git-rm'd per the lifecycle convention; durable rationale is in
the ledger. The World/C21 milestone stays open (#170 remains).

closes #158
2026-07-01 03:15:39 +02:00
Brummel 008692c043 feat(0094): content-addressed reproduction — aura reproduce <family-id> (iter 1)
Restore C18 reproducibility for generated (blueprint-sweep) runs (#158, acc 2):
a sweep's topology is stored once, content-addressed by the topology_hash the
manifest already carries, and `aura reproduce <family-id>` re-derives every
persisted member bit-identically (C1) from the stored blueprint.

- aura-registry: a dumb content-addressed bytes store — Registry::put_blueprint
  / get_blueprint / blueprints_dir over runs/blueprints/<hash>.json. No sha2 dep
  (caller owns the hash; reproduction's bit-identical compare is the integrity
  check); absent id -> Ok(None) (treat-as-empty, like load).
- aura-cli: run_blueprint_member extracted from blueprint_sweep_family's member
  closure (behaviour-preserving; keystone stays green) so the sweep AND reproduce
  re-run the identical reduce-mode path — bit-identity by construction. The store
  write hooks the sweep persist seam (one blueprint per family, shared topo).
  reproduce_family_in loads each member's blueprint by hash, reconstructs the
  point from the recorded params (point_from_params, inverse of zip_params, over
  the WRAPPED signal's param_space so the prefixed knob names match), re-runs, and
  compares metrics. `["reproduce", id]` dispatch arm; refuse-don't-guess on an
  unknown id / missing stored blueprint (exit 2), DIVERGED -> exit 1.

Dependency decision (per-case review, ratified): enabled serde_json
`float_roundtrip`. Stored member metrics round-trip through families.jsonl as
f64 JSON; the default parser can be 1 ULP off, which would make disk-loaded !=
recomputed and "bit-identical" (C1) physically impossible. The feature guarantees
exact f64 round-trip. Blast-radius clean (full workspace suite green); it serves
determinism, the invariant reproduction rests on. Canary:
f64_blueprint_param_survives_store_round_trip_bit_identically.

Two plan bugs caught + fixed by the implement loop and verified: reproduce's
param_space must come from the wrapped signal (prefixed names, the #167 lockstep),
and the family id is 0-indexed (live-0 / smacross-0, not -1).

Verified: full workspace suite green (51 suites); clippy -D warnings clean; and
the binary end-to-end — `aura sweep` (3 members, one shared stored blueprint) then
`aura reproduce live-0` (3/3 bit-identical incl the open-at-end member), unknown
id -> exit 2. acc 1 (graph introspect --content-id) + acc 3 (Tier-1 id stability)
land in iteration 2 (plan 0094b).

refs #158
2026-07-01 02:55:52 +02:00
Brummel 481add0ca2 feat(0093): sweep-from-blueprint — aura sweep <blueprint.json> over named axes
Cycle 2 of the World/C21 milestone (#166): the World now orchestrates FAMILIES
from blueprint-data. `aura sweep <blueprint.json> --axis <name>=<csv> …` loads an
OPEN signal blueprint, grids the by-name axes against its param_space, builds each
member via cycle-1's wrap_stage1r seam, and aggregates to a FamilyKind::Sweep
family — byte-identical in shape to the hard-wired sweep.

- `parse_sweep_blueprint_args` + `SweepBlueprintArgs`: repeatable `--axis name=csv`
  (lexed i64-then-f64; resolve_axes kind-checks), `--name`/`--trace`, the shared
  `--real/--from/--to` accumulator → DataChoice.
- `blueprint_sweep_family` (pure) + `run_blueprint_sweep` (IO) mirror
  stage1_r_sweep_family with three deviations: by-name axes (verbatim param_space
  names, e.g. `stage1_signal.fast.length`), reload-per-member (Composite is !Clone —
  its build closures are Box<dyn Fn>, #164 — so a fresh signal is re-loaded from the
  doc per member), and a per-member shared topology_hash. Members share the hash
  (same signal topology, only params vary); member_key distinguishes them.
- The `aura sweep <file.json>` `.json`-discriminator arm mirrors cycle 1; the
  hard-wired `--strategy` sweep is untouched (#159).

Task-3 design gap resolved: a sweep needs an OPEN blueprint (open knobs to grid),
distinct from cycle-1's fully-bound run fixture (a bound blueprint has an empty
param_space). New `tests/fixtures/stage1_signal_open.json` =
blueprint_to_json(stage1_signal(None,None)); a sweep must bind ALL open knobs, so
the E2E grids fast×slow (2×2 = 4 members).

Verified end-to-end by running the binary: `aura sweep stage1_signal_open.json
--axis stage1_signal.fast.length=2,4 --axis stage1_signal.slow.length=8,16 --trace f`
persists a discoverable 4-member Sweep family; an unknown axis fails clean. Member
equivalence vs run_signal_stage1r + shared-hash/distinct-member_key unit-tested.
Full workspace suite green; clippy --all-targets -D warnings clean.

refs #166
2026-07-01 00:27:16 +02:00
Brummel 2b1c24668d test(0092): run-from-blueprint E2E + bit-identical keystone + demo fixtures
Closes the cycle-1 test coverage for #165. The `aura run <blueprint.json>` CLI
arm itself (the .json dispatch + parse_blueprint_run_args + BlueprintRunArgs)
shipped in ff4a1b3; this adds the coverage that proves it and the in-repo demo
blueprint:

- crates/aura-cli/tests/fixtures/stage1_signal.json — the canonical SMA-cross
  signal (blueprint_to_json of stage1_signal(2,4)); emit_demo_signal_fixture
  (#[ignore]) regenerates it.
- crates/aura-cli/tests/fixtures/unknown_node.json — one type set to "Nope" for
  the fail-clean guard.
- loaded_signal_runs_bit_identical_to_rust_built (the keystone, C1): a signal
  serialized -> loaded via blueprint_from_json -> run is bit-identical to its
  Rust-built twin, incl. topology_hash (64-hex).
- topology_hash_is_stable_and_distinguishes (#158 anchor property).
- cli_run.rs E2E: `aura run <demo>` exits 0 with topology_hash in the manifest;
  an unknown-node blueprint fails clean with UnknownNodeType (the #160 closed-set
  guard at the data-plane face, invariant 9).

Verified end-to-end by actually running the binary: `aura run stage1_signal.json`
-> RunReport with a 64-hex topology_hash, sim-optimal+risk-executor broker, R
metrics; `aura run unknown_node.json` -> UnknownNodeType("Nope"), exit 2. Full
workspace suite green; clippy --all-targets -D warnings clean.

refs #165
2026-06-30 20:48:02 +02:00
Brummel d925a747cb feat(0086): cost-flag CLI ergonomics — units note, named diagnostics, non-R-harness guard
#153 (friction surfaced by the milestone-#148 close fieldtest). Three cost-flag
fixes on `aura run`, all on the pure parse_run_args grammar:

- Units discoverable: a COST_FLAGS_NOTE appended to the run usage + --help
  (price units, charged in R as cost/|entry-stop|, per-ENGINE-cycle carry,
  >= 0, stage1-r only).
- A negative rate names the cause + flag ("<flag> must be non-negative, got <v>")
  via a factored parse_nonneg_rate helper, not a bare usage dump.
- A cost flag on a non-R harness (sma/macd) is refused (exit 2, "cost flags
  require an R-evaluator harness (stage1-r)") instead of silently ignored — the
  named spec_gap, decided reject/fail-loud (refuse-don't-guess, recorded on #153),
  pinned by a new C10 ledger note.

Internal RunArgs/RunData/HarnessKind gain #[derive(Debug)] (behaviour-neutral) so
the pure parse_run_args unit tests can use expect_err.

Verified independently: cargo test -p aura-cli green, the eight stage1-r cost
goldens byte-unchanged; clippy -p aura-cli --all-targets -D warnings clean. Only
the grammar/usage strings and the previously-unguarded sma-cost path changed;
the cost-model graph and R math are untouched.

closes #153
2026-06-29 12:12:22 +02:00
Brummel f5e00a9c72 feat(0085): per-cycle-held accrual — CarryCost + ChargeMode (approach B)
Cycle 5 of milestone #148 (cost-model graph in R): the per-cycle-held accrual
mechanism — C10's "per-cycle-held factors accrue over the hold". A carry/funding cost
is charged for every cycle a position is held, so the net_r_equity equity curve bleeds
continuously over the hold (approach B, "ja, mach B") rather than stepping at close.

Mechanism (logged on #148): ChargeMode { AtClose, PerHeldCycle } is a property of the
cost factor, read by the one shared CostRunner (no second runner type). The AtClose arm
is the pre-cycle-5 eval tokens VERBATIM (IEEE-754 byte-identity). The PerHeldCycle arm
accrues `per` into `acc` each held cycle, dumps the accrued total into `cum` at close
(resetting acc), and marks the open position via a growing open_cost_in_r. The bleed
lives in open_cost_in_r, which the net_r_equity tap already subtracts — so summarize_r
and the CLI net_eq wiring are UNCHANGED, and the cycle-0083/0084 net_expectancy_r
goldens (-614.3134020253314 flat, -615.0304388396047 composed) + the C18 no-cost golden
stay byte-identical.

CarryCost is a ConstantCost twin differing only in charge_mode() -> PerHeldCycle (a
labelled stress parameter, the flat base of the accrual family). Wired through the
existing cost_graph/CostSum aggregation; a per-trade ConstantCost and a per-held-cycle
CarryCost compose in one run (the costs sum per-field — the composed golden is exactly
the sum of the two single-cost charges). New run-path --carry-per-cycle flag
(sweep/walkforward/mc pass None, as the existing cost flags do).

Tests: 2 RED-first unit B-proofs (open_cost grows over the hold, dumps the total at
close, acc resets between trades — the discriminator a scalar net_expectancy_r cannot
see); the CarryCost twin's 5 unit tests; 2 captured net_expectancy_r goldens
(-768.2095026600887 carry, -1383.7939051991182 composed); a net_r_equity-bleeds-over-
the-hold integration test (the terminal open hold's r_equity-net_r_equity gap strictly
grows); plus negative-rate-refused-exit-2, monotone-in-rate, and zero-rate-exactly-free
guards.

Verified: cargo test --workspace (0 failures), cargo build --workspace, cargo clippy
--workspace --all-targets -- -D warnings clean. summarize_r / aura-analysis untouched.
Deferred to later #148 cycles (logged there): notional-based carry, the calendar-aware
overnight swap, sweep-path cost.

refs #148
2026-06-29 01:00:51 +02:00
Brummel face1879a6 audit(0084): cycle close — drift-clean (code); C10 cycle-0084 note + doc fixes
Cycle 4 of milestone #148 (cost-graph composite-builder). Architect drift review
over fc52b4f..d5c44dd: drift_found, every item dispositioned.

What holds (architect, evidence-of-review): C16/C9 — cost_graph lives in
aura-composites, the factors in aura-std, aura-engine stays domain-free; the
composite is ordinary downstream nodes. C11 behaviour-preservation is structural,
not just asserted — the composite inlines to the same flat fan-in as the deleted
CLI block (identical node set/order, cost[k].field slot map, geometry fan, net_eq
terms/signs, cost_rec cols), so the cycle-0083 net_expectancy_r goldens stay
byte-identical. C23 — role/port names non-load-bearing; 4 fixtures pin the role-set
+ output triple.

Resolved this commit:
- [high] ledger C10: the cycle-0083 note's "Still deferred (decision E)" is removed
  (E shipped this cycle) and a cycle-0084 realization note added.
- [medium] cost_graph .leak() provenance (aura-composites/src/lib.rs): the
  "risk_executor precedent" was imprecise — that .leak() lives in risk_executor's
  TEST (one-shot, leak-safe), not its production builder. Doc corrected: the leak is
  fine for one-shot run-path construction but must be interned (the COL_PORTS
  production pattern) before cost reaches the sweep path.
- [low] cli_run.rs golden docstring cited the deleted `slot * COST_WIDTH + f` CLI
  wiring; updated to the cost_graph composite's per-node cost[k].<port> wiring.

Deferred (tracked, not pending) -> #152:
- [medium] the cost[k].<port> index-namespacing restated across CostSum / cost_graph
  / the CLI (a build-validated lockstep — a divergent name fails loudly at g.build(),
  NOT the silent positional kind 0083 collapsed), AND the per-build .leak() that
  becomes an allocation regression on the per-member sweep path. Both are harmless on
  the current run-only cost path; the proper fix is one sweep-safe single-source
  cost-port-name contract (interned), best built with the deferred sweep-cost cycle.

Regression gate: cargo test --workspace (0 failures), clippy --workspace --all-targets
-- -D warnings clean, cargo doc clean — the architect is the primary gate (no dedicated
regression script). Ephemera (spec 0084 + plan 0084) git rm'd.

Milestone "Cost-model graph (in R)" remains OPEN.

refs #148
2026-06-28 22:06:06 +02:00
Brummel 6b53c239dd feat(0083): CostNode trait + shared cost-record contract
Lift the duplicated cost-node skeleton — shared verbatim by ConstantCost and
VolSlippageCost and locked by convention against CostSum — into one abstraction:

- A new aura-std/src/cost.rs owns the cost-record contract (COST_WIDTH,
  COST_FIELD_NAMES, GEOMETRY_WIDTH), the CostNode factor trait (one hook:
  cost_numerator, in price units), the generic CostRunner<F> adapter that holds
  the shared state (cum, out) and the co-temporality skeleton (geometry gating,
  numerator/latched R-normalization, closed/open charge, 3-field emit), and a
  cost_node_builder schema assembler.
- ConstantCost and VolSlippageCost become thin CostNode factors; their new()
  returns CostRunner<Self>, so every existing call site and unit test binds
  transparently and stays green verbatim.
- CostSum and main.rs drop their local triple consts for the shared
  COST_FIELD_NAMES — the cycle-2 audit-flagged by-convention 3-field lockstep is
  now a structural single-source contract (the four copies of the triple collapse
  to one). main.rs also reads COST_WIDTH in place of the literal slot stride.

Behaviour-preserving: the builders emit unchanged schemas (same port names), so
the wiring, the net_r_equity seam, and summarize_r are untouched; the
numerator/latched token form is preserved verbatim (IEEE-754 byte-identity). The
existing suite is the regression net — all green: the per-node unit tests pass
verbatim (0.5/1.5, 0.375/1.375), the composition E2E exact, the C18 no-cost
golden byte-identical. Two new CLI characterization goldens pin the exact
net_expectancy_r of the flat and composed cost paths (the prior tests only
asserted net < gross, which a numerator drift would pass silently). New cost.rs
runner/contract tests + the HalfSpreadCost author doctest cover the new surface.

Deviation from the plan: CostNode::name() was dropped (the plan over-specified
it) — it is dead surface, nothing consumes it (CostRunner forwards label();
cost_node_builder takes the name as an explicit arg). Removed from the trait,
both impls, the doctest, and the test stubs; build + suite + clippy green.

Verified: cargo build --workspace --all-targets clean; cargo test --workspace
0 failures; cargo clippy --workspace --all-targets -- -D warnings clean; doctest
green.

refs #148
2026-06-28 19:26:34 +02:00
Brummel 7f3756a395 feat(0082): cost-graph composition — VolSlippageCost + CostSum net-R aggregate
Cycle 2 of the "Cost-model graph (in R)" milestone (#148): the milestone's real
architectural claim — the cost graph composes. Two cost nodes now sum into one
net-R curve while summarize_r and the net_r_equity tap stay structurally
unchanged.

- VolSlippageCost (aura-std): a second, STATE-DEPENDENT cost node; per-trade
  charge = slip_vol_mult * volatility / |entry-stop|, in R. The vol is an
  independent short-horizon realized range (SLIP_VOL_LENGTH=5, distinct from the
  stop's EWMA-3) — scaling slippage by the stop's own vol would collapse cost-in-R
  to a constant (indistinguishable from ConstantCost).
- CostSum (aura-std): the cost-graph OUTPUT node — sums N cost nodes' 3-field
  cost-in-R records per-field into one aggregate. summarize_r and net_r_equity
  read the aggregate (one home for cost; n=1 is the identity, so the cost path is
  uniform). A future CostNode trait (deferred) will unify the cost-triple the two
  producer nodes currently restate by-convention.
- Run path: --cost-per-trade and --slip-vol-mult combine, their costs summing into
  the net-R curve; a hoisted vol proxy (RollingMax-RollingMin) keeps the single
  feed. Run-path-scoped; sweep/walkforward/mc pass None.

Co-temporality contract (the load-bearing design decision; corrected from the
signed spec after the implement-loop correctly BLOCKED Task 3 on it). summarize_r
positional-joins cost[i] <-> record[i], so the cost stream must be co-temporal
1:1 with the PM record. A cost node is therefore gated ONLY by the PM geometry
(closed/open/entry/stop); a not-yet-warm state input (the vol proxy warms later
than PM) contributes 0 cost that cycle rather than withholding and desyncing the
stream. This makes co-temporality structural + warmup-independent, preserves the
C18 golden, and generalizes to any future cost factor. The rejected alternative
(a key-join in summarize_r) would have moved the golden and pushed cost-graph
logic into the post-run fold. Recorded on #148.

Tests: VolSlippageCost + CostSum unit sets (incl. the co-temporal-zero-during-
warmup case); the CLI composition run (both flags -> net_both < net_flat,
net_r_equity persisted); node-level EXACT additive composition (net_both ==
net_flat + net_vol - gross over the real nodes), the aggregate net_r_equity ==
post-run net total, and CostSum(1) identity. Full workspace suite green; clippy
-D warnings clean; the no-cost C18 golden byte-identical (the regression floor).

Deferred (later cycles of this milestone): the general CostNode trait + cost-graph
composite-builder (now justified by two concrete nodes), the conviction-weighting
R-aggregation axis, and cost on the reduce-mode sweep path. Spec + plan amended to
the corrected contract; both are cycle ephemera (git rm at cycle close).

refs #148
2026-06-28 16:51:48 +02:00
Brummel 3fe4684190 feat(0081): cost-model graph cycle 1 — ConstantCost node + net-R seam
Lands the first cycle of the "Cost-model graph (in R)" milestone: cost is
now a composable in-graph node, not a post-run scalar (C10).

- ConstantCost (aura-std): a flat round-trip cost per trade, emitted in R
  as a 3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r} isomorphic
  to PositionManagement's R-triple. R-pure: cost_in_r = cost_per_trade /
  |entry-stop|; notional cancels, no equity held.
- summarize_r folds the cost stream into net_expectancy_r, replacing its
  scalar round_trip_cost (one home for cost, no double-count). Positional
  co-temporal join; an empty stream is the gross-R baseline.
- net_r_equity tap (stage1_r_graph): LinComb(4) over [cum_realized_r,
  unrealized_r, -cum_cost_in_r, -open_cost_in_r] -> Recorder, a sibling of
  r_equity; --cost-per-trade on the run path wires the node + tap + fold.
- A no-cost run is byte-identical: no net_r_equity tap, net == gross, the
  C18 golden unchanged.

Tests: exact-subsumption (the node-stream net == mean(r_i - cost_i) over
ALL trades incl. the window-end open one), the in-graph net_r_equity tap +
net < gross E2E, the no-cost golden held. Full suite green; clippy clean.

Implement-time decisions ratified (recorded on #148): the cost stream is
recorded 3-wide (the node's full output; summarize_r reads col 0/2, col 1
feeds the in-graph net curve) — resolving a 2-wide/3-wide inconsistency in
the plan's Step 3; Trade drops its `latched` field (cost now arrives in R),
so r_col ENTRY_PRICE/STOP_PRICE become #[allow(dead_code)] layout contract.

Scoped to the run path (non-reduce); sweep/walkforward/mc pass None — cost
on the reduce-mode sweep path and OOS-pooling cost are a deferred follow-up.

Known pre-existing concern (not this cycle): aura-std lib.rs module doc
still names "realistic broker profiles", retired by the C10 rework.

refs #148
2026-06-28 14:41:45 +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 2e77ab1dda feat(0075): Monte-Carlo R-bootstrap for OOS validation (iter 2)
Add `aura mc --strategy stage1-r [--real <SYM>]`: run the stage1-r
walk-forward, pool its OOS per-trade R series, and report a moving-block
bootstrap E[R] distribution / CI (one `mc_r_bootstrap` line with an `e_r`
quantile block + P(E[R]<=0)). Completes spec 0075's Monte-Carlo half on top
of iter 1's walk-forward R-reporting.

Engine: `r_bootstrap(rs, n_resamples, block_len, seed) -> RBootstrap` — a
deterministic moving-block bootstrap (non-circular, final block truncated;
block_len=1 = i.i.d. trade-shuffle; clamped to [1,n]; empty -> all-zero),
reusing the existing MetricStats/quantile and SplitMix64.

CLI: parse_mc_args/McArgs split — bare/`--name`/`--trace` route to the
unchanged synthetic seed-resweep `run_mc` (byte-for-byte preserved); the R
path accepts ONLY `--strategy stage1-r`, so a bare `--real` stays a usage
error (mc_rejects_real_flag_with_usage_exit_2 preserved, doc-comment
refreshed). run_mc_r_bootstrap reuses walkforward_family + RMetrics.trade_rs.

Corrective fixes over the plan snippets, both verified: the mc usage string
carries a `usage:` token (the new `["mc", rest @ ..]` arm now handles the
`--real` rejection, and the preserved golden asserts `stderr.contains
("usage")`); `#[allow(clippy::large_enum_variant)]` on McArgs keeps the
unboxed Stage1RGrid field layout.

Scope: single `--real <SYM>` per invocation; cross-symbol pooling is a
deferred follow-on (needs a multi-symbol grammar). Verified: cargo build
clean, full `cargo test --workspace` green (609 passed), clippy -D warnings
clean; bare `aura mc`, SMA walkforward, stage1-r single-run, and C18
round-trip goldens all preserved.

refs #139
2026-06-26 12:12:06 +02:00
Brummel f286bb85f7 feat(0075): walk-forward strategy-selectable + R-reporting (iter 1)
Make `aura walkforward --strategy stage1-r [--real <SYM>]` roll IS->OOS
windows, sweep the stage1-r grid in-sample, pick the winner by an R metric
(sqn_normalized), run it out-of-sample, and report per-window + pooled OOS
R-metrics. The bare SMA walkforward path is byte-identical (its dispatch arm
is verbatim today's body; the pooled `oos_r` block is emitted only when a
window carries an `r` block).

Engine:
- RMetrics gains an in-memory `trade_rs: Vec<f64>` (realised R per closed
  trade), excluded from serde (`#[serde(skip)]`) and from a hand-written
  PartialEq, so the C18 wire shape and every existing equality assertion /
  round-trip stay unchanged. summarize_r now retains the per-trade R vector
  it used to drop.
- New `r_metrics_from_rs(&[f64])` reduces a flat (pooled across-window) R
  series to RMetrics. Its R-distribution arithmetic is copied verbatim from
  summarize_r (the byte-pinned floats must not be algebraically refactored);
  the two copies are guarded in lockstep by a cross-reducer equality test.
  net_expectancy_r = expectancy_r (exact at the Stage-1 cost=0 invariant);
  conviction_terciles_r = [0,0,0] (per-trade conviction is not pooled).

CLI:
- walkforward gains --strategy + the four stage1-r grid flags (reusing
  parse_csv_list / Stage1RGrid); walkforward_family is strategy-dispatched.
- Windowed stage1-r helpers: stage1_r_sweep_over (reduce-mode folded IS
  sweep, O(trades)/member) and run_oos_r (non-reduce OOS run, for the
  stitched pip-equity curve), plus stage1_r_space.

Frictionless Stage-1 R (costs are Stage-2). Verified: cargo build clean,
full `cargo test --workspace` green (SMA walkforward, synthetic mc,
stage1_r_single_run_output_golden, and the C18 round-trips all preserved),
clippy -D warnings clean. Monte-Carlo R-bootstrap is iter 2.

refs #139
2026-06-26 11:21:27 +02:00
Brummel 7e4b91ab5f feat(0074): source real-path pip from the geometry sidecar; drop the authored floor
The data-server now ships per-symbol geometry sidecars, so aura no longer
hard-codes instrument geometry. A consumption grep showed production reads
exactly one `InstrumentSpec` field — `pip_size` — at the real-data path; the
other eight (incl. `instrument_id`) were constructed but never read.

- aura-cli resolves the real-path pip from the recorded sidecar
  (`instrument_geometry` -> `InstrumentGeometry.pip_size`), refusing (exit 2)
  on absent geometry. The authored table no longer gates: any symbol with a
  sidecar + bars now runs (new test: USDJPY, never vetted, resolves its JPY
  pip 0.01 from the sidecar).
- Removed the redundant authored floor: `InstrumentSpec`, `instrument_spec`,
  `VETTED_SYMBOLS`, and the table-only tests. The `InstrumentGeometry` seam
  (the surviving pip source) and its nameability guard are kept.
- `instrument_id` (the "find out"): no production consumer. The position-event
  table's `instrument_id` is a decoupled caller parameter; the engine never
  imports an instrument spec. Dropped with the struct.
- The example `pip_size_of` is re-sourced to the sidecar; the pure-structural
  blueprint tests use a literal pip to stay archive-independent.

Speculative deploy-grade fields and the #124 override/floor tiers are deferred
to the Stage-2 broker that will consume them, read from the sidecar geometry
then. Ledger (C8/C15/#22/#106) updated; #124 collapses to "recorded sidecar
geometry -> refuse" in the meantime.

Verified: cargo test --workspace (all green), clippy -D warnings clean, cargo
doc clean, with the local archive present so the sidecar paths run.

refs #124
2026-06-25 19:06:12 +02:00
Brummel 216cc8c417 feat(0072): stage1 mean-reversion candidate — EWMA Bollinger fade (#137)
Third price-only Stage-1 R strategy: aura sweep --strategy stage1-meanrev fades
deviation from a rolling mean (price above mean+k*sigma -> short, below -> long,
latched +-1), feeding the unchanged bias -> RiskExecutor (vol-stop = R) -> flat-1R
seam. sigma = Sqrt(Ema((price-Ema(price))^2)) reuses the vol_stop deviation-
squared-then-smoothed shape, so the band is built from existing aura-std nodes
(Ema/Sub/Mul/Sqrt/LinComb/Add/Gt/Latch) -> ZERO new nodes. No Delay: the current
bar legitimately belongs to its own band (causal, C2). Swept knobs: --window (one
Ema length ganged across mean+variance) and --band-k; downstream vol-stop R knobs
unchanged.

An in-file test reads the SHIPPED graph's exposure tap to pin the fade polarity
(short above the band, long below) — a latch-set/reset copy-paste from breakout
would invert the signal yet leave the fold-vs-raw and window-grid CLI tests
green. All 588 workspace tests pass (existing R/sma/momentum/breakout goldens
byte-identical); clippy clean.

refs #137
2026-06-25 14:27:30 +02:00
Brummel d845c509d3 feat(0071): stage1 breakout candidate — Donchian channel signal (#137)
A second Stage-1 R strategy candidate for the edge-research milestone, a structurally
different trend mechanic than the refuted SMA-momentum: a Donchian channel breakout.
Swaps only the signal leg of the stage1-r graph —
close -> Delay(1) -> {RollingMax,RollingMin}(N) -> {Gt,Gt} -> {Latch,Latch} ->
Sub = bias in {-1,0,+1} — keeping the vol-stop R definition unchanged, so it screens
under the identical R yardstick (clean A/B vs momentum).

- aura-std: new RollingMax / RollingMin nodes (sliding-window extremum via a monotonic
  deque, O(1) amortized; mirror the Sma/Delay ring-buffer shape, warm-up skip-emit,
  C7/C8). The +/-1 direction latch composes from two existing Latch nodes + Sub (no new
  latch type).
- aura-cli: stage1_breakout_graph + a manual-grid stage1_breakout_sweep_family
  (fully-bound graph per point, sidestepping parameter-ganging since one channel length
  drives both rolling nodes); --strategy stage1-breakout + --channel grid flag; usage
  strings updated.
- Causality (C2): one Delay(1) on close feeds both rolling nodes, so each channel covers
  close[t-N..t-1] — the current bar is excluded. Pinned by a contrastive e2e test (a
  strictly rising series up-breaks every warmed bar; a window including the current bar
  never could) plus a +/-1-latch-hold test.

All existing goldens byte-identical (stage1-r/sma/momentum untouched); folded-no-trace
== raw-trace metrics; workspace tests + clippy green.

refs #137
2026-06-25 13:10:37 +02:00
Brummel eb8653488f feat(0070): wire folding sink reductions into the stage1-r sweep (closes #138)
Completes the M3 fix for BLOCKER #138. The no-trace stage1-r sweep now folds its
reductions online: `stage1_r_graph(reduce=true)` wires `SeriesReducer` (equity,
exposure -> one summary row each) and `GatedRecorder` (gated on
`closed_this_cycle` -> only the trade rows + the final row), and the folded
consumer rebuilds `RunMetrics` from those compact outputs. Per-member memory drops
from O(cycles) (~2 GiB over ~5.5M one-minute bars) to O(trades), so full-history
multi-member sweeps no longer exhaust RAM+swap. The `r_equity` tap is omitted when
reducing (it is consumed only by the `--trace` persistence path).

`reduce = trace.is_none()`: the `--trace` path and the single `run_stage1_r` keep
the raw recorders + `summarize`/`summarize_r` over full rows — the byte-for-byte
reference. A new CLI-seam test
(`sweep_strategy_stage1_r_folded_no_trace_metrics_equal_raw_trace_metrics`) proves
the folded no-trace sweep reports byte-identical per-member metrics to the raw
`--trace` path.

Verified: cargo test --workspace green (42 suites); cargo clippy --workspace
--all-targets -- -D warnings clean; stage1-r goldens byte-identical.

closes #138
2026-06-25 11:24:27 +02:00
Brummel e94b79eb98 test(stage1-r-sweep): RED — grid all four knobs from CSV flags
Executable-spec for the strategy-research enabling change: expose the
stage1-r sweep's hardcoded timescales as CLI grids so a slow
time-series-momentum edge can be screened across timeframes.

- sweep_strategy_stage1_r_grids_all_four_knobs_from_csv_flags (RED):
  `aura sweep --strategy stage1-r --fast 240 --slow 960
  --stop-length 240 --stop-k 2.0` must yield a 1-member family whose
  member params are fast=240, slow=960, stop_length=240, stop_k=2.0.
  Fails today: the four flags are rejected (exit 2), gridding absent.
- sweep_strategy_stage1_r_no_flags_keeps_the_default_grid (guard,
  green today): a no-flags sweep still yields the {2,3}×{6,12} 4-member
  family with the stop pinned at 3 / 2.0, so existing goldens stay
  byte-green once the feature lands.

GREEN side follows via implement mini-mode.

refs #137
2026-06-25 00:11:03 +02:00
Brummel 89d967a940 feat(0067): SQN100 metric + stage1-r sweep --trace
Two additive follow-ups to the cycle-0065/0066 R-sweep surface.

Part A (#130) — n-normalized SQN (SQN100): RMetrics gains sqn_normalized =
(mean_R/stdev_R)·√(min(n,100)), a turnover-robust ranking objective comparable
across sweep members with different trade counts. The raw sqn is kept
byte-identical (computed sqrt(n)*mean/sd, NOT via a factored shared ratio, so
the existing metric does not drift even by 1 ULP). The field carries
#[serde(default)] for C18 back-compat (a pre-0067 r: block deserialises with
sqn_normalized = 0.0). The registry learns Metric::SqnNormalized (opt-in; the
default ranker and raw sqn are unchanged), so `runs family rank sqn_normalized`
works. The 0066 single-run golden is re-baselined (the r: block gains the
field; for n=3 ≤ 100 the cap is inactive so sqn_normalized == sqn).

Part B (#135) — wire --trace for the stage1-r sweep: stage1_r_sweep_family now
honours --trace, persisting each member's equity/exposure/r_equity under
runs/traces/<n>/<member_key>/ via the existing persist_traces_r (mirroring
momentum_sweep_family); the interim run_sweep refusal guard is dropped. --trace
is now symmetric across all three sweep strategies and a swept member is
chartable (chart <n>/<member> --tap r_equity).

Tests: SQN100 cap / below-cap / empty + serde back-compat (engine); rank-by
sqn_normalized + unknown-metric message (registry); an E2E guard that both
SQN values fold from recorded records and agree below the cap (the cross-crate
column seam); inverted stage1-r --trace persistence + a CLI rank-by-
sqn_normalized integration. Full workspace suite green (527 passed, 0 failed);
clippy --all-targets -D warnings clean.

Derived fork decisions logged on #130 and #135 (cap = fixed const 100; opt-in
metric, default ranker unchanged; serde(default); Part B mirrors
momentum_sweep_family + reuses persist_traces_r).

closes #130
closes #135
2026-06-24 18:10:54 +02:00
Brummel 499121e5c8 fix(stage1-r): record fixed R-defining params in swept member manifests
Cycle-0066 close audit found a C18/C10 gap: a stage1-r sweep member's manifest recorded only the floated knobs (fast.length, slow.length) via zip_params, omitting the fixed R-defining params (stop_length, stop_k, bias_scale) that the single run records. A family member must be reproducible from its own manifest (C18), and the constant stop -- which defines 1R -- must be visible so cross-member SQN comparability (C10) is auditable from the family record, not just the swept fast/slow knobs. The sweep run_one now appends the three fixed params, matching the single run; the integration test pins their presence per member.

Verified: full workspace suite 521 passed / 0 failed; clippy -D warnings clean.

refs #133
2026-06-24 16:21:26 +02:00
Brummel b8ed6f4858 feat(stage1-r): R sweep families + rank-by-SQN
Realizes #133 (cycle 0066): SQN becomes the single-number objective for ranking a
Stage-1 sweep family by R signal quality.

aura-registry — metric_cmp learns three higher-is-better R metrics (sqn,
expectancy_r, net_expectancy_r) that reach into RunReport.metrics.r; a member with
r:None sorts to the worst position (treated as -inf via total_cmp), so a pip-only
family ranked by an R metric degrades to ordinal order without error. The
UnknownMetric "known:" list is extended. No persistence change — the family store
already carries the r block.

aura-cli — `aura sweep --strategy stage1-r` sweeps the stage1-r harness over a
fast×slow SIGNAL grid (4 members), each folding the dense R-record via summarize_r so
its RunReport carries r:Some(..). The grid varies only the signal: the stop and
sizing are held fixed because risk_budget is R-invariant (#128) and bias.scale is
sign-only under flat-1R (both degenerate axes for the ranked metric), and because the
stop defines the R unit — varying it would break cross-member SQN comparability (the
motivation for the deferred SQN100, #130). The stage1-r topology is extracted into a
shared stage1_r_graph(.., fast_len, slow_len) helper: the single run binds
Some(2)/Some(4) (output byte-unchanged), only the sweep floats the knobs.

Per-member --trace tracing is deferred (a follow-up): rather than accept the flag and
silently write nothing (a quality-review catch), `aura sweep --strategy stage1-r
--trace` is refused explicitly (exit 2); --name records the rankable family.

Verification — a golden characterization test (stage1_r_single_run_output_golden,
added against HEAD before the refactor) verifies the helper extraction is
byte-preserving; full workspace suite 521 passed / 0 failed; clippy --all-targets
-D warnings clean. The grounding-check that signed the spec first BLOCKed a false
"already byte-pinned" claim, which is why the golden is added rather than assumed.
All load-bearing forks (signal-only grid, None-sorts-worst, --trace refusal, helper
extraction, golden shape) are recorded on the #133 decision log.

closes #133
refs #117 #130
2026-06-24 16:15:45 +02:00
Brummel 68605b7d88 refactor(stage1-r): rename the strategy-output param namespace exposure -> bias
Complete the deferred rename tail from cycle 0065's #126 (which renamed only the
typed metric, keeping the param namespace uniformly "exposure" so the single-run
rename would not smuggle in a ~30-site sweep-pinned change). Now renamed together
as one consistent unit, all driven by the Bias node's instance name:

- Bias::builder().named("exposure") -> .named("bias") (the harness builders +
  the engine fixtures in test_fixtures.rs / blueprint.rs);
- the derived knob path exposure.scale -> bias.scale (Composite::param_space
  derives the knob from the node name) -- the sweep .axis/.range bindings and
  .with() points (blueprint.rs, main.rs), the walkforward ParamSpecs
  (walkforward.rs), the member_key dir names, and the JSON byte-pins in
  cli_run.rs / report.rs / main.rs tests;
- the exposure_scale manifest param label -> bias_scale (the single-run manifest
  builders + the streaming_seam / real_bars fixtures).

The rename is surgical: the three targets (named("exposure"), exposure.scale,
exposure_scale) are syntactically distinct from the deliberately-kept "exposure"
forms, which are untouched -- SimBroker's pre-reframe exposure input slot +
prev_exposure + the exposure-integral; the LongOnly / gate / latch ports named
"exposure"; and the on-disk "exposure" tap label (the recorded bias series that
feeds `chart --tap exposure`, decoupled from the node name via
ColumnarTrace::from_rows("exposure", ...)).

No on-disk back-compat break: the knob path is a runtime param address, and
recorded params in old runs.jsonl / families.jsonl are inert data strings (a
pre-existing recorded "exposure.scale" still loads, it is just data).

INDEX.md cycle-0065 realization note amended: the param namespace is recorded as
the now-completed tail; the SimBroker slot + the on-disk tap label remain the
deliberate permanent keeps (#117), and exposure_sign_flips stays a serde alias.

Verified: cargo build --workspace --all-targets clean; clippy --all-targets
-D warnings clean; full suite 514 passed / 0 failed (source + byte-pins renamed
together); live `aura run` emits "bias_scale", `aura sweep` emits "bias.scale".

closes #134
refs #126 #117
2026-06-24 14:40:51 +02:00
Brummel 5c6de45b6e fix(cli): list valid taps on bad --tap, make --help uniform across subcommands
Two CLI-UX defects the Stage-1 R fieldtest surfaced (#131):

1. Tap discovery. `aura chart <name> --tap <bad>` rejected an unknown tap but
   the error omitted the valid names. filter_to_tap now enumerates the run's
   available taps in the message ("... (available: equity, exposure)"), so the
   user can correct a typo instead of guessing -- refuse, but help.

2. Uniform --help. `aura chart --help` errored with "unexpected chart argument
   '--help'" (exit 2) and `aura run --help` leaked usage to stderr via the
   error path, while bare `aura --help` printed to stdout (exit 0). main() now
   intercepts `--help`/`-h` anywhere in the invocation, prints usage to stdout,
   and exits 0 -- uniform for every subcommand. The now-dead bare-flag match arm
   is removed (the early intercept covers it).

RED-first: the existing chart_tap_nonexistent test gains an available-taps
assertion (was: message had no names), and a new per_subcommand_help test pins
`aura <sub> --help` -> stdout usage + exit 0 for run/chart/sweep/walkforward/
mc/graph/runs (was: chart --help exited 2). Unknown-subcommand stays exit 2.

Verified: clippy --all-targets -D warnings clean; full suite 514 passed / 0
failed (one new test); live `aura chart --help` prints usage to stdout, exit 0.

closes #131
2026-06-24 14:35:51 +02:00
Brummel c6c1d3bb10 feat(stage1-r): rename the exposure_sign_flips metric to bias_sign_flips (#126)
Complete the typed-field half of the exposure->bias rename - the one the cycle-0065
close audit flagged as aging worst (a serde-stable on-disk key).

- RunMetrics.exposure_sign_flips -> bias_sign_flips with
  #[serde(alias = "exposure_sign_flips")], so legacy runs.jsonl still deserialises
  (pinned by the legacy-read tests, which keep the old key); new output serialises
  bias_sign_flips (the byte-pin tests updated to the new key).
- The registry rank metric: Metric::ExposureSignFlips -> BiasSignFlips, the rank
  string accepts BOTH "bias_sign_flips" and "exposure_sign_flips" (CLI back-compat),
  the error/listing updated; the rank tests exercise both names.
- The MC aggregate field renamed too (McAggregate is hand-rendered to JSON, not
  serde-derived - no alias needed).

Scope held to the typed metric. The strategy-output PARAM NAMESPACE - the
.named("exposure") Bias instance, its exposure.scale knob path (Composite::param_space
derives the knob from the node name), and the exposure_scale manifest label - is a
coherent ~30-site, sweep-pinned surface (the implement loop correctly flagged renaming
the knob as an unscoped expansion touching walkforward + the cli_run byte-pins). Kept
uniformly as "exposure" and deferred to a follow-on so the cluster renames as one
consistent unit. SimBroker's pre-reframe exposure concept and the on-disk "exposure"
tap label stay by the #117 fork decision.

cargo test --workspace green; clippy --workspace --all-targets -D warnings clean; a
live `aura run --harness stage1-r` emits "bias_sign_flips".

closes #126
refs #117
2026-06-24 13:11:46 +02:00
Brummel a6fc48daa7 feat(stage1-r): operate the R layer from the CLI (iter 3)
`aura run --harness stage1-r [--real <SYM>] [--trace <n>]` now runs a Stage-1 R
backtest and prints its signal-quality metrics (the R block) in the RunReport
JSON - one shell call, the research loop's primary verb, ergonomic for Claude
to drive.

What shipped:
- vol_stop + risk_executor promoted from test fixtures to public aura-engine
  composite-builders, with a StopRule{Fixed,Vol} axis (C11). aura-std becomes a
  normal dependency of aura-engine (the composites are the engine's convenience
  layer over the standard nodes - the sibling tier of summarize_r; the graph
  stays acyclic: aura-engine -> aura-std -> aura-core). Both fixtures call the
  public fns; a Vol-backed RED test pins the new match arm.
- stage1_r_blueprint fans one bias into BOTH a SimBroker (pip) and the
  RiskExecutor (R), so one report carries both yardsticks honestly. run_stage1_r
  folds summarize (pip) + summarize_r (R, round_trip_cost 0.0 - Stage-1 is
  frictionless signal quality) and sets RunMetrics.r = Some(..). An r_equity tap
  (cum_realized_r + unrealized_r via LinComb) persists through a separate 3-tap
  persist_traces_r and renders via the existing `aura chart --tap r_equity`.
- `aura run --harness <sma|macd|stage1-r>`: a parse_run_args tokenizer replaces
  the five `run` literal-slice arms so --harness/--real/--from/--to/--trace
  compose in any order; --macd kept as a back-compat alias; --harness sma the
  default. A compile-time match over Rust-authored built-ins - not a runtime node
  registry, not a DSL (C9/C17): the CLI runs an authored harness, it does not
  wire one.

Back-compat: --harness sma/macd leave RunMetrics.r = None, so skip_serializing_if
keeps pip-only and legacy runs.jsonl JSON byte-unchanged; the plain-run tap list
stays ["equity","exposure"].

Refactor beyond the plan (verified behavior-equivalent): the old standalone
parse_real_args was orphaned by the new tokenizer, so it and its now-redundant
unit test were removed and run's --real parsing inlined into parse_run_args. The
--real strictness (empty-symbol, non-i64 ms, repeated-flag, window-requires-real
-> exit 2) is preserved and now pinned by
parse_run_args_rejects_malformed_real_and_repeated_flags (added to restore the
deleted coverage). All five run_real integration tests stay green.

Verification: cargo build/test --workspace green (every crate, 0 failed); clippy
--workspace --all-targets -D warnings clean. The full diff was adversarially
reviewed (run-arg refactor equivalence against the removed function, harness
wiring, C1/C2/C9/C17, the serde back-compat pin) - verdict SOUND; the one flagged
coverage gap is closed here.

Follow-on (noted, not done): the stage1-r manifest reuses the
"sim-optimal(pip_size=...)" broker label; a dedicated "risk-executor" label would
read more honestly for a dual-tap harness that runs a RiskExecutor branch.

closes #129
refs #117 #128
2026-06-24 12:09:58 +02:00
Brummel 476342d7b1 feat(chart): decimate served pages + run-context header (visual World cut 2)
Hardens the families-comparison chart page (#107) so it is openable and legible on
real multi-year data. Two halves, both confined to the CLI render path (engine +
registry untouched, C14):

#108 — serve-time min-max decimation. A pure `decimate(ChartData, buckets)`
transform partitions the shared union-ts spine into <=~2000 buckets and emits each
series' per-bucket min+max (nulls preserved), bounding the served page to a few
thousand points regardless of the underlying M1 point count. Applied in emit_chart
on both the single-run and family paths; a no-op under budget (every existing
fixture). Full recorded data stays on disk; only the page is thinned. For a
walk-forward family the null-fill blow-up collapses for free (an all-null bucket
stays null). Deterministic (C1).

#102 — a run-context header. A `ChartMeta` (name/commit/window/broker/seed/taps,
+ member count for a family, + bound params for a single run) is built from the
RunManifest in build_chart_data / build_comparison_chart_data (which gain a `name`
arg), injected into window.AURA_TRACES.meta, and rendered by a new pure buildHeader
in chart-viewer.js (headless .mjs-guarded). The family window is the SPAN across
members (min from, max to), not the first member's window — the only reading that
labels a disjoint walk-forward family's true OOS coverage (it collapses to the
shared window for sweep/MC). Reuses the existing render_value for param display
(no new Scalar Display, keeping C7's tag-only param plane). The earlier
"manifest is NOT carried into the page" render pin is flipped to a positive one.

Verified: cargo test --workspace = 446 passed / 0 failed; cargo clippy --workspace
--all-targets -D warnings clean. New coverage: 5 decimate unit tests, single-run +
family meta wiring (incl. the span-window invariant), a buildHeader headless guard,
and two end-to-end cli_run tests pinning the served-page meta at the binary boundary.

closes #108
closes #102
2026-06-22 12:37:09 +02:00
Brummel 4c64feb9ed feat(chart): aura chart <family> overlays a family's members; --tap + write-guard
`aura chart <name>` now branches on the name's on-disk kind: a single run charts
its taps as before; a FAMILY overlays one tap (default equity) across all its
members in one self-contained uPlot page, consistent across sweep / MC /
walk-forward. Members share ONE y-scale (they measure one quantity — that is what
makes the overlay comparable, unlike the single-run overlay of different taps),
and align on the union-ts spine via the existing join_on_ts: sweep/MC members
share the window (true overlay), walk-forward members are disjoint OOS windows
(null-complementary -> the stitched curve). One mechanism, three correct readings.

- build_comparison_chart_data (one labelled Series per member, shared y_scale_id);
  filter_to_tap (single-run --tap restricts to one tap; no --tap = all taps,
  unchanged); parse_chart_args (`<name> [--tap <t>] [--panels]`, any order);
  emit_chart name-kind branch; USAGE updated.
- Write-guard: ensure_name_free is now called once per tracing command (run /
  run --macd / run --real -> Run; sweep / mc / walkforward -> Family) so a name
  cannot be reused across kinds — name resolution stays a total function.

Refuse-don't-guess throughout (exit 2, never panic). Engine untouched (C9/C14).
Tasks 2-3 of plan 0061; Task 1 (registry) landed in 9b2adcb.

Verified: cargo test --workspace green (no failures; new unit + integration tests
incl. family-overlay, single-run --tap filter, cross-kind collision RED-then-green),
cargo clippy --workspace --all-targets -D warnings clean, cargo build --workspace
clean. Benign clippy-forced deltas vs the literal plan (no behaviour change): a
local MemberRows type alias (type_complexity), let-chains in the six guards
(collapsible_if), and dropping the now-dead TraceStoreError import.

closes #107
2026-06-21 18:54:17 +02:00
Brummel 8e5d14b2bb feat(real-family): --real parser + dispatch wiring; complete the feature
Plan 0060 Tasks 4-5 (#106): wire `aura sweep|walkforward --real` end to end.

- A shared RealWindowGrammar accumulator holds the `--real <SYMBOL> [--from <ms>]
  [--to <ms>]` grammar (flag-repeat strictness, empty-symbol + window-without-real
  rejection) for both parsers, mirroring parse_real_args.
- parse_sweep_args grows the DataChoice 4th element; parse_walkforward_args is new
  (no --strategy axis). The sweep + walkforward dispatch arms become rest-matched,
  build the DataSource via from_choice (build-or-refuse on un-vetted symbol /
  absent data), and run. USAGE documents the --real tails. mc stays exact-matched,
  so `aura mc --real` falls through to usage + exit(2) — Fork A: MC excluded (its
  seed varies a synthetic price-walk, undefined over real data).
- Gated integration tests (skip on no local data): sweep --real EURUSD persists 4
  portable member dirs over real bars and charts; walkforward --real EURUSD
  persists one oos<ns> dir per rolling window; same real sweep twice is
  byte-identical (C1). Un-gated: un-vetted symbol, window-without-real, and
  mc --real each refuse with exit 2.
- Remove the now-redundant `#[allow(dead_code)]` from the Task-1 provider: every
  const, both enums, and every method are live once the wiring lands.

Verified end-to-end over real EURUSD data: sweep --real -> 4 members; walkforward
--real over 2024 -> 9 rolling OOS windows; all refusals exit 2. Full workspace
test + clippy -D warnings green.

closes #106
2026-06-21 15:29:28 +02:00
Brummel 0b73a75d4b feat(momentum): bool-param demo strategy + aura sweep --strategy selector
Proves the #105 generic portable member key end-to-end: a demo strategy whose
params have nothing in common with the SMA-cross demo, swept by the same
machinery, with a bool axis surfacing — conformant — in the member dir names.

- momentum strategy: price -> Ema(length:i64) -> Sub(price-ema) ->
  Exposure(scale:f64) -> LongOnly(enabled:bool) -> SimBroker. Three swept params
  of three kinds incl. a bool; the exposure sink taps the post-gate exposure so
  the bool's effect is in the trace.
- aura sweep gains --strategy <sma|momentum> via a pure, unit-tested
  parse_sweep_args (mirroring parse_real_args); default = SMA-cross, so the
  existing bare / --name / --trace forms are unchanged.
- `aura sweep --strategy momentum --trace mom` persists 8 member dirs named e.g.
  ema.length-5_exposure.scale-0.5_longonly.enabled-true — the bool in the dir
  name, every name matching [A-Za-z0-9._-], each chartable.
- ledger (docs/design/INDEX.md): the C22/#101 member-key note's sweep clause
  amended to the generalised portable form (MC seed{N} / walk-forward oos{ns}
  unchanged).

Self-verified: cargo build --workspace, cargo test --workspace (all green incl.
momentum_param_space_is_ema_exposure_longonly, the 8-point determinism test,
the parse_sweep_args grammar test, and the momentum_sweep_trace integration test
asserting 8 portable bool-bearing dirs + a chartable member), cargo clippy
--workspace --all-targets -D warnings.

refs #105
2026-06-21 11:32:46 +02:00
Brummel 27f850dc52 feat(sweep-key): generic filesystem-portable member key + LongOnly bool node
Generalises the #105 foot-gun: the family-member trace key was derived from two
hardcoded axis names (f{fast}s{slow}), collision-free only over the built-in SMA
grid. It is now derived from the axes that actually VARY
(SweepBinder::varying_axes) and rendered as a filesystem-conformant directory
component:
- charset restricted to [A-Za-z0-9._-] (valid on Linux/Windows/macOS, also
  URL-path- and cloud-sync-safe); any other byte sanitised;
- case-less values (i64/timestamp decimal, bool true/false, f64 via Display, no
  scientific notation) so two members of one family never differ only by letter
  case -> no silent overwrite on case-insensitive filesystems (NTFS/APFS);
- length-capped (MAX_KEY=200) with a version-stable FNV-1a fallback for the
  unbounded-grid edge.

Pinned singleton axes are omitted; the SMA sweep's member dirs change from f2s4
to signals.trend.fast.length-2_signals.trend.slow.length-4 (its --trace
integration test + the stale prose updated accordingly).

Adds the first aura-std node with a bool *param*, LongOnly (a long-only exposure
gate: enabled=true clamps short exposure to >=0, false passes through) — the
block the momentum demo strategy (next commit) sweeps to prove the key generic
over a bool axis. The engine change is one additive pure read accessor; no
existing signature changes, trace state stays out of the engine.

Self-verified: cargo build --workspace, cargo test --workspace (all green incl.
the LongOnly node tests, the member_key worked-examples corpus, the varying_axes
accessor test, and the updated SMA sweep-trace integration test), cargo clippy
--workspace --all-targets -D warnings.

refs #105
2026-06-21 11:15:22 +02:00
Brummel d3cb5f8052 feat(family-traces): persist per-member streams for sweep/MC/walk-forward
Family runs (sweep, Monte-Carlo, walk-forward) ran a full harness per
member and drained its equity+exposure recorders, but immediately folded
the streams to metrics and discarded them — so the C21 family-comparison
view had no per-member data to chart. A new opt-in `--trace <name>` on
`aura sweep|mc|walkforward` now persists each member's streams to disk,
mirroring the single-run `aura run --trace` path. Any single member is
chartable today via `aura chart <name>/<member_key>`; the comparison view
itself is a later cycle.

Design:
- Persist INSIDE each per-member closure (aura-cli), reusing persist_traces
  verbatim — the engine and the family types are untouched. The closures
  already held the drained rows; they now bind them once, build the
  manifest, persist when --trace is set, then fold as before.
- Content-derived, deterministic member keys (MC seed{N}, sweep f{fast}s{slow},
  walk-forward oos{ns}). The engine HOFs are Fn + Sync — members run in
  parallel, so a runtime ordinal counter would be non-deterministic (a C1
  break); keys derive from what each closure receives deterministically.
  sweep_member_key panics (naming the axis) if the grid lacks a key axis,
  rather than silently collapsing two points to a shared dir.
- Each member is a standalone run-dir at runs/traces/<name>/<member_key>/
  (persist_traces with a slash-name; TraceStore nests via create_dir_all),
  so the existing single-run viewer charts a member with no view-side code.
- Opt-in: without --trace, stdout and the run registry are byte-unchanged;
  the cardinality cost of N members x full resolution stays user-chosen.

Out of scope (deferred): the family-comparison view (overlay/small-multiples),
decimation/LOD, a binary trace container.

Verified locally: cargo build --workspace, cargo test --workspace (all green,
incl. 3 new family persistence tests + a sweep determinism test + 2
sweep_member_key unit tests), cargo clippy --workspace --all-targets
-D warnings (clean). The pre-existing family determinism and single-run
trace tests pass unchanged (no-trace path is byte-identical).

closes #104
2026-06-20 19:56:36 +02:00
Brummel 15ee6d5f4f feat(trace-charts): serve persisted traces as a web chart (iter 2)
Iteration 2 (serve half) of the visual face — spec 0056 / plan 0056:
read a persisted run's traces and serve them as a self-contained uPlot
HTML chart. Completes the goal "tap data from a graph, serve it as a
chart in the browser".

- chart-viewer.js (first-party): a pure buildCharts(traces, mode) that
  maps the injected window.AURA_TRACES to uPlot configs (no DOM) — overlay
  = one plot, every series on its own y-scale over the shared xs; panels =
  one plot per series, all on the same timestamp x. A browser-only mount()
  instantiates the vendored global uPlot. Guarded by a node .mjs DOM test
  (+ a sparse/gaps variant) shelling out exactly like viewer_dot.rs.
- render_chart_html + ChartMode/ChartData/Series (aura-cli/render.rs):
  mirrors render_html — self-contained page, uPlot (1.6.32, vendored
  d534f10) + chart-viewer.js inlined via include_str!, data injected as
  window.AURA_TRACES.
- emit_chart + build_chart_data (aura-cli/main.rs): the spec-§6 3-step
  union-spine alignment — xs = sorted-dedup union of every tap's ts; a
  synthetic empty-payload spine + ALL taps as symmetric join_on_ts sides
  (no tap privileged, no point dropped); flatten each (tap,column) to a
  Series of Option<f64>. New arms `aura chart <name> [--panels]`; a
  missing run is stderr + exit 2 (TraceStoreError::NotFound), never a
  panic. USAGE advertises the verb in lockstep.

Verified by the orchestrator: `cargo test --workspace` green (incl. the
node guards, render_chart_html, and the chart e2e); `cargo clippy
--workspace --all-targets -D warnings` exit 0; and a real end-to-end —
`aura run --trace demo && aura chart demo` emits a 57KB self-contained
page (doctype + inlined uPlot + AURA_TRACES carrying the equity/exposure
series), `--panels` reflects the panels mode, `aura chart nope` exits 2.
Implementer deviation folded in (compiler-prescribed): serde added to
aura-cli/Cargo.toml for the Series derive (Cargo.lock updated).

refs #101
2026-06-19 00:52:45 +02:00
Brummel 8bb5256041 feat(trace-charts): persist recorder taps to disk (iter 1)
Iteration 1 (persist half) of the visual face — spec 0056 / plan 0055:
tap data from a graph and save it to disk so it can later be charted
(the serve/chart half is iteration 2).

- ColumnarTrace (aura-engine): a drained tap <-> columnar SoA form
  ({tap, kinds, ts, columns}); values coerced to f64 for plotting with
  the base type preserved in `kinds` (C7); to_rows rebuilds uniformly-f64
  rows for the serve-side join. Pure (C1), beside join_on_ts/summarize.
- TraceStore (aura-registry): a per-run directory under
  runs/traces/<name>/ — one <tap>.json per tap + an index.json (manifest
  + tap order), beside the families.jsonl sibling store. Missing run =
  NotFound (no panic); corrupt file = Parse{file}.
- CLI: a shared persist_traces helper threaded into run_sample /
  run_macd / run_sample_real via an orthogonal `trace: Option<&str>`;
  new arms `aura run [--macd] --trace <name>` and `--trace <name>` on
  `run --real`. The default (no --trace) run is byte-for-byte unchanged.

Verified by the orchestrator: `cargo test --workspace` green (incl. the
4 columnar_trace, 3 trace_store, 4 cli_run e2e, and the 4-tuple
parse_real_args tests); `cargo clippy --workspace --all-targets -D
warnings` exit 0. Implementer deviations folded in, all
compiler-prescribed: #[derive(Debug)] on RunTraces (the RED tests
panic-format a Result over it), #[allow(clippy::type_complexity)] on
the parse_real_args 4-tuple (mirrors the crate's sample_harness idiom).
Default-run byte-identity stays pinned by run_prints_json_and_exits_zero.

refs #101
2026-06-19 00:14:18 +02:00
Brummel 3dac2c0c99 refactor(aura-cli): retire the dead-end runs list/rank CLI surface
Since cycle 0045 the flat runs.jsonl store has had no producer (sweep/mc/
walkforward persist to the family store; `aura run` does not persist), so
`aura runs list` / `runs rank` read a store nothing populates — a live dead-end
surface, the honesty gap the Runway milestone closes.

Resolve the C18 deferred fork (INDEX.md) as (b) RETIRE: drop the two CLI commands
+ their dispatch arms, usage fragments, and the now-unused load_runs_or_exit.
Families (sweep/mc/walkforward, C21) subsume standalone over-time comparison. The
aura-registry flat lib API (append/load/rank_by/optimize) is RETAINED — optimize
backs walk-forward's in-sample step, rank_by backs `runs family ... rank`.
`runs families` / `runs family` are untouched.

Decision recorded on #73 (user-directed (b), 2026-06-18). Cosmetic sub-item
(json! family-wrapper -> to_json() stdout key-order unification) deferred to a
follow-up: json! routes the embedded report through serde_json::Value, which
re-alphabetizes keys, so it needs manual JSON construction, not a trivial swap.

RED-first: the retire test asserted `runs list`/`rank` now exit 2 (was exit 0).
Verified: cargo build --workspace, clippy --all-targets -D warnings, and
cargo test --workspace all green.

closes #73
2026-06-18 19:53:26 +02:00
Brummel 26bec6d64a feat: per-instrument pip metadata channel for the sim-optimal broker
The data-server symbol stream carries only price bars, no instrument metadata.
So SimBroker was constructed with one global pip_size literal (0.0001, correct
only for 5-decimal FX), and `aura run --real <symbol>` emitted wrong-unit pip
equity for any non-FX instrument (AAPL.US, BTCUSD off by orders of magnitude).

Add the missing channel to *specify* per-instrument pip:

- aura-ingest: `InstrumentSpec { pip_size }` + `instrument_spec(symbol)` — a
  typed, Rust-authored vetted lookup (NOT Aura.toml; the later Aura.toml schema
  can subsume it). Seeded GER40/FRA40 = 1.0 (index points), EURUSD/GBPUSD/USDCAD
  = 0.0001 (5-dp FX majors); None for an un-specced symbol.
- aura-cli: `sample_harness` and `sim_optimal_manifest` gain a `pip_size`
  parameter; `run_sample_real` looks the pip up by symbol BEFORE any data access
  and refuses (exit 2) an un-specced instrument rather than guessing — honest by
  construction. The looked-up pip reaches both the broker divisor and the
  manifest broker label (`format!`). Every synthetic caller passes the unchanged
  0.0001, now named `SYNTHETIC_PIP_SIZE`. SimBroker (aura-std) is untouched.

Scope: narrowed to the CLI real path — the actual bug. The runnable GER40
examples already bake the correct 1.0 and are NOT buggy; centralizing them
through the lookup is deferred to #98.

Decisions (user-directed, recorded on #22): metadata channel = typed
InstrumentSpec at the source edge; un-specced real symbol refuses; seed =
GER40/FRA40 + FX majors; example refactor deferred. The spec went through the
grounding-check gate; the auto-sign panel surfaced the refusal-behaviour and
scope as genuine design decisions, escalated and resolved by the user.

Tests: instrument_spec unit; manifest pip rendering (1.0 -> "1", 0.0001
unchanged); non-gated CLI refusal (exit 2, no stdout leak, vetted symbol clears
the lookup); gated GER40 binary-boundary honesty (pip_size=1 in the emitted
manifest, deterministic). The pre-existing run_sample_real determinism test was
retargeted AAPL.US -> GER40 (AAPL.US is un-specced and would now refuse at the
lookup); the AAPL.US bounded-window streaming property still lives in
aura-ingest/tests/streaming_seam.rs (literal source, no spec lookup).

Verified: cargo build --workspace, clippy --all-targets -D warnings, and
cargo test --workspace all green; the gated GER40 path ran with local data.

closes #22
2026-06-18 19:36:43 +02:00
Brummel 86746e3d5d refactor(aura-core): Scalar as a native tagged enum, disjoint from Cell; typed RunManifest.params
Scalar was `struct { kind: ScalarKind, cell: Cell }` — "a Cell wearing a kind hat." The recorded reason for that shape was migration ease, which is not a design rationale (CLAUDE.md: rationale != effort), so the struct had no substantive defense. Redefine it as the native tagged union it conceptually is:

    enum Scalar { I64(i64), F64(f64), Bool(bool), Timestamp(Timestamp) }

This is substantively better on four axes: kind/bits skew becomes unrepresentable (illegal states gone); accessors panic loudly on the wrong variant instead of a release-mode silent bit reinterpret; PartialEq derives the documented IEEE-754 / cross-kind value semantics (the hand-roll, needed only because the struct inherited Cell's bitwise compare, is gone); and serde is a plain derive emitting the externally-tagged wire form ({"I64":10}/{"F64":2.5}) — the private ScalarRepr shadow enum that motivated this was never needed. It is also C7-honest: erased-on-the-hot-path (Cell) and self-describing-at-the-edge (Scalar) are two disjoint types bridged by explicit conversion.

The whole public API is preserved (Scalar's fields were private), so call sites do not churn: the enum change is contained to scalar.rs, where cell() now encodes and from_cell() decodes per kind. Cell and ScalarKind are untouched.

With Scalar serializable, lift RunManifest.params from Vec<(String, f64)> to Vec<(String, Scalar)>: the param's kind (an i64 length vs an f64 scale) now survives into the C18 record (runs.jsonl) and the CLI JSON instead of collapsing to f64. scalar_as_param_f64 is deleted; sim_optimal_manifest passes typed params through. This is a deliberate wire-shape change — params now render as tagged scalars; the JSON-asserting tests are updated to the new shape on purpose.

Hand-authored manifest params across the CLI's single-run/mc/macd sites use honest kinds (lengths -> i64, scales -> f64) so they match the sweep path (which already derives correct kinds via zip_params); their in-binary JSON assertions are re-tagged accordingly.

Walk-forward fork resolves with no code change: WindowRun.chosen_params stays Vec<Scalar> (the serializable record carrier), reduced to f64 only at the param_stability statistic boundary (scalar_as_f64 retained). Glossary 'cell' entry updated to describe Scalar as the disjoint tagged union, not 'a cell plus its kind tag'.

Gates: build --all-targets, test --workspace (incl. new scalar_serde_round_trips), clippy -D warnings, doc --no-deps — all clean.
2026-06-16 17:11:25 +02:00
Brummel bcd1072ca8 feat(aura-cli): aura mc + family-aware runs + window_of migration (0045 iter 2)
Iteration 2 of spec 0045 — the CLI surface for orchestration families.

window_of migration: every manifest-window scan (run_sample, sweep_family,
walkforward_family, sweep_over, run_oos, run_macd) now reads the window from
Source::bounds() via window_of(&sources) instead of prices.first()/.last().
Behaviour-preserving (the walk-forward roller boundaries are bar-aligned, so
the producer-supplied window equals the old first/last) — pinned by the
unchanged run_sample (1,7) and walk-forward determinism tests.

aura mc: new built-in Monte-Carlo family on the CLI (monte_carlo over a
built-in seed set + the sample harness re-seeded per draw), persisted via
append_family and rendered as one member line per seed (carrying the
family_id) plus an mc_aggregate line. McAggregate is not Serialize, so the
aggregate line is built from its three MetricStats blocks.

family-aware runs: aura sweep / walkforward / mc now persist to the family
store via append_family with an optional --name (per-kind default), printing
the assigned family_id per member. aura runs families lists family headers
(id, kind, member count); aura runs family <id> [rank <metric>] lists/ranks
one family's members as a unit. An unknown family id is an empty family
(exit 0); an unknown metric is a usage error (exit 2).

Two pre-existing cli_run.rs E2E tests were adapted to the new family
contract (a required consequence of sweep/walkforward switching from the
flat store to the family store — a plan gap caught at implement time): the
sweep-output test now asserts the family-wrapper, and the runs list/rank
flow became the family list / per-family-rank flow (which also exercises the
per-name counter sweep-0/sweep-1 end-to-end). A new E2E test drives aura mc
through the binary.

Known cosmetic detail: the family-wrapped sweep/mc/walkforward stdout nests
the report via serde_json::json! (to_value -> alphabetical keys, leading
"broker"), while runs family / runs list print via RunReport::to_json()
(declaration order, leading "commit"). Both valid JSON; key order is not
load-bearing and the stored families.jsonl uses to_string (declaration
order), so the round-trip is unaffected.

Verification: cargo test --workspace green; cargo clippy --workspace
--all-targets -D warnings clean.

refs #70
2026-06-15 18:27:54 +02:00
Brummel 94af4c788c feat(aura-cli): enrich the graph sample into a trend+momentum blend showcase
The built-in sample that `aura graph` renders and `aura sweep` runs is now a
"trend + momentum, weighted blend" strategy that exercises four authoring/viewer
capabilities the single-level sample left dark:
- multiply-nested composites: root -> signals -> {trend = sma_cross, momentum = macd};
- a multi-param node inside a composite: blend = LinComb(3) (weights[0]/[1] shown);
- a multi-output node: momentum (the macd composite) re-exports macd/signal/histogram,
  two of which the blend consumes;
- bind(): blend.weights[2] is fixed as a structural constant, so it drops out of the
  sweepable param surface (and renders absent).

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

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

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

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

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

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

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

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

closes #54
2026-06-11 19:43:36 +02:00
Brummel 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 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 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 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 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 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 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
Brummel 409b770ac6 test(aura-cli): RED for strict aura run arg-parse
`aura run extra-garbage` keys only on the first token being `run` and
ignores the rest, so it prints the full report and exits 0 -- a typo'd
`aura run --sweep` masquerades as a successful run instead of getting a
hint. The 0010 cycle_scope ("any other or missing subcommand -> usage +
exit 2") did not unambiguously cover `run <token>`; #16 ratifies the
strict reading.

This RED pins it: a trailing unexpected token takes the error path
(usage on stderr, empty stdout, exit 2), mirroring the no-args sibling;
bare `aura run` is held to its current happy path (JSON on stdout, exit
0) by a positive-preservation assertion so the strict guard cannot
regress it.

refs #16
2026-06-05 00:17:10 +02:00