feat(0089): construction diagnostics read by-identifier (closes #162)
The cycle-0088 fieldtest found three presentation defects on the construction
op-script surface, all where the surface dropped out of its by-identifier register.
Presentation only — every fault still fires identically (same exit code, same
op/finalize attribution); only the rendered cause changes.
- Item 1 (finalize by-identifier). The holistic finalize fault leaked the raw
index Debug form (`UnconnectedPort { node: 2, slot: 1 }`). `GraphSession::finish()`
now translates the three reachable index-carrying CompileErrors into new
by-identifier OpError variants (UnconnectedPort{node,slot} / RoleKindMismatch{role}
/ UnboundRootRole{role}) using the session's `ids`/`schemas` and the composite's
`input_roles()` — the holistic gate calls (validate_wiring,
check_param_namespace_injective, check_root_roles_bound) stay byte-for-byte
unchanged (C24: no second validator; only the `.map_err` closures changed).
`aura graph build` now prints `finalize: slot sub.rhs is unconnected`.
- Item 2 (bind mismatch prose). `format_op_error`'s BadParam arm matches the
BindOpError variants instead of `{:?}`: `op 1 (add): param fast.length expects I64
but got F64`, matching the connect mismatch register.
- Item 3 (discoverable bind form). `introspect --node` shows the typed-Scalar bind
form: `param length:I64 (bind {"I64": <v>})`.
Item 1 spans both crates atomically (adding OpError variants forces the exhaustive
format_op_error match; the finish() translation flips behavioural test pins in both
crates). Five test touches: the two in-file unit tests, the two E2E twins plan-recon
found that the spec's testing strategy under-enumerated
(construction_e2e.rs, tests/graph_construct.rs), and a new unbound-root-role test;
the implementer added RoleKindMismatch / Ambiguous/UnknownParam coverage too. The
CLI finalize test was tightened (orchestrator) to pin `sub.rhs` and the absence of a
raw `node:` index.
Spec boss-signed on a grounding-check PASS (docs/specs+plans/0089). Verified: full
workspace suite + clippy green; the three messages reproduce exactly against the
built binary on the fieldtest fixtures.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aura_engine::{blueprint_to_json, replay, GraphSession, Op, OpError, Scalar, ScalarKind};
|
||||
use aura_engine::{blueprint_to_json, replay, BindOpError, GraphSession, Op, OpError, Scalar, ScalarKind};
|
||||
use aura_std::{std_vocabulary, std_vocabulary_types};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -86,7 +86,16 @@ fn format_op_error(e: &OpError) -> String {
|
||||
}
|
||||
OpError::SlotAlreadyWired { node, slot } => format!("slot {node}.{slot} already wired"),
|
||||
OpError::WouldCycle { from, to } => format!("connecting {from} -> {to} would close a cycle"),
|
||||
OpError::BadParam { node, err } => format!("bad param on {node}: {err:?}"),
|
||||
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:?}")
|
||||
}
|
||||
},
|
||||
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"),
|
||||
OpError::Incomplete(ce) => format!("{ce:?}"),
|
||||
}
|
||||
}
|
||||
@@ -143,7 +152,7 @@ pub fn introspect_node(type_id: &str) -> Result<String, String> {
|
||||
out.push_str(&format!(" out {}:{:?}\n", field.name, field.kind));
|
||||
}
|
||||
for p in builder.params() {
|
||||
out.push_str(&format!(" param {}:{:?}\n", p.name, p.kind));
|
||||
out.push_str(&format!(" param {}:{:?} (bind {{\"{:?}\": <v>}})\n", p.name, p.kind, p.kind));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -269,7 +278,9 @@ mod tests {
|
||||
]"#;
|
||||
let err = super::build_from_str(doc).unwrap_err();
|
||||
assert!(err.starts_with("finalize: "), "finalize fault, got: {err}");
|
||||
assert!(err.contains("Unconnected"), "names the unconnected slot, got: {err}");
|
||||
assert!(err.contains("sub.rhs") && err.contains("is unconnected"),
|
||||
"names the unconnected slot by-identifier, got: {err}");
|
||||
assert!(!err.contains("node:"), "no raw machine index in the message, got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -281,6 +292,45 @@ mod tests {
|
||||
assert!(super::introspect_node("Nope").is_err(), "rejects an unknown type");
|
||||
}
|
||||
|
||||
/// A bind whose value-kind mismatches the param reads as prose, like the connect
|
||||
/// mismatch — not a Debug struct.
|
||||
#[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");
|
||||
}
|
||||
|
||||
/// A bind to a param the node does not declare reads as prose naming the node
|
||||
/// and the unknown param — not a Debug struct.
|
||||
#[test]
|
||||
fn build_from_str_unknown_bind_param_reads_as_prose() {
|
||||
let doc = r#"[{"op":"add","type":"SMA","as":"fast","bind":{"window":{"I64":2}}}]"#;
|
||||
let err = super::build_from_str(doc).unwrap_err();
|
||||
assert_eq!(err, "op 0 (add): node fast has no param \"window\"");
|
||||
}
|
||||
|
||||
/// The ambiguous-param branch of the bind-error presenter is phrased as prose
|
||||
/// naming the node and the ambiguous param. No std primitive carries two
|
||||
/// same-named params, so the branch is unreachable end-to-end and is pinned on
|
||||
/// the presenter directly.
|
||||
#[test]
|
||||
fn ambiguous_bind_param_reads_as_prose() {
|
||||
let msg = format_op_error(&OpError::BadParam {
|
||||
node: "fast".to_string(),
|
||||
err: BindOpError::AmbiguousParam("length".to_string()),
|
||||
});
|
||||
assert_eq!(msg, "node fast has ambiguous param \"length\"");
|
||||
}
|
||||
|
||||
/// `introspect --node` shows the typed-Scalar bind-value form so a hand-author
|
||||
/// does not have to read source to learn the `{"I64": <v>}` wrapping.
|
||||
#[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\": <v>})"), "shows the bind-value form: {out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn introspect_unwired_reports_open_slots_of_a_partial_document() {
|
||||
let doc = r#"[
|
||||
|
||||
Reference in New Issue
Block a user