audit: close #284/#285 cycle — GraphBuilder::tap twin restores C24 symmetry
Cycle-close tidy for the #284 (tap construction op) + #285 (docs) cycle. The architect drift review (cb6211c..HEAD) came back drift-clean on the ledger contracts the cycle touched — C27 machinery untouched, a faithful `expose` twin (C25 preserved), lockstep op<->OpDoc / OpError<->format_op_error arms paired, and the #285 docs verified accurate against aura-std/src/session.rs — save one drift item, fixed here. Drift item (fixed): the op-script gained `Op::Tap` but `GraphBuilder` had no `tap()` twin, softening C24 (replay-equals-builder = one construction semantics) and C17 (builder-primacy) — every other op verb has a builder twin. Unlike the op-script's `doc` gap (a ledger-recorded #125 scope cut), this asymmetry had no rationale; a tap is a first-class authoring act on both surfaces, so denying only the fluent builder was an accident of incremental delivery, not a design cut. Fix: `GraphBuilder::tap` (builder.rs), the exact output-side mirror of `expose` — a `taps: Vec<(String, OutPort)>` accumulator resolved at `build()` into `Tap { name, from: TapWire }` and threaded via `.with_taps`. Plus a construction.rs lockstep test `tap_replay_matches_builder` proving the op-script `Op::Tap` replay and the `GraphBuilder::tap` twin serialize to byte-identical blueprint JSON and compile to identical `FlatGraph.taps` — the tap op is the by-identifier face of the same construction semantics, not a second one. Carry-on (architect debt-low, no action): commit 43a1cc4's body phrase "op-built composites silently lost their taps" over-claims a prior drop (no tap op existed pre-cycle); the "latent" qualifier keeps it honest, so the wording is left as-is rather than rewriting an already-landed commit. Verification: full `cargo test --workspace` green; `cargo clippy --workspace --all-targets -- -D warnings` clean. The bench is report-only by project convention and was not run — this cycle adds a construction-time op + docs, no hot-path change. Fieldtest: the #284 `tap` surface is exercised from the public interface by two E2E consumer-path tests (op-script -> `aura graph build` -> `aura run` -> persisted trace) plus two worked authoring examples hand-verified through the built binary; #285 is documentation. The per-cycle fieldtest is satisfied. Spec + plan working files (docs/specs/tap-construction-op.md, docs/plans/tap-construction-op.md) discarded per audit Step 5 (git-ignored, never committed; their design intent lives in the commit bodies + the ledger).
This commit is contained in:
@@ -10,7 +10,9 @@
|
||||
|
||||
use aura_core::{NodeSchema, ScalarKind};
|
||||
|
||||
use crate::blueprint::{BlueprintNode, CompileError, Composite, Gang, GangFault, GangMember, OutField, Role};
|
||||
use crate::blueprint::{
|
||||
BlueprintNode, CompileError, Composite, Gang, GangFault, GangMember, OutField, Role, Tap, TapWire,
|
||||
};
|
||||
use crate::harness::{Edge, Target};
|
||||
|
||||
/// A typed, `Copy` reference to a node added to a [`GraphBuilder`], carrying the
|
||||
@@ -92,6 +94,7 @@ pub struct GraphBuilder {
|
||||
edges: Vec<(OutPort, InPort)>,
|
||||
roles: Vec<(String, Option<ScalarKind>, Vec<InPort>)>,
|
||||
out: Vec<(String, OutPort)>,
|
||||
taps: Vec<(String, OutPort)>,
|
||||
gangs: Vec<(String, Vec<ParamRef>)>,
|
||||
}
|
||||
|
||||
@@ -106,6 +109,7 @@ impl GraphBuilder {
|
||||
edges: Vec::new(),
|
||||
roles: Vec::new(),
|
||||
out: Vec::new(),
|
||||
taps: Vec::new(),
|
||||
gangs: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -149,6 +153,15 @@ impl GraphBuilder {
|
||||
self.out.push((name.to_string(), from));
|
||||
}
|
||||
|
||||
/// Declare a measurement tap on a producer's output field under `name`
|
||||
/// (resolved at build) — the output-side twin of [`GraphBuilder::expose`]: a
|
||||
/// recorded observation point (a `Composite.taps` entry, #284/C27), not a
|
||||
/// boundary output. The builder-surface counterpart of the op-script `tap` op,
|
||||
/// so a Rust-authored graph reaches taps the same way an op-script does (C24).
|
||||
pub fn tap(&mut self, from: OutPort, name: &str) {
|
||||
self.taps.push((name.to_string(), from));
|
||||
}
|
||||
|
||||
/// Fuse two or more sibling params into one public knob (resolved at build).
|
||||
pub fn gang(&mut self, name: &str, members: impl IntoIterator<Item = ParamRef>) {
|
||||
self.gangs.push((name.to_string(), members.into_iter().collect()));
|
||||
@@ -185,6 +198,12 @@ impl GraphBuilder {
|
||||
output.push(OutField { node: from.node, field, name: name.clone() });
|
||||
}
|
||||
|
||||
let mut taps = Vec::with_capacity(self.taps.len());
|
||||
for (name, from) in &self.taps {
|
||||
let field = self.resolve_field(from)?;
|
||||
taps.push(Tap { name: name.clone(), from: TapWire { node: from.node, field } });
|
||||
}
|
||||
|
||||
let mut gangs = Vec::with_capacity(self.gangs.len());
|
||||
for (name, refs) in &self.gangs {
|
||||
let mut members = Vec::with_capacity(refs.len());
|
||||
@@ -230,7 +249,7 @@ impl GraphBuilder {
|
||||
let kind = kind.unwrap_or(ScalarKind::F64);
|
||||
gangs.push(Gang { name: name.clone(), kind, members });
|
||||
}
|
||||
let mut composite = Composite::new(self.name, self.nodes, edges, roles, output);
|
||||
let mut composite = Composite::new(self.name, self.nodes, edges, roles, output).with_taps(taps);
|
||||
if let Some(doc) = self.doc {
|
||||
composite = composite.with_doc(doc);
|
||||
}
|
||||
|
||||
@@ -1096,4 +1096,56 @@ mod tests {
|
||||
assert_eq!(replay_flat.edges, built_flat.edges);
|
||||
assert_eq!(replay_flat.sources, built_flat.sources);
|
||||
}
|
||||
|
||||
/// C24 lockstep extended to taps (#284): the op-script's `Op::Tap` replay and
|
||||
/// the `GraphBuilder::tap` twin serialize to byte-identical blueprint JSON and
|
||||
/// compile to byte-identical `FlatGraph.taps` — the tap op is the by-identifier
|
||||
/// face of the same construction semantics `GraphBuilder::build` runs, not a
|
||||
/// second one. Guards the builder/op-script tap symmetry (both surfaces can
|
||||
/// declare a tap, and they resolve it identically).
|
||||
#[test]
|
||||
fn tap_replay_matches_builder() {
|
||||
use crate::blueprint_serde::blueprint_to_json;
|
||||
use crate::GraphBuilder;
|
||||
use aura_core::Scalar;
|
||||
use aura_std::{Sma, Sub};
|
||||
|
||||
// (a) op-script: an SMA crossover with a tap on the raw spread (sub.value).
|
||||
let ops = vec![
|
||||
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] },
|
||||
Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] },
|
||||
Op::Add { type_id: "Sub".into(), as_name: Some("sub".into()), bind: vec![] },
|
||||
Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] },
|
||||
Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() },
|
||||
Op::Connect { from: "slow.value".into(), to: "sub.rhs".into() },
|
||||
Op::Tap { from: "sub.value".into(), as_name: "spread".into() },
|
||||
Op::Expose { from: "sub.value".into(), as_name: "bias".into() },
|
||||
];
|
||||
let replayed = super::replay("g", ops, &std_vocabulary).expect("replays");
|
||||
|
||||
// (b) the GraphBuilder twin — same adds/feeds/connects + g.tap(...) / g.expose(...).
|
||||
let mut g = GraphBuilder::new("g");
|
||||
let fast = g.add(Sma::builder().named("fast"));
|
||||
let slow = g.add(Sma::builder().named("slow"));
|
||||
let sub = g.add(Sub::builder().named("sub"));
|
||||
let price = g.source_role("price", ScalarKind::F64);
|
||||
g.feed(price, [fast.input("series"), slow.input("series")]);
|
||||
g.connect(fast.output("value"), sub.input("lhs"));
|
||||
g.connect(slow.output("value"), sub.input("rhs"));
|
||||
g.tap(sub.output("value"), "spread");
|
||||
g.expose(sub.output("value"), "bias");
|
||||
let built = g.build().expect("root resolves");
|
||||
|
||||
assert_eq!(
|
||||
blueprint_to_json(&replayed).expect("replay serializes"),
|
||||
blueprint_to_json(&built).expect("builder serializes"),
|
||||
);
|
||||
|
||||
let params = [Scalar::i64(2), Scalar::i64(4)];
|
||||
let replay_flat = replayed.compile_with_params(¶ms).expect("replay compiles");
|
||||
let built_flat = built.compile_with_params(¶ms).expect("builder compiles");
|
||||
assert_eq!(replay_flat.taps, built_flat.taps);
|
||||
assert_eq!(replay_flat.edges, built_flat.edges);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user