Skip to content

Sprint 9: campaign lifecycle — retroactive achievement backfill and recurring timed events - #25

Merged
hynding merged 12 commits into
mainfrom
sprint-9-campaign-lifecycle
Jul 10, 2026
Merged

Sprint 9: campaign lifecycle — retroactive achievement backfill and recurring timed events#25
hynding merged 12 commits into
mainfrom
sprint-9-campaign-lifecycle

Conversation

@hynding

@hynding hynding commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the Sprint 9 plan (docs/superpowers/plans/2026-07-09-sprint-9-campaign-lifecycle.md) — the final two campaign-engine v1.x slices. Definitions now reach backward and forward in time:

  • Retroactive achievement backfill: POST /v1/achievements/:id/backfill (sk-only) replays the stored event log against a definition in one idempotent transaction — progress raises via SQL GREATEST (never lowers; race-safe against live ingestion), unlocks via the existing unique index, and unlock pointsValue bonuses written with the exact live-path ledger shape and gated on the unlock insert, so two users with identical histories get identical wallets regardless of when the definition shipped. Concurrent backfills of one achievement return 409 backfill_in_progress (pg_try_advisory_xact_lock — no pool-connection pileup). Summary response: { usersEvaluated, progressRaised, unlocksGranted, pointsAwarded }; re-runs are zero-grant no-ops.
  • Recurring timed events: recurrence: none|daily|weekly|monthly + optional recurrenceEndsAt as virtual occurrences — pure O(1) window arithmetic in core (UTC-instant; monthly anchors to the original day-of-month with end-of-month clamping), zero new tables. Webhook lifecycle transitions fire per occurrence under an occurrence_key (migration 0008, additive, '' default keeps every existing claim row working); multipliers apply inside every occurrence; the live feed reports the current-or-next occurrence's window in the existing startsAt/endsAt fields (existing countdown widgets work unchanged) plus additive recurrence/nextOccurrenceStartsAt.
  • SDK: backfillAchievement(id) (secret-key posture); recurring live-event parsing with old-server back-compat defaults. Demo: backfill operator form (sk server action) beside the coupon checker; recurring Weekly Happy Hour seed; new campaign-lifecycle e2e.

Behavior notes for existing consumers

  • Stats behavior change: GET /v1/stats now includes a timed event only when a participation window intersects the queried range — previously out-of-range events appeared zero-filled. Also a boundary shift: a window with endsAt == from previously counted occurredAt == from events (inclusive BETWEEN); windows are now half-open [from, to), so that window is excluded. Recurring events aggregate participants distinct-per-event across all occurrence windows in range (capped at the 400 most recent; documented in OpenAPI).
  • Webhook payloads (additive): recurring transitions carry data.occurrence: { startsAt, endsAt }; data.startsAt/endsAt remain the definition's values; messageId/HMAC contract unchanged. Redeliveries per occurrence follow the existing at-least-once semantics.
  • New error code backfill_in_progress (409). WebhookDeliveryStore port signatures widened with occurrenceKey — internal port, core is unpublished; noted in the changeset prose.
  • Backfill moves wallets and leaderboards by design (retroactive unlock bonuses, ledger rows dated at grant time); documented in the README with the idempotence contract.
  • Live events (additive): recurrence (default none) and nextOccurrenceStartsAt (nullable) — old SDK versions parse unchanged (defaults); new SDKs parse old servers unchanged.
  • CMS: the reserved-unused recurrence json field (Sprint 3) is replaced by the typed enumeration — no data risk. The config-plane scan feed keeps still-recurring events visible with an end-side 28-day pad so bounded recurrences' final-occurrence webhooks can't scroll out early.
  • Migration 0008 is additive (column with '' default + index swap); no data backfill; existing dev volumes need docker compose --profile stack down -v for the new seed event.

Test plan

  • Full turbo 27/27 green post fix-wave (contracts 74, core 124, adapter-db 76 Testcontainers incl. true-retroactivity/live-ingest-race/try-lock-conflict, adapter-strapi 53, sdk 45, api 179, widgets 30)
  • Fresh-seeded compose stack: 7/7 Playwright specs green incl. the new campaign-lifecycle loop (recurrence live-feed assertions with response-relative +7d math; idempotent zero-grant backfill; demo form round-trip)
  • Hand-verified live: a definition created mid-flight backfilled real grants ({usersEvaluated:5, progressRaised:5, unlocksGranted:1, pointsAwarded:40}, wallet 80→120); a 2-minute recurring event's live/ending_soon/ended claims visible per-occurrence-key in runtime.timed_event_notifications; scan-feed cutoff boundary cases (2h-past included, 28d-past excluded)

Review notes

Nine tasks through the full gate cycle — all approved first-pass (a sprint first) — plus a final whole-branch review: verdict ready to merge after fix wave 38add4a (end-side scan-feed cutoff — the one real bug, dropping bounded recurrences' final-occurrence webhooks under normal operation; try-lock backfill conflict; doc notes). The occurrence math's boundary arithmetic (monthly original-day anchoring, strict cutoff, end-exclusivity) was hand-verified by reviewers at two gates. Follow-ups filed: #23 (recurrence scale hygiene: delivered-claims retention, per-tick cost, stats bind params), #24 (legacy-NULL scan-filter proof).

🤖 Generated with Claude Code

hynding and others added 12 commits July 9, 2026 12:17
…, recurring events)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… store, multi-window stats (migration 0008)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve feed and stats windows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…print 9 wrap

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ill conflict, docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hynding
hynding merged commit e4ed969 into main Jul 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant