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:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user