audit: cycle 0107 tidy (drift resolved by ledger/glossary lift)

Architect verdict: drift_found — the code holds its contracts (C1
determinism incl. lowest-index fault attribution and doc-seeded
deflation; C16 seam: aura-campaign excludes ingest/std/composites,
MemberRunner isolates the condemned CLI scaffolding; the walk_forward
lockstep set moved together), the docs lagged the tree. All six drift
items resolved in this commit:

- C18: 'no executor exists' retired (0106 text now points forward);
  new cycle-0107 realization records the executor, the aura-campaign
  home, ListSpace, the campaign_runs.jsonl sibling store, zero-survivor
  exit-0 semantics, emit/persist_taps split, the #196 on-ramp, and the
  machinery-true std::walk_forward correction.
- C16: aura-campaign added to the non-node crate enumeration
  (cycle-0107 realization; explicitly not C21's World).
- Glossary: campaign document entry corrected (run verb, execution
  prose); new 'campaign run' entry; blueprint entry gains the on-ramp
  verbs.

Carry-on debt (self-noted in code, tracked): metric-roster
triplication #190, deflation-constant duplication #199; cross-cell
fault orphans + nested-parallelism oversubscription noted in the
tasks-6-7 commit body. Follow-ups filed on the milestone: #200
(mc/generalize stage execution), #201 (persist_taps wiring).

Regression: cargo test --workspace 987/0; clippy -D warnings clean;
cargo doc 0 warnings. Spec + plan removed at close per convention
(durable rationale lives in the ledger; the canonical record is the
git history).

closes #198
This commit is contained in:
2026-07-03 22:07:44 +02:00
parent 367f8678f1
commit 428f83c542
4 changed files with 52 additions and 5779 deletions
+46 -3
View File
@@ -1219,6 +1219,15 @@ one-implementor abstraction. **Deferred to the World / C21 layer (explicitly *no
making `RunReport` generic over a metric type and turning the orchestration/registry layer
into a reusable domain-agnostic substrate — until then `RunReport` / `RunManifest` /
`summarize` stay trace-coupled in the engine.
**Realization (cycle 0107, #198 — the campaign-execution leaf).** The non-node engine
workspace gains **`aura-campaign`**: campaign-execution *semantics* (cell enumeration,
preflight, gate evaluation, winner selection, walk-forward rolling, realization assembly —
see C18) as a leaf library crate whose deps deliberately exclude `aura-ingest` /
`aura-std` / `aura-composites`; harness construction and data binding enter only through
the one-method `MemberRunner` seam, so consumers (the `aura` CLI today; the playground and
tests tomorrow) bind their own runners while the semantics live here once. It is
explicitly NOT C21's World (a project-side program): it realizes one campaign document
and owns no topology, no data sources, no UI.
### C17 — Authoring surface
**Guarantee.** All *logic* — nodes, strategies, **and experiments/harnesses**
@@ -1382,9 +1391,43 @@ canonical bytes; gets are `Ok(None)` treat-as-empty. The **referential tier**
(`validate_campaign_refs`) resolves process/strategy refs against the stores (identity refs
by store scan) and checks each campaign axis — name AND declared `ScalarKind`, the axis
carries its kind once — against the referenced blueprint's `param_space`. Campaign P1
control constructs (axes/gates/ladders per #188) carry intent only: **no executor exists**;
executor need is re-tested against the intent-persistence diagnosis before one is scoped
(#189 triage, decided item 6).
control constructs (axes/gates/ladders per #188) carried intent only in this cycle: no
executor existed yet — executor need was re-tested against the intent-persistence
diagnosis (#189 triage, decided item 6; the cycle-0106 fieldtest F7 verdict was that
evidence), and the v1 executor shipped the next cycle (below).
**Realization (cycle 0107, #198/#196 — the campaign executor and its realization store).**
`aura campaign run <file|content-id>` 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
*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
`GridSpace`/`RandomSpace` — a gate's survivor subset has no cartesian structure),
per-member gates via the 14-name `member_metric` roster (an R-predicate over a missing R
block fails conservatively), walk-forward re-rolled in the doc's epoch-ms unit
(`WindowRoller`; IS windows search ONLY the survivor points; OOS winner reports carry
`manifest.selection`), deflation nulls seeded from the doc's `seed` (C1: realization is a
pure function of doc + stores + data). Harness/data binding stays consumer-side behind the
one-method **`MemberRunner`** seam — the CLI binds the shipped loaded-blueprint reduce
convention with unique suffix-join of raw axis names onto the wrapped `param_space`. The
registry grew the **`campaign_runs.jsonl`** JSONL sibling (beside `runs.jsonl` /
`families.jsonl`): one thin `CampaignRunRecord` per run — campaign/process ids, seed, and
per-cell realized stage prefixes linking family ids, gate survivor ordinals, and sweep
selections — over untouched family records, run-counted per campaign id. Zero survivors
truncate a cell's realized prefix and exit 0 (a null result is a valid research result);
`emit` is honored (`family_table`/`selection_report` lines), `persist_taps` is deferred
LOUDLY on stderr (no name→tap wiring exists yet). The **blueprint on-ramp** (#196) closes
the F5 authoring gap: `aura graph register` (store put keyed by content id == topology
hash), `aura graph introspect --params` (the RAW `param_space` namespace campaign axes
are validated against), and a blueprint-file mode on `--content-id`. The
`std::walk_forward` vocabulary was corrected to machinery-true fields
(`in_sample_ms`/`out_of_sample_ms`/`step_ms`/`mode``WindowRoller`'s three lengths and
both `RollMode`s; the shipped `folds` slot mapped to nothing the machinery accepts), with
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).
**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
+6 -2
View File
@@ -29,7 +29,7 @@ A strategy's primary, backtestable DAG output: one signed, bounded `f64 ∈ [-1,
### blueprint
**Avoid:** —
The param-generic, input-role-generic graph-as-data produced by running a Rust builder; it carries free numeric params and free input roles before bootstrap. Bootstrapped into a frozen instance by binding params + data + seed.
The param-generic, input-role-generic graph-as-data produced by running a Rust builder; it carries free numeric params and free input roles before bootstrap. Bootstrapped into a frozen instance by binding params + data + seed. Registered and inspected headless (`aura graph register`, `aura graph introspect --params` — the raw `param_space` namespace campaign axes bind against; cycle 0107/#196).
### bootstrap
**Avoid:** —
@@ -45,7 +45,11 @@ 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 and validated headless (`aura campaign validate|introspect|register`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent execution semantics are a separate, later concern.
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.
### campaign run
**Avoid:** campaign execution record, realized campaign
One execution of a `campaign document` (`aura campaign run <file|content id>`; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids and gate survivor ordinals over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation nulls seed from the doc's `seed`.
### cdylib
**Avoid:** —
File diff suppressed because it is too large Load Diff
-513
View File
@@ -1,513 +0,0 @@
# 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 <file|content-id>` 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": "<blueprint-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": "<process-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<ParamSpec>, points: Vec<Vec<Scalar>> }
impl ListSpace {
pub fn new(space: &[ParamSpec], points: Vec<Vec<Scalar>>) -> Result<Self, SweepError>
// 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<Vec<Cell>> // 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<String, Axis>, // 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<RunReport, MemberFault>;
}
/// 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<CellOutcome>, // per-cell stage payloads for emit rendering
}
pub struct CellOutcome { pub families: Vec<StageFamily>, pub selections: Vec<StageSelectionOut> }
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<CampaignOutcome, ExecFault>
```
`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<f64>` 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<CellRealization>,
}
#[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<StageRealization>,// 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<String>, // sweep + walk_forward stages
#[serde(default, skip_serializing_if = "Option::is_none")]
pub survivor_ordinals: Option<Vec<usize>>, // gate stages; ordinals index the
// nearest preceding family_id-bearing stage's family
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selection: Option<StageSelection>, // 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<usize, RegistryError>
/// Missing file => Ok(vec![]) — the store convention.
pub fn load_campaign_runs(&self) -> Result<Vec<CampaignRunRecord>, 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 <target>` 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 <blueprint.json>` — 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 <FILE>` 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 <FILE|ID>` — 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/<hash>.json`.
2. `aura campaign run <content-id>` 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.