feat(cli,ingest): binding module + generalized column openers (#231 tasks 1-2)

crates/aura-cli/src/binding.rs: the closed column vocabulary (open/high/
low/close/spread/volume, price as the close alias) with ResolvedBinding
— one ordered plan both halves of the single-price weld will share.
resolve_binding applies override-wins-over-name-default, refuses unknown
roles / bad override values / the price+close double-alias / a rebound
reserved close role, synthesizes the broker-only close entry, and sorts
into canonical M1Field order (the C4 merge tie-break contract). 12
exact-prose unit tests. Module-wide dead_code allow is scaffolding until
the wrap seam consumes it next.

aura-ingest: open_columns / open_columns_window open any field set in
the given order (None if any field lacks an overlapping archive file);
open_ohlc now delegates with the fixed OHLC order — behaviour-identical,
still guarded by the open_ohlc_seam bit-identity test.

Post-review polish: RESERVED_CLOSE_ROLE constant couples the synthesized
close's two sites; dead_code note reworded condition-based.

Verified: binding 12/12, ingest absence test green, full workspace suite
green, clippy -D warnings clean; independent quality review approved.

refs #231
This commit is contained in:
2026-07-11 00:49:22 +02:00
parent 753ab5f0ee
commit 595c98b264
3 changed files with 439 additions and 5 deletions
+72 -5
View File
@@ -312,6 +312,53 @@ impl aura_engine::Source for M1FieldSource {
}
}
/// Open one real [`M1FieldSource`] per requested field for `symbol` over
/// `[from_ms, to_ms]` (inclusive Unix-ms, `None` = open-ended), in the GIVEN
/// field order — callers pass the canonical `M1Field` declaration order (the
/// C4 merge tie-break order, #92), so source index i is field i. All sources
/// share the one `Arc<DataServer>` (one `FileCache`), so a window's bars are
/// parsed once and reused across the field decodes (C12).
///
/// Returns `None` if any field has no archived file overlapping the window
/// (the [`open_ohlc`] contract — file-level absence; since every archive file
/// carries all fields, one absent field means all are). A window that
/// overlaps a file but holds zero matching bars yields sources whose first
/// `peek` is `None`, not a `None` here.
pub fn open_columns(
server: &Arc<DataServer>,
symbol: &str,
from_ms: Option<i64>,
to_ms: Option<i64>,
fields: &[M1Field],
) -> Option<Vec<Box<dyn aura_engine::Source>>> {
fields
.iter()
.map(|f| {
M1FieldSource::open(server, symbol, from_ms, to_ms, *f)
.map(|s| Box::new(s) as Box<dyn aura_engine::Source>)
})
.collect()
}
/// [`open_columns`] over an epoch-ns [`Timestamp`] window — the engine-side
/// mirror, exactly as [`M1FieldSource::open_window`] mirrors
/// [`M1FieldSource::open`]; the ns→ms crossing happens once, here.
pub fn open_columns_window(
server: &Arc<DataServer>,
symbol: &str,
from: Option<Timestamp>,
to: Option<Timestamp>,
fields: &[M1Field],
) -> Option<Vec<Box<dyn aura_engine::Source>>> {
open_columns(
server,
symbol,
from.map(epoch_ns_to_unix_ms),
to.map(epoch_ns_to_unix_ms),
fields,
)
}
/// Open the four real OHLC [`M1FieldSource`]s for `symbol` over the closed
/// epoch-ns window `[from, to]`, in the FIXED order open, high, low, close — the
/// C4 merge tie-break order a resampler's `Barrier(0)` group depends on (#92).
@@ -330,11 +377,13 @@ pub fn open_ohlc(
from: Timestamp,
to: Timestamp,
) -> Option<Vec<Box<dyn aura_engine::Source>>> {
let open = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::Open)?;
let high = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::High)?;
let low = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::Low)?;
let close = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::Close)?;
Some(vec![Box::new(open), Box::new(high), Box::new(low), Box::new(close)])
open_columns_window(
server,
symbol,
Some(from),
Some(to),
&[M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close],
)
}
/// Construct the default data-server over the local archive at
@@ -398,6 +447,24 @@ mod tests {
}
}
#[test]
fn open_columns_propagates_file_level_absence_as_none() {
// The open_ohlc contract, generalized: file-level absence (unknown
// symbol / no overlapping archive file) is None for the whole set —
// hermetic (a bogus path has no archive whether or not local data
// exists), like instrument_geometry_none_for_unknown_symbol above.
let server = Arc::new(DataServer::new("/nonexistent-archive-path"));
assert!(open_columns(&server, "NOSYM", None, None, &[M1Field::Close]).is_none());
assert!(open_columns_window(
&server,
"NOSYM",
Some(Timestamp(0)),
Some(Timestamp(1)),
&[M1Field::Open, M1Field::Close]
)
.is_none());
}
/// A hand-built M1 bar with distinct per-field values so a transpose test
/// can tell the columns apart.
fn full_bar(time_ms: i64) -> M1Parsed {