Sprint 9: campaign lifecycle — retroactive achievement backfill and recurring timed events - #25
Merged
Merged
Conversation
…, recurring events) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…per-occurrence webhook ports
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:POST /v1/achievements/:id/backfill(sk-only) replays the stored event log against a definition in one idempotent transaction — progress raises via SQLGREATEST(never lowers; race-safe against live ingestion), unlocks via the existing unique index, and unlockpointsValuebonuses 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 return409 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.recurrence: none|daily|weekly|monthly+ optionalrecurrenceEndsAtas 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 anoccurrence_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 existingstartsAt/endsAtfields (existing countdown widgets work unchanged) plus additiverecurrence/nextOccurrenceStartsAt.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; recurringWeekly Happy Hourseed; new campaign-lifecycle e2e.Behavior notes for existing consumers
GET /v1/statsnow 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 withendsAt == frompreviously countedoccurredAt == fromevents (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).data.occurrence: { startsAt, endsAt };data.startsAt/endsAtremain the definition's values;messageId/HMAC contract unchanged. Redeliveries per occurrence follow the existing at-least-once semantics.backfill_in_progress(409).WebhookDeliveryStoreport signatures widened withoccurrenceKey— internal port, core is unpublished; noted in the changeset prose.recurrence(defaultnone) andnextOccurrenceStartsAt(nullable) — old SDK versions parse unchanged (defaults); new SDKs parse old servers unchanged.recurrencejson 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.''default + index swap); no data backfill; existing dev volumes needdocker compose --profile stack down -vfor the new seed event.Test plan
{usersEvaluated:5, progressRaised:5, unlocksGranted:1, pointsAwarded:40}, wallet 80→120); a 2-minute recurring event'slive/ending_soon/endedclaims visible per-occurrence-key inruntime.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