diff --git a/docs/specs/0107-campaign-executor.md b/docs/specs/0107-campaign-executor.md new file mode 100644 index 0000000..061c580 --- /dev/null +++ b/docs/specs/0107-campaign-executor.md @@ -0,0 +1,513 @@ +# 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.