From 564a767974ec35b758caac600878ede8a8bfc9e1 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Jul 2026 05:14:50 +0200 Subject: [PATCH] fix(aura-cli): sweep --list-axes joins the gated intake class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fieldtest finding B1: the one remaining authored-envelope FILE route, sweep --list-axes, skipped the root-name gate — a hand-edited "name":"../escape" listed mangled, non-bindable wrapped axis names at exit 0 while every sibling intake refused. The discovery surface now gates before any axis line prints (exit 1, shared prose), restoring both the #331 every-authored-intake claim and #328's discovery-surface identity. RED-first: the e2e refusal test was written and observed failing (exit 0, axis lines printed) before the fix. Re-enumeration of every read_to_string-fed envelope load confirms the class is now closed; C24 additionally ratifies the fieldtest's SG1 — the refusal core sentence is byte-uniform across sites while the leading context prefix varies by site convention. refs #331, #328 --- crates/aura-cli/src/graph_construct.rs | 6 +++ crates/aura-cli/src/main.rs | 18 ++++++++- crates/aura-cli/tests/cli_run.rs | 42 +++++++++++++++++++++ docs/design/contracts/c24-blueprint-data.md | 15 +++++--- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index 6cf2ad6..60a9a68 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -913,6 +913,12 @@ pub(crate) fn composite_from_any(text: &str, env: &aura_runner::project::Env) -> /// reason, on the routes that bypass `validate_and_register_axes` /// entirely (#331 cycle-close — these used to plant an ungated envelope /// in the store; see each fn's own comment). +/// - `list_blueprint_axes` (`aura sweep --list-axes`, main.rs): no +/// registry write and no trace directory here, but a shape-violating root +/// name otherwise mangles through `wrapped_to_raw_axis` into a printed, +/// non-bindable axis name instead of refusing (#331 fieldtest finding +/// c331_2e) — the class rule is every CLI intake reading an authored +/// envelope from a file gates the root name, not only the writing ones. /// /// All of these share the identical `name_gate` + `name_gate_fault_prose` /// primitives this wrapper uses, so the refusal wording is byte-identical diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 0cfdf23..1d2e65d 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -815,15 +815,29 @@ impl IcReport { /// alongside it, so the discovery surface must list it regardless too. Names /// are RAW `.` paths (#328: the wrapped `..` /// form is retired from the surface) — exactly what `--axis` binds. +/// +/// **Gated (#331 fieldtest finding).** This is a FRESH-FILE intake exactly like +/// `register` / `introspect --content-id ` / `introspect --params ` +/// (`composite_from_authored_text`'s doc comment lists the class): a hand-edited +/// envelope with a shape-violating root name (`"../x"`) must be refused before +/// any axis line prints, not mangled through `wrapped_to_raw_axis` into a +/// non-bindable name. Gated immediately after load, before any axis derivation +/// — same placement discipline as the other sites — at exit 1, matching the +/// register-route convention (this route never `put_blueprint`s anything, +/// unlike the exit-2 sweep-EXECUTION route `run_blueprint_sweep` gates). fn list_blueprint_axes(doc: &str, env: &aura_runner::project::Env) { + let signal = blueprint_from_json(doc, &|t| env.resolve(t)) + .expect("doc parse-validated at the dispatch boundary"); + if let Err(msg) = graph_construct::gate_authored_root_name(signal.name()) { + eprintln!("aura: {msg}"); + std::process::exit(1); + } let space = blueprint_axis_probe(doc, env).param_space(); for p in &space { // Strip the wrapper's one leading node segment (#328): open names print // RAW, matching `--axis`'s own accepted namespace. println!("{}:{:?}", aura_runner::axes::wrapped_to_raw_axis(&p.name), p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp } - let signal = blueprint_from_json(doc, &|t| env.resolve(t)) - .expect("doc parse-validated at the dispatch boundary"); for b in signal.bound_param_space() { // `bound_param_space()`'s `.name` is already RAW (#203) — no blueprint-name // concatenation (#328: the blueprint name stays out of axis paths, C23). diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index f4d489d..b6d9e9a 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -5510,6 +5510,48 @@ fn aura_sweep_list_axes_rejects_malformed_blueprint() { assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}"); } +/// Property (#331 fieldtest finding, cycle-331-blueprint-name c331_2e): `--list-axes` +/// is a FRESH-FILE intake exactly like `register` / `introspect --content-id` / +/// `introspect --params`, but its own root-name gate was missing — a hand-edited +/// envelope with a shape-violating root name (`"../x"`) printed mangled, +/// non-bindable axis names instead of being refused. The route now shares +/// `gate_authored_root_name` + `name_gate_fault_prose` and exits 1 — the +/// register-route convention (this route never `put_blueprint`s anything, unlike +/// the exit-2 sweep-EXECUTION route pinned by +/// `aura_sweep_synthetic_blueprint_refuses_a_hand_crafted_envelope_with_a_bad_root_name`) +/// — printing no axis line at all. +#[test] +fn aura_sweep_list_axes_refuses_a_hand_crafted_envelope_with_a_bad_root_name() { + let dir = temp_cwd("sweep_list_axes_bad_root_name_refusal"); + let fixture_text = + std::fs::read_to_string(format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR"))) + .expect("read r_sma_open.json fixture"); + let bad = fixture_text.replacen("\"name\":\"sma_signal\"", "\"name\":\"../x\"", 1); + assert!(bad.contains("\"name\":\"../x\""), "the root-name replace landed: {bad}"); + let file = dir.join("bad-root-name.json"); + std::fs::write(&file, &bad).expect("write envelope fixture"); + + let out = Command::new(BIN) + .args(["sweep", file.to_str().unwrap(), "--list-axes"]) + .current_dir(&dir) + .output() + .expect("spawn aura sweep --list-axes (bad root name)"); + assert_eq!( + out.status.code(), + Some(1), + "a shape-violating root name must be refused (register-route convention), not listed: status={:?} stderr={}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); + assert!( + stderr.contains("blueprint name \"../x\" is invalid") && stderr.contains("must not contain '/' or '\\'"), + "names the offending root name with the shared gate prose: {stderr}" + ); + assert!(out.stdout.is_empty(), "a refused root name must print no axis line: {out:?}"); + let _ = std::fs::remove_dir_all(&dir); +} + /// E2E (#210 c0110 fieldtest, "aura sweep rejects the op-script /// array with a raw serde error"): `aura sweep`'s blueprint slot accepts only /// the built blueprint envelope; an un-built op-script (a JSON array) is diff --git a/docs/design/contracts/c24-blueprint-data.md b/docs/design/contracts/c24-blueprint-data.md index 48aca77..928d1b9 100644 --- a/docs/design/contracts/c24-blueprint-data.md +++ b/docs/design/contracts/c24-blueprint-data.md @@ -205,11 +205,16 @@ replayed in order; nodes are referenced **by identifier**, ports as dotted `..`) applied at this op intake **plus every CLI intake that reads an authored blueprint envelope from a file** — `register`, `introspect --content-id `, the bare graph-file viewer, `run`, `introspect - --params `, and each of `sweep`/`walkforward`/`mc`/`generalize`'s - file-reading entry points (synthetic and `--real` alike) — one class of - intake, one gate. Store read-back (`reproduce`, `use` resolution, - `introspect`/`--params` by content id) stays deliberately ungated — C29: a - registered artifact is never retroactively invalidated. + --params `, `sweep --list-axes`, and each of + `sweep`/`walkforward`/`mc`/`generalize`'s file-reading entry points + (synthetic and `--real` alike) — one class of intake, one gate. The + refusal's core sentence (`name_gate_fault_prose`) is byte-uniform across + every one of these sites; only the leading context prefix varies — + `run` and the bare graph-file viewer prepend `aura: :`, while + `register`, the sweep-axis family (`validate_and_register_axes`), and + `introspect` prepend bare `aura: `. Store read-back (`reproduce`, `use` + resolution, `introspect`/`--params` by content id) stays deliberately + ungated — C29: a registered artifact is never retroactively invalidated. - `source` — `{"op":"source","role":,"kind":}` — declare a root source role producing a base column of `kind`. - `input` — `{"op":"input","role":}` — declare a root input role (kind inferred