feat(campaign,registry,engine): record widenings + preflight v2 fault set (0108 tasks 1-2)

Task 1: RBootstrap and Generalization gain serde derives;
lineage.rs gains StageBootstrap (per_survivor | pooled_oos) and
CampaignGeneralization; StageRealization.bootstrap and
CampaignRunRecord.generalizations land serde-default sparse (a
pre-0108 campaign_runs.jsonl line still parses — pinned), threaded
None/empty through every literal site. No behaviour change.

Task 2: ExecFault::UnsupportedStage is gone; the preflight now admits
the v2 shape sweep [gate]* [wf]? [mc]? [generalize]? via a monotone
rank walk (precise per-pair PipelineShape prose) and statically
refuses: single-instrument generalize (GeneralizeNeedsInstruments),
non-R generalize metrics via check_r_metric (GeneralizeNonRMetric —
no fourth roster site), zero mc params (ZeroBootstrapParam). CLI
prose swapped accordingly. The loop's E2E phase added a tier-boundary
pin: process validate accepts [sweep, mc, walk_forward] while campaign
run refuses it data-free.

KNOWN RED (documented in the plan, flips in task 5):
crates/aura-cli/tests/research_docs.rs::campaign_run_v1_boundary_refuses_mc_process
still pins the retired 'not executable in v1' prose. Interim gates:
registry 57/0, campaign 33/0, cli --bin 111/0, workspace build clean.

refs #200
This commit is contained in:
2026-07-04 00:38:25 +02:00
parent 3454459e6d
commit ab778f2361
8 changed files with 607 additions and 60 deletions
+46 -4
View File
@@ -44,10 +44,6 @@ pub(crate) fn is_content_id(s: &str) -> bool {
/// (stage index + block id), the `doc_fault_prose`/`ref_fault_prose` register.
fn exec_fault_prose(f: &ExecFault) -> String {
match f {
ExecFault::UnsupportedStage { stage, block } => format!(
"process stage {stage} ({block}) is not executable in v1; executable \
pipeline shape: std::sweep [std::gate]* [std::walk_forward]"
),
ExecFault::PipelineShape { detail } => {
format!("process pipeline is not executable: {detail}")
}
@@ -68,6 +64,16 @@ fn exec_fault_prose(f: &ExecFault) -> String {
ExecFault::DeflatePlateauConflict { stage } => format!(
"process stage {stage}: sweep deflate: true composes only with select \"argmax\""
),
ExecFault::GeneralizeNeedsInstruments { available } => format!(
"campaign has {available} instrument(s); std::generalize needs at least 2"
),
ExecFault::GeneralizeNonRMetric { metric } => format!(
"std::generalize metric \"{metric}\" is not an R metric (gross/net R \
families generalize; pip metrics do not)"
),
ExecFault::ZeroBootstrapParam { stage, field } => format!(
"process stage {stage}: monte_carlo {field} must be > 0"
),
ExecFault::Window { stage, detail } => format!(
"process stage {stage}: walk_forward windows do not fit the campaign \
window: {detail}"
@@ -484,6 +490,42 @@ mod tests {
);
}
#[test]
/// generalize's static instrument-arity refusal is Debug-free and
/// names the available count plus the required floor.
fn generalize_needs_instruments_prose_names_the_shortfall() {
let prose = exec_fault_prose(&ExecFault::GeneralizeNeedsInstruments { available: 1 });
assert_eq!(prose, "campaign has 1 instrument(s); std::generalize needs at least 2");
assert!(!prose.contains("GeneralizeNeedsInstruments"), "Debug leak: {prose}");
}
#[test]
/// generalize's R-metric refusal names the metric and the R-only
/// rationale (pip metrics are not comparable across instruments).
fn generalize_non_r_metric_prose_names_the_metric() {
let prose = exec_fault_prose(&ExecFault::GeneralizeNonRMetric {
metric: "total_pips".into(),
});
assert_eq!(
prose,
"std::generalize metric \"total_pips\" is not an R metric (gross/net R \
families generalize; pip metrics do not)"
);
assert!(!prose.contains("GeneralizeNonRMetric"), "Debug leak: {prose}");
}
#[test]
/// the zero-bootstrap-param refusal is path-addressed (stage index)
/// and names the offending field.
fn zero_bootstrap_param_prose_names_stage_and_field() {
let prose = exec_fault_prose(&ExecFault::ZeroBootstrapParam {
stage: 3,
field: "block_len",
});
assert_eq!(prose, "process stage 3: monte_carlo block_len must be > 0");
assert!(!prose.contains("ZeroBootstrapParam"), "Debug leak: {prose}");
}
#[test]
/// The only shape treated as a direct store address is a bare 64-char
/// lowercase-hex token; anything else (wrong length, uppercase, non-hex)