memorio 4.9.35 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +307 -359
  3. package/SECURITY.md +17 -1
  4. package/SUMMARY.md +59 -45
  5. package/adr/001-state-proxy-model.md +96 -0
  6. package/adr/002-observer-semantics.md +180 -0
  7. package/adr/003-deep-mutation-semantics.md +129 -0
  8. package/adr/004-array-mutation-semantics.md +128 -0
  9. package/adr/005-scheduler-contract.md +149 -0
  10. package/adr/006-context-isolation.md +92 -0
  11. package/adr/007-mutation-records.md +118 -0
  12. package/adr/008-transactions.md +106 -0
  13. package/adr/009-history-model.md +110 -0
  14. package/adr/README.md +46 -0
  15. package/adr/template.md +49 -0
  16. package/examples/basic.ts +115 -115
  17. package/examples/browser-vanilla.html +358 -358
  18. package/examples/cache.ts +72 -72
  19. package/examples/cross-platform-guards.ts +57 -57
  20. package/examples/history.ts +104 -0
  21. package/examples/idb.ts +109 -109
  22. package/examples/multi-tenant-context.ts +44 -44
  23. package/examples/node-server.ts +308 -308
  24. package/examples/observer.ts +60 -60
  25. package/examples/platform.ts +115 -115
  26. package/examples/react-app.tsx +362 -362
  27. package/examples/react-observer.tsx +63 -63
  28. package/examples/semantic-memory.ts +60 -60
  29. package/examples/session-advanced.ts +91 -91
  30. package/examples/sqlite-batched-writes.ts +57 -57
  31. package/examples/state-advanced.ts +89 -89
  32. package/examples/store-advanced.ts +117 -117
  33. package/examples/sync.ts +90 -0
  34. package/examples/typed-and-schema.ts +102 -100
  35. package/examples/useObserver.tsx +140 -141
  36. package/global.cjs +4594 -0
  37. package/global.d.ts +8 -0
  38. package/global.js +4532 -0
  39. package/index.cjs +700 -678
  40. package/index.d.ts +1 -0
  41. package/index.js +680 -677
  42. package/llms.txt +72 -4
  43. package/markdown/AUDIT-REPORT.md +135 -0
  44. package/markdown/CACHE.md +100 -0
  45. package/markdown/CHANGELOG.md +243 -0
  46. package/markdown/DEVTOOLS.md +129 -0
  47. package/markdown/DISPATCH.md +177 -0
  48. package/markdown/HISTORY.md +199 -0
  49. package/markdown/IDB.md +178 -0
  50. package/markdown/IMPORT.md +153 -0
  51. package/markdown/INSPECT.md +123 -0
  52. package/markdown/LOGGER.md +154 -0
  53. package/markdown/MEMORY-ATTACHMENT.md +96 -0
  54. package/markdown/MEMORY.md +162 -0
  55. package/markdown/OBSERVER.md +209 -0
  56. package/markdown/PLATFORM.md +271 -0
  57. package/markdown/PROJECT.md +311 -0
  58. package/markdown/SCHEMA.md +176 -0
  59. package/markdown/SECURITY.md +330 -0
  60. package/markdown/SESSION.md +165 -0
  61. package/markdown/SQLITE.md +190 -0
  62. package/markdown/STATE.md +160 -0
  63. package/markdown/STORE.md +171 -0
  64. package/markdown/SYNC.md +319 -0
  65. package/markdown/TYPED.md +165 -0
  66. package/markdown/USEOBSERVER.md +257 -0
  67. package/modules/redux.cjs +320 -167
  68. package/modules/redux.cjs.map +1 -1
  69. package/modules/redux.js +320 -167
  70. package/modules/redux.js.map +1 -1
  71. package/package.json +13 -3
  72. package/types/env.d.ts +19 -9
  73. package/types/exports.d.ts +20 -0
  74. package/types/history.d.ts +13 -1
  75. package/types/memorio.d.ts +17 -5
  76. package/types/mutation.d.ts +75 -0
@@ -0,0 +1,160 @@
1
+ > **Status:** Published
2
+ > **Date:** 2026-09-12
3
+ > **Deciders:** Memorio 5.x Core Team
4
+ > **Scope**: API Reference
5
+ > **Standard**: Memorio API Specification v5
6
+ >
7
+ ---
8
+ # State - Memorio
9
+
10
+ > ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
11
+
12
+ State is a reactive global state manager using JavaScript Proxies. It's simple, powerful, and requires no setup. Data persists only in memory during the session.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install memorio
18
+ ```
19
+
20
+ ```javascript
21
+ import { state } from 'memorio';
22
+ ```
23
+
24
+ That's it. `state` is ready to use.
25
+
26
+ > **Classic `import`**: `state` is also available as a named export from the global entrypoint.
27
+ > `import 'memorio/global'` exposes the same proxy as `globalThis.state`.
28
+
29
+ ---
30
+
31
+ ## Quick Examples
32
+
33
+ ### Example 1: Basic Usage
34
+
35
+ ```javascript
36
+ // Set a value
37
+ state.name = 'Mario';
38
+ state.age = 25;
39
+
40
+ // Get a value
41
+ console.debug(state.name); // "Mario"
42
+
43
+ // Simple object
44
+ state.user = { name: 'Luigi', level: 1 };
45
+ ```
46
+
47
+ ### Example 2: Intermediate
48
+
49
+ ```javascript
50
+ // Array operations
51
+ state.items = [1, 2, 3];
52
+ state.items.push(4);
53
+ console.debug(state.items); // [1, 2, 3, 4]
54
+
55
+ // Nested objects
56
+ state.config = { theme: 'dark', lang: 'en' };
57
+ state.config.theme = 'light';
58
+
59
+ // List all states
60
+ console.debug(state.list);
61
+ ```
62
+
63
+ ### Example 3: Advanced
64
+
65
+ ```javascript
66
+ // Lock state to prevent modifications
67
+ state.frozenConfig = { maxUsers: 100 };
68
+ state.frozenConfig.lock();
69
+ // Now state.frozenConfig cannot be modified
70
+
71
+ // Path tracking
72
+ const path = state.user.path;
73
+ console.debug(path.name); // "user"
74
+ console.debug(path.profile.name); // "user.profile"
75
+
76
+ // Get full path as string
77
+ console.debug(state.user.__path); // "state.user"
78
+
79
+ // Protected keys (internal use)
80
+ console.debug(protect); // Array of protected keys
81
+ ```
82
+
83
+ ---
84
+
85
+ ## API Reference
86
+
87
+ ### Properties
88
+
89
+ | Property | Type | Description |
90
+ |----------|------|-------------|
91
+ | `state.list` | Array | Get all current state keys (deep copy) |
92
+ | `state.path` | Object | Get path tracker for current location |
93
+ | `state.__path` | string | Get full path as string |
94
+
95
+ ### Methods
96
+
97
+ | Method | Parameters | Description |
98
+ |--------|------------|-------------|
99
+ | `state.remove(key)` | `key: string` | Remove a specific state |
100
+ | `state.removeAll()` | none | Clear all states |
101
+
102
+ ### Lock
103
+
104
+ ```javascript
105
+ // Lock an object or array
106
+ state.myArray = [1, 2, 3];
107
+ state.myArray.lock();
108
+
109
+ // Now any modification will fail
110
+ state.myArray.push(4); // Error: state 'myArray' is locked
111
+ ```
112
+
113
+ ---
114
+
115
+ ## How It Works
116
+
117
+ Memorio uses JavaScript `Proxy` to intercept get/set operations on the global `state` object. This allows:
118
+
119
+ 1. **Reactivity** - Any change can trigger observers
120
+ 2. **Nested objects** - Deep path tracking
121
+ 3. **Type safety** - Full TypeScript support
122
+
123
+ ---
124
+
125
+ ## Platform Notes
126
+
127
+ | Platform | Support | Notes |
128
+ |----------|---------|-------|
129
+ | Browser | ✅ Full | In-memory, lost on refresh |
130
+ | Node.js | ✅ Full | In-memory, lost on restart |
131
+ | Deno | ✅ Full | In-memory, lost on restart |
132
+ | Edge Workers | ✅ Full | In-memory, lost on function cold start |
133
+
134
+ **Note**: In server environments (Node.js/Deno), use `memorio.createContext()` for request isolation.
135
+
136
+ ---
137
+
138
+ ## Best Practices
139
+
140
+ 1. Use descriptive keys: `state.userProfile` not `state.up`
141
+ 2. Group related data: `state.cart.items` not `state.cartItems`
142
+ 3. Lock static config: `state.appConfig.lock()`
143
+ 4. Clean up on logout: `state.removeAll()`
144
+ 5. Use path tracking for debugging: `state.myData.__path`
145
+
146
+ ---
147
+
148
+ ## Common Errors
149
+
150
+ ```javascript
151
+ // Error: protected key
152
+ state._internal = 'value';
153
+ // Output: "key _internal is protected"
154
+
155
+ // Error: locked state
156
+ state.locked = { x: 1 };
157
+ state.locked.lock();
158
+ state.locked.x = 2;
159
+ // Output: "Error: state 'locked' is locked"
160
+ ```
@@ -0,0 +1,171 @@
1
+ > **Status:** Published
2
+ > **Date:** 2026-09-12
3
+ > **Deciders:** Memorio 5.x Core Team
4
+ > **Scope**: API Reference
5
+ > **Standard**: Memorio API Specification v5
6
+ >
7
+ ---
8
+ # Store - Memorio
9
+
10
+ > 🖥️ **Browser & Edge**: Uses localStorage for persistence
11
+ > ⚙️ **Node.js/Deno**: Falls back to in-memory storage (not persistent)
12
+
13
+ Store provides persistent localStorage management with a simple API. Data survives page refreshes and browser restarts.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install memorio
19
+ ```
20
+
21
+ ```javascript
22
+ import { store } from 'memorio';
23
+ ```
24
+
25
+ > **Classic `import`**: `store` is also available via the global entrypoint.
26
+ > `import 'memorio/global'` exposes the same instance as `globalThis.store`.
27
+
28
+ ---
29
+
30
+ ## Quick Examples
31
+
32
+ ### Example 1: Basic Usage
33
+
34
+ ```javascript
35
+ // Save data
36
+ store.set('username', 'Mario');
37
+ store.set('score', 1500);
38
+
39
+ // Read data
40
+ console.debug(store.get('username')); // "Mario"
41
+ console.debug(store.get('score')); // 1500
42
+
43
+ // Check if using real persistence
44
+ console.debug(store.isPersistent); // true in browser, false in Node.js/Deno
45
+ ```
46
+
47
+ ### Example 2: Intermediate
48
+
49
+ ```javascript
50
+ // Store objects
51
+ store.set('user', { name: 'Luigi', level: 5 });
52
+ const user = store.get('user');
53
+ console.debug(user.name); // "Luigi"
54
+
55
+ // Remove single item
56
+ store.remove('username');
57
+
58
+ // Check size
59
+ const totalSize = store.size();
60
+ console.debug(`${totalSize} bytes`);
61
+ ```
62
+
63
+ ### Example 3: Advanced
64
+
65
+ ```javascript
66
+ // Get storage quota (returns Promise<[usage, quota]> in KB)
67
+ const [used, total] = await store.quota();
68
+ console.debug(`Using ${used} out of ${total} KB`);
69
+
70
+ // Get total size in characters
71
+ const size = store.size();
72
+ console.debug(`${size} bytes`);
73
+
74
+ // Clear all data
75
+ store.removeAll();
76
+ // or use alias
77
+ store.clearAll();
78
+
79
+ // Handle errors gracefully
80
+ try {
81
+ store.set('largeData', hugeObject);
82
+ } catch (err) {
83
+ console.error('Storage full:', err);
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## API Reference
90
+
91
+ ### Methods
92
+
93
+ | Method | Parameters | Returns | Description |
94
+ |--------|------------|---------|-------------|
95
+ | `store.get(name)` | `name: string` | `any` | Get value from storage |
96
+ | `store.set(name, value)` | `name: string, value: any` | `void` | Save value to storage |
97
+ | `store.remove(name)` | `name: string` | `boolean` | Remove single item |
98
+ | `store.delete(name)` | `name: string` | `boolean` | Alias for remove |
99
+ | `store.removeAll()` | none | `boolean` | Clear all storage |
100
+ | `store.clearAll()` | none | `boolean` | Alias for removeAll |
101
+ | `store.size()` | none | `number` | Get total size in characters |
102
+ | `store.quota()` | none | `Promise<[number, number]>` | Get storage usage/quota in KB |
103
+
104
+ ### Properties
105
+
106
+ | Property | Type | Description |
107
+ |----------|------|-------------|
108
+ | `store.isPersistent` | `boolean` | `true` if using real localStorage, `false` if in-memory fallback |
109
+
110
+ ### Supported Types
111
+
112
+ ```javascript
113
+ // All JSON-serializable types work
114
+ store.set('string', 'hello');
115
+ store.set('number', 42);
116
+ store.set('boolean', true);
117
+ store.set('array', [1, 2, 3]);
118
+ store.set('object', { key: 'value' });
119
+ store.set('null', null);
120
+ store.set('undefined', null); // converted to null
121
+ ```
122
+
123
+ ### Not Supported
124
+
125
+ ```javascript
126
+ // Functions will log an error
127
+ store.set('myFunc', () => {});
128
+ // Output: "It's not secure to store functions."
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Platform Comparison
134
+
135
+ | Feature | Store | Session | Cache | IDB |
136
+ |---------|-------|---------|-------|-----|
137
+ | **Storage** | localStorage | sessionStorage | Memory | IndexedDB |
138
+ | **Lifetime** | Forever | Until tab closes | Until refresh | Forever |
139
+ | **Capacity** | ~5-10 MB | ~5-10 MB | Unlimited | 50+ MB |
140
+ | **Platform** | Browser/Edge | Browser/Edge | All | Browser |
141
+ | **Persistence** | ✅ true | N/A | ❌ false | ✅ true |
142
+
143
+ ---
144
+
145
+ ## How It Works
146
+
147
+ Store wraps the browser's `localStorage` API with:
148
+
149
+ - Automatic JSON serialization/deserialization
150
+ - Error handling for parse failures
151
+ - Size calculation
152
+ - Quota monitoring
153
+
154
+ ---
155
+
156
+ ## Storage Limits
157
+
158
+ - **Chrome/Safari**: ~5-10 MB
159
+ - **Firefox**: ~10 MB
160
+ - **Edge**: ~5-10 MB
161
+
162
+ Use `store.quota()` to monitor usage.
163
+
164
+ ---
165
+
166
+ ## Best Practices
167
+
168
+ 1. Prefix keys: `store.set('app_username', '...')`
169
+ 2. Check before set: `if (store.get('key')) { ... }`
170
+ 3. Handle quota: Try/catch around large data
171
+ 4. Clean up: `store.removeAll()` on logout
@@ -0,0 +1,319 @@
1
+ > **Status:** Published
2
+ > **Date:** 2026-09-12
3
+ > **Deciders:** Memorio 5.x Core Team
4
+ > **Scope**: API Reference
5
+ > **Standard**: Memorio API Specification v5
6
+ >
7
+ ---
8
+ # Synchronization & Cloud (optional)
9
+
10
+ `memorio.memory` is **local-first**. Data is created and served from the device;
11
+ the cloud is only ever a **transport/persistence provider**, never the source of
12
+ truth. Enabling sync does not replace local storage - it *mirrors* it.
13
+
14
+ ```
15
+ memorio
16
+ │
17
+ ┌────────┴────────┐
18
+ │ Memory Engine │
19
+ └────────┬────────┘
20
+ ┌────────────┼────────────┐
21
+ ▼ ▼ ▼
22
+ local SQLite cloud
23
+ memory durable sync
24
+ ```
25
+
26
+ ## 1. The rule: the data is born local
27
+
28
+ ```ts
29
+ memorio.memory.remember('user.language', 'Italian', { scope: 'local' })
30
+ // ↓ local first
31
+ // store / sessionStorage / IndexedDB / sql.js
32
+ // ↓ sync / push (when online)
33
+ // cloud provider
34
+ ```
35
+
36
+ The cloud therefore does not **replace** memory: it **replicates** it. This
37
+ gives you: offline-first, lowest latency, data available immediately,
38
+ synchronization when online, multi-device, multi-user, centralized persistence.
39
+
40
+ We deliberately do **not** provide:
41
+
42
+ ```ts
43
+ // ❌ two mental models
44
+ memory.cloud.save(...)
45
+ ```
46
+
47
+ Instead:
48
+
49
+ ```ts
50
+ memorio.memory.remember('user.language', 'Italian')
51
+ // and a single configuration point:
52
+ memorio.memory.configure({ sync: { provider: myCloudProvider, namespace: '…' } })
53
+ ```
54
+
55
+ ## 2. Scopes (isolation, not a security boundary)
56
+
57
+ | Scope | Lifetime | Syncs by default |
58
+ |---|---|---|
59
+ | `'device'` | this browser/device only | no (sticky) |
60
+ | `'user'` | follows the user across devices | yes (requires provider + namespace) |
61
+ | `'shared'` | shared across users / tenant | yes (requires provider + namespace) |
62
+
63
+ > As with `memorio.createContext`, **scoping is a naming convention, not a
64
+ > security boundary.** Enforce real isolation server-side.
65
+
66
+ ## 3. SQLite as the local durable store
67
+
68
+ SQLite (`sql.js`) is **in-memory by default** (volatie per page load). It becomes
69
+ the durable journal/value store when you opt in:
70
+
71
+ - `sqlite.config({ persistence: true })` / `sqlite.db.create('app', { persistence: true })`
72
+ snapshot the database to `store` (localStorage) and restore it on reopen.
73
+ - Writes are snapshotted via sql.js `updateHook` (debounced).
74
+ - `sqlite.db.persist(name)` forces an immediate save; `sqlite.db.close(name)`
75
+ flushes + closes; `sqlite.db.download(name, file?)` triggers a browser
76
+ `.sqlite` download (dev convenience).
77
+
78
+ See `docs/markdown/SQLITE.md` for the full SQLite reference.
79
+
80
+ ## 4. The local operation journal
81
+
82
+ The **sync journal** is the durable op log that drives cloud reconciliation.
83
+ It is persisted on `store` (localStorage) - **not** on an in-memory sql.js db,
84
+ because pending operations must survive a refresh for offline-first to work.
85
+
86
+ | Method | Returns | Notes |
87
+ |---|---|---|
88
+ | `memory.journal.append(entry, operation)` | `Promise<MemoryEntry>` | records `remember\|update\|forget\|expire\|confirm\|supersede` with `sync:'pending'` |
89
+ | `memory.journal.pending()` | `Promise<MemoryEntry[]>` | rows where `sync != 'synced'`, for the current namespace |
90
+ | `memory.journal.markSynced(ids)` | `Promise<number>` | advances rows to `synced` (namespace-scoped) |
91
+ | `memory.journal.get(id)` | `Promise<MemoryEntry \| null>` | single entry, namespace-scoped |
92
+ | `memory.journal.clear()` | `Promise<void>` | wipes the current namespace's journal |
93
+ | `memory.journal.replay()` | `Promise<SyncAck>` | pushes `pending()` to the provider, marks synced, optional `pull` |
94
+ | `memory.journal.status()` | `Promise<'store'>` | the substrate in use |
95
+
96
+ We sync **operations of memory**, never a raw database dump:
97
+
98
+ ```
99
+ user A device A
100
+ remember X ─────► local ─────► sync ─────► cloud
101
+ forget Z ──────► local ─────► sync ─────► cloud
102
+ ```
103
+
104
+ ## 5. Conflict resolution
105
+
106
+ The cloud must not simply say "last write wins." Memorio tags every entry with:
107
+
108
+ - `confidence` (0–1, user/system trust in the value)
109
+ - `lastConfirmedAt` / `updatedAt` (epoch ms)
110
+ - `version` (monotonic per-key counter)
111
+ - `source` / `scope`
112
+
113
+ Remote conflicts are surfaced as `sync:'conflict'` rows via
114
+ `journal.pending()`; the provider's `resolve(op)` hint decides locally. Example:
115
+
116
+ ```
117
+ Laptop: language=Italian, confidence=0.92
118
+ Phone: language=English, confidence=0.61
119
+ → higher-confidence entry wins locally; the provider decides for shared scope.
120
+ ```
121
+
122
+ ## 6. Configuring a backend
123
+
124
+ Sync is **opt-in**. You supply an application-owned `provider` that knows how to
125
+ talk to your backend (REST, WebSocket, Supabase, a custom agent server, …).
126
+
127
+ ```ts
128
+ memorio.memory.configure({
129
+ namespace: 'user:123:device:abc', // tenant/user/device - partitions the journal
130
+ provider: {
131
+ push(ops) { return fetch('/api/sync', { method: 'POST', body: JSON.stringify(ops), headers: authHeaders }) }
132
+ pull(since) { return fetch(`/api/sync?since=${since}`).then(r => r.json()) }
133
+ resolve(op) { return op.confidence >= 0.8 ? 'local' : 'remote' }
134
+ },
135
+ auto: true // auto-replay on focus/online (default true)
136
+ })
137
+ ```
138
+
139
+ ```ts
140
+ interface SyncProvider {
141
+ push(ops: MemoryEntry[]): Promise<{ synced: string[]; conflicts?: string[]; error?: string }>
142
+ pull?(since?: number): Promise<MemoryEntry[]>
143
+ resolve?(op: MemoryEntry): Promise<'local' | 'remote' | 'merge'>
144
+ }
145
+ ```
146
+
147
+ `memorio.memory.ready` resolves once the local journal substrate is chosen.
148
+
149
+ ## 7. Security (NIST / OWASP / NSA posture)
150
+
151
+ - **Memorio never handles credentials.** No passwords, tokens, or API keys are
152
+ read from or stored by memorio. Authentication/authorization live in your
153
+ `provider`/backend (OWASP A01: Broken Access Control).
154
+ - **Namespace isolation.** The journal is keyed by `namespace:id` at the storage
155
+ layer; there is **no API** to enumerate or open another namespace's journal. A
156
+ client holding a forged/fake namespace simply sees its own (empty) journal.
157
+ - **No dynamic code.** Journal entries are strictly JSON-round-tripped,
158
+ size-capped (10 MB/entry), and never `eval`'d. The sql.js loader never
159
+ `import()`s a bare specifier that could be hijacked at build time.
160
+ - **Trust boundary:** memorio owns the local durable copy + operation log; the
161
+ provider/backend owns remote-side auth and conflict resolution. Memorio
162
+ surfaces `conflict`/`error` rows; it does not fabricate a winner.
163
+ - **Data-at-rest (NSA/CISA).** memorio's `store`/`idb`/`sqlite` snapshots are
164
+ **not encrypted**. If you persist user data server-side or ship it through your
165
+ backend, encrypt it server-side with keys you manage - memorio treats the local
166
+ store as untrusted-from-the-browser and does not attest its own integrity.
167
+
168
+ ## 8. Where data lives
169
+
170
+ | Substrate | API | Volatile? | Persistent? |
171
+ |---|---|---|---|
172
+ | in-memory `Proxy` | `state` | yes (per tab) | no |
173
+ | `localStorage` / Map | `store` | no | yes (browser) |
174
+ | `sessionStorage` / Map | `session` | no | per-tab (browser) |
175
+ | IndexedDB | `idb`, `memory` durable | no | yes |
176
+ | sql.js (WASM heap) | `sqlite` | **yes** | only with `persistence: true` (snapshot → `store`) |
177
+ | sync journal | `memory.journal` | no | yes (`store`) |
178
+
179
+ ---
180
+
181
+ ## 9. Evolving the journal toward multi-device consistency
182
+
183
+ Moving from sequential, single-device sync to concurrent offline edits across
184
+ multiple devices is a classic local-first challenge. Below are six
185
+ architectural strategies, in increasing order of sophistication, that can be
186
+ layered onto the existing journal **without** adopting a full CRDT framework
187
+ (Yjs, Automerge, etc.).
188
+
189
+ > **Scope note.** These strategies target `memorio.memory` first - it already
190
+ > carries the metadata a journal needs (`confidence`, `source`, `tag`, `scope`,
191
+ > `createdAt`, `lastConfirmedAt`). If synchronization is ever extended to
192
+ > `state` or `store`, those layers must gain HLC timestamps and path-level
193
+ > fields explicitly - they cannot inherit them from `memory`.
194
+
195
+ ### 9.1 Field-level / path-level journaling
196
+
197
+ Recording an entire entity in the journal makes orthogonal edits un-mergeable:
198
+
199
+ ```json
200
+ { "op": "set", "path": "user.role", "value": "admin", "timestamp": 1710000000 }
201
+ ```
202
+
203
+ Device A writes `user.name`, device B writes `user.role` - both are patch
204
+ operations on the **same entity**. An entity-level journal would produce one
205
+ opaque `UPDATE user = {…}` and one write would clobber the other. Path-level
206
+ patches merge automatically because the paths are disjoint.
207
+
208
+ > **Not a panacea.** Path-level journaling merges edits to *different* fields.
209
+ > Two devices writing the **same** path concurrently still need explicit conflict
210
+ > resolution (Section 5). HLC tells you *when* the events happened; it does not
211
+ > tell you *which value wins* when events are truly concurrent on the same path.
212
+
213
+ ### 9.2 Causal ordering with Hybrid Logical Clocks (HLC)
214
+
215
+ Wall-clock timestamps alone fail under clock drift. Associate every journal
216
+ entry with an HLC that combines a physical component, a logical counter, and a
217
+ node identifier:
218
+
219
+ ```
220
+ hlc:1710000005:2:deviceB
221
+ ```
222
+
223
+ This gives constant-size causal ordering (vs. vector clocks, which grow with
224
+ the number of writers - problematic for a bounded journal). The sync engine can
225
+ then apply causally-dependent operations in order and invoke the conflict
226
+ resolver only for genuinely concurrent writes on the same path.
227
+
228
+ ### 9.3 Ordering collections without full OT - fractional indexing
229
+
230
+ Arrays and ordered lists are the hardest non-CRDT case. Numeric indices shift
231
+ when a peer inserts or deletes nearby. Two lightweight options:
232
+
233
+ 1. **Keyed collections** - treat list items as a `Map<id, value>` rather than a
234
+ positional array. No index renumbering needed.
235
+ 2. **Fractional indexing** - assign each element a sortable key between its
236
+ neighbours (e.g. `1.0`, `2.0` → insert at `1.5`). On repeated re-inserts
237
+ between the same pair, keys grow in length and should be rebalanced
238
+ periodically. Use a mature library (`fractional-indexing` on npm) rather than
239
+ reimplementing the arithmetic.
240
+
241
+ ### 9.4 Explicit deletions (tombstones)
242
+
243
+ A bare "remove" entry can be resurrected as a "zombie" when a concurrent
244
+ update is replayed after it. Instead, record every `forget` / `delete` as a
245
+ first-class journal event with its own HLC:
246
+
247
+ ```json
248
+ { "op": "delete", "path": "user.role", "timestamp": "hlc:1710000005:0:deviceA" }
249
+ ```
250
+
251
+ The tombstone participates in the same causal comparison as `set` events: if
252
+ the delete's HLC succeeds the update's, the field is gone; if it precedes, the
253
+ update is re-applied. Keep tombstones around until all known peers have
254
+ acknowledged them, then garbage-collect during a maintenance sweep.
255
+
256
+ ### 9.5 Hybrid sync - server-assisted consensus
257
+
258
+ Since memorio's cloud role is transport-and-acknowledgement (not source of
259
+ truth), the backend can resolve conflicts on the server and return the
260
+ canonical sequence:
261
+
262
+ | Pattern | Description |
263
+ |---|---|
264
+ | **Optimistic local apply** | Apply the local journal entry immediately and emit reactive events. |
265
+ | **Server ack** | Backend validates causal order against the central state and returns the official sequence. |
266
+ | **Client journal rebase** | Confirmed entries are purged from the local journal; unconfirmed local entries are replayed on top of the acknowledged state. |
267
+
268
+ ### 9.6 Validation - convergence simulation
269
+
270
+ Causal correctness is theoretical until you test it across replay orderings:
271
+
272
+ - Generate random concurrent `set` / `delete` operations across N simulated
273
+ devices (shared paths and disjoint paths).
274
+ - Replay the operation log in every plausible ordering on each simulated
275
+ device.
276
+ - Assert **state convergence**: every device arrives at the same final state
277
+ regardless of delivery order.
278
+ - Include hand-crafted pathological cases (`update` vs concurrent `delete` on
279
+ the same path, `insert` vs concurrent `delete` on the same array index,
280
+ interleaved reorders).
281
+
282
+ ### 9.7 Migration
283
+
284
+ Path-level journal entries with HLC and tombstones are a format change from
285
+ entity-level entries. Mitigate with:
286
+
287
+ - An explicit **version header** on every journal entry.
288
+ - A clear migration policy: either a one-time compaction pass that folds
289
+ legacy entity-level entries into the current state and starts a fresh
290
+ journal, or a dual-format reader that can replay both formats during the
291
+ transition window.
292
+
293
+ ### 9.8 Recommended architecture diagram
294
+
295
+ ```text
296
+ [ Local Mutation ]
297
+ │
298
+ ▼
299
+ [ Field-Level Patch (set/delete) + HLC Timestamp ]
300
+ │
301
+ ├───► Local State (immediate reactive update)
302
+ │
303
+ └───► Local Journal (incl. tombstones for deletes)
304
+ │
305
+ (Online Sync)
306
+ │
307
+ ▼
308
+ [ Backend Conflict Resolver ]
309
+ (concurrent path → resolveConflict;
310
+ otherwise apply in HLC order)
311
+ │
312
+ ▼
313
+ [ State Ack / Rebased Journal ]
314
+ ```
315
+
316
+ Applying granular path-level patches, HLC for causality, fractional indexing
317
+ for ordered collections, explicit tombstones for deletions, and a convergence
318
+ test suite lets memorio handle high-frequency concurrent offline edits across
319
+ devices without the overhead of a full CRDT stack.