fix(calendar-sync): hook date off-by-one + back-catalog + structure guard (FM-CAL-SYNC-HOTFIX)#76
Merged
Conversation
…ture guard Three calendar-sync fixes (FM-CAL-SYNC-HOTFIX) + a CI ride-along. No Chronicle-side change. 1. Off-by-one (LIVE): the Calendaria realtime note* hooks deliver RAW 0-indexed month/dayOfMonth with an absolute year, but the module read them verbatim — mis-dating every pushed event −1 month/−1 day (a 1st-of-month or January note sent an invalid month 0 / day 0). Verified against Calendaria release-1.1.3 source (api.mjs toPublic, note-manager hook emission). New pure helper chronicleDateFromCalendariaStartDate normalizes by date SHAPE (dayOfMonth → +1; public `day` → verbatim; year never touched), and _calendariaNoteToChronicleEvent now resolves via CALENDARIA.api.getNote() (authoritative toPublic) first with the raw payload as fallback — zero double-correction. The false "1-indexed months" comment is gone. 2. Back-catalog (SILENT): initial sync did a bare GET /calendar/events, which Chronicle serves month-filtered (current month only), so every event outside the current month was never ID-mapped. It now paginates ?year=&month= across the current year ±1 from the cached calendar structure (bounded, deduped, logged). The create loop holds the _syncing guard so pulled events don't echo back through the noteCreated hook as duplicate POSTs. 3. Structure mismatch (LIVE, B-R2): when the active Calendaria calendar's structure differs from Chronicle's (month count / per-month day counts / weekday count; moons/seasons/eras excluded), calendar sync is paused for the session in BOTH directions across all pull funnels and push handlers, with a persistent dashboard banner + "Sync Paused" badge + an Overview alert, one ui.notifications.warn, and the state added to the diagnostics export. Fails open (only a confirmed, both-sides-readable mismatch pauses). Journals/characters/maps are untouched. CI (B-R8): the full suite (node --test 'tools/test-*.mjs') now runs on push + PR alongside the descriptor check. Tests: tools/test-calendar-sync-hotfix.mjs (17) — date normalization + wiring, fetch-coordinate bounds, the no-echo _syncing guard, and structure comparison (incl. the operator's Gregorian 12mo/7wd vs Therin 15mo/6wd case). 592 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bAhi59Sajsmd5bGCu7Smp
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bAhi59Sajsmd5bGCu7Smp
8 tasks
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.
Three calendar-sync fixes + a CI ride-along, one PR, no Chronicle-side change. Fixes a LIVE production mis-dating bug.
Dispatch:
FM-CAL-SYNC-HOTFIX. Diagnosis:reports/foundry/2026-07-07-fm-cal-event-sync-verify.md§2/§3 (anchors re-verified in Step-0). Binding:decisions/2026-07-10-beta-transition-rulings.mdB-R2 (item 3) + B-R8 (CI).The fixes
1. Off-by-one (LIVE) —
_calendariaNoteToChronicleEventCalendaria's realtime
note*hooks deliver raw 0-indexedmonth/dayOfMonthwith an absoluteyear; the module read them verbatim, mis-dating every pushed event −1 month / −1 day (a 1st-of-month or January note sent an invalid month 0 / day 0). New pure helperchronicleDateFromCalendariaStartDatenormalizes by date shape (dayOfMonth→ +1; publicday→ verbatim; year never touched), and the method now resolves viaCALENDARIA.api.getNote()(authoritativetoPublic) first with the raw payload as fallback — zero double-correction. The false// 1-indexed monthscomment is gone.2. Back-catalog (SILENT) —
_syncChronicleEventsToCalendariaNotesInitial sync did a bare
GET /calendar/events, which Chronicle serves month-filtered (current month only) — so every event outside the current month was never ID-mapped. It now paginates?year=&month=across the current year ±1 from the cached structure (bounded, deduped, logged), and holds the_syncingguard across the create loop so pulled events don't echo back through thenoteCreatedhook as duplicate POSTs.3. Structure mismatch (LIVE, B-R2) — the guard
When the active Calendaria calendar's structure differs from Chronicle's (month count / per-month day counts / weekday count; moons/seasons/eras excluded per B-R2), calendar sync pauses for the session in BOTH directions (all 3 pull funnels + all 9 push handlers), with a persistent dashboard banner + "Sync Paused" badge + Overview alert, one
ui.notifications.warn, and the state added to the diagnostics export. Fails open (only a confirmed, both-sides-readable mismatch pauses). Journals/characters/maps are untouched.CI ride-along (B-R8)
The full suite (
node --test 'tools/test-*.mjs') now runs on push + PR alongside the descriptor check.Step-0 (verified before building — no stop-and-flag)
Sayshal/Calendaria@ee04e44):api.mjstoPublicdoesmonth=(month??0)+1,day=(dayOfMonth??0)+1, deletesdayOfMonth, year untouched (proving raw is 0-indexed);getNote()returnstoPublicStub(...);note-managerfires the raw stub with notoPublic. The report's diagnosis is correct.GET /calendareager-loads per-monthdays+weekdays→ the guard compares honestly.Test plan
node -cclean;node tools/check-package-descriptor.mjsgreen.node --test 'tools/test-*.mjs'— 592 pass / 0 fail (575 existing + 17 new intools/test-calendar-sync-hotfix.mjs: date normalization + getNote precedence/no-double-correction/fallback, the Jan-1 zero case, fetch-coordinate bounds, the no-echo_syncingguard, and structure comparison incl. the operator's Gregorian 12mo/7wd vs Therin 15mo/6wd case).CALENDARIA.api.getNote()exposes the public date atnote.flagData.startDate(if not, the raw fallback still dates correctly — see report).Out of scope (per dispatch)
Multi-GM double-push idempotency; extended event fields / recurrence mapping (awaits the ratified contract amendment); the structure importer (coordinator design in flight); any Chronicle-side change. A SimpleCalendar structure guard is a noted follow-up.
After merge: cut a module release + update the package in Chronicle admin (a merged PR is not a deployed fix).
🤖 Generated with Claude Code
https://claude.ai/code/session_014bAhi59Sajsmd5bGCu7Smp
Generated by Claude Code