Add a name-addressed tap construction op so declared taps aren't authored by raw node index #284

Closed
opened 2026-07-18 00:48:23 +02:00 by claude · 3 comments
Collaborator

Surfaced by the measurement-milestone fieldtest (#282 declared taps), as the friction behind the tap-wire panic.

Problem

Declared taps (C27) are only authorable today by hand-patching the raw serialized blueprint envelope: a taps array of { name, from: { node, field } } where node is a raw flat/interior index. There is no construction op (the aura graph/op-script surface) that adds a tap by a name-addressed wire. So the author must count node indices by hand — and an index miscount is the expected mistake, which (until the companion bug fix) panicked the run.

Direction (not prescribing)

A tap op in the construction op-script vocabulary that names its source wire the way the rest of the op surface addresses nodes (by the handle/name the builder already uses for connect/feed), so the author writes tap("p_long", <node-handle>.output("value")) rather than patching { "node": 14, "field": 0 }. This eliminates the index-miscount class at the source and makes taps a first-class authoring act rather than an envelope patch.

Context

The milestone's three capabilities are reachable and correct from the public surface (fieldtest confirmed the origin study is answerable by a hand-authored blueprint + one recorded run). This issue is the ergonomic follow-up: make the tap-authoring path as safe and named as the rest of the construction surface.

Surfaced by the measurement-milestone fieldtest (#282 declared taps), as the friction behind the tap-wire panic. ## Problem Declared taps (C27) are only authorable today by hand-patching the raw serialized blueprint envelope: a `taps` array of `{ name, from: { node, field } }` where `node` is a raw flat/interior **index**. There is no construction op (the `aura graph`/op-script surface) that adds a tap by a name-addressed wire. So the author must count node indices by hand — and an index miscount is the expected mistake, which (until the companion bug fix) panicked the run. ## Direction (not prescribing) A `tap` op in the construction op-script vocabulary that names its source wire the way the rest of the op surface addresses nodes (by the handle/name the builder already uses for `connect`/`feed`), so the author writes `tap("p_long", <node-handle>.output("value"))` rather than patching `{ "node": 14, "field": 0 }`. This eliminates the index-miscount class at the source and makes taps a first-class authoring act rather than an envelope patch. ## Context The milestone's three capabilities are reachable and correct from the public surface (fieldtest confirmed the origin study is answerable by a hand-authored blueprint + one recorded run). This issue is the ergonomic follow-up: make the tap-authoring path as safe and named as the rest of the construction surface.
claude self-assigned this 2026-07-18 15:26:53 +02:00
Author
Collaborator

Design settled for the tap construction op (autonomous, 2026-07-18) — no fork; the op is the exact output-side twin of the existing expose op (same node.field addressing via split_port + resolve_output_field, same as-renamed op-doc serde shape), differing only in appending a Tap to the composite instead of an OutField.

Two small derived decisions recorded:

  1. Fold in a latent fix: GraphSession::finish() builds Composite::new(..).with_gangs(..) but never .with_taps(..), so an op-built composite silently drops any taps today (#282 added the taps field + with_taps + the validate_wiring thread, but never wired the op surface to populate it). The tap op threads .with_taps(self.taps) in finish.
  2. Duplicate tap names are NOT rejected at the op layer — consistent with #282, which does not enforce tap-name uniqueness at construction; the single-run CLI dedups at bind time (DuplicateBind). No new op-layer error.

Scope: the op + its CLI op-doc serde only; no change to the Tap/FlatTap/bind_tap/aura-run machinery. Ready for spec production → plan → implement.

Design settled for the tap construction op (autonomous, 2026-07-18) — no fork; the op is the exact output-side twin of the existing expose op (same node.field addressing via split_port + resolve_output_field, same as-renamed op-doc serde shape), differing only in appending a Tap to the composite instead of an OutField. Two small derived decisions recorded: 1. Fold in a latent fix: GraphSession::finish() builds Composite::new(..).with_gangs(..) but never .with_taps(..), so an op-built composite silently drops any taps today (#282 added the taps field + with_taps + the validate_wiring thread, but never wired the op surface to populate it). The tap op threads .with_taps(self.taps) in finish. 2. Duplicate tap names are NOT rejected at the op layer — consistent with #282, which does not enforce tap-name uniqueness at construction; the single-run CLI dedups at bind time (DuplicateBind). No new op-layer error. Scope: the op + its CLI op-doc serde only; no change to the Tap/FlatTap/bind_tap/aura-run machinery. Ready for spec production → plan → implement.
Author
Collaborator

Correction to the prior decision note (2 of 2): reversing the "no op-layer duplicate-tap-name rejection" call. The grounding-check surfaced that the sibling expose op DOES dedup its boundary output names (out_names → OpError::DuplicateOutput). Being a faithful twin, the tap op therefore dedups tap names too (tap_names → new OpError::DuplicateTap) — and it is the better choice: a duplicate tap name collides as a recorded series (two .json in the trace store), and catching it at authoring time with a clean typed op error is exactly the ergonomic win #284 is about (stricter than the raw-envelope blueprint layer and than the runtime CLI DuplicateBind). Also corrected in the spec: the unknown-source-node path resolves via node_index → OpError::UnknownIdentifier (the actual expose behaviour), not a non-existent UnknownNode variant (a false claim the grounding-check caught and I fixed at the spec root before re-grounding).

Correction to the prior decision note (2 of 2): reversing the "no op-layer duplicate-tap-name rejection" call. The grounding-check surfaced that the sibling expose op DOES dedup its boundary output names (out_names → OpError::DuplicateOutput). Being a faithful twin, the tap op therefore dedups tap names too (tap_names → new OpError::DuplicateTap) — and it is the better choice: a duplicate tap name collides as a recorded series (two <name>.json in the trace store), and catching it at authoring time with a clean typed op error is exactly the ergonomic win #284 is about (stricter than the raw-envelope blueprint layer and than the runtime CLI DuplicateBind). Also corrected in the spec: the unknown-source-node path resolves via node_index → OpError::UnknownIdentifier (the actual expose behaviour), not a non-existent UnknownNode variant (a false claim the grounding-check caught and I fixed at the spec root before re-grounding).
Author
Collaborator

Spec signed (grounding-check PASS — /boss auto-sign)

The tap-construction-op spec cleared the grounding-check hard-gate (fresh-context re-ground on the corrected spec). Under /boss the Step-6 signature IS this PASS.

Ratified current-behaviour assumptions (each backed by a currently-green test or a compiler-enforced structural fact):

  • unknown source node -> OpError::UnknownIdentifier via node_index (test: connect_unknown_node_identifier_rejected)
  • name-dedup twin of expose: out_names/DuplicateOutput -> new tap_names/DuplicateTap (test: expose_success_and_duplicate_output_rejected)
  • bad output field -> UnknownOutPort via resolve_output_field (test: expose_unknown_out_port_rejected)
  • Composite.taps hoists into FlatGraph.taps through compile_with_params (test: compile_hoists_an_interior_composite_tap_to_the_root)
  • finish() currently drops taps (.with_gangs but no .with_taps) — the latent gap this op fixes
  • format_op_error/apply/kind_label/From<OpDoc> are exhaustive matches -> the new Tap/DuplicateTap arms are compiler-enumerated

Proceeding to plan + implement (2 tasks: engine op in construction.rs, CLI serde in graph_construct.rs).

## Spec signed (grounding-check PASS — /boss auto-sign) The `tap`-construction-op spec cleared the grounding-check hard-gate (fresh-context re-ground on the corrected spec). Under /boss the Step-6 signature IS this PASS. Ratified current-behaviour assumptions (each backed by a currently-green test or a compiler-enforced structural fact): - unknown source node -> `OpError::UnknownIdentifier` via `node_index` (test: `connect_unknown_node_identifier_rejected`) - name-dedup twin of `expose`: `out_names`/`DuplicateOutput` -> new `tap_names`/`DuplicateTap` (test: `expose_success_and_duplicate_output_rejected`) - bad output field -> `UnknownOutPort` via `resolve_output_field` (test: `expose_unknown_out_port_rejected`) - `Composite.taps` hoists into `FlatGraph.taps` through `compile_with_params` (test: `compile_hoists_an_interior_composite_tap_to_the_root`) - `finish()` currently drops taps (`.with_gangs` but no `.with_taps`) — the latent gap this op fixes - `format_op_error`/`apply`/`kind_label`/`From<OpDoc>` are exhaustive matches -> the new `Tap`/`DuplicateTap` arms are compiler-enumerated Proceeding to plan + implement (2 tasks: engine op in construction.rs, CLI serde in graph_construct.rs).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#284