memorio 4.7.1 → 4.7.3

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.
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
- ## v4.6.1 (Security Patch) - 2026-08-14 CRITICAL Security Fix
7
+ ## v4.6.1 (Security Patch) - 2026-08-14 - CRITICAL Security Fix
8
8
 
9
9
  ### 🔐 Security NOTICE (v4.6.0)
10
10
 
@@ -32,7 +32,7 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
32
32
 
33
33
  ---
34
34
 
35
- ## v4.6.0 (Previous - SECURITY ISSUE) - 2026-08-13 Refactoring & Documentation
35
+ ## v4.6.0 (Previous - SECURITY ISSUE) - 2026-08-13 - Refactoring & Documentation
36
36
 
37
37
  **⚠️ WARNING**: This version had a hardcoded PAT token that was later remediated in v4.6.1**
38
38
 
@@ -75,7 +75,7 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
75
75
 
76
76
  ---
77
77
 
78
- ## v3.0.2 (Current) - 2026-05-19 Bug Fix, Security & API Expansion
78
+ ## v3.0.2 (Current) - 2026-05-19 - Bug Fix, Security & API Expansion
79
79
 
80
80
  ### 🐛 Bug Fixes
81
81
 
@@ -98,9 +98,9 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
98
98
 
99
99
  - Added JSDoc to `observerFunction` in `functions/observer/index.ts`
100
100
  - Added JSDoc to `cache` global in `functions/cache/index.ts`
101
- - `lint` and `tsc` pass clean 0 vulnerabilities from `npm audit`
101
+ - `lint` and `tsc` pass clean - 0 vulnerabilities from `npm audit`
102
102
 
103
- ### 🆕 API New in 3.0.2
103
+ ### 🆕 API - New in 3.0.2
104
104
 
105
105
  | Function | Description |
106
106
  |----------|-------------|
@@ -136,10 +136,10 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
136
136
 
137
137
  ---
138
138
 
139
- ## v2.9.0 2026-05-13
139
+ ## v2.9.0 - 2026-05-13
140
140
 
141
141
  ### Added
142
- - DevTools `memorio.devtools.inspect()`, `stats()`, `exportData()`
142
+ - DevTools - `memorio.devtools.inspect()`, `stats()`, `exportData()`
143
143
  - Logger with full history, stats and export
144
144
  - Platform detection (`isBrowser`, `isNode`, `isDeno`, `isEdge`, `getCapabilities`)
145
145
  - Session isolation via `crypto.randomUUID()`
@@ -154,7 +154,7 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
154
154
 
155
155
  ---
156
156
 
157
- ## v2.5.0 2026-02-17
157
+ ## v2.5.0 - 2026-02-17
158
158
 
159
159
  - Initial release of memorio (state, store, session, cache, idb)
160
160
  - Observer pattern (`observer`)
@@ -0,0 +1,192 @@
1
+ # History, Undo / Redo, Snapshot, Diff, Trace - Memorio
2
+
3
+ > ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
4
+
5
+ Memorio provides a lightweight time-travel system for `state` mutations: snapshots, diffs, undo/redo, and a full mutation trace log.
6
+
7
+ History tracking is **opt-in** — it is disabled by default to avoid overhead. Enable it when you need undo/redo or trace capabilities.
8
+
9
+ ---
10
+
11
+ ## Enable History
12
+
13
+ ```javascript
14
+ import 'memorio'
15
+
16
+ memorio.enableHistory() // enable tracking
17
+ // ... mutate state ...
18
+ memorio.state.user = { name: 'Sara' }
19
+ memorio.state.counter = 42
20
+ ```
21
+
22
+ > Without `enableHistory()`, mutations are not recorded and `undo()`/`redo()`/`trace()` return empty results.
23
+
24
+ ---
25
+
26
+ ## Snapshot & Diff
27
+
28
+ Snapshot captures the entire `state` tree at a point in time. Diff compares a snapshot against current state to see what changed.
29
+
30
+ ```javascript
31
+ // Enable history (snapshots work regardless, but trace/undo need it)
32
+ memorio.enableHistory()
33
+
34
+ // Take a snapshot
35
+ state.user = { name: 'Sara', age: 30 }
36
+ const snap = memorio.snapshot()
37
+
38
+ // Make changes
39
+ state.user.name = 'Luigi'
40
+ state.counter = 100
41
+ state.items = ['a', 'b']
42
+
43
+ // Diff against the snapshot
44
+ const changes = memorio.diff(snap)
45
+ console.debug(changes)
46
+ // [
47
+ // { path: 'user.name', oldValue: 'Sara', newValue: 'Luigi' },
48
+ // { path: 'counter', oldValue: undefined, newValue: 100 },
49
+ // { path: 'items', oldValue: undefined, newValue: ['a', 'b'] }
50
+ // ]
51
+ ```
52
+
53
+ This is essential for AI agents: take a snapshot, make changes, inspect the diff, and decide whether to commit or rollback.
54
+
55
+ ---
56
+
57
+ ## Undo / Redo
58
+
59
+ ```javascript
60
+ state.user = { name: 'Sara' }
61
+ state.counter = 100
62
+ state.items = ['a', 'b']
63
+
64
+ memorio.undo() // removes state.items
65
+ memorio.undo() // counter → undefined
66
+ memorio.redo() // counter → 100 again
67
+
68
+ memorio.canUndo() // true
69
+ memorio.canRedo() // true (after above undo + redo cycle)
70
+ ```
71
+
72
+ - `undo()`: Restores the previous state by inverting the most recent mutation.
73
+ - `redo()`: Re-applies the most recently undone mutation.
74
+ - `canUndo()` / `canRedo()`: Check availability before calling.
75
+
76
+ ### Max history depth
77
+
78
+ ```javascript
79
+ memorio.setMaxHistory(50) // keep at most 50 mutations per stack (default: 100)
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Rollback (full state restore)
85
+
86
+ Unlike undo (which works one step at a time), `rollback` replaces the entire state from a snapshot:
87
+
88
+ ```javascript
89
+ const snap = memorio.snapshot()
90
+
91
+ state.experiment = { result: 'failed' }
92
+ state.counter = 999
93
+
94
+ // Discard everything and restore to snapshot
95
+ memorio.rollback(snap)
96
+ // state.experiment is now gone
97
+ // state.counter is back to its snapshot value
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Trace (mutation log)
103
+
104
+ The trace log records every mutation with timestamp, path, action, and before/after values:
105
+
106
+ ```javascript
107
+ memorio.enableHistory()
108
+
109
+ state.user.name = 'Sara'
110
+ state.counter = 1
111
+ state.counter = 2
112
+
113
+ const log = memorio.trace()
114
+ console.debug(log)
115
+ // [
116
+ // { path: 'user.name', action: 'set', newValue: 'Sara', previousValue: undefined, timestamp: 1725... },
117
+ // { path: 'counter', action: 'set', newValue: 1, previousValue: undefined, timestamp: 1725... },
118
+ // { path: 'counter', action: 'set', newValue: 2, previousValue: 1, timestamp: 1725... }
119
+ // ]
120
+ ```
121
+
122
+ This is useful for:
123
+ - **AI debugging**: inspect what changed and when
124
+ - **Event sourcing**: export the log and replay state from scratch
125
+ - **Audit trails**: log all mutations for compliance
126
+
127
+ ### Export / import trace
128
+
129
+ ```javascript
130
+ const log = memorio.trace()
131
+ localStorage.setItem('memorio-trace', JSON.stringify(log))
132
+
133
+ // Later, replay:
134
+ const saved = JSON.parse(localStorage.getItem('memorio-trace'))
135
+ for (const record of saved) {
136
+ if (record.action === 'set') {
137
+ state[record.path] = record.newValue
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Clear History
145
+
146
+ ```javascript
147
+ memorio.clearHistory() // wipe undo/redo stacks + trace log
148
+ memorio.clearRedo() // clear only the redo stack (undo stack preserved)
149
+ ```
150
+
151
+ > `clearHistory()` does NOT reset the current `state` — only the history tracking data.
152
+
153
+ ---
154
+
155
+ ## Full API
156
+
157
+ | Method | Parameters | Returns | Description |
158
+ |--------|-----------|---------|-------------|
159
+ | `memorio.snapshot()` | none | `Record<string, any>` | Deep clone of current state |
160
+ | `memorio.diff(snap)` | `snap` | `DiffEntry[]` | Changed paths with old/new values |
161
+ | `memorio.undo()` | none | `MutationRecord \| undefined` | Undo last mutation |
162
+ | `memorio.redo()` | none | `MutationRecord \| undefined` | Redo last undone mutation |
163
+ | `memorio.canUndo()` | none | `boolean` | Whether undo is available |
164
+ | `memorio.canRedo()` | none | `boolean` | Whether redo is available |
165
+ | `memorio.rollback(snap)` | `snap` | `void` | Restore full state from snapshot |
166
+ | `memorio.trace()` | none | `MutationRecord[]` | List of all recorded mutations |
167
+ | `memorio.enableHistory(enabled?)` | `boolean` | `void` | Enable/disable tracking |
168
+ | `memorio.clearHistory()` | none | `void` | Clear all history stacks |
169
+ | `memorio.clearRedo()` | none | `void` | Clear only redo stack |
170
+ | `memorio.setMaxHistory(max)` | `number` | `void` | Set max stack depth |
171
+ | `memorio.getMaxHistory()` | none | `number` | Get current max depth |
172
+
173
+ ---
174
+
175
+ ## How It Works
176
+
177
+ 1. When `historyEnabled` is true, the state proxy's callback (`buildProxy` callback) fires on every `set`/`delete` trap, recording a `MutationRecord` with path, action, oldValue, newValue, and timestamp.
178
+ 2. Records are pushed to both a trace log (`mutations`) and an undo stack.
179
+ 3. Any new mutation clears the redo stack.
180
+ 4. `undo()` pops from the undo stack, pushes to the redo stack, and applies the inverse operation (restoring the previous value, or deleting if it was new).
181
+ 5. `redo()` pops from the redo stack, pushes back to the undo stack, and re-applies the original mutation.
182
+ 6. During undo/redo, history tracking is temporarily disabled to prevent recursive recording.
183
+ 7. `diff()` does a recursive key-by-key comparison between the snapshot and current `deepRaw(state)`.
184
+
185
+ ---
186
+
187
+ ## Best Practices
188
+
189
+ 1. **Always snapshot before AI experimentation** — `const snap = memorio.snapshot()` gives you a safe rollback point.
190
+ 2. **Call `diff()` before `rollback()`** — inspect what changed first; sometimes you only need to revert one key.
191
+ 3. **Keep `maxHistory` reasonable** — the default (100) is fine for most apps. Lower it for memory-constrained environments.
192
+ 4. **Don't rely on trace for sensitive data** — the trace log records *all* values written, including tokens/PII. Clear it or disable tracing in production paths.
@@ -0,0 +1,116 @@
1
+ # Introspection & Inspection - Memorio
2
+
3
+ > ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
4
+
5
+ Introspection utilities let you programmatically discover, verify, and read the shape of the global `state` proxy. Essential for AI agents that need to check whether a path exists before writing, or enumerate available keys before reading.
6
+
7
+ ---
8
+
9
+ ## stateKeys()
10
+
11
+ Returns all top-level keys currently on the `state` proxy. Excludes internal properties.
12
+
13
+ ```javascript
14
+ import 'memorio'
15
+
16
+ state.user = { name: 'Sara' }
17
+ state.counter = 42
18
+
19
+ memorio.stateKeys() // ['user', 'counter']
20
+ ```
21
+
22
+ ---
23
+
24
+ ## pathExists(path)
25
+
26
+ Checks whether a dotted path exists in state. Returns `true` if the path resolves to a non-undefined value.
27
+
28
+ ```javascript
29
+ state.user = { name: 'Sara', profile: { age: 30 } }
30
+
31
+ memorio.pathExists('user') // true
32
+ memorio.pathExists('user.name') // true
33
+ memorio.pathExists('user.profile') // true
34
+ memorio.pathExists('user.profile.age') // true
35
+ memorio.pathExists('user.age') // false
36
+ memorio.pathExists('nonexistent') // false
37
+ ```
38
+
39
+ This is critical for AI agents: always check `pathExists` before writing to a nested path to avoid creating unintended intermediate objects.
40
+
41
+ ---
42
+
43
+ ## stateType(path)
44
+
45
+ Returns the runtime type of the value at a given state path.
46
+
47
+ ```javascript
48
+ state.count = 42
49
+ state.name = 'Sara'
50
+ state.items = [1, 2, 3]
51
+
52
+ memorio.stateType('count') // 'number'
53
+ memorio.stateType('name') // 'string'
54
+ memorio.stateType('items') // 'array'
55
+ memorio.stateType('missing') // 'undefined'
56
+ ```
57
+
58
+ ---
59
+
60
+ ## stateGet(path)
61
+
62
+ Returns the value at a dotted path, deep-cloned to prevent accidental mutation of state.
63
+
64
+ ```javascript
65
+ state.user = { name: 'Sara', tags: ['admin'] }
66
+
67
+ const user = memorio.stateGet('user') // { name: 'Sara', tags: ['admin'] }
68
+ user.name = 'Luigi' // mutates the clone, not state
69
+ state.user.name // still 'Sara'
70
+ ```
71
+
72
+ ---
73
+
74
+ ## stateSchema()
75
+
76
+ Generates a full schema report of the current state tree — every path with its type and whether it's defined.
77
+
78
+ ```javascript
79
+ state.user = { name: 'Sara', age: 30 }
80
+ state.theme = 'dark'
81
+
82
+ memorio.stateSchema()
83
+ // [
84
+ // { path: 'user', type: 'object', defined: true },
85
+ // { path: 'user.name', type: 'string', defined: true },
86
+ // { path: 'user.age', type: 'number', defined: true },
87
+ // { path: 'theme', type: 'string', defined: true }
88
+ // ]
89
+ ```
90
+
91
+ This is the **most useful for AI agents** — it gives a complete picture of what's in state and what types the values are, in a single call. Perfect for:
92
+ - Discovering available state before generating code
93
+ - Validating that expected paths exist
94
+ - Understanding the shape of nested objects
95
+
96
+ ---
97
+
98
+ ## Full API
99
+
100
+ | Method | Parameters | Returns | Description |
101
+ |--------|-----------|---------|-------------|
102
+ | `memorio.stateKeys()` | none | `string[]` | Top-level state keys |
103
+ | `memorio.pathExists(path)` | `string` | `boolean` | Whether a path resolves to a value |
104
+ | `memorio.stateType(path)` | `string` | `string` | Runtime type at path |
105
+ | `memorio.stateGet(path)` | `string` | `any` | Deep-cloned value at path |
106
+ | `memorio.stateSchema()` | none | `SchemaEntry[]` | Full state tree report |
107
+
108
+ ---
109
+
110
+ ## How It Works
111
+
112
+ All introspection functions read from the global `state` proxy via `deepRaw()` — the same function used internally by the state proxy to unwrap itself before storing. This ensures consistent, non-proxied values are returned.
113
+
114
+ - `pathExists` and `stateGet` split the path on `.` and traverse the state tree.
115
+ - `stateSchema` recursively walks the state object, collecting every path and its type.
116
+ - All returned values from `stateGet` and `snapshot` are deep clones (via JSON round-trip) to prevent accidental mutation.
@@ -0,0 +1,155 @@
1
+ # Memory System
2
+
3
+ `memorio.memory` provides a semantic memory layer — a key/value store with type safety, TTL, confidence scoring, tagging, and scope-based persistence.
4
+
5
+ ## Core API
6
+
7
+ ### `memorio.memory.remember(key, value, opts?)`
8
+
9
+ Stores a memory entry.
10
+
11
+ ```ts
12
+ await memorio.memory.remember('user.name', 'Dario', {
13
+ type: 'preference',
14
+ confidence: 0.92,
15
+ scope: 'local',
16
+ ttl: null,
17
+ tags: ['user', 'profile'],
18
+ source: 'conversation'
19
+ })
20
+ ```
21
+
22
+ | Option | Type | Default | Description |
23
+ |--------|------|---------|-------------|
24
+ | `id` | `string` | `key` | Custom entry ID |
25
+ | `type` | `'fact' \| 'preference' \| 'decision' \| 'task' \| 'context'` | `'fact'` | Semantic classification |
26
+ | `confidence` | `number` | `1.0` | Trust score 0.0–1.0 |
27
+ | `scope` | `'hot' \| 'session' \| 'local' \| 'durable'` | auto | Storage backend (see below) |
28
+ | `ttl` | `number \| null` | `null` | Milliseconds before expiry (`null` = never) |
29
+ | `tags` | `string \| string[]` | `[]` | Metadata for filtering |
30
+ | `source` | `string` | `undefined` | Origin of the memory |
31
+
32
+ ### `memorio.memory.recall(query, opts?)`
33
+
34
+ Retrieves a memory by key.
35
+
36
+ ```ts
37
+ const name = await memorio.memory.recall('user.name')
38
+ const highConfidence = await memorio.memory.recall('project', {
39
+ minConfidence: 0.8
40
+ })
41
+ ```
42
+
43
+ | Option | Type | Default | Description |
44
+ |--------|------|---------|-------------|
45
+ | `type` | `MemoryType \| MemoryType[]` | `*` | Filter by type |
46
+ | `tags` | `string \| string[]` | `*` | Filter by tags |
47
+ | `minConfidence` | `number` | `0` | Minimum confidence threshold |
48
+ | `includeObsolete` | `boolean` | `false` | Include expired/superseded entries |
49
+
50
+ ### `memorio.memory.update(key, value, opts?)`
51
+
52
+ Updates an existing memory. Creates a **superseded copy** of the old entry (preserving history).
53
+
54
+ ```ts
55
+ await memorio.memory.update('user.name', 'Alice', { confidence: 0.95 })
56
+ ```
57
+
58
+ ### `memorio.memory.forget(key)`
59
+
60
+ Permanently removes a memory.
61
+
62
+ ```ts
63
+ await memorio.memory.forget('user.temp')
64
+ ```
65
+
66
+ ### `memorio.memory.context(opts?)`
67
+
68
+ Returns ranked, relevant memories. Uses recency × confidence × type/scope boost algorithm.
69
+
70
+ ```ts
71
+ const ctx = await memorio.memory.context({
72
+ tags: 'project',
73
+ types: ['decision', 'preference'],
74
+ minConfidence: 0.7,
75
+ maxEntries: 10,
76
+ scopes: ['local', 'durable']
77
+ })
78
+ ```
79
+
80
+ Returns:
81
+ ```ts
82
+ Array<{
83
+ key: string
84
+ value: any
85
+ type: MemoryType
86
+ confidence: number
87
+ scope: MemoryScope
88
+ tags: string[]
89
+ age: number // milliseconds since createdAt
90
+ accessCount: number
91
+ lastAccessedAt: number
92
+ }>
93
+ ```
94
+
95
+ ### `memorio.memory.stats()`
96
+
97
+ Returns usage statistics:
98
+
99
+ ```ts
100
+ {
101
+ total: number
102
+ byScope: Record<MemoryScope, number>
103
+ byType: Partial<Record<MemoryType, number>>
104
+ expired: number
105
+ }
106
+ ```
107
+
108
+ ### `memorio.memory.forgetExpired()`
109
+
110
+ Cleans all expired entries. Returns count removed.
111
+
112
+ ### `memorio.memory.clear()`
113
+
114
+ Wipes all memories and the index.
115
+
116
+ ## Memory Entry Model
117
+
118
+ ```ts
119
+ interface MemoryEntry<T = any> {
120
+ id: string
121
+ key: string
122
+ value: T
123
+ type: 'fact' | 'preference' | 'decision' | 'task' | 'context'
124
+ confidence: number
125
+ scope: 'hot' | 'session' | 'local' | 'durable'
126
+ ttl?: number | null
127
+ tags: string[]
128
+ source?: string
129
+ status: 'active' | 'obsolete' | 'superseded'
130
+ createdAt: number
131
+ lastConfirmedAt: number
132
+ supersededId?: string | null
133
+ }
134
+ ```
135
+
136
+ ## Scopes
137
+
138
+ | Scope | Storage | TTL | Cross-session | Size limit |
139
+ |-------|---------|-----|---------------|------------|
140
+ | `hot` | `state` proxy | ✅ | ❌ | ~5MB (RAM) |
141
+ | `session` | `sessionStorage` | ✅ | Tab only | ~5MB |
142
+ | `local` | `localStorage` | ✅ | ✅ | ~10MB |
143
+ | `durable` | `IndexedDB` | ✅ | ✅ | ~1GB+ |
144
+
145
+ Default scope is `local` for values ≤100KB, `durable` for larger values.
146
+
147
+ ## Memory Lifecycle
148
+
149
+ | Event | Behavior |
150
+ |-------|----------|
151
+ | `remember()` on existing key | Old entry → `superseded`, new entry → `active` |
152
+ | Entry with TTL expires | Status → `obsolete` (cleaned by `forgetExpired()`) |
153
+ | `recall()` expired entry | Returns `null` unless `includeObsolete: true` |
154
+ | `update()` | Creates superseded copy + updated active entry |
155
+ | `clear()` | Wipes all memories + index |
@@ -0,0 +1,169 @@
1
+ # Schema Validation - Memorio
2
+
3
+ > ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
4
+
5
+ Schema validation guards your `state` against invalid writes. It runs inside the state proxy's `set` trap, so any `state.somePath = value` that violates a registered schema is rejected at runtime — before the value is ever stored.
6
+
7
+ Schema validation is **opt-in** and **zero-dependency**.
8
+
9
+ ---
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import 'memorio'
15
+
16
+ // Register a validator for a top-level state key
17
+ memorio.registerSchema('user', {
18
+ type: 'object',
19
+ required: ['name', 'email'],
20
+ properties: {
21
+ name: { type: 'string', min: 1 },
22
+ email: { type: 'string', pattern: /^[^@]+@[^@]+$/ },
23
+ age: { type: 'number', min: 0, max: 150 }
24
+ }
25
+ })
26
+
27
+ // Valid write — accepted
28
+ state.user = { name: 'Sara', email: 'sara@test.com', age: 30 }
29
+
30
+ // Invalid write — rejected, returns false
31
+ state.user = { name: 'Sara' } // missing 'email'
32
+ state.user = { name: 42, email: 'x' } // wrong type for 'name'
33
+ state.user = { age: -5 } // out of range
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Schema Definition
39
+
40
+ A `Schema` object supports the following fields:
41
+
42
+ | Field | Type | Description |
43
+ |-------|------|-------------|
44
+ | `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array' \| 'any'` | Runtime type check |
45
+ | `required` | `string[]` | Property names that must exist (objects only) |
46
+ | `properties` | `Record<string, Schema>` | Nested property schemas (validated recursively) |
47
+ | `min` | `number` | Number: minimum value. String: minimum length |
48
+ | `max` | `number` | Number: maximum value. String: maximum length |
49
+ | `pattern` | `RegExp` | Regex the string value must match |
50
+ | `enum` | `any[]` | Whitelist of allowed values |
51
+ | `validator` | `(value) => boolean \| string` | Custom validator function |
52
+
53
+ ### Custom validator functions
54
+
55
+ For logic that's hard to express declaratively, pass a function instead of a schema object:
56
+
57
+ ```javascript
58
+ memorio.registerSchema('counter', (value) => {
59
+ if (typeof value !== 'number') return 'counter must be a number'
60
+ if (value < 0) return 'counter must be >= 0'
61
+ return true
62
+ })
63
+ ```
64
+
65
+ A custom validator receives the raw value. Return `true` to accept, or a **string** describing the error to reject.
66
+
67
+ ---
68
+
69
+ ## Path-based registration
70
+
71
+ Schemas are keyed by their **state path**, relative to `state`:
72
+
73
+ | API call | Catches |
74
+ |----------|---------|
75
+ | `registerSchema('user', schema)` | `state.user = value` |
76
+ | `registerSchema('user.age', schema)` | `state.user.age = value` |
77
+ | `registerSchema('items', schema)` | `state.items = value` |
78
+
79
+ The full dotted path is constructed from the proxy's tree depth. Nested sets propagate the full path automatically.
80
+
81
+ ---
82
+
83
+ ## Manual validation
84
+
85
+ You can validate a value without writing it to state:
86
+
87
+ ```javascript
88
+ memorio.validate('user', { name: 'Sara', email: 'sara@test.com' })
89
+ // { valid: true }
90
+
91
+ memorio.validate('user', { name: 'Sara' })
92
+ // { valid: false, errors: ["user: missing required property 'email'"] }
93
+ ```
94
+
95
+ When no schema is registered for a path, `validate` returns `{ valid: true }`.
96
+
97
+ ---
98
+
99
+ ## Schema management
100
+
101
+ ```javascript
102
+ memorio.listSchemas() // ['user', 'theme', 'items', 'counter']
103
+ memorio.unregisterSchema('counter') // removes the schema
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Full API
109
+
110
+ | Method | Parameters | Returns | Description |
111
+ |--------|-----------|---------|-------------|
112
+ | `memorio.registerSchema(path, schema)` | `string`, `Schema \| fn` | `void` | Register a validator |
113
+ | `memorio.validate(path, value)` | `string`, `any` | `{ valid, errors? }` | Manually validate a value |
114
+ | `memorio.unregisterSchema(path)` | `string` | `boolean` | Remove a registered schema |
115
+ | `memorio.listSchemas()` | none | `string[]` | List all registered paths |
116
+ | `memorio.registerSchema()` is also importable | `registerSchema` | named export | same function |
117
+
118
+ ---
119
+
120
+ ## Combine with Typed Stores
121
+
122
+ Schema validation gives you **runtime** safety; typed stores give you **compile-time** safety. Use both for full coverage:
123
+
124
+ ```typescript
125
+ import 'memorio'
126
+
127
+ interface AppState {
128
+ user: { name: string; email: string; age: number }
129
+ theme: 'light' | 'dark'
130
+ }
131
+
132
+ const app = memorio.typed<AppState>()
133
+
134
+ memorio.registerSchema('user', {
135
+ type: 'object',
136
+ required: ['name', 'email'],
137
+ properties: {
138
+ name: { type: 'string', min: 1 },
139
+ email: { type: 'string', pattern: /^[^@]+@[^@]+$/ },
140
+ age: { type: 'number', min: 0, max: 150 }
141
+ }
142
+ })
143
+
144
+ app.user = { name: '', email: 'bad' } // ❌ TypeScript: age missing
145
+ // ❌ Runtime: missing required fields
146
+ app.user = { name: 'Sara', email: 'ok', age: 30 } // ✅ both checks pass
147
+ ```
148
+
149
+ See [Typed Stores](TYPED.md) for compile-time type safety.
150
+
151
+ ---
152
+
153
+ ## How it works
154
+
155
+ 1. When you call `registerSchema(path, schema)`, the schema is stored in an internal `Map`.
156
+ 2. On every `state.set` operation, the proxy's `set` trap computes the full path (e.g. `'user.name'`).
157
+ 3. If a schema is registered for that path, the value is validated.
158
+ 4. If validation fails, the write is rejected (`return false`), and an error is logged via `console.error` (when `memorio.debug = true`) or `console.debug` (via the internal `message` helper).
159
+ 5. If no schema is registered, the write proceeds normally.
160
+
161
+ The validation adds negligible overhead when no schemas are registered (a single `Map` lookup that returns `undefined`).
162
+
163
+ ---
164
+
165
+ ## Limitations
166
+
167
+ - Schema validation hooks into the `state` proxy only. `store`, `session`, and `cache` are not validated (they use separate storage). Use `validate()` before writing to other modules.
168
+ - Path matching is **exact**: `registerSchema('user')` guards `state.user = ...`, but does **not** recursively validate `state.user.name = 'new'`. Register schemas at each path you need to guard.
169
+ - The schema system is not a replacement for server-side validation. It protects against accidental misuse and provides defense-in-depth in the browser.
@@ -310,7 +310,7 @@ If you discover a security vulnerability in Memorio:
310
310
 
311
311
  ---
312
312
 
313
- ### v2.7.0 Previous
313
+ ### v2.7.0 - Previous
314
314
 
315
315
  - ✅ Added `crypto.getRandomValues()` fallback
316
316
  - ✅ Added key length validation (512 chars)