feat(aura-core,aura-engine,aura-cli): node-instance naming retires ParamAlias

Every blueprint node now carries a name. A primitive builder gains an optional
instance name (Sma::builder().named("fast")); when omitted it defaults to the
node's type label, ASCII-lowercased verbatim ("SMA"->"sma", "SimBroker"->
"simbroker"). param_space() is now uniformly <node>.<param> at every level
including the root (sma_cross.fast.length, exposure.scale). Fan-in
distinguishability (C9) and signature_of re-key onto the node name: two same-type
siblings both defaulting to "sma" collide and IndistinguishableFanIn fires, so
one act -- naming the legs "fast"/"slow" -- fixes both the naming collision and
the fan-in check. The index-addressed ParamAlias overlay, Composite.params, the
Composite::new 5th argument, aliases_on, and check_alias_indices are removed
(Role.name and OutField.name untouched). Ledger C9 and C23 amended.

This is why the change is correct, not merely convenient: blueprints are
value-empty (C11), so the thing that would otherwise distinguish two SMAs --
their length -- is not present at construction; identity must come from a name,
not a deferred value. Closes the #56 friction surfaced by the param-space &
sweep milestone fieldtest (a freshly-authored 2-SMA cross was unbindable and
would not bootstrap without two hand-counted ParamAlias overlays).

Two plan defects were corrected during implementation and verified:
- the three new fan-in/path tests authored sma_cross at root level, which would
  qualify to "fast.length" (root prefix is empty) and is not the nested case the
  fan-in check inspects; nesting sma_cross under a root (a shared
  sma_cross_under_root helper) restores the asserted "sma_cross.fast.length" and
  IndistinguishableFanIn{node:2};
- three cycle-0030 named-binder tests bound the real harness by the old names
  (sma_cross.fast / scale) and were migrated to the new path strings, surfaced by
  the workspace test gate.

Verified by the orchestrator: cargo build/test --workspace green (198 tests,
0 red), clippy --all-targets -D warnings clean. model_to_json emits the type
label + factory param names (node-name-independent), so sample-model.json and the
graph_model golden are byte-identical (untouched). NodeSchema gained a Default
derive (the builder default-name test constructs an empty schema). fieldtests/
are frozen non-workspace records, not migrated.

closes #56
This commit is contained in:
2026-06-11 11:51:30 +02:00
parent d8900900b5
commit ffed8cc612
8 changed files with 298 additions and 455 deletions
+36 -2
View File
@@ -76,6 +76,7 @@ pub struct ParamSpec {
/// pre-build (#43).
pub struct PrimitiveBuilder {
name: &'static str,
instance_name: Option<String>,
schema: NodeSchema,
// The build closure's type is exactly the recipe contract (a param slice in, a
// boxed node out); a type alias would not clarify it.
@@ -92,7 +93,20 @@ impl PrimitiveBuilder {
schema: NodeSchema,
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
) -> Self {
Self { name, schema, build: Box::new(build) }
Self { name, instance_name: None, schema, build: Box::new(build) }
}
/// Set this node instance's name. Must be non-empty.
pub fn named(mut self, name: &str) -> Self {
debug_assert!(!name.is_empty(), "node name must be non-empty");
self.instance_name = Some(name.to_string());
self
}
/// The resolved node name: the explicit instance name if set, else the
/// type label ASCII-lowercased verbatim (e.g. "SimBroker" -> "simbroker").
pub fn node_name(&self) -> String {
self.instance_name
.clone()
.unwrap_or_else(|| self.name.to_ascii_lowercase())
}
/// The full declared signature (read pre-build by `Composite::param_space`,
/// `BlueprintNode::signature`, and the renderer).
@@ -120,7 +134,7 @@ impl PrimitiveBuilder {
/// case), and an **empty** `output` (`vec![]`) declares a **pure consumer**
/// (sink role, C8) — a node with no output port. Built once at wiring, never on
/// the hot path — the `Vec`s are fine here.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct NodeSchema {
pub inputs: Vec<PortSpec>,
pub output: Vec<FieldSpec>,
@@ -215,6 +229,26 @@ mod tests {
assert_eq!(none.label(), "Sub");
}
#[test]
fn node_name_defaults_to_lowercased_type_label() {
// unnamed → the type label, ASCII-lowercased verbatim (no snake_case)
let unnamed = PrimitiveBuilder::new(
"SimBroker",
NodeSchema::default(),
|_| panic!("not built in this test"),
);
assert_eq!(unnamed.node_name(), "simbroker");
// explicit name overrides
let named = PrimitiveBuilder::new(
"SMA",
NodeSchema::default(),
|_| panic!("not built in this test"),
)
.named("fast");
assert_eq!(named.node_name(), "fast");
}
#[test]
fn primitive_builder_build_runs_the_closure() {
let f = PrimitiveBuilder::new(