@odla-ai/chapter 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,505 @@
1
+ # Adoption runbook: convert an existing site to Chapter
2
+
3
+ Audience: an LLM or coding agent converting an existing or deployed website,
4
+ Worker, database, auth flow, or operating membership product.
5
+
6
+ Read this file completely before changing the application. Also read the
7
+ installed package `README.md`, `package.json` exports, and `dist/*.d.ts`. The
8
+ existing product stays live until measured Chapter parity and cutover are
9
+ approved.
10
+
11
+ If the repository is empty and no operating state exists, use
12
+ [`greenfield.md`](./greenfield.md).
13
+
14
+ ## Completion means conversion, not installation
15
+
16
+ The following are canary evidence only:
17
+
18
+ - installing `@odla-ai/chapter`;
19
+ - writing `defineChapter()` or rendering `ChapterAdmin`;
20
+ - provisioning an ODLA tenant;
21
+ - serving a login screen or one Chapter API route;
22
+ - deploying a parallel Worker with copied sample data.
23
+
24
+ Adoption is complete only when the real product routes and journeys use Chapter
25
+ as the primary implementation, live data is reconciled and continuously
26
+ handled, production reads and writes have crossed explicit gates, and both
27
+ runtime and data rollback remain proven.
28
+
29
+ ## Non-negotiable boundaries
30
+
31
+ 1. Never mutate, reset, delete, detach, or repoint current production resources
32
+ during development adoption. Preserve Workers, Pages projects, domains,
33
+ databases, R2/KV, queues, Workflows, cron, webhooks, Clerk, Stripe, email,
34
+ Google calendars, source repositories, and immutable history.
35
+ 2. Never print or commit secrets. Dev and production use separate credentials
36
+ and explicitly targeted Worker environments.
37
+ 3. Preserve existing public URLs, content, SEO, header/navigation, responsive
38
+ layout, theme behavior, accessibility, and branded assets unless the human
39
+ approves a product change. Framework replacement is not redesign authority.
40
+ 4. Preserve data semantics before deleting code. Chapter schema, routes, sends,
41
+ auth, account, payment, and scheduling defaults are hypotheses until parity
42
+ tests prove them.
43
+ 5. Runtime rollback and data rollback are different. Restoring an old Worker
44
+ cannot recover writes accepted after Chapter became authoritative.
45
+ 6. No silent fallback. A Chapter-primary failure is explicit and measured; a
46
+ legacy fallback, if approved, is named, time-bounded, and visible.
47
+ 7. Keep ODLA rules default-deny and every admin/provider mutation
48
+ server-mediated.
49
+
50
+ ## Phase 0 — Freeze the operating baseline
51
+
52
+ Create or extend root `MIGRATION.md`; never replace its history. Record state
53
+ `inventoried` only after the human approves the baseline.
54
+
55
+ ### Inventory the product
56
+
57
+ - every public, member, admin, API, webhook, health, asset, feed, and 404 route;
58
+ - current production/dev origins, Worker/Pages names and version ids, routes,
59
+ DNS, compatibility settings, build automation, and rollback versions;
60
+ - D1/SQL/ODLA/KV/R2/files and every reader/writer;
61
+ - queues, Workflow definitions, cron, provider webhooks, retry/outbox behavior;
62
+ - Clerk app/instances, JWT claims, roles/allowlists, redirects, and first-admin
63
+ process;
64
+ - Stripe products/prices/webhooks/idempotency, Google calendars, Email Service;
65
+ - current schema/rules, row counts, earliest/latest timestamps, natural keys,
66
+ relations, projections, and owner-edited fields;
67
+ - public/private content boundaries and every raw HTML/Markdown/inline-JSON
68
+ render sink;
69
+ - telemetry, freshness/error alerts, deploy commands, and CI auto-deploys.
70
+
71
+ ### Freeze visual and behavior evidence
72
+
73
+ Capture representative production screenshots and measured layouts at 390,
74
+ 768, 1280, and 1440 pixels in every supported theme. Record:
75
+
76
+ - header height, wordmark/nav alignment, responsive breakpoints, page geometry;
77
+ - form fields/labels/errors, Clerk containment, tables/cards/rails, loading,
78
+ empty, error, unauthorized, and 404 states;
79
+ - full public route/link inventory, titles, descriptions, canonical URLs,
80
+ structured data, robots/sitemap/feed behavior;
81
+ - join, payment, booking, member, admin, CRM, email, and refund journeys.
82
+
83
+ Do not rely on visual memory. These fixtures are acceptance authority until a
84
+ human explicitly approves a redesign.
85
+
86
+ ### Define migration budgets
87
+
88
+ Record accepted count/hash differences, maximum sync lag/newest-data age, route
89
+ and status parity, latency/error budget, visual tolerances, identity merge
90
+ policy, deterministic synthetic user/application ids, and an observation
91
+ window. Record both rollback plans before any Chapter write.
92
+
93
+ Maintain a migration matrix:
94
+
95
+ | Track | Legacy authority | Chapter/ODLA dev target | Current reads | Current writes | Evidence/freshness | Rollback | State |
96
+ | --- | --- | --- | --- | --- | --- | --- | --- |
97
+ | runtime/bindings | | | | | | | |
98
+ | membership data/CRM | | | | | | | |
99
+ | jobs/webhooks/outboxes | | | | | | | |
100
+ | Clerk/auth | | | | | | | |
101
+ | Stripe/payments | | | | | | | |
102
+ | calendar/booking | | | | | | | |
103
+ | email | | | | | | | |
104
+ | public/member/admin UI | | | | | | | |
105
+ | o11y/alerts | | | | | | | |
106
+
107
+ Allowed states: `not-started`, `canary`, `parallel`, `verified`,
108
+ `primary-dev`, `primary-prod`, `retired`, or `n/a` with a reason.
109
+
110
+ ## Phase 1 — Model Chapter without deleting anything
111
+
112
+ Install the version-matched agent guidance and inspect the automation boundary:
113
+
114
+ ```sh
115
+ npx @odla-ai/cli@0.17.1 setup
116
+ npx @odla-ai/cli@0.17.1 capabilities --json
117
+ ```
118
+
119
+ Pin the exact known-good package matrix from the installed Chapter README.
120
+ For this runbook, first require
121
+ `npm view @odla-ai/chapter@0.20.2 version` to succeed. Install every runtime,
122
+ toolchain, CLI, and security package with the exact versions in that README;
123
+ never use `latest`, a range, or `--legacy-peer-deps`.
124
+
125
+ Write `src/chapter.config.mjs` from the inventory. Make every behavior decision
126
+ explicit:
127
+
128
+ - `mode`, `services`, complete light/dark brand tokens;
129
+ - price/currency/interval and owner-approved policy copy;
130
+ - email addresses/templates/debug redirect and send trigger;
131
+ - scheduling/timezone and pipeline stages/bookable/approvable subsets;
132
+ - exact application fields, caps, disclaimer requirement, Clerk-visible
133
+ `profileFields`, and CRM-projected `crmFields`;
134
+ - claim/table auth, ladder/super-admin tier;
135
+ - account side effect (`none`, `create`, or emailed `invite`);
136
+ - approve/refund policy and any network targets/field allowlists.
137
+
138
+ ### Schema/rule parity gate
139
+
140
+ Before deleting old schema/rules/provisioning:
141
+
142
+ 1. freeze the legacy schema and rules as reviewed test fixtures;
143
+ 2. normalize only approved representational differences;
144
+ 3. compare `createChapterIntegration(chapter)` namespaces, attrs, links, rules,
145
+ and seeds to the fixtures;
146
+ 4. fail on missing fields, widened rules, cardinality changes, renamed natural
147
+ keys, new client-readable data, or unapproved seed differences;
148
+ 5. record every intentional delta and migration mapping in `MIGRATION.md`.
149
+
150
+ `createChapterIntegration()` inserts the group seed only when absent. Existing
151
+ runtime owner edits do not update from later config changes. Compare the current
152
+ group row field by field, classify ownership, and write an explicit migration
153
+ for approved differences.
154
+
155
+ ### Behavior-delta gate
156
+
157
+ At minimum audit:
158
+
159
+ - services default (`db`, `calendar`, `o11y`);
160
+ - per-field and request-body caps;
161
+ - account default `none` and outbound effects of `create`/`invite`;
162
+ - stock `JoinIsland` generates a fresh `submissionId` on each invocation, so an
163
+ ambiguous retry after a lost response is not idempotent even though a
164
+ concurrent click is disabled;
165
+ - Clerk-client-readable `public_metadata.profile`;
166
+ - application-to-CRM projection fields;
167
+ - required disclaimer acknowledgement and `disclaimerAckAt`;
168
+ - initial/bookable/approvable stages and never-backwards transitions;
169
+ - when each lifecycle email fires;
170
+ - Chapter recognizes only the literal `prod` environment as live mail; the
171
+ CLI-valid alias `production` follows the non-production fail-safe;
172
+ - approve promotion/email and refund/subscription behavior;
173
+ - Chapter route names and member/admin routing;
174
+ - Chapter's insert-only group seed and runtime-edited fields;
175
+ - payment readiness versus actual webhook readiness;
176
+ - calendar's HTTP-200 `schedulingReady: false` degradation.
177
+
178
+ The stock `applications` namespace is fixed. Its configurable form strings are
179
+ limited to `firstName`, `lastName`, `email`, `referral`, `referralName`,
180
+ `whoYouAre`, `linkedin`, `message`, `phone`, and `state`; `focus` and
181
+ `disclaimerAck` are special. An unknown configured field can pass config
182
+ validation and then fail at the database. Preserve a host schema/submit route
183
+ for every legacy field that cannot be mapped without loss.
184
+
185
+ The current `prices.currency` and `prices.interval` values do not control
186
+ storage, join config, formatting, or the Stripe subscription. Treat the live
187
+ Stripe Price as authoritative and make provider-side amount/currency/interval
188
+ equality a cutover gate.
189
+
190
+ Keep one test per accepted behavior decision so a package upgrade cannot change
191
+ the site silently.
192
+
193
+ ### Make the dev descriptor executable
194
+
195
+ Before Phase 2, create or adapt `odla.config.mjs`. A repository with no ODLA
196
+ config may use the installed exact CLI's `init` command as a starting point,
197
+ but the reviewed result must declare:
198
+
199
+ - the existing product's one app id/name and only `envs: ["dev"]`;
200
+ - explicit `chapter.services`;
201
+ - `integrations: [createChapterIntegration(chapter)]` plus any compatible,
202
+ non-overlapping host integrations;
203
+ - the intended dev Clerk publishable key, calendar ids, o11y service, local
204
+ credential paths, and a null/off-route dev link.
205
+
206
+ Do not provision both a legacy inline schema and the Chapter integration for the
207
+ same namespaces. After the schema/rule parity gate passes, freeze the legacy
208
+ descriptor as a test fixture and make the reviewed Chapter integration the
209
+ active dev descriptor. Production configuration remains absent.
210
+
211
+ Create an off-route dev Worker entry using
212
+ `chapterWorker({ chapter, routes: legacyRoutes })`, wrapped with o11y, and an
213
+ explicit `wrangler ... --env dev` target. Keeping all legacy handlers as host
214
+ routes at this point changes no route ownership; it only makes the canary and
215
+ Chapter data surface executable.
216
+
217
+ ### Add a fail-closed migration readiness route
218
+
219
+ The built-in `/api/health` is only `{ "ok": true }`; it does not touch the
220
+ database. Add an admin-only host route or executable command that returns `503`
221
+ until all of these match the acceptance manifest:
222
+
223
+ - literal tenant/app/env and Chapter/package release;
224
+ - expected schema/rules digest and required group/config rows;
225
+ - reconciled counts, natural-key collisions, relation integrity, and newest
226
+ source timestamp within the freshness budget;
227
+ - zero unhandled quarantines and bounded/zero pending outbox work;
228
+ - healthy continuous sync and the Chapter-primary-to-legacy rollback mirror.
229
+
230
+ Return only redacted counts/digests. This gate is allowed to fail during initial
231
+ canary work; it must be mechanically green before switching either reads or
232
+ writes, and after every rollback recovery.
233
+
234
+ ## Phase 2 — Create an isolated dev canary
235
+
236
+ Run:
237
+
238
+ ```sh
239
+ npx odla-ai doctor
240
+ npx odla-ai provision --dry-run
241
+ ```
242
+
243
+ The dev topology must be distinct from production:
244
+
245
+ - `envs: ["dev"]`, tenant `<app-id>--dev`;
246
+ - explicit dev Worker name and `wrangler ... --env dev`;
247
+ - no production route, DNS, database binding, queue, Workflow, cron, webhook,
248
+ Git writer, Stripe live resource, email recipient, or Google write authority;
249
+ - `debugEmail` enabled and Stripe test mode only;
250
+ - dev Clerk `pk_test_...` from the intended workspace/application;
251
+ - o11y wrapped around the real canary handler with safe low-cardinality labels.
252
+
253
+ Show the dry-run target, schema/rules, services, link, and secret recipients.
254
+ After the human approves the device code:
255
+
256
+ ```sh
257
+ npx odla-ai provision --email <existing-odla-account> --write-dev-vars --push-secrets
258
+ npm run build
259
+ npx wrangler deploy --env dev
260
+ npx odla-ai smoke --env dev
261
+ ```
262
+
263
+ If credential transfer alone must be retried, use the non-rotating
264
+ `npx odla-ai secrets push --env dev`; do not reprovision or rotate by default.
265
+ Verify the deployed Chapter health marker, a database-backed read, and an
266
+ unauthenticated private-route rejection. Record the URL and Cloudflare version
267
+ id. A canary advances matrix rows only to `canary`; the migration-readiness
268
+ route is expected to remain `503` until Phase 3 evidence passes.
269
+
270
+ ## Phase 3 — Adopt data with replay and reconciliation
271
+
272
+ Skip historical backfill only when the human-approved inventory proves the
273
+ site has no operating data. Otherwise:
274
+
275
+ 1. Write a checked-in mapping for every legacy table/collection/file and
276
+ relation: natural key, target namespace, attrs, omissions/nulls, edge
277
+ direction, ownership, tombstone/deletion behavior, and provenance.
278
+ 2. Freeze a source snapshot or bounded high-water mark. Record counts,
279
+ earliest/latest timestamps, and a digest/immutable id.
280
+ 3. Backfill in bounded deterministic chunks using stable mutation ids.
281
+ Persist checkpoints and a rejection ledger; each input is written, skipped
282
+ with reason, or failed/retryable.
283
+ 4. Replay from an earlier checkpoint and prove no duplicate applications,
284
+ people, companies, meetings, email log entries, relations, or side effects.
285
+ 5. Generate a redacted reconciliation report: counts, required attrs, duplicate
286
+ natural keys, dangling relations, identity collisions, status/price/role
287
+ differences, and newest timestamps.
288
+ 6. Preserve human-owned fields. Automated projection omits fields it does not
289
+ own rather than reading and writing stale copies.
290
+
291
+ ### Continuous operation
292
+
293
+ Choose one durable event boundary for production-shaped changes. New legacy
294
+ applications, payments, role changes, meetings, CRM edits, and emails must reach
295
+ the dev Chapter projection through a measured outbox/queue/replay process.
296
+ Record oldest pending age, attempts, quarantine, and source-to-target lag.
297
+
298
+ Backfill alone is not conversion. Advance to `continuously-synced` only after
299
+ normal schedules/webhooks are observed and failures replay from durable state.
300
+
301
+ ### Shadow reads
302
+
303
+ For list/detail/search/admin dashboard/member status/join config, read both
304
+ legacy and Chapter in dev while still serving the legacy response. Compare
305
+ normalized status, ids, ordering, counts, fields, relations, price/policy, and
306
+ freshness. Log bounded diffs, never private row bodies.
307
+
308
+ At separate human checkpoints:
309
+
310
+ 1. switch dev reads to Chapter and make failures explicit;
311
+ 2. switch accepted dev writes to Chapter;
312
+ 3. durably mirror Chapter-primary writes back to the legacy rollback authority;
313
+ 4. simulate ODLA unavailable and rollback-mirror unavailable.
314
+
315
+ Every authenticated/admin mutator must use this authority contract or be
316
+ disabled. A separate Chapter demo route does not satisfy primary-dev.
317
+
318
+ ## Phase 4 — Adopt the Worker and routes in safe order
319
+
320
+ Do not replace schema, worker, routes, and UI in one unreviewable change.
321
+
322
+ 1. Re-assert that the active dev descriptor is the reviewed
323
+ `createChapterIntegration(chapter)` from Phase 1 and that the legacy
324
+ provisioner remains only as a parity fixture.
325
+ 2. Start from the off-route `chapterWorker({ chapter, routes })` canary. Host
326
+ routes run before built-ins and receive Chapter's existing context
327
+ (`verifyUser`, `makeDb`, `roleFor`, `isAdmin`); do not verify JWTs twice.
328
+ 3. Initially keep every bespoke endpoint as a host route. Alias legacy URLs to
329
+ Chapter behavior instead of breaking clients.
330
+ 4. Compare built-in and legacy behavior route by route; only then remove a host
331
+ route that Chapter fully owns.
332
+ 5. Keep static fallback and API handling explicit so unknown APIs cannot fall
333
+ through to a successful SPA document.
334
+
335
+ Test public/private method policies, body limits, idempotency, auth 401 vs 403,
336
+ safe redirects, webhook signatures/replay, scheduling conflict,
337
+ replay-deduplicated and concurrent email sends, and unavailable-provider
338
+ degradation. Chapter's email log prevents a later replay after success; it does
339
+ not serialize two concurrent sends, so preserve a stricter existing outbox or
340
+ provider-idempotency contract.
341
+
342
+ ## Phase 5 — Adopt UI without losing the product
343
+
344
+ ### Preserve first
345
+
346
+ Render the Chapter component beside the current implementation in a dev-only
347
+ fixture. Compare markup/behavior and the Phase 0 screenshots before deletion.
348
+
349
+ - Import `@odla-ai/ui/themes/salt/app.css` before host CSS.
350
+ - Map the legacy design tokens into `brand.palette` and `paletteDark`; do not
351
+ carry an unbounded parallel theme system.
352
+ - Public pages remain site-owned. Preserve their content hierarchy, imagery,
353
+ URLs, SEO, and responsive geometry.
354
+ - Replace join orchestration with `JoinIsland`, member behavior with
355
+ `MembersArea`, and admin with `<ChapterAdmin chapter={chapter}/>` only after
356
+ field/auth/action parity.
357
+ - The host still supplies member CSS and the Clerk wrapper around
358
+ `MembersArea`; Chapter does not ship a whole public stylesheet.
359
+ - Scope host form styles. Bare `input`, `button`, `select`, `textarea`, `label`,
360
+ or global box-model rules must not corrupt Clerk/vendor DOM.
361
+
362
+ ### Required visual acceptance
363
+
364
+ At 390, 768, 1280, and 1440 pixels in every theme, compare:
365
+
366
+ - identical product header/navigation position between public/member/admin;
367
+ - wordmark, text baseline, theme control, active state, and hover/focus;
368
+ - join fields/errors, payment and slot picker, Clerk sign-in containment;
369
+ - member cards/actions and every admin section/table/drawer;
370
+ - no clipped labels, card text escape, accidental document scroll, or
371
+ horizontal overflow;
372
+ - loading, empty, error, unauthorized, 404, and reduced-motion states.
373
+
374
+ The human approves deployed screenshots/browser behavior. Passing CSS tests is
375
+ not visual parity.
376
+
377
+ ## Phase 6 — Provider and authorization parity
378
+
379
+ ### Clerk
380
+
381
+ Use the same Clerk application/workspace. Chapter claim mode requires session
382
+ claims for both email and role; hub table mode uses lowercase `admins` rows.
383
+ Prove existing allowed/forbidden users, role ladder, super-admin behavior,
384
+ malformed/expired sessions, sign-out, and return targets.
385
+
386
+ If the legacy verifier enforces JWT `aud` or `azp`, keep that host verifier in
387
+ front of Chapter. This Chapter release verifies issuer but does not expose or
388
+ enforce the CLI-provisioned Clerk audience. Add wrong-audience and
389
+ wrong-authorized-party fixtures; do not cut over auth until the legacy contract
390
+ still rejects them.
391
+
392
+ When Chapter needs server-side account/role operations, set the named
393
+ app-readable `clerk_secret_key` through:
394
+
395
+ ```sh
396
+ npx odla-ai secrets set clerk_secret_key --env dev --stdin
397
+ ```
398
+
399
+ Do not substitute `secrets set-clerk-key`; it writes the platform-reserved
400
+ `$clerk_secret`, which Chapter cannot read under the named key. The generic
401
+ command does not validate Clerk instance prefixes: independently confirm dev
402
+ uses `sk_test_`. Never put `sk_live_` in dev.
403
+
404
+ ### Stripe
405
+
406
+ Use test resources and an isolated dev webhook. Store `stripe_secret_key` and
407
+ `stripe_webhook_secret` write-only; reconcile group `stripePublishableKey` and
408
+ `stripePriceId`. Read that exact Stripe Price from Stripe and prove its first
409
+ charge, recurring amount, currency, and interval match the rendered and
410
+ approved contract. `paymentsReady: true` proves neither that equality nor
411
+ webhook readiness; Chapter's built-in money formatter is USD/dollar-specific in
412
+ this release. Preserve a host UI/route for non-USD or mismatched pricing. Test
413
+ successful payment, duplicate webhook, failure, refund, and subscription
414
+ cancellation before switching the UI.
415
+
416
+ ### Calendar and email
417
+
418
+ Complete dev Google booking consent, reconnecting any grant from the retired
419
+ read-only mirror, then prove actual slots, booking, rebooking, cancellation,
420
+ and invitation behavior. `schedulingReady: false` with HTTP 200 is a failed
421
+ readiness gate, not a pass.
422
+
423
+ Use a verified dev sender plus `debugEmail`; prove every lifecycle template,
424
+ recipient redirect, replay deduplication, concurrent-send behavior, and the
425
+ selected account invitation effect. Never let dev send to imported members.
426
+
427
+ ## Phase 7 — Full deployed-dev journey
428
+
429
+ Check in an executable acceptance manifest with:
430
+
431
+ - actual dev URLs and Worker version ids;
432
+ - deterministic synthetic member/application ids;
433
+ - expected routes/statuses, schema/counts/freshness, and Chapter backend marker;
434
+ - expected migration-readiness inputs and a `200` only when each is green;
435
+ - expected account, payment, booking, email, CRM, and admin outcomes;
436
+ - expected o11y service/release and safe trace;
437
+ - replay and failure-injection outcomes;
438
+ - visual viewport/theme fixtures.
439
+
440
+ Run the real journey:
441
+
442
+ 1. browse every public route/deep link and verify SEO/404/method behavior;
443
+ 2. submit the configured join form, including consent;
444
+ 3. execute Stripe test payment and authoritative webhook;
445
+ 4. book/rebook the intro call and receive the debug-routed email;
446
+ 5. sign in as provisional/member/admin and use the real member/admin pages;
447
+ 6. perform every admin mutation family and verify Chapter-primary audit plus
448
+ legacy rollback mirror;
449
+ 7. replay the event and prove no duplicate row, charge, booking, account, or
450
+ email;
451
+ 8. inject ODLA/provider/mirror failure, observe explicit failure/retry, recover;
452
+ 9. verify freshness/continuous-sync budget and visual parity;
453
+ 10. require the fail-closed migration-readiness gate to return `200`;
454
+ 11. follow safe o11y evidence across the real Worker.
455
+
456
+ `doctor`, unit tests, build, CLI smoke, a login page, and a human-looking admin
457
+ screen are necessary but not sufficient.
458
+
459
+ ## Phase 8 — Production parallel run and cutover
460
+
461
+ Do not add production until the human approves the full dev report and both
462
+ rollback plans.
463
+
464
+ 1. Add only the literal `prod` environment using the production instance of the
465
+ same Clerk app, Stripe live resources, approved email sender, calendars,
466
+ links, and distinct secret targets. Do not use the CLI-valid alias
467
+ `production`: Chapter would suppress or debug-redirect lifecycle mail.
468
+ 2. Run production build/tests/security scan, `doctor`, and
469
+ `provision --dry-run`; inspect CI/Workers Builds so merging cannot
470
+ auto-deploy the legacy production target unexpectedly.
471
+ 3. After explicit approval, provision with `--yes` and deploy a production
472
+ Chapter candidate off-route. Record its version id.
473
+ 4. Repeat historical backfill, reconciliation, continuous sync, shadow reads,
474
+ provider checks, and a production-safe synthetic journey against production
475
+ source data. Dev data is not a production backfill.
476
+ 5. At one checkpoint, require the fail-closed readiness gate to return `200`,
477
+ then switch production reads. Verify freshness, auth, visual behavior, and
478
+ explicit failure handling while legacy writes continue.
479
+ 6. At a second checkpoint, record a write high-water mark, require readiness
480
+ again, and make Chapter authoritative for writes. Keep the durable legacy
481
+ rollback mirror and continuously reconcile it.
482
+ 7. Abort when write loss, unbounded lag, unexplained diff, auth failure,
483
+ freshness breach, or accepted error budget is crossed.
484
+
485
+ ### Runtime rollback
486
+
487
+ Restore the recorded known-good Worker version plus routes, bindings, triggers,
488
+ and domains. Verify representative routes and health. Keep Chapter resources
489
+ and telemetry for investigation; do not delete them.
490
+
491
+ ### Data rollback
492
+
493
+ Pause/drain new Chapter-primary work; enumerate accepted writes since the
494
+ checkpoint; reverse-project missing writes with stable idempotency keys;
495
+ reconcile queues, counts, timestamps, identities, status, meetings, charges,
496
+ and email; only then report legacy reads healthy. Never reset either store.
497
+
498
+ Keep both rollback capabilities through the approved observation window
499
+ (normally at least 72 hours). Retire each legacy reader, writer, binding,
500
+ webhook, job, and secret only in a separate human-approved change with a fresh
501
+ journey afterward.
502
+
503
+ Close `MIGRATION.md` only when every applicable track is `primary-prod` or
504
+ `retired`, the observation window passes, and the human confirms the existing
505
+ product—not merely a Chapter canary—has been converted.