feat(s-orgtz-unify): unify the org timezone onto the working calendar (R56)#294
Merged
Conversation
…g calendar Canonical org tz resolved cal.tz -> org.tz -> env -> UTC (D-1); hybrid contextvar + explicit mechanism (D-2); folds in _fmt_date render hardening, the OVERDUE now_is_working gate, and a next_review_due backfill CLI (D-3); R8 cutover stays UTC (D-4). Closes the named org-tz cross-cutting residual and R55 D-3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…contextvar leak) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…calendar tz via pick_tz (parity) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…en missing-row test (parity, not == DEFAULT_CALENDAR) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…boundary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts the org-tz contextvar Closes the review I1 gap: the HTTP propagation test is only mutation-distinguishing ~14/24h; this monkeypatched unit test of get_current_user is time-independent and fully mutation-distinguishing (no Docker). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…allers pass current_org_tz()
…org_tz; sweeps compute today explicitly
… tz; wrap the escalation render
render.py _fmt_date: when value is an aware datetime, astimezone(current_org_tz()).date() instead
of the raw UTC .date() — fixes the east-of-UTC date shown one day early for a UTC-stored due_at.
Naive datetimes and date objects are unchanged. Two new unit tests: aware→Tokyo reconvert is
mutation-distinguishing (UTC fallback shows 2026-06-28; Tokyo context shows 2026-06-29), naive/date
pass-through confirmed.
escalation.py process_task_timers: import using_org_tz alongside pick_tz; wrap the fired=0 + for
step in due_steps(…) block in with using_org_tz(calendar.tz): so that emit_task_event → render →
_fmt_date sees the calendar tz (which equals resolve_org_tz by pick_tz parity — no extra DB hit).
The await session.commit() and return fired remain outside the with. Mechanical reindent only.
digest.py: no change needed — bundle_user_digest renders only recipient.first_name / item_count /
items (pre-rendered title strings) / prefs_link; no {{ … | date }} slot for a due_at datetime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e canonical org tz Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…olved canonical tz Replace every `_org_tz()` (env-tz contextvar, unset in tests → UTC) in the two affected integration files with `await _canonical_tz()` — a thin async helper that calls `resolve_default_org_tz(session)`, the same DB chain the server now uses (cal.tz → org.timezone → env → UTC). test_periodic_review.py (10 sites): - module-level `_org_tz` dropped from the vault.review import - two inline `from … import _org_tz` re-imports removed - `_canonical_tz()` module-level helper added - all 10 expectation sites (eff_from_dt.astimezone, datetime.now, di.last_reviewed_at.astimezone, backdated_eff_from.astimezone) rewritten test_reports.py (1 site): - `_canonical_tz()` helper added - redundant inline `get_sessionmaker`/`_org_tz` imports removed - `today_date` in test_checklist_overdue_review_leg rewritten (the "yesterday" written to next_review_due must be yesterday in the server's tz so the overdue_review flag flips correctly under a divergent calendar.timezone) In CI (org calendar tz == UTC == env UTC) behaviour is identical. The rewrite removes fragility against test_org_clock.py temporarily mutating the calendar tz to non-UTC — if a restore is skipped/reordered an env-UTC expectation would mismatch the canonical-non-UTC server. Mutation-verify (step 4): deferred to CI/live-smoke (requires Docker). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the canonical tz (final-review) Three worker sweeps (sweep_reviews, sweep_mgmt_reviews, sweep_acks) enqueued task.assigned notifications with the org-tz contextvar unset, so _fmt_date fell back to UTC for a cal.tz-aware due_at — rendering the UTC-previous-day date for east-of-UTC orgs (regression of the Codex #291 _fmt_date fix). _cutover similarly called current_org_tz() (contextvar) instead of an explicit resolve, so the Beat release path stored next_review_due in the wrong tz frame. Fixes: - services/vault/review.py sweep_reviews: with using_org_tz(cal.tz) around enqueue - services/mgmt_review/cadence.py sweep_mgmt_reviews: with using_org_tz(cal.tz) around enqueue - services/ack/sweep.py sweep_acks: resolve_org_tz once per doc + using_org_tz(org_tz) around enqueue - services/vault/lifecycle.py _cutover: replace current_org_tz() with await resolve_org_tz(session, doc.org_id) Adds a CI-authoritative integration test: sweep_reviews under Asia/Tokyo calendar asserts the notification body contains the Tokyo-local Monday date, not the UTC-previous-day (Sunday) — mutation-distinguishing against the missing wrap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…snap test The §3.4 _fmt_date change re-converts via current_org_tz(); the materialize/sweep render now runs inside using_org_tz(cal.tz), so this pre-existing test must set the context. Caught by the full -m unit suite (the targeted per-task suites missed this cross-file _fmt_date caller in test_duedate_snap.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fication test CI integration run PROVED the production fix works (body rendered the Tokyo date 2026-06-29, not the UTC Sunday). The test's (7 - isoweekday()) %7 computed the next SUNDAY, set next_review_due there, and the sweep correctly SNAPPED due_at forward to Monday — so the asserted (unsnapped) Sunday mismatched. Use (1 - isoweekday()) %7 to target a real Monday (a Tokyo working day, no snap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CoJoA13
added a commit
that referenced
this pull request
Jun 26, 2026
Slice-history narrative + CLAUDE.md learning (drop oldest S-notify-3b for the ~8 cap) + Current-status pointer for the org-timezone unification (R56). No code change. Co-authored-by: Claude Opus 4.8 (1M context) <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.
S-orgtz-unify — unify the org timezone onto the working calendar (R56)
Collapses the three drifting org-timezone sources (env
easysynq_org_timezone,organization.timezone,working_calendar.timezone) into one canonical, DB-resolved tz so review dates, thereview_statebadge, snap/timer, and notification rendering all judge dates in one frame. Closes the named cross-cutting residual + R29's D-3 divergence.What
services/common/org_clock.py(new):resolve_org_tz(chainworking_calendar.timezone → organization.timezone → env → UTC, fail-safe) +pick_tz+ a request/sweep-scopedContextVar(current_org_tz/set_request_org_tz[no reset, per-request-task isolation] /using_org_tz[reset CM]).resolve_working_calendarsources its tz from the samepick_tz→ parity by construction.today_org()is contextvar-backed — set at the auth boundary (get_current_user) and wrapped around every worker render; the compute functions (compute_next_review_due,read_cadence,_last_released_effective_from) take an explicitorg_tz._fmt_datere-converts aware datetimes to the org tz; OVERDUE is nownow_is_working-gated (closes R55 D-5's weekend-pierce exemption);cli/backfill_review_dates.pyrecomputes storednext_review_dueinto the canonical tz (idempotent,--dry-run; MR next-due is derived → no backfill).Scope
BE + CLI + docs only. No migration (head stays
0067) · no new permission key · no web/FE change · env var kept as the bottom fallback ·organization.timezone&working_calendar.timezonestay independently editable (no S-notify-7 regression). New register decision R56 + amend-notes on R8 & R55.Review
Built subagent-driven (10 tasks, a fresh implementer + task-reviewer each). A final whole-branch review (opus +
diff-critic) caught one east-of-UTC-only false-PASS (masked on the dev Chicago org + CI's UTC): three worker sweeps (sweep_reviews,sweep_mgmt_reviews, ack sweep) rendered the"Due by"date with the contextvar unset →_fmt_datereframed the cal.tzdue_atto the previous UTC day (regressing the Codex #291 fix), and_cutoverstorednext_review_duein UTC via therelease_dueBeat — fixed by wrapping each worker enqueue inusing_org_tz(cal.tz)and resolving the tz explicitly in_cutover. The full-m unitgate then caught a pre-existing_fmt_datetest needing the new render context — also fixed.Tests
Local:
ruff+mypy src(412 files) +-m unit1062 passed green. Integration + migrations are CI-gated (no local Docker). Deltas: +12 unit / +5 integration, web unchanged (1292, no FE).🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com