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:
2026-07-04 02:22:36 +02:00
parent 891c40d64b
commit 0bb203a38f
2 changed files with 116 additions and 0 deletions
+61
View File
@@ -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}");
+55
View File
@@ -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