feat: full-workspace C28 guard — the shell boundary is structural (#295 part 2)

Completes the shell-boundary cycle (tasks 10-13):

- c28_layering now enumerates the FULL workspace: every crate row is
  reconciled against its real production [dependencies], a completeness
  assertion pins the table to the on-disk crates/ set (a new crate can no
  longer escape the guard silently), the shell/assembly imported-by-
  nothing rule is asserted, and a shell-content check pins aura-cli to
  no-[lib] plus a closed allow-list of argv/translation/presentation
  modules. A new domain module in the shell now fails the suite.
- Ledger amendments: C28 gains the assembly position (aura-runner) and
  the corrected import-rule prose (the ratified aura-campaign ->
  aura-backtest production edge, #291/#292); phase 3 is marked done with
  the #297 process::exit residual named; a provenance note records this
  as structural-debt closure, not a demonstrated downstream blocker.
  C25 records the control-surface decision: the text artifact vocabulary
  is canonical, every control surface (CLI executor verbs, a future
  host, an MCP face, a World program) is a projection/executor over it —
  the which-projection-next ranking deliberately open on #295. C14 gets
  the executor-face amendment, C26 the binding-module relocation.
- aura_campaign::MemberRunner's doc names the shipped default
  implementor (aura-runner::DefaultMemberRunner) while the column keeps
  zero dependency on it.
- New library-only E2E fixture (aura-runner/tests/world_member_run_e2e):
  runs the canonical member recipe over a tiny synthetic archive with no
  aura-cli in the link graph, and pins C1 determinism (two independently
  constructed runners produce a byte-identical RunReport).
- campaign_run.rs module doc: intra-doc MemberRunner link re-anchored to
  aura_campaign::MemberRunner (the impl moved out with part 1).

Verification: cargo test --workspace green (1473 passed, 0 failed);
clippy --workspace --all-targets -D warnings clean; cargo doc clean of
NEW warnings (the five unresolved-link warnings in aura-std/aura-backtest
predate this cycle byte-identically at the anchor — #288-era doc drift,
left for the cycle audit).

refs #295
This commit is contained in:
2026-07-21 06:35:31 +02:00
parent 5c2ac982bc
commit 170c6c82dc
7 changed files with 360 additions and 18 deletions
Generated
+1
View File
@@ -295,6 +295,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"toml", "toml",
"zip",
] ]
[[package]] [[package]]
+4
View File
@@ -61,6 +61,10 @@ pub struct CellSpec {
/// them to its harness convention and runs the member over `cell.instrument` /// them to its harness convention and runs the member over `cell.instrument`
/// restricted to `window_ms` (inclusive epoch-ms, a sub-range of /// restricted to `window_ms` (inclusive epoch-ms, a sub-range of
/// `cell.window_ms` — a walk-forward stage passes sub-windows). /// `cell.window_ms` — a walk-forward stage passes sub-windows).
///
/// The shipped default implementation is `aura_runner::DefaultMemberRunner`
/// (the C28 assembly crate); this crate deliberately does not depend on it —
/// consumers pass the runner in (#295).
pub trait MemberRunner: Sync { pub trait MemberRunner: Sync {
fn run_member( fn run_member(
&self, &self,
+1 -1
View File
@@ -1,7 +1,7 @@
//! `aura campaign run` — the driver that turns a stored campaign document into //! `aura campaign run` — the driver that turns a stored campaign document into
//! a realized run-set (#198). The execution *semantics* (preflight, cell loop, //! a realized run-set (#198). The execution *semantics* (preflight, cell loop,
//! stage sequencing, selection, registry writes) live in `aura-campaign`; the //! stage sequencing, selection, registry writes) live in `aura-campaign`; the
//! [`MemberRunner`] implementation over the shipped loaded-blueprint machinery //! [`MemberRunner`](aura_campaign::MemberRunner) implementation over the shipped loaded-blueprint machinery
//! and the trace-persistence disk-layout writers live in `aura-runner`'s //! and the trace-persistence disk-layout writers live in `aura-runner`'s
//! `runner` module (#295 Task 7 — `DefaultMemberRunner` + //! `runner` module (#295 Task 7 — `DefaultMemberRunner` +
//! `persist_campaign_traces`). This module owns what is CLI-specific: target //! `persist_campaign_traces`). This module owns what is CLI-specific: target
+9
View File
@@ -30,3 +30,12 @@ serde_json = { workspace = true }
sha2 = "0.10" sha2 = "0.10"
libloading = "0.8" libloading = "0.8"
toml = "0.8" toml = "0.8"
[dev-dependencies]
# zip: hand-packs a tiny synthetic M1 archive for the library-reachability E2E
# (tests/world_member_run_e2e.rs) — already transitive via the data-server
# dependency above (a normal, non-dev dependency there); declared directly
# here so test code may `use zip::...` (mirrors aura-ingest's Cargo.toml and
# aura-cli's tests/common/mod.rs, no new dependency actually enters the build
# graph).
zip = "2"
@@ -0,0 +1,152 @@
//! Property (#295): the canonical member-run recipe — [`DefaultMemberRunner`]
//! driven through [`aura_campaign::CellSpec`] — is reachable as a **pure
//! library call**: no `aura` binary, no `aura-cli` crate in the dependency
//! graph (this test binary links only `aura-runner` + its own deps). This is
//! the acceptance evidence `examples/world_member_run.rs` demonstrates by
//! printing; here it is pinned by assertion instead of eyeballed output, and
//! runs over a tiny synthetic archive rather than skipping when no local host
//! archive is mounted.
//!
//! A second property rides along for free at no extra fixture cost (C1,
//! determinism): two independently constructed runners (fresh `DataServer`,
//! fresh `DefaultMemberRunner`) driven over byte-identical input produce a
//! byte-identical `RunReport` — same input, same run, reproducibly.
use std::collections::BTreeMap;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use aura_campaign::{CellSpec, MemberRunner};
use aura_ingest::DataServer;
use aura_runner::{DefaultMemberRunner, Env};
const SYMBOL: &str = "TESTSYM";
// 2024-06-03T08:00:00Z (a real Monday) — arbitrary, fixed, deterministic.
const BASE_MS: i64 = 1_717_401_600_000;
const N_BARS: i64 = 60;
/// A unique scratch directory under the OS temp root, removed on drop —
/// mirrors `aura-ingest`'s own `tests::ScratchDir` (pid + atomic counter, no
/// `tempfile` dev-dependency needed for one throwaway fixture dir).
struct ScratchDir(PathBuf);
impl ScratchDir {
fn new() -> Self {
static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir()
.join(format!("aura-runner-e2e-{}-{n}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create scratch dir");
Self(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// Packs one `RawM1Record`-shaped 48-byte little-endian record — the exact
/// on-disk format `data-server` reads (mirrors `aura-ingest`'s and
/// `aura-cli`'s own synthetic-archive test helpers).
fn pack_record(unix_ms_time: i64, open: f64, high: f64, low: f64, close: f64) -> [u8; 48] {
const DELPHI_EPOCH_OFFSET_DAYS: f64 = 25569.0;
const MS_PER_DAY: f64 = 86_400_000.0;
let dt = unix_ms_time as f64 / MS_PER_DAY + DELPHI_EPOCH_OFFSET_DAYS;
let mut rec = [0u8; 48];
rec[0..8].copy_from_slice(&dt.to_le_bytes());
rec[8..16].copy_from_slice(&open.to_le_bytes());
rec[16..24].copy_from_slice(&high.to_le_bytes());
rec[24..32].copy_from_slice(&low.to_le_bytes());
rec[32..40].copy_from_slice(&close.to_le_bytes());
rec[40..44].copy_from_slice(&0.5_f32.to_le_bytes());
rec[44..48].copy_from_slice(&100_i32.to_le_bytes());
rec
}
/// Writes a tiny, deterministic one-month M1 archive (`N_BARS` consecutive
/// 1-minute bars, strictly ascending close so the shipped Donchian breakout
/// blueprint actually latches a non-flat bias) plus its geometry sidecar,
/// under `dir`. Returns the inclusive `(first_ms, last_ms)` bar span.
fn write_tiny_archive(dir: &Path) -> (i64, i64) {
let path = dir.join(format!("{SYMBOL}_2024_06.m1"));
let file = std::fs::File::create(&path).expect("create synthetic m1 zip");
let mut zip = zip::ZipWriter::new(file);
zip.start_file::<String, ()>(format!("{SYMBOL}.bin"), Default::default())
.expect("start synthetic m1 zip entry");
for i in 0..N_BARS {
let ms = BASE_MS + i * 60_000;
let close = 100.0 + i as f64 * 2.0; // strictly ascending -> a real breakout
let open = close - 1.0;
let high = close + 0.5;
let low = open - 0.5;
zip.write_all(&pack_record(ms, open, high, low, close)).expect("write record");
}
zip.finish().expect("finish synthetic m1 zip");
std::fs::write(
dir.join(format!("{SYMBOL}.meta.json")),
"{\"schemaVersion\":2,\"symbol\":\"TESTSYM\",\"digits\":1,\"pipSize\":1,\
\"tickSize\":0.1,\"lotSize\":1,\"baseAsset\":\"TESTSYM\",\"quoteAsset\":\"EUR\"}",
)
.expect("write synthetic geometry sidecar");
(BASE_MS, BASE_MS + (N_BARS - 1) * 60_000)
}
/// One fresh `DefaultMemberRunner` over its own `DataServer` instance pointed
/// at `data_dir`, running `cell` end to end.
fn run_once(
data_dir: &Path,
cell: &CellSpec,
window_ms: (i64, i64),
) -> Result<aura_backtest::RunReport, aura_campaign::MemberFault> {
let env = Env::std();
let server = std::sync::Arc::new(DataServer::new(data_dir));
let runner = DefaultMemberRunner::new(&env, server, BTreeMap::new(), Vec::new());
runner.run_member(cell, &[], window_ms)
}
#[test]
/// Property: `DefaultMemberRunner::run_member` — the shipped C28 assembly
/// recipe — runs a real member backtest to completion through nothing but
/// `aura-runner` + `aura-campaign` library calls, and does so deterministically:
/// two independently constructed runners over byte-identical archive input
/// produce a byte-identical `RunReport` (C1).
fn default_member_runner_is_reachable_as_a_pure_library_call_and_is_deterministic() {
let scratch = ScratchDir::new();
let (first_ms, last_ms) = write_tiny_archive(scratch.path());
let blueprint_json =
include_str!("../../aura-cli/examples/r_breakout.json").to_string();
let cell = CellSpec {
strategy_ordinal: 0,
strategy_id: aura_research::content_id_of(&blueprint_json),
blueprint_json,
axes: BTreeMap::new(),
instrument: SYMBOL.to_string(),
window_ms: (first_ms, last_ms),
regime: None,
regime_ordinal: 0,
};
let report_a = run_once(scratch.path(), &cell, (first_ms, last_ms))
.expect("the member run must succeed over its own tiny archive");
let report_b = run_once(scratch.path(), &cell, (first_ms, last_ms))
.expect("a second, independently constructed runner must succeed identically");
assert_eq!(
report_a, report_b,
"same cell, same archive, two independent DefaultMemberRunner instances: \
the RunReport must be byte-identical (C1 determinism)"
);
assert!(
report_a.metrics.total_pips.abs() > 0.0,
"the fixture's strictly-ascending closes must drive a non-flat run, else \
the determinism check above is vacuous (both sides equally empty): {:?}",
report_a.metrics
);
}
@@ -12,6 +12,27 @@ fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..") PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..")
} }
/// Every `crates/*` directory that has its own `Cargo.toml` — the actual workspace
/// membership, read from disk rather than duplicated by hand, so the `allowed` table
/// below can be checked for completeness instead of silently omitting a crate.
fn workspace_crate_names() -> Vec<String> {
let crates_dir = workspace_root().join("crates");
let mut names: Vec<String> = std::fs::read_dir(&crates_dir)
.unwrap_or_else(|e| panic!("read {}: {e}", crates_dir.display()))
.filter_map(|entry| {
let entry = entry.expect("dir entry");
let path = entry.path();
if path.join("Cargo.toml").is_file() {
Some(entry.file_name().into_string().expect("utf8 dir name"))
} else {
None
}
})
.collect();
names.sort();
names
}
/// The `aura-*` keys under `[dependencies]` (NOT `[dev-dependencies]`) of a crate. /// The `aura-*` keys under `[dependencies]` (NOT `[dev-dependencies]`) of a crate.
fn prod_intra_workspace_deps(crate_name: &str) -> Vec<String> { fn prod_intra_workspace_deps(crate_name: &str) -> Vec<String> {
let manifest = workspace_root() let manifest = workspace_root()
@@ -35,6 +56,7 @@ fn prod_intra_workspace_deps(crate_name: &str) -> Vec<String> {
fn node_crates_obey_the_c28_import_direction() { fn node_crates_obey_the_c28_import_direction() {
// (crate, the intra-workspace [dependencies] C28 permits for its layer) // (crate, the intra-workspace [dependencies] C28 permits for its layer)
let allowed: &[(&str, &[&str])] = &[ let allowed: &[(&str, &[&str])] = &[
("aura-core", &[]), // foundation: no aura-* deps
("aura-analysis", &[]), // foundation: no aura-* deps ("aura-analysis", &[]), // foundation: no aura-* deps
("aura-measurement", &["aura-core", "aura-analysis"]), ("aura-measurement", &["aura-core", "aura-analysis"]),
("aura-std", &["aura-core"]), ("aura-std", &["aura-core"]),
@@ -46,7 +68,74 @@ fn node_crates_obey_the_c28_import_direction() {
"aura-vocabulary", "aura-vocabulary",
&["aura-core", "aura-std", "aura-market", "aura-strategy", "aura-backtest"], &["aura-core", "aura-std", "aura-market", "aura-strategy", "aura-backtest"],
), ),
(
"aura-composites",
&["aura-core", "aura-engine", "aura-std", "aura-strategy", "aura-backtest"],
),
("aura-ingest", &["aura-core", "aura-engine"]),
("aura-research", &["aura-core"]),
(
"aura-registry",
&["aura-core", "aura-engine", "aura-backtest", "aura-analysis", "aura-research"],
),
(
"aura-campaign",
&[
"aura-core",
"aura-engine",
"aura-backtest",
"aura-analysis",
"aura-registry",
"aura-research",
],
),
(
"aura-runner",
&[
"aura-core",
"aura-engine",
"aura-std",
"aura-strategy",
"aura-backtest",
"aura-composites",
"aura-ingest",
"aura-registry",
"aura-research",
"aura-campaign",
"aura-vocabulary",
],
),
("aura-bench", &["aura-core", "aura-engine", "aura-std", "aura-ingest"]),
(
"aura-cli",
&[
"aura-core",
"aura-engine",
"aura-std",
"aura-strategy",
"aura-backtest",
"aura-registry",
"aura-research",
"aura-campaign",
"aura-vocabulary",
"aura-ingest",
"aura-measurement",
"aura-runner",
],
),
]; ];
let mut table_names: Vec<&str> = allowed.iter().map(|(name, _)| *name).collect();
table_names.sort();
let disk_names = workspace_crate_names();
assert_eq!(
table_names,
disk_names.iter().map(String::as_str).collect::<Vec<_>>(),
"C28 full-workspace: `allowed` must enumerate exactly the crates under `crates/` \
(add the new crate's permitted set here, even a foundation crate with no aura-* \
deps) — otherwise it escapes the C28 guard silently"
);
for (crate_name, permitted) in allowed { for (crate_name, permitted) in allowed {
for dep in prod_intra_workspace_deps(crate_name) { for dep in prod_intra_workspace_deps(crate_name) {
assert!( assert!(
@@ -56,4 +145,48 @@ fn node_crates_obey_the_c28_import_direction() {
); );
} }
} }
for (crate_name, permitted) in allowed {
for shell_or_assembly in ["aura-cli", "aura-runner"] {
let exempt = *crate_name == "aura-cli" && shell_or_assembly == "aura-runner";
assert!(
exempt || !permitted.contains(&shell_or_assembly),
"C28 assembly violation: `{crate_name}`'s permitted set names \
`{shell_or_assembly}`, but the shell/assembly crates are imported \
by nothing except the shell itself"
);
}
}
}
#[test]
fn the_shell_defines_no_domain_modules_and_no_lib_target() {
let root = workspace_root();
let manifest_text =
std::fs::read_to_string(root.join("crates/aura-cli/Cargo.toml")).unwrap();
let manifest: toml::Value =
toml::from_str(&manifest_text).expect("Cargo.toml parses as TOML");
assert!(
manifest.get("lib").is_none(),
"C28: the shell exports nothing — aura-cli must stay a pure binary crate"
);
let mut modules: Vec<String> = std::fs::read_dir(root.join("crates/aura-cli/src"))
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.collect();
modules.sort();
assert_eq!(
modules,
[
"campaign_run.rs",
"graph_construct.rs",
"main.rs",
"render.rs",
"research_docs.rs",
"scaffold.rs",
"verb_sugar.rs",
],
"C28 (#295): the shell holds argv/dispatch, argv->document translation, \
and presentation only — a new module needs a library home"
);
} }
+60 -17
View File
@@ -1253,6 +1253,13 @@ manifest stamps the untouched bound defaults (`defaults`, one-directionally
widened beside `params`, which keeps its "what varied" reproduce semantics) so widened beside `params`, which keeps its "what varied" reproduce semantics) so
a raw reader of a fully-bound run no longer sees a misleading `"params": []`; a raw reader of a fully-bound run no longer sees a misleading `"params": []`;
the blueprint itself carries no such duplication. the blueprint itself carries no such duplication.
**Amendment (2026-07-20, #295 — the programmatic face is an executor).** The
member-run recipe — the definition of what a standard aura backtest is — is
library content (`aura-runner`/`aura-backtest`/`aura-measurement`, C28
assembly position), not CLI content: the binary is argv → document →
executor → presentation, and a downstream World program reaches the same
recipe with no binary involved. See C25's control-surface amendment for the
projection rule.
### C15 — Resampling-as-node; sessions/calendars ### C15 — Resampling-as-node; sessions/calendars
**Guarantee.** A resampler is a node (finer stream → coarser bar stream), **Guarantee.** A resampler is a node (finer stream → coarser bar stream),
@@ -2508,6 +2515,15 @@ throughput lives. Realization: cycle 0106 (#189) shipped roles 5/6b their
artifacts (process/campaign documents, C18 realization note); roles 4/7/8 remain artifacts (process/campaign documents, C18 realization note); roles 4/7/8 remain
technically present but faceless (no addressed verb families yet); role homes in technically present but faceless (no addressed verb families yet); role homes in
the project layout and docs-by-role are open (#192 context). the project layout and docs-by-role are open (#192 context).
**Amendment (2026-07-20, #295 — control surfaces are projections).** The
text-first invariant fixes the control-surface layering: the text artifact
vocabulary (blueprints, process/campaign documents, registry records) is the
canonical layer, and every control surface — the one-shot CLI's executor
verbs, any future long-running host, any MCP face, a World program — is a
projection/executor over those artifacts, never a second home for intent.
Which projection is built next (document-first flag-vocabulary completion, a
typed-protocol host, an MCP face, or none until demand) is deliberately open
— recorded as an open direction fork on #295.
### C26 — Harness input binding: role names bind archive columns ### C26 — Harness input binding: role names bind archive columns
**Guarantee.** A strategy blueprint declares WHICH data it consumes as the NAMES **Guarantee.** A strategy blueprint declares WHICH data it consumes as the NAMES
@@ -2518,10 +2534,11 @@ the campaign cell's (or run invocation's) instrument. A campaign document may
**override** the name default per role via the additive `data.bindings` block **override** the name default per role via the additive `data.bindings` block
(`role name → column name`; serde `default` + skip-if-empty, so binding-less (`role name → column name`; serde `default` + skip-if-empty, so binding-less
documents keep their content ids) — the 6b rebind seam: the blueprint's content documents keep their content ids) — the 6b rebind seam: the blueprint's content
identity never changes. Resolution (`aura-cli`'s `binding` module, identity never changes. Resolution (`aura-runner`'s `binding` module,
`resolve_binding`) produces ONE ordered plan consumed by BOTH halves of the old `resolve_binding` — moved out of the shell with #295) produces ONE ordered
weld: the columns are opened (`aura_ingest::open_columns`, the generalization of plan consumed by BOTH halves of the old weld: the columns are opened
`open_ohlc`/#92) and the wrapped root roles declared (`wrap_r`) in the **same (`aura_ingest::open_columns`, the generalization of `open_ohlc`/#92) and the
wrapped root roles declared (`aura-runner::member`'s `wrap_r`) in the **same
canonical order** — `M1Field` declaration order (open, high, low, close, spread, canonical order** — `M1Field` declaration order (open, high, low, close, spread,
volume), filtered to the consumed set — which is the C4 merge tie-break order, volume), filtered to the consumed set — which is the C4 merge tie-break order,
so role i receives source i by construction. A close column is always part of so role i receives source i by construction. A close column is always part of
@@ -2544,7 +2561,7 @@ the binding VALUE-space grows additively from archive columns to recorded-stream
references — a root role like `sentiment` becomes bindable to a recorded stream references — a root role like `sentiment` becomes bindable to a recorded stream
then, and until then refuses with the vocabulary-naming message. The role-name then, and until then refuses with the vocabulary-naming message. The role-name
key-space and the campaign `bindings` carrier are unchanged by that growth. key-space and the campaign `bindings` carrier are unchanged by that growth.
**Why.** The single-price weld (`wrap_r`'s hard-wired `price`←close and the six **Why.** The single-price weld (`aura-runner::member`'s `wrap_r`'s hard-wired `price`←close and the six
`M1Field::Close`-only open sites) made every strategy monocular regardless of `M1Field::Close`-only open sites) made every strategy monocular regardless of
what its blueprint declared; binding by role name keeps the blueprint the single what its blueprint declared; binding by role name keeps the blueprint the single
source of its own data needs (C24: topology-as-data carries its input contract), source of its own data needs (C24: topology-as-data carries its input contract),
@@ -2625,19 +2642,32 @@ outer:
Beside the ladder stands the **process column** — the research-process machinery Beside the ladder stands the **process column** — the research-process machinery
(the run registry C18, the process/campaign document types, campaign execution). (the run registry C18, the process/campaign document types, campaign execution).
It consumes run *artifacts*, not market streams, and orchestrates runs of any It consumes run *artifacts*, not market streams, and orchestrates runs of any
ladder layer; it stands beside the ladder, not on a rung. The **shell** (the ladder layer; it stands beside the ladder, not on a rung. Between ladder/column
`aura` CLI) imports everything and is imported by nothing. and the shell stands the **assembly** position (`aura-runner`, #295): 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` — importing the ladder rungs, `aura-composites`,
`aura-ingest`, and the process column, and imported only by the shell and by
downstream World programs. The **shell** (the `aura` CLI) imports everything,
is imported by nothing, exports nothing, and holds no domain logic:
argv/dispatch, argv→document translation, and presentation only (rendering
stays in the shell until the C22 web face provides its second consumer — #295
deferral).
**Import rule.** An outer ladder layer may import inner layers, never the **Import rule.** An outer ladder layer may import inner layers, never the
reverse; *measurement* and *strategy* are siblings (no import either way); reverse; *measurement* and *strategy* are siblings (no import either way);
*backtest* may import *strategy*. The process column imports the engine layer *backtest* may import *strategy*. The process column imports the engine layer
plus the run-artifact and metric interfaces, and is imported by no ladder crate; plus the run-artifact and metric interfaces — realized since #291/#292 as a
column-internal edges are free. The shell imports all; nothing imports the production dependency on `aura-backtest` (the trading instantiation
shell. **`dev-dependencies` are exempt** — tests may cross layers (the existing `M = RunMetrics`) — and is imported by no ladder crate; column-internal edges
`engine`/`ingest`/`registry``std` edges are dev-only and stay). The rule are free. The assembly position imports ladder and column alike and is
binds **crate structure, not graph composition**: a blueprint may feed a imported by neither; the shell imports all; nothing imports the shell — both
measurement statistic into a bias input — data flow inside a graph is free (C24); now enforced by the full-workspace `c28_layering` table plus its shell-content
layers govern imports, graphs compose freely. check (#295). **`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 **Forbids.** An inner layer importing an outer one — most sharply the engine
layer importing backtest reductions, or market importing strategy. A ladder crate layer importing backtest reductions, or market importing strategy. A ladder crate
@@ -2719,9 +2749,16 @@ cleanly by layer, so the layering is realized only partially:
`aura-backtest`, the statistics kernel to `aura-analysis`, dependency `aura-backtest`, the statistics kernel to `aura-analysis`, dependency
inversion, ladder enforced by the structural test; realizes item 1 of #147); inversion, ladder enforced by the structural test; realizes item 1 of #147);
(3) the #286 (3) the #286
shape dispatch plus the per-run scaffold as a library (seeds an `aura-backtest` shape dispatch plus the per-run scaffold as a library **done** (#295: the
crate, gives *measurement* its run verb, removes the measured O(cycles) run-time pure per-run scaffold pieces live in `aura-backtest::scaffold`; the composed
retention); (4) split the `aura-std` roster along engine / market / strategy / recipe — harness assembly, binding, translators, the default `MemberRunner`
— is the assembly crate `aura-runner`; the measurement rung is seeded as
`aura-measurement` with the IC; the shell is reduced to
argv/translation/presentation, enforced structurally. Residual: ~20 refusal
sites inside `aura-runner`'s single-run verb paths still terminate the
process; their conversion to `RunnerError` propagation is tracked as #297
the campaign path already refuses via `MemberFault`, never a process exit);
(4) split the `aura-std` roster along engine / market / strategy /
backtest — **done** (#288: four `aura-core`-only node crates, the closed roster backtest — **done** (#288: four `aura-core`-only node crates, the closed roster
moved to `aura-vocabulary`, the ladder direction enforced by a structural test); moved to `aura-vocabulary`, the ladder direction enforced by a structural test);
(5) split `aura-analysis` into generic statistics and backtest metrics — (5) split `aura-analysis` into generic statistics and backtest metrics —
@@ -2731,6 +2768,12 @@ cleanly by layer, so the layering is realized only partially:
names now realized (`aura-market`/`aura-strategy`/`aura-backtest`/ names now realized (`aura-market`/`aura-strategy`/`aura-backtest`/
`aura-vocabulary`); full evidence on #288, #286 and the milestone. `aura-vocabulary`); full evidence on #288, #286 and the milestone.
Provenance note (#295): the shell-boundary cut closes a ratified structural
debt (this contract's own phase plan) and un-blocks the future World program;
it was not a demonstrated downstream blocker — the first downstream project's
observed friction was document-vocabulary gaps, never Rust-level recipe
reachability.
--- ---
## Open architectural threads not yet resolved ## Open architectural threads not yet resolved