diff --git a/CLAUDE.md b/CLAUDE.md
index 8e6a57bf..bcd52a12 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -26,11 +26,24 @@ cargo fmt --all
### Crate Dependency Graph
```
-nexus-store --> nexus (kernel)
-nexus-fjall --> nexus-store
-nexus-macros <-- nexus (kernel, optional via "derive" feature)
+nexus-store --> nexus (kernel)
+nexus-wake --> nexus-store (in-process wake registry; owns tokio/foldhash/parking_lot)
+nexus-inmemory --> nexus-store, nexus-wake (in-memory adapter; nexus-store's test fixture via dev-dep cycle)
+nexus-fjall --> nexus-store, nexus-wake
+nexus-postgres --> nexus-store, nexus-wake
+nexus-macros <-- nexus (kernel, optional via "derive" feature)
```
+`nexus-store` itself has **no** tokio/foldhash/parking_lot dependency (#300): the
+subscription loop is generic over the `WakeSource` trait, and the tokio-backed
+in-process impl (`StreamNotifiers`) lives in `nexus-wake`. `nexus-store`'s own
+tests reach `InMemoryStore` through a **path-only dev-dependency on
+`nexus-inmemory`** (a legal dev-dep cycle) — which unifies types **only for
+integration tests in `tests/`**; the lib-test target recompiles the crate under
+`cfg(test)` as a distinct crate, so inline white-box test mods that need a store
+(`catchup.rs`, `subscription_cursor.rs`) use the in-crate `test_support.rs`
+double instead.
+
### Kernel Crate (`nexus`) — Flat Module Layout
- **`aggregate.rs`** — `Aggregate` trait (binds State + Error + Id), implemented on a **bare marker** type (a unit struct, never instantiated — state lives in `AggregateRoot`). `AggregateRoot` (read-only state container with version tracking) exposes two public driver methods: `replay(version, &event)` (rehydration in strict version order, also the supported path for manual / no-store event sourcing) and `commit_persisted(version, &events)` (the **single** post-persist sync — advances the version *and* folds the events into state atomically, so the version-ahead-of-state desync is unrepresentable by construction; #212). The lower-level `advance_version`/`apply_events`/`apply_event` are now **private in-crate helpers** (`commit_persisted` and the `testing` fixtures call them; `pub(crate)` for `apply_events`, fully private for the other two), no longer `#[doc(hidden)] pub` — complying with rule 4 ("`#[doc(hidden)]` is not access control") before the 1.0 freeze. `AggregateRoot` also carries the inherent `handle(&self, cmd)` **dispatch** that forwards to `A::handle(self.state(), cmd)`, so a loaded `AggregateRoot` is directly decidable. `AggregateState` trait (`initial()`, `apply(self, &Event) -> Self`; requires `Clone` for panic safety). `Handle` trait (per-command **decide** function, `handle(state: &State, cmd) -> Events`) — a pure function of `(state, command)`, implemented **on the marker**, with no access to version/identity (a decision never branches on persistence position); supertrait is `Aggregate`. There is **no entity newtype** and **no `AggregateEntity`/`from_root`** — `Handle` on the local marker coheres even for foreign command types, and the seam between "load returns `AggregateRoot`" and "decide" is dissolved by the dispatch method. Configurable limit: `MAX_REHYDRATION_EVENTS` (default 1M, `NonZeroUsize`). One `Handle` per command type (two impls differing only in `N` for the same `C` are ambiguous at the dispatch call site).
@@ -69,13 +82,13 @@ Flat layout — one file per concept, no module subdirectories. The earlier `cod
- `RawEventStore` trait: byte-level `append`, `read_stream`, and `read_all`, plus the provided `into_store(self) -> Store` (#244 — de-nests `Store::new`). `append` stamps each event with the next `GlobalSeq`. `read_stream` returns the adapter's concrete owned `Stream` associated type (a bounded per-stream scan from a `Version`); `read_all` returns the `AllStream` associated type (a bounded `$all` scan from a `GlobalSeq`). Both `from` bounds are **inclusive** — strict-after resume is the subscription loop's concern (`next_pos`), not the store's.
- `GlobalSeq`: store-local global sequence number stamped on every event at append time — the `Version` analogue across *all* of one producer's streams, and the position an all-streams subscription resumes from. **Monotonic but not gapless** — an aborted append may burn values, so consumers must tolerate gaps.
- **`batch.rs`** — `BatchSize` / `BatchSizeError`: a validated read/refill batch size carrying the `1..=MAX_BATCH` invariant by construction (`MAX_BATCH = 4096` is the compile-time ceiling on resident rows, `DEFAULT_BATCH = 256`), so an out-of-range value is unrepresentable past `BatchSize::new` and a builder that accepts a `BatchSize` cannot be handed an invalid one. Read paths never materialize more than `batch_size` event rows at once — the bound ties a config knob to a memory invariant. `BatchSizeError` is a `thiserror` struct carrying the rejected value and the ceiling.
-- **Subscription machinery (`subscription.rs` / `wake.rs` / `catchup.rs` / `subscription_cursor.rs` / `notify.rs`)** — feature-gated under `subscription`. The catch-up-then-live-tail loop lives **here in nexus-store as ONE generic, no-`Box` machine**, reusable by *any* adapter that implements `RawEventStore + WakeSource` — the postgres-readiness win (the loop was extracted from `nexus-fjall`, which had grown a bespoke per-adapter copy).
+- **Subscription machinery (`subscription.rs` / `wake.rs` / `catchup.rs` / `subscription_cursor.rs`)** — feature-gated under `subscription` (a **dep-free** gate since #300). The catch-up-then-live-tail loop lives **here in nexus-store as ONE generic, no-`Box` machine**, reusable by *any* adapter that implements `RawEventStore + WakeSource` — the postgres-readiness win (the loop was extracted from `nexus-fjall`, which had grown a bespoke per-adapter copy). The in-process wake impl (`StreamNotifiers`) lives in the separate `nexus-wake` crate.
- `subscription.rs` — `Subscription`, the user-facing **generic builder**. Built via `Subscription::new(&store)` from a `Store` handle (one `Arc` clone). `subscribe(&id, from)` / `subscribe_all(from)` are **synchronous and fallible**: they return `Result, ::Error>> + Send, ::Error>` (per-stream; `$all` yields `Step<(AllPosition, PersistedEnvelope)>`) — register failure surfaces eagerly as the `Err`, read failures stream in-band as `Err` items (two distinct error domains, rule 3). **The item is `Step`-tagged because the catch-up→live boundary is what makes a subscription a subscription** (#250): a finite read has no such boundary, so phase rides on `subscribe` itself rather than a separate `subscribe_typed`/`cursor` method — phase and decode are the two orthogonal axes, composed by combinators (see `decoded.rs`'s `StepStreamExt`), never welded into one method name. The cursor **never returns `None`** (parks when caught up). `from: None` = beginning; `from: Some(v)` = events *strictly after* `v` (strict-after resume; no duplicate on reopen). The stream is `!Unpin` (it is the `unfold` of the live loop), so consumers `pin!` it — the zero-cost (no-`Box`) tradeoff. There is **no adapter-facing subscription trait** — the old `RawSubscription` / `RawAllSubscription` + `sealed` module were deleted; an adapter need only implement `RawEventStore` + `WakeSource`.
- `step.rs` — `Step` = `Event(T) | CaughtUp` (#250), the frozen two-variant phase marker every subscription item carries. `CaughtUp` is emitted **exactly once** at the backlog→live boundary the `live_stepped` loop already detects; everything before it is replay, everything after is live. Exhaustive (non-error enum, no `#[non_exhaustive]`); no `FellBehind` (the loop doesn't distinguish a lagging live consumer — a consumer watches positions). `Step::map` transforms the payload and passes `CaughtUp` through, so the marker flows unbroken through `subscribe`'s tag-drop and `.decoded()`'s decode.
- `wake.rs` — adapter-pluggable wake, used only as a generic bound (never `dyn`). `WakeSource: Send + Sync + 'static` with `register(&self, stream: Option<&[u8]>) -> Result` (`None` = `$all`) and `wake(&self, stream)` (called *after* a durable commit). `WakeRegistration` with `arm(&self) -> impl Future