@directive-run/knowledge 0.4.2 → 0.7.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.
- package/LICENSE +1 -1
- package/core/anti-patterns.md +10 -26
- package/core/core-patterns.md +1 -1
- package/core/history.md +344 -0
- package/core/multi-module.md +1 -1
- package/core/react-adapter.md +1 -1
- package/core/system-api.md +115 -1
- package/examples/ab-testing.ts +1 -1
- package/examples/ai-checkpoint.ts +1 -1
- package/examples/ai-guardrails.ts +1 -1
- package/examples/batch-resolver.ts +1 -1
- package/examples/checkers.ts +5 -2
- package/examples/contact-form.ts +3 -3
- package/examples/counter.ts +2 -1
- package/examples/dashboard-loader.ts +4 -4
- package/examples/debounce-constraints.ts +2 -10
- package/examples/error-boundaries.ts +1 -1
- package/examples/form-wizard.ts +7 -7
- package/examples/fraud-analysis.ts +2 -6
- package/examples/multi-module.ts +1 -0
- package/examples/newsletter.ts +3 -3
- package/examples/pagination.ts +1 -1
- package/examples/permissions.ts +2 -2
- package/examples/provider-routing.ts +1 -1
- package/examples/shopping-cart.ts +13 -16
- package/examples/sudoku.ts +18 -11
- package/examples/time-machine.ts +2 -1
- package/examples/topic-guard.ts +3 -3
- package/examples/url-sync.ts +1 -1
- package/package.json +3 -3
- package/core/time-travel.md +0 -238
package/LICENSE
CHANGED
package/core/anti-patterns.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
19 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
|
|
4
4
|
|
|
5
5
|
## 1. Unnecessary Type Casting on Facts/Derivations
|
|
6
6
|
|
|
@@ -155,23 +155,7 @@ const status = facts.auth.status;
|
|
|
155
155
|
const token = facts.auth.token;
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
## 11.
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
// WRONG – there is no builder pattern
|
|
162
|
-
const mod = module("counter")
|
|
163
|
-
.schema({ count: t.number() })
|
|
164
|
-
.build();
|
|
165
|
-
|
|
166
|
-
// CORRECT – use createModule with object syntax
|
|
167
|
-
const mod = createModule("counter", {
|
|
168
|
-
schema: {
|
|
169
|
-
facts: { count: t.number() },
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## 12. Returning Data from Resolvers
|
|
158
|
+
## 11. Returning Data from Resolvers
|
|
175
159
|
|
|
176
160
|
```typescript
|
|
177
161
|
// WRONG – resolvers return void, not data
|
|
@@ -188,7 +172,7 @@ resolve: async (req, context) => {
|
|
|
188
172
|
},
|
|
189
173
|
```
|
|
190
174
|
|
|
191
|
-
##
|
|
175
|
+
## 12. Async Logic in `init`
|
|
192
176
|
|
|
193
177
|
```typescript
|
|
194
178
|
// WRONG – init is synchronous, facts assignment only
|
|
@@ -211,7 +195,7 @@ constraints: {
|
|
|
211
195
|
},
|
|
212
196
|
```
|
|
213
197
|
|
|
214
|
-
##
|
|
198
|
+
## 13. Missing `settle()` After `start()`
|
|
215
199
|
|
|
216
200
|
```typescript
|
|
217
201
|
// WRONG – constraints fire on start, resolvers are async
|
|
@@ -224,7 +208,7 @@ await system.settle();
|
|
|
224
208
|
console.log(system.facts.data); // Resolved
|
|
225
209
|
```
|
|
226
210
|
|
|
227
|
-
##
|
|
211
|
+
## 14. Missing `crossModuleDeps` Declaration
|
|
228
212
|
|
|
229
213
|
```typescript
|
|
230
214
|
// WRONG – accessing auth facts without declaring dependency
|
|
@@ -251,7 +235,7 @@ const dataModule = createModule("data", {
|
|
|
251
235
|
});
|
|
252
236
|
```
|
|
253
237
|
|
|
254
|
-
##
|
|
238
|
+
## 15. String Literal for `require`
|
|
255
239
|
|
|
256
240
|
```typescript
|
|
257
241
|
// WRONG – require must be an object with type property
|
|
@@ -271,7 +255,7 @@ constraints: {
|
|
|
271
255
|
},
|
|
272
256
|
```
|
|
273
257
|
|
|
274
|
-
##
|
|
258
|
+
## 16. Passthrough Derivations
|
|
275
259
|
|
|
276
260
|
```typescript
|
|
277
261
|
// WRONG – derivation just returns a fact value unchanged
|
|
@@ -283,7 +267,7 @@ derive: {
|
|
|
283
267
|
// system.facts.count instead of system.derive.count
|
|
284
268
|
```
|
|
285
269
|
|
|
286
|
-
##
|
|
270
|
+
## 17. Deep Import Paths
|
|
287
271
|
|
|
288
272
|
```typescript
|
|
289
273
|
// WRONG – internal module paths are not public API
|
|
@@ -297,7 +281,7 @@ import { createModule, createSystem } from "@directive-run/core";
|
|
|
297
281
|
import { loggingPlugin } from "@directive-run/core/plugins";
|
|
298
282
|
```
|
|
299
283
|
|
|
300
|
-
##
|
|
284
|
+
## 18. Async `when()` Without `deps`
|
|
301
285
|
|
|
302
286
|
```typescript
|
|
303
287
|
// WRONG – async constraints need explicit deps for tracking
|
|
@@ -328,7 +312,7 @@ constraints: {
|
|
|
328
312
|
},
|
|
329
313
|
```
|
|
330
314
|
|
|
331
|
-
##
|
|
315
|
+
## 19. No Error Handling on Failing Resolvers
|
|
332
316
|
|
|
333
317
|
```typescript
|
|
334
318
|
// WRONG – unhandled errors crash the system
|
package/core/core-patterns.md
CHANGED
|
@@ -108,7 +108,7 @@ import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
|
|
|
108
108
|
const system = createSystem({
|
|
109
109
|
module: myModule,
|
|
110
110
|
plugins: [loggingPlugin(), devtoolsPlugin()],
|
|
111
|
-
|
|
111
|
+
history: { maxSnapshots: 100 },
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
// Multi-module – namespaced access: system.facts.auth.token
|
package/core/history.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# History & Snapshots
|
|
2
|
+
|
|
3
|
+
Directive records fact changes as snapshots, enabling undo/redo, replay, export/import, and changeset grouping.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "Should I enable history?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
What's the use case?
|
|
9
|
+
├── Debugging during development → Yes, enable with maxSnapshots cap
|
|
10
|
+
├── Production app → No, disable for performance
|
|
11
|
+
├── Bug reproduction → Enable, use export() to share
|
|
12
|
+
├── Testing → Usually no – use assertFact/assertDerivation instead
|
|
13
|
+
└── Demo / presentation → Yes, great for showing state changes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Enabling History
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createSystem } from "@directive-run/core";
|
|
20
|
+
|
|
21
|
+
const system = createSystem({
|
|
22
|
+
module: myModule,
|
|
23
|
+
history: {
|
|
24
|
+
maxSnapshots: 100, // Cap memory usage (default: 100)
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
History is disabled by default. When disabled, `system.history` is `null`. When enabled, snapshots are recorded automatically after every fact mutation.
|
|
30
|
+
|
|
31
|
+
## Filtering Snapshot Events
|
|
32
|
+
|
|
33
|
+
By default every event that changes facts creates a snapshot. Use `history.snapshotEvents` on your module to limit which events create snapshots — keeps undo/redo clean by excluding UI-only events like selection or timer ticks.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const game = createModule("game", {
|
|
37
|
+
schema: gameSchema,
|
|
38
|
+
|
|
39
|
+
// Only these events appear in undo/redo history.
|
|
40
|
+
// Omit to snapshot ALL events (the default).
|
|
41
|
+
history: {
|
|
42
|
+
snapshotEvents: [
|
|
43
|
+
"inputNumber",
|
|
44
|
+
"requestHint",
|
|
45
|
+
"newGame",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
events: {
|
|
50
|
+
tick: (facts) => { /* no snapshot */ },
|
|
51
|
+
selectCell: (facts, { index }) => { /* no snapshot */ },
|
|
52
|
+
inputNumber: (facts, { value }) => { /* creates snapshot */ },
|
|
53
|
+
requestHint: (facts) => { /* creates snapshot */ },
|
|
54
|
+
newGame: (facts, { difficulty }) => { /* creates snapshot */ },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Filtering by Module
|
|
60
|
+
|
|
61
|
+
In a multi-module system, control which modules create snapshots at the system level:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const system = createSystem({
|
|
65
|
+
modules: { ui: uiModule, game: gameModule },
|
|
66
|
+
history: {
|
|
67
|
+
maxSnapshots: 100,
|
|
68
|
+
snapshotModules: ["game"], // Only game events create snapshots
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Rules:**
|
|
74
|
+
- `snapshotEvents` omitted → all events snapshot (per module)
|
|
75
|
+
- `snapshotModules` omitted → all modules snapshot (per system)
|
|
76
|
+
- Both provided → they intersect (module must be in `snapshotModules` AND event in `snapshotEvents`)
|
|
77
|
+
- Direct fact mutations (`system.facts.x = 5`) always snapshot regardless of filtering
|
|
78
|
+
- `snapshotEvents` entries are type-checked against schema events
|
|
79
|
+
|
|
80
|
+
## Core API: `system.history` (`HistoryAPI`)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const history = system.history; // HistoryAPI | null
|
|
84
|
+
|
|
85
|
+
if (history) {
|
|
86
|
+
// Read-only state
|
|
87
|
+
history.snapshots; // Snapshot[] — all recorded snapshots
|
|
88
|
+
history.currentIndex; // number — position in the snapshot array
|
|
89
|
+
history.isPaused; // boolean — whether recording is paused
|
|
90
|
+
|
|
91
|
+
// Navigation
|
|
92
|
+
history.goBack(); // Step backward one snapshot (changeset-aware)
|
|
93
|
+
history.goBack(3); // Step backward 3 snapshots
|
|
94
|
+
history.goForward(); // Step forward one snapshot (changeset-aware)
|
|
95
|
+
history.goForward(3); // Step forward 3 snapshots
|
|
96
|
+
history.goTo(snapshotId); // Jump to a specific snapshot by its ID
|
|
97
|
+
history.replay(); // Jump to the first snapshot
|
|
98
|
+
|
|
99
|
+
// Export / Import (JSON strings)
|
|
100
|
+
history.export(); // Serialize entire history to JSON string
|
|
101
|
+
history.import(json); // Restore history from JSON string
|
|
102
|
+
|
|
103
|
+
// Changesets — group multiple snapshots into one undo/redo unit
|
|
104
|
+
history.beginChangeset("Move piece");
|
|
105
|
+
// ... mutations happen ...
|
|
106
|
+
history.endChangeset();
|
|
107
|
+
|
|
108
|
+
// Recording control
|
|
109
|
+
history.pause(); // Temporarily stop recording snapshots
|
|
110
|
+
history.resume(); // Resume recording
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Snapshot Structure
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
interface Snapshot {
|
|
118
|
+
id: number; // Auto-incrementing identifier
|
|
119
|
+
timestamp: number; // When captured (Date.now())
|
|
120
|
+
facts: Record<string, unknown>; // Complete copy of all fact values
|
|
121
|
+
trigger: string; // What caused this snapshot (e.g., "fact:count")
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Framework Hook: `useHistory` (`HistoryState`)
|
|
126
|
+
|
|
127
|
+
Each framework adapter provides a reactive `useHistory` hook that re-renders on snapshot changes. Returns `null` when history is disabled, otherwise a `HistoryState` object that wraps the core API with convenience properties.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interface SnapshotMeta {
|
|
131
|
+
id: number; // Snapshot identifier
|
|
132
|
+
timestamp: number; // When captured
|
|
133
|
+
trigger: string; // What caused this snapshot
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface HistoryState {
|
|
137
|
+
// Convenience booleans (not on core API)
|
|
138
|
+
canGoBack: boolean; // True when currentIndex > 0
|
|
139
|
+
canGoForward: boolean; // True when currentIndex < totalSnapshots - 1
|
|
140
|
+
currentIndex: number;
|
|
141
|
+
totalSnapshots: number;
|
|
142
|
+
|
|
143
|
+
// Snapshot access (metadata only — keeps re-renders cheap)
|
|
144
|
+
snapshots: SnapshotMeta[];
|
|
145
|
+
getSnapshotFacts: (id: number) => Record<string, unknown> | null;
|
|
146
|
+
|
|
147
|
+
// Navigation
|
|
148
|
+
goTo: (snapshotId: number) => void;
|
|
149
|
+
goBack: (steps: number) => void;
|
|
150
|
+
goForward: (steps: number) => void;
|
|
151
|
+
replay: () => void;
|
|
152
|
+
|
|
153
|
+
// Session persistence
|
|
154
|
+
exportSession: () => string; // Wraps history.export()
|
|
155
|
+
importSession: (json: string) => void; // Wraps history.import()
|
|
156
|
+
|
|
157
|
+
// Changesets
|
|
158
|
+
beginChangeset: (label: string) => void;
|
|
159
|
+
endChangeset: () => void;
|
|
160
|
+
|
|
161
|
+
// Recording control
|
|
162
|
+
isPaused: boolean;
|
|
163
|
+
pause: () => void;
|
|
164
|
+
resume: () => void;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### React Example
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { useHistory } from "@directive-run/react";
|
|
172
|
+
|
|
173
|
+
function HistoryToolbar() {
|
|
174
|
+
const history = useHistory(system);
|
|
175
|
+
if (!history) return null;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div>
|
|
179
|
+
<button onClick={() => history.goBack()} disabled={!history.canGoBack}>Undo</button>
|
|
180
|
+
<button onClick={() => history.goForward()} disabled={!history.canGoForward}>Redo</button>
|
|
181
|
+
<span>{history.currentIndex + 1} / {history.totalSnapshots}</span>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Undo/Redo Pattern
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const system = createSystem({
|
|
191
|
+
module: editorModule,
|
|
192
|
+
history: { maxSnapshots: 200 },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
system.start();
|
|
196
|
+
|
|
197
|
+
// User makes changes
|
|
198
|
+
system.facts.text = "Hello";
|
|
199
|
+
system.facts.text = "Hello, world";
|
|
200
|
+
system.facts.text = "Hello, world!";
|
|
201
|
+
|
|
202
|
+
const history = system.history!;
|
|
203
|
+
|
|
204
|
+
// Undo (one step back)
|
|
205
|
+
history.goBack();
|
|
206
|
+
console.log(system.facts.text); // "Hello, world"
|
|
207
|
+
|
|
208
|
+
history.goBack();
|
|
209
|
+
console.log(system.facts.text); // "Hello"
|
|
210
|
+
|
|
211
|
+
// Redo (one step forward)
|
|
212
|
+
history.goForward();
|
|
213
|
+
console.log(system.facts.text); // "Hello, world"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## How Snapshots Work
|
|
217
|
+
|
|
218
|
+
Snapshots are taken **once per reconciliation cycle**, not per individual fact change. All synchronous fact mutations within the same event handler batch into a single snapshot. One `goBack()` reverts all changes from that event.
|
|
219
|
+
|
|
220
|
+
## Changesets: Grouping Multiple Events
|
|
221
|
+
|
|
222
|
+
Changesets group snapshots from **multiple separate events** into one undo/redo unit. Useful when a single user action triggers a sequence of events.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const history = system.history!;
|
|
226
|
+
|
|
227
|
+
// Two separate events = two snapshots
|
|
228
|
+
system.events.pickUp({ index: 0 }); // → snapshot 1
|
|
229
|
+
system.events.place({ from: 0, to: 1 }); // → snapshot 2
|
|
230
|
+
// goBack() only reverts the place — need two goBack() calls
|
|
231
|
+
|
|
232
|
+
// With changeset — grouped into one undo unit
|
|
233
|
+
history.beginChangeset("Move piece");
|
|
234
|
+
system.events.pickUp({ index: 0 }); // → snapshot 1 ─┐
|
|
235
|
+
system.events.place({ from: 0, to: 1 }); // → snapshot 2 ─┘ grouped
|
|
236
|
+
history.endChangeset();
|
|
237
|
+
// One goBack() reverts both events
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Also works for direct fact mutations across separate synchronous blocks:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
history.beginChangeset("Update user");
|
|
244
|
+
system.facts.firstName = "Alice";
|
|
245
|
+
system.facts.lastName = "Smith";
|
|
246
|
+
// These batch into one snapshot synchronously, but changeset
|
|
247
|
+
// still works if an async gap separates them
|
|
248
|
+
history.endChangeset();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Always close your changesets. If you forget `endChangeset()`, all subsequent mutations get grouped into the same changeset.
|
|
252
|
+
|
|
253
|
+
## Exporting and Importing History
|
|
254
|
+
|
|
255
|
+
Serialize the full snapshot history for bug reports or debugging.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const history = system.history!;
|
|
259
|
+
|
|
260
|
+
// Export — returns a JSON string
|
|
261
|
+
const exported = history.export();
|
|
262
|
+
localStorage.setItem("debug-session", exported);
|
|
263
|
+
|
|
264
|
+
// Import — restore from a JSON string
|
|
265
|
+
const saved = localStorage.getItem("debug-session");
|
|
266
|
+
if (saved) {
|
|
267
|
+
history.import(saved);
|
|
268
|
+
|
|
269
|
+
// Step through the user's exact state sequence
|
|
270
|
+
history.goTo(0); // First snapshot
|
|
271
|
+
history.goTo(5); // When the bug occurred
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Performance: maxSnapshots
|
|
276
|
+
|
|
277
|
+
Every fact mutation creates a snapshot. Cap the number to control memory:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// Low memory — keeps last 20 snapshots, discards oldest
|
|
281
|
+
history: { maxSnapshots: 20 },
|
|
282
|
+
|
|
283
|
+
// Development — generous cap for deep debugging
|
|
284
|
+
history: { maxSnapshots: 500 },
|
|
285
|
+
|
|
286
|
+
// Default if not specified
|
|
287
|
+
history: true, // maxSnapshots defaults to 100
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
When the cap is reached, the oldest snapshot is discarded (ring buffer / FIFO).
|
|
291
|
+
|
|
292
|
+
## Common Mistakes
|
|
293
|
+
|
|
294
|
+
### Enabling history in production
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// WRONG — snapshots consume memory on every mutation
|
|
298
|
+
const system = createSystem({
|
|
299
|
+
module: myModule,
|
|
300
|
+
history: true,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// CORRECT — gate on environment
|
|
304
|
+
const system = createSystem({
|
|
305
|
+
module: myModule,
|
|
306
|
+
history: process.env.NODE_ENV === "development"
|
|
307
|
+
? { maxSnapshots: 100 }
|
|
308
|
+
: false,
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Forgetting to end a changeset
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// WRONG — changeset never closed, all subsequent mutations are grouped
|
|
316
|
+
history.beginChangeset("update");
|
|
317
|
+
system.facts.name = "Alice";
|
|
318
|
+
// ... forgot endChangeset()
|
|
319
|
+
system.facts.unrelated = true; // Still part of the changeset!
|
|
320
|
+
|
|
321
|
+
// CORRECT — always close changesets
|
|
322
|
+
history.beginChangeset("update");
|
|
323
|
+
system.facts.name = "Alice";
|
|
324
|
+
history.endChangeset();
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Accessing history when disabled
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// WRONG — history not enabled, system.history is null
|
|
331
|
+
const system = createSystem({ module: myModule });
|
|
332
|
+
system.history.goBack(); // TypeError!
|
|
333
|
+
|
|
334
|
+
// CORRECT — check before using
|
|
335
|
+
if (system.history) {
|
|
336
|
+
system.history.goBack();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Or enable it
|
|
340
|
+
const system = createSystem({
|
|
341
|
+
module: myModule,
|
|
342
|
+
history: true,
|
|
343
|
+
});
|
|
344
|
+
```
|
package/core/multi-module.md
CHANGED
package/core/react-adapter.md
CHANGED
|
@@ -129,7 +129,7 @@ function GameBoard() {
|
|
|
129
129
|
// System created on mount, destroyed on unmount
|
|
130
130
|
const gameSystem = useSystem({
|
|
131
131
|
module: gameModule,
|
|
132
|
-
|
|
132
|
+
history: true,
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
const score = useSelector(gameSystem, (s) => s.facts.score);
|
package/core/system-api.md
CHANGED
|
@@ -27,7 +27,7 @@ import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
|
|
|
27
27
|
const system = createSystem({
|
|
28
28
|
module: myModule,
|
|
29
29
|
plugins: [loggingPlugin(), devtoolsPlugin()],
|
|
30
|
-
|
|
30
|
+
history: { maxSnapshots: 100 },
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// Multi-module – namespaced access
|
|
@@ -212,6 +212,120 @@ system.constraints.isDisabled("fetchWhenReady"); // true
|
|
|
212
212
|
system.constraints.enable("fetchWhenReady");
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
## SSR and Hydration
|
|
216
|
+
|
|
217
|
+
Four mechanisms for populating a system with external state:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// 1. initialFacts – simplest, at construction time
|
|
221
|
+
const system = createSystem({
|
|
222
|
+
module: myModule,
|
|
223
|
+
initialFacts: { userId: "user-1", name: "Alice" },
|
|
224
|
+
});
|
|
225
|
+
system.start();
|
|
226
|
+
|
|
227
|
+
// 2. system.hydrate() – async, before start()
|
|
228
|
+
const system = createSystem({ module: myModule });
|
|
229
|
+
await system.hydrate(async () => {
|
|
230
|
+
const res = await fetch('/api/state');
|
|
231
|
+
|
|
232
|
+
return res.json();
|
|
233
|
+
});
|
|
234
|
+
system.start();
|
|
235
|
+
|
|
236
|
+
// 3. system.restore() – sync, applies facts from a snapshot
|
|
237
|
+
const system = createSystem({ module: myModule });
|
|
238
|
+
system.restore(serverSnapshot);
|
|
239
|
+
system.start();
|
|
240
|
+
|
|
241
|
+
// 4. DirectiveHydrator + useHydratedSystem (React only)
|
|
242
|
+
// Server: getDistributableSnapshot() → serialize
|
|
243
|
+
// Client: <DirectiveHydrator snapshot={s}><App /></DirectiveHydrator>
|
|
244
|
+
// useHydratedSystem(module) inside App
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### SSR Lifecycle
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Server: create → start → settle → snapshot → destroy
|
|
251
|
+
const system = createSystem({
|
|
252
|
+
module: pageModule,
|
|
253
|
+
initialFacts: { userId: req.user.id },
|
|
254
|
+
});
|
|
255
|
+
system.start();
|
|
256
|
+
await system.settle(5000); // Throws on timeout
|
|
257
|
+
const snapshot = system.getSnapshot();
|
|
258
|
+
system.stop();
|
|
259
|
+
system.destroy();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Snapshot Types
|
|
263
|
+
|
|
264
|
+
- `SystemSnapshot` – facts only, used with `getSnapshot()` / `restore()`
|
|
265
|
+
- `DistributableSnapshot` – facts + derivations + metadata + TTL, used with `getDistributableSnapshot()` and `DirectiveHydrator`
|
|
266
|
+
|
|
267
|
+
### Avoiding Singletons
|
|
268
|
+
|
|
269
|
+
Never use module-level systems in SSR – they share state across concurrent requests. Always create a fresh system per request and destroy it when done.
|
|
270
|
+
|
|
271
|
+
## Runtime Dynamics
|
|
272
|
+
|
|
273
|
+
All four subsystems (constraints, resolvers, derivations, effects) share a uniform dynamic definition interface:
|
|
274
|
+
|
|
275
|
+
### Enable / Disable (Constraints & Effects only)
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
system.constraints.disable("id");
|
|
279
|
+
system.constraints.enable("id");
|
|
280
|
+
system.constraints.isDisabled("id");
|
|
281
|
+
|
|
282
|
+
system.effects.disable("id");
|
|
283
|
+
system.effects.enable("id");
|
|
284
|
+
system.effects.isEnabled("id");
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Register / Assign / Unregister (All 4 subsystems)
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Register a new definition at runtime
|
|
291
|
+
system.constraints.register("id", { when: ..., require: ... });
|
|
292
|
+
system.resolvers.register("id", { requirement: "TYPE", resolve: ... });
|
|
293
|
+
system.derive.register("id", (facts) => facts.count * 3);
|
|
294
|
+
system.effects.register("id", { run: (facts) => { ... } });
|
|
295
|
+
|
|
296
|
+
// Override an existing definition
|
|
297
|
+
system.constraints.assign("id", { when: ..., require: ... });
|
|
298
|
+
|
|
299
|
+
// Remove a dynamically registered definition (static = no-op + dev warning)
|
|
300
|
+
system.constraints.unregister("id");
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Semantics: `register` throws if ID exists. `assign` throws if ID doesn't exist. `unregister` on static ID = dev warning, no-op. All three are deferred if called during reconciliation.
|
|
304
|
+
|
|
305
|
+
### Introspection
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
system.constraints.isDynamic("id"); // true if registered at runtime
|
|
309
|
+
system.constraints.listDynamic(); // all dynamic constraint IDs
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### getOriginal / restoreOriginal
|
|
313
|
+
|
|
314
|
+
When `assign()` overrides a static definition, the original is saved:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
system.getOriginal("constraint", "id"); // returns original definition
|
|
318
|
+
system.restoreOriginal("constraint", "id"); // restores it, returns true/false
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Dynamic Module Registration
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
system.registerModule("chat", chatModule); // adds module to running system
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
See docs: https://directive.run/docs/advanced/runtime
|
|
328
|
+
|
|
215
329
|
## Common Mistakes
|
|
216
330
|
|
|
217
331
|
### Reading facts before settling
|
package/examples/ab-testing.ts
CHANGED
package/examples/checkers.ts
CHANGED
|
@@ -43,11 +43,14 @@ import {
|
|
|
43
43
|
import type { CacheStats } from "@directive-run/ai";
|
|
44
44
|
import {
|
|
45
45
|
type CircuitState,
|
|
46
|
-
createAgentMetrics,
|
|
47
46
|
createCircuitBreaker,
|
|
48
47
|
createOTLPExporter,
|
|
49
|
-
createObservability,
|
|
50
48
|
} from "@directive-run/core/plugins";
|
|
49
|
+
// createObservability is alpha (not in bundle) — direct source import
|
|
50
|
+
import {
|
|
51
|
+
createObservability,
|
|
52
|
+
createAgentMetrics,
|
|
53
|
+
} from "../../../packages/core/src/plugins/observability.lab.js";
|
|
51
54
|
import {
|
|
52
55
|
analysisAgent,
|
|
53
56
|
chatAgent,
|
package/examples/contact-form.ts
CHANGED
|
@@ -197,8 +197,8 @@ const contactForm = createModule("contact-form", {
|
|
|
197
197
|
facts.subject !== "" &&
|
|
198
198
|
facts.message.trim().length >= 10,
|
|
199
199
|
|
|
200
|
-
canSubmit: (facts,
|
|
201
|
-
if (!
|
|
200
|
+
canSubmit: (facts, derived) => {
|
|
201
|
+
if (!derived.isValid) {
|
|
202
202
|
return false;
|
|
203
203
|
}
|
|
204
204
|
if (facts.status !== "idle") {
|
|
@@ -322,6 +322,6 @@ const contactForm = createModule("contact-form", {
|
|
|
322
322
|
|
|
323
323
|
export const system = createSystem({
|
|
324
324
|
module: contactForm,
|
|
325
|
-
|
|
325
|
+
trace: true,
|
|
326
326
|
plugins: [devtoolsPlugin({ name: "contact-form" })],
|
|
327
327
|
});
|
package/examples/counter.ts
CHANGED
|
@@ -371,5 +371,6 @@ const numberMatch = createModule("number-match", {
|
|
|
371
371
|
export const system = createSystem({
|
|
372
372
|
module: numberMatch,
|
|
373
373
|
plugins: [devtoolsPlugin({ name: "number-match" })],
|
|
374
|
-
|
|
374
|
+
history: true,
|
|
375
|
+
trace: true,
|
|
375
376
|
});
|
|
@@ -180,10 +180,10 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
180
180
|
return resources.some((r) => r.status === "loading");
|
|
181
181
|
},
|
|
182
182
|
|
|
183
|
-
combinedStatus: (facts,
|
|
184
|
-
const loaded =
|
|
185
|
-
const anyErr =
|
|
186
|
-
const anyLoad =
|
|
183
|
+
combinedStatus: (facts, derived) => {
|
|
184
|
+
const loaded = derived.loadedCount;
|
|
185
|
+
const anyErr = derived.anyError;
|
|
186
|
+
const anyLoad = derived.anyLoading;
|
|
187
187
|
const allIdle = [
|
|
188
188
|
facts.profile,
|
|
189
189
|
facts.preferences,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { createSystem } from "@directive-run/core";
|
|
15
15
|
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
16
|
+
import { el } from "@directive-run/el";
|
|
16
17
|
import {
|
|
17
18
|
debounceSearchModule,
|
|
18
19
|
debounceSearchSchema,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
|
|
25
26
|
const system = createSystem({
|
|
26
27
|
module: debounceSearchModule,
|
|
27
|
-
|
|
28
|
+
trace: true,
|
|
28
29
|
plugins: [devtoolsPlugin({ name: "debounce-constraints" })],
|
|
29
30
|
});
|
|
30
31
|
system.start();
|
|
@@ -85,15 +86,6 @@ const tickInterval = setInterval(() => {
|
|
|
85
86
|
// Sliders
|
|
86
87
|
|
|
87
88
|
|
|
88
|
-
// ============================================================================
|
|
89
|
-
// Helpers
|
|
90
|
-
// ============================================================================
|
|
91
|
-
|
|
92
|
-
function escapeHtml(text: string): string {
|
|
93
|
-
|
|
94
|
-
return div.innerHTML;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
89
|
// ============================================================================
|
|
98
90
|
// Initial Render
|
|
99
91
|
// ============================================================================
|
|
@@ -344,7 +344,7 @@ let currentStrategy: RecoveryStrategy = "retry-later";
|
|
|
344
344
|
|
|
345
345
|
export const system = createSystem({
|
|
346
346
|
module: dashboardModule,
|
|
347
|
-
|
|
347
|
+
trace: true,
|
|
348
348
|
plugins: [perf, devtoolsPlugin({ name: "error-boundaries" })],
|
|
349
349
|
errorBoundary: {
|
|
350
350
|
onResolverError: (_error, resolver) => {
|
package/examples/form-wizard.ts
CHANGED
|
@@ -127,23 +127,23 @@ export const wizardModule = createModule("wizard", {
|
|
|
127
127
|
return facts.plan !== "";
|
|
128
128
|
},
|
|
129
129
|
|
|
130
|
-
currentStepValid: (facts,
|
|
130
|
+
currentStepValid: (facts, derived) => {
|
|
131
131
|
if (facts.currentStep === 0) {
|
|
132
|
-
return
|
|
132
|
+
return derived.step0Valid;
|
|
133
133
|
}
|
|
134
134
|
if (facts.currentStep === 1) {
|
|
135
|
-
return
|
|
135
|
+
return derived.step1Valid;
|
|
136
136
|
}
|
|
137
137
|
if (facts.currentStep === 2) {
|
|
138
|
-
return
|
|
138
|
+
return derived.step2Valid;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
return false;
|
|
142
142
|
},
|
|
143
143
|
|
|
144
|
-
canAdvance: (facts,
|
|
144
|
+
canAdvance: (facts, derived) => {
|
|
145
145
|
return (
|
|
146
|
-
|
|
146
|
+
derived.currentStepValid && facts.currentStep < facts.totalSteps - 1
|
|
147
147
|
);
|
|
148
148
|
},
|
|
149
149
|
|
|
@@ -329,7 +329,7 @@ export const system = createSystem({
|
|
|
329
329
|
wizard: wizardModule,
|
|
330
330
|
validation: validationModule,
|
|
331
331
|
},
|
|
332
|
-
|
|
332
|
+
trace: true,
|
|
333
333
|
plugins: [
|
|
334
334
|
devtoolsPlugin({ name: "form-wizard" }),
|
|
335
335
|
persistencePlugin({
|
|
@@ -646,12 +646,8 @@ export const fraudAnalysisModule = createModule("fraud", {
|
|
|
646
646
|
export const system = createSystem({
|
|
647
647
|
module: fraudAnalysisModule,
|
|
648
648
|
plugins: [devtoolsPlugin({ name: "fraud-analysis", panel: true })],
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
maxSnapshots: 50,
|
|
652
|
-
runHistory: true,
|
|
653
|
-
maxRuns: 100,
|
|
654
|
-
},
|
|
649
|
+
history: { maxSnapshots: 50 },
|
|
650
|
+
trace: { maxRuns: 100 },
|
|
655
651
|
});
|
|
656
652
|
|
|
657
653
|
// ============================================================================
|
package/examples/multi-module.ts
CHANGED
package/examples/newsletter.ts
CHANGED
|
@@ -102,8 +102,8 @@ const newsletter = createModule("newsletter", {
|
|
|
102
102
|
|
|
103
103
|
isValid: (facts) => EMAIL_REGEX.test(facts.email),
|
|
104
104
|
|
|
105
|
-
canSubmit: (facts,
|
|
106
|
-
if (!
|
|
105
|
+
canSubmit: (facts, derived) => {
|
|
106
|
+
if (!derived.isValid) {
|
|
107
107
|
return false;
|
|
108
108
|
}
|
|
109
109
|
if (facts.status !== "idle") {
|
|
@@ -209,6 +209,6 @@ const newsletter = createModule("newsletter", {
|
|
|
209
209
|
|
|
210
210
|
export const system = createSystem({
|
|
211
211
|
module: newsletter,
|
|
212
|
-
|
|
212
|
+
trace: true,
|
|
213
213
|
plugins: [devtoolsPlugin({ name: "newsletter" })],
|
|
214
214
|
});
|
package/examples/pagination.ts
CHANGED
|
@@ -255,6 +255,6 @@ export const listModule = createModule("list", {
|
|
|
255
255
|
|
|
256
256
|
export const system = createSystem({
|
|
257
257
|
modules: { filters: filtersModule, list: listModule },
|
|
258
|
-
|
|
258
|
+
trace: true,
|
|
259
259
|
plugins: [loggingPlugin(), devtoolsPlugin({ name: "pagination" })],
|
|
260
260
|
});
|
package/examples/permissions.ts
CHANGED
|
@@ -138,7 +138,7 @@ export const permissionsModule = createModule("permissions", {
|
|
|
138
138
|
canManageUsers: (facts) => facts.self.permissions.includes("users.manage"),
|
|
139
139
|
canViewAnalytics: (facts) =>
|
|
140
140
|
facts.self.permissions.includes("analytics.view"),
|
|
141
|
-
isAdmin: (_facts,
|
|
141
|
+
isAdmin: (_facts, derived) => derived.canManageUsers,
|
|
142
142
|
permissionCount: (facts) => facts.self.permissions.length,
|
|
143
143
|
},
|
|
144
144
|
|
|
@@ -322,6 +322,6 @@ export const system = createSystem({
|
|
|
322
322
|
permissions: permissionsModule,
|
|
323
323
|
content: contentModule,
|
|
324
324
|
},
|
|
325
|
-
|
|
325
|
+
trace: true,
|
|
326
326
|
plugins: [devtoolsPlugin({ name: "permissions" })],
|
|
327
327
|
});
|
|
@@ -187,23 +187,23 @@ export const cartModule = createModule("cart", {
|
|
|
187
187
|
return facts.self.items.length === 0;
|
|
188
188
|
},
|
|
189
189
|
|
|
190
|
-
discount: (facts,
|
|
191
|
-
const sub =
|
|
190
|
+
discount: (facts, derived) => {
|
|
191
|
+
const sub = derived.subtotal;
|
|
192
192
|
|
|
193
193
|
return sub * (facts.self.couponDiscount / 100);
|
|
194
194
|
},
|
|
195
195
|
|
|
196
|
-
tax: (facts,
|
|
197
|
-
const sub =
|
|
198
|
-
const disc =
|
|
196
|
+
tax: (facts, derived) => {
|
|
197
|
+
const sub = derived.subtotal;
|
|
198
|
+
const disc = derived.discount;
|
|
199
199
|
|
|
200
200
|
return (sub - disc) * 0.08;
|
|
201
201
|
},
|
|
202
202
|
|
|
203
|
-
total: (_facts,
|
|
204
|
-
const sub =
|
|
205
|
-
const disc =
|
|
206
|
-
const tx =
|
|
203
|
+
total: (_facts, derived) => {
|
|
204
|
+
const sub = derived.subtotal;
|
|
205
|
+
const disc = derived.discount;
|
|
206
|
+
const tx = derived.tax;
|
|
207
207
|
|
|
208
208
|
return sub - disc + tx;
|
|
209
209
|
},
|
|
@@ -214,8 +214,8 @@ export const cartModule = createModule("cart", {
|
|
|
214
214
|
);
|
|
215
215
|
},
|
|
216
216
|
|
|
217
|
-
freeShipping: (_facts,
|
|
218
|
-
const sub =
|
|
217
|
+
freeShipping: (_facts, derived) => {
|
|
218
|
+
const sub = derived.subtotal;
|
|
219
219
|
|
|
220
220
|
return sub >= 75;
|
|
221
221
|
},
|
|
@@ -414,9 +414,6 @@ export const system = createSystem({
|
|
|
414
414
|
auth: authModule,
|
|
415
415
|
},
|
|
416
416
|
plugins: [devtoolsPlugin({ name: "shopping-cart", panel: true })],
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
maxSnapshots: 50,
|
|
420
|
-
runHistory: true,
|
|
421
|
-
},
|
|
417
|
+
history: { maxSnapshots: 50 },
|
|
418
|
+
trace: true,
|
|
422
419
|
});
|
package/examples/sudoku.ts
CHANGED
|
@@ -109,7 +109,14 @@ export const sudokuSchema = {
|
|
|
109
109
|
|
|
110
110
|
export const sudokuGame = createModule("sudoku", {
|
|
111
111
|
schema: sudokuSchema,
|
|
112
|
-
|
|
112
|
+
history: {
|
|
113
|
+
snapshotEvents: [
|
|
114
|
+
"inputNumber",
|
|
115
|
+
"toggleNote",
|
|
116
|
+
"requestHint",
|
|
117
|
+
"newGame",
|
|
118
|
+
],
|
|
119
|
+
},
|
|
113
120
|
|
|
114
121
|
init: (facts) => {
|
|
115
122
|
const { puzzle, solution } = generatePuzzle("easy");
|
|
@@ -147,10 +154,10 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
147
154
|
return findConflicts(facts.grid);
|
|
148
155
|
},
|
|
149
156
|
|
|
150
|
-
conflictIndices: (facts,
|
|
157
|
+
conflictIndices: (facts, derived) => {
|
|
151
158
|
const indices = new Set<number>();
|
|
152
159
|
const givens = facts.givens;
|
|
153
|
-
for (const c of
|
|
160
|
+
for (const c of derived.conflicts) {
|
|
154
161
|
// Only highlight player-placed cells, not givens
|
|
155
162
|
if (!givens.has(c.index)) {
|
|
156
163
|
indices.add(c.index);
|
|
@@ -160,8 +167,8 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
160
167
|
return indices;
|
|
161
168
|
},
|
|
162
169
|
|
|
163
|
-
hasConflicts: (_facts,
|
|
164
|
-
return
|
|
170
|
+
hasConflicts: (_facts, derived) => {
|
|
171
|
+
return derived.conflicts.length > 0;
|
|
165
172
|
},
|
|
166
173
|
|
|
167
174
|
filledCount: (facts) => {
|
|
@@ -176,16 +183,16 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
176
183
|
return count;
|
|
177
184
|
},
|
|
178
185
|
|
|
179
|
-
progress: (_facts,
|
|
180
|
-
return Math.round((
|
|
186
|
+
progress: (_facts, derived) => {
|
|
187
|
+
return Math.round((derived.filledCount / 81) * 100);
|
|
181
188
|
},
|
|
182
189
|
|
|
183
190
|
isComplete: (facts) => {
|
|
184
191
|
return isBoardComplete(facts.grid);
|
|
185
192
|
},
|
|
186
193
|
|
|
187
|
-
isSolved: (_facts,
|
|
188
|
-
return
|
|
194
|
+
isSolved: (_facts, derived) => {
|
|
195
|
+
return derived.isComplete && !derived.hasConflicts;
|
|
189
196
|
},
|
|
190
197
|
|
|
191
198
|
selectedPeers: (facts) => {
|
|
@@ -206,8 +213,8 @@ export const sudokuGame = createModule("sudoku", {
|
|
|
206
213
|
return facts.grid[sel];
|
|
207
214
|
},
|
|
208
215
|
|
|
209
|
-
sameValueIndices: (facts,
|
|
210
|
-
const val =
|
|
216
|
+
sameValueIndices: (facts, derived) => {
|
|
217
|
+
const val = derived.highlightValue;
|
|
211
218
|
if (val === 0) {
|
|
212
219
|
return new Set<number>();
|
|
213
220
|
}
|
package/examples/time-machine.ts
CHANGED
|
@@ -142,6 +142,7 @@ const canvasModule = createModule("canvas", {
|
|
|
142
142
|
|
|
143
143
|
export const system = createSystem({
|
|
144
144
|
module: canvasModule,
|
|
145
|
-
|
|
145
|
+
history: { maxSnapshots: 200 },
|
|
146
|
+
trace: true,
|
|
146
147
|
plugins: [devtoolsPlugin({ name: "time-machine" })],
|
|
147
148
|
});
|
package/examples/topic-guard.ts
CHANGED
|
@@ -106,12 +106,12 @@ export const topicGuardModule = createModule("topic-guard", {
|
|
|
106
106
|
.length;
|
|
107
107
|
},
|
|
108
108
|
|
|
109
|
-
blockRate: (facts,
|
|
110
|
-
const total =
|
|
109
|
+
blockRate: (facts, derived) => {
|
|
110
|
+
const total = derived.messageCount;
|
|
111
111
|
if (total === 0) {
|
|
112
112
|
return "0%";
|
|
113
113
|
}
|
|
114
|
-
const blocked =
|
|
114
|
+
const blocked = derived.blockedCount;
|
|
115
115
|
const rate = Math.round((blocked / total) * 100);
|
|
116
116
|
|
|
117
117
|
return `${rate}%`;
|
package/examples/url-sync.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directive-run/knowledge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Knowledge files, examples, and validation for Directive — the constraint-driven TypeScript runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jason Comes",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"tsx": "^4.19.2",
|
|
51
51
|
"typescript": "^5.7.2",
|
|
52
52
|
"vitest": "^3.0.0",
|
|
53
|
-
"@directive-run/core": "0.
|
|
54
|
-
"@directive-run/ai": "0.
|
|
53
|
+
"@directive-run/core": "0.7.0",
|
|
54
|
+
"@directive-run/ai": "0.7.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsx scripts/generate-api-skeleton.ts && tsx scripts/extract-examples.ts && tsup",
|
package/core/time-travel.md
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
# Time-Travel Debugging
|
|
2
|
-
|
|
3
|
-
Directive can record every fact change as a snapshot, enabling undo/redo, replay, and state export for bug reports.
|
|
4
|
-
|
|
5
|
-
## Decision Tree: "Should I enable time-travel?"
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
What's the use case?
|
|
9
|
-
├── Debugging during development → Yes, enable with maxSnapshots cap
|
|
10
|
-
├── Production app → No, disable for performance
|
|
11
|
-
├── Bug reproduction → Enable, use exportHistory() to share
|
|
12
|
-
├── Testing → Usually no – use assertFact/assertDerivation instead
|
|
13
|
-
└── Demo / presentation → Yes, great for showing state changes
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Enabling Time-Travel
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
import { createSystem } from "@directive-run/core";
|
|
20
|
-
|
|
21
|
-
const system = createSystem({
|
|
22
|
-
module: myModule,
|
|
23
|
-
debug: {
|
|
24
|
-
timeTravel: true, // Enable snapshot recording
|
|
25
|
-
maxSnapshots: 100, // Cap memory usage (default: 50)
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Time-travel is disabled by default. Snapshots are recorded automatically on every fact mutation.
|
|
31
|
-
|
|
32
|
-
## The TimeTravelAPI
|
|
33
|
-
|
|
34
|
-
Access via `system.debug.timeTravel`:
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
const tt = system.debug.timeTravel;
|
|
38
|
-
|
|
39
|
-
// Navigation
|
|
40
|
-
tt.canUndo(); // boolean – is there a previous snapshot?
|
|
41
|
-
tt.canRedo(); // boolean – is there a next snapshot?
|
|
42
|
-
tt.undo(); // Restore previous snapshot
|
|
43
|
-
tt.redo(); // Restore next snapshot
|
|
44
|
-
|
|
45
|
-
// Direct access
|
|
46
|
-
tt.getSnapshots(); // Array of all snapshots
|
|
47
|
-
tt.goToSnapshot(index); // Jump to a specific snapshot by index
|
|
48
|
-
|
|
49
|
-
// Each snapshot contains:
|
|
50
|
-
// {
|
|
51
|
-
// facts: { ... }, – full fact state at that point
|
|
52
|
-
// timestamp: number, – when the snapshot was taken
|
|
53
|
-
// label?: string, – optional label from changeset
|
|
54
|
-
// changedKeys: string[], – which facts changed
|
|
55
|
-
// }
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Undo/Redo Pattern
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
const system = createSystem({
|
|
62
|
-
module: editorModule,
|
|
63
|
-
debug: { timeTravel: true, maxSnapshots: 200 },
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
system.start();
|
|
67
|
-
|
|
68
|
-
// User makes changes
|
|
69
|
-
system.facts.text = "Hello";
|
|
70
|
-
system.facts.text = "Hello, world";
|
|
71
|
-
system.facts.text = "Hello, world!";
|
|
72
|
-
|
|
73
|
-
// Undo last change
|
|
74
|
-
const tt = system.debug.timeTravel;
|
|
75
|
-
tt.undo();
|
|
76
|
-
console.log(system.facts.text); // "Hello, world"
|
|
77
|
-
|
|
78
|
-
tt.undo();
|
|
79
|
-
console.log(system.facts.text); // "Hello"
|
|
80
|
-
|
|
81
|
-
// Redo
|
|
82
|
-
tt.redo();
|
|
83
|
-
console.log(system.facts.text); // "Hello, world"
|
|
84
|
-
|
|
85
|
-
// Check navigation state
|
|
86
|
-
tt.canUndo(); // true
|
|
87
|
-
tt.canRedo(); // true
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Changesets: Grouping Related Changes
|
|
91
|
-
|
|
92
|
-
Multiple fact mutations can be grouped into a single undoable unit.
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
const tt = system.debug.timeTravel;
|
|
96
|
-
|
|
97
|
-
// Without changeset – each mutation is a separate snapshot
|
|
98
|
-
system.facts.firstName = "Alice";
|
|
99
|
-
system.facts.lastName = "Smith";
|
|
100
|
-
// Two snapshots, two undos needed
|
|
101
|
-
|
|
102
|
-
// With changeset – grouped into one snapshot
|
|
103
|
-
tt.beginChangeset("Update user name");
|
|
104
|
-
system.facts.firstName = "Alice";
|
|
105
|
-
system.facts.lastName = "Smith";
|
|
106
|
-
tt.endChangeset();
|
|
107
|
-
// One snapshot, one undo restores both
|
|
108
|
-
|
|
109
|
-
// Undo reverts the entire changeset
|
|
110
|
-
tt.undo();
|
|
111
|
-
// Both firstName and lastName are restored
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Use changesets for logically related mutations: form submissions, multi-field updates, resolver results.
|
|
115
|
-
|
|
116
|
-
## Exporting and Importing History
|
|
117
|
-
|
|
118
|
-
Serialize the full snapshot history for bug reports or debugging.
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
const tt = system.debug.timeTravel;
|
|
122
|
-
|
|
123
|
-
// Export – returns a serializable object
|
|
124
|
-
const historyData = tt.exportHistory();
|
|
125
|
-
// Send to server, save to file, attach to bug report
|
|
126
|
-
console.log(JSON.stringify(historyData));
|
|
127
|
-
|
|
128
|
-
// Import – restore history from exported data
|
|
129
|
-
tt.importHistory(historyData);
|
|
130
|
-
|
|
131
|
-
// Now you can step through the user's exact state sequence
|
|
132
|
-
tt.goToSnapshot(0); // Start
|
|
133
|
-
tt.goToSnapshot(5); // When the bug occurred
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Snapshot Inspection
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
const tt = system.debug.timeTravel;
|
|
140
|
-
const snapshots = tt.getSnapshots();
|
|
141
|
-
|
|
142
|
-
// Walk through all snapshots
|
|
143
|
-
for (const snap of snapshots) {
|
|
144
|
-
console.log(`[${new Date(snap.timestamp).toISOString()}]`);
|
|
145
|
-
console.log(` Changed: ${snap.changedKeys.join(", ")}`);
|
|
146
|
-
if (snap.label) {
|
|
147
|
-
console.log(` Label: ${snap.label}`);
|
|
148
|
-
}
|
|
149
|
-
console.log(` Facts:`, snap.facts);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Jump to a specific point
|
|
153
|
-
tt.goToSnapshot(3);
|
|
154
|
-
console.log(system.facts); // State as of snapshot 3
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Performance: maxSnapshots
|
|
158
|
-
|
|
159
|
-
Every fact mutation creates a snapshot. Cap the number to control memory:
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
// Low memory – keeps last 20 snapshots, discards oldest
|
|
163
|
-
debug: { timeTravel: true, maxSnapshots: 20 },
|
|
164
|
-
|
|
165
|
-
// Development – generous cap for deep debugging
|
|
166
|
-
debug: { timeTravel: true, maxSnapshots: 500 },
|
|
167
|
-
|
|
168
|
-
// Default if not specified
|
|
169
|
-
debug: { timeTravel: true }, // maxSnapshots defaults to 50
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
When the cap is reached, the oldest snapshot is discarded (FIFO). Redo history beyond the cap is lost.
|
|
173
|
-
|
|
174
|
-
## Common Mistakes
|
|
175
|
-
|
|
176
|
-
### Enabling time-travel in production
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
// WRONG – snapshots consume memory on every mutation
|
|
180
|
-
const system = createSystem({
|
|
181
|
-
module: myModule,
|
|
182
|
-
debug: { timeTravel: true },
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// CORRECT – gate on environment
|
|
186
|
-
const system = createSystem({
|
|
187
|
-
module: myModule,
|
|
188
|
-
debug: {
|
|
189
|
-
timeTravel: process.env.NODE_ENV === "development",
|
|
190
|
-
maxSnapshots: 100,
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Forgetting to end a changeset
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// WRONG – changeset never closed, all subsequent mutations are grouped
|
|
199
|
-
tt.beginChangeset("update");
|
|
200
|
-
system.facts.name = "Alice";
|
|
201
|
-
// ... forgot endChangeset()
|
|
202
|
-
system.facts.unrelated = true; // Still part of the changeset!
|
|
203
|
-
|
|
204
|
-
// CORRECT – always close changesets
|
|
205
|
-
tt.beginChangeset("update");
|
|
206
|
-
system.facts.name = "Alice";
|
|
207
|
-
tt.endChangeset();
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Accessing time-travel when disabled
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
// WRONG – timeTravel not enabled, system.debug.timeTravel is null
|
|
214
|
-
const system = createSystem({ module: myModule });
|
|
215
|
-
system.debug.timeTravel.undo(); // TypeError!
|
|
216
|
-
|
|
217
|
-
// CORRECT – check before using
|
|
218
|
-
const tt = system.debug.timeTravel;
|
|
219
|
-
if (tt) {
|
|
220
|
-
tt.undo();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Or enable it
|
|
224
|
-
const system = createSystem({
|
|
225
|
-
module: myModule,
|
|
226
|
-
debug: { timeTravel: true },
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### No maxSnapshots cap with frequent mutations
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
// WRONG – unbounded snapshots in a high-frequency update loop
|
|
234
|
-
debug: { timeTravel: true }, // Default cap is 50, which is fine
|
|
235
|
-
|
|
236
|
-
// Be explicit when mutation rate is high
|
|
237
|
-
debug: { timeTravel: true, maxSnapshots: 30 },
|
|
238
|
-
```
|