Architect review over 024e865..HEAD (issues #296/#348/#297/#299). Holds confirmed: zero process::exit in the assembly crate with the milestone promise pinned in-process; C1/C18 record shapes untouched with guard and stamp sharing one label constructor; the C30/#348 re-arm transition-safe with no stale references. Drift resolved in this commit: the C14 contract now records the context-borne reproduce guard classes (#299), C30 carries the source-level-identity limit in the contract itself (not only in build.rs), the guide's exit-class prose no longer names the retired run verb, C28 durably enumerates the deliberate print residuum, and the pip_or_refuse doc names reproduce (not 'the CLI shell') as the from_choice home. Bench: all 5 fingerprints OK (engine/ingest/campaign_sweep/campaign_heavy/ cli_fixed_cost); metric deltas within load noise on a busy box (loadavg 12 warning), report-only, no baseline moved.
14 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.
#297 (closed, 2026-07-26). aura-runner no longer terminates the host
process anywhere: the 14 recon-enumerated std::process::exit sites inside
the single-run verb paths (member.rs ×8, measure.rs ×5, translate.rs
×1 — the "~24" this paragraph used to cite predates the #319 dual-grammar
retirement and never updated in lockstep) are now RunnerError { exit_code, message } constructors threaded through a fallible chain; the two entry
points, run_signal_r and run_measurement, return
Result<(_, Vec<String>), RunnerError>, and the CLI's exec_blueprint_leg
remaps via one shared exit_on_runner_error helper (the dispatch_reproduce
pattern). The one hole in "a sweep worker never process-exits" — cost_knob,
reachable from run_blueprint_member — is closed the same cycle: the
campaign worker maps its fault into the cell's own MemberFault channel
(per-cell isolation, this contract's stated campaign guarantee unchanged),
while the campaign trace-persist path — the only other production caller —
reports it through its existing string-error channel into the campaign
summary's exit. Refusal prose is byte-identical everywhere;
only who prints it changed, plus — per the adjudicated C14 partition — the
exit class of refusals whose fault is in the content of what argv named.
What remains printing in the assembly crate is the deliberate note/info
residuum — the fold/trace notes and the traces-persisted line
(runner.rs) and the stale-dylib warning (project.rs) — benign
diagnostics that print and never exit; migrating them needs a note-channel
design of its own.
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