feat(graph): name-addressed tap construction op — declared taps without raw indices
Declared taps (C27, #282) were authorable only by hand-patching the raw blueprint envelope's `taps` array, where `node` is a raw interior index — an index miscount was the expected mistake and the root friction behind the two `aura run` panics the measurement-milestone fieldtest found. Add a `tap` op to the construction op-script vocabulary — the exact output-side twin of `expose`. It names its source wire by the dotted `"node.field"` identifier the op surface uses everywhere (`split_port` + `resolve_output_field` + `node_index`), so `{"op":"tap","from":"fast.value","as":"fast_ma"}` replaces patching `{"node":14,"field":0}`. Engine (construction.rs): `Op::Tap`, its `tap()` handler (a verbatim twin of `expose` — same resolvers, same name-dedup, appending a `Tap` to a new `taps`/`tap_names` namespace), and `OpError::DuplicateTap` (the twin of `DuplicateOutput`, stricter than the raw envelope, which enforces no tap-name uniqueness). `finish()` now threads `.with_taps(self.taps)` into the built `Composite` — fixing a latent drop #282 left (it built `.with_gangs` but never `.with_taps`, so op-built composites silently lost their taps). CLI (graph_construct.rs): `OpDoc::Tap` (externally tagged, `"as"`-renamed like `expose`) + the `kind_label` / `From` / `format_op_error` arms (the last compiler-enumerated by the exhaustive match). No change to the `Tap` / `FlatTap` / `bind_tap` / `aura run` machinery — the op-declared tap flows through the existing #282 compile + record path unchanged. Verification: 7 new tests green (4 engine: resolve+append, bad-port / ghost-node refusal, duplicate-name refusal, finish-threads-into-FlatGraph; 1 CLI serde round-trip; 2 CLI E2E over the real `aura graph build` / `aura run` subprocess — one pinning the exact serialized `"taps":[{"name":"fast_ma","from":{"node":0,"field":0}}]`, one proving an op-authored tap round-trips through build -> run -> persisted trace). Full workspace suite green; clippy clean. The RED-before-GREEN step was verified by a temporary stash of the production edits (the `Tap` / `with_taps` machinery pre-existed from #282, so a fresh RED-only test could not be authored) — the new tests fail without the edits, pass with them. Docs (the authoring-guide `tap` row, the ledger op-list `tap` line, and a worked tap-authoring example) are deliberately out of this commit's scope — they land in #285, the docs follow-up in this same cycle. closes #284
This commit is contained in:
@@ -44,6 +44,11 @@ enum OpDoc {
|
||||
#[serde(rename = "as")]
|
||||
as_name: String,
|
||||
},
|
||||
Tap {
|
||||
from: String,
|
||||
#[serde(rename = "as")]
|
||||
as_name: String,
|
||||
},
|
||||
Gang {
|
||||
#[serde(rename = "as")]
|
||||
as_name: String,
|
||||
@@ -61,6 +66,7 @@ impl OpDoc {
|
||||
OpDoc::Feed { .. } => "feed",
|
||||
OpDoc::Connect { .. } => "connect",
|
||||
OpDoc::Expose { .. } => "expose",
|
||||
OpDoc::Tap { .. } => "tap",
|
||||
OpDoc::Gang { .. } => "gang",
|
||||
}
|
||||
}
|
||||
@@ -77,6 +83,7 @@ impl From<OpDoc> for Op {
|
||||
OpDoc::Feed { role, into } => Op::Feed { role, into },
|
||||
OpDoc::Connect { from, to } => Op::Connect { from, to },
|
||||
OpDoc::Expose { from, as_name } => Op::Expose { from, as_name },
|
||||
OpDoc::Tap { from, as_name } => Op::Tap { from, as_name },
|
||||
OpDoc::Gang { as_name, into } => Op::Gang { as_name, into },
|
||||
}
|
||||
}
|
||||
@@ -90,6 +97,7 @@ fn format_op_error(e: &OpError) -> String {
|
||||
OpError::UnknownNodeType(t) => format!("unknown node type {t:?}"),
|
||||
OpError::DuplicateIdentifier(s) => format!("duplicate identifier {s:?}"),
|
||||
OpError::DuplicateOutput(s) => format!("duplicate output name {s:?}"),
|
||||
OpError::DuplicateTap(s) => format!("duplicate tap name {s:?}"),
|
||||
OpError::DuplicateRole(s) => format!("duplicate role {s:?}"),
|
||||
OpError::UnknownIdentifier(s) => format!("unknown node identifier {s:?}"),
|
||||
OpError::UnknownRole(s) => format!("unknown role {s:?}"),
|
||||
@@ -702,4 +710,30 @@ mod tests {
|
||||
assert!(out.contains("fast.series"), "fast.series is still open: {out}");
|
||||
assert!(!out.contains("sub.lhs"), "sub.lhs is covered: {out}");
|
||||
}
|
||||
|
||||
/// The `tap` op-doc parses (externally tagged, `"as"`-renamed like `expose`)
|
||||
/// and `aura graph build` emits a blueprint whose serialized `taps` names the
|
||||
/// wire — the name-addressed authoring path the fieldtest wanted, no raw index.
|
||||
#[test]
|
||||
fn tap_opdoc_builds_a_blueprint_carrying_the_tap() {
|
||||
let doc = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"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":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||
{"op":"tap","from":"fast.value","as":"fast_ma"},
|
||||
{"op":"expose","from":"sub.value","as":"bias"}
|
||||
]"#;
|
||||
// the tap-doc kind_label registers as "tap"
|
||||
let docs: Vec<OpDoc> = serde_json::from_str(doc).expect("document parses");
|
||||
assert_eq!(docs[7].kind_label(), "tap");
|
||||
// and the built blueprint carries the tap (an un-tapped composite emits no
|
||||
// `taps` key, so its presence is the proof the op threaded through)
|
||||
let json = super::build_from_str(doc, &crate::project::Env::std()).expect("tap op-doc builds");
|
||||
assert!(json.contains("\"taps\""), "blueprint carries the taps section: {json}");
|
||||
assert!(json.contains("fast_ma"), "the tap names the wire: {json}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user