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:
@@ -27,7 +27,7 @@ use aura_engine::FamilySelection;
|
||||
use aura_registry::CampaignRunRecord;
|
||||
use aura_research::{
|
||||
campaign_to_json, content_id_of, parse_campaign, parse_process, validate_campaign,
|
||||
validate_process, CampaignDoc, DocRef,
|
||||
validate_process, Axis, CampaignDoc, DocRef,
|
||||
};
|
||||
|
||||
use aura_runner::axes::is_content_id;
|
||||
@@ -124,11 +124,14 @@ pub(crate) enum RunPresentation {
|
||||
/// `aura campaign run <target>`: resolve, gate, execute, emit. Every `Err`
|
||||
/// is a refusal `campaign_cmd` renders as `aura: {msg}` + exit 1; an `Ok`
|
||||
/// carries the failed-cell count (#272) so the caller can exit 3 on a
|
||||
/// completed-with-failures run via `exit_on_campaign_result`.
|
||||
/// completed-with-failures run via `exit_on_campaign_result`. `overrides`
|
||||
/// (#319 Task 4, the `exec --override` campaign leg) is threaded through
|
||||
/// unchanged to `run_campaign_returning`, the one injection seam.
|
||||
pub(crate) fn run_campaign(
|
||||
target: &str,
|
||||
env: &Env,
|
||||
parallel_instruments: NonZeroUsize,
|
||||
overrides: &[(String, Scalar)],
|
||||
) -> Result<usize, String> {
|
||||
// Project gate FIRST: nothing (not even the file-sugar registration)
|
||||
// touches a store outside a project.
|
||||
@@ -162,7 +165,7 @@ pub(crate) fn run_campaign(
|
||||
));
|
||||
};
|
||||
|
||||
run_campaign_by_id(&campaign_id, env, RunPresentation::Full, parallel_instruments)
|
||||
run_campaign_by_id(&campaign_id, env, RunPresentation::Full, parallel_instruments, overrides)
|
||||
}
|
||||
|
||||
/// An executed campaign plus the context its presenter and the dissolved-verb
|
||||
@@ -192,8 +195,9 @@ pub(crate) fn run_campaign_by_id(
|
||||
env: &Env,
|
||||
presentation: RunPresentation,
|
||||
parallel_instruments: NonZeroUsize,
|
||||
overrides: &[(String, Scalar)],
|
||||
) -> Result<usize, String> {
|
||||
let run = run_campaign_returning(campaign_id, env, parallel_instruments)?;
|
||||
let run = run_campaign_returning(campaign_id, env, parallel_instruments, overrides)?;
|
||||
present_campaign(run, presentation, env)
|
||||
}
|
||||
|
||||
@@ -204,18 +208,23 @@ pub(crate) fn run_campaign_by_id(
|
||||
/// outcome bundled with the context the presenter needs. Shared by
|
||||
/// `run_campaign_by_id` (which then presents) and a forthcoming dissolved-verb
|
||||
/// sugar path that reads the outcome and self-prints (lands in a later task,
|
||||
/// alongside the generalize translator's own runner).
|
||||
/// alongside the generalize translator's own runner). `overrides` (#319
|
||||
/// Task 4) is injected into the parsed document AFTER intrinsic validation
|
||||
/// and BEFORE the referential gate — the stored bytes and the recorded
|
||||
/// `campaign_id` stay those of the ORIGINAL document; only the executed
|
||||
/// run-set changes, its audit record being the raw member manifests.
|
||||
pub(crate) fn run_campaign_returning(
|
||||
campaign_id: &str,
|
||||
env: &Env,
|
||||
parallel_instruments: NonZeroUsize,
|
||||
overrides: &[(String, Scalar)],
|
||||
) -> Result<CampaignRun, String> {
|
||||
let registry = env.registry();
|
||||
let campaign_text = registry
|
||||
.get_campaign(campaign_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("no campaign {campaign_id} in the project store"))?;
|
||||
let campaign = parse_campaign(&campaign_text)
|
||||
let mut campaign = parse_campaign(&campaign_text)
|
||||
.map_err(|e| doc_error_prose("stored campaign document", &e))?;
|
||||
let faults = validate_campaign(&campaign);
|
||||
if !faults.is_empty() {
|
||||
@@ -225,6 +234,25 @@ pub(crate) fn run_campaign_returning(
|
||||
));
|
||||
}
|
||||
|
||||
// --override NODE.PARAM=VALUE (#246 residue, #319 Task 4): inject as a
|
||||
// single-value axis over the named bound param into EVERY strategy
|
||||
// entry. Collision with a document-declared axis refuses (an override
|
||||
// overrides a bound value, never an axis); an unknown path then falls
|
||||
// out of `validate_campaign_refs` below with its existing did-you-mean
|
||||
// prose — no `RefFault` change, no registry change.
|
||||
for (path, val) in overrides {
|
||||
for entry in &mut campaign.strategies {
|
||||
if entry.axes.contains_key(path) {
|
||||
eprintln!(
|
||||
"aura: exec: --override `{path}` collides with a declared axis \
|
||||
of the campaign; an override overrides a bound value, never an axis"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
entry.axes.insert(path.clone(), single_value_axis(*val));
|
||||
}
|
||||
}
|
||||
|
||||
// Referential gate: zero faults or refuse (the campaign-validate seam).
|
||||
let resolve = |t: &str| env.resolve(t);
|
||||
let ref_faults = registry
|
||||
@@ -355,6 +383,17 @@ fn present_campaign(
|
||||
let emit_family = campaign.presentation.emit.iter().any(|e| e == "family_table");
|
||||
let emit_selection = campaign.presentation.emit.iter().any(|e| e == "selection_report");
|
||||
for cell_out in &outcome.cells {
|
||||
// #313 zero-trade note (#319 Task 5, migrated from the retiring
|
||||
// `dispatch_walkforward`/`run_walkforward_sugar` paths): every campaign
|
||||
// cell's `std::walk_forward` family, if any, gets the same shared
|
||||
// `note_zero_trade_windows` producer over its OOS members' trade
|
||||
// counts — independent of `emit` (a stderr diagnostic, not gated
|
||||
// stdout presentation).
|
||||
if let Some(wf) = cell_out.families.iter().find(|f| f.block == "std::walk_forward") {
|
||||
crate::diag::note_zero_trade_windows(
|
||||
wf.reports.iter().map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
|
||||
);
|
||||
}
|
||||
if emit_family {
|
||||
for fam in &cell_out.families {
|
||||
for report in &fam.reports {
|
||||
@@ -417,6 +456,13 @@ fn present_campaign(
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
/// Build the single-value `Axis` an `exec --override` injects (#319 Task 4):
|
||||
/// exactly the shape the document form `{"kind": "I64", "values": [v]}`
|
||||
/// deserializes to — `Scalar::kind` names the tag, one value in the list.
|
||||
fn single_value_axis(v: Scalar) -> Axis {
|
||||
Axis { kind: v.kind(), values: vec![v] }
|
||||
}
|
||||
|
||||
/// #272: the fault-kind label the failed-cell summary line names — the same
|
||||
/// closed `CellFaultKind` vocabulary an aggregate over `campaign_runs.jsonl`
|
||||
/// counts by. Debug-leak-free (a fixed lowercase-snake label, not a Debug
|
||||
|
||||
+43
-25
@@ -1079,6 +1079,38 @@ fn parse_scalar_csv(csv: &str) -> Option<Vec<Scalar>> {
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Lex `exec --override NODE.PARAM=VALUE` tokens (#319 Task 3/4): the same
|
||||
/// int-then-float rule `parse_scalar_csv` uses for `--axis` values, shared by
|
||||
/// both `exec` legs (the blueprint leg's reopen recipe and the campaign
|
||||
/// leg's single-value-axis injection) so the wording and the parse rule
|
||||
/// cannot drift between them. A malformed token refuses at the usage
|
||||
/// boundary (exit 2) — intake, not a per-leg concern.
|
||||
fn parse_override_tokens(tokens: &[String]) -> Vec<(String, Scalar)> {
|
||||
let mut ov = Vec::new();
|
||||
for tok in tokens {
|
||||
let Some((path_s, val_s)) = tok.split_once('=') else {
|
||||
eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`");
|
||||
std::process::exit(2);
|
||||
};
|
||||
if path_s.trim().is_empty() {
|
||||
eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let val = match val_s.trim().parse::<i64>() {
|
||||
Ok(i) => Scalar::i64(i),
|
||||
Err(_) => match val_s.trim().parse::<f64>() {
|
||||
Ok(f) => Scalar::f64(f),
|
||||
Err(_) => {
|
||||
eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`");
|
||||
std::process::exit(2);
|
||||
}
|
||||
},
|
||||
};
|
||||
ov.push((path_s.trim().to_string(), val));
|
||||
}
|
||||
ov
|
||||
}
|
||||
|
||||
// ============================== clap parser surface ==============================
|
||||
// The declarative argument grammar. clap owns argv tokenizing, scoped `--help`,
|
||||
// `--version`, `--flag=value`, `--`, and long-option abbreviation; the `dispatch_*`
|
||||
@@ -1968,7 +2000,13 @@ document declares its persisted taps itself"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
exit_on_campaign_result(crate::campaign_run::run_campaign(&a.target, env, a.parallel_instruments));
|
||||
let overrides = parse_override_tokens(&a.r#override);
|
||||
exit_on_campaign_result(crate::campaign_run::run_campaign(
|
||||
&a.target,
|
||||
env,
|
||||
a.parallel_instruments,
|
||||
&overrides,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1987,30 +2025,10 @@ document declares its persisted taps itself"
|
||||
/// all (invariant 7 — a real-data or seeded single run is a one-cell
|
||||
/// campaign document).
|
||||
fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &aura_runner::project::Env) {
|
||||
// --override NODE.PARAM=VALUE (#319 Task 3): lexed with the same
|
||||
// int-then-float rule `parse_scalar_csv` uses for `--axis` values.
|
||||
let mut ov: Vec<(String, Scalar)> = Vec::new();
|
||||
for tok in overrides {
|
||||
let Some((path_s, val_s)) = tok.split_once('=') else {
|
||||
eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`");
|
||||
std::process::exit(2);
|
||||
};
|
||||
if path_s.trim().is_empty() {
|
||||
eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let val = match val_s.trim().parse::<i64>() {
|
||||
Ok(i) => Scalar::i64(i),
|
||||
Err(_) => match val_s.trim().parse::<f64>() {
|
||||
Ok(f) => Scalar::f64(f),
|
||||
Err(_) => {
|
||||
eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`");
|
||||
std::process::exit(2);
|
||||
}
|
||||
},
|
||||
};
|
||||
ov.push((path_s.trim().to_string(), val));
|
||||
}
|
||||
// --override NODE.PARAM=VALUE (#319 Task 3): lexed by the shared
|
||||
// `parse_override_tokens` (the campaign leg's injection, Task 4, reuses
|
||||
// the same lexer).
|
||||
let ov = parse_override_tokens(overrides);
|
||||
|
||||
let doc = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {path}: {e}");
|
||||
|
||||
@@ -352,6 +352,7 @@ pub(crate) fn run_sweep_sugar(
|
||||
env,
|
||||
crate::campaign_run::RunPresentation::MemberLinesOnly,
|
||||
aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -397,6 +398,7 @@ pub(crate) fn run_generalize_sugar(
|
||||
&campaign_id,
|
||||
env,
|
||||
aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
&[],
|
||||
)?;
|
||||
|
||||
// Reprint the verb's aggregate line from the recorded cross-instrument
|
||||
@@ -523,6 +525,7 @@ pub(crate) fn run_walkforward_sugar(
|
||||
&campaign_id,
|
||||
env,
|
||||
aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
&[],
|
||||
)?;
|
||||
|
||||
if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) {
|
||||
@@ -690,6 +693,7 @@ pub(crate) fn run_mc_sugar(
|
||||
&campaign_id,
|
||||
env,
|
||||
aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
&[],
|
||||
)?;
|
||||
|
||||
if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) {
|
||||
|
||||
Reference in New Issue
Block a user