feat(cli): aura campaign runs — read-back for stored realizations (fieldtest 0108 F11)
The recorded bootstrap/generalization evidence is no longer write-only:
'aura campaign runs' lists every stored record (campaign id, run
counter, cell count, first cell's stage signature); 'aura campaign
runs <id> [run]' dumps the BARE stored line(s) — the store form,
byte-identical, never the {"campaign_run": ...} run-emit wrapper (the
F10 parsing trap). A lookup, not an action: empty/unmatched notes and
exits 0 (the runs-family convention); outside a project the campaign
family's project refusal.
RED-first (tdd-author handoff): hermetic seed of one hand-written
store line into the fixture project, list + byte-identical dump pinned,
observed failing on clap's unknown-subcommand exit 2.
Gates: workspace 1027/0, clippy -D warnings clean.
closes #206
This commit is contained in:
@@ -287,6 +287,9 @@ pub enum CampaignSub {
|
||||
/// 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 },
|
||||
/// List stored campaign realizations, or dump one campaign's records
|
||||
/// (the bare store lines, not the run-emit wrapper).
|
||||
Runs { campaign: Option<String>, run: Option<usize> },
|
||||
}
|
||||
|
||||
/// The one authoring trap the referential tier can name outright: a ref that
|
||||
@@ -325,6 +328,63 @@ pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
|
||||
}
|
||||
|
||||
/// `aura campaign …` — the dispatch entry main.rs calls.
|
||||
/// `aura campaign runs [CAMPAIGN-ID] [RUN]` (#206): the read-back for stored
|
||||
/// realizations. Without an id: one scannable line per record. With an id
|
||||
/// (optionally narrowed to one run counter): the BARE stored record line(s)
|
||||
/// — the store form, never the `{"campaign_run": …}` run-emit wrapper (the
|
||||
/// 0108 fieldtest F10 parsing trap). A lookup, not an action: an empty store
|
||||
/// or unmatched id notes it and exits 0 (the `runs family` convention).
|
||||
fn campaign_runs(campaign: Option<&str>, run: Option<usize>, env: &Env) -> Result<(), String> {
|
||||
if env.provenance().is_none() {
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|d| d.display().to_string())
|
||||
.unwrap_or_default();
|
||||
return Err(format!(
|
||||
"campaign runs needs a project: the realization store lives under the \
|
||||
project runs root (no Aura.toml found up from {cwd})"
|
||||
));
|
||||
}
|
||||
let records = env.registry().load_campaign_runs().map_err(|e| e.to_string())?;
|
||||
match campaign {
|
||||
None => {
|
||||
if records.is_empty() {
|
||||
println!("no campaign runs recorded");
|
||||
return Ok(());
|
||||
}
|
||||
for r in &records {
|
||||
let signature: Vec<&str> = r
|
||||
.cells
|
||||
.first()
|
||||
.map(|c| c.stages.iter().map(|s| s.block.as_str()).collect())
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
"{} run {} — {} cell(s), {}",
|
||||
r.campaign,
|
||||
r.run,
|
||||
r.cells.len(),
|
||||
if signature.is_empty() { "no stages".to_string() } else { signature.join(" -> ") },
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(id) => {
|
||||
let matched: Vec<_> = records
|
||||
.iter()
|
||||
.filter(|r| r.campaign == id && run.is_none_or(|n| r.run == n))
|
||||
.collect();
|
||||
if matched.is_empty() {
|
||||
println!("no campaign runs recorded for {id}");
|
||||
return Ok(());
|
||||
}
|
||||
for r in matched {
|
||||
// serde round-trip == the stored bytes (compact, declaration
|
||||
// order, sparse fields skipped) — the bare store form.
|
||||
println!("{}", serde_json::to_string(r).map_err(|e| e.to_string())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
let result = match &cmd.sub {
|
||||
CampaignSub::Validate { file } => validate_campaign_file(file, env),
|
||||
@@ -334,6 +394,7 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
}
|
||||
CampaignSub::Register { file } => register_campaign(file, env),
|
||||
CampaignSub::Run { target } => crate::campaign_run::run_campaign(target, env),
|
||||
CampaignSub::Runs { campaign, run } => campaign_runs(campaign.as_deref(), *run, env),
|
||||
};
|
||||
if let Err(m) = result {
|
||||
eprintln!("aura: {m}");
|
||||
|
||||
@@ -845,6 +845,61 @@ fn campaign_run_unknown_id_refuses() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: the recorded campaign realizations are READABLE BACK. A read-back
|
||||
/// verb (`aura campaign runs`) both lists every stored campaign-run record on a
|
||||
/// scannable line carrying its campaign id, and — addressed by that id — dumps
|
||||
/// the BARE persisted store record (the `campaign_runs.jsonl` line form), NOT
|
||||
/// the `{"campaign_run": …}` stdout wrapper the `campaign run` emit uses (F10,
|
||||
/// fieldtest 0108). Today `runs` is an unrecognized campaign subcommand (clap
|
||||
/// exit 2) — the feature is absent.
|
||||
///
|
||||
/// Hermetic seed: one hand-written record line in the exact bare store form
|
||||
/// serde emits (mirroring the aura-registry pre-widening parse line —
|
||||
/// canonical field order, sparse-skip fields omitted — so a re-serialized dump
|
||||
/// is byte-identical). Appending a real record needs a full campaign run over
|
||||
/// data; hand-writing the store line is the only data-free path and pins the
|
||||
/// bare-store-line contract exactly.
|
||||
#[test]
|
||||
fn campaign_runs_lists_and_dumps_the_bare_stored_record() {
|
||||
let _fixture = project_lock();
|
||||
let dir = built_project();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
std::fs::create_dir_all(&runs_dir).expect("create runs dir");
|
||||
let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone())]);
|
||||
|
||||
let campaign_id = "deadbeef".repeat(8); // a 64-hex content id
|
||||
let record_line = format!(
|
||||
r#"{{"campaign":"{campaign_id}","process":"beef","run":0,"seed":42,"cells":[{{"strategy":"3f9c","instrument":"GER40","window_ms":[1725148800000,1727740799999],"stages":[{{"block":"std::sweep","family_id":"sweep-0"}},{{"block":"std::gate","survivor_ordinals":[0,1,2,3]}}]}}]}}"#
|
||||
);
|
||||
std::fs::write(runs_dir.join("campaign_runs.jsonl"), format!("{record_line}\n"))
|
||||
.expect("seed the campaign-run store");
|
||||
|
||||
// LIST mode: one scannable line per stored record, carrying the campaign
|
||||
// id (bare 64-hex or a prefix), exit 0.
|
||||
let (list_out, list_code) = run_code_in(dir, &["campaign", "runs"]);
|
||||
assert_eq!(list_code, Some(0), "list exits 0: {list_out}");
|
||||
assert!(
|
||||
list_out.contains(&campaign_id[..8]),
|
||||
"the list line carries the campaign id (bare or prefix): {list_out}"
|
||||
);
|
||||
|
||||
// DUMP mode: addressed by the campaign content id, prints the BARE stored
|
||||
// record line — the store form, NOT the `{{"campaign_run": …}}` run-emit
|
||||
// wrapper (F10) — byte-identical to the seeded line, exit 0.
|
||||
let (dump_out, dump_code) = run_code_in(dir, &["campaign", "runs", &campaign_id]);
|
||||
assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}");
|
||||
assert!(
|
||||
!dump_out.contains("\"campaign_run\""),
|
||||
"dump prints the bare store record, not the run-emit wrapper (F10): {dump_out}"
|
||||
);
|
||||
assert_eq!(
|
||||
dump_out.trim(),
|
||||
record_line,
|
||||
"dump is byte-identical to the bare stored line (F10): {dump_out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The v2 boundary, order side: `[sweep, monte_carlo, walk_forward]` is
|
||||
/// intrinsically valid — `process register` ACCEPTS it (asserted inside
|
||||
/// `register_process_doc`; the intrinsic-tier ground is the shipped
|
||||
|
||||
Reference in New Issue
Block a user