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

14 KiB

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.