docs(sync): document archive-wins rule, singleton LWW, and stale op handling

Document 6 gaps found during documentation audit:

- Archive-wins conflict resolution rule (moveToArchive always wins
  over field-level updates regardless of timestamps)
- Two-level archive resurrection prevention (ConflictResolutionService
  + bulkOperationsMetaReducer pre-scan filtering)
- Singleton entity LWW update handling (adapter vs singleton vs
  unsupported storage patterns)
- Stale moveToArchive operation preservation with merged vector clocks
- Fix upload batch size from 100 to 25 in operation-rules.md and
  operation-log-architecture.md
- Update meta-reducer chain from simplified 7-phase to accurate
  8-phase, 15-entry structure matching meta-reducer-registry.ts
This commit is contained in:
Johannes Millan 2026-01-30 12:50:16 +01:00
parent d618238864
commit 4ab86ba0fe
6 changed files with 241 additions and 31 deletions

View file

@ -245,18 +245,27 @@ Last-Write-Wins automatically resolves conflicts using timestamps.
**Key Invariants:**
| Invariant | Reason |
| --------------------------------- | --------------------------------------------- |
| Preserve original timestamp | Prevents unfair advantage in future conflicts |
| Merge ALL clocks | New op dominates everything known |
| Reject ALL pending ops for entity | Prevents stale ops from being uploaded |
| Mark rejected BEFORE applying | Crash safety |
| Remote wins on tie | Server-authoritative |
| Invariant | Reason |
| --------------------------------- | -------------------------------------------------------------------------- |
| Archive-wins rule | `moveToArchive` always wins over field-level updates, bypassing timestamps |
| Preserve original timestamp | Prevents unfair advantage in future conflicts |
| Merge ALL clocks | New op dominates everything known |
| Reject ALL pending ops for entity | Prevents stale ops from being uploaded |
| Mark rejected BEFORE applying | Crash safety |
| Remote wins on tie | Server-authoritative |
**Stale Operation Special Cases:**
| Operation Type | Stale Handling |
| --------------- | --------------------------------------------------------------------------------------------- |
| Regular UPDATE | Re-created with current entity state + merged clock |
| DELETE | Re-created with original payload + merged clock (entity gone from store) |
| `moveToArchive` | Re-created with original payload + merged clock (entity removed from NgRx by archive reducer) |
**Key Files:**
- `conflict-resolution.service.ts` - LWW resolution
- `stale-operation-resolver.service.ts` - Stale op handling
- `conflict-resolution.service.ts` - LWW resolution + archive-wins rule
- `stale-operation-resolver.service.ts` - Stale op handling (incl. moveToArchive special case)
---
@ -362,51 +371,67 @@ Meta-reducers enable atomic multi-entity changes in a single reducer pass.
```
┌─────────────────────────────────────────────────────────────────┐
META-REDUCER CHAIN
META-REDUCER CHAIN (8 Phases, 15 Entries)
├─────────────────────────────────────────────────────────────────┤
│ │
│ Action Dispatched │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 1: operationCaptureMetaReducer │ │
│ │ Captures persistent actions → FIFO queue │ │
│ │ Phase 1: operationCaptureMetaReducer [MUST BE FIRST] │ │
│ │ Captures original state BEFORE modifications │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 2: bulkOperationsMetaReducer │ │
│ │ Applies bulk ops during sync/hydration │ │
│ │ Unwraps bulk dispatches for hydration/sync │ │
│ │ Pre-scans for archive ops to prevent │ │
│ │ resurrection via LWW Update │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 3: lwwUpdateMetaReducer │ │
│ │ Handles LWW Update actions (conflict wins) │ │
│ │ Phase 3: undoTaskDeleteMetaReducer │ │
│ │ Captures task context before deletion │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 4: Tag/Project/IssueProvider Shared Reducers │ │
│ │ Cascade deletions across entities │ │
│ │ Phase 4: Core CRUD Meta-Reducers (dependency order) │ │
│ │ • taskSharedCrudMetaReducer │ │
│ │ • taskBatchUpdateMetaReducer │ │
│ │ • taskSharedLifecycleMetaReducer │ │
│ │ • taskSharedSchedulingMetaReducer │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 5: Planner/ShortSyntax Shared Reducers │ │
│ │ Handle task movements and parsing │ │
│ │ Phase 5: Entity-Specific Cascades │ │
│ │ • projectSharedMetaReducer │ │
│ │ • tagSharedMetaReducer │ │
│ │ • issueProviderSharedMetaReducer │ │
│ │ • taskRepeatCfgSharedMetaReducer │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 6: Feature Reducers (task, project, tag, etc.) │ │
│ │ Core state updates │ │
│ │ Phase 6: plannerSharedMetaReducer │ │
│ │ Syncs with task.dueDay and TODAY_TAG │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 7: validateAndFixDataConsistency │ │
│ │ Repair any inconsistencies │ │
│ │ Phase 7: Synthetic Multi-Step Operations │ │
│ │ • shortSyntaxSharedMetaReducer │ │
│ │ • lwwUpdateMetaReducer │ │
│ │ (adapter / singleton / unsupported patterns) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 8: actionLoggerReducer [MUST BE LAST] │ │
│ │ Pure logging only │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
@ -415,6 +440,13 @@ Meta-reducers enable atomic multi-entity changes in a single reducer pass.
└─────────────────────────────────────────────────────────────────┘
```
**Critical Ordering Rules:**
- `operationCaptureMetaReducer` must be at index 0 (captures pre-modification state)
- `bulkOperationsMetaReducer` must be at index 1 (unwraps bulk before other reducers run)
- `actionLoggerReducer` must be last (logs final state)
- Development mode validates these constraints at startup
**Why Meta-Reducers for Multi-Entity Changes?**
| Approach | Problem |
@ -544,8 +576,15 @@ Bulk application optimizes performance by applying many operations in a single d
│ │ │
│ ▼ (in bulkOperationsMetaReducer) │
│ ┌────────────────────────────────────────┐ │
│ │ // Pre-scan: collect archived IDs │ │
│ │ archivedIds = findArchiveOps(ops) │ │
│ │ │ │
│ │ for (op of operations) { │ │
│ │ action = convertOpToAction(op) │ │
│ │ // Skip LWW Updates for archived │ │
│ │ if (isLwwUpdate(action) && │ │
│ │ archivedIds.has(entityId)) │ │
│ │ continue; │ │
│ │ state = reducer(state, action) │ │
│ │ } │ │
│ │ return state │ │