feat(scripts): repair legacy-migrated series with wrong-frame BYDAY - #2535
Merged
Conversation
Weekly recurring series migrated from the legacy DB during the 2026-07-29 Sync cutover had their RRULE BYDAY captured in the wrong timezone frame relative to schedule.timeZone, causing Compass to materialize a phantom, wrong-weekday occurrence every week alongside the correct one (imported separately as an exception holding the real Google-sourced instant). Adds a standalone CLI command (not an umzug migration - compass_sync is a deliberately isolated database the migrate runner can't reach) that detects affected weekly single-BYDAY series masters, derives the correct weekday from the strong majority (>=80%) of the series' own exceptions (tolerating rare legitimate one-off reschedules), rewrites BYDAY, shifts the master's own anchor date only when no exception already covers it, and reprojects via the existing reprojectMaster/replaceForEvent machinery. Local read-model correction only - traced the full outbound path and confirmed nothing pushes a raw Mongo write back to Google. Run against prod: 37 series across 9 tenants fixed (all >=93% exception consensus, most 100%), 273 skipped as genuinely ambiguous or under-evidenced rather than guessed. Verified idempotent (rerun after apply reports 0 further fixes) and spot-checked via direct read that duplicate weekly occurrences are gone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cli.test.ts exercises each registered command's dispatch individually; add the missing case for the new command alongside its siblings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tyler-dane
added a commit
that referenced
this pull request
Aug 1, 2026
The one-off repair (PR #2535) has run successfully against prod: 37 legacy-migrated weekly series fixed across 9 tenants (all >=93% exception consensus, 35 of 37 at 100%), verified via direct DB reads and an idempotent rerun reporting 0 further changes. The population it targeted is closed - the current write/import paths are frame-consistent and cannot reproduce it - so the command has nothing left to do and no self-hosting relevance. Mirrors how the 2026-07-29 Sync-cutover one-off tooling (migrate-provider-state.ts, preseed-sync.ts, etc.) was deleted the day after confirmed use (commit 07816a8), rather than accumulating as permanent dead code. The full before/after data and methodology are preserved in PR #2535's description. Co-authored-by: Claude Sonnet 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
Some recurring weekly series migrated into
compass_syncfrom the legacy DB have their RRULEBYDAYcaptured in the wrong timezone frame relative to the event's ownschedule.timeZone. Compass expandsBYDAYinschedule.timeZone(occurrence-projection.tsfloatingAnchor/floatingRules), so an affected series renders a phantom, wrong-weekday duplicate occurrence every week, alongside the correct one (which comes from a separately-importedexceptionrecord holding the real Google-sourced instant). Surfaced live via the founder's own "Review Week" series, reported after the #2525 crash fix shipped.Adds a standalone, one-off CLI command — not an umzug migration, since
packages/scripts/src/migrations/only ever connects toprod_calendar, andcompass_syncis a deliberately isolated database/credential. Mirrors the existingpurge-corrupt-sync-events/purge-userpattern: connects viaSyncMongoService, default dry-run,--applyto write.For each affected weekly single-
BYDAYseries:schedule.start— not a naive unanimous vote, which would wrongly skip a series with even one legitimate one-off reschedule buried in years of history (the founder's own series is 711 of 712 exceptions on Saturday, 1 on Sunday).BYDAY=component of the rule —FREQ/INTERVAL/UNTIL/COUNT/WKSTuntouched.schedule.start/end) only when it's on the wrong weekday AND no exception already covers that exact instant — otherwise that one historical date would keep generating its own phantom forever, sinceoccurrence-projection.tsalways materializes DTSTART regardless ofBYDAY.EventRepository.replaceExisting(owner-scoped, schema-validated) and reprojects via the existingreprojectMaster/replaceForEventmachinery — the same transactional path every live user edit already goes through.This is a local read-model correction only. Traced the full outbound path (
command.routes.ts→provider-command.service.ts; the job dispatcher'scommandApply/reconcilekinds have no producer in this build) — nothing watches a raw Mongo write and pushes it to Google.Already run against production
Per the internal migration playbook's triage (idempotent per-doc repair, light ceremony — independent per-series writes, convergent on rerun, drives the already-transactional occurrence-replace path every live edit already races against):
--dry-runagainst prod, cross-checked against an independent manual aggregation.--applyrun: 37 series fixed across 9 tenants (all ≥93% exception consensus, 35 of 37 at 100%), 273 skipped as genuinely ambiguous or under-evidenced rather than guessed, 1,706 already correct.BYDAY=SAand materialize exactly one occurrence per week; re-running in dry-run mode reportsfixed=0(full convergence, idempotent).Cleanup plan
This is a one-off tool for a closed, already-migrated population (confirmed: the current write/import paths are frame-consistent, so this cannot recur for new data). Once this PR is confirmed merged and deployed, a follow-up PR deletes the command file, its subdirectory, and its
cli.ts/cli.test.tsregistration — mirroring how the 2026-07-29 Sync-cutover one-off tooling (migrate-provider-state.ts,preseed-sync.ts, etc.) was deleted the day after confirmed use, rather than accumulating as permanent dead code.Test plan
bun test:scripts— 33 pass, including 8 new tests for the repair logic itself (BYDAY rewrite, anchor-date shift, the "already-excepted, no shift needed" case, strong-majority-with-outlier, ambiguous-history skip, no-exceptions skip, already-correct no-op, and idempotent rerun) and a newcli.test.tscase covering command dispatch.bun run type-check— clean.biome check— clean on all touched files.--dry-run/--apply/re---dry-runcycle against prod, described above.🤖 Generated with Claude Code