Skip to content

Commit 82d8f62

Browse files
tyler-daneclaude
andauthored
refactor: cut the event runtime over to calendar-owned contracts (#2017)
* refactor(backend): use calendar-owned event repository Packet 03 backend cutover (phases 1-2): every backend event path now runs on the strict calendar-owned contracts from packets 01-02. - one event repository owns all event Mongo access; ownership is proven through the calendar (no event.user queries); range reads are the two-branch timed/date-only design with the series join preserved - strict HTTP surface: EventListQuery/CreateEventInput (client ids, A25)/ ReplaceEventInput/DeleteEventInput/ReorderEventsInput plus the new someday<->scheduled transition endpoint (A24); EventMutationError envelope with new DUPLICATE_EVENT_ID code; legacy applyTo wire values retired - recurrence pipeline rebuilt (parser/generator/executor) on EventRecord + RecurrenceScope; materialized instances; thisAndFollowing still splits via UNTIL; someday series no longer materialize duplicate instances - compass->google propagation resolves the owning calendar, writes via events.patch bodies (A28) with explicit calendar ids, keeps Google effects outside the Mongo transaction; series edits patch the base once instead of fanning out per-instance inserts - google import + webhook propagation flow through mapGoogleEvent and match by (calendarId, externalReference.eventId); backfill targets google-calendar events with null externalReference (origin retired, A34) - revoke prune deletes google-calendar events, archives those calendars (isActive false, A16), preserves all local/someday data - SSE publishes the ServerMessage union on one event name (A27): eventsChanged with real calendar/event ids, syncStatusChanged, importCompleted with accurate counts, userMetadataChanged; legacy SSE names removed - calendar API returns strict Calendar models; POST /api/calendars removed (A15); select accepts the bulk isVisible contract - known 03 limitation recorded in plan 05: scope-"this" edits on synced series occurrences do not yet propagate as Google exceptions - frozen 2025 migrations untouched behaviorally (raw-collection casts only); the superseded prototype backfill TEST is removed - it can only be fed by legacy fixtures the runtime no longer produces, and packet 02's backfill tests own that coverage with hand-rolled legacy docs backend 63/63 suites (477 tests), core 265, scripts 139, type-check green. Web compiles via minimal mechanical shims marked TODO(packet-03-phase-3); web behavior cutover follows in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): cut data and state layers over to calendar-owned contracts Calendars module + sentinel, SSE ServerMessage parsing, event API on the new routes, remote/local repositories, IndexedDB v4 with legacy migration and new-shape demo seed, normalized Event queries/cache (overlap semantics now match the backend; legacy minus-one-day week hack removed), mutations with client ids + transition replacing the convert pair, undo/redo preserved, revoke prune by calendar provider. Component/view layer still bridges through event.legacy-bridge.ts pending the final web pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): correct someday edit, test isolation, and e2e helpers for cutover - someday edits silently vanished: the sidebar submit path forced order=-1, which fails the strict nonnegative SortOrderSchema during replace-input assembly; placeholder now applies only to new drafts - cross-file web test failures fixed at their real roots (query-key field names in useDayEvents; a leaked authenticated useSession mock from useSubscribeCmdItems) - e2e IndexedDB helper reads the nested LocalEventRecord shape - view model drops malformed cache entries loudly instead of crash-looping - docs: architecture domain model reflects the shipped contracts; plan 03 carries an explicit status note (component-layer bridge dissolution is the remaining work before its boxes are checked) Full matrix green: backend 63/63 (477), web 1242, core 265, scripts 139, type-check clean, full local e2e 11/11. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): reset event-repository source between test files event.repository.source.store.ts caches its local/remote resolution in module-level singletons (lastSessionExists/hasComputed), outside Zustand and therefore invisible to the reset-stores registry. Bun test workers share the module registry across files, so any earlier file resolving an authenticated session leaked a 'remote' source into every later file in the same worker — directory-traversal test order differs between macOS and Linux, so this only surfaced in CI. Register the store's reset in the shared registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): restore the real offline-data-store registry after mocking it Four test files replace @web/common/storage/offline-data/offline-data.store.registry wholesale via mock.module, which leaks process-wide for the rest of the bun test run. Two of them mock getOfflineDataStore as a bare mock() (resolves to undefined), so any later file in the same worker that calls it for real throws 'undefined is not an object'. Bun test-file load order is directory traversal, which differs between macOS (APFS) and Linux (ext4/CI), so this only surfaced on CI, on useWeekShortcuts.test.tsx, which seeds an event via getOfflineDataStore().putEvent(...) and does not mock the registry itself. Capture the real module via require() before each mock.module() call and restore it in afterAll — kills the class for any future file, in any order. Verified with the deliberately-adversarial order (all four mocking files loaded before the previously-failing file) and 3 consecutive full-suite runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Revert "fix(web): restore the real offline-data-store registry after mocking it" This reverts commit 847663e. * fix(web): make the offline-data-store mock leak fix order-independent The prior fix (per-file require()-before-mock capture) was unsound: if an earlier test file in the same bun worker had already mocked the registry, that file's own 'real' capture was itself the earlier file's mock, not the true module — so restoration just propagated whichever mock happened to run first. CI's Linux directory-traversal file order differs from local macOS order and hit this case, so the useWeekShortcuts crash persisted despite the previous fix passing locally. Correct fix: capture the true module once in web.preload.ts, which runs before any test file's top-level code (guaranteed unpolluted), and restore it in the shared global afterEach that already runs after every single test regardless of file. The four files that replace the registry via mock.module now re-assert their own mock in beforeEach instead of relying on their one-time top-level call, so the global restore doesn't strand their own later tests. This makes the fix's correctness independent of file load order entirely, rather than dependent on being first. Verified with the full local suite (TZ=UTC, matching CI) 3 consecutive runs, plus explicit forward/reverse/worst-case file orderings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): replace mock.module offline-store mocking with a runtime override Both prior attempts to fix the offline-data-store mock leak assumed mock.module's replacement is visible to every consumer at call time. It isn't: mock.module redirects future module *resolution*, but a module that already captured a live binding to an export (e.g. local.event.repository.ts calling getOfflineDataStore() internally, bound whenever that module was first imported/linked) keeps referencing whatever was current at ITS OWN first import, permanently — no later mock.module call can retroactively fix an already-linked consumer. This explains why the previous fix still failed on CI (Linux file-load order determines which mock a given consumer gets frozen to) and why a second symptom appeared (DayCalendarGrid getting local.event.repository.test.ts's getEvents-less mock). Correct fix: offline-data.store.registry.ts now holds a plain testOverrides object that every export reads at CALL time, not import time. This is real runtime state inside one module instance, immune to Bun's module-resolution caching entirely. The four suites that need to fake IndexedDB access call setOfflineDataStoreTestOverrides(...) in beforeEach instead of mock.module; web.preload.ts's shared afterEach clears overrides after every test. Verified: full local suite green 3x at TZ=UTC (matching CI), plus forward, reverse, and worst-case explicit file orderings including the two files that previously exposed real symptoms (useWeekShortcuts, DayCalendarGrid). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): replace offline-store test overrides with constructor seams The testOverrides object in offline-data.store.registry.ts put test vocabulary in production code and needed paragraph-length comments in five files to explain why tests worked - both smells pointing at the same root cause: the tests were substituting a module when they only needed to substitute a collaborator. - offline-data.store.registry.ts: reverted to its original form (-40 lines of override machinery) - LocalEventRepository takes its store accessor as a constructor default; RemoteEventRepository takes the api and local repository the same way; initializeDatabaseWithErrorHandling takes the initializer as a parameter default - the same deps-with-defaults convention local-event-sync.util.ts already uses - the four test files pass plain fakes through those seams; no mock.module on the registry remains anywhere, so the cross-file leak class is gone by construction rather than by cleanup bookkeeping; remote repository tests now assert delegation to the local repository instead of reaching through it into store internals - useTaskState.test needs no substitute at all: with fake-indexeddb in the preload, the real ensureOfflineDataStoreReady is fast and is exactly what the hook promises to do Net -108 lines. Full web suite green 3x at TZ=UTC plus worst-case explicit file orderings; type-check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): extract transaction wrapper, drop unreachable active checks startSession/withTransaction/endSession was copy-pasted in create, replace, delete, and transition; one private withEventTransaction helper now owns it. Two !calendar.isActive branches removed: getOwnedActiveCalendar already filters isActive in its query, so they could never fire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): dedupe series materialization in event generator Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): extract scheduleStartMs helper in event parser The timed/allDay/someday to epoch-ms ternary appeared four times across analyzeReplace and analyzeDelete; semantics preserved exactly per site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): remove dead exports and narration comment deleteOne on the event repository and getScheduleStartMs in recur.util had zero callers repo-wide; plus one comment that restated the next line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): delete dead computeSomedayEventsRequestFilter Exported with no call sites anywhere in packages/web after the someday queries moved to period/anchor params. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): dedupe event-query-cache writer boilerplate All seven cache writers repeated the same iterate-matching-entries, setQueryData, null-guard scaffolding; one shared forEachEventQuery helper owns it now. Net -28 lines, behavior identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): share timed/all-day grid event derivation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): extract order predicate and trim restated storage comments Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web): type someday-repository test schedule via schema parses Replaces an as-unknown-as Parameters<> cast with real branded values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scripts): widen the flaky heap bound in the backfill memory test Shared CI runners showed 245-296 MB of GC-timing noise for identical code against the 250 MB threshold. The assertion exists to catch accumulation proportional to the dataset (gigabytes at this fixture size), so 400 MB keeps the guarantee while tolerating runner variance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plan): record the staging-first v1 rollout strategy (A36) Staging cuts over with the runtime-cutover merge and receives every packet continuously (main auto-deploys staging); production stays on the pre-cutover release, keeps writing legacy data (the backfill is rerunnable), and cuts over exactly once - runbook then the manual Deploy Production action - after the 09 gates pass on staging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): teach simplify where to find complexity Detector-ordered hunting list distilled from this PR's simplification pass: size-first triage, comment density as a mechanism smell, mock volume and mock.module seams, test-only hooks in production code, cast smells, copy-pasted scaffolding, dead/unreachable code, hidden-shared-state coupling, and bridge audits - plus the restraint counterweight (don't unify lookalikes, don't break repo-wide conventions locally, report what you inspected and left alone). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent cb83f16 commit 82d8f62

199 files changed

Lines changed: 10714 additions & 18675 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/simplify/SKILL.md

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,65 @@ make code easier for other contributors to verify and change.
1919
- Workflow: find existing abstractions first → apply the principles below →
2020
verify with focused checks → report the diff with principles applied.
2121

22+
## Finding Complexity — Where to Start
23+
24+
Don't read the diff top to bottom hoping smells jump out. Hunt with
25+
detectors, in rough order of payoff (each proven on this codebase):
26+
27+
1. **Size first.** `git diff <base> --numstat | sort -rn | head -20` — the
28+
biggest churn concentrates the smells. Start there, not alphabetically.
29+
2. **Comment density.** Simple code is mostly self-explanatory. A paragraph
30+
of comments explaining _why something works_ means the mechanism is too
31+
clever — fix the mechanism and the comments evaporate (a test-override
32+
registry that needed the same 6-line explanation in five files collapsed
33+
into ordinary constructor defaults that needed none). One-liners that
34+
restate the next line (`// save the event`) are pure deletions. Keep only
35+
comments stating a constraint the code cannot show (transaction
36+
boundaries, spec references, cross-realm quirks).
37+
3. **Mock volume and shape.** Lots of mocks = missing seam. `mock.module` on
38+
a shared singleton is the worst offender: it mutates the process-wide
39+
module registry, cannot be un-leaked for already-linked consumers, and
40+
fails order-dependently (Linux CI orders files differently than macOS).
41+
The fix is never better mock bookkeeping — it's a constructor/parameter
42+
default (`constructor(getStore = getOfflineDataStore)`) and a plain fake.
43+
Search: `rg "mock.module" --glob "*.test.*"` and treat each hit on a
44+
multi-consumer module as a candidate seam.
45+
4. **Test-only hooks in production code.** Any `setXForTests`,
46+
`testOverrides`, `__test__` export, or `if (process.env.NODE_ENV ===
47+
"test")` branch in a production module means tests are steering design
48+
from the wrong side. Replace with dependency injection at the caller.
49+
5. **Casts.** `as unknown as`, `as any`, and `Parameters<...>[n]` casts in
50+
tests mean the fake isn't shaped like the real collaborator — usually
51+
because the seam is a whole module instead of a small interface. In
52+
production code, casts at construction sites of branded/validated types
53+
are tolerable; casts that silence a structural mismatch are debt.
54+
Search: `rg "as unknown as"`.
55+
6. **Copy-pasted scaffolding.** The same open/do/close or
56+
iterate/write/guard shape repeated across siblings (session +
57+
transaction + endSession in four service methods; seven cache writers
58+
each re-implementing iterate-entries → setQueryData → null-guard). Extract
59+
ONE helper owning the shape; the variants become one-liners.
60+
7. **Dead and unreachable code.** Every exported symbol in the diff:
61+
`rg "<name>" --type ts` across all packages — zero call sites means
62+
delete (including its tests). Every defensive branch: read the callee —
63+
a `!x.isActive` check after a query that already filters `isActive: true`
64+
can never fire and only misleads readers about invariants.
65+
8. **Coupling via hidden shared state.** Tests that pass alone but fail in
66+
suite (or only on CI) point at module-level mutable state that no reset
67+
registry covers. The fix is to register the reset or inject the state —
68+
never to reorder tests or widen timeouts.
69+
9. **Bridges and adapters.** Transition scaffolding is legitimate, but audit
70+
it: every adapter function must have a live call site (`rg` each export),
71+
and the file should shrink release over release, not grow.
72+
73+
Balance — restraint is part of the skill. Don't unify code that only looks
74+
similar (two query builders with genuinely different shapes stay separate).
75+
Don't break a repo-wide convention locally just because it repeats (the
76+
per-handler try/catch shape in controllers is uniform across the codebase;
77+
"fixing" one file creates inconsistency, not simplicity). When you decide
78+
NOT to change something you inspected, say so in the report with the reason
79+
— "considered, left alone because X" is as valuable as a diff.
80+
2281
## Before Making Changes
2382

2483
1. **Necessity**: does this change only address what's required?
@@ -157,13 +216,13 @@ const getLabel = (type: string) => LABELS[type] ?? "Unknown";
157216

158217
## Complexity Thresholds
159218

160-
| Metric | Prefer | Flag | Action |
161-
| -------------------- | ---------- | ---------- | ---------------------------- |
162-
| Function length | < 20 lines | > 30 lines | Split or extract |
163-
| Nesting depth | ≤ 2 levels | > 3 levels | Guard clauses, early returns |
164-
| Parameters | ≤ 3 | > 4 | Options object or context |
165-
| Conditional branches | ≤ 3 | > 4 | Record lookup or polymorphism|
166-
| Similar blocks | 0 | 2+ | Extract and parameterize |
219+
| Metric | Prefer | Flag | Action |
220+
| -------------------- | ---------- | ---------- | ----------------------------- |
221+
| Function length | < 20 lines | > 30 lines | Split or extract |
222+
| Nesting depth | ≤ 2 levels | > 3 levels | Guard clauses, early returns |
223+
| Parameters | ≤ 3 | > 4 | Options object or context |
224+
| Conditional branches | ≤ 3 | > 4 | Record lookup or polymorphism |
225+
| Similar blocks | 0 | 2+ | Extract and parameterize |
167226

168227
## Verify
169228

docs/architecture/event-and-task-domain-model.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
The event domain is the most cross-cutting part of Compass. Read this before changing event shape, recurrence logic, sync behavior, or local persistence.
44

5-
## Target Contracts (sub-calendar v1, pre-cutover)
6-
7-
Strict calendar-owned contracts exist alongside the legacy model and become the
8-
runtime shape at the sub-calendar v1 cutover. Until then the legacy sections
9-
below describe live behavior; the contracts describe where every consumer is
10-
headed.
5+
## Current Contracts (sub-calendar v1)
6+
7+
The runtime now uses the strict calendar-owned contracts everywhere: Mongo
8+
storage, the HTTP API, SSE, IndexedDB, and the web data/state layers. The
9+
legacy sections further down describe the RETIRED pre-cutover model; they
10+
remain only until the last web components stop converting shapes through
11+
`packages/web/src/events/queries/event.legacy-bridge.ts`, after which the
12+
legacy types are deleted.
1113

1214
- `packages/core/src/types/domain-primitives.ts` — branded ids, `DateOnly`,
1315
`DateTime` (RFC 3339 with offset), `TimeZone`, `SortOrder`, `RRule`.

docs/self-hosting/event-migration-runbook.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ Neither migration touches the legacy `event` collection, and nothing starts
1616
reading `event_new` until the separate runtime-cutover release. Running these
1717
early is safe; the app keeps serving from legacy data.
1818

19+
## Rollout strategy (sub-calendar v1)
20+
21+
The v1 rollout is staging-first (decision A36 in
22+
`handoff/someday/master-doc.md`):
23+
24+
1. **Staging cuts over now.** Run the full procedure below (migrate → verify →
25+
pause → rename → deploy) against staging when the runtime-cutover release
26+
merges. Merges to `main` auto-deploy staging, so every subsequent v1 packet
27+
lands there continuously.
28+
2. **Production stays on the pre-cutover release** the whole time. The
29+
`Deploy Production` workflow is manual (`workflow_dispatch`) — do not run
30+
it while v1 work is in flight. Production keeps writing legacy `event`
31+
data; that is fine, because the backfill is rerunnable and the final
32+
production migration run happens inside the production cutover window.
33+
3. **Production cuts over once, at the end**: after every packet is merged
34+
and the plan `09` verification gates pass on staging, execute this runbook
35+
against production a single time, then run the `Deploy Production` action.
36+
That is the entire production rollout.
37+
38+
Dev databases follow staging: run the migrations + rename locally (or wipe the
39+
dev database) as soon as you work on a post-cutover branch.
40+
1941
## Preflight
2042

2143
1. **Back up first.** Follow [Back up & restore](./backup-and-restore.md) in

e2e/utils/event-test-utils.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ type SomedaySection = "week" | "month";
44

55
const LOCAL_DB_NAME = "compass-local";
66

7+
// LocalEventRecord (B13) nests the event under `.event`; title lives at
8+
// `.event.content.title` (kind "details") and the schedule is a discriminated
9+
// union ("timed"/"allDay" use start/end, "someday" uses anchorDate) rather
10+
// than flat startDate/endDate columns on the row.
711
export const getSavedEventsByTitle = (page: Page, title: string) =>
812
page.evaluate(async (eventTitle) => {
913
const db = await new Promise<IDBDatabase>((resolve, reject) => {
@@ -13,22 +17,43 @@ export const getSavedEventsByTitle = (page: Page, title: string) =>
1317
request.onsuccess = () => resolve(request.result);
1418
});
1519

20+
type LocalEventRecord = {
21+
event: {
22+
content: { kind: string; title?: string };
23+
schedule:
24+
| { kind: "timed" | "allDay"; start: string; end: string }
25+
| { kind: "someday"; anchorDate: string };
26+
};
27+
};
28+
1629
try {
17-
return await new Promise<
18-
{ endDate?: string; startDate?: string; title?: string }[]
19-
>((resolve, reject) => {
20-
const transaction = db.transaction("events", "readonly");
21-
const request = transaction.objectStore("events").getAll();
22-
23-
request.onerror = () => reject(request.error);
24-
request.onsuccess = () => {
25-
resolve(
26-
request.result.filter(
27-
(event: { title?: string }) => event.title === eventTitle,
28-
),
29-
);
30-
};
31-
});
30+
const records = await new Promise<LocalEventRecord[]>(
31+
(resolve, reject) => {
32+
const transaction = db.transaction("events", "readonly");
33+
const request = transaction.objectStore("events").getAll();
34+
35+
request.onerror = () => reject(request.error);
36+
request.onsuccess = () => resolve(request.result);
37+
},
38+
);
39+
40+
return records
41+
.filter(
42+
(record) =>
43+
record.event.content.kind === "details" &&
44+
record.event.content.title === eventTitle,
45+
)
46+
.map(({ event }) => ({
47+
title: event.content.title,
48+
startDate:
49+
event.schedule.kind === "someday"
50+
? event.schedule.anchorDate
51+
: event.schedule.start,
52+
endDate:
53+
event.schedule.kind === "someday"
54+
? event.schedule.anchorDate
55+
: event.schedule.end,
56+
}));
3257
} finally {
3358
db.close();
3459
}

handoff/someday/03-event-runtime-cutover.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
Move all core, backend, scripts, and web runtime paths from the legacy
66
user-owned event document to the final strict calendar-owned event contracts.
77

8+
Status (2026-07-11): substantially implemented on
9+
`refactor/event-runtime-cutover`. Backend, storage, HTTP/SSE contracts,
10+
IndexedDB, and the web data/state layers are fully cut over with all suites
11+
and the full local e2e run green. Remaining before this packet's boxes can be
12+
checked: dissolve `packages/web/src/events/queries/event.legacy-bridge.ts` by
13+
converting the component/view layer (draft store, grid drag/resize, forms,
14+
sidebar, shortcuts) off `Schema_Event`, then delete the legacy web event
15+
types. `event_new.types.ts` stays as long as the frozen 2025 migrations
16+
import it. Scope-`this` provider sync for series occurrences is deferred to
17+
`05` (recorded there).
18+
819
Depends on: `01-domain-contracts.md`, `02-safe-event-data-migration.md`.
920

1021
## Design constraint

handoff/someday/05-calendar-aware-crud.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ Depends on: `04-initial-multi-calendar-import.md`.
4747
4. Pass the resolved provider calendar through Compass-to-Google planning and
4848
effects. Provider success updates the event's provider reference in the same
4949
owning calendar.
50+
Known `03` gap this packet closes: scope-`this` edits/deletes on a synced
51+
series occurrence do not propagate to Google (the `03` pipeline has no way
52+
to resolve Google's per-instance event id for Compass-created series).
53+
Imported occurrences already carry their instance id in
54+
`externalReference`; Compass-created series need an `events.instances`
55+
lookup before the per-occurrence `events.patch`/delete.
5056
5. Scope Google-to-Compass matching by calendar plus provider event id so equal
5157
event ids in different calendars cannot collide.
5258
6. Enforce read-only roles before optimistic writes reach Google. Return a

handoff/someday/09-v1-release-hardening.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ and one `freeBusyReader`) plus the Compass-local calendar:
7272

7373
## Migration and rollback rehearsal
7474

75+
Per A36 the production cutover happens exactly once, only after every gate in
76+
this file passes on staging; until then the `Deploy Production` action is not
77+
run and production stays on the pre-cutover release.
78+
7579
1. Restore a sanitized production-shaped backup into staging.
7680
2. Record source counts/category hashes and create a fresh backup.
7781
3. Run forward migrations without modifying old collections.

0 commit comments

Comments
 (0)