Commit Graph

13 Commits

Author SHA1 Message Date
Brummel 94cdbf90cc feat(campaign,cli): permit --select plateau in the walk_forward stage (#215)
The campaign walk_forward stage picked its per-window in-sample winner by
optimize_deflated (argmax) unconditionally, dropping the stage's `select`
field, and both the preflight and the `walkforward` verb's `--real` campaign
sub-branch refused plateau:* with a forward-pointer here (the #210 decision-4
deferral; the synthetic non-`--real` sub-branch has accepted plateau since
#173). Honour plateau selection when no gate precedes the wf stage.

- Preflight: the plateau-in-wf refusal is narrowed to gate-preceded stages.
  With no preceding gate the survivors are the full sweep grid, so the
  parameter lattice (grid.axis_lens) is exact and optimize_plateau is sound; a
  gate filters survivors below the grid, breaking the lattice, so plateau after
  any gate stays refused. The refusal is static — document validity must be
  data-independent (a gate's filtering depends on the data) — and keeps the
  existing "a gated survivor subset has no parameter lattice" message, which now
  describes exactly the only case it fires.
- Executor: run_walk_forward_stage threads the per-cell grid.axis_lens and picks
  each window's in-sample winner via select_sweep_winner — the same dispatch the
  sweep stage uses. (Argmax, deflate=true) is byte-identical to the old
  optimize_deflated call, so the argmax grade and content-id pins are unchanged;
  the plateau arms read axis_lens and ignore deflate.
- CLI: the walkforward verb threads --select through translate_walkforward /
  run_walkforward_sugar (a walkforward-local param, not the shared four-verb
  SugarInvocation), mapping the parsed Selection to the research SelectRule via
  select_rule_of; the refusal and its select_is_plateau helper are deleted.
  Scope is walkforward-only — the mc verb's wf-stage select is untouched.

The plateau:worst grade anchor (stitched_total_pips = -9683776.67) is distinct
from the argmax anchor (-10398606.67), proving the plateau path genuinely
changes winner selection rather than falling back to argmax. A gate-free plateau
process validates clean while a gate-preceded one is refused (a new
campaign-validate e2e). Argmax defaults stay byte-identical.

closes #215
2026-07-10 03:05:35 +02:00
Brummel e7efe1f765 feat(cli): walkforward + mc go blueprint-generic (#220 slice 2)
Tasks 3-4 of the verb-axis-generalization plan:

- walkforward: the welded r-sma campaign branch moves out of the None
  arm into the blueprint mode, split on --real (campaign sugar) vs the
  unchanged synthetic family path; WalkforwardCmd drops
  --strategy/--fast/--slow; the full-window clip block, wf_ms_sizes and
  WINNER_SELECTION_METRIC move unchanged; the --select plateau #215
  refusal survives. walkforward_summary_json_from_reports is de-welded:
  the axis-name list is passed in from the invocation instead of the
  const AXES r-sma quadruple.
- mc: McCmd drops the welded flags and gains --axis; the blueprint arm
  splits on --real (campaign pipeline: requires --axis, refuses
  --seeds) vs the unchanged synthetic seed family (--seeds); block-len/
  resamples/seed defaults and the --name/--trace refusal stay
  byte-identical.
- translate_walkforward/run_walkforward_sugar and translate_mc/
  run_mc_sugar adopt SugarInvocation (+ WfWindows/McKnobs); ZERO
  #[allow(clippy::too_many_arguments)] remain in verb_sugar.rs (#214
  complete).
- Tests: wf/mc e2e groups migrate argv-only (grade anchors keep all
  pinned floats verbatim); collateral usage/retired-token tests assert
  clap's structural rejection of the deleted flags; the synthetic
  family control groups pass unmodified.

Necessary deviation from the plan's literal step text: re-added
#[allow(clippy::type_complexity)] on walkforward_args_from (clippy -D
warnings flags the 6-tuple; mirrors its siblings). Held quality nits
(validate/strip block now quadruplicated across the four dispatch fns;
inline exit-mapping recurrence) are deliberate plan-holds — dedup
follows as the rule-of-three cleanup at cycle end.

Verification: full workspace suite green in the loop's independent
verify; build + clippy -D warnings clean.

refs #220, refs #214
2026-07-09 14:48:59 +02:00
Brummel a2294c7c3a feat(cli): SugarInvocation + generalize goes blueprint-generic (#220 slice 1)
Tasks 1-2 of the verb-axis-generalization plan (spec auto-signed on a
grounding-check PASS, decision log on #220):

- verb_sugar gains the shared SugarInvocation<'_> carrier (#214) with
  VolStop and the factored doc_axes_from/risk_from/probe_params_from
  helpers; translate_sweep/run_sweep_sugar adopt it (no behaviour
  change — sweep argv and family output byte-identical), and
  translate_generalize/run_generalize_sugar follow, dropping their
  too_many_arguments allows (4 remain for wf/mc).
- generalize is de-welded: GeneralizeCmd takes a blueprint positional
  plus repeatable --axis <wrapped-name>=<value> (one value per axis);
  --strategy/--fast/--slow are deleted; dispatch_generalize runs the
  sweep sequence (read, wrapped-namespace validation, canonicalize +
  topology_hash registration, wrapped_to_raw_axis strip) with a new
  usage closure. RGrid is deleted workspace-wide.
- cli_run generalize tests migrate argv-only; the exact-grade anchor
  keeps all pinned floats verbatim; generalize_refuses_a_non_r_sma_
  strategy is deleted (its premise — a strategy gate — dissolves by
  design); the multi-value refusal now fires per --axis.

Verification: full workspace suite green in the loop's independent
verify; build + clippy clean. Held quality finding (kept as a
plan-hold): dispatch_generalize duplicates dispatch_sweep's
validation/strip block verbatim — dedup follows once wf/mc land the
same block (rule-of-three, end of this cycle).

refs #220, refs #214
2026-07-09 13:13:59 +02:00
Brummel 07a73fb66e docs(cli,ledger): sweep #159 demo-retirement drift — stale comments + ledger notes
Cycle-close audit tidy for the #159 demo-retirement arc: behaviour-free
reword of prose/comments/docs left pointing at symbols the arc deleted.
Adversarially verified by the audit workflow; cargo build --workspace
--all-targets green.

- README: drop the removed `--harness` token; restrict the legacy built-in
  form claim to walkforward/mc/generalize (run/sweep no longer resolve one).
- glossary: the built-in `--strategy` sweep surface is retired (#159), not a
  live inline path.
- INDEX.md ledger: append [HISTORY] supersession markers (house style) to the
  dated realization notes advertising the retired `--harness` / built-in
  `--strategy` CLI forms (cost-flag #153, cycles 0065/0066/0067, name-res,
  #210 status); historical text preserved.
- aura-engine test-fixture docs: drop dead cross-crate refs to the deleted
  aura-cli fns sample_harness / build_sample and the deleted `macd` composite.
- cli_run.rs: reword the #159-cut-2 pin doc to the current clap generic-usage
  reality (no strategy_from / Strategy::RBreakout); r_sma_sweep_family ->
  blueprint_sweep_family.
- verb_sugar.rs: fix the pre-existing `E[R]` unresolved-doc-link warning.

Remaining tidy — the main.rs comment cluster plus its rustdoc link at :1546
(the second cargo-doc warning) — held for a follow-up issue. The dead
cost-graph branch is tracked in #221. refs #159
2026-07-08 11:12:22 +02:00
Brummel 7f5f576f41 feat(cli): dissolve aura mc --strategy r-sma --real (#210)
The FOURTH and LAST verb dissolution. `aura mc --strategy r-sma --real`
now runs as thin sugar over the one campaign executor: a generated,
content-addressed [std::sweep(argmax), std::walk_forward, std::monte_carlo]
campaign document whose terminal monte_carlo stage produces the same
StageBootstrap::PooledOos(RBootstrap). The sugar reads it from
run.outcome.record.cells[].stages[].bootstrap and reprints the existing
mc_r_bootstrap_json — one line, byte-identical to the inline path.

Pure sugar, NO executor change (aura-campaign/aura-engine/aura-research
diff-free). Mirrors the shipped walkforward dissolution:
- verb_sugar.rs: GeneratedMc, translate_mc, register_generated_mc,
  run_mc_sugar (record-read + one grade line, no member lines).
- main.rs: mc_args_from + the dispatch_mc built-in-arm split gated on
  `--strategy r-sma && --real`.

The one new load-bearing decision is the seed mapping: translate_mc sets
campaign.seed = the mc --seed. The wf winners are argmax hence
deflation-seed-independent (proven by the shipped walkforward anchor), so
the pooled OOS series is unchanged while the terminal r_bootstrap at that
seed reproduces the inline bootstrap. A new e2e
(mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series)
pins exactly this: two --seed values yield identical n_trades but a moved
E[R] mean.

Fork C retention (as walkforward): the arm splits, run_mc_r_bootstrap /
run_mc stay inline for the fenced synthetic/non-real paths (until #159).
The three synthetic-r-sma tests use no --real, so they never enter the
sugar branch and run the inline path unchanged. --name/--trace are
rejected on the sugar path, matching the inline mc r-path.

Verification: the committed byte-identity anchor
mc_r_bootstrap_real_e2e_pins_the_exact_current_grade runs the real GER40
archive and passes byte-for-byte through the new path; full workspace
suite green; clippy --all-targets -D warnings clean.

refs #210
2026-07-07 00:23:33 +02:00
Brummel a047af65fb feat(cli): dissolve aura walkforward --strategy r-sma --real (#210 T3-T4)
The dispatch split lands: `dispatch_walkforward`'s built-in arm now routes
`--strategy r-sma --real` through the one campaign path (`run_walkforward_sugar`
-> generated campaign doc -> executor -> summary reprint), reproducing the
`{"walkforward":{…}}` line byte-for-byte. Everything else — synthetic (any
strategy) and non-r-sma real — stays on the inline `run_walkforward`, fenced
until #159 (Reading A). The committed anchor
`walkforward_real_e2e_pins_the_exact_current_grade` passes: the dissolved path
reproduces windows=9, stitched_total_pips, oos_r, and the param_stability means
exactly. `--select plateau:*` is refused (exit 2 -> #215, Fork B); a multi-value
stop is refused (Fork A).

Two plan guesses were wrong and the anchor + real-data e2e caught both; the fixes
are documented inline:

- Window resolution: the plan mirrored generalize's `(--from,--to) -> (f,t)`
  shortcut, but the inline `wf_full_span` ALWAYS clips `--from`/`--to` to the
  archive's actual first/last bar (`probe_window`), even when both flags are
  explicit — a holiday/weekend edge shifts every IS/OOS window's calendar
  placement (stitched_total_pips diverged by ~5.16M, param_stability means intact,
  confirming only window boundaries moved). The dissolved path now calls
  `source.full_window(env)` unconditionally, matching the inline behaviour;
  verified byte-for-byte against the still-fenced inline path.
- Reducer axis match: the campaign path records the blueprint axes wrapped
  (`sma_signal.fast.length`) while the stop rides unwrapped via the risk regime, so
  the reducer's exact-name match panicked on real data. Fixed with the existing
  `campaign_run::raw_matches_wrapped` (#203) wrap-convention helper.

Tests: the dissolution-proof e2e `walkforward_dissolves_through_the_campaign_path`
(one process + one campaign + one campaign-run auto-registered, windows=9) plus
front-end refusal coverage (multi-value stop, `--select plateau`, missing knobs,
empty `--real`, unknown `--select`, `--name`/`--trace` exclusivity). Full workspace
suite green; `clippy --all-targets -D warnings` clean.

refs #210
2026-07-06 23:05:08 +02:00
Brummel a72cd6b115 feat(cli): walkforward campaign translator + summary reducer (#210 T1-T2)
The additive machinery for the walkforward dissolution, unwired this slice
(dispatch_walkforward untouched — `aura walkforward` behaviour byte-for-byte
unchanged). Two pieces, both mirroring the generalize seam:

- `translate_walkforward` + `register_generated_wf` + `GeneratedWalkforward`
  (verb_sugar.rs): argv -> a `[std::sweep(argmax), std::walk_forward]` process +
  a campaign carrying the multi-value fast/slow IS-refit grid over one instrument
  under a single Vol risk regime (the stop). The preceding sweep enumerates the
  survivor grid; the wf stage refits it per window.
- `walkforward_summary_json_from_reports` (main.rs): the pure-sugar reducer that
  reconstructs the verb's summary line from the campaign's recorded per-window OOS
  reports — `stitched_total_pips` = the per-window total_pips summed in roll order,
  `param_stability` over the four r-sma axes via the same `MetricStats::from_values`,
  pooled `oos_r` via `r_metrics_from_rs`. Byte-identical to the inline
  `walkforward_summary_json` (the committed anchor gates it once Task 3 wires it).

Both carry `#[allow(dead_code)]` until Task 3's dispatch split calls them.

The third `Generated*`/`register_generated_*` trio duplicates the sweep/generalize
shape by design (per-verb mirror); the bundle consolidation is deferred to #214.
Unit test note: `r_metrics_from_rs` clears `trade_rs` (it is the terminal pooled
reducer), so the reducer's characterization fixture rebuilds each window's
`RMetrics` with `trade_rs` restored, modelling what a real OOS window's report
carries (from `summarize_r`).

refs #210
2026-07-06 22:12:47 +02:00
Brummel 256ec7320c feat(cli): dissolve aura generalize into the campaign path (#210 T3)
`dispatch_generalize` synthesizes the bare `sma_signal` blueprint, resolves the
window (explicit --from/--to, else the first symbol's full window), and calls
the new `run_generalize_sugar`; the inline `run_generalize` is deleted. The
sugar builds the selection-bearing campaign doc, runs it through
`run_campaign_returning`, reprints the byte-identical `{"generalize":…}` +
`{"family_id":…}` lines, and persists the CrossInstrument family.

The exact-grade anchor stays green through the path shift — proving the two stop
mechanisms (former grid axis, now the risk-regime seam -> StopRule::Vol) yield
identical R. The family pin relaxes to "exactly one CrossInstrument family"
since the campaign executor now also persists a per-instrument Sweep family per
cell (the durable audit trail; #210 Q4). Two new e2e pin the window fallback and
the family member attribution order.

Deviations from the plan (necessary, verified): cross_instrument_members returns
Vec<aura_engine::RunReport> (aura_registry::RunReport is not pub); a shared
validate_before_register helper is factored out of run_sweep_sugar (DRY); the
run_generalize doc-comment and the now-unused `generalization` import removed.

refs #210
2026-07-06 17:42:36 +02:00
Brummel ed8b179e34 feat(cli): the generalize campaign translator (#210 T2)
`translate_generalize` builds the selection-bearing campaign document
(`[std::sweep(argmax), std::generalize]`, a single risk regime carrying the
stop, the candidate as single-value raw axes) + `GeneratedGeneralize` +
`register_generated_g`, mirroring the sweep translator. A determinism unit test
pins identical-invocation -> identical content id and the regime/pipeline shape.
The new items are unused until the dispatch-rewire task (#210 T3) wires the
runner; a bridge `#![allow(dead_code)]` (removed in T3) mirrors the sweep
sibling's landing.

refs #210
2026-07-06 17:04:56 +02:00
Brummel 2f9f3571ae feat(research): RiskRegime vocabulary + CampaignDoc.risk field + validation (#210 T1-2)
The first slice of the risk-regime structural axis (spec/plan abab1f1):

- aura-research: a serializable `RiskRegime` closed vocabulary (externally
  tagged, sole variant `Vol { length, k }` — a content-addressable mirror of
  the shipped two-variant StopRule, so a future Fixed regime is additive), and
  an optional top-level `CampaignDoc.risk: Vec<RiskRegime>` that absent-
  serializes (the `description` precedent) — content-id parity for every
  risk-less document.
- validate_campaign refuses a non-positive regime (length < 1 or k <= 0, NaN
  included) via a new `DocFault::BadRegime`; the CLI renders it as prose at the
  research_docs seam (register/validate paths).
- The three CLI-seam E2E tests pin that a well-formed risk section passes
  `campaign validate` and a bad one is refused with prose (exit 1, no Debug
  leak) — the field is reachable at the seam a data author drives, not only in
  library units.

Adding the field is compile-breaking at every `CampaignDoc { .. }` literal
(serde default does not cover Rust construction), so this slice also threads
`risk: vec![]` into the sites across aura-cli and aura-campaign — the
field-addition's compile-gate completion the plan should have scoped here. The
BadRegime prose reads `risk[i]: ...` (the implementer's terser wording over the
plan's illustrative string; the spec pinned no exact text). Full workspace
suite green.

refs #210
2026-07-06 14:02:33 +02:00
Brummel c9d962f0ea fix(cli): sweep-entry ergonomics — honest refusals, no store litter (#210 c0110 fieldtest)
Three test-first fixes from the cycle-0110 fieldtest findings:

- Axis-name preflight on the dissolved sweep arm: every --axis name is
  checked against the wrapped probe namespace (the --list-axes names)
  BEFORE the #203 strip; an unknown or raw-form name is refused
  house-style echoing exactly what the user typed, with a --list-axes
  pointer — no more stripped-then-mangled fragments from the deep
  referential refusal. Data-free, fires before the archive is touched.

- Validate-before-register in the sweep sugar: intrinsic doc checks +
  executor preflight + the pure bind_axes coverage check (reused, not
  re-inlined; now pub(crate)) all run in-memory before
  register_generated — a refused invocation leaves no generated
  document in the content-addressed store and no campaign_runs.jsonl
  line (previously P2/P3 refusals littered runs/campaigns, one doc
  malformed).

- blueprint_slot_prose: the sweep/walkforward/mc loaded-blueprint slots
  share one dispatch-boundary presenter — an op-script array gets a
  targeted 'run aura graph build first' hint, every load error goes
  through blueprint_load_prose + the missing-project hint, and the raw
  {e:?} Debug leak (#184's pattern, deliberately left on these three
  slots back then) is gone. Two stale Debug-leak pins flipped, two new
  refusal tests.

Suite 1060/0, clippy -D warnings clean.

refs #210
2026-07-04 19:48:57 +02:00
Brummel b7aaa0ba59 feat(cli): dissolve the real-data blueprint sweep into the campaign path (#210 T4)
aura sweep <bp.json> --real ... --axis ... now runs as thin sugar over
its generated campaign document through the one campaign executor: the
dispatch registers the canonical blueprint (topology_hash IS
content_id_of over the same canonical bytes, so the strategy ref
resolves against the existing single store write), strips the CLI's
wrapped axis names to the raw campaign namespace (#203 bind
convention), converts the probed archive window back through the
ingest seam's own epoch_ns_to_unix_ms (made pub; C3 — the seam owns
the ms/ns convention, never an inline division), and hands off to
verb_sugar::run_sweep_sugar. The inline real-data arm is gone; the
synthetic arm and --list-axes are untouched.

campaign_run gains RunPresentation {Full, MemberLinesOnly}:
run_campaign keeps Full (record line unchanged, project gate
unchanged); the sugar path runs MemberLinesOnly — the generated doc's
emit already limits stdout to family_table member lines, the mode
suppresses only the final record line. The record append is identical
in both modes; no project gate on the by-id path (the dissolved verb
works project-less, exactly as before).

The characterization pin survives the re-cut with exactly the one
sanctioned assertion flip (instrument-absence -> instrument==GER40)
and now also pins the generated artifacts: one process + one campaign
document auto-registered (dedup on a second identical invocation),
raw axis keys in the stored document, the stored window a ms sub-range
of the request, C1 at the member-report level with the registry's
run-suffix increment isolated.

Suite 1055/0, clippy clean, gated real-data paths executed locally.

refs #210
2026-07-04 19:01:27 +02:00
Brummel fd0b21d070 feat(cli): verb_sugar translator — argv to generated campaign documents (#210 T3)
The pure half of the sweep re-cut: translate_sweep maps a real-data
blueprint-sweep invocation (axes, name, symbol, window, canonical
blueprint bytes) to a selection-free single-stage process document plus
a campaign document carrying exactly the invocation's intent (seed 0 —
no stage consumes it — so identical invocations dedupe onto identical
content ids); register_generated puts both into the content-addressed
stores. Unit tests pin determinism, name flow, mixed-kind-axis refusal,
and that generated documents pass intrinsic validation + preflight.

Dead-code-allowed until the dispatch rewire wires the module (next
task removes the bridge attribute). Plan bytes reconciled with three
necessary deviations the spec gate surfaced: derive(Debug) for
unwrap_err, ScalarKind lives in aura_core, and 'gen' is reserved in
edition 2024.

refs #210
2026-07-04 18:01:51 +02:00