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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user