Files
AILang/docs/plans/embedding-abi-m5.2.md
T
Brummel 9cc9d9c517 plan: embedding-abi-m5.2 — data-server adapter + symbol-fan swarm + leak-proof
M5 iteration 2 (spec ae905de; builds on m5.1 204c171). 3 tasks:
(1) promote data-server dev-dep -> real dep + additive `adapter`
module (tick_to_px / MidPriceStream lazy Iterator / fold_symbol),
RED-first on tick_to_px; (2) `swarm_runner` [[bin]] — one thread per
symbol, each owning its Kernel (Ctx: !Send makes one-ctx-per-thread
a compile-time guarantee), prints RESULT <sym> <acc_bits> <n>;
(3) integration test spawning that bin as a subprocess under
AILANG_RC_STATS=1 (atexit stat line is unobservable in-process —
runtime/rc.c:132-138), asserting per-symbol bit-exact vs an
independent same-order host reference + Σallocs==Σfrees (ported from
embed_tick_e2e.rs:84-104), skip-if-absent mirroring data-server's
own precedent.

Boss decisions recorded in-plan for the audit: TSan needs NO new
nightly harness — per-thread-ctx race-freedom is compile-time
enforced by Ctx:!Send AND already green-tested by the C-host
embed_swarm_tsan.rs (same pattern); m5.2 adds no new AILang-side
concurrency primitive. data-server dev->real dep is Invariant-1
sanctioned (compiler crates untouched; gate shifts to "AILang
workspace graph data-server count == 0" + zero compiler-surface
diff). Time-shard + friction-harvest deferred to m5.3.
2026-05-19 01:22:19 +02:00

572 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# embedding-abi-m5.2 — data-server adapter + symbol-fan swarm + leak-proof — Implementation Plan
> **Parent spec:** `docs/specs/2026-05-19-embedding-abi-m5.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Add the `data-server` adapter layer to `ail-embed` and the
symbol-fan thread-swarm existence proof: real Pepperstone ticks →
`(ask+bid)/2` mid → the shipped M3 kernel, one symbol per thread with
its own `ailang_ctx_t`, each thread bit-exact vs an independent host
reference, the whole run globally leak-free (`AILANG_RC_STATS`).
**Architecture:** Builds on shipped m5.1 (`204c171`: workspace-
excluded `ail-embed`, data-server-free `Kernel` core). `data-server`
promotes dev-dep → real dep (sanctioned: `ail-embed` is the sole
data-server↔AILang meeting point; Invariant 1 governs the *compiler*
crates, unchanged). A new additive `adapter` module maps
`TickParsed`→mid and lazily streams chunks into `Kernel::run`. A
`swarm_runner` bin spawns one thread per symbol (each thread owns its
`Kernel`; `Ctx: !Send` makes one-ctx-per-thread a *compile-time*
guarantee). An integration test runs that bin as a subprocess under
`AILANG_RC_STATS=1`, asserting per-symbol bit-exactness and
Σallocs==Σfrees. Time-shard + friction-harvest are deferred to m5.3.
**Tech Stack:** Rust (edition 2021); `data-server` (path
`../../libs/data-server`) — `DataServer`, `SymbolChunkIter`,
`records::{TickParsed,DataFormat}`, `DEFAULT_DATA_PATH`;
`std::thread`; the M3 staticlib via the m5.1 `build.rs`.
---
**Files this plan creates or modifies:**
- Modify: `ail-embed/Cargo.toml` — move `data-server` from `[dev-dependencies]` to `[dependencies]`; add explicit `[[bin]] swarm_runner`
- Modify: `ail-embed/src/lib.rs:7` — add one line `pub mod adapter;` (core logic untouched)
- Create: `ail-embed/src/adapter.rs``MidPriceStream` (lazy `Iterator<Item=f64>` over `SymbolChunkIter`) + `fold_symbol`; the sole data-server-knowing code
- Create: `ail-embed/src/bin/swarm_runner.rs` — symbol-fan swarm harness; one thread per symbol, prints `RESULT <sym> <acc_bits:016x> <n>`
- Test: `ail-embed/tests/swarm.rs``skip_if_no_data`; spawn `swarm_runner` under `AILANG_RC_STATS=1`; assert per-symbol bit-exact vs independent reference + Σallocs==Σfrees
Cross-reference anchors (read-only, do not edit):
- `crates/ail/tests/embed_tick_e2e.rs:84-104` — the dual-stat-line `ailang_rc_stats:` Σ-summation discipline to port (attribution-model comment :84-90; parse loop :94-103; `seen` guard :104)
- `crates/ail/tests/embed_swarm_tsan.rs:23-42` — the green C-host per-thread-ctx TSan precedent that ratifies the race-freedom m5.2 reuses (cited, not re-run — see Boss decisions)
- `runtime/rc.c:124-138``ailang_rc_stats_atexit` + the `__attribute__((constructor)) ailang_rc_stats_install` registration (host-language-agnostic; why the leak-proof needs a subprocess)
- `../../libs/data-server/src/lib.rs:166,174,179,236``DataServer::new` / `has_symbol` / `symbols` / `stream_tick`
- `../../libs/data-server/src/records.rs:85-89``TickParsed{time_ms:i64,ask:f64,bid:f64}` (pub fields, `Copy`); `DataFormat::Tick`
- `ail-embed/src/lib.rs:77-100` — the `Kernel` API the adapter builds on (`Kernel::new`, `Kernel::run<I:IntoIterator<Item=f64>>`)
---
### Task 1: Promote `data-server` to a real dep + the adapter module
**Files:**
- Modify: `ail-embed/Cargo.toml`
- Modify: `ail-embed/src/lib.rs:7`
- Create: `ail-embed/src/adapter.rs`
- [ ] **Step 1: Write the failing unit test**
Create `ail-embed/src/adapter.rs` containing *only* the test module
below (the function it calls does not exist yet — RED):
```rust
//! The sole `data-server`-knowing layer of `ail-embed`: maps
//! `data_server::records::TickParsed` to the single scalar the M3
//! `Tick` record carries (mid price), and lazily streams
//! `SymbolChunkIter` chunks into the data-server-free `Kernel`
//! (host-side chunk unroll — the adapter owns chunk iteration).
#[cfg(test)]
mod tests {
use super::tick_to_px;
use data_server::records::TickParsed;
/// mid = (ask + bid) / 2. Exact in f64 for these inputs.
#[test]
fn tick_to_px_is_mid() {
let t = TickParsed { time_ms: 0, ask: 2.0, bid: 4.0 };
assert_eq!(tick_to_px(&t), 3.0);
}
}
```
- [ ] **Step 2: Wire the module + dependency so the test can build**
In `ail-embed/src/lib.rs`, add the module declaration immediately
after the `use std::ffi::c_void;` line (currently `src/lib.rs:7`).
The exact existing line is:
```rust
use std::ffi::c_void;
```
Replace it verbatim with:
```rust
use std::ffi::c_void;
/// The sole `data-server`-knowing layer (spec §2). Additive; the
/// embedding core below has zero finance knowledge.
pub mod adapter;
```
In `ail-embed/Cargo.toml`, move the `data-server` line out of
`[dev-dependencies]` into `[dependencies]`. The exact current block
is:
```toml
[dependencies]
[dev-dependencies]
data-server = { path = "../../libs/data-server" }
zip = "2"
tempfile = "3"
```
Replace it verbatim with:
```toml
[dependencies]
# The adapter + swarm_runner bin link data-server (ail-embed is the
# sole data-server↔AILang meeting point — Invariant 1 governs the
# *compiler* crates, not this crate). Path is manifest-relative
# (data-server is at /home/brummel/dev/libs/data-server).
data-server = { path = "../../libs/data-server" }
[dev-dependencies]
zip = "2"
tempfile = "3"
[[bin]]
name = "swarm_runner"
path = "src/bin/swarm_runner.rs"
```
- [ ] **Step 3: Run the test to verify it fails**
Run: `cargo test --manifest-path ail-embed/Cargo.toml --lib tick_to_px_is_mid`
Expected: FAIL — compile error `cannot find function \`tick_to_px\`
in this scope` (or `unresolved import \`super::tick_to_px\``). The
adapter functions are unimplemented.
- [ ] **Step 4: Write the adapter implementation**
Prepend the following to `ail-embed/src/adapter.rs`, *above* the
`#[cfg(test)] mod tests` block (keep the module doc comment at the
top of the file):
```rust
use std::sync::Arc;
use data_server::records::TickParsed;
use data_server::{DataServer, SymbolChunkIter};
use crate::Kernel;
/// Mid price — the single scalar the M3 `Tick` record carries.
#[inline]
pub fn tick_to_px(t: &TickParsed) -> f64 {
(t.ask + t.bid) / 2.0
}
/// Lazy `Iterator<Item = f64>` over a `data-server` tick stream,
/// yielding the per-tick mid. Pulls `next_chunk` on demand (the
/// adapter owns chunk iteration; chunk boundaries are invisible to
/// the fold) so a whole symbol is never materialised.
pub struct MidPriceStream {
it: SymbolChunkIter<TickParsed>,
chunk: Option<Arc<[TickParsed]>>,
idx: usize,
}
impl MidPriceStream {
pub fn new(it: SymbolChunkIter<TickParsed>) -> Self {
Self { it, chunk: None, idx: 0 }
}
}
impl Iterator for MidPriceStream {
type Item = f64;
fn next(&mut self) -> Option<f64> {
loop {
if let Some(c) = &self.chunk {
if self.idx < c.len() {
let px = tick_to_px(&c[self.idx]);
self.idx += 1;
return Some(px);
}
}
match self.it.next_chunk() {
Some(c) => {
self.chunk = Some(c);
self.idx = 0;
}
None => return None,
}
}
}
}
/// Fold up to `max_ticks` of `symbol`'s mid-price stream through the
/// M3 kernel. `None` if the symbol has no tick stream. Returns
/// `(Σ mid, count)` — the kernel's `acc += px; n += 1` over the
/// bounded prefix, identical-order to a host reference fold.
pub fn fold_symbol(
server: &Arc<DataServer>,
symbol: &str,
max_ticks: usize,
) -> Option<(f64, i64)> {
let it = server.stream_tick(symbol)?;
Some(Kernel::new().run(MidPriceStream::new(it).take(max_ticks)))
}
```
- [ ] **Step 5: Run the test to verify it passes**
Run: `cargo test --manifest-path ail-embed/Cargo.toml --lib tick_to_px_is_mid`
Expected: PASS — `test adapter::tests::tick_to_px_is_mid ... ok`,
`1 passed; 0 failed`.
- [ ] **Step 6: Verify the m5.1 suite still passes (no core regression)**
Run: `cargo test --manifest-path ail-embed/Cargo.toml`
Expected: PASS — `kernel_run_sums_prices` (lib),
`tick_to_px_is_mid` (lib), `hermetic_smoke_data_server_roundtrip`
(smoke) all `ok`; `0 failed` total. The m5.1 core (`Kernel`,
`Ctx`, box helpers) is unchanged; only `pub mod adapter;` was added.
---
### Task 2: The symbol-fan swarm binary
A separate process so the C-runtime `AILANG_RC_STATS` atexit line is
observable (an in-process test exits after the harness, never seeing
it — `runtime/rc.c:132-138`). One thread per symbol; each thread
constructs its own `Kernel` — `Ctx: !Send` makes one-ctx-per-thread
a *compile-time* guarantee (a `Kernel` cannot be moved across a
thread boundary).
**Files:**
- Create: `ail-embed/src/bin/swarm_runner.rs`
- [ ] **Step 1: Write `ail-embed/src/bin/swarm_runner.rs`**
```rust
//! Symbol-fan swarm harness (spec §3 symbol-fan / Testing §2). Run
//! as a subprocess by `tests/swarm.rs` under `AILANG_RC_STATS=1` so
//! the C-runtime atexit `ailang_rc_stats:` line is observable.
//!
//! argv[1] = data dir (defaults to `data_server::DEFAULT_DATA_PATH`).
//! Enumerates up to `N_SYMBOLS` symbols that have tick files
//! (requires >= 2 for a meaningful swarm), spawns one thread per
//! symbol — each thread owns its `Kernel` (`Ctx: !Send` forces
//! one ctx per thread) — folds at most `MAX_TICKS` mids, and prints
//! one `RESULT <symbol> <acc_bits:016x> <count>` line per symbol to
//! stdout. Exits 0 on success, non-zero (with a message) otherwise.
use std::sync::Arc;
use ail_embed::adapter::fold_symbol;
use data_server::records::DataFormat;
use data_server::{DataServer, DEFAULT_DATA_PATH};
/// Deterministic bounded prefix per symbol (chunk-aligned via
/// `Iterator::take`): enough real volume to be a genuine proof,
/// bounded so the E2E stays ~seconds. m5.3 owns the friction
/// timing; here it is only a runtime bound.
const MAX_TICKS: usize = 2_000_000;
/// Max threads/symbols fanned. Real data has >= 2 tick symbols
/// (EURUSD, XAUUSD); take min(N_SYMBOLS, available).
const N_SYMBOLS: usize = 4;
fn main() {
let data_dir = std::env::args()
.nth(1)
.unwrap_or_else(|| DEFAULT_DATA_PATH.to_string());
let server = Arc::new(DataServer::new(&data_dir));
let mut tick_syms: Vec<String> = server
.symbols()
.into_iter()
.filter(|s| {
server.file_count(s, DataFormat::Tick).unwrap_or(0) > 0
})
.map(|s| s.to_string())
.collect();
tick_syms.truncate(N_SYMBOLS);
if tick_syms.len() < 2 {
eprintln!(
"swarm_runner: need >= 2 tick symbols under {data_dir}, found {}",
tick_syms.len()
);
std::process::exit(2);
}
let handles: Vec<_> = tick_syms
.into_iter()
.map(|sym| {
let srv = Arc::clone(&server);
// Each thread builds its own Kernel inside the closure.
// `Kernel`/`Ctx` are `!Send`, so this is the ONLY way
// this compiles — one ctx per thread, enforced by the
// type system, not by convention.
std::thread::spawn(move || {
let (acc, n) = fold_symbol(&srv, &sym, MAX_TICKS)
.expect("symbol has a tick stream");
(sym, acc, n)
})
})
.collect();
for h in handles {
let (sym, acc, n) = h.join().expect("worker thread panicked");
println!("RESULT {sym} {:016x} {n}", acc.to_bits());
}
}
```
- [ ] **Step 2: Verify the swarm binary compiles**
Run: `cargo build --manifest-path ail-embed/Cargo.toml --bin swarm_runner`
Expected: PASS — exit 0, `Finished` line. That it compiles *is* the
compile-time per-thread-ctx proof: `fold_symbol` constructs a
`Kernel` (`!Send`) inside each `thread::spawn` closure; a shared-ctx
design would be `error[E0277]: \`*mut c_void\` cannot be sent between
threads safely` and fail here.
---
### Task 3: Integration test — symbol-fan leak-free + bit-exact
Adds no production code; spawns the Task 2 bin and asserts the spec
acceptance. Skip-if-absent mirrors `data-server`'s own precedent.
**Files:**
- Create: `ail-embed/tests/swarm.rs`
- [ ] **Step 1: Write `ail-embed/tests/swarm.rs`**
```rust
//! Symbol-fan swarm E2E (spec Testing §2). Runs the `swarm_runner`
//! bin as a subprocess under `AILANG_RC_STATS=1` (the C-runtime
//! atexit stat line, runtime/rc.c:124-138, is only observable from a
//! separate process that exits). Asserts, per symbol, the kernel
//! `(acc,n)` is BIT-EXACT vs an independent same-order host
//! reference fold, and globally Σallocs==Σfrees across all
//! `ailang_rc_stats:` lines (the M1/M2/M3 dual-stat-line model,
//! ported from crates/ail/tests/embed_tick_e2e.rs:84-104). Skips
//! when /mnt tick data is absent — mirroring data-server's own
//! tests/data_server.rs::skip_if_no_data() precedent.
use std::process::Command;
use std::sync::Arc;
use data_server::records::DataFormat;
use data_server::{DataServer, DEFAULT_DATA_PATH};
// Must match swarm_runner.rs.
const MAX_TICKS: usize = 2_000_000;
const N_SYMBOLS: usize = 4;
fn skip_if_no_data() -> bool {
!std::path::Path::new(DEFAULT_DATA_PATH).exists()
}
/// Independent host reference: same symbols, same order, same cap,
/// pure-Rust fold (`acc += mid; n += 1`). Bit-exact with the kernel
/// fold because the addition order is identical.
fn reference(symbol: &str) -> (f64, i64) {
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
let mut it = server.stream_tick(symbol).expect("tick stream");
let (mut acc, mut n) = (0.0_f64, 0_i64);
'outer: while let Some(chunk) = it.next_chunk() {
for r in chunk.iter() {
if n as usize >= MAX_TICKS {
break 'outer;
}
acc += (r.ask + r.bid) / 2.0;
n += 1;
}
}
(acc, n)
}
#[test]
fn symbol_fan_swarm_leak_free_and_bit_exact() {
if skip_if_no_data() {
eprintln!("skipping: {DEFAULT_DATA_PATH} absent (mirrors data-server)");
return;
}
let out = Command::new(env!("CARGO_BIN_EXE_swarm_runner"))
.arg(DEFAULT_DATA_PATH)
.env("AILANG_RC_STATS", "1")
.output()
.expect("spawn swarm_runner");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"swarm_runner exited {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
out.status.code()
);
// Per-symbol: kernel (acc,n) bit-exact vs independent reference.
let mut symbols_checked = 0usize;
for line in stdout.lines().filter(|l| l.starts_with("RESULT ")) {
let mut t = line.split_whitespace();
let _ = t.next(); // "RESULT"
let sym = t.next().expect("symbol field");
let acc_bits = u64::from_str_radix(
t.next().expect("acc-bits field"), 16,
).expect("acc-bits hex");
let n: i64 = t.next().expect("n field").parse().expect("n int");
let kernel_acc = f64::from_bits(acc_bits);
let (ref_acc, ref_n) = reference(sym);
assert_eq!(
kernel_acc.to_bits(), ref_acc.to_bits(),
"{sym}: kernel acc bit-exact vs same-order host reference"
);
assert_eq!(n, ref_n, "{sym}: tick count matches reference");
assert!(ref_n > 0, "{sym}: non-empty stream");
symbols_checked += 1;
}
assert!(
(2..=N_SYMBOLS).contains(&symbols_checked),
"expected 2..={N_SYMBOLS} symbols fanned, got {symbols_checked}"
);
// Global leak-freedom: Σallocs == Σfrees across ALL stat lines
// (the swarm_runner child has exited, so its atexit g_rc line +
// every per-ctx ailang_ctx_free readback are present). Ported
// from embed_tick_e2e.rs:94-104; the invariant is the global Σ,
// not per-line balance (M2 TLS-ctx cross-attribution).
let mut allocs: u64 = 0;
let mut frees: u64 = 0;
let mut seen = 0usize;
for line in stderr.lines().filter(|l| l.starts_with("ailang_rc_stats:")) {
seen += 1;
for tok in line.split_whitespace() {
if let Some(v) = tok.strip_prefix("allocs=") {
allocs += v.parse::<u64>().expect("allocs= u64");
} else if let Some(v) = tok.strip_prefix("frees=") {
frees += v.parse::<u64>().expect("frees= u64");
}
}
}
assert!(seen > 0, "no ailang_rc_stats line; stderr:\n{stderr}");
assert_eq!(
allocs, frees,
"globally leak-free: Σallocs==Σfrees across {seen} stat line(s)"
);
}
```
- [ ] **Step 2: Run the swarm E2E**
Run: `cargo test --manifest-path ail-embed/Cargo.toml --test swarm symbol_fan_swarm_leak_free_and_bit_exact -- --nocapture`
Expected (real data present on this machine): PASS — `test
symbol_fan_swarm_leak_free_and_bit_exact ... ok`, `1 passed;
0 failed`. (~seconds: a few million per-tick FFI calls × N symbols.)
If `/mnt/tickdata/Pepperstone` were absent it would print the
skip line and still pass — but it IS present here, so it must
actually run and assert.
- [ ] **Step 3: Full crate suite green**
Run: `cargo test --manifest-path ail-embed/Cargo.toml`
Expected: PASS — lib (`kernel_run_sums_prices`,
`adapter::tests::tick_to_px_is_mid`), `smoke`
(`hermetic_smoke_data_server_roundtrip`), `swarm`
(`symbol_fan_swarm_leak_free_and_bit_exact`) all `ok`; `0 failed`
across all four test binaries.
- [ ] **Step 4: Invariant 1 — AILang workspace graph still data-server-free**
Run: `cargo metadata --format-version 1 --manifest-path Cargo.toml --no-deps 2>/dev/null | grep -c data-server | cat`
Expected: `0` — promoting `data-server` to a real dep of `ail-embed`
does NOT enter the AILang workspace graph (`ail-embed` is its own
workspace root via the empty `[workspace]` table). Invariant 1 holds.
- [ ] **Step 5: No compiler-surface diff**
Run: `git status --porcelain | grep -E '^\?\?|^ ?M' | grep -vE '^.. (ail-embed/|docs/|bench/orchestrator-stats/)'`
Expected: empty output — m5.2 touches **only** `ail-embed/**`,
`docs/`, and the stats file. **Zero** diff to `crates/ailang-*`,
`crates/ail/`, `runtime/`, `examples/*.ail`, and (unlike m5.1) not
even the root `Cargo.toml`. The no-language-change guarantee.
---
## Boss decisions (recorded for the audit)
- **TSan: no new harness; the property is compile-time-enforced +
already green-tested.** The spec's "sanitiser-clean (no data race
across per-thread ctxs)" acceptance is discharged by two facts,
not a new nightly-Rust-TSan binary: (1) `Ctx`/`Kernel` are `!Send`
(raw `*mut c_void`), so the swarm is *structurally forced* into
one-ctx-per-thread — a shared-ctx design is `error[E0277]` and
cannot compile (Task 2 Step 2 is therefore a compile-time proof);
(2) the C-runtime race-freedom of that exact per-thread-ctx
pattern is ratified green by `crates/ail/tests/embed_swarm_tsan.rs`
(C host, 8 pthreads, own ctx each, `clang -fsanitize=thread`,
exit-clean). m5.2 introduces no new AILang-side concurrency
primitive (data-server's internal cache concurrency is
data-server's own concern, not an AILang invariant). Re-proving
with `-Z sanitizer=thread` would add a nightly-toolchain
dependency to `ail-embed` for zero additional AILang coverage —
speculative infra by the project's own criterion. The architect
may re-examine this at milestone close.
- **`data-server` dev-dep → real dep is sanctioned.** Invariant 1
governs the *compiler* crates (`ailang-*`, `crates/ail`,
`runtime/`), which gain nothing. `ail-embed` is explicitly the
*sole* data-server↔AILang meeting point; the Invariant-1 gate is
therefore "AILang workspace graph has zero data-server" (Step 4) +
"zero compiler-surface diff" (Step 5), NOT "ail-embed has zero
deps" (which only held for m5.1's pre-adapter state).
- **Deferred to m5.3** (not this plan): the time-shard
boundary-invisibility proof (spec §3 time-shard / Testing §3) and
the friction-harvest timing measurement + milestone close-out.
## Self-review (planner Step 5)
1. **Spec coverage.** §2 adapter → Task 1 (`adapter.rs`:
`tick_to_px`/`MidPriceStream`/`fold_symbol`). §3 symbol-fan →
Task 2 (`swarm_runner`). Testing §2 (symbol-fan, skip-if-absent)
→ Task 3. Leak-freedom (`AILANG_RC_STATS` Σ) → Task 3 Step 1
stat-parse + Step 2. Sanitiser/per-ctx-race → Boss decisions
(compile-time `!Send` + the cited green `embed_swarm_tsan.rs`),
verified by Task 2 Step 2's compile gate. Invariant 1 →
Task 3 Steps 4-5. Time-shard / friction explicitly deferred to
m5.3 — not a coverage gap for *this* iteration.
2. **Placeholder scan.** No "TBD/TODO/implement later/similar
to/add appropriate". Constants `MAX_TICKS`/`N_SYMBOLS` are
concrete values, duplicated verbatim in bin and test (Task 2/3),
not placeholders.
3. **Type/name consistency.** `adapter`, `tick_to_px`,
`MidPriceStream`, `MidPriceStream::new`, `fold_symbol`, `Kernel`,
`swarm_runner`, `CARGO_BIN_EXE_swarm_runner`, `RESULT` line
format (`RESULT <sym> {:016x} <n>` written in Task 2 / parsed in
Task 3), `MAX_TICKS=2_000_000`, `N_SYMBOLS=4`,
`symbol_fan_swarm_leak_free_and_bit_exact`, `tick_to_px_is_mid`
— identical across tasks and verification filters.
4. **Step granularity.** Each step is one file write or one
command; all 2-5 min. Largest paste (Task 3 Step 1) is a single
verbatim test file.
5. **No commit steps.** None. Work stays in the working tree.
6. **Pin/replacement substring contiguity.** The two verbatim
replacements (Task 1 Step 2: the `use std::ffi::c_void;` line and
the `[dependencies]`/`[dev-dependencies]` block) are not paired
with any `contains(...)`/grep over their own body. The greps
(Task 3 Steps 4-5) assert over `cargo metadata`/`git status`
output, not over a verbatim text body this plan writes — no
contiguity hazard.
7. **Compile-gate vs deferred-caller.** No signature change to an
existing function with callers. The only existing-file edits are
additive (`pub mod adapter;`; Cargo dep move). Task 1 Step 6's
crate build gate is satisfied within Task 1 (adapter complete).
No later task is required to make an earlier compile gate pass.
8. **Verification filter strings resolve.** `tick_to_px_is_mid`,
`--bin swarm_runner`, `--test swarm`,
`symbol_fan_swarm_leak_free_and_bit_exact` are all defined *by
this plan* in the same tasks that filter on them — resolve by
construction. Task 1 Step 6 / Task 3 Step 3 use the unfiltered
suite with explicit `0 failed` assertions; Steps 4-5 assert an
explicit `grep -c … == 0` / empty-output — "nothing ran" cannot
masquerade as success.