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:
2026-07-14 13:07:40 +02:00
parent a55e4cfd10
commit ea4e79d73f
9 changed files with 442 additions and 91 deletions
+46 -2
View File
@@ -71,6 +71,11 @@ pub enum StageBlock {
MonteCarlo { resamples: u32, block_len: u32 },
#[serde(rename = "std::generalize")]
Generalize { metric: String },
/// Enumerate-only leading stage (#256 fork B): yields the axis grid's
/// parameter points to the next stage; executes and persists nothing.
/// Fieldless — the axes live in the campaign document.
#[serde(rename = "std::grid")]
Grid,
}
fn is_false(b: &bool) -> bool {
@@ -224,6 +229,14 @@ pub const PROCESS_BLOCKS: &[BlockSchema] = &[
needs >= 2 instruments and an R-expectancy metric to execute",
slots: &[SlotInfo { name: "metric", kind: SlotKind::MetricName, required: true }],
},
BlockSchema {
id: "std::grid",
doc: "enumerate the axis grid as parameter points for the next stage \
(enumerate-only leading stage): executes and persists nothing; \
legal only as the first stage, immediately before \
std::walk_forward",
slots: &[],
},
];
// ---------------------------------------------------------------------------
@@ -374,6 +387,7 @@ fn stage_from_value(v: &serde_json::Value) -> Result<StageBlock, String> {
"std::generalize" => Ok(StageBlock::Generalize {
metric: require_str(m, "metric", &id)?,
}),
"std::grid" => Ok(StageBlock::Grid),
_ => unreachable!("schema membership checked above"),
}
}
@@ -858,6 +872,7 @@ pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault> {
}
}
StageBlock::MonteCarlo { .. } => {}
StageBlock::Grid => {}
}
if matches!(stage, StageBlock::MonteCarlo { .. } | StageBlock::Generalize { .. }) {
terminal_seen = true;
@@ -2105,16 +2120,45 @@ mod tests {
);
}
/// #256 fork B: the fieldless enumerate-only stage round-trips through
/// the schema-strict parser, and any slot on it is refused (its slot
/// list is empty, so the generic unknown-slot check covers every key).
#[test]
fn grid_block_round_trips_and_refuses_slots() {
let json = serde_json::to_string(&StageBlock::Grid).expect("serialize");
assert_eq!(json, r#"{"block":"std::grid"}"#);
let back: StageBlock = serde_json::from_str(&json).expect("parse back");
assert_eq!(back, StageBlock::Grid);
let err = serde_json::from_str::<StageBlock>(
r#"{"block":"std::grid","metric":"sqn"}"#,
)
.expect_err("a slot on the fieldless std::grid must be refused");
assert!(err.to_string().contains("unknown slot"), "got: {err}");
}
#[test]
fn vocabularies_enumerate_every_block_with_typed_slots() {
let ids: Vec<&str> = process_vocabulary().iter().map(|b| b.id).collect();
assert_eq!(
ids,
vec!["std::sweep", "std::gate", "std::walk_forward", "std::monte_carlo", "std::generalize"]
vec![
"std::sweep",
"std::gate",
"std::walk_forward",
"std::monte_carlo",
"std::generalize",
"std::grid"
]
);
for b in process_vocabulary().iter().chain(campaign_vocabulary()) {
assert!(!b.doc.is_empty(), "block {} has no doc line", b.id);
assert!(!b.slots.is_empty(), "block {} has no slots", b.id);
// std::grid is the one legitimate fieldless block (#256); every
// other block keeps its documented-slots guard.
assert!(
b.id == "std::grid" || !b.slots.is_empty(),
"block {} has no slots",
b.id
);
for s in b.slots {
assert!(!slot_kind_label(s.kind).is_empty());
}