@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Jason Comes
3
+ Copyright (c) 2026 Sizls LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  # Anti-Patterns
2
2
 
3
- 20 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
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. Builder/Chaining API
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
- ## 13. Async Logic in `init`
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
- ## 14. Missing `settle()` After `start()`
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
- ## 15. Missing `crossModuleDeps` Declaration
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
- ## 16. String Literal for `require`
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
- ## 17. Passthrough Derivations
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
- ## 18. Deep Import Paths
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
- ## 19. Async `when()` Without `deps`
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
- ## 20. No Error Handling on Failing Resolvers
315
+ ## 19. No Error Handling on Failing Resolvers
332
316
 
333
317
  ```typescript
334
318
  // WRONG – unhandled errors crash the system
@@ -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
- debug: { timeTravel: true, maxSnapshots: 100 },
111
+ history: { maxSnapshots: 100 },
112
112
  });
113
113
 
114
114
  // Multi-module – namespaced access: system.facts.auth.token
@@ -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
+ ```
@@ -277,7 +277,7 @@ const system = createSystem({
277
277
  // initOrder: ["auth", "data", "cart"], // Explicit order
278
278
 
279
279
  plugins: [loggingPlugin()],
280
- debug: { timeTravel: true },
280
+ history: true,
281
281
  });
282
282
 
283
283
  // Hydrate from async source (call before start)
@@ -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
- debug: { timeTravel: true },
132
+ history: true,
133
133
  });
134
134
 
135
135
  const score = useSelector(gameSystem, (s) => s.facts.score);
@@ -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
- debug: { timeTravel: true, maxSnapshots: 100 },
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
@@ -308,6 +308,6 @@ const abTesting = createModule("ab-testing", {
308
308
 
309
309
  export const system = createSystem({
310
310
  module: abTesting,
311
- debug: { runHistory: true },
311
+ trace: true,
312
312
  plugins: [devtoolsPlugin({ name: "ab-testing" })],
313
313
  });
@@ -238,7 +238,7 @@ const pipelineModule = createModule("pipeline", {
238
238
 
239
239
  export const system = createSystem({
240
240
  module: pipelineModule,
241
- debug: { runHistory: true },
241
+ trace: true,
242
242
  plugins: [devtoolsPlugin({ name: "ai-checkpoint" })],
243
243
  });
244
244
 
@@ -161,7 +161,7 @@ const guardrailModule = createModule("guardrails", {
161
161
 
162
162
  export const system = createSystem({
163
163
  module: guardrailModule,
164
- debug: { runHistory: true },
164
+ trace: true,
165
165
  plugins: [devtoolsPlugin({ name: "ai-guardrails" })],
166
166
  });
167
167
 
@@ -296,6 +296,6 @@ const batchModule = createModule("batch-loader", {
296
296
 
297
297
  export const system = createSystem({
298
298
  module: batchModule,
299
- debug: { runHistory: true },
299
+ trace: true,
300
300
  plugins: [devtoolsPlugin({ name: "batch-resolver" })],
301
301
  });
@@ -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,
@@ -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, derive) => {
201
- if (!derive.isValid) {
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
- debug: { runHistory: true },
325
+ trace: true,
326
326
  plugins: [devtoolsPlugin({ name: "contact-form" })],
327
327
  });
@@ -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
- debug: { timeTravel: true, runHistory: true },
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, derive) => {
184
- const loaded = derive.loadedCount;
185
- const anyErr = derive.anyError;
186
- const anyLoad = derive.anyLoading;
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
- debug: { runHistory: true },
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
- debug: { runHistory: true },
347
+ trace: true,
348
348
  plugins: [perf, devtoolsPlugin({ name: "error-boundaries" })],
349
349
  errorBoundary: {
350
350
  onResolverError: (_error, resolver) => {
@@ -127,23 +127,23 @@ export const wizardModule = createModule("wizard", {
127
127
  return facts.plan !== "";
128
128
  },
129
129
 
130
- currentStepValid: (facts, derive) => {
130
+ currentStepValid: (facts, derived) => {
131
131
  if (facts.currentStep === 0) {
132
- return derive.step0Valid;
132
+ return derived.step0Valid;
133
133
  }
134
134
  if (facts.currentStep === 1) {
135
- return derive.step1Valid;
135
+ return derived.step1Valid;
136
136
  }
137
137
  if (facts.currentStep === 2) {
138
- return derive.step2Valid;
138
+ return derived.step2Valid;
139
139
  }
140
140
 
141
141
  return false;
142
142
  },
143
143
 
144
- canAdvance: (facts, derive) => {
144
+ canAdvance: (facts, derived) => {
145
145
  return (
146
- derive.currentStepValid && facts.currentStep < facts.totalSteps - 1
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
- debug: { runHistory: true },
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
- debug: {
650
- timeTravel: true,
651
- maxSnapshots: 50,
652
- runHistory: true,
653
- maxRuns: 100,
654
- },
649
+ history: { maxSnapshots: 50 },
650
+ trace: { maxRuns: 100 },
655
651
  });
656
652
 
657
653
  // ============================================================================
@@ -15,6 +15,7 @@
15
15
  * - No asCombined() helper needed
16
16
  */
17
17
 
18
+ import { el } from "@directive-run/el";
18
19
  import { getFacts, system } from "./system";
19
20
 
20
21
  // DOM Elements
@@ -102,8 +102,8 @@ const newsletter = createModule("newsletter", {
102
102
 
103
103
  isValid: (facts) => EMAIL_REGEX.test(facts.email),
104
104
 
105
- canSubmit: (facts, derive) => {
106
- if (!derive.isValid) {
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
- debug: { runHistory: true },
212
+ trace: true,
213
213
  plugins: [devtoolsPlugin({ name: "newsletter" })],
214
214
  });
@@ -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
- debug: { runHistory: true },
258
+ trace: true,
259
259
  plugins: [loggingPlugin(), devtoolsPlugin({ name: "pagination" })],
260
260
  });
@@ -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, derive) => derive.canManageUsers,
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
- debug: { runHistory: true },
325
+ trace: true,
326
326
  plugins: [devtoolsPlugin({ name: "permissions" })],
327
327
  });
@@ -221,7 +221,7 @@ const routerModule = createModule("router", {
221
221
 
222
222
  export const system = createSystem({
223
223
  module: routerModule,
224
- debug: { runHistory: true },
224
+ trace: true,
225
225
  plugins: [devtoolsPlugin({ name: "provider-routing" })],
226
226
  });
227
227
 
@@ -187,23 +187,23 @@ export const cartModule = createModule("cart", {
187
187
  return facts.self.items.length === 0;
188
188
  },
189
189
 
190
- discount: (facts, derive) => {
191
- const sub = derive.subtotal;
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, derive) => {
197
- const sub = derive.subtotal;
198
- const disc = derive.discount;
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, derive) => {
204
- const sub = derive.subtotal;
205
- const disc = derive.discount;
206
- const tx = derive.tax;
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, derive) => {
218
- const sub = derive.subtotal;
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
- debug: {
418
- timeTravel: true,
419
- maxSnapshots: 50,
420
- runHistory: true,
421
- },
417
+ history: { maxSnapshots: 50 },
418
+ trace: true,
422
419
  });
@@ -109,7 +109,14 @@ export const sudokuSchema = {
109
109
 
110
110
  export const sudokuGame = createModule("sudoku", {
111
111
  schema: sudokuSchema,
112
- snapshotEvents: ["inputNumber", "toggleNote", "requestHint", "newGame"],
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, derive) => {
157
+ conflictIndices: (facts, derived) => {
151
158
  const indices = new Set<number>();
152
159
  const givens = facts.givens;
153
- for (const c of derive.conflicts) {
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, derive) => {
164
- return derive.conflicts.length > 0;
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, derive) => {
180
- return Math.round((derive.filledCount / 81) * 100);
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, derive) => {
188
- return derive.isComplete && !derive.hasConflicts;
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, derive) => {
210
- const val = derive.highlightValue;
216
+ sameValueIndices: (facts, derived) => {
217
+ const val = derived.highlightValue;
211
218
  if (val === 0) {
212
219
  return new Set<number>();
213
220
  }
@@ -142,6 +142,7 @@ const canvasModule = createModule("canvas", {
142
142
 
143
143
  export const system = createSystem({
144
144
  module: canvasModule,
145
- debug: { timeTravel: true, maxSnapshots: 200, runHistory: true },
145
+ history: { maxSnapshots: 200 },
146
+ trace: true,
146
147
  plugins: [devtoolsPlugin({ name: "time-machine" })],
147
148
  });
@@ -106,12 +106,12 @@ export const topicGuardModule = createModule("topic-guard", {
106
106
  .length;
107
107
  },
108
108
 
109
- blockRate: (facts, derive) => {
110
- const total = derive.messageCount;
109
+ blockRate: (facts, derived) => {
110
+ const total = derived.messageCount;
111
111
  if (total === 0) {
112
112
  return "0%";
113
113
  }
114
- const blocked = derive.blockedCount;
114
+ const blocked = derived.blockedCount;
115
115
  const rate = Math.round((blocked / total) * 100);
116
116
 
117
117
  return `${rate}%`;
@@ -328,6 +328,6 @@ export const system = createSystem({
328
328
  url: urlModule,
329
329
  products: productsModule,
330
330
  },
331
- debug: { runHistory: true },
331
+ trace: true,
332
332
  plugins: [devtoolsPlugin({ name: "url-sync" })],
333
333
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/knowledge",
3
- "version": "0.4.2",
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.4.2",
54
- "@directive-run/ai": "0.4.2"
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",
@@ -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
- ```