Commit Graph

95 Commits

Author SHA1 Message Date
Brummel 7e4e9a8827 feat(cli): annotator-stage seam flips + doc tidy + gated e2e with the mc suffix (0108 task 5)
The retired v1-boundary test is replaced by v2 pins: mc-before-wf
refuses with the executor shape prose (while process validate accepts
the same doc — the tier boundary observable across two commands, added
by the task-2 e2e phase and kept), single-instrument generalize
refuses data-free on the built fixture project, and the two
addressing-mode tests' stale rationale comments now name the
member-data seam they actually hit. PROCESS_BLOCKS doc strings drop
'in v1' (introspection text only — no content id moves). The gated
real-data e2e extends its process with the monte_carlo suffix and
asserts the pooled_oos bootstrap in the final campaign_run line — ran
its full assert path on this host.

Task-5 duplicates the plan had double-drafted (task 2's e2e phase
pre-landed three of its tests) were correctly NOT re-added.

Gates: workspace 1025/0 (fully green again), clippy -D warnings clean.

refs #200
2026-07-04 01:28:06 +02:00
Brummel ab778f2361 feat(campaign,registry,engine): record widenings + preflight v2 fault set (0108 tasks 1-2)
Task 1: RBootstrap and Generalization gain serde derives;
lineage.rs gains StageBootstrap (per_survivor | pooled_oos) and
CampaignGeneralization; StageRealization.bootstrap and
CampaignRunRecord.generalizations land serde-default sparse (a
pre-0108 campaign_runs.jsonl line still parses — pinned), threaded
None/empty through every literal site. No behaviour change.

Task 2: ExecFault::UnsupportedStage is gone; the preflight now admits
the v2 shape sweep [gate]* [wf]? [mc]? [generalize]? via a monotone
rank walk (precise per-pair PipelineShape prose) and statically
refuses: single-instrument generalize (GeneralizeNeedsInstruments),
non-R generalize metrics via check_r_metric (GeneralizeNonRMetric —
no fourth roster site), zero mc params (ZeroBootstrapParam). CLI
prose swapped accordingly. The loop's E2E phase added a tier-boundary
pin: process validate accepts [sweep, mc, walk_forward] while campaign
run refuses it data-free.

KNOWN RED (documented in the plan, flips in task 5):
crates/aura-cli/tests/research_docs.rs::campaign_run_v1_boundary_refuses_mc_process
still pins the retired 'not executable in v1' prose. Interim gates:
registry 57/0, campaign 33/0, cli --bin 111/0, workspace build clean.

refs #200
2026-07-04 00:38:25 +02:00
Brummel 4da47cb919 fix(cli): aura new skips git init inside an existing work tree (0107 fieldtest, orthogonal)
The cargo-new mirror completed: a scaffold landing inside a tracked
tree no longer nests a .git (the gitlink/submodule hazard that bit the
fieldtest corpus); outside a work tree the best-effort init is
unchanged. Oracle: git rev-parse --is-inside-work-tree in the target
dir; any failure reads as not-in-a-work-tree so the init stays
best-effort.

RED-first (tdd-author handoff): the inside-a-work-tree pin observed
failing on the nested .git; the outside guard pinned against
over-removal.

Gates: workspace 1003/0, clippy -D warnings clean.

closes #204
2026-07-03 23:19:34 +02:00
Brummel 8ef829b8cc feat(cli): process|campaign introspect --metrics — the roster split becomes discoverable (0107 F9)
The 17-name metric vocabulary lists with applicability tags derived
from aura-campaign's existing pub rosters (rankable | gate |
annotation) — no fourth roster site (#190); the mode rides the shared
introspect struct and answers for both doc families. The three
metric refusals (intrinsic unknown-metric, executor unrankable and
gate-not-per-member) now point at the mode, so the
exists-but-not-rankable split (profit_factor is emitted per member yet
not selectable) is no longer trial-and-error.

RED-first (tdd-author handoff): six same-seam pins (roster lines +
tags, one-mode guard incl. the extended usage line, both-families
parity, three refusal pointers) observed failing, then the mode +
hints landed.

The worked-examples half of #197 stays open (docwriter territory
post-stability, per the issue itself).

Gates: workspace 1001/0, clippy -D warnings clean.

refs #197
2026-07-03 23:15:16 +02:00
Brummel 1d87397c95 feat(research): draft placeholder refs ({} / null) read as open slots in the unwired probe
A campaign draft can now validate-so-far AND report its open ref slots
(0106 fieldtest F4): open_slots_campaign treats an absent ref, {} and
null uniformly as the open slot (strategies[i].ref with the two-arm
hint; process.ref with the content-id-only hint). Draft affordance of
the probe only — the strict parse tier keeps refusing placeholders (a
placeholder is not a valid document form; canonical bytes stay
untouched).

RED-first (tdd-author handoff): library pin + CLI --unwired e2e
observed failing (probe reported 'no open slots' over placeholder
refs), then the two ref predicates routed through ref_slot_is_open.

Gates: workspace 995/0, clippy -D warnings clean.

closes #195
2026-07-03 23:07:10 +02:00
Brummel 63d2657850 feat(cli): one bare content-id display form; CLI targets tolerate the content: prefix (0106 F3 / 0107 F6)
The copyable token is now the valid ref form everywhere: all three
register lines print 'registered <family> <id> (<path>)' and both doc
introspect --content-id modes print the bare 64-hex id (converging on
graph introspect's existing form). CLI FILE-or-id targets (campaign
run, graph introspect --params) strip a leading 'content:' before
resolving — a CLI arg is ephemeral input, leniency costs nothing. Doc
ref FIELDS stay bare-only (two accepted spellings would make
semantically identical docs hash apart — canonical-form byte
stability outranks input leniency); instead the referential not-found
prose appends 'refs use the bare 64-hex id — drop the content: prefix'
when the offending ref carries it.

RED-first (tdd-author handoff): eight same-seam assertions across the
three surfaces observed failing on the prefixed forms, then the five
display sites, two target ladders, and the conditional hint landed.

Gates: workspace 993/0, clippy -D warnings clean.

closes #194
2026-07-03 23:01:20 +02:00
Brummel c3d62f2ce3 feat(cli): graph register / --params accept the op-script form (fieldtest 0107 F5)
Both verbs now route through the same composite_from_any shape
discrimination file-mode --content-id shipped with (op-script array ->
one-build tail; envelope object -> loader), so the three on-ramp verbs
agree on accepted shapes and an op-script registers under the same
shape-invariant content id as its built envelope — no more raw serde
refusal ('invalid type: map, expected u32') hiding the graph build
bridge.

RED-first (tdd-author handoff): two same-seam behavioural pins
(register stores the envelope id for an op-script; --params matches
the envelope's axis lines) observed failing on the serde refusal, then
the two call sites rerouted.

Gates: workspace 990/0, clippy -D warnings clean.

closes #202
2026-07-03 22:49:12 +02:00
Brummel 2968fbef40 feat(registry,cli): referential tier requires full open-param coverage; bind refusal speaks the raw namespace
validate == runnable (fieldtest 0107 F7): validate_campaign_refs now
checks the reverse direction — every open param of the resolved
strategy must be bound by some campaign axis — emitting the new
RefFault::ParamNotCovered { strategy, param } with the RAW
param_space() path; ref_fault_prose phrases it ('open param "X" is
bound by no campaign axis'). The executor's every-open-knob-required
rule is thereby mirrored at validate time, and campaign run's
referential gate catches an uncovered knob before any member binds.
The e2e fixture that legitimately under-covered (fast only) gains the
slow.length axis.

Second half, RED-first: the member-bind refusal no longer leaks the
wrapped bind-time path — it names the raw campaign-axis namespace with
the wrapped path as a parenthetical ('open param "threshold" ...
(wrapped path: sma.threshold)').

RED evidence: E0599 on the pinned ParamNotCovered test (tdd-author
handoff), then the flipped bind_axes prose pin observed failing on the
wrapped form.

Gates: workspace 988/0, clippy -D warnings clean.

closes #203
2026-07-03 22:44:26 +02:00
Brummel 367f8678f1 feat(cli): #196 blueprint on-ramp — graph register, introspect --params, blueprint-file --content-id (0107 task 10)
Closes the F5 authoring gap: a campaign's strategy ref + axes are now
authorable from the public surface without running a sweep first.
'aura graph register <blueprint.json>' puts the canonical form into the
content-addressed store (id == topology hash) and prints the stored
path via the pub blueprint_path accessor; 'aura graph introspect
--params <FILE|64-hex-ID>' lists the RAW composite param_space — the
exact namespace campaign axes are validated against (the wrapped
--list-axes namespace stays the sweep-verb view); --content-id gains an
optional FILE that shape-discriminates a blueprint envelope from an
op-list document (stdin behaviour byte-identical without FILE).
is_content_id promoted pub(crate) and shared with graph_construct so
the two FILE-or-id surfaces cannot drift on the id shape.

RED evidence was produced retroactively by the loop (stash-probe: the
5 new tests fail for the expected reasons without the src changes) —
ordering deviation noted, outcome unaffected.

Gates: graph_ 46/0, workspace 987/0, clippy -D warnings clean.

closes #196
refs #198
2026-07-03 22:00:14 +02:00
Brummel aeb0366aed feat(cli): aura campaign run — the executor verb over the MemberRunner driver (0107 tasks 8-9)
campaign_run.rs: target resolution (file is register-then-run sugar;
bare 64-hex is the canonical id — #198 decision 1), project gate before
any store write, zero-fault referential gate, process fetch + v1
preflight, then aura_campaign::execute with the CLI MemberRunner over
the shipped loaded-blueprint convention: per-member blueprint reload,
reduce-mode wrap, unique suffix-join of raw campaign axis names onto
the wrapped param_space, windowed real data via the shipped ms->ns
source seam (absent archive/zero-bar windows -> NoData). Emission:
family_table / selection_report lines gated on the doc's emit list
(names debug_asserted against emit_vocabulary), the campaign_run
record line always, persist_taps deferred LOUDLY on stderr before
execution (F7 lesson), zero-survivor cells noted on stderr with exit 0.
exec_fault_prose/member_fault_prose keep the Debug-free house seam.

Seam tests: 8 campaign_run_* e2e tests incl. outside-project, bogus
target, unknown id, v1-boundary (mc process registers fine, run
refuses), persist_taps ordering, and the gated real-data
sweep->gate->walkforward e2e — which ran its full assert path on this
host (local GER40 2024-09 archive) rather than the data-less skip.
Also: exec.rs campaign-id guard tightened to lowercase hex, matching
is_content_id and the store's self-keyed form (review nit).

Gates: workspace 975/0, clippy -D warnings clean.

refs #198
2026-07-03 21:33:29 +02:00
Brummel 67aff34923 feat(research): std::walk_forward vocabulary corrected to machinery-true fields (0107 task 1)
folds/in_sample_bars/out_of_sample_bars mapped to nothing
WindowRoller::new accepts; the block now carries in_sample_ms/
out_of_sample_ms/step_ms/mode (WfMode: rolling|anchored) — the
roller's three lengths in the campaign window's own epoch-ms unit
plus both shipped RollModes (#198 decision 2). New intrinsic fault
ZeroWalkForwardLength (path-addressed) refuses zero lengths at the
doc tier, earlier than the roller's NonPositiveLength. Golden
canonical pin, PROCESS_FIXTURE, introspection tables, and the
aura-cli prose/fixture twins moved together; fieldtest corpus
untouched (historical record — stored walk_forward docs get new
content ids by design).

Gates: aura-research 14/14, aura-cli green, workspace build clean.

refs #198
2026-07-03 19:17:54 +02:00
Brummel ebe85683c9 audit: cycle 0106 tidy — ledger/glossary lift, unwired-hint fix, spec/plan retired
Architect drift review (holds: C14 headless authoring, C16/C17 no-DSL with
aura-research a true leaf, content-id move behaviour-preserving per the
untouched id goldens). Resolution per item:

- fix (docs): C18 gains the cycle-0106 realization note (processes/ +
  campaigns/ stores, document canonical form, the content-id primitive's
  library home, the referential tier, no-executor status); the #109
  analysis-meta-level open thread records the artifact half shipped v1 and
  what stays open (executor question, verb dissolution 'once it carries',
  the remaining #188 amendment package). Glossary: new campaign document +
  process document entries; content id generalized to all three artifact
  classes; experiment marked superseded by the campaign document (#188
  re-cut of the builder-API thread).
- fix (code): the --unwired guide advertised an identity_id process ref
  that validate_campaign refuses — the process.ref open-slot hint now says
  content-id-only (strategy refs keep both); lib + seam pins updated.
- filed forward: #190 (metric-vocabulary single-sourcing, the 0105 roster
  drift class), #191 (identity-ref store scan is O(store), performance
  only).
- carry-on: regression gates green by hand — cargo test --workspace 916/0,
  clippy -D warnings clean, cargo doc --no-deps 0 warnings; no baseline to
  ratify (project has no metric baseline scripts).

Cycle 0106 spec and plan retired per convention (git rm); durable record
is the ledger notes above plus #188/#189.

refs #189
2026-07-03 16:27:02 +02:00
Brummel a9e047eca7 feat(cli): 0106 tasks 11-12 — aura campaign verb family + workspace gates
The campaign half of research_docs.rs: validate (intrinsic always;
referential tier when a project env is discovered, with the explicit
skip line outside one), introspect (--vocabulary/--block/--unwired/
--content-id, same exactly-one-of usage guard), register via the
registry's campaign_path accessor, and RefFault prose (Display-free,
unit-pinned). Five binary seam tests (skip line, empty-axis prose +
exit 1, section vocabulary, spec-example open slots, content-addressed
registration) reuse the file's temp_cwd helper — the plan's
tempfile::tempdir() snippet deviated from the crate's zero-extra-dep
test convention and was adapted, disclosed in the loop's report.

The referential CLI branch inside a real loadable project stays
fieldtest territory by the plan's own scope note (project::load needs a
built dylib); its logic is pinned by the aura-registry tests.

Gates: cargo test --workspace 916/0 (885 pre-cycle + 31 new), clippy -D
warnings clean, cargo doc --no-deps 0 warnings. The worked spec example
runs end-to-end headless: author both documents as text, validate with
per-tier report lines, introspect vocabularies and open slots, obtain
stable content ids, register into the store — no Rust compile, no run
executed.

closes #189
2026-07-03 16:18:22 +02:00
Brummel db09e5de52 feat(cli): 0106 task 10 — aura process verb family (validate/introspect/register)
New research_docs.rs module: role-addressed process verbs with house-style
fault prose (Display-free enums phrased at the seam), the introspect
exactly-one-of usage guard (exit 2), and content-addressed registration.
Seven binary seam tests pin intrinsic-ok/refusal prose, vocabulary/block/
unwired/content-id introspection, exit codes, and the stored file's
location under the runs root.

Quality-gate finding folded in (and back into the plan): register no
longer re-derives the store path from runs_root — aura-registry now
exposes process_path/campaign_path, the same single mapping its put/get
pair routes through, so the printed path cannot drift from the store
layout.

Verification: research_docs seam tests 7/0, registry 49/0, clippy clean.

refs #189
2026-07-03 15:36:47 +02:00
Brummel 57f401f2ab refactor(std): std_vocabulary_roster! macro — one source for the vocabulary roster
The three hand-kept copies of the 22-key zero-arg roster in
aura-std/vocabulary.rs (std_vocabulary match arms, std_vocabulary_types
list, the test's inline array) collapse into one private declarative
macro invoked once with the "TypeId" => Type pairs — the resolver and
the enumerable list now agree by construction (the #160 failure mode:
a node resolvable but silently absent from graph introspect
--vocabulary, or vice versa). Byte-preserving: same fn signatures, same
22 ids, same order; lib.rs re-export and every consumer untouched; the
vocabulary stays a closed compiled-in set (invariant 9 / C24 — a
compile-time expansion, no registry). Adding a zero-arg node is now one
roster line plus the conscious count-pin bumps.

Tests: the two unit tests reshaped (round-trip iterates the generated
list with PrimitiveBuilder::label() as the independent oracle; the shape
test keeps the count pin as deliberate friction). The E2E phase added
one cross-boundary pin beyond the plan — kept:
graph_introspect_vocabulary_lists_exactly_the_closed_roster_count checks
the same count through Env::type_ids() and the CLI print loop across the
real process boundary, which no roster-internal test can see.

Accepted residual (documented at the roster site, recorded on #160):
a new zero-arg node never rostered at all stays unguarded — no
enumeration of zero-arg builders exists — and fails safe in both
directions (clean UnknownNodeType on load; merely absent from
--vocabulary).

Verification: cargo build clean; cargo test --workspace 885 passed /
0 failed (884 baseline + 1 new e2e); clippy --all-targets -D warnings
clean; cargo doc --no-deps 0 warnings.

closes #160, refs #180
2026-07-02 21:56:07 +02:00
Brummel 45fb06dba3 feat(engine+cli): topology-identity hash — --identity-id beside --content-id
Engine (aura-engine/blueprint_serde): blueprint_to_json factored into
build_doc/serialize_doc (byte-preserving — canonical golden unchanged) and
blueprint_identity_json added: the canonical document with every
non-load-bearing debug symbol (C23) blanked (composite name, instance
names, bound-param names, role names, output re-export names) while
everything load-bearing survives (type ids, node order, edges, role
targets/order, output pairs/order, bound pos/kind/value — openness stays
identity-bearing). Re-exported from lib.rs. Six property tests: renamed
twins (equal identity, unequal canonical), open-vs-bound, bound-value,
edge-swap, nested-composite interior names, role/output renames — the
last two are additive beyond the plan and cover the recursion and
role/output arms the planned four did not.

CLI (aura-cli): graph introspect gains --identity-id, a sibling of
--content-id through the same shared content_id SHA-256 primitive;
composite_from_str factored out of build_from_str (fault strings
byte-identical). The exactly-one introspect dispatch is DELIBERATELY
relaxed: the two id flags form one group and may combine — one build,
both ids, one per line, content id first. In-crate cross-path twin test
(Rust sma_signal vs op-script twin: distinct content ids, one identity
id) beside the existing cross-surface pins; four e2e tests incl. the
previously uncovered count!=1 usage exit-2 path.

topology_hash, the blueprint store, reproduce, and every --content-id
byte stay untouched (spec acceptance 3; all existing pins green).

Verification: cargo build clean; cargo test --workspace 884 passed /
0 failed (873 baseline + 11 new); clippy -D warnings clean; doc build
0 warnings. The implement-loop's Task-4 spec-compliance block was a
plan-byte count mismatch only (plan under-counted pre-existing
blueprint_serde tests 6-vs-7 and did not anticipate the two
sibling-accepted extra tests); gates re-run by hand, all green.

closes #171, refs #180
2026-07-02 21:24:18 +02:00
Brummel 560f594902 feat(cli): aura new — CLI wiring, load-bypass guard, authoring-loop e2e (0103 tasks 2-4)
Command::New + NewCmd + dispatch_new (usage faults exit 2, runtime
refusals exit 1, the C14 partition); the main() guard exempts aura new
from the eager project load so scaffolding works inside unbuilt trees.

E2e (tests/project_new.rs, 6 tests): scaffold -> cargo build ->
aura run twice byte-identical with manifest.project.namespace stamped;
introspect lists the project type beside std; refusals for existing
destination (1), invalid names (2), missing engine path with
--engine-path hint (1); aura new inside an unbuilt project tree
succeeds (the bypass proof); namespace override reaches the built
vocabulary (an additive e2e beyond the plan, kept).

Verification: cargo build --workspace clean; cargo test --workspace
873 passed / 0 failed; clippy -D warnings clean.

refs #180
2026-07-02 19:18:44 +02:00
Brummel 4928e289f7 feat(project): the project-as-crate load boundary (cycle 0102)
A research project is now a loadable external cdylib crate. Inside a
directory whose ancestry holds an Aura.toml, aura discovers the project
root cargo-style, locates the compiled dylib via cargo metadata (debug
default, --release opt-in), loads it load-and-hold, and refuses
mismatches before trusting anything: the AURA_PROJECT descriptor
(aura-core::project, #[repr(C)]) carries a C-ABI stamp prefix (rustc +
aura-core version, baked per consuming build by the new aura-core
build.rs) validated before any Rust-ABI field is read. The vocabulary
charter gates the merged resolution: project type ids are ::-namespaced
(std stays bare), duplicates refuse, and the enumerable type-id list
must agree with the resolver, so introspection can never silently omit
a project type.

All blueprint verbs resolve through the merged project + std vocabulary
via a per-invocation Env threaded through the dispatch chains;
registry, trace-store, and data paths anchor at the project runs root
(Aura.toml [paths], paths-only by design — instrument geometry stays
the recorded sidecar, C15). RunManifest gains the Tier-1 project
provenance field (namespace + dylib sha256 + best-effort commit),
stamped beside topology_hash on the blueprint-run paths; pre-0102
registry lines load unchanged. Default node names strip the namespace,
so :: never reaches the param-path address space.

Proven by the demo-project fixture (built by the e2e via cargo,
path-dep on this workspace): run twice bit-identical, provenance
recorded, introspection lists demo::* beside std, registry anchors at
the discovered root from a subdirectory; the badcharter fixture proves
the charter refusal through the real libloading path; a never-built
project refuses with a cargo-build hint. Outside a project every path
collapses to the previous literals — goldens and manifest pins
byte-identical.

Verification: cargo build --workspace clean; cargo test --workspace 862
passed / 0 failed (incl. 7 project_load e2e); clippy -D warnings clean
(one precedent-matching allow(too_many_arguments) on run_oos_blueprint,
whose arity the Env threading raised to 8); doc build unchanged.
Docs/ledger aligned: Aura.toml field lists are paths-only in
project-layout.md, glossary, C16/C17; new C13 realization note records
the per-invocation-reload reading and the load-and-hold one-shot scope
boundary.

New deps, per-case review (aura-cli leaf binary only, never the frozen
artifact): libloading, toml.

refs #180
2026-07-02 18:13:37 +02:00
Brummel 9402afe958 polish(cli): normalize usage-message casing to the clap house style
Every hand-rolled usage line now reads 'Usage: aura <verb> ...' — capital
prefix plus the program name, the shape clap itself and the built-in run
branch already spoke. 10 literal edits: 6 lowercase 'usage:' flips (graph
introspect, run blueprint x2, runs family, mc blueprint, mc built-in), 4 bare
closures gain the prefix (sweep/walkforward, blueprint + built-in branches);
the mc built-in line names the program in both |-separated alternatives.
Refusal diagnostics, the conforming run_args_from literal, clap output, and
the fieldtests corpus stay byte-identical; exit codes keep the usage=2 /
runtime=1 partition.

Lockstep pin: the single casing-sensitive assertion (mc --real) flips to
capital 'Usage'. Three additive regression tests pin the house style itself
(bare-closure prefix, both mc alternatives, mc blueprint branch) — the
pre-existing pins checked only verb tokens / a single Usage occurrence;
deviation from the spec's no-new-test-bytes wording recorded on the issue.

Verification: cargo test --workspace rc=0 (cli_run 133 passed), clippy -D
warnings clean, grep gates exact (zero lowercase 'usage:' literals; 11
'Usage: aura' source lines), smoke 'aura sweep --strategy bogus' prints the
house-style line and exits 2.

closes #179
2026-07-02 12:56:23 +02:00
Brummel 4de6d5cbad rename: retire the stage1-* family for the r-family (r-sma / r-breakout / r-meanrev)
The stage1 ordinal has been dead vocabulary since the C10 reframe dropped
the two-stage research model: a 1 structurally implies a 2 that no longer
exists. The family is renamed by its live discriminator - the R yardstick -
with members named by their signal, uniform with their signal-named
siblings (sma, macd, momentum):

- selectors: stage1-r -> r-sma, stage1-breakout -> r-breakout,
  stage1-meanrev -> r-meanrev (old tokens are usage errors, exit 2 - no
  silent alias)
- identifiers: Strategy::RSma/RBreakout/RMeanRev, HarnessKind::RSma,
  r_sma_*/r_breakout_*/r_meanrev_*, wrap_r, run_signal_r, RGrid, R_SMA_*
- persisted identity: the sma_signal composite (param prefix
  sma_signal.fast.length / .slow.length; fixtures regenerated; content-ids
  shift - no test pins a literal hash, the registry parses no record names)
- e2e test files git-mv'd to r_sma_e2e / r_breakout_e2e / r_meanrev_e2e
- dead Stage-1/Stage-2 prose reworded to post-C10 vocabulary across
  rustdoc, Cargo.tomls, ledger live lines, and the glossary (historical
  entries stay; fieldtests corpus untouched)
- CLAUDE.md invariant 7 rewritten to record the ratified C10 reframe
  faithfully (the token-swap alone would have laundered the retired
  gated-currency/realistic-broker design into unmarked live prose); the
  unbacked account-mode clause dropped

Verification: cargo build/test --workspace green (51 targets), clippy
-D warnings clean, doc build clean, acceptance grep gate leaves exactly
the one resampling-stage false positive (harness.rs), smoke: --harness
r-sma runs, --harness stage1-r exits 2 with the new usage line.

Decision log: forks and rationale recorded on the issue (reconciliation
+ implementation-phase comments).

closes #174
2026-07-02 12:03:09 +02:00
Brummel 88e9c9a539 fix(cli): symmetric sweep/mc family-verb polish (closed error, mc seed label, mandatory-knob docs)
Three polish items surfaced by the World/C21 milestone fieldtest:

- A sweep of a fully-bound (closed) blueprint is refused up front with a clear
  "this blueprint is fully bound; nothing to sweep" message — the symmetric
  inverse of blueprint_mc_family's closed-blueprint guard — instead of leaking a
  misleading UnknownKnob for a knob that is not unknown but bound out.
  blueprint_sweep_family's error contract widens to String; the sweep terminal's
  BindError (a genuine unknown/kind-mismatch axis) still surfaces verbatim.
- mc reproduce lines now carry each member's seed=<N> label. Monte-Carlo members
  hold no tuning params, so the member label was blank; the seed is each draw's
  realization identity. Sweep / walk-forward labels (which echo params) unchanged.
- docs (design ledger + glossary): state that every open knob --list-axes prints
  is mandatory on sweep / walkforward — a subset is refused with the missing knob
  named (BindError::MissingKnob), there is no default; pin a knob you do not want
  to vary with a single-value axis.

RED-first for the two behaviour items. One existing unknown-axis E2E is re-pointed
from the closed fixture (which now hits the new "nothing to sweep" pre-check) to
the open fixture; it still exercises UnknownKnob("nope") specifically — resolve
raises UnknownKnob in phase 1 before MissingKnob in phase 2 — so the property is
preserved, not weakened.

closes #178
2026-07-02 09:41:54 +02:00
Brummel e20dd9ae49 Merge main into the clap-adoption cycle — re-integrate the two parallel CLI fixes
main advanced with two `fix(cli)` commits (dec0780, 6a775f7) landed by a parallel
session while the clap adoption (cycles 0098/0099) was built from 780d823. Both
touch behaviour the clap migration rewrote or sits beside; this merge preserves
them rather than letting the rewrite silently drop them:

- dec0780 (open-blueprint guard, #176): `aura run <open blueprint.json>` must
  refuse with a clean exit 2, never a `compile_with_params` arity panic (exit
  101). The fix lived in the old hand-rolled `run` .json-arm, which the clap
  migration replaced — re-integrated into `dispatch_run`'s .json branch (the same
  `blueprint_axis_probe().param_space()` check + "closed blueprint" diagnostic).
- 6a775f7 (walkforward axis-once, #177): axis validation hoisted to a single
  dispatch-boundary pre-flight, in the execution fn `blueprint_walkforward_family`
  — which the migration did not touch, so it auto-merged unchanged.

Both fixes are verified by their own RED tests
(`aura_run_rejects_an_open_blueprint_without_panicking`,
`aura_walkforward_emits_an_unknown_axis_rejection_once`), and both are consistent
with this cycle's iteration-2 exit-code classification: an open blueprint and an
unknown axis are argument faults, so usage errors → exit 2.

The conflict was confined to `fn main` in crates/aura-cli/src/main.rs (the clap
`Cli::parse()` dispatch vs the old argv `match`); resolved to the clap side plus
the re-integrated guard. cli_run.rs auto-merged (their two new tests + this
cycle's renegotiated pins).

Verified green (orchestrator, this session): cargo build --workspace, cargo clippy
--workspace --all-targets -- -D warnings, and cargo test --workspace all clean.
2026-07-02 00:53:54 +02:00
Brummel fa42bf3878 feat(0099): CLI exit-code split (iteration 2) — usage=2, runtime=1
Apply the clean exit-code partition (#175 deviation #8) on top of the clap
migration: runtime failures now exit 1, usage errors stay exit 2, with no
same-class inconsistency.

Partition (attribution principle): exit 2 = a command-line fault (a bad flag
value, an inapplicable combination, or the content of an argv-named file);
exit 1 = the command was well-formed but the environment / recorded state it
needs is missing, or piped stdin data is bad. The 6 boundary cases the spec's
"parse-time vs run-time" rule under-specified are resolved on #175
(malformed/open argv blueprint -> usage 2; stdin op-script content -> runtime 1;
applicability refusals -> usage 2; unknown --axis / --metric -> usage 2).

Flipped 2->1 (runtime): no local data, no recorded geometry, no recorded
run/family, "run has no tap named", trace-name collision, family/trace
persist-write failures (normalizing the lone generalize inconsistency -- all
persist -> 1 together), missing/corrupt content-addressed store state (the
reproduce path), chart-read failures, and graph stdin op-script content +
stdin-read I/O. reproduce-diverged stays exit 1. Usage sites (clap parse,
argv-applicability guards, the dual-grammar blueprint-file read/parse) stay
exit 2.

Tests: ~8 domain-refusal pins flipped Some(2)->Some(1) (message substrings
unchanged), the four runtime tests renamed _exit_2 -> _exit_1, and a partition
property test added. Orchestrator fix after the loop: 3 sibling no-data
skip-guards (run_real_with_trace + the two generalize cross-instrument tests)
that the plan's Task-1 list missed -- left at Some(2), they would fall through
to assert Some(0) and FAIL on a data-less machine; flipped to Some(1) for
consistency with the flipped code.

Deferred (cosmetic, filed forward): full "Usage:" / "usage:" / bare
error-message casing normalization -- no functional impact, high pin-churn.

Verified green (orchestrator, this session): cargo build --workspace, cargo
clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all
clean (0 failed across every test binary).

This completes the #175 clap-adoption cycle (iteration 1 = clap migration at
366170a; iteration 2 = this exit-code split).

refs #175
2026-07-01 20:05:42 +02:00
Brummel 366170aeab feat(0098): CLI clap migration (iteration 1) — scoped help, --version, GNU flags
Replace the hand-rolled aura-cli argv parser with a clap derive parser.
Delivers, all from one declarative source:
- scoped `aura <sub> --help` (stdout, exit 0) — each subcommand's own Options
  section; retires the #131 uniform-global-blob help
- `--version` / `-V` → `aura 0.1.0` (stdout, exit 0)
- GNU `--flag=value`, the `--` end-of-options terminator, and long-option
  abbreviation (`--harn` → `--harness`)

Architecture: one root Cli/Command enum, one `*Cmd` struct per subcommand. The
four dual-grammar subcommands (run/sweep/walkforward/mc) map the loaded-blueprint
branch as an optional `[blueprint]` positional + a post-parse `is_file()`
dispatch on a single-sourced predicate; each built-in handler re-asserts a
stray-positional guard (refuse-don't-guess) so clap's optional positional cannot
silently swallow a typo (pinned by two new E2E tests). The execution layer
(run_*/emit_*/runs_*/reproduce_*) is untouched — only arg-plumbing changes, via
thin `*_from` adapters reusing the existing value helpers.

Exit codes behaviour-preserved: clap parse errors exit 2 (matching the old
usage-error=2); domain refusals stay exit 2; reproduce-diverged stays exit 1.
The exit-code split (2=usage / 1=runtime) is iteration 2. clap is admitted under
the C16 per-case review (research-side CLI, invariant 8 untouched — a dev-loop
compile tax, not a frozen-artifact tax).

Forced plan corrections (implementer; verified against the diff):
- Step 6's deletion of the `parse_*_args` fns orphaned ~34 grammar unit tests
  that called them; those were deleted and that grammar coverage moved to the
  renegotiated cli_run.rs E2E pins (`parse_select` kept + retested).
- run_malformed_cost_value_...: a bad cost value is now a clap value-parse error
  (no "Usage:" line); renegotiated to pin exit 2 + empty stdout + the flag name
  "--cost-per-trade"; the units note stays pinned via `run --help`.
- `allow_hyphen_values` on the three cost flags so `--cost-per-trade -0.5`
  reaches the non-negativity guard (else clap rejects -0.5 as an unknown option).
- Deleted the now-dead `RealWindowGrammar` (clippy -D warnings).

Orchestrator additions after the loop:
- Enabled long-option abbreviation (`infer_long_args` on the root — one
  attribute, it propagates to subcommands) + a test, closing spec acceptance
  criterion 4 which the plan's RED tests did not cover. Decided to deliver
  rather than amend the spec: the two ratified decisions conflict on this
  low-tier item — Fork B (GNU compliance, getopt_long abbreviates) vs F5
  (LLM/automation-first, human-only niceties deprioritized) — and the cost
  tiebreaker (one attribute, not per-struct work) resolves to deliver.

Carry-on debt (not this iteration): the error-message "Usage:"/"usage:"/bare
casing is now mixed (clap's capitalized "Usage:" beside preserved lowercase aura
messages); a deliberate quality-hold since the casings are pinned by
renegotiated/preserved tests. Cosmetic; a candidate for the iteration-2
exit-code-split cleanup.

Verified green (orchestrator, this session): cargo build --workspace, cargo
clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all
clean (0 failed across every test binary, including the renegotiated cli_run
pins and the new --version / scoped-help / GNU-flag / dual-grammar tests).

refs #175
2026-07-01 18:56:46 +02:00
Brummel 6a775f700f fix(cli): walkforward validates its --axis grid once, not once per window
`aura walkforward <bp> --axis <name>=<v>` emitted a rejected axis's
diagnostic more than once (a racy 1-3x) when the grid did not resolve.
`blueprint_walkforward_family` resolved the axes *inside* the per-window
closure, and `walk_forward` fans that closure out across the windows in
parallel — so several windows each `eprintln!` the same `BindError` and
`exit(2)` before any one exit tears the process down.

Axis resolution is window-independent, so it is hoisted to a single
dispatch-boundary pre-flight (mirroring `aura sweep`, which validates its
axes once before any member runs): the first in-sample window is resolved
up front, surfacing the `BindError` exactly once; the per-window closure
then resolves already-validated axes and cannot re-raise (`expect`).

RED-first: cli_run.rs `aura_walkforward_emits_an_unknown_axis_rejection_once`
drives the verb 20x and asserts the rejection is single every run (the emit
count was racy, so a single invocation would be a flaky assertion). Surfaced
by the World/C21 milestone fieldtest.

closes #177
refs #170
2026-07-01 17:41:09 +02:00
Brummel dec07809df fix(cli): aura run refuses an open blueprint with a clean exit 2, not a panic
`aura run <blueprint.json>` bootstraps the loaded signal over the EMPTY
param point (a closed-blueprint assumption) and `.expect()`s
`compile_with_params`, which panics (exit 101, ParamArity) when the
blueprint carries >=1 free knob. The sibling `aura mc` already refuses an
open blueprint cleanly at the dispatch boundary; `run` now does the same.

The `["run", ..]` `.json`-discriminator arm probes the wrapped param_space
(`blueprint_axis_probe`, the single source of that wrap) and, when it is
non-empty, emits a "run requires a closed blueprint … N free knob(s) —
bind them or use `aura sweep --axis`" diagnostic and exits 2 — mirroring
`blueprint_mc_family`'s closed-guard. No panic on a naive consumer's first
move (author an open blueprint, run it).

RED-first: cli_run.rs `aura_run_rejects_an_open_blueprint_without_panicking`
pins exit 2 + the named requirement + no `panicked`. Surfaced by the
World/C21 milestone fieldtest.

closes #176
refs #170
2026-07-01 17:28:02 +02:00
Brummel ff20f74e8f feat(0097): IS-refit walk-forward over a loaded blueprint — aura walkforward <bp.json> --axis
Ship the World/C21 milestone's last piece (Arm A, #173): a true in-sample-refit
walk-forward over a loaded blueprint. `aura walkforward <blueprint.json> --axis
<name>=<csv> [--axis …] [--select argmax|plateau:mean|plateau:worst]` rolls
24/12/12 IS/OOS windows over the synthetic span; per IS window it re-optimizes
the blueprint's params over the user's --axis grid (the #169 prefixed names),
selects the winner by sqn_normalized, runs it out-of-sample, and aggregates the
per-window OOS reports into a FamilyKind::WalkForward family — content-addressed
(put_blueprint) and aura-reproduce bit-identical.

Structure mirrors the hard-wired stage1-r walk-forward arm one substitution
deep: the generic engine walk_forward driver + select_winner + the synthetic
schedule are reused; only the per-window IS-sweep (blueprint_sweep_over) and
OOS-run (run_oos_blueprint) source the loaded blueprint via cycle-0096's
blueprint_axis_probe. reproduce_family_in gains a WalkForward arm reconstructing
each OOS window's windowed slice from the member manifest's window bounds (winner
params via the shared manifest->cells recovery, as the MonteCarlo/Sweep arms do).

Panic-safety: blueprint_sweep_over returns Result<_, BindError>; a bad/unknown
--axis is a clean in-closure exit 2 (mirroring the hard-wired arm's select_winner
handling), never a panic — pinned by the rejects_an_unknown_axis E2E. A no-axis
invocation (nothing to re-fit) is a usage error; a malformed blueprint is rejected
at the dispatch boundary (exit 2, no panic). Reduce-mode members carry R-metrics
(no raw pip curve) → empty stitched equity; oos_r is the meaningful summary (C10
R-first yardstick).

Held quality finding adjudicated: run_oos_blueprint's plan-prescribed redundant
`pip: f64` (== data.pip_size()) removed — derived internally to match its siblings
run_oos_r / blueprint_sweep_over, which also drops the too_many_arguments
band-aid #[allow] (8->7 args). Two plan transcription typos the loop caught and
fixed verified correct: BindError renders via Debug ({e:?}), parse_select via
map_err (Result, not Option).

Verified myself: cargo build --workspace, cargo clippy --workspace --all-targets
-- -D warnings, cargo test --workspace all green; 2 new unit tests + 5 new
cli_run E2Es (persist shape, reproduce bit-identical, no-axis, malformed-safety,
unknown-axis) pass; no regression in the hard-wired walkforward / sweep / mc /
reproduce suites. Engine/registry untouched (invariants 1/2/8/9).

closes #173
2026-07-01 16:38:58 +02:00
Brummel bd2003e217 feat(0096): list a loaded blueprint's sweepable axes — aura sweep <bp.json> --list-axes
A data-level author can now discover a loaded blueprint's open sweepable knobs
before sweeping: `aura sweep <blueprint.json> --list-axes` prints one
`<name>:<kind>` line per open knob, in param_space() order, then exits 0. Before
this the only way to learn the names was to guess a `--axis` and read the
MissingKnob/UnknownKnob error — which names only the rejected knob, never the
valid set. Prerequisite for #173 (IS-refit walk-forward must know which axes to
re-fit per in-sample window).

Design (derived, recorded on #169): the query lands on the SWEEP verb, not
`graph introspect`, because the accepted axis names are produced by the sweep's
own harness-wrapping (wrap_stage1r nests the loaded signal as a
BlueprintNode::Composite named "stage1_signal", so collect_params prefixes them
— stage1_signal.fast.length, not the raw fast.length). Only the sweep verb owns
that wrapping. A new blueprint_axis_probe helper single-sources the wrapped
probe and now subsumes the two previously-inline copies (blueprint_sweep_family
+ blueprint_mc_family), so listed == swept by construction (and stays so across
#159's harness retirement) — no second source of truth.

--list-axes is a standalone query (valueless flag handled before the per-flag
value-consume; rejected with exit 2 if combined with any other flag). A closed
blueprint prints nothing (exit 0). A malformed blueprint is rejected at the
existing dispatch boundary (exit 2, UnknownNodeType), never a panic.

Tests: unit blueprint_axis_probe (prefixed names + empty for closed) + unit
parse grammar (standalone), and five cli_run E2Es — prints names, closed-empty,
rejects-other-flags, malformed-safety (no panic), and a round-trip that DERIVES
the names from --list-axes and feeds them back into a working sweep (pins the
listed==swept invariant #169 exists to close). Verified myself: cargo build
--workspace, cargo test --workspace, cargo clippy --workspace --all-targets -D
warnings all green; no regression in the refactored sweep/mc probes. Engine and
registry untouched (invariants 1/8/9).

closes #169
2026-07-01 14:54:45 +02:00
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