feat(aura-cli): surviving-surface prose — scaffold template, concepts help, sweep-vehicle tests

Slice 5 of the #319 retirement. The scaffolder's project CLAUDE.md and the
CLI concepts help now teach only the surviving surface (exec over both
document classes, --override, graph introspect --params for discovery;
traces via exec --tap / presentation.persist_taps). Sweep vehicles moved
onto campaign documents driven by exec (project_new's starter quickstarts,
cli_broken_pipe — whose bare-sweep EPIPE probe was latently vacuous and now
streams a real 4-member campaign, project_sweep_campaign's real-data leg,
graph_construct's gang-axis parity pair as paired one-cell campaigns);
pure discovery sites became graph introspect --params, and the line-identity
test kept its literal --params expectations minus the sweep half. Help pins
rewritten; the scaffold template's in-src pin follows the new bytes.

refs #319
This commit is contained in:
2026-07-25 19:34:07 +02:00
parent 5dc8e03249
commit 24782caaec
8 changed files with 573 additions and 221 deletions
+202 -70
View File
@@ -5,6 +5,9 @@
use std::io::Write;
use std::process::{Command, Stdio};
mod common;
use common::{fresh_project_with_data, ScratchGuard, ScratchPath};
const BIN: &str = env!("CARGO_BIN_EXE_aura");
/// Run `aura <args>` with `stdin_doc` piped in; return (stdout, stderr, success).
@@ -714,26 +717,25 @@ fn graph_params_lists_raw_axis_namespace() {
);
}
/// Property (#328, cycle-close tidy fix): `graph introspect --params` and
/// `aura sweep --list-axes` are two independently-dispatched discovery
/// surfaces over the SAME raw axis namespace — the open pass shares one
/// wrap-strip convention, the bound pass shares one `bound_param_space()` +
/// `render_value` lexicon (`list_blueprint_axes`, main.rs; `params_lines`,
/// this crate). Pinned here as a same-fixture LINE EQUALITY (not just two
/// independently-asserted shapes) so the duplicated formatting cannot
/// silently drift apart across an edit to either surface.
/// Property (#328, #319 retirement residue): `graph introspect --params`
/// prints the raw axis namespace — one `{name}:{kind:?}` line per open param,
/// then each bound param with its default (`list_blueprint_axes`, main.rs;
/// `params_lines`, this crate). This test used to also pin `aura sweep
/// --list-axes` as a byte-identical SECOND discovery surface over the same
/// namespace (guarding the two independently-dispatched surfaces against
/// drift); `sweep` is retired, so only `graph introspect --params`'s own
/// literal output survives here — see `graph_params_lists_raw_axis_namespace`
/// above for the sibling pin over the same fixture.
#[test]
fn graph_params_and_sweep_list_axes_are_line_identical() {
let dir = temp_cwd("params-vs-list-axes");
fn graph_params_prints_raw_axis_names() {
let dir = temp_cwd("params-raw-axis-names");
let bp = fixture("r_sma_open.json");
let (params_out, params_err, params_code) =
run_in(&dir, &["graph", "introspect", "--params", &bp]);
assert_eq!(params_code, Some(0), "graph introspect --params: {params_out} {params_err}");
let (axes_out, axes_err, axes_code) = run_in(&dir, &["sweep", &bp, "--list-axes"]);
assert_eq!(axes_code, Some(0), "sweep --list-axes: {axes_out} {axes_err}");
assert_eq!(
params_out, axes_out,
"the two discovery surfaces must print byte-identical lines for the same blueprint"
params_out, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
"the raw open params, then the raw bound param with its default"
);
}
@@ -1262,13 +1264,85 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
);
}
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
/// `research_docs.rs`'s constant of the same name.
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
"format_version": 1,
"kind": "process",
"name": "sweep-only",
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
}"#;
/// Register `doc` as a process document in `dir`'s project store; returns its
/// id — copied verbatim from `research_docs.rs`'s recipe of the same name.
fn register_process_doc(dir: &std::path::Path, file: &str, doc: &str) -> String {
std::fs::write(dir.join(file), doc).expect("write process doc");
let (out, err, code) = run_in(dir, &["process", "register", file]);
assert_eq!(code, Some(0), "process register failed: {out} {err}");
out.lines()
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
/// A one-strategy campaign document over `bp_id`/`proc_id`, with a raw
/// `axes_json` object literal spliced in verbatim — a campaign document
/// refuses an empty `axes` map ("axes is empty"), so a "closed" comparison
/// cell (nothing to vary) passes a single-value axis over one of its OWN
/// already-bound params at its own default (a no-op reopen, #246: every
/// bound param is an equally re-openable axis). #319: campaigns are `exec`'s
/// surviving surface for binding a genuinely OPEN param to a value —
/// `--override` only reopens an already-BOUND one
/// (`aura_runner::member::override_paths` skips a name already in the open
/// space), so the gang-axis parity tests below run both the closed example
/// and its open-plus-axis twin through this SAME campaign machinery, over
/// the same synthetic archive window, keeping their metrics directly
/// comparable.
fn one_cell_campaign_doc(bp_id: &str, proc_id: &str, window: (i64, i64), axes_json: &str) -> String {
format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "gang-axis-vehicle",
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }}, "axes": {{{axes_json}}} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
from = window.0,
to = window.1,
)
}
/// Execs a one-cell campaign document (written to `dir.join(file)`) and
/// returns its single member's `report.metrics`.
fn one_cell_campaign_metrics(dir: &std::path::Path, file: &str) -> serde_json::Value {
let (out, err, code) = run_in(dir, &["exec", file]);
assert_eq!(code, Some(0), "campaign exec failed: {out} {err}");
let line = out
.lines()
.find(|l| l.starts_with(r#"{"family_id":"#))
.unwrap_or_else(|| panic!("no member line: {out} {err}"));
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
v["report"]["metrics"].clone()
}
/// Property (#61 + #159 cut 2): binding the shipped open example's ganged
/// `channel_length` axis through the REAL `aura sweep` CLI pipeline (argv ->
/// `parse_axes` -> `compile_with_cells`'s gang expansion) reproduces the exact
/// metrics of running the shipped CLOSED example, whose `channel_hi`/
/// `channel_lo` are separately hardwired to the same value. The equivalence
/// between a gang and its member-bound twin is already pinned at the builder/
/// engine level (aura-engine's `gang_e2e.rs`, and this crate's
/// `channel_length` axis through a REAL campaign axis (#319: the surviving
/// surface — `compile_with_cells`'s gang expansion, same as the retired
/// `aura sweep --axis`) reproduces the exact metrics of running the shipped
/// CLOSED example, whose `channel_hi`/`channel_lo` are separately hardwired
/// to the same value. Both legs run as one-cell campaigns over the same
/// synthetic archive window, so the comparison is data-source-agnostic (only
/// the axis-vs-hardwire binding differs). The equivalence between a gang and
/// its member-bound twin is already pinned at the builder/engine level
/// (aura-engine's `gang_e2e.rs`, and this crate's
/// `r_breakout_example_loaded_runs_identically_to_the_carved_signal`, which
/// calls `run_signal_r` directly); this instead drives the actual public
/// binary end-to-end on the actual shipped files, so a regression in the
@@ -1278,25 +1352,53 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
/// equivalence still holds.
#[test]
fn open_r_breakout_fixture_gang_axis_matches_the_closed_example() {
let closed_dir = temp_cwd("r-breakout-gang-axis-closed");
let (closed_stdout, closed_stderr, closed_code) = run_in(&closed_dir, &["exec", &example("r_breakout.json")]);
assert_eq!(closed_code, Some(0), "closed run stderr: {closed_stderr}");
let closed: serde_json::Value =
serde_json::from_str(closed_stdout.trim()).expect("closed run report parses as JSON");
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),
ScratchPath::File(dir.join("breakout-gang.process.json")),
ScratchPath::File(dir.join("breakout-gang-closed.campaign.json")),
ScratchPath::File(dir.join("breakout-gang-open.campaign.json")),
]);
let sweep_dir = temp_cwd("r-breakout-gang-axis-sweep");
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
&sweep_dir,
&["sweep", &fixture("r_breakout_open.json"), "--axis", "channel_length=3"],
);
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
let lines: Vec<&str> = sweep_stdout.lines().collect();
assert_eq!(lines.len(), 1, "one grid point for a single-value axis: {sweep_stdout}");
let member: serde_json::Value = serde_json::from_str(lines[0]).expect("member line parses as JSON");
let (reg_closed_out, reg_closed_err, reg_closed_code) =
run_in(&dir, &["graph", "register", &example("r_breakout.json")]);
assert_eq!(reg_closed_code, Some(0), "register closed: {reg_closed_out} {reg_closed_err}");
let closed_id = registered_id(&reg_closed_out);
let (reg_open_out, reg_open_err, reg_open_code) =
run_in(&dir, &["graph", "register", &fixture("r_breakout_open.json")]);
assert_eq!(reg_open_code, Some(0), "register open: {reg_open_out} {reg_open_err}");
let open_id = registered_id(&reg_open_out);
let proc_id = register_process_doc(&dir, "breakout-gang.process.json", SWEEP_ONLY_PROCESS_DOC);
// The shared SYMA window used throughout the suite's campaign vehicles
// (inside `fresh_project_with_data`'s Jan-Aug 2024 synthetic archive).
let window = (1709251200000, 1719791999999);
std::fs::write(
dir.join("breakout-gang-closed.campaign.json"),
// No-op axis: `delay.lag` reopened at its own bound default (1).
one_cell_campaign_doc(&closed_id, &proc_id, window, r#""delay.lag": { "kind": "I64", "values": [1] }"#),
)
.expect("write closed campaign doc");
std::fs::write(
dir.join("breakout-gang-open.campaign.json"),
one_cell_campaign_doc(
&open_id,
&proc_id,
window,
r#""channel_length": { "kind": "I64", "values": [3] }"#,
),
)
.expect("write open campaign doc");
let closed_metrics = one_cell_campaign_metrics(&dir, "breakout-gang-closed.campaign.json");
let open_metrics = one_cell_campaign_metrics(&dir, "breakout-gang-open.campaign.json");
assert_eq!(
member["report"]["metrics"], closed["metrics"],
"channel_length=3, bound via the real sweep CLI, must fan out to both channel_hi \
open_metrics, closed_metrics,
"channel_length=3, bound via the campaign axis, must fan out to both channel_hi \
and channel_lo identically to the closed example's hardwired channel=3"
);
}
@@ -1330,39 +1432,69 @@ fn open_r_meanrev_fixture_lists_its_axis_namespace() {
);
}
/// Property (#61 + #159 cut 3): a sweep over TWO axes at once — the ganged
/// `window` knob (fusing `mean_window.length`/`var_window.length`) alongside
/// the independent, un-ganged `band.factor` knob — binds both correctly
/// through the real CLI grid, matching the shipped closed example's hardwired
/// window=3/band=2.0. This is a distinct risk from the r_breakout gang test
/// (a lone gang axis): a mis-scoped gang-expansion map that shifts sibling
/// param positions could silently mis-bind the co-present un-ganged axis
/// instead (or vice-versa) even though each axis alone still resolves.
/// Property (#61 + #159 cut 3): a campaign over TWO axes at once (#319: the
/// surviving surface for what `aura sweep --axis` used to do inline) — the
/// ganged `window` knob (fusing `mean_window.length`/`var_window.length`)
/// alongside the independent, un-ganged `band.factor` knob — binds both
/// correctly through the real CLI grid, matching the shipped closed
/// example's hardwired window=3/band=2.0. This is a distinct risk from the
/// r_breakout gang test (a lone gang axis): a mis-scoped gang-expansion map
/// that shifts sibling param positions could silently mis-bind the
/// co-present un-ganged axis instead (or vice-versa) even though each axis
/// alone still resolves. Both legs run as one-cell campaigns over the same
/// synthetic archive window, so the comparison is data-source-agnostic.
#[test]
fn open_r_meanrev_fixture_gang_plus_plain_axis_matches_the_closed_example() {
let closed_dir = temp_cwd("r-meanrev-gang-axis-closed");
let (closed_stdout, closed_stderr, closed_code) = run_in(&closed_dir, &["exec", &example("r_meanrev.json")]);
assert_eq!(closed_code, Some(0), "closed run stderr: {closed_stderr}");
let closed: serde_json::Value =
serde_json::from_str(closed_stdout.trim()).expect("closed run report parses as JSON");
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),
ScratchPath::File(dir.join("meanrev-gang.process.json")),
ScratchPath::File(dir.join("meanrev-gang-closed.campaign.json")),
ScratchPath::File(dir.join("meanrev-gang-open.campaign.json")),
]);
let sweep_dir = temp_cwd("r-meanrev-gang-axis-sweep");
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
&sweep_dir,
&[
"sweep", &fixture("r_meanrev_open.json"),
"--axis", "window=3",
"--axis", "band.factor=2.0",
],
);
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
let lines: Vec<&str> = sweep_stdout.lines().collect();
assert_eq!(lines.len(), 1, "one grid point for two single-value axes: {sweep_stdout}");
let member: serde_json::Value = serde_json::from_str(lines[0]).expect("member line parses as JSON");
let (reg_closed_out, reg_closed_err, reg_closed_code) =
run_in(&dir, &["graph", "register", &example("r_meanrev.json")]);
assert_eq!(reg_closed_code, Some(0), "register closed: {reg_closed_out} {reg_closed_err}");
let closed_id = registered_id(&reg_closed_out);
let (reg_open_out, reg_open_err, reg_open_code) =
run_in(&dir, &["graph", "register", &fixture("r_meanrev_open.json")]);
assert_eq!(reg_open_code, Some(0), "register open: {reg_open_out} {reg_open_err}");
let open_id = registered_id(&reg_open_out);
let proc_id = register_process_doc(&dir, "meanrev-gang.process.json", SWEEP_ONLY_PROCESS_DOC);
let window = (1709251200000, 1719791999999);
std::fs::write(
dir.join("meanrev-gang-closed.campaign.json"),
// No-op axis: `mean_window.length` reopened at its own bound default (3).
one_cell_campaign_doc(
&closed_id,
&proc_id,
window,
r#""mean_window.length": { "kind": "I64", "values": [3] }"#,
),
)
.expect("write closed campaign doc");
std::fs::write(
dir.join("meanrev-gang-open.campaign.json"),
one_cell_campaign_doc(
&open_id,
&proc_id,
window,
r#""window": { "kind": "I64", "values": [3] }, "band.factor": { "kind": "F64", "values": [2.0] }"#,
),
)
.expect("write open campaign doc");
let closed_metrics = one_cell_campaign_metrics(&dir, "meanrev-gang-closed.campaign.json");
let open_metrics = one_cell_campaign_metrics(&dir, "meanrev-gang-open.campaign.json");
assert_eq!(
member["report"]["metrics"], closed["metrics"],
"window=3 (ganged) + band.factor=2.0 (plain), bound via the real sweep CLI, must \
open_metrics, closed_metrics,
"window=3 (ganged) + band.factor=2.0 (plain), bound via the campaign axes, must \
match the closed example's hardwired window=3/band=2.0"
);
}
@@ -2076,10 +2208,10 @@ fn graph_build_use_docless_source_refuses_with_the_c29_shape_exit_1() {
/// The worked acceptance flow's last leg (spec §Concrete code shapes): a
/// pattern registered with an open param splices under an instance, and
/// `aura sweep --list-axes` on the CONSUMER shows the path-qualified bare
/// (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the existing
/// nested-composite param-prefix discipline, reached through `use` this
/// time, not a Rust-authored nested composite.
/// `aura graph introspect --params` on the CONSUMER shows the path-qualified
/// bare (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the
/// existing nested-composite param-prefix discipline, reached through `use`
/// this time, not a Rust-authored nested composite.
#[test]
fn graph_build_use_end_to_end_axes() {
let dir = temp_cwd("use-end-to-end-axes");
@@ -2114,8 +2246,8 @@ fn graph_build_use_end_to_end_axes() {
std::fs::write(&consumer_bp, &build.stdout).expect("write consumer envelope");
let (axes_out, axes_err, axes_code) =
run_in(&dir, &["sweep", consumer_bp.to_str().unwrap(), "--list-axes"]);
assert_eq!(axes_code, Some(0), "list-axes: {axes_out} {axes_err}");
run_in(&dir, &["graph", "introspect", "--params", consumer_bp.to_str().unwrap()]);
assert_eq!(axes_code, Some(0), "graph introspect --params: {axes_out} {axes_err}");
assert_eq!(
axes_out, "trend.sma.length:I64\n",
"the spliced instance's open param surfaces path-qualified, bare (unbound) RAW form (#328)"