@directive-run/claude-plugin 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/LICENSE +26 -0
  3. package/README.md +113 -0
  4. package/dist/index.cjs +2 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +66 -0
  7. package/dist/index.d.ts +66 -0
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +64 -0
  11. package/skills/README.md +9 -0
  12. package/skills/building-ai-agents/SKILL.md +414 -0
  13. package/skills/building-ai-agents/ai-adapters.md +257 -0
  14. package/skills/building-ai-agents/ai-agents-streaming.md +307 -0
  15. package/skills/building-ai-agents/ai-communication.md +304 -0
  16. package/skills/building-ai-agents/api-skeleton.md +5 -0
  17. package/skills/building-ai-agents/examples.md +849 -0
  18. package/skills/building-ai-orchestrators/SKILL.md +401 -0
  19. package/skills/building-ai-orchestrators/ai-multi-agent.md +375 -0
  20. package/skills/building-ai-orchestrators/ai-orchestrator.md +466 -0
  21. package/skills/building-ai-orchestrators/ai-tasks.md +268 -0
  22. package/skills/building-ai-orchestrators/api-skeleton.md +5 -0
  23. package/skills/building-ai-orchestrators/examples.md +1609 -0
  24. package/skills/building-directive-systems/SKILL.md +499 -0
  25. package/skills/building-directive-systems/api-skeleton.md +5 -0
  26. package/skills/building-directive-systems/examples.md +4149 -0
  27. package/skills/building-directive-systems/multi-module.md +327 -0
  28. package/skills/building-directive-systems/plugins.md +361 -0
  29. package/skills/building-directive-systems/react-adapter.md +376 -0
  30. package/skills/building-directive-systems/system-api.md +463 -0
  31. package/skills/getting-started-with-directive/SKILL.md +198 -0
  32. package/skills/getting-started-with-directive/api-skeleton.md +5 -0
  33. package/skills/getting-started-with-directive/core-patterns.md +240 -0
  34. package/skills/getting-started-with-directive/examples.md +87 -0
  35. package/skills/getting-started-with-directive/sitemap.md +202 -0
  36. package/skills/hardening-ai-systems/SKILL.md +377 -0
  37. package/skills/hardening-ai-systems/ai-budget-resilience.md +408 -0
  38. package/skills/hardening-ai-systems/ai-guardrails-memory.md +370 -0
  39. package/skills/hardening-ai-systems/ai-security.md +400 -0
  40. package/skills/hardening-ai-systems/api-skeleton.md +5 -0
  41. package/skills/hardening-ai-systems/examples.md +628 -0
  42. package/skills/migrating-to-directive/SKILL.md +249 -0
  43. package/skills/migrating-to-directive/anti-patterns.md +382 -0
  44. package/skills/migrating-to-directive/api-skeleton.md +5 -0
  45. package/skills/migrating-to-directive/core-patterns.md +240 -0
  46. package/skills/migrating-to-directive/examples.md +511 -0
  47. package/skills/migrating-to-directive/schema-types.md +273 -0
  48. package/skills/reviewing-directive-code/SKILL.md +232 -0
  49. package/skills/reviewing-directive-code/anti-patterns.md +382 -0
  50. package/skills/reviewing-directive-code/api-skeleton.md +5 -0
  51. package/skills/reviewing-directive-code/core-patterns.md +240 -0
  52. package/skills/reviewing-directive-code/examples.md +463 -0
  53. package/skills/reviewing-directive-code/naming.md +321 -0
  54. package/skills/scaffolding-directive-modules/SKILL.md +218 -0
  55. package/skills/scaffolding-directive-modules/api-skeleton.md +5 -0
  56. package/skills/scaffolding-directive-modules/core-patterns.md +240 -0
  57. package/skills/scaffolding-directive-modules/examples.md +1386 -0
  58. package/skills/scaffolding-directive-modules/naming.md +321 -0
  59. package/skills/scaffolding-directive-modules/schema-types.md +273 -0
  60. package/skills/testing-ai-systems/SKILL.md +398 -0
  61. package/skills/testing-ai-systems/ai-debug-observability.md +329 -0
  62. package/skills/testing-ai-systems/ai-mcp-rag.md +293 -0
  63. package/skills/testing-ai-systems/ai-testing-evals.md +485 -0
  64. package/skills/testing-ai-systems/api-skeleton.md +5 -0
  65. package/skills/testing-ai-systems/examples.md +1261 -0
  66. package/skills/testing-directive-code/SKILL.md +479 -0
  67. package/skills/testing-directive-code/api-skeleton.md +5 -0
  68. package/skills/testing-directive-code/examples.md +156 -0
  69. package/skills/testing-directive-code/history.md +351 -0
  70. package/skills/testing-directive-code/testing.md +412 -0
  71. package/skills/writing-directive-constraints/SKILL.md +486 -0
  72. package/skills/writing-directive-constraints/api-skeleton.md +5 -0
  73. package/skills/writing-directive-constraints/constraints.md +274 -0
  74. package/skills/writing-directive-constraints/error-boundaries.md +330 -0
  75. package/skills/writing-directive-constraints/examples.md +1361 -0
  76. package/skills/writing-directive-constraints/resolvers.md +367 -0
  77. package/skills/writing-directive-modules/SKILL.md +342 -0
  78. package/skills/writing-directive-modules/anti-patterns.md +382 -0
  79. package/skills/writing-directive-modules/api-skeleton.md +5 -0
  80. package/skills/writing-directive-modules/core-patterns.md +240 -0
  81. package/skills/writing-directive-modules/examples.md +1639 -0
  82. package/skills/writing-directive-modules/naming.md +321 -0
  83. package/skills/writing-directive-modules/schema-types.md +273 -0
@@ -0,0 +1,479 @@
1
+ ---
2
+ name: testing-directive-code
3
+ description: "Test Directive modules and systems using createTestSystem, mockResolver, assertFact, assertDerivation, assertRequirement, settleWithFakeTimers, and flushMicrotasks. Use when writing tests for Directive modules, constraints, resolvers, derivations, effects, or time-travel behavior."
4
+ ---
5
+
6
+ # Testing Directive Code
7
+
8
+ ## Prerequisites
9
+
10
+ This skill applies when the project uses `@directive-run/core`. If not found in `package.json`, suggest installing it: `npm install @directive-run/core`.
11
+
12
+ ## When Claude Should Use This Skill
13
+
14
+ **Auto-invoke when the user:**
15
+ - Says "write a test", "add tests for this module", or "how do I test this resolver"
16
+ - Shows a Directive module and asks how to verify its behavior
17
+ - Asks about mocking resolvers, fake timers, or asserting facts in tests
18
+ - Asks about testing time-travel or snapshot behavior
19
+
20
+ **Do NOT invoke when:**
21
+ - Writing the module under test (see `writing-directive-modules.md`)
22
+ - Asking about system composition (see `building-directive-systems.md`)
23
+ - Asking about constraint patterns without a testing context (see `writing-directive-constraints.md`)
24
+
25
+ ---
26
+
27
+ ## Decision Tree: What Are You Testing?
28
+
29
+ ```
30
+ What are you testing?
31
+ ├── A single module in isolation → createTestSystem(module)
32
+ ├── Multiple modules together → createTestSystemFromModules({ a, b })
33
+ ├── A constraint fires correctly → Set facts, call assertRequirement
34
+ ├── A resolver mutates facts → Mock resolver, trigger, settle, assertFact
35
+ ├── A derivation computes correctly → Set facts, call assertDerivation
36
+ ├── Async/retry behavior → settleWithFakeTimers(system, vi)
37
+ ├── Intermediate state (mid-resolve) → flushMicrotasks()
38
+ └── Time-travel / snapshots → Enable history in createTestSystem options
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Imports
44
+
45
+ ```typescript
46
+ import {
47
+ createTestSystem,
48
+ createTestSystemFromModules,
49
+ mockResolver,
50
+ flushMicrotasks,
51
+ settleWithFakeTimers,
52
+ assertFact,
53
+ assertDerivation,
54
+ assertRequirement,
55
+ } from "@directive-run/core/testing";
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Creating Test Systems
61
+
62
+ ### Single module
63
+
64
+ ```typescript
65
+ // Minimal – testing defaults: time-travel off, no plugins, sync settling
66
+ const system = createTestSystem(myModule);
67
+
68
+ // With initial state
69
+ const system = createTestSystem(myModule, {
70
+ initialFacts: { count: 5, phase: "loading" },
71
+ });
72
+
73
+ // With mocked resolvers
74
+ const system = createTestSystem(myModule, {
75
+ mockResolvers: [
76
+ mockResolver("FETCH_USER", async (req, context) => {
77
+ context.facts.user = { id: req.userId, name: "Test User" };
78
+ }),
79
+ ],
80
+ });
81
+
82
+ // Combined
83
+ const system = createTestSystem(myModule, {
84
+ initialFacts: { isAuthenticated: true, profile: null },
85
+ mockResolvers: [
86
+ mockResolver("FETCH_PROFILE", async (req, context) => {
87
+ context.facts.profile = { bio: "Test bio" };
88
+ }),
89
+ ],
90
+ });
91
+ ```
92
+
93
+ ### Multi-module
94
+
95
+ ```typescript
96
+ const system = createTestSystemFromModules(
97
+ { auth: authModule, cart: cartModule },
98
+ {
99
+ mockResolvers: [
100
+ mockResolver("AUTHENTICATE", async (req, context) => {
101
+ context.facts.auth.token = "test-token";
102
+ context.facts.auth.isAuthenticated = true;
103
+ }),
104
+ ],
105
+ },
106
+ );
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Testing Constraints
112
+
113
+ Set facts to satisfy the constraint's `when()`, then assert the requirement was emitted.
114
+
115
+ ```typescript
116
+ describe("fetchWhenAuth constraint", () => {
117
+ it("emits FETCH_USER when authenticated without profile", () => {
118
+ const system = createTestSystem(userModule);
119
+
120
+ system.facts.isAuthenticated = true;
121
+ system.facts.profile = null;
122
+
123
+ assertRequirement(system, "FETCH_USER");
124
+ });
125
+
126
+ it("does NOT emit when profile already loaded", () => {
127
+ const system = createTestSystem(userModule, {
128
+ initialFacts: {
129
+ isAuthenticated: true,
130
+ profile: { id: "1", name: "Alice" },
131
+ },
132
+ });
133
+
134
+ const { requirements } = system.inspect();
135
+ const fetchReqs = requirements.filter((r) => r.type === "FETCH_USER");
136
+ expect(fetchReqs).toHaveLength(0);
137
+ });
138
+
139
+ it("emits dynamic requirement with correct payload", () => {
140
+ const system = createTestSystem(userModule, {
141
+ initialFacts: { isAuthenticated: true, userId: "u-42", profile: null },
142
+ });
143
+
144
+ // assertRequirement can check payload
145
+ assertRequirement(system, "FETCH_USER", { userId: "u-42" });
146
+ });
147
+ });
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Testing Resolvers
153
+
154
+ Mock the resolver, trigger the constraint, settle, then assert fact mutations.
155
+
156
+ ```typescript
157
+ describe("fetchUser resolver", () => {
158
+ it("stores fetched user and updates phase", async () => {
159
+ const system = createTestSystem(userModule, {
160
+ mockResolvers: [
161
+ mockResolver("FETCH_USER", async (req, context) => {
162
+ context.facts.user = { id: req.userId, name: "Mocked User" };
163
+ context.facts.phase = "loaded";
164
+ }),
165
+ ],
166
+ });
167
+
168
+ system.facts.isAuthenticated = true;
169
+ system.facts.user = null;
170
+
171
+ await system.settle();
172
+
173
+ assertFact(system, "user", { id: expect.any(String), name: "Mocked User" });
174
+ assertFact(system, "phase", "loaded");
175
+ });
176
+
177
+ it("handles resolver error gracefully", async () => {
178
+ const system = createTestSystem(userModule, {
179
+ mockResolvers: [
180
+ mockResolver("FETCH_USER", async () => {
181
+ throw new Error("Network failure");
182
+ }),
183
+ ],
184
+ });
185
+
186
+ system.facts.isAuthenticated = true;
187
+ system.facts.user = null;
188
+
189
+ // If error boundary uses "skip", settle resolves normally
190
+ await system.settle();
191
+ assertFact(system, "phase", "error");
192
+ });
193
+ });
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Testing Derivations
199
+
200
+ Set facts, then read the derived value. No need to settle – derivations are synchronous.
201
+
202
+ ```typescript
203
+ describe("isOverBudget derivation", () => {
204
+ it("returns true when total exceeds budget", () => {
205
+ const system = createTestSystem(budgetModule, {
206
+ initialFacts: { total: 150, budget: 100 },
207
+ });
208
+
209
+ assertDerivation(system, "isOverBudget", true);
210
+ });
211
+
212
+ it("recomputes reactively when facts change", () => {
213
+ const system = createTestSystem(budgetModule, {
214
+ initialFacts: { total: 50, budget: 100 },
215
+ });
216
+
217
+ assertDerivation(system, "isOverBudget", false);
218
+
219
+ system.facts.total = 200;
220
+
221
+ assertDerivation(system, "isOverBudget", true);
222
+ });
223
+ });
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Testing with Fake Timers
229
+
230
+ Use `settleWithFakeTimers` when resolvers use retry delays, timeouts, or exponential backoff.
231
+
232
+ ```typescript
233
+ import { describe, it, expect, vi } from "vitest";
234
+ import { createTestSystem, settleWithFakeTimers, mockResolver, assertFact } from "@directive-run/core/testing";
235
+
236
+ describe("retry behavior", () => {
237
+ it("retries on failure with exponential backoff", async () => {
238
+ vi.useFakeTimers();
239
+ let attempts = 0;
240
+
241
+ const system = createTestSystem(myModule, {
242
+ mockResolvers: [
243
+ mockResolver("FETCH_DATA", async (req, context) => {
244
+ attempts += 1;
245
+ if (attempts < 3) {
246
+ throw new Error("Temporary failure");
247
+ }
248
+ context.facts.data = "success";
249
+ }),
250
+ ],
251
+ });
252
+
253
+ system.facts.needsData = true;
254
+
255
+ // Advances fake timers through retry delays and waits for settlement
256
+ await settleWithFakeTimers(system, vi);
257
+
258
+ expect(attempts).toBe(3);
259
+ assertFact(system, "data", "success");
260
+
261
+ vi.useRealTimers();
262
+ });
263
+ });
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Testing Intermediate Async State
269
+
270
+ Use `flushMicrotasks()` to process pending promises without fully settling.
271
+
272
+ ```typescript
273
+ it("shows loading phase during resolution", async () => {
274
+ const system = createTestSystem(myModule, {
275
+ mockResolvers: [
276
+ mockResolver("FETCH_DATA", async (req, context) => {
277
+ context.facts.phase = "loading";
278
+ await new Promise((resolve) => setTimeout(resolve, 100));
279
+ context.facts.phase = "done";
280
+ }),
281
+ ],
282
+ });
283
+
284
+ system.facts.trigger = true;
285
+
286
+ await flushMicrotasks();
287
+ assertFact(system, "phase", "loading");
288
+
289
+ await system.settle();
290
+ assertFact(system, "phase", "done");
291
+ });
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Testing Effects
297
+
298
+ Effects run after reconciliation. Settle first, then check side effects.
299
+
300
+ ```typescript
301
+ describe("logPhase effect", () => {
302
+ it("logs when phase changes", async () => {
303
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
304
+
305
+ const system = createTestSystem(myModule, {
306
+ initialFacts: { phase: "idle" },
307
+ });
308
+
309
+ system.facts.phase = "loading";
310
+ await system.settle();
311
+
312
+ expect(consoleSpy).toHaveBeenCalledWith("Phase: loading");
313
+
314
+ consoleSpy.mockRestore();
315
+ });
316
+ });
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Testing Time-Travel
322
+
323
+ Enable time-travel in the test system config. In most cases, prefer `assertFact` over snapshot testing.
324
+
325
+ ```typescript
326
+ describe("history", () => {
327
+ it("can undo a fact change", () => {
328
+ const system = createTestSystem(editorModule, {
329
+ history: { maxSnapshots: 20 },
330
+ });
331
+
332
+ system.facts.text = "Hello";
333
+ system.facts.text = "Hello, world";
334
+
335
+ const history = system.history;
336
+ expect(system.facts.text).toBe("Hello, world");
337
+
338
+ history.goBack();
339
+ expect(system.facts.text).toBe("Hello");
340
+ });
341
+
342
+ it("can export and import history", () => {
343
+ const system = createTestSystem(myModule, {
344
+ history: true,
345
+ });
346
+
347
+ system.facts.count = 1;
348
+ system.facts.count = 2;
349
+
350
+ const history = system.history;
351
+ const exported = history.export();
352
+
353
+ history.import(exported);
354
+ history.goTo(0);
355
+ expect(system.facts.count).toBe(1);
356
+ });
357
+ });
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Critical Anti-Patterns
363
+
364
+ ### 1. Testing real resolvers (hitting real APIs)
365
+
366
+ ```typescript
367
+ // WRONG – slow, flaky, external dependency
368
+ const system = createTestSystem(myModule);
369
+ system.facts.needsFetch = true;
370
+ await system.settle(); // Makes real HTTP call
371
+
372
+ // CORRECT – always mock resolvers in unit tests
373
+ const system = createTestSystem(myModule, {
374
+ mockResolvers: [mockResolver("FETCH", async (req, context) => {
375
+ context.facts.data = { mocked: true };
376
+ })],
377
+ });
378
+ ```
379
+
380
+ ### 2. Forgetting to settle before asserting async results
381
+
382
+ ```typescript
383
+ // WRONG – resolver hasn't completed
384
+ system.facts.trigger = true;
385
+ assertFact(system, "result", "done"); // Fails
386
+
387
+ // CORRECT
388
+ system.facts.trigger = true;
389
+ await system.settle();
390
+ assertFact(system, "result", "done");
391
+ ```
392
+
393
+ ### 3. Resolver parameter naming
394
+ Always use `(req, context)` – never `(req, ctx)` or `(request, context)`. Applies to `mockResolver` callbacks too.
395
+
396
+ ### 4. Testing implementation details instead of behavior
397
+
398
+ ```typescript
399
+ // WRONG – testing internal resolver call count without behavior check
400
+ expect(mockFn).toHaveBeenCalledTimes(1);
401
+
402
+ // CORRECT – test the observable outcome (facts, derivations)
403
+ assertFact(system, "user", { id: "1", name: "Alice" });
404
+ assertDerivation(system, "isLoggedIn", true);
405
+ ```
406
+
407
+ ### 5. Using createSystem instead of createTestSystem
408
+
409
+ ```typescript
410
+ // WRONG – production system, no test defaults, runs plugins
411
+ const system = createSystem({ module: myModule });
412
+
413
+ // CORRECT – isolated, no plugins, testing utilities available
414
+ const system = createTestSystem(myModule);
415
+ ```
416
+
417
+ ### 6. Not resetting fake timers
418
+
419
+ ```typescript
420
+ // WRONG – leaks fake timers to other tests
421
+ vi.useFakeTimers();
422
+ await settleWithFakeTimers(system, vi);
423
+ // Missing vi.useRealTimers()
424
+
425
+ // CORRECT – always restore in afterEach or at the end of the test
426
+ vi.useFakeTimers();
427
+ await settleWithFakeTimers(system, vi);
428
+ vi.useRealTimers();
429
+ ```
430
+
431
+ ### 7. Enabling time-travel when you don't need it
432
+
433
+ ```typescript
434
+ // Prefer assertFact/assertDerivation for most tests.
435
+ // Only enable time-travel when specifically testing undo/redo or snapshot export.
436
+ ```
437
+
438
+ ### 8. Forgetting to destroy the system between tests
439
+
440
+ ```typescript
441
+ // CORRECT – use afterEach to clean up
442
+ let system: ReturnType<typeof createTestSystem>;
443
+
444
+ beforeEach(() => {
445
+ system = createTestSystem(myModule);
446
+ });
447
+
448
+ afterEach(() => {
449
+ system.destroy();
450
+ });
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Assertion Quick Reference
456
+
457
+ ```typescript
458
+ // Assert a fact value
459
+ assertFact(system, "phase", "done");
460
+ assertFact(system, "user", { id: expect.any(String) });
461
+
462
+ // Assert a derivation value
463
+ assertDerivation(system, "isLoading", false);
464
+ assertDerivation(system, "itemCount", 3);
465
+
466
+ // Assert a requirement was emitted
467
+ assertRequirement(system, "FETCH_USER");
468
+ assertRequirement(system, "FETCH_USER", { userId: "u-42" }); // With payload
469
+
470
+ // Inspect raw state
471
+ const { facts, requirements, resolvers, inflight } = system.inspect();
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Reference Files
477
+
478
+ - `testing.md` – full testing API, createTestSystem options, multi-module test setup
479
+ - `history.md` – history API, changesets, export/import, performance considerations
@@ -0,0 +1,5 @@
1
+ # API Skeleton
2
+
3
+ > Auto-generated. Do not edit.
4
+
5
+ > Source JSON not found. Build website docs first.
@@ -0,0 +1,156 @@
1
+ # Examples
2
+
3
+ > Auto-generated from extracted examples. Do not edit manually.
4
+
5
+ ## time-machine
6
+
7
+ ```typescript
8
+ // Example: time-machine
9
+ // Source: examples/time-machine/src/module.ts
10
+ // Pure module file — no DOM wiring
11
+
12
+ /**
13
+ * Time Machine — Directive Module
14
+ *
15
+ * Drawing canvas where each stroke is a fact mutation. Full time-travel:
16
+ * undo/redo, export/import JSON, replay animation, changesets, snapshot slider.
17
+ */
18
+
19
+ import {
20
+ type ModuleSchema,
21
+ createModule,
22
+ createSystem,
23
+ t,
24
+ } from "@directive-run/core";
25
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ export interface Stroke {
32
+ id: string;
33
+ x: number;
34
+ y: number;
35
+ color: string;
36
+ size: number;
37
+ }
38
+
39
+ export interface TimelineEntry {
40
+ time: number;
41
+ event: string;
42
+ detail: string;
43
+ type:
44
+ | "stroke"
45
+ | "undo"
46
+ | "redo"
47
+ | "changeset"
48
+ | "export"
49
+ | "import"
50
+ | "replay"
51
+ | "goto";
52
+ }
53
+
54
+ // ============================================================================
55
+ // Timeline (external mutable array, same pattern as fraud-analysis)
56
+ // ============================================================================
57
+
58
+ export const timeline: TimelineEntry[] = [];
59
+
60
+ export function addTimeline(
61
+ event: string,
62
+ detail: string,
63
+ type: TimelineEntry["type"],
64
+ ): void {
65
+ timeline.unshift({ time: Date.now(), event, detail, type });
66
+ if (timeline.length > 50) {
67
+ timeline.length = 50;
68
+ }
69
+ }
70
+
71
+ // ============================================================================
72
+ // Schema
73
+ // ============================================================================
74
+
75
+ export const schema = {
76
+ facts: {
77
+ strokes: t.array<Stroke>(),
78
+ currentColor: t.string(),
79
+ brushSize: t.number(),
80
+ changesetActive: t.boolean(),
81
+ changesetLabel: t.string(),
82
+ },
83
+ derivations: {
84
+ strokeCount: t.number(),
85
+ canUndo: t.boolean(),
86
+ canRedo: t.boolean(),
87
+ currentIndex: t.number(),
88
+ totalSnapshots: t.number(),
89
+ },
90
+ events: {
91
+ addStroke: { x: t.number(), y: t.number() },
92
+ setColor: { value: t.string() },
93
+ setBrushSize: { value: t.number() },
94
+ clearCanvas: {},
95
+ },
96
+ requirements: {},
97
+ } satisfies ModuleSchema;
98
+
99
+ // ============================================================================
100
+ // Module
101
+ // ============================================================================
102
+
103
+ const canvasModule = createModule("canvas", {
104
+ schema,
105
+
106
+ init: (facts) => {
107
+ facts.strokes = [];
108
+ facts.currentColor = "#5ba3a3";
109
+ facts.brushSize = 12;
110
+ facts.changesetActive = false;
111
+ facts.changesetLabel = "";
112
+ },
113
+
114
+ derive: {
115
+ strokeCount: (facts) => facts.strokes.length,
116
+ // These will be updated from the time-travel manager
117
+ canUndo: () => false,
118
+ canRedo: () => false,
119
+ currentIndex: () => 0,
120
+ totalSnapshots: () => 0,
121
+ },
122
+
123
+ events: {
124
+ addStroke: (facts, { x, y }) => {
125
+ const stroke: Stroke = {
126
+ id: `s${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
127
+ x,
128
+ y,
129
+ color: facts.currentColor,
130
+ size: facts.brushSize,
131
+ };
132
+ facts.strokes = [...facts.strokes, stroke];
133
+ },
134
+ setColor: (facts, { value }) => {
135
+ facts.currentColor = value;
136
+ },
137
+ setBrushSize: (facts, { value }) => {
138
+ facts.brushSize = value;
139
+ },
140
+ clearCanvas: (facts) => {
141
+ facts.strokes = [];
142
+ },
143
+ },
144
+ });
145
+
146
+ // ============================================================================
147
+ // System with Time-Travel
148
+ // ============================================================================
149
+
150
+ export const system = createSystem({
151
+ module: canvasModule,
152
+ history: { maxSnapshots: 200 },
153
+ trace: true,
154
+ plugins: [devtoolsPlugin({ name: "time-machine" })],
155
+ });
156
+ ```