style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
+21
-18
@@ -11,27 +11,27 @@ autonomous: true
|
||||
requirements: [PUSH-REMIND-RESILIENCE]
|
||||
must_haves:
|
||||
truths:
|
||||
- "A shared timed event whose start is in (now, now+16min] triggers a reminder push on the next scan, even if the cron tick at the ideal 15-min mark was missed/late (catch-up)."
|
||||
- "A given shared timed event fires EXACTLY ONCE across all scans while it sits in the catch-up window — no cross-tick double-fire."
|
||||
- "An event whose dtstart has already passed (dtstart <= now) does NOT trigger a reminder."
|
||||
- 'A shared timed event whose start is in (now, now+16min] triggers a reminder push on the next scan, even if the cron tick at the ideal 15-min mark was missed/late (catch-up).'
|
||||
- 'A given shared timed event fires EXACTLY ONCE across all scans while it sits in the catch-up window — no cross-tick double-fire.'
|
||||
- 'An event whose dtstart has already passed (dtstart <= now) does NOT trigger a reminder.'
|
||||
- "The notification body reflects the actual lead time (e.g. 'Starts in 8 min'), guarded to a minimum of 1."
|
||||
- "Existing Phase-5 behaviours stay green: isShared-only (D-05), allDay excluded (D-07), always-visible notification (D-11), empty push_subscriptions -> zero sends/no crash (D-16), per-event and per-sub error isolation (T-05-17/18/19)."
|
||||
- 'Existing Phase-5 behaviours stay green: isShared-only (D-05), allDay excluded (D-07), always-visible notification (D-11), empty push_subscriptions -> zero sends/no crash (D-16), per-event and per-sub error isolation (T-05-17/18/19).'
|
||||
artifacts:
|
||||
- path: "apps/api/src/broker/reminderScheduler.ts"
|
||||
provides: "Resilient catch-up reminder scan with per-uid exactly-once dedup, in-memory (D-12)."
|
||||
contains: "runReminderCheck"
|
||||
- path: "apps/api/tests/broker/reminderScheduler.test.ts"
|
||||
provides: "Updated tests covering single-fire across consecutive ticks, missed-tick recovery, already-started exclusion, plus retained Phase-5 behaviours."
|
||||
contains: "missed"
|
||||
- path: 'apps/api/src/broker/reminderScheduler.ts'
|
||||
provides: 'Resilient catch-up reminder scan with per-uid exactly-once dedup, in-memory (D-12).'
|
||||
contains: 'runReminderCheck'
|
||||
- path: 'apps/api/tests/broker/reminderScheduler.test.ts'
|
||||
provides: 'Updated tests covering single-fire across consecutive ticks, missed-tick recovery, already-started exclusion, plus retained Phase-5 behaviours.'
|
||||
contains: 'missed'
|
||||
key_links:
|
||||
- from: "apps/api/src/broker/reminderScheduler.ts"
|
||||
to: "calendarEvents.dtstartUtc"
|
||||
via: "WHERE gt(now) AND lte(now+16min)"
|
||||
- from: 'apps/api/src/broker/reminderScheduler.ts'
|
||||
to: 'calendarEvents.dtstartUtc'
|
||||
via: 'WHERE gt(now) AND lte(now+16min)'
|
||||
pattern: "gt\\(.*dtstartUtc"
|
||||
- from: "apps/api/src/broker/reminderScheduler.ts"
|
||||
to: "dispatchPush"
|
||||
via: "fan-out per subscription, then mark uid sent"
|
||||
pattern: "dispatchPush"
|
||||
- from: 'apps/api/src/broker/reminderScheduler.ts'
|
||||
to: 'dispatchPush'
|
||||
via: 'fan-out per subscription, then mark uid sent'
|
||||
pattern: 'dispatchPush'
|
||||
---
|
||||
|
||||
<objective>
|
||||
@@ -87,6 +87,7 @@ Output: Updated `reminderScheduler.ts` (in-memory only, no schema/deps/Redis per
|
||||
Comments / decisions to preserve and update: D-05, D-07, D-11, D-12, D-16, T-05-17, T-05-18, T-05-19, WR-01, CR-01 must remain referenced in the header/inline comments with their meanings updated for per-uid dedup and the catch-up window. Add a short inline CAVEAT comment near the dedup: if an event's dtstart is RESCHEDULED earlier after a reminder already fired, it will not re-fire — acceptable for v1.
|
||||
|
||||
Do NOT: add schema/migration changes, new dependencies, Redis, or DB persistence. Do NOT change pushDispatcher or its call signature. Do NOT place fenced code blocks anywhere — this is directive prose.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/api typecheck</automated>
|
||||
@@ -117,6 +118,7 @@ Output: Updated `reminderScheduler.ts` (in-memory only, no schema/deps/Redis per
|
||||
- Keep the all-day (D-07), non-shared (D-05) empty-result tests as-is. Update the file's top docblock to describe the catch-up `(now, now+16min]` window and per-uid exactly-once dedup instead of the old `[now+14,now+16]` / minuteBucket description.
|
||||
|
||||
Do NOT place fenced code blocks in this action prose. Do NOT introduce real DB or real push; keep everything mocked.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @familysync/api test</automated>
|
||||
@@ -133,13 +135,14 @@ Output: Updated `reminderScheduler.ts` (in-memory only, no schema/deps/Redis per
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- A shared timed event in `(now, now+16min]` fires a reminder on the next scan after a missed/late tick (catch-up) — Test 1 closed.
|
||||
- Each such event fires EXACTLY ONCE across consecutive ticks (cross-tick double-fire bug fixed).
|
||||
- Events with `dtstart <= now` never fire.
|
||||
- Notification body reflects actual lead time, guarded to >= 1 min.
|
||||
- All prior Phase-5 decisions/threat mitigations preserved (D-05/D-07/D-11/D-12/D-16, T-05-17/18/19, WR-01, CR-01).
|
||||
- In-memory only: no schema change, no new deps, no Redis/DB persistence. Only `reminderScheduler.ts` and its test touched.
|
||||
</success_criteria>
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/quick/260610-hbu-make-phase-5-reminder-scheduler-resilien/260610-hbu-SUMMARY.md` when done.
|
||||
|
||||
+14
-11
@@ -1,6 +1,6 @@
|
||||
---
|
||||
phase: quick-260610-hbu
|
||||
plan: "01"
|
||||
plan: '01'
|
||||
subsystem: api/broker
|
||||
tags: [push-notifications, reminder-scheduler, resilience, dedup]
|
||||
dependency_graph:
|
||||
@@ -16,13 +16,13 @@ key_files:
|
||||
- apps/api/src/broker/reminderScheduler.ts
|
||||
- apps/api/tests/broker/reminderScheduler.test.ts
|
||||
decisions:
|
||||
- "Window changed from [now+14min, now+16min] to (now, now+16min] for missed-tick catch-up"
|
||||
- "Dedup key changed from uid:minuteBucket (Set) to bare uid (Map<uid, dtstartMs>) for cross-tick exactly-once"
|
||||
- 'Window changed from [now+14min, now+16min] to (now, now+16min] for missed-tick catch-up'
|
||||
- 'Dedup key changed from uid:minuteBucket (Set) to bare uid (Map<uid, dtstartMs>) for cross-tick exactly-once'
|
||||
- "Body changed from hardcoded 'Starts in 15 min' to lead-accurate 'Starts in N min'"
|
||||
- "CR-01 pruning changed from minuteBucket-age to started-event (dtstartMs <= now)"
|
||||
- 'CR-01 pruning changed from minuteBucket-age to started-event (dtstartMs <= now)'
|
||||
metrics:
|
||||
duration: "~15min"
|
||||
completed: "2026-06-10"
|
||||
duration: '~15min'
|
||||
completed: '2026-06-10'
|
||||
tasks_completed: 2
|
||||
files_modified: 2
|
||||
---
|
||||
@@ -33,10 +33,10 @@ metrics:
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| Task | Name | Commit | Files |
|
||||
|------|------|--------|-------|
|
||||
| 1 | Catch-up window + per-uid dedup in reminderScheduler.ts | 3fdb242 | apps/api/src/broker/reminderScheduler.ts |
|
||||
| 2 | Update tests for catch-up + single-fire + missed-tick | 93bb2c1 | apps/api/tests/broker/reminderScheduler.test.ts |
|
||||
| Task | Name | Commit | Files |
|
||||
| ---- | ------------------------------------------------------- | ------- | ----------------------------------------------- |
|
||||
| 1 | Catch-up window + per-uid dedup in reminderScheduler.ts | 3fdb242 | apps/api/src/broker/reminderScheduler.ts |
|
||||
| 2 | Update tests for catch-up + single-fire + missed-tick | 93bb2c1 | apps/api/tests/broker/reminderScheduler.test.ts |
|
||||
|
||||
## Verification Output
|
||||
|
||||
@@ -90,13 +90,14 @@ grep -n "minuteBucket" apps/api/src/broker/reminderScheduler.ts
|
||||
|
||||
**Dispatch loop check:** `sentReminders.has(uid:minuteBucket)` replaced with `sentReminders.has(uid)`. WR-01 mark-after-dispatch preserved: `sentReminders.set(uid, dtstartMs)` after fan-out loop.
|
||||
|
||||
**Notification body:** Hardcoded `'Starts in 15 min'` replaced with `\`Starts in ${minutes} min\`` where `minutes = Math.max(1, Math.round((dtstartMs - now) / 60000))`.
|
||||
**Notification body:** Hardcoded `'Starts in 15 min'` replaced with `\`Starts in ${minutes} min\``where`minutes = Math.max(1, Math.round((dtstartMs - now) / 60000))`.
|
||||
|
||||
**CR-01 pruning:** Stale minuteBucket iteration replaced with started-event pruning: delete entries whose stored `dtstartMs <= now.getTime()`.
|
||||
|
||||
### Task 2 — reminderScheduler.test.ts
|
||||
|
||||
New tests added:
|
||||
|
||||
- **SINGLE-FIRE across 3 consecutive ticks:** 3 calls to `runReminderCheck` with event still in window; asserts `dispatchPush` called exactly once.
|
||||
- **MISSED-TICK-RECOVERY:** No scan at the ideal 15-min mark; call at 8 min before start; asserts dispatch fires.
|
||||
- **ALREADY-STARTED:** dtstart <= now excluded by SQL `gt`; mocked as empty rows; 0 dispatches.
|
||||
@@ -105,10 +106,12 @@ New tests added:
|
||||
- **Fan-out:** 2 subscriber rows for one event uid; 2 dispatches.
|
||||
|
||||
Old minuteBucket tests removed:
|
||||
|
||||
- `(eventUid, minuteBucket) twice within the same run` — replaced by per-uid SINGLE-FIRE test.
|
||||
- `CR-01: re-dispatch in next minute bucket` — replaced by CR-01 started-event pruning test.
|
||||
|
||||
Retained:
|
||||
|
||||
- D-07 all-day exclusion test.
|
||||
- D-05 non-shared exclusion test.
|
||||
- WR-01 mark-after-dispatch test (updated to per-uid dedup language).
|
||||
|
||||
+27
-27
@@ -19,13 +19,13 @@ overrides_applied: 0
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|---------|
|
||||
| 1 | A shared timed event in (now, now+16min] fires on the next scan even after a missed/late tick (catch-up). | VERIFIED | `gt(calendarEvents.dtstartUtc, now)` at line 103; lower bound is `now`, not `now+14min`. Any scan while event is still future will find it. MISSED-TICK-RECOVERY test fires at 8-min lead. |
|
||||
| 2 | A given event fires EXACTLY ONCE across all scans while in the catch-up window — no cross-tick double-fire. | VERIFIED | `sentReminders` is `Map<string, number>` keyed on bare uid (line 47); `if (sentReminders.has(uid)) continue` at line 145. SINGLE-FIRE test asserts `dispatchPush` called exactly once across 3 consecutive ticks. |
|
||||
| 3 | An event whose dtstart has already passed (dtstart <= now) does NOT trigger a reminder. | VERIFIED | `gt(calendarEvents.dtstartUtc, now)` at line 103 excludes already-started events at the DB level. "already-started" test mocks empty rows, asserts 0 dispatches. |
|
||||
| 4 | Notification body reflects actual lead time (e.g. "Starts in 8 min"), guarded to minimum 1. | VERIFIED | Line 150: `const minutes = Math.max(1, Math.round((event.dtstartUtc.getTime() - now.getTime()) / 60000))` then `body: \`Starts in ${minutes} min\`` at line 156. Not hardcoded. |
|
||||
| 5 | Existing Phase-5 behaviours stay green: isShared-only (D-05), allDay excluded (D-07), always-visible notification (D-11), empty push_subscriptions -> zero sends/no crash (D-16), per-event and per-sub error isolation (T-05-18/T-05-19). | VERIFIED | All present in source (lines 101-102 for D-05/D-07; line 98 for D-11 via dispatchPush; lines 141-186 for T-05-18/19 try/catch isolation). Tests for D-05, D-07, D-16, T-05-19 all pass (10/10). |
|
||||
| # | Truth | Status | Evidence |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | A shared timed event in (now, now+16min] fires on the next scan even after a missed/late tick (catch-up). | VERIFIED | `gt(calendarEvents.dtstartUtc, now)` at line 103; lower bound is `now`, not `now+14min`. Any scan while event is still future will find it. MISSED-TICK-RECOVERY test fires at 8-min lead. |
|
||||
| 2 | A given event fires EXACTLY ONCE across all scans while in the catch-up window — no cross-tick double-fire. | VERIFIED | `sentReminders` is `Map<string, number>` keyed on bare uid (line 47); `if (sentReminders.has(uid)) continue` at line 145. SINGLE-FIRE test asserts `dispatchPush` called exactly once across 3 consecutive ticks. |
|
||||
| 3 | An event whose dtstart has already passed (dtstart <= now) does NOT trigger a reminder. | VERIFIED | `gt(calendarEvents.dtstartUtc, now)` at line 103 excludes already-started events at the DB level. "already-started" test mocks empty rows, asserts 0 dispatches. |
|
||||
| 4 | Notification body reflects actual lead time (e.g. "Starts in 8 min"), guarded to minimum 1. | VERIFIED | Line 150: `const minutes = Math.max(1, Math.round((event.dtstartUtc.getTime() - now.getTime()) / 60000))` then `body: \`Starts in ${minutes} min\`` at line 156. Not hardcoded. |
|
||||
| 5 | Existing Phase-5 behaviours stay green: isShared-only (D-05), allDay excluded (D-07), always-visible notification (D-11), empty push_subscriptions -> zero sends/no crash (D-16), per-event and per-sub error isolation (T-05-18/T-05-19). | VERIFIED | All present in source (lines 101-102 for D-05/D-07; line 98 for D-11 via dispatchPush; lines 141-186 for T-05-18/19 try/catch isolation). Tests for D-05, D-07, D-16, T-05-19 all pass (10/10). |
|
||||
|
||||
**Score:** 5/5 truths verified
|
||||
|
||||
@@ -33,36 +33,36 @@ overrides_applied: 0
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `apps/api/src/broker/reminderScheduler.ts` | Resilient catch-up scan with per-uid exactly-once dedup, `runReminderCheck` exported | VERIFIED | 213 lines; contains `runReminderCheck`, `startReminderScheduler`; `Map<string, number>` dedup; `gt` lower bound; lead-accurate body |
|
||||
| `apps/api/tests/broker/reminderScheduler.test.ts` | Tests covering single-fire, missed-tick recovery, already-started exclusion, plus retained Phase-5 behaviours | VERIFIED | 409 lines; contains "SINGLE-FIRE", "MISSED-TICK-RECOVERY", "already-started", D-05/D-07/D-16/WR-01/CR-01/T-05-19 tests |
|
||||
| Artifact | Expected | Status | Details |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `apps/api/src/broker/reminderScheduler.ts` | Resilient catch-up scan with per-uid exactly-once dedup, `runReminderCheck` exported | VERIFIED | 213 lines; contains `runReminderCheck`, `startReminderScheduler`; `Map<string, number>` dedup; `gt` lower bound; lead-accurate body |
|
||||
| `apps/api/tests/broker/reminderScheduler.test.ts` | Tests covering single-fire, missed-tick recovery, already-started exclusion, plus retained Phase-5 behaviours | VERIFIED | 409 lines; contains "SINGLE-FIRE", "MISSED-TICK-RECOVERY", "already-started", D-05/D-07/D-16/WR-01/CR-01/T-05-19 tests |
|
||||
|
||||
---
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `reminderScheduler.ts` | `calendarEvents.dtstartUtc` | `WHERE gt(now) AND lte(now+16min)` | VERIFIED | Line 103: `gt(calendarEvents.dtstartUtc, now)`. Line 104: `lte(calendarEvents.dtstartUtc, windowEnd)`. |
|
||||
| `reminderScheduler.ts` | `dispatchPush` | fan-out per subscription, then mark uid sent | VERIFIED | Lines 163-174: per-sub loop calling `await dispatchPush(sub, notification)`. Line 179: `sentReminders.set(uid, ...)` after the fan-out loop (WR-01 preserved). |
|
||||
| From | To | Via | Status | Details |
|
||||
| ---------------------- | --------------------------- | -------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `reminderScheduler.ts` | `calendarEvents.dtstartUtc` | `WHERE gt(now) AND lte(now+16min)` | VERIFIED | Line 103: `gt(calendarEvents.dtstartUtc, now)`. Line 104: `lte(calendarEvents.dtstartUtc, windowEnd)`. |
|
||||
| `reminderScheduler.ts` | `dispatchPush` | fan-out per subscription, then mark uid sent | VERIFIED | Lines 163-174: per-sub loop calling `await dispatchPush(sub, notification)`. Line 179: `sentReminders.set(uid, ...)` after the fan-out loop (WR-01 preserved). |
|
||||
|
||||
---
|
||||
|
||||
### Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
|----------|---------|--------|--------|
|
||||
| TypeScript compilation (strict) | `pnpm --filter @familysync/api typecheck` | exit 0, no output | PASS |
|
||||
| All 10 reminderScheduler tests pass | `pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts` | 1 file passed, 10/10 tests passed | PASS |
|
||||
| `gt(dtstartUtc, now)` lower bound present | `grep -n "gt(" reminderScheduler.ts` | line 103: `gt(calendarEvents.dtstartUtc, now)` | PASS |
|
||||
| `gte` import/usage removed | `grep -n "gte" reminderScheduler.ts` | no matches | PASS |
|
||||
| `minuteBucket` variable fully removed | `grep -n "minuteBucket" reminderScheduler.ts` | only in comments, no variable | PASS |
|
||||
| Dedup store is `Map<string, number>` keyed by uid | `grep -n "Map" reminderScheduler.ts` | line 47: `new Map<string, number>()` | PASS |
|
||||
| Lead-accurate body with Math.max guard | `grep -n "Math.max" reminderScheduler.ts` | line 150: `Math.max(1, Math.round(...))` | PASS |
|
||||
| WR-01 mark-after-dispatch preserved | `grep -n "sentReminders.set" reminderScheduler.ts` | line 179: after fan-out loop | PASS |
|
||||
| CR-01 started-event pruning | `grep -n "dtstartMs" reminderScheduler.ts` | lines 192-194: prune on `dtstartMs <= now.getTime()` | PASS |
|
||||
| Required test names present | grep for SINGLE-FIRE, MISSED, already-started in test file | all found at lines 178, 214, 119 | PASS |
|
||||
| Behavior | Command | Result | Status |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------ |
|
||||
| TypeScript compilation (strict) | `pnpm --filter @familysync/api typecheck` | exit 0, no output | PASS |
|
||||
| All 10 reminderScheduler tests pass | `pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts` | 1 file passed, 10/10 tests passed | PASS |
|
||||
| `gt(dtstartUtc, now)` lower bound present | `grep -n "gt(" reminderScheduler.ts` | line 103: `gt(calendarEvents.dtstartUtc, now)` | PASS |
|
||||
| `gte` import/usage removed | `grep -n "gte" reminderScheduler.ts` | no matches | PASS |
|
||||
| `minuteBucket` variable fully removed | `grep -n "minuteBucket" reminderScheduler.ts` | only in comments, no variable | PASS |
|
||||
| Dedup store is `Map<string, number>` keyed by uid | `grep -n "Map" reminderScheduler.ts` | line 47: `new Map<string, number>()` | PASS |
|
||||
| Lead-accurate body with Math.max guard | `grep -n "Math.max" reminderScheduler.ts` | line 150: `Math.max(1, Math.round(...))` | PASS |
|
||||
| WR-01 mark-after-dispatch preserved | `grep -n "sentReminders.set" reminderScheduler.ts` | line 179: after fan-out loop | PASS |
|
||||
| CR-01 started-event pruning | `grep -n "dtstartMs" reminderScheduler.ts` | lines 192-194: prune on `dtstartMs <= now.getTime()` | PASS |
|
||||
| Required test names present | grep for SINGLE-FIRE, MISSED, already-started in test file | all found at lines 178, 214, 119 | PASS |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user