plan: 0096 discover a loaded blueprint's sweepable axes
One iteration, three tasks: (1) extract blueprint_axis_probe and subsume the two existing inline copies of the wrapped-namespace probe (blueprint_sweep_family + blueprint_mc_family) into it — the single source the spec's rationale requires; (2) the --list-axes surface (SweepBlueprintArgs field, valueless-flag parse + standalone guard, list_blueprint_axes, dispatch short-circuit); (3) four E2Es (open lists prefixed names, closed empty, combined-flags reject, malformed-blueprint safety — no panic). refs #169
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
# Discover a loaded blueprint's sweepable axes — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0096-discover-a-loaded-blueprints-sweepable-axes.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to
|
||||
> run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Add `aura sweep <blueprint.json> --list-axes` — a read-only query
|
||||
sub-mode that lists a loaded blueprint's open sweepable knobs (one
|
||||
`<name>:<kind>` per line, exactly as `--axis` names them) then exits.
|
||||
|
||||
**Architecture:** The axis names `--axis` accepts are produced by the sweep's
|
||||
harness-wrapping (`wrap_stage1r(loaded_signal)` nests the signal composite, so
|
||||
`collect_params` prefixes the names — `stage1_signal.fast.length`, not the raw
|
||||
`fast.length`). So the query lands on the SWEEP verb and reuses the SAME probe
|
||||
the sweep resolves against, via a new shared `blueprint_axis_probe` helper that
|
||||
also subsumes the two existing inline copies of that probe
|
||||
(`blueprint_sweep_family`, `blueprint_mc_family`) — one source of truth for the
|
||||
wrapped namespace. All changes are in `crates/aura-cli`; engine/registry
|
||||
untouched (invariants 1/8/9).
|
||||
|
||||
**Tech Stack:** `crates/aura-cli/src/main.rs` (the dispatch, the arg grammar,
|
||||
the two new fns, the probe refactor), `crates/aura-cli/tests/cli_run.rs`
|
||||
(E2Es). Read-only references: `Composite::param_space` /`collect_params`
|
||||
(`crates/aura-engine/src/blueprint.rs`), `ParamSpec`/`ScalarKind`
|
||||
(`crates/aura-core`).
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-cli/src/main.rs` — new `blueprint_axis_probe` +
|
||||
`list_blueprint_axes` fns; `SweepBlueprintArgs.list_axes` field;
|
||||
`parse_sweep_blueprint_args` `--list-axes` recognition + standalone-guard;
|
||||
`["sweep", ..]` dispatch short-circuit; refactor `blueprint_sweep_family`
|
||||
(~:3186-3191) and `blueprint_mc_family` (~:3235-3240) to the shared probe.
|
||||
- Test: `crates/aura-cli/src/main.rs` (`#[cfg(test)] mod tests`, ~:4193) —
|
||||
unit `blueprint_axis_probe` + unit `parse_sweep_blueprint_args` list-axes.
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` — 4 new E2Es beside the existing
|
||||
sweep block (~:3269-3610).
|
||||
|
||||
Line numbers are the current-tree anchors from recon; they may drift ±a few —
|
||||
match on the surrounding code, not the number.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Shared probe helper `blueprint_axis_probe` (single-source the wrapped namespace)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (new fn ~:3166; refactor ~:3186-3191 and ~:3235-3240)
|
||||
- Test: `crates/aura-cli/src/main.rs` (`mod tests`)
|
||||
|
||||
- [ ] **Step 1: Write the failing unit test**
|
||||
|
||||
Add to the `#[cfg(test)] mod tests` block (near the existing
|
||||
`blueprint_sweep_member_equals_single_run_and_shares_topology_hash`). It reads
|
||||
the two committed fixtures via `include_str!` (path relative to `src/main.rs`):
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn blueprint_axis_probe_lists_prefixed_open_knobs() {
|
||||
// The open fixture's two SMA lengths are the sweepable knobs; the probe
|
||||
// wraps the signal (name "stage1_signal") so the names are prefixed —
|
||||
// exactly what `--axis` binds.
|
||||
let open = include_str!("../tests/fixtures/stage1_signal_open.json");
|
||||
let space = blueprint_axis_probe(open).param_space();
|
||||
let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["stage1_signal.fast.length", "stage1_signal.slow.length"]);
|
||||
assert!(space.iter().all(|p| matches!(p.kind, ScalarKind::I64)));
|
||||
|
||||
// A closed blueprint (both lengths bound) has no open axes.
|
||||
let closed = include_str!("../tests/fixtures/stage1_signal.json");
|
||||
assert!(blueprint_axis_probe(closed).param_space().is_empty());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails (RED — unresolved name)**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura blueprint_axis_probe`
|
||||
Expected: FAIL to compile — `cannot find function 'blueprint_axis_probe' in this scope`.
|
||||
|
||||
- [ ] **Step 3: Add the `blueprint_axis_probe` helper**
|
||||
|
||||
Insert just above `fn blueprint_sweep_family` (~:3166):
|
||||
|
||||
```rust
|
||||
/// The exact wrapped probe the loaded-blueprint sweep resolves its axes
|
||||
/// against: the loaded signal wrapped in the stage1-r scaffolding (stop bound,
|
||||
/// reduce, no cost), taps discarded. `param_space()` on it is the axis
|
||||
/// namespace `--axis` binds; `.axis()` consumes it to seed a sweep. Single
|
||||
/// source for the sweep terminal, the MC closed-check, AND `--list-axes`, so
|
||||
/// the listed names track the swept names by construction (incl. across #159's
|
||||
/// harness retirement). The reload is infallible under the SAME
|
||||
/// dispatch-boundary contract the callers already rely on: the doc is
|
||||
/// `blueprint_from_json`-validated at the `["sweep", ..]` / `["mc", ..]`
|
||||
/// boundary before this runs, so a malformed doc has already exited 2 and
|
||||
/// never reaches the `.expect`.
|
||||
fn blueprint_axis_probe(doc: &str) -> Composite {
|
||||
let signal = blueprint_from_json(doc, &|t| std_vocabulary(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||
let (tx_eq, _) = mpsc::channel();
|
||||
let (tx_ex, _) = mpsc::channel();
|
||||
let (tx_r, _) = mpsc::channel();
|
||||
let (tx_req, _) = mpsc::channel();
|
||||
wrap_stage1r(signal, tx_eq, tx_ex, tx_r, tx_req, false, true, None)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Subsume the `blueprint_sweep_family` inline probe**
|
||||
|
||||
In `blueprint_sweep_family`, replace the four `let (tx_*, _) = mpsc::channel();`
|
||||
lines plus `let probe = wrap_stage1r(reload(doc), tx_eq, tx_ex, tx_r, tx_req,
|
||||
false, true, None);` (~:3186-3190) with a single call. The following
|
||||
`let space = probe.param_space();` and the later `probe.axis(...)` stay
|
||||
unchanged (the helper returns the `Composite`, so both the borrow and the
|
||||
consume still work):
|
||||
|
||||
```rust
|
||||
// a single throwaway floated build, only to resolve param_space (borrow) then seed
|
||||
// the named axes (move) — its taps are never drained. The wrapped probe is the one
|
||||
// `blueprint_axis_probe` single-sources (identical `false, true, None` wrap).
|
||||
let probe = blueprint_axis_probe(doc);
|
||||
let space = probe.param_space();
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Subsume the `blueprint_mc_family` inline probe**
|
||||
|
||||
In `blueprint_mc_family`, replace the four `let (tx_*, _) = mpsc::channel();`
|
||||
lines plus `let space = wrap_stage1r(reload(doc), tx_eq, tx_ex, tx_r, tx_req,
|
||||
false, true, None).param_space();` (~:3235-3240) with:
|
||||
|
||||
```rust
|
||||
// probe the wrapped param_space (the same probe the sweep resolves against);
|
||||
// MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
|
||||
let space = blueprint_axis_probe(doc).param_space();
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Verify the new test passes and the refactor is behaviour-preserving**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura blueprint_axis_probe`
|
||||
Expected: PASS (1 test).
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura blueprint_mc_family`
|
||||
Expected: PASS (the existing `blueprint_mc_family_seeds_differ` and
|
||||
`blueprint_mc_family_rejects_an_open_blueprint` — the closed-check still works,
|
||||
proving the MC probe refactor preserved `param_space`).
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura blueprint_sweep_member_equals_single_run_and_shares_topology_hash`
|
||||
Expected: PASS (the sweep probe refactor preserved axis resolution).
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: `Finished` — 0 errors.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: The `--list-axes` surface (arg field, parse grammar, list fn, dispatch)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs` (`SweepBlueprintArgs` ~:3907; `parse_sweep_blueprint_args` ~:3917-3945; new `list_blueprint_axes` fn; dispatch ~:4112-4118)
|
||||
- Test: `crates/aura-cli/src/main.rs` (`mod tests`)
|
||||
|
||||
- [ ] **Step 1: Write the failing unit test**
|
||||
|
||||
Add beside the existing `parse_sweep_blueprint_args_grammar` test:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn parse_sweep_blueprint_args_list_axes_is_standalone() {
|
||||
let a = parse_sweep_blueprint_args(&["--list-axes"]).expect("--list-axes alone parses");
|
||||
assert!(a.list_axes);
|
||||
assert!(a.axes.is_empty());
|
||||
// mutually exclusive with a real sweep, in either order:
|
||||
assert!(parse_sweep_blueprint_args(&["--list-axes", "--axis", "stage1_signal.fast.length=2,3"]).is_err());
|
||||
assert!(parse_sweep_blueprint_args(&["--axis", "stage1_signal.fast.length=2,3", "--list-axes"]).is_err());
|
||||
// a bare sweep (no --list-axes) still requires >= 1 axis and reports list_axes=false:
|
||||
let b = parse_sweep_blueprint_args(&["--axis", "stage1_signal.fast.length=2,3"]).expect("bare sweep parses");
|
||||
assert!(!b.list_axes);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails (RED — no `list_axes` field)**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura parse_sweep_blueprint_args`
|
||||
Expected: FAIL to compile — `no field 'list_axes' on type 'SweepBlueprintArgs'`.
|
||||
|
||||
- [ ] **Step 3: Add the `list_axes` field to `SweepBlueprintArgs`**
|
||||
|
||||
At ~:3907:
|
||||
|
||||
```rust
|
||||
struct SweepBlueprintArgs {
|
||||
axes: Vec<(String, Vec<Scalar>)>,
|
||||
name: String,
|
||||
persist: bool,
|
||||
data: DataChoice,
|
||||
list_axes: bool,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Recognize `--list-axes` (valueless) + standalone-guard in `parse_sweep_blueprint_args`**
|
||||
|
||||
`--list-axes` carries no value, but the loop consumes a value per flag
|
||||
(`t.split_first()`), so handle it *before* that split. Add a `list_axes` local,
|
||||
a valueless arm at the top of the loop, and a post-loop standalone branch.
|
||||
The standalone check is `rest.len() != 1` — `parse` receives only the flags
|
||||
tail, so a lone `--list-axes` means the tail is exactly one token.
|
||||
|
||||
Replace the body from `let mut tail = rest;` through the final `Ok(...)`
|
||||
(~:3922-3944) with:
|
||||
|
||||
```rust
|
||||
let mut list_axes = false;
|
||||
let mut tail = rest;
|
||||
while let Some((flag, t)) = tail.split_first() {
|
||||
if *flag == "--list-axes" {
|
||||
list_axes = true;
|
||||
tail = t;
|
||||
continue;
|
||||
}
|
||||
let (value, t) = t.split_first().ok_or_else(usage)?;
|
||||
if data.accept(flag, value, &usage)? {
|
||||
tail = t;
|
||||
continue;
|
||||
}
|
||||
match *flag {
|
||||
"--axis" => {
|
||||
let (n, csv) = value.split_once('=').ok_or_else(usage)?;
|
||||
if n.is_empty() || axes.iter().any(|(a, _)| a == n) { return Err(usage()); } // empty / duplicate axis
|
||||
let vals = parse_scalar_csv(csv).ok_or_else(usage)?;
|
||||
axes.push((n.to_string(), vals));
|
||||
}
|
||||
"--name" if name.is_none() => name = Some(((*value).to_string(), false)),
|
||||
"--trace" if name.is_none() => name = Some(((*value).to_string(), true)),
|
||||
_ => return Err(usage()),
|
||||
}
|
||||
tail = t;
|
||||
}
|
||||
if list_axes {
|
||||
// A query, not a sweep: it must stand alone (the flags tail is only itself).
|
||||
if rest.len() != 1 {
|
||||
return Err("--list-axes lists axes and takes no other flags".to_string());
|
||||
}
|
||||
return Ok(SweepBlueprintArgs {
|
||||
axes,
|
||||
name: "sweep".to_string(),
|
||||
persist: false,
|
||||
data: data.finish(&usage)?,
|
||||
list_axes: true,
|
||||
});
|
||||
}
|
||||
if axes.is_empty() { return Err(usage()); } // a sweep needs at least one axis
|
||||
let (name, persist) = name.unwrap_or_else(|| ("sweep".to_string(), false));
|
||||
Ok(SweepBlueprintArgs { axes, name, persist, data: data.finish(&usage)?, list_axes: false })
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the `list_blueprint_axes` fn**
|
||||
|
||||
Insert next to `blueprint_axis_probe` (above `blueprint_sweep_family`):
|
||||
|
||||
```rust
|
||||
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
|
||||
/// sweepable knob, in `param_space()` order; a closed blueprint prints nothing.
|
||||
/// Names are exactly what `--axis` binds (same probe as the sweep terminal).
|
||||
fn list_blueprint_axes(doc: &str) {
|
||||
for p in blueprint_axis_probe(doc).param_space() {
|
||||
println!("{}:{:?}", p.name, p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Thread the dispatch destructure + short-circuit**
|
||||
|
||||
In the `["sweep", rest @ ..]` blueprint branch (~:4112-4118), extend the
|
||||
destructure with `list_axes` and short-circuit to the listing before the run:
|
||||
|
||||
```rust
|
||||
let SweepBlueprintArgs { axes, name, persist, data, list_axes } =
|
||||
parse_sweep_blueprint_args(&rest[1..]).unwrap_or_else(|msg| {
|
||||
eprintln!("aura: {msg}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
if list_axes {
|
||||
list_blueprint_axes(&doc);
|
||||
return;
|
||||
}
|
||||
run_blueprint_sweep(&doc, &axes, &name, persist, DataSource::from_choice(data));
|
||||
return;
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Verify the unit test passes and the workspace builds**
|
||||
|
||||
Run: `cargo test -p aura-cli --bin aura parse_sweep_blueprint_args`
|
||||
Expected: PASS (the existing `parse_sweep_blueprint_args_grammar` +
|
||||
`parse_sweep_blueprint_args_list_axes_is_standalone`).
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: `Finished` — 0 errors (the `SweepBlueprintArgs` field addition is
|
||||
threaded at both its construction sites — the two `Ok(SweepBlueprintArgs {…})`
|
||||
in `parse_sweep_blueprint_args` and the dispatch destructure — so the build is
|
||||
whole).
|
||||
|
||||
---
|
||||
|
||||
## Task 3: E2E coverage (the four user-facing behaviours)
|
||||
|
||||
**Files:**
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` (new tests beside the sweep block ~:3269-3610)
|
||||
|
||||
- [ ] **Step 1: Write the four E2Es**
|
||||
|
||||
Use the sweep block's fixture-path convention — an absolute
|
||||
`env!("CARGO_MANIFEST_DIR")` path (cwd-independent; `--list-axes` writes nothing
|
||||
to disk, so no `temp_cwd` is needed). The malformed case mirrors
|
||||
`aura_run_rejects_an_unknown_node_blueprint`.
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn aura_sweep_list_axes_prints_prefixed_names() {
|
||||
let bp = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["sweep", &bp, "--list-axes"])
|
||||
.output()
|
||||
.expect("spawn aura sweep --list-axes");
|
||||
assert_eq!(out.status.code(), Some(0));
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8");
|
||||
assert_eq!(stdout, "stage1_signal.fast.length:I64\nstage1_signal.slow.length:I64\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aura_sweep_list_axes_closed_is_empty() {
|
||||
let bp = format!("{}/tests/fixtures/stage1_signal.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["sweep", &bp, "--list-axes"])
|
||||
.output()
|
||||
.expect("spawn aura sweep --list-axes (closed)");
|
||||
assert_eq!(out.status.code(), Some(0));
|
||||
assert!(out.stdout.is_empty(), "a closed blueprint has no open axes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aura_sweep_list_axes_rejects_other_flags() {
|
||||
let bp = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["sweep", &bp, "--list-axes", "--axis", "stage1_signal.fast.length=2,3"])
|
||||
.output()
|
||||
.expect("spawn aura sweep --list-axes + --axis");
|
||||
assert_eq!(out.status.code(), Some(2));
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(stderr.contains("--list-axes lists axes"), "stderr was: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aura_sweep_list_axes_rejects_malformed_blueprint() {
|
||||
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["sweep", &bp, "--list-axes"])
|
||||
.output()
|
||||
.expect("spawn aura sweep --list-axes (malformed)");
|
||||
assert_eq!(out.status.code(), Some(2), "malformed blueprint must be rejected, not panic");
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(stderr.contains("UnknownNodeType"), "stderr was: {stderr}");
|
||||
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the four E2Es**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run aura_sweep_list_axes`
|
||||
Expected: PASS — 4 tests (`aura_sweep_list_axes_prints_prefixed_names`,
|
||||
`..._closed_is_empty`, `..._rejects_other_flags`, `..._rejects_malformed_blueprint`).
|
||||
The single filter `aura_sweep_list_axes` matches exactly these 4 named tests.
|
||||
|
||||
- [ ] **Step 3: Full-suite + lint gate**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — no regressions across the workspace.
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: `Finished` — 0 warnings.
|
||||
|
||||
---
|
||||
|
||||
## Self-review (planner Step 5)
|
||||
|
||||
1. **Spec coverage:** §Goal/§Architecture → Tasks 1-2 (probe on the sweep verb,
|
||||
prefixed names); §Concrete code shapes → Tasks 1-2 (all four shown shapes:
|
||||
`blueprint_axis_probe`, `list_blueprint_axes`, `SweepBlueprintArgs.list_axes`,
|
||||
dispatch); §Error handling → Task 2 Step 4 (standalone guard) + Task 3 Step 1
|
||||
(malformed + closed cases); §Testing strategy (all four deliverable tests) →
|
||||
Task 1 unit, Task 2 unit, Task 3 E2Es; §Acceptance criteria 1-6 → covered by
|
||||
the E2Es + the build/clippy gate (Task 3 Step 3). ✓
|
||||
2. **Placeholder scan:** no TBD/TODO/"similar to"/"add appropriate". ✓
|
||||
3. **Type/name consistency:** `blueprint_axis_probe`, `list_blueprint_axes`,
|
||||
`SweepBlueprintArgs.list_axes`, `parse_sweep_blueprint_args`,
|
||||
`blueprint_sweep_family`, `blueprint_mc_family`, `wrap_stage1r`, `ScalarKind`
|
||||
consistent across tasks and match the current tree. ✓
|
||||
4. **Step granularity:** each step is one edit or one command. ✓
|
||||
5. **No commit steps.** ✓
|
||||
6. **Pin/replacement contiguity:** the E2E asserts `stdout ==
|
||||
"stage1_signal.fast.length:I64\nstage1_signal.slow.length:I64\n"` and
|
||||
`stderr.contains("--list-axes lists axes")` / `"UnknownNodeType"` — each pinned
|
||||
substring appears contiguously in the corresponding impl (`println!` format
|
||||
and the `Err("--list-axes lists axes and takes no other flags")` literal /
|
||||
the existing engine `UnknownNodeType` error). ✓
|
||||
7. **Compile-gate vs. deferred-caller:** the `SweepBlueprintArgs.list_axes`
|
||||
field addition (Task 2 Step 3) breaks exactly two sites — the two
|
||||
`Ok(SweepBlueprintArgs {…})` returns (threaded in Step 4) and the dispatch
|
||||
destructure (threaded in Step 6) — ALL inside Task 2, before its Step 7 build
|
||||
gate. No caller deferred past a build gate. ✓
|
||||
8. **Verification filters resolve:** `blueprint_axis_probe` (new, Task 1 Step 1),
|
||||
`blueprint_mc_family` (existing `blueprint_mc_family_seeds_differ` +
|
||||
`_rejects_an_open_blueprint`), `blueprint_sweep_member_equals_single_run_and_shares_topology_hash`
|
||||
(existing), `parse_sweep_blueprint_args` (existing `_grammar` + new
|
||||
`_list_axes_is_standalone`), `aura_sweep_list_axes` (the 4 new E2Es, shared
|
||||
prefix) — every filter matches ≥1 named test in the post-task tree. ✓
|
||||
Reference in New Issue
Block a user