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:
Generated
+1
@@ -295,6 +295,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"toml",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -61,6 +61,10 @@ pub struct CellSpec {
|
||||
/// them to its harness convention and runs the member over `cell.instrument`
|
||||
/// restricted to `window_ms` (inclusive epoch-ms, a sub-range of
|
||||
/// `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 {
|
||||
fn run_member(
|
||||
&self,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! `aura campaign run` — the driver that turns a stored campaign document into
|
||||
//! a realized run-set (#198). The execution *semantics* (preflight, cell loop,
|
||||
//! 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
|
||||
//! `runner` module (#295 Task 7 — `DefaultMemberRunner` +
|
||||
//! `persist_campaign_traces`). This module owns what is CLI-specific: target
|
||||
|
||||
@@ -30,3 +30,12 @@ serde_json = { workspace = true }
|
||||
sha2 = "0.10"
|
||||
libloading = "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("..")
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn prod_intra_workspace_deps(crate_name: &str) -> Vec<String> {
|
||||
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() {
|
||||
// (crate, the intra-workspace [dependencies] C28 permits for its layer)
|
||||
let allowed: &[(&str, &[&str])] = &[
|
||||
("aura-core", &[]), // foundation: no aura-* deps
|
||||
("aura-analysis", &[]), // foundation: no aura-* deps
|
||||
("aura-measurement", &["aura-core", "aura-analysis"]),
|
||||
("aura-std", &["aura-core"]),
|
||||
@@ -46,7 +68,74 @@ fn node_crates_obey_the_c28_import_direction() {
|
||||
"aura-vocabulary",
|
||||
&["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 dep in prod_intra_workspace_deps(crate_name) {
|
||||
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
@@ -1253,6 +1253,13 @@ manifest stamps the untouched bound defaults (`defaults`, one-directionally
|
||||
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": []`;
|
||||
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
|
||||
**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
|
||||
technically present but faceless (no addressed verb families yet); role homes in
|
||||
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
|
||||
**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
|
||||
(`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
|
||||
identity never changes. Resolution (`aura-cli`'s `binding` module,
|
||||
`resolve_binding`) produces ONE ordered plan consumed by BOTH halves of the old
|
||||
weld: the columns are opened (`aura_ingest::open_columns`, the generalization of
|
||||
`open_ohlc`/#92) and the wrapped root roles declared (`wrap_r`) in the **same
|
||||
identity never changes. Resolution (`aura-runner`'s `binding` module,
|
||||
`resolve_binding` — moved out of the shell with #295) produces ONE ordered
|
||||
plan consumed by BOTH halves of the old weld: the columns are opened
|
||||
(`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,
|
||||
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
|
||||
@@ -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
|
||||
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.
|
||||
**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
|
||||
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),
|
||||
@@ -2625,19 +2642,32 @@ outer:
|
||||
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. The **shell** (the
|
||||
`aura` CLI) imports everything and is imported by nothing.
|
||||
ladder layer; it stands beside the ladder, not on a rung. Between ladder/column
|
||||
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
|
||||
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, and is imported by no ladder crate;
|
||||
column-internal edges are free. 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.
|
||||
plus the run-artifact and metric interfaces — realized since #291/#292 as a
|
||||
production dependency on `aura-backtest` (the trading instantiation
|
||||
`M = RunMetrics`) — and is imported by no ladder crate; column-internal edges
|
||||
are free. The assembly position imports ladder and column alike and is
|
||||
imported by neither; the shell imports all; nothing imports the shell — both
|
||||
now enforced by the full-workspace `c28_layering` table plus its shell-content
|
||||
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
|
||||
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
|
||||
inversion, ladder enforced by the structural test; realizes item 1 of #147);
|
||||
(3) the #286
|
||||
shape dispatch plus the per-run scaffold as a library (seeds an `aura-backtest`
|
||||
crate, gives *measurement* its run verb, removes the measured O(cycles) run-time
|
||||
retention); (4) split the `aura-std` roster along engine / market / strategy /
|
||||
shape dispatch plus the per-run scaffold as a library — **done** (#295: the
|
||||
pure per-run scaffold pieces live in `aura-backtest::scaffold`; the composed
|
||||
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
|
||||
moved to `aura-vocabulary`, the ladder direction enforced by a structural test);
|
||||
(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`/
|
||||
`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
|
||||
|
||||
Reference in New Issue
Block a user