# Campaign Executor — Design Spec (cycle 0107) **Date:** 2026-07-03 **Status:** Draft — /boss autonomous run; sign-off gate = grounding-check PASS **Authors:** orchestrator + Claude **Reference issues:** #198 (executor, this cycle's anchor), #196 (blueprint on-ramp, rides along) **Design record:** all eleven executor forks + the home fork are decided on #198 (fork-triage comment 2026-07-03 + user decision comment 2026-07-03); this spec encodes those decisions and adds no new forks. ## Goal `aura campaign run ` turns a persisted, content-addressed campaign document into a realized run-set: it resolves the campaign's refs, executes its process pipeline (v1: `std::sweep [std::gate]* [std::walk_forward]?`) once per (strategy, instrument, window) cell over the existing family machinery, evaluates gates per-member, records a campaign-level realization linking the per-stage family records, and honours data-level presentation. The execution semantics live in a **new library crate `aura-campaign`**; the CLI is one of its consumers (#198 home decision: future consumers — playground, tests — reach the same library). ## Architecture Three layers, one new: 1. **`aura-campaign` (new leaf library crate)** — campaign *semantics*: cell enumeration, preflight (v1 boundary, rankable metrics, gate-metric resolvability), stage sequencing, per-member gate evaluation, winner selection (via `aura-registry`'s `optimize`/`optimize_deflated`/ `optimize_plateau`), walk-forward rolling (via `aura-engine`'s `WindowRoller`/`walk_forward`), realization assembly, and the registry writes (`append_family` per stage, `append_campaign_run` at the end). Deps: `aura-core`, `aura-engine`, `aura-analysis`, `aura-registry`, `aura-research`, `serde`/`serde_json`. Deliberately **no** `aura-ingest`, no `aura-std`/`aura-composites`: harness and data binding enter through a one-method trait (`MemberRunner`), so the deploy-condemned CLI scaffolding never becomes a library dep and a differently-binding consumer (playground) implements the same seam. 2. **`aura-cli`** — the `MemberRunner` implementation over the shipped loaded-blueprint convention (`wrap_r` + reduce-mode member runs + `M1FieldSource` windowed real-data binding — the machinery `run_blueprint_member`/`windowed_sources` already ship), the `campaign run` verb, and the #196 on-ramp verbs. 3. **`aura-registry`** — the new `CampaignRunRecord` sibling store (`campaign_runs.jsonl` beside `runs.jsonl`/`families.jsonl`), following the registry's growth pattern: a thin linking record over untouched family records (#198 decision 4). Plus one corrective slice in **`aura-research`**: the `std::walk_forward` vocabulary is corrected to machinery-true fields (#198 decision 2 — the shipped `folds` slot maps to nothing `WindowRoller::new` accepts), and one small addition in **`aura-engine`**: a `ListSpace` (explicit point set implementing `Space`), because a gate produces an arbitrary member subset that the cartesian `GridSpace` cannot represent. Determinism (C1): cells run sequentially in declared doc order; *within* a stage, members run disjointly in parallel via the engine `sweep` primitive (parallelism across sims, never within one). Every stochastic sub-procedure (deflation nulls) seeds from the doc's declared `seed`, so a campaign's realization is a pure function of the document + stores + data. ## Concrete code shapes ### The user-facing program (the acceptance-criterion evidence) The methodology designer / campaign designer writes two documents and runs them. `methodology.json` — note the corrected `std::walk_forward` fields (lengths in epoch-ms, the unit the campaign's own windows already use; 90/30/30 days shown): ```json { "format_version": 1, "kind": "process", "name": "screen-then-walkforward", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax", "deflate": true }, { "block": "std::gate", "all": [ { "metric": "net_expectancy_r", "cmp": "gt", "value": 0.0 } ] }, { "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000, "step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" } ] } ``` `campaign.json` (schema unchanged from cycle 0106): ```json { "format_version": 1, "kind": "campaign", "name": "sma-screen-eurusd", "data": { "instruments": ["EURUSD"], "windows": [ { "from_ms": 1136073600000, "to_ms": 1154390400000 } ] }, "strategies": [ { "ref": { "content_id": "" }, "axes": { "sma_cross.fast.length": { "kind": "I64", "values": [2, 3, 4] }, "sma_cross.slow.length": { "kind": "I64", "values": [6, 9, 12] } } } ], "process": { "ref": { "content_id": "" } }, "seed": 7, "presentation": { "persist_taps": [], "emit": ["selection_report"] } } ``` The authoring loop, inside a built project: ``` $ aura graph register strategy.json # NEW (#196): blueprint into the store registered blueprint content:3f9c… (runs/blueprints/3f9c….json) $ aura graph introspect --params strategy.json # NEW (#196): the campaign-axis namespace sma_cross.fast.length:I64 sma_cross.slow.length:I64 $ aura process register methodology.json registered process content:aa12… (runs/processes/aa12….json) $ aura campaign run campaign.json # file is sugar: registers, then runs by id {"selection_report":{"family_id":"bb34aa55-0-EURUSD-w0-s0-0","stage":0,"block":"std::sweep","winner_ordinal":4,"params":[["sma_cross.fast.length",{"I64":3}],["sma_cross.slow.length",{"I64":9}]],"selection":{…FamilySelection…}}} {"campaign_run":{"campaign":"bb34aa55…","process":"aa12…","run":0,"seed":7,"cells":[{"strategy":"3f9c…","instrument":"EURUSD","window_ms":[1136073600000,1154390400000],"stages":[{"block":"std::sweep","family_id":"bb34aa55-0-EURUSD-w0-s0-0","selection":{…}},{"block":"std::gate","survivor_ordinals":[0,3,4,7]},{"block":"std::walk_forward","family_id":"bb34aa55-0-EURUSD-w0-s2-0"}]}]}} $ echo $? 0 ``` Zero survivors at a gate: the cell's realization stops after the empty gate stage, stderr notes it, the run continues to the next cell and **exits 0** (#198 decision 8 — a null result is a valid research result). `emit` gating: `family_table` prints the existing per-member `{"family_id":…,"report":…}` lines for each family-producing stage; `selection_report` prints one line per selection-bearing stage; the final `{"campaign_run":…}` line always prints. Non-empty `persist_taps` prints `aura: persist_taps not yet honored (N tap(s) ignored)` on stderr once per run (#198 decision 6 — loud deferral, never silent). ### aura-research: the walk_forward vocabulary correction Before (shipped 0106; `folds` maps to nothing the machinery accepts — `WindowRoller::new(span, is_len, oos_len, step, mode)` has no fold count, `crates/aura-engine/src/walkforward.rs:73`): ```rust #[serde(rename = "std::walk_forward")] WalkForward { folds: u32, in_sample_bars: u64, out_of_sample_bars: u64, metric: String, select: SelectRule }, ``` After (machinery-true: the roller's three lengths in epoch-ms — the campaign window's own unit; ms→ns happens once at the driver's source seam, the established `open_window` pattern — plus the roller's mode, both `RollMode` variants exposed): ```rust #[serde(rename = "std::walk_forward")] WalkForward { in_sample_ms: u64, out_of_sample_ms: u64, step_ms: u64, mode: WfMode, metric: String, select: SelectRule }, /// Wire form: "rolling" | "anchored" — the two shipped `RollMode`s. #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum WfMode { #[serde(rename = "rolling")] Rolling, #[serde(rename = "anchored")] Anchored } ``` Co-moving sites (the schema-propagation set, `crates/aura-research/src/lib.rs`): the `PROCESS_BLOCKS` `std::walk_forward` `SlotInfo` list (new slot kind label for `mode`: reuse `SlotKind::SelectRule`? No — add `SlotKind::WfMode` with label `"one of: rolling | anchored"`); the `stage_from_value` arm (`require_u64` × 3 + a `mode` parser mirroring `select_from`); the `validate_process` WalkForward arm gains machinery-true numeric checks — `DocFault::ZeroWalkForwardLength { stage: usize, field: &'static str }` when any of the three lengths is 0 (`WindowRoller::new` refuses `NonPositiveLength`; the doc tier now refuses it earlier with a path-addressed fault); canonical field order becomes `block, in_sample_ms, out_of_sample_ms, step_ms, mode, metric, select`. Consequences accepted: every walk_forward-bearing process doc gets a new content id; the golden canonical pin (`lib.rs:1039-1063`), the `PROCESS_FIXTURE`/`PROCESS_DOC` test constants, and the open-slot tests move with it. The tracked fieldtest-corpus store files under `fieldtests/cycle-0106-research-artifacts/` are a historical record and are **not** touched. ### aura-engine: ListSpace A gate's survivor set is an arbitrary point subset — no cartesian structure — so it needs a first-class `Space` beside `GridSpace`/`RandomSpace`: ```rust /// Explicit point set over a param space. Points are validated against /// `space` kinds at construction (same contract as GridSpace::new). pub struct ListSpace { space: Vec, points: Vec> } impl ListSpace { pub fn new(space: &[ParamSpec], points: Vec>) -> Result // Arity per point == space.len(); kind per slot == spec.kind; empty point // list is allowed (a legitimately-empty survivor set never reaches sweep — // the executor early-terminates — but the type does not forbid it). } impl Space for ListSpace { fn points(&self) -> Vec> // Scalar→Cell lowering, declared kinds fn param_specs(&self) -> &[ParamSpec] } ``` `sweep(&list_space, run_one)` then gives the same disjoint parallelism and enumeration-order determinism the grid path has. ### aura-campaign: the library ```rust /// One structural cell: (strategy, instrument, window) — #198 decision 7. pub struct CellSpec { pub strategy_ordinal: usize, pub strategy_id: String, // resolved blueprint content id pub blueprint_json: String, // canonical bytes from the store pub axes: BTreeMap, // campaign axes (raw param_space names) pub instrument: String, pub window_ms: (i64, i64), } /// The harness/data binding seam — the ONLY thing a consumer implements. /// Params arrive as (raw axis name, value) pairs; the implementation binds /// them to its harness convention and runs the member over `instrument` /// restricted to `window_ms` (inclusive epoch-ms). pub trait MemberRunner: Sync { fn run_member( &self, cell: &CellSpec, params: &[(String, Scalar)], window_ms: (i64, i64), // ⊆ cell.window_ms (walk-forward sub-windows) ) -> Result; } /// Display-free faults (the CLI phrases them — the RefFault pattern). pub enum MemberFault { NoData { instrument: String, window_ms: (i64, i64) }, Bind(String), Run(String) } pub enum ExecFault { // preflight + runtime refusals UnsupportedStage { stage: usize, block: String }, // v1 boundary (mc/generalize) PipelineShape { detail: String }, // not sweep [gate]* [wf]? UnrankableMetric { stage: usize, metric: String }, // not in the registry's rankable set GateMetricNotPerMember { stage: usize, metric: String },// annotation metrics (deflated_score…) PlateauInWalkForward { stage: usize }, // survivor subsets have no lattice DeflatePlateauConflict { stage: usize }, // sweep: deflate=true + select plateau:* Member(MemberFault), Registry(RegistryError), } /// Everything the run produced, for the consumer to render. pub struct CampaignOutcome { pub record: CampaignRunRecord, // what append_campaign_run stored pub run: usize, // assigned counter pub cells: Vec, // per-cell stage payloads for emit rendering } pub struct CellOutcome { pub families: Vec, pub selections: Vec } pub fn execute( campaign: &CampaignDoc, process: &ProcessDoc, strategies: &[(String, String)], // (content id, canonical blueprint json), resolved by the caller runner: &dyn MemberRunner, registry: &Registry, ) -> Result ``` `execute` in order: 1. **Preflight** (all refusals before any member runs — the F7 lesson applied forward): v1 pipeline shape is exactly `std::sweep (std::gate)* (std::walk_forward)?` (subsumes #198 decision 5 — `std::monte_carlo`/`std::generalize` refuse with prose naming the v1 boundary); every `metric` slot on sweep/walk_forward is in the registry's rankable set (`total_pips`, `max_drawdown`, `bias_sign_flips`, `sqn`, `sqn_normalized`, `expectancy_r`, `net_expectancy_r` — `resolve_metric`'s roster; the doc tier's 17-name vocabulary is wider by design and stays untouched); every gate predicate metric is per-member-resolvable (the 14 `RMetrics`+`RunMetrics` scalars; the three selection-annotation names refuse); walk_forward `select` is not `plateau:*` (a plateau needs the grid lattice, which a gated survivor subset no longer has); sweep `deflate: true` composes only with `argmax`. 2. **Cells**: for each strategy (doc order) × instrument (doc order) × window (doc order): run the pipeline. 3. **Sweep stage**: grid = odometer over the cell's axes (BTreeMap order, last axis fastest — `GridSpace` order discipline); members run via engine `sweep` over a `ListSpace` of the enumerated points, `run_one` → `runner.run_member(cell, zip-named point, cell.window_ms)`; `append_family(name, FamilyKind::Sweep, member reports)`; selection = `optimize` (argmax) / `optimize_plateau` (lattice = axis lens) / `optimize_deflated(family, metric, 1000, 5, campaign.seed)` when `deflate: true`. Winner + `FamilySelection` go into the realization record (stage-level annotation; family member records stay plain — decision 3: `select` names the *recorded selection*, the population flows on). 4. **Gate stage**: population = the surviving member set (initially all sweep members); a member survives iff **all** predicates hold on its report; `member_metric(report, name) -> Option` resolves the 14 per-member scalars; an R-metric predicate against a report with `metrics.r == None` fails (conservative, deterministic). Empty survivor set → record the stage with `survivor_ordinals: []`, stop this cell's pipeline, continue to the next cell (decision 8). 5. **Walk-forward stage**: `WindowRoller::new(cell.window_ms, in_sample_ms, out_of_sample_ms, step_ms, mode)` — the library works entirely in ms (`Timestamp` is unit-agnostic `i64`; the driver owns ms→ns at its source seam). Per window (engine `walk_forward`, parallel windows): IS family = engine `sweep` over a `ListSpace` of the survivor points restricted to `w.is` → winner by the stage's metric/select (argmax → `optimize_deflated` with the campaign seed — the shipped `select_winner` convention) → OOS = `runner.run_member(cell, winner params, w.oos)` with `manifest.selection = Some(selection)` stamped on the OOS report (the shipped per-window convention; these are new records, not mutations). `WindowRun { chosen_params, oos_equity: vec![], oos_report }` (empty stitching segment — the reduce-mode precedent). `append_family(name, FamilyKind::WalkForward, per-OOS-window reports)`. 6. **Record**: `append_campaign_run(&record)`. Family naming convention (deterministic, self-describing): `"{campaign_id[..8]}-{strategy_ordinal}-{instrument}-w{window_ordinal}-s{stage_ordinal}"`; the family id is the registry-derived `"{name}-{run}"`, so re-running the same campaign appends run 1, 2, … under the same names (decision 9: v1 re-runs whole; C1 makes the re-run bit-identical). ### aura-registry: the realization record ```rust /// Campaign realization: a THIN linking record over untouched family records /// (#198 decision 4). One JSONL line per campaign run in campaign_runs.jsonl, /// sibling of runs.jsonl/families.jsonl — the registry's growth pattern. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CampaignRunRecord { pub campaign: String, // campaign doc content id pub process: String, // process doc content id pub run: usize, // per-campaign counter (next_run pattern) pub seed: u64, pub cells: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CellRealization { pub strategy: String, // blueprint content id pub instrument: String, pub window_ms: (i64, i64), pub stages: Vec,// realized prefix (stops after an empty gate) } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StageRealization { pub block: String, // "std::sweep" | "std::gate" | "std::walk_forward" #[serde(default, skip_serializing_if = "Option::is_none")] pub family_id: Option, // sweep + walk_forward stages #[serde(default, skip_serializing_if = "Option::is_none")] pub survivor_ordinals: Option>, // gate stages; ordinals index the // nearest preceding family_id-bearing stage's family #[serde(default, skip_serializing_if = "Option::is_none")] pub selection: Option, // sweep stages (wf selections live // in the wf family members' manifests) } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StageSelection { pub winner_ordinal: usize, pub params: Vec<(String, Scalar)>, pub selection: FamilySelection, } impl Registry { /// Assigns record.run (max stored run for this campaign id + 1, else 0), /// appends one line to campaign_runs.jsonl, returns the assigned run. pub fn append_campaign_run(&self, record: &CampaignRunRecord) -> Result /// Missing file => Ok(vec![]) — the store convention. pub fn load_campaign_runs(&self) -> Result, RegistryError> } ``` ### aura-cli: the verb + the driver ```rust // research_docs.rs — fourth CampaignSub variant: pub enum CampaignSub { Validate {…}, Introspect(…), Register {…}, Run { target: String } } ``` `campaign run ` flow: target is a file iff `Path::is_file` (else a content id — bare 64-hex; anything else is a runtime refusal naming both readings). File path: parse + intrinsic-validate + `put_campaign` (register-then-run — decision 1: a file is sugar that resolves to the id). Requires a loaded project (`env.provenance().is_some()`) — refusal otherwise (`campaign run needs a project: strategies resolve against the project store and vocabulary`). Referential tier must return **zero** faults (the shipped `validate_campaign_refs` + `ref_fault_prose` seam). Process fetched via `get_process` + parsed; strategies resolved: `ContentId` → `get_blueprint`; `IdentityId` → the shipped identity scan. Then `aura_campaign::execute` with the CLI's `MemberRunner`: - **Bind**: reload `blueprint_from_json(cell.blueprint_json, env.resolve)` (Composite is !Clone — the shipped reload-per-member pattern), wrap via the shipped reduce-mode convention (`wrap_r(sig, …, false, true, None)`), resolve each raw axis name against the wrapped `param_space()` by **unique suffix-join** (wrapped name == raw name or ends with `"." + raw name` — the established suffix-match pattern; zero or >1 matches → `MemberFault::Bind` with precise prose), bind values, bootstrap, run. - **Data**: real sources via the shipped windowed path (`M1FieldSource::open_window`, ms→ns at that seam; pip via the shipped geometry refusal). A sub-window with no overlapping bars → `MemberFault::NoData` (refusal, exit 1 — a declared window without data is a campaign error). - Member manifests: `seed = 0` (seed-free real-data runs — the shipped convention), `instrument = Some(symbol)`, `topology_hash = Some(strategy id)`, `project = env.provenance()`. Stdout/stderr: JSON lines on stdout (emit-gated family/selection lines + the always-on final `campaign_run` line), prose on stderr (`aura: …`, exit 1 on refusals, exit 2 usage — the house convention). ### aura-cli: the #196 on-ramp - `aura graph register ` — parse via `blueprint_from_json` (project vocabulary), canonicalize (`blueprint_to_json`), id = `content_id_of` (== the topology hash the family verbs already store), `put_blueprint(id, canonical)`, print `registered blueprint content:{id} ({path})` via a public `Registry::blueprint_path` accessor (the `process_path` pattern). - `aura graph introspect --content-id ` accepts a blueprint document too: shape-discriminated (a blueprint envelope vs an op-list document), each canonicalized by its own rules; a blueprint file's printed id is its store key. - `aura graph introspect --params ` — resolve the blueprint (file, or content id from the store), print the **raw** `param_space()` one line per param as `{name}:{kind:?}` — exactly the namespace campaign axes are validated against (`validate_campaign_refs` checks the raw space; the wrapped `--list-axes` namespace on `aura sweep` is the sweep-verb view, not the campaign view). ## Components | Component | Crate | New/changed | |---|---|---| | `WalkForward` stage fields + `WfMode` + `ZeroWalkForwardLength` fault + schema tables | aura-research | changed (schema correction, #198 d2) | | `ListSpace` | aura-engine | new | | `CampaignRunRecord`/`CellRealization`/`StageRealization`/`StageSelection`, `append_campaign_run`/`load_campaign_runs`, `blueprint_path` | aura-registry | new | | `CellSpec`, `MemberRunner`, `MemberFault`, `ExecFault`, `member_metric`, `execute`, `CampaignOutcome` | aura-campaign | new crate | | `CampaignSub::Run` + driver impl + prose seam extensions | aura-cli | new | | `graph register` / `--content-id` blueprint mode / `--params` | aura-cli | new (#196) | `member_metric`'s 14-name roster is a third metric-roster site (beside aura-research's 17-name vocabulary and aura-registry's 7-name rankable set) — deliberate in v1, referenced to #190 (metric-roster single-sourcing, filed); drift fails safe (an unknown name is a preflight refusal, never a wrong number). ## Data flow ``` campaign.json ──file-sugar──▶ put_campaign ──▶ content id content id ──get_campaign──▶ CampaignDoc ──referential tier (0 faults)──▶ get_process ──▶ ProcessDoc ──preflight──▶ for strategy × instrument × window (doc order): ── the cell loop sweep: axes ──odometer──▶ ListSpace ──engine sweep──▶ runner.run_member×N ──▶ append_family(Sweep) ──▶ optimize[_deflated|_plateau] ──▶ StageSelection gate: survivors ──all-predicates over member_metric──▶ ordinals (empty ⇒ cell stops) wf: WindowRoller(ms) ──walk_forward──▶ per window: IS sweep of the survivor points over w.is ──▶ winner ──▶ runner.run_member(w.oos) ──▶ append_family(WalkForward) ──▶ CampaignRunRecord ──append_campaign_run──▶ campaign_runs.jsonl stdout: emit-gated family/selection lines + final campaign_run line; exit 0 ``` ## Error handling - **Usage (exit 2):** clap parse errors; nothing new. - **Refusals (exit 1, prose on stderr):** no project; target neither file nor content id; unknown campaign/process id (`get_*` → `Ok(None)`); intrinsic or referential faults (existing prose seams); every `ExecFault` (new `exec_fault_prose` beside `doc_fault_prose`/`ref_fault_prose`, Debug-leak-free, path-addressed: stage index + block id); `MemberFault::NoData` names instrument + window. - **Exit 0:** completed realization — including zero-survivor cells (decision 8) and an empty `emit` list (quiet stdout except the final record line). - All faults fire **before** any member runs where statically checkable (preflight), so a refused campaign burns no compute. ## Testing strategy 1. **aura-research (schema correction):** updated golden canonical pin + new content id; parse/refusal tests for the new fields (`mode` vocabulary, missing slots); `ZeroWalkForwardLength` faults; introspection slot lines. 2. **aura-engine:** `ListSpace` construction refusals (arity/kind), point order preserved, `sweep` over a `ListSpace` runs exactly the given points (report-tagged fake runner closure). 3. **aura-registry:** `append_campaign_run` run-counter semantics + `load_campaign_runs` missing-file → empty; record round-trip. 4. **aura-campaign (the semantics, hermetic):** a fake `MemberRunner` returning synthesized deterministic reports drives: pipeline-shape and v1 refusals; rankable-metric and gate-metric preflights; gate filtering (all predicates, `metrics.r == None` fails an R predicate); zero-survivor early termination (record prefix, next cell still runs); walk-forward windows × survivors (roller math in ms, IS restricted to survivors, OOS winner stamped); deflation seeded from the doc seed (same doc ⇒ identical realization, twice); family naming; realization assembly. 5. **aura-cli seam tests** (`run_code_in`/`temp_cwd` pattern): `campaign run` refusals outside a project; file-vs-id addressing (bogus target prose); v1-boundary prose on an mc-bearing process; persist_taps stderr line; `graph register`/`--params`/`--content-id` blueprint mode against the demo fixture project. 6. **Gated real-data e2e** (the `real_bars.rs` skip-pattern): where the local data directory exists, a full sweep→gate→walk_forward campaign over a real symbol runs, asserts exit 0, a parseable final `campaign_run` line, families present in the registry; skips with a note elsewhere so `cargo test --workspace` stays green anywhere. ## Acceptance criteria 1. The worked program above runs end-to-end inside a built project with local data: `aura campaign run campaign.json` exits 0, appends one `CampaignRunRecord` linking ≥1 sweep family id and (survivors permitting) one walk-forward family id, and prints the emit-gated lines plus the final record line. The F7 trail no longer ends at `runs/campaigns/.json`. 2. `aura campaign run ` over the registered id produces the same realization (file addressing is sugar; id is canonical — decision 1). 3. A campaign whose process gates everything out exits 0 with the truncated realization recorded (decision 8). 4. A process using `std::monte_carlo`/`std::generalize`, a non-rankable selection metric, an annotation-metric gate, or `plateau` in walk_forward refuses with precise prose before any member runs. 5. The corrected `std::walk_forward` vocabulary parses/validates/introspects with machinery-true fields; `folds` is gone; zero-length fields are path-addressed intrinsic faults. 6. `aura graph register` + `--params` + blueprint-file `--content-id` close the F5 authoring gap: a campaign's strategy ref + axes are authorable from the public surface without running a sweep first (#196). 7. Suite green (`cargo test --workspace`), clippy clean, doc build clean — the standing gates.