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
package/SECURITY.md CHANGED
@@ -34,7 +34,23 @@ Addresses OWASP Top 10 (2021):
34
34
  - A09:2021 - Security Logging and Monitoring Failures (DevTools inspect, Logger module)
35
35
  - A10:2021 - Server-Side Request Forgery (N/A - no network requests)
36
36
 
37
- ## Reporting Security Issues
37
+ ## Global API Security
38
+
39
+ Memorio does **not** expose its APIs on `globalThis` by default. The opt-in
40
+ entrypoint `import 'memorio/global'` installs `state`, `store`, `session`,
41
+ `cache`, `idb`, `sqlite`, `observer`, and `useObserver` on `globalThis`.
42
+ This is an explicit consumer choice - not inferred from `NODE_ENV` or
43
+ bundler environment variables.
44
+
45
+ For application code, prefer explicit named imports:
46
+
47
+ ```js
48
+ import { state } from 'memorio'
49
+ ```
50
+
51
+ See [SECURITY-HARDENING.md](./SECURITY-HARDENING.md) for a full hardening
52
+ guide covering storage encryption, context isolation, logger data capture,
53
+ and platform capability assumptions.
38
54
 
39
55
  If you find a security vulnerability:
40
56
 
package/SUMMARY.md CHANGED
@@ -1,45 +1,59 @@
1
- # Table of contents
2
-
3
- * [README](README.md)
4
-
5
- ## Core Modules
6
-
7
- * [State](markdown/STATE.md) - Reactive global state management
8
- * [Observer](markdown/OBSERVER.md) - Object observation pattern
9
- * [useObserver](markdown/USEOBSERVER.md) - React hook for state observation
10
- * [Dispatch](markdown/DISPATCH.md) - Event system for vanilla JS applications
11
- * [Cache](markdown/CACHE.md) - In-memory caching (lost on refresh)
12
- * [Store](markdown/STORE.md) - Persistent localStorage management
13
- * [Session](markdown/SESSION.md) - Temporary sessionStorage management
14
- * [IDB](markdown/IDB.md) - IndexedDB for large data storage
15
- * [SQLite](markdown/SQLITE.md) - SQLite in the browser via sql.js (WASM)
16
-
17
- ## Typed & Validated
18
-
19
- * [Typed Stores](markdown/TYPED.md) - Compile-time type safety for state access
20
- * [Schema Validation](markdown/SCHEMA.md) - Runtime validation of state mutations
21
-
22
- ## History & Introspection
23
-
24
- * [History](markdown/HISTORY.md) - Snapshot, diff, undo/redo, trace
25
- * [Introspection](markdown/INSPECT.md) - stateKeys, pathExists, stateType, stateSchema
26
-
27
- ## Memory System
28
-
29
- * [Memory](markdown/MEMORY.md) - Semantic memory layer with TTL, confidence, scopes
30
- * [Node Attachment](markdown/MEMORY-ATTACHMENT.md) - Dynamic node attachment system
31
- * [Synchronization](markdown/SYNC.md) - Local-first sync, journal, conflict resolution, multi-device evolution
32
-
33
- ## Platform & Compatibility
34
-
35
- * [Platform & Context Isolation](markdown/PLATFORM.md) - Cross-platform support, session isolation
36
- * [Security](markdown/SECURITY.md) - Security measures, vulnerability prevention
37
- * [Changelog](markdown/CHANGELOG.md) - Version history and migration guide
38
- * [Classic Import](markdown/IMPORT.md) - Named export guide for `import { state } from 'memorio'`
39
-
40
- ## Additional Resources
41
-
42
- * [License](../LICENSE.md)
43
- * [Contributing](../CONTRIBUTING.md)
44
- * [Code of Conduct](../CODE_OF_CONDUCT.md)
45
- * [Security](../SECURITY.md)
1
+ # Riepilogo dei file - memorio examples & markdown
2
+
3
+ ## 📁 examples/ (20 file)
4
+
5
+ | File | Cosa mostra |
6
+ |---|---|
7
+ | `basic.ts` | Tour introduttivo: platform detection, `state`, `store`, `session` in un unico script. |
8
+ | `cache.ts` | Uso base della cache in-memory: set/get, oggetti complessi, cleanup. |
9
+ | `cross-platform-guards.ts` | Come usare `getCapabilities()` invece di `isBrowser()`/`isNode()` per gestire in modo esplicito gli ambienti dove `store`/`session`/`idb` non sono durevoli. |
10
+ | `history.ts` *(nuovo)* | State Intelligence: `snapshot()`/`diff()`/`rollback()` per il pattern "prova → ispeziona → conferma o annulla", undo/redo passo-passo, trace log con export/replay. |
11
+ | `idb.ts` | CRUD completo su IndexedDB: database, tabelle, record, info (size/version/exist). |
12
+ | `multi-tenant-context.ts` | Isolamento dati per richiesta/tenant con `memorio.createContext()` in un handler server-side, incluso cleanup. |
13
+ | `node-server.ts` | Memorio lato server: cache in-memory, fallback di `store`/`session`, contesti multi-tenant, più snippet commentati (Express, WebSocket, job queue, CLI). |
14
+ | `observer.ts` | Pattern observer: singolo valore, oggetti interi, più observer sullo stesso path, cleanup. |
15
+ | `platform.ts` | Platform detection dettagliata + isolamento contesti per server multi-tenant, con verifica esplicita dell'isolamento tra due utenti. |
16
+ | `react-app.tsx` | App React completa (header, profilo, cart, notifiche, settings, login/logout) costruita solo su `state`/`store`/`useObserver`. |
17
+ | `react-observer.tsx` | `useObserver` in due modalità (auto-discovery vs deps espliciti) combinato con `typed<T>()` e `registerSchema()`. |
18
+ | `semantic-memory.ts` | Memoria applicativa per un'app LLM-backed: remember/update con confidence e source, retrieval con `memory.context()` - con nota esplicita che non è ricerca semantica per embedding. |
19
+ | `session-advanced.ts` | Uso avanzato di `session`: auth token, bozza di form, carrello, dimensione dello storage. |
20
+ | `sqlite-batched-writes.ts` | Come evitare di serializzare l'intero DB SQLite ad ogni riga: batch di insert seguito da un solo flush. |
21
+ | `state-advanced.ts` | Stato annidato, array, locking di un valore (`.lock()`), path tracking, rimozione stato. |
22
+ | `store-advanced.ts` | `store` avanzato: persistenza, quota, alias dei metodi, gestione errori, serializzazione di vari tipi. |
23
+ | `sync.ts` *(nuovo)* | Local-first sync: configurazione di un `SyncProvider` (push/pull/resolve), scope `device`/`user`/`shared`, ispezione e replay del journal. |
24
+ | `typed-and-schema.ts` | Tutti gli snippet di `TYPED.md` e `SCHEMA.md` in un unico file TypeScript funzionante: typed state + validazione runtime combinati. |
25
+ | `useObserver.tsx` | Guida step-by-step all'hook `useObserver`: dipendenza singola, multiple, auto-discovery, sync con `useState`, mini to-do app. |
26
+ | `browser-vanilla.html` | Demo HTML/JS pura (nessun bundler) con UI per state, store, session, cache, observer e platform info. |
27
+
28
+ ## 📁 markdown/ (24 file)
29
+
30
+ | File | Cosa documenta |
31
+ |---|---|
32
+ | `AUDIT-REPORT.md` | Audit di sicurezza/performance/affidabilità/qualità del codice per la v4.9.5 (datato 2026-09-06), con azioni correttive già applicate. |
33
+ | `CACHE.md` | Reference della cache in-memory: API, quando usarla, limiti. |
34
+ | `CHANGELOG.md` | Storico versioni dalla v2.5.0 alla `Unreleased`, con bugfix, refactoring e note di sicurezza per ogni release. |
35
+ | `DEVTOOLS.md` | Strumenti di debug da console del browser per ispezionare/gestire lo stato di Memorio. |
36
+ | `DISPATCH.md` | Sistema di eventi pub/sub per app vanilla JS (alternativa a `useObserver` fuori da React). |
37
+ | `HISTORY.md` | Reference completa di time-travel: enable/snapshot/diff/undo/redo/rollback/trace, con "how it works" e best practice. |
38
+ | `IDB.md` | Reference IndexedDB: creazione DB/tabelle, CRUD, info sul database. |
39
+ | `IMPORT.md` | I due stili di import (named vs `memorio/global`) e perché condividono la stessa istanza. |
40
+ | `INSPECT.md` | Utility di introspezione per scoprire/verificare la forma dello `state` a runtime - utile per agenti AI. |
41
+ | `LOGGER.md` | Middleware di logging automatico per tracciare le modifiche di stato in console. |
42
+ | `MEMORY-ATTACHMENT.md` | Sistema di "attachment" tra elementi di memoria - estensione opzionale della memoria semantica. |
43
+ | `MEMORY.md` | Reference del layer di memoria semantica: remember/update/context, TTL, confidence, tag, scope. |
44
+ | `OBSERVER.md` | Reference del pattern observer per reagire ai cambi di stato. |
45
+ | `PLATFORM.md` | Platform detection + sistema di context isolation per applicazioni multi-tenant server-side. |
46
+ | `PROJECT.md` | Scheda di progetto interna: versione, team, moduli inclusi, stack tecnologico, target. |
47
+ | `SCHEMA.md` | Validazione runtime dei percorsi di stato: schema oggetto/array/enum/funzione custom. |
48
+ | `SECURITY.md` | Postura di sicurezza del progetto (minacce coperte, cosa NON fa Memorio, come viene gestito l'accesso ai dati). |
49
+ | `SESSION.md` | Reference di `session` (sessionStorage) con fallback in-memory fuori dal browser. |
50
+ | `SQLITE.md` | Reference del layer SQLite via `sql.js`: caricamento lazy, persistenza, query. |
51
+ | `STATE.md` | Reference dello stato reattivo basato su Proxy - il layer centrale di Memorio. |
52
+ | `STORE.md` | Reference di `store` (localStorage) con fallback in-memory fuori dal browser. |
53
+ | `SYNC.md` | Sincronizzazione local-first opzionale: scope, journal, conflict resolution, strategie avanzate per multi-device (HLC, tombstones, fractional indexing). |
54
+ | `TYPED.md` | `memorio.typed<T>()` per la sicurezza dei tipi a compile-time sullo stesso proxy di `state`. |
55
+ | `USEOBSERVER.md` | Reference dell'hook React `useObserver`: modalità auto-discovery vs deps espliciti, tutte le forme di `deps` supportate. |
56
+
57
+ ---
58
+
59
+ **Nota di copertura:** ogni doc in `markdown/` ha ora almeno un esempio corrispondente in `examples/`, **tranne** `DEVTOOLS.md`, `DISPATCH.md`, `INSPECT.md`, `LOGGER.md` e `MEMORY-ATTACHMENT.md` - utile saperlo se in futuro vuoi completare anche quelli.
@@ -0,0 +1,96 @@
1
+ # ADR-001: State Proxy Model
2
+
3
+ > **Status:** Accepted
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Memorio's reactive state is built on a JavaScript `Proxy`. Every read of
10
+ a nested object re-wraps the child in a Proxy so that deep
11
+ `set`/`delete` traps fire and dispatch events. The design must answer:
12
+
13
+ - How are proxy wrappers cached to avoid exponential allocation on
14
+ repeated reads?
15
+ - What is the reference-identity contract between the proxy and the
16
+ raw target?
17
+ - Which property names are reserved and cannot be used as state keys?
18
+ - How are non-plain objects (Date, Set, Map, functions) handled?
19
+
20
+ ## Assumptions
21
+
22
+ - The Proxy target is always a plain `Object` or `Array`.
23
+ - A single `buildProxy` invocation creates one Proxy per raw object.
24
+ - The Proxy is shared across all import paths via `globalThis`
25
+ singletons (see ADR-006).
26
+ - `__DEV__` is a build-time constant (replaced to `false` in
27
+ production, `true` in dev/test).
28
+
29
+ ## Decision
30
+
31
+ ### Wrapper caching
32
+
33
+ `buildProxy` caches the wrapper in a `WeakMap<rawTarget, proxyWrapper>`.
34
+ On every `get` trap, after unwrapping with `TARGET`, the result is looked
35
+ up in the cache; if present, the existing wrapper is returned instead of
36
+ allocating a new one. This keeps repeated nested reads at O(1) in
37
+ allocation and O(1) in proxy depth.
38
+
39
+ ### Reference identity
40
+
41
+ - `TARGET` is a `Symbol` accessor on the proxy that returns the raw
42
+ underlying target. `state[TARGET]` === raw root.
43
+ - `state.a[TARGET]` === raw target's `a` property.
44
+ - `JSON.stringify(proxy)` works because `ownKeys` and
45
+ `getOwnPropertyDescriptor` delegate to `Reflect`.
46
+ - `deepRaw(value)` unwraps all Memorio proxies recursively using
47
+ `TARGET` and a `WeakSet` for cycle safety, returning a plain clone
48
+ with no wrapper references.
49
+
50
+ ### Reserved property names
51
+
52
+ `protect` array in `core/internal.ts` reserves these keys on the root
53
+ `state` proxy:
54
+
55
+ ```
56
+ list, state, store, idb, cache, sqlite, observer, useObserver,
57
+ remove, removeAll, _platform, _capabilities, _sessionId
58
+ ```
59
+
60
+ Setting any of these keys as state data is rejected by the `set` trap
61
+ with a console error. Attempting to set any `_`-prefixed key that
62
+ collides with internal bookkeeping is also guarded.
63
+
64
+ ### Non-plain objects
65
+
66
+ - `Date`, `Set`, `Map`, `RegExp`, functions, and other non-plain
67
+ instances are returned by reference — they are **not** re-wrapped in a
68
+ Proxy.
69
+ - `deepRaw` only traverses `Object` and `Array` constructors; all other
70
+ types are returned by reference.
71
+
72
+ ### Path tracking
73
+
74
+ Every `get` trap for a string property sets `internal.lastAccessedPath`
75
+ to the full dotted path (`state.foo.bar`). When
76
+ `internal.tracking` is true, the path is also added to
77
+ `internal.trackedPaths`.
78
+
79
+ ## Consequences
80
+
81
+ - **Positive:** O(1) wrapper allocation on repeated nested reads; no
82
+ proxy-depth accumulation on spread updates (regression tested).
83
+ - **Positive:** `TARGET` symbol prevents prototype-pollution attacks
84
+ via `__proto__`/`constructor`/`prototype` because those are string
85
+ keys that are simply treated as data keys on the raw target.
86
+ - **Positive:** `deepRaw` produces clean data for persistence and
87
+ snapshots without leaking proxy wrappers.
88
+ - **Negative:** The `WeakMap` cache is per-module-instance, but since
89
+ the proxy target is a singleton on `globalThis`, the cache is
90
+ effectively shared across import paths.
91
+ - **Negative:** Non-plain objects cannot be deeply observed for inner
92
+ mutations — this is an accepted limitation documented in the API.
93
+
94
+ ## Compliance tests
95
+
96
+ - `tests/vitest/tests/contracts/adr-001-state.test.ts`
@@ -0,0 +1,180 @@
1
+ # ADR-002: Observer Semantics
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Memorio must freeze observer behavior so that developers can reason
10
+ about *when* and *why* their callbacks fire. The observer system is the
11
+ bridge between state mutations and UI updates, between the Mutation
12
+ Engine and the reactive system. Before adding advanced features (impact
13
+ analysis, simulation, causal graph), the observer contract must be
14
+ explicit and deterministic.
15
+
16
+ The following questions must have explicit answers:
17
+
18
+ 1. Does a deep mutation notify the leaf observer?
19
+ 2. Does a deep mutation notify parent observers?
20
+ 3. What happens for array mutations?
21
+ 4. What is the reference identity contract for observer callbacks?
22
+ 5. Are notifications synchronous or asynchronous?
23
+ 6. Are multiple mutations batched into a single notification?
24
+ 7. What is the ordering guarantee when multiple observers listen to
25
+ overlapping paths?
26
+ 8. Can an observer mutate state synchronously?
27
+ 9. What happens when an observer throws?
28
+ 10. What happens when an observer is removed during dispatch?
29
+ 11. How are nested transactions handled with respect to observer
30
+ notification?
31
+
32
+ ## Assumptions
33
+
34
+ - The state Proxy fires exactly one callback per `set` or `delete`
35
+ trap (no double-firing for the same logical mutation).
36
+ - The dispatch layer (`core/dispatch.ts`) is the sole event bus.
37
+ - `useObserver` is a thin wrapper over `observer` + `dispatch.listen`.
38
+ - History tracking is opt-in (`enableHistory(true)`).
39
+
40
+ ## Decision
41
+
42
+ ### 1. Leaf notification
43
+
44
+ A mutation at path `state.user.profile.name` dispatches an event on
45
+ exactly the path `state.user.profile.name`. Only observers registered
46
+ on that exact path receive the notification. Observers on ancestor
47
+ paths (`state.user.profile`, `state.user`, `state`) do **not** fire for
48
+ a leaf mutation unless the mutation replaces the ancestor itself.
49
+
50
+ ```js
51
+ observer('state.user.profile.name', cb) // fires
52
+ observer('state.user.profile', cb) // does NOT fire
53
+ observer('state.user', cb) // does NOT fire
54
+ ```
55
+
56
+ This is the **exact-path observer** model. It is intentional:
57
+ coarse-grained ancestor notification is handled by `useObserver`'s
58
+ auto-discovery mode (which explicitly registers on each accessed path),
59
+ not by event bubbling.
60
+
61
+ ### 2. Deep mutation notification
62
+
63
+ When `state.user.profile.name = "Alice"` executes, the set trap fires
64
+ on the `name` property of the `profile` proxy. The callback receives
65
+ `{ path: 'user.profile.name', ... }`. The event dispatched is
66
+ `state.user.profile.name`. Only the leaf observer fires.
67
+
68
+ ### 3. Array mutations
69
+
70
+ Array mutations at index `i` dispatch on the **array path**
71
+ (`state.items`), not the index path. This is because array identity
72
+ matters for UI reconciliation (React list rendering).
73
+
74
+ ```js
75
+ state.items.push(3) // dispatches 'state.items'
76
+ state.items[0] = 99 // dispatches 'state.items'
77
+ state.items.sort() // dispatches 'state.items'
78
+ ```
79
+
80
+ ### 4. Reference identity
81
+
82
+ Observer callbacks receive the event object. The callback function
83
+ itself is stored by reference in the dispatch layer. Re-registering the
84
+ same function on the same path replaces the old listener (single-slot
85
+ behavior in `observer`, multi-subscriber in `dispatch.listen`).
86
+
87
+ `useObserver` deduplicates identical path registrations.
88
+
89
+ ### 5. Synchronous vs asynchronous
90
+
91
+ - **Event dispatch** (`dispatch.set`) is **synchronous** —
92
+ `globalThis.dispatchEvent` runs listeners inline.
93
+ - **Callback notification** via `dispatch.listen` is **asynchronous**
94
+ — wrapped in `queueMicrotask(cb)` (or `Promise.resolve().then(cb)`
95
+ as fallback). This ensures that by the time the callback fires, the
96
+ state Proxy has already committed the mutation.
97
+
98
+ This means:
99
+
100
+ ```js
101
+ state.counter = 1
102
+ // state.counter === 1 is TRUE here (mutation already committed)
103
+ // observer callback hasn't run yet (next microtask)
104
+ ```
105
+
106
+ ### 6. Batching
107
+
108
+ There is **no automatic batching** of observer callbacks. Each mutation
109
+ dispatches its own microtask. Multiple synchronous mutations in a
110
+ transaction produce multiple notifications — one per mutation.
111
+
112
+ A future scheduler (ADR-005) may introduce microtask/raf batching for
113
+ UI frameworks, but the core engine does not coalesce notifications.
114
+
115
+ ### 7. Ordering guarantee
116
+
117
+ When multiple observers listen to the same path, they fire in
118
+ **registration order** (FIFO). The dispatch layer maintains a list of
119
+ handlers per event name.
120
+
121
+ When multiple mutations occur synchronously (without history), they
122
+ dispatch in mutation order. When history is enabled, the undo/redo
123
+ stack preserves mutation order via array index.
124
+
125
+ ### 8. Observer mutating state
126
+
127
+ An observer callback **may** mutate state. Because notifications are
128
+ asynchronous (microtask), the mutation triggers a new dispatch cycle
129
+ with its own microtask. There is no immediate re-entrancy. However,
130
+ deeply recursive state mutations from observers are considered a
131
+ application-level bug and are **not** guarded at the engine level.
132
+
133
+ ### 9. Observer throwing
134
+
135
+ If an observer callback throws, the error propagates as an unhandled
136
+ rejection (since the callback runs inside a microtask). The dispatch
137
+ layer does **not** catch or swallow errors. A throwing observer does
138
+ not prevent other observers on the same path from firing (each
139
+ callback is wrapped in its own microtask boundary via
140
+ `Promise.resolve().then`).
141
+
142
+ ### 10. Observer removed during dispatch
143
+
144
+ Since dispatch is asynchronous (microtask), calling
145
+ `dispatch.remove(path)` or `observer.remove(path)` during a callback
146
+ removes the listener from subsequent dispatches. The current dispatch
147
+ cycle is unaffected — all handlers registered at dispatch time fire.
148
+
149
+ ### 11. Transactions and observer notification
150
+
151
+ During a transaction, each mutation inside the transaction dispatches
152
+ normally. The transaction grouping does not suppress notifications.
153
+ However, `undo()` and `redo()` temporarily disable history recording
154
+ (via `internal.historyEnabled = false`), and the inverse/forward
155
+ operations fire the proxy callback — which will dispatch to observers.
156
+
157
+ A future enhancement may batch observer notifications for all mutations
158
+ within a transaction into a single synthetic notification. This ADR
159
+ does **not** define that behavior yet.
160
+
161
+ ## Consequences
162
+
163
+ - **Positive:** Deterministic, FIFO-ordered, asynchronous notification
164
+ gives observers a consistent view of state on every call.
165
+ - **Positive:** Microtask scheduling avoids "state not yet committed"
166
+ bugs that plague synchronous observer models.
167
+ - **Positive:** Exact-path matching means observers fire only when the
168
+ specific watched path changes — no spurious re-renders.
169
+ - **Positive:** No batching in the core engine keeps it simple and
170
+ framework-agnostic. Batching is a scheduler-layer concern.
171
+ - **Negative:** Multiple synchronous mutations produce multiple
172
+ microtask notifications — consumers that need coalescing must
173
+ implement it (or use the future scheduler).
174
+ - **Negative:** Throwing observers produce unhandled rejections rather
175
+ than being caught — this is intentional (fail fast) but may surprise
176
+ consumers expecting error isolation.
177
+
178
+ ## Compliance tests
179
+
180
+ - `tests/vitest/tests/contracts/adr-002-observer.test.ts`
@@ -0,0 +1,129 @@
1
+ # ADR-003: Deep Mutation Semantics
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Deep mutations (e.g. `state.user.profile.name = "Alice"`) are the most
10
+ common source of subtle bugs in proxy-based state libraries. The
11
+ 4.7.3 baseline had forensic defects around deep mutation notifications:
12
+ observers not firing, paths not resolved correctly, payloads missing
13
+ arguments. While these defects were reported as not reproducible in
14
+ repo v4.9.10, the semantics remain **undocumented** — they rely on
15
+ accidental Proxy behavior rather than an explicit contract.
16
+
17
+ This ADR makes the deep mutation semantics explicit.
18
+
19
+ ## Assumptions
20
+
21
+ - `buildProxy` recursively wraps nested plain objects and arrays.
22
+ - Each Proxy tracks its dotted path via the `tree` array.
23
+ - The set trap computes `path = objPath(key, tree)` — a dotted path
24
+ relative to `state` (e.g. `user.profile.name`).
25
+ - The callback receives `{ action, path, newValue, previousValue }`.
26
+ - The proxy stores **raw** values (via `deepRaw`) on the raw target.
27
+
28
+ ## Decision
29
+
30
+ ### Deep mutation notification scope
31
+
32
+ When `state.user.profile.name = "Alice"` executes:
33
+
34
+ 1. The set trap on the deepest proxy (the `profile` object) fires.
35
+ 2. `path` is computed as `user.profile.name` (relative to `state`).
36
+ 3. The callback receives `{ path: "user.profile.name", ... }`.
37
+ 4. `dispatch.set("state.user.profile.name", ...)` is called.
38
+ 5. **Only** observers on `state.user.profile.name` fire.
39
+ 6. `state.user.profile.name` is updated on the raw target to `"Alice"`.
40
+
41
+ Observers on `state.user.profile`, `state.user`, or `state` do **not**
42
+ receive a notification for this mutation. This is the exact-path
43
+ contract (see ADR-002, §1).
44
+
45
+ ### Reference identity after deep mutation
46
+
47
+ After `state.user.profile.name = "Alice"`:
48
+ - `state.user` returns the **same** proxy wrapper (cached via WeakMap).
49
+ - `state.user.profile` returns the **same** proxy wrapper.
50
+ - `state.user.profile.name` returns `"Alice"` (the new primitive).
51
+ - `state.user.profile[TARGET]` returns the same raw object as before
52
+ (the object was mutated in place, not replaced).
53
+
54
+ ### Intermediate object creation
55
+
56
+ If a deep path does not exist yet (`state.user.profile` is undefined),
57
+ and a consumer writes `state.user.profile = { name: "Alice" }`:
58
+ 1. The set trap on `user` fires with `path = "user.profile"`.
59
+ 2. The full object `{ name: "Alice" }` is stored as the raw value.
60
+ 3. A new proxy wrapper is created for `{ name: "Alice" }` on the next
61
+ read.
62
+ 4. The event dispatched is `state.user.profile` — **not**
63
+ `state.user.profile.name`.
64
+
65
+ There is **no automatic intermediate object creation** during deep
66
+ assignment to nested proxies. If `state.user` is undefined and the
67
+ consumer writes `state.user.profile.name = "Alice"`, this throws a
68
+ TypeError (cannot set property 'profile' of undefined).
69
+
70
+ To set deeply nested values that don't exist yet, use
71
+ `memorio.mutate("state.user.profile.name", "Alice")` which creates
72
+ intermediate objects programmatically.
73
+
74
+ ### No-op mutations
75
+
76
+ If a set trap assigns the same value (`state.x = state.x`), the set
77
+ trap still fires. The callback still dispatches the event. However,
78
+ the `mutationToPatch` function returns `null` for `before === after`,
79
+ so no patch is stored in the commit log. The undo/redo stack records
80
+ the mutation (with `before === after`), but undo is a no-op for such
81
+ entries.
82
+
83
+ A future enhancement may add value-comparison in the set trap to skip
84
+ no-op dispatches. This ADR does **not** implement that.
85
+
86
+ ### Delete semantics
87
+
88
+ `delete state.user.profile.name`:
89
+ 1. The `deleteProperty` trap on the `profile` proxy fires.
90
+ 2. `path` is computed as `user.profile.name`.
91
+ 3. The callback receives `{ action: "delete", path, previousValue }`.
92
+ 4. The event dispatched is `state.user.profile.name`.
93
+ 5. The property is removed from the raw target.
94
+
95
+ ### Array deep mutation
96
+
97
+ When a nested object inside an array is mutated (`state.items[0].name = "X"`):
98
+ 1. The set trap fires on the proxy wrapping `items[0]`.
99
+ 2. `path` is computed as `items.0.name` (array index joined by dots).
100
+ 3. The event dispatched is `state.items.0.name`.
101
+ 4. **Additionally**, because arrays use identity-based reconciliation,
102
+ the observer contract (ADR-002 §3) states that array observers fire
103
+ on the **array path** (`state.items`), not the element path. However,
104
+ the dispatch only fires `state.items.0.name`. Array observers
105
+ registered via `observer('state.items', ...)` will **not** fire for
106
+ `items[0].name` mutations.
107
+
108
+ > **Note:** This is a known limitation. Array element mutations that
109
+ > change object properties do not notify the array-path observer.
110
+ > A future scheduler or mutation engine enhancement may address this
111
+ > by dispatching on both `state.items.0.name` and `state.items`.
112
+ > This ADR documents the current behavior.
113
+
114
+ ## Consequences
115
+
116
+ - **Positive:** Deep mutations are deterministic — exactly one event
117
+ per mutation, on the exact leaf path.
118
+ - **Positive:** Reference identity is preserved — the same proxy wrapper
119
+ is reused, preventing proxy-depth accumulation (regression tested).
120
+ - **Positive:** `memorio.mutate()` provides a safe path-based API for
121
+ setting deeply nested values that don't exist yet.
122
+ - **Negative:** Array element object mutations don't notify array-path
123
+ observers — consumers must observe the specific element path.
124
+ - **Negative:** No-op set traps still dispatch events — this is
125
+ intentional for consistency but may cause unnecessary re-renders.
126
+
127
+ ## Compliance tests
128
+
129
+ - `tests/vitest/tests/contracts/adr-003-deep-mutation.test.ts`
@@ -0,0 +1,128 @@
1
+ # ADR-004: Array Mutation Semantics
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+ > **Deciders:** Memorio 5.x Core Team
6
+
7
+ ## Context
8
+
9
+ Arrays are the most error-prone data structure in proxy-based state
10
+ libraries. The 4.7.3 baseline handled `push` and `pop` but had no
11
+ explicit contract for `sort`, `splice`, `shift`, `unshift`, `fill`,
12
+ `reverse`, `length` changes, or whole-array replacement. Section 9 of
13
+ the Memorio 5.x plan demands explicit, regression-tested behavior for
14
+ every array operation.
15
+
16
+ A mutation that persists but does not notify, or notifies but does not
17
+ persist, is considered a **semantic defect** unless explicitly
18
+ documented.
19
+
20
+ ## Assumptions
21
+
22
+ - Arrays are plain JS arrays stored on the Proxy raw target.
23
+ - Array methods (`push`, `pop`, `splice`, etc.) mutate the array in
24
+ place and trigger the set trap on the Proxy wrapping the array.
25
+ - The set trap's `key` for array mutations is the array index or
26
+ `length`.
27
+ - The callback receives `{ action, path, newValue, previousValue }`.
28
+ - The state Proxy dispatches via `dispatch.set("state." + path)`.
29
+
30
+ ## Decision
31
+
32
+ ### Mutation → Persistence mapping
33
+
34
+ Every array operation below **must** persist to the underlying raw
35
+ array and **must** dispatch an event on the **array path**
36
+ (`state.<arrayPath>`), not on the individual index path. The event
37
+ path is computed from the Proxy `tree` (e.g. for `state.items`, the
38
+ tree is `["items"]`, so the path is `items` and the event is
39
+ `state.items`).
40
+
41
+ | Operation | Persists? | Dispatches on | Patch op |
42
+ |-----------|-----------|---------------|----------|
43
+ | `state.arr[i] = val` | Yes | `state.arr` | `replace` (index i) |
44
+ | `state.arr.push(val)` | Yes | `state.arr` | `add` (at index `arr.length`) |
45
+ | `state.arr.pop()` | Yes | `state.arr` | `remove` (last index) |
46
+ | `state.arr.shift()` | Yes | `state.arr` | `remove` (index 0) |
47
+ | `state.arr.unshift(val)` | Yes | `state.arr` | `add` (at index 0) |
48
+ | `state.arr.splice(i, n, ...items)` | Yes | `state.arr` | `remove` + `add` |
49
+ | `state.arr.sort()` | Yes | `state.arr` | `replace` (whole array) |
50
+ | `state.arr.reverse()` | Yes | `state.arr` | `replace` (whole array) |
51
+ | `state.arr.fill(val, start, end)` | Yes | `state.arr` | `replace` (whole array) |
52
+ | `state.arr.length = n` (truncate) | Yes | `state.arr` | `remove` (removed indices) |
53
+ | `state.arr.length = n` (extend) | Yes | `state.arr` | `add` (new indices) |
54
+ | `state.arr = newArray` | Yes | `state.arr` | `replace` (whole array) |
55
+ | `state.arr[i].prop = val` | Yes | `state.arr.i.prop` | `replace` (leaf) |
56
+
57
+ ### sort() persistence
58
+
59
+ `sort()` mutates the array in place and returns a reference to the same
60
+ (sorted) array. The Proxy set trap fires because `length` changes and
61
+ indices are reassigned. The event dispatched is `state.arr`. The
62
+ mutation is **persisted** to the raw target.
63
+
64
+ **Known constraint:** `sort()` with a custom comparator must also
65
+ persist correctly. The comparator function is applied by V8's native
66
+ `Array.prototype.sort`; the Proxy only sees the resulting index
67
+ reassignments.
68
+
69
+ ### splice() semantics
70
+
71
+ `splice()` can add, remove, or replace elements at an index. The Proxy
72
+ sees multiple set/delete traps. The event dispatched is `state.arr`
73
+ (the first trap fires, and subsequent traps on the same array path
74
+ are de-duplicated by the dispatch layer's microtask scheduling).
75
+
76
+ ### length changes
77
+
78
+ Setting `arr.length = n` where `n < arr.length` truncates the array.
79
+ The deleteProperty trap fires for each removed index. Setting
80
+ `arr.length = n` where `n > arr.length` extends the array with
81
+ `undefined` holes; the set trap fires for `length`.
82
+
83
+ ### Whole-array replacement
84
+
85
+ `state.arr = [1, 2, 3]` replaces the entire array. The set trap fires
86
+ once with `path = "arr"`, `newValue = [1, 2, 3]` (deepRaw'd). The
87
+ event dispatched is `state.arr`. Observers on `state.arr` fire.
88
+ Previous array observers (on the old array instance) are orphaned —
89
+ the consumer must re-register if they observe specific indices.
90
+
91
+ ### Array element object mutation
92
+
93
+ `state.arr[0].name = "X"` mutates an object inside the array. This
94
+ fires the set trap on the Proxy wrapping `arr[0]` with
95
+ `path = "arr.0.name"`. The event dispatched is `state.arr.0.name`.
96
+
97
+ Per ADR-003, this does **not** notify observers on `state.arr`. This
98
+ is a known limitation. Consumers who need to observe changes to
99
+ objects within arrays must observe the specific element path
100
+ (`state.arr.0.name`) or use `useObserver` auto-discovery.
101
+
102
+ ### Event deduplication
103
+
104
+ Because a single array operation (e.g. `splice`) can produce multiple
105
+ set/delete traps, the dispatch layer may fire `state.arr` multiple
106
+ times within the same synchronous block. Each dispatch schedules a
107
+ separate microtask. Observers on `state.arr` will fire **multiple
108
+ times** — once per trap. A future scheduler enhancement may coalesce
109
+ these. This ADR documents the current behavior.
110
+
111
+ ## Consequences
112
+
113
+ - **Positive:** Every array operation persists and notifies — no
114
+ silent failures.
115
+ - **Positive:** Array observers fire on the array path, not individual
116
+ indices — this matches React list-reconciliation mental model.
117
+ - **Positive:** `sort()`, `splice()`, and other complex operations are
118
+ explicitly tested for persistence + notification.
119
+ - **Negative:** Multiple traps from a single array operation may
120
+ produce multiple observer notifications — consumers must
121
+ de-duplicate if needed.
122
+ - **Negative:** Array element object mutations do not notify the
123
+ array-path observer — this is a fundamental limitation of the
124
+ exact-path observer model.
125
+
126
+ ## Compliance tests
127
+
128
+ - `tests/vitest/tests/contracts/adr-004-arrays.test.ts`