Skip to content

feat(calendar): pause date-push for real-time calendars (RC-4)#79

Merged
keyxmakerx merged 1 commit into
mainfrom
claude/cordinator-dispatch-caller-table-lk1kj3
Jul 13, 2026
Merged

feat(calendar): pause date-push for real-time calendars (RC-4)#79
keyxmakerx merged 1 commit into
mainfrom
claude/cordinator-dispatch-caller-table-lk1kj3

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Cites: 2026-05-21-core-tenets §T-O2 (consumer-verified wire contract), §T-O5 (tenet citation); decisions/2026-07-11-r13-rulings.md RC-4; dispatches/foundry/FM-REALTIME-DATE-SIGNAL.md
Security implication: none — this only suppresses an outbound write under a server-declared read-only condition; no new attack surface, no new trust boundary.
Consumer-verified: internal/plugins/syncapi/calendar_api_handler.go:95 ("tracks_real_time": cal.UsesRealTime() on GET /api/v1/campaigns/:id/calendar/date, bare object, verified directly against Chronicle's merged origin/main); internal/plugins/calendar/model.go:143 (UsesRealTime() bool { return c.Mode == ModeRealLife && c.TracksRealTime }); internal/plugins/calendar/service.go:2074 (SetDateguardManualDateChangeapperror.NewValidation, internal/apperror/errors.go:152 confirms 422); internal/plugins/syncapi/routes.go:155,194 (route wiring for both verbs). GET /calendar (GetCalendar, handler line 58) does not emit the field — confirmed by reading the handler body.
Foundry compatibility: n/a — no Foundry-runtime API surface touched (no ApplicationV2/Handlebars/Hooks changes), pure REST-client gating logic. Verified via unit tests only (node --test), consistent with how the equivalent FM-ENVELOPE-AUDIT / FM-CAL-BACKCATALOG-FIX PRs were verified.
Mockup: n/a — no UI change.

What this changes

Chronicle #529 (merged) now serves tracks_real_time on GET /calendar/date — the composed UsesRealTime() predicate. This PR makes the module respect it: when a calendar tracks real-world time, Foundry's own date pushes are paused (pull and event sync keep working normally), because Chronicle is now wall-clock-authoritative for that calendar's date.

Why

Without this, every GM date-advance/Calendaria hook keeps firing PUT /calendar/date at a calendar Chronicle will reject (422, its W3 guard) or — on an older deploy without the guard — could otherwise fight the live clock. RC-4 (decisions/2026-07-11-r13-rulings.md) calls for the module to stop making those doomed writes rather than relying solely on the server-side rejection.

Step-0: re-grep for put('/calendar/date' (line-number drift check)

The dispatch cited calendar-sync.mjs:731/:882/:911 and sync-dashboard.mjs:2057. Re-grepped against fresh main (deb818b, post-#78) before editing:

scripts/calendar-sync.mjs:731
scripts/calendar-sync.mjs:882
scripts/calendar-sync.mjs:911
scripts/sync-dashboard.mjs:2057

No drift — all four dispatch-cited lines matched exactly. All four (and only four) put('/calendar/date' call sites in the repo were gated; no fifth site was found.

Design

New pure/session-singleton helper scripts/_realtime-date-guard.mjs (mirrors the existing _calendar-probe-state.mjs pattern of extracting testable logic out of the stateful sync classes), so calendar-sync.mjs's three hook-triggered pushes and sync-dashboard.mjs's manual push button share one implementation instead of forking the guard twice (the dispatch's own stop-and-flag concern):

  • shouldSkipDatePush(api) — fetch-before-push: GET /calendar/date, read payload?.tracks_real_time === true defensively (FM-ENVELOPE-AUDIT convention), show the notice + return true (skip) if set. Re-run on every push attempt (pushes are rare; a probe failure just lets the push proceed and fail/succeed on its own terms — never blocks on a network hiccup).
  • isRealTimeRejection(err) — classifies a PUT 422 as the W3 backstop. Mirrors _calendar-probe-state.mjs's calendarStateFromError status-anchoring: prefers explicit err.status, else parses the api-client's authoritative "Chronicle API error <status>:" prefix — never a bare digit run (so an entity literally named "Room 422" can't misclassify).
  • notifyRealTimePushPaused() — one-time-per-session ui.notifications.warn, backed by a module-level singleton (not a class field) so the "shown once" state is genuinely shared across the two independent classes without either reaching into the other's instance.

All four call sites got the identical two-line change: a shouldSkipDatePush check before the PUT, and an isRealTimeRejection branch in the existing catch that calls notifyRealTimePushPaused() and returns — instead of falling through to the generic console.error.

Files changed

File Change
scripts/_realtime-date-guard.mjs New. Shared guard (98 lines).
scripts/calendar-sync.mjs Import + gate _onCalendariaDateTimeChange (:731→:733), _onLocalDateChange (:882→:886), _onSimpleCalendarDateChange (:911→:917 post-edit).
scripts/sync-dashboard.mjs Import + gate _onPushDate (:2057→:2059 post-edit).
tools/test-realtime-date-signal.mjs New. 29 tests (see below).
API-CONTRACT.md GET /calendar/date response gains tracks_real_time; new prose on the read-only-dates contract and the 422 backstop under PUT /calendar/date.
CLAUDE.md New Code Conventions bullet mirroring the existing envelope-shape bullet's style.
.ai.md New "Real-time calendar date-push guard" subsection under Sync Guard; new Files-table row; Calendar row in the API Endpoints table notes the field.

Deviations from the dispatch (honest-deviation rule)

  • The dispatch suggested a _realTimeNoticeShown class field mirroring the structure guard (calendar-sync.mjs:~327, which is a per-CalendarSync-instance boolean). I used a module-level singleton in the new _realtime-date-guard.mjs instead, specifically because the "once per session" requirement has to hold across two independent classes (CalendarSync and SyncDashboard) — a class field on CalendarSync alone can't dedupe a notice triggered from sync-dashboard.mjs's _onPushDate without one class reaching into the other's private state, which felt worse than a small shared pure-helper module (the repo already has four of these: _calendar-probe-state.mjs, _settings-validation.mjs, _overview-model.mjs, _member-mapping.mjs). Behaviorally identical to the dispatch's intent (one notice, once per session, everywhere); pinned by the last two tests in the new suite (the one-time notice is shared across calendar-sync.mjs AND sync-dashboard.mjs call sites).
  • The dashboard-push-awkwardness the dispatch flagged as a possible stop-and-flag didn't materialize: _onPushDate's existing try/catch shape took the identical two-line change as the other three sites. No fork was needed, so no flag.

Test plan

  • node --test tools/test-realtime-date-signal.mjs29/29 pass. Covers: pure-helper unit tests for tracksRealTime/isRealTimeRejection/shouldSkipDatePush/notifyRealTimePushPaused (including the "bare digit run" 422 misclassification guard and the probe-failure-doesn't-block case); all three calendar-sync.mjs sites × (skip-on-true / proceed-on-false / proceed-on-absent / 422-sets-guard-without-throwing / non-422-still-logs); SyncDashboard._onPushDate × (skip / proceed / 422-backstop); cross-module once-per-session sharing.
  • node --test tools/test-*.mjs646/646 pass (617 pre-existing + 29 new; zero regressions).
  • node tools/check-package-descriptor.mjschronicle-package.json: OK (0 warnings).
  • Manual verification in Foundry — not performed (headless environment; no live Foundry client available). This PR is unit-test-verified only, consistent with prior FM PRs from this pipeline (fix(calendar-sync): back-catalog envelope, date-push indexing, reentrant guard (FM-CAL-BACKCATALOG-FIX) #77, audit(envelope): sweep all list-consuming get() callers — zero remaining mismatches (FM-ENVELOPE-AUDIT) #78).

Tenet self-check

  • T-B1 security: n/a — no new trust boundary; this suppresses a write, doesn't add one.
  • T-B2 plugin isolation: n/a — Foundry-module repo only, no Chronicle-side change (out of scope per dispatch).
  • T-B3 production UI: n/a — no UI change beyond a ui.notifications.warn call, matching the existing structure-guard notice's shape exactly.
  • T-B4 dual-audience docs: .ai.md and CLAUDE.md both updated with the same convention-bullet style used for FM-ENVELOPE-AUDIT.

Stop-and-flag

None encountered — Step-0 found zero line drift, and the dashboard-push-awkwardness concern the dispatch flagged as a possible stop-and-flag didn't materialize (see Deviations above).


Generated by Claude Code

Chronicle #529 now serves tracks_real_time on GET /calendar/date (the
composed UsesRealTime() predicate). Gate all four PUT /calendar/date
sites through a shared scripts/_realtime-date-guard.mjs: fetch-before-push
check re-run on every push (self-heals a mid-session flag flip), plus
treating a 422 W3 rejection as the same condition rather than a retryable
error. One GM notice per session, shared across calendar-sync.mjs and
sync-dashboard.mjs via a module-level singleton. Pull/event sync untouched.

Cites: 2026-05-21-core-tenets §T-O2, §T-O5

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgwKzUUUY26C1oFjxY4Hfu
@keyxmakerx keyxmakerx merged commit d94e678 into main Jul 13, 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