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:
@@ -232,6 +232,31 @@ pub fn execute(
|
||||
Ok(CampaignOutcome { record, run, cells: cells_out })
|
||||
}
|
||||
|
||||
/// The inter-stage population crossing `run_cell`'s seam: parameter points
|
||||
/// only (from `std::grid`) or executed members (from `std::sweep`). Preflight
|
||||
/// guarantees report-consuming stages (`std::gate`, `std::monte_carlo`'s
|
||||
/// per-survivor arm) never see `Points`; the defensive faults below keep that
|
||||
/// invariant loud if the shape rules ever drift.
|
||||
enum StageFlow {
|
||||
/// (ordinal into the nearest preceding family_id-bearing stage's family,
|
||||
/// param point, member report) — the shape preflight established before
|
||||
/// std::sweep populated it.
|
||||
Members(Vec<(usize, Vec<Scalar>, RunReport)>),
|
||||
Points(Vec<Vec<Scalar>>),
|
||||
}
|
||||
|
||||
impl StageFlow {
|
||||
/// Every flow can cross the seam as bare points — the walk_forward need.
|
||||
fn points(&self) -> Vec<Vec<Scalar>> {
|
||||
match self {
|
||||
StageFlow::Points(points) => points.clone(),
|
||||
StageFlow::Members(members) => {
|
||||
members.iter().map(|(_, point, _)| point.clone()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one cell's pipeline: stages in doc order, a realized prefix that stops
|
||||
/// at an empty gate (decision 8 — the cell truncates, the campaign continues).
|
||||
fn run_cell(
|
||||
@@ -247,15 +272,26 @@ fn run_cell(
|
||||
let mut families: Vec<StageFamily> = Vec::new();
|
||||
let mut selections: Vec<StageSelectionOut> = Vec::new();
|
||||
let mut stages: Vec<StageRealization> = Vec::new();
|
||||
// The surviving population: (ordinal into the nearest preceding
|
||||
// family_id-bearing stage's family, param point, member report).
|
||||
let mut survivors: Vec<(usize, Vec<Scalar>, RunReport)> = Vec::new();
|
||||
// The surviving population crossing the stage seam (see `StageFlow`).
|
||||
let mut flow = StageFlow::Members(Vec::new());
|
||||
// The cell's nominated generalize candidate: last wf window's OOS report
|
||||
// when a walk_forward ran, else the sweep winner; None when truncated.
|
||||
let mut nominee: Option<(Vec<(String, Scalar)>, RunReport)> = None;
|
||||
|
||||
for (stage_ordinal, stage) in process.pipeline.iter().enumerate() {
|
||||
match stage {
|
||||
StageBlock::Grid => {
|
||||
// Enumerate-only (#256 fork B): the grid's parameter points
|
||||
// cross the seam; nothing executes, nothing persists.
|
||||
flow = StageFlow::Points(grid.points.clone());
|
||||
stages.push(StageRealization {
|
||||
block: "std::grid".to_string(),
|
||||
family_id: None,
|
||||
survivor_ordinals: None,
|
||||
selection: None,
|
||||
bootstrap: None,
|
||||
});
|
||||
}
|
||||
StageBlock::Sweep { selection } => {
|
||||
// Deterministic, self-describing family name (the "-{run}"
|
||||
// suffix is appended by the registry).
|
||||
@@ -318,12 +354,14 @@ fn run_cell(
|
||||
}
|
||||
// The whole family flows on (decision 3: `select` names the
|
||||
// recorded selection, never a filter).
|
||||
survivors = family
|
||||
.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| (i, scalar_point(&family.space, &p.params), p.report.clone()))
|
||||
.collect();
|
||||
flow = StageFlow::Members(
|
||||
family
|
||||
.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| (i, scalar_point(&family.space, &p.params), p.report.clone()))
|
||||
.collect(),
|
||||
);
|
||||
families.push(StageFamily {
|
||||
stage: stage_ordinal,
|
||||
block: "std::sweep",
|
||||
@@ -338,13 +376,21 @@ fn run_cell(
|
||||
// None`) fails the member — conservative and deterministic
|
||||
// (the metric NAME was already preflighted against
|
||||
// `PER_MEMBER_METRICS`).
|
||||
survivors.retain(|(_, _, report)| {
|
||||
let StageFlow::Members(members) = &mut flow else {
|
||||
return Err(ExecFault::PipelineShape {
|
||||
detail: format!(
|
||||
"stage {stage_ordinal}: std::gate needs executed \
|
||||
members (preflight admits no std::grid predecessor)"
|
||||
),
|
||||
});
|
||||
};
|
||||
members.retain(|(_, _, report)| {
|
||||
all.iter().all(|p| {
|
||||
member_metric(report, &p.metric)
|
||||
.is_some_and(|value| predicate_holds(&p.cmp, value, p.value))
|
||||
})
|
||||
});
|
||||
let ordinals: Vec<usize> = survivors.iter().map(|(i, _, _)| *i).collect();
|
||||
let ordinals: Vec<usize> = members.iter().map(|(i, _, _)| *i).collect();
|
||||
let empty = ordinals.is_empty();
|
||||
stages.push(StageRealization {
|
||||
block: "std::gate".to_string(),
|
||||
@@ -368,8 +414,7 @@ fn run_cell(
|
||||
// The seam crossing is plain points: the walk-forward stage
|
||||
// re-sweeps the surviving param points and never needs the
|
||||
// gate bookkeeping's ordinals or reports.
|
||||
let survivor_points: Vec<Vec<Scalar>> =
|
||||
survivors.iter().map(|(_, point, _)| point.clone()).collect();
|
||||
let survivor_points: Vec<Vec<Scalar>> = flow.points();
|
||||
let fam = run_walk_forward_stage(
|
||||
seed,
|
||||
cell,
|
||||
@@ -415,28 +460,39 @@ fn run_cell(
|
||||
// Zero-trade members (r: None) feed an empty slice —
|
||||
// the engine's defined all-zero degenerate, recorded
|
||||
// explicitly (a null result is a valid result).
|
||||
None => StageBootstrap::PerSurvivor(
|
||||
survivors
|
||||
.iter()
|
||||
.map(|(ordinal, _, report)| {
|
||||
let rs = report
|
||||
.metrics
|
||||
.r
|
||||
.as_ref()
|
||||
.map(|r| r.net_trade_rs.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
(
|
||||
*ordinal,
|
||||
r_bootstrap(
|
||||
rs,
|
||||
*resamples as usize,
|
||||
*block_len as usize,
|
||||
seed,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
None => {
|
||||
let StageFlow::Members(members) = &flow else {
|
||||
return Err(ExecFault::PipelineShape {
|
||||
detail: format!(
|
||||
"stage {stage_ordinal}: std::monte_carlo \
|
||||
per-survivor bootstrap needs executed members \
|
||||
(preflight admits no std::grid predecessor)"
|
||||
),
|
||||
});
|
||||
};
|
||||
StageBootstrap::PerSurvivor(
|
||||
members
|
||||
.iter()
|
||||
.map(|(ordinal, _, report)| {
|
||||
let rs = report
|
||||
.metrics
|
||||
.r
|
||||
.as_ref()
|
||||
.map(|r| r.net_trade_rs.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
(
|
||||
*ordinal,
|
||||
r_bootstrap(
|
||||
rs,
|
||||
*resamples as usize,
|
||||
*block_len as usize,
|
||||
seed,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
};
|
||||
stages.push(StageRealization {
|
||||
block: "std::monte_carlo".to_string(),
|
||||
|
||||
@@ -196,6 +196,7 @@ pub fn preflight(process: &ProcessDoc, campaign: &CampaignDoc) -> Result<(), Exe
|
||||
fn shape_rank(stage: &StageBlock) -> usize {
|
||||
match stage {
|
||||
StageBlock::Sweep { .. } => 0,
|
||||
StageBlock::Grid => 0,
|
||||
StageBlock::Gate { .. } => 1,
|
||||
StageBlock::WalkForward { .. } => 2,
|
||||
StageBlock::MonteCarlo { .. } => 3,
|
||||
@@ -205,6 +206,7 @@ pub fn preflight(process: &ProcessDoc, campaign: &CampaignDoc) -> Result<(), Exe
|
||||
fn block_id(stage: &StageBlock) -> &'static str {
|
||||
match stage {
|
||||
StageBlock::Sweep { .. } => "std::sweep",
|
||||
StageBlock::Grid => "std::grid",
|
||||
StageBlock::Gate { .. } => "std::gate",
|
||||
StageBlock::WalkForward { .. } => "std::walk_forward",
|
||||
StageBlock::MonteCarlo { .. } => "std::monte_carlo",
|
||||
@@ -216,16 +218,33 @@ pub fn preflight(process: &ProcessDoc, campaign: &CampaignDoc) -> Result<(), Exe
|
||||
// every violation: a rank drop is an out-of-order stage (an annotator
|
||||
// before a population stage, anything after std::generalize), an adjacent
|
||||
// equal rank above the gate tier is a duplicate suffix stage.
|
||||
if !matches!(process.pipeline.first(), Some(StageBlock::Sweep { .. })) {
|
||||
if !matches!(
|
||||
process.pipeline.first(),
|
||||
Some(StageBlock::Sweep { .. } | StageBlock::Grid)
|
||||
) {
|
||||
return Err(ExecFault::PipelineShape {
|
||||
detail: "the first stage must be std::sweep".to_string(),
|
||||
detail: "the first stage must be std::sweep or std::grid".to_string(),
|
||||
});
|
||||
}
|
||||
// #256 fork B: an enumerate-only first stage yields points, not executed
|
||||
// members — every stage except std::walk_forward consumes member reports,
|
||||
// so std::grid must be immediately followed by std::walk_forward (in
|
||||
// particular it is never terminal).
|
||||
if matches!(process.pipeline.first(), Some(StageBlock::Grid))
|
||||
&& !matches!(process.pipeline.get(1), Some(StageBlock::WalkForward { .. }))
|
||||
{
|
||||
return Err(ExecFault::PipelineShape {
|
||||
detail: "a std::grid first stage must be immediately followed by \
|
||||
std::walk_forward (every other stage consumes executed \
|
||||
member reports)"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
let mut prev = &process.pipeline[0];
|
||||
for (i, stage) in process.pipeline.iter().enumerate().skip(1) {
|
||||
if matches!(stage, StageBlock::Sweep { .. }) {
|
||||
if matches!(stage, StageBlock::Sweep { .. } | StageBlock::Grid) {
|
||||
return Err(ExecFault::PipelineShape {
|
||||
detail: format!("stage {i}: only the first stage may be std::sweep"),
|
||||
detail: format!("stage {i}: only the first stage may be {}", block_id(stage)),
|
||||
});
|
||||
}
|
||||
let (rank, prev_rank) = (shape_rank(stage), shape_rank(prev));
|
||||
@@ -248,6 +267,7 @@ pub fn preflight(process: &ProcessDoc, campaign: &CampaignDoc) -> Result<(), Exe
|
||||
// per-stage slot rules (shape already established above).
|
||||
for (i, stage) in process.pipeline.iter().enumerate() {
|
||||
match stage {
|
||||
StageBlock::Grid => {}
|
||||
StageBlock::Sweep { selection } => match selection {
|
||||
Some(sel) => {
|
||||
if !RANKABLE_METRICS.contains(&sel.metric.as_str()) {
|
||||
@@ -731,6 +751,67 @@ mod tests {
|
||||
assert!(preflight(&process, &c).is_ok());
|
||||
}
|
||||
|
||||
/// #256 fork B: the enumerate-only leading stage is accepted exactly in
|
||||
/// the dissolved walkforward/mc shapes — first, immediately before
|
||||
/// std::walk_forward.
|
||||
#[test]
|
||||
fn preflight_accepts_a_grid_first_stage_followed_by_walk_forward() {
|
||||
let c = campaign();
|
||||
let wf_shape =
|
||||
process_of(vec![StageBlock::Grid, wf_stage("sqn_normalized", SelectRule::Argmax)]);
|
||||
assert!(preflight(&wf_shape, &c).is_ok());
|
||||
let mc_shape = process_of(vec![
|
||||
StageBlock::Grid,
|
||||
wf_stage("sqn_normalized", SelectRule::Argmax),
|
||||
mc_stage(1000, 5),
|
||||
]);
|
||||
assert!(preflight(&mc_shape, &c).is_ok());
|
||||
}
|
||||
|
||||
/// #256 fork B: every other placement is refused — grid terminal, grid
|
||||
/// before a report-consuming stage (gate / monte_carlo / generalize),
|
||||
/// grid anywhere but first. Every refusal is a PipelineShape prose fault.
|
||||
#[test]
|
||||
fn preflight_refuses_grid_placement_violations() {
|
||||
let c = campaign();
|
||||
let c2 = campaign_two_instruments();
|
||||
let adjacency = "immediately followed by std::walk_forward";
|
||||
let cases: Vec<(Vec<StageBlock>, &str, &CampaignDoc)> = vec![
|
||||
(vec![StageBlock::Grid], adjacency, &c),
|
||||
(
|
||||
vec![
|
||||
StageBlock::Grid,
|
||||
gate_stage("net_expectancy_r"),
|
||||
wf_stage("sqn_normalized", SelectRule::Argmax),
|
||||
],
|
||||
adjacency,
|
||||
&c,
|
||||
),
|
||||
(vec![StageBlock::Grid, mc_stage(1000, 5)], adjacency, &c),
|
||||
(
|
||||
vec![StageBlock::Grid, generalize_stage("net_expectancy_r")],
|
||||
adjacency,
|
||||
&c2,
|
||||
),
|
||||
(
|
||||
vec![
|
||||
sweep_stage("sqn_normalized", SelectRule::Argmax, false),
|
||||
StageBlock::Grid,
|
||||
],
|
||||
"only the first stage may be std::grid",
|
||||
&c,
|
||||
),
|
||||
];
|
||||
for (pipeline, needle, camp) in cases {
|
||||
let err =
|
||||
preflight(&process_of(pipeline), camp).expect_err("placement violation");
|
||||
let ExecFault::PipelineShape { detail } = &err else {
|
||||
panic!("expected PipelineShape, got {err:?}");
|
||||
};
|
||||
assert!(detail.contains(needle), "detail {detail:?} misses {needle:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A selection-free sweep followed by any other stage is refused: a
|
||||
/// downstream stage would have no selection/nominee to consume.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user