feat: construction add names a node via name, mirroring the builder (#157)

The op-script authoring form is meant to be the Rust builder lifted into data —
every op is a 1:1 image of a GraphBuilder method. The one un-builder-ish key was
`add`'s `"as"`: the builder names a node with `.named("fast")`, so the data echo
is `"name"`, not the SQL-ish aliasing word `as`. Renamed the `add` wire key
`as -> name`.

`expose` keeps `as` — there it is a genuine alias (`expose bias.bias as bias`,
mirroring the builder's positional name arg). `source`/`input` keep their terse
verbs: those are plain shortenings of `source_role`/`input_role`, not a foreign
concept the way `as`-for-naming was.

CLI-side only: a one-attribute serde rename on the `OpDoc` Add DTO
(`#[serde(rename = "name")]`), since the engine `Op` is serde-free (the engine
field stays `as_name`, untouched). Updated the op-list test documents to the
`name` key (add ops only; expose's `as` left intact) and added an explicit pin
(`add_names_node_via_name_key`). The cycle-0088 fieldtest corpus
(fieldtests/cycle-0088-construction-op-script/*.json) is left on the old `as`
form as historical cycle-0088 evidence (it is not run by the suite).

Verified: full workspace suite + clippy green; `aura graph build` on a `name`-keyed
doc names the node and emits it in the #155 blueprint.
This commit is contained in:
2026-06-30 12:22:45 +02:00
parent 7ad7f58798
commit 3c4b667955
2 changed files with 37 additions and 20 deletions
+28 -11
View File
@@ -22,7 +22,9 @@ enum OpDoc {
Add {
#[serde(rename = "type")]
type_id: String,
#[serde(rename = "as", default)]
// `name` mirrors the builder's `.named("fast")`; `add`'s node identifier is a
// naming, not an aliasing (contrast `expose`'s `as`, which is a real alias).
#[serde(rename = "name", default)]
as_name: Option<String>,
#[serde(default)]
bind: BTreeMap<String, Scalar>,
@@ -220,8 +222,8 @@ mod tests {
fn opdoc_document_deserializes_and_replays() {
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","as":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","as":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
@@ -243,8 +245,8 @@ mod tests {
fn build_from_str_emits_blueprint_json_for_valid_document() {
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","as":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","as":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
@@ -258,9 +260,24 @@ mod tests {
assert!(json.contains("\"SMA\""), "carries the SMA nodes: {json}");
}
/// `add` names a node via `name` (mirroring the builder's `.named("fast")`),
/// not the old SQL-ish `as`: the identifier resolves in later ops and the
/// emitted #155 blueprint carries it.
#[test]
fn add_names_node_via_name_key() {
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast"},
{"op":"feed","role":"price","into":["fast.series"]},
{"op":"expose","from":"fast.value","as":"out"}
]"#;
let json = super::build_from_str(doc).expect("name-keyed add builds");
assert!(json.contains("\"name\":\"fast\""), "the blueprint carries the node name: {json}");
}
#[test]
fn build_from_str_reports_unknown_node_at_its_op_index() {
let doc = r#"[{"op":"add","type":"SMA","as":"fast"},{"op":"add","type":"Nope"}]"#;
let doc = r#"[{"op":"add","type":"SMA","name":"fast"},{"op":"add","type":"Nope"}]"#;
let err = super::build_from_str(doc).unwrap_err();
assert_eq!(err, "op 1 (add): unknown node type \"Nope\"");
}
@@ -270,8 +287,8 @@ mod tests {
// sub.rhs left unwired -> the holistic 0-arm fires at the finalize step.
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","as":"fast"},
{"op":"add","type":"SMA","as":"slow"},
{"op":"add","type":"SMA","name":"fast"},
{"op":"add","type":"SMA","name":"slow"},
{"op":"add","type":"Sub"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"}
@@ -296,7 +313,7 @@ mod tests {
/// 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 doc = r#"[{"op":"add","type":"SMA","name":"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");
}
@@ -305,7 +322,7 @@ mod tests {
/// 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 doc = r#"[{"op":"add","type":"SMA","name":"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\"");
}
@@ -334,7 +351,7 @@ mod tests {
#[test]
fn introspect_unwired_reports_open_slots_of_a_partial_document() {
let doc = r#"[
{"op":"add","type":"SMA","as":"fast"},
{"op":"add","type":"SMA","name":"fast"},
{"op":"add","type":"Sub"},
{"op":"connect","from":"fast.value","to":"sub.lhs"}
]"#;
+9 -9
View File
@@ -27,8 +27,8 @@ fn run(args: &[&str], stdin_doc: &str) -> (String, String, bool) {
const SIGNAL_DOC: &str = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","as":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","as":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
@@ -43,8 +43,8 @@ const SIGNAL_DOC: &str = r#"[
/// reject. The smallest document that is op-valid yet whole-document-invalid.
const UNCONNECTED_DOC: &str = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","as":"fast"},
{"op":"add","type":"SMA","as":"slow"},
{"op":"add","type":"SMA","name":"fast"},
{"op":"add","type":"SMA","name":"slow"},
{"op":"add","type":"Sub"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"}
@@ -60,7 +60,7 @@ fn graph_build_emits_blueprint_for_a_valid_document() {
#[test]
fn graph_build_fails_fast_at_the_offending_op() {
let doc = r#"[{"op":"add","type":"SMA","as":"fast"},{"op":"add","type":"Nope"}]"#;
let doc = r#"[{"op":"add","type":"SMA","name":"fast"},{"op":"add","type":"Nope"}]"#;
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
assert!(!ok, "non-zero exit");
assert!(stdout.is_empty(), "no blueprint emitted on error: {stdout}");
@@ -88,7 +88,7 @@ fn graph_introspect_node_answers_ports_without_a_build() {
#[test]
fn graph_introspect_unwired_reports_open_slots() {
let doc = r#"[
{"op":"add","type":"SMA","as":"fast"},
{"op":"add","type":"SMA","name":"fast"},
{"op":"add","type":"Sub"},
{"op":"connect","from":"fast.value","to":"sub.lhs"}
]"#;
@@ -151,7 +151,7 @@ fn graph_introspect_node_rejects_an_unknown_type() {
/// bind-error ergonomics at the binary, not merely the in-process presenter.
#[test]
fn graph_build_renders_a_bind_kind_mismatch_as_prose() {
let doc = r#"[{"op":"add","type":"SMA","as":"fast","bind":{"length":{"F64":2.0}}}]"#;
let doc = r#"[{"op":"add","type":"SMA","name":"fast","bind":{"length":{"F64":2.0}}}]"#;
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
assert!(!ok, "non-zero exit on a bad bind");
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
@@ -188,8 +188,8 @@ fn graph_introspect_node_shows_the_typed_bind_form() {
fn graph_build_renders_role_kind_mismatch_at_finalize_by_identifier() {
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"Sub","as":"sub"},
{"op":"add","type":"And","as":"conj"},
{"op":"add","type":"Sub","name":"sub"},
{"op":"add","type":"And","name":"conj"},
{"op":"feed","role":"price","into":["sub.lhs","conj.a"]}
]"#;
let (stdout, stderr, ok) = run(&["graph", "build"], doc);