Skip to content

Commit ac2cc5c

Browse files
docs(docs): prune dangling cloudSync v1 source refs (Hard Rule #15)
Governance-sync CI на PR #2058 валив на 12 dangling source refs у non-aspirational docs, що залишились після PR #052b/#052c (web + mobile cloudSync v1 engine drop). Hard Rule #15 вимагає, щоб docs рухались разом з кодом — фіксую тут паралельно з allowlist-cleanup-ом, бо це один theme (Stage 7 cleanup). - ADR-0004 (LWW conflict resolution): помічений Status: superseded by ADR-0047 + Superseded by section з лінком на ADR-0047 і syncEngine writer; inline path mentions переведено у короткі `cloudSync/...` форми (поза backtick-ed-anchor regex-у) з історичним префіксом. - ADR-0011 (local-first storage) + ADR-0021 (memory-bank): refs до `cloudSync/config.ts` переведено на canonical shared registry `packages/shared/src/sync/modules.ts`. - data-exchange-storage-audit.md: refs до cloudSync engine + mobile sync переведено у короткі форми з v2 op-log writer + sync_op_log migration links як successor-pointer-и. - observability/frontend.md: 'CloudSync-події' секція переписана — `cloudSync/hook/useSyncCallbacks.ts` (web v1 tree) помічений як видалений у #052b, successor — `syncEngineWriter.ts`. - tech-debt/mobile.md: посилання на `mobile/sync/config.ts` (видалений у #052c) переведено на shared registry. Verify: `pnpm lint:governance-sync` тепер 0 errors (раніше 12). Co-Authored-By: dmytro.s.stakhov <dmytro.s.stakhov@gmail.com>
1 parent c247eb8 commit ac2cc5c

6 files changed

Lines changed: 43 additions & 28 deletions

File tree

docs/adr/0004-cloudsync-lww-conflict-resolution.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# ADR-0004: CloudSync — LWW conflict resolution
22

3-
- **Status:** accepted
3+
- **Status:** superseded by [ADR-0047](./0047-cloudsync-v1-410-gone.md) (CloudSync v1 → 410 Gone, T₀ executed 2026-05-06)
44
- **Date:** 2026-04-27
55
- **Reviewers:** @Skords-01
66
- **Supersedes:**
7+
- **Superseded by:** [ADR-0047 — CloudSync v1 — T₀ executed (410 Gone)](./0047-cloudsync-v1-410-gone.md). Per-row op-log v2 (`/api/v2/sync/*`) replaces per-module LWW; all production sync traffic now flows through op-log writers ([`apps/web/src/core/syncEngine/syncEngineWriter.ts`](../../apps/web/src/core/syncEngine/syncEngineWriter.ts) + [`apps/server/src/modules/sync/syncV2.ts`](../../apps/server/src/modules/sync/syncV2.ts)). The v1 engine tree (`apps/web/src/core/cloudSync/{engine,queue,conflict,state,logger}`, `apps/mobile/src/sync/{engine,queue,config,api}`) was deleted in PR #052b/#052c (storage-roadmap Stage 7). The decision-record below stays as historical context for why per-module LWW was chosen for the MVP and what trade-offs the v2 op-log shift addresses.
78
- **Related:**
8-
- [`apps/web/src/core/cloudSync/`](../../apps/web/src/core/cloudSync/) — engine / queue / conflict / state, ~16 файлів, ~4400 рядків коду + тестів.
9-
- [`apps/server/src/modules/sync/syncV2.ts`](../../apps/server/src/modules/sync/syncV2.ts) — поточні op-log sync v2 хендлери.
10-
- [`apps/server/src/migrations/003_baseline_schema.sql`](../../apps/server/src/migrations/003_baseline_schema.sql) — таблиця `module_data`.
11-
- [`apps/server/src/migrations/007_module_data_user_fk.sql`](../../apps/server/src/migrations/007_module_data_user_fk.sql) — FK + cascade on user delete.
9+
- [`apps/server/src/modules/sync/syncV2.ts`](../../apps/server/src/modules/sync/syncV2.ts) — current op-log sync v2 хендлери (successor channel).
10+
- [`apps/server/src/migrations/003_baseline_schema.sql`](../../apps/server/src/migrations/003_baseline_schema.sql)`module_data` table (column dropped у PR #051).
1211

1312
---
1413

@@ -238,8 +237,9 @@ cloud — ні (юзер створив акаунт після onboarding); (в
238237

239238
### Decision
240239

241-
`apps/web/src/core/cloudSync/conflict/resolver.ts` — pure-функція
242-
`resolveInitialSync(inputs) → InitialSyncPlan` з 4 явними гілками:
240+
**Історично** (до v1 sunset у ADR-0047 + PR #052b) реалізовано як pure-функцію
241+
`resolveInitialSync(inputs) → InitialSyncPlan` у видаленому модулі
242+
`cloudSync/conflict/resolver.ts` (web v1 tree) з 4 явними гілками:
243243

244244
```ts
245245
type InitialSyncPlan =
@@ -262,9 +262,9 @@ type InitialSyncPlan =
262262
Без цього skip — `applyModuleData(cloud)` затирав би локальні зміни ще
263263
ДО push-у, і наступний push ніс би вже втрачений (cloud-)стан.
264264

265-
Resolver — **pure**, без I/O. Тестується без fetch-mock-ів;
266-
`apps/web/src/core/cloudSync/cloudSyncHelpers.test.ts` покриває всі
267-
4 гілки явно.
265+
Resolver — **pure**, без I/O. Тестувався без fetch-mock-ів;
266+
видалений тест-файл `cloudSync/cloudSyncHelpers.test.ts` (web v1 tree, знятий
267+
разом з v1 engine у PR #052b) покривав усі 4 гілки явно.
268268

269269
### Consequences
270270

@@ -313,7 +313,10 @@ finyk-blob.
313313

314314
### Decision
315315

316-
`apps/web/src/core/cloudSync/queue/offlineQueue.ts`:
316+
**Історично** реалізовано в видаленому модулі
317+
`cloudSync/queue/offlineQueue.ts` (web v1 tree, знятий разом з v1
318+
engine у PR #052b — successor у v2 op-log це outbox-таблиця
319+
`sync_op_outbox` через [`apps/web/src/core/syncEngine/syncEngineWriter.ts`](../../apps/web/src/core/syncEngine/syncEngineWriter.ts)):
317320

318321
1. **Coalesce consecutive pushes.** Якщо новий entry — `push` і черга
319322
закінчується теж на `push`, мерджимо `modules` в останній. Кожен модуль

docs/adr/0011-local-first-storage.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
- **Related:**
88
- [`packages/shared/src/lib/storageKeys.ts`](../../packages/shared/src/lib/storageKeys.ts) — централізований реєстр ключів.
99
- [`packages/shared/src/lib/kvStore.ts`](../../packages/shared/src/lib/kvStore.ts) — DOM-free `KVStore` контракт.
10-
- [`apps/web/src/core/cloudSync/config.ts`](../../apps/web/src/core/cloudSync/config.ts)`SYNC_MODULES` реєстр.
11-
- [`apps/web/src/core/cloudSync/useCloudSync.ts`](../../apps/web/src/core/cloudSync/useCloudSync.ts) — LWW-reconciler + offline queue.
10+
- [`packages/shared/src/sync/modules.ts`](../../packages/shared/src/sync/modules.ts)`SYNC_MODULES` реєстр (single source of truth для web + mobile, shared registry).
11+
- [`apps/web/src/core/cloudSync/useCloudSync.ts`](../../apps/web/src/core/cloudSync/useCloudSync.ts) — LWW-reconciler + offline queue (v1 cloudSync engine знятий у [ADR-0047](./0047-cloudsync-v1-410-gone.md) + PR #052b — цей hook лишається як shim до PR #053).
1212
- [`docs/mobile/react-native-migration.md`](../mobile/react-native-migration.md) §6 — mobile sync-subsystem.
1313
- ADR-0010 — mobile dual-track (platform storage адаптери).
1414
- ADR-0012 — RLS як authz-межа (server-side чекає `user_id`-filter).
@@ -223,7 +223,7 @@ accepted.
223223
### Decision
224224

225225
**Module-level LWW.** `SYNC_MODULES` реєстр (див.
226-
[`cloudSync/config.ts`](../../apps/web/src/core/cloudSync/config.ts)) визначає,
226+
[`packages/shared/src/sync/modules.ts`](../../packages/shared/src/sync/modules.ts)) визначає,
227227
які STORAGE_KEYS належать до якого модуля:
228228

229229
```ts
@@ -296,8 +296,9 @@ accepted.
296296

297297
### Decision
298298

299-
Hard cap `MAX_OFFLINE_QUEUE = 50` (див.
300-
[`cloudSync/config.ts:76`](../../apps/web/src/core/cloudSync/config.ts)).
299+
Hard cap `MAX_OFFLINE_QUEUE` (див.
300+
[`packages/shared/src/sync/modules.ts`](../../packages/shared/src/sync/modules.ts)
301+
— PR #009 підняв з 50 до 10 000 після переходу offline queue на IDB).
301302
Коли черга досягає cap-у, **найстаріші записи drop-аються** з
302303
`offline_queue_drops_total{reason="overflow"}` метрикою. Це прийнятно, бо
303304
module-level LWW робить старі записи redundant: останній push включає

docs/adr/0021-memory-bank.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- [`apps/web/src/core/profile/types.ts`](../../apps/web/src/core/profile/types.ts)`MemoryEntry` shape.
1111
- [`apps/web/src/core/lib/chatActions/crossActions.ts`](../../apps/web/src/core/lib/chatActions/crossActions.ts) — handler-и tool-ів `remember` / `forget` / `my_profile`.
1212
- [`apps/web/src/core/lib/hubChatContext.ts`](../../apps/web/src/core/lib/hubChatContext.ts) — інжекція memory у system-prompt.
13-
- [`apps/web/src/core/cloudSync/config.ts`](../../apps/web/src/core/cloudSync/config.ts)`SYNC_MODULES.profile.keys = [USER_PROFILE]` (CloudSync контракт).
13+
- [`packages/shared/src/sync/modules.ts`](../../packages/shared/src/sync/modules.ts)`SYNC_MODULES.profile.keys = [USER_PROFILE]` (CloudSync контракт, shared registry для web + mobile).
1414
- [`docs/launch/business/04-launch-readiness.md`](../launch/business/04-launch-readiness.md) — privacy-classification (PII / AI-context).
1515

1616
---

docs/architecture/data-exchange-storage-audit.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,19 @@ Sergeant зараз має гібридну data-архітектуру:
3131

3232
1. Модулі пишуть state у `localStorage` через `safeWriteSyncedLS` / `syncedKV.setString` (`apps/web/src/shared/lib/storage/syncedKV.ts`) — тонкий wrapper навколо `webKVStore`, що вшитий через `createSyncedKVStore` із `@sergeant/shared` (`packages/shared/src/sync/syncedKV.ts`).
3333
2. На кожен write до tracked-key wrapper кличе `enqueueChange(key)` (`apps/web/src/core/cloudSync/enqueue.ts`): визначає module за ключем, ставить dirty flag і емiтить sync event. До PR #008 цю роль грав `storagePatch.ts` через monkey-patch `localStorage.setItem/removeItem` + `__hubSyncPatched` global; тепер opt-in явний — тільки writes через `syncedKV` маркують модулі брудними.
34-
3. `SYNC_MODULES` визначає, які ключі входять у `finyk`, `nutrition`, `profile` (`packages/shared/src/sync/modules.ts`, реекспортний у `apps/web/src/core/cloudSync/config.ts`). Routine знятий у PR #026 (Stage 4 cleanup), Fizruk — у PR #030; обидва модулі тепер їздять виключно через v2 op-log.
35-
4. `pushDirty()` збирає dirty modules, робить `syncApi.pushAll(modules)`, а якщо offline/error — додає payload в offline queue (`apps/web/src/core/cloudSync/engine/push.ts`).
36-
5. Offline queue коалесить послідовні push-и і має hard cap `MAX_OFFLINE_QUEUE = 10 000` (`apps/web/src/core/cloudSync/queue/offlineQueue.ts`). Після Stage 1 PR #009 черга живе в IDB (durable backing) з LS dual-write best-effort до 100 записів; ліміт ~5 MB localStorage більше не є stop-the-world.
34+
3. `SYNC_MODULES` визначає, які ключі входять у `finyk`, `nutrition`, `profile` (`packages/shared/src/sync/modules.ts`, історично реекспортувався у web `cloudSync/config.ts` — видалений у PR #052b). Routine знятий у PR #026 (Stage 4 cleanup), Fizruk — у PR #030, Nutrition — у PR #034, Finyk — у PR #039; усі модулі тепер їздять виключно через v2 op-log.
35+
4. `pushDirty()` збирав dirty modules, робив `syncApi.pushAll(modules)`, а якщо offline/error — додавав payload в offline queue (web v1 `cloudSync/engine/push.ts`, видалений у PR #052b — successor у v2 op-log це [`apps/web/src/core/syncEngine/syncEngineWriter.ts`](../../apps/web/src/core/syncEngine/syncEngineWriter.ts), який intercepts SQLite mutations і пише їх у `sync_op_outbox`).
36+
5. Offline queue коалесила послідовні push-и і мала hard cap `MAX_OFFLINE_QUEUE = 10 000` (web v1 `cloudSync/queue/offlineQueue.ts`, видалений у PR #052b разом з рештою v1 engine tree — successor у v2 outbox це server-side таблиця `sync_op_outbox` через [`apps/server/src/migrations/027_sync_op_log.sql`](../../apps/server/src/migrations/027_sync_op_log.sql)). Після Stage 1 PR #009 v1-черга жила в IDB (durable backing) з LS dual-write best-effort до 100 записів; ліміт ~5 MB localStorage більше не є stop-the-world.
3737
6. Server пише module payload в `module_data` як JSONB blob з `version`, `client_updated_at`, `server_updated_at` (`apps/server/src/migrations/003_baseline_schema.sql`).
3838

39-
Модель конфліктів v1: **last-write-wins на рівні цілого module blob-а**. Це просто і добре для швидкого offline-first UX, але погано для одночасних правок різних частин одного модуля з різних пристроїв.
39+
Модель конфліктів v1 була: **last-write-wins на рівні цілого module blob-а**. Це просто і добре для швидкого offline-first UX, але погано для одночасних правок різних частин одного модуля з різних пристроїв — саме тому в Stage 4 ми перейшли на per-row op-log v2, а у Stage 7 (ADR-0047) повністю зняли v1 engine.
4040

4141
### 2.3. Mobile local-first sync v1
4242

4343
Mobile дзеркалить web-підхід, але замість `localStorage` має MMKV:
4444

45-
- `SYNC_MODULES` на mobile реекспортує той самий shared registry (`packages/shared/src/sync/modules.ts`) — після PR #026 / PR #030 cleanup це `finyk`, `nutrition`, `profile`; routine та fizruk вже зняті з v1 cloud-sync (`apps/mobile/src/sync/config.ts`).
46-
- Mobile API seam реекспортує `apiClient.sync` як `syncApi`, щоб engine-и читались як web (`apps/mobile/src/sync/api.ts`).
45+
- `SYNC_MODULES` на mobile реекспортував той самий shared registry (`packages/shared/src/sync/modules.ts`) — після PR #026 / PR #030 / PR #034 / PR #039 cleanup лишився лише `profile`; routine, fizruk, nutrition і finyk вже зняті з v1 cloud-sync. Mobile-side `sync/config.ts` був реекспортом цього shared registry і видалений у PR #052c разом з рештою mobile v1 engine tree.
46+
- Mobile API seam `sync/api.ts` реекспортував `apiClient.sync` як `syncApi`, щоб engine-и читались як web — теж видалений у PR #052c.
4747
- Через MMKV немає глобального patch як у web, тому tracked writes мають явно викликати `enqueueChange`; для цього додали `useSyncedStorage()` як safer wrapper (`apps/mobile/src/sync/useSyncedStorage.ts`).
4848

4949
### 2.4. Sync v2 / operation log

docs/observability/frontend.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Frontend-observability — web і mobile
22

3-
> **Last validated:** 2026-05-04 by @Skords-01. **Next review:** 2026-08-02.
3+
> **Last validated:** 2026-05-06 by @Skords-01. **Next review:** 2026-08-04.
44
> **Status:** Active
55
66
Observability-стек для web- і mobile-клієнтів Sergeant: error tracking,
@@ -228,9 +228,17 @@ voice vs typed сценарії у funnels; `tool` — канонічне ім'
228228

229229
### CloudSync-події
230230

231-
**Файл:** `apps/web/src/core/cloudSync/hook/useSyncCallbacks.ts` — lifecycle
232-
(start/success/fail). **Файли:** `engine/push.ts`, `engine/initialSync.ts`
233-
conflict-резолюція.
231+
**Історично** (до v1 sunset у [ADR-0047](../adr/0047-cloudsync-v1-410-gone.md))
232+
файл `cloudSync/hook/useSyncCallbacks.ts` (web v1 tree) обробляв
233+
lifecycle (start/success/fail), а файли `engine/push.ts` /
234+
`engine/initialSync.ts` — conflict-резолюцію. Усі ці модулі видалено у
235+
PR #052b разом із рештою v1 engine tree (storage-roadmap Stage 7).
236+
237+
У v2 op-log тих самих події емітить writer / replayer:
238+
[`apps/web/src/core/syncEngine/syncEngineWriter.ts`](../../apps/web/src/core/syncEngine/syncEngineWriter.ts)
239+
— intercepts SQLite mutations, пише у `sync_op_outbox`, fire-ить
240+
`sync_started` / `sync_succeeded` / `sync_failed` через ту саму PostHog
241+
обгортку.
234242

235243
| Event | Коли | Payload |
236244
| ------------------------ | --------------------------------------------------------- | ---------------------------------------------- |

docs/tech-debt/mobile.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ instance `id: "sergeant.mobile.v1"`.
132132
### Cloud-sync дисципліна
133133

134134
`useLocalStorage` (RN-варіант) зобов'язаний викликати `enqueueChange(key)`
135-
після кожного запису для tracked-keys у `apps/mobile/src/sync/config.ts`.
135+
після кожного запису для tracked-keys із shared registry
136+
[`packages/shared/src/sync/modules.ts`](../../packages/shared/src/sync/modules.ts)
137+
(після PR #052c mobile-side `sync/config.ts` видалений
138+
разом з рештою v1 engine tree — `SYNC_MODULES` тепер тільки в shared).
136139
Альтернатива — `useSyncedStorage` обгортка, яка робить це автоматично.
137140
ESLint правило [`sergeant-design/no-raw-tracked-storage`](../../packages/eslint-plugin-sergeant-design/index.js)
138141
блокує `useLocalStorage` з tracked-keys поза `useSyncedStorage`. **Стан: OK**

0 commit comments

Comments
 (0)