bd0c557f16
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
189 lines
12 KiB
Markdown
189 lines
12 KiB
Markdown
# 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<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](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)
|