feat(cli): generalize no-window fallback resolves the intersection of all symbols' windows

The no---from/--to fallback in dispatch_generalize probes EACH listed
symbol's full archive window (same partial bounds threaded through) and
resolves the one shared campaign window as their intersection — latest
start, earliest end — instead of symbols[0]'s window alone, so the
cross-instrument floor is measured over a genuinely common period.
Disjoint archives refuse at runtime (exit 1) naming each symbol's span.
The intersect-or-refuse arithmetic lives in the pure
intersect_shared_window helper (unit-tested: single symbol, overlapping,
disjoint); the explicit --from+--to path stays byte-identical. The
superseded shape test is renamed to
generalize_without_explicit_window_resolves_a_shared_window_and_completes
with its doc comment reworded to the intersection contract.

closes #213
This commit is contained in:
2026-07-11 14:02:41 +02:00
parent bb8c01895b
commit d3ad9f4e73
2 changed files with 100 additions and 16 deletions
+90 -7
View File
@@ -2959,8 +2959,10 @@ fn exit_axis_register_error(e: AxisRegisterError) -> ! {
/// seam-crossing (never reimplement the division inline) so the executor's /// seam-crossing (never reimplement the division inline) so the executor's
/// `unix_ms_to_epoch_ns` re-normalizes exactly once downstream, not twice. /// `unix_ms_to_epoch_ns` re-normalizes exactly once downstream, not twice.
/// Shared by the campaign dispatchers — sweep/walkforward/mc always clip; /// Shared by the campaign dispatchers — sweep/walkforward/mc always clip;
/// generalize reaches here only through its no-explicit-window fallback (an /// generalize reaches here once per listed symbol, only through its
/// explicit `--from`+`--to` pair passes through unclipped there BY DESIGN). /// no-explicit-window fallback, and intersects the per-symbol results into the
/// one shared window (#213); an explicit `--from`+`--to` pair passes through
/// unclipped there BY DESIGN.
fn campaign_window_ms(choice: DataChoice, env: &project::Env) -> (i64, i64) { fn campaign_window_ms(choice: DataChoice, env: &project::Env) -> (i64, i64) {
let source = DataSource::from_choice(choice, env); let source = DataSource::from_choice(choice, env);
let (from_ts, to_ts) = source.full_window(env); let (from_ts, to_ts) = source.full_window(env);
@@ -2970,6 +2972,30 @@ fn campaign_window_ms(choice: DataChoice, env: &project::Env) -> (i64, i64) {
) )
} }
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.
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))
}
}
/// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or /// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or
/// the built-in harness-kind dispatch. /// the built-in harness-kind dispatch.
fn dispatch_run(a: RunCmd, env: &project::Env) { fn dispatch_run(a: RunCmd, env: &project::Env) {
@@ -3124,13 +3150,35 @@ fn dispatch_generalize(a: GeneralizeCmd, env: &project::Env) {
.unwrap_or_else(|e| exit_axis_register_error(e)); .unwrap_or_else(|e| exit_axis_register_error(e));
// Window: the explicit --from/--to when both present (byte-identical to the // Window: the explicit --from/--to when both present (byte-identical to the
// retired welded path, verified by the exact-grade anchor); otherwise the // retired welded path, verified by the exact-grade anchor); otherwise the
// first symbol's full archive window as the single shared campaign window. // INTERSECTION of every listed symbol's full archive window the only span
// over which every instrument actually has data (#213). A single listed
// symbol degenerates to its own full window unchanged.
let (from_ms, to_ms) = match (from, to) { let (from_ms, to_ms) = match (from, to) {
(Some(f), Some(t)) => (f, t), (Some(f), Some(t)) => (f, t),
_ => campaign_window_ms( _ => {
DataChoice::Real { symbol: symbols[0].clone(), from_ms: from, to_ms: to }, let windows: Vec<(i64, i64)> = symbols
env, .iter()
), .map(|s| {
campaign_window_ms(
DataChoice::Real { symbol: s.clone(), from_ms: from, to_ms: to },
env,
)
})
.collect();
match intersect_shared_window(&symbols, &windows) {
Ok(w) => w,
Err(spans) => {
for (s, w) in spans {
eprintln!("aura: generalize: {s} spans [{}, {}]", w.0, w.1);
}
eprintln!(
"aura: generalize: no window is shared across all listed symbols \
(their archives do not overlap)"
);
std::process::exit(1);
}
}
}
}; };
let inv = verb_sugar::SugarInvocation { let inv = verb_sugar::SugarInvocation {
axes: &raw_axes, axes: &raw_axes,
@@ -3601,6 +3649,41 @@ fn main() {
mod tests { mod tests {
use super::*; 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))]),
);
}
/// Regenerates the shipped r_breakout examples from the carved signal. Run by hand: /// Regenerates the shipped r_breakout examples from the carved signal. Run by hand:
/// `cargo test --bin aura emit_r_breakout_examples -- --ignored`. The examples are /// `cargo test --bin aura emit_r_breakout_examples -- --ignored`. The examples are
/// green-by-construction (serialised from the builder, never hand-authored). /// green-by-construction (serialised from the builder, never hand-authored).
+10 -9
View File
@@ -3000,18 +3000,19 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
assert_eq!(count("campaigns"), 2, "two distinct candidates persist two distinct campaign documents"); assert_eq!(count("campaigns"), 2, "two distinct candidates persist two distinct campaign documents");
} }
/// Property (#210 T3, dissolution): omitting `--from`/`--to` still completes — /// Property (#210 T3, dissolution; window semantics superseded by #213):
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in /// omitting `--from`/`--to` still completes — the dispatch rewrite's
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's /// window-resolution fallback (`dispatch_generalize` in main.rs) resolves ONE
/// full archive geometry and applies it to every instrument in the campaign /// shared campaign window from the INTERSECTION of the listed symbols' full
/// archive geometry and applies it to every instrument in the campaign
/// document (a `CampaignDoc` carries a single shared window, unlike the old /// document (a `CampaignDoc` carries a single shared window, unlike the old
/// inline `run_generalize`, which let each instrument resolve its OWN /// inline `run_generalize`, which let each instrument resolve its OWN
/// independent full window). A wrong index or an empty-`symbols` panic in that /// independent full window). A wrong intersection (or a panic on the
/// new fallback would only surface when `--from`/`--to` are absent — every /// per-symbol probe) in that fallback would only surface when `--from`/`--to`
/// other generalize e2e pins the explicit-window path, leaving this one /// are absent — every other generalize e2e pins the explicit-window path,
/// unexercised. Gated on local GER40/USDJPY data. /// leaving this one unexercised. Gated on local GER40/USDJPY data.
#[test] #[test]
fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_window() { fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() {
let (cwd, _g) = fresh_project(); let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))