diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index f39b62b..f2e7677 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -1797,7 +1797,8 @@ introspectable construction service**: a declarative, replayable by-identifier op-script (`aura graph build` / `introspect` over a JSON op-list, the engine `GraphSession` / `replay`) builds a runnable blueprint through validated ops — the engine's construction gates split into *eager* per-op checks (name resolution, edge -kind-match, the double-wire arm, param bind) and *holistic* finalize checks (wiring +kind-match, the double-wire arm, param bind, and acyclicity — a `connect` that would close a +cycle is rejected eagerly, #161) and *holistic* finalize checks (wiring totality, param-namespace injectivity, root-role boundness), **both cadences calling the same extracted predicates** (`edge_kind_check`, the shared resolution helpers, `check_root_roles_bound` — no second validator) — plus build-free introspection over @@ -1806,7 +1807,20 @@ slots). It **emits** the #155 blueprint; acceptance met: a graph built purely th the ops compiles identical (C1) to its Rust-built twin, an invalid op is rejected at the op naming the cause, introspection answers without a build. The engine `Op` stays serde-free (the wire DTO is CLI-side); the vocabulary's enumerable companion -(`std_vocabulary_types`) lives in `aura-std` (no registry, invariant 9). **Remaining** +(`std_vocabulary_types`) lives in `aura-std` (no registry, invariant 9). **Cycle +0089 follow-up (#161 / #162, after the cycle-0088 fieldtest).** The eager acyclicity +gate above (`GraphSession::closes_cycle`, a reachability check at the closing +`connect`) closed a fieldtest-found hole where `graph build` accepted a non-DAG +op-list (invariant 5 / C9). **Lockstep:** it is a *second* home of invariant-5 +alongside the bootstrap Kahn sort (`harness.rs`, `BootstrapError::Cycle`); the two +must co-evolve when the explicit delay/register node — invariant-5's sole legal +feedback — lands, or a valid delay-feedback graph the bootstrap accepts would be +rejected at construction. The holistic *finalize* faults now also read +**by-identifier** (#162): `finish()` translates the index-carrying `CompileError`s +(`UnconnectedPort` / `RoleKindMismatch` / `UnboundRootRole`) into by-identifier +`OpError` variants — still *calling* the unchanged holistic gates (the +no-second-validator lockstep preserved), only translating their result. +**Remaining** (each its own milestone issue): the forward-compat *must-understand* gate (#156); content-addressed topology identity in the manifest (#158, C18); retiring the pre-C24 hard-wired `aura-cli` harnesses (`HarnessKind`, `run_stage1_r`, `*_sweep_family`) once diff --git a/docs/plans/0089-construction-diagnostics-polish.md b/docs/plans/0089-construction-diagnostics-polish.md deleted file mode 100644 index 8571125..0000000 --- a/docs/plans/0089-construction-diagnostics-polish.md +++ /dev/null @@ -1,276 +0,0 @@ -# Construction diagnostics polish (#162) — Implementation Plan - -> **Parent spec:** `docs/specs/0089-construction-diagnostics-polish.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run -> this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** Make the construction op-script surface read by-identifier and as prose -at the three places it currently leaks raw machine indices or Debug structs — -presentation only, no fault is added, suppressed, or re-attributed. - -**Architecture:** Item 1 (by-identifier finalize) spans both crates atomically: -adding `OpError` variants forces the exhaustive `format_op_error` match to gain -arms (compile gate), and the `finish()` translation flips four behavioural test -pins across both crates (test-run gate) — so all of item 1 + its five test touches -land in Task 1. Items 2 & 3 are additive CLI-presenter changes with no existing-test -breakage (Task 2). - -**Tech Stack:** `aura-engine` (`construction.rs` — `OpError`, `GraphSession::finish`), -`aura-cli` (`graph_construct.rs` — `format_op_error`, `introspect_node`), with -`CompileError`/`Role` in `aura-engine/src/blueprint.rs` and `BindOpError` re-exported -at `aura-engine/src/lib.rs:90`. - -**Files this plan creates or modifies:** - -- Modify: `crates/aura-engine/src/construction.rs:44-71` — three new by-identifier `OpError` variants -- Modify: `crates/aura-engine/src/construction.rs:306-317` — `finish()` translates the index-carrying `CompileError`s -- Modify: `crates/aura-engine/src/construction.rs:540-551` — update `finish_reports_incomplete_on_unwired_slot` -- Test: `crates/aura-engine/src/construction.rs` — new `finish_reports_unbound_input_root_role_by_identifier` -- Modify: `crates/aura-cli/src/graph_construct.rs:71-92` — three item-1 presenter arms in `format_op_error` -- Modify: `crates/aura-cli/src/graph_construct.rs:259-273` — update `build_from_str_reports_unconnected_slot_at_finalize` -- Modify: `crates/aura-engine/tests/construction_e2e.rs:114-131` — update `replay_attributes_holistic_fault_to_finalize_step` (recon-found twin) -- Modify: `crates/aura-cli/tests/graph_construct.rs:114` — update `graph_build_reports_a_holistic_fault_at_finalize` (recon-found twin) -- Modify: `crates/aura-cli/src/graph_construct.rs:89` — `BadParam` arm as prose (item 2) -- Modify: `crates/aura-cli/src/graph_construct.rs:146` — `introspect_node` param line bind hint (item 3) -- Modify: `crates/aura-cli/src/graph_construct.rs:9` — add `BindOpError` to the import -- Test: `crates/aura-cli/src/graph_construct.rs` — two new tests (item 2, item 3) - ---- - -## Task 1: Item 1 — by-identifier finalize faults (engine variants + finish + CLI arms + 5 test touches) - -**Files:** -- Modify: `crates/aura-engine/src/construction.rs:44-71` (OpError), `:306-317` (finish), `:540-551` (test) -- Modify: `crates/aura-cli/src/graph_construct.rs:71-92` (format_op_error arms) -- Modify: `crates/aura-engine/tests/construction_e2e.rs:114-131` -- Modify: `crates/aura-cli/tests/graph_construct.rs:114` - -- [ ] **Step 1: Add the three by-identifier `OpError` variants** - -In `crates/aura-engine/src/construction.rs`, insert between `BadParam { … }` (`:67`) -and `Incomplete(CompileError)` (`:70`): - -```rust - /// Holistic totality fault, by-identifier: an interior input slot covered by - /// no edge or role target (the finalize 0-cover arm). - UnconnectedPort { node: String, slot: String }, - /// Holistic role-kind fault, by-identifier: a root input role fans into slots - /// of differing scalar kinds. - RoleKindMismatch { role: String }, - /// Holistic root fault, by-identifier: a root input role has no bound source. - UnboundRootRole { role: String }, -``` - -- [ ] **Step 2: Translate the index-carrying CompileErrors in `finish()`** - -In `construction.rs`, replace the three holistic-gate `.map_err` calls in -`finish()` (`:313-315`). The gate *calls* are unchanged (C24: no second validator); -only the `.map_err` closures change. The stale doc comment at `:302-305` ("Any -fault wraps as `OpError::Incomplete`") must be updated to note the by-identifier -translation. Replace the body from `check_param_namespace_injective` through -`Ok(c)`: - -```rust - check_param_namespace_injective(&c.param_space()).map_err(OpError::Incomplete)?; - validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output()).map_err(|e| match e { - CompileError::UnconnectedPort { node, slot } => OpError::UnconnectedPort { - node: self.ids[node].clone(), - slot: self.schemas[node].inputs[slot].name.clone(), - }, - CompileError::RoleKindMismatch { role } => OpError::RoleKindMismatch { - role: c.input_roles()[role].name.clone(), - }, - other => OpError::Incomplete(other), - })?; - check_root_roles_bound(c.input_roles()).map_err(|e| match e { - CompileError::UnboundRootRole { role } => OpError::UnboundRootRole { - role: c.input_roles()[role].name.clone(), - }, - other => OpError::Incomplete(other), - })?; - Ok(c) -``` - -(`self.ids` / `self.schemas` are not moved by `Composite::new` and stay borrowable; -`c.input_roles()` indexes the role by its finalize index.) - -- [ ] **Step 3: Add the three presenter arms in `format_op_error`** - -In `crates/aura-cli/src/graph_construct.rs`, insert before the `Incomplete(ce)` arm -(`:90`). This is the compile-gate co-land — the new variants make the match -non-exhaustive until these arms exist: - -```rust - OpError::UnconnectedPort { node, slot } => format!("slot {node}.{slot} is unconnected"), - OpError::RoleKindMismatch { role } => format!("input role {role} fans into slots of differing kinds"), - OpError::UnboundRootRole { role } => format!("root input role {role} is unbound"), -``` - -- [ ] **Step 4: Build the workspace (compile gate)** - -Run: `cargo build --workspace 2>&1 | tail -3` -Expected: `Finished` with no `error[E0004]` (the `format_op_error` match is exhaustive again). - -- [ ] **Step 5: Update the in-file engine test `finish_reports_incomplete_on_unwired_slot`** - -In `construction.rs`, the assertion at `:550` currently reads -`assert!(matches!(err, OpError::Incomplete(CompileError::UnconnectedPort { .. })));`. -Replace it (and the test's doc/name stays, but the matched variant is now the -by-identifier one) with: - -```rust - let err = s.finish().err().unwrap(); - assert!(matches!(&err, OpError::UnconnectedPort { node, slot } if node == "sub" && slot == "rhs"), - "finalize names the open slot by-identifier, got {err:?}"); -``` - -- [ ] **Step 6: Add the new unbound-Input-root-role finish test** - -In `construction.rs`'s `#[cfg(test)] mod tests`, add (mirror -`input_role_reserved_and_duplicate_rejected` `:485` for reserving an `Input` role -and `finish_reports_incomplete_on_unwired_slot` `:540` for the finish-error shape; -use the same `session(...)` helper the sibling tests use): - -```rust - /// A reserved `Input` root role that is fed but never source-bound finishes with - /// the by-identifier `UnboundRootRole` (not a raw role index). - #[test] - fn finish_reports_unbound_input_root_role_by_identifier() { - let mut s = session("g"); - s.apply(Op::Input { role: "ext".into() }).unwrap(); - s.apply(Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), bind: vec![] }).unwrap(); - s.apply(Op::Feed { role: "ext".into(), into: vec!["sub.lhs".into(), "sub.rhs".into()] }).unwrap(); - s.apply(Op::Expose { from: "sub.value".into(), as_name: "out".into() }).unwrap(); - let err = s.finish().err().unwrap(); - assert!(matches!(&err, OpError::UnboundRootRole { role } if role == "ext"), - "an unbound Input root role finishes by-identifier, got {err:?}"); - } -``` - -- [ ] **Step 7: Update the engine E2E twin (recon-found)** - -In `crates/aura-engine/tests/construction_e2e.rs`, the assertion at `:131` in -`replay_attributes_holistic_fault_to_finalize_step` matches -`(i, OpError::Incomplete(CompileError::UnconnectedPort { .. })) if i == finalize_index`. -The finalize attribution (`i == finalize_index`) is unchanged; only the wrapped -variant changes. Replace the matched pattern's variant with the by-identifier one, -keeping the index check: - -```rust - assert!(matches!(&err, (i, OpError::UnconnectedPort { .. }) if *i == finalize_index), - "the holistic fault is still attributed to the finalize step, got {err:?}"); -``` - -(Read the test's surrounding lines `:114-131` first to keep the `finalize_index` -binding and any `CompileError` import that becomes unused — drop a now-unused -`CompileError` import if clippy flags it.) - -- [ ] **Step 8: Update the CLI E2E twin (recon-found)** - -In `crates/aura-cli/tests/graph_construct.rs`, the assertion at `:114` in -`graph_build_reports_a_holistic_fault_at_finalize` checks -`stderr.contains("Unconnected")`. The CLI now renders `finalize: slot sub.rhs is -unconnected` (lowercase, by-identifier). Replace that assertion with: - -```rust - assert!(stderr.contains("finalize: slot") && stderr.contains("is unconnected"), - "the holistic fault renders by-identifier, got: {stderr}"); -``` - -(Read the `UNCONNECTED_DOC` const at `:44` to confirm which slot it leaves open; if -that doc leaves `sub.rhs` open the message will be `slot sub.rhs is unconnected`, but -the assertion above is slot-agnostic so it holds regardless.) - -- [ ] **Step 9: Run the full suite + clippy (task gate)** - -Run: `cargo test --workspace 2>&1 | grep -c 'test result: FAILED'` -Expected: `0` - -Run: `cargo clippy --workspace --all-targets -- -D warnings 2>&1 | grep -E 'error|warning|Finished'` -Expected: `Finished` (no error/warning lines). - ---- - -## Task 2: Items 2 & 3 — bind-mismatch prose + discoverable bind form (CLI, additive) - -**Files:** -- Modify: `crates/aura-cli/src/graph_construct.rs:9` (import), `:89` (BadParam arm), `:146` (introspect param line) -- Test: `crates/aura-cli/src/graph_construct.rs` — two new tests - -- [ ] **Step 1: Add `BindOpError` to the import** - -In `crates/aura-cli/src/graph_construct.rs:9`, extend the import (re-export confirmed -at `aura-engine/src/lib.rs:90`): - -```rust -use aura_engine::{blueprint_to_json, replay, BindOpError, GraphSession, Op, OpError, Scalar, ScalarKind}; -``` - -- [ ] **Step 2: Rewrite the `BadParam` arm as prose (item 2)** - -In `format_op_error`, replace the `BadParam` arm at `:89` -(`OpError::BadParam { node, err } => format!("bad param on {node}: {err:?}"),`) with: - -```rust - OpError::BadParam { node, err } => match err { - BindOpError::UnknownParam(p) => format!("node {node} has no param {p:?}"), - BindOpError::AmbiguousParam(p) => format!("node {node} has ambiguous param {p:?}"), - BindOpError::KindMismatch { param, expected, got } => { - format!("param {node}.{param} expects {expected:?} but got {got:?}") - } - }, -``` - -- [ ] **Step 3: Add the bind-form hint to `introspect_node` (item 3)** - -In `introspect_node`, replace the param line at `:146` -(`out.push_str(&format!(" param {}:{:?}\n", p.name, p.kind));`) with: - -```rust - out.push_str(&format!(" param {}:{:?} (bind {{\"{:?}\": }})\n", p.name, p.kind, p.kind)); -``` - -- [ ] **Step 4: Build (compile check)** - -Run: `cargo build -p aura-cli 2>&1 | tail -2` -Expected: `Finished` (no errors). - -- [ ] **Step 5: Add the two CLI tests** - -In `graph_construct.rs`'s `#[cfg(test)] mod tests`, add: - -```rust - /// A bind whose value-kind mismatches the param reads as prose, like the connect - /// mismatch — not a Debug struct (item 2). - #[test] - fn build_from_str_bad_bind_kind_reads_as_prose() { - let doc = r#"[{"op":"add","type":"SMA","as":"fast","bind":{"length":{"F64":2.0}}}]"#; - let err = super::build_from_str(doc).unwrap_err(); - assert_eq!(err, "op 0 (add): param fast.length expects I64 but got F64"); - } - - /// `introspect --node` shows the typed-Scalar bind-value form so a hand-author - /// does not have to read source to learn the `{"I64": }` wrapping (item 3). - #[test] - fn introspect_node_shows_the_bind_form() { - let out = super::introspect_node("SMA").expect("SMA is in the vocabulary"); - assert!(out.contains("(bind {\"I64\": })"), "shows the bind-value form: {out}"); - } -``` - -- [ ] **Step 6: Run the two new tests** - -Run: `cargo test -p aura-cli build_from_str_bad_bind_kind_reads_as_prose 2>&1 | grep -E 'test result|FAILED'` -Expected: `test result: ok. 1 passed` - -Run: `cargo test -p aura-cli introspect_node_shows_the_bind_form 2>&1 | grep -E 'test result|FAILED'` -Expected: `test result: ok. 1 passed` - -- [ ] **Step 7: Full suite + clippy (task gate)** - -Run: `cargo test --workspace 2>&1 | grep -c 'test result: FAILED'` -Expected: `0` - -Run: `cargo clippy --workspace --all-targets -- -D warnings 2>&1 | grep -E 'error|warning|Finished'` -Expected: `Finished` (no error/warning lines). diff --git a/docs/specs/0089-construction-diagnostics-polish.md b/docs/specs/0089-construction-diagnostics-polish.md deleted file mode 100644 index 14ebd25..0000000 --- a/docs/specs/0089-construction-diagnostics-polish.md +++ /dev/null @@ -1,220 +0,0 @@ -# Construction diagnostics polish (#162) — Design Spec - -**Date:** 2026-06-30 -**Status:** Draft — awaiting user spec review -**Authors:** orchestrator + Claude - -## Goal - -Make the construction op-script surface (#157) read consistently **by-identifier** -and as **prose** at the three edges where it currently leaks raw machine indices -or `#[derive(Debug)]` structs. The cycle-0088 fieldtest -(`fieldtests/cycle-0088-construction-op-script/FINDINGS.md`) confirmed the happy -path, `--unwired`, and the eager per-op diagnostics already read well; these three -defects are the remaining inconsistencies. All three are **presentation only** — -every fault is already detected correctly and exits non-zero; this spec changes -*what the message says*, never *whether the fault fires*. - -## Architecture - -The construction surface keeps its two-layer split: the engine (`construction.rs`) -carries faults as `OpError` *data* (by-identifier `String`s), and the CLI -(`graph_construct.rs::format_op_error`) is a thin presenter that renders that data -— the engine error types are `Display`-free by convention. Every `OpError` variant -already carries by-identifier data **except** the holistic-finalize arm -`Incomplete(CompileError)`, which wraps the engine's index-based compile error and -is rendered with `{:?}`. The fix preserves the split: - -- **Item 1** translates the index-carrying finalize `CompileError`s into new - by-identifier `OpError` variants **inside `GraphSession::finish()`** (the layer - that owns the id↔name map), leaving the holistic gates (`validate_wiring`, - `check_param_namespace_injective`, `check_root_roles_bound`) byte-for-byte - unchanged. The CLI gains presenter arms for the new variants. -- **Item 2 & 3** are CLI-presenter-only: `format_op_error`'s `BadParam` arm and - `introspect_node`'s param line. - -## Concrete code shapes - -### Worked author example — before → after (the acceptance evidence) - -Driven through the built CLI on the fieldtest's own fixtures -(`fieldtests/cycle-0088-construction-op-script/`): - -**Item 1 — open interior slot (`c0088_2_partial.json`, `sub.rhs` left unwired):** -``` -# before -$ aura graph build < c0088_2_partial.json -aura: finalize: UnconnectedPort { node: 2, slot: 1 } -# after -aura: finalize: slot sub.rhs is unconnected -``` -(`introspect --unwired` already reports the same slot as `sub.rhs:F64`; the build -finalize fault now agrees instead of forcing the author to count nodes.) - -**Item 2 — bad bind kind (`c0088_3g_bad_bind_kind.json`):** -``` -# before -aura: op 1 (add): bad param on fast: KindMismatch { param: "length", expected: I64, got: F64 } -# after -aura: op 1 (add): param fast.length expects I64 but got F64 -``` -(reads like the parallel connect mismatch `kind mismatch g.value -> fast.series -(producer Bool, consumer F64)`.) - -**Item 3 — discover the bind form:** -``` -# before -$ aura graph introspect --node SMA -SMA - in series:F64 - out value:F64 - param length:I64 -# after -SMA - in series:F64 - out value:F64 - param length:I64 (bind {"I64": }) -``` -(the naive `"bind":{"length":2}` no longer needs source-reading to repair — -the wrapped-Scalar form is shown where the param is declared.) - -### Item 1 — engine: by-identifier finalize variants - -New `OpError` variants (construction.rs), carrying `String` ids like every -sibling variant: -```rust -pub enum OpError { - // … existing … - /// Holistic totality fault, by-identifier: an interior input slot covered by - /// no edge or role target. - UnconnectedPort { node: String, slot: String }, - /// Holistic role-kind fault, by-identifier: a role fans into slots of - /// differing scalar kinds. - RoleKindMismatch { role: String }, - /// Holistic root fault, by-identifier: a root input role has no bound source. - UnboundRootRole { role: String }, - // Incomplete(CompileError) stays — see "stays Incomplete" below. -} -``` - -`finish()` translates the three index-carrying, reachable holistic faults using -the session's still-owned `ids` / `schemas` and the composite's `input_roles()` -(the holistic gate calls themselves are unchanged): -```rust -pub fn finish(self) -> Result { - let roles: Vec = self.roles.into_iter() - .map(|(name, source, targets)| Role { name, targets, source }).collect(); - let c = Composite::new(self.name, self.nodes, self.edges, roles, self.out); - check_param_namespace_injective(&c.param_space()) - .map_err(OpError::Incomplete)?; // DuplicateParamPath: already by-identifier - validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output()) - .map_err(|e| match e { - CompileError::UnconnectedPort { node, slot } => OpError::UnconnectedPort { - node: self.ids[node].clone(), - slot: self.schemas[node].inputs[slot].name.clone(), - }, - CompileError::RoleKindMismatch { role } => OpError::RoleKindMismatch { - role: c.input_roles()[role].name.clone(), - }, - other => OpError::Incomplete(other), // internal/unreachable-on-valid-replay: BadInteriorIndex/OutputPortOutOfRange/Bootstrap/DoubleWiredPort - })?; - check_root_roles_bound(c.input_roles()) - .map_err(|e| match e { - CompileError::UnboundRootRole { role } => OpError::UnboundRootRole { - role: c.input_roles()[role].name.clone(), - }, - other => OpError::Incomplete(other), - })?; - Ok(c) -} -``` - -**Stays `Incomplete(CompileError)` (stated for the validity floor):** -`DuplicateParamPath(String)` already carries a by-identifier path; `DoubleWiredPort` -is caught eagerly by `cover()`/`feed` (`SlotAlreadyWired`) so it is unreachable at -finish; `BadInteriorIndex` / `OutputPortOutOfRange` / `Bootstrap` are -construction-internal inconsistencies a validly-replayed op-list does not produce -and carry no per-node identifier. After this change, every finalize fault a -hand-author can actually trigger reads by-identifier. - -### Item 1 — CLI presenter arms (graph_construct.rs `format_op_error`) -```rust -OpError::UnconnectedPort { node, slot } => format!("slot {node}.{slot} is unconnected"), -OpError::RoleKindMismatch { role } => format!("input role {role} fans into slots of differing kinds"), -OpError::UnboundRootRole { role } => format!("root input role {role} is unbound"), -``` -(`build_from_str` already prefixes a finalize fault with `finalize: `, so the -rendered line is `finalize: slot sub.rhs is unconnected`.) - -### Item 2 — CLI: bind mismatch as prose (graph_construct.rs `format_op_error`) -```rust -// before -OpError::BadParam { node, err } => format!("bad param on {node}: {err:?}"), -// after — match the BindOpError variants, phrased like the connect mismatch -OpError::BadParam { node, err } => match err { - BindOpError::UnknownParam(p) => format!("node {node} has no param {p:?}"), - BindOpError::AmbiguousParam(p) => format!("node {node} has ambiguous param {p:?}"), - BindOpError::KindMismatch { param, expected, got } => - format!("param {node}.{param} expects {expected:?} but got {got:?}"), -}, -``` -(`BindOpError` is imported alongside the existing `OpError` import.) - -### Item 3 — CLI: show the bind form (graph_construct.rs `introspect_node`) -```rust -// before -out.push_str(&format!(" param {}:{:?}\n", p.name, p.kind)); -// after -out.push_str(&format!(" param {}:{:?} (bind {{\"{:?}\": }})\n", p.name, p.kind, p.kind)); -``` - -## Components - -- `crates/aura-engine/src/construction.rs` — three new `OpError` variants; `finish()` - maps the reachable index-carrying `CompileError`s to them. No other engine change; - the holistic gate functions are untouched. -- `crates/aura-cli/src/graph_construct.rs` — `format_op_error` gains the three - item-1 arms and rewrites the `BadParam` arm (item 2); `introspect_node` appends - the bind-form hint (item 3); imports `BindOpError`. - -## Data flow - -Unchanged. A finalize fault still flows `finish() -> Err(OpError) -> replay() -Err((ops.len(), OpError)) -> build_from_str "finalize: {cause}" -> stderr, exit 2`. -Only the `OpError` value (now by-identifier) and its rendered `{cause}` differ. - -## Error handling - -This spec *is* error-message handling. No new failure path is introduced and no -existing fault is suppressed or downgraded: every input that errored before still -errors with the same exit code and the same op/finalize attribution — only the -human-readable cause changes from raw-index/Debug to by-identifier/prose. - -## Testing strategy - -- **Engine (construction.rs):** update `finish_reports_incomplete_on_unwired_slot` - to assert the new `OpError::UnconnectedPort { node, slot }` with `node == "sub"`, - `slot == "rhs"` (was `matches!(.., Incomplete(CompileError::UnconnectedPort{..}))`). - Add a test that an unbound `Input` root role finishes with - `OpError::UnboundRootRole { role }` naming the role. -- **CLI (graph_construct.rs):** update `build_from_str_reports_unconnected_slot_at_finalize` - to assert the message `starts_with("finalize: ")`, `contains("sub.rhs")`, and does - NOT contain `"node:"` (no raw index). Add: a bad-bind-kind doc builds-errs with - `param fast.length expects I64 but got F64` (item 2); `introspect_node("SMA")` - output contains `(bind {"I64": })` (item 3). -- Full gate: `cargo test --workspace`, `cargo build --workspace`, - `cargo clippy --workspace --all-targets -- -D warnings`. - -Note: `std_vocabulary` lives in `aura-std`, a test-only dev-dependency of -`aura-engine`, so the engine-side construction tests drive it the way the existing -ones already do. - -## Acceptance criteria - -- Each of the three before→after worked examples reproduces exactly against the - built CLI on the named fixtures. -- No finalize fault reachable on a well-formed-but-incomplete authored op-list - renders a raw `node:`/`slot:`/`role:` index or a `KindMismatch { … }` Debug - struct; the bind form is visible from `introspect --node`. -- The faults still fire identically (same exit code, same op/finalize index); only - the rendered cause changes. Full workspace suite + clippy green.