98342246f6
Fieldtest spec_gap (#335): the timestamp a fold's one summary row carries was undocumented. Ratified as-is (derived decision, minuted on the issue): the row is emitted at finalize and stamped with the instant of the last contributing (warm) value (TapFold::last_ts) — first alone pins the first contributing instant, and min/max deliberately do not carry the extremum's instant (a whole-window row privileges no interior instant, and the extremum ts would cost extra state for no consumer). The issue's initial finalize-ts reading (and the first draft of this ratification) was refuted in review against tap_fold.rs: a tap that goes cold before run end stamps the last WARM eval's instant, not the run-end instant — observationally identical only while the tap stays warm to the end, which is what the fieldtest fixtures did. Correction minuted on the issue. Recorded in C27's Current state and the glossary tap-plan paragraph; the fold-registry roster doc lines state it on the surface itself (sibling commit). closes #335
105 lines
6.9 KiB
Markdown
105 lines
6.9 KiB
Markdown
# 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](c26-input-binding.md)). 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 consumer 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. What consumes a tap is declared per run by a **tap plan**
|
|
(#283): a `Named { label, params }` subscription resolves against a layered
|
|
**fold registry** (core vocabulary `record | count | sum | mean | min | max |
|
|
first | last`; growth is a new Rust entry, C25, injectable by higher layers
|
|
without a core edit), and `Live(closure)` is the single deliberately non-data
|
|
variant. Both declared-tap entry points — the single-run path (`run_signal_r`)
|
|
and the bare measurement path (`run_measurement`) — bind through one shared
|
|
wiring pair, so they cannot drift: `record` persists the full series at constant
|
|
memory through the trace store's streamed write path (`env.trace_store()`), folds
|
|
keep an O(1) accumulator and land one summary row at finalize, 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](c20-strategy-harness.md)/[C21](c21-world.md)). 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](c18-registry.md)); 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](c26-input-binding.md)):
|
|
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
|
|
subscription seam is `aura-runner::tap_plan` (#283): `TapPlan`/`TapSubscription`,
|
|
the layered `FoldRegistry` (each entry carries a `doc` line — the help surface
|
|
and the roster-enumerating refusal — plus a scalar-typed param schema; all core
|
|
entries are param-less today, the seam ships in every entry's build signature),
|
|
and the shared `bind_tap_plan`/`BoundTaps` pair called by both declared-tap entry
|
|
points, `run_signal_r` (`aura-runner::member`) and `run_measurement`
|
|
(`aura-runner::measure`) — both arms of the single CLI verb `aura run`, whose
|
|
repeatable `--tap TAP=FOLD` selector (#310) makes the `Named` selection
|
|
data-reachable: no flag keeps the record-all default, any flag replaces the
|
|
plan entirely (unlisted taps stay unbound/inert). The boundary is thereby
|
|
fixed in place: *selecting* a subscription is run-mode authority, exercised
|
|
by the run-mode owner — on the one-shot path the CLI invocation itself, a
|
|
projection exercising this contract's authority, not a second home for
|
|
intent under [C25](c25-role-model.md) — while *adding* a fold stays a Rust
|
|
entry (role 2). The campaign/document carrier, and the reconciliation of the
|
|
presentation-tap namespace (`persist_taps`) with declared taps it requires,
|
|
is deferred to the Measurement-reachable milestone (#312/#327, minuted on
|
|
#312). The `record`
|
|
consumer (`aura-runner::tap_recorder::TapRecorder`) holds the trace store's
|
|
streaming writer in-graph — `initialize` opens (deferred acquisition), `eval`
|
|
appends `(Timestamp, Cell)` (zero per-cycle heap, #77), `finalize` reports
|
|
exactly one terminal outcome; fold consumers (`aura-std::TapFold`) land one
|
|
summary row, emitted at finalize and stamped with the instant of the last
|
|
contributing (warm) value — `first` alone pins the first contributing
|
|
instant; `min`/`max` deliberately do not carry the extremum's timestamp (a
|
|
whole-window row privileges no interior instant) — ratified as-is, #335;
|
|
live closures run inline
|
|
(`aura-std::TapLive`). 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](c23-graph-compilation.md));
|
|
the mechanism ships now, verified sound.
|
|
|
|
## See also
|
|
- [C26](c26-input-binding.md) — the input-side twin (`input_roles`); `check_root_roles_bound`, the mandatory-input counterpart
|
|
- [C23](c23-graph-compilation.md) — compilation/lowering and the deferred DCE cycle the tap design anticipates
|
|
- [C18](c18-registry.md) — the trace store and registry the tap series feed; order statistics live sink-side
|
|
- [C20](c20-strategy-harness.md), [C21](c21-world.md) — the tier ontology behind run-mode recording authority
|
|
- [C8](c08-node-contract.md) — the node/sink contract (a no-out-edge producer as a valid runnable sink)
|