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:
+46
-3
@@ -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
@@ -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
@@ -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.
|
||||
Reference in New Issue
Block a user