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;bbac29dand 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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user