swactor/crates/simulation/BLOCKED.md

388 lines
17 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.

# 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.