memorio 4.9.35 → 5.1.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 (82) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +95 -516
  3. package/SECURITY.md +159 -33
  4. package/SUMMARY.md +59 -45
  5. package/adr/001-state-proxy-model.md +95 -0
  6. package/adr/002-observer-semantics.md +179 -0
  7. package/adr/003-deep-mutation-semantics.md +128 -0
  8. package/adr/004-array-mutation-semantics.md +127 -0
  9. package/adr/005-scheduler-contract.md +148 -0
  10. package/adr/006-context-isolation.md +91 -0
  11. package/adr/007-mutation-records.md +117 -0
  12. package/adr/008-transactions.md +105 -0
  13. package/adr/009-history-model.md +109 -0
  14. package/adr/README.md +46 -0
  15. package/adr/template.md +48 -0
  16. package/bin/cli.js +68 -0
  17. package/examples/basic.ts +115 -115
  18. package/examples/browser-vanilla.html +358 -358
  19. package/examples/cache.ts +72 -72
  20. package/examples/cross-platform-guards.ts +57 -57
  21. package/examples/history.ts +104 -0
  22. package/examples/idb.ts +109 -109
  23. package/examples/multi-tenant-context.ts +44 -44
  24. package/examples/node-server.ts +308 -308
  25. package/examples/observer.ts +60 -60
  26. package/examples/platform.ts +115 -115
  27. package/examples/react-app.tsx +362 -362
  28. package/examples/react-observer.tsx +63 -63
  29. package/examples/semantic-memory.ts +60 -60
  30. package/examples/session-advanced.ts +91 -91
  31. package/examples/sqlite-batched-writes.ts +57 -57
  32. package/examples/state-advanced.ts +89 -89
  33. package/examples/store-advanced.ts +117 -117
  34. package/examples/sync.ts +90 -0
  35. package/examples/typed-and-schema.ts +102 -100
  36. package/examples/useObserver.tsx +140 -141
  37. package/global.cjs +5733 -0
  38. package/global.d.ts +8 -0
  39. package/global.js +5667 -0
  40. package/index.cjs +2202 -1041
  41. package/index.d.ts +2 -0
  42. package/index.js +2178 -1040
  43. package/llms.txt +113 -8
  44. package/markdown/AUDIT-REPORT.md +134 -0
  45. package/markdown/CACHE.md +191 -0
  46. package/markdown/DEVTOOLS.md +128 -0
  47. package/markdown/DISPATCH.md +176 -0
  48. package/markdown/HISTORY.md +198 -0
  49. package/markdown/IDB.md +177 -0
  50. package/markdown/IMPORT.md +152 -0
  51. package/markdown/INSPECT.md +122 -0
  52. package/markdown/LOGGER.md +153 -0
  53. package/markdown/MEMORY-ATTACHMENT.md +95 -0
  54. package/markdown/MEMORY.md +161 -0
  55. package/markdown/OBSERVER.md +208 -0
  56. package/markdown/PLATFORM.md +277 -0
  57. package/markdown/REDUX.md +54 -0
  58. package/markdown/SCHEMA.md +175 -0
  59. package/markdown/SESSION.md +164 -0
  60. package/markdown/SQLITE.md +189 -0
  61. package/markdown/STATE.md +159 -0
  62. package/markdown/STORE.md +170 -0
  63. package/markdown/SYNC.md +318 -0
  64. package/markdown/TYPED.md +164 -0
  65. package/markdown/USEOBSERVER.md +256 -0
  66. package/modules/redux.cjs +701 -177
  67. package/modules/redux.cjs.map +1 -1
  68. package/modules/redux.js +701 -177
  69. package/modules/redux.js.map +1 -1
  70. package/package.json +26 -4
  71. package/types/broadcast.d.ts +61 -0
  72. package/types/computed.d.ts +96 -0
  73. package/types/encryption.d.ts +129 -0
  74. package/types/env.d.ts +19 -9
  75. package/types/exports.d.ts +29 -0
  76. package/types/history.d.ts +13 -1
  77. package/types/memorio.d.ts +25 -6
  78. package/types/mutation.d.ts +75 -0
  79. package/types/security.d.ts +67 -0
  80. package/types/session.d.ts +23 -5
  81. package/types/store.d.ts +19 -3
  82. package/vsix/memorio.vsix +0 -0
@@ -0,0 +1,128 @@
1
+ # ADR-003: Deep Mutation Semantics
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ Deep mutations (e.g. `state.user.profile.name = "Alice"`) are the most
9
+ common source of subtle bugs in proxy-based state libraries. The
10
+ 4.7.3 baseline had forensic defects around deep mutation notifications:
11
+ observers not firing, paths not resolved correctly, payloads missing
12
+ arguments. While these defects were reported as not reproducible in
13
+ repo v5.1.0, the semantics remain **undocumented** - they rely on
14
+ accidental Proxy behavior rather than an explicit contract.
15
+
16
+ This ADR makes the deep mutation semantics explicit.
17
+
18
+ ## Assumptions
19
+
20
+ - `buildProxy` recursively wraps nested plain objects and arrays.
21
+ - Each Proxy tracks its dotted path via the `tree` array.
22
+ - The set trap computes `path = objPath(key, tree)` - a dotted path
23
+ relative to `state` (e.g. `user.profile.name`).
24
+ - The callback receives `{ action, path, newValue, previousValue }`.
25
+ - The proxy stores **raw** values (via `deepRaw`) on the raw target.
26
+
27
+ ## Decision
28
+
29
+ ### Deep mutation notification scope
30
+
31
+ When `state.user.profile.name = "Alice"` executes:
32
+
33
+ 1. The set trap on the deepest proxy (the `profile` object) fires.
34
+ 2. `path` is computed as `user.profile.name` (relative to `state`).
35
+ 3. The callback receives `{ path: "user.profile.name", ... }`.
36
+ 4. `dispatch.set("state.user.profile.name", ...)` is called.
37
+ 5. **Only** observers on `state.user.profile.name` fire.
38
+ 6. `state.user.profile.name` is updated on the raw target to `"Alice"`.
39
+
40
+ Observers on `state.user.profile`, `state.user`, or `state` do **not**
41
+ receive a notification for this mutation. This is the exact-path
42
+ contract (see ADR-002, §1).
43
+
44
+ ### Reference identity after deep mutation
45
+
46
+ After `state.user.profile.name = "Alice"`:
47
+ - `state.user` returns the **same** proxy wrapper (cached via WeakMap).
48
+ - `state.user.profile` returns the **same** proxy wrapper.
49
+ - `state.user.profile.name` returns `"Alice"` (the new primitive).
50
+ - `state.user.profile[TARGET]` returns the same raw object as before
51
+ (the object was mutated in place, not replaced).
52
+
53
+ ### Intermediate object creation
54
+
55
+ If a deep path does not exist yet (`state.user.profile` is undefined),
56
+ and a consumer writes `state.user.profile = { name: "Alice" }`:
57
+ 1. The set trap on `user` fires with `path = "user.profile"`.
58
+ 2. The full object `{ name: "Alice" }` is stored as the raw value.
59
+ 3. A new proxy wrapper is created for `{ name: "Alice" }` on the next
60
+ read.
61
+ 4. The event dispatched is `state.user.profile` - **not**
62
+ `state.user.profile.name`.
63
+
64
+ There is **no automatic intermediate object creation** during deep
65
+ assignment to nested proxies. If `state.user` is undefined and the
66
+ consumer writes `state.user.profile.name = "Alice"`, this throws a
67
+ TypeError (cannot set property 'profile' of undefined).
68
+
69
+ To set deeply nested values that don't exist yet, use
70
+ `memorio.mutate("state.user.profile.name", "Alice")` which creates
71
+ intermediate objects programmatically.
72
+
73
+ ### No-op mutations
74
+
75
+ If a set trap assigns the same value (`state.x = state.x`), the set
76
+ trap still fires. The callback still dispatches the event. However,
77
+ the `mutationToPatch` function returns `null` for `before === after`,
78
+ so no patch is stored in the commit log. The undo/redo stack records
79
+ the mutation (with `before === after`), but undo is a no-op for such
80
+ entries.
81
+
82
+ A future enhancement may add value-comparison in the set trap to skip
83
+ no-op dispatches. This ADR does **not** implement that.
84
+
85
+ ### Delete semantics
86
+
87
+ `delete state.user.profile.name`:
88
+ 1. The `deleteProperty` trap on the `profile` proxy fires.
89
+ 2. `path` is computed as `user.profile.name`.
90
+ 3. The callback receives `{ action: "delete", path, previousValue }`.
91
+ 4. The event dispatched is `state.user.profile.name`.
92
+ 5. The property is removed from the raw target.
93
+
94
+ ### Array deep mutation
95
+
96
+ When a nested object inside an array is mutated (`state.items[0].name = "X"`):
97
+ 1. The set trap fires on the proxy wrapping `items[0]`.
98
+ 2. `path` is computed as `items.0.name` (array index joined by dots).
99
+ 3. The event dispatched is `state.items.0.name`.
100
+ 4. **Additionally**, because arrays use identity-based reconciliation,
101
+ the observer contract (ADR-002 §3) states that array observers fire
102
+ on the **array path** (`state.items`), not the element path. However,
103
+ the dispatch only fires `state.items.0.name`. Array observers
104
+ registered via `observer('state.items', ...)` will **not** fire for
105
+ `items[0].name` mutations.
106
+
107
+ > **Note:** This is a known limitation. Array element mutations that
108
+ > change object properties do not notify the array-path observer.
109
+ > A future scheduler or mutation engine enhancement may address this
110
+ > by dispatching on both `state.items.0.name` and `state.items`.
111
+ > This ADR documents the current behavior.
112
+
113
+ ## Consequences
114
+
115
+ - **Positive:** Deep mutations are deterministic - exactly one event
116
+ per mutation, on the exact leaf path.
117
+ - **Positive:** Reference identity is preserved - the same proxy wrapper
118
+ is reused, preventing proxy-depth accumulation (regression tested).
119
+ - **Positive:** `memorio.mutate()` provides a safe path-based API for
120
+ setting deeply nested values that don't exist yet.
121
+ - **Negative:** Array element object mutations don't notify array-path
122
+ observers - consumers must observe the specific element path.
123
+ - **Negative:** No-op set traps still dispatch events - this is
124
+ intentional for consistency but may cause unnecessary re-renders.
125
+
126
+ ## Compliance tests
127
+
128
+ - `tests/vitest/tests/contracts/adr-003-deep-mutation.test.ts`
@@ -0,0 +1,127 @@
1
+ # ADR-004: Array Mutation Semantics
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ Arrays are the most error-prone data structure in proxy-based state
9
+ libraries. The 4.7.3 baseline handled `push` and `pop` but had no
10
+ explicit contract for `sort`, `splice`, `shift`, `unshift`, `fill`,
11
+ `reverse`, `length` changes, or whole-array replacement. Section 9 of
12
+ the Memorio 5.x plan demands explicit, regression-tested behavior for
13
+ every array operation.
14
+
15
+ A mutation that persists but does not notify, or notifies but does not
16
+ persist, is considered a **semantic defect** unless explicitly
17
+ documented.
18
+
19
+ ## Assumptions
20
+
21
+ - Arrays are plain JS arrays stored on the Proxy raw target.
22
+ - Array methods (`push`, `pop`, `splice`, etc.) mutate the array in
23
+ place and trigger the set trap on the Proxy wrapping the array.
24
+ - The set trap's `key` for array mutations is the array index or
25
+ `length`.
26
+ - The callback receives `{ action, path, newValue, previousValue }`.
27
+ - The state Proxy dispatches via `dispatch.set("state." + path)`.
28
+
29
+ ## Decision
30
+
31
+ ### Mutation → Persistence mapping
32
+
33
+ Every array operation below **must** persist to the underlying raw
34
+ array and **must** dispatch an event on the **array path**
35
+ (`state.<arrayPath>`), not on the individual index path. The event
36
+ path is computed from the Proxy `tree` (e.g. for `state.items`, the
37
+ tree is `["items"]`, so the path is `items` and the event is
38
+ `state.items`).
39
+
40
+ | Operation | Persists? | Dispatches on | Patch op |
41
+ |-----------|-----------|---------------|----------|
42
+ | `state.arr[i] = val` | Yes | `state.arr` | `replace` (index i) |
43
+ | `state.arr.push(val)` | Yes | `state.arr` | `add` (at index `arr.length`) |
44
+ | `state.arr.pop()` | Yes | `state.arr` | `remove` (last index) |
45
+ | `state.arr.shift()` | Yes | `state.arr` | `remove` (index 0) |
46
+ | `state.arr.unshift(val)` | Yes | `state.arr` | `add` (at index 0) |
47
+ | `state.arr.splice(i, n, ...items)` | Yes | `state.arr` | `remove` + `add` |
48
+ | `state.arr.sort()` | Yes | `state.arr` | `replace` (whole array) |
49
+ | `state.arr.reverse()` | Yes | `state.arr` | `replace` (whole array) |
50
+ | `state.arr.fill(val, start, end)` | Yes | `state.arr` | `replace` (whole array) |
51
+ | `state.arr.length = n` (truncate) | Yes | `state.arr` | `remove` (removed indices) |
52
+ | `state.arr.length = n` (extend) | Yes | `state.arr` | `add` (new indices) |
53
+ | `state.arr = newArray` | Yes | `state.arr` | `replace` (whole array) |
54
+ | `state.arr[i].prop = val` | Yes | `state.arr.i.prop` | `replace` (leaf) |
55
+
56
+ ### sort() persistence
57
+
58
+ `sort()` mutates the array in place and returns a reference to the same
59
+ (sorted) array. The Proxy set trap fires because `length` changes and
60
+ indices are reassigned. The event dispatched is `state.arr`. The
61
+ mutation is **persisted** to the raw target.
62
+
63
+ **Known constraint:** `sort()` with a custom comparator must also
64
+ persist correctly. The comparator function is applied by V8's native
65
+ `Array.prototype.sort`; the Proxy only sees the resulting index
66
+ reassignments.
67
+
68
+ ### splice() semantics
69
+
70
+ `splice()` can add, remove, or replace elements at an index. The Proxy
71
+ sees multiple set/delete traps. The event dispatched is `state.arr`
72
+ (the first trap fires, and subsequent traps on the same array path
73
+ are de-duplicated by the dispatch layer's microtask scheduling).
74
+
75
+ ### length changes
76
+
77
+ Setting `arr.length = n` where `n < arr.length` truncates the array.
78
+ The deleteProperty trap fires for each removed index. Setting
79
+ `arr.length = n` where `n > arr.length` extends the array with
80
+ `undefined` holes; the set trap fires for `length`.
81
+
82
+ ### Whole-array replacement
83
+
84
+ `state.arr = [1, 2, 3]` replaces the entire array. The set trap fires
85
+ once with `path = "arr"`, `newValue = [1, 2, 3]` (deepRaw'd). The
86
+ event dispatched is `state.arr`. Observers on `state.arr` fire.
87
+ Previous array observers (on the old array instance) are orphaned -
88
+ the consumer must re-register if they observe specific indices.
89
+
90
+ ### Array element object mutation
91
+
92
+ `state.arr[0].name = "X"` mutates an object inside the array. This
93
+ fires the set trap on the Proxy wrapping `arr[0]` with
94
+ `path = "arr.0.name"`. The event dispatched is `state.arr.0.name`.
95
+
96
+ Per ADR-003, this does **not** notify observers on `state.arr`. This
97
+ is a known limitation. Consumers who need to observe changes to
98
+ objects within arrays must observe the specific element path
99
+ (`state.arr.0.name`) or use `useObserver` auto-discovery.
100
+
101
+ ### Event deduplication
102
+
103
+ Because a single array operation (e.g. `splice`) can produce multiple
104
+ set/delete traps, the dispatch layer may fire `state.arr` multiple
105
+ times within the same synchronous block. Each dispatch schedules a
106
+ separate microtask. Observers on `state.arr` will fire **multiple
107
+ times** - once per trap. A future scheduler enhancement may coalesce
108
+ these. This ADR documents the current behavior.
109
+
110
+ ## Consequences
111
+
112
+ - **Positive:** Every array operation persists and notifies - no
113
+ silent failures.
114
+ - **Positive:** Array observers fire on the array path, not individual
115
+ indices - this matches React list-reconciliation mental model.
116
+ - **Positive:** `sort()`, `splice()`, and other complex operations are
117
+ explicitly tested for persistence + notification.
118
+ - **Negative:** Multiple traps from a single array operation may
119
+ produce multiple observer notifications - consumers must
120
+ de-duplicate if needed.
121
+ - **Negative:** Array element object mutations do not notify the
122
+ array-path observer - this is a fundamental limitation of the
123
+ exact-path observer model.
124
+
125
+ ## Compliance tests
126
+
127
+ - `tests/vitest/tests/contracts/adr-004-arrays.test.ts`
@@ -0,0 +1,148 @@
1
+ # ADR-005: Scheduler Contract
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ The Memorio 5.x plan (Section 10) requires an explicit, configurable
9
+ scheduler. The 4.7.3 baseline dispatches observer callbacks via
10
+ `queueMicrotask` (in `core/dispatch.ts`). There is no configuration
11
+ point, no batching, and no flush control. This must be formalized into
12
+ a contract that supports future UI-framework integration (React
13
+ batched mode, requestAnimationFrame, manual flush for tests).
14
+
15
+ The scheduler contract must define: ordering, batching, reentrancy,
16
+ error isolation, nested mutations, transaction interaction, and flush
17
+ behavior.
18
+
19
+ ## Assumptions
20
+
21
+ - The core engine dispatches events via `dispatch.set(name, value)`.
22
+ - `dispatch.listen(name, cb)` wraps `cb` in a microtask boundary.
23
+ - The scheduler is orthogonal to the Mutation Engine (ADR-007) and
24
+ Transactions (ADR-008) - it only affects *when* observer callbacks
25
+ fire, not *what* is recorded.
26
+ - `__DEV__` is a build-time constant.
27
+
28
+ ## Decision
29
+
30
+ ### Current default: microtask
31
+
32
+ The current dispatch layer uses `queueMicrotask` (or
33
+ `Promise.resolve().then(cb)` fallback). This means:
34
+
35
+ 1. **Ordering:** Observer callbacks fire in registration order (FIFO),
36
+ within the microtask queue, after the current synchronous block
37
+ completes.
38
+ 2. **Asynchronous:** Callbacks fire after the mutation is committed
39
+ to the raw Proxy target.
40
+ 3. **No batching:** Each `dispatch.set` schedules its own microtask.
41
+
42
+ ### Proposed scheduler configuration
43
+
44
+ A future `createMemorio({ scheduler: ... })` or
45
+ `memorio.configureScheduler({ mode, batching })` API would allow:
46
+
47
+ ```ts
48
+ type SchedulerMode = "sync" | "microtask" | "macrotask" | "raf" | "manual"
49
+ ```
50
+
51
+ | Mode | Behavior |
52
+ |------|----------|
53
+ | `sync` | Callbacks fire synchronously during `dispatch.set`. |
54
+ | `microtask` | Callbacks fire in the microtask queue (default). |
55
+ | `macrotask` | Callbacks fire via `setTimeout(cb, 0)` (macro task). |
56
+ | `raf` | Callbacks fire via `requestAnimationFrame`. |
57
+ | `manual` | Callbacks only fire when `flush()` is called. |
58
+
59
+ ### Batching semantics (proposed)
60
+
61
+ When `batching: true` (only meaningful with `microtask` or `macrotask`
62
+ mode):
63
+
64
+ 1. All `dispatch.set` calls within a single synchronous block are
65
+ collected.
66
+ 2. Each unique event name is invoked once, after the synchronous
67
+ block completes, in the scheduled task.
68
+ 3. The order of batched callbacks follows the order of first
69
+ `dispatch.set` for each unique event name.
70
+
71
+ When `batching: false` (default):
72
+
73
+ 1. Each `dispatch.set` schedules its own callback independently.
74
+
75
+ ### Reentrancy
76
+
77
+ - Reentrant `dispatch.set` calls (observer callbacks that dispatch new
78
+ events) are queued after the current callback completes.
79
+ - `sync` mode has no reentrancy boundary - recursive
80
+ `dispatch.set` calls fire immediately, risking stack overflow.
81
+ - No built-in guard against infinite dispatch loops. Consumers must
82
+ break the cycle.
83
+
84
+ ### Error isolation
85
+
86
+ - A throwing callback in `microtask`/`macrotask`/`raf` mode produces
87
+ an unhandled rejection / error. Other callbacks scheduled in the same
88
+ tick are unaffected (each runs in its own closure).
89
+ - In `sync` mode, a throwing callback propagates to the caller of
90
+ `dispatch.set`.
91
+ - A future enhancement may wrap each callback in `try/catch` with an
92
+ optional error reporter. This ADR does **not** implement that.
93
+
94
+ ### Nested mutations
95
+
96
+ Mutations inside observer callbacks create a new dispatch cycle. They
97
+ are **not** coalesced with the current cycle. This means:
98
+
99
+ ```js
100
+ state.counter = 1
101
+ // microtask fires:
102
+ observer('state.counter', () => {
103
+ state.counter = 2 // → schedules a NEW microtask
104
+ })
105
+ ```
106
+
107
+ ### Transaction interaction
108
+
109
+ - During a transaction, each mutation dispatches normally (no
110
+ suppression).
111
+ - `undo()` / `redo()` set `internal.historyEnabled = false` to prevent
112
+ re-recording, but the Proxy set trap still dispatches events to
113
+ observers.
114
+ - A future "transaction-level notification" may suppress individual
115
+ mutation dispatches during a transaction and fire a single
116
+ "transaction-end" event. This ADR does **not** define that.
117
+
118
+ ### Flush behavior
119
+
120
+ - In `manual` mode, `memorio.flush()` (or equivalent) processes all
121
+ queued callbacks. This is primarily for testing.
122
+ - In other modes, "flush" is implicit (microtask queue drains
123
+ naturally).
124
+
125
+ ## Current status
126
+
127
+ The scheduler is currently **not configurable**. The default
128
+ `microtask` behavior is in effect. This ADR is **Proposed** - the
129
+ configuration API is a Phase 2/3 enhancement. The compliance tests
130
+ for the default behavior lock in the current semantics so that future
131
+ scheduler changes do not break existing code.
132
+
133
+ ## Consequences
134
+
135
+ - **Positive:** Documenting the microtask default gives consumers a
136
+ deterministic mental model.
137
+ - **Positive:** The proposed API is framework-agnostic - all modes are
138
+ expressible as platform primitives.
139
+ - **Positive:** `manual` mode enables deterministic testing.
140
+ - **Negative:** No batching means multiple synchronous mutations each
141
+ produce a microtask - this is a known performance limitation for
142
+ bulk updates.
143
+ - **Negative:** No error isolation means a buggy observer can crash
144
+ the app via unhandled rejection.
145
+
146
+ ## Compliance tests
147
+
148
+ - `tests/vitest/tests/contracts/adr-005-scheduler.test.ts`
@@ -0,0 +1,91 @@
1
+ # ADR-006: Context Isolation Model
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ Memorio must support multiple isolated state trees within a single
9
+ process - for tests, micro-frontends, SSR, workers, and multi-tenant
10
+ environments. The 4.7.3 baseline uses `createContext` /
11
+ `memorio.isolate(name)` to create context-prefixed state, but the
12
+ mechanics and guarantees are undocumented.
13
+
14
+ This ADR defines how context isolation works, what the isolation
15
+ boundary is, and how it interacts with the Mutation Engine (ADR-007).
16
+
17
+ ## Assumptions
18
+
19
+ - The core `state`, `store`, `session`, `cache` singletons live on
20
+ `globalThis` and are shared across all import paths (ESM, CJS,
21
+ absolute-path imports).
22
+ - A "context" is a string ID that prefixes internal store keys.
23
+ - `memorio.setContext(id)` sets the active context ID at runtime.
24
+ - Server-side code must create per-request contexts from trusted
25
+ server-side data (never client-controlled input).
26
+
27
+ ## Decision
28
+
29
+ ### Global singleton vs. isolated context
30
+
31
+ - The global `state` / `store` / `session` / `cache` are **singletons**
32
+ on `globalThis`. Importing `memorio` in any module returns the same
33
+ instances. This is the default and is intended for single-instance
34
+ applications (e.g. one browser tab, one Next.js server instance).
35
+ - `memorio.isolate(name)` creates a new context and returns
36
+ context-scoped proxies. These proxies share the same underlying
37
+ engine but read/write to context-prefixed keys in `store`.
38
+
39
+ ### Context ID
40
+
41
+ - Context IDs are strings. They must be unique within a process.
42
+ - Context IDs are **organizational**, not a security boundary. They
43
+ do not prevent unauthorized access to another context's data within
44
+ the same process. Server-side authorization must be enforced at the
45
+ application layer.
46
+ - `internal.currentContext` is a module-level string on the singleton
47
+ `internal` object. Mutations record the active context ID.
48
+
49
+ ### Context-scoped state
50
+
51
+ When `internal.currentContext` is set to `ctxId`:
52
+
53
+ 1. `state.set(key, value)` writes to `state` but records
54
+ `context: ctxId` in the MutationRecord.
55
+ 2. `store.set(key, value)` writes to storage with key
56
+ `ctxId:key` (key-prefix isolation).
57
+ 3. `session.set(key, value)` writes to storage with key
58
+ `ctxId:key`.
59
+ 4. Reads from `store.get(key)` and `session.get(key)` read from
60
+ `ctxId:key`.
61
+
62
+ ### Default context
63
+
64
+ When `internal.currentContext` is `null` (the default), all state and
65
+ store operations use the un-prefixed keys. This is the "default" or
66
+ "global" context.
67
+
68
+ ### Multi-tenant caveat
69
+
70
+ `state` (the Proxy) is always global - it is a single reactive tree.
71
+ Context isolation applies to **persistence** (`store`, `session`) and
72
+ **mutation records** (`context` field). To achieve true state-tree
73
+ isolation, consumers should use `memorio.isolate(name)` which creates
74
+ a new context, or instantiate isolated Memorio instances.
75
+
76
+ ## Consequences
77
+
78
+ - **Positive:** Single global singleton simplifies the common case
79
+ (one app, one state tree).
80
+ - **Positive:** Context-prefixed persistence enables multi-tenant
81
+ server-side usage without separate state trees.
82
+ - **Positive:** Mutation records carry context, enabling causal-graph
83
+ partitioning (Phase 4).
84
+ - **Negative:** `state` itself is not multi-tenant - consumers needing
85
+ isolated state trees must use `isolate()` or separate processes.
86
+ - **Negative:** Context isolation is organizational, not a security
87
+ boundary - documented clearly in AGENTS.md.
88
+
89
+ ## Compliance tests
90
+
91
+ - `tests/vitest/tests/contracts/adr-006-context.test.ts`
@@ -0,0 +1,117 @@
1
+ # ADR-007: Mutation Records
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ Section 6 of the Memorio 5.x plan requires every state mutation to pass
9
+ through a normalized internal `MutationRecord`. The 4.7.3 baseline has
10
+ an informal `MutationRecord` interface in `core/internal.ts` with
11
+ `path`, `action`, `newValue`, `previousValue`, `timestamp`. The 5.0
12
+ codebase has extended this with `id`, `operation`, `hlc`, `context`,
13
+ `source`, and `transactionId` (see `core/mutation/types.ts`).
14
+
15
+ This ADR formalizes the canonical record structure and guarantees.
16
+
17
+ ## Assumptions
18
+
19
+ - Every `set`/`delete` trap on the state Proxy calls the
20
+ `buildProxy` callback once.
21
+ - The callback constructs a `Mutation` via `createMutation()` which:
22
+ - Generates an HLC-based ID (`m_<encoded>`).
23
+ - Captures `before` = previous value, `after` = new value.
24
+ - Tags the mutation with the active transaction ID (if any).
25
+ - Sets `context` from `internal.currentContext`.
26
+ - `recordMutation()` is the single write point to the trace log and
27
+ undo/redo stacks (in `core/internal.ts`).
28
+ - History tracking is opt-in via `enableHistory(true)`.
29
+
30
+ ## Decision
31
+
32
+ ### Canonical MutationRecord structure
33
+
34
+ ```ts
35
+ interface MutationRecord {
36
+ // --- Canonical Memorio 5 fields ---
37
+ id: string // HLC-based UUID: "m_<encoded-hlc>"
38
+ path: string // Dotted path relative to state: "user.profile.name"
39
+ operation: 'set' | 'delete' | 'insert' | 'remove' | 'replace' | 'length'
40
+ before: any // Value before mutation (deepRaw'd)
41
+ after: any // Value after mutation (deepRaw'd)
42
+ timestamp: number // Epoch ms
43
+ hlc: string // Serialized HLC: "hlc:<physical>:<logical>:<node>"
44
+
45
+ // --- Metadata ---
46
+ context?: string // Active context ID
47
+ source?: string // Optional attribution (e.g. "profile.save")
48
+ transactionId?: string // Active transaction ID, if any
49
+ parentId?: string // Parent mutation ID (for causal graph)
50
+
51
+ // --- Legacy fields (backward compatibility) ---
52
+ action: 'set' | 'delete' // Mirrors operation (set/delete only)
53
+ newValue: any // Alias for `after`
54
+ previousValue: any // Alias for `before`
55
+ }
56
+ ```
57
+
58
+ ### Guarantees
59
+
60
+ 1. **Every mutation gets a unique ID.** The ID is generated via
61
+ `generateMutationId()` which calls `hlc.tick()` - guaranteeing
62
+ causal ordering within a single node. The node suffix ensures
63
+ uniqueness across devices.
64
+
65
+ 2. **Before/after are deepRaw'd.** Proxy wrappers are never stored in
66
+ the record - only plain values. This ensures serializable,
67
+ comparison-safe records.
68
+
69
+ 3. **Single write point.** `internal.recordMutation()` is the only
70
+ function that pushes to `mutations`, `undoStack`, or `redoStack`.
71
+ No other code path mutates these arrays directly.
72
+
73
+ 4. **Redo stack clears on new mutation.** Any mutation (including
74
+ undo/redo inverse writes, since those temporarily disable history)
75
+ must NOT clear the redo stack - undo/redo use
76
+ `internal.historyEnabled = false` to bypass `recordMutation`.
77
+
78
+ 5. **Max history depth.** Both stacks are trimmed to
79
+ `internal.maxHistory` (default: 100). Trimming removes from the
80
+ front (shift), preserving recency.
81
+
82
+ 6. **No-op mutations.** If `before === after`, the record is still
83
+ created and stored (the mutation happened, even if the value
84
+ didn't change). `mutationToPatch()` returns `null` for no-ops, so
85
+ no patch is stored in a commit's patch list.
86
+
87
+ ### Reentrancy guard
88
+
89
+ The `mutate()` function sets `internal._recording = false` before
90
+ writing to the state Proxy, then calls `recordMutation()` itself. This
91
+ prevents the Proxy callback from double-recording the same mutation.
92
+
93
+ ### Transaction tagging
94
+
95
+ During an active transaction, `createMutation()` calls
96
+ `tagForTransaction()` to attach the current transaction ID.
97
+ `registerMutationInTransaction(id)` adds the mutation ID to the
98
+ transaction's mutation list.
99
+
100
+ ## Consequences
101
+
102
+ - **Positive:** A single, well-typed record structure serves undo,
103
+ redo, trace, diff, and future replay/revert.
104
+ - **Positive:** HLC-based IDs provide causal ordering for the causal
105
+ graph and simulation engine.
106
+ - **Positive:** Legacy fields maintain full backward compatibility
107
+ with 4.x consumers.
108
+ - **Negative:** Storing both legacy and canonical fields doubles the
109
+ record size - acceptable for the opt-in history feature.
110
+ - **Negative:** No-op mutations are stored - a future optimization
111
+ could skip them in the set trap, but the Mutation Engine must remain
112
+ the source of truth.
113
+
114
+ ## Compliance tests
115
+
116
+ - `tests/vitest/tests/contracts/adr-007-mutation-records.test.ts`
117
+ - `tests/vitest/tests/mutation-engine.test.ts` (existing)