From 13941a24d590a0a4fc9ab67e5a5b0c0fab688b35 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 18 Jun 2026 21:13:56 +0200 Subject: [PATCH] =?UTF-8?q?fieldtest:=20milestone=20runway=20=E2=80=94=20r?= =?UTF-8?q?eal-data=20seam,=204=20examples,=206=20findings=20(gate=20green?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Milestone-close fieldtest for "Runway — real-data ergonomics & honesty hardening". Four curated end-to-end scenarios derived top-down from the milestone promise, run as a downstream consumer against real GER40/EURUSD M1 bars (public interface only; no crates/*/src read): 1. per-instrument pip honesty (#22) — GER40 index pip 1.0 AND EURUSD forex pip 0.0001 both honest end-to-end; AAPL.US/FOOBAR un-vetted refused exit 2, no stdout leak 2. real GER40 M1 open via open_ohlc from aura-ingest ALONE (#80/#81/#92) — no data-server dep, 53173 real bars, C4 O/H/L/C order, ts-window inverse 3. multi-tap ts-join (#93) — two taps at different cadences, 27744 rows, 0 by-ts mismatches; warm-up None rows where a positional zip would corrupt 4. run-registry coherence (#73/#82) — list/rank retired exit 2, families/ family live, two-store Registry read-back + rustdoc Findings: 4 working, 1 friction, 1 spec_gap. - friction: Cell exposes only from_i64/from_f64/... while the side-by-side Scalar is a tagged enum with public I64(..)/F64(..) variants. The natural param-authoring entry point is Scalar (Cell is the hot-path buffer carrier), so the wrong reach is a self-correcting compile snag — ergonomic only. Filed as a follow-up idea. - spec_gap: unknown family id exits 0 empty — ratified into the C18 note in the preceding commit (treat-as-empty contract). Gate verdict: GREEN. All seven axes mapped to a covering scenario and exercised on real bars; both finding classifications and the one material coverage gap were adversarially re-verified. That gap — #22's FX-vetted pip face, untested by the original scenario set — was probed (EURUSD -> sim-optimal(pip_size=0.0001), FX-scaled, not index-mis-scaled) and folded into scenario 1, so the pip promise is demonstrated on both positive faces. Every scenario re-run independently on HEAD before commit. --- .../0055-fieldtest-runway-real-data-seam.md | 173 +++ fieldtests/milestone-runway/Cargo.lock | 1010 +++++++++++++++++ fieldtests/milestone-runway/Cargo.toml | 52 + .../milestone-runway/out_rw_1_pip_honesty.txt | 33 + .../out_rw_2_open_ohlc_real.txt | 20 + .../out_rw_3_multitap_ts_join.txt | 15 + .../out_rw_4_runs_coherence.txt | 35 + .../milestone-runway/rw_2_open_ohlc_real.rs | 250 ++++ .../milestone-runway/rw_3_multitap_ts_join.rs | 216 ++++ 9 files changed, 1804 insertions(+) create mode 100644 docs/specs/0055-fieldtest-runway-real-data-seam.md create mode 100644 fieldtests/milestone-runway/Cargo.lock create mode 100644 fieldtests/milestone-runway/Cargo.toml create mode 100644 fieldtests/milestone-runway/out_rw_1_pip_honesty.txt create mode 100644 fieldtests/milestone-runway/out_rw_2_open_ohlc_real.txt create mode 100644 fieldtests/milestone-runway/out_rw_3_multitap_ts_join.txt create mode 100644 fieldtests/milestone-runway/out_rw_4_runs_coherence.txt create mode 100644 fieldtests/milestone-runway/rw_2_open_ohlc_real.rs create mode 100644 fieldtests/milestone-runway/rw_3_multitap_ts_join.rs diff --git a/docs/specs/0055-fieldtest-runway-real-data-seam.md b/docs/specs/0055-fieldtest-runway-real-data-seam.md new file mode 100644 index 0000000..64e5bdd --- /dev/null +++ b/docs/specs/0055-fieldtest-runway-real-data-seam.md @@ -0,0 +1,173 @@ +# Fieldtest — milestone Runway (real-data ergonomics & honesty hardening) — 2026-06-18 + +**Status:** Draft — awaiting orchestrator triage +**Author:** fieldtester (dispatched by fieldtest skill, milestone-close gate) + +## Scope + +Milestone-close fieldtest for **Runway — real-data ergonomics & honesty +hardening** (commit range `aedaa5d..HEAD`, HEAD `0d185fb`). The milestone's +promise: *a downstream consumer can run an honest real-data backtest end to +end* — open the real GER40 M1 archive via the canonical opener from +`aura-ingest` alone, run a strategy against real bars, get a PnL scaled by the +**correct per-instrument pip** (an index must not be mis-scaled as forex), be +**refused with a clear diagnostic** when the instrument has no vetted pip spec, +join multi-tap sink traces honestly on timestamp, and find the run-registry +surface coherent (the dead-end `runs list`/`rank` retired; `runs families`/ +`family` live; the two-store Registry discoverable from the public docs). + +Scenarios were derived **top-down from the promise**, not as a union of the +per-cycle axes. Public interface only: design ledger (`docs/design/INDEX.md`), +`docs/glossary.md`, `docs/project-layout.md`, `cargo doc` rustdoc, `git log`, +`CLAUDE.md`. No `crates/*/src` (incl. the engine's own +`crates/aura-ingest/examples/`, which sits under the forbidden code root) was +read. Built and ran HEAD from the working tree throughout: workspace built with +`cargo build --workspace`; every fixture/CLI run via `cargo run` so HEAD source +is always what executes. The two Rust fixtures path-dep the engine crates as a +real C16 project would; the CLI scenarios invoke the `aura` binary directly. + +## Examples + +### `fieldtests/milestone-runway/out_rw_1_pip_honesty.txt` — per-instrument pip honesty (#22) +- CLI invocations of `aura run --real` on a vetted index and on un-vetted + instruments. +- Fits the scope: this is the headline honesty fix — "symbol data carries no + pip metadata, so a real run guessed". A consumer must get the right pip on a + vetted symbol and a refusal on an un-vetted one. +- Outcome: ran. `GER40` → exit 0, manifest broker `sim-optimal(pip_size=1)` + (correct index point, not the FX `0.0001`), `total_pips=977.38`. `AAPL.US` + (un-specced) and `FOOBAR` (un-specced + no archive) → **exit 2**, stderr + diagnostic, **no stdout leak**. The refusal fires at the pip lookup *before* + data access (FOOBAR, which has no archive, gets the pip-refusal diagnostic, + not a data-not-found one). Matched expected. + +### `fieldtests/milestone-runway/rw_2_open_ohlc_real.rs` — real GER40 M1 ingestion via the canonical opener (#80/#81/#92) +- A consumer opens the real GER40 archive with `aura_ingest::{default_data_server, + DEFAULT_DATA_PATH, open_ohlc}`, wires the four OHLC sources into a 4-source-role + harness (SMA-cross-on-close → Exposure → SimBroker(pip 1.0)), and runs to + completion over real bars. +- Fits the scope: it exercises the "build a real-data source from `aura-ingest` + alone" promise (#81), the canonical 4-source merge order (#92), and the + timestamp-window open (#80) in one realistic task. +- Outcome: built (after one fix — see the `Cell` constructor friction) and ran. + `open_ohlc` returned exactly 4 sources O/H/L/C, each reporting `bounds()` + without materializing; 53173 real bars streamed on all four taps; OHLC + ordering (`low<=open<=high`) held on **53173/53173** bars; close-driven equity + produced (`+314.46` pips at pip 1.0). The crate has **no `data-server` + dependency** — the #81 crutch the prior milestone-18 fixture needed is gone. + Matched expected. + +### `fieldtests/milestone-runway/rw_3_multitap_ts_join.rs` — multi-tap trace honesty (#93) +- A consumer records two taps (exposure + cumulative equity) off the same real + GER40 run — the taps fire at **different cadences** — and fuses them with + `aura_engine::join_on_ts` / `JoinedRow`. +- Fits the scope: directly the #93 promise — "join multi-tap sink traces + honestly on timestamp, not by index." +- Outcome: built and ran. Exposure fired 27725×, equity 27744× (differ by 19); + `join_on_ts` produced exactly one `JoinedRow` per spine entry (27744); a + by-timestamp cross-check of every `Some`/`None` side against the exposure + stream found **0 mismatches**; the first rows are the warm-up bars where + exposure had not yet fired (`None`) — precisely the cardinality-misalignment a + positional zip would corrupt. Matched expected. + +### `fieldtests/milestone-runway/out_rw_4_runs_coherence.txt` — run-registry coherence (#73/#82) +- CLI invocations confirming `runs list`/`rank` are retired and + `runs families`/`runs family … rank` work, plus a rustdoc check of the + two-store `Registry::open`. +- Fits the scope: the registry-honesty half of the promise. +- Outcome: ran. `runs list` and `runs rank` → **exit 2** with the usage line + (clean diagnostic; both absent from the usage string). `runs families` → + exit 0 listing the family; `runs family rank total_pips` → exit 0 with + ranked members. The `Registry::open` rustdoc documents the two + directory-co-located stores and the per-directory-isolation consequence (#82). + Matched expected — with one edge observation (bogus family id, below). + +## Findings + +### [working] Per-instrument pip honesty is correct and refuses cleanly +- Example: `out_rw_1_pip_honesty.txt`. +- Vetted `GER40` emits `sim-optimal(pip_size=1)` in the manifest (index point, + not the FX `0.0001`); un-vetted `AAPL.US`/`FOOBAR` exit 2 with a stderr + diagnostic that names the symbol and the fix ("add it to the instrument + table") and **no stdout leak**. The refusal gates at the lookup before any + data access. This is the milestone's headline honesty win, working end to end. +- Recommended action: carry on. + +### [working] A real-data source builds from `aura-ingest` alone — the `data-server` crutch is gone +- Example: `rw_2_open_ohlc_real.rs` + its Cargo.toml. +- `default_data_server()`, `DEFAULT_DATA_PATH`, `DataServer` (re-export), and + `open_ohlc` are all reachable from `aura-ingest`; the fixture crate carries + **no** `data-server` dependency. The prior milestone-18 fixture had to pull + `data-server` as a git dep because `M1FieldSource::open` wanted + `&Arc` and aura-ingest re-exported neither it nor the path. #81 + closes that. `open_ohlc` vouches for the C4 O/H/L/C order so the consumer + never spells it out (#92); 53173 real bars streamed to completion. +- Recommended action: carry on. + +### [working] Multi-tap traces fuse honestly on timestamp +- Example: `rw_3_multitap_ts_join.rs`. +- Two real-data taps at genuinely different cadences (Δ19 rows) join to exactly + one `JoinedRow` per spine entry, every `Some`/`None` side aligning to the + source stream **by timestamp** (0 by-ts mismatches over 27744 rows). The + Option-per-side semantics surfaced the warm-up `None` rows exactly where a + zip-by-index would have silently shifted every later row. The `Recorder` + rustdoc states the cadence rule ("join on the recorded timestamp") at the + point of use, so a consumer is steered to `join_on_ts` rather than a zip. +- Recommended action: carry on. + +### [working] Run-registry surface is coherent; two-store behaviour is documented +- Example: `out_rw_4_runs_coherence.txt`. +- `runs list`/`rank` are gone from the usage string and exit 2; `runs families`/ + `runs family … rank` work and emit JSON. `Registry::open`'s rustdoc documents + the two directory-co-located stores (`runs.jsonl` + the fixed-name + `families.jsonl` sibling) and the per-directory (not per-filename) isolation + consequence at the bind point (#82) — exactly the "discoverable from the + public docs" half of the promise. +- Recommended action: carry on. + +### [friction] Hand-authoring a param-cell vector needs `Cell::from_i64`, not the obvious `Cell::I64` +- Example: `rw_2_open_ohlc_real.rs` (first compile attempt). +- A consumer bootstrapping a harness with literal params naturally reaches for + `Cell::I64(3)` / `Cell::F64(1.0)` (mirroring `Scalar::I64` / `Scalar::F64`, + which ARE tuple-style and used right beside it for grid values). `Cell` instead + exposes only `from_i64` / `from_f64` / `from_bool` / `from_ts` constructors, so + the first build failed with `no associated item 'I64' found for struct Cell`. + The compiler's suggestion was excellent (it listed `from_i64` etc.), so the fix + was a 5-second one — but the asymmetry with `Scalar`'s tuple variants is a real + papercut for hand-authored params (the prior fieldtests sidestepped it by only + ever feeding `Cell`s that came pre-built out of a `GridSpace`). Friction, not a + bug: the task completed and the diagnostic was clean. +- Recommended action: plan a tidy iteration — either add `Cell::I64`/`F64`/etc. + tuple-style constructors (or `From`/`From` impls) to match `Scalar`, + or document the `from_*` constructors on `Cell`'s rustdoc as the param-authoring + entry point. (Low priority; ergonomic only.) + +### [spec_gap] `runs family ` exits 0 with empty output, not a "no such family" diagnostic +- Example: `out_rw_4_runs_coherence.txt`, final block. +- `aura runs family nope-99` (a family id that does not exist) prints nothing and + exits **0**. The retired commands exit 2 with a usage line, and a real consumer + who typos a family id would reasonably expect a non-zero exit or a "no such + family `nope-99`" message rather than silent success that looks + indistinguishable from "a real but empty family". The top-level usage string, + the ledger (C18 realization note), and the glossary do not constrain what an + unknown-but-well-formed family id should do, so I cannot tell whether exit 0 + + empty is the intended contract (treat-as-empty, mirroring how a missing store + is `Ok(vec![])`) or an oversight. Two readings are equally plausible: + (a) **treat-as-empty** — consistent with `Registry::load` treating a missing + file as an empty registry, so an unknown id is "a family with zero members"; + (b) **not-found is an error** — consistent with the retired commands and with + typo-safety. I picked no behaviour to assert in a fixture; I recorded the + observation. +- Recommended action: ratify the intended behaviour into the C18 realization + note (and the CLI usage), or tighten it to a non-zero exit + diagnostic if (b). + +## Recommendation summary + +| Finding | Class | Action | +|---|---|---| +| Per-instrument pip honesty (vetted pip + refusal) | working | carry-on | +| Real-data source builds from `aura-ingest` alone (`open_ohlc`, re-exports) | working | carry-on | +| Multi-tap traces fuse on timestamp (`join_on_ts`) | working | carry-on | +| Run-registry coherence + two-store rustdoc | working | carry-on | +| `Cell` lacks tuple-style constructors (asymmetry with `Scalar`) | friction | plan (tidy iteration) | +| `runs family ` → exit 0, empty | spec_gap | ratify / tighten the design ledger | diff --git a/fieldtests/milestone-runway/Cargo.lock b/fieldtests/milestone-runway/Cargo.lock new file mode 100644 index 0000000..70c4b31 --- /dev/null +++ b/fieldtests/milestone-runway/Cargo.lock @@ -0,0 +1,1010 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "aura-core" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "aura-engine" +version = "0.1.0" +dependencies = [ + "aura-core", + "serde", + "serde_json", +] + +[[package]] +name = "aura-ingest" +version = "0.1.0" +dependencies = [ + "aura-core", + "aura-engine", + "data-server", +] + +[[package]] +name = "aura-std" +version = "0.1.0" +dependencies = [ + "aura-core", + "chrono", + "chrono-tz", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-server" +version = "0.1.0" +source = "git+http://192.168.178.103:3000/Brummel/data-server.git?branch=main#0f5e6655b90eeab0f43a8b0f330b8d1632e80ed4" +dependencies = [ + "chrono", + "regex", + "zip", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "runway-fieldtest" +version = "0.0.0" +dependencies = [ + "aura-core", + "aura-engine", + "aura-ingest", + "aura-std", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/fieldtests/milestone-runway/Cargo.toml b/fieldtests/milestone-runway/Cargo.toml new file mode 100644 index 0000000..704b207 --- /dev/null +++ b/fieldtests/milestone-runway/Cargo.toml @@ -0,0 +1,52 @@ +# Standalone downstream-consumer crate for the MILESTONE fieldtest of "Runway — +# real-data ergonomics & honesty hardening". +# +# This is the milestone-close fieldtest: it exercises the honesty-hardened +# real-data ingestion seam end to end, the way a downstream researcher would — +# open the real GER40 M1 archive via the canonical opener from `aura-ingest` +# ALONE (the #81 promise: a real-data source builds without naming the external +# `data-server` crate), run a strategy against real bars, and fuse multi-tap sink +# traces honestly on timestamp (#93). +# +# Like the per-cycle fixtures, this is NOT a member of the aura workspace — it +# path-deps the engine crates exactly as a real C16 research project would, then +# drives the PUBLIC surface ONLY (design ledger + glossary + `cargo doc` rustdoc; +# no crates/*/src was read). NB the Runway milestone's whole point is that a +# real-data consumer no longer reaches for `data-server` directly: there is NO +# `data-server` dependency in this manifest — `aura-ingest` re-exports DataServer +# + DEFAULT_DATA_PATH + default_data_server() + open_ohlc() now (#81/#92). The +# prior milestone-18 fixture had to pull `data-server` as a git dep; this one +# proves that crutch is gone. +# +# Built/run via, e.g.: +# cargo run --manifest-path fieldtests/milestone-runway/Cargo.toml --bin rw_2_open_ohlc_real +# so HEAD source is always what runs. +# +# Empty [workspace] table: marks this fixture crate as its OWN workspace root. +[workspace] + +[package] +name = "runway-fieldtest" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +aura-core = { path = "../../crates/aura-core" } +aura-engine = { path = "../../crates/aura-engine" } +aura-ingest = { path = "../../crates/aura-ingest" } +aura-std = { path = "../../crates/aura-std" } + +[[bin]] +name = "rw_2_open_ohlc_real" +path = "rw_2_open_ohlc_real.rs" + +[[bin]] +name = "rw_3_multitap_ts_join" +path = "rw_3_multitap_ts_join.rs" + +# Scenario 1 (per-instrument pip honesty) and scenario 4 (run-registry coherence) +# are exercised directly through the `aura` binary, since the canonical consumer +# form for a CLI tool is the CLI invocation itself, not a Rust program. Their +# transcripts live in out_rw_1_pip_honesty.txt / out_rw_4_runs_coherence.txt; +# there is no [[bin]] for either. diff --git a/fieldtests/milestone-runway/out_rw_1_pip_honesty.txt b/fieldtests/milestone-runway/out_rw_1_pip_honesty.txt new file mode 100644 index 0000000..6dc600b --- /dev/null +++ b/fieldtests/milestone-runway/out_rw_1_pip_honesty.txt @@ -0,0 +1,33 @@ +# Runway milestone fieldtest — scenario 1: per-instrument pip honesty (#22) +# +# The task: a downstream researcher runs a real backtest on a VETTED index and +# on an UN-VETTED instrument, and must get an honest pip on the first, a clean +# REFUSAL (non-zero exit, no silent mis-scaled output) on the second. +# +# Consumer form: the canonical CLI invocation itself (aura run --real). +# Built/run via 'cargo run' so HEAD is what runs. + +$ aura run --real GER40 --from 1420070400000 --to 1422748800000 +{"manifest":{"commit":"0d185fb4c3d2be3002760dbb9d9a8a2ceed795e0","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["exposure_scale",{"F64":0.5}]],"window":[1420156800000000000,1422648780000000000],"seed":0,"broker":"sim-optimal(pip_size=1)"},"metrics":{"total_pips":977.3799999994587,"max_drawdown":654.9300000000931,"exposure_sign_flips":8155}} +exit=0 + +# ^ VETTED INDEX: broker label reports sim-optimal(pip_size=1) — the correct +# index point, NOT the FX default 0.0001. Honest PnL. + +$ aura run --real EURUSD --from 1420070400000 --to 1422748800000 +{"manifest":{"commit":"0d185fb4c3d2be3002760dbb9d9a8a2ceed795e0","params":[["sma_fast",{"I64":2}],["sma_slow",{"I64":4}],["exposure_scale",{"F64":0.5}]],"window":[1420156800000000000,1422654780000000000],"seed":0,"broker":"sim-optimal(pip_size=0.0001)"},"metrics":{"total_pips":-0.8768415000001664,"max_drawdown":0.9156020000001746,"exposure_sign_flips":8503}} +exit=0 + +# ^ VETTED FX MAJOR: broker label reports sim-optimal(pip_size=0.0001) — the +# correct forex pip, NOT the index point 1. total_pips=-0.8768 is FX-scaled +# (sub-pip on this window), NOT a mis-scaled index number (~977 on GER40 above +# over the same window). The pip promise's SECOND positive face holds: index +# point AND forex pip both demonstrated end-to-end, each at its own scale. + +$ aura run --real AAPL.US --from 1420070400000 --to 1422748800000 # un-vetted (instrument_spec -> None) +aura: no vetted pip/instrument spec for symbol 'AAPL.US' — refusing to run a real instrument with a guessed pip (add it to the instrument table) +exit=2 (stdout above is the stderr diagnostic only — no mis-scaled stdout leak) + +$ aura run --real FOOBAR --from 1420070400000 --to 1422748800000 # un-vetted AND no archive +aura: no vetted pip/instrument spec for symbol 'FOOBAR' — refusing to run a real instrument with a guessed pip (add it to the instrument table) +exit=2 (pip-refusal fires at the lookup, BEFORE any data access) diff --git a/fieldtests/milestone-runway/out_rw_2_open_ohlc_real.txt b/fieldtests/milestone-runway/out_rw_2_open_ohlc_real.txt new file mode 100644 index 0000000..cbe1309 --- /dev/null +++ b/fieldtests/milestone-runway/out_rw_2_open_ohlc_real.txt @@ -0,0 +1,20 @@ +default_data_server() over DEFAULT_DATA_PATH = /mnt/tickdata/Pepperstone +instrument_spec(GER40) = pip_size 1 +open_ohlc(GER40, [1420070400000..1425254400000]) -> 4 sources (expect 4: O,H,L,C) + source[0] open: bounds Some((1420070400000, 1425254400000)) + source[1] high: bounds Some((1420070400000, 1425254400000)) + source[2] low: bounds Some((1420070400000, 1425254400000)) + source[3] close: bounds Some((1420070400000, 1425254400000)) + +harness param-space slots: + sma_cross.fast.length : I64 + sma_cross.slow.length : I64 + exposure.scale : F64 + +streamed to completion: open=53173 high=53173 low=53173 equity=53173 bars recorded +OHLC ordering (low<=open<=high) holds on 53173/53173 real bars + +GER40 close SMA-cross over real bars: 53173 equity points, final cumulative = 314.46 pips (pip_size=1.0) +real bar window actually streamed: [1420156800000..1425254400000] (unix-ms) + +OK: real GER40 OHLC opened from aura-ingest alone -> 4 sources, C4 order -> ran to completion. diff --git a/fieldtests/milestone-runway/out_rw_3_multitap_ts_join.txt b/fieldtests/milestone-runway/out_rw_3_multitap_ts_join.txt new file mode 100644 index 0000000..eefdb9c --- /dev/null +++ b/fieldtests/milestone-runway/out_rw_3_multitap_ts_join.txt @@ -0,0 +1,15 @@ +two taps, DIFFERENT cadences: exposure fired 27725 times, equity fired 27744 times + -> lengths differ by 19 rows; zip-by-index would silently misalign every row past the first divergence + +join_on_ts: spine(equity)=27744 rows -> 27744 JoinedRows (exactly one per spine entry) +exposure side: present(Some)=27725, absent(None)=19, by-ts cross-check mismatches=0 + +first 6 joined rows (equity spine | exposure side): + ts=1420156800000 equity=0.0000 exposure=None (had not fired at this ts) + ts=1420156919999 equity=0.0000 exposure=None (had not fired at this ts) + ts=1420156980000 equity=0.0000 exposure=None (had not fired at this ts) + ts=1420157040000 equity=0.0000 exposure=None (had not fired at this ts) + ts=1420157099999 equity=0.0000 exposure=None (had not fired at this ts) + ts=1420157160000 equity=0.0000 exposure=None (had not fired at this ts) + +OK: two real-data taps at different cadences fused HONESTLY on timestamp (27725 aligned, 19 side-absent), never by index. diff --git a/fieldtests/milestone-runway/out_rw_4_runs_coherence.txt b/fieldtests/milestone-runway/out_rw_4_runs_coherence.txt new file mode 100644 index 0000000..d09bc2b --- /dev/null +++ b/fieldtests/milestone-runway/out_rw_4_runs_coherence.txt @@ -0,0 +1,35 @@ +# Runway milestone fieldtest — scenario 4: run-registry coherence (#73/#82) +# +# The task: a downstream researcher confirms the dead-end 'runs list'/'rank' +# surface is RETIRED (clean diagnostic, non-zero exit) while 'runs families'/ +# 'runs family' still work, and the two-store Registry behaviour is discoverable +# from the public docs. +# +# Consumer form: the canonical CLI invocation itself. Built/run via 'cargo run'. + +## top-level usage (note: no 'runs list', no 'runs rank') +$ aura +aura: usage: aura run [--macd] | aura run --real [--from ] [--to ] | aura graph | aura sweep [--name ] | aura mc [--name ] | aura walkforward [--name ] | aura runs families | aura runs family [rank ] +exit=2 + +## RETIRED surface — must refuse +$ aura runs list +aura: usage: aura run [--macd] | aura run --real [--from ] [--to ] | aura graph | aura sweep [--name ] | aura mc [--name ] | aura walkforward [--name ] | aura runs families | aura runs family [rank ] +exit=2 + +$ aura runs rank total_pips +aura: usage: aura run [--macd] | aura run --real [--from ] [--to ] | aura graph | aura sweep [--name ] | aura mc [--name ] | aura walkforward [--name ] | aura runs families | aura runs family [rank ] +exit=2 + +## LIVE surface — must work (after a sweep persisted family 'ft-coherence-0') +$ aura runs families +{"family_id":"ft-coherence-0","kind":"Sweep","members":4} +exit=0 + +$ aura runs family ft-coherence-0 rank total_pips # (first member shown; full list in run output) +{"manifest":{"commit":"0d185fb4c3d2be3002760dbb9d9a8a2ceed795e0","params":[["signals.trend.fast.length",{"I64":2}],["signals.trend.slow.length",{"I64":4}],["signals.momentum.fast.length",{"I64":2}],["signals.momentum.slow.length",{"I64":4}],["signals.momentum.signal.length",{"I64":3}],["signals.blend.weights[0]",{"F64":1.0}],["signals.blend.weights[1]",{"F64":1.0}],["exposure.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal(pip_size=0.0001)"},"metrics":{"total_pips":0.26428361277333234,"max_drawdown":0.15423679012344482,"exposure_sign_flips":2}} +... (3 more members) ... exit=0 + +## edge: a BOGUS family id +$ aura runs family nope-99 +exit=0 <-- NB: empty output + exit 0, NOT a 'no such family' diagnostic (see findings) diff --git a/fieldtests/milestone-runway/rw_2_open_ohlc_real.rs b/fieldtests/milestone-runway/rw_2_open_ohlc_real.rs new file mode 100644 index 0000000..ad18478 --- /dev/null +++ b/fieldtests/milestone-runway/rw_2_open_ohlc_real.rs @@ -0,0 +1,250 @@ +// Runway milestone fieldtest — scenario 2: the real GER40 M1 ingestion path via +// the CANONICAL 4-source OHLC opener (#80/#81/#92). +// +// The task a downstream researcher does: "open the real GER40 M1 archive and run +// my SMA-cross-on-close strategy against real bars, to completion — and I want to +// reach for nothing but `aura-ingest` to do it." +// +// What this exercises, top-down from the milestone promise: +// - #81: a real-data source builds from `aura-ingest` ALONE. This file imports +// NOTHING from `data-server`; it uses aura_ingest::{default_data_server, +// DEFAULT_DATA_PATH, open_ohlc}. The Cargo.toml has no `data-server` dep. +// - #92: `open_ohlc` is the single vetted home of the C4 merge order +// (open, high, low, close). A consumer never spells the order out. +// - #80: M1FieldSource opens a timestamp window; here via the OHLC opener over +// a closed epoch-ns [from, to]. +// +// PUBLIC INTERFACE ONLY: API from `cargo doc` rustdoc + the design ledger +// (C3/C4 ingestion, C12 source seam, C7 SoA). No crates/*/src was read. + +use std::sync::mpsc; + +use aura_core::{Cell, Firing, Scalar, ScalarKind, Timestamp}; +use aura_engine::{ + f64_field, summarize, BlueprintNode, Composite, Edge, OutField, Role, Source, Target, +}; +use aura_ingest::{default_data_server, instrument_spec, open_ohlc, DEFAULT_DATA_PATH}; +use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; + +const SYMBOL: &str = "GER40"; + +fn sma_cross() -> Composite { + Composite::new( + "sma_cross", + vec![ + Sma::builder().named("fast").into(), + Sma::builder().named("slow").into(), + Sub::builder().into(), + ], + vec![ + Edge { from: 0, to: 2, slot: 0, from_field: 0 }, + Edge { from: 1, to: 2, slot: 1, from_field: 0 }, + ], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + source: None, + }], + vec![OutField { node: 2, field: 0, name: "out".into() }], + ) +} + +// Harness with FOUR source-roles in the C4 merge order open/high/low/close, to +// mirror what open_ohlc hands back. The strategy reads CLOSE (the canonical price +// field the SMA-cross consumes). open/high/low are routed into per-field recording +// taps, so this run proves all four real OHLC streams flow to completion — not +// just close. Equity (close-driven) is recorded too. +#[allow(clippy::type_complexity)] +fn ohlc_harness() -> ( + Composite, + mpsc::Receiver<(Timestamp, Vec)>, // equity (close strategy) + mpsc::Receiver<(Timestamp, Vec)>, // open tap + mpsc::Receiver<(Timestamp, Vec)>, // high tap + mpsc::Receiver<(Timestamp, Vec)>, // low tap +) { + let (tx_eq, rx_eq) = mpsc::channel(); + let (tx_o, rx_o) = mpsc::channel(); + let (tx_h, rx_h) = mpsc::channel(); + let (tx_l, rx_l) = mpsc::channel(); + + // node indices: + // 0 sma_cross (close), 1 Exposure, 2 SimBroker, 3 equity-rec, + // 4 open-rec, 5 high-rec, 6 low-rec + let bp = Composite::new( + "ohlc_harness", + vec![ + BlueprintNode::Composite(sma_cross()), + Exposure::builder().into(), + SimBroker::builder(1.0).into(), // GER40 index pip = 1.0 + Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(), + Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_o).into(), + Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_h).into(), + Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_l).into(), + ], + vec![ + Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // sma_cross -> exposure + Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker.slot0 + Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // broker equity -> rec + ], + // FOUR source-roles in C4 merge order: open, high, low, close. The opener + // hands back Box in exactly this order; run() maps the source + // vec positionally onto the source-roles in declaration order. + vec![ + Role { + name: "open".into(), + targets: vec![Target { node: 4, slot: 0 }], + source: Some(ScalarKind::F64), + }, + Role { + name: "high".into(), + targets: vec![Target { node: 5, slot: 0 }], + source: Some(ScalarKind::F64), + }, + Role { + name: "low".into(), + targets: vec![Target { node: 6, slot: 0 }], + source: Some(ScalarKind::F64), + }, + Role { + name: "close".into(), + // close feeds the strategy AND the broker's price slot (slot 1). + targets: vec![ + Target { node: 0, slot: 0 }, + Target { node: 2, slot: 1 }, + ], + source: Some(ScalarKind::F64), + }, + ], + vec![], + ); + (bp, rx_eq, rx_o, rx_h, rx_l) +} + +fn ms(t: Timestamp) -> i64 { + t.0 / 1_000_000 +} + +fn main() { + // #81: the data server builds from aura-ingest alone, over the local archive. + let server = default_data_server(); + println!("default_data_server() over DEFAULT_DATA_PATH = {DEFAULT_DATA_PATH}"); + + // The pip the milestone vouches for: a vetted index quotes in points (1.0). + match instrument_spec(SYMBOL) { + Some(spec) => println!("instrument_spec({SYMBOL}) = pip_size {}", spec.pip_size), + None => { + eprintln!("{SYMBOL} is un-specced — a real run must refuse; aborting fixture"); + std::process::exit(1); + } + } + + // A ~2-month window of GER40 starting 2015-01-01 (epoch-ns; open_ohlc's unit). + let day_ns: i64 = 86_400_000 * 1_000_000; + let from = Timestamp(1_420_070_400_000 * 1_000_000); // 2015-01-01 UTC + let to = Timestamp(from.0 + 60 * day_ns); + + // #92: the SINGLE vetted home of the C4 merge order. We never spell out + // open/high/low/close — the opener does, and hands back exactly four sources. + let sources: Vec> = match open_ohlc(&server, SYMBOL, from, to) { + Some(s) => s, + None => { + eprintln!("no local {SYMBOL} archive overlapping the window; skipping"); + std::process::exit(0); + } + }; + println!( + "open_ohlc({SYMBOL}, [{}..{}]) -> {} sources (expect 4: O,H,L,C)", + ms(from), + ms(to), + sources.len(), + ); + assert_eq!(sources.len(), 4, "open_ohlc vouches for four OHLC sources"); + + // Each source reports its data extent without materializing (C18/#71). + for (i, s) in sources.iter().enumerate() { + let field = ["open", "high", "low", "close"][i]; + let b = s.bounds(); + println!( + " source[{i}] {field}: bounds {:?}", + b.map(|(f, t)| (ms(f), ms(t))) + ); + } + + let (bp, rx_eq, rx_o, rx_h, rx_l) = ohlc_harness(); + println!("\nharness param-space slots:"); + let space = bp.param_space(); + for ps in &space { + println!(" {} : {:?}", ps.name, ps.kind); + } + + // Bootstrap with concrete params: fast=3, slow=20, exposure scale=1.0. + let params: Vec = vec![Cell::from_i64(3), Cell::from_i64(20), Cell::from_f64(1.0)]; + let mut h = bp + .bootstrap_with_cells(¶ms) + .expect("chosen params kind-check against param_space"); + + // Run to completion over the four REAL OHLC streams, k-way-merged by ts (C3/C4). + h.run(sources); + drop(h); + + let eq_rows: Vec<_> = rx_eq.try_iter().collect(); + let o_rows: Vec<_> = rx_o.try_iter().collect(); + let hi_rows: Vec<_> = rx_h.try_iter().collect(); + let lo_rows: Vec<_> = rx_l.try_iter().collect(); + + println!( + "\nstreamed to completion: open={} high={} low={} equity={} bars recorded", + o_rows.len(), + hi_rows.len(), + lo_rows.len(), + eq_rows.len(), + ); + assert!(!o_rows.is_empty(), "real open bars streamed"); + assert!(!hi_rows.is_empty(), "real high bars streamed"); + assert!(!lo_rows.is_empty(), "real low bars streamed"); + assert!(!eq_rows.is_empty(), "close-driven equity produced"); + + // The four OHLC fields are the same bars: O/H/L taps share the timestamp set. + assert_eq!(o_rows.len(), hi_rows.len(), "O and H are the same bar stream"); + assert_eq!(o_rows.len(), lo_rows.len(), "O and L are the same bar stream"); + for k in 0..o_rows.len() { + assert_eq!(o_rows[k].0, hi_rows[k].0, "bar {k}: O/H share ts"); + assert_eq!(o_rows[k].0, lo_rows[k].0, "bar {k}: O/L share ts"); + } + // The OHLC ordering invariant on real bars: low <= open,high and high >= open,low. + let mut ohlc_ok = 0usize; + for k in 0..o_rows.len() { + let o = o_rows[k].1[0].as_f64(); + let hi = hi_rows[k].1[0].as_f64(); + let lo = lo_rows[k].1[0].as_f64(); + if lo <= o && o <= hi && lo <= hi { + ohlc_ok += 1; + } + } + println!( + "OHLC ordering (low<=open<=high) holds on {}/{} real bars", + ohlc_ok, + o_rows.len() + ); + assert_eq!(ohlc_ok, o_rows.len(), "every real bar respects low<=open<=high"); + + let equity = f64_field(&eq_rows, 0); + // exposure stream unused here; summarize wants both — reuse equity sign? No: + // summarize needs an exposure series. We only recorded equity; report PnL only. + let final_pips = equity.last().map(|(_, v)| *v).unwrap_or(0.0); + println!( + "\nGER40 close SMA-cross over real bars: {} equity points, final cumulative = {:.2} pips (pip_size=1.0)", + equity.len(), + final_pips, + ); + + // first/last bar timestamps as a sanity window. + let first = o_rows.first().map(|(t, _)| ms(*t)).unwrap_or(0); + let last = o_rows.last().map(|(t, _)| ms(*t)).unwrap_or(0); + println!("real bar window actually streamed: [{first}..{last}] (unix-ms)"); + + // summarize is available but needs exposure; we keep the report PnL-shaped. + let _ = summarize; // referenced for clarity of the public surface + + println!("\nOK: real GER40 OHLC opened from aura-ingest alone -> 4 sources, C4 order -> ran to completion."); +} diff --git a/fieldtests/milestone-runway/rw_3_multitap_ts_join.rs b/fieldtests/milestone-runway/rw_3_multitap_ts_join.rs new file mode 100644 index 0000000..bbbfdee --- /dev/null +++ b/fieldtests/milestone-runway/rw_3_multitap_ts_join.rs @@ -0,0 +1,216 @@ +// Runway milestone fieldtest — scenario 3: multi-tap trace honesty (#93). +// +// The task a downstream researcher does: "I record two taps off the same real +// GER40 run — the exposure stream and the cumulative-equity stream — and I want a +// single per-bar trace that lines each tap up against the other. The taps fire at +// DIFFERENT cadences (exposure warms up later than the raw bar clock; equity only +// fires once the broker has a prev price), so a positional zip-by-index would +// misalign. I want them joined on the recorded TIMESTAMP." +// +// What this exercises, top-down from the milestone promise: #93's `join_on_ts` / +// `JoinedRow` — exactly one joined row per spine entry, each side Some(row) where +// it fired at that ts and None where it did not. The honest fusion the milestone +// promises: traces join on timestamp, never by index. +// +// It also re-uses the #81/#92 real-data seam (open_ohlc from aura-ingest alone) so +// the join runs over REAL bars, not a synthetic toy. +// +// PUBLIC INTERFACE ONLY: API from `cargo doc` rustdoc + the design ledger +// (C8/C18 post-run reduction, C3 no in-graph join, C1 one row per ts). No +// crates/*/src was read. + +use std::sync::mpsc; + +use aura_core::{Cell, Firing, Scalar, ScalarKind, Timestamp}; +use aura_engine::{join_on_ts, BlueprintNode, Composite, Edge, OutField, Role, Source, Target}; +use aura_ingest::{default_data_server, open_ohlc}; +use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; + +const SYMBOL: &str = "GER40"; + +fn sma_cross() -> Composite { + Composite::new( + "sma_cross", + vec![ + Sma::builder().named("fast").into(), + Sma::builder().named("slow").into(), + Sub::builder().into(), + ], + vec![ + Edge { from: 0, to: 2, slot: 0, from_field: 0 }, + Edge { from: 1, to: 2, slot: 1, from_field: 0 }, + ], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + source: None, + }], + vec![OutField { node: 2, field: 0, name: "out".into() }], + ) +} + +// A single-price-role harness with TWO taps off different nodes: +// - exposure tap (off the Exposure node) — fires once SMA-cross warms up +// - equity tap (off the SimBroker) — fires once the broker has a prev price +// Their cadences differ, so their recorded streams have different lengths and +// firing instants — the exact #93 shape. +#[allow(clippy::type_complexity)] +fn harness_two_taps() -> ( + Composite, + mpsc::Receiver<(Timestamp, Vec)>, // exposure tap + mpsc::Receiver<(Timestamp, Vec)>, // equity tap +) { + let (tx_ex, rx_ex) = mpsc::channel(); + let (tx_eq, rx_eq) = mpsc::channel(); + // 0 sma_cross, 1 Exposure, 2 SimBroker, 3 exposure-rec, 4 equity-rec + let bp = Composite::new( + "two_tap_harness", + vec![ + BlueprintNode::Composite(sma_cross()), + Exposure::builder().into(), + SimBroker::builder(1.0).into(), + Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(), + Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(), + ], + vec![ + Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // sma_cross -> exposure + Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker.slot0 + Edge { from: 1, to: 3, slot: 0, from_field: 0 }, // exposure -> exposure tap + Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // equity -> equity tap + ], + vec![Role { + name: "price".into(), + targets: vec![ + Target { node: 0, slot: 0 }, // strategy + Target { node: 2, slot: 1 }, // broker price slot + ], + source: Some(ScalarKind::F64), + }], + vec![], + ); + (bp, rx_ex, rx_eq) +} + +fn ms(t: Timestamp) -> i64 { + t.0 / 1_000_000 +} + +fn main() { + let server = default_data_server(); + + // ~1 month of GER40 from 2015-01-01 (epoch-ns), close-driven price. + let day_ns: i64 = 86_400_000 * 1_000_000; + let from = Timestamp(1_420_070_400_000 * 1_000_000); + let to = Timestamp(from.0 + 31 * day_ns); + + let sources: Vec> = match open_ohlc(&server, SYMBOL, from, to) { + Some(s) => s, + None => { + eprintln!("no local {SYMBOL} archive overlapping the window; skipping"); + std::process::exit(0); + } + }; + // The strategy here uses ONE price role (close). open_ohlc gives four; we take + // the close source (index 3 in the fixed O/H/L/C order #92 vouches for). + let close: Box = sources.into_iter().nth(3).expect("close is source 3"); + + let (bp, rx_ex, rx_eq) = harness_two_taps(); + let params: Vec = vec![Cell::from_i64(3), Cell::from_i64(20), Cell::from_f64(1.0)]; + let mut h = bp.bootstrap_with_cells(¶ms).expect("params kind-check"); + h.run(vec![close]); + drop(h); + + let exposure_rows: Vec<(Timestamp, Vec)> = rx_ex.try_iter().collect(); + let equity_rows: Vec<(Timestamp, Vec)> = rx_eq.try_iter().collect(); + + println!( + "two taps, DIFFERENT cadences: exposure fired {} times, equity fired {} times", + exposure_rows.len(), + equity_rows.len(), + ); + // The whole point of #93: a positional zip-by-index is WRONG when the lengths + // differ. Show the mismatch the index approach would hit: + if exposure_rows.len() != equity_rows.len() { + println!( + " -> lengths differ by {} rows; zip-by-index would silently misalign every row \ + past the first divergence", + (exposure_rows.len() as i64 - equity_rows.len() as i64).abs(), + ); + } + + // The honest fusion: join on the recorded timestamp. Use the equity stream as + // the spine (the trace we anchor on), exposure as a side stream. + let exp_slice: &[(Timestamp, Vec)] = &exposure_rows; + let joined = join_on_ts(&equity_rows, &[exp_slice]); + + println!( + "\njoin_on_ts: spine(equity)={} rows -> {} JoinedRows (exactly one per spine entry)", + equity_rows.len(), + joined.len(), + ); + assert_eq!( + joined.len(), + equity_rows.len(), + "exactly one JoinedRow per spine entry (C1/C8)" + ); + + // Every joined row's ts is the spine ts, in spine order. + for (k, jr) in joined.iter().enumerate() { + assert_eq!(jr.ts, equity_rows[k].0, "row {k}: joined ts == spine ts"); + } + + // Where the exposure side fired at the spine ts -> Some; where it did not -> None. + // Cross-check Some-rows against the exposure stream by ts (not by index!). + use std::collections::HashMap; + let exp_by_ts: HashMap = + exposure_rows.iter().map(|(t, r)| (t.0, r[0].as_f64())).collect(); + + let mut present = 0usize; + let mut absent = 0usize; + let mut mismatches = 0usize; + for jr in &joined { + match &jr.sides[0] { + Some(side_row) => { + present += 1; + let joined_val = side_row[0].as_f64(); + match exp_by_ts.get(&jr.ts.0) { + Some(truth) if (*truth - joined_val).abs() < 1e-12 => {} + _ => mismatches += 1, + } + } + None => { + absent += 1; + // a None side MUST mean exposure genuinely did not fire at this ts. + if exp_by_ts.contains_key(&jr.ts.0) { + mismatches += 1; + } + } + } + } + println!( + "exposure side: present(Some)={present}, absent(None)={absent}, by-ts cross-check mismatches={mismatches}", + ); + assert_eq!(mismatches, 0, "every Some/None matches the exposure stream BY TIMESTAMP"); + + // Show the first few divergent rows: spine bars where exposure had NOT yet + // warmed up (None) vs where both fired (Some) — proving the alignment is by ts. + println!("\nfirst 6 joined rows (equity spine | exposure side):"); + for jr in joined.iter().take(6) { + let eq = jr.spine[0].as_f64(); + let ex = jr.sides[0].as_ref().map(|r| r[0].as_f64()); + println!( + " ts={} equity={:.4} exposure={}", + ms(jr.ts), + eq, + match ex { + Some(v) => format!("Some({v:.4})"), + None => "None (had not fired at this ts)".into(), + } + ); + } + + println!( + "\nOK: two real-data taps at different cadences fused HONESTLY on timestamp \ + ({present} aligned, {absent} side-absent), never by index." + ); +}