docs(claude): document sync field-meta overhaul (F1-F7) in CLAUDE.md + DATA_LAYER_AUDIT

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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 01:06:05 +02:00
parent 899fccd455
commit 98d07a8d48
2 changed files with 58 additions and 15 deletions

View file

@ -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, '<label>'), async () => { … })` |
| Schreib Local-Type | omit `updatedAt: string` field — it's derived, not stored |
| Add a new singleton | Bootstrap server-side in mana-auth (see `services/mana-auth/src/services/bootstrap-singletons.ts`) instead of `ensureDoc()` on the client |
Plan with full per-phase rationale: [`docs/plans/sync-field-meta-overhaul.md`](../../docs/plans/sync-field-meta-overhaul.md).
## At-Rest Encryption
User-typed content in **27 tables** is encrypted with **AES-GCM-256** before it touches IndexedDB. Master key lives in `mana-auth` (KEK-wrapped) and is fetched on login.

View file

@ -185,14 +185,35 @@ Logout / Tab-Close → MemoryKeyProvider.setKey(null) → Cyphertext bleibt
### Eckdaten
- **120+ Collections** in einer einzigen IndexedDB
- **Schema-Versionen** 110 (V9 fügte updatedAt-Indexes hinzu, V10 die `_activity` Tabelle)
- **Schema-Versionen** 154 (v53 ersetzte `updatedAt`-Indizes durch `_updatedAtIndex`, v54 fügte `_clientIdentity` für stabile Client-IDs hinzu)
- **Eager Apps**: mana, todo, calendar, contacts, tags, links — syncen beim Start
- **Lazy Apps**: starten Sync erst beim ersten Modul-Besuch via `ensureAppSynced()`
- **Conflict Resolution**: ✅ echter Field-Level LWW via `__fieldTimestamps`
- **Conflict Resolution**: ✅ Origin-gated Field-Level LWW via `__fieldMeta` (siehe Sync Field-Meta Overhaul unten)
- **Soft Delete** ist Standard via `deletedAt`
- **At-Rest-Encryption**: AES-GCM-256, server-wrapped Master Key, 27 Tabellen aktiv (Rollout abgeschlossen)
- **Zero-Knowledge-Modus** (Phase 9 Opt-In): Recovery-Code-basiertes Wrapping ohne Server-seitige Entschlüsselbarkeit, mit Rotate-im-Active-State-Support
### Sync Field-Meta Overhaul (2026-04-26, F1F7 SHIPPED)
Sieben Phasen, die vier strukturelle Bugs in der Conflict-Detection abgeräumt haben:
- **F1** (`7766ea502`) `__fieldMeta` ersetzt `__fieldTimestamps` + `__fieldActors` + `__lastActor`. Ein Hidden-Field statt drei. Wire-Format `FieldChange = { value, at }` (vorher `{ value, updatedAt }`). mana-sync DB-Spalte `field_timestamps``field_meta`, neue Spalte `origin TEXT`.
- **F2** (`ad5e04a55`) Conflict-Trigger gated auf `localFieldMeta.origin === 'user' && !options.isInitialHydration`. `originFromActor()` in shared-ai mappt actor.kind → `'user' | 'agent' | 'system' | 'migration'`. applyServerChanges stempelt alle Replays als `'server-replay'`. Initial-Pull-Hydration suppress alle Conflicts (Belt-and-Suspenders).
- **F3** (`6bb9d77be`) `updatedAt` raus aus dem Wire. Read-side computed via `deriveUpdatedAt(record) = max(__fieldMeta[*].at)`. Lokale `_updatedAtIndex`-Schatten-Spalte für Dexie-`orderBy`-Sortierung. v53 Dexie-Migration kopiert `updatedAt``_updatedAtIndex` für existing rows. ~382 stamping-sites über 121 Files entfernt.
- **F4** (`c07db300b`) Server-side Singleton-Bootstrap in mana-auth's `/register`. `bootstrapUserSingletons(userId)` schreibt `userContext` direkt in `mana_sync.sync_changes` mit `client_id='system:bootstrap'` + `origin='system'`. Default-Inhalt mirror't `emptyUserContext()`.
- **F5** (`d78f57c04`) `userContextStore.ensureDoc()` Public-API entfernt. Internal `getOrCreateLocalDoc()` bleibt als Fallback für brand-new clients deren First-Pull noch nicht durch ist. UI mountet ohne ensureDoc-Race.
- **F6** (`a031493fe`) Stable `client_id` in Dexie-Tabelle `_clientIdentity`. `restoreClientIdFromDexie()` läuft im (app)-Layout vor `createUnifiedSync` und reconciliated Dexie ↔ localStorage. Dexie ist canonical, localStorage ist fast-read-cache. Survives clear-site-data und incognito flush.
- **F7** (`2a8e8ff98`) `repair-silent-twin.ts` + `legacy-avatar.ts` Migrationen ersatzlos gelöscht — pre-live, keine Live-Daten brauchen sie. Orphan-localStorage-Flags-Sweep im Boot (`migrations-cleanup.ts`, `119cd2cf8`) räumt die zugehörigen Flags auf.
Die vier Bug-Wurzeln (siehe ursprüngliche Diagnose 2026-04-26):
1. ❌ `updatedAt` als syncbares Feld → entfernt in F3
2. ❌ Conflict-Detection unterscheidet User-Edit nicht von Replay → Origin-Gate in F2
3. ❌ Singleton race-anfällig → Server-Bootstrap (F4) + Public-API-Drop (F5)
4. ❌ `client_id` an localStorage gebunden → Dexie-canonical (F6)
Plan + Implementation-Notes: [`docs/plans/sync-field-meta-overhaul.md`](../../../../../../docs/plans/sync-field-meta-overhaul.md).
---
## 2. Behobene Schwachstellen
@ -220,15 +241,15 @@ Logout / Tab-Close → MemoryKeyProvider.setKey(null) → Cyphertext bleibt
### 🟢 Mittel — alle erledigt oder in Backlog
| # | Problem | Status |
| --- | ---------------------------------------- | -------------------------------------------------------- |
| 12 | `changes: any[]` in `applyServerChanges` | ✅ Sprint 3.1 — `SyncChange` Interface |
| 13 | SSE-Buffer akkumuliert ganze Events | ✅ Backlog 2 — pipelined Reader/Apply mit indexOf-Parser |
| 14 | Tombstone-Cleanup-Routine fehlt | ✅ Sprint 4+ — `data-layer-listeners.ts` boot+24h Loop |
| 15 | Conflict-Visualisierung im UI | 🟢 Backlog — UX-Entscheidung |
| 16 | Keine Unit-Tests für `sync.ts` | ✅ Sprint 3.3 — 20 Tests gegen fake-indexeddb |
| 17 | Composite Keys / Multi-Account Indizes | 🟢 Backlog — wenn Multi-Account aktiv wird |
| 18 | Audit-Log / Activity Feed | ✅ Backlog 3 — `_activity` Table + read API + prune |
| # | Problem | Status |
| --- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 12 | `changes: any[]` in `applyServerChanges` | ✅ Sprint 3.1 — `SyncChange` Interface |
| 13 | SSE-Buffer akkumuliert ganze Events | ✅ Backlog 2 — pipelined Reader/Apply mit indexOf-Parser |
| 14 | Tombstone-Cleanup-Routine fehlt | ✅ Sprint 4+ — `data-layer-listeners.ts` boot+24h Loop |
| 15 | Conflict-Visualisierung im UI | ✅ Sprint 4+ Backlog C — Toast + Restore (`SyncConflictToast`) shipped, F2 (2026-04-26) gated den Trigger auf `origin === 'user'` damit nur echte User-Edits surface |
| 16 | Keine Unit-Tests für `sync.ts` | ✅ Sprint 3.3 — 20 Tests gegen fake-indexeddb |
| 17 | Composite Keys / Multi-Account Indizes | 🟢 Backlog — wenn Multi-Account aktiv wird |
| 18 | Audit-Log / Activity Feed | ✅ Backlog 3 — `_activity` Table + read API + prune |
---