refactor: extract the member-run recipe into library crates (#295 part 1)

The shell no longer defines what an aura backtest is. Tasks 1-9 of the
shell-boundary cycle — structural extraction, behaviour byte-identical:

- aura-runner (new; the C28 assembly position): input binding (the C26
  module, moved whole), the C1-load-bearing param<->config translators,
  harness assembly (wrap_r / run_signal_r / run_blueprint_member and the
  probe/reopen cluster), axis + risk-regime conventions (bind_axes et
  al.), the campaign family builders + MC guards, reproduce
  (process::exit -> RunnerError, shell remaps to identical bytes), the
  measurement-run orchestration, project loading (Env / cdylib load /
  charter / staleness, moved whole), the coverage gap-walk (deduplicated
  onto interior_gap_months), and DefaultMemberRunner — the public
  implementation of aura_campaign::MemberRunner (ex CliMemberRunner).
  The MemberRunner trait stays in aura-campaign; the column still
  imports no harness/data-binding machinery.
- aura-measurement (new; seeds C28 rung 3): the IcMetrics/IcKey
  vocabulary + information_coefficient, verbatim incl. serde derives
  (#294 duplicate-timestamp semantics move as-is, unresolved).
- aura-backtest: the pure per-run scaffold (point_from_params,
  wf_ms_sizes / fit_wf_ms_sizes, intersect_shared_window).
- shell residue: main.rs / campaign_run.rs keep argv/dispatch,
  argv->document translation, and presentation only;
  walkforward_summary_json_from_reports now calls the public
  aura_engine::param_stability instead of its inline twin.
- worked example (crates/aura-runner/examples/world_member_run.rs): a
  World program runs a member backtest through the library alone.

Held quality findings, adjudicated: the plan's literal pub-visibility
list for binding.rs kept (cosmetic); ~20 refusal sites inside
aura-runner (family/member/measure/translate) still process::exit —
behaviour-identical today, conversion to RunnerError is filed forward
(refs #297); rustfmt line-width drift on re-pathed call sites left for
a repo-wide fmt decision.

Verification: cargo test --workspace green (1471 passed, 0 failed);
cargo clippy --workspace --all-targets -D warnings clean. Byte-identity
is pinned by the untouched shell E2E suites (cli_run, measure_ic,
run_measurement, research_docs, run_refuses_unrunnable_blueprint,
tap_recording — zero assertion edits).

Remaining in this cycle: full-workspace c28_layering + shell-content
check, ledger amendments (C28 assembly position, C25/C14 control-surface
consequence, C26 realization note).

refs #295
This commit is contained in:
2026-07-21 05:20:27 +02:00
parent 2cf4574e33
commit 5c2ac982bc
29 changed files with 5048 additions and 4370 deletions
+5
View File
@@ -9,6 +9,7 @@
mod mc;
mod metrics;
mod position_management;
pub mod scaffold;
mod sim_broker;
// `assemble_mc` stays module-private (it was never `pub` in the pre-#291 engine
@@ -23,6 +24,10 @@ pub use position_management::{
ExitReason, FIELD_NAMES as PM_FIELD_NAMES, PositionManagement,
RECORD_KINDS as PM_RECORD_KINDS, WIDTH as PM_WIDTH,
};
pub use scaffold::{
fit_wf_ms_sizes, intersect_shared_window, point_from_params, wf_ms_sizes, WF_REAL_IS_NS,
WF_REAL_OOS_NS, WF_REAL_STEP_NS,
};
pub use sim_broker::SimBroker;
/// The trading instantiation of the engine's metric-generic run record (C28
+166
View File
@@ -0,0 +1,166 @@
//! The pure per-run scaffold: bootstrap-adjacent arithmetic that has no
//! dependency on the CLI shell — reconstructing a bootstrap point from
//! recorded params, sizing the real walk-forward roller, and intersecting
//! per-symbol data windows. Relocated verbatim from `aura-cli::main` (#295)
//! so it is reachable (and unit-testable) without the shell.
use aura_core::{Cell, ParamSpec, Scalar};
/// Reconstruct a member's bootstrap point from its recorded named params — the inverse
/// of `zip_params(space, point)`. Walks the reloaded signal's `param_space` in order
/// (deterministic for the same blueprint) and reads each knob's value from the manifest.
///
/// `Err` carries the exact refusal message a manifest missing a param the reloaded
/// space expects has always produced (corrupted-on-disk data): this crate has no
/// process-exit register of its own (#295 — a pure library function reports a
/// refusal, it does not end the process), so the caller is the one that turns this
/// into `aura:` + exit 1, byte-identical to before.
pub fn point_from_params(space: &[ParamSpec], params: &[(String, Scalar)]) -> Result<Vec<Cell>, String> {
space
.iter()
.map(|ps| {
params
.iter()
.find(|(n, _)| n == &ps.name)
.map(|(_, s)| s.cell())
.ok_or_else(|| format!("manifest is missing param {}", ps.name))
})
.collect()
}
/// Real walk-forward roller sizes (Fork D/F). `WindowRoller` takes sizes in the
/// stream's epoch-unit; for real M1 that is nanoseconds. A classic 3-month
/// in-sample / 1-month out-of-sample / 1-month step (contiguous OOS tiling).
const WF_DAY_NS: i64 = 86_400_000_000_000;
pub const WF_REAL_IS_NS: i64 = 90 * WF_DAY_NS;
pub const WF_REAL_OOS_NS: i64 = 30 * WF_DAY_NS;
pub const WF_REAL_STEP_NS: i64 = 30 * WF_DAY_NS;
/// The real walk-forward roller sizes in ms (the campaign wf stage works in ms:
/// span `cell.window_ms`, sizes the `StageBlock` `_ms` fields). `WF_REAL_*_NS` are
/// nanoseconds; divide by 1e6. The ms sizes over the doc window reproduce the same
/// calendar windows the inline ns roller produces (anchor-gated).
pub fn wf_ms_sizes() -> (u64, u64, u64) {
(
(WF_REAL_IS_NS / 1_000_000) as u64,
(WF_REAL_OOS_NS / 1_000_000) as u64,
(WF_REAL_STEP_NS / 1_000_000) as u64,
)
}
/// Fit the fixed real-archive walk-forward roller (`wf_ms_sizes`, 90/30/30 days)
/// to a resolved campaign window `(from_ms, to_ms)` (#239). When the fixed
/// IS+OOS span fits inside the window, the sizes come back byte-identical (every
/// existing anchor/e2e over a year-plus window pins this branch). When the window
/// is shorter than IS+OOS, the roller scales DOWN to the window, preserving the
/// fixed 3:1 IS:OOS ratio, with `step_ms == oos_ms` (a window of exactly IS+OOS
/// then yields exactly one roll — never zero). Pure and unit-testable; both
/// `dispatch_walkforward`/`dispatch_mc` consume it in place of the unconditional
/// `wf_ms_sizes()` stamp. The executor's own fit check (`campaign_run.rs`) stays
/// untouched — it still validates an AUTHORED document's declared window as-is.
pub fn fit_wf_ms_sizes(from_ms: i64, to_ms: i64) -> (u64, u64, u64) {
let (is_ms, oos_ms, step_ms) = wf_ms_sizes();
let span_ms = to_ms.saturating_sub(from_ms).max(0) as u64;
if is_ms + oos_ms <= span_ms {
return (is_ms, oos_ms, step_ms);
}
// Preserve the fixed IS:OOS ratio derived from the constants themselves (not a
// bare literal): OOS = span/(ratio+1) (floor), IS = ratio*OOS, so
// IS+OOS = (ratio+1)*OOS <= span always holds, and step == OOS (one roll over
// the fit window, matching the fixed-size branch's `step_ms == oos_ms`).
let ratio = is_ms / oos_ms;
let oos_fit = span_ms / (ratio + 1);
let is_fit = oos_fit * ratio;
(is_fit, oos_fit, oos_fit)
}
type SymbolSpans<'a> = Vec<(&'a str, (i64, i64))>;
/// The pure decision `generalize`'s no-explicit-window fallback reduces to once
/// every listed symbol's full window is resolved (#213): the shared window is
/// the intersection (latest start, earliest end); `Err` carries each symbol
/// paired with its own resolved window when the intersection is empty (the
/// archives never overlap), for the caller's eprintln-then-exit(1) refusal.
/// Kept separate from `dispatch_generalize` so the intersect-or-refuse
/// arithmetic is unit-testable on synthetic windows — real archives on this
/// host never disjoint (every symbol's tail reaches the live present), so the
/// refusal branch has no reachable e2e fixture.
pub fn intersect_shared_window<'a>(
symbols: &'a [String],
windows: &[(i64, i64)],
) -> Result<(i64, i64), SymbolSpans<'a>> {
let shared_from = windows.iter().map(|w| w.0).max().expect("caller passes at least one window");
let shared_to = windows.iter().map(|w| w.1).min().expect("caller passes at least one window");
if shared_from > shared_to {
Err(symbols.iter().map(String::as_str).zip(windows.iter().copied()).collect())
} else {
Ok((shared_from, shared_to))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A single listed symbol degenerates to its own full window unchanged
/// (#213) — the intersection of one window with itself is that window.
#[test]
fn intersect_shared_window_of_a_single_symbol_is_its_own_window() {
let symbols = vec!["GER40".to_string()];
let windows = vec![(100, 200)];
assert_eq!(intersect_shared_window(&symbols, &windows), Ok((100, 200)));
}
/// Overlapping multi-symbol windows resolve to the intersection — the
/// latest start, earliest end (#213) — never `symbols[0]`'s own window.
#[test]
fn intersect_shared_window_of_overlapping_windows_is_latest_start_earliest_end() {
let symbols = vec!["AAPL.US".to_string(), "GER40".to_string()];
let windows = vec![(0, 300), (100, 200)];
assert_eq!(intersect_shared_window(&symbols, &windows), Ok((100, 200)));
}
/// Disjoint archives (no shared instant across all listed symbols) refuse
/// rather than silently pooling a floor over different periods per
/// instrument (#213); the error names each symbol next to its own span,
/// which the caller eprintln's before exiting 1 — the boundary this
/// covers is otherwise unreachable in an e2e fixture on this host, since
/// every archived symbol's tail reaches the live present (no two windows
/// are ever genuinely disjoint here).
#[test]
fn intersect_shared_window_of_disjoint_windows_refuses_with_every_span() {
let symbols = vec!["A".to_string(), "B".to_string()];
let windows = vec![(0, 100), (200, 300)];
assert_eq!(
intersect_shared_window(&symbols, &windows),
Err(vec![("A", (0, 100)), ("B", (200, 300))]),
);
}
/// Property: a window that already fits the fixed 90/30/30-day roller passes
/// it through byte-identical (the year-plus anchor/e2e grade pins rely on this
/// branch never perturbing the fixed sizes).
#[test]
fn fit_wf_ms_sizes_passes_through_when_the_window_already_fits() {
let day_ms: i64 = 24 * 60 * 60 * 1_000;
let from_ms = 0;
let to_ms = 121 * day_ms; // > 90 + 30 days
assert_eq!(fit_wf_ms_sizes(from_ms, to_ms), wf_ms_sizes());
}
/// Property: a window shorter than IS+OOS scales the roller DOWN to the
/// window, preserving the fixed 3:1 IS:OOS ratio, with `step_ms == oos_ms`
/// (one roll over the fit window) and `is_ms + oos_ms <= span_ms` always.
#[test]
fn fit_wf_ms_sizes_scales_down_to_a_short_window_at_3_to_1() {
let day_ms: i64 = 24 * 60 * 60 * 1_000;
let from_ms = 0;
let to_ms = 30 * day_ms; // far shorter than the fixed 90+30-day roller
let span_ms = (to_ms - from_ms) as u64;
let (is_ms, oos_ms, step_ms) = fit_wf_ms_sizes(from_ms, to_ms);
assert_eq!(is_ms, oos_ms * 3, "the fit preserves the fixed 3:1 IS:OOS ratio");
assert_eq!(step_ms, oos_ms, "one roll over the fit window: step == oos");
assert!(is_ms + oos_ms <= span_ms, "the fit roller must fit inside the window");
assert!(oos_ms > 0, "a 30-day window must yield a non-degenerate fit");
}
}