docs(ledger, glossary): tap-subscribers cycle tidy — C8/C27/C28 lifts, tap subscription vocabulary

Cycle-close audit for the #283/#77 tap-subscribers cycle
(09994b8..6e3f394). Architect review: code contract-clean; what
holds — C1/C7/C8 (empty-output caller-built consumers, (Timestamp,
Cell) zero-heap payload, lifecycle hooks inside the sequential loop,
byte-identical twin-run and streamed-vs-legacy byte-equality pins),
C28 (assembly-crate placement of the writer-holding consumer,
c28_layering guard green), C25 (registry growth = new Rust entry,
data-expressible Named form, roster-enumerating refusals). All drift
items were doc lifts, resolved in this commit:

- C8: the lifecycle is now a symmetric pair — initialize() as
  finalize's start-of-stream mirror (infallible by signature;
  acquire-in-initialize, degrade-to-inert, surface-once-at-finalize);
  history sidecar records the refinement and the #77 resolution the
  old Deferred paragraph still carried as open.
- C27: Guarantee and Current state now describe the shipped model —
  tap plan (Named{label, params} | Live) over the layered fold
  registry, BOTH entry points (run_signal_r AND run_measurement) on
  the one shared wiring pair, record streaming at constant memory.
- C28: aura-runner now defines one graph node of its own (the
  in-graph record consumer — exactly the graph-meets-persistence
  assembly position); the #297 refusal-site count grows ~20 → ~24
  (the four tap-plan refusals, already typed before the exit).
- glossary §tap: the subscription/fold-registry sense added; Avoid
  line steers probe/monitor/scope away from the observation slot.

Bench (report-only, loadavg 5.8 > 2 on the box — read with care),
all fingerprints OK, no regression:
  engine_throughput    bars_per_s 14005262   wall_s 0.714s   Δ bars_per_s +3.0% wall_s -3.0%
  ingest_throughput    bars_per_s 12970670   wall_s 0.387s   Δ bars_per_s +0.5% wall_s -0.5%
  campaign_sweep       cpu_percent 2202.0   peak_rss_mb 92.2   wall_s 1.396s   Δ cpu_percent +1.5% peak_rss_mb -6.1% wall_s -1.5%
  campaign_heavy       cpu_percent 2155.0   peak_rss_mb 94.8   wall_s 5.585s   Δ cpu_percent +1.8% peak_rss_mb -3.5% wall_s -0.4%
  cli_fixed_cost       help_ms 1.5ms   run_ms 3.6ms   Δ help_ms -1.8% run_ms -2.6%
No baseline update (report-only surface; no ratify due).

Correction on 6e3f394's body: "no Probe symbol is introduced
anywhere" — introduced is the operative word; the pre-existing
aura-core test fixture `struct Probe` stays, as the cycle's design
record specified.

Follow-ups on the tracker: #308 (legacy Recorder surface migration),
#307 (binary trace format). Cycle spec/plan working files discarded.

refs #283 refs #77
This commit is contained in:
2026-07-21 23:56:14 +02:00
parent 6e3f394b48
commit bd0c557f16
5 changed files with 75 additions and 23 deletions
@@ -157,3 +157,17 @@ text — `\n`/`\t`/`\r` named, other control chars as `\u00XX`), the viewer show
it in both composite view states (collapsed tooltip, expanded cluster frame) and
the root's as a muted header line. The construction op-script vocabulary
deliberately has no doc-carrying surface yet (scope cut recorded on #125).
**Refinement (2026-07-21 — start-of-stream `initialize` lifecycle; #77 resolved,
#283).** The contract gains `finalize`'s mirror: `initialize()`, default no-op,
called once per node in topological order before the first source value (a
`Harness::run` prologue with its own once-per-run mirror test). Consumers acquire
run resources there — the in-graph record consumer (`TapRecorder`, `aura-runner`)
opens its streaming trace writer in `initialize`, appends per fired warm cycle,
and finishes/reports exactly one terminal outcome at `finalize`; the hook is
infallible by signature, so an acquisition failure degrades the node to inert and
surfaces once, terminally, at `finalize`. This closes the #77 line the cycle-0070
entry left open: declared-tap consumers now move `(Timestamp, Cell)` (zero
per-cycle heap), and the `Recorder``Probe` rename is retired — `Recorder` keeps
its name on the legacy live/`--trace`/test-tap surface, whose migration onto the
subscriber shape is tracked as #308.
+17 -9
View File
@@ -32,8 +32,10 @@ a zero-width `Some(&[])`, and it pushes its record to a destination it holds as
field (a channel, a chart handle). A node may record **and** return a forwarded
output in the same `eval` (the "both" case). `None` = filter / not-yet-warmed-up /
pure sink. Sources are pure producers; sinks are pure consumers. Beside `eval`,
the contract has a second lifecycle phase, `finalize()` — a default-no-op hook the
engine calls once per node, in topological order, after the source loop drains —
the contract has a symmetric lifecycle pair — default-no-op hooks the engine calls
once per node, in topological order: `initialize()` before the first source value,
letting a consumer acquire its run resources (e.g. the file a recording sink
streams into) at stream start, and `finalize()` after the source loop drains,
letting a sink fold online and flush one compact summary at stream end instead of
streaming a row every fired cycle. A node also exposes `label()`, a single-line,
non-load-bearing render symbol (C23).
@@ -55,7 +57,12 @@ boundary is the determinism / graph-as-data boundary (C1/C7). `finalize` gives a
folding sink O(trades)/O(1) owned accumulator state and one summary row, instead of
an unbounded channel that buffers O(cycles) rows until the run ends; it runs once
*after* the deterministic event loop, adding no within-sim concurrency (C1), and
holds only owned state, no interior mutability (C7).
holds only owned state, no interior mutability (C7). `initialize` mirrors it at
stream start (#283): resource acquisition happens inside the same deterministic
sequence, and the hook is infallible by signature — a consumer that fails to
acquire stores the error, degrades to inert, and surfaces the failure once,
terminally, at `finalize` (the run completes; refusal-worthy faults are caught
pre-run, at construction/bind time).
## Current state
@@ -70,7 +77,8 @@ structural axis, C20, never a numeric sweep param).
`trait Node` declares `lookbacks() -> Vec<usize>`, `eval(&mut self, ctx) ->
Option<&[Cell]>`, `label() -> String` (default a placeholder every shipped node
overrides; overrides carry identifying params so `SMA(2)` vs `SMA(4)` disambiguate
in a graph render, #13), and `finalize(&mut self)` (default no-op). There is **no**
in a graph render, #13), and the lifecycle pair `initialize(&mut self)` /
`finalize(&mut self)` (both default no-op). There is **no**
`Node::schema()` — the signature is pre-build data, not a built-node method. The
signature is declared once on the value-empty recipe `PrimitiveBuilder`
(`aura-core/src/node.rs`), whose `schema()` / `params()` are read pre-build by the
@@ -83,9 +91,10 @@ against the producer's `output` at bootstrap. A sink's empty `output` therefore
makes it structurally unwireable as an in-graph producer — no edge can bind a field
of a zero-output node. The run loop (`aura-engine/src/harness.rs`) debug-asserts
`row.len()` equals the declared output width, and that `lookbacks()` arity equals
`signature().inputs.len()`. The end-of-stream `finalize` epilogue is realized in
`Harness::run` (`aura-engine/src/harness.rs`), which calls `finalize()` once per
node in topological order after the source loop drains; `GatedRecorder` and
`signature().inputs.len()`. The lifecycle pair is realized in `Harness::run`
(`aura-engine/src/harness.rs`): an `initialize()` prologue once per node in
topological order before the source loop, and the `finalize()` epilogue after it
drains — each pinned by its own once-per-run mirror test; `GatedRecorder` and
`SeriesReducer` (`aura-std`) are the folding siblings of the per-cycle `Recorder`
(`aura-std/src/recorder.rs`), which survives for the live / `--trace` / test-tap
path.
@@ -124,8 +133,7 @@ untouched), blanked in the identity projection by `strip_debug_symbols`, dissolv
at inline like the name, and surfaced read-only in the graph model and viewer
(C22).
Deferred: the recording sink's own per-cycle allocation — the `Recorder``Probe`
rename and its accumulate-vs-stream choice — stays open (#77). Naming input ports
Deferred: Naming input ports
(`PortSpec.name`) makes swap-prone same-kind slots self-documenting but does not add
a name-consuming wiring validation; a swapped same-kind wiring is still only
kind-checked (#21). The construction op-script vocabulary has no doc-carrying
+27 -8
View File
@@ -11,14 +11,22 @@ recursion), landing in `FlatGraph.taps` as a `FlatTap { name, node, field }` who
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**
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. 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.
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
@@ -54,8 +62,19 @@ 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`.
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 CLI verbs pass a record-all plan. 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; 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));
+14 -5
View File
@@ -107,7 +107,14 @@ The workspace realizes the full ladder today, cut by layer, with only the
The closed node roster lives in `aura-vocabulary`. The process column is
`aura-registry` (C18) + `aura-research` + `aura-campaign`; the assembly position
is `aura-runner`; the shell is `aura-cli`.
is `aura-runner` — which since #283 also *defines* one graph node of its own, the
in-graph record consumer `TapRecorder` (`aura-runner/src/tap_recorder.rs`): it
holds an `aura-registry` streaming writer, so it can live neither in `aura-std`
(engine layer, must not know the process column) nor in `aura-registry`
(persistence, not graph vocabulary) — the assembly crate is exactly where graph
and persistence meet, alongside the `tap_plan` wiring
(`TapPlan`/`FoldRegistry`/`bind_tap_plan`) that binds it. The shell is
`aura-cli`.
The dependency direction obeys the rule across the whole stack. The engine names
no concrete metric type: `aura_engine::RunReport<M>` is generic over its metric
@@ -160,10 +167,12 @@ demand-gated, no tracking issue): measurement runs as sweep-family citizens
(report unification, campaign engine generic-over-`M`), until a concrete
family/campaign demand exists.
**Deferred.** ~20 refusal sites inside `aura-runner`'s single-run verb paths still
terminate the process (`std::process::exit`); their conversion to `RunnerError`
propagation is tracked as **#297** (the campaign path already refuses via
`MemberFault`, never a process exit).
**Deferred.** ~24 refusal sites inside `aura-runner`'s single-run verb paths still
terminate the process (`std::process::exit`; the #283 tap-plan refusals added four
— typed as `TapPlanError` before the exit, so the eventual conversion is a
mechanical rewrap); their conversion to `RunnerError` propagation is tracked as
**#297** (the campaign path already refuses via `MemberFault`, never a process
exit).
## See also
- [C1](c01-determinism.md) — determinism / bit-identity, the correctness invariant the layer cuts preserve
+3 -1
View File
@@ -318,11 +318,13 @@ The harness's structural parameterization — which strategy, instrument(s), bro
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`--list-axes`) is **required** — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (`--axis name=<one-value>`), there is no default. `aura sweep --axis` takes the `--list-axes`-printed, root-composite-wrapped name (e.g. `graph.fast.length`) — not the raw `param_space` name (`fast.length`) that `graph introspect --params` and a campaign document's axes use; the CLI strips exactly one leading wrapper segment (#210). The `aura sweep` CLI verb is now thin sugar over the `campaign document` path — its blueprint form (`<bp.json> --real`) translates to a generated, content-addressed campaign run through the one executor (#210); the built-in `--strategy` sweep surface was retired by #159 (its hard-wired harnesses removed) — sweep now runs from a blueprint + `--axis`, and a retired `--strategy` token falls to the generic usage error. A ganged pair contributes ONE axis.
### tap
**Avoid:**
**Avoid:** probe, monitor, scope (for the observation slot — "probe" is taken by the sweep-terminal `blueprint_axis_probe` sense; the #77 `Recorder``Probe` rename was retired, 2026-07-21)
A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. In a `campaign document`, `persist_taps` names taps from the CLOSED vocabulary `equity | exposure | r_equity | net_r_equity` (`tap_vocabulary`, 0109/#201) — a new observable is a new vocabulary entry or an authored blueprint sink, never an open node-path namespace. Persisted taps are charted by the printed handle: a campaign run's via the record's `trace_name`, a `sweep`/`walkforward --trace` family's via the family handle the run prints (`aura chart <handle>`; its members are keyed `<cell>/<member>` in the chart) — or, equivalently, by the `--trace <NAME>` the user chose, when `NAME` uniquely resolves against the recorded campaign documents (#238; a name reused across runs refuses rather than guessing).
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura run` constructs a recorder at each and persists the series through the trace store; a sweep leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both CLI verbs (`aura run`, `aura measure`) pass a record-all plan, so their semantics are unchanged.
### topology hash
**Avoid:** —
The `content id` of a run's signal blueprint in its run-record role: stamped into the manifest as `topology_hash`, keying the reproduction store and anchoring `aura reproduce` — the same SHA-256 over the same canonical bytes. Distinct from the debug-name-blind `identity id`.