71405d4d86
Verdict: the 0106 F7 wire is closed — campaign+process documents drive aura campaign run through sweep -> gate -> walk_forward over the real GER40 archive, realization recorded as a thin family-linking CampaignRunRecord, bit-identical on re-run and across file/id addressing (C1). 0106 F1/F5 on-ramp gaps confirmed repaired; the honest-refusal surface (v1 boundary, empty gate exit 0 + truncation, plateau/deflate composition, stale wf vocabulary, bad targets, persist_taps loud deferral) exercised and clean. 0 bugs. Findings triaged: F5 -> #202 (op-script vs envelope shape on the on-ramp verbs), F6 -> #194 comment (content: prefix trap re-confirmed, now spans graph register), F7 -> #203 (open-param coverage at validate; raw-vs-wrapped path prose), F8 -> RED-first fix follows this commit (SpanTooShort Debug leak), F9 -> #197 comment (rankable/per-member roster split undiscoverable), F10 -> ratified store-before-gate on #198 (derived: consistent with campaign register's intrinsic-only gate; content-addressed store is an idempotent intent archive). Orthogonal: #204 (aura new nested-.git gitlink hazard). refs #198
216 lines
13 KiB
Markdown
216 lines
13 KiB
Markdown
# Fieldtest — cycle 0107 — campaign executor + blueprint on-ramp — 2026-07-03
|
||
|
||
**Role:** downstream consumer (methodology / campaign designer), public interface
|
||
only (ledger C16/C18/C20/C24/C25, glossary, CLI `--help` + introspection verbs,
|
||
the 0106 fieldtest corpus as shape hints). No `crates/` source read. Binary
|
||
built from HEAD (`cargo build --workspace`, `target/debug/aura`, commit
|
||
`428f83c`). Real data: scaffolded `demo_project/` (`aura new`), `cargo build`
|
||
for the dylib, ran the executor over the local GER40 2024-09 Pepperstone
|
||
archive.
|
||
|
||
## Fixtures (this directory)
|
||
|
||
- `c0107_1_smacross.json` — SMA-cross op-script, lengths **bound** (content id `573262…`)
|
||
- `c0107_1_smacross_open.json` / `_open_built.json` — lengths **unbound** (sweepable), op-script + its `graph build` envelope (content id `597d71…`)
|
||
- `c0107_2_process.json` — sweep(sqn,argmax,deflate) → gate(expectancy_r>0) → walk_forward(rolling, ms lengths) (content id `5a88…`)
|
||
- `c0107_2_campaign.json` — campaign missing a `bias.scale` axis (validates, then fails run)
|
||
- `c0107_2_campaign_full.json` — the runnable campaign (all 3 open params covered), GER40, 2024-09
|
||
- `c0107_2_realization_campaign_runs.jsonl` — the recorded realization (3 identical runs + 1 gate-all)
|
||
- `c0107_3_process_{mc,gateall,badmetric}.json` + `c0107_3_campaign_*.json` — refusal probes
|
||
- `c0107_4_process_{plateau_wf,deflate_plateau,wf_toobig}.json` + `c0107_4_campaign_*.json` + `c0107_4_campaign_prefixref.json` — trip-up probes
|
||
- `demo_project/` — `aura new` scaffold + all docs (its `runs/` store is project-gitignored; the realization is copied out to the corpus root as evidence)
|
||
|
||
## Findings
|
||
|
||
### F1 [working] The full loop closes — 0106 F7 is answered
|
||
`aura campaign run c0107_2_campaign_full.json` (file = register-then-run) and
|
||
`aura campaign run 533aed18…` (id) both execute the cell
|
||
`GER40 × [2024-09-01,10-01]` through `std::sweep → std::gate →
|
||
std::walk_forward` over real data (exit 0). The gate `expectancy_r > 0` kept
|
||
survivors `[0,1,2]` (dropped the losing `fast=4/slow=16` member,
|
||
`expectancy_r=-0.022`); the sweep argmax winner was ordinal 0 (`sqn=1.054`);
|
||
walk-forward rolled 2 OOS folds re-sweeping **only** the survivor points and
|
||
stamped `manifest.selection` on the OOS winners. Emit honored: `family_table` +
|
||
`selection_report` lines, always the `campaign_run` record. The stored
|
||
`CampaignRunRecord` (`runs/campaign_runs.jsonl`) links
|
||
`{campaign, process, run, seed, cells[{stages: family_id / survivor_ordinals /
|
||
selection}]}` over untouched `families.jsonl` records, run-counted per campaign
|
||
id (3 successful runs → run 0/1/2). Intent → execution → recorded realization
|
||
is one continuous, reproducible path. → carry-on.
|
||
|
||
### F2 [working] Determinism is bit-identical across re-run and addressing mode
|
||
Two file runs and one id-addressed run of the same campaign are
|
||
**byte-identical** after normalizing only the per-campaign run counter in
|
||
family ids (`diff` clean on both pairs). Deflation nulls seed from the doc
|
||
`seed=42` (`overfit_probability`, `deflated_score` reproduce). Realization is a
|
||
pure function of doc + stores + data, as C1 promises. → carry-on.
|
||
|
||
### F3 [working] 0106 F1 closed — the document envelope is now introspectable
|
||
`aura campaign introspect --unwired <bare {}>` enumerates the whole envelope,
|
||
including the previously-invisible `seed`:
|
||
```
|
||
open slot: format_version (required, must be 1)
|
||
open slot: kind (required, must be "campaign")
|
||
open slot: name / data / strategies / process.ref / seed / presentation …
|
||
```
|
||
`process introspect --unwired {}` likewise names
|
||
`format_version/kind/name/pipeline`. The field names (`data`, `strategies`,
|
||
`process.ref`, `presentation`) that 0106 F1 could not map are now spelled out.
|
||
→ carry-on.
|
||
|
||
### F4 [working] The honest-refusal surface is loud, precise, and correctly disposed
|
||
All probed refusals were clean, actionable prose with the right exit code:
|
||
- v1 boundary: `process stage 1 (std::monte_carlo) is not executable in v1;
|
||
executable pipeline shape: std::sweep [std::gate]* [std::walk_forward]`
|
||
(exit 1). `std::generalize` is symmetric.
|
||
- gate-everything-out (`expectancy_r > 100`): exit **0**, `family_table` +
|
||
sweep `selection_report` still emitted, walk-forward **truncated**, loud
|
||
stderr `cell …: gate at stage 1 left no survivors; cell realization
|
||
truncated`.
|
||
- `plateau:mean` inside walk_forward: `walk_forward cannot use a plateau select
|
||
(a gated survivor subset has no parameter lattice)` (exit 1).
|
||
- `deflate:true` + non-argmax: `sweep deflate: true composes only with select
|
||
"argmax"` (exit 1).
|
||
- old 0106 wf vocab (`folds`): `block std::walk_forward: unknown slot "folds"`
|
||
(exit 1) — the 0107 machinery-true vocabulary correction is enforced.
|
||
- bogus file / unknown 64-hex id / uppercase-hex / outside-project: each exit 1
|
||
with a precise message (`neither a readable .json file nor a 64-hex content
|
||
id` / `no campaign <id> in the project store` / `campaign run needs a
|
||
project: … no Aura.toml found up from <cwd>`).
|
||
- `persist_taps` deferred **LOUDLY** on stderr before execution
|
||
(`persist_taps not yet honored (N tap(s) ignored)`), the 0106 F7 lesson
|
||
honored.
|
||
→ carry-on.
|
||
|
||
### F5 [friction] The on-ramp rejects the op-script; the `graph build` bridge is undocumented
|
||
The corpus's canonical strategy form is an **op-script** (`c0106_2_strategy.json`,
|
||
`c0107_1_*`). `aura graph introspect --content-id FILE` accepts it
|
||
(shape-discriminated) — but `aura graph register FILE` and `aura graph
|
||
introspect --params FILE` **reject** it with a raw serde message:
|
||
```
|
||
aura: blueprint document is not valid JSON: invalid type: map, expected u32 at line 2 column 2
|
||
```
|
||
Only the envelope emitted by `aura graph build < op-script > envelope.json` is
|
||
accepted. Nothing in `--help`, the glossary `blueprint` entry, or the error
|
||
names this `graph build` step, and the three on-ramp verbs disagree on which
|
||
shapes they accept. (Once bridged, the content id is shape-invariant —
|
||
op-script and envelope both hash to `573262…`, so `register` and a ref computed
|
||
from the op-script agree; this part is correct.)
|
||
Repro: `aura graph register c0107_1_smacross_open.json`.
|
||
→ plan: shape-discriminate the op-list in `register`/`--params` (as
|
||
`--content-id` already does), or make the error name the `graph build` bridge.
|
||
|
||
### F6 [friction] The `content:` prefix trap persists and the on-ramp emits the wrong-to-paste form
|
||
`graph|process|campaign register` and `introspect --content-id` all print
|
||
`content:<hash>`, but every `ref` wants the **bare** hash. A campaign whose
|
||
`process.ref` carries the copied `content:5a88…` form **passes intrinsic
|
||
validation** and then fails referential:
|
||
```
|
||
aura: campaign references do not resolve:
|
||
process content:5a88…6ea not found in the project store
|
||
```
|
||
The error now echoes the offending prefixed ref (an improvement over 0106 F3)
|
||
but never says "strip the `content:` prefix", and `register` still prints
|
||
exactly the form that breaks. This directly taxes the on-ramp the cycle set out
|
||
to smooth.
|
||
Repro: `aura campaign validate c0107_4_campaign_prefixref.json`.
|
||
→ plan: one bare display form across verbs, or accept/strip the `content:`
|
||
prefix in refs (carried from 0106 F3, still open: #194).
|
||
|
||
### F7 [friction] `campaign validate` passes but `campaign run` aborts when an open param is uncovered
|
||
Referential validation reports `all references resolve, axes are in the param
|
||
space` (exit 0), but it only checks that each axis **name** is in `param_space`
|
||
— not that **every** open param is covered by an axis. The open blueprint has
|
||
three open params (`fast.length`, `slow.length`, `bias.scale`); a campaign
|
||
sweeping only the two lengths **validates**, then aborts at member-bind:
|
||
```
|
||
aura: a member failed to bind: open param "graph.bias.scale" of strategy 597d71… is bound by no campaign axis
|
||
```
|
||
The executor's "every open knob required, no default" rule (documented for
|
||
`sweep`/`walkforward` in the glossary) is not mirrored in the validator, so
|
||
"valid" ≠ "runnable". (Secondary: the bind error uses the wrapped
|
||
`graph.bias.scale` path while `--params` and the axis use the raw `bias.scale`
|
||
— two namespaces for one knob.)
|
||
Repro: `aura campaign validate c0107_2_campaign.json` (passes) then
|
||
`aura campaign run c0107_2_campaign.json` (aborts).
|
||
→ plan: extend the referential tier to require full open-param coverage,
|
||
surfacing the missing knob at validate time; reconcile the raw-vs-wrapped param
|
||
path in the bind message.
|
||
|
||
### F8 [friction] The walk-forward window-fit refusal leaks a raw Rust `Debug` struct
|
||
A walk_forward whose `in_sample_ms` exceeds the campaign window refuses
|
||
correctly, but in internal `{:?}` form, unlike its clean sibling refusals:
|
||
```
|
||
aura: process stage 1: walk_forward windows do not fit the campaign window:
|
||
SpanTooShort { span: (Timestamp(1725148800000), Timestamp(1727740800000)), need: 315964799999 }
|
||
```
|
||
The `SpanTooShort {…}` struct, the `Timestamp()` wrapper, and the
|
||
unit-unlabeled `need` (ms) break the "Debug-free house seam" the cycle
|
||
otherwise maintains, and a user cannot easily see that `need` is
|
||
`in_sample_ms + out_of_sample_ms` in ms against the window span.
|
||
Repro: `aura campaign run c0107_4_campaign_wf_toobig.json`.
|
||
→ debug (RED-pin the prose form).
|
||
|
||
### F9 [doc-gap] The rankable / gate metric roster is enumerable by no verb, nor by the refusal
|
||
Block hints say `metric name (see metric_vocabulary)`, but no verb lists it
|
||
(`--vocabulary` is the block list; `--block metric_vocabulary` → `unknown
|
||
block`). `process validate` accepts **any** string (`profit_factor` passes
|
||
intrinsic), and the run-time refusal names no roster:
|
||
```
|
||
aura: process stage 0: metric "profit_factor" is not rankable (winner selection needs one of the registry's rankable metrics)
|
||
```
|
||
`profit_factor` is itself emitted in every member's metrics block, so the
|
||
exists-but-not-rankable split is invisible. A campaign author must guess the
|
||
sweep/gate/wf metric names (0106 F6, still open: #197).
|
||
→ tighten docs: expose the rankable + gate metric rosters via `process
|
||
introspect` (or enumerate them in the refusal).
|
||
|
||
### F10 [spec_gap] Register-then-run stores the campaign before the gate — unrunnable docs accumulate
|
||
Every `campaign run <file>` issued left a `runs/campaigns/<id>.json` (9
|
||
stored), including campaigns that then failed the referential gate
|
||
(`prefixref`) or preflight (`mc`/`badmetric`/`plateauwf`/`deflateplateau`/
|
||
`wf_toobig`). The store thus accumulates never-runnable and
|
||
referentially-broken documents, with no cleanup. The ledger is silent on
|
||
whether the file sugar should gate before storing, and whether the
|
||
content-addressed store should hold referentially-broken docs.
|
||
→ ratify (accept intrinsic-keyed store as-is) or tighten (gate before storing).
|
||
|
||
## Verdict
|
||
|
||
**Yes — the campaign+process pair now delivers the full loop the 0106 verdict
|
||
said was one wire short.** F7 (0106) is closed end to end: a campaign document
|
||
+ its referenced process document drive `aura campaign run` through
|
||
`sweep → gate → walk_forward` over the real GER40 archive, and the run's
|
||
realization is recorded as a thin, family-linking `CampaignRunRecord` that is
|
||
content-addressed, run-counted, and **bit-identical on re-run** — persisted
|
||
intent finally has a consumer, and every id, axis, gate, and selection earns
|
||
its reason to exist. The authoring on-ramp that 0106 flagged is substantially
|
||
repaired too: `graph register` + `introspect --params` (file and store-id) +
|
||
`--content-id` file mode give a blueprint ref a resolvable path (F5-0106), and
|
||
`--unwired {}` makes the whole envelope, `seed` included, derivable (F1-0106).
|
||
The honest-refusal surface is a genuine strength — v1 boundary, empty gate
|
||
(exit 0 + truncation), plateau/deflate composition, stale wf vocabulary, and
|
||
every bad target are refused loudly, precisely, and with the correct
|
||
disposition.
|
||
|
||
What remains is friction concentrated at the *first-author* edge, not the
|
||
loop: the on-ramp verbs disagree on op-script vs envelope shape and hide the
|
||
`graph build` bridge (F5); the `content:` prefix that every
|
||
`register`/`--content-id` prints is the exact form that silently breaks a ref
|
||
(F6); `campaign validate` blesses a campaign the executor then rejects for an
|
||
uncovered open param (F7); one preflight fault leaks a raw `Debug` struct
|
||
(F8); and the metric roster a process must name is still discoverable nowhere
|
||
(F9). None block the loop; all tax the newcomer. One spec silence to settle:
|
||
whether register-then-run should store before or after the gate (F10).
|
||
|
||
Counts: working 4 (F1–F4), friction 4 (F5–F8), doc-gap 1 (F9), spec-gap 1
|
||
(F10), bugs 0.
|
||
|
||
## Orthogonal concern (out of 0107 scope)
|
||
|
||
`aura new` (cycle 0103) scaffolds a **nested `.git` repo** inside the project
|
||
directory — a gitlink/submodule hazard when the scaffold is embedded in the
|
||
tracked `fieldtests/` corpus. `demo_project/.git` (and the large
|
||
`demo_project/target/`) were removed so the corpus commits cleanly. Worth a
|
||
scaffold-side note (`--no-git` or skip when nested under a repo).
|