plan: 0036 sample-showcase-blueprint
Five tasks for the sample-showcase cycle: (1) the enriched blueprint source — LinComb import, the `signals` composite (trend = sma_cross, momentum = macd, blend = LinComb(3) with weights[2] bound), the node-0 swap in sample_blueprint_with_sinks, the re-pathed sweep_family axes + a showcase_prices warm-up stream; (2) re-path the two in-crate tests (bootstrap-runs-drains, sweep odometer); (3) re-capture the sample-model.json render fixture; (4) a new headless depth-2 nesting guard (viewer_nested_depth.mjs + .rs); (5) the workspace build/test/clippy gate. refs #62
This commit is contained in:
@@ -0,0 +1,497 @@
|
||||
# Sample Showcase Blueprint — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0036-sample-showcase-blueprint.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Enrich the `aura graph` / `aura sweep` sample blueprint into a "trend +
|
||||
momentum, weighted blend" strategy that demonstrates multiply-nested composites, a
|
||||
multi-param node inside a composite, a multi-output (MACD-like) node, and `bind()`.
|
||||
|
||||
**Architecture:** A new `signals` composite (root → `signals` → {`trend` =
|
||||
`sma_cross`, `momentum` = `macd`}, plus a `LinComb(3)` `blend`) replaces the bare
|
||||
`sma_cross` at node 0 of `sample_blueprint_with_sinks`. Pure authoring over
|
||||
already-shipped primitives — no engine/viewer change. The sweep re-paths its axes
|
||||
to the new param-space; a longer warm-up stream lets the enriched signal run; the
|
||||
render fixture is re-captured; a new headless guard protects depth-2 nesting.
|
||||
|
||||
**Tech Stack:** `crates/aura-cli/src/main.rs` (the sample + sweep + their in-crate
|
||||
tests), `crates/aura-cli/tests/fixtures/sample-model.json` (render golden), a new
|
||||
`crates/aura-cli/tests/` headless guard pair. Reuses `aura_std::LinComb` + the
|
||||
existing `sma_cross`/`macd` builders. No change to `aura-engine` or
|
||||
`graph-viewer.js`.
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-cli/src/main.rs:17` — add `LinComb` to the `aura_std` import.
|
||||
- Modify: `crates/aura-cli/src/main.rs` — add `fn signals(name: &str) -> Composite`
|
||||
and `fn showcase_prices() -> Vec<(Timestamp, Scalar)>`; swap node 0 of
|
||||
`sample_blueprint_with_sinks` (`:173`); re-path the `sweep_family` axes (`:228-230`)
|
||||
and swap its closure stream (`:236`).
|
||||
- Modify: `crates/aura-cli/src/main.rs:503-540` — re-path the two in-crate tests
|
||||
(`sample_blueprint_with_sinks_bootstraps_runs_and_drains`,
|
||||
`sweep_report_renders_four_points_in_odometer_order`).
|
||||
- Modify: `crates/aura-cli/tests/fixtures/sample-model.json` — re-captured enriched model.
|
||||
- Create: `crates/aura-cli/tests/viewer_nested_depth.mjs` — headless depth-2 DOT-id guard.
|
||||
- Create: `crates/aura-cli/tests/viewer_nested_depth.rs` — `.rs` wrapper that runs the `.mjs` under `node`.
|
||||
|
||||
**Untouched (must stay green):** `run_sample`/`sample_harness`, `run_macd`/`macd()`/
|
||||
`macd_point`, `crates/aura-engine/src/graph_model.rs` `model_golden` (own minimal
|
||||
`sample_root()`), `crates/aura-cli/assets/graph-viewer.js`, `crates/aura-cli/src/render.rs`.
|
||||
|
||||
**Parse-the-bytes gate (planner self-review item 9):** the project declares no
|
||||
spec-validation parsers in its CLAUDE.md project facts → documented no-op. The
|
||||
inlined Rust is source-language (the `implement` compile gate catches it, not the
|
||||
parse gate); the `.mjs` is JS with no declared parser; the fixture JSON is
|
||||
machine-generated (Task 3), not hand-inlined. Gate fires empty.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Enriched blueprint source
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs:17` (import), plus new fns + the
|
||||
`sample_blueprint_with_sinks` node-0 swap (`:173`) + `sweep_family` axes (`:228-230`)
|
||||
and stream (`:236`).
|
||||
|
||||
- [ ] **Step 1: Add `LinComb` to the `aura_std` import**
|
||||
|
||||
Replace `crates/aura-cli/src/main.rs:17`:
|
||||
|
||||
```rust
|
||||
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
use aura_std::{Ema, Exposure, LinComb, Recorder, SimBroker, Sma, Sub};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the `signals` composite builder**
|
||||
|
||||
Insert this fn immediately after `fn sma_cross(...)` (which ends at `:155`), before
|
||||
`fn sample_blueprint_with_sinks`:
|
||||
|
||||
```rust
|
||||
/// The blended signal: a trend leg (SMA-cross) and a momentum leg (MACD), combined
|
||||
/// by a weighted sum. A multiply-nested composite (root → signals → {trend,
|
||||
/// momentum}); the blend is a multi-param node living inside it, with one weight
|
||||
/// bound as a structural constant (so it drops out of the sweepable surface).
|
||||
fn signals(name: &str) -> Composite {
|
||||
Composite::new(
|
||||
name,
|
||||
vec![
|
||||
BlueprintNode::Composite(sma_cross("trend")), // 0 trend leg (one f64 "cross")
|
||||
BlueprintNode::Composite(macd("momentum")), // 1 momentum leg (3 outputs)
|
||||
// 2 blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal].
|
||||
// weights[2] is bound (a fixed signal-line weight) → removed from param_space;
|
||||
// weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.*
|
||||
// (and renders the `blend:` prefix).
|
||||
LinComb::builder(3)
|
||||
.named("blend")
|
||||
.bind("weights[2]", Scalar::F64(0.5))
|
||||
.into(),
|
||||
],
|
||||
vec![
|
||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // trend.cross → blend.term[0]
|
||||
Edge { from: 1, to: 2, slot: 1, from_field: 2 }, // momentum.histogram → blend.term[1]
|
||||
Edge { from: 1, to: 2, slot: 2, from_field: 1 }, // momentum.signal → blend.term[2]
|
||||
],
|
||||
vec![Role {
|
||||
name: "price".into(),
|
||||
targets: vec![
|
||||
Target { node: 0, slot: 0 }, // price → trend role 0
|
||||
Target { node: 1, slot: 0 }, // price → momentum role 0
|
||||
],
|
||||
source: None,
|
||||
}],
|
||||
vec![OutField { node: 2, field: 0, name: "signal".into() }],
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the `showcase_prices` warm-up stream**
|
||||
|
||||
Insert this fn next to `signals` (anywhere in the non-test region; suggested: right
|
||||
after `synthetic_prices`, which ends at `:36`):
|
||||
|
||||
```rust
|
||||
/// A warm-up-adequate synthetic stream for the enriched sample/sweep: ~18 ticks
|
||||
/// rising, falling, then rising again so the trend SMA spread and the MACD
|
||||
/// EMA-of-EMA histogram both warm up and flip sign. It shares the proven warm-up
|
||||
/// profile of `macd_prices` (the enriched sample embeds the same `macd` composite,
|
||||
/// so it needs the same warm-up length); the flat `run_sample` keeps the shorter
|
||||
/// `synthetic_prices`. Deterministic and fixed (C1).
|
||||
fn showcase_prices() -> Vec<(Timestamp, Scalar)> {
|
||||
[
|
||||
1.0000_f64, 1.0008, 1.0021, 1.0039, 1.0062, 1.0090, 1.0083, 1.0061, 1.0034,
|
||||
1.0012, 0.9998, 1.0006, 1.0024, 1.0047, 1.0069, 1.0086, 1.0097, 1.0092,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &p)| (Timestamp(i as i64 + 1), Scalar::F64(p)))
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Swap node 0 of `sample_blueprint_with_sinks`**
|
||||
|
||||
Replace `crates/aura-cli/src/main.rs:173`:
|
||||
|
||||
```rust
|
||||
BlueprintNode::Composite(sma_cross("sma_cross")),
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
BlueprintNode::Composite(signals("signals")),
|
||||
```
|
||||
|
||||
(The other four root nodes, the root edges `:179-184`, and the `price` role `:185-192`
|
||||
are unchanged: `signals` re-exports a single f64 `"signal"`, the same one-output shape
|
||||
`sma_cross` re-exported as `"cross"`, so `signals.out0 → Exposure` keeps working.)
|
||||
|
||||
- [ ] **Step 5: Re-path the `sweep_family` axes**
|
||||
|
||||
Replace the axis chain at `crates/aura-cli/src/main.rs:228-230`:
|
||||
|
||||
```rust
|
||||
bp.axis("sma_cross.fast.length", [2, 3])
|
||||
.axis("sma_cross.slow.length", [4, 5])
|
||||
.axis("exposure.scale", [0.5])
|
||||
```
|
||||
|
||||
with the eight free-param axes (two real on the trend lengths, the rest singletons →
|
||||
still 4 points; `signals.blend.weights[2]` is bound, so it is **not** an axis):
|
||||
|
||||
```rust
|
||||
bp.axis("signals.trend.fast.length", [2, 3])
|
||||
.axis("signals.trend.slow.length", [4, 5])
|
||||
.axis("signals.momentum.fast.length", [2])
|
||||
.axis("signals.momentum.slow.length", [4])
|
||||
.axis("signals.momentum.signal.length", [3])
|
||||
.axis("signals.blend.weights[0]", [1.0])
|
||||
.axis("signals.blend.weights[1]", [1.0])
|
||||
.axis("exposure.scale", [0.5])
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Swap the sweep closure's stream to `showcase_prices`**
|
||||
|
||||
In the same `sweep_family` closure, replace the single call at
|
||||
`crates/aura-cli/src/main.rs:236`:
|
||||
|
||||
```rust
|
||||
let prices = synthetic_prices();
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
let prices = showcase_prices();
|
||||
```
|
||||
|
||||
(The rest of the closure — window, run, drain, fold — is unchanged.)
|
||||
|
||||
- [ ] **Step 7: Build + clippy gate (source only)**
|
||||
|
||||
Run: `cargo build -p aura-cli`
|
||||
Expected: compiles, `0 errors` (the in-crate tests still reference the old param
|
||||
strings — they compile fine as data; they FAIL at test-time, which Task 2 fixes).
|
||||
|
||||
Run: `cargo clippy -p aura-cli --all-targets -- -D warnings`
|
||||
Expected: clean — `signals` is used by the node-0 swap and `showcase_prices` by the
|
||||
sweep closure, so no `dead_code` warning.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Re-path the in-crate tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs:503-517` (`sample_blueprint_with_sinks_bootstraps_runs_and_drains`)
|
||||
- Modify: `crates/aura-cli/src/main.rs:519-540` (`sweep_report_renders_four_points_in_odometer_order`)
|
||||
|
||||
- [ ] **Step 1: Re-path + re-stream the bootstrap-runs-drains test**
|
||||
|
||||
Replace the body of `sample_blueprint_with_sinks_bootstraps_runs_and_drains`
|
||||
(`:507-514`) — set all eight free params via `.with()` (the bound `weights[2]` is
|
||||
NOT set) and run the warm-up stream:
|
||||
|
||||
```rust
|
||||
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
||||
let mut h = bp
|
||||
.with("signals.trend.fast.length", 2)
|
||||
.with("signals.trend.slow.length", 4)
|
||||
.with("signals.momentum.fast.length", 2)
|
||||
.with("signals.momentum.slow.length", 4)
|
||||
.with("signals.momentum.signal.length", 3)
|
||||
.with("signals.blend.weights[0]", 1.0)
|
||||
.with("signals.blend.weights[1]", 1.0)
|
||||
.with("exposure.scale", 0.5)
|
||||
.bootstrap()
|
||||
.expect("sample blueprint compiles under a valid point");
|
||||
h.run(vec![showcase_prices()]);
|
||||
```
|
||||
|
||||
(The two `assert!(... drained empty)` lines at `:515-516` are unchanged — the
|
||||
18-tick stream warms every leg, so both sinks drain non-empty.)
|
||||
|
||||
- [ ] **Step 2: Re-path the sweep-report odometer assertions**
|
||||
|
||||
Replace the four param-row assertions at `crates/aura-cli/src/main.rs:530-533` with
|
||||
the re-pathed rows (param-space render order: trend.fast, trend.slow, momentum
|
||||
fast/slow/signal, blend weights[0]/[1], exposure.scale — the two trend lengths vary,
|
||||
trend.slow fastest, the rest constant). Each row is one contiguous string:
|
||||
|
||||
```rust
|
||||
assert!(lines[0].contains(r#""params":[["signals.trend.fast.length",2.0],["signals.trend.slow.length",4.0],["signals.momentum.fast.length",2.0],["signals.momentum.slow.length",4.0],["signals.momentum.signal.length",3.0],["signals.blend.weights[0]",1.0],["signals.blend.weights[1]",1.0],["exposure.scale",0.5]]"#), "line0: {}", lines[0]);
|
||||
assert!(lines[1].contains(r#""params":[["signals.trend.fast.length",2.0],["signals.trend.slow.length",5.0],["signals.momentum.fast.length",2.0],["signals.momentum.slow.length",4.0],["signals.momentum.signal.length",3.0],["signals.blend.weights[0]",1.0],["signals.blend.weights[1]",1.0],["exposure.scale",0.5]]"#), "line1: {}", lines[1]);
|
||||
assert!(lines[2].contains(r#""params":[["signals.trend.fast.length",3.0],["signals.trend.slow.length",4.0],["signals.momentum.fast.length",2.0],["signals.momentum.slow.length",4.0],["signals.momentum.signal.length",3.0],["signals.blend.weights[0]",1.0],["signals.blend.weights[1]",1.0],["exposure.scale",0.5]]"#), "line2: {}", lines[2]);
|
||||
assert!(lines[3].contains(r#""params":[["signals.trend.fast.length",3.0],["signals.trend.slow.length",5.0],["signals.momentum.fast.length",2.0],["signals.momentum.slow.length",4.0],["signals.momentum.signal.length",3.0],["signals.blend.weights[0]",1.0],["signals.blend.weights[1]",1.0],["exposure.scale",0.5]]"#), "line3: {}", lines[3]);
|
||||
```
|
||||
|
||||
(Lines `:520-529` — the 4-line count, the `{"manifest":{"commit":"` prefix check —
|
||||
and the metric-key block `:534-539` are unchanged: structurally still 4 points with
|
||||
the same metric keys.)
|
||||
|
||||
- [ ] **Step 3: Run the in-crate tests**
|
||||
|
||||
Run: `cargo test -p aura-cli`
|
||||
Expected: PASS — including `sample_blueprint_with_sinks_bootstraps_runs_and_drains`,
|
||||
`sweep_report_renders_four_points_in_odometer_order`, `sweep_report_is_deterministic`
|
||||
(re-runs the enriched sweep; deterministic), and the untouched `run_sample_*` /
|
||||
`run_macd_*` / `macd_param_space_*` tests. (The integration guards still read the
|
||||
not-yet-re-captured fixture — they stay green on the old fixture until Task 3.)
|
||||
|
||||
Note: if a `sweep_report` row mismatches at runtime, the param **names** are fixed
|
||||
(param-space order above) — re-capture only the **values/order** by reading the
|
||||
actual failing-assert output (`{}` prints the real line) and correcting that row;
|
||||
do not change the param names.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Re-capture the `sample-model.json` fixture
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/tests/fixtures/sample-model.json`
|
||||
|
||||
- [ ] **Step 1: Add a temporary capture test**
|
||||
|
||||
Insert into `crates/aura-cli/src/main.rs` `mod tests` (e.g. after
|
||||
`sample_blueprint_with_sinks_bootstraps_runs_and_drains`):
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn capture_sample_model_tmp() {
|
||||
println!("AURA_MODEL_CAPTURE:{}", aura_engine::model_to_json(&sample_blueprint()));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Capture the enriched model into the fixture**
|
||||
|
||||
Run (one positional filter; prefix-grep isolates the model line, strip it, write the file):
|
||||
|
||||
```
|
||||
cargo test -p aura-cli capture_sample_model_tmp -- --nocapture 2>/dev/null \
|
||||
| grep '^AURA_MODEL_CAPTURE:' | sed 's/^AURA_MODEL_CAPTURE://' \
|
||||
> crates/aura-cli/tests/fixtures/sample-model.json
|
||||
```
|
||||
|
||||
Expected: `crates/aura-cli/tests/fixtures/sample-model.json` is one line of valid
|
||||
JSON whose root node `"0"` is now `{"comp":"signals"}` and whose `"composites"`
|
||||
object carries `signals`, `trend`, `momentum` (and no longer a top-level
|
||||
`sma_cross`). Verify it parses:
|
||||
|
||||
Run: `node -e "JSON.parse(require('fs').readFileSync('crates/aura-cli/tests/fixtures/sample-model.json','utf8')); console.log('OK valid JSON')"`
|
||||
Expected: `OK valid JSON`
|
||||
|
||||
- [ ] **Step 3: Remove the temporary capture test**
|
||||
|
||||
Delete the `capture_sample_model_tmp` test added in Step 1.
|
||||
|
||||
- [ ] **Step 4: Confirm the fixture consumers stay green**
|
||||
|
||||
Run: `cargo test -p aura-cli --test viewer_dot`
|
||||
Expected: PASS (the re-captured fixture still yields valid Graphviz ids at depth 1;
|
||||
`viewer_dot.rs` is the `.rs` wrapper that runs `viewer_dot_ids.mjs`).
|
||||
|
||||
Run: `cargo test -p aura-cli render_html_is_self_contained`
|
||||
Expected: PASS (the live render still contains `"type":"Exposure"` and starts at `{"root":`).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Depth-2 render guard
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/aura-cli/tests/viewer_nested_depth.mjs`
|
||||
- Create: `crates/aura-cli/tests/viewer_nested_depth.rs`
|
||||
|
||||
- [ ] **Step 1: Write the headless depth-2 guard**
|
||||
|
||||
Create `crates/aura-cli/tests/viewer_nested_depth.mjs`:
|
||||
|
||||
```javascript
|
||||
// Headless depth-2 nesting guard for the `aura graph` viewer (graph-viewer.js).
|
||||
//
|
||||
// Property protected: the viewer renders MULTIPLY-nested composites — a composite
|
||||
// inside a composite inside the root — with valid Graphviz node identifiers at
|
||||
// every depth. genDot builds ids as `n` + the node-key path joined by `__`
|
||||
// (graph-viewer.js: `cid = "n" + cpath.join("__")`), so a depth-2 interior node
|
||||
// gets a three-segment id like `n0__0__0`. A digit-led non-numeral id (`0__0__0`)
|
||||
// would be misparsed by Graphviz and collapse the interior; this guard pins that
|
||||
// the `n`-prefix rule holds at depth 2, beyond the depth-1 viewer_dot_ids.mjs guard.
|
||||
//
|
||||
// It loads the real viewer module and the re-captured sample fixture (which is now
|
||||
// the enriched root → signals → {trend, momentum} graph), then EXPANDS RECURSIVELY:
|
||||
// discover the top-level expandable (signals), expand, re-discover the newly
|
||||
// revealed expandables (trend, momentum), expand those — until none remain — and
|
||||
// asserts (a) every DOT id is valid and (b) at least one three-segment (depth-2) id
|
||||
// was actually produced, so the guard cannot pass vacuously on a flat model.
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const model = JSON.parse(readFileSync(join(here, "fixtures", "sample-model.json"), "utf8"));
|
||||
|
||||
// Stub the global BEFORE requiring the viewer (mirrors the browser bootstrap).
|
||||
global.window = { AURA_MODEL: model };
|
||||
const { normalizeModel, genDot } = require(join(here, "..", "assets", "graph-viewer.js"));
|
||||
const ROOT = normalizeModel(model).root;
|
||||
|
||||
// Expand every expandable composite, re-discovering deeper ones each pass, until
|
||||
// the expandable set stops growing (reaches every nesting level).
|
||||
let expanded = new Set();
|
||||
let dot = "";
|
||||
for (let pass = 0; pass < 16; pass++) {
|
||||
const g = genDot(ROOT, "root", expanded, false);
|
||||
dot = g.dot;
|
||||
const fresh = g.expandable.filter((id) => !expanded.has(id));
|
||||
if (!fresh.length) break;
|
||||
fresh.forEach((id) => expanded.add(id));
|
||||
}
|
||||
|
||||
// Collect every node identifier referenced in the final DOT (node-def lines + edge
|
||||
// endpoints), mirroring viewer_dot_ids.mjs.
|
||||
const ids = new Set();
|
||||
for (const m of dot.matchAll(/^\s*([^\s\[]+)\s*\[label=/gm)) ids.add(m[1]);
|
||||
for (const m of dot.matchAll(/(\S+?):[a-z]\w*:[a-z]\s*->\s*(\S+?):[a-z]\w*:[a-z]/g)) {
|
||||
ids.add(m[1]);
|
||||
ids.add(m[2]);
|
||||
}
|
||||
|
||||
const QUOTED = /^".*"$/s;
|
||||
const NUMERAL = /^-?(\.[0-9]+|[0-9]+(\.[0-9]*)?)$/;
|
||||
const BARE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
const isValid = (id) => QUOTED.test(id) || NUMERAL.test(id) || BARE.test(id);
|
||||
|
||||
const bad = [...ids].filter((id) => !isValid(id));
|
||||
if (bad.length) {
|
||||
console.error(
|
||||
"INVALID DOT node identifier(s) at nesting depth — Graphviz will misparse a\n" +
|
||||
"digit-prefixed, non-numeral id and collapse the nested composite:\n " +
|
||||
bad.map((id) => JSON.stringify(id)).join("\n ")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Anti-vacuous: at least one three-segment (depth-2) id must exist, proving the
|
||||
// guard actually expanded two composite levels (root → signals → trend/momentum).
|
||||
const depth2 = [...ids].filter((id) => /^n\d+__\d+__\d+/.test(id));
|
||||
if (!depth2.length) {
|
||||
console.error(
|
||||
"no depth-2 (three-segment `nA__B__C`) id found — the guard did not reach two\n" +
|
||||
"nesting levels; the fixture is not multiply-nested or the expand loop stalled."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`OK — ${ids.size} DOT ids valid; ${depth2.length} reached nesting depth 2.`);
|
||||
process.exit(0);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the `.rs` wrapper**
|
||||
|
||||
Create `crates/aura-cli/tests/viewer_nested_depth.rs` (mirrors `viewer_name_prefix.rs`):
|
||||
|
||||
```rust
|
||||
//! Integration guard: the `aura graph` viewer renders multiply-nested composites
|
||||
//! (a composite inside a composite inside the root) with valid Graphviz node ids
|
||||
//! at every depth.
|
||||
//!
|
||||
//! Like `viewer_dot_ids.rs`/`viewer_dot.rs`, the property lives in JavaScript
|
||||
//! (`genDot` in assets/graph-viewer.js), so this shells out to `node` running the
|
||||
//! headless guard `tests/viewer_nested_depth.mjs`, which loads the real viewer
|
||||
//! module and the re-captured sample fixture and expands it recursively to depth 2.
|
||||
//!
|
||||
//! `node` is REQUIRED: if it is not on PATH this test FAILS (a skipped guard is
|
||||
//! not a guard).
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn viewer_renders_nested_composites_to_depth_2_with_valid_ids() {
|
||||
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("viewer_nested_depth.mjs");
|
||||
assert!(
|
||||
script.exists(),
|
||||
"guard script missing at {}",
|
||||
script.display()
|
||||
);
|
||||
|
||||
let out = match Command::new("node").arg(&script).output() {
|
||||
Ok(out) => out,
|
||||
Err(e) => panic!(
|
||||
"node is required for the viewer depth-2 nesting guard but could not be run \
|
||||
({e}); install Node.js or ensure `node` is on PATH"
|
||||
),
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"viewer depth-2 nesting guard failed (exit {:?}).\n--- node stdout ---\n{stdout}\n--- node stderr ---\n{stderr}",
|
||||
out.status.code()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the new guard**
|
||||
|
||||
Run: `cargo test -p aura-cli --test viewer_nested_depth`
|
||||
Expected: PASS — `OK — N DOT ids valid; M reached nesting depth 2.` (M ≥ 1). This
|
||||
confirms the depth-2 nesting renders with valid ids against the re-captured fixture.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Workspace gate
|
||||
|
||||
**Files:** none (verification only).
|
||||
|
||||
- [ ] **Step 1: Full build**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: compiles, `0 errors`.
|
||||
|
||||
- [ ] **Step 2: Full test suite**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — all crates, including the re-pathed in-crate tests, the re-captured
|
||||
fixture consumers, the new `viewer_nested_depth` guard, and the untouched engine
|
||||
`model_golden` / `nested_composite_inlines` / `param_space_*` tests.
|
||||
|
||||
- [ ] **Step 3: Clippy**
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: clean, no warnings.
|
||||
Reference in New Issue
Block a user