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
12 KiB
C28 — Internal stratification: the trading ladder, the process column, the shell
Guarantee. The aura workspace's crates realize a layered responsibility model along two axes. A ladder, ordered by domain specificity from inner to outer:
- engine — the domain-free deterministic streaming runtime and vocabulary
(cells/kinds/freshness/firing, graph build, compile, run, taps/trace — C1–C9,
C19, C23, C24), the domain-free node roster (arithmetic/logic/rolling nodes
plus the generic
Recorder/GatedRecorder/SeriesReducersinks), and generic statistics (the Monte-Carlo and moving-block-bootstrap kernels). The layer name is wider than the crateaura-engine: it spansaura-core,aura-engine, the domain-free node roster (aura-std), and the generic-statistics surface (aura-analysis). - market — what a market is, carrying no analysis or strategy intent:
instruments and pip/point geometry (C15), session anchoring, bar resampling,
market-data ingestion (
aura-ingest). - measurement — descriptive statistics over market streams (session statistics, conditional rates, distributions). Sibling of strategy: both consume market, neither imports the other.
- strategy — the definition of trading signals: bias, stop rules, sizing, cost-model nodes.
- backtest — the evaluation of strategies: simulated execution without
money (the
SimBroker, position-management), the R-metric reductions and the position-event table, and the per-run scaffold. Backtest mirrors execution on the research side — execution semantics in R units, money exiled. - execution — money, account-term sizing, the live broker; already exiled to the deploy edge by C10/C13, existing today only as that ratified boundary.
Beside the ladder stands the process column — the research-process machinery
(the run registry C18, the process/campaign document types, campaign execution).
It consumes run artifacts, not market streams, and orchestrates runs of any
ladder layer; it stands beside the ladder, not on a rung. Between ladder/column
and the shell stands the assembly position (aura-runner): it composes the
rungs into the canonical member-run recipe — harness assembly, input binding
(C26), the C1-load-bearing param↔config translators, and the shipped default
MemberRunner (DefaultMemberRunner) — importing the ladder rungs,
aura-composites, aura-ingest, and the process column, and imported only by
the shell and by downstream World programs. It also carries a direct production
dependency on the external data-server tree (inherited from the shell with the
recipe: the runner constructs archive servers itself). The external tree
therefore enters the workspace at exactly three points — the ingestion edge
(aura-ingest), the assembly position (aura-runner), and the shell
(aura-cli, which imports everything by definition) — and at no other ladder or
column crate; the c28_layering guard pins the exact set. The shell (the
aura CLI, aura-cli) imports everything, is imported by nothing, exports
nothing, and holds no domain logic: argv/dispatch, argv→document translation
(including the op-script construction front-end, graph_construct),
presentation, and the aura new project scaffolder (authoring-tooling template
emission — shell-resident, like rendering, until a second consumer wants it).
Rendering stays in the shell until the C22 web face provides its second consumer.
Import rule. An outer ladder layer may import inner layers, never the
reverse; measurement and strategy are siblings (no import either way);
backtest may import strategy. The process column imports the engine layer
plus the run-artifact and metric interfaces — a production dependency on
aura-backtest for the trading instantiation M = RunMetrics (#291/#292) — and
is imported by no ladder crate; column-internal edges are free (the
aura-registry → aura-research edge is legal). The assembly position imports
ladder and column alike and is imported by neither; the shell imports all;
nothing imports the shell. dev-dependencies are exempt — tests may cross
layers (the existing engine/ingest/registry → std edges are dev-only and
stay). The rule binds crate structure, not graph composition: a blueprint may
feed a measurement statistic into a bias input — data flow inside a graph is free
(C24); layers govern imports, graphs compose freely.
Forbids. An inner layer importing an outer one — most sharply the engine layer importing backtest reductions, or market importing strategy. A ladder crate importing the process column. Money (currency P&L, account sizing) inside the strategy or backtest layers (C10/C13 — it lives only at the execution/deploy edge). Once a second consumer warrants the split, keeping multiple layers welded in one crate's roster.
Why. The stack is a framework specialized for traders: the inner engine is a
domain-free analysis substrate (the same machinery would measure robot telemetry
or any timestamped process), the outer rungs the trading specialization, and the
two together are the "game engine for traders" (C16's game/engine split applied
inside the workspace, from the engine|project boundary to the intra-stack
layering). Separable responsibilities behind narrow, contract-defined seams let a
contributor focus on one rung without absorbing the rest, and make each boundary
auditable. Enforcement is the crate graph itself: once crates are cut along these
cells, a layer violation is a compile error and every new edge a visible
Cargo.toml diff — no linter, no discipline appeals. Five of the six seams are
pre-existing ratified contracts and this model only names them as layer
boundaries: the node contract (C8, engine ↔ any vocabulary), blueprint-as-data
(C24, topology ↔ engine — its shape selecting the run scaffold is the additive
#286 refinement), declared taps (C27, run ↔ analysis — the only way values leave
a run), the run registry (C18, run ↔ process column), and the deploy edge
(C10/C13, everything ↔ money). The sixth — the process column ↔ metric interface
— is the one this contract introduced: a named metric vocabulary supplied by
measurement and backtest instead of baked in R-only (#147, now realized).
Current state
The workspace realizes the full ladder today, cut by layer, with only the execution rung unbuilt. The rung→crate map:
- engine —
aura-core+aura-engine+aura-std(the engine node roster: arithmetic/logic/rolling + the generic sinks) +aura-analysis(domain-free statistics + selection provenance). - market —
aura-market(session,resample) +aura-ingest. - measurement —
aura-measurement(the IC vocabulary + reduction). - strategy —
aura-strategy(bias/stop_rule/sizer+ the cost nodes). - backtest —
aura-backtest(sim_broker,position_management, the R-metric reductions, and the per-run scaffoldaura-backtest::scaffold). - execution — no crate exists; it is the C10/C13 deploy edge only.
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 — 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
payload M (crates/aura-engine/src/report.rs), and aura-backtest supplies the
trading instantiation M = RunMetrics (pub type RunReport = aura_engine::RunReport<RunMetrics>, crates/aura-backtest/src/lib.rs) — the
aura-backtest → aura-engine edge is outer→inner. The engine's production
[dependencies] are aura-core + aura-analysis only; aura-analysis is
reduced to the domain-free half ([dependencies] = serde only), holding the
statistics kernel (MetricStats/quantile/resample_block/SplitMix64) and the
selection-provenance types, while the pip/R reductions and the Monte-Carlo
assembly live in aura-backtest. The former monolithic aura-std node roster is
split by layer: aura-std (engine nodes), aura-market, and aura-strategy are
each aura-core-only node crates, while aura-backtest carries both the backtest
node modules (sim_broker, position_management) and the outer-rung metric
layer, so it depends on aura-engine as well as aura-core. The additive #286
shape dispatch (blueprint shape selecting the run scaffold) is realized; the pure
per-run scaffold pieces live in aura-backtest::scaffold, and aura-runner
composes them with harness assembly, input binding, the param↔config translators,
and the shipped DefaultMemberRunner. The shell is reduced to argv/dispatch,
argv→document translation, and presentation.
The direction is enforced structurally, not by discipline. The Rust compiler only
rejects import cycles; an acyclic-but-outward edge (e.g. aura-std → aura-market)
would compile silently, so crates/aura-vocabulary/tests/c28_layering.rs reads
every crate's production [dependencies] and asserts each intra-workspace edge
stays within the inner set C28 permits for that layer. The allowed table is the
full workspace — all 17 crates under crates/ — and the test asserts the
table enumerates exactly the crates found on disk (table_names == disk_names),
so a newly added crate cannot escape the guard silently: even a foundation crate
with no aura-* deps must be listed. [dev-dependencies] are not inspected
(C28-exempt). Two further checks ride the same file: the shell-content check
(aura-cli is a pure binary with no lib target and a fixed module set —
argv/translation/presentation only), and the external-data-server pin (the tree
may be a production dependency of exactly aura-ingest, aura-runner, and
aura-cli).
#147 disposition (retired; user-ratified direction 2026-07-20, shipped the same
day). The process-column metric interface is generic. The deliberately narrow
MetricVocabulary trait (resolve/roster/direction/value/one null draw) lives in
aura-analysis (crates/aura-analysis/src/lib.rs) and is re-exported through
aura-engine; the R vocabulary (RunMetricKey, r_based, the rosters, the
centred moving-block null_draw) is supplied by aura-backtest; the registry's
rank/optimize/deflate machinery is generic over M: MetricVocabulary, with
refusal prose derived from the carried roster. The IC (aura-measurement) is the
second production implementor, bringing its within-run permutation null. The C10
money wall stays monomorphic — check_r_metric/generalization accept only the R
vocabulary (r_based is the enforcement point). Still deferred ("A2",
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. ~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 — determinism / bit-identity, the correctness invariant the layer cuts preserve
- C8 — the node contract (engine ↔ vocabulary seam)
- C10 / C13 — the deploy edge (everything ↔ money)
- C16 — game/engine split, applied here inside the workspace
- C18 — the run registry (run ↔ process column seam)
- C23 — names are non-load-bearing
- C24 — blueprint-as-data (topology ↔ engine seam)
- C26 — input binding
- C27 — declared taps (run ↔ analysis seam)
History: c28-stratification.history.md