swactor/crates/simulation/BLOCKED.md

389 lines
17 KiB
Markdown
Raw Normal View History

2026-05-21 19:48:02 +00:00
# Simulator BLOCKED — Stage 6 / §10.2 / §4.4 contracts unfulfilled
This file is filed per TESTING_SPEC §14. The local v1 parity bar is
green for the surface this implementation chose to bind to, but
three load-bearing contracts named by the plan and TESTING_SPEC have
*not* been satisfied at full scope. The remaining work is not a v1
"out-of-scope" item under TESTING_SPEC §15, so the only honest
artifact to ship is this BLOCKED.md plus the qualified `BUILD_REPORT.md`
next to it.
Iteration 14 fixed one of the four findings (Finding D — facade
surface descriptor now derived from trait declarations by
`build.rs`); the other three remain open.
**Note on layout (added in path-refresh pass):** since this file was
first filed, the workspace consolidated several crates
(`runtime-facade`, `sim-detector`, `sim-driver`, `lint-deterministic`)
into `crates/simulation` (PRs #39, #50, #52). Paths below reflect the
post-consolidation layout. The underlying contracts and the substantive
gaps they call out are unchanged.
| Symbol | Was | Now |
|-------------------------|-------------------------------------------|----------------------------------------------------|
| Runtime facade traits | `crates/runtime-facade/src/lib.rs` | `crates/simulation/src/runtime/mod.rs` |
| Surface-lock build | `crates/runtime-facade/build.rs` | `crates/simulation/build.rs` |
| Surface fingerprint | `crates/runtime-facade/surface.lock` | `crates/simulation/surface.lock` |
| Lint config | `crates/lint-deterministic/banned.toml` | `crates/simulation/banned.toml` |
| Lint scanner | `crates/lint-deterministic/src/lib.rs` | `crates/simulation/src/lint.rs` |
| Detector probes | `crates/sim-detector/src/lib.rs` | `crates/simulation/src/detector.rs` |
| Sim binary entry | `crates/sim-driver/src/main.rs` | `crates/simulation/src/bin/sim-driver.rs` |
---
## A. plan §"Stage 6 — TIER CHECKPOINT" — distribution migration
### Contract
> Migrate `crates/distribution` to the facade: every direct
> `Instant`, `SystemTime`, `tokio::spawn`, `tokio::time::sleep`,
> `rand::thread_rng`, `HashMap` iteration, `env::var`, `std::fs`,
> `std::net` use is rewritten against the runtime facade. Lint scope
> widens to include distribution; scan reports zero violations.
### What's actually happening
The lint scanner's audit scope (`crates/simulation/banned.toml::
[scope]::include`) is exactly one directory:
```
include = [
"crates/simulation/src",
]
```
`crates/distribution` is not in scope. The crate still calls the
real runtime directly across at least these files:
- `crates/distribution/src/swim/member_list.rs`
- `crates/distribution/src/diagnostics/aggregator.rs`
- `crates/distribution/src/diagnostics/sink.rs`
- `crates/distribution/src/diagnostics/probes.rs`
- `crates/distribution/src/diagnostics/host_introspect.rs`
- `crates/distribution/src/diagnostics/collector/udp_echo.rs`
- `crates/distribution/src/diagnostics/collector/state.rs`
- `crates/distribution/src/diagnostics/collector/handlers.rs`
- `crates/distribution/src/cache.rs`
- `crates/distribution/src/iroh_driver.rs`
- `crates/distribution/src/peer_auth.rs`
- `crates/distribution/src/node_metadata.rs`
- `crates/distribution/src/kademlia/repair.rs`
- `crates/distribution/src/kademlia/directory.rs`
These call sites use `tokio::spawn`, `tokio::time::sleep`,
`std::time::Instant`, `std::time::SystemTime`, `rand::thread_rng`,
`std::net::UdpSocket`, and `std::collections::HashMap` directly.
Live-fire reproduces the gap: drop a `use std::time::SystemTime;`
into `crates/distribution/src/_violator_test.rs`; the lint scan
still exits 0, because that directory is not in scope.
### The problem, plainly
The whole point of the runtime facade is so that peer code becomes a
pure function of `(spec, seed)` — same inputs produce byte-identical
bundle outputs, in prod and in sim. Every direct `Instant::now()`,
`tokio::spawn`, or `HashMap` iteration in `distribution` breaks that
guarantee on the production diagnostics path: the diagnostics
aggregator's per-peer state lives in a `HashMap`, so its iteration
order varies run-to-run with hash randomisation. A replay can't
reproduce a recorded run if a `HashMap::iter` shows up between input
and output.
Until distribution is migrated, the "sim and prod are the same code
with a different runtime" claim only holds for the simulation crate
itself. The peer code that actually does the work is exempt.
### Potential action items
The migration is big and has a forced order. A reasonable plan:
1. **Grow the facade `Spawn` surface to be async-capable.**
The current trait (`crates/simulation/src/runtime/mod.rs:91`)
only takes `Box<dyn FnOnce() + Send + 'static>`. Distribution's
diagnostics pipeline is built on `tokio::spawn(async move {...})`.
Add `fn spawn_future(&self, fut: BoxFuture<'static, ()>)` (or
equivalent), implement on `runtime::prod` with `tokio::spawn`
and on `runtime::sim` against the engine's fiber queue.
This is a `surface.lock` bump.
2. **Decide on the `HashMap` policy and apply it crate-wide.**
Replace prod-path `HashMap` / `HashSet` with `IndexMap` /
`IndexSet` (preserves insertion order, fast) or `BTreeMap` /
`BTreeSet` (preserves sort order, slower but no extra dep).
Aggregator, member-list, and kademlia routing-table state are
the highest-leverage call sites — diagnostics aggregator first
because it's the largest source of replay divergence.
3. **Migrate the call sites in waves.** Suggested order, smallest
blast radius first:
- `cache.rs` and `node_metadata.rs` (mostly time reads).
- `peer_auth.rs` (time + RNG).
- `swim/member_list.rs` (spawn + time + map iteration).
- `kademlia/` (spawn + time + map iteration).
- `diagnostics/*` (spawn + time + UDP + map iteration — biggest).
- `iroh_driver.rs` (depends on Finding C's facade growth — defer).
4. **Widen lint scope.** Add `"crates/distribution/src"` to
`banned.toml::[scope]::include` and fix the violations the
scanner reports until it exits 0. Do this *last*, after the
migration waves above land — otherwise the scanner blocks every
intermediate commit.
5. **Add a live-fire regression test.** After (4) is green, drop a
throwaway `use std::time::SystemTime;` somewhere in
`distribution/src/` and confirm the scanner catches it; revert.
Record the result in the iteration notes.
This is a multi-iteration project. Budget at least three.
---
## B. TESTING_SPEC §10.2 — detector must run on the sim facade
### Contract
> The detector binary must run inside the reference scenario as an
> additional peer.
### What's actually happening
`crates/simulation/src/facade/sim/bundle.rs:81` calls
`crate::detector::run_all()` synchronously from inside the bundle
writer. The detector code lives at
`crates/simulation/src/detector.rs` and every D01–D12 probe body
reaches for the real `std::*` / `tokio` surface
(`SystemTime::now()`, `Instant::now()`, `std::thread::spawn`,
`std::net::UdpSocket`, `std::net::TcpStream`,
`File::open("/dev/urandom")`, `gethostname`, `pid`, allocator
addresses, `ThreadId`). Each violation is silenced with an inline
`// lint-deterministic: allow …` marker.
The detector is therefore observing the host runtime, not the sim
runtime. `techniques.md` admits this verbatim for D01 ("The v1 sim
does not intercept `std::time::*`; the detector running in-process
therefore reads the host wall clock") and D02 ("v1 reads the host
directly, so the two deltas are microseconds apart in practice").
`t_detector::sim_indistinguishable` consequently passes whether or
not the sim leaks — there is no sim facade between the probe and
the host to leak through.
### The problem, plainly
NORTH_STAR's load-bearing claim is "no means, statistical or
otherwise, of detecting that it is in a sim." The test that's
supposed to enforce that claim is currently a no-op: the probes are
literally not pointed at the simulated runtime. It's the same as
running an integrity test against `cat` and concluding the data is
intact — there's no system under test.
The `lint-deterministic: allow` markers are load-bearing audit
evidence that this work was not done. Removing them without doing
the work would just move the failure from a semantic check to a
mechanical one.
### Potential action items
Two changes have to land together; doing only one of them is worse
than doing neither (it advertises a guarantee that isn't there).
1. **Make the sim backend actually intercept the std/tokio surface.**
`crates/simulation/src/runtime/sim.rs` (133 lines) is mostly
stubs. Concretely:
- `Clock::now()` must return a virtual-time-derived
`SystemTime`, not pass through to `SystemTime::now()`.
- `Udp::bind` must return a socket whose `send_to` /
`recv_from` ride the engine's link graph instead of OS
sockets.
- `Spawn` must enqueue onto the engine's fiber scheduler
instead of `tokio::spawn`.
- `ProcessMeta::hostname` / `pid` must return per-node sim
values, not the host's.
2. **Route the detector probes through the facade.** Open
`crates/simulation/src/detector.rs` and, for each probe:
- Replace `SystemTime::now()` with `facade.clock().now()`.
- Replace `Instant::now()` with `facade.clock().monotonic()`.
- Replace `std::thread::spawn` with `facade.spawn().spawn(...)`.
- Replace `std::net::UdpSocket::bind` with
`facade.udp().bind(...)`.
- Drop the `// lint-deterministic: allow …` markers as each
site converts. By the end the file should be allow-marker-free.
3. **Run the detector as a real peer.** Add a `host` of kind
`detector` to the reference scenario spec. In the bundle writer
(`crates/simulation/src/facade/sim/bundle.rs:79–88`), delete the
in-process `detector::run_all()` call. Instead, have the engine
schedule a fiber on that host that runs the same probes via the
sim facade and writes its verdict stream into the bundle just
like any other host's records.
4. **Tighten the test.** Once the above is in, modify the sim
facade in a controlled way to leak (e.g., have `Clock::now()`
return wall-clock instead of virtual). The relevant probe
verdict should flip from `Indistinguishable` to `DetectedSim`
and `t_detector::sim_indistinguishable` should fail.
This depends on Finding A only for the `Spawn` async surface
growth, so it can begin in parallel with A's migration waves.
---
## C. plan §"Stage 6 — TIER CHECKPOINT" — real transports on the sim facade
### Contract
> Real `quinn` linked against sim UDP — no QUIC fork.
> Real iroh stack (MagicSock, discovery, NodeMap, connection cache)
> linked against sim facade — no iroh fork.
And TESTING_SPEC §5.2:
> The same `quinn` / `iroh` / `iroh-relay` / `distribution` /
> `swactor` source files run in both binaries.
### What's actually happening
`crates/simulation/src/engine.rs` (885 lines) is a discrete-event
record synthesiser. Its imports are `std::cmp::Ordering`,
`std::collections::{BTreeMap, BTreeSet, BinaryHeap}`,
`serde::{Deserialize, Serialize}`, and types from `crate::spec`.
`grep -n 'quinn\|iroh\|distribution\|swactor'
crates/simulation/src/engine.rs` returns zero matches.
The engine emits `HostStart` / `HostStop` / `HostCrash` /
`Snapshot` / `Mutation` events on a synthetic `loopback` host and
writes them to the bundle. The corpus is shaped to look like real
peer-code output, but no production peer code is in the call
graph.
(Update from the original BLOCKED.md: the `sim-driver` binary used
to pin `#[used] static` references to defeat dead-code stripping —
the `t_same_binary::symbol_overlap` test exists to detect that
gimmick. The pin gimmick was removed during the consolidation;
`symbol_overlap` is now listed as removed in
`tests/parity-bar/expected_failures.txt:33`. The deeper claim is
unchanged: peer code is not exercised through the sim engine.)
### The problem, plainly
§5.2's whole point is that there should be one implementation of
QUIC, one implementation of iroh, one implementation of
distribution — and the *only* difference between prod and sim is
which backend implements the runtime facade. If the sim binary
runs synthesised events that mimic peer-code output, then the sim
is not testing peer code — it's testing the simulator's model of
peer code, which is exactly the thing the parity bar is supposed
to make unnecessary.
This is why the dependency chain matters: real `quinn` needs an
async UDP socket, real `iroh` needs `quinn` plus a tokio runtime
and DNS, real `distribution` needs `iroh::Endpoint`. Each layer
sits on the one below it.
### Potential action items
This is the largest body of work in the BLOCKED set. A reasonable
sequencing:
1. **Async UDP on the facade.** Grow `traits::Udp` /
`traits::UdpSocket` (`crates/simulation/src/runtime/mod.rs:64–
73`) from the current sync `bind / send_to / recv_from` shape to
something that implements `quinn::AsyncUdpSocket`. In prod this
wraps `tokio::net::UdpSocket`; in sim it wraps a queue tied to
the engine's link graph and clock. `surface.lock` bump.
2. **Virtual `tokio::time::Instant` projection.** iroh's internals
call `tokio::time::Instant::now()` and `tokio::time::sleep`
directly (it does not consult our facade). Either (a) build a
sim-tokio runtime so `tokio::time` reads from the engine's
virtual clock when iroh is linked into the sim binary, or
(b) accept that iroh-as-shipped can't run under the sim and
patch iroh upstream. (a) is the path TESTING_SPEC §5.2 implies.
3. **Async `Spawn`.** Same growth as Finding A (1); A and C share
this prerequisite.
4. **DNS table in sim.** `traits::Dns`
(`crates/simulation/src/runtime/mod.rs:85`) needs to answer
relay-URL queries from a per-node DNS table the spec configures,
not from the host resolver. iroh's relay discovery won't work
otherwise.
5. **Build a `quinn::Endpoint` from facade pieces.** Once (1) and
(2) are in, construct a `quinn::Endpoint` from the facade's UDP
socket and feed it to iroh's `MagicSock` setup.
6. **Re-point `distribution`.** Wire `distribution`'s transport
construction off direct `iroh::Endpoint::builder()` calls and
onto the facade-built endpoint. (Finding A's migration is a
precondition.)
7. **Replace synthetic engine emission with real peer-code
execution.** This is the big one. `engine.rs` currently *is* the
event source; under (6), real peer code becomes the event source
and the engine becomes scheduling + link graph + clock. Most of
`engine.rs`'s synthetic emit logic deletes; the schema-floor
parity bars then bind to events real code produced.
8. **Re-enable the parity-bar checks currently in
`expected_failures.txt`.** `t_replay::*`, the §6/§7 schema
coverage suite, and `t_same_binary::shared_load_bearing` are
parked behind Stage 6/7. They light up as the real-peer-code
path comes online.
Expect this to span four to six iterations. Don't try to land it as
one PR.
---
## D. TESTING_SPEC §4.4 — facade surface lock — *fixed in iteration 14*
### Contract
> Each trait's method signatures hashed in declaration order.
### Resolution
Was: `SURFACE_DESCRIPTOR = include_str!("surface_descriptor.txt")`,
a 38-line hand-edited file. A trait edit that did not also touch
the text file passed the lock unchanged.
Now: `crates/simulation/build.rs` parses
`crates/simulation/src/runtime/mod.rs` with `syn` at build time,
walks `pub mod traits`, emits one line per trait method signature
into `$OUT_DIR/surface_descriptor.txt`, and the runtime module
embeds the build output via `include_str!`. Live-fire verified:
adding `fn stealth_method(&self) -> u32` to `pub trait Clock` flips
the descriptor hash and fails `live_fingerprint_matches_locked`.
Reverted.
This finding is closed. Listed here for the record because the
other three are open against the same TIER CHECKPOINT.
---
## What the next agent should do
Either:
1. Pick up the chain at A → B → C in order. A is the prerequisite
for the lint widening; A's facade growth (async `Spawn`) and B's
sim-facade interception together unlock C's real-transport
linkage. Plan on multiple tier-checkpoint-sized iterations; this
is not a single-iteration cleanup.
2. Raise a TESTING_SPEC defect under §14.2 if a specific check is
itself wrong — e.g., if §10.2's "additional peer" wording proves
impractical given the workspace constraint `no-modify src/` and
the facade should instead intercept at the bundle-writer
boundary. A defect would need to quote NORTH_STAR / SPEC /
OBSERVABILITY and ship as a separate commit titled
`TESTING_SPEC: correct §N.M` per §14.
What the next agent should *not* do is widen the
`lint-deterministic: allow` markers, leave the engine wholly
synthetic, or paper over symbol-presence checks. The bar in this
repo is what the spec says.