feat(cli): annotator-stage seam flips + doc tidy + gated e2e with the mc suffix (0108 task 5)
The retired v1-boundary test is replaced by v2 pins: mc-before-wf refuses with the executor shape prose (while process validate accepts the same doc — the tier boundary observable across two commands, added by the task-2 e2e phase and kept), single-instrument generalize refuses data-free on the built fixture project, and the two addressing-mode tests' stale rationale comments now name the member-data seam they actually hit. PROCESS_BLOCKS doc strings drop 'in v1' (introspection text only — no content id moves). The gated real-data e2e extends its process with the monte_carlo suffix and asserts the pooled_oos bootstrap in the final campaign_run line — ran its full assert path on this host. Task-5 duplicates the plan had double-drafted (task 2's e2e phase pre-landed three of its tests) were correctly NOT re-added. Gates: workspace 1025/0 (fully green again), clippy -D warnings clean. refs #200
This commit is contained in:
@@ -644,8 +644,9 @@ fn seed_blueprint(dir: &Path, name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Register `doc` as a process document in the project store; returns its id.
|
/// Register `doc` as a process document in the project store; returns its id.
|
||||||
/// Asserts register exits 0 — an intrinsically valid document (an mc-bearing
|
/// Asserts register exits 0 — an intrinsically valid document (annotator
|
||||||
/// one included) must always register; only `campaign run` draws the v1 line.
|
/// stages in any order included) must always register; only `campaign run`'s
|
||||||
|
/// preflight draws the executable-shape line.
|
||||||
fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
|
fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
|
||||||
write_doc(dir, file, doc);
|
write_doc(dir, file, doc);
|
||||||
let (out, code) = run_code_in(dir, &["process", "register", file]);
|
let (out, code) = run_code_in(dir, &["process", "register", file]);
|
||||||
@@ -690,8 +691,10 @@ fn campaign_doc_json(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An mc-bearing process: intrinsically VALID (register accepts it) but past
|
/// An mc-bearing process: intrinsically valid AND (since the v2 executor) an
|
||||||
/// the executable v1 boundary (`campaign run` refuses it at preflight).
|
/// executable pipeline shape — `sweep -> monte_carlo` annotates each sweep
|
||||||
|
/// survivor. The two addressing-mode tests below run it over the [1, 2] 1970
|
||||||
|
/// window, so they refuse deterministically at the member-data seam.
|
||||||
const MC_PROCESS_DOC: &str = r#"{
|
const MC_PROCESS_DOC: &str = r#"{
|
||||||
"format_version": 1,
|
"format_version": 1,
|
||||||
"kind": "process",
|
"kind": "process",
|
||||||
@@ -702,7 +705,38 @@ const MC_PROCESS_DOC: &str = r#"{
|
|||||||
]
|
]
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
/// The minimal executable v1 pipeline (one sweep stage).
|
/// Intrinsically valid but NOT an executable v2 shape: the annotator sits
|
||||||
|
/// before the population stage (`walk_forward` after `monte_carlo`). The
|
||||||
|
/// intrinsic tier deliberately permits this order (the shipped
|
||||||
|
/// `validate_process_permits_walk_forward_after_an_annotator` pin in
|
||||||
|
/// aura-research), so register accepts it; only `campaign run`'s preflight
|
||||||
|
/// refuses it.
|
||||||
|
const MC_BEFORE_WF_PROCESS_DOC: &str = r#"{
|
||||||
|
"format_version": 1,
|
||||||
|
"kind": "process",
|
||||||
|
"name": "mc-before-wf",
|
||||||
|
"pipeline": [
|
||||||
|
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
||||||
|
{ "block": "std::monte_carlo", "resamples": 100, "block_len": 5 },
|
||||||
|
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||||||
|
"step_ms": 604800000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" }
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
/// An executable generalize-bearing shape (`sweep -> generalize`). The
|
||||||
|
/// refusal under test is the campaign-side static guard (std::generalize
|
||||||
|
/// needs >= 2 instruments), not the pipeline shape.
|
||||||
|
const GENERALIZE_PROCESS_DOC: &str = r#"{
|
||||||
|
"format_version": 1,
|
||||||
|
"kind": "process",
|
||||||
|
"name": "gen-screen",
|
||||||
|
"pipeline": [
|
||||||
|
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
||||||
|
{ "block": "std::generalize", "metric": "net_expectancy_r" }
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
/// The minimal executable pipeline (one sweep stage).
|
||||||
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
||||||
"format_version": 1,
|
"format_version": 1,
|
||||||
"kind": "process",
|
"kind": "process",
|
||||||
@@ -710,10 +744,14 @@ const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
|||||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
/// The full v1 shape for the gated e2e: sweep -> gate -> walk_forward. The
|
/// The full annotated shape for the gated e2e: sweep -> gate -> walk_forward
|
||||||
/// gate (`n_trades ge 0`) passes every member, so walk-forward always has
|
/// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so
|
||||||
/// survivors. Roller: 14d IS / 7d OOS / 7d step in epoch-ms, tiling the
|
/// walk-forward always has survivors; the mc annotator bootstraps the pooled
|
||||||
/// ~30-day GER40 Sept-2024 campaign window.
|
/// per-window OOS trade-R series (200 resamples, block 5). Roller: 14d IS /
|
||||||
|
/// 7d OOS / 7d step in epoch-ms, tiling the ~30-day GER40 Sept-2024 campaign
|
||||||
|
/// window. NO generalize stage: this campaign has ONE instrument (a second
|
||||||
|
/// local archive is not guaranteed), and the single-instrument refusal is
|
||||||
|
/// pinned data-free in `campaign_run_refuses_single_instrument_generalize`.
|
||||||
const WF_PROCESS_DOC: &str = r#"{
|
const WF_PROCESS_DOC: &str = r#"{
|
||||||
"format_version": 1,
|
"format_version": 1,
|
||||||
"kind": "process",
|
"kind": "process",
|
||||||
@@ -722,7 +760,8 @@ const WF_PROCESS_DOC: &str = r#"{
|
|||||||
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
||||||
{ "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 0.0 } ] },
|
{ "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,
|
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||||||
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
|
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" },
|
||||||
|
{ "block": "std::monte_carlo", "resamples": 200, "block_len": 5 }
|
||||||
]
|
]
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
@@ -769,29 +808,130 @@ fn campaign_run_unknown_id_refuses() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The v1 boundary: `process register` ACCEPTS an mc-bearing document (it is
|
/// The v2 boundary, order side: `[sweep, monte_carlo, walk_forward]` is
|
||||||
/// intrinsically valid — asserted inside `register_process_doc`); only
|
/// intrinsically valid — `process register` ACCEPTS it (asserted inside
|
||||||
/// `campaign run` refuses it, at preflight, before any member runs (so no
|
/// `register_process_doc`; the intrinsic-tier ground is the shipped
|
||||||
/// data is needed), with path-addressed Debug-free prose.
|
/// `validate_process_permits_walk_forward_after_an_annotator` pin) — but the
|
||||||
|
/// executable pipeline shape fixes the suffix order (`std::sweep [std::gate]*
|
||||||
|
/// [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`), so
|
||||||
|
/// `campaign run` refuses it at the data-free preflight, before any member
|
||||||
|
/// runs, with Debug-free PipelineShape prose.
|
||||||
#[test]
|
#[test]
|
||||||
fn campaign_run_v1_boundary_refuses_mc_process() {
|
fn campaign_run_refuses_mc_before_walk_forward() {
|
||||||
let _fixture = project_lock();
|
let _fixture = project_lock();
|
||||||
let dir = built_project();
|
let dir = built_project();
|
||||||
let runs_dir = dir.join("runs");
|
let runs_dir = dir.join("runs");
|
||||||
std::fs::remove_dir_all(&runs_dir).ok();
|
std::fs::remove_dir_all(&runs_dir).ok();
|
||||||
let _cleanup = ScratchGuard(vec![
|
let _cleanup = ScratchGuard(vec![
|
||||||
ScratchPath::Dir(runs_dir.clone()),
|
ScratchPath::Dir(runs_dir.clone()),
|
||||||
ScratchPath::File(dir.join("mc.process.json")),
|
ScratchPath::File(dir.join("mcwf.process.json")),
|
||||||
ScratchPath::File(dir.join("mc.campaign.json")),
|
ScratchPath::File(dir.join("mcwf.campaign.json")),
|
||||||
]);
|
]);
|
||||||
let bp_id = seed_blueprint(dir, "campaign-run-mc-seed");
|
let bp_id = seed_blueprint(dir, "campaign-run-mcwf-seed");
|
||||||
let proc_id = register_process_doc(dir, "mc.process.json", MC_PROCESS_DOC);
|
let proc_id = register_process_doc(dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC);
|
||||||
write_doc(dir, "mc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
write_doc(dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||||
let (out, code) = run_code_in(dir, &["campaign", "run", "mc.campaign.json"]);
|
let (out, code) = run_code_in(dir, &["campaign", "run", "mcwf.campaign.json"]);
|
||||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||||
assert!(out.contains("not executable in v1"), "stdout/stderr: {out}");
|
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
|
||||||
assert!(out.contains("std::monte_carlo"), "the prose names the block: {out}");
|
assert!(
|
||||||
assert!(!out.contains("UnsupportedStage"), "Debug leak: {out}");
|
out.contains("std::walk_forward cannot follow std::monte_carlo"),
|
||||||
|
"the detail names the ordering violation: {out}"
|
||||||
|
);
|
||||||
|
assert!(!out.contains("not executable in v1"), "retired v1 prose resurfaced: {out}");
|
||||||
|
assert!(!out.contains("PipelineShape"), "Debug leak: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// is read), so the single-GER40 fixture campaign refuses with exit 1 before
|
||||||
|
/// any member runs. This is the ONE home for the single-instrument refusal;
|
||||||
|
/// the gated real-data e2e does not duplicate it (its campaign is also
|
||||||
|
/// single-instrument, and this refusal is data-free by construction).
|
||||||
|
#[test]
|
||||||
|
fn campaign_run_refuses_single_instrument_generalize() {
|
||||||
|
let _fixture = project_lock();
|
||||||
|
let dir = built_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("gen1.process.json")),
|
||||||
|
ScratchPath::File(dir.join("gen1.campaign.json")),
|
||||||
|
]);
|
||||||
|
let bp_id = seed_blueprint(dir, "campaign-run-gen1-seed");
|
||||||
|
let proc_id = register_process_doc(dir, "gen1.process.json", GENERALIZE_PROCESS_DOC);
|
||||||
|
write_doc(dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||||
|
let (out, code) = run_code_in(dir, &["campaign", "run", "gen1.campaign.json"]);
|
||||||
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||||
|
assert!(
|
||||||
|
out.contains("std::generalize needs at least 2"),
|
||||||
|
"the refusal names the block and the minimum: {out}"
|
||||||
|
);
|
||||||
|
assert!(out.contains("1 instrument"), "the refusal names the available count: {out}");
|
||||||
|
assert!(!out.contains("GeneralizeNeedsInstruments"), "Debug leak: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An executable generalize-bearing shape whose metric is a pip metric, not
|
||||||
|
/// an R metric (`total_pips` is a known, registered scalar — `process
|
||||||
|
/// register` accepts it — but not R-denominated).
|
||||||
|
const GENERALIZE_NON_R_PROCESS_DOC: &str = r#"{
|
||||||
|
"format_version": 1,
|
||||||
|
"kind": "process",
|
||||||
|
"name": "gen-pip-screen",
|
||||||
|
"pipeline": [
|
||||||
|
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
||||||
|
{ "block": "std::generalize", "metric": "total_pips" }
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
/// The v2 boundary, metric side: `std::generalize`'s metric guard
|
||||||
|
/// (`check_r_metric`) is a STATIC preflight fact independent of the
|
||||||
|
/// instrument-arity guard above it in the same match arm — with >= 2
|
||||||
|
/// campaign instruments (so arity passes) and a pip metric, `campaign run`
|
||||||
|
/// must still refuse data-free, naming the metric, and never fall through to
|
||||||
|
/// the instrument-count prose or a Debug-formatted fault. A regression that
|
||||||
|
/// let the arity check short-circuit past the metric check (or vice versa)
|
||||||
|
/// would silently accept a pip-denominated `std::generalize`.
|
||||||
|
#[test]
|
||||||
|
fn campaign_run_refuses_generalize_non_r_metric() {
|
||||||
|
let _fixture = project_lock();
|
||||||
|
let dir = built_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("genpip.process.json")),
|
||||||
|
ScratchPath::File(dir.join("genpip.campaign.json")),
|
||||||
|
]);
|
||||||
|
let bp_id = seed_blueprint(dir, "campaign-run-genpip-seed");
|
||||||
|
let proc_id = register_process_doc(dir, "genpip.process.json", GENERALIZE_NON_R_PROCESS_DOC);
|
||||||
|
// Two instruments (arity passes), so the metric guard is the one that
|
||||||
|
// actually fires. The [1, 2] 1970 window never matters: this refusal is
|
||||||
|
// data-free, before any member runs.
|
||||||
|
let campaign_doc = format!(
|
||||||
|
r#"{{
|
||||||
|
"format_version": 1,
|
||||||
|
"kind": "campaign",
|
||||||
|
"name": "run-seam-genpip",
|
||||||
|
"data": {{ "instruments": ["GER40", "EURUSD"], "windows": [ {{ "from_ms": 1, "to_ms": 2 }} ] }},
|
||||||
|
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||||||
|
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
|
||||||
|
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
|
||||||
|
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||||||
|
"seed": 7,
|
||||||
|
"presentation": {{ "persist_taps": [], "emit": [] }}
|
||||||
|
}}"#
|
||||||
|
);
|
||||||
|
write_doc(dir, "genpip.campaign.json", &campaign_doc);
|
||||||
|
let (out, code) = run_code_in(dir, &["campaign", "run", "genpip.campaign.json"]);
|
||||||
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||||
|
assert!(
|
||||||
|
out.contains("std::generalize metric \"total_pips\" is not an R metric"),
|
||||||
|
"the refusal names the offending metric: {out}"
|
||||||
|
);
|
||||||
|
assert!(!out.contains("2 instrument"), "the arity guard must not fire when arity is fine: {out}");
|
||||||
|
assert!(!out.contains("GeneralizeNonRMetric"), "Debug leak: {out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A v2 process doc with `std::walk_forward` following the annotator
|
/// A v2 process doc with `std::walk_forward` following the annotator
|
||||||
@@ -896,51 +1036,6 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
|
|||||||
assert!(!out.contains("ZeroBootstrapParam"), "Debug leak: {out}");
|
assert!(!out.contains("ZeroBootstrapParam"), "Debug leak: {out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A v2 process doc that ends in `std::generalize`.
|
|
||||||
const GENERALIZE_PROCESS_DOC: &str = r#"{
|
|
||||||
"format_version": 1,
|
|
||||||
"kind": "process",
|
|
||||||
"name": "generalize-screen",
|
|
||||||
"pipeline": [
|
|
||||||
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
|
||||||
{ "block": "std::generalize", "metric": "net_expectancy_r" }
|
|
||||||
]
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
/// Property (0108 task 2): `std::generalize`'s campaign-level instrument-
|
|
||||||
/// arity guard (>= 2 instruments) is a STATIC executor-preflight refusal,
|
|
||||||
/// wired end to end from the campaign document's `data.instruments` through
|
|
||||||
/// to CLI stdout, firing before any member runs (data-free) and naming the
|
|
||||||
/// actual instrument count. `campaign_doc_json`'s fixture campaign carries
|
|
||||||
/// exactly one instrument (`GER40`), so this exercises the campaign
|
|
||||||
/// parameter `preflight` newly reads (previously unused in v1) rather than
|
|
||||||
/// anything reachable from the process document alone. A regression that
|
|
||||||
/// left the campaign parameter unread would silently accept a single-
|
|
||||||
/// instrument `std::generalize` and only fail later, opaquely, inside the
|
|
||||||
/// generalization math.
|
|
||||||
#[test]
|
|
||||||
fn campaign_run_refuses_single_instrument_generalize() {
|
|
||||||
let _fixture = project_lock();
|
|
||||||
let dir = built_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("gen.process.json")),
|
|
||||||
ScratchPath::File(dir.join("gen.campaign.json")),
|
|
||||||
]);
|
|
||||||
let bp_id = seed_blueprint(dir, "campaign-run-gen-seed");
|
|
||||||
let proc_id = register_process_doc(dir, "gen.process.json", GENERALIZE_PROCESS_DOC);
|
|
||||||
write_doc(dir, "gen.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
|
||||||
let (out, code) = run_code_in(dir, &["campaign", "run", "gen.campaign.json"]);
|
|
||||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
||||||
assert!(
|
|
||||||
out.contains("campaign has 1 instrument(s); std::generalize needs at least 2"),
|
|
||||||
"stdout/stderr: {out}"
|
|
||||||
);
|
|
||||||
assert!(!out.contains("GeneralizeNeedsInstruments"), "Debug leak: {out}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Non-empty persist_taps defers LOUDLY, and the note's ORDER is pinned: it
|
/// Non-empty persist_taps defers LOUDLY, and the note's ORDER is pinned: it
|
||||||
/// prints before member execution, so it is asserted here on a run
|
/// prints before member execution, so it is asserted here on a run
|
||||||
/// that refuses at the member-data seam. The [1, 2] epoch-ms window (1970)
|
/// that refuses at the member-data seam. The [1, 2] epoch-ms window (1970)
|
||||||
@@ -978,12 +1073,13 @@ fn campaign_run_persist_taps_deferred_loudly() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gated real-data e2e (the `cli_run.rs` skip idiom): a full
|
/// Gated real-data e2e (the `cli_run.rs` skip idiom): a full
|
||||||
/// sweep -> gate -> walk_forward campaign over GER40 Sept-2024 where the
|
/// sweep -> gate -> walk_forward -> monte_carlo campaign over GER40
|
||||||
/// local archive is present — exit 0, emit-gated family/selection lines, a
|
/// Sept-2024 where the local archive is present — exit 0, emit-gated
|
||||||
/// final line parseable as JSON with top-level `campaign_run` linking a sweep
|
/// family/selection lines, a final line parseable as JSON with top-level
|
||||||
/// family id and a walk-forward family id, and the `campaign_runs.jsonl`
|
/// `campaign_run` linking a sweep family id, a walk-forward family id, and a
|
||||||
/// sibling store written. Skips with a note elsewhere so
|
/// pooled-OOS bootstrap on the terminal mc stage, and the
|
||||||
/// `cargo test --workspace` stays green on a data-less machine.
|
/// `campaign_runs.jsonl` sibling store written. Skips with a note elsewhere
|
||||||
|
/// so `cargo test --workspace` stays green on a data-less machine.
|
||||||
#[test]
|
#[test]
|
||||||
fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
||||||
let _fixture = project_lock();
|
let _fixture = project_lock();
|
||||||
@@ -1030,7 +1126,11 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
|||||||
let cells = v["campaign_run"]["cells"].as_array().expect("cells array");
|
let cells = v["campaign_run"]["cells"].as_array().expect("cells array");
|
||||||
assert_eq!(cells.len(), 1, "one (strategy, instrument, window) cell: {record_line}");
|
assert_eq!(cells.len(), 1, "one (strategy, instrument, window) cell: {record_line}");
|
||||||
let stages = cells[0]["stages"].as_array().expect("stages array");
|
let stages = cells[0]["stages"].as_array().expect("stages array");
|
||||||
assert_eq!(stages.len(), 3, "sweep + gate + walk_forward realized: {record_line}");
|
assert_eq!(
|
||||||
|
stages.len(),
|
||||||
|
4,
|
||||||
|
"sweep + gate + walk_forward + monte_carlo realized: {record_line}"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
stages[0]["family_id"].as_str().is_some(),
|
stages[0]["family_id"].as_str().is_some(),
|
||||||
"sweep stage links a family: {record_line}"
|
"sweep stage links a family: {record_line}"
|
||||||
@@ -1044,6 +1144,21 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
|||||||
stages[2]["family_id"].as_str().is_some(),
|
stages[2]["family_id"].as_str().is_some(),
|
||||||
"walk-forward stage links a family: {record_line}"
|
"walk-forward stage links a family: {record_line}"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stages[3]["block"].as_str(),
|
||||||
|
Some("std::monte_carlo"),
|
||||||
|
"the annotator stage is realized last: {record_line}"
|
||||||
|
);
|
||||||
|
let pooled = &stages[3]["bootstrap"]["pooled_oos"];
|
||||||
|
assert!(
|
||||||
|
pooled.is_object(),
|
||||||
|
"an mc stage after walk_forward records ONE pooled-OOS bootstrap: {record_line}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pooled["n_resamples"].as_u64(),
|
||||||
|
Some(200),
|
||||||
|
"the bootstrap carries the doc's resamples verbatim: {record_line}"
|
||||||
|
);
|
||||||
// Emit-gated lines: per-member family_table lines (4 sweep members plus
|
// Emit-gated lines: per-member family_table lines (4 sweep members plus
|
||||||
// the walk-forward OOS members) and at least one selection_report line.
|
// the walk-forward OOS members) and at least one selection_report line.
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1061,6 +1176,76 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gated real-data e2e, the OTHER mc bootstrap input shape (#200 d1): with no
|
||||||
|
/// walk_forward in the pipeline (`sweep -> monte_carlo`), the terminal
|
||||||
|
/// annotator's dual-input dispatch takes the `PerSurvivor` arm, not
|
||||||
|
/// `PooledOos` — one bootstrap per surviving sweep member, keyed by the
|
||||||
|
/// member's ordinal into the sweep family, rather than one pooled bootstrap
|
||||||
|
/// over walk-forward OOS windows. `campaign_run_real_e2e_sweep_gate_walkforward`
|
||||||
|
/// above only exercises `PooledOos`; this is the only e2e coverage of
|
||||||
|
/// `PerSurvivor` with real data (the addressing-mode tests use it only as a
|
||||||
|
/// data-free refusal fixture).
|
||||||
|
#[test]
|
||||||
|
fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
|
||||||
|
let _fixture = project_lock();
|
||||||
|
let dir = built_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("mc.process.json")),
|
||||||
|
ScratchPath::File(dir.join("mc.campaign.json")),
|
||||||
|
]);
|
||||||
|
let bp_id = seed_blueprint(dir, "campaign-run-mc-persurvivor-seed");
|
||||||
|
let proc_id = register_process_doc(dir, "mc.process.json", MC_PROCESS_DOC);
|
||||||
|
// Same GER40 Sept-2024 window as the gated wf e2e above.
|
||||||
|
write_doc(
|
||||||
|
dir,
|
||||||
|
"mc.campaign.json",
|
||||||
|
&campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""),
|
||||||
|
);
|
||||||
|
let (out, code) = run_code_in(dir, &["campaign", "run", "mc.campaign.json"]);
|
||||||
|
|
||||||
|
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||||
|
if code == Some(1)
|
||||||
|
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
|
||||||
|
{
|
||||||
|
eprintln!("skip: no local GER40 data for the campaign e2e");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||||
|
let record_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(record_line).expect("campaign_run line parses as JSON");
|
||||||
|
let cells = v["campaign_run"]["cells"].as_array().expect("cells array");
|
||||||
|
let stages = cells[0]["stages"].as_array().expect("stages array");
|
||||||
|
assert_eq!(stages.len(), 2, "sweep + monte_carlo realized: {record_line}");
|
||||||
|
assert_eq!(
|
||||||
|
stages[1]["block"].as_str(),
|
||||||
|
Some("std::monte_carlo"),
|
||||||
|
"the annotator stage is realized last: {record_line}"
|
||||||
|
);
|
||||||
|
let per_survivor = stages[1]["bootstrap"]["per_survivor"]
|
||||||
|
.as_array()
|
||||||
|
.expect("no walk_forward precedes: the PerSurvivor arm, not pooled_oos");
|
||||||
|
assert_eq!(
|
||||||
|
per_survivor.len(),
|
||||||
|
4,
|
||||||
|
"one bootstrap per surviving sweep member (4 axis combinations, no gate): {record_line}"
|
||||||
|
);
|
||||||
|
let (ordinal, bootstrap) = (per_survivor[0][0].as_u64(), &per_survivor[0][1]);
|
||||||
|
assert_eq!(ordinal, Some(0), "each entry is keyed by ordinal into the sweep family: {record_line}");
|
||||||
|
assert_eq!(
|
||||||
|
bootstrap["n_resamples"].as_u64(),
|
||||||
|
Some(100),
|
||||||
|
"the bootstrap carries the doc's resamples verbatim: {record_line}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `run_campaign` resolves a target two ways — a readable file (register-then-
|
/// `run_campaign` resolves a target two ways — a readable file (register-then-
|
||||||
/// run sugar) or a bare content id (direct store address) — then funnels BOTH
|
/// run sugar) or a bare content id (direct store address) — then funnels BOTH
|
||||||
/// through one shared post-resolution path: "fetch the stored canonical bytes
|
/// through one shared post-resolution path: "fetch the stored canonical bytes
|
||||||
@@ -1069,8 +1254,11 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
|||||||
/// that property observably: registering a document and then running it once
|
/// that property observably: registering a document and then running it once
|
||||||
/// by FILE and once by its own resulting content ID must refuse with the
|
/// by FILE and once by its own resulting content ID must refuse with the
|
||||||
/// byte-identical prose — proof the two addressing modes are not two
|
/// byte-identical prose — proof the two addressing modes are not two
|
||||||
/// independently drifting code paths. No real data needed: both runs refuse
|
/// independently drifting code paths. No real data needed: with the [1, 2]
|
||||||
/// at the (data-free) v1-pipeline-shape preflight.
|
/// 1970 window, both runs pass the (now mc-permitting) preflight and refuse
|
||||||
|
/// at the member-data seam — no recorded geometry on a data-less host, no
|
||||||
|
/// overlapping archive window on a data-ful one — exit 1 and byte-identical
|
||||||
|
/// prose either way (the message depends only on env/instrument/window).
|
||||||
#[test]
|
#[test]
|
||||||
fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
||||||
let _fixture = project_lock();
|
let _fixture = project_lock();
|
||||||
@@ -1106,6 +1294,10 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
|||||||
|
|
||||||
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
|
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
|
||||||
let id_line = id_out.lines().find(|l| l.starts_with("aura: ")).expect("id refusal line");
|
let id_line = id_out.lines().find(|l| l.starts_with("aura: ")).expect("id refusal line");
|
||||||
|
assert!(
|
||||||
|
file_line.contains("no recorded geometry") || file_line.contains("no data for instrument"),
|
||||||
|
"the refusal is the member-data seam (mc processes execute now): {file_line}"
|
||||||
|
);
|
||||||
assert_eq!(file_line, id_line, "file- and id-addressed runs must refuse identically");
|
assert_eq!(file_line, id_line, "file- and id-addressed runs must refuse identically");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1115,7 +1307,9 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
|||||||
/// observably: running the SAME registered campaign by bare id and by its
|
/// observably: running the SAME registered campaign by bare id and by its
|
||||||
/// `content:`-prefixed form must refuse identically (proof the prefix is
|
/// `content:`-prefixed form must refuse identically (proof the prefix is
|
||||||
/// stripped, not read as a second, unresolvable address). Both refuse at the
|
/// stripped, not read as a second, unresolvable address). Both refuse at the
|
||||||
/// data-free v1 preflight (MC process), so no real data is needed.
|
/// member-data seam (the mc process is executable in v2; the [1, 2] 1970
|
||||||
|
/// window guarantees a deterministic data refusal), so no real data is
|
||||||
|
/// needed.
|
||||||
#[test]
|
#[test]
|
||||||
fn campaign_run_tolerates_content_prefix_on_target() {
|
fn campaign_run_tolerates_content_prefix_on_target() {
|
||||||
let _fixture = project_lock();
|
let _fixture = project_lock();
|
||||||
@@ -1153,6 +1347,10 @@ fn campaign_run_tolerates_content_prefix_on_target() {
|
|||||||
"a content:-prefixed target must resolve as the bare id, not refuse as an unknown target: {pfx_line}"
|
"a content:-prefixed target must resolve as the bare id, not refuse as an unknown target: {pfx_line}"
|
||||||
);
|
);
|
||||||
let bare_line = bare_out.lines().find(|l| l.starts_with("aura: ")).expect("bare refusal line");
|
let bare_line = bare_out.lines().find(|l| l.starts_with("aura: ")).expect("bare refusal line");
|
||||||
|
assert!(
|
||||||
|
bare_line.contains("no recorded geometry") || bare_line.contains("no data for instrument"),
|
||||||
|
"the refusal is the member-data seam (mc processes execute now): {bare_line}"
|
||||||
|
);
|
||||||
assert_eq!(bare_line, pfx_line, "bare-id and content:-prefixed runs must resolve identically");
|
assert_eq!(bare_line, pfx_line, "bare-id and content:-prefixed runs must resolve identically");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ pub const PROCESS_BLOCKS: &[BlockSchema] = &[
|
|||||||
},
|
},
|
||||||
BlockSchema {
|
BlockSchema {
|
||||||
id: "std::monte_carlo",
|
id: "std::monte_carlo",
|
||||||
doc: "R-bootstrap over realised R (terminal annotator in v1)",
|
doc: "R-bootstrap over realised R (terminal annotator)",
|
||||||
slots: &[
|
slots: &[
|
||||||
SlotInfo { name: "resamples", kind: SlotKind::U32, required: true },
|
SlotInfo { name: "resamples", kind: SlotKind::U32, required: true },
|
||||||
SlotInfo { name: "block_len", kind: SlotKind::U32, required: true },
|
SlotInfo { name: "block_len", kind: SlotKind::U32, required: true },
|
||||||
@@ -196,7 +196,7 @@ pub const PROCESS_BLOCKS: &[BlockSchema] = &[
|
|||||||
},
|
},
|
||||||
BlockSchema {
|
BlockSchema {
|
||||||
id: "std::generalize",
|
id: "std::generalize",
|
||||||
doc: "cross-instrument worst-case floor (terminal annotator in v1)",
|
doc: "cross-instrument worst-case floor (terminal annotator)",
|
||||||
slots: &[SlotInfo { name: "metric", kind: SlotKind::MetricName, required: true }],
|
slots: &[SlotInfo { name: "metric", kind: SlotKind::MetricName, required: true }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user