Skip to content

fix(calendar-sync): hook date off-by-one + back-catalog + structure guard (FM-CAL-SYNC-HOTFIX)#76

Merged
keyxmakerx merged 2 commits into
mainfrom
claude/chronicle-c-bugs-verify-r2-4jed83
Jul 11, 2026
Merged

fix(calendar-sync): hook date off-by-one + back-catalog + structure guard (FM-CAL-SYNC-HOTFIX)#76
keyxmakerx merged 2 commits into
mainfrom
claude/chronicle-c-bugs-verify-r2-4jed83

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

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.md B-R2 (item 3) + B-R8 (CI).

The fixes

1. Off-by-one (LIVE) — _calendariaNoteToChronicleEvent

Calendaria's realtime note* hooks deliver raw 0-indexed month/dayOfMonth with an absolute year; 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 helper chronicleDateFromCalendariaStartDate normalizes by date shape (dayOfMonth → +1; public day → verbatim; year never touched), and the method 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) — _syncChronicleEventsToCalendariaNotes

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 structure (bounded, deduped, logged), and holds the _syncing guard across the create loop so pulled events don't echo back through the noteCreated hook 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)

  • Calendaria 1.1.3 indexing verified FIRST-HAND from upstream source (Sayshal/Calendaria@ee04e44): api.mjs toPublic does month=(month??0)+1, day=(dayOfMonth??0)+1, deletes dayOfMonth, year untouched (proving raw is 0-indexed); getNote() returns toPublicStub(...); note-manager fires the raw stub with no toPublic. The report's diagnosis is correct.
  • GET /calendar eager-loads per-month days + weekdays → the guard compares honestly.

Test plan

  • node -c clean; node tools/check-package-descriptor.mjs green.
  • node --test 'tools/test-*.mjs'592 pass / 0 fail (575 existing + 17 new in tools/test-calendar-sync-hotfix.mjs: date normalization + getNote precedence/no-double-correction/fallback, the Jan-1 zero case, fetch-coordinate bounds, the no-echo _syncing guard, and structure comparison incl. the operator's Gregorian 12mo/7wd vs Therin 15mo/6wd case).
  • Adversarial review (find → verify) — caught and fixed a back-catalog echo (duplicate re-POST); off-by-one confirmed clean.
  • Live smoke (operator gate — not headlessly testable) on the live v14 world (Calendaria 1.1.3, Calendar of Therin): (a) create a note on the 1st of a month → it lands on the 1st in Chronicle (not the prior month's last day); (b) the "Sync Paused — structure mismatch" banner shows (Therin ≠ Gregorian) and journals/actors/maps still sync; (c) after a matching calendar exists, sync resumes and the back-catalog maps with no duplicates; (d) confirm CALENDARIA.api.getNote() exposes the public date at note.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

claude added 2 commits July 11, 2026 04:19
…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
@keyxmakerx keyxmakerx merged commit 4523bff into main Jul 11, 2026
1 check 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.

2 participants