Files
suaveplan/openspec/changes/event-sourcing-completion/tasks.md
2026-05-27 23:29:53 +01:00

118 lines
14 KiB
Markdown

# Tasks — Event-sourcing completion
> Every checkbox MUST reflect work actually done in this change. The seven capabilities closed here were silently dropped during the original genesis migration (audit dated 2026-05-02). Do not retroactively tick boxes for surface that already shipped.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/packages/universal/event-sourcing/src/{outbox,store,replay,subscription,snapshot,versioning,event-sourcing-events}.ts` end-to-end
- [ ] 1.2 Read `openspec/specs/event-sourcing/spec.md` (if it exists; create the cap dir if not)
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 0a (parity), 13 (spec-conformance), 14 (source mapping), 14a (dependents)
- [ ] 1.4 Draft `proposal.md` covering all seven gaps (`EventOutbox`, `DatabaseEventStore`, `DatabaseSnapshotStore`, `EventReplayer`, `EventBus + SubscriptionFilter`, `SnapshotManager`, `UpcasterChain`)
- [ ] 1.5 Fill `## Source mapping` in `proposal.md` with upstream line ranges for every requirement; every "no source line" requirement justified
- [ ] 1.6 Draft spec delta at `specs/event-sourcing/spec.md`
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
- [ ] 1.8 `openspec change validate event-sourcing-completion` exits with no errors
## 2. Implementation
- [ ] 2.1 `EventOutbox` interface + `MemoryEventOutbox` class — port from upstream `outbox/outbox-pattern.ts` into `packages/events/event-sourcing/src/outbox/event-outbox.ts` (interface) and `packages/events/event-sourcing/src/outbox/memory-outbox.ts` (impl)
- [ ] 2.2 `OutboxRelay` worker — extract relay loop into a standalone class with injectable `Clock` and `AbortSignal`; new file `packages/events/event-sourcing/src/outbox/relay.ts`
- [ ] 2.3 `DatabaseEventStore<TEvent>` — port from upstream `store/database-event-store.ts` into `packages/events/event-sourcing/src/store/database-event-store.ts`; replace `EventStoreDatabaseAdapter` with `DatabaseAdapter` from `@suaveplan/db`
- [ ] 2.4 `DatabaseSnapshotStore<TState>` — port from upstream `store/database-snapshot-store.ts` into `packages/events/event-sourcing/src/store/database-snapshot-store.ts`; add `serializer` / `deserializer` config hooks
- [ ] 2.5 `EventReplayer` — port from upstream `replay/event-replayer.ts` into `packages/events/event-sourcing/src/replay/event-replayer.ts`; thread `AbortSignal` through `replay`, `replayByType`, `replayEvents`
- [ ] 2.6 `EventBus` + `SubscriptionFilter` — port from upstream `subscription/event-subscription.ts` into `packages/events/event-sourcing/src/subscription/event-bus.ts`; replace module-scope counter with injectable `IdGenerator`
- [ ] 2.7 `DeadLetterSink` interface + `MemoryDeadLetterSink` — new file `packages/events/event-sourcing/src/subscription/dead-letter.ts`; capture handler exceptions during `EventBus.publish`
- [ ] 2.8 `SnapshotManager<TState>` — port from upstream `snapshot/snapshot-manager.ts` into `packages/events/event-sourcing/src/snapshot/snapshot-manager.ts`; replace `new Date()` with injectable `Clock`
- [ ] 2.9 `UpcasterChain` ordered registry — port from upstream `versioning/event-upcaster.ts` into `packages/events/event-sourcing/src/upcasting/chain.ts`
- [ ] 2.10 `detectSchemaVersion`, `createUpcastedEvent`, `createRenamedEvent` helpers — co-locate next to `UpcasterChain`
- [ ] 2.11 `createFieldRenameUpcaster`, `createDefaultFieldUpcaster`, `createRemoveFieldUpcaster` builders — co-locate next to `UpcasterChain`
- [ ] 2.12 Wire legacy `registerUpcaster` / `clearUpcasters` / `upcastEvents` to delegate to a module-scoped `UpcasterChain` so both APIs share one registry
- [ ] 2.13 Add `ES_*` codes (`OUTBOX_TABLE_INIT_FAILED`, `OUTBOX_PUBLISH_FAILED`, `OUTBOX_RELAY_INTERRUPTED`, `EVENT_INVALID`, `EVENT_HANDLER_FAILED`, `REPLAY_CANCELLED`, `REPLAY_FILTER_INVALID`, `SUBSCRIPTION_INVALID`, `DATABASE_TABLE_INIT_FAILED`, `SNAPSHOT_HANDLER_FAILED`, `UPCASTER_DUPLICATE`) to `errors/event-sourcing-error.ts`
- [ ] 2.14 Wire OTel counters / spans on every public entrypoint: `outbox.relayed`, `outbox.failed`, `outbox.pending_size`, `database.events_appended`, `database.snapshots_saved`, `replay.events_processed`, `replay.duration_ms`, `subscription.published`, `subscription.dead_lettered`, `snapshot.created`, `snapshot.pruned`, `upcaster.applied`, `upcaster.skipped`
- [ ] 2.15 Wire Zod schemas for `OutboxConfig`, `DatabaseEventStoreConfig`, `DatabaseSnapshotStoreConfig`, `ReplayFilter`, `ReplayConfig`, `SubscriptionOptions`, `SubscriptionFilter`, `SnapshotManagerConfig`, `EventUpcaster` to `schemas/event-sourcing.schemas.ts`
- [ ] 2.16 Update `index.ts` barrel — re-export every new symbol; no `export *` (one explicit subpath per module)
- [ ] 2.17 Update `package.json` `exports` map — add subpaths `./outbox/event-outbox`, `./outbox/memory-outbox`, `./outbox/relay`, `./store/database-event-store`, `./store/database-snapshot-store`, `./replay/event-replayer`, `./subscription/event-bus`, `./subscription/dead-letter`, `./snapshot/snapshot-manager`, `./upcasting/chain`
- [ ] 2.18 Update `package.json` `dependencies` — add `@suaveplan/db` (`workspace:^`)
## 3. Tests
- [ ] 3.1 `packages/events/event-sourcing/src/outbox/event-outbox.test.ts` — interface contract; `MemoryEventOutbox` insertion order; per-row attempts counter; `pendingCount` / `failedCount` / `cleanup`
- [ ] 3.2 `packages/events/event-sourcing/src/outbox/event-outbox.spec-conformance.test.ts` — Zod-rejected configs emit `OUTBOX_TABLE_INIT_FAILED`; injected `IdGenerator` deterministic; OTel counter `outbox.pending_size`
- [ ] 3.3 `packages/events/event-sourcing/src/outbox/relay.test.ts` — successful relay marks rows published; failure increments attempts; `AbortSignal` aborts in-flight loops with `OUTBOX_RELAY_INTERRUPTED`; injected `Clock` deterministic
- [ ] 3.4 `packages/events/event-sourcing/src/store/database-event-store.test.ts` — append + load round-trip; optimistic-concurrency conflict raises `CONCURRENCY_CONFLICT`; `getEventsByType` honours `limit`; `count()` returns total
- [ ] 3.5 `packages/events/event-sourcing/src/store/database-event-store.spec-conformance.test.ts` — table auto-create gated on `autoCreate: true`; `transaction()` rollback on adapter exception; emits `event:appended` lifecycle event
- [ ] 3.6 `packages/events/event-sourcing/src/store/database-snapshot-store.test.ts` — save / load latest-per-aggregate; configurable serializer round-trips a non-JSON state; `delete` returns `false` for unknown aggregate; `getInterval` returns config
- [ ] 3.7 `packages/events/event-sourcing/src/replay/event-replayer.test.ts` — full replay invokes handler in stream order; filter narrows by `eventTypes` / `aggregateId` / `fromDate` / `toDate`; pause / resume / cancel / reset state machine; `onProgress` final tick has `completed: true`; batch size honoured
- [ ] 3.8 `packages/events/event-sourcing/src/replay/event-replayer.spec-conformance.test.ts` — abort signal mid-batch rejects with `REPLAY_CANCELLED`; bad filter rejected with `REPLAY_FILTER_INVALID`; OTel histogram `replay.duration_ms`
- [ ] 3.9 `packages/events/event-sourcing/src/subscription/event-bus.test.ts` — `subscribe` / `publish` round-trip; filter narrows by `eventTypes` / `aggregateIds` / `aggregateType`; group round-robin distribution; `unsubscribe` removes record; `subscriberCount` reflects active subscriptions
- [ ] 3.10 `packages/events/event-sourcing/src/subscription/event-bus.spec-conformance.test.ts` — handler exceptions route to `DeadLetterSink` with `EVENT_HANDLER_FAILED` cause; injected `IdGenerator` produces deterministic subscription IDs; `clear()` resets `groupCounters`
- [ ] 3.11 `packages/events/event-sourcing/src/subscription/dead-letter.test.ts` — `MemoryDeadLetterSink` capture; iteration order; `clear()` resets; `getEntries()` snapshot is immutable
- [ ] 3.12 `packages/events/event-sourcing/src/snapshot/snapshot-manager.test.ts` — `load` rebuilds from snapshot + tail events; auto-create at `interval`; pruning at `maxSnapshots`; `shouldCreateSnapshot` predicate; `getSnapshotHistory` per aggregate
- [ ] 3.13 `packages/events/event-sourcing/src/snapshot/snapshot-manager.spec-conformance.test.ts` — injected `Clock` stamps `takenAt` deterministically; `aggregate:loaded` lifecycle event carries `restoredFromSnapshot`; reducer exception raises `SNAPSHOT_HANDLER_FAILED`
- [ ] 3.14 `packages/events/event-sourcing/src/upcasting/chain.test.ts` — `register` orders by `fromVersion`; `upcast` walks chain v1→v2→v3; `getUpcastersForType` filters; `clear()` empties registry; `hasUpcast` predicate
- [ ] 3.15 `packages/events/event-sourcing/src/upcasting/chain.spec-conformance.test.ts` — `detectSchemaVersion` defaults to `1`; duplicate `(eventType, fromVersion)` registration raises `UPCASTER_DUPLICATE`; `createFieldRenameUpcaster` / `createDefaultFieldUpcaster` / `createRemoveFieldUpcaster` round-trip
- [ ] 3.16 Cross-package `tests/dep-graph.test.ts` — `@suaveplan/event-sourcing` declares `@suaveplan/db` as a runtime dep; the package still has zero browser-only deps
- [ ] 3.17 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely-async DB-adapter integration)
## 4. Documentation
- [ ] 4.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference) — `outbox/event-outbox.md`, `outbox/memory-outbox.md`, `outbox/relay.md`, `store/database-event-store.md`, `store/database-snapshot-store.md`, `replay/event-replayer.md`, `subscription/event-bus.md`, `subscription/dead-letter.md`, `snapshot/snapshot-manager.md`, `upcasting/chain.md`
- [ ] 4.2 README links every co-located `.md` under `## Modules`
- [ ] 4.3 Update `CLAUDE.md` `event-sourcing` package listing — remove "only Aggregate / aggregate-repository / MemoryEventStore / Projection / flat upcaster" wording; describe the full restored surface
- [ ] 4.4 Refresh `STATUS.md` "Completed on genesis" entry for `event-sourcing` to reflect parity
## 5. Pre-archive verification (the 5-gate triple)
- [ ] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the monorepo
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/event-sourcing` — green
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/event-sourcing` — 100/100/100/100
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/event-sourcing` — exit 0, no `error` findings
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/event-sourcing` — green
- [ ] 5.6 Banned-pattern scan zero output: `grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" packages/events/event-sourcing/src/ --include="*.ts"`
- [ ] 5.7 Missing `.md` scan zero output (per AGENTS.md §13 step 5)
- [ ] 5.8 README check `test -f packages/events/event-sourcing/README.md`
- [ ] 5.9 Bare-version check zero output (per AGENTS.md §13 step 7)
- [ ] 5.10 Subpath count matches feature module count (per AGENTS.md §13 step 8)
## 6. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 6.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim (especially `DatabaseEventStore<TEvent>`, `DatabaseSnapshotStore<TState>`, `SnapshotManager<TState>`)
- [ ] 6.2 Zod schemas wired at every declared boundary (`OutboxConfig`, `DatabaseEventStoreConfig`, `DatabaseSnapshotStoreConfig`, `ReplayFilter`, `ReplayConfig`, `SubscriptionOptions`, `SubscriptionFilter`, `SnapshotManagerConfig`, `EventUpcaster`)
- [ ] 6.3 Every error code added in §2.13 thrown by at least one production code path; no orphans
- [ ] 6.4 Strict-mode behaviors tested both on and off (e.g. `autoCreate: true` vs `false`; `paused` vs running replay; `cancel()` vs `reset()`)
- [ ] 6.5 OTel counters / histograms emitted from production paths and asserted with a real in-memory `MeterProvider`
- [ ] 6.6 Lifecycle dedup tested where claimed (`SnapshotManager.shouldCreateSnapshot` idempotent on identical inputs; `EventBus.unsubscribe()` on already-unsubscribed record is a no-op)
- [ ] 6.7 No retroactive tick — every box above reflects work done in this change
## 7. Archive
- [ ] 7.1 Fold spec delta into `openspec/specs/event-sourcing/spec.md` (creating the cap file if missing)
- [ ] 7.2 Move `openspec/changes/event-sourcing-completion/` → `openspec/archive/<yyyy-mm-dd>-event-sourcing-completion/`
- [ ] 7.3 Refresh `MANIFEST.md` (dep set changed: `@suaveplan/event-sourcing` now depends on `@suaveplan/db`)
- [ ] 7.4 Update `STATUS.md` to mark the seven dropped capabilities as restored
## Audit Note (2026-05-02)
Audit verdict: **DEFERRED — 0/68 tasks evidenced in code**.
Evidence reviewed:
- `packages/events/event-sourcing/src/` contains `core/{aggregate,event-store,aggregate-repository}/`, `projection/builder/`, `upcasting/upcaster/`, `errors/`, `schemas/`, `types/`, `constants/`. No `outbox/`, no `replay/`, no `subscription/`, no `snapshot/`, no `store/database-*`.
- `package.json` does not declare `@suaveplan/db` as a runtime dep (the `DatabaseEventStore` and `DatabaseSnapshotStore` need it).
- No new `ES_*` error codes (`OUTBOX_*`, `REPLAY_*`, `SUBSCRIPTION_*`, `SNAPSHOT_HANDLER_FAILED`, `UPCASTER_DUPLICATE`, `EVENT_HANDLER_FAILED`, etc.) in `errors/event-sourcing-error.ts`.
- Existing `upcasting/upcaster/` is a single class — no `UpcasterChain` ordered registry, no `createFieldRenameUpcaster` / `createDefaultFieldUpcaster` / `createRemoveFieldUpcaster` builders.
Missing pieces:
1. Outbox subsystem (`EventOutbox` interface, `MemoryEventOutbox`, `OutboxRelay` worker with injectable `Clock` + `AbortSignal`).
2. `DatabaseEventStore<TEvent>` + `DatabaseSnapshotStore<TState>` (requires adding `@suaveplan/db` dep).
3. `EventReplayer` (with pause/resume/cancel/reset state machine) + `EventBus` with `SubscriptionFilter` + `MemoryDeadLetterSink` + `SnapshotManager<TState>` + `UpcasterChain` ordered registry.
Decision: leave change open. Seven dropped capabilities, ten new subpath exports, eleven new error codes, dep graph change — all blocked behind real implementation work.