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,149 @@
1
+ # ADR-005: Scheduler Contract
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ The Memorio 5.x plan (Section 10) requires an explicit, configurable
10
+ scheduler. The 4.7.3 baseline dispatches observer callbacks via
11
+ `queueMicrotask` (in `core/dispatch.ts`). There is no configuration
12
+ point, no batching, and no flush control. This must be formalized into
13
+ a contract that supports future UI-framework integration (React
14
+ batched mode, requestAnimationFrame, manual flush for tests).
15
+
16
+ The scheduler contract must define: ordering, batching, reentrancy,
17
+ error isolation, nested mutations, transaction interaction, and flush
18
+ behavior.
19
+
20
+ ## Assumptions
21
+
22
+ - The core engine dispatches events via `dispatch.set(name, value)`.
23
+ - `dispatch.listen(name, cb)` wraps `cb` in a microtask boundary.
24
+ - The scheduler is orthogonal to the Mutation Engine (ADR-007) and
25
+ Transactions (ADR-008) — it only affects *when* observer callbacks
26
+ fire, not *what* is recorded.
27
+ - `__DEV__` is a build-time constant.
28
+
29
+ ## Decision
30
+
31
+ ### Current default: microtask
32
+
33
+ The current dispatch layer uses `queueMicrotask` (or
34
+ `Promise.resolve().then(cb)` fallback). This means:
35
+
36
+ 1. **Ordering:** Observer callbacks fire in registration order (FIFO),
37
+ within the microtask queue, after the current synchronous block
38
+ completes.
39
+ 2. **Asynchronous:** Callbacks fire after the mutation is committed
40
+ to the raw Proxy target.
41
+ 3. **No batching:** Each `dispatch.set` schedules its own microtask.
42
+
43
+ ### Proposed scheduler configuration
44
+
45
+ A future `createMemorio({ scheduler: ... })` or
46
+ `memorio.configureScheduler({ mode, batching })` API would allow:
47
+
48
+ ```ts
49
+ type SchedulerMode = "sync" | "microtask" | "macrotask" | "raf" | "manual"
50
+ ```
51
+
52
+ | Mode | Behavior |
53
+ |------|----------|
54
+ | `sync` | Callbacks fire synchronously during `dispatch.set`. |
55
+ | `microtask` | Callbacks fire in the microtask queue (default). |
56
+ | `macrotask` | Callbacks fire via `setTimeout(cb, 0)` (macro task). |
57
+ | `raf` | Callbacks fire via `requestAnimationFrame`. |
58
+ | `manual` | Callbacks only fire when `flush()` is called. |
59
+
60
+ ### Batching semantics (proposed)
61
+
62
+ When `batching: true` (only meaningful with `microtask` or `macrotask`
63
+ mode):
64
+
65
+ 1. All `dispatch.set` calls within a single synchronous block are
66
+ collected.
67
+ 2. Each unique event name is invoked once, after the synchronous
68
+ block completes, in the scheduled task.
69
+ 3. The order of batched callbacks follows the order of first
70
+ `dispatch.set` for each unique event name.
71
+
72
+ When `batching: false` (default):
73
+
74
+ 1. Each `dispatch.set` schedules its own callback independently.
75
+
76
+ ### Reentrancy
77
+
78
+ - Reentrant `dispatch.set` calls (observer callbacks that dispatch new
79
+ events) are queued after the current callback completes.
80
+ - `sync` mode has no reentrancy boundary — recursive
81
+ `dispatch.set` calls fire immediately, risking stack overflow.
82
+ - No built-in guard against infinite dispatch loops. Consumers must
83
+ break the cycle.
84
+
85
+ ### Error isolation
86
+
87
+ - A throwing callback in `microtask`/`macrotask`/`raf` mode produces
88
+ an unhandled rejection / error. Other callbacks scheduled in the same
89
+ tick are unaffected (each runs in its own closure).
90
+ - In `sync` mode, a throwing callback propagates to the caller of
91
+ `dispatch.set`.
92
+ - A future enhancement may wrap each callback in `try/catch` with an
93
+ optional error reporter. This ADR does **not** implement that.
94
+
95
+ ### Nested mutations
96
+
97
+ Mutations inside observer callbacks create a new dispatch cycle. They
98
+ are **not** coalesced with the current cycle. This means:
99
+
100
+ ```js
101
+ state.counter = 1
102
+ // microtask fires:
103
+ observer('state.counter', () => {
104
+ state.counter = 2 // → schedules a NEW microtask
105
+ })
106
+ ```
107
+
108
+ ### Transaction interaction
109
+
110
+ - During a transaction, each mutation dispatches normally (no
111
+ suppression).
112
+ - `undo()` / `redo()` set `internal.historyEnabled = false` to prevent
113
+ re-recording, but the Proxy set trap still dispatches events to
114
+ observers.
115
+ - A future "transaction-level notification" may suppress individual
116
+ mutation dispatches during a transaction and fire a single
117
+ "transaction-end" event. This ADR does **not** define that.
118
+
119
+ ### Flush behavior
120
+
121
+ - In `manual` mode, `memorio.flush()` (or equivalent) processes all
122
+ queued callbacks. This is primarily for testing.
123
+ - In other modes, "flush" is implicit (microtask queue drains
124
+ naturally).
125
+
126
+ ## Current status
127
+
128
+ The scheduler is currently **not configurable**. The default
129
+ `microtask` behavior is in effect. This ADR is **Proposed** — the
130
+ configuration API is a Phase 2/3 enhancement. The compliance tests
131
+ for the default behavior lock in the current semantics so that future
132
+ scheduler changes do not break existing code.
133
+
134
+ ## Consequences
135
+
136
+ - **Positive:** Documenting the microtask default gives consumers a
137
+ deterministic mental model.
138
+ - **Positive:** The proposed API is framework-agnostic — all modes are
139
+ expressible as platform primitives.
140
+ - **Positive:** `manual` mode enables deterministic testing.
141
+ - **Negative:** No batching means multiple synchronous mutations each
142
+ produce a microtask — this is a known performance limitation for
143
+ bulk updates.
144
+ - **Negative:** No error isolation means a buggy observer can crash
145
+ the app via unhandled rejection.
146
+
147
+ ## Compliance tests
148
+
149
+ - `tests/vitest/tests/contracts/adr-005-scheduler.test.ts`
@@ -0,0 +1,92 @@
1
+ # ADR-006: Context Isolation Model
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Memorio must support multiple isolated state trees within a single
10
+ process — for tests, micro-frontends, SSR, workers, and multi-tenant
11
+ environments. The 4.7.3 baseline uses `createContext` /
12
+ `memorio.isolate(name)` to create context-prefixed state, but the
13
+ mechanics and guarantees are undocumented.
14
+
15
+ This ADR defines how context isolation works, what the isolation
16
+ boundary is, and how it interacts with the Mutation Engine (ADR-007).
17
+
18
+ ## Assumptions
19
+
20
+ - The core `state`, `store`, `session`, `cache` singletons live on
21
+ `globalThis` and are shared across all import paths (ESM, CJS,
22
+ absolute-path imports).
23
+ - A "context" is a string ID that prefixes internal store keys.
24
+ - `memorio.setContext(id)` sets the active context ID at runtime.
25
+ - Server-side code must create per-request contexts from trusted
26
+ server-side data (never client-controlled input).
27
+
28
+ ## Decision
29
+
30
+ ### Global singleton vs. isolated context
31
+
32
+ - The global `state` / `store` / `session` / `cache` are **singletons**
33
+ on `globalThis`. Importing `memorio` in any module returns the same
34
+ instances. This is the default and is intended for single-instance
35
+ applications (e.g. one browser tab, one Next.js server instance).
36
+ - `memorio.isolate(name)` creates a new context and returns
37
+ context-scoped proxies. These proxies share the same underlying
38
+ engine but read/write to context-prefixed keys in `store`.
39
+
40
+ ### Context ID
41
+
42
+ - Context IDs are strings. They must be unique within a process.
43
+ - Context IDs are **organizational**, not a security boundary. They
44
+ do not prevent unauthorized access to another context's data within
45
+ the same process. Server-side authorization must be enforced at the
46
+ application layer.
47
+ - `internal.currentContext` is a module-level string on the singleton
48
+ `internal` object. Mutations record the active context ID.
49
+
50
+ ### Context-scoped state
51
+
52
+ When `internal.currentContext` is set to `ctxId`:
53
+
54
+ 1. `state.set(key, value)` writes to `state` but records
55
+ `context: ctxId` in the MutationRecord.
56
+ 2. `store.set(key, value)` writes to storage with key
57
+ `ctxId:key` (key-prefix isolation).
58
+ 3. `session.set(key, value)` writes to storage with key
59
+ `ctxId:key`.
60
+ 4. Reads from `store.get(key)` and `session.get(key)` read from
61
+ `ctxId:key`.
62
+
63
+ ### Default context
64
+
65
+ When `internal.currentContext` is `null` (the default), all state and
66
+ store operations use the un-prefixed keys. This is the "default" or
67
+ "global" context.
68
+
69
+ ### Multi-tenant caveat
70
+
71
+ `state` (the Proxy) is always global — it is a single reactive tree.
72
+ Context isolation applies to **persistence** (`store`, `session`) and
73
+ **mutation records** (`context` field). To achieve true state-tree
74
+ isolation, consumers should use `memorio.isolate(name)` which creates
75
+ a new context, or instantiate isolated Memorio instances.
76
+
77
+ ## Consequences
78
+
79
+ - **Positive:** Single global singleton simplifies the common case
80
+ (one app, one state tree).
81
+ - **Positive:** Context-prefixed persistence enables multi-tenant
82
+ server-side usage without separate state trees.
83
+ - **Positive:** Mutation records carry context, enabling causal-graph
84
+ partitioning (Phase 4).
85
+ - **Negative:** `state` itself is not multi-tenant — consumers needing
86
+ isolated state trees must use `isolate()` or separate processes.
87
+ - **Negative:** Context isolation is organizational, not a security
88
+ boundary — documented clearly in AGENTS.md.
89
+
90
+ ## Compliance tests
91
+
92
+ - `tests/vitest/tests/contracts/adr-006-context.test.ts`
@@ -0,0 +1,118 @@
1
+ # ADR-007: Mutation Records
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Section 6 of the Memorio 5.x plan requires every state mutation to pass
10
+ through a normalized internal `MutationRecord`. The 4.7.3 baseline has
11
+ an informal `MutationRecord` interface in `core/internal.ts` with
12
+ `path`, `action`, `newValue`, `previousValue`, `timestamp`. The 5.0
13
+ codebase has extended this with `id`, `operation`, `hlc`, `context`,
14
+ `source`, and `transactionId` (see `core/mutation/types.ts`).
15
+
16
+ This ADR formalizes the canonical record structure and guarantees.
17
+
18
+ ## Assumptions
19
+
20
+ - Every `set`/`delete` trap on the state Proxy calls the
21
+ `buildProxy` callback once.
22
+ - The callback constructs a `Mutation` via `createMutation()` which:
23
+ - Generates an HLC-based ID (`m_<encoded>`).
24
+ - Captures `before` = previous value, `after` = new value.
25
+ - Tags the mutation with the active transaction ID (if any).
26
+ - Sets `context` from `internal.currentContext`.
27
+ - `recordMutation()` is the single write point to the trace log and
28
+ undo/redo stacks (in `core/internal.ts`).
29
+ - History tracking is opt-in via `enableHistory(true)`.
30
+
31
+ ## Decision
32
+
33
+ ### Canonical MutationRecord structure
34
+
35
+ ```ts
36
+ interface MutationRecord {
37
+ // --- Canonical Memorio 5 fields ---
38
+ id: string // HLC-based UUID: "m_<encoded-hlc>"
39
+ path: string // Dotted path relative to state: "user.profile.name"
40
+ operation: 'set' | 'delete' | 'insert' | 'remove' | 'replace' | 'length'
41
+ before: any // Value before mutation (deepRaw'd)
42
+ after: any // Value after mutation (deepRaw'd)
43
+ timestamp: number // Epoch ms
44
+ hlc: string // Serialized HLC: "hlc:<physical>:<logical>:<node>"
45
+
46
+ // --- Metadata ---
47
+ context?: string // Active context ID
48
+ source?: string // Optional attribution (e.g. "profile.save")
49
+ transactionId?: string // Active transaction ID, if any
50
+ parentId?: string // Parent mutation ID (for causal graph)
51
+
52
+ // --- Legacy fields (backward compatibility) ---
53
+ action: 'set' | 'delete' // Mirrors operation (set/delete only)
54
+ newValue: any // Alias for `after`
55
+ previousValue: any // Alias for `before`
56
+ }
57
+ ```
58
+
59
+ ### Guarantees
60
+
61
+ 1. **Every mutation gets a unique ID.** The ID is generated via
62
+ `generateMutationId()` which calls `hlc.tick()` — guaranteeing
63
+ causal ordering within a single node. The node suffix ensures
64
+ uniqueness across devices.
65
+
66
+ 2. **Before/after are deepRaw'd.** Proxy wrappers are never stored in
67
+ the record — only plain values. This ensures serializable,
68
+ comparison-safe records.
69
+
70
+ 3. **Single write point.** `internal.recordMutation()` is the only
71
+ function that pushes to `mutations`, `undoStack`, or `redoStack`.
72
+ No other code path mutates these arrays directly.
73
+
74
+ 4. **Redo stack clears on new mutation.** Any mutation (including
75
+ undo/redo inverse writes, since those temporarily disable history)
76
+ must NOT clear the redo stack — undo/redo use
77
+ `internal.historyEnabled = false` to bypass `recordMutation`.
78
+
79
+ 5. **Max history depth.** Both stacks are trimmed to
80
+ `internal.maxHistory` (default: 100). Trimming removes from the
81
+ front (shift), preserving recency.
82
+
83
+ 6. **No-op mutations.** If `before === after`, the record is still
84
+ created and stored (the mutation happened, even if the value
85
+ didn't change). `mutationToPatch()` returns `null` for no-ops, so
86
+ no patch is stored in a commit's patch list.
87
+
88
+ ### Reentrancy guard
89
+
90
+ The `mutate()` function sets `internal._recording = false` before
91
+ writing to the state Proxy, then calls `recordMutation()` itself. This
92
+ prevents the Proxy callback from double-recording the same mutation.
93
+
94
+ ### Transaction tagging
95
+
96
+ During an active transaction, `createMutation()` calls
97
+ `tagForTransaction()` to attach the current transaction ID.
98
+ `registerMutationInTransaction(id)` adds the mutation ID to the
99
+ transaction's mutation list.
100
+
101
+ ## Consequences
102
+
103
+ - **Positive:** A single, well-typed record structure serves undo,
104
+ redo, trace, diff, and future replay/revert.
105
+ - **Positive:** HLC-based IDs provide causal ordering for the causal
106
+ graph and simulation engine.
107
+ - **Positive:** Legacy fields maintain full backward compatibility
108
+ with 4.x consumers.
109
+ - **Negative:** Storing both legacy and canonical fields doubles the
110
+ record size — acceptable for the opt-in history feature.
111
+ - **Negative:** No-op mutations are stored — a future optimization
112
+ could skip them in the set trap, but the Mutation Engine must remain
113
+ the source of truth.
114
+
115
+ ## Compliance tests
116
+
117
+ - `tests/vitest/tests/contracts/adr-007-mutation-records.test.ts`
118
+ - `tests/vitest/tests/mutation-engine.test.ts` (existing)
@@ -0,0 +1,106 @@
1
+ # ADR-008: Transactions
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Section 11 of the Memorio 5.x plan requires transactions to group
10
+ multiple mutations into one logical operation. The 5.0 codebase
11
+ implements `beginTransaction`, `commitTransaction`,
12
+ `abortTransaction`, `listTransactions`, `currentTransaction` in
13
+ `core/mutation/transaction.ts`. This ADR formalizes the semantics.
14
+
15
+ ## Assumptions
16
+
17
+ - Mutations inside a transaction are tagged with the transaction ID
18
+ via `createMutation()` → `tagForTransaction()`.
19
+ - The transaction registry lives on `globalThis`
20
+ (`__memorio_transaction_registry__`) to survive module duplication.
21
+ - `internal.currentTransactionId` tracks the active transaction.
22
+ - `memorio.transaction(fn, opts)` is a convenience alias for
23
+ `beginTransaction`.
24
+
25
+ ## Decision
26
+
27
+ ### Transaction lifecycle
28
+
29
+ ```
30
+ beginTransaction(source, message, metadata)
31
+ → creates TransactionContext { id, startedAt, active: true }
32
+ → sets internal.currentTransactionId = id
33
+ → mutations tagged with id
34
+
35
+ commitTransaction()
36
+ → marks context active: false, sets committedAt
37
+ → clears internal.currentTransactionId
38
+ → returns { id, mutations[], startedAt, committedAt, source, message, metadata }
39
+
40
+ abortTransaction()
41
+ → rolls back all tagged mutations (applies inverse)
42
+ → removes mutation IDs from undoStack (no redo push)
43
+ → deletes from registry, clears currentTransactionId
44
+ → returns the Transaction record
45
+ ```
46
+
47
+ ### Transaction ID generation
48
+
49
+ - Generated via `generateTransactionId()` → `tx_<encoded-hlc>`.
50
+ - Monotonically ordered within a node via HLC.
51
+
52
+ ### Nested transactions
53
+
54
+ `beginTransaction` called while a transaction is already active
55
+ creates a **new** transaction context (with a new ID). The previous
56
+ transaction remains active in the registry — it is **not** suspended
57
+ or paused. `commitTransaction()` only commits the **most recently
58
+ started** active transaction.
59
+
60
+ This is a **flat namespace** model: transactions are independent, not
61
+ hierarchical. A mutation made after the inner `beginTransaction` is
62
+ tagged with the inner transaction's ID, not the outer.
63
+
64
+ > **Future:** A hierarchical model (with parentId) may be introduced.
65
+ > This ADR documents the current flat behavior.
66
+
67
+ ### Rollback on abort
68
+
69
+ `abortTransaction` finds each tagged mutation in `undoStack` by its
70
+ record ID, removes it from the stack (without pushing to redo), and
71
+ applies the inverse (`_applyInverse`). The inverse writes go through
72
+ the Proxy with `historyEnabled = false` and `_recording = false`, so
73
+ they are not recorded.
74
+
75
+ ### Transaction record immutability
76
+
77
+ Once committed or aborted, a transaction's `mutations` list is frozen
78
+ (immutable). The registry retains the context for
79
+ `listTransactions()` inspection, but `active` is `false`.
80
+
81
+ ### Mutate inside a transaction
82
+
83
+ `memorio.mutate(path, value, { source })` inside a transaction:
84
+ 1. Calls `createMutation(...)` which auto-tags with the active
85
+ transaction ID via `tagForTransaction()`.
86
+ 2. Writes to state with `_recording = false` (no double recording).
87
+ 3. Calls `recordMutation(mutation)` — the mutation is recorded in the
88
+ trace log AND tagged in the transaction.
89
+
90
+ ## Consequences
91
+
92
+ - **Positive:** Mutations are grouped and attributable to a logical
93
+ operation — essential for explain() and impact() in Phase 4.
94
+ - **Positive:** `abortTransaction` provides atomic rollback — all
95
+ mutations are undone.
96
+ - **Positive:** Registry on `globalThis` survives module duplication.
97
+ - **Negative:** Nested transactions are flat (not hierarchical) —
98
+ the outer transaction is not aborted if the inner is aborted.
99
+ - **Negative:** Rollback via inverse application requires all
100
+ `before` values to be present in the MutationRecord — for
101
+ very large objects this is memory-intensive.
102
+
103
+ ## Compliance tests
104
+
105
+ - `tests/vitest/tests/contracts/adr-008-transactions.test.ts`
106
+ - `tests/vitest/tests/mutation-engine.test.ts` (existing, §06-07)
@@ -0,0 +1,110 @@
1
+ # ADR-009: History Model (Snapshot / Delta)
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Section 12 and Section 13 of the Memorio 5.x plan require an immutable,
10
+ Git-inspired history model with commits, snapshots, deltas, and undo/redo
11
+ as distinct concepts. The 4.7.3 baseline implements undo/redo on a
12
+ linear mutation stack (in `core/internal.ts` and
13
+ `functions/history/index.ts`). This ADR defines the upgrade path to
14
+ the full Commit → Snapshot → Delta model.
15
+
16
+ ## Assumptions
17
+
18
+ - The current undo/redo stack stores flat `MutationRecord` entries.
19
+ - `enableHistory(true)` activates recording.
20
+ - `memorio.snapshot()` captures a deep clone of state.
21
+ - `memorio.trace()` returns the flat mutation log.
22
+
23
+ ## Decision
24
+
25
+ ### Current state (4.7.3 baseline)
26
+
27
+ - Mutations are stored as `MutationRecord[]` in `undoStack` and
28
+ `redoStack` on the singleton `internal` object.
29
+ - `undo()` pops from `undoStack`, pushes to `redoStack`, applies the
30
+ inverse to state.
31
+ - `redo()` pops from `redoStack`, pushes to `undoStack`, re-applies.
32
+ - `trace()` returns the full `mutations` log (trimmed to
33
+ `maxHistory`).
34
+ - No commit concept exists — every mutation is an individual entry.
35
+
36
+ ### Target state (Memorio 5.0)
37
+
38
+ ```
39
+ Commit (immutable, append-only)
40
+ ├── parent: CommitId | null
41
+ ├── patches: Patch[] (RFC 6902: add | remove | replace)
42
+ ├── metadata: { source?, message?, actor?, timestamp, hlc }
43
+ └── snapshot?: SnapshotRef (materialized every N commits)
44
+ ```
45
+
46
+ - **Commits** are the unit of history. `memorio.commit(message, fn)`
47
+ groups all mutations in `fn` into one commit.
48
+ - **Snapshots** are deep clones of state, materialized every
49
+ `snapshotInterval` commits (default: 100) to bound replay cost.
50
+ - **Deltas** are the `Patch[]` that transform a snapshot to the next
51
+ state. Only patches are stored between snapshots — never full
52
+ state copies.
53
+ - **Undo/Redo** operate at the commit level, not the individual
54
+ mutation level. Undoing a commit applies the inverse of every patch
55
+ in the commit.
56
+
57
+ ### Migration path
58
+
59
+ 1. **Phase 1 (this sprint):** Keep the existing flat mutation stack.
60
+ Document that `undo()`/`redo()` operate per-mutation, not per-commit.
61
+ Add `memorio.commit()` as a grouping mechanism without changing the
62
+ underlying stack.
63
+ 2. **Phase 3:** Replace the flat stack with the Commit → Snapshot →
64
+ Delta model. `undo()`/`redo()` will operate per-commit.
65
+
66
+ ### Undo vs. Revert (explicit distinction)
67
+
68
+ - **Undo** (`memorio.undo()`): Moves the current position backward in
69
+ the undo stack. The mutation is preserved on the redo stack.
70
+ - **Revert** (`memorio.history.revert(commitId)`): Creates a **new**
71
+ mutation that reverses a historical commit. The original commit
72
+ remains in history (immutable). This is a future Phase 3 feature.
73
+
74
+ ### maxCommits and maxHistory
75
+
76
+ - `maxCommits` (default: 10,000): Maximum number of commit entries
77
+ retained. Older commits are trimmed (FIFO).
78
+ - `maxHistory` (default: 100): Maximum mutations per undo/redo stack
79
+ (legacy, applies to the current flat model).
80
+
81
+ ### Replay safety
82
+
83
+ - Replay must be deterministic. Patches are pure (no closures, no
84
+ functions). `state.path = value` is the inverse of `delete
85
+ state.path` in a patch-based replay.
86
+ - External dependencies (Date.now, Math.random, crypto) must be
87
+ injected via a replay context to maintain determinism.
88
+
89
+ ## Consequences
90
+
91
+ - **Positive:** Commits group mutations logically — essential for
92
+ explain(), business history, and audit trails.
93
+ - **Positive:** Snapshot/delta model bounds memory and replay cost.
94
+ - **Positive:** Undo and Revert are distinct operations — no
95
+ conflation.
96
+ - **Negative:** The current flat stack must be replaced in Phase 3,
97
+ which is a breaking change to `undo()`/`redo()` semantics
98
+ (per-mutation → per-commit).
99
+ - **Negative:** Snapshot materialization at intervals is a trade-off
100
+ between memory and CPU — defaults must be benchmark-driven.
101
+
102
+ ## Compliance tests
103
+
104
+ - `tests/vitest/tests/contracts/adr-009-history.test.ts`
105
+ - `tests/vitest/tests/mutation-engine.test.ts` (existing, §04-07)
106
+
107
+ ## Benchmarks
108
+
109
+ - `experimental/benchmarks/replay.bench.ts` — measures replay cost
110
+ with and without interval snapshots.
package/adr/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Architecture Decision Records (ADRs)
2
+
3
+ This directory contains Architecture Decision Records (ADRs) for the
4
+ **Memorio 5.x** "Application State Intelligence Runtime" initiative.
5
+
6
+ ## Format
7
+
8
+ ADRs follow the [MADR](https://adr.github.io/) (Markdown Architectural
9
+ Decision Records) format. Each decision record answers four questions:
10
+
11
+ 1. **Context** — What is the issue that we were dealing with when we
12
+ started thinking about it?
13
+ 2. **Decision** — What is the change that we will make (that resolves
14
+ the issue)?
15
+ 3. **Consequences** — What becomes easier or more difficult to do
16
+ because of this change?
17
+
18
+ ## Numbering
19
+
20
+ | ADR | Title | Status |
21
+ |-----|-------|--------|
22
+ | [001](001-state-proxy-model.md) | State Proxy Model | Accepted |
23
+ | [002](002-observer-semantics.md) | Observer Semantics | Proposed |
24
+ | [003](003-deep-mutation-semantics.md) | Deep Mutation Semantics | Proposed |
25
+ | [004](004-array-mutation-semantics.md) | Array Mutation Semantics | Proposed |
26
+ | [005](005-scheduler-contract.md) | Scheduler Contract | Proposed |
27
+ | [006](006-context-isolation.md) | Context Isolation Model | Accepted |
28
+ | [007](007-mutation-records.md) | Mutation Records | Accepted |
29
+ | [008](008-transactions.md) | Transactions | Accepted |
30
+ | [009](009-history-model.md) | History Model (Snapshot/Delta) | Proposed |
31
+
32
+ ## Lifecycle
33
+
34
+ New ADRs start as **Proposed**, move to **Accepted** when the decision
35
+ is implemented and tests pass, and become **Superseded** when a later
36
+ ADR supersedes them. A **Deprecated** status marks decisions that are no
37
+ longer relevant but kept for historical context.
38
+
39
+ ## Compliance
40
+
41
+ Every ADR that defines a behavioral contract must have a corresponding
42
+ compliance test suite under `tests/vitest/tests/contracts/`. The test
43
+ file name mirrors the ADR number (e.g. `adr-004-arrays.test.ts`).
44
+
45
+ A release must not be published if any **Accepted** ADR has failing
46
+ compliance tests.
@@ -0,0 +1,49 @@
1
+ # ADR Template — MADR format
2
+
3
+ > **Status:** `{Proposed | Accepted | Superseded by [ADR-XXX](adr-XXX.md) | Deprecated}`
4
+ > **Date:** YYYY-MM-DD
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ > What is the issue that we were dealing with when we started thinking
10
+ > about it? What are the forces at play? Consider the unique constraints
11
+ > of this project — the need for deterministic, explainable state that
12
+ > remembers, understands, replays, and simulates application state.
13
+
14
+ ## Assumptions
15
+
16
+ > List any assumptions the decision relies on (e.g. “state mutations are
17
+ > synchronous,” “the Proxy target is always a plain object or array”).
18
+
19
+ ## Decision
20
+
21
+ > What is the change that we will make? Describe the chosen approach in
22
+ > detail. This section must be precise enough to serve as an implementation
23
+ > contract. Include:
24
+ >
25
+ > - The precise semantics (what happens, what does not happen)
26
+ > - Edge cases and error behavior
27
+ > - Performance characteristics
28
+ > - Backward-compatibility implications
29
+
30
+ ## Consequences
31
+
32
+ > What becomes easier or more difficult to do because of this change?
33
+ > Consider:
34
+ >
35
+ > - **Positive** — benefits gained (e.g. improved developer experience,
36
+ > better performance, deterministic behavior)
37
+ > - **Negative** — trade-offs and drawbacks (e.g. loss of flexibility,
38
+ > migration complexity, runtime overhead)
39
+ > - **Testability** — how this decision is covered by compliance tests
40
+
41
+ ## Compliance tests
42
+
43
+ > Reference the test file(s) that verify this contract. Every Accepted
44
+ > ADR must list at least one compliance test file path relative to the
45
+ > repository root.
46
+
47
+ ```
48
+
49
+ File: _ADR_TITLE_PLACEHOLDER.md