From ae2fac3212f931bf5a3b277ba36fbe4010bf7b9d Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 4 Jul 2026 01:33:00 +0200 Subject: [PATCH] audit: cycle 0108 tidy (drift resolved by ledger/glossary lift) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architect verdict: drift_found — the expected pre-audit doc lag; the code holds (C1: doc-seeded bootstrap + BTreeMap-ordered generalize; C14/C23 sparse widening pinned by the pre-0108-line parse test; terminal-annotator dataflow confirmed — nothing flows out, no new emit kind, no content-id movement). All items resolved here: - C18: 0107's 'refuse loudly at preflight' points forward; new cycle-0108 realization paragraph records the v2 shape, the dual-input stage bootstrap, campaign-scope generalize, the tier boundary (test-pinned on both sides), the record widenings, and the stringly wf-detection debt. - Glossary: campaign document -> v2 shape; bootstrap and Monte-Carlo entries disambiguate the three senses (construction vs statistical r_bootstrap vs seed axis — the std::monte_carlo stage IS the trade-sequence bootstrap, named for r_bootstrap's arguments); generalize gains the campaign-scope realization. Regression: cargo test --workspace 1025/0; clippy -D warnings clean; cargo doc 0 warnings. Spec + plan removed at close per convention. closes #200 --- docs/design/INDEX.md | 32 +- docs/glossary.md | 8 +- docs/plans/0108-annotator-stages.md | 2747 --------------------------- docs/specs/0108-annotator-stages.md | 280 --- 4 files changed, 34 insertions(+), 3033 deletions(-) delete mode 100644 docs/plans/0108-annotator-stages.md delete mode 100644 docs/specs/0108-annotator-stages.md diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 3865c36..ba5afde 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -1399,8 +1399,9 @@ evidence), and the v1 executor shipped the next cycle (below). `aura campaign run ` executes a campaign (a file is register-then-run sugar; the content id is canonical): zero-fault referential gate, then the process pipeline — v1 executable shape `std::sweep [std::gate]* [std::walk_forward]?` -(`std::monte_carlo`/`std::generalize` refuse loudly at preflight, as does every other -shape) — runs once per (strategy, instrument, window) cell in doc order. Execution +(`std::monte_carlo`/`std::generalize` refused loudly at preflight in this cycle; they +execute since cycle 0108, below) — runs once per (strategy, instrument, window) cell in +doc order. Execution *semantics* live in the **`aura-campaign` library crate** (#198 home decision: reachable beyond the CLI; NOT C21's project-side World): grid odometer over the campaign axes, members through the engine `sweep` over a **`ListSpace`** (explicit point set beside @@ -1428,6 +1429,33 @@ both `RollMode`s; the shipped `folds` slot mapped to nothing the machinery accep a new `ZeroWalkForwardLength` intrinsic fault; wf-bearing process docs get new content ids by design (the 0106 fieldtest corpus stays as the historical record). Known debt: metric-roster triplication (#190), deflation-constant duplication (#199). +**Realization (cycle 0108, #200 — the annotator stages execute).** The v2 executable +shape is `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? +[std::generalize]?` — an ordered optional annotator suffix, each at most once, +`std::generalize` strictly last; the executor preflight refuses what the intrinsic tier +deliberately admits (`[sweep, mc, walk_forward]` is intrinsically valid — the tier +boundary is test-pinned on both sides), plus three new static guards +(single-instrument generalize, non-R generalize metric via the registry's +`check_r_metric`, zero mc `resamples`/`block_len`). **Both annotators are terminal** +(unanimous #200 triage): nothing flows out of them; filtering stays the gate's monopoly. +`std::monte_carlo` bootstraps the stage's *incoming R-evidence* with one semantics, +input-shaped by position — after a walk_forward, ONE `r_bootstrap` over the wf family's +pooled per-window OOS `trade_rs` in roll order (`PooledOos`); after sweep/gates, one +`r_bootstrap` per surviving member's fresh in-memory series (`PerSurvivor`, ordinals into +the population family; a zero-trade member records the engine's defined all-zero +degenerate) — seeded from the campaign doc's `seed` (C1; `trade_rs` is `#[serde(skip)]`, +so annotators run in-executor or not at all). `std::generalize` executes at **campaign +scope**: after all cells, per (strategy, window) the per-cell *nominees* (last wf +window's OOS report, else the sweep winner; none on gate truncation) across instruments +feed the shipped `generalization()` when ≥ 2 exist — divergent per-instrument winners +are exposed via their params, never averaged away; a shortfall is recorded, not computed +around. Realization: `StageRealization.bootstrap` (`StageBootstrap::PerSurvivor | +PooledOos`) and `CampaignRunRecord.generalizations` (`CampaignGeneralization` keyed +strategy × window with `winners`/`missing`), both serde-default sparse — pre-0108 +`campaign_runs.jsonl` lines parse unchanged (C14/C23). No document content id moved +(introspection doc-strings dropped "in v1" only). Noted debt: the mc arm detects the wf +family by the stringly `block == "std::walk_forward"` literal (a pre-existing lockstep +pattern with the realization's block strings). **Guarantee.** Construction is a distinct phase, recursive at every level. Each node type has a **factory** `params → sized concrete node` (e.g. `SMA(length)` sizes its ring buffer). A **blueprint** is the param-generic, input-role-generic diff --git a/docs/glossary.md b/docs/glossary.md index 0fe9538..04f8fcb 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -33,7 +33,7 @@ The param-generic, input-role-generic graph-as-data produced by running a Rust b ### bootstrap **Avoid:** — -The distinct, recursive construction phase that binds `(blueprint + param-set + data bindings + seed)` into a frozen instance — buffers sized, topology fixed. The explicit name for the "wiring / graph build" that C7/C12 reference — the construction/compilation sense, *not* the statistical resampling or yield-curve bootstrap (aura's resampling axis is `Monte-Carlo`). +The distinct, recursive construction phase that binds `(blueprint + param-set + data bindings + seed)` into a frozen instance — buffers sized, topology fixed. The explicit name for the "wiring / graph build" that C7/C12 reference — the construction/compilation sense. Disambiguation: the *statistical* moving-block bootstrap of a trade-R series (`r_bootstrap`, `RBootstrap`) is a different thing that keeps its statistics name — it appears as the deflation null, the `aura mc` R path, and since 0108 the `std::monte_carlo` stage's `stage bootstrap` annotation; context (construction vs annotation) disambiguates. ### bot **Avoid:** — @@ -45,7 +45,7 @@ A downstream consumer node, never part of the strategy: the signal-quality side ### campaign document **Avoid:** experiment doc, campaign file -The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist, tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|run`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v1 pipeline shape `std::sweep [std::gate]* [std::walk_forward]?`, cycle 0107/#198) into a `campaign run` realization. +The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist, tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|run`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. ### campaign run **Avoid:** campaign execution record, realized campaign @@ -77,7 +77,7 @@ A composable downstream **C9 graph of cost nodes**, in **R**, that **approximate ### cross-instrument generalization **Avoid:** cross-symbol pooling, pooled generalization -The validation read that grades how consistently one *brought* candidate holds across a set of instruments, scored on its weakest one — the across-instrument axis of the anti-false-discovery discipline. Realised by `aura generalize`; an aggregator (a recomputable family score), never a selector that picks a winner. +The validation read that grades how consistently one *brought* candidate holds across a set of instruments, scored on its weakest one — the across-instrument axis of the anti-false-discovery discipline. Realised by `aura generalize` and, since 0108 (#200), by the `std::generalize` process stage at campaign scope (per (strategy, window) over the cells' nominees across instruments, recorded in the `campaign run`'s `generalizations`); an aggregator (a recomputable family score), never a selector that picks a winner. ### cycle **Avoid:** — @@ -149,7 +149,7 @@ The reproducible metadata record of a run (node-commit + params + data-window + ### Monte-Carlo **Avoid:** MC -An orchestration axis running N seeded realizations that perturb the input (the input stream, not the trade sequence); each realization is itself deterministic given its seed (Monte-Carlo = sweep over seeds). +An orchestration axis running N seeded realizations that perturb the input (the input stream, not the trade sequence); each realization is itself deterministic given its seed (Monte-Carlo = sweep over seeds). Distinct from the `std::monte_carlo` *process stage* (0108/#200): that block is the moving-block bootstrap of a per-trade R series (`r_bootstrap` — it perturbs the trade sequence, exactly what this axis does not), named for its fields (`resamples`, `block_len`) being that primitive's arguments; the seed-axis sense stays this entry's. ### neighbourhood score **Avoid:** smoothed score diff --git a/docs/plans/0108-annotator-stages.md b/docs/plans/0108-annotator-stages.md deleted file mode 100644 index be20f61..0000000 --- a/docs/plans/0108-annotator-stages.md +++ /dev/null @@ -1,2747 +0,0 @@ -# Annotator Stages — Implementation Plan (cycle 0108) - -> **Parent spec:** `docs/specs/0108-annotator-stages.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run -> this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** `aura campaign run` executes the two annotator stages: the v2 -pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? -[std::generalize]?` — mc bootstraps the stage's incoming R-evidence -(per-survivor / pooled-OOS), generalize runs at campaign scope; both terminal; -no document content id moves. - -**Architecture:** Task 1 widens the records serde-default (engine/registry + -literal threading, workspace-green). Task 2 swaps the fault set + preflight to -v2 (aura-campaign + aura-cli prose) — it leaves ONE known-RED aura-cli -integration test (`campaign_run_v1_boundary_refuses_mc_process`), flipped by -Task 5; Tasks 2-4 gate on `cargo test -p aura-campaign` + `cargo test -p -aura-cli --bin aura` + `cargo build --workspace`. Tasks 3-4 land the mc arm and -the campaign-scope generalize phase (fake-runner tests). Task 5 flips the seam -tests, tidies doc strings, extends the gated e2e, and restores full workspace -green. - -**Tech Stack:** Rust workspace — aura-engine, aura-registry, aura-campaign, -aura-cli, aura-research (doc-string text only). - -**Task-order constraints:** strictly sequential 1→5. Task 3 quotes the -pre-Task-2 `unreachable!` arm bytes in exec.rs (Task 2 must not touch exec.rs); -Task 4 quotes Task 1's `generalizations: Vec::new()` literal bytes; Task 5 -pins prose Task 2 defines. - ---- -### Task 1: Record widenings + serde derives (aura-engine, aura-registry) - -Widen the campaign-run lineage records for the 0108 annotator stages (spec `docs/specs/0108-annotator-stages.md`, #200): `RBootstrap` and `Generalization` become serde-serializable; `lineage.rs` gains `StageBootstrap` + `CampaignGeneralization`; `StageRealization` gains a sparse `bootstrap` field and `CampaignRunRecord` a sparse `generalizations` field — both serde-default so every pre-0108 `campaign_runs.jsonl` line stays parseable (the C14/C23 widening convention). This task changes NO behaviour: every new field is `None`/empty at every existing construction site; a later task of this cycle computes them. - -**Files:** -- Modify: `crates/aura-engine/src/mc.rs` — `RBootstrap` derive (:160); verify `MetricStats` (:47) untouched -- Modify: `crates/aura-registry/src/lineage.rs` — imports (:22, :25), `CampaignRunRecord` widening (:73-83), `StageRealization` widening (:102-111), new `StageBootstrap` + `CampaignGeneralization` before `impl Registry` (:122); verify :198 needs no change -- Modify: `crates/aura-registry/src/lib.rs` — re-export extension (:30-35), `Generalization` derive (:664), test-literal threading (:1707, :1756, :1781) -- Modify: `crates/aura-campaign/src/exec.rs` — literal threading (:142, :202, :238, :271) -- Modify: `crates/aura-registry/tests/campaign_run_store_e2e.rs` — literal threading (:46-79) -- Test: `crates/aura-registry/src/lib.rs` tests module — NEW `campaign_run_record_roundtrips_with_annotator_fields`, NEW `campaign_run_line_without_new_fields_still_parses` - -- [ ] **Step 1: RED — add the two new registry tests (they must NOT compile yet)** - -In `crates/aura-registry/src/lib.rs`, at the end of the `tests` module (the tail of `campaign_run_record_roundtrips` is the last test in the file), replace: - -```rust - let run = reg.append_campaign_run(&record).expect("append"); - assert_eq!(run, 0); - assert_eq!(reg.load_campaign_runs().expect("load"), vec![record]); - } -} -``` - -with: - -```rust - let run = reg.append_campaign_run(&record).expect("append"); - assert_eq!(run, 0); - assert_eq!(reg.load_campaign_runs().expect("load"), vec![record]); - } - - #[test] - fn campaign_run_record_roundtrips_with_annotator_fields() { - use aura_engine::r_bootstrap; - let path = temp_family_dir("campaign_run_annotator_roundtrip"); - let reg = Registry::open(&path); - // r_bootstrap is pure given its seed (C1), and serde_json round-trips - // finite f64 exactly, so whole-record PartialEq holds across the store. - let per_a = r_bootstrap(&[0.5, -0.2, 0.3, 1.1], 8, 2, 7); - let per_b = r_bootstrap(&[-0.4, 0.9], 8, 1, 7); - let pooled = r_bootstrap(&[0.2, 0.2, -0.1, 0.6, 0.4], 8, 2, 7); - let record = CampaignRunRecord { - campaign: "feed".to_string(), - process: "beef".to_string(), - run: 0, // matches the counter's first assignment, so whole-record PartialEq holds - seed: 7, - cells: vec![ - CellRealization { - strategy: "3f9c".to_string(), - instrument: "EURUSD".to_string(), - window_ms: (0, 1000), - stages: vec![StageRealization { - block: "std::monte_carlo".to_string(), - family_id: None, - survivor_ordinals: None, - selection: None, - bootstrap: Some(StageBootstrap::PerSurvivor(vec![ - (0, per_a), - (2, per_b), - ])), - }], - }, - CellRealization { - strategy: "3f9c".to_string(), - instrument: "GER40".to_string(), - window_ms: (0, 1000), - stages: vec![StageRealization { - block: "std::monte_carlo".to_string(), - family_id: None, - survivor_ordinals: None, - selection: None, - bootstrap: Some(StageBootstrap::PooledOos(pooled)), - }], - }, - ], - generalizations: vec![CampaignGeneralization { - strategy_ordinal: 0, - window_ordinal: 0, - generalization: Some(Generalization { - selection_metric: "net_expectancy_r".to_string(), - n_instruments: 2, - worst_case: 0.04, - sign_agreement: 2, - per_instrument: vec![ - ("EURUSD".to_string(), 0.04), - ("GER40".to_string(), 0.11), - ], - }), - winners: vec![ - ( - "EURUSD".to_string(), - vec![("sma_cross.fast.length".to_string(), Scalar::i64(3))], - ), - ( - "GER40".to_string(), - vec![("sma_cross.fast.length".to_string(), Scalar::i64(5))], - ), - ], - missing: vec!["US500".to_string()], - }], - }; - let run = reg.append_campaign_run(&record).expect("append annotator record"); - assert_eq!(run, 0); - assert_eq!( - reg.load_campaign_runs().expect("load annotator record"), - vec![record], - "bootstrap (both variants) and generalizations survive the round-trip", - ); - } - - /// A pre-0108 stored line — no `bootstrap`, no `generalizations` — still - /// parses (the C14/C23 serde-default widening convention): existing - /// `campaign_runs.jsonl` stores survive the record widening unchanged. - #[test] - fn campaign_run_line_without_new_fields_still_parses() { - let path = temp_family_dir("campaign_run_pre_widening"); - let store = path.with_file_name("campaign_runs.jsonl"); - let line = r#"{"campaign":"cafe","process":"beef","run":0,"seed":7,"cells":[{"strategy":"3f9c","instrument":"EURUSD","window_ms":[0,1000],"stages":[{"block":"std::gate","survivor_ordinals":[0,2]}]}]}"#; - fs::write(&store, format!("{line}\n")).expect("write pre-widening line"); - let reg = Registry::open(&path); - let loaded = reg.load_campaign_runs().expect("pre-widening line parses"); - assert_eq!(loaded.len(), 1); - assert_eq!(loaded[0].cells[0].stages[0].bootstrap, None); - assert!(loaded[0].generalizations.is_empty()); - } -} -``` - -(`Registry`, `CampaignRunRecord`, `CellRealization`, `StageRealization`, `StageBootstrap`, `CampaignGeneralization`, `Generalization`, `fs`, and `temp_family_dir` all resolve via the module's existing `use super::*;` once the later steps land; `Scalar` is already imported in the tests module; `r_bootstrap` is imported locally above because the crate root does not import it.) - -- [ ] **Step 2: RED run — the new tests must fail to compile** - -Run: `cargo test -p aura-registry campaign_run_record_roundtrips_with_annotator_fields` - -Expected: compilation FAILURE (this is the RED evidence for the widening): errors naming the not-yet-existing items — `cannot find ... StageBootstrap` / `cannot find ... CampaignGeneralization` (E0422/E0433) and no field `bootstrap` on `StageRealization` / no field `generalizations` on `CampaignRunRecord` (E0560). Do NOT proceed if it compiles. - -- [ ] **Step 3: aura-engine — serde derives on `RBootstrap`** - -First verify `MetricStats` (the `e_r` field type) already derives serde — Run: `grep -n -B 1 "pub struct MetricStats" crates/aura-engine/src/mc.rs` - -Expected: the derive line reads `#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]` — `MetricStats` needs NO edit. - -Then in `crates/aura-engine/src/mc.rs`, replace: - -```rust -#[derive(Clone, Debug, PartialEq)] -pub struct RBootstrap { -``` - -with: - -```rust -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct RBootstrap { -``` - -(Fully-qualified `serde::` paths, matching the `MetricStats` derive in this file — `mc.rs` has no `use serde` import and must not gain one.) - -- [ ] **Step 4: aura-registry lineage.rs — imports + the two new types** - -In `crates/aura-registry/src/lineage.rs`, replace: - -```rust -use aura_engine::{FamilySelection, McFamily, RunReport, SweepFamily, WalkForwardResult}; -use serde::{Deserialize, Serialize}; - -use crate::{Registry, RegistryError}; -``` - -with: - -```rust -use aura_engine::{ - FamilySelection, McFamily, RBootstrap, RunReport, SweepFamily, WalkForwardResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::{Generalization, Registry, RegistryError}; -``` - -Then, in the same file, insert the two new types immediately before the `impl Registry` block by replacing: - -```rust -impl Registry { - /// Assign a fresh per-name `run` index (one past the highest `run` already -``` - -with: - -```rust -/// The recorded result of a `std::monte_carlo` stage (cycle 0108, #200 d1/d4): -/// the payload mirrors the stage's input shape. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum StageBootstrap { - /// After sweep/gates: one bootstrap per surviving member, - /// `(ordinal into the population family, result)`. - PerSurvivor(Vec<(usize, RBootstrap)>), - /// After a walk_forward: one bootstrap over the pooled per-window - /// OOS trade-R series (roll order). - PooledOos(RBootstrap), -} - -/// The campaign-scope generalize annotation (#200 d2): recorded at the scope -/// it is computed, keyed (strategy, window) across instruments. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CampaignGeneralization { - pub strategy_ordinal: usize, - pub window_ordinal: usize, - /// None when fewer than 2 instrument cells produced a winner (gate - /// truncation) — the shortfall is recorded, never computed around. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub generalization: Option, - /// Per-instrument winning params — divergent winners are exposed, - /// never averaged away. Present whenever >= 1 winner exists. - pub winners: Vec<(String, Vec<(String, Scalar)>)>, - /// Instrument cells that contributed no winner (gate-truncated), by name. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub missing: Vec, -} - -impl Registry { - /// Assign a fresh per-name `run` index (one past the highest `run` already -``` - -- [ ] **Step 5: aura-registry lineage.rs — widen `StageRealization` and `CampaignRunRecord`** - -In `crates/aura-registry/src/lineage.rs`, replace: - -```rust -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct StageRealization { - pub block: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub family_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub survivor_ordinals: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub selection: Option, -} -``` - -with: - -```rust -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct StageRealization { - pub block: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub family_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub survivor_ordinals: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub selection: Option, - /// std::monte_carlo stages only (cycle 0108). Absent pre-0108 — the - /// serde-default widening keeps existing stored lines parseable. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bootstrap: Option, -} -``` - -Then replace: - -```rust -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CampaignRunRecord { - /// Campaign document content id. - pub campaign: String, - /// Process document content id. - pub process: String, - /// Per-campaign run counter — assigned on append, never caller-supplied. - pub run: usize, - pub seed: u64, - pub cells: Vec, -} -``` - -with: - -```rust -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CampaignRunRecord { - /// Campaign document content id. - pub campaign: String, - /// Process document content id. - pub process: String, - /// Per-campaign run counter — assigned on append, never caller-supplied. - pub run: usize, - pub seed: u64, - pub cells: Vec, - /// Campaign-scope generalize annotations (cycle 0108), one per - /// (strategy, window) across instruments. Absent pre-0108 — the - /// serde-default widening keeps existing stored lines parseable. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub generalizations: Vec, -} -``` - -Then verify the struct-update site needs NO change — Run: `grep -n "record.clone()" crates/aura-registry/src/lineage.rs` - -Expected: exactly one hit, `let stored = CampaignRunRecord { run, ..record.clone() };` (in `append_campaign_run`) — struct-update syntax carries the new `generalizations` field from `record` automatically; do not edit it. - -- [ ] **Step 6: aura-registry lib.rs — `Generalization` serde derives + re-export extension** - -In `crates/aura-registry/src/lib.rs`, replace: - -```rust -#[derive(Clone, Debug, PartialEq)] -pub struct Generalization { -``` - -with: - -```rust -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct Generalization { -``` - -(Fully-qualified `serde::` paths — `lib.rs` has no `use serde` import and must not gain one; the fields are unchanged.) - -Then replace: - -```rust -mod lineage; -pub use lineage::{ - group_families, mc_member_reports, sweep_member_reports, walkforward_member_reports, - CampaignRunRecord, CellRealization, Family, FamilyKind, FamilyRunRecord, StageRealization, - StageSelection, -}; -``` - -with: - -```rust -mod lineage; -pub use lineage::{ - group_families, mc_member_reports, sweep_member_reports, walkforward_member_reports, - CampaignGeneralization, CampaignRunRecord, CellRealization, Family, FamilyKind, - FamilyRunRecord, StageBootstrap, StageRealization, StageSelection, -}; -``` - -- [ ] **Step 7: thread the new fields through the existing registry test literals** - -In `crates/aura-registry/src/lib.rs`, replace (the `campaign_run_record` helper): - -```rust - fn campaign_run_record(campaign: &str) -> CampaignRunRecord { - CampaignRunRecord { - campaign: campaign.to_string(), - process: "proc-id".to_string(), - run: 99, - seed: 7, - cells: vec![], - } - } -``` - -with: - -```rust - fn campaign_run_record(campaign: &str) -> CampaignRunRecord { - CampaignRunRecord { - campaign: campaign.to_string(), - process: "proc-id".to_string(), - run: 99, - seed: 7, - cells: vec![], - generalizations: vec![], - } - } -``` - -Then, in the existing `campaign_run_record_roundtrips` test, replace (the tail of the sweep-stage literal plus the head of the gate-stage literal): - -```rust - neighbourhood_score: None, - n_neighbours: None, - }, - }), - }, - StageRealization { - block: "std::gate".to_string(), -``` - -with: - -```rust - neighbourhood_score: None, - n_neighbours: None, - }, - }), - bootstrap: None, - }, - StageRealization { - block: "std::gate".to_string(), -``` - -Then replace (the gate-stage literal's tail plus the record's tail): - -```rust - family_id: None, - survivor_ordinals: Some(vec![0, 3, 4, 7]), - selection: None, - }, - ], - }], - }; -``` - -with: - -```rust - family_id: None, - survivor_ordinals: Some(vec![0, 3, 4, 7]), - selection: None, - bootstrap: None, - }, - ], - }], - generalizations: vec![], - }; -``` - -- [ ] **Step 8: thread the new fields through aura-campaign's literal sites** - -In `crates/aura-campaign/src/exec.rs`, replace (the `execute` record assembly): - -```rust - let mut record = CampaignRunRecord { - campaign: campaign_id.to_string(), - process: process_id, - run: 0, - seed: campaign.seed, - cells: cells_rec, - }; -``` - -with: - -```rust - let mut record = CampaignRunRecord { - campaign: campaign_id.to_string(), - process: process_id, - run: 0, - seed: campaign.seed, - cells: cells_rec, - // Empty for now: the campaign-scope generalize phase (a later task of - // this cycle) computes and fills this after the cell loop. - generalizations: Vec::new(), - }; -``` - -Then replace (the sweep-stage realization in `run_cell`): - -```rust - stages.push(StageRealization { - block: "std::sweep".to_string(), - family_id: Some(family_id.clone()), - survivor_ordinals: None, - selection: Some(StageSelection { winner_ordinal, params, selection }), - }); -``` - -with: - -```rust - stages.push(StageRealization { - block: "std::sweep".to_string(), - family_id: Some(family_id.clone()), - survivor_ordinals: None, - selection: Some(StageSelection { winner_ordinal, params, selection }), - bootstrap: None, - }); -``` - -Then replace (the gate-stage realization): - -```rust - stages.push(StageRealization { - block: "std::gate".to_string(), - family_id: None, - survivor_ordinals: Some(ordinals), - selection: None, - }); -``` - -with: - -```rust - stages.push(StageRealization { - block: "std::gate".to_string(), - family_id: None, - survivor_ordinals: Some(ordinals), - selection: None, - bootstrap: None, - }); -``` - -Then replace (the walk-forward-stage realization): - -```rust - stages.push(StageRealization { - block: "std::walk_forward".to_string(), - family_id: Some(fam.family_id.clone()), - survivor_ordinals: None, - selection: None, - }); -``` - -with: - -```rust - stages.push(StageRealization { - block: "std::walk_forward".to_string(), - family_id: Some(fam.family_id.clone()), - survivor_ordinals: None, - selection: None, - bootstrap: None, - }); -``` - -(These four sites plus the two in Step 9 are ALL existing construction sites of the two widened types outside `lineage.rs` — verified by `grep -rn "StageRealization {" crates` / `grep -rn "CampaignRunRecord {" crates`.) - -- [ ] **Step 9: thread the new fields through the registry e2e test** - -In `crates/aura-registry/tests/campaign_run_store_e2e.rs`, replace (the tail of the `campaign_run` fixture fn): - -```rust - }), - }], - }], - } -} -``` - -with: - -```rust - }), - bootstrap: None, - }], - }], - generalizations: vec![], - } -} -``` - -- [ ] **Step 10: GREEN — full gates** - -Run: `cargo test -p aura-registry campaign_run_record_roundtrips_with_annotator_fields` - -Expected: `test result: ok. 1 passed; 0 failed` (both `StageBootstrap` variants and a populated `generalizations` survive the disk round-trip with whole-record equality). - -Run: `cargo test -p aura-registry campaign_run_line_without_new_fields_still_parses` - -Expected: `test result: ok. 1 passed; 0 failed` (a pre-0108 JSONL line parses; the new fields default to `None`/empty). - -Run: `cargo test -p aura-registry` - -Expected: every test target reports `test result: ok` with `0 failed` — all pre-existing registry tests (counter, round-trip, e2e store tests) stay green after the threading. - -Run: `cargo test -p aura-campaign` - -Expected: every test target reports `test result: ok` with `0 failed` — the existing aura-campaign suite (currently 29 tests: 20 lib + 7 + 2 integration) is unchanged in count and all green; this task adds no campaign behaviour. - -Run: `cargo build --workspace` - -Expected: builds with 0 errors (aura-cli and every other consumer compile against the widened records — the new fields are additive and defaulted, so no site outside the ones threaded above needs changes). -### Task 2: preflight v2 + fault-set swap (aura-campaign + aura-cli prose) - -**Files:** Modify `crates/aura-campaign/src/lib.rs` (import :21; `ExecFault` :64-85; `preflight` :158-253; test helpers + preflight tests :420-503). Modify `crates/aura-cli/src/campaign_run.rs` (`exec_fault_prose` :45-87; `mod tests` :444-546). Tests: the rewritten `preflight_*` suite in `crates/aura-campaign/src/lib.rs`; three new `*_prose_names_*` pins in `crates/aura-cli/src/campaign_run.rs`. Line numbers are pre-task positions. - -Context (spec `docs/specs/0108-annotator-stages.md`, #200): the v1 executor refused `std::monte_carlo`/`std::generalize` outright via `ExecFault::UnsupportedStage`. This task replaces that boundary with the v2 preflight: the executable pipeline shape becomes `std::sweep (std::gate)* (std::walk_forward)? (std::monte_carlo)? (std::generalize)?` — each suffix stage at most once, order fixed, `std::generalize` strictly last — plus three new static guards (`GeneralizeNeedsInstruments`, `GeneralizeNonRMetric`, `ZeroBootstrapParam`). `UnsupportedStage` disappears; the CLI prose swaps accordingly. This task touches NO execution code: do NOT modify `crates/aura-campaign/src/exec.rs` (its `MonteCarlo | Generalize` `unreachable!` arm at exec.rs:279-281 is replaced by Task 3; no test in this task's gate drives `execute()` with an mc/generalize pipeline, so that arm cannot fire here). Do NOT modify `crates/aura-cli/tests/research_docs.rs` (Task 5 flips it — see the LOUD note at the end). - -- [ ] **Step 1: RED — rewrite the aura-campaign preflight tests to the v2 semantics** - -In `crates/aura-campaign/src/lib.rs`, inside `mod tests`, append the mc/generalize stage helpers and a two-instrument campaign fixture. Replace: - -```rust - fn wf_stage(metric: &str, select: SelectRule) -> StageBlock { - StageBlock::WalkForward { - in_sample_ms: 4000, - out_of_sample_ms: 1000, - step_ms: 1000, - mode: WfMode::Rolling, - metric: metric.to_string(), - select, - } - } -``` - -with: - -```rust - fn wf_stage(metric: &str, select: SelectRule) -> StageBlock { - StageBlock::WalkForward { - in_sample_ms: 4000, - out_of_sample_ms: 1000, - step_ms: 1000, - mode: WfMode::Rolling, - metric: metric.to_string(), - select, - } - } - - fn mc_stage(resamples: u32, block_len: u32) -> StageBlock { - StageBlock::MonteCarlo { resamples, block_len } - } - - fn generalize_stage(metric: &str) -> StageBlock { - StageBlock::Generalize { metric: metric.to_string() } - } - - /// [`campaign`] with a second instrument — generalize's static arity - /// guard needs >= 2 to pass. - fn campaign_two_instruments() -> CampaignDoc { - let mut c = campaign(); - c.data.instruments.push("GER40".to_string()); - c - } -``` - -In the same file, replace the v1 acceptance test: - -```rust - #[test] - fn preflight_accepts_the_v1_shape() { - let c = campaign(); - // the full v1 shape: sweep (gate)* (walk_forward)? - let full = process_of(vec![ - sweep_stage("sqn_normalized", SelectRule::Argmax, true), - gate_stage("net_expectancy_r"), - wf_stage("sqn_normalized", SelectRule::Argmax), - ]); - assert!(preflight(&full, &c).is_ok()); - // degenerate accepted shapes: bare sweep (plateau select without - // deflate is legal on a sweep); sweep + gates without walk_forward. - let bare = process_of(vec![sweep_stage("total_pips", SelectRule::PlateauMean, false)]); - assert!(preflight(&bare, &c).is_ok()); - let gated = process_of(vec![ - sweep_stage("expectancy_r", SelectRule::Argmax, false), - gate_stage("n_trades"), - gate_stage("win_rate"), - ]); - assert!(preflight(&gated, &c).is_ok()); - } -``` - -with: - -```rust - #[test] - fn preflight_accepts_the_v2_shapes() { - let c = campaign(); - let c2 = campaign_two_instruments(); - // the full v2 shape: sweep (gate)* (walk_forward)? (monte_carlo)? - // (generalize)? - let full = process_of(vec![ - sweep_stage("sqn_normalized", SelectRule::Argmax, true), - gate_stage("net_expectancy_r"), - wf_stage("sqn_normalized", SelectRule::Argmax), - mc_stage(1000, 5), - generalize_stage("net_expectancy_r"), - ]); - assert!(preflight(&full, &c2).is_ok()); - // degenerate accepted shapes: bare sweep (plateau select without - // deflate is legal on a sweep); sweep + gates without walk_forward. - let bare = process_of(vec![sweep_stage("total_pips", SelectRule::PlateauMean, false)]); - assert!(preflight(&bare, &c).is_ok()); - let gated = process_of(vec![ - sweep_stage("expectancy_r", SelectRule::Argmax, false), - gate_stage("n_trades"), - gate_stage("win_rate"), - ]); - assert!(preflight(&gated, &c).is_ok()); - // suffix stages compose independently: mc without wf (single - // instrument fine), generalize without mc (needs 2 instruments), - // wf + mc without generalize. - let mc_only = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - mc_stage(1000, 5), - ]); - assert!(preflight(&mc_only, &c).is_ok()); - let gen_only = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - generalize_stage("expectancy_r"), - ]); - assert!(preflight(&gen_only, &c2).is_ok()); - let wf_mc = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - wf_stage("sqn", SelectRule::Argmax), - mc_stage(100, 3), - ]); - assert!(preflight(&wf_mc, &c).is_ok()); - } -``` - -Replace the v1-boundary refusal test: - -```rust - #[test] - fn preflight_refuses_mc_and_generalize_stages() { - let c = campaign(); - let mc = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - StageBlock::MonteCarlo { resamples: 100, block_len: 5 }, - ]); - assert!(matches!( - preflight(&mc, &c), - Err(ExecFault::UnsupportedStage { stage: 1, block }) if block == "std::monte_carlo" - )); - let g = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - StageBlock::Generalize { metric: "expectancy_r".to_string() }, - ]); - assert!(matches!( - preflight(&g, &c), - Err(ExecFault::UnsupportedStage { stage: 1, block }) if block == "std::generalize" - )); - } -``` - -with: - -```rust - /// v2 shape: an annotator may not precede a population stage, and - /// std::generalize is strictly last. `[sweep, mc, walk_forward]` is - /// intrinsically valid at the document tier yet refused here (spec - /// acceptance criterion 3). - #[test] - fn preflight_refuses_annotator_order_violations() { - let c2 = campaign_two_instruments(); - let cases: Vec<(Vec, &str)> = vec![ - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - mc_stage(100, 5), - wf_stage("sqn", SelectRule::Argmax), - ], - "stage 2: std::walk_forward cannot follow std::monte_carlo", - ), - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - mc_stage(100, 5), - gate_stage("expectancy_r"), - ], - "stage 2: std::gate cannot follow std::monte_carlo", - ), - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - generalize_stage("expectancy_r"), - mc_stage(100, 5), - ], - "stage 2: std::monte_carlo cannot follow std::generalize", - ), - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - wf_stage("sqn", SelectRule::Argmax), - gate_stage("expectancy_r"), - ], - "stage 2: std::gate cannot follow std::walk_forward", - ), - ]; - for (pipeline, want) in cases { - match preflight(&process_of(pipeline), &c2) { - Err(ExecFault::PipelineShape { detail }) => assert_eq!(detail, want), - other => panic!("expected PipelineShape({want}), got {other:?}"), - } - } - } - - /// Each v2 suffix stage appears at most once. - #[test] - fn preflight_refuses_duplicate_suffix_stages() { - let c2 = campaign_two_instruments(); - let cases: Vec<(Vec, &str)> = vec![ - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - wf_stage("sqn", SelectRule::Argmax), - wf_stage("sqn", SelectRule::Argmax), - ], - "stage 2: std::walk_forward may appear at most once", - ), - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - mc_stage(100, 5), - mc_stage(100, 5), - ], - "stage 2: std::monte_carlo may appear at most once", - ), - ( - vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - generalize_stage("expectancy_r"), - generalize_stage("expectancy_r"), - ], - "stage 2: std::generalize may appear at most once", - ), - ]; - for (pipeline, want) in cases { - match preflight(&process_of(pipeline), &c2) { - Err(ExecFault::PipelineShape { detail }) => assert_eq!(detail, want), - other => panic!("expected PipelineShape({want}), got {other:?}"), - } - } - } - - /// generalize's static arity guard: the campaign must carry >= 2 - /// instruments (the shipped generalization() would only refuse at - /// runtime, after members ran). - #[test] - fn preflight_refuses_single_instrument_generalize() { - let c = campaign(); // one instrument - let p = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - generalize_stage("expectancy_r"), - ]); - assert!(matches!( - preflight(&p, &c), - Err(ExecFault::GeneralizeNeedsInstruments { available: 1 }) - )); - } - - /// generalize's metric guard: check_r_metric refuses pip metrics AND - /// unknown names — both surface as GeneralizeNonRMetric. - #[test] - fn preflight_refuses_non_r_generalize_metric() { - let c2 = campaign_two_instruments(); - for bad in ["total_pips", "no_such_metric"] { - let p = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - generalize_stage(bad), - ]); - assert!(matches!( - preflight(&p, &c2), - Err(ExecFault::GeneralizeNonRMetric { metric }) if metric == bad - )); - } - } - - /// monte_carlo zero params are static refusals (instead of the engine's - /// defined all-zero degenerate at runtime), naming the offending field. - #[test] - fn preflight_refuses_zero_bootstrap_params() { - let c = campaign(); - let zero_resamples = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - mc_stage(0, 5), - ]); - assert!(matches!( - preflight(&zero_resamples, &c), - Err(ExecFault::ZeroBootstrapParam { stage: 1, field: "resamples" }) - )); - let zero_block = process_of(vec![ - sweep_stage("sqn", SelectRule::Argmax, false), - mc_stage(100, 0), - ]); - assert!(matches!( - preflight(&zero_block, &c), - Err(ExecFault::ZeroBootstrapParam { stage: 1, field: "block_len" }) - )); - } -``` - - -Still in `mod tests`, the wf-mid case in `preflight_refuses_non_sweep_first_and_double_sweep` stays RED-relevant but its comment is now wrong (v2 relaxes wf-strictly-last: annotators may follow wf; a *gate* after wf is still a shape violation). Replace: - -```rust - // walk_forward anywhere but last is a shape refusal too. - let wf_mid = process_of(vec![ -``` - -with: - -```rust - // a gate after walk_forward is still a shape refusal (v2 relaxes - // wf-final only for the annotator suffix: mc/generalize may follow). - let wf_mid = process_of(vec![ -``` - -(The assertion itself — `Err(ExecFault::PipelineShape { .. })` — still holds under v2 and is not changed. All other tests in :484-573 — unrankable metric, annotation gate metric, plateau-in-wf, i64 fit, deflate/plateau conflict — were read and verified to hold unchanged under v2; none pins wf-strictly-last prose.) - -- [ ] **Step 2: Run the RED check** - -Run: - -``` -cargo test -p aura-campaign 2>&1 | tail -25 -``` - -Expected: compilation FAILS with `error[E0599]`-class errors — no variant `GeneralizeNeedsInstruments`, `GeneralizeNonRMetric`, `ZeroBootstrapParam` found for enum `ExecFault`. This compile failure is the RED absence record for the new fault set (the tests reference variants that do not yet exist; a runtime-RED state is unreachable because the variant swap is compile-coupled). - -- [ ] **Step 3: Swap the ExecFault variant set** - -In `crates/aura-campaign/src/lib.rs`, replace the import at line 21: - -```rust -use aura_registry::RegistryError; -``` - -with: - -```rust -use aura_registry::{check_r_metric, RegistryError}; -``` - -Remove `UnsupportedStage` and update the `PipelineShape` doc to the v2 shape — replace: - -```rust -#[derive(Debug)] -pub enum ExecFault { - /// v1 boundary: `std::monte_carlo` / `std::generalize` are not executable. - UnsupportedStage { stage: usize, block: String }, - /// The pipeline is not `std::sweep (std::gate)* (std::walk_forward)?`. - PipelineShape { detail: String }, -``` - -with: - -```rust -#[derive(Debug)] -pub enum ExecFault { - /// The pipeline is not `std::sweep (std::gate)* (std::walk_forward)? - /// (std::monte_carlo)? (std::generalize)?`. - PipelineShape { detail: String }, -``` - -Add the three new static-fault variants (doc comments per the spec), keeping the preflight faults grouped before the runtime faults — replace: - -```rust - /// sweep `deflate: true` with a non-argmax select rule. - DeflatePlateauConflict { stage: usize }, - /// `WindowRoller` construction refusals at runtime. - Window { stage: usize, detail: String }, -``` - -with: - -```rust - /// sweep `deflate: true` with a non-argmax select rule. - DeflatePlateauConflict { stage: usize }, - /// generalize needs >= 2 instruments in the campaign (static). - GeneralizeNeedsInstruments { available: usize }, - /// generalize's metric must be an R metric (static — the intrinsic tier - /// accepts any vocabulary name; the shipped generalization() would only - /// refuse at runtime). - GeneralizeNonRMetric { metric: String }, - /// monte_carlo with resamples == 0 or block_len == 0 (static, instead of - /// the engine's defined all-zero degenerate). - ZeroBootstrapParam { stage: usize, field: &'static str }, - /// `WindowRoller` construction refusals at runtime. - Window { stage: usize, detail: String }, -``` - -- [ ] **Step 4: Replace preflight with the v2 shape sweep + static guards** - -In `crates/aura-campaign/src/lib.rs`, replace the entire v1 `preflight` (doc comment through closing brace): - -```rust -/// Statically refuse everything refusable before any member runs (the F7 -/// lesson applied forward): the v1 executable pipeline shape is exactly -/// `std::sweep (std::gate)* (std::walk_forward)?`; every sweep/walk_forward -/// selection metric is in [`RANKABLE_METRICS`]; every gate predicate metric -/// is in [`PER_MEMBER_METRICS`]; walk_forward must not select `plateau:*` -/// (a gated survivor subset has no grid lattice); sweep `deflate: true` -/// composes only with `argmax`; walk_forward lengths must fit `i64` (the -/// roller's Timestamp unit). The campaign parameter is the seam for -/// campaign-level static checks (none in v1, hence unused). -pub(crate) fn preflight(process: &ProcessDoc, _campaign: &CampaignDoc) -> Result<(), ExecFault> { - // v1 boundary first: an mc/generalize stage refuses wherever it sits, - // before any shape complaint about the same stage. - for (i, stage) in process.pipeline.iter().enumerate() { - let block = match stage { - StageBlock::MonteCarlo { .. } => "std::monte_carlo", - StageBlock::Generalize { .. } => "std::generalize", - _ => continue, - }; - return Err(ExecFault::UnsupportedStage { stage: i, block: block.to_string() }); - } - // shape: exactly `std::sweep (std::gate)* (std::walk_forward)?`. - if !matches!(process.pipeline.first(), Some(StageBlock::Sweep { .. })) { - return Err(ExecFault::PipelineShape { - detail: "the first stage must be std::sweep".to_string(), - }); - } - let last = process.pipeline.len() - 1; - for (i, stage) in process.pipeline.iter().enumerate().skip(1) { - match stage { - StageBlock::Sweep { .. } => { - return Err(ExecFault::PipelineShape { - detail: format!("stage {i}: only the first stage may be std::sweep"), - }); - } - StageBlock::WalkForward { .. } if i != last => { - return Err(ExecFault::PipelineShape { - detail: format!("stage {i}: std::walk_forward must be the final stage"), - }); - } - _ => {} - } - } - // per-stage slot rules (shape already established above). - for (i, stage) in process.pipeline.iter().enumerate() { - match stage { - StageBlock::Sweep { metric, select, deflate } => { - if !RANKABLE_METRICS.contains(&metric.as_str()) { - return Err(ExecFault::UnrankableMetric { stage: i, metric: metric.clone() }); - } - if *deflate && *select != SelectRule::Argmax { - return Err(ExecFault::DeflatePlateauConflict { stage: i }); - } - } - StageBlock::Gate { all } => { - for p in all { - if !PER_MEMBER_METRICS.contains(&p.metric.as_str()) { - return Err(ExecFault::GateMetricNotPerMember { - stage: i, - metric: p.metric.clone(), - }); - } - } - } - StageBlock::WalkForward { - in_sample_ms, - out_of_sample_ms, - step_ms, - mode: _, - metric, - select, - } => { - if !RANKABLE_METRICS.contains(&metric.as_str()) { - return Err(ExecFault::UnrankableMetric { stage: i, metric: metric.clone() }); - } - if matches!(select, SelectRule::PlateauMean | SelectRule::PlateauWorst) { - return Err(ExecFault::PlateauInWalkForward { stage: i }); - } - for (field, len) in [ - ("in_sample_ms", *in_sample_ms), - ("out_of_sample_ms", *out_of_sample_ms), - ("step_ms", *step_ms), - ] { - if i64::try_from(len).is_err() { - return Err(ExecFault::PipelineShape { - detail: format!("stage {i}: walk_forward {field} does not fit i64"), - }); - } - } - } - StageBlock::MonteCarlo { .. } | StageBlock::Generalize { .. } => { - unreachable!("refused by the v1 scan above") - } - } - } - Ok(()) -} -``` - -with: - -```rust -/// Statically refuse everything refusable before any member runs (the F7 -/// lesson applied forward): the v2 executable pipeline shape is exactly -/// `std::sweep (std::gate)* (std::walk_forward)? (std::monte_carlo)? -/// (std::generalize)?` — each suffix stage at most once, order fixed, -/// `std::generalize` strictly last; every sweep/walk_forward selection -/// metric is in [`RANKABLE_METRICS`]; every gate predicate metric is in -/// [`PER_MEMBER_METRICS`]; walk_forward must not select `plateau:*` (a gated -/// survivor subset has no grid lattice); sweep `deflate: true` composes only -/// with `argmax`; walk_forward lengths must fit `i64` (the roller's -/// Timestamp unit); monte_carlo `resamples` and `block_len` must be > 0 -/// (static, instead of the engine's defined all-zero degenerate); generalize -/// needs >= 2 campaign instruments and an R selection metric (the registry's -/// `check_r_metric`). The campaign parameter carries the campaign-level -/// static checks (generalize's instrument arity). -pub(crate) fn preflight(process: &ProcessDoc, campaign: &CampaignDoc) -> Result<(), ExecFault> { - // Position of a stage in the fixed v2 shape; gates (rank 1) may repeat, - // every other rank appears at most once and ranks never decrease. - fn shape_rank(stage: &StageBlock) -> usize { - match stage { - StageBlock::Sweep { .. } => 0, - StageBlock::Gate { .. } => 1, - StageBlock::WalkForward { .. } => 2, - StageBlock::MonteCarlo { .. } => 3, - StageBlock::Generalize { .. } => 4, - } - } - fn block_id(stage: &StageBlock) -> &'static str { - match stage { - StageBlock::Sweep { .. } => "std::sweep", - StageBlock::Gate { .. } => "std::gate", - StageBlock::WalkForward { .. } => "std::walk_forward", - StageBlock::MonteCarlo { .. } => "std::monte_carlo", - StageBlock::Generalize { .. } => "std::generalize", - } - } - // shape: `std::sweep (std::gate)* (std::walk_forward)? (std::monte_carlo)? - // (std::generalize)?` — a monotone rank walk over adjacent pairs captures - // every violation: a rank drop is an out-of-order stage (an annotator - // before a population stage, anything after std::generalize), an adjacent - // equal rank above the gate tier is a duplicate suffix stage. - if !matches!(process.pipeline.first(), Some(StageBlock::Sweep { .. })) { - return Err(ExecFault::PipelineShape { - detail: "the first stage must be std::sweep".to_string(), - }); - } - let mut prev = &process.pipeline[0]; - for (i, stage) in process.pipeline.iter().enumerate().skip(1) { - if matches!(stage, StageBlock::Sweep { .. }) { - return Err(ExecFault::PipelineShape { - detail: format!("stage {i}: only the first stage may be std::sweep"), - }); - } - let (rank, prev_rank) = (shape_rank(stage), shape_rank(prev)); - if rank < prev_rank { - return Err(ExecFault::PipelineShape { - detail: format!( - "stage {i}: {} cannot follow {}", - block_id(stage), - block_id(prev) - ), - }); - } - if rank == prev_rank && rank > 1 { - return Err(ExecFault::PipelineShape { - detail: format!("stage {i}: {} may appear at most once", block_id(stage)), - }); - } - prev = stage; - } - // per-stage slot rules (shape already established above). - for (i, stage) in process.pipeline.iter().enumerate() { - match stage { - StageBlock::Sweep { metric, select, deflate } => { - if !RANKABLE_METRICS.contains(&metric.as_str()) { - return Err(ExecFault::UnrankableMetric { stage: i, metric: metric.clone() }); - } - if *deflate && *select != SelectRule::Argmax { - return Err(ExecFault::DeflatePlateauConflict { stage: i }); - } - } - StageBlock::Gate { all } => { - for p in all { - if !PER_MEMBER_METRICS.contains(&p.metric.as_str()) { - return Err(ExecFault::GateMetricNotPerMember { - stage: i, - metric: p.metric.clone(), - }); - } - } - } - StageBlock::WalkForward { - in_sample_ms, - out_of_sample_ms, - step_ms, - mode: _, - metric, - select, - } => { - if !RANKABLE_METRICS.contains(&metric.as_str()) { - return Err(ExecFault::UnrankableMetric { stage: i, metric: metric.clone() }); - } - if matches!(select, SelectRule::PlateauMean | SelectRule::PlateauWorst) { - return Err(ExecFault::PlateauInWalkForward { stage: i }); - } - for (field, len) in [ - ("in_sample_ms", *in_sample_ms), - ("out_of_sample_ms", *out_of_sample_ms), - ("step_ms", *step_ms), - ] { - if i64::try_from(len).is_err() { - return Err(ExecFault::PipelineShape { - detail: format!("stage {i}: walk_forward {field} does not fit i64"), - }); - } - } - } - StageBlock::MonteCarlo { resamples, block_len } => { - if *resamples == 0 { - return Err(ExecFault::ZeroBootstrapParam { stage: i, field: "resamples" }); - } - if *block_len == 0 { - return Err(ExecFault::ZeroBootstrapParam { stage: i, field: "block_len" }); - } - } - StageBlock::Generalize { metric } => { - let available = campaign.data.instruments.len(); - if available < 2 { - return Err(ExecFault::GeneralizeNeedsInstruments { available }); - } - if check_r_metric(metric).is_err() { - return Err(ExecFault::GeneralizeNonRMetric { metric: metric.clone() }); - } - } - } - } - Ok(()) -} -``` - -(The i64-fit, rankable, gate-metric, and plateau/deflate checks are byte-identical to v1 — only the doc comment, the parameter name, the shape walk, and the `MonteCarlo`/`Generalize` arms change. The shape walk is complete: any violation of the v2 regex manifests as an adjacent rank inversion or an adjacent duplicate above the gate tier.) - -- [ ] **Step 5: GREEN gate on aura-campaign** - -Run: - -``` -cargo test -p aura-campaign preflight_ -``` - -Expected: the lib test target reports `test result: ok. 12 passed` (the 6 rewritten/new v2 tests + the 6 surviving v1 refusal tests); other targets report 0 matched. Then run the full crate suite: - -``` -cargo test -p aura-campaign -``` - -Expected: every target (`lib` unit tests incl. `wf_tests`, `tests/execute.rs`) reports `test result: ok` with 0 failed. NOTE: at this point `aura-cli` does NOT compile (its `exec_fault_prose` still matches the removed variant) — that is expected and fixed in Steps 6-7; `cargo test -p aura-campaign` does not build aura-cli. - -- [ ] **Step 6: RED — pin the three new fault proses in aura-cli** - -In `crates/aura-cli/src/campaign_run.rs`, inside `mod tests`, append three prose pins. Replace: - -```rust - fn gate_metric_not_per_member_prose_points_at_the_metrics_introspection() { - let prose = exec_fault_prose(&ExecFault::GateMetricNotPerMember { - stage: 1, - metric: "deflated_score".into(), - }); - assert!(prose.contains("is not a per-member"), "intrinsic message kept: {prose}"); - assert!( - prose.contains("aura process introspect --metrics"), - "the refusal must point at the enumeration verb: {prose}" - ); - } -``` - -with: - -```rust - fn gate_metric_not_per_member_prose_points_at_the_metrics_introspection() { - let prose = exec_fault_prose(&ExecFault::GateMetricNotPerMember { - stage: 1, - metric: "deflated_score".into(), - }); - assert!(prose.contains("is not a per-member"), "intrinsic message kept: {prose}"); - assert!( - prose.contains("aura process introspect --metrics"), - "the refusal must point at the enumeration verb: {prose}" - ); - } - - #[test] - /// 0108: generalize's static instrument-arity refusal is Debug-free and - /// names the available count plus the required floor. - fn generalize_needs_instruments_prose_names_the_shortfall() { - let prose = exec_fault_prose(&ExecFault::GeneralizeNeedsInstruments { available: 1 }); - assert_eq!(prose, "campaign has 1 instrument(s); std::generalize needs at least 2"); - assert!(!prose.contains("GeneralizeNeedsInstruments"), "Debug leak: {prose}"); - } - - #[test] - /// 0108: generalize's R-metric refusal names the metric and the R-only - /// rationale (pip metrics are not comparable across instruments). - fn generalize_non_r_metric_prose_names_the_metric() { - let prose = exec_fault_prose(&ExecFault::GeneralizeNonRMetric { - metric: "total_pips".into(), - }); - assert_eq!( - prose, - "std::generalize metric \"total_pips\" is not an R metric (gross/net R \ - families generalize; pip metrics do not)" - ); - assert!(!prose.contains("GeneralizeNonRMetric"), "Debug leak: {prose}"); - } - - #[test] - /// 0108: the zero-bootstrap-param refusal is path-addressed (stage index) - /// and names the offending field. - fn zero_bootstrap_param_prose_names_stage_and_field() { - let prose = exec_fault_prose(&ExecFault::ZeroBootstrapParam { - stage: 3, - field: "block_len", - }); - assert_eq!(prose, "process stage 3: monte_carlo block_len must be > 0"); - assert!(!prose.contains("ZeroBootstrapParam"), "Debug leak: {prose}"); - } -``` - -Run: - -``` -cargo test -p aura-cli --bin aura 2>&1 | tail -25 -``` - -Expected: compilation FAILS — an E0599-class error on the stale `ExecFault::UnsupportedStage` pattern (the variant no longer exists) and/or `error[E0004]` (non-exhaustive match: `GeneralizeNeedsInstruments`, `GeneralizeNonRMetric`, `ZeroBootstrapParam` not covered). This is the RED record for the prose arms. - -- [ ] **Step 7: Swap the exec_fault_prose arms** - -In `crates/aura-cli/src/campaign_run.rs`, remove the `UnsupportedStage` arm — replace: - -```rust -fn exec_fault_prose(f: &ExecFault) -> String { - match f { - ExecFault::UnsupportedStage { stage, block } => format!( - "process stage {stage} ({block}) is not executable in v1; executable \ - pipeline shape: std::sweep [std::gate]* [std::walk_forward]" - ), - ExecFault::PipelineShape { detail } => { - format!("process pipeline is not executable: {detail}") - } -``` - -with: - -```rust -fn exec_fault_prose(f: &ExecFault) -> String { - match f { - ExecFault::PipelineShape { detail } => { - format!("process pipeline is not executable: {detail}") - } -``` - -Add the three new arms, mirroring the enum's variant order (after `DeflatePlateauConflict`, before `Window`) — replace: - -```rust - ExecFault::DeflatePlateauConflict { stage } => format!( - "process stage {stage}: sweep deflate: true composes only with select \"argmax\"" - ), - ExecFault::Window { stage, detail } => format!( - "process stage {stage}: walk_forward windows do not fit the campaign \ - window: {detail}" - ), -``` - -with: - -```rust - ExecFault::DeflatePlateauConflict { stage } => format!( - "process stage {stage}: sweep deflate: true composes only with select \"argmax\"" - ), - ExecFault::GeneralizeNeedsInstruments { available } => format!( - "campaign has {available} instrument(s); std::generalize needs at least 2" - ), - ExecFault::GeneralizeNonRMetric { metric } => format!( - "std::generalize metric \"{metric}\" is not an R metric (gross/net R \ - families generalize; pip metrics do not)" - ), - ExecFault::ZeroBootstrapParam { stage, field } => format!( - "process stage {stage}: monte_carlo {field} must be > 0" - ), - ExecFault::Window { stage, detail } => format!( - "process stage {stage}: walk_forward windows do not fit the campaign \ - window: {detail}" - ), -``` - -Deliberately NO `(see: aura process introspect --metrics)` pointers on these three arms — the spec's prose for them (spec section "aura-cli") carries none; pointers stay confined to the two metric-roster refusals that already have them. - -- [ ] **Step 8: GREEN gate on the aura-cli unit tests + workspace build** - -The `campaign_run.rs` unit tests live in the `aura` bin target (aura-cli has no lib target), so they are invoked with `--bin aura`. Run: - -``` -cargo test -p aura-cli --bin aura prose_names -``` - -Expected: `test result: ok. 3 passed` (the three new pins), remainder filtered out. Then the full bin unit-test target: - -``` -cargo test -p aura-cli --bin aura -``` - -Expected: `test result: ok` with 0 failed (113 passed = the 110 pre-existing bin unit tests + the 3 new pins; the invariant is 0 failed and the 3 new tests present). Finally: - -``` -cargo build --workspace -``` - -Expected: builds clean (no errors; no new warnings). - -**LOUD BOUNDARY NOTE — expected RED left behind by this task:** the aura-cli *integration* test `campaign_run_v1_boundary_refuses_mc_process` in `crates/aura-cli/tests/research_docs.rs` (around :777-795; its :794 asserts `!out.contains("UnsupportedStage")` inside a test that also asserts the now-removed `"not executable in v1"` prose and exit 1) WILL FAIL after this task — an mc-bearing process now passes preflight by design. Task 5 flips that test to the v2 semantics. Do NOT touch `crates/aura-cli/tests/research_docs.rs` in this task, and do NOT run `cargo test -p aura-cli` (unfiltered, all targets) as a gate here. The intermediate gate for this task is exactly: `cargo test -p aura-campaign` + `cargo test -p aura-cli --bin aura` + `cargo build --workspace` (the two crate suites EXCLUDING the aura-cli integration tests, plus the workspace build). Likewise expected: `crates/aura-campaign/src/exec.rs:279-281` still holds the `MonteCarlo | Generalize` `unreachable!` arm — Task 3 replaces it with the execution arms; no test in this task's gate reaches it. -### Task 3: Execute the `std::monte_carlo` stage arm (dual-input bootstrap annotation) - -Depends on Task 1 (registry/engine widenings: `StageBootstrap`, `StageRealization.bootstrap`, serde on `RBootstrap`) and Task 2 (preflight v2 accepts the annotator suffix). Line references are anchors into the pre-cycle file state — always match on the quoted bytes, not on line numbers. - -**Files:** -- Modify: `crates/aura-campaign/src/exec.rs` (import blocks at the top of the file; the `StageBlock::MonteCarlo | StageBlock::Generalize` unreachable arm near the end of `run_cell`'s stage loop; a new private helper after `scalar_point`) -- Test: `crates/aura-campaign/tests/execute.rs` (imports; `planted_report` gains a filled `trade_rs`; new `wf_stage` constructor; new `ZeroTradeRunner` fake; three new `execute_mc_*` tests) - -- [ ] **Step 1: RED — extend the fake and add the three mc tests** - -In `crates/aura-campaign/tests/execute.rs`, replace: - -```rust -use aura_engine::{RMetrics, RunManifest, RunMetrics, RunReport, SelectionMode}; -use aura_registry::{FamilyKind, Registry}; -use aura_research::{ - Axis, CampaignDoc, Cmp, DataSection, DocKind, DocRef, Predicate, Presentation, ProcessDoc, - ProcessRef, SelectRule, StageBlock, StrategyEntry, Window, -}; -``` - -with: - -```rust -use aura_engine::{r_bootstrap, RMetrics, RunManifest, RunMetrics, RunReport, SelectionMode}; -use aura_registry::{FamilyKind, Registry, StageBootstrap}; -use aura_research::{ - Axis, CampaignDoc, Cmp, DataSection, DocKind, DocRef, Predicate, Presentation, ProcessDoc, - ProcessRef, SelectRule, StageBlock, StrategyEntry, WfMode, Window, -}; -``` - -In the same file, replace: - -```rust -fn planted_report(cell: &CellSpec, params: &[(String, Scalar)], window_ms: (i64, i64)) -> RunReport { - let fast = param_i64(params, "fast"); -``` - -with: - -```rust -/// The deterministic 4-trade R series a planted report carries (matches -/// `n_trades: 4`; non-constant so block resampling has structure). In-memory -/// only: `trade_rs` is serde-skipped and excluded from `PartialEq`, so every -/// existing equality and round-trip assertion stays green. -fn planted_trade_rs(net: f64) -> Vec { - vec![net, -net, 2.0 * net, net] -} - -fn planted_report(cell: &CellSpec, params: &[(String, Scalar)], window_ms: (i64, i64)) -> RunReport { - let fast = param_i64(params, "fast"); -``` - -In the same file, replace: - -```rust - net_expectancy_r: net, - conviction_terciles_r: [0.0, 0.0, 0.0], - trade_rs: Vec::new(), -``` - -with: - -```rust - net_expectancy_r: net, - conviction_terciles_r: [0.0, 0.0, 0.0], - trade_rs: planted_trade_rs(net), -``` - -In the same file, replace: - -```rust -impl MemberRunner for FakeRunner { - fn run_member( - &self, - cell: &CellSpec, - params: &[(String, Scalar)], - window_ms: (i64, i64), - ) -> Result { - if let Some((_, fault)) = self.faults.iter().find(|(p, _)| p.as_slice() == params) { - return Err(fault.clone()); - } - Ok(planted_report(cell, params, window_ms)) - } -} -``` - -with: - -```rust -impl MemberRunner for FakeRunner { - fn run_member( - &self, - cell: &CellSpec, - params: &[(String, Scalar)], - window_ms: (i64, i64), - ) -> Result { - if let Some((_, fault)) = self.faults.iter().find(|(p, _)| p.as_slice() == params) { - return Err(fault.clone()); - } - Ok(planted_report(cell, params, window_ms)) - } -} - -/// Wraps the clean fake and strips the R block from ONE param point's report — -/// the zero-trade survivor for the mc degenerate pin. -struct ZeroTradeRunner { - inner: FakeRunner, - strip: Vec<(String, Scalar)>, -} - -impl MemberRunner for ZeroTradeRunner { - fn run_member( - &self, - cell: &CellSpec, - params: &[(String, Scalar)], - window_ms: (i64, i64), - ) -> Result { - let mut report = self.inner.run_member(cell, params, window_ms)?; - if params == self.strip.as_slice() { - report.metrics.r = None; - } - Ok(report) - } -} -``` - -In the same file, replace: - -```rust -fn gate_stage(value: f64) -> StageBlock { - StageBlock::Gate { - all: vec![Predicate { metric: "net_expectancy_r".to_string(), cmp: Cmp::Gt, value }], - } -} -``` - -with: - -```rust -fn gate_stage(value: f64) -> StageBlock { - StageBlock::Gate { - all: vec![Predicate { metric: "net_expectancy_r".to_string(), cmp: Cmp::Gt, value }], - } -} - -/// 2 rolling windows over the campaign window [1_000, 9_000]: IS 4_000 ms, -/// OOS 2_000 ms, step 2_000 ms -> OOS [5_000, 6_999] and [7_000, 8_999] -/// (window 2 would need OOS end 10_999 > 9_000, so the roller stops at 2). -fn wf_stage() -> StageBlock { - StageBlock::WalkForward { - in_sample_ms: 4_000, - out_of_sample_ms: 2_000, - step_ms: 2_000, - mode: WfMode::Rolling, - metric: "net_expectancy_r".to_string(), - select: SelectRule::Argmax, - } -} -``` - -In the same file, append at the very end (after the closing brace of `execute_refuses_malformed_campaign_id`): - -```rust -#[test] -fn execute_mc_after_gate_bootstraps_each_survivor() { - let reg = temp_registry("mc_per_survivor"); - let doc = campaign(&["EURUSD"]); // seed: 7 - let proc_doc = process(vec![ - sweep_stage(false), - gate_stage(0.3), - StageBlock::MonteCarlo { resamples: 200, block_len: 2 }, - ]); - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("mc-after-gate campaign executes"); - - // planted nets .26/.29/.36/.39 -> gate gt 0.3 keeps ordinals 2 and 3 - let cell = &out.record.cells[0]; - assert_eq!(cell.stages.len(), 3); - let mc = &cell.stages[2]; - assert_eq!(mc.block, "std::monte_carlo"); - assert_eq!(mc.family_id, None); - assert_eq!(mc.survivor_ordinals, None); - assert_eq!(mc.selection, None); - - // each survivor's bootstrap == a hand-called r_bootstrap on its planted - // trade_rs, seeded from the campaign doc (the deflation convention) - let net = |fast: i64, slow: i64| (fast * 10 + slow) as f64 / 100.0; - let expected = StageBootstrap::PerSurvivor(vec![ - (2, r_bootstrap(&planted_trade_rs(net(3, 6)), 200, 2, 7)), - (3, r_bootstrap(&planted_trade_rs(net(3, 9)), 200, 2, 7)), - ]); - assert_eq!(mc.bootstrap, Some(expected)); - - // the record round-trips through the store with the bootstrap intact - let runs = reg.load_campaign_runs().expect("load campaign runs"); - assert_eq!(runs[0], out.record); -} - -#[test] -fn execute_mc_after_wf_pools_the_oos_series() { - let reg = temp_registry("mc_pooled"); - let doc = campaign(&["EURUSD"]); // seed: 7, window [1_000, 9_000] - let proc_doc = process(vec![ - sweep_stage(false), - wf_stage(), - StageBlock::MonteCarlo { resamples: 200, block_len: 3 }, - ]); - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("mc-after-wf campaign executes"); - - // the roller yields 2 windows; both pick the planted argmax (3,9) - let wf = &out.cells[0].families[1]; - assert_eq!(wf.block, "std::walk_forward"); - assert_eq!(wf.reports.len(), 2); - - // pooled input = the wf family reports' trade_rs concatenated in report - // order (roll order per walkforward_member_reports) - let mut pooled: Vec = Vec::new(); - for report in &wf.reports { - pooled.extend_from_slice(&report.metrics.r.as_ref().expect("planted R block").trade_rs); - } - let net = (3 * 10 + 9) as f64 / 100.0; - let mut expected_pool = planted_trade_rs(net); - expected_pool.extend(planted_trade_rs(net)); - assert_eq!(pooled, expected_pool); - - let mc = &out.record.cells[0].stages[2]; - assert_eq!(mc.block, "std::monte_carlo"); - assert_eq!(mc.family_id, None); - assert_eq!(mc.bootstrap, Some(StageBootstrap::PooledOos(r_bootstrap(&pooled, 200, 3, 7)))); -} - -#[test] -fn execute_mc_zero_trade_member_records_degenerate() { - let reg = temp_registry("mc_zero_trade"); - let doc = campaign(&["EURUSD"]); // seed: 7 - let proc_doc = - process(vec![sweep_stage(false), StageBlock::MonteCarlo { resamples: 200, block_len: 2 }]); - // ordinal 0 == odometer point (2,6): its report carries no R block at all - let runner = ZeroTradeRunner { - inner: FakeRunner::clean(), - strip: vec![("fast".to_string(), Scalar::i64(2)), ("slow".to_string(), Scalar::i64(6))], - }; - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &runner, ®) - .expect("a zero-trade member is a valid result, not a fault"); - - let mc = &out.record.cells[0].stages[1]; - assert_eq!(mc.block, "std::monte_carlo"); - let Some(StageBootstrap::PerSurvivor(entries)) = &mc.bootstrap else { - panic!("expected a per-survivor bootstrap, got {:?}", mc.bootstrap); - }; - // no gate ran: all 4 members survive, each annotated - assert_eq!(entries.iter().map(|(o, _)| *o).collect::>(), vec![0, 1, 2, 3]); - // the r: None member records the engine's defined all-zero degenerate - assert_eq!(entries[0].1, r_bootstrap(&[], 200, 2, 7)); - assert_eq!(entries[0].1.n_trades, 0); - assert_eq!(entries[0].1.e_r.mean, 0.0); - assert_eq!(entries[0].1.prob_le_zero, 0.0); -} -``` - -- [ ] **Step 2: Run — confirm RED** - -Run: `cargo test -p aura-campaign execute_mc` -Expected: the test target compiles; the three `execute_mc_*` tests FAIL, each panicking with `internal error: entered unreachable code: preflight refuses non-v1 stages before any member runs` (preflight v2 now admits the stage, the executor arm does not exist yet). No other test fails. - -- [ ] **Step 3: import `r_bootstrap` and `StageBootstrap` in the executor** - -In `crates/aura-campaign/src/exec.rs`, replace: - -```rust -use aura_engine::{ - sweep, walk_forward, GridSpace, ListSpace, RollMode, RunManifest, RunMetrics, RunReport, - Space, SweepFamily, SweepPoint, WalkForwardError, WindowBounds, WindowRoller, WindowRun, -}; -use aura_registry::{ - optimize, optimize_deflated, optimize_plateau, sweep_member_reports, - walkforward_member_reports, CampaignRunRecord, CellRealization, FamilyKind, PlateauMode, - Registry, StageRealization, StageSelection, -}; -``` - -with: - -```rust -use aura_engine::{ - r_bootstrap, sweep, walk_forward, GridSpace, ListSpace, RollMode, RunManifest, RunMetrics, - RunReport, Space, SweepFamily, SweepPoint, WalkForwardError, WindowBounds, WindowRoller, - WindowRun, -}; -use aura_registry::{ - optimize, optimize_deflated, optimize_plateau, sweep_member_reports, - walkforward_member_reports, CampaignRunRecord, CellRealization, FamilyKind, PlateauMode, - Registry, StageBootstrap, StageRealization, StageSelection, -}; -``` - -- [ ] **Step 4: add the `pooled_trade_rs` helper** - -In `crates/aura-campaign/src/exec.rs`, replace: - -```rust -/// A family point's tag-free cells lifted back to self-describing scalars in -/// `space` slot order — the survivor-point form the walk-forward stage -/// re-sweeps through a `ListSpace`. -fn scalar_point(space: &[ParamSpec], point: &[Cell]) -> Vec { - space.iter().zip(point).map(|(ps, c)| Scalar::from_cell(ps.kind, *c)).collect() -} -``` - -with: - -```rust -/// A family point's tag-free cells lifted back to self-describing scalars in -/// `space` slot order — the survivor-point form the walk-forward stage -/// re-sweeps through a `ListSpace`. -fn scalar_point(space: &[ParamSpec], point: &[Cell]) -> Vec { - space.iter().zip(point).map(|(ps, c)| Scalar::from_cell(ps.kind, *c)).collect() -} - -/// The pooled walk-forward OOS trade-R series: the family reports' `trade_rs` -/// concatenated in report order — which IS roll order (the wf stage builds its -/// family via `walkforward_member_reports`, per-window OOS reports in roll -/// order). Reports without an R block contribute nothing. -fn pooled_trade_rs(reports: &[RunReport]) -> Vec { - let mut pooled = Vec::new(); - for report in reports { - if let Some(r) = &report.metrics.r { - pooled.extend_from_slice(&r.trade_rs); - } - } - pooled -} -``` - -- [ ] **Step 5: split the unreachable arm into the mc execution and the generalize no-op** - -In `crates/aura-campaign/src/exec.rs` (`run_cell`'s stage loop), replace: - -```rust - StageBlock::MonteCarlo { .. } | StageBlock::Generalize { .. } => { - unreachable!("preflight refuses non-v1 stages before any member runs") - } -``` - -with: - -```rust - StageBlock::MonteCarlo { resamples, block_len } => { - // Terminal annotator (#200 d1/d3): nothing flows out. Dual - // input: after a walk_forward, ONE bootstrap over the pooled - // per-window OOS trade-R series (roll order); after - // sweep/gates, one bootstrap per surviving member. Seeded from - // the campaign seed (the deflation convention, C1). - let bootstrap = - match families.iter().rev().find(|f| f.block == "std::walk_forward") { - Some(fam) => StageBootstrap::PooledOos(r_bootstrap( - &pooled_trade_rs(&fam.reports), - *resamples as usize, - *block_len as usize, - seed, - )), - // Zero-trade members (r: None) feed an empty slice — - // the engine's defined all-zero degenerate, recorded - // explicitly (a null result is a valid result). - None => StageBootstrap::PerSurvivor( - survivors - .iter() - .map(|(ordinal, _, report)| { - let rs = report - .metrics - .r - .as_ref() - .map(|r| r.trade_rs.as_slice()) - .unwrap_or(&[]); - ( - *ordinal, - r_bootstrap( - rs, - *resamples as usize, - *block_len as usize, - seed, - ), - ) - }) - .collect(), - ), - }; - stages.push(StageRealization { - block: "std::monte_carlo".to_string(), - family_id: None, - survivor_ordinals: None, - selection: None, - bootstrap: Some(bootstrap), - }); - } - StageBlock::Generalize { .. } => { - // Campaign-scope stage (#200 d2): `execute` runs it AFTER the - // cell loop, over the per-cell nominees across instruments — - // nothing realizes per cell. - } -``` - -- [ ] **Step 6: Run — confirm GREEN** - -Run: `cargo test -p aura-campaign execute_mc` -Expected: `3 passed; 0 failed` in the `execute` test target (other targets filter to 0 tests). - -- [ ] **Step 7: Run — full package suite** - -Run: `cargo test -p aura-campaign` -Expected: all tests pass, `0 failed` in every target (the filled `trade_rs` changes no pinned value: it is serde-skipped and `PartialEq`-excluded, and no aura-campaign test pins `deflated_score`/`overfit_probability` numerics). - -- [ ] **Step 8: Run — workspace build** - -Run: `cargo build --workspace` -Expected: builds cleanly, no errors, no new warnings. - -### Task 4: The campaign-scope generalize phase (`CellOutcome.nominee` + `CampaignRunRecord.generalizations`) - -Depends on Tasks 1-3 (`CampaignGeneralization` + the `generalizations: Vec::new()` placeholder in the record literal from Task 1; `wf_stage()`/`planted_trade_rs()` test helpers from Task 3). Line references are anchors — match on the quoted bytes. - -**Files:** -- Modify: `crates/aura-campaign/src/exec.rs` (registry import block; `CellOutcome` struct; `run_cell` nominee population at four sites; `execute`'s cell loop + a post-loop generalize phase replacing the record literal's placeholder) -- Test: `crates/aura-campaign/tests/execute.rs` (imports; a `generalize_stage` constructor; three new `execute_generalize_*` tests) - -- [ ] **Step 1: RED — add the three generalize tests** - -In `crates/aura-campaign/tests/execute.rs`, replace: - -```rust -use aura_registry::{FamilyKind, Registry, StageBootstrap}; -``` - -with: - -```rust -use aura_registry::{generalization, FamilyKind, Registry, StageBootstrap}; -``` - -In the same file, replace: - -```rust -fn wf_stage() -> StageBlock { - StageBlock::WalkForward { - in_sample_ms: 4_000, - out_of_sample_ms: 2_000, - step_ms: 2_000, - mode: WfMode::Rolling, - metric: "net_expectancy_r".to_string(), - select: SelectRule::Argmax, - } -} -``` - -with: - -```rust -fn wf_stage() -> StageBlock { - StageBlock::WalkForward { - in_sample_ms: 4_000, - out_of_sample_ms: 2_000, - step_ms: 2_000, - mode: WfMode::Rolling, - metric: "net_expectancy_r".to_string(), - select: SelectRule::Argmax, - } -} - -fn generalize_stage() -> StageBlock { - StageBlock::Generalize { metric: "net_expectancy_r".to_string() } -} -``` - -In the same file, append at the very end (after the closing brace of `execute_mc_zero_trade_member_records_degenerate`): - -```rust -#[test] -fn execute_generalize_across_two_instruments() { - let reg = temp_registry("generalize_two"); - // the fake negates "AAA" nets -> divergent winners across instruments - let doc = campaign(&["AAA", "BBB"]); // seed: 7 - let proc_doc = process(vec![sweep_stage(false), wf_stage(), generalize_stage()]); - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("generalize campaign executes"); - - assert_eq!(out.record.generalizations.len(), 1); - let g = &out.record.generalizations[0]; - assert_eq!((g.strategy_ordinal, g.window_ordinal), (0, 0)); - assert!(g.missing.is_empty()); - - // winners carry each instrument's nominated params (the last wf window's - // chosen point): AAA argmaxes its NEGATED nets -> (2,6); BBB -> (3,9) - assert_eq!( - g.winners, - vec![ - ( - "AAA".to_string(), - vec![("fast".to_string(), Scalar::i64(2)), ("slow".to_string(), Scalar::i64(6))], - ), - ( - "BBB".to_string(), - vec![("fast".to_string(), Scalar::i64(3)), ("slow".to_string(), Scalar::i64(9))], - ), - ], - ); - - // the stored grade == the shipped generalization() hand-called over the - // two nominated reports (cells are in instrument order: AAA then BBB) - let pairs: Vec<(String, &RunReport)> = out - .cells - .iter() - .zip(["AAA", "BBB"]) - .map(|(cell, inst)| { - let (_, report) = cell.nominee.as_ref().expect("nominee present"); - (inst.to_string(), report) - }) - .collect(); - let expected = generalization(&pairs, "net_expectancy_r").expect("hand-called generalization"); - assert_eq!(g.generalization.as_ref(), Some(&expected)); - - // divergence is exposed, never averaged: worst case is AAA's negated net - let aaa_net = -((2 * 10 + 6) as f64 / 100.0); - let bbb_net = (3 * 10 + 9) as f64 / 100.0; - assert_eq!( - expected.per_instrument, - vec![("AAA".to_string(), aaa_net), ("BBB".to_string(), bbb_net)], - ); - assert_eq!(expected.worst_case, aaa_net); - assert_eq!(expected.sign_agreement, 1); - - // the record round-trips through the store with the generalization intact - let runs = reg.load_campaign_runs().expect("load campaign runs"); - assert_eq!(runs[0], out.record); -} - -#[test] -fn execute_generalize_shortfall_records_missing() { - let reg = temp_registry("generalize_shortfall"); - // AAA's planted nets are all negative -> the gt-0 gate empties its cell - let doc = campaign(&["AAA", "BBB"]); - let proc_doc = process(vec![sweep_stage(false), gate_stage(0.0), generalize_stage()]); - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("a truncated cell is a recorded shortfall, not a fault"); - - assert!(out.cells[0].nominee.is_none(), "gate-truncated cell nominates nothing"); - assert_eq!(out.record.generalizations.len(), 1); - let g = &out.record.generalizations[0]; - assert_eq!(g.generalization, None, "fewer than 2 winners -> no grade computed"); - assert_eq!(g.missing, vec!["AAA".to_string()]); - assert_eq!(g.winners.len(), 1); - assert_eq!(g.winners[0].0, "BBB"); - assert_eq!( - g.winners[0].1, - vec![("fast".to_string(), Scalar::i64(3)), ("slow".to_string(), Scalar::i64(9))], - ); -} - -#[test] -fn execute_generalize_only_after_sweep() { - let reg = temp_registry("generalize_sweep_only"); - let doc = campaign(&["EURUSD", "GER40"]); - let proc_doc = process(vec![sweep_stage(false), generalize_stage()]); - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("sweep-only generalize campaign executes"); - - // no wf ran: each cell nominates its sweep winner (planted argmax (3,9)) - let winner_params = - vec![("fast".to_string(), Scalar::i64(3)), ("slow".to_string(), Scalar::i64(9))]; - for cell in &out.cells { - let (params, report) = cell.nominee.as_ref().expect("sweep winner nominated"); - assert_eq!(params, &winner_params); - assert_eq!(report.manifest.params, winner_params); - } - - let g = &out.record.generalizations[0]; - assert_eq!( - g.winners, - vec![ - ("EURUSD".to_string(), winner_params.clone()), - ("GER40".to_string(), winner_params.clone()), - ], - ); - assert!(g.missing.is_empty()); - let grade = g.generalization.as_ref().expect("two winners -> graded"); - let net = (3 * 10 + 9) as f64 / 100.0; - assert_eq!(grade.n_instruments, 2); - assert_eq!(grade.sign_agreement, 2); - assert_eq!(grade.worst_case, net); -} -``` - -- [ ] **Step 2: Run — confirm RED** - -Run: `cargo test -p aura-campaign execute_generalize` -Expected: the `execute` test target FAILS TO COMPILE with `error[E0609]: no field 'nominee'` on `CellOutcome` (the record's `generalizations` field already exists from Task 1; `nominee` does not exist yet). - -- [ ] **Step 3: import `generalization` and `CampaignGeneralization` in the executor** - -In `crates/aura-campaign/src/exec.rs`, replace: - -```rust -use aura_registry::{ - optimize, optimize_deflated, optimize_plateau, sweep_member_reports, - walkforward_member_reports, CampaignRunRecord, CellRealization, FamilyKind, PlateauMode, - Registry, StageBootstrap, StageRealization, StageSelection, -}; -``` - -with: - -```rust -use aura_registry::{ - generalization, optimize, optimize_deflated, optimize_plateau, sweep_member_reports, - walkforward_member_reports, CampaignGeneralization, CampaignRunRecord, CellRealization, - FamilyKind, PlateauMode, Registry, StageBootstrap, StageRealization, StageSelection, -}; -``` - -- [ ] **Step 4: widen `CellOutcome` with the nominee** - -In `crates/aura-campaign/src/exec.rs`, replace: - -```rust -/// Per-cell stage payloads, for the consumer's emit rendering. -#[derive(Clone, Debug)] -pub struct CellOutcome { - pub families: Vec, - pub selections: Vec, -} -``` - -with: - -```rust -/// Per-cell stage payloads, for the consumer's emit rendering. -#[derive(Clone, Debug)] -pub struct CellOutcome { - pub families: Vec, - pub selections: Vec, - /// The cell's nominated generalize candidate (spec-fixed): the LAST - /// walk-forward window's OOS report + chosen params when a wf stage ran - /// (the freshest out-of-sample evidence for the finally-selected params), - /// else the sweep stage's winner; `None` for gate-truncated cells. - pub nominee: Option<(Vec<(String, Scalar)>, RunReport)>, -} -``` - -- [ ] **Step 5: populate the nominee in `run_cell` (four sites)** - -In `crates/aura-campaign/src/exec.rs`, replace: - -```rust - // The surviving population: (ordinal into the nearest preceding - // family_id-bearing stage's family, param point, member report). - let mut survivors: Vec<(usize, Vec, RunReport)> = Vec::new(); -``` - -with: - -```rust - // The surviving population: (ordinal into the nearest preceding - // family_id-bearing stage's family, param point, member report). - let mut survivors: Vec<(usize, Vec, RunReport)> = Vec::new(); - // The cell's nominated generalize candidate: last wf window's OOS report - // when a walk_forward ran, else the sweep winner; None when truncated. - let mut nominee: Option<(Vec<(String, Scalar)>, RunReport)> = None; -``` - -In the same file (sweep arm), replace: - -```rust - selections.push(StageSelectionOut { - stage: stage_ordinal, - block: "std::sweep", - family_id: family_id.clone(), - winner_ordinal, - params: params.clone(), - selection: selection.clone(), - }); - stages.push(StageRealization { -``` - -with: - -```rust - selections.push(StageSelectionOut { - stage: stage_ordinal, - block: "std::sweep", - family_id: family_id.clone(), - winner_ordinal, - params: params.clone(), - selection: selection.clone(), - }); - // Nominate the sweep winner (superseded by a later - // walk_forward stage; cleared by an empty gate). - nominee = Some((params.clone(), winner.report.clone())); - stages.push(StageRealization { -``` - -In the same file (gate arm), replace: - -```rust - if empty { - break; // decision 8: truncate this cell, keep running cells - } -``` - -with: - -```rust - if empty { - nominee = None; // a truncated cell nominates no candidate - break; // decision 8: truncate this cell, keep running cells - } -``` - -In the same file (walk_forward arm), replace: - -```rust - families.push(fam); -``` - -with: - -```rust - // Nominate the LAST window's OOS report (roll order per - // walkforward_member_reports) with its chosen params — the wf - // stage stamps them on the fresh OOS report's manifest. - nominee = fam - .reports - .last() - .map(|last| (last.manifest.params.clone(), last.clone())); - families.push(fam); -``` - -In the same file (`run_cell`'s return), replace: - -```rust - Ok(( - CellOutcome { families, selections }, -``` - -with: - -```rust - Ok(( - CellOutcome { families, selections, nominee }, -``` - -- [ ] **Step 6: collect nominees in the cell loop and run the campaign-scope generalize phase** - -In `crates/aura-campaign/src/exec.rs` (`execute`), replace: - -```rust - let mut cells_out: Vec = Vec::new(); - let mut cells_rec: Vec = Vec::new(); -``` - -with: - -```rust - let mut cells_out: Vec = Vec::new(); - let mut cells_rec: Vec = Vec::new(); - // Per-(strategy_ordinal, window_ordinal) nominees across instruments, in - // cell-loop (instrument) order — the campaign-scope generalize input. - type Nominee = Option<(Vec<(String, Scalar)>, RunReport)>; - let mut nominees: BTreeMap<(usize, usize), Vec<(String, Nominee)>> = BTreeMap::new(); -``` - -In the same function, replace: - -```rust - cells_out.push(outcome); - cells_rec.push(realization); -``` - -with: - -```rust - nominees - .entry((strategy_ordinal, window_ordinal)) - .or_default() - .push((instrument.clone(), outcome.nominee.clone())); - cells_out.push(outcome); - cells_rec.push(realization); -``` - -In the same function, replace (the record literal as Task 1 left it): - -```rust - let mut record = CampaignRunRecord { - campaign: campaign_id.to_string(), - process: process_id, - run: 0, - seed: campaign.seed, - cells: cells_rec, - // Empty for now: the campaign-scope generalize phase (a later task of - // this cycle) computes and fills this after the cell loop. - generalizations: Vec::new(), - }; -``` - -with: - -```rust - // Campaign-scope generalize (#200 d2): for each (strategy, window), grade - // the per-instrument nominees via the shipped `generalization`; fewer than - // 2 winners is a recorded shortfall, never computed around. - let generalize_metric = process.pipeline.iter().find_map(|s| match s { - StageBlock::Generalize { metric } => Some(metric.clone()), - _ => None, - }); - let mut generalizations: Vec = Vec::new(); - if let Some(metric) = generalize_metric { - for ((strategy_ordinal, window_ordinal), cells) in &nominees { - let winners: Vec<(String, Vec<(String, Scalar)>)> = cells - .iter() - .filter_map(|(inst, nom)| { - nom.as_ref().map(|(params, _)| (inst.clone(), params.clone())) - }) - .collect(); - let missing: Vec = cells - .iter() - .filter(|(_, nom)| nom.is_none()) - .map(|(inst, _)| inst.clone()) - .collect(); - let pairs: Vec<(String, &RunReport)> = cells - .iter() - .filter_map(|(inst, nom)| nom.as_ref().map(|(_, report)| (inst.clone(), report))) - .collect(); - let grade = if pairs.len() >= 2 { - Some(generalization(&pairs, &metric).map_err(ExecFault::Registry)?) - } else { - None - }; - generalizations.push(CampaignGeneralization { - strategy_ordinal: *strategy_ordinal, - window_ordinal: *window_ordinal, - generalization: grade, - winners, - missing, - }); - } - } - - let mut record = CampaignRunRecord { - campaign: campaign_id.to_string(), - process: process_id, - run: 0, - seed: campaign.seed, - cells: cells_rec, - generalizations, - }; -``` - -- [ ] **Step 7: Run — confirm GREEN** - -Run: `cargo test -p aura-campaign execute_generalize` -Expected: `3 passed; 0 failed` in the `execute` test target (other targets filter to 0 tests). - -- [ ] **Step 8: Run — full package suite** - -Run: `cargo test -p aura-campaign` -Expected: all tests pass, `0 failed` in every target (pipelines without a generalize stage record `generalizations: []`, which serde-skips, so every pre-existing record equality and round-trip pin is untouched). - -- [ ] **Step 9: Run — workspace build** - -Run: `cargo build --workspace` -Expected: builds cleanly, no errors, no new warnings. -### Task 5: CLI seam flips + doc tidy + gated e2e extension - -**Files:** -- Modify: `crates/aura-cli/tests/research_docs.rs` — fixture prose + new process fixtures (`MC_PROCESS_DOC` :693-703, `register_process_doc` doc :646-648, `SWEEP_ONLY_PROCESS_DOC` doc :705); test rewrite (`campaign_run_v1_boundary_refuses_mc_process` :772-795 → two v2 seam tests); addressing-mode tests re-seamed (:917-963, :965-1010); gated e2e extension (`WF_PROCESS_DOC` :713-727, `campaign_run_real_e2e_sweep_gate_walkforward` :833-915) -- Modify: `crates/aura-research/src/lib.rs` — `PROCESS_BLOCKS` doc strings :191, :199 (introspection text only; `vocabularies_enumerate_every_block_with_typed_slots` :1348 asserts only non-empty doc + slots, so no pin moves) -- Test: all pins live in `crates/aura-cli/tests/research_docs.rs` (the tests this task rewrites/extends) - -All `Run` commands execute from the repo root `/home/brummel/dev/aura`. Line anchors refer to the tree before this task's own edits (Tasks 1-4 do not touch these two files). - -- [ ] **Step 1: Observe the stale v1 pin failing (RED evidence for the flip)** - -Run: -```bash -cargo test -p aura-cli --test research_docs campaign_run_v1_boundary_refuses_mc_process -``` -Expected: 1 failed — the assertion `out.contains("not executable in v1")` no longer holds: since Task 2's preflight v2, an mc-bearing process passes the shape check and the run proceeds to the member-data seam (exit 1 with data prose, not the retired v1 prose). This is the stale pin this task replaces. - -- [ ] **Step 2: Retire the stale v1 fixture prose; add the two new process fixtures** - -In `crates/aura-cli/tests/research_docs.rs`, replace (old): -```rust -/// Register `doc` as a process document in the project store; returns its id. -/// Asserts register exits 0 — an intrinsically valid document (an mc-bearing -/// one included) must always register; only `campaign run` draws the v1 line. -``` -with (new): -```rust -/// Register `doc` as a process document in the project store; returns its id. -/// Asserts register exits 0 — an intrinsically valid document (annotator -/// stages in any order included) must always register; only `campaign run`'s -/// preflight draws the executable-shape line. -``` - -Then replace (old): -```rust -/// An mc-bearing process: intrinsically VALID (register accepts it) but past -/// the executable v1 boundary (`campaign run` refuses it at preflight). -const MC_PROCESS_DOC: &str = r#"{ - "format_version": 1, - "kind": "process", - "name": "mc-screen", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::monte_carlo", "resamples": 100, "block_len": 5 } - ] -}"#; - -/// The minimal executable v1 pipeline (one sweep stage). -``` -with (new): -```rust -/// An mc-bearing process: intrinsically valid AND (since the v2 executor) an -/// executable pipeline shape — `sweep -> monte_carlo` annotates each sweep -/// survivor. The two addressing-mode tests below run it over the [1, 2] 1970 -/// window, so they refuse deterministically at the member-data seam. -const MC_PROCESS_DOC: &str = r#"{ - "format_version": 1, - "kind": "process", - "name": "mc-screen", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::monte_carlo", "resamples": 100, "block_len": 5 } - ] -}"#; - -/// Intrinsically valid but NOT an executable v2 shape: the annotator sits -/// before the population stage (`walk_forward` after `monte_carlo`). The -/// intrinsic tier deliberately permits this order (the shipped -/// `validate_process_permits_walk_forward_after_an_annotator` pin in -/// aura-research), so register accepts it; only `campaign run`'s preflight -/// refuses it. -const MC_BEFORE_WF_PROCESS_DOC: &str = r#"{ - "format_version": 1, - "kind": "process", - "name": "mc-before-wf", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::monte_carlo", "resamples": 100, "block_len": 5 }, - { "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, - "step_ms": 604800000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" } - ] -}"#; - -/// An executable generalize-bearing shape (`sweep -> generalize`). The -/// refusal under test is the campaign-side static guard (std::generalize -/// needs >= 2 instruments), not the pipeline shape. -const GENERALIZE_PROCESS_DOC: &str = r#"{ - "format_version": 1, - "kind": "process", - "name": "gen-screen", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::generalize", "metric": "net_expectancy_r" } - ] -}"#; - -/// The minimal executable pipeline (one sweep stage). -``` - -- [ ] **Step 3: Replace the v1-boundary test with the two v2 refusal seam tests** - -In `crates/aura-cli/tests/research_docs.rs`, replace (old): -```rust -/// The v1 boundary: `process register` ACCEPTS an mc-bearing document (it is -/// intrinsically valid — asserted inside `register_process_doc`); only -/// `campaign run` refuses it, at preflight, before any member runs (so no -/// data is needed), with path-addressed Debug-free prose. -#[test] -fn campaign_run_v1_boundary_refuses_mc_process() { - let _fixture = project_lock(); - let dir = built_project(); - let runs_dir = dir.join("runs"); - std::fs::remove_dir_all(&runs_dir).ok(); - let _cleanup = ScratchGuard(vec![ - ScratchPath::Dir(runs_dir.clone()), - ScratchPath::File(dir.join("mc.process.json")), - ScratchPath::File(dir.join("mc.campaign.json")), - ]); - let bp_id = seed_blueprint(dir, "campaign-run-mc-seed"); - let proc_id = register_process_doc(dir, "mc.process.json", MC_PROCESS_DOC); - write_doc(dir, "mc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "mc.campaign.json"]); - assert_eq!(code, Some(1), "stdout/stderr: {out}"); - assert!(out.contains("not executable in v1"), "stdout/stderr: {out}"); - assert!(out.contains("std::monte_carlo"), "the prose names the block: {out}"); - assert!(!out.contains("UnsupportedStage"), "Debug leak: {out}"); -} -``` -with (new): -```rust -/// The v2 boundary, order side: `[sweep, monte_carlo, walk_forward]` is -/// intrinsically valid — `process register` ACCEPTS it (asserted inside -/// `register_process_doc`; the intrinsic-tier ground is the shipped -/// `validate_process_permits_walk_forward_after_an_annotator` pin) — but the -/// executable pipeline shape fixes the suffix order (`std::sweep [std::gate]* -/// [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`), so -/// `campaign run` refuses it at the data-free preflight, before any member -/// runs, with Debug-free PipelineShape prose. -#[test] -fn campaign_run_refuses_mc_before_walk_forward() { - let _fixture = project_lock(); - let dir = built_project(); - let runs_dir = dir.join("runs"); - std::fs::remove_dir_all(&runs_dir).ok(); - let _cleanup = ScratchGuard(vec![ - ScratchPath::Dir(runs_dir.clone()), - ScratchPath::File(dir.join("mcwf.process.json")), - ScratchPath::File(dir.join("mcwf.campaign.json")), - ]); - let bp_id = seed_blueprint(dir, "campaign-run-mcwf-seed"); - let proc_id = register_process_doc(dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC); - write_doc(dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "mcwf.campaign.json"]); - assert_eq!(code, Some(1), "stdout/stderr: {out}"); - assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}"); - assert!( - out.contains("std::walk_forward cannot follow std::monte_carlo"), - "the detail names the ordering violation: {out}" - ); - assert!(!out.contains("not executable in v1"), "retired v1 prose resurfaced: {out}"); - assert!(!out.contains("PipelineShape"), "Debug leak: {out}"); -} - -/// The v2 boundary, campaign side: a generalize-bearing process is an -/// executable shape, but `std::generalize` needs >= 2 instruments in the -/// campaign — a STATIC preflight fact (the referential gate has run, no data -/// is read), so the single-GER40 fixture campaign refuses with exit 1 before -/// any member runs. This is the ONE home for the single-instrument refusal; -/// the gated real-data e2e does not duplicate it (its campaign is also -/// single-instrument, and this refusal is data-free by construction). -#[test] -fn campaign_run_refuses_single_instrument_generalize() { - let _fixture = project_lock(); - let dir = built_project(); - let runs_dir = dir.join("runs"); - std::fs::remove_dir_all(&runs_dir).ok(); - let _cleanup = ScratchGuard(vec![ - ScratchPath::Dir(runs_dir.clone()), - ScratchPath::File(dir.join("gen1.process.json")), - ScratchPath::File(dir.join("gen1.campaign.json")), - ]); - let bp_id = seed_blueprint(dir, "campaign-run-gen1-seed"); - let proc_id = register_process_doc(dir, "gen1.process.json", GENERALIZE_PROCESS_DOC); - write_doc(dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "gen1.campaign.json"]); - assert_eq!(code, Some(1), "stdout/stderr: {out}"); - assert!( - out.contains("std::generalize needs at least 2"), - "the refusal names the block and the minimum: {out}" - ); - assert!(out.contains("1 instrument"), "the refusal names the available count: {out}"); - assert!(!out.contains("GeneralizeNeedsInstruments"), "Debug leak: {out}"); -} -``` - -- [ ] **Step 4: Verify the pinned prose against the tree's actual fault strings, then run the two seam tests** - -Run: -```bash -grep -n "PipelineShape" crates/aura-campaign/src/lib.rs -``` -Expected: the v2 preflight's shape-violation arms (landed by Task 2). Read the arm that fires for a `walk_forward` following a `monte_carlo`: its `detail` must contain the byte-string asserted in Step 3, `std::walk_forward cannot follow std::monte_carlo`. If Task 2's landed detail wording differs, update ONLY the Step-3 assertion string to the executor's actual bytes (the test pins the executor's exact prose; do not change the executor). - -Run: -```bash -grep -n "needs at least 2" crates/aura-cli/src/campaign_run.rs -``` -Expected: one hit inside `exec_fault_prose`'s `GeneralizeNeedsInstruments` arm (house prose per the spec: `campaign has {available} instrument(s); std::generalize needs at least 2`). If absent or worded differently, align the two Step-3 substring assertions (`std::generalize needs at least 2`, `1 instrument`) to the landed prose the same way. - -Run: -```bash -cargo test -p aura-cli --test research_docs campaign_run_refuses -``` -Expected: `2 passed` (exactly the two new tests match this filter), 0 failed. - -- [ ] **Step 5: Point the two addressing-mode tests at the member-data seam (comments name what actually refuses now)** - -In `crates/aura-cli/tests/research_docs.rs`, replace (old): -```rust -/// independently drifting code paths. No real data needed: both runs refuse -/// at the (data-free) v1-pipeline-shape preflight. -``` -with (new): -```rust -/// independently drifting code paths. No real data needed: with the [1, 2] -/// 1970 window, both runs pass the (now mc-permitting) preflight and refuse -/// at the member-data seam — no recorded geometry on a data-less host, no -/// overlapping archive window on a data-ful one — exit 1 and byte-identical -/// prose either way (the message depends only on env/instrument/window). -``` - -Then replace (old): -```rust - let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line"); - let id_line = id_out.lines().find(|l| l.starts_with("aura: ")).expect("id refusal line"); - assert_eq!(file_line, id_line, "file- and id-addressed runs must refuse identically"); -``` -with (new): -```rust - let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line"); - let id_line = id_out.lines().find(|l| l.starts_with("aura: ")).expect("id refusal line"); - assert!( - file_line.contains("no recorded geometry") || file_line.contains("no data for instrument"), - "the refusal is the member-data seam (mc processes execute now): {file_line}" - ); - assert_eq!(file_line, id_line, "file- and id-addressed runs must refuse identically"); -``` - -Then replace (old): -```rust -/// stripped, not read as a second, unresolvable address). Both refuse at the -/// data-free v1 preflight (MC process), so no real data is needed. -``` -with (new): -```rust -/// stripped, not read as a second, unresolvable address). Both refuse at the -/// member-data seam (the mc process is executable in v2; the [1, 2] 1970 -/// window guarantees a deterministic data refusal), so no real data is -/// needed. -``` - -Then replace (old): -```rust - let bare_line = bare_out.lines().find(|l| l.starts_with("aura: ")).expect("bare refusal line"); - assert_eq!(bare_line, pfx_line, "bare-id and content:-prefixed runs must resolve identically"); -``` -with (new): -```rust - let bare_line = bare_out.lines().find(|l| l.starts_with("aura: ")).expect("bare refusal line"); - assert!( - bare_line.contains("no recorded geometry") || bare_line.contains("no data for instrument"), - "the refusal is the member-data seam (mc processes execute now): {bare_line}" - ); - assert_eq!(bare_line, pfx_line, "bare-id and content:-prefixed runs must resolve identically"); -``` - -Run: -```bash -cargo test -p aura-cli --test research_docs campaign_run_by_content_id_matches_file_sugar_refusal -``` -Expected: 1 passed. - -Run: -```bash -cargo test -p aura-cli --test research_docs campaign_run_tolerates_content_prefix_on_target -``` -Expected: 1 passed. - -- [ ] **Step 6: Extend the gated e2e with the mc annotator suffix** - -In `crates/aura-cli/tests/research_docs.rs`, replace (old): -```rust -/// The full v1 shape for the gated e2e: sweep -> gate -> walk_forward. The -/// gate (`n_trades ge 0`) passes every member, so walk-forward always has -/// survivors. Roller: 14d IS / 7d OOS / 7d step in epoch-ms, tiling the -/// ~30-day GER40 Sept-2024 campaign window. -const WF_PROCESS_DOC: &str = r#"{ - "format_version": 1, - "kind": "process", - "name": "screen-then-walkforward", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 0.0 } ] }, - { "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, - "step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" } - ] -}"#; -``` -with (new): -```rust -/// The full annotated shape for the gated e2e: sweep -> gate -> walk_forward -/// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so -/// walk-forward always has survivors; the mc annotator bootstraps the pooled -/// per-window OOS trade-R series (200 resamples, block 5). Roller: 14d IS / -/// 7d OOS / 7d step in epoch-ms, tiling the ~30-day GER40 Sept-2024 campaign -/// window. NO generalize stage: this campaign has ONE instrument (a second -/// local archive is not guaranteed), and the single-instrument refusal is -/// pinned data-free in `campaign_run_refuses_single_instrument_generalize`. -const WF_PROCESS_DOC: &str = r#"{ - "format_version": 1, - "kind": "process", - "name": "screen-then-walkforward", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 0.0 } ] }, - { "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, - "step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }, - { "block": "std::monte_carlo", "resamples": 200, "block_len": 5 } - ] -}"#; -``` - -Then replace (old): -```rust -/// Gated real-data e2e (the `cli_run.rs` skip idiom): a full -/// sweep -> gate -> walk_forward campaign over GER40 Sept-2024 where the -/// local archive is present — exit 0, emit-gated family/selection lines, a -/// final line parseable as JSON with top-level `campaign_run` linking a sweep -/// family id and a walk-forward family id, and the `campaign_runs.jsonl` -/// sibling store written. Skips with a note elsewhere so -/// `cargo test --workspace` stays green on a data-less machine. -``` -with (new): -```rust -/// Gated real-data e2e (the `cli_run.rs` skip idiom): a full -/// sweep -> gate -> walk_forward -> monte_carlo campaign over GER40 -/// Sept-2024 where the local archive is present — exit 0, emit-gated -/// family/selection lines, a final line parseable as JSON with top-level -/// `campaign_run` linking a sweep family id, a walk-forward family id, and a -/// pooled-OOS bootstrap on the terminal mc stage, and the -/// `campaign_runs.jsonl` sibling store written. Skips with a note elsewhere -/// so `cargo test --workspace` stays green on a data-less machine. -``` - -Then replace (old): -```rust - let stages = cells[0]["stages"].as_array().expect("stages array"); - assert_eq!(stages.len(), 3, "sweep + gate + walk_forward realized: {record_line}"); -``` -with (new): -```rust - let stages = cells[0]["stages"].as_array().expect("stages array"); - assert_eq!( - stages.len(), - 4, - "sweep + gate + walk_forward + monte_carlo realized: {record_line}" - ); -``` - -Then replace (old): -```rust - assert!( - stages[2]["family_id"].as_str().is_some(), - "walk-forward stage links a family: {record_line}" - ); -``` -with (new): -```rust - assert!( - stages[2]["family_id"].as_str().is_some(), - "walk-forward stage links a family: {record_line}" - ); - assert_eq!( - stages[3]["block"].as_str(), - Some("std::monte_carlo"), - "the annotator stage is realized last: {record_line}" - ); - let pooled = &stages[3]["bootstrap"]["pooled_oos"]; - assert!( - pooled.is_object(), - "an mc stage after walk_forward records ONE pooled-OOS bootstrap: {record_line}" - ); - assert_eq!( - pooled["n_resamples"].as_u64(), - Some(200), - "the bootstrap carries the doc's resamples verbatim: {record_line}" - ); -``` - -Run: -```bash -cargo test -p aura-cli --test research_docs campaign_run_real_e2e_sweep_gate_walkforward -``` -Expected: 1 passed — on this machine the local GER40 archive is present, so the full assert path runs (4 stages, `std::monte_carlo` last, `pooled_oos` object with `n_resamples` 200). On a data-less host the test prints `skip: no local GER40 data for the campaign e2e` and still passes. - -- [ ] **Step 7: aura-research doc-string tidy — "(terminal annotator in v1)" loses the version tag** - -In `crates/aura-research/src/lib.rs`, replace (old): -```rust - doc: "R-bootstrap over realised R (terminal annotator in v1)", -``` -with (new): -```rust - doc: "R-bootstrap over realised R (terminal annotator)", -``` - -Then replace (old): -```rust - doc: "cross-instrument worst-case floor (terminal annotator in v1)", -``` -with (new): -```rust - doc: "cross-instrument worst-case floor (terminal annotator)", -``` - -Run: -```bash -grep -rn "terminal annotator in v1" crates -``` -Expected: no output, exit 1 (the string survives nowhere; no test pins the parenthetical — the describe test asserts slots + non-empty doc only). - -Run: -```bash -cargo test -p aura-research -``` -Expected: all tests pass, 0 failed (introspection TEXT only — no document bytes, no content-id movement). - -- [ ] **Step 8: End gates** - -Run: -```bash -cargo test -p aura-cli campaign_run -``` -Expected: 0 failed in every test binary. The `research_docs` binary runs the 11 `campaign_run_*` integration tests (all pass; the gated e2e may print the skip note on a data-less host and still passes); the module-path match also runs the `campaign_run::tests::*` unit tests in the `aura` binary — all pass. - -Run: -```bash -cargo test --workspace -``` -Expected: green — 0 failed across all crates. - -Run: -```bash -cargo clippy --workspace --all-targets -- -D warnings -``` -Expected: clean, exit 0. diff --git a/docs/specs/0108-annotator-stages.md b/docs/specs/0108-annotator-stages.md deleted file mode 100644 index 2840364..0000000 --- a/docs/specs/0108-annotator-stages.md +++ /dev/null @@ -1,280 +0,0 @@ -# Annotator Stages — mc/generalize Execution — Design Spec (cycle 0108) - -**Date:** 2026-07-03 -**Status:** Draft — /boss autonomous run; sign-off gate = grounding-check PASS -**Authors:** orchestrator + Claude -**Reference issue:** #200 (this cycle's anchor). All five design forks are -decided on #200 (fork-triage comment 2026-07-03, five-lens swarm, all -convergent on cited ground); this spec encodes those decisions and adds none. - -## Goal - -`aura campaign run` executes the two annotator stages the v1 executor refused: -the v2 executable pipeline shape is -`std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`. -`std::monte_carlo` bootstraps the stage's incoming R-evidence (per-survivor -after sweep/gates; the pooled per-window OOS series after a walk_forward) via -the shipped `r_bootstrap`; `std::generalize` runs at campaign scope over the -per-cell selection winners of each (strategy, window) across instruments via -the shipped `generalization`. Both are terminal annotators — nothing flows out -of them (#200 decision 3, unanimous) — and no document-schema bytes change: -every existing content id survives this cycle. - -## Architecture - -All execution changes live in **`aura-campaign`** (preflight v2 + the two -stage executions); **`aura-registry`** widens the realization records with -sparse serde-default fields (existing `campaign_runs.jsonl` lines stay -parseable — the C14/C23 widening convention) and derives serde on -`Generalization`; **`aura-engine`** derives serde on `RBootstrap`; -**`aura-cli`** gains prose for the new faults and loses the v1-boundary -refusal for mc/generalize (its pinned tests flip to the new semantics). -Determinism (C1): the bootstrap seeds from the doc's `seed` (the deflation -convention); all annotator inputs are the executor's fresh in-memory reports — -`trade_rs` is `#[serde(skip)]` and empty after any registry round-trip, so the -annotators run inside `execute` or not at all. - -## Concrete code shapes - -### The user-facing program - -The 0107 fieldtest process grows the annotator suffix — no other document -changes (axes, campaign, data as in -`fieldtests/cycle-0107-campaign-executor/c0107_2_campaign_full.json`): - -```json -{ - "format_version": 1, - "kind": "process", - "name": "screen-wf-bootstrap-generalize", - "pipeline": [ - { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax", "deflate": true }, - { "block": "std::gate", "all": [ { "metric": "expectancy_r", "cmp": "gt", "value": 0.0 } ] }, - { "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, - "step_ms": 604800000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" }, - { "block": "std::monte_carlo", "resamples": 1000, "block_len": 5 }, - { "block": "std::generalize", "metric": "net_expectancy_r" } - ] -} -``` - -``` -$ aura campaign run campaign.json # >= 2 instruments for generalize -…family/selection lines as today… -{"campaign_run":{"campaign":"…","process":"…","run":0,"seed":42,"cells":[…, - {"block":"std::walk_forward","family_id":"…"}, - {"block":"std::monte_carlo","bootstrap":{"pooled_oos":{"e_r":{…},"prob_le_zero":0.031,"n_trades":57,"block_len":5,"n_resamples":1000}}}…], - "generalizations":[{"strategy_ordinal":0,"window_ordinal":0,"generalization":{"selection_metric":"net_expectancy_r","n_instruments":2,"worst_case":0.04,"sign_agreement":2,"per_instrument":[["EURUSD",0.04],["GER40",0.11]]},"winners":[["EURUSD",[["sma_cross.fast.length",{"I64":3}],…]],["GER40",[…]]]}]}} -$ echo $? -0 -``` - -A process without walk_forward: the mc stage annotates **each surviving -member** instead (`"bootstrap":{"per_survivor":[[0,{…RBootstrap…}],[2,{…}]]}`, -ordinals indexing the population family — the `survivor_ordinals` convention). - -### aura-engine - -```rust -// mc.rs — RBootstrap currently derives Clone/Debug/PartialEq only: -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct RBootstrap { … unchanged fields … } -// (MetricStats, its field type, gains the same derives if it lacks them.) -``` - -### aura-registry - -```rust -// lib.rs — Generalization gains serde derives (fields unchanged). - -// lineage.rs — the mc annotation, payload mirroring the input shape (#200 d1/d4): -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum StageBootstrap { - /// After sweep/gates: one bootstrap per surviving member, - /// `(ordinal into the population family, result)`. - PerSurvivor(Vec<(usize, RBootstrap)>), - /// After a walk_forward: one bootstrap over the pooled per-window - /// OOS trade-R series (roll order). - PooledOos(RBootstrap), -} - -pub struct StageRealization { - …existing fields… - /// std::monte_carlo stages only. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bootstrap: Option, -} - -/// The campaign-scope generalize annotation (#200 d2): recorded at the scope -/// it is computed, keyed (strategy, window) across instruments. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CampaignGeneralization { - pub strategy_ordinal: usize, - pub window_ordinal: usize, - /// None when fewer than 2 instrument cells produced a winner (gate - /// truncation) — the shortfall is recorded, never computed around. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub generalization: Option, - /// Per-instrument winning params — divergent winners are exposed, - /// never averaged away. Present whenever >= 1 winner exists. - pub winners: Vec<(String, Vec<(String, Scalar)>)>, - /// Instrument cells that contributed no winner (gate-truncated), by name. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub missing: Vec, -} - -pub struct CampaignRunRecord { - …existing fields… - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub generalizations: Vec, -} -``` - -### aura-campaign — preflight v2 - -The v1 shape check is replaced by the v2 shape -`sweep (gate)* (walk_forward)? (monte_carlo)? (generalize)?` (each suffix -stage at most once, order fixed). `UnsupportedStage` disappears; new faults: - -```rust -pub enum ExecFault { - …existing minus UnsupportedStage… - /// generalize needs >= 2 instruments in the campaign (static). - GeneralizeNeedsInstruments { available: usize }, - /// generalize's metric must be an R metric (static — the intrinsic tier - /// accepts any vocabulary name; the shipped generalization() would only - /// refuse at runtime). - GeneralizeNonRMetric { metric: String }, - /// monte_carlo with resamples == 0 or block_len == 0 (static, instead of - /// the engine's defined all-zero degenerate). - ZeroBootstrapParam { stage: usize, field: &'static str }, -} -``` - -Shape violations (annotator before a population stage, duplicates, generalize -not last, mc before wf) stay `PipelineShape { detail }` with precise prose. -The generalize R-metric check reuses the registry's `check_r_metric`. - -### aura-campaign — execution - -```rust -// run_cell, new arm (replaces the unreachable! for MonteCarlo): -StageBlock::MonteCarlo { resamples, block_len } => { - let bootstrap = match &wf_family { - // after walk_forward: pool the wf family's OOS trade_rs in roll - // order (the shipped pooled_oos_trade_rs pattern), one bootstrap - Some(fam) => StageBootstrap::PooledOos(r_bootstrap( - &pooled_trade_rs(&fam.reports), *resamples as usize, *block_len as usize, seed)), - // after sweep/gates: one bootstrap per surviving member over its - // fresh in-memory trade_rs; zero-trade members record the defined - // all-zero degenerate explicitly - None => StageBootstrap::PerSurvivor( - survivors.iter().map(|(ordinal, _, report)| (*ordinal, r_bootstrap( - report.metrics.r.as_ref().map(|r| r.trade_rs.as_slice()).unwrap_or(&[]), - *resamples as usize, *block_len as usize, seed))).collect()), - }; - stages.push(StageRealization { block: "std::monte_carlo".into(), - family_id: None, survivor_ordinals: None, selection: None, - bootstrap: Some(bootstrap) }); -} - -// execute(), after the cell loop — the campaign-scope generalize phase: -// for each (strategy_ordinal, window_ordinal): collect each instrument -// cell's FINAL selection winner report (the wf stage's per-window winners -// pooled? NO — the cell's nominated candidate is the last selection-bearing -// stage's winner REPORT: the sweep winner when no wf ran, else the wf -// family's last-window OOS report); >= 2 winners -> generalization(&pairs, -// metric) with winners+missing recorded; < 2 -> generalization: None, -// shortfall in `missing`. -``` - -The cell's **nominated report** for generalize is defined as: the OOS report -of the *last* walk-forward window when a wf stage ran (the freshest -out-of-sample evidence for the finally-selected params), else the sweep -stage's winner report. This is a definition this spec fixes so the executor, -the record, and the tests agree; it is recorded implicitly by `winners` -carrying the nominated params per instrument. - -`run_cell` therefore returns (in `CellOutcome`) what the campaign phase needs: -the nominated winner report + params per cell (a small `nominee: -Option<(Vec<(String, Scalar)>, RunReport)>` on `CellOutcome`), `None` for -gate-truncated cells. - -### aura-cli - -- `exec_fault_prose`: arms for the three new faults (house style, e.g. - `campaign has {available} instrument(s); std::generalize needs at least 2`, - `process stage {stage}: monte_carlo {field} must be > 0`, - `std::generalize metric "{metric}" is not an R metric (gross/net R families - generalize; pip metrics do not)`); the `UnsupportedStage` arm and its - "not executable in v1" prose disappear (compile-driven by the variant - removal). -- The `campaign_run_v1_boundary_refuses_mc_process` seam test flips: an - mc-bearing process now RUNS (fake or real data permitting) — replaced by - pins on the new refusals (mc-before-wf shape, generalize single-instrument). -- `PROCESS_BLOCKS` doc strings in aura-research: "(terminal annotator in v1)" - → "(terminal annotator)" — introspection TEXT only, no document bytes, no - content-id movement (verify: no golden pins the parenthetical; the describe - tests pin slot lines). - -## Components - -| Change | Crate | -|---|---| -| `RBootstrap` (+`MetricStats` if needed) serde derives | aura-engine | -| `Generalization` serde derives; `StageBootstrap`, `CampaignGeneralization`, `StageRealization.bootstrap`, `CampaignRunRecord.generalizations` | aura-registry | -| preflight v2 (shape + 3 static guards), mc arm, campaign-scope generalize phase, `CellOutcome.nominee`, `pooled_trade_rs` helper | aura-campaign | -| fault prose swap, seam-test flips, doc-string tidy | aura-cli, aura-research | - -## Data flow - -``` -cell loop (unchanged) … wf stage - └─ mc stage: wf ran ? pooled OOS series → one RBootstrap - : per-survivor trade_rs → Vec<(ordinal, RBootstrap)> - → StageRealization.bootstrap -after all cells: for each (strategy, window): - nominees across instruments (>= 2 ? generalization() : shortfall) - → CampaignRunRecord.generalizations -stdout: the campaign_run line carries both (serde) — no new emit kinds -``` - -## Error handling - -Exit codes unchanged (refusals 1 before any member runs; realization exit 0). -New refusals are static preflight faults with Debug-free prose. A zero-trade -bootstrap input is NOT an error: the engine's defined all-zero `RBootstrap` -is recorded explicitly (a null result is a valid research result). - -## Testing strategy - -1. **aura-registry:** record round-trip with `bootstrap`/`generalizations` - populated; a pre-widening `campaign_runs.jsonl` line (no new fields) still - parses (serde-default pin). -2. **aura-campaign (fake runner):** per-survivor mc (ordinals + per-member - RBootstrap against hand-computed `r_bootstrap` on planted `trade_rs`); - pooled-OOS mc after wf (input = wf family reports' trade_rs in roll - order); zero-trade degenerate recorded; deterministic twice (doc seed); - generalize over 2 fake instruments (worst_case/sign_agreement against the - shipped function, winners + params recorded); gate-truncated shortfall - (`generalization: None`, `missing` named); all new preflight refusals - (shape suffix order, duplicates, mc-before-wf, generalize-not-last, - single-instrument, non-R metric, zero resamples/block_len). -3. **aura-cli seam:** new-fault prose pins (Debug-free); the flipped - v1-boundary test; `--metrics`/introspection untouched. -4. **Gated real-data e2e:** extend the 0107 e2e process with the annotator - suffix where local data exists (2 instruments if a second archive exists, - else the mc-only suffix + a generalize-refusal assert); skip elsewhere. - -## Acceptance criteria - -1. The worked process above runs end-to-end (data permitting): exit 0, the - `campaign_run` line and stored record carry `bootstrap` and (with >= 2 - instruments) `generalizations`. -2. A no-wf pipeline annotates every surviving member (`per_survivor`). -3. All new preflight refusals fire before any member runs, with precise - prose; `[sweep, mc, walk_forward]` — intrinsically valid — is refused. -4. Existing stored `campaign_runs.jsonl` lines parse unchanged; no document - content id moves this cycle. -5. Suite green, clippy clean, doc build clean.