feat(research,campaign,cli): std::grid — the enumerate-only leading stage
closes #256
Fork B (owner decision 2026-07-14): the dissolved walkforward/mc
translations' leading sweep executed the full grid over the whole campaign
window and persisted a Sweep family, yet only the enumerated parameter
points ever crossed the stage seam (the wf stage re-sweeps them per IS
window itself). The leading stage is now the fieldless vocabulary block
std::grid: it enumerates, executes nothing, persists nothing.
- aura-research: StageBlock::Grid ({"block":"std::grid"}), schema-strict
parse arm (empty slot list: every key but "block" is refused by the
generic unknown-slot check), PROCESS_BLOCKS entry, intrinsic-tier no-op
arm; the vocabulary test's non-empty-slots guard carries a pinned
std::grid-only exception (a nominal slot would misdescribe the
vocabulary to describe_block consumers).
- aura-campaign: the inter-stage seam is a typed two-armed StageFlow
(points-only vs executed members); gate / mc-per-survivor fence the
points-only arm with defensive PipelineShape faults; preflight admits
std::grid only as the first stage and only immediately before
std::walk_forward (every other neighbor consumes executed reports).
- aura-cli: translate_walkforward / translate_mc lead with
StageBlock::Grid; the two family-shape E2E pins flip to zero Sweep
families; translate_generalize keeps its executed sweep(argmax) — its
generalize stage consumes the argmax winner report as the cell nominee.
- docs: dated #256 amendment in the ledger's verb-dissolution narrative;
the authoring-guide vocabulary transcript gains the std::grid line.
Behaviour preservation: the exact-grade real-data pins
(walkforward_real_e2e_pins_the_exact_current_grade,
mc_r_bootstrap_real_e2e_pins_the_exact_current_grade) pass unmodified —
survivor points reach the wf stage in the same odometer order as before.
Measured (the #256 acceptance measurement; debug build, real GER40 2025,
2x2 grid, `aura walkforward --real`, 3 runs each):
before (a55e4cf): 6.27 / 6.18 / 6.18 s
after: 4.52 / 4.49 / 4.45 s (~ -27% wall clock)
Suite: cargo test --workspace green (0 failed); clippy clean.
Decision log: #256 comments (fork rationale incl. the StageFlow seam and
the slot-guard exception).
This commit is contained in:
@@ -3293,8 +3293,8 @@ fn walkforward_dissolved_refuses_name_and_trace_together() {
|
||||
/// generated campaign document (carrying the `--name` handle and the stop as a
|
||||
/// non-empty single risk regime), and one campaign-run record — mirroring the
|
||||
/// generalize dissolution's audit trail. The persisted family set is exactly one
|
||||
/// `Sweep` family (the pipeline's leading `std::sweep(argmax)` stage) plus one
|
||||
/// `WalkForward` family (the per-window OOS reports) — the observable proof that
|
||||
/// `WalkForward` family (the per-window OOS reports) and NO `Sweep` family —
|
||||
/// the leading `std::grid` stage only enumerates (#256) — the observable proof that
|
||||
/// `dispatch_walkforward`'s real-data path runs through the one campaign
|
||||
/// executor rather than the deleted inline roller. Gated on the shared GER40
|
||||
/// archive; skips cleanly on a data refusal.
|
||||
@@ -3363,8 +3363,8 @@ fn walkforward_dissolves_through_the_campaign_path() {
|
||||
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
|
||||
1,
|
||||
"one Sweep family from the pipeline's leading argmax stage: {fams_out}"
|
||||
0,
|
||||
"the enumerate-only std::grid leading stage persists no Sweep family: {fams_out}"
|
||||
);
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(),
|
||||
@@ -5312,9 +5312,10 @@ fn mc_dissolves_a_non_r_sma_blueprint() {
|
||||
/// path — a successful run durably auto-registers exactly one generated process
|
||||
/// document, one generated campaign document (carrying the constant "mc" name and the
|
||||
/// stop as a non-empty single risk regime), and one campaign-run record, and emits the
|
||||
/// single `mc_r_bootstrap` grade line. The pipeline's leading argmax sweep and its
|
||||
/// walk_forward stage each persist one family; the terminal monte_carlo stage is an
|
||||
/// annotator and adds NO family. This is the observable proof the inline path is gone
|
||||
/// single `mc_r_bootstrap` grade line. The pipeline's walk_forward stage persists
|
||||
/// the one family; the leading `std::grid` stage only enumerates (#256) and the
|
||||
/// terminal monte_carlo stage is an annotator — neither adds a family. This is
|
||||
/// the observable proof the inline path is gone
|
||||
/// and the dissolution runs through the executor. Gated on the GER40 2025 archive;
|
||||
/// skips cleanly on a data refusal.
|
||||
#[test]
|
||||
@@ -5391,8 +5392,8 @@ fn mc_dissolves_through_the_campaign_path() {
|
||||
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
|
||||
1,
|
||||
"one Sweep family from the pipeline's leading argmax stage: {fams_out}"
|
||||
0,
|
||||
"the enumerate-only std::grid leading stage persists no Sweep family: {fams_out}"
|
||||
);
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(),
|
||||
@@ -5416,9 +5417,9 @@ fn mc_dissolves_through_the_campaign_path() {
|
||||
/// data-carrying window far shorter than the roller", which a fixed recent window
|
||||
/// satisfies without the full-archive probe sweep a live-span derivation costs —
|
||||
/// the archive only grows forward, so the window never falls off its end. The
|
||||
/// leading sweep stage runs first over the short window; the refusal today fires at
|
||||
/// stage 1 (the walk_forward roller), so this bites only when the window genuinely has
|
||||
/// data. Gated on the GER40 archive; skips cleanly on a data-less host.
|
||||
/// leading std::grid stage only enumerates (nothing runs over the short window); the
|
||||
/// refusal fires at stage 1 (the walk_forward roller), so this bites only when the
|
||||
/// window genuinely has data. Gated on the GER40 archive; skips cleanly on a data-less host.
|
||||
#[test]
|
||||
fn mc_real_fits_the_injected_roller_to_a_short_window() {
|
||||
if !local_data_present() {
|
||||
|
||||
@@ -1498,6 +1498,137 @@ fn campaign_validate_accepts_a_terminal_selection_free_sweep() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A hand-authored (non-sugar) `[std::grid, std::walk_forward]` process — the
|
||||
/// #256 fork B shape the `walkforward`/`mc` verb sugar now generates
|
||||
/// internally, but authored here directly as an op-script the way a data-only
|
||||
/// project author would reach it.
|
||||
const GRID_THEN_WF_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "grid-then-walkforward",
|
||||
"pipeline": [
|
||||
{ "block": "std::grid" },
|
||||
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||||
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
/// The same leading `std::grid` stage, but with a `std::gate` interposed
|
||||
/// before `std::walk_forward` — the one placement `campaign run`'s preflight
|
||||
/// must refuse (#256 fork B: a `std::grid` first stage yields bare parameter
|
||||
/// points, not executed member reports, so it is legal ONLY immediately
|
||||
/// before `std::walk_forward`).
|
||||
const GRID_THEN_GATE_THEN_WF_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "grid-then-gate-then-walkforward",
|
||||
"pipeline": [
|
||||
{ "block": "std::grid" },
|
||||
{ "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 0.0 } ] },
|
||||
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||||
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
|
||||
]
|
||||
}"#;
|
||||
|
||||
/// Property (#256 fork B): `campaign run`'s data-free preflight, exercised
|
||||
/// through the real CLI binary over a hand-authored op-script (not the
|
||||
/// verb-sugar translation), refuses a `std::grid` first stage that is not
|
||||
/// immediately followed by `std::walk_forward` — here, a `std::gate`
|
||||
/// interposed between them — before any member runs (the [1, 2] 1970-epoch-ms
|
||||
/// window is never reached). A regression that let this placement through
|
||||
/// would hand `std::gate` bare parameter points instead of the executed
|
||||
/// member reports it retains on, silently corrupting the gate's survivor
|
||||
/// filter. The detail is Debug-free prose, not the `PipelineShape`/`ExecFault`
|
||||
/// identifier.
|
||||
#[test]
|
||||
fn campaign_run_refuses_a_grid_stage_not_immediately_before_walk_forward() {
|
||||
let (dir, _fixture) = fresh_project();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("gridgate.process.json")),
|
||||
ScratchPath::File(dir.join("gridgate.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-gridgate-seed");
|
||||
let proc_id = register_process_doc(
|
||||
&dir,
|
||||
"gridgate.process.json",
|
||||
GRID_THEN_GATE_THEN_WF_PROCESS_DOC,
|
||||
);
|
||||
write_doc(
|
||||
&dir,
|
||||
"gridgate.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridgate.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains(
|
||||
"a std::grid first stage must be immediately followed by std::walk_forward"
|
||||
),
|
||||
"the detail names the adjacency rule: {out}"
|
||||
);
|
||||
assert!(!out.contains("PipelineShape"), "Debug leak: {out}");
|
||||
}
|
||||
|
||||
/// Property (#256 fork B): a hand-authored `[std::grid, std::walk_forward]`
|
||||
/// op-script — not the verb-sugar translation, which already pins this shape
|
||||
/// in `cli_run.rs` — runs to completion over the synthetic SYMA archive, and
|
||||
/// the persisted `CampaignRunRecord`'s leading `std::grid` stage carries no
|
||||
/// `family_id`: the enumerate-only leading stage crosses the stage seam as
|
||||
/// bare points and never executes or persists a family of its own, while the
|
||||
/// following `std::walk_forward` stage does. (`aura runs families` is not
|
||||
/// used here: `seed_blueprint` itself runs a real `aura sweep` to register
|
||||
/// the blueprint, which stamps its own incidental Sweep family and would
|
||||
/// confound a registry-wide family count — the record's own stage array is
|
||||
/// the unconfounded source of truth.) A regression that made a hand-authored
|
||||
/// `std::grid` stage execute or persist a family would flip
|
||||
/// `stages[0]["family_id"]` from null here even though the sugar-generated
|
||||
/// shape (a distinct code path) stayed green.
|
||||
#[test]
|
||||
fn campaign_run_synthetic_e2e_grid_then_wf_persists_no_sweep_family() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("gridwf.process.json")),
|
||||
ScratchPath::File(dir.join("gridwf.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-gridwf-seed");
|
||||
let proc_id = register_process_doc(&dir, "gridwf.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||||
// SYMA's synthetic archive spans 2024-01..08; Mar-01..Jun-30 (~17 weeks)
|
||||
// gives the (14d, 7d, 7d) roller a comfortable tiling (the cost-e2e
|
||||
// siblings' window).
|
||||
write_doc(
|
||||
&dir,
|
||||
"gridwf.campaign.json",
|
||||
&campaign_doc_json_for(
|
||||
"SYMA",
|
||||
&bp_id,
|
||||
&proc_id,
|
||||
(1709251200000, 1719791999999),
|
||||
"",
|
||||
"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridwf.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
|
||||
let line = out
|
||||
.lines()
|
||||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||||
.expect("the always-on final campaign_run line");
|
||||
let v: serde_json::Value = serde_json::from_str(line).expect("record parses");
|
||||
let stages = &v["campaign_run"]["cells"][0]["stages"];
|
||||
assert_eq!(stages[0]["block"], "std::grid");
|
||||
assert!(stages[0]["family_id"].is_null(), "std::grid persists no family: {stages}");
|
||||
assert_eq!(stages[1]["block"], "std::walk_forward");
|
||||
assert!(!stages[1]["family_id"].is_null(), "std::walk_forward persists its family: {stages}");
|
||||
}
|
||||
|
||||
/// The v2 boundary, campaign side: a generalize-bearing process is an
|
||||
/// executable shape, but `std::generalize` needs >= 2 instruments in the
|
||||
/// campaign — a STATIC preflight fact (the referential gate has run, no data
|
||||
|
||||
Reference in New Issue
Block a user