feat(cli): aura campaign run — the executor verb over the MemberRunner driver (0107 tasks 8-9)

campaign_run.rs: target resolution (file is register-then-run sugar;
bare 64-hex is the canonical id — #198 decision 1), project gate before
any store write, zero-fault referential gate, process fetch + v1
preflight, then aura_campaign::execute with the CLI MemberRunner over
the shipped loaded-blueprint convention: per-member blueprint reload,
reduce-mode wrap, unique suffix-join of raw campaign axis names onto
the wrapped param_space, windowed real data via the shipped ms->ns
source seam (absent archive/zero-bar windows -> NoData). Emission:
family_table / selection_report lines gated on the doc's emit list
(names debug_asserted against emit_vocabulary), the campaign_run
record line always, persist_taps deferred LOUDLY on stderr before
execution (F7 lesson), zero-survivor cells noted on stderr with exit 0.
exec_fault_prose/member_fault_prose keep the Debug-free house seam.

Seam tests: 8 campaign_run_* e2e tests incl. outside-project, bogus
target, unknown id, v1-boundary (mc process registers fine, run
refuses), persist_taps ordering, and the gated real-data
sweep->gate->walkforward e2e — which ran its full assert path on this
host (local GER40 2024-09 archive) rather than the data-less skip.
Also: exec.rs campaign-id guard tightened to lowercase hex, matching
is_content_id and the store's self-keyed form (review nit).

Gates: workspace 975/0, clippy -D warnings clean.

refs #198
This commit is contained in:
2026-07-03 21:33:29 +02:00
parent b15ad07208
commit aeb0366aed
7 changed files with 907 additions and 7 deletions
+9 -5
View File
@@ -67,7 +67,7 @@ fn guard_one_mode(cmd: &DocIntrospectCmd, family: &str) {
}
}
fn doc_error_prose(what: &str, e: &DocError) -> String {
pub(crate) fn doc_error_prose(what: &str, e: &DocError) -> String {
match e {
DocError::NotJson(msg) => format!("{what} is not JSON: {msg}"),
DocError::BadFormatVersion(v) => {
@@ -84,7 +84,7 @@ fn doc_error_prose(what: &str, e: &DocError) -> String {
}
}
fn doc_fault_prose(f: &DocFault) -> String {
pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
match f {
DocFault::EmptyPipeline => "the pipeline is empty — a process needs at least one stage".into(),
DocFault::UnknownMetric { stage, metric } => {
@@ -121,7 +121,7 @@ fn read_doc(file: &PathBuf) -> Result<String, String> {
std::fs::read_to_string(file).map_err(|e| format!("cannot read {}: {e}", file.display()))
}
fn fault_block(header: &str, lines: Vec<String>) -> String {
pub(crate) fn fault_block(header: &str, lines: Vec<String>) -> String {
format!("{header}\n {}", lines.join("\n "))
}
@@ -244,9 +244,12 @@ pub enum CampaignSub {
Introspect(DocIntrospectCmd),
/// Register a valid campaign document into the store under the runs root.
Register { file: PathBuf },
/// Execute a stored campaign into a realized run-set (a .json file is
/// register-then-run sugar; the canonical address is the content id).
Run { target: String },
}
fn ref_fault_prose(f: &RefFault) -> String {
pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
match f {
RefFault::ProcessNotFound(id) => format!("process {id} not found in the project store"),
RefFault::StrategyNotFound(id) => format!("strategy {id} not found in the blueprint store"),
@@ -272,6 +275,7 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
introspect_campaign(i)
}
CampaignSub::Register { file } => register_campaign(file, env),
CampaignSub::Run { target } => crate::campaign_run::run_campaign(target, env),
};
if let Err(m) = result {
eprintln!("aura: {m}");
@@ -279,7 +283,7 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
}
}
fn parse_valid_campaign(file: &PathBuf) -> Result<CampaignDoc, String> {
pub(crate) fn parse_valid_campaign(file: &PathBuf) -> Result<CampaignDoc, String> {
let text = read_doc(file)?;
let doc = parse_campaign(&text).map_err(|e| doc_error_prose("campaign document", &e))?;
let faults = validate_campaign(&doc);