From bd0c557f16ee213a828b0b17cc05e00341241cd2 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 21 Jul 2026 23:56:14 +0200 Subject: [PATCH] =?UTF-8?q?docs(ledger,=20glossary):=20tap-subscribers=20c?= =?UTF-8?q?ycle=20tidy=20=E2=80=94=20C8/C27/C28=20lifts,=20tap=20subscript?= =?UTF-8?q?ion=20vocabulary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../contracts/c08-node-contract.history.md | 14 ++++++++ docs/design/contracts/c08-node-contract.md | 26 +++++++++----- docs/design/contracts/c27-declared-taps.md | 35 ++++++++++++++----- docs/design/contracts/c28-stratification.md | 19 +++++++--- docs/glossary.md | 4 ++- 5 files changed, 75 insertions(+), 23 deletions(-) diff --git a/docs/design/contracts/c08-node-contract.history.md b/docs/design/contracts/c08-node-contract.history.md index cbd0d42..1cbe136 100644 --- a/docs/design/contracts/c08-node-contract.history.md +++ b/docs/design/contracts/c08-node-contract.history.md @@ -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. diff --git a/docs/design/contracts/c08-node-contract.md b/docs/design/contracts/c08-node-contract.md index 5817054..9e18ea2 100644 --- a/docs/design/contracts/c08-node-contract.md +++ b/docs/design/contracts/c08-node-contract.md @@ -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`, `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 diff --git a/docs/design/contracts/c27-declared-taps.md b/docs/design/contracts/c27-declared-taps.md index b8ffa1a..5985383 100644 --- a/docs/design/contracts/c27-declared-taps.md +++ b/docs/design/contracts/c27-declared-taps.md @@ -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` 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` 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)); diff --git a/docs/design/contracts/c28-stratification.md b/docs/design/contracts/c28-stratification.md index 2b4d2d8..6592ea1 100644 --- a/docs/design/contracts/c28-stratification.md +++ b/docs/design/contracts/c28-stratification.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` 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 diff --git a/docs/glossary.md b/docs/glossary.md index 48ff14f..c12f97e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -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=`), 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 (` --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 `; its members are keyed `/` in the chart) — or, equivalently, by the `--trace ` 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`.