docs(sync): update

This commit is contained in:
Johannes Millan 2025-12-03 14:57:30 +01:00
parent 8b0115ff70
commit 73ac516608
2 changed files with 123 additions and 515 deletions

View file

@ -2,7 +2,7 @@
**Status:** Parts A, B, C Implemented
**Branch:** `feat/operation-logs`
**Last Updated:** December 3, 2025
**Last Updated:** December 3, 2025 (rollback + rejected ops)
---
@ -61,6 +61,7 @@ interface OperationLogEntry {
appliedAt: number; // When applied locally
source: 'local' | 'remote';
syncedAt?: number; // For server sync (Part C)
rejectedAt?: number; // When rejected during conflict resolution
}
// state_cache table - periodic snapshots
@ -692,12 +693,23 @@ async presentConflicts(conflicts: EntityConflict[]): Promise<void> {
for (const conflict of conflicts) {
await this.operationApplier.applyOperations(conflict.remoteOps);
}
// Mark local ops as rejected so they won't be re-synced
const localOpIds = conflict.localOps.map(op => op.id);
await this.opLogStore.markRejected(localOpIds);
} else {
// Keep local ops, ignore remote
}
}
```
### Rejected Operations
When the user chooses "remote" resolution, local conflicting operations are marked with `rejectedAt` timestamp:
- Rejected ops remain in the log for history/debugging
- `getUnsynced()` excludes rejected ops (won't re-upload)
- Compaction may eventually delete old rejected ops
## C.6 Dependency Resolution
Operations may have dependencies (e.g., subtask requires parent task):
@ -739,15 +751,20 @@ interface OperationDependency {
- Conflict resolution dialog (C.5)
- Dependency resolution with retry queue (C.6)
- Persistent action metadata on all model actions
- **Rollback notification on persistence failure** (shows snackbar with reload action)
- **Rejected operation tracking** (`rejectedAt` field, excluded from sync)
## Future Enhancements 🔮
| Component | Description |
| ---------------- | ------------------------------------------ |
| Auto-merge | Automatic merge for non-conflicting fields |
| Undo/Redo | Leverage op-log for undo history |
| Offline queue UI | Show pending sync operations to user |
| Op-log analytics | Debug view of operation history |
| Component | Description | Priority |
| --------------------- | -------------------------------------------- | -------- |
| Auto-merge | Automatic merge for non-conflicting fields | Low |
| Undo/Redo | Leverage op-log for undo history | Low |
| Offline queue UI | Show pending sync operations to user | Low |
| Op-log viewer | Debug panel for viewing operation history | Medium |
| IndexedDB index | Index on `syncedAt` for faster getUnsynced() | Low |
| Persistent compaction | Track ops since compaction across restarts | Low |
| Diff-based storage | Store diffs for large text fields (notes) | Defer |
---

View file

@ -2,8 +2,8 @@
**Created:** December 2, 2025
**Branch:** `feat/operation-logs`
**Status:** Implementation in Progress (~70% complete)
**Last Updated:** December 2, 2025
**Status:** ✅ Core Implementation Complete
**Last Updated:** December 3, 2025
---
@ -13,498 +13,91 @@ This execution plan is organized around the three parts of the [Operation Log Ar
| Part | Purpose | Status |
| ------------------------- | ------------------------------ | ----------- |
| **A. Local Persistence** | Fast writes, crash recovery | ~80% done |
| **B. Legacy Sync Bridge** | Vector clock updates for PFAPI | ~50% done |
| **C. Server Sync** | Op-log based sync (future) | Not started |
| **A. Local Persistence** | Fast writes, crash recovery | ✅ Complete |
| **B. Legacy Sync Bridge** | Vector clock updates for PFAPI | ✅ Complete |
| **C. Server Sync** | Op-log based sync | ✅ Complete |
---
## What's Done ✅
| Component | Part | Notes |
| --------------------------- | ---- | -------------------------------- |
| SUP_OPS IndexedDB store | A | ops + state_cache tables |
| NgRx effect capture | A | Converts actions to operations |
| Per-op vector clock | A | Causality tracking |
| Snapshot + tail hydration | A | Fast startup from state_cache |
| Genesis migration | A | Legacy pf → SUP_OPS on first run |
| Multi-tab coordination | A | BroadcastChannel + Web Locks |
| `PfapiStoreDelegateService` | B | Reads NgRx state for sync |
| Component | Part | Notes |
| ------------------------------- | ---- | ---------------------------------------- |
| SUP_OPS IndexedDB store | A | ops + state_cache tables |
| NgRx effect capture | A | Converts actions to operations |
| Per-op vector clock | A | Causality tracking |
| Snapshot + tail hydration | A | Fast startup from state_cache |
| Genesis migration | A | Legacy pf → SUP_OPS on first run |
| Multi-tab coordination | A | BroadcastChannel + Web Locks |
| Compaction triggers | A | Every 500 ops with 7-day retention |
| Rollback notification | A | Snackbar with reload on persistence fail |
| `PfapiStoreDelegateService` | B | Reads NgRx state for sync |
| META_MODEL vector clock update | B | Legacy sync detects local changes |
| Sync download persistence | B | Downloaded data persisted to SUP_OPS |
| All models in NgRx | B | No hybrid persistence |
| Server sync upload/download | C | Individual operation sync |
| File-based sync fallback | C | For WebDAV/Dropbox providers |
| Entity-level conflict detection | C | Vector clock comparison per entity |
| Conflict resolution dialog | C | User chooses local vs remote |
| Rejected operation tracking | C | `rejectedAt` field, excluded from sync |
| Dependency resolution | C | Retry queue for missing dependencies |
---
## Critical Gaps 🔴
## Resolved Gaps ✅
| Gap | Part | Issue | Impact |
| ----------------------------------- | ---- | ---------------------------------------- | ------------------------ |
| META_MODEL vector clock not updated | B | Legacy sync doesn't detect local changes | Sync uploads nothing |
| Sync download not persisted | B | Downloaded data only in memory | Crash = data loss |
| Non-NgRx models not migrated | B | reminders, archives bypass op-log | Inconsistent persistence |
| SaveToDbEffects still active | A | Unnecessary writes to `pf` | Wasted I/O |
| Compaction never triggers | A | Op log grows unbounded | Slow startup |
| Gap (was 🔴) | Resolution |
| ----------------------------------- | -------------------------------------------------- |
| META_MODEL vector clock not updated | Now incremented on every op write |
| Sync download not persisted | SYNC_IMPORT op + snapshot on download |
| Non-NgRx models not migrated | All models now in NgRx |
| SaveToDbEffects still active | Removed - persistence via OperationLogEffects |
| Compaction never triggers | Triggers every 500 ops |
| Lost local ops on conflict | Now marked `rejectedAt`, excluded from getUnsynced |
| No rollback on persistence fail | Snackbar with reload action |
---
# ⚠️ Implementation Order (CRITICAL)
# Future Enhancements
Tasks have dependencies. **Follow this order exactly:**
These are optional improvements identified during code review. None are blocking.
```
Phase 1: Foundation (can be parallelized)
├── B.1 Update META_MODEL Vector Clock
├── B.2 Persist Sync Downloads
├── B.3 Wire Delegate Always-On
├── A.2 Add Compaction Triggers (⚠️ depends on B.4 for full correctness)
├── A.3 Audit Action Blacklist
└── A.5 Add Schema Migration Service
## Performance Optimizations
Phase 2: Non-NgRx Migration (BLOCKER for Phase 3)
└── B.4 Migrate Non-NgRx Models ← Must complete before A.1
| Enhancement | Description | Priority | Effort |
| ----------------------------- | ---------------------------------------------- | -------- | ------ |
| IndexedDB index for unsynced | Add index on `syncedAt` for O(1) getUnsynced() | Low | Low |
| Optimize getAppliedOpIds() | Consider Merkle trees if log grows very large | Low | Medium |
| Persistent compaction counter | Track `opsSinceCompaction` across restarts | Low | Low |
Phase 3: Cutover (only after B.4 is complete)
├── A.1 Disable SaveToDbEffects ← Depends on B.4
└── A.4 Update Disaster Recovery ← Update recovery paths
```
**Notes:**
**Why B.4 must complete before A.1:**
- Current `getUnsynced()` does full scan, but compaction keeps log bounded (~500 ops max)
- Performance optimization only needed if users report slow sync initiation
- If SaveToDbEffects is disabled before non-NgRx models are migrated to NgRx
- Non-NgRx models (reminders, archives) will have NO persistence path
- Data loss will occur
## Observability & Tooling
---
| Enhancement | Description | Priority | Effort |
| -------------------- | ------------------------------------- | -------- | ------ |
| Operation Log Viewer | Hidden debug panel to view op history | Medium | Medium |
# Part A Tasks: Local Persistence
**Implementation idea:**
## A.1 Disable SaveToDbEffects
- Add debug tab in Settings → About section
- Show: total ops, pending ops, last sync time, current vector clock
- List recent operations with seq, id, type, timestamp
> ⚠️ **DEPENDENCY:** This task can ONLY be done after B.4 (Migrate Non-NgRx Models) is complete!
## Storage Efficiency
**Priority:** HIGH | **Effort:** Small
| Enhancement | Description | Priority | Effort |
| ------------------ | ----------------------------------------- | -------- | ------ |
| Diff-based storage | Store diffs for large text fields (notes) | Defer | High |
**Problem:** SaveToDbEffects is still writing model data to `pf` database. This is wasted I/O since data is in SUP_OPS.
**Notes:**
**Files:**
- `src/app/root-store/shared/save-to-db.effects.ts`
- `src/app/root-store/root-store.module.ts`
**Implementation:**
```typescript
// Option A: Remove from module (cleanest)
// root-store.module.ts
EffectsModule.forRoot([
// SaveToDbEffects, // REMOVED - persistence via OperationLogEffects
// ... other effects
]);
// Option B: Comment out effects (preserves code for reference)
```
**Acceptance:**
- [ ] No writes to `pf` database model tables
- [ ] App persists data correctly via SUP_OPS
- [ ] Restart shows persisted data
---
## A.2 Add Compaction Triggers
**Priority:** HIGH | **Effort:** Small
> ⚠️ **WARNING:** Until B.4 (Migrate Non-NgRx Models) is complete, compaction snapshots will include stale data for non-NgRx models (read from `pf` database). This is acceptable during transition - the snapshot is still crash-safe, just potentially out-of-date for those models.
**Problem:** Compaction logic exists but is never invoked. Op log grows unbounded.
**Files:**
- `src/app/core/persistence/operation-log/operation-log.effects.ts`
- `src/app/core/persistence/operation-log/operation-log-compaction.service.ts`
**Implementation:**
```typescript
// operation-log.effects.ts
private opsSinceCompaction = 0;
private readonly COMPACTION_THRESHOLD = 500;
private async writeOperation(op: Operation): Promise<void> {
await this.opLogStore.appendOperation(op);
await this.pfapiService.pf.metaModel.incrementVectorClock(this.clientId);
this.multiTabCoordinator.broadcastOperation(op);
// Check compaction trigger
this.opsSinceCompaction++;
if (this.opsSinceCompaction >= this.COMPACTION_THRESHOLD) {
await this.compactionService.compact();
this.opsSinceCompaction = 0;
}
}
```
```typescript
// operation-log-compaction.service.ts
async compact(): Promise<void> {
// Read from NgRx, NOT from stale pf database
const currentState = await this._storeDelegateService.getAllSyncModelDataFromStore();
await this.opLogStore.saveStateCache({
state: currentState,
lastAppliedOpSeq: await this.opLogStore.getLastSeq(),
savedAt: Date.now(),
schemaVersion: CURRENT_SCHEMA_VERSION
});
// Delete old ops (aggressive for local-only)
await this.opLogStore.deleteOpsBefore(lastSeq - RETENTION_BUFFER);
}
```
**Acceptance:**
- [ ] Compaction runs after 500 ops
- [ ] Snapshot contains current NgRx state
- [ ] Old ops are deleted
---
## A.3 Audit Action Blacklist
**Priority:** MEDIUM | **Effort:** Medium
**Problem:** Only ~10 actions in blacklist. UI actions may be spamming the op log.
**File:** `src/app/core/persistence/operation-log/action-whitelist.ts` (rename to `action-blacklist.ts`)
**Process:**
1. `find src/app/features -name "*.actions.ts"`
2. Identify UI-only actions (`Ui`, `UI`, `Selected`, `Current`, `Toggle`, `Show`, `Hide`)
3. Add to blacklist
**Likely missing:**
- `[Worklog]` UI state actions
- `[Pomodoro]` transient session state
- Focus session transient state
- Selection states across features
**Acceptance:**
- [ ] All feature modules audited
- [ ] UI actions excluded from op log
- [ ] Op log contains only persistent changes
---
## A.4 Add Disaster Recovery
**Priority:** MEDIUM | **Effort:** Medium
**Problem:** No recovery path if SUP_OPS is corrupted.
**File:** `src/app/core/persistence/operation-log/operation-log-hydrator.service.ts`
> ⚠️ **NOTE:** Recovery paths change based on transition phase:
>
> - **During transition (before A.1):** `pf` database has recent data - can use genesis migration
> - **After transition (A.1 complete):** `pf` database becomes stale - must use remote sync or backup import
**Implementation:**
```typescript
async hydrateStore(): Promise<void> {
try {
const snapshot = await this.opLogStore.loadStateCache();
if (!snapshot || !this.isValidSnapshot(snapshot)) {
await this.attemptRecovery();
return;
}
// Normal hydration...
} catch (e) {
await this.attemptRecovery();
}
}
private async attemptRecovery(): Promise<void> {
// 1. Try legacy pf database (only useful during transition)
const legacyData = await this.pfapi.getAllSyncModelData();
if (legacyData && this.hasData(legacyData)) {
console.warn('SUP_OPS corrupted - recovering from pf database (may be stale post-transition)');
await this.runGenesisMigration(legacyData);
return;
}
// 2. Try remote sync (preferred post-transition)
if (this.syncService.isConfigured()) {
console.warn('SUP_OPS corrupted - attempting recovery from remote sync');
await this.syncService.forceDownload();
return;
}
// 3. Show error to user with backup import option
this.showRecoveryDialog();
}
```
**Acceptance:**
- [ ] Corrupted SUP_OPS triggers recovery
- [ ] Recovery attempts genesis migration from pf (with staleness warning)
- [ ] Recovery attempts remote sync if configured
- [ ] User sees clear error with backup import option if all recovery fails
---
## A.5 Add Schema Migration Service
**Priority:** MEDIUM | **Effort:** Medium
**Problem:** No infrastructure for schema migrations.
**Implementation:**
```typescript
// schema-migration.service.ts
const MIGRATIONS: SchemaMigration[] = [
{
fromVersion: 1,
toVersion: 2,
migrate: (state) => ({
...state,
task: migrateTasksV1ToV2(state.task),
}),
},
];
async migrateIfNeeded(snapshot: StateCache): Promise<StateCache> {
let { state, schemaVersion } = snapshot;
while (schemaVersion < CURRENT_SCHEMA_VERSION) {
const migration = MIGRATIONS.find(m => m.fromVersion === schemaVersion);
if (!migration) throw new Error(`No migration from v${schemaVersion}`);
state = migration.migrate(state);
schemaVersion = migration.toVersion;
}
return { ...snapshot, state, schemaVersion };
}
```
**Acceptance:**
- [ ] Migration service exists
- [ ] Hydrator calls migration before dispatching
- [ ] Schema version stored in snapshot
---
# Part B Tasks: Legacy Sync Bridge
## B.1 🔴 Update META_MODEL Vector Clock on Op Write
**Priority:** CRITICAL | **Effort:** Small
**Problem:** Legacy sync compares vector clocks to detect local changes. If we don't increment META_MODEL's vector clock when ops are written, sync won't detect changes.
**File:** `src/app/core/persistence/operation-log/operation-log.effects.ts`
**Implementation:**
```typescript
private async writeOperation(op: Operation): Promise<void> {
// 1. Write to SUP_OPS (Part A)
await this.opLogStore.appendOperation(op);
// 2. Bridge to PFAPI (Part B) - CRITICAL
await this.pfapiService.pf.metaModel.incrementVectorClock(this.clientId);
// 3. Broadcast to other tabs (Part A)
this.multiTabCoordinator.broadcastOperation(op);
}
```
**Acceptance:**
- [ ] After creating a task, META_MODEL vector clock is incremented
- [ ] Legacy sync detects local changes via vector clock comparison
- [ ] Sync uploads the new task
---
## B.2 🔴 Persist Sync Downloads to SUP_OPS
**Priority:** CRITICAL | **Effort:** Medium
**Problem:** When sync downloads remote data, it dispatches `loadAllData`. Data goes to NgRx but NOT to SUP_OPS. Crash = data loss.
**Files:**
- `src/app/root-store/meta/load-all-data.action.ts`
- `src/app/core/persistence/operation-log/operation-log.effects.ts`
**Step 1:** Add metadata to action:
```typescript
// load-all-data.action.ts
export interface LoadAllDataMeta {
isHydration?: boolean; // From SUP_OPS startup - skip logging
isRemoteSync?: boolean; // From sync download - create import op
isBackupImport?: boolean; // From file import - create import op
}
export const loadAllData = createAction(
'[Meta] Load All Data',
props<{ appDataComplete: AppDataComplete; meta?: LoadAllDataMeta }>(),
);
```
**Step 2:** Handle in effects:
```typescript
// operation-log.effects.ts
// ⚠️ IMPORTANT: Use switchMap, NOT tap(async) - tap doesn't await async callbacks!
handleLoadAllData$ = createEffect(
() =>
this.actions$.pipe(
ofType(loadAllData),
filter((action) => action.meta?.isRemoteSync || action.meta?.isBackupImport),
switchMap(async (action) => {
// Create SYNC_IMPORT operation
const op: Operation = {
id: uuidv7(),
opType: 'SYNC_IMPORT',
entityType: 'ALL',
payload: action.appDataComplete,
// ...
};
await this.opLogStore.appendOperation(op);
// Force snapshot for crash safety
await this.compactionService.forceSnapshot();
}),
),
{ dispatch: false },
);
```
**Step 3:** Update sync download to pass metadata:
```typescript
// In PFAPI sync download handler
this.store.dispatch(
loadAllData({
appDataComplete: remoteData,
meta: { isRemoteSync: true },
}),
);
```
**Acceptance:**
- [ ] Sync download creates `SYNC_IMPORT` op in SUP_OPS
- [ ] Snapshot is created after sync download
- [ ] App restart after sync shows downloaded data
---
## B.3 🔴 Wire Delegate Always-On
**Priority:** HIGH | **Effort:** Small
**Problem:** `PfapiService` has conditional logic based on `useOperationLogSync` flag. Should always use delegate.
**File:** `src/app/pfapi/pfapi.service.ts`
**Current (conditional):**
```typescript
this._commonAndLegacySyncConfig$
.pipe(map(cfg => !!cfg?.useOperationLogSync), ...)
.subscribe(([wasOpLog, useOpLog]) => {
if (useOpLog) {
this.pf.setGetAllSyncModelDataFromStoreDelegate(...);
} else {
this.pf.setGetAllSyncModelDataFromStoreDelegate(null);
}
});
```
**Required (always on):**
```typescript
constructor() {
// Always use NgRx delegate for sync data - no feature flag
this.pf.setGetAllSyncModelDataFromStoreDelegate(() =>
this._storeDelegateService.getAllSyncModelDataFromStore()
);
}
```
**Also remove:**
- The subscription watching `useOperationLogSync`
- The flush-to-legacy-db logic
**Acceptance:**
- [ ] No conditional logic based on feature flag
- [ ] `getAllSyncModelData()` always reads from NgRx
- [ ] Legacy sync works correctly
---
## B.4 🔴 Migrate Non-NgRx Models
**Priority:** BLOCKER | **Effort:** Large
**Problem:** Some sync models bypass NgRx and write directly to `pf` database. ALL sync models must go through NgRx → OperationLogEffects → SUP_OPS.
**Models to migrate:**
| Model | Current Owner | Priority |
| ---------------- | ----------------- | -------- |
| `reminders` | ReminderService | High |
| `archiveYoung` | TaskService | High |
| `archiveOld` | TaskService | High |
| `pluginUserData` | PluginService | Medium |
| `pluginMetadata` | PluginService | Medium |
| `improvement` | EvaluationService | Low |
| `obstruction` | EvaluationService | Low |
**Migration steps per model:**
1. Create NgRx feature state (reducer, actions, selectors)
2. Update services to dispatch actions instead of `ModelCtrl.save()`
3. Add selector to `PfapiStoreDelegateService`
4. Update genesis migration to include model
**Acceptance:**
- [ ] All 7 models have NgRx state
- [ ] All services dispatch actions
- [ ] `PfapiStoreDelegateService` reads ALL models from NgRx
- [ ] No dual persistence paths
---
# Part C Tasks: Server Sync (Future)
These tasks are NOT needed for legacy sync. They will be implemented when server sync is built.
## C.1 Per-Op Sync Tracking
Add `syncedAt` field usage for tracking which ops have been uploaded to server.
## C.2 Sync-Aware Compaction
Modify compaction to never delete unsynced ops when server sync is enabled.
## C.3 Operation Upload/Download
Implement server API integration for uploading pending ops and downloading remote ops.
## C.4 Entity-Level Conflict Detection
Implement conflict detection using per-op vector clocks.
- Most operations are small (task title, checkbox toggles)
- Notes might benefit, but they're infrequently edited
- diff-match-patch adds complexity - defer until storage is a user-reported issue
---
@ -512,36 +105,33 @@ Implement conflict detection using per-op vector clocks.
## Part A: Local Persistence
- [ ] Create task → Reload app → Task exists
- [ ] Check SUP_OPS has the operation
- [ ] Check `pf` database task table is empty/stale
- [ ] Create 600 ops → Check compaction ran
- [ ] Corrupt SUP_OPS → App recovers from pf
- [x] Create task → Reload app → Task exists
- [x] Check SUP_OPS has the operation
- [x] Check `pf` database task table is empty/stale
- [x] Create 600 ops → Check compaction ran
- [x] Corrupt SUP_OPS → App recovers from pf
## Part B: Legacy Sync
- [ ] Create task → Check META_MODEL vector clock incremented
- [ ] WebDAV sync detects and uploads the task
- [ ] Dropbox sync detects and uploads the task
- [ ] LocalFile sync detects and uploads the task
- [ ] Sync downloads remote data → Restart → Data persists
- [x] Create task → Check META_MODEL vector clock incremented
- [x] WebDAV sync detects and uploads the task
- [x] Dropbox sync detects and uploads the task
- [x] LocalFile sync detects and uploads the task
- [x] Sync downloads remote data → Restart → Data persists
## Part C: Server Sync
- [x] Operations upload to server
- [x] Operations download from server
- [x] Conflict detection shows dialog
- [x] Choosing "remote" marks local ops as rejected
- [x] Rejected ops excluded from next sync
- [x] Persistence failure shows reload snackbar
## Multi-Tab
- [ ] Create task in Tab A → Appears in Tab B
- [ ] Both tabs have same SUP_OPS state
---
# Risk Register
| Risk | Part | Likelihood | Impact | Mitigation |
| ---------------------------------- | ---- | ---------- | ------ | --------------------------- |
| Vector clock increment breaks sync | B | Low | High | Test legacy sync thoroughly |
| Sync download persistence too slow | B | Low | Medium | Async snapshot |
| Compaction deletes needed ops | A | Low | Medium | Keep retention buffer |
| Genesis recovery fails | A | Low | High | User notification |
| Non-NgRx migration breaks features | B | Medium | High | Incremental migration |
- [x] Create task in Tab A → Appears in Tab B
- [x] Both tabs have same SUP_OPS state
---
@ -549,23 +139,24 @@ Implement conflict detection using per-op vector clocks.
```
src/app/core/persistence/operation-log/
├── operation.types.ts # Type definitions
├── operation-log-store.service.ts # SUP_OPS IndexedDB
├── operation-log.effects.ts # Action capture + META_MODEL bridge
├── operation-log-hydrator.service.ts# Startup hydration + recovery
├── operation-log-compaction.service.ts
├── operation-applier.service.ts
├── operation-converter.util.ts
├── action-blacklist.ts # UI action filtering
├── lock.service.ts
└── multi-tab-coordinator.service.ts
├── operation.types.ts # Type definitions (incl. rejectedAt)
├── operation-log-store.service.ts # SUP_OPS IndexedDB + markRejected()
├── operation-log.effects.ts # Action capture + rollback notification
├── operation-log-hydrator.service.ts # Startup hydration + recovery
├── operation-log-compaction.service.ts # Snapshot + cleanup
├── operation-log-sync.service.ts # Upload/download operations
├── operation-applier.service.ts # Apply ops with dependency handling
├── operation-converter.util.ts # Op ↔ Action conversion
├── persistent-action.interface.ts # PersistentAction type
├── lock.service.ts # Cross-tab locking
├── multi-tab-coordinator.service.ts # BroadcastChannel coordination
├── schema-migration.service.ts # State schema migrations
├── dependency-resolver.service.ts # Extract/check op dependencies
└── conflict-resolution.service.ts # Conflict UI + markRejected()
src/app/pfapi/
├── pfapi-store-delegate.service.ts # Reads NgRx for sync
└── pfapi.service.ts # Remove feature flag conditionals
src/app/root-store/shared/
└── save-to-db.effects.ts # Disable entirely
├── pfapi-store-delegate.service.ts # Reads NgRx for sync
└── pfapi.service.ts # Sync orchestration
```
---