Files
Aura/docs/design/contracts/c28-stratification.md
T
claude bd0c557f16 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
2026-07-21 23:56:14 +02:00

12 KiB
Raw Blame History

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:

  1. engine — the domain-free deterministic streaming runtime and vocabulary (cells/kinds/freshness/firing, graph build, compile, run, taps/trace — C1C9, C19, C23, C24), the domain-free node roster (arithmetic/logic/rolling nodes plus the generic Recorder/GatedRecorder/SeriesReducer sinks), and generic statistics (the Monte-Carlo and moving-block-bootstrap kernels). The layer name is wider than the crate aura-engine: it spans aura-core, aura-engine, the domain-free node roster (aura-std), and the generic-statistics surface (aura-analysis).
  2. 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).
  3. measurement — descriptive statistics over market streams (session statistics, conditional rates, distributions). Sibling of strategy: both consume market, neither imports the other.
  4. strategy — the definition of trading signals: bias, stop rules, sizing, cost-model nodes.
  5. 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.
  6. 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/registrystd 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:

  • engineaura-core + aura-engine + aura-std (the engine node roster: arithmetic/logic/rolling + the generic sinks) + aura-analysis (domain-free statistics + selection provenance).
  • marketaura-market (session, resample) + aura-ingest.
  • measurementaura-measurement (the IC vocabulary + reduction).
  • strategyaura-strategy (bias/stop_rule/sizer + the cost nodes).
  • backtestaura-backtest (sim_broker, position_management, the R-metric reductions, and the per-run scaffold aura-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