c54f34ef08
The projection now sits on top of any IEventStore, not just SQLite. Stores publish a simple `inserts: SharedFlow<Event>` (one event per successful insert), and the projection itself replays the NIP rules against that stream: - NIP-01 supersession with the lexical-id tiebreaker, for both replaceables (kind 0/3/10000-19999) and addressables (30000-39999). Both now share a single in-place update path. - NIP-09 deletions, with the original-author check (cross-author kind-5s are inert, matching the store). - NIP-62 right-to-vanish, scoped by the projection's relay arg. - NIP-40 expiration via a per-projection ticker that drops slots whose expiration tag has lapsed. Out-of-band store mutations (`delete(id)`, `clearDB()`, the periodic `deleteExpiredEvents()` sweep) are no longer visible to projections — they're maintenance ops; projections re-seed when their scope is restarted. Removed from SQLiteEventStore: - ChangeLogModule + temp-table + AFTER DELETE trigger. - StoreChange sealed type and the per-mutation drain/publish path. Both SQLiteEventStore and FsEventStore now satisfy `IEventStore.inserts`. FsEventStore.insertLocked returns Boolean so no-op idempotent retries (canonical already on disk) don't re-publish. Tests: - EventStoreProjectionTest moves to `store/projection/`, gains four new cases: NIP-01 out-of-order rejection, NIP-09 cross-author inertness, NIP-62 cross-author no-op, NIP-40 ticker-driven removal. - 13/13 projection tests + 232/232 total store tests pass. https://claude.ai/code/session_01Jny85MTu1ynKgFBgysfWu5