The single-file ledger had grown to 2968 lines / ~42k tokens, mixing current design law with accreted history: 59 cycle-stamped realization blocks, 18 [HISTORY] passages, 22 supersession markers, and the C10 / C22 / C24 reframe sagas layered several supersessions deep. A code-grounding audit (31 agents, adversarially verified) confirmed 11 defects stated as current truth: stale crate homes from the C28 #288 roster split (cost nodes, PositionManagement, PositionEvent, Session), the renamed InputSpec->PortSpec, the pre-#241 project model in C16 and the open-threads section, a stale HarnessKind retirement deferral in C24, and three C28-internal inconsistencies. New shape, per the ailang precedent: - INDEX.md stays the sole addressable entry point: foundation, external components, a C-id-keyed contract map (one line per contract), and only the genuinely open architectural threads. - contracts/cNN-<slug>.md carries each contract's current truth only: Guarantee / Forbids / Why with ratified refinements integrated, plus a code-anchored Current state. All confirmed defects are fixed here; crate anchors were re-verified against the tree. - contracts/cNN-<slug>.history.md (18 sidecars) and INDEX.history.md preserve every superseded block verbatim, stamps and issue refs intact, under a frozen-record banner. Nothing was deleted: superseded design intent remains an addressable working-tree artifact, off the per-cycle audit walk. - Ledger discipline is now stated in INDEX.md: live files are edited in place at cycle close, superseded text moves verbatim to the sidecar, and a supersession marker in a live file is itself an audit finding. Every contract file was verified against its old text by an independent zero-loss pass (statement-by-statement) plus a code-accuracy spot check; C-ids and contract titles are unchanged, so existing C-id citations in code, tests, and issues resolve as before.
4.4 KiB
C27 — Declared taps: named measurement points bind sinks run-mode-aware
Guarantee. A blueprint may declare taps — named, pure output-side
declarations { name, from: {node, field} }, the output-side twin of input_roles
(C26). A tap names an interior producer's output field
without naming a sink, exactly as a Role names an abstract input without naming a
source. At compile the tap resolves — and, for an interior composite, hoists to
the root — through the same lowering remap edges and OutField re-exports use
(resolve_tap_wire, a flat_taps accumulator threaded through the lowering
recursion), landing in FlatGraph.taps as a FlatTap { name, node, field } whose
name survives compile and is load-bearing for by-name binding (like
SourceSpec.role, #275).
Binding is run-mode-aware: the run-mode-owning layer constructs a sink (a
Recorder) at a bound tap via FlatGraph::bind_tap, which takes a caller-built
Box<dyn Node> sink (so the engine keeps its aura-core-only production
dependency — it never constructs a domain sink type) and appends it plus an edge
before bootstrap. The single-run path (run_signal_r) binds and records each
declared tap, persisting the series through the existing trace store
(env.trace_store()), so the tap columns surface through the same tooling the
campaign path feeds; a sweep/reduce run leaves taps unbound.
Forbids. A tap carrying a channel endpoint or effect in the serialized artefact
— recording policy is run-mode authority, not fragment-embedded (a fragment must
not drag its measurement decisions into every harness that embeds it; the tier
ontology, C20/C21). The engine
constructing a domain sink type (the aura-core-only wall — the sink is
caller-built). Order statistics (median, etc.) inside the graph — they stay
sink/analysis-side (C18); multi-instrument study
inputs stay harness/World tier.
Non-error. An unbound tap is inert, not a fault — unlike an unbound root
input role, which check_root_roles_bound rejects (C26):
observation is optional, a fed input is mandatory. A declared-but-unbound tap
compiles and runs, its producer evaluating and its output discarded (a no-out-edge
producer is a valid runnable sink — the Kahn sort emits it,
check_ports_connected gates only inputs).
Why. Observability must be expressible in a hand-authored blueprint — the measurement-shaped study computes in the graph and surfaces via taps, no throwaway Rust harness — while recording stays a run-mode decision, not a fragment-embedded effect. Taps are designed DCE-compatible (a bound tap is a natural DCE root, an unbound tap a dead declaration) but this contract does not depend on DCE: an unbound tap's sink is simply never constructed (build-time elision, which the engine already tolerates). (#282, 2026-07-18.)
Current state
The tap types are realized in aura-engine: the authoring-level Tap
(crates/aura-engine/src/blueprint.rs, the output-side twin of Role), the
compiled FlatTap { name, node, field } and the FlatGraph.taps field
(crates/aura-engine/src/harness.rs), and FlatGraph::bind_tap, which appends a
caller-built Box<dyn Node> sink plus an edge and raises a typed UndeclaredTap
on a tap the graph does not declare (duplicate detection across binds is the
caller's — the method keeps no cross-call state). Lowering resolves and hoists taps
via resolve_tap_wire and the flat_taps accumulator (blueprint.rs). The
single-run bind/record path is run_signal_r in aura-runner::member; the
sweep/reduce path never calls bind_tap.
The chain-pruning benefit — a sweep paying zero for the study wires behind an unbound tap — is deferred to the future DCE cycle (C23); the mechanism ships now, verified sound.
See also
- C26 — the input-side twin (
input_roles);check_root_roles_bound, the mandatory-input counterpart - C23 — compilation/lowering and the deferred DCE cycle the tap design anticipates
- C18 — the trace store and registry the tap series feed; order statistics live sink-side
- C20, C21 — the tier ontology behind run-mode recording authority
- C8 — the node/sink contract (a no-out-edge producer as a valid runnable sink)