From 066638ec25bc27365fcfc47f37c62b3871f96e6e Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 18 Jun 2026 13:03:03 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20cycle=200052=20close=20=E2=80=94=20aud?= =?UTF-8?q?it=20clean,=20retire=20spec=20+=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cycle 0052 (real-data source-open seam, Runway-1) closes drift-clean. Audit (architect drift review over aedaa5d..HEAD against docs/design + the CLAUDE.md domain invariants): - C3 (one ms<->ns crossing) strengthened — the inverse is seam-private and the only consumer-side divide left; every consumer ns_to_ms / unix_ms_to_epoch_ns wrap is gone. - C4 (O/H/L/C merge tie-break order) now in exactly one vetted home (open_ohlc); the hand-spelled four-field loops are gone. - C12 (one shared Arc/cache) preserved; C7 surface additive. - No drift, no debt, no ledger entry needs updating. Recommendation: carry on. Regression gate: no dedicated regression scripts configured (optional per the conventions); build/test/clippy --workspace is the gate and ran green — the gated open_ohlc_seam behaviour-preservation test executed against the local archive (not skipped), proving byte-identity vs the ms-path. Per the artifact lifecycle (committed while the cycle is live, git rm at cycle close), the ephemeral spec and plan are retired here; their durable rationale lives in the implementation commit 8c9a1b4 and the git history. --- docs/plans/0052-real-data-source-open-seam.md | 773 ------------------ docs/specs/0052-real-data-source-open-seam.md | 249 ------ 2 files changed, 1022 deletions(-) delete mode 100644 docs/plans/0052-real-data-source-open-seam.md delete mode 100644 docs/specs/0052-real-data-source-open-seam.md diff --git a/docs/plans/0052-real-data-source-open-seam.md b/docs/plans/0052-real-data-source-open-seam.md deleted file mode 100644 index 3819adc..0000000 --- a/docs/plans/0052-real-data-source-open-seam.md +++ /dev/null @@ -1,773 +0,0 @@ -# Real-data source-open seam — Implementation Plan - -> **Parent spec:** `docs/specs/0052-real-data-source-open-seam.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run -> this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** Consolidate the `aura-ingest` source-open surface so a real OHLC bundle -builds from `aura-ingest` alone in the engine's native epoch-ns `Timestamp` -currency, removing the per-consumer `ns_to_ms` hand-divide (#80), the duplicated -order-sensitive OHLC opener (#92), and the direct `data_server` reach (#81) — -behaviour-preserving (byte-identical `RunReport`s). - -**Architecture:** Four additive items in `crates/aura-ingest/src/lib.rs` (private -`epoch_ns_to_unix_ms`, `M1FieldSource::open_window`, free `open_ohlc`, -`default_data_server` + `pub use data_server::{DataServer, DEFAULT_DATA_PATH}`) -own the one ms↔ns crossing. Then the consumer layer (the shared -`breakout_real.rs` helpers + every GER40 example/test) migrates to the -Timestamp-native surface. The shared helper file is `#[path] mod`-included into -every consumer target, so deleting `open_ohlc_sources` / retyping -`utc_month_window_ms` + `report_from_trace` breaks all six compile-driven -consumers at once — they migrate in one atomic task. `ger40_breakout_compare.rs` -opens OHLC by its own hand-loop and calls neither retyped helper, so it survives -the build unmigrated (the compiler-invisible site) and is migrated by inspection -in its own task. - -**Tech Stack:** Rust, `aura-ingest` crate (lib + examples + integration tests), -`cargo build/test/clippy --workspace`. Gated tests skip-with-note when the local -Pepperstone archive is absent. - ---- - -## Files this plan creates or modifies - -- Modify: `crates/aura-ingest/src/lib.rs` — the four additive items + round-trip test (Task 1). -- Create: `crates/aura-ingest/tests/open_ohlc_seam.rs` — gated behaviour-preservation test (Task 2). -- Modify: `crates/aura-ingest/examples/shared/breakout_real.rs` — remove `open_ohlc_sources`, retype `utc_month_window_ms`→`utc_month_window` and `report_from_trace` (Task 3). -- Modify: `crates/aura-ingest/examples/ger40_breakout_real.rs` (Task 3). -- Modify: `crates/aura-ingest/examples/ger40_breakout_sweep.rs` (Task 3). -- Modify: `crates/aura-ingest/examples/ger40_breakout_walkforward.rs` — also delete local `ns_to_ms` (Task 3). -- Modify: `crates/aura-ingest/tests/ger40_breakout_world.rs` — also delete local `ns_to_ms`, two call sites (Task 3). -- Modify: `crates/aura-ingest/tests/ger40_breakout_real.rs` (Task 3). -- Modify: `crates/aura-ingest/tests/ger40_breakout_blueprint.rs` — two call sites (Task 3). -- Modify: `crates/aura-ingest/examples/ger40_breakout_compare.rs` — compiler-invisible, by inspection (Task 4). - -**Out of scope (do NOT touch):** the `WindowRoller` origin/end constructions -(`ger40_breakout_walkforward.rs:100-105`, `tests/ger40_breakout_world.rs:165-168`) -build the roll span from chrono via the public `unix_ms_to_epoch_ns` — the -sanctioned ms→ns *entry* of wall-clock into engine currency, not the forbidden -consumer-side ns→ms divide. They already produce `Timestamp` and are not -perturbed by the retype. The gated single-field ms-path tests (`real_bars.rs`, -`unbounded_window.rs`, `ger40_archive_utc_dst.rs`, `streaming_seam.rs`) keep their -`data_server` import (out of scope per spec AC1). - ---- - -## Task 1: `aura-ingest` library surface — the four additive items - -**Files:** -- Modify: `crates/aura-ingest/src/lib.rs` - -This task is purely additive: the existing ms-based `open` and -`unix_ms_to_epoch_ns` are untouched, so nothing else in the workspace breaks. - -- [ ] **Step 1: Reconcile the `data_server` import to add the re-export** - -In `crates/aura-ingest/src/lib.rs`, replace the import line 25: - -```rust -use data_server::{DataServer, SymbolChunkIter}; -``` - -with (keeps `SymbolChunkIter` for the struct field; `DataServer` now arrives via a -public re-export so internal code still sees it): - -```rust -use data_server::SymbolChunkIter; - -/// Re-export of the data-server archive entry points (#81): a real-data source -/// builds from `aura-ingest` alone — a consumer never names the external -/// `data_server` crate directly. -pub use data_server::{DataServer, DEFAULT_DATA_PATH}; -``` - -- [ ] **Step 2: Add the seam-private inverse `epoch_ns_to_unix_ms`** - -Immediately after `unix_ms_to_epoch_ns` (right after its closing brace at -`lib.rs:34`), add: - -```rust -/// Inverse of [`unix_ms_to_epoch_ns`]: project aura's canonical epoch-ns -/// [`Timestamp`] back to data-server's Unix-millisecond time. **Private** — the -/// seam owns the ms↔ns convention (C3); a consumer threads `Timestamp`s and never -/// converts. Floor division by 1e6 ns/ms (archived M1 data has no sub-ms instant). -fn epoch_ns_to_unix_ms(ts: Timestamp) -> i64 { - ts.0 / 1_000_000 -} -``` - -- [ ] **Step 3: Add `M1FieldSource::open_window`** - -In the `impl M1FieldSource` block, immediately after `open` (after its closing -brace at `lib.rs:217`, before the `advance` doc comment), add: - -```rust - /// Open over the `[from, to]` window in aura's native epoch-ns [`Timestamp`] - /// currency — the engine-side mirror of [`open`](Self::open), which takes - /// data-server's Unix-ms. Each bound is mapped through the seam-private - /// `epoch_ns_to_unix_ms` and delegated to `open`, so the ms↔ns crossing - /// happens once, here, and a consumer never divides. `None` bounds preserve - /// open-ended windows exactly as `open` does. - pub fn open_window( - server: &Arc, - symbol: &str, - from: Option, - to: Option, - field: M1Field, - ) -> Option { - Self::open( - server, - symbol, - from.map(epoch_ns_to_unix_ms), - to.map(epoch_ns_to_unix_ms), - field, - ) - } -``` - -- [ ] **Step 4: Add the free fns `open_ohlc` and `default_data_server`** - -Immediately after the `impl aura_engine::Source for M1FieldSource` block closes -(after `lib.rs:272`, before the `#[cfg(test)]` line at `lib.rs:274`), add: - -```rust -/// 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). -/// This is the single vetted home of that order; consumers never spell it out. -/// All four sources share the one `Arc` (one `FileCache`), so a -/// window's bars are parsed once and reused across the four field decodes, and -/// the same `Arc` flows across the disjoint sims of a sweep / walk-forward (C12). -/// -/// Returns `None` if any field has no archived file overlapping the window (the -/// caller skips cleanly), propagating each [`open_window`](M1FieldSource::open_window)'s -/// file-level `Option`. A window that overlaps a file but holds zero bars yields -/// four sources whose first `peek` is `None`, not a `None` here. -pub fn open_ohlc( - server: &Arc, - symbol: &str, - from: Timestamp, - to: Timestamp, -) -> Option>> { - 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)]) -} - -/// Construct the default data-server over the local archive at -/// [`DEFAULT_DATA_PATH`], wrapped in the `Arc` the streaming sources share (#81). -/// Build it once and clone the `Arc` across a family's sims — one cache, never -/// one server per field (C12). -pub fn default_data_server() -> Arc { - Arc::new(DataServer::new(DEFAULT_DATA_PATH)) -} -``` - -- [ ] **Step 5: Add the hermetic round-trip test** - -In `mod tests`, immediately after `unix_ms_to_epoch_ns_scales_ms_to_ns` (after its -closing brace at `lib.rs:287`), add: - -```rust - #[test] - fn epoch_ns_to_unix_ms_inverts_unix_ms_to_epoch_ns() { - // The seam-owned inverse round-trips the forward normalization for every - // ms instant, so a Timestamp window bound fed back to data-server's ms - // contract recovers the exact ms (C3: one currency crossing, owned here). - for ms in [0_i64, 1, 1_488_326_400_000, 1_727_000_000_000] { - assert_eq!(epoch_ns_to_unix_ms(unix_ms_to_epoch_ns(ms)), ms); - } - } -``` - -- [ ] **Step 6: Build and run the lib tests** - -Run: `cargo build -p aura-ingest` -Expected: clean build, 0 errors. - -Run: `cargo test -p aura-ingest --lib` -Expected: `test result: ok.` — includes -`epoch_ns_to_unix_ms_inverts_unix_ms_to_epoch_ns ... ok` and -`unix_ms_to_epoch_ns_scales_ms_to_ns ... ok`. - ---- - -## Task 2: gated behaviour-preservation test (AC5) - -**Files:** -- Create: `crates/aura-ingest/tests/open_ohlc_seam.rs` - -Proves the consolidated Timestamp opener reproduces the untouched ms-path `open` -bit-for-bit. Depends only on Task 1; written against the unchanged `build_harness` -/ `drain_trace` shared helpers and the ms-path `open`, so it compiles and passes -both before and after the Task 3 consumer migration. - -- [ ] **Step 1: Write the test file** - -Create `crates/aura-ingest/tests/open_ohlc_seam.rs` with exactly: - -```rust -//! Gated behaviour-preservation proof for the consolidated OHLC opener (#80/#92): -//! the library `open_ohlc` over an epoch-ns `Timestamp` window produces a recorded -//! `(held, equity)` series BIT-IDENTICAL to opening the four `M1FieldSource`s by -//! hand over the equivalent Unix-ms window via the untouched ms-path `open`. Same -//! instants, same bars, same numbers — the migration changes the surface, not the -//! behaviour. -//! -//! Mirrors the other gated GER40 tests: skips with a note where the local -//! Pepperstone archive is absent, so `cargo test --workspace` stays green -//! anywhere; exercises the real path where the files exist. - -use std::sync::Arc; - -use aura_engine::{Harness, Source}; -use chrono::TimeZone; -use data_server::{DataServer, DEFAULT_DATA_PATH}; - -use aura_ingest::{open_ohlc, M1Field, M1FieldSource}; - -#[path = "../examples/shared/breakout_real.rs"] -mod breakout_real; -use breakout_real::*; - -/// Run a fresh harness over `sources` and drain the recorded `(held, equity)` -/// series (the same fold the determinism tests use). -fn series_from(mut h: Harness, taps: &Taps, sources: Vec>) -> Vec<(f64, f64)> { - h.run(sources); - drop(h); - let trace = drain_trace(taps); - trace.iter().map(|b| (b.held, b.equity)).collect() -} - -/// Property: the Timestamp `open_ohlc` path and the hand-rolled ms-path `open` -/// (×4, in O/H/L/C order) over the equivalent window yield bit-identical recorded -/// series — behaviour-preserving consolidation (AC5). -#[test] -fn open_ohlc_timestamp_path_matches_ms_path_open() { - let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH)); - if !server.has_symbol(SYMBOL) { - eprintln!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)"); - return; - } - - // September 2024 (inclusive UTC), the window the other gated tests pin. - let from_ms = chrono::Utc.with_ymd_and_hms(2024, 9, 1, 0, 0, 0).unwrap().timestamp_millis(); - let to_ms = chrono::Utc.with_ymd_and_hms(2024, 9, 30, 23, 59, 59).unwrap().timestamp_millis(); - let from_ts = aura_ingest::unix_ms_to_epoch_ns(from_ms); - let to_ts = aura_ingest::unix_ms_to_epoch_ns(to_ms); - - // Path A: the consolidated Timestamp opener. - let (h_a, taps_a) = build_harness(); - let sources_a = open_ohlc(&server, SYMBOL, from_ts, to_ts).expect("window overlaps GER40 data"); - let series_a = series_from(h_a, &taps_a, sources_a); - assert!(!series_a.is_empty(), "window resolved to zero bars"); - - // Path B: the untouched ms-path open, four fields by hand in O/H/L/C order. - let fields = [M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close]; - let mut sources_b: Vec> = Vec::with_capacity(4); - for &f in &fields { - let s = M1FieldSource::open(&server, SYMBOL, Some(from_ms), Some(to_ms), f) - .expect("window overlaps GER40 data"); - sources_b.push(Box::new(s)); - } - let (h_b, taps_b) = build_harness(); - let series_b = series_from(h_b, &taps_b, sources_b); - - assert_eq!( - series_a, series_b, - "open_ohlc (Timestamp) must reproduce the ms-path open bit-identically (behaviour-preserving, #80/#92)", - ); -} -``` - -- [ ] **Step 2: Build and run the new test target** - -Run: `cargo test -p aura-ingest --test open_ohlc_seam` -Expected: `test result: ok. 1 passed` (the test runs where the archive exists; it -prints a `skip:` note and still passes where the archive is absent). - ---- - -## Task 3: atomic consumer migration (shared retype + the six compile-driven consumers) - -**Files:** -- Modify: `crates/aura-ingest/examples/shared/breakout_real.rs` -- Modify: `crates/aura-ingest/examples/ger40_breakout_real.rs` -- Modify: `crates/aura-ingest/examples/ger40_breakout_sweep.rs` -- Modify: `crates/aura-ingest/examples/ger40_breakout_walkforward.rs` -- Modify: `crates/aura-ingest/tests/ger40_breakout_world.rs` -- Modify: `crates/aura-ingest/tests/ger40_breakout_real.rs` -- Modify: `crates/aura-ingest/tests/ger40_breakout_blueprint.rs` - -**Atomicity note for the executor:** `breakout_real.rs` is `#[path] mod`-included -into all six consumer targets. Steps 1-4 (the shared retype) make every consumer -fail to compile until Steps 5-10 thread them. Do NOT build between edit steps — -the build gate is Steps 11-13, after all seven files are edited. `compare.rs` is -deliberately NOT touched here (it calls neither retyped helper and still -compiles); it is Task 4. - -**The single migration shape applied at every call site:** -- `open_ohlc_sources(server, from_ms, to_ms)` → `open_ohlc(server, SYMBOL, from, to)` (now `Timestamp` bounds). -- `utc_month_window_ms(y, m)` → `utc_month_window(y, m)` (returns `(Timestamp, Timestamp)`). -- a local helper's `from_ms: i64, to_ms: i64` params → `from: Timestamp, to: Timestamp`; its `RunManifest.window` `(aura_ingest::unix_ms_to_epoch_ns(from_ms), …(to_ms))` → `(from, to)`. -- `Arc::new(DataServer::new(DEFAULT_DATA_PATH))` → `default_data_server()`. -- `use data_server::{DataServer, DEFAULT_DATA_PATH};` → an `aura_ingest` re-export import; `ns_to_ms(w.is.0)` etc. → `w.is.0` straight through. - -- [ ] **Step 1: shared `breakout_real.rs` — prune now-unused imports** - -Delete line 40 (`use std::sync::Arc;` — only `open_ohlc_sources` used it) and -line 50 (`use data_server::DataServer;` — same). In the `aura_engine` import -(lines 43-46) remove `Source` (only `open_ohlc_sources`'s return type used it). -The import becomes: - -```rust -use aura_engine::{ - summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness, RunManifest, RunReport, - SourceSpec, Target, -}; -``` - -- [ ] **Step 2: shared `breakout_real.rs` — remove `open_ohlc_sources`** - -Delete the whole helper including its doc comment — lines 309-328 (from -`/// Open the four real OHLC …` through the closing `}` of `open_ohlc_sources`). - -- [ ] **Step 3: shared `breakout_real.rs` — retype `utc_month_window_ms` → `utc_month_window`** - -Replace the helper at lines 330-346 with: - -```rust -/// Build the inclusive window for the whole calendar month `(year, month)` in -/// UTC: `[first instant of the 1st, last ms of the last day]`, in aura's native -/// epoch-ns [`Timestamp`] currency. UTC keeps the boundary trivial to reason -/// about; the Session node still indexes bars in Berlin wall-clock inside the -/// graph. The SAME UTC instants as before, typed as `Timestamp` (each bound -/// wrapped through the one `unix_ms_to_epoch_ns` seam), so the consumer layer is -/// Timestamp-native and no caller round-trips ms→Timestamp→ms. -pub fn utc_month_window(year: i32, month: u32) -> (Timestamp, Timestamp) { - let from = chrono::Utc - .with_ymd_and_hms(year, month, 1, 0, 0, 0) - .unwrap() - .timestamp_millis(); - // first instant of the next month, minus one ms => last ms of this month. - let (ny, nm) = if month == 12 { (year + 1, 1) } else { (year, month + 1) }; - let next = chrono::Utc - .with_ymd_and_hms(ny, nm, 1, 0, 0, 0) - .unwrap() - .timestamp_millis(); - ( - aura_ingest::unix_ms_to_epoch_ns(from), - aura_ingest::unix_ms_to_epoch_ns(next - 1), - ) -} -``` - -- [ ] **Step 4: shared `breakout_real.rs` — retype `report_from_trace`** - -In the helper at lines 407-431: change the doc line 406 from -`/// the requested `[from_ms, to_ms]` normalized to epoch-ns.` to -`/// the requested `[from, to]` epoch-ns window.`; change the signature and the -`window:` field. The signature line 407 becomes: - -```rust -pub fn report_from_trace(trace: &[BarTrace], from: Timestamp, to: Timestamp) -> RunReport { -``` - -and the `window:` field (lines 422-425) becomes: - -```rust - window: (from, to), -``` - -- [ ] **Step 5: `examples/ger40_breakout_real.rs`** - -Delete line 23 (`use std::sync::Arc;`) and replace line 27 -(`use data_server::{DataServer, DEFAULT_DATA_PATH};`) with: - -```rust -use aura_ingest::{default_data_server, open_ohlc, DEFAULT_DATA_PATH}; -``` - -Line 40: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -Line 49: `let (from_ms, to_ms) = utc_month_window_ms(YEAR, MONTH);` → -`let (from, to) = utc_month_window(YEAR, MONTH);` - -Line 51: `let Some(sources) = open_ohlc_sources(&server, from_ms, to_ms) else {` → -`let Some(sources) = open_ohlc(&server, SYMBOL, from, to) else {` - -Line 75: `let report = report_from_trace(&trace, from_ms, to_ms);` → -`let report = report_from_trace(&trace, from, to);` - -- [ ] **Step 6: `examples/ger40_breakout_sweep.rs`** - -Replace line 21 (`use data_server::{DataServer, DEFAULT_DATA_PATH};`) with (keep -`use std::sync::Arc;` at line 16 — `Arc::clone` is still used at line 110): - -```rust -use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH}; -``` - -Retype `run_point` (line 36) — params `from_ms: i64, to_ms: i64` → -`from: Timestamp, to: Timestamp`: - -```rust -fn run_point(server: &Arc, point: &[Cell], from: Timestamp, to: Timestamp) -> RunReport { -``` - -Line 42: `open_ohlc_sources(server, from_ms, to_ms)` → -`open_ohlc(server, SYMBOL, from, to)` - -Lines 59-62 (the `window:` field) → `window: (from, to),` - -Line 71: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -Lines 78-79: -`let (from_ms, _) = utc_month_window_ms(2024, 1);` → -`let (from, _) = utc_month_window(2024, 1);` and -`let (_, to_ms) = utc_month_window_ms(2024, 12);` → -`let (_, to) = utc_month_window(2024, 12);` - -Line 112: `run_point(&server_for_closure, pt, from_ms, to_ms)` → -`run_point(&server_for_closure, pt, from, to)` - -- [ ] **Step 7: `examples/ger40_breakout_walkforward.rs`** - -Replace line 27 (`use data_server::{DataServer, DEFAULT_DATA_PATH};`) with (keep -`use std::sync::Arc;` — `Arc::clone` at line 128 still used): - -```rust -use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH}; -``` - -Delete the local `ns_to_ms` helper and its doc comment — lines 38-43 (from -`/// ns → ms: …` through the closing `}`). - -Retype `run_point` (line 48) — `from_ms: i64, to_ms: i64` → -`from: Timestamp, to: Timestamp`: - -```rust -fn run_point( - server: &Arc, - point: &[Cell], - from: Timestamp, - to: Timestamp, -) -> (RunReport, Vec<(Timestamp, f64)>) { -``` - -Line 59: `open_ohlc_sources(server, from_ms, to_ms)` → -`open_ohlc(server, SYMBOL, from, to)` - -Lines 76-79 (the `window:` field) → `window: (from, to),` - -Line 89: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -**Leave lines 100-105 unchanged** (the `WindowRoller` origin/end via -`aura_ingest::unix_ms_to_epoch_ns` — out of scope, the sanctioned wall-clock entry). - -Line 146: delete `let (is_from, is_to) = (ns_to_ms(w.is.0), ns_to_ms(w.is.1));` - -Line 148: `run_point(&server_for_closure, pt, is_from, is_to).0` → -`run_point(&server_for_closure, pt, w.is.0, w.is.1).0` - -Line 164: delete `let (oos_from, oos_to) = (ns_to_ms(w.oos.0), ns_to_ms(w.oos.1));` - -Lines 165-166: `run_point(&server_for_closure, &chosen, oos_from, oos_to)` → -`run_point(&server_for_closure, &chosen, w.oos.0, w.oos.1)` - -- [ ] **Step 8: `tests/ger40_breakout_world.rs`** - -Replace line 30 (`use data_server::{DataServer, DEFAULT_DATA_PATH};`) with (keep -`use std::sync::Arc;` — `Arc::clone` at lines 118/186 still used): - -```rust -use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH}; -``` - -Delete the local `ns_to_ms` helper and its doc comment — lines 36-41. - -Retype `run_point` (line 49) — `from_ms: i64, to_ms: i64` → -`from: Timestamp, to: Timestamp`: - -```rust -fn run_point(server: &Arc, point: &[Cell], from: Timestamp, to: Timestamp) -> RunReport { -``` - -Line 54: `open_ohlc_sources(server, from_ms, to_ms)` → -`open_ohlc(server, SYMBOL, from, to)` - -Lines 70-71 (the `window:` field) → `window: (from, to),` - -Line 94: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -Line 100: `let (from_ms, to_ms) = utc_month_window_ms(2024, 9);` → -`let (from, to) = utc_month_window(2024, 9);` - -Line 120: `run_point(&server_for_closure, pt, from_ms, to_ms)` → -`run_point(&server_for_closure, pt, from, to)` - -Line 155: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -**Leave lines 165-168 unchanged** (the `WindowRoller` origin/end — out of scope). - -Line 204: delete `let (is_from, is_to) = (ns_to_ms(w.is.0), ns_to_ms(w.is.1));` - -Line 206: `run_point(&server_for_closure, pt, is_from, is_to)` → -`run_point(&server_for_closure, pt, w.is.0, w.is.1)` - -Line 222: delete `let (oos_from, oos_to) = (ns_to_ms(w.oos.0), ns_to_ms(w.oos.1));` - -Line 223: `let oos_report = run_point(&server_for_closure, &chosen, oos_from, oos_to);` → -`let oos_report = run_point(&server_for_closure, &chosen, w.oos.0, w.oos.1);` - -Line 228: `let sources = open_ohlc_sources(&server_for_closure, oos_from, oos_to)` → -`let sources = open_ohlc(&server_for_closure, SYMBOL, w.oos.0, w.oos.1)` - -- [ ] **Step 9: `tests/ger40_breakout_real.rs`** - -Replace line 15 (`use data_server::{DataServer, DEFAULT_DATA_PATH};`) with (keep -`use std::sync::Arc;` — `Arc` is still named in `run_real`'s -signature; add `Timestamp`, which this file does not yet import): - -```rust -use aura_core::Timestamp; -use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH}; -``` - -Retype `run_real` (line 28) — `from_ms: i64, to_ms: i64` → -`from: Timestamp, to: Timestamp`: - -```rust -fn run_real( - server: &Arc, - from: Timestamp, - to: Timestamp, -) -> (aura_engine::RunReport, Vec<(f64, f64)>) { -``` - -Line 34: `open_ohlc_sources(server, from_ms, to_ms)` → -`open_ohlc(server, SYMBOL, from, to)` - -Line 42: `let report = report_from_trace(&trace, from_ms, to_ms);` → -`let report = report_from_trace(&trace, from, to);` - -Line 59: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -Line 65: `let (from_ms, to_ms) = utc_month_window_ms(YEAR, MONTH);` → -`let (from, to) = utc_month_window(YEAR, MONTH);` - -Line 67: `let (r1, s1) = run_real(&server, from_ms, to_ms);` → -`let (r1, s1) = run_real(&server, from, to);` - -Line 85: `let (r2, s2) = run_real(&server, from_ms, to_ms);` → -`let (r2, s2) = run_real(&server, from, to);` - -- [ ] **Step 10: `tests/ger40_breakout_blueprint.rs`** - -Replace line 21 (`use data_server::{DataServer, DEFAULT_DATA_PATH};`) with (keep -`use std::sync::Arc;` — `Arc` named in the run helpers; extend the -`aura_core` import at line 19 to add `Timestamp`): - -```rust -use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH}; -``` - -and change line 19 `use aura_core::Scalar;` to `use aura_core::{Scalar, Timestamp};`. - -Retype `run_blueprint` (line 75) and `run_flatgraph` (line 91) — both -`from_ms: i64, to_ms: i64` → `from: Timestamp, to: Timestamp`: - -```rust -fn run_blueprint(server: &Arc, from: Timestamp, to: Timestamp) -> Vec<(f64, f64)> { -``` -```rust -fn run_flatgraph(server: &Arc, from: Timestamp, to: Timestamp) -> Vec<(f64, f64)> { -``` - -Line 83: `open_ohlc_sources(server, from_ms, to_ms)` → -`open_ohlc(server, SYMBOL, from, to)` - -Line 94: `open_ohlc_sources(server, from_ms, to_ms)` → -`open_ohlc(server, SYMBOL, from, to)` - -Line 111: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -Line 117: `let (from_ms, to_ms) = utc_month_window_ms(YEAR, MONTH);` → -`let (from, to) = utc_month_window(YEAR, MONTH);` - -Lines 119, 123, 130: `run_blueprint(&server, from_ms, to_ms)` / -`run_flatgraph(&server, from_ms, to_ms)` → `(&server, from, to)` in all three. - -- [ ] **Step 11: workspace build gate** - -Run: `cargo build --workspace --all-targets` -Expected: clean build, 0 errors (every compile-driven consumer threaded; -`compare.rs` still compiles with its old hand-loop — it is migrated in Task 4). - -- [ ] **Step 12: workspace test gate** - -Run: `cargo test --workspace` -Expected: `test result: ok` across all crates, 0 failed. The gated GER40 tests -(`ger40_breakout_real`, `ger40_breakout_world`, `ger40_breakout_blueprint`, -`open_ohlc_seam`) run where the archive exists and print a `skip:` note otherwise — -either way no failures, and the C1/C23 determinism assertions prove the migrated -path is byte-identical. - -- [ ] **Step 13: clippy gate** - -Run: `cargo clippy --workspace --all-targets -- -D warnings` -Expected: 0 warnings (no unused-import warnings — the pruned `Arc` / `data_server` -/ `Source` imports of Step 1 and Steps 5-10 are accounted for). - ---- - -## Task 4: `compare.rs` by-inspection migration + acceptance verification - -**Files:** -- Modify: `crates/aura-ingest/examples/ger40_breakout_compare.rs` - -The compiler-invisible site: it opens OHLC by its own hand-loop and builds its -window inline via chrono, so nothing in Task 3 flags it. Migrated here by -inspection, then the acceptance greps verify the whole migration. - -- [ ] **Step 1: imports** - -Delete line 23 only if `Arc` becomes unused — it does NOT (`run_cell`'s signature -names `&Arc`), so **keep** `use std::sync::Arc;`. Delete line 27 -(`use chrono::TimeZone;` — its only use is the inline window removed below). -Replace line 29 (`use data_server::{DataServer, DEFAULT_DATA_PATH};`) and line 31 -(`use aura_ingest::{M1Field, M1FieldSource};`) with a single import: - -```rust -use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH}; -``` - -In the `aura_engine` import (line 26) remove `Source` (only the hand-loop used it): - -```rust -use aura_engine::{summarize, RunMetrics}; -``` - -- [ ] **Step 2: `run_cell` signature + replace the hand-loop with `open_ohlc`** - -Retype `run_cell` (line 55) — `from_ms: i64, to_ms: i64` → -`from: Timestamp, to: Timestamp`: - -```rust -fn run_cell( - server: &Arc, - symbol: &str, - bar_period: i64, - from: Timestamp, - to: Timestamp, -) -> Option<(RunMetrics, u32)> { -``` - -Update the doc line 51 from `/// `[from_ms, to_ms]` of real `symbol` OHLC` to -`/// `[from, to]` (epoch-ns) of real `symbol` OHLC`. - -Replace the hand-spelled loop (lines 74-80): - -```rust - let fields = [M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close]; - let mut srcs: Vec> = Vec::with_capacity(4); - for &f in &fields { - let s = M1FieldSource::open(server, symbol, Some(from_ms), Some(to_ms), f)?; - srcs.push(Box::new(s)); - } - h.run(srcs); -``` - -with: - -```rust - let srcs = open_ohlc(server, symbol, from, to)?; - h.run(srcs); -``` - -- [ ] **Step 3: replace the inline chrono window with `utc_month_window`** - -Replace the inline window in `main` (lines 113-121): - -```rust - // Full year 2024 for body. - let from_ms = chrono::Utc - .with_ymd_and_hms(2024, 1, 1, 0, 0, 0) - .unwrap() - .timestamp_millis(); - let to_ms = chrono::Utc - .with_ymd_and_hms(2024, 12, 31, 23, 59, 59) - .unwrap() - .timestamp_millis(); -``` - -with (the same full-year UTC span via the shared Timestamp helper — the -end-of-year boundary widens by <1s and captures no extra minute bar): - -```rust - // Full year 2024 (the same UTC span, via the shared Timestamp helper). - let (from, _) = utc_month_window(2024, 1); - let (_, to) = utc_month_window(2024, 12); -``` - -- [ ] **Step 4: `default_data_server` + threaded call sites** - -Line 107: `let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));` → -`let server = default_data_server();` - -Line 138: `run_cell(&server, symbol, BAR_MINUTES, from_ms, to_ms)` → -`run_cell(&server, symbol, BAR_MINUTES, from, to)` - -Line 159: `run_cell(&server, SYMBOL, period, from_ms, to_ms)` → -`run_cell(&server, SYMBOL, period, from, to)` - -- [ ] **Step 5: build + clippy gate** - -Run: `cargo build --workspace --all-targets` -Expected: clean build, 0 errors. - -Run: `cargo clippy --workspace --all-targets -- -D warnings` -Expected: 0 warnings. - -- [ ] **Step 6: acceptance grep — `open_ohlc_sources` fully gone (AC6)** - -Run: `grep -rn 'open_ohlc_sources' crates/aura-ingest` -Expected: no output (exit status 1) — the helper, every call, and every prose -mention are removed. - -- [ ] **Step 7: acceptance grep — no consumer-side `ns_to_ms` (AC2)** - -Run: `grep -rn 'ns_to_ms' crates/aura-ingest/examples crates/aura-ingest/tests` -Expected: no output (exit status 1) — both local `ns_to_ms` divides are deleted. -(The seam-internal `epoch_ns_to_unix_ms` lives in `src/lib.rs`, outside this scope, -and does not contain the substring `ns_to_ms`.) - -- [ ] **Step 8: acceptance grep — no `data_server` import in any migrated consumer (AC1)** - -Run: `grep -ln 'use data_server' crates/aura-ingest/examples/ger40_breakout_*.rs crates/aura-ingest/tests/ger40_breakout_*.rs` -Expected: no output (exit status 1) — every GER40 OHLC consumer builds from -`aura-ingest` alone. (The out-of-scope single-field ms-path tests `real_bars.rs`, -`unbounded_window.rs`, `ger40_archive_utc_dst.rs`, `streaming_seam.rs` keep theirs; -they are not matched by this glob.) - -- [ ] **Step 9: acceptance grep — `epoch_ns_to_unix_ms` is private (AC4)** - -Run: `grep -n 'fn epoch_ns_to_unix_ms' crates/aura-ingest/src/lib.rs` -Expected: exactly one line, `fn epoch_ns_to_unix_ms(ts: Timestamp) -> i64 {` — -**not** prefixed with `pub`. (A `grep -n 'pub fn epoch_ns_to_unix_ms'` returns -nothing.) - -- [ ] **Step 10: final full-suite gate** - -Run: `cargo test --workspace` -Expected: `test result: ok` across all crates, 0 failed. diff --git a/docs/specs/0052-real-data-source-open-seam.md b/docs/specs/0052-real-data-source-open-seam.md deleted file mode 100644 index 1b3ee4c..0000000 --- a/docs/specs/0052-real-data-source-open-seam.md +++ /dev/null @@ -1,249 +0,0 @@ -# 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 `RunReport`s. - -## 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). - -1. **Seam-owned inverse (private).** `epoch_ns_to_unix_ms(Timestamp) -> i64`, - the inverse of the existing `unix_ms_to_epoch_ns`. **Not** `pub`: the seam - owns the ms↔ns convention (C3); a consumer never converts. (This is the #80 - decision — see the reconciliation comment on #80.) - -2. **Timestamp-window opener.** `M1FieldSource::open_window(server, symbol, - from: Option, to: Option, field) -> Option`, - mirroring the existing `open` but in the engine's native `Timestamp` - (epoch-ns) currency; it maps each bound through `epoch_ns_to_unix_ms` and - delegates to `open`. `Option` preserves open-ended windows, as `open` does. - -3. **Canonical OHLC opener.** `open_ohlc(server, symbol, from: Timestamp, to: - Timestamp) -> Option>>` builds the four `M1FieldSource`s - for `Open, High, Low, Close` **in that fixed order** (the C4 merge tie-break - the resampler's `Barrier(0)` group depends on, #92), each via `open_window`, - all sharing the one `Arc`. Closed `Timestamp` window (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. - -4. **Default-archive convenience + re-exports.** `aura-ingest` re-exports - `DataServer` and `DEFAULT_DATA_PATH` from `data_server`, and adds - `default_data_server() -> Arc` = `Arc::new(DataServer::new( - DEFAULT_DATA_PATH))`. One shared `Arc` (one cache) flows into - `open_ohlc` across the four fields and across disjoint sims — the C12 - sharing the streaming model rests on. (This is the #81 decision; the - issue-sketched per-field `open_default_archive` was rejected because it would - build one cache per field — see the reconciliation comment on #81.) - -**Return type — `Vec` not `[_; 4]`.** `Harness::run` consumes -`Vec>` (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: - -```rust -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: - -```rust -use aura_ingest::{default_data_server, open_ohlc}; // builds from aura-ingest alone - -let server = default_data_server(); // one shared Arc (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) - -```rust -// 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, symbol: &str, - from: Option, to: Option, field: M1Field, -) -> Option { - 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, symbol: &str, from: Timestamp, to: Timestamp, -) -> Option>>; // O,H,L,C order, shares `server` - -pub fn default_data_server() -> Arc; // 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 ..), 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, hardcoded `SYMBOL`); the - library `open_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) to `utc_month_window(year, month) -> - (Timestamp, Timestamp)` — the *same* UTC instants, typed as the - engine-native `Timestamp` (it wraps each bound through the existing - `unix_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 take `from/to: Timestamp` and drop the - `unix_ms_to_epoch_ns` wrap — same shape as the other shared helpers. (The - `utc_month_window` retype turns this into a compile error, so the type - checker reaches it; named here for completeness.) -- `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), imports `data_server` directly (:29), and - builds its window inline via chrono (:114-121); it calls **neither** - `open_ohlc_sources` **nor** `utc_month_window_ms`, so deleting/retyping those - does **not** flag it. It must be migrated by inspection: replace the loop with - `open_ohlc`, the inline chrono window with `utc_month_window`, and the - `data_server` import with the `aura-ingest` re-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 the - `WindowBounds` `Timestamp` bounds (`w.is.0`, `w.is.1`, `w.oos.0`, `w.oos.1`) - straight to `open_ohlc`. -- **Calendar-window consumers**: their bounds come from `utc_month_window` (now - `Timestamp`); thread that `Timestamp` window through any local helper - (`run_point` / `run_blueprint` / `run_flatgraph`, whose `from_ms/to_ms: i64` - params become `from/to: Timestamp`) into `open_ohlc`. Internal `RunManifest` - construction that previously wrapped `unix_ms_to_epoch_ns(from_ms)` now - receives the `Timestamp` directly (drop the wrap). -- **Direct `data_server::` importers**: switch `use data_server::{DataServer, - DEFAULT_DATA_PATH}` to the `aura-ingest` re-exports, and `Arc::new( - DataServer::new(DEFAULT_DATA_PATH))` to `default_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) with `open_ohlc`, its inline - chrono window (:114-121) with `utc_month_window`, and its direct `data_server` - import (:29) with the `aura-ingest` re-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>` → `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)) == - x` round-trip, beside the existing `unix_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 **same** `RunReport` as 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 - -1. A single-field source (`M1FieldSource::open_window`) and the OHLC bundle - (`open_ohlc`) both build using only `aura-ingest`'s public exports. In the - migrated OHLC / walk-forward consumer layer (the examples + tests this spec - names, including `ger40_breakout_compare.rs`) **no `data_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`) and - `aura-cli`'s single-field open legitimately keep their `data_server` import — - they are not part of this OHLC-opener migration. -2. A real-data walk-forward feeds `WindowRoller` `Timestamp` bounds straight - into `open_ohlc` with zero consumer-side `ns_to_ms` / `/ 1_000_000`. -3. The four OHLC sources are produced in `Open, High, Low, Close` order by the - single library helper (the C4 merge tie-break lives in one vetted place). -4. `epoch_ns_to_unix_ms` is private (the seam owns the convention; not part of - the public surface). -5. Behaviour-preserving: the full suite is green and every run result is - byte-identical to before (same `RunReport`s); the existing ms-based `open` - and `unix_ms_to_epoch_ns` *signatures* are untouched (consumer files do - change — that is the migration, not a behaviour change). -6. The migration is complete: `open_ohlc_sources` and every consumer-side - `ns_to_ms` are gone, `utc_month_window` is `Timestamp`-typed, and every - former caller opens via `open_ohlc`. The type checker enumerates most of the - set (deleting `open_ohlc_sources` / retyping `utc_month_window` / - `report_from_trace` turns each into a compile error), **except** - `ger40_breakout_compare.rs`, which opens OHLC via its own hand-spelled - O/H/L/C loop and imports `data_server` directly — it compiles unchanged and - is migrated by inspection, not by a compile error. No ms-based shim remains - in the consumer layer.