From 98d07a8d48b44387ffd90be76c8014538a5c2824 Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 27 Apr 2026 01:06:05 +0200 Subject: [PATCH] docs(claude): document sync field-meta overhaul (F1-F7) in CLAUDE.md + DATA_LAYER_AUDIT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/mana/CLAUDE.md: - Data-flow diagram updated: __fieldMeta + _updatedAtIndex + origin replace the older __fieldTimestamps / __fieldActors / __lastActor trio. - New "Conflict-Detection" sub-section in §Data Layer summarizes the four moving parts (origin-gating, derived updatedAt, server-side singleton bootstrap, stable client_id) with a "use this" cheatsheet for the patterns you'll reach for when writing new module code. DATA_LAYER_AUDIT.md: - Eckdaten line points at v53/v54 instead of "v9 added updatedAt indexes". Conflict-Resolution bullet says "Origin-gated Field-Level LWW via __fieldMeta" (was: __fieldTimestamps). - New "Sync Field-Meta Overhaul (2026-04-26, F1-F7 SHIPPED)" sub-section with one paragraph per phase + commit hash + the four bug-roots that were closed. - Punkt 15 (Conflict-Visualisierung) flipped from "🟢 Backlog" to "✅ Sprint 4+ Backlog C shipped, F2 origin-gated the trigger so only real user edits surface". Future sessions reading the repo cold get the post-overhaul architecture from these two files instead of having to chase the plan + commit log. Closes Punkt 10 of the F1-F7 follow-up audit. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/mana/CLAUDE.md | 30 +++++++++++-- .../apps/web/src/lib/data/DATA_LAYER_AUDIT.md | 43 ++++++++++++++----- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/apps/mana/CLAUDE.md b/apps/mana/CLAUDE.md index 50eacd02c..1205ade4c 100644 --- a/apps/mana/CLAUDE.md +++ b/apps/mana/CLAUDE.md @@ -72,10 +72,10 @@ table.add(encryptedRecord) ← Dexie write │ ▼ Dexie hooks (database.ts): - - stamp userId - - stamp __fieldTimestamps per field - - stamp __lastActor + __fieldActors (user / ai / system — see AI Workbench) - - record into _pendingChanges (tagged with appId + actor) + - stamp userId (user-level tables only) + - stamp __fieldMeta[k] = { at, actor, origin } per field + - stamp _updatedAtIndex (local-only shadow for indexed sorts) + - record into _pendingChanges (tagged with appId + actor + origin) - record into _activity │ ▼ @@ -95,6 +95,28 @@ applyServerChanges → Dexie hooks (suppressed) → liveQuery → decryptRecord **Deep dive**: [`apps/web/src/lib/data/DATA_LAYER_AUDIT.md`](apps/web/src/lib/data/DATA_LAYER_AUDIT.md) — sync engine, retry/backoff, quota recovery, telemetry, RLS, encryption rollout, threat model. **Single most important file for understanding how the app works under the hood.** +### Conflict-Detection (post 2026-04-26 sync-field-meta-overhaul) + +The four bug-roots that made the conflict-toast fire spuriously have all been closed. Architecture today: + +- **`__fieldMeta`** (single hidden field per record, replaces the older `__fieldTimestamps` / `__fieldActors` / `__lastActor` triple). Shape: `{ [field]: { at, actor, origin } }`. The Dexie creating/updating hook stamps it on every write; consumers read it via `readFieldMeta()` and `deriveUpdatedAt()` from `$lib/data/sync`. +- **Origin-tracking**: `originFromActor(actor)` in `@mana/shared-ai` maps `actor.kind` onto `'user' | 'agent' | 'system' | 'migration' | 'server-replay'`. The conflict surface fires only when `localFieldMeta.origin === 'user'` — replay-deltas from server pulls, agent writes, migration helpers, and bootstrap inserts never surface as toasts. +- **`updatedAt` is no longer a synced data field.** Type-converters compute `updatedAt` on read as `max(__fieldMeta[*].at)` via `deriveUpdatedAt(local)`. For Dexie-indexed sort, every record carries a non-synced `_updatedAtIndex` shadow column that the hook stamps automatically — `orderBy('_updatedAtIndex')` instead of `orderBy('updatedAt')`. +- **Server-side singleton bootstrap**: mana-auth's `/register` flow writes the `userContext` singleton straight into `mana_sync.sync_changes` with `origin: 'system'`. The webapp's `getOrCreateLocalDoc()` survives only as a fallback for the rare race where the first pull hasn't landed yet. +- **Stable `client_id`**: Dexie table `_clientIdentity` (single row keyed by `id='self'`) is the canonical source of the per-device sync identity. `restoreClientIdFromDexie()` runs once at boot and reconciles localStorage ↔ Dexie — a localStorage wipe gets restored from Dexie, the server keeps seeing the same client. + +When writing new code: + +| Pattern | Use this | +| --- | --- | +| Read "last modified" for UI | `deriveUpdatedAt(local)` (returns ISO string) | +| Sort a Dexie query by recency | `.orderBy('_updatedAtIndex')` | +| Stamp a system/migration write | wrap in `runAsAsync(makeSystemActor(SYSTEM_MIGRATION, '