audit: #331 cycle close — every authored intake gates the root name, cli_fixed_cost re-pinned

Architect drift review (drift_found) resolved fix-path on all three
items. [high] The sweep/walkforward/mc family builders and
validate_and_register_axes read authored envelope files and
put_blueprint'd them ungated — falsifying the C29 exemption's premise
that the store is populated through gated intakes; all four sites now
gate the root name before the family/axis build (placing the gate
after the build produced a misleading unknown-axis error instead of
the gate refusal — RED-verified), with an e2e pin on the synthetic
sweep route. The decision was gate-the-routes, not weaken-the-prose:
the exemption's premise is the invariant, not a wording choice.
[medium] C24 now states the gate rule as the class it is (the op
intake plus every CLI intake reading an authored envelope from a
file; store read-back exempt, C29). [low] C23 reconciles
names-as-debug-symbols with the render name's one operational role
(trace-directory key — never compilation, identity, or execution
semantics).

Ratify: aura-bench cli_fixed_cost baseline re-pinned
(run_line_fnv 6bb0d796f760d140 -> 9bdbc3acf7b2926a). The moved metric
was caused by the #328 tidy (4474814), which switched single-run
manifest defaults from wrapped to raw axis names after that cycle's
bench rerun had already passed; bbac29d and this cycle's HEAD produce
fingerprint-identical record lines (verified with both binaries on
one scratch project), so this cycle only inherits the stale pin. All
five surfaces fingerprint OK after the re-pin.

refs #331, #328
This commit is contained in:
2026-07-25 04:55:12 +02:00
parent 10570b75f8
commit 4ff85b94e5
6 changed files with 151 additions and 45 deletions
@@ -1,16 +1,16 @@
{
"surface": "cli_fixed_cost",
"metrics": {
"help_ms": 1.5046899999999999,
"run_ms": 3.63305
"help_ms": 1.487171,
"run_ms": 3.376192
},
"fingerprint": "run_line_fnv=6bb0d796f760d140",
"fingerprint": "run_line_fnv=9bdbc3acf7b2926a",
"reps": 3,
"host": {
"hostname": "Raki",
"nproc": 24
},
"profile": "release",
"commit": "9c7f60b",
"date": "2026-07-23"
"commit": "10570b7",
"date": "2026-07-25"
}
+25 -13
View File
@@ -893,19 +893,31 @@ pub(crate) fn composite_from_any(text: &str, env: &aura_runner::project::Env) ->
/// function — C29's "registered artifacts are never retroactively
/// invalidated" stays intact.
///
/// **One deliberate exception:** `aura run <blueprint.json>`'s loaded-blueprint
/// branch (`dispatch_run`, main.rs) also reads a fresh FILE and reaches the
/// same unsanitized `traces/<name>/` seam (via `run_signal_r`/`run_measurement`
/// -> `bind_tap_plan` -> `TraceStore::begin_run`), so it too must gate the root
/// name — but it does NOT route through this wrapper, because its grammar is
/// deliberately narrower than `composite_from_any`'s (envelope-only, no
/// op-script array fallback; a bare `blueprint_from_json` call with its own
/// load-error prose/hint convention). Routing it through this wrapper would
/// silently loosen that grammar to also accept op-scripts. It instead calls
/// [`gate_authored_root_name`] directly, sharing the identical `name_gate` +
/// `name_gate_fault_prose` primitives this wrapper uses — so the refusal wording
/// is byte-identical across all five authored-file-intake routes even though
/// the shape-discrimination step is not shared by `dispatch_run`.
/// **Deliberate exceptions — direct `gate_authored_root_name` callers
/// (main.rs).** A handful of fresh-FILE intakes reach the same unsanitized
/// `traces/<name>/` seam (or `put_blueprint` the loaded envelope straight
/// into the registry) without going through this wrapper, because each
/// already parses the document its own way and only needs the shared root-
/// name gate bolted on, not the shape-discrimination `composite_from_any`
/// does:
/// - `aura run <blueprint.json>` (`dispatch_run`): envelope-only grammar (no
/// op-script fallback), feeds `signal.name()` into
/// `run_signal_r`/`run_measurement` -> `bind_tap_plan` ->
/// `TraceStore::begin_run`.
/// - `validate_and_register_axes` (shared by `generalize`,
/// `sweep --real`, `walkforward --real`, `mc --real`): `put_blueprint`s
/// the loaded envelope by topology hash before any of those four verbs
/// touches an archive.
/// - `run_blueprint_sweep` / `run_blueprint_walkforward` / `run_blueprint_mc`
/// (the synthetic, no-`--real` family builders): same `put_blueprint`
/// 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).
///
/// All of these share the identical `name_gate` + `name_gate_fault_prose`
/// primitives this wrapper uses, so the refusal wording is byte-identical
/// across every authored-file-intake route even though the shape-
/// discrimination step is not shared by them.
pub(crate) fn composite_from_authored_text(
text: &str,
env: &aura_runner::project::Env,
+60 -23
View File
@@ -901,6 +901,18 @@ fn run_blueprint_sweep(
env: &aura_runner::project::Env,
) {
let _ = persist; // reserved for the deferred per-member trace path; the family record below is unconditional
// #331 cycle-close: this fn `put_blueprint`s the loaded fresh-FILE
// envelope into the registry below — gate the root name up front,
// before the family ever builds (a shape-violating name can otherwise
// confuse the wrapped/raw axis resolution the family build performs,
// producing a misleading unrelated error instead of this refusal; see
// `gate_authored_root_name`'s doc comment for the full route list).
let blueprint = 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(blueprint.name()) {
eprintln!("aura: {msg}");
std::process::exit(2);
}
let family = blueprint_sweep_family(doc, axes, &data, env).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(2);
@@ -915,11 +927,7 @@ fn run_blueprint_sweep(
.topology_hash
.clone()
.expect("a blueprint sweep stamps every member's topology_hash");
let canonical = blueprint_to_json(
&blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary"),
)
.expect("a loaded blueprint re-serializes");
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
@@ -947,6 +955,16 @@ fn run_blueprint_walkforward(
doc: &str, axes: &[(String, Vec<Scalar>)], name: &str, data: DataSource, select: Selection,
env: &aura_runner::project::Env,
) {
// #331 cycle-close: this fn `put_blueprint`s the loaded fresh-FILE
// envelope into the registry below — gate the root name up front,
// before the family ever builds (see `run_blueprint_sweep`'s identical
// comment on why this must precede the family build, not just the
// registry write).
let blueprint = 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(blueprint.name()) {
eprintln!("aura: {msg}");
std::process::exit(2);
}
let result = blueprint_walkforward_family(doc, axes, &data, select, env);
let reg = env.registry();
let topo = result.windows[0]
@@ -956,10 +974,7 @@ fn run_blueprint_walkforward(
.topology_hash
.clone()
.expect("a blueprint walk-forward stamps every member's topology_hash");
let canonical = blueprint_to_json(
&blueprint_from_json(doc, &|t| env.resolve(t)).expect("doc parse-validated at the dispatch boundary"),
)
.expect("a loaded blueprint re-serializes");
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(1); });
let id = match reg.append_family(name, FamilyKind::WalkForward, &walkforward_member_reports(&result)) {
Ok(id) => id,
@@ -983,6 +998,17 @@ fn run_blueprint_walkforward(
/// `FamilyKind::MonteCarlo` family (C18/C21 lineage), and print each draw's member line
/// (carrying the seed) plus the aggregate — mirroring `run_blueprint_sweep`.
fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource, env: &aura_runner::project::Env) {
// #331 cycle-close: this fn `put_blueprint`s the loaded fresh-FILE
// envelope into the registry below — gate the root name up front,
// before the family ever builds (see `run_blueprint_sweep`'s identical
// comment on why this must precede the family build, not just the
// registry write).
let blueprint = 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(blueprint.name()) {
eprintln!("aura: {msg}");
std::process::exit(2);
}
let family = blueprint_mc_family(doc, n_seeds, &data, env).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(2);
@@ -995,11 +1021,7 @@ fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource, env:
.topology_hash
.clone()
.expect("a blueprint mc stamps every member's topology_hash");
let canonical = blueprint_to_json(
&blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary"),
)
.expect("a loaded blueprint re-serializes");
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
eprintln!("aura: {e}");
std::process::exit(1);
@@ -1699,13 +1721,19 @@ fn parse_axes(
Ok(axes)
}
/// The three distinguishable ways `validate_and_register_axes` can fail: an
/// unknown axis name is a usage error (exit 2, echoed before the archive is
/// touched); a registry write failure is a runtime error (exit 1); running
/// outside a project (no `Aura.toml` found up from cwd, #218's gate) is also
/// a runtime error (exit 1) — the strategy document cannot resolve against a
/// project store/vocabulary that doesn't exist.
/// The four distinguishable ways `validate_and_register_axes` can fail: a
/// shape-violating root name is a usage error (exit 2, echoed before the
/// blueprint ever reaches the registry — #331 cycle-close: this fn
/// `put_blueprint`s a fresh-FILE envelope and so must gate it exactly like
/// `register`/`run`/the other authored-file intakes, see
/// `gate_authored_root_name`'s doc comment); an unknown axis name is a usage
/// error (exit 2, echoed before the archive is touched); a registry write
/// failure is a runtime error (exit 1); running outside a project (no
/// `Aura.toml` found up from cwd, #218's gate) is also a runtime error —
/// exit 1, since the strategy document cannot resolve against a project
/// store/vocabulary that doesn't exist.
enum AxisRegisterError {
BadName(String),
UnknownAxis(String),
Registry(String),
NoProject(String),
@@ -1738,6 +1766,10 @@ fn validate_and_register_axes(
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary");
// #331 cycle-close: this fn `put_blueprint`s a fresh-FILE envelope below —
// gate the root name before it lands in the registry (shared choke point
// with every other authored-file intake).
graph_construct::gate_authored_root_name(blueprint.name()).map_err(AxisRegisterError::BadName)?;
let raw_bound: HashSet<String> =
blueprint.bound_param_space().into_iter().map(|b| b.name).collect();
for (n, _) in axes {
@@ -1774,11 +1806,16 @@ fn validate_and_register_axes(
/// Exit-map for a `validate_and_register_axes` failure — the stderr line and
/// exit code every campaign dispatcher (sweep/generalize/walkforward/mc)
/// preserves: an unknown axis is a usage error (exit 2, echoed before the
/// archive is touched); a registry write failure or a missing project (no
/// `Aura.toml` found up from cwd, #218's gate) is a runtime error (exit 1).
/// preserves: a shape-violating root name (#331) or an unknown axis is a
/// usage error (exit 2, echoed before the archive is touched); a registry
/// write failure or a missing project (no `Aura.toml` found up from cwd,
/// #218's gate) is a runtime error (exit 1).
fn exit_axis_register_error(e: AxisRegisterError) -> ! {
match e {
AxisRegisterError::BadName(m) => {
eprintln!("aura: {m}");
std::process::exit(2)
}
AxisRegisterError::UnknownAxis(m) => {
eprintln!("aura: {m}");
std::process::exit(2)
+47
View File
@@ -2449,6 +2449,53 @@ fn aura_walkforward_synthetic_blueprint_refuses_a_wrapped_form_axis_name() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#331 cycle-close, high finding): the SYNTHETIC (no `--real`)
/// blueprint sweep reads a fresh-FILE envelope and `put_blueprint`s it into
/// the registry unconditionally (`run_blueprint_sweep`, keyed by the
/// family's shared topology hash) — before this fix that write was UNGATED,
/// so `aura sweep bad.json` could plant a shape-violating root name
/// (`"../x"`) in the store, breaking the C29 exemption's premise that "the
/// store is populated through the gated intakes" (`composite_from_any`'s
/// doc comment). A hand-edited envelope whose root name violates
/// `name_gate` must now refuse with the shared `name_gate_fault_prose`
/// wording (byte-identical to `register`/`run`/etc.) at the site's usual
/// exit code (2, matching every other blueprint-load refusal in this
/// function), and nothing must land in `runs/blueprints`.
#[test]
fn aura_sweep_synthetic_blueprint_refuses_a_hand_crafted_envelope_with_a_bad_root_name() {
let dir = temp_cwd("sweep_synthetic_bad_root_name_refusal");
let fixture_text = std::fs::read_to_string(format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")))
.expect("read r_sma.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(), "--axis", "fast.length=2,4"])
.current_dir(&dir)
.output()
.expect("spawn aura sweep (synthetic, bad root name)");
assert_eq!(
out.status.code(),
Some(2),
"a shape-violating root name must be refused, not registered: 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 not print a member line: {out:?}");
assert!(
!dir.join("runs").exists(),
"a refused root name must leave no store — nothing may land in runs/blueprints"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#247, #328 render-seam echo): every sweep-verb boundary
/// `BindError` renders as prose in the same register as its unknown-axis
/// sibling (`aura: axis <name>: names no param of this blueprint … — see
@@ -9,7 +9,12 @@ entirely (there is no composite in the flat graph). The blueprint's field /
input-role *names* are **non-load-bearing** — the wiring resolves by index, and names
survive at most as **informative debug symbols** (exactly as `FieldSpec.name` already
is, C8), kept for tracing / rendering (C9 graph-as-data) but carrying no run
semantics. The flat graph is then the target of **behaviour-preserving optimisation**
semantics. (Reconciling with the root composite's own render name, #331: that
claim stands unweakened — the name never influences compilation, identity, or
execution semantics — but at *run* time, outside compilation, it keys the trace
directory (`traces/<name>/`); that operational role, not any load-bearing
weight inside the flat graph, is exactly why the authored-blueprint intakes
shape-gate it.) The flat graph is then the target of **behaviour-preserving optimisation**
— any transform that leaves every observable sink trace bit-identical (C1 is the
correctness invariant) — on two levels:
+8 -3
View File
@@ -202,9 +202,14 @@ replayed in order; nodes are referenced **by identifier**, ports as dotted
(#331), script-level and at-most-once (a second refuses); omitted, the
built composite keeps the CLI's own seed default (`"graph"`). Gated by a
shared shape check (non-empty, single path segment, no `/`, `\`, `.` or
`..`) applied at both data-borne birth routes — this op intake and the
blueprint-envelope root name — since store read-back (reproduce, `use`
resolution) stays ungated per C29.
`..`) applied at this op intake **plus every CLI intake that reads an
authored blueprint envelope from a file** — `register`, `introspect
--content-id <FILE>`, the bare graph-file viewer, `run`, `introspect
--params <FILE>`, 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.
- `source``{"op":"source","role":<str>,"kind":<ScalarKind>}` — declare a root
source role producing a base column of `kind`.
- `input``{"op":"input","role":<str>}` — declare a root input role (kind inferred