feat(aura-cli): exec --override on the campaign leg + zero-trade note migration

Slice 3 of the #319 retirement. The campaign leg's --override injects a
single-value axis over the named bound param into every strategy entry —
after intrinsic validation, before the referential gate, so an unknown
path speaks validate_campaign_refs' existing did-you-mean prose and a
kind mismatch its existing fault; collision with a document-declared axis
refuses (an override overrides a bound value, never an axis, exit 2).
The stored bytes and the recorded campaign_id stay those of the original
document; the audit record of an override is the raw member manifests.
The walk-forward zero-trade aura: note: diagnostic now has a campaign-
path producer call (present_campaign per-cell), so it survives the
quintet's removal at today's strength.

refs #319
This commit is contained in:
2026-07-25 18:56:56 +02:00
parent da19e27b6a
commit 696d7fe59a
4 changed files with 322 additions and 31 deletions
+223
View File
@@ -492,3 +492,226 @@ fn exec_override_unknown_path_refuses_with_the_override_prose() {
"stdout/stderr: {out}"
);
}
// ---------------------------------------------------------------------------
// Task 4: `--override` on the campaign leg.
// ---------------------------------------------------------------------------
/// A bound param not covered by the document's own axes (`bias.scale`,
/// `examples/r_sma.json`'s `Bias` node — `seed_blueprint` only reopens
/// `fast.length`/`slow.length`, so `bias.scale` stays bound at 0.5 in the
/// stored blueprint): `--override bias.scale=0.75` reaches every realized
/// member's manifest raw (recorded in `params`, dropped from `defaults`) —
/// mirrors `exec_blueprint_override_reopens_and_records_the_value_in_params`
/// on the campaign leg.
#[test]
fn exec_campaign_override_reaches_the_member_manifests_raw() {
let (dir, _fixture) = fresh_project_with_data();
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("ovreach.process.json")),
ScratchPath::File(dir.join("ovreach.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "exec-override-reach-seed");
let proc_id = register_process_doc(&dir, "ovreach.process.json", SWEEP_ONLY_PROCESS_DOC);
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
write_doc(&dir, "ovreach.campaign.json", &doc);
let (out, code) =
run_code_in(&dir, &["exec", "ovreach.campaign.json", "--override", "bias.scale=0.75"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let members: Vec<serde_json::Value> = out
.lines()
.filter(|l| l.starts_with(r#"{"family_id":"#))
.map(|l| serde_json::from_str(l).expect("member line parses"))
.collect();
assert!(!members.is_empty(), "at least one member line: {out}");
for m in &members {
let params = m["report"]["manifest"]["params"].as_array().expect("params array");
assert!(
params
.iter()
.any(|e| e[0] == "bias.scale" && e[1] == serde_json::json!({"F64": 0.75})),
"the overridden value must land in every member's params: {m}"
);
let defaults = m["report"]["manifest"]["defaults"].as_array().expect("defaults array");
assert!(
!defaults.iter().any(|e| e[0] == "bias.scale"),
"an overridden bound param drops out of defaults: {m}"
);
}
}
/// An override path colliding with a document-declared axis (`fast.length`,
/// declared by `campaign_doc_json_for`) refuses: an override overrides a
/// bound value, never an axis.
#[test]
fn exec_campaign_override_colliding_with_a_document_axis_refuses() {
let (dir, _fixture) = fresh_project_with_data();
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("ovcollide.process.json")),
ScratchPath::File(dir.join("ovcollide.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "exec-override-collide-seed");
let proc_id = register_process_doc(&dir, "ovcollide.process.json", SWEEP_ONLY_PROCESS_DOC);
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
write_doc(&dir, "ovcollide.campaign.json", &doc);
let (out, code) =
run_code_in(&dir, &["exec", "ovcollide.campaign.json", "--override", "fast.length=9"]);
assert_eq!(code, Some(2), "stdout/stderr: {out}");
assert!(out.contains("--override"), "stdout/stderr: {out}");
assert!(out.contains("declared axis"), "stdout/stderr: {out}");
}
/// An override path naming no param of any strategy (a typo'd raw name)
/// falls out of `validate_campaign_refs`'s existing `AxisNotInParamSpace`
/// did-you-mean prose — pinned literally in `research_docs.rs` (:797-822);
/// here only the family of the message is asserted.
#[test]
fn exec_campaign_override_unknown_path_speaks_the_did_you_mean_family() {
let (dir, _fixture) = fresh_project_with_data();
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("ovtypo.process.json")),
ScratchPath::File(dir.join("ovtypo.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "exec-override-typo-seed");
let proc_id = register_process_doc(&dir, "ovtypo.process.json", SWEEP_ONLY_PROCESS_DOC);
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
write_doc(&dir, "ovtypo.campaign.json", &doc);
let (out, code) =
run_code_in(&dir, &["exec", "ovtypo.campaign.json", "--override", "fast.lenght=9"]);
assert_ne!(code, Some(0), "stdout/stderr: {out}");
assert!(
out.contains("did you mean") || out.contains("not in the param space"),
"stdout/stderr: {out}"
);
}
// ---------------------------------------------------------------------------
// Task 5: zero-trade note migration to the campaign walk-forward leg.
// ---------------------------------------------------------------------------
/// A `[std::grid, std::walk_forward]` process — the #256 fork B shape a
/// hand-authored (non-sugar) op-script uses — copied verbatim from
/// `research_docs.rs::GRID_THEN_WF_PROCESS_DOC`.
const GRID_THEN_WF_PROCESS_DOC: &str = r#"{
"format_version": 1,
"kind": "process",
"name": "grid-then-walkforward",
"pipeline": [
{ "block": "std::grid" },
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
]
}"#;
/// `campaign_doc_json_for`'s shape with the axis VALUES parameterized (Task 5
/// needs the degenerate zero-trade point — both SMA lengths pinned equal —
/// which the fixed 2/4 and 8/16 defaults don't reach).
fn campaign_doc_json_walkforward(
instrument: &str,
bp_id: &str,
proc_id: &str,
window: (i64, i64),
fast_values: &str,
slow_values: &str,
) -> String {
format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "run-seam-wf",
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }},
"slow.length": {{ "kind": "I64", "values": [{slow_values}] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
from = window.0,
to = window.1,
)
}
/// E2E (#313, campaign path): a walk-forward whose every window records zero
/// trades emits exactly one zero-trade note on `exec`'s campaign leg — the
/// same producer `cli_run.rs::aura_walkforward_all_zero_trade_windows_emit_one_note`
/// pins on the retiring synthetic-verb path, now reached through a
/// hand-authored `[std::grid, std::walk_forward]` campaign document. Equal
/// fast/slow lengths make the SMA difference constantly zero, so no window
/// trades — the same degenerate point, over real (SYMA) rather than
/// synthetic data (the property is data-source-agnostic).
#[test]
fn exec_campaign_walkforward_all_zero_trade_windows_emit_one_note() {
let (dir, _fixture) = fresh_project_with_data();
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("wfzero.process.json")),
ScratchPath::File(dir.join("wfzero.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "exec-wf-zero-seed");
let proc_id = register_process_doc(&dir, "wfzero.process.json", GRID_THEN_WF_PROCESS_DOC);
let doc = campaign_doc_json_walkforward(
"SYMA",
&bp_id,
&proc_id,
(1709251200000, 1719791999999),
"8",
"8",
);
write_doc(&dir, "wfzero.campaign.json", &doc);
let (out, code) = run_code_in(&dir, &["exec", "wfzero.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
assert_eq!(
out.matches("recorded zero trades").count(),
1,
"exactly one zero-trade note: {out}"
);
assert!(out.contains("aura: note: "), "the note carries the class marker: {out}");
}
/// E2E (#313 negative twin, campaign path): a walk-forward with at least one
/// traded window emits no zero-trade note — the fixture is the same document
/// shape as the zero-trade test above, but with the axes campaign_doc_json_for
/// already uses (fast∈{2,4}, slow∈{8,16} — known to produce trades over the
/// synthetic SYMA archive, per `campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap`).
#[test]
fn exec_campaign_walkforward_with_trades_emits_no_zero_trade_note() {
let (dir, _fixture) = fresh_project_with_data();
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("wftrades.process.json")),
ScratchPath::File(dir.join("wftrades.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "exec-wf-trades-seed");
let proc_id = register_process_doc(&dir, "wftrades.process.json", GRID_THEN_WF_PROCESS_DOC);
let doc = campaign_doc_json_walkforward(
"SYMA",
&bp_id,
&proc_id,
(1709251200000, 1719791999999),
"2, 4",
"8, 16",
);
write_doc(&dir, "wftrades.campaign.json", &doc);
let (out, code) = run_code_in(&dir, &["exec", "wftrades.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
assert!(!out.contains("recorded zero trades"), "a traded run emits no zero-trade note: {out}");
}