Consolidate aura-ingest's OHLC source-open surface onto the engine-native epoch-ns Timestamp currency: a private seam-owned inverse, a Timestamp-window opener, a canonical OHLC bundle opener (fixed C4 O/H/L/C order), and a DataServer/DEFAULT_DATA_PATH re-export + default_data_server convenience — so a real-data source builds from aura-ingest alone, with the one ms<->ns crossing owned by the seam. Behaviour-preserving (byte-identical RunReports). Grounded against the tree via the ground-spec-0052 workflow (25/28 assertions confirmed; compare.rs compiler-invisible site + AC1 scope fix applied). refs #80, #81, #92
14 KiB
Real-data source-open seam — Design Spec
Date: 2026-06-18
Status: Grounded (workflow ground-spec-0052: 25/28 assertions confirmed; 1 blocker + 1 scope fix applied below); proceeding to planner per user instruction
Authors: orchestrator + Claude
Cycle: Runway-1 — milestone "Runway — real-data ergonomics & honesty hardening". Closes #80, #81, #92.
Note: Per-cycle specs are git-tracked but ephemeral — committed while their cycle is live and removed (git rm) at cycle close (the git-ignore of 28958f2 was reverted in aedaa5d). This file is a working artifact for the planner this cycle, never a durable API reference.
Goal
Consolidate the aura-ingest public surface so a real-data source — a single
base column and the OHLC bundle — is buildable from aura-ingest alone,
with the epoch-ns ↔ Unix-ms window-currency owned by the seam. This removes two
field-tested frictions: the per-consumer ns_to_ms hand-divide (a transposable
silent-wrong-window class, #80) and the duplicated, order-sensitive OHLC opener
(#92), plus the undocumented direct reach into the external data_server crate
(#81). Behaviour-preserving: existing runs reach byte-identical RunReports.
Architecture
Four additions to crates/aura-ingest/src/lib.rs, all additive; the existing
ms-based M1FieldSource::open and unix_ms_to_epoch_ns are untouched (the
behaviour-preserving guarantee for current call sites).
-
Seam-owned inverse (private).
epoch_ns_to_unix_ms(Timestamp) -> i64, the inverse of the existingunix_ms_to_epoch_ns. Notpub: the seam owns the ms↔ns convention (C3); a consumer never converts. (This is the #80 decision — see the reconciliation comment on #80.) -
Timestamp-window opener.
M1FieldSource::open_window(server, symbol, from: Option<Timestamp>, to: Option<Timestamp>, field) -> Option<Self>, mirroring the existingopenbut in the engine's nativeTimestamp(epoch-ns) currency; it maps each bound throughepoch_ns_to_unix_msand delegates toopen.Optionpreserves open-ended windows, asopendoes. -
Canonical OHLC opener.
open_ohlc(server, symbol, from: Timestamp, to: Timestamp) -> Option<Vec<Box<dyn Source>>>builds the fourM1FieldSources forOpen, High, Low, Closein that fixed order (the C4 merge tie-break the resampler'sBarrier(0)group depends on, #92), each viaopen_window, all sharing the oneArc<DataServer>. ClosedTimestampwindow (a backtest / walk-forward fold is always bounded). This is the single vetted home of the O/H/L/C order; the trap of hand-spelling it four times is gone. -
Default-archive convenience + re-exports.
aura-ingestre-exportsDataServerandDEFAULT_DATA_PATHfromdata_server, and addsdefault_data_server() -> Arc<DataServer>=Arc::new(DataServer::new( DEFAULT_DATA_PATH)). One sharedArc<DataServer>(one cache) flows intoopen_ohlcacross the four fields and across disjoint sims — the C12 sharing the streaming model rests on. (This is the #81 decision; the issue-sketched per-fieldopen_default_archivewas rejected because it would build one cache per field — see the reconciliation comment on #81.)
Return type — Vec not [_; 4]. Harness::run consumes
Vec<Box<dyn Source>> (harness.rs:371), so a Vec feeds straight in with no
conversion; the order-safety #92 asks for is delivered by the single vetted
helper regardless of container. (Issue #92 sketched [_; 4] as an "e.g."; this
is the non-load-bearing container choice.)
Concrete code shapes
User-facing worked example — the walk-forward fold closure (the #80/#81/#92 evidence)
Before (today; crates/aura-ingest/examples/ger40_breakout_walkforward.rs
and tests/ger40_breakout_world.rs), each consumer re-deriving the seam's job:
use data_server::{DataServer, DEFAULT_DATA_PATH}; // #81: direct reach into the external crate
// #80: local hand-divide, re-written per consumer; transposable from/to
fn ns_to_ms(ts: Timestamp) -> i64 { ts.0 / 1_000_000 }
// #92: local open_ohlc_sources, hardcoded to SYMBOL, O/H/L/C order spelled by hand
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
let result = walk_forward(roller, space, move |w: WindowBounds| -> WindowRun {
let (is_from, is_to) = (ns_to_ms(w.is.0), ns_to_ms(w.is.1)); // <- the divide
let sources = open_ohlc_sources(&server, is_from, is_to).expect("…");
/* bootstrap + run + summarize */
});
After — the seam owns the currency, the opener, and the server construction:
use aura_ingest::{default_data_server, open_ohlc}; // builds from aura-ingest alone
let server = default_data_server(); // one shared Arc<DataServer> (C12)
let result = walk_forward(roller, space, move |w: WindowBounds| -> WindowRun {
let sources = open_ohlc(&server, SYMBOL, w.is.0, w.is.1) // Timestamp in — no divide
.expect("window overlaps GER40 data");
/* bootstrap + run + summarize */
});
No ns_to_ms, no local OHLC helper, no data_server:: import.
Before → after API surface (secondary)
// lib.rs — ADD (private):
fn epoch_ns_to_unix_ms(ts: Timestamp) -> i64 { ts.0 / 1_000_000 }
// lib.rs — ADD on impl M1FieldSource:
pub fn open_window(
server: &Arc<DataServer>, symbol: &str,
from: Option<Timestamp>, to: Option<Timestamp>, field: M1Field,
) -> Option<Self> {
Self::open(server, symbol, from.map(epoch_ns_to_unix_ms), to.map(epoch_ns_to_unix_ms), field)
}
// lib.rs — ADD (free fns):
pub fn open_ohlc(
server: &Arc<DataServer>, symbol: &str, from: Timestamp, to: Timestamp,
) -> Option<Vec<Box<dyn Source>>>; // O,H,L,C order, shares `server`
pub fn default_data_server() -> Arc<DataServer>; // Arc::new(DataServer::new(DEFAULT_DATA_PATH))
// lib.rs — ADD (re-exports):
pub use data_server::{DataServer, DEFAULT_DATA_PATH};
// UNCHANGED (behaviour-preserving): pub fn open(.. Option<i64> ..), pub fn unix_ms_to_epoch_ns
Components
crates/aura-ingest/src/lib.rs— the four additions + re-exports above.crates/aura-ingest/examples/shared/breakout_real.rs:- remove the local
open_ohlc_sources(Unix-ms, hardcodedSYMBOL); the libraryopen_ohlc(server, symbol, from: Timestamp, to: Timestamp)replaces every use of it (symbol now a parameter). - retype the shared window-builder
utc_month_window_ms(year, month) -> (i64, i64)(breakout_real.rs:334) toutc_month_window(year, month) -> (Timestamp, Timestamp)— the same UTC instants, typed as the engine-nativeTimestamp(it wraps each bound through the existingunix_ms_to_epoch_ns). This is what makes the whole consumer layer Timestamp-native, so no caller round-trips ms→Timestamp→ms. - retype
report_from_trace(breakout_real.rs:407,from_ms/to_ms: i64, window wrap at :422-425) to takefrom/to: Timestampand drop theunix_ms_to_epoch_nswrap — same shape as the other shared helpers. (Theutc_month_windowretype turns this into a compile error, so the type checker reaches it; named here for completeness.)
- remove the local
crates/aura-ingest/examples/ger40_breakout_compare.rs— the one compiler-invisible migration site. It opens OHLC via its own hand-spelled O/H/L/C loop (compare.rs:74-79), importsdata_serverdirectly (:29), and builds its window inline via chrono (:114-121); it calls neitheropen_ohlc_sourcesnorutc_month_window_ms, so deleting/retyping those does not flag it. It must be migrated by inspection: replace the loop withopen_ohlc, the inline chrono window withutc_month_window, and thedata_serverimport with theaura-ingestre-exports — else it silently survives with exactly the #92 order-trap and #81 direct-import this spec kills.
Migration policy (all callers of the removed helper)
open_ohlc_sources has call sites well beyond the two walk-forward consumers —
8 call expressions across 6 files (today: examples/ger40_breakout_walkforward.rs,
examples/ger40_breakout_sweep.rs, examples/ger40_breakout_real.rs,
tests/ger40_breakout_world.rs (×2, :54/:228), tests/ger40_breakout_real.rs,
tests/ger40_breakout_blueprint.rs (×2, :83/:94)). The planner migrates every
expression, not one-per-file. Separately, ger40_breakout_compare.rs opens
OHLC without this helper (its own loop) and so is not in this census — it is
the compiler-invisible site handled below. All migrate to the same
Timestamp-native surface — there is no ms-based shim retained, and the only
epoch-ns↔Unix-ms conversion left anywhere in the consumer layer is the one inside
the seam:
- Walk-forward consumers: delete the local
ns_to_ms; pass theWindowBoundsTimestampbounds (w.is.0,w.is.1,w.oos.0,w.oos.1) straight toopen_ohlc. - Calendar-window consumers: their bounds come from
utc_month_window(nowTimestamp); thread thatTimestampwindow through any local helper (run_point/run_blueprint/run_flatgraph, whosefrom_ms/to_ms: i64params becomefrom/to: Timestamp) intoopen_ohlc. InternalRunManifestconstruction that previously wrappedunix_ms_to_epoch_ns(from_ms)now receives theTimestampdirectly (drop the wrap). - Direct
data_server::importers: switchuse data_server::{DataServer, DEFAULT_DATA_PATH}to theaura-ingestre-exports, andArc::new( DataServer::new(DEFAULT_DATA_PATH))todefault_data_server(). - The compiler-invisible site —
ger40_breakout_compare.rs: migrated by inspection, not by a compile error (it calls neither removed/retyped helper). Replace its hand-spelled O/H/L/C loop (:74-79) withopen_ohlc, its inline chrono window (:114-121) withutc_month_window, and its directdata_serverimport (:29) with theaura-ingestre-exports +default_data_server().
Deleting open_ohlc_sources and retyping utc_month_window / report_from_trace
turns every site except ger40_breakout_compare.rs into a compile error until
migrated, so the type checker enumerates that set; the single shape above is
applied to each, and compare.rs is migrated by inspection. Every run is
byte-identical (same instants, same windows) — behaviour-preserving.
Data flow
WindowRoller → WindowBounds { is, oos: (Timestamp, Timestamp) } → open_ohlc
(Timestamp) → epoch_ns_to_unix_ms (the one ms↔ns crossing, internal) →
DataServer::stream_m1_windowed (Unix-ms) → four M1FieldSource (O/H/L/C) →
Vec<Box<dyn Source>> → Harness::run. The currency boundary is crossed
exactly once, inside the seam.
Error handling
open_ohlc returns None if any of the four field opens returns None (no
archived file overlaps the window) — the same file-level ? short-circuit and
the same Some-with-empty vs None semantics the current open /
open_ohlc_sources already document (lib.rs:126-135). A window overlapping a
file but holding zero bars yields sources whose first peek is None, not a
None from open_ohlc.
Testing strategy
- Hermetic (no DataServer):
epoch_ns_to_unix_ms(unix_ms_to_epoch_ns(x)) == xround-trip, beside the existingunix_ms_to_epoch_ns_scales_ms_to_ns(lib.rs:279). This pins the seam-owned inverse without an archive. - Gated (real data, skip-with-note when the archive is absent, mirroring
real_bars.rs/streaming_seam.rs/ger40_*):open_ohlc(&server, SYMBOL, from_ts, to_ts)yields four sources and a Timestamp-window backtest reaches the sameRunReportas the existing ms-path open over the equivalent window (behaviour-preserving proof).- the migrated walk-forward example + world test run green with zero
ns_to_ms(the #80 evidence).
- Full workspace suite green, unchanged (
cargo test --workspace); clippy clean. No existing ms-based call site changes behaviour.
Acceptance criteria
- A single-field source (
M1FieldSource::open_window) and the OHLC bundle (open_ohlc) both build using onlyaura-ingest's public exports. In the migrated OHLC / walk-forward consumer layer (the examples + tests this spec names, includingger40_breakout_compare.rs) nodata_server::import remains. Out of scope: the gated single-field ms-path tests (real_bars.rs,unbounded_window.rs,ger40_archive_utc_dst.rs,streaming_seam.rs) andaura-cli's single-field open legitimately keep theirdata_serverimport — they are not part of this OHLC-opener migration. - A real-data walk-forward feeds
WindowRollerTimestampbounds straight intoopen_ohlcwith zero consumer-sidens_to_ms// 1_000_000. - The four OHLC sources are produced in
Open, High, Low, Closeorder by the single library helper (the C4 merge tie-break lives in one vetted place). epoch_ns_to_unix_msis private (the seam owns the convention; not part of the public surface).- Behaviour-preserving: the full suite is green and every run result is
byte-identical to before (same
RunReports); the existing ms-basedopenandunix_ms_to_epoch_nssignatures are untouched (consumer files do change — that is the migration, not a behaviour change). - The migration is complete:
open_ohlc_sourcesand every consumer-sidens_to_msare gone,utc_month_windowisTimestamp-typed, and every former caller opens viaopen_ohlc. The type checker enumerates most of the set (deletingopen_ohlc_sources/ retypingutc_month_window/report_from_traceturns each into a compile error), exceptger40_breakout_compare.rs, which opens OHLC via its own hand-spelled O/H/L/C loop and importsdata_serverdirectly — it compiles unchanged and is migrated by inspection, not by a compile error. No ms-based shim remains in the consumer layer.