feat(0088): §A shared gate predicates — edge_kind_check, resolution helpers, try_bind

Iteration-1 §A of the construction service (#157): the surface-agnostic shared
predicates the eager op-script path and the holistic finalize gates both call —
no second validator (C24).

- edge_kind_check (blueprint.rs): the per-edge producer/consumer kind check,
  lifted verbatim out of validate_wiring's edge loop into a pub(crate) fn that
  validate_wiring now calls — behaviour-preserving (same Bootstrap(KindMismatch)
  variant; the existing compile-time gate test stays green).
- resolve_input_slot / resolve_output_field (builder.rs): the exactly-one-match
  name→index resolution extracted as pub(crate) fns over (&NodeSchema, &str), so
  the runtime-String op-script names share GraphBuilder's resolution; GraphBuilder
  delegates and maps to the unchanged BuildError variants.
- try_bind + BindOpError (aura-core node.rs): the fallible Result twin of bind
  (bind keeps its panic contract and pinned messages verbatim; try_bind is a
  separate method reporting the same three conditions as values).
- check_param_namespace_injective + validate_wiring widened to pub(crate) for the
  finalize-stage reuse by the upcoming GraphSession::finish (§B).

Partial iteration: §B (the GraphSession/Op/replay surface + introspection) and
the §A→§B integration land next (plan Tasks 4-8). compile_with_params /
check_ports_connected are behaviourally unchanged. Full suite green; clippy clean.

Plan correction folded in: Task 3's test originally referenced aura_std::Sma —
infeasible inside aura-core (aura-std depends on aura-core; the reverse edge is a
dependency cycle). Adapted to a local PrimitiveBuilder probe with identical
assertions.

refs #157
This commit is contained in:
2026-06-29 18:38:34 +02:00
parent a5b887a955
commit ea1ca32dbd
5 changed files with 241 additions and 37 deletions
+70 -22
View File
@@ -162,17 +162,14 @@ impl GraphBuilder {
.schemas
.get(p.node)
.ok_or(BuildError::BadHandle { node: p.node })?;
let m: Vec<usize> = schema
.inputs
.iter()
.enumerate()
.filter(|(_, port)| port.name == p.name)
.map(|(i, _)| i)
.collect();
match m.as_slice() {
[i] => Ok((p.node, *i)),
[] => Err(BuildError::UnknownInPort { node: p.node, name: p.name.to_string() }),
_ => Err(BuildError::AmbiguousInPort { node: p.node, name: p.name.to_string() }),
match resolve_input_slot(schema, p.name) {
Ok(i) => Ok((p.node, i)),
Err(PortResolveError::Unknown) => {
Err(BuildError::UnknownInPort { node: p.node, name: p.name.to_string() })
}
Err(PortResolveError::Ambiguous) => {
Err(BuildError::AmbiguousInPort { node: p.node, name: p.name.to_string() })
}
}
}
@@ -183,21 +180,59 @@ impl GraphBuilder {
.schemas
.get(p.node)
.ok_or(BuildError::BadHandle { node: p.node })?;
let m: Vec<usize> = schema
.output
.iter()
.enumerate()
.filter(|(_, f)| f.name == p.name)
.map(|(i, _)| i)
.collect();
match m.as_slice() {
[i] => Ok(*i),
[] => Err(BuildError::UnknownOutPort { node: p.node, name: p.name.to_string() }),
_ => Err(BuildError::AmbiguousOutPort { node: p.node, name: p.name.to_string() }),
match resolve_output_field(schema, p.name) {
Ok(i) => Ok(i),
Err(PortResolveError::Unknown) => {
Err(BuildError::UnknownOutPort { node: p.node, name: p.name.to_string() })
}
Err(PortResolveError::Ambiguous) => {
Err(BuildError::AmbiguousOutPort { node: p.node, name: p.name.to_string() })
}
}
}
}
/// A name→index resolution outcome, shared by `GraphBuilder` (mapped to
/// `BuildError`) and `GraphSession` (mapped to `OpError`). Over `&str`, so it
/// serves both `&'static str` literals and runtime document names.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PortResolveError {
Unknown,
Ambiguous,
}
/// Resolve an input-port name to its slot index by exactly-one-match.
pub(crate) fn resolve_input_slot(schema: &NodeSchema, name: &str) -> Result<usize, PortResolveError> {
let m: Vec<usize> = schema
.inputs
.iter()
.enumerate()
.filter(|(_, port)| port.name == name)
.map(|(i, _)| i)
.collect();
match m.as_slice() {
[i] => Ok(*i),
[] => Err(PortResolveError::Unknown),
_ => Err(PortResolveError::Ambiguous),
}
}
/// Resolve an output-field name to its field index by exactly-one-match.
pub(crate) fn resolve_output_field(schema: &NodeSchema, name: &str) -> Result<usize, PortResolveError> {
let m: Vec<usize> = schema
.output
.iter()
.enumerate()
.filter(|(_, f)| f.name == name)
.map(|(i, _)| i)
.collect();
match m.as_slice() {
[i] => Ok(*i),
[] => Err(PortResolveError::Unknown),
_ => Err(PortResolveError::Ambiguous),
}
}
#[cfg(test)]
mod tests {
use super::{BuildError, GraphBuilder};
@@ -297,6 +332,19 @@ mod tests {
);
}
#[test]
fn shared_port_resolution_matches_exactly_one() {
use super::{resolve_input_slot, resolve_output_field, PortResolveError};
use crate::BlueprintNode;
use aura_std::Sub;
let schema = BlueprintNode::from(Sub::builder()).signature(); // lhs, rhs -> value
assert_eq!(resolve_input_slot(&schema, "lhs"), Ok(0));
assert_eq!(resolve_input_slot(&schema, "rhs"), Ok(1));
assert_eq!(resolve_input_slot(&schema, "nope"), Err(PortResolveError::Unknown));
assert_eq!(resolve_output_field(&schema, "value"), Ok(0));
assert_eq!(resolve_output_field(&schema, "nope"), Err(PortResolveError::Unknown));
}
#[test]
fn unknown_in_port_is_reported_at_build() {
let mut g = GraphBuilder::new("x");