Commit Graph

67 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 717a0b70af feat(0094b): content-id surface + Tier-1 stability — aura graph introspect --content-id (iter 2)
Completes #158 acceptance (acc 1 + acc 3) on top of iteration 1's reproduction:

- acc 1: `aura graph introspect --content-id` reads an op-script and prints the
  content id of its canonical blueprint — the SHA256 (hex) of the same
  blueprint_to_json bytes `graph build` emits. Extracted the single `content_id`
  primitive shared by this surface AND topology_hash, so the two command paths
  (this surface / hashing a `graph build` output) agree by construction. Verified
  live: both e2be81b2… for the SMA-cross op-script. Deterministic + distinguishes
  topologies; a malformed op-list fails cleanly (exit 2, no partial hash).
- acc 3: a Tier-1 optional field the blueprint does not use is tolerated by the
  loader (#156) and absent from the canonical omit-defaults form, so the content
  id is unchanged (content_id_is_stable_across_a_tolerated_tier1_field); plus
  content-id stability across the serialize -> reload -> re-serialize store
  round-trip (#164), the property reproduction rests on.

Finding (documented, filed forward): an op-script and the Rust `stage1_signal`
builder produce DIFFERENT canonical forms — the composite debug-name ("graph" vs
"stage1_signal"), a named vs unnamed Sub, and an unbound vs bound Bias.scale — so
they are different topologies by the byte definition and get different content
ids. Content-addressing keys on the canonical form, which currently includes the
composite debug-name (a non-load-bearing symbol, invariant 11). Whether the
content id should exclude the debug-name (a topology-canonical form distinct from
the byte-canonical form, which would also change the shipped cycle-0092
topology_hash values) is a forward design question, not this cycle.

Verified: full workspace suite green (51 suites); clippy -D warnings clean.

refs #158
2026-07-01 03:05:37 +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 b8b83cf1c9 feat(0091): byte-canonical blueprint emit; record the equality-surface decision
#164 — tighten the C24 "canonical, versioned" blueprint contract on two corners
the milestone fieldtest surfaced. Both forks were derivable; resolved with
rationale (recorded on #164).

Fork 1 — canonical artifact carries no trailing newline. `blueprint_to_json`
returns the JSON value with no trailing newline (647 bytes for the SMA-cross
fixture); `aura graph build` had framed it with a `println!` (648). The library
serializer is the single canonical source — the form content-addressed topology
identity (#158) hashes — so the CLI is a transport that must not mutate the
bytes: `build_cmd` now `print!`s the canonical bytes verbatim, making the CLI and
library emit paths byte-identical. RED-first: the new E2E
`graph_build_emit_is_byte_canonical_no_trailing_newline` pinned the trailing
newline (FAIL), green after the one-line change. The two cycle-0088 `.out.json`
goldens are re-recorded at 647 bytes. The loader stays lenient (a trailing
newline on input still parses, Tier-1 robustness; the milestone fieldtest's
`mt_4` tolerance check is unaffected).

Fork 2 — the canonical JSON is the blueprint equality/identity surface; no
second in-memory PartialEq/Debug is added. `Composite`/`BlueprintNode` cannot
derive them: `PrimitiveBuilder` (aura-core node.rs) holds a
`build: Box<dyn Fn(&[Cell]) -> Box<dyn Node>>` closure, neither comparable nor
printable. Equality could only be defined over the serialized data, of which
`blueprint_to_json` is already the single source; a separate in-memory equality
notion would be a redundant second source of truth and a drift hazard against
the form #158 hashes. Recorded the decision (discharges acceptance box 2's "or
JSON-string is the intended equality surface" arm) in the C24 ledger; no code
added for it.

Verified: cargo test --workspace green (51 suites, incl. the new RED->green E2E);
cargo clippy --workspace --all-targets -D warnings clean.

closes #164
2026-06-30 15:57:15 +02:00
Brummel 3c4b667955 feat: construction add names a node via name, mirroring the builder (#157)
The op-script authoring form is meant to be the Rust builder lifted into data —
every op is a 1:1 image of a GraphBuilder method. The one un-builder-ish key was
`add`'s `"as"`: the builder names a node with `.named("fast")`, so the data echo
is `"name"`, not the SQL-ish aliasing word `as`. Renamed the `add` wire key
`as -> name`.

`expose` keeps `as` — there it is a genuine alias (`expose bias.bias as bias`,
mirroring the builder's positional name arg). `source`/`input` keep their terse
verbs: those are plain shortenings of `source_role`/`input_role`, not a foreign
concept the way `as`-for-naming was.

CLI-side only: a one-attribute serde rename on the `OpDoc` Add DTO
(`#[serde(rename = "name")]`), since the engine `Op` is serde-free (the engine
field stays `as_name`, untouched). Updated the op-list test documents to the
`name` key (add ops only; expose's `as` left intact) and added an explicit pin
(`add_names_node_via_name_key`). The cycle-0088 fieldtest corpus
(fieldtests/cycle-0088-construction-op-script/*.json) is left on the old `as`
form as historical cycle-0088 evidence (it is not run by the suite).

Verified: full workspace suite + clippy green; `aura graph build` on a `name`-keyed
doc names the node and emits it in the #155 blueprint.
2026-06-30 12:22:45 +02:00
Brummel f1ab81d1b8 feat(0089): construction diagnostics read by-identifier (closes #162)
The cycle-0088 fieldtest found three presentation defects on the construction
op-script surface, all where the surface dropped out of its by-identifier register.
Presentation only — every fault still fires identically (same exit code, same
op/finalize attribution); only the rendered cause changes.

- Item 1 (finalize by-identifier). The holistic finalize fault leaked the raw
  index Debug form (`UnconnectedPort { node: 2, slot: 1 }`). `GraphSession::finish()`
  now translates the three reachable index-carrying CompileErrors into new
  by-identifier OpError variants (UnconnectedPort{node,slot} / RoleKindMismatch{role}
  / UnboundRootRole{role}) using the session's `ids`/`schemas` and the composite's
  `input_roles()` — the holistic gate calls (validate_wiring,
  check_param_namespace_injective, check_root_roles_bound) stay byte-for-byte
  unchanged (C24: no second validator; only the `.map_err` closures changed).
  `aura graph build` now prints `finalize: slot sub.rhs is unconnected`.
- Item 2 (bind mismatch prose). `format_op_error`'s BadParam arm matches the
  BindOpError variants instead of `{:?}`: `op 1 (add): param fast.length expects I64
  but got F64`, matching the connect mismatch register.
- Item 3 (discoverable bind form). `introspect --node` shows the typed-Scalar bind
  form: `param length:I64  (bind {"I64": <v>})`.

Item 1 spans both crates atomically (adding OpError variants forces the exhaustive
format_op_error match; the finish() translation flips behavioural test pins in both
crates). Five test touches: the two in-file unit tests, the two E2E twins plan-recon
found that the spec's testing strategy under-enumerated
(construction_e2e.rs, tests/graph_construct.rs), and a new unbound-root-role test;
the implementer added RoleKindMismatch / Ambiguous/UnknownParam coverage too. The
CLI finalize test was tightened (orchestrator) to pin `sub.rhs` and the absence of a
raw `node:` index.

Spec boss-signed on a grounding-check PASS (docs/specs+plans/0089). Verified: full
workspace suite + clippy green; the three messages reproduce exactly against the
built binary on the fieldtest fixtures.
2026-06-30 11:49:03 +02:00
Brummel 25e452aaf7 feat(0088): §C construction op-script CLI — aura graph build/introspect
Iteration-2 §C of the construction service (#157), the CLI shell over the
iteration-1 engine core (ea1ca32 + 27ac4dc): a declarative, replayable JSON
op-list document drives the engine's per-op-fallible construction surface.

- graph_construct.rs (new, aura-cli): an `OpDoc` serde DTO (`#[serde(tag="op",
  rename_all="lowercase")]` + field renames type/as) deserializes the document
  and maps 1:1 into the serde-free engine `Op` — the wire format is a CLI
  concern, the engine owns no second representation. Bind values use the typed
  Scalar form (`{"I64":2}`), kinds the capitalized #155 form (`"F64"`).
- `aura graph build`: read the op-list from stdin, replay through std_vocabulary,
  emit the #155 blueprint JSON on success; on the first failing op print
  `op N (kind): cause` to stderr (the op-kind recovered from the retained parsed
  list) / `finalize: cause` for a holistic fault, and exit non-zero. A CLI-side
  format_op_error phrases each OpError (the engine error types stay Display-free).
- `aura graph introspect`: `--vocabulary` (std_vocabulary_types), `--node <T>`
  (a type's ports/kinds + param paths off the pre-build schema), `--unwired`
  (a partial document's open slots via GraphSession::unwired) — all build-free.
- Two new argv arms over the flat match; the bare `["graph"]` HTML-render arm
  (sample_blueprint, #159's concern) is left intact.

#157 acceptance now demonstrated through the CLI (E2E in tests/graph_construct.rs,
8 tests): a document builds to the #155 blueprint that compiles identical to its
Rust-built twin (C1); an invalid op is rejected at the op, named; introspection
answers vocabulary / ports+kinds / unwired slots without a build. Full suite
green (51 suites); clippy --all-targets -D warnings clean.

The JSON op-list reuses #155's Scalar/ScalarKind serde shapes; full harnesses
(SimBroker/Recorder construction-arg builders) await #156. Wire-format forks
derived + recorded on #157.

refs #157
2026-06-29 21:30:46 +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 c750b9fbc3 test: RED — family-emitting CLI commands must survive an early-closing stdout reader
Captures the pre-existing broken-pipe panic surfaced by the milestone fieldtest
([bug] finding): every multi-line subcommand (sweep / mc / walkforward /
runs family / generalize) panics "failed printing to stdout: Broken pipe
(os error 32)" and exits 101 when the stdout reader closes early (| head,
| less, a closed UI pane), instead of terminating cleanly. Root cause: Rust's
runtime resets SIGPIPE to SIG_IGN at startup, so a write to a closed pipe
returns EPIPE and println! panics rather than the process dying quietly.

crates/aura-cli/tests/cli_broken_pipe.rs spawns `aura sweep`, closes the stdout
read end mid-stream, and asserts no panic in stderr + exit != 101. Verified RED
(reproduces 6/6). The GREEN fix follows in the next commit.

refs #146
2026-06-26 21:18:46 +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 2c43296c2c feat(stage1-r): bias rename + stop-rule + position-management + summarize_r (iter 1)
Iteration 1 of the Stage-1 "R-based signal quality" cycle (spec 0065): a
strategy's signal quality, measured in R on its unsized bias stream, feed-forward.
New discrete-trade machinery, NOT a SimBroker extension.

What lands:
- exposure -> bias (refs #126): the Exposure node/type/file/output-field renamed to
  Bias (computation unchanged: clamp(signal/scale,-1,+1); the SEMANTICS change — the
  output is the unsized strategy bias, sizing leaves the strategy). Scoped to the
  semantic core; behaviour-preserving cosmetics are deferred (see below).
- stop-rule nodes (refs #119): VolStop = k*EMA(|price - prev_price|) (one fused node;
  the volatility that defines 1R, close-to-close — true-range ATR deferred, needs
  OHLC) + FixedStop (constant, the test fixture / fixed-vs-vol structural-axis sibling).
- PositionManagement (refs #127): the stateful heart. Latches the entry-cycle stop
  distance as the FROZEN R-denominator (never re-read), marks/exits no-look-ahead like
  SimBroker (a position earns from the next cycle; an exit realises against the cycle's
  close), and emits ONE dense per-cycle R-record (C8). Stop-outs are NOT capped at -1R
  (a gap through the stop realises R < -1 — the honest loss tail); R is computed
  size-invariantly (size = (exit-entry)*dir / latched_dist cancels). The trade ledger
  is the rows where closed_this_cycle; the R-equity is cum_realized + unrealized; a
  position open at window end is the last row (open=true) — explicit, never silent MtM.
- summarize_r + RMetrics (refs #129): the post-run fold (NOT an in-graph node —
  recorders drain post-run). E[R], win-rate, profit-factor, avg win/loss R, by-trade
  max R-drawdown, n_open_at_end (window-end force-close). SQN / conviction terciles /
  net-of-cost / RunMetrics.r are iteration 2.

Design adversarially hardened before implementation (12-juror refute panel; decisions
on #117). Ratified implementer deviations from the plan snippet, verified by hand:
- col-4 `direction` tracks the OPEN position at cycle end (window-end synthesis; names
  the reopened leg on a reversal), falling back to the closed trade's dir only when
  flat. summarize_r does not read col 4; the R-metrics are unaffected.
- .named("exposure") kept on the 4 previously-unnamed Bias nodes so the auto-derived
  param-path stays exposure.scale (no manifest/dir-name drift) — the unnamed nodes
  would otherwise flip to bias.scale.
- intra-doc links [`Exposure`] -> [`Bias`] in sim_broker/latch + the sample-model test
  pin (cosmetic, behaviour-neutral; broken intra-doc links fail cargo doc).
- the E2E drives a full bootstrapped Harness (stronger than the plan's direct-node
  drive — exercises the real cross-crate producer->consumer seam).

Deferred (behaviour-preserving, separate cosmetic pass — NOT this iteration):
RunMetrics.exposure_sign_flips / Metric::ExposureSignFlips, SimBroker/LongOnly
"exposure" input ports, the "exposure" tap/trace names, and the .named("exposure")
instance labels. Iteration 2: the Sizer seam, the RiskExecutor composite (+ the Veto
documented-seam), summarize_r enrichment, RunMetrics.r, and the CLI/recording surface.

Verification (orchestrator-run, not agent-claimed):
- cargo build --workspace: clean.
- cargo test --workspace: all green, 0 failed (incl. the new bias/stop_rule/
  position_management unit tests, the summarize_r arithmetic tests, and the
  stage1_r_e2e capstone + layout guard).
- cargo clippy --workspace --all-targets -- -D warnings: clean (exit 0).
- Keystone RED tests pass: no_lookahead_bias_exit_realises_the_held_move,
  stop_out_is_not_capped_at_minus_one_r, no_gap_stop_is_exactly_minus_one_r,
  reversal_closes_one_leg_and_reopens, open_at_window_end_is_carried_on_the_last_row.

refs #117 #119 #126 #127 #129
2026-06-23 19:48: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 06fdafa925 feat(trace-charts): keyboard chart control — pan / zoom-toward-cursor / reset / toggle
Keyboard control for the served chart, deliberately the discrete counterpart to the
glitchy hand-rolled pointer drag that was just removed: each key is one clean
setScale("x", …), deterministic, with none of uPlot's pointer-pan jank.

- arrows pan (15% of the visible span), +/- zoom, 0/Home reset, x toggles the spine.
- zoom anchors on the CURSOR: the x-value under the mouse keeps its relative position,
  so the point under the pointer stays put while zooming (centre when the mouse is off
  the chart). The mouse position is tracked per chart via posToVal on mousemove.
- keys drive every chart instance together, so panels stay x-aligned.
- uPlot's default mouse (drag = box-zoom, double-click = reset) is retained — you have
  both.

The load-bearing math is the pure keyXRange(key, lo, hi, anchor, ext) helper (the rest
is browser-only DOM wiring in mount); it is node-guarded by chart_viewer_keys.mjs, which
pins the zoom-toward-cursor invariant (anchor keeps its relative position) with an
off-centre anchor so a centre-zoom regression fails loudly. All five chart guards green.

refs #103
2026-06-19 23:28:54 +02:00
Brummel 08a272c73d fix(trace-charts): drop glitchy custom wheel/pan, restore uPlot default mouse control
The hand-rolled wheel-zoom + drag-pan (wheelZoomPlugin) was hakelig/glitchy —
uPlot is not built to be driven that way. Remove the plugin and the cursor.drag
override so uPlot's own defaults govern the mouse: drag = box-zoom on x,
double-click = reset to full range, with the legend value-readout and crosshair
unchanged.

Everything else from the Tier-1 pass stays: the ordinal (gap-collapsed) x-axis
default + continuous toggle, the ordinal-label honesty (ticks/legend report the
real xs[i]), and panel cursor-sync (cursor.sync key retained; only the drag
override and the plugin are dropped). The x-mode guard is flipped to pin the
revert — overlay carries no custom plugin and no cursor.drag override, panels
carry no plugin — and stays green alongside the other three chart guards.

refs #103
2026-06-19 23:15:39 +02:00
Brummel 45cc1c44c9 feat(trace-charts): ordinal x-axis default + zoom/pan/cursor-sync (viewer Tier-1)
The served trace chart's x-axis no longer plots against raw timestamps by
default — non-trading gaps (nights, weekends) left horizontal blanks that
dominated the view. The x spine is now ordinal by default: every recorded slot
sits at an even position, so gaps collapse and the signal fills the width. A
header toggle flips to continuous (real-time-proportional) spacing and back;
default ordinal honours the requested "x-axis should not be continuous", with
continuous as the one-click option.

Comfort, all on the read/render side (the engine and the Rust serve contract
are untouched — window.AURA_TRACES still carries the real xs, the ordinal
indices are derived client-side):
- ordinal mode keeps the axis honest: ticks and the legend value-readout map a
  collapsed index back to the real xs[i], never the index itself.
- wheel-zoom (x, cursor-anchored), drag-pan, double-click-reset on the plot
  (uPlot's default drag-box-zoom is disabled so drag pans).
- panels share a cursor.sync key, so the crosshair lines up across stacked
  panels.

Confined to chart-viewer.js (buildCharts gains an xMode param + a wheelZoomPlugin
factory; mount re-renders on the toggle) and render.rs (CHART_HEAD toggle button
+ a chart-only style). The two twin node guards (build/gaps) re-point to the
ordinal default in lockstep; a new x-mode guard pins ordinal-default / continuous
/ panel-sync / plugin wiring, and a label-honesty guard (gappy-traces fixture,
xs far from the index spine) pins that ordinal ticks report the real timestamp.

Verified: cargo test --workspace green (incl. the four chart guards + render
inline tests), clippy --all-targets -D warnings clean, e2e (aura run --trace +
aura chart) emits a self-contained page defaulting to ordinal with the toggle.

Spec docs/specs/0057, plan docs/plans/0057 (boss-signed; grounding-check PASS).
Derived-fork decision log in the issue comments.

closes #103
2026-06-19 19:00:33 +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