# 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 — C1–C9, 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`/`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 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` 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`, `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](c01-determinism.md) — determinism / bit-identity, the correctness invariant the layer cuts preserve - [C8](c08-node-contract.md) — the node contract (engine ↔ vocabulary seam) - [C10](c10-bias-r-cost.md) / [C13](c13-hot-reload-frozen-deploy.md) — the deploy edge (everything ↔ money) - [C16](c16-engine-project-split.md) — game/engine split, applied here inside the workspace - [C18](c18-registry.md) — the run registry (run ↔ process column seam) - [C23](c23-graph-compilation.md) — names are non-load-bearing - [C24](c24-blueprint-data.md) — blueprint-as-data (topology ↔ engine seam) - [C26](c26-input-binding.md) — input binding - [C27](c27-declared-taps.md) — declared taps (run ↔ analysis seam) > History: [c28-stratification.history.md](c28-stratification.history.md)