@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,1639 @@
1
+ # Examples
2
+
3
+ > Auto-generated from extracted examples. Do not edit manually.
4
+
5
+ ## counter
6
+
7
+ ```typescript
8
+ // Example: counter
9
+ // Source: examples/counter/src/module.ts
10
+ // Pure module file — no DOM wiring
11
+
12
+ /**
13
+ * Counter — The simplest Directive module.
14
+ *
15
+ * Demonstrates: facts, events, derivations, one constraint, one resolver.
16
+ * Total: ~40 lines.
17
+ */
18
+
19
+ import {
20
+ type ModuleSchema,
21
+ createModule,
22
+ createSystem,
23
+ t,
24
+ } from "@directive-run/core";
25
+
26
+ const schema = {
27
+ facts: {
28
+ count: t.number(),
29
+ },
30
+ derivations: {
31
+ doubled: t.number(),
32
+ isPositive: t.boolean(),
33
+ },
34
+ events: {
35
+ increment: {},
36
+ decrement: {},
37
+ reset: {},
38
+ },
39
+ requirements: {
40
+ CLAMP_TO_ZERO: {},
41
+ },
42
+ } satisfies ModuleSchema;
43
+
44
+ export const counterModule = createModule("counter", {
45
+ schema,
46
+
47
+ init: (facts) => {
48
+ facts.count = 0;
49
+ },
50
+
51
+ derive: {
52
+ doubled: (facts) => facts.count * 2,
53
+ isPositive: (facts) => facts.count > 0,
54
+ },
55
+
56
+ events: {
57
+ increment: (facts) => {
58
+ facts.count += 1;
59
+ },
60
+ decrement: (facts) => {
61
+ facts.count -= 1;
62
+ },
63
+ reset: (facts) => {
64
+ facts.count = 0;
65
+ },
66
+ },
67
+
68
+ // When count goes negative, automatically fix it
69
+ constraints: {
70
+ noNegative: {
71
+ when: (facts) => facts.count < 0,
72
+ require: { type: "CLAMP_TO_ZERO" },
73
+ },
74
+ },
75
+
76
+ resolvers: {
77
+ clamp: {
78
+ requirement: "CLAMP_TO_ZERO",
79
+ resolve: async (_req, context) => {
80
+ context.facts.count = 0;
81
+ },
82
+ },
83
+ },
84
+ });
85
+
86
+ export const system = createSystem({ module: counterModule });
87
+ ```
88
+
89
+ ## contact-form
90
+
91
+ ```typescript
92
+ // Example: contact-form
93
+ // Source: examples/contact-form/src/module.ts
94
+ // Pure module file — no DOM wiring
95
+
96
+ /**
97
+ * Contact Form — Directive Module
98
+ *
99
+ * Multi-field contact form showcasing validation, constraints, and resolvers:
100
+ * - Facts: name, email, subject, message, touched, status, errorMessage, etc.
101
+ * - Derivations: field errors, isValid, canSubmit, messageCharCount
102
+ * - Events: updateField, touchField, submit, reset
103
+ * - Constraints: submitForm, resetAfterSuccess
104
+ * - Resolvers: simulated async send, auto-reset after delay
105
+ * - Effects: logging status transitions
106
+ *
107
+ * Uses a simulated setTimeout instead of a real API so no account is needed.
108
+ */
109
+
110
+ import {
111
+ type ModuleSchema,
112
+ createModule,
113
+ createSystem,
114
+ t,
115
+ } from "@directive-run/core";
116
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
117
+
118
+ // ============================================================================
119
+ // Constants
120
+ // ============================================================================
121
+
122
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
123
+ const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)
124
+
125
+ // ============================================================================
126
+ // Types
127
+ // ============================================================================
128
+
129
+ export interface TimelineEntry {
130
+ time: number;
131
+ event: string;
132
+ detail: string;
133
+ type: string;
134
+ }
135
+
136
+ // ============================================================================
137
+ // Timeline (external mutable array, same pattern as fraud-analysis)
138
+ // ============================================================================
139
+
140
+ export const timeline: TimelineEntry[] = [];
141
+
142
+ export function addTimelineEntry(
143
+ event: string,
144
+ detail: string,
145
+ type: string,
146
+ ): void {
147
+ timeline.unshift({ time: Date.now(), event, detail, type });
148
+ }
149
+
150
+ // ============================================================================
151
+ // Logs helper
152
+ // ============================================================================
153
+
154
+ export function log(msg: string): void {
155
+ console.log(`[contact-form] ${msg}`);
156
+
157
+ // Classify and add to timeline
158
+ if (msg.startsWith("Sending:")) {
159
+ addTimelineEntry("submit", msg.replace("Sending: ", ""), "submit");
160
+ } else if (msg.includes("succeeded")) {
161
+ addTimelineEntry("success", msg, "submit");
162
+ } else if (msg.includes("failed")) {
163
+ addTimelineEntry("error", msg, "error");
164
+ } else if (msg.startsWith("Status:")) {
165
+ addTimelineEntry("status", msg.replace("Status: ", ""), "field");
166
+ } else if (msg.includes("Auto-resetting")) {
167
+ addTimelineEntry("auto-reset", msg, "reset");
168
+ } else if (msg === "Form reset") {
169
+ addTimelineEntry("reset", "Form cleared", "reset");
170
+ } else if (msg.includes("ready")) {
171
+ addTimelineEntry("init", msg, "field");
172
+ }
173
+ }
174
+
175
+ // ============================================================================
176
+ // Schema
177
+ // ============================================================================
178
+
179
+ export const schema = {
180
+ facts: {
181
+ name: t.string(),
182
+ email: t.string(),
183
+ subject: t.string(),
184
+ message: t.string(),
185
+ touched: t.object<Record<string, boolean>>(),
186
+ status: t.string<"idle" | "submitting" | "success" | "error">(),
187
+ errorMessage: t.string(),
188
+ lastSubmittedAt: t.number(),
189
+ submissionCount: t.number(),
190
+ },
191
+ derivations: {
192
+ nameError: t.string(),
193
+ emailError: t.string(),
194
+ subjectError: t.string(),
195
+ messageError: t.string(),
196
+ isValid: t.boolean(),
197
+ canSubmit: t.boolean(),
198
+ messageCharCount: t.number(),
199
+ },
200
+ events: {
201
+ updateField: { field: t.string(), value: t.string() },
202
+ touchField: { field: t.string() },
203
+ submit: {},
204
+ reset: {},
205
+ },
206
+ requirements: {
207
+ SEND_MESSAGE: {},
208
+ RESET_AFTER_DELAY: {},
209
+ },
210
+ } satisfies ModuleSchema;
211
+
212
+ // ============================================================================
213
+ // Module
214
+ // ============================================================================
215
+
216
+ const contactForm = createModule("contact-form", {
217
+ schema,
218
+
219
+ init: (facts) => {
220
+ facts.name = "";
221
+ facts.email = "";
222
+ facts.subject = "";
223
+ facts.message = "";
224
+ facts.touched = {};
225
+ facts.status = "idle";
226
+ facts.errorMessage = "";
227
+ facts.lastSubmittedAt = 0;
228
+ facts.submissionCount = 0;
229
+ },
230
+
231
+ derive: {
232
+ nameError: (facts) => {
233
+ if (!facts.touched.name) {
234
+ return "";
235
+ }
236
+ if (!facts.name.trim()) {
237
+ return "Name is required";
238
+ }
239
+ if (facts.name.trim().length < 2) {
240
+ return "Name must be at least 2 characters";
241
+ }
242
+
243
+ return "";
244
+ },
245
+
246
+ emailError: (facts) => {
247
+ if (!facts.touched.email) {
248
+ return "";
249
+ }
250
+ if (!facts.email.trim()) {
251
+ return "Email is required";
252
+ }
253
+ if (!EMAIL_REGEX.test(facts.email)) {
254
+ return "Enter a valid email address";
255
+ }
256
+
257
+ return "";
258
+ },
259
+
260
+ subjectError: (facts) => {
261
+ if (!facts.touched.subject) {
262
+ return "";
263
+ }
264
+ if (!facts.subject) {
265
+ return "Please select a subject";
266
+ }
267
+
268
+ return "";
269
+ },
270
+
271
+ messageError: (facts) => {
272
+ if (!facts.touched.message) {
273
+ return "";
274
+ }
275
+ if (!facts.message.trim()) {
276
+ return "Message is required";
277
+ }
278
+ if (facts.message.trim().length < 10) {
279
+ return "Message must be at least 10 characters";
280
+ }
281
+
282
+ return "";
283
+ },
284
+
285
+ isValid: (facts) =>
286
+ facts.name.trim().length >= 2 &&
287
+ EMAIL_REGEX.test(facts.email) &&
288
+ facts.subject !== "" &&
289
+ facts.message.trim().length >= 10,
290
+
291
+ canSubmit: (facts, derived) => {
292
+ if (!derived.isValid) {
293
+ return false;
294
+ }
295
+ if (facts.status !== "idle") {
296
+ return false;
297
+ }
298
+ if (
299
+ facts.lastSubmittedAt > 0 &&
300
+ Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
301
+ ) {
302
+ return false;
303
+ }
304
+
305
+ return true;
306
+ },
307
+
308
+ messageCharCount: (facts) => facts.message.length,
309
+ },
310
+
311
+ events: {
312
+ updateField: (facts, { field, value }) => {
313
+ const key = field as "name" | "email" | "subject" | "message";
314
+ if (key in facts && typeof facts[key] === "string") {
315
+ (facts as unknown as Record<string, string>)[key] = value;
316
+ }
317
+ },
318
+
319
+ touchField: (facts, { field }) => {
320
+ facts.touched = { ...facts.touched, [field]: true };
321
+ },
322
+
323
+ submit: (facts) => {
324
+ facts.touched = { name: true, email: true, subject: true, message: true };
325
+ facts.status = "submitting";
326
+ },
327
+
328
+ reset: (facts) => {
329
+ facts.name = "";
330
+ facts.email = "";
331
+ facts.subject = "";
332
+ facts.message = "";
333
+ facts.touched = {};
334
+ facts.status = "idle";
335
+ facts.errorMessage = "";
336
+ },
337
+ },
338
+
339
+ constraints: {
340
+ submitForm: {
341
+ when: (facts) => facts.status === "submitting",
342
+ require: { type: "SEND_MESSAGE" },
343
+ },
344
+
345
+ resetAfterSuccess: {
346
+ when: (facts) => facts.status === "success",
347
+ require: { type: "RESET_AFTER_DELAY" },
348
+ },
349
+ },
350
+
351
+ resolvers: {
352
+ sendMessage: {
353
+ requirement: "SEND_MESSAGE",
354
+ resolve: async (_req, context) => {
355
+ log(
356
+ `Sending: ${context.facts.name} <${context.facts.email}> [${context.facts.subject}]`,
357
+ );
358
+
359
+ await new Promise((resolve) => setTimeout(resolve, 1500));
360
+
361
+ if (Math.random() < 0.2) {
362
+ context.facts.status = "error";
363
+ context.facts.errorMessage =
364
+ "Simulated error — try again (20% failure rate for demo).";
365
+ log("Submission failed (simulated)");
366
+
367
+ return;
368
+ }
369
+
370
+ context.facts.status = "success";
371
+ context.facts.lastSubmittedAt = Date.now();
372
+ context.facts.submissionCount++;
373
+ log(`Submission #${context.facts.submissionCount} succeeded`);
374
+ },
375
+ },
376
+
377
+ resetAfterDelay: {
378
+ requirement: "RESET_AFTER_DELAY",
379
+ resolve: async (_req, context) => {
380
+ log("Auto-resetting in 3 seconds...");
381
+ await new Promise((resolve) => setTimeout(resolve, 3000));
382
+ context.facts.name = "";
383
+ context.facts.email = "";
384
+ context.facts.subject = "";
385
+ context.facts.message = "";
386
+ context.facts.touched = {};
387
+ context.facts.status = "idle";
388
+ context.facts.errorMessage = "";
389
+ log("Form reset");
390
+ },
391
+ },
392
+ },
393
+
394
+ effects: {
395
+ logSubmission: {
396
+ deps: ["status", "submissionCount"],
397
+ run: (facts, prev) => {
398
+ if (!prev) {
399
+ return;
400
+ }
401
+
402
+ if (facts.status !== prev.status) {
403
+ log(`Status: ${prev.status} → ${facts.status}`);
404
+ }
405
+ },
406
+ },
407
+ },
408
+ });
409
+
410
+ // ============================================================================
411
+ // System
412
+ // ============================================================================
413
+
414
+ export const system = createSystem({
415
+ module: contactForm,
416
+ trace: true,
417
+ plugins: [devtoolsPlugin({ name: "contact-form" })],
418
+ });
419
+ ```
420
+
421
+ ## newsletter
422
+
423
+ ```typescript
424
+ // Example: newsletter
425
+ // Source: examples/newsletter/src/module.ts
426
+ // Pure module file — no DOM wiring
427
+
428
+ /**
429
+ * Newsletter Signup — Directive Module
430
+ *
431
+ * Demonstrates all six primitives with the simplest possible module:
432
+ * - Facts: email, touched, status, errorMessage, lastSubmittedAt
433
+ * - Derivations: emailError (touch-gated), isValid, canSubmit (rate-limited)
434
+ * - Events: updateEmail, touchEmail, submit
435
+ * - Constraints: subscribe (status === 'submitting'), resetAfterSuccess
436
+ * - Resolvers: simulated async subscribe, auto-reset after delay
437
+ * - Effects: logging status transitions
438
+ *
439
+ * Uses a simulated setTimeout instead of a real API so no account is needed.
440
+ */
441
+
442
+ import {
443
+ type ModuleSchema,
444
+ createModule,
445
+ createSystem,
446
+ t,
447
+ } from "@directive-run/core";
448
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
449
+
450
+ // ============================================================================
451
+ // Constants
452
+ // ============================================================================
453
+
454
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
455
+ const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)
456
+
457
+ // ============================================================================
458
+ // Logs (external mutable array, same pattern as fraud-analysis)
459
+ // ============================================================================
460
+
461
+ export const logs: string[] = [];
462
+
463
+ export function addLog(msg: string): void {
464
+ console.log(`[newsletter] ${msg}`);
465
+ logs.push(`${new Date().toLocaleTimeString()}: ${msg}`);
466
+ }
467
+
468
+ // ============================================================================
469
+ // Schema
470
+ // ============================================================================
471
+
472
+ export const schema = {
473
+ facts: {
474
+ email: t.string(),
475
+ touched: t.boolean(),
476
+ status: t.string<"idle" | "submitting" | "success" | "error">(),
477
+ errorMessage: t.string(),
478
+ lastSubmittedAt: t.number(),
479
+ },
480
+ derivations: {
481
+ emailError: t.string(),
482
+ isValid: t.boolean(),
483
+ canSubmit: t.boolean(),
484
+ },
485
+ events: {
486
+ updateEmail: { value: t.string() },
487
+ touchEmail: {},
488
+ submit: {},
489
+ },
490
+ requirements: {
491
+ SUBSCRIBE: {},
492
+ RESET_AFTER_DELAY: {},
493
+ },
494
+ } satisfies ModuleSchema;
495
+
496
+ // ============================================================================
497
+ // Module
498
+ // ============================================================================
499
+
500
+ const newsletter = createModule("newsletter", {
501
+ schema,
502
+
503
+ init: (facts) => {
504
+ facts.email = "";
505
+ facts.touched = false;
506
+ facts.status = "idle";
507
+ facts.errorMessage = "";
508
+ facts.lastSubmittedAt = 0;
509
+ },
510
+
511
+ derive: {
512
+ emailError: (facts) => {
513
+ if (!facts.touched) {
514
+ return "";
515
+ }
516
+ if (!facts.email.trim()) {
517
+ return "Email is required";
518
+ }
519
+ if (!EMAIL_REGEX.test(facts.email)) {
520
+ return "Enter a valid email address";
521
+ }
522
+
523
+ return "";
524
+ },
525
+
526
+ isValid: (facts) => EMAIL_REGEX.test(facts.email),
527
+
528
+ canSubmit: (facts, derived) => {
529
+ if (!derived.isValid) {
530
+ return false;
531
+ }
532
+ if (facts.status !== "idle") {
533
+ return false;
534
+ }
535
+ if (
536
+ facts.lastSubmittedAt > 0 &&
537
+ Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
538
+ ) {
539
+ return false;
540
+ }
541
+
542
+ return true;
543
+ },
544
+ },
545
+
546
+ events: {
547
+ updateEmail: (facts, { value }) => {
548
+ facts.email = value;
549
+ },
550
+
551
+ touchEmail: (facts) => {
552
+ facts.touched = true;
553
+ },
554
+
555
+ submit: (facts) => {
556
+ facts.touched = true;
557
+ facts.status = "submitting";
558
+ },
559
+ },
560
+
561
+ constraints: {
562
+ subscribe: {
563
+ when: (facts) => facts.status === "submitting",
564
+ require: { type: "SUBSCRIBE" },
565
+ },
566
+
567
+ resetAfterSuccess: {
568
+ when: (facts) => facts.status === "success",
569
+ require: { type: "RESET_AFTER_DELAY" },
570
+ },
571
+ },
572
+
573
+ resolvers: {
574
+ // Simulated submission — no API account needed
575
+ subscribe: {
576
+ requirement: "SUBSCRIBE",
577
+ resolve: async (_req, context) => {
578
+ addLog(`Subscribing: ${context.facts.email}`);
579
+
580
+ // Simulate network delay
581
+ await new Promise((resolve) => setTimeout(resolve, 1500));
582
+
583
+ // Simulate occasional failure (20% chance)
584
+ if (Math.random() < 0.2) {
585
+ context.facts.status = "error";
586
+ context.facts.errorMessage =
587
+ "Simulated error — try again (20% failure rate for demo).";
588
+ addLog("Subscription failed (simulated)");
589
+
590
+ return;
591
+ }
592
+
593
+ context.facts.status = "success";
594
+ context.facts.lastSubmittedAt = Date.now();
595
+ addLog("Subscription succeeded");
596
+ },
597
+ },
598
+
599
+ resetAfterDelay: {
600
+ requirement: "RESET_AFTER_DELAY",
601
+ resolve: async (_req, context) => {
602
+ addLog("Auto-resetting in 5 seconds...");
603
+ await new Promise((resolve) => setTimeout(resolve, 5000));
604
+ context.facts.email = "";
605
+ context.facts.touched = false;
606
+ context.facts.status = "idle";
607
+ context.facts.errorMessage = "";
608
+ addLog("Form reset");
609
+ },
610
+ },
611
+ },
612
+
613
+ effects: {
614
+ logSubscription: {
615
+ deps: ["status"],
616
+ run: (facts, prev) => {
617
+ if (!prev) {
618
+ return;
619
+ }
620
+
621
+ if (facts.status !== prev.status) {
622
+ addLog(`Status: ${prev.status} → ${facts.status}`);
623
+ }
624
+ },
625
+ },
626
+ },
627
+ });
628
+
629
+ // ============================================================================
630
+ // System
631
+ // ============================================================================
632
+
633
+ export const system = createSystem({
634
+ module: newsletter,
635
+ trace: true,
636
+ plugins: [devtoolsPlugin({ name: "newsletter" })],
637
+ });
638
+ ```
639
+
640
+ ## feature-flags
641
+
642
+ ```typescript
643
+ // Example: feature-flags
644
+ // Source: examples/feature-flags/src/module.ts
645
+ // Pure module file — no DOM wiring
646
+
647
+ /**
648
+ * Feature Flags Directive Module (Example)
649
+ *
650
+ * Mirrors the real feature flag system running on directive.run.
651
+ * 8 flags with two interaction patterns:
652
+ *
653
+ * 1. Maintenance mode &ndash; disables chat, search, playground, and vote API
654
+ * 2. Onboarding toast &rarr; depends on brand switcher (constraint auto-enables)
655
+ */
656
+ import { createModule, t } from "@directive-run/core";
657
+
658
+ export const featureFlagsModule = createModule("feature-flags", {
659
+ schema: {
660
+ facts: {
661
+ // Individual feature toggles
662
+ chatEnabled: t.boolean(),
663
+ searchEnabled: t.boolean(),
664
+ playgroundEnabled: t.boolean(),
665
+ brandSwitcherEnabled: t.boolean(),
666
+ themeSelectorEnabled: t.boolean(),
667
+ onboardingToastEnabled: t.boolean(),
668
+ versionSelectorEnabled: t.boolean(),
669
+ voteApiEnabled: t.boolean(),
670
+
671
+ // Context
672
+ maintenanceMode: t.boolean(),
673
+ },
674
+ derivations: {
675
+ canUseChat: t.boolean(),
676
+ canUseSearch: t.boolean(),
677
+ canUsePlayground: t.boolean(),
678
+ canUseBrandSwitcher: t.boolean(),
679
+ canUseThemeSelector: t.boolean(),
680
+ canShowOnboardingToast: t.boolean(),
681
+ canUseVersionSelector: t.boolean(),
682
+ canUseVoteApi: t.boolean(),
683
+ enabledCount: t.number(),
684
+ allFeaturesEnabled: t.boolean(),
685
+ },
686
+ events: {
687
+ configure: {
688
+ chatEnabled: t.boolean(),
689
+ searchEnabled: t.boolean(),
690
+ playgroundEnabled: t.boolean(),
691
+ brandSwitcherEnabled: t.boolean(),
692
+ themeSelectorEnabled: t.boolean(),
693
+ onboardingToastEnabled: t.boolean(),
694
+ versionSelectorEnabled: t.boolean(),
695
+ voteApiEnabled: t.boolean(),
696
+ },
697
+ setMaintenanceMode: { enabled: t.boolean() },
698
+ toggleFlag: { flag: t.string(), enabled: t.boolean() },
699
+ resetAll: {},
700
+ },
701
+ requirements: {
702
+ ENABLE_BRAND_SWITCHER: {},
703
+ LOG_MAINTENANCE_WARNING: {},
704
+ },
705
+ },
706
+
707
+ init: (facts) => {
708
+ facts.chatEnabled = true;
709
+ facts.searchEnabled = true;
710
+ facts.playgroundEnabled = true;
711
+ facts.brandSwitcherEnabled = true;
712
+ facts.themeSelectorEnabled = true;
713
+ facts.onboardingToastEnabled = true;
714
+ facts.versionSelectorEnabled = true;
715
+ facts.voteApiEnabled = true;
716
+
717
+ facts.maintenanceMode = false;
718
+ },
719
+
720
+ derive: {
721
+ canUseChat: (facts) => facts.chatEnabled && !facts.maintenanceMode,
722
+ canUseSearch: (facts) => facts.searchEnabled && !facts.maintenanceMode,
723
+ canUsePlayground: (facts) =>
724
+ facts.playgroundEnabled && !facts.maintenanceMode,
725
+ canUseBrandSwitcher: (facts) => facts.brandSwitcherEnabled,
726
+ canUseThemeSelector: (facts) => facts.themeSelectorEnabled,
727
+ canShowOnboardingToast: (facts) =>
728
+ facts.onboardingToastEnabled && facts.brandSwitcherEnabled,
729
+ canUseVersionSelector: (facts) => facts.versionSelectorEnabled,
730
+ canUseVoteApi: (facts) => facts.voteApiEnabled && !facts.maintenanceMode,
731
+ enabledCount: (facts) => {
732
+ let count = 0;
733
+ if (facts.chatEnabled) count++;
734
+ if (facts.searchEnabled) count++;
735
+ if (facts.playgroundEnabled) count++;
736
+ if (facts.brandSwitcherEnabled) count++;
737
+ if (facts.themeSelectorEnabled) count++;
738
+ if (facts.onboardingToastEnabled) count++;
739
+ if (facts.versionSelectorEnabled) count++;
740
+ if (facts.voteApiEnabled) count++;
741
+
742
+ return count;
743
+ },
744
+ allFeaturesEnabled: (facts) =>
745
+ facts.chatEnabled &&
746
+ facts.searchEnabled &&
747
+ facts.playgroundEnabled &&
748
+ facts.brandSwitcherEnabled &&
749
+ facts.themeSelectorEnabled &&
750
+ facts.onboardingToastEnabled &&
751
+ facts.versionSelectorEnabled &&
752
+ facts.voteApiEnabled,
753
+ },
754
+
755
+ events: {
756
+ configure: (facts, payload) => {
757
+ facts.chatEnabled = payload.chatEnabled;
758
+ facts.searchEnabled = payload.searchEnabled;
759
+ facts.playgroundEnabled = payload.playgroundEnabled;
760
+ facts.brandSwitcherEnabled = payload.brandSwitcherEnabled;
761
+ facts.themeSelectorEnabled = payload.themeSelectorEnabled;
762
+ facts.onboardingToastEnabled = payload.onboardingToastEnabled;
763
+ facts.versionSelectorEnabled = payload.versionSelectorEnabled;
764
+ facts.voteApiEnabled = payload.voteApiEnabled;
765
+ },
766
+
767
+ setMaintenanceMode: (facts, { enabled }) => {
768
+ facts.maintenanceMode = enabled;
769
+ },
770
+
771
+ toggleFlag: (facts, { flag, enabled }) => {
772
+ const key = flag as keyof typeof facts;
773
+ if (key in facts && typeof facts[key] === "boolean") {
774
+ (facts as Record<string, boolean>)[key] = enabled;
775
+ }
776
+ },
777
+
778
+ resetAll: (facts) => {
779
+ facts.chatEnabled = true;
780
+ facts.searchEnabled = true;
781
+ facts.playgroundEnabled = true;
782
+ facts.brandSwitcherEnabled = true;
783
+ facts.themeSelectorEnabled = true;
784
+ facts.onboardingToastEnabled = true;
785
+ facts.versionSelectorEnabled = true;
786
+ facts.voteApiEnabled = true;
787
+ facts.maintenanceMode = false;
788
+ },
789
+ },
790
+
791
+ constraints: {
792
+ onboardingRequiresBrandSwitcher: {
793
+ when: (facts) =>
794
+ facts.onboardingToastEnabled && !facts.brandSwitcherEnabled,
795
+ require: { type: "ENABLE_BRAND_SWITCHER" },
796
+ },
797
+
798
+ maintenanceWarning: {
799
+ when: (facts) => facts.maintenanceMode,
800
+ require: { type: "LOG_MAINTENANCE_WARNING" },
801
+ },
802
+ },
803
+
804
+ resolvers: {
805
+ enableBrandSwitcher: {
806
+ requirement: "ENABLE_BRAND_SWITCHER",
807
+ resolve: async (_req, context) => {
808
+ context.facts.brandSwitcherEnabled = true;
809
+ },
810
+ },
811
+
812
+ logMaintenanceWarning: {
813
+ requirement: "LOG_MAINTENANCE_WARNING",
814
+ resolve: async (_req, _context) => {
815
+ console.warn(
816
+ "[feature-flags] Maintenance mode is active. Chat, search, playground, and vote API are disabled.",
817
+ );
818
+ },
819
+ },
820
+ },
821
+
822
+ effects: {
823
+ logChanges: {
824
+ deps: [
825
+ "chatEnabled",
826
+ "searchEnabled",
827
+ "playgroundEnabled",
828
+ "brandSwitcherEnabled",
829
+ "themeSelectorEnabled",
830
+ "onboardingToastEnabled",
831
+ "versionSelectorEnabled",
832
+ "voteApiEnabled",
833
+ "maintenanceMode",
834
+ ],
835
+ run: (facts, prev) => {
836
+ if (!prev) {
837
+ return;
838
+ }
839
+
840
+ const flags = [
841
+ "chatEnabled",
842
+ "searchEnabled",
843
+ "playgroundEnabled",
844
+ "brandSwitcherEnabled",
845
+ "themeSelectorEnabled",
846
+ "onboardingToastEnabled",
847
+ "versionSelectorEnabled",
848
+ "voteApiEnabled",
849
+ "maintenanceMode",
850
+ ] as const;
851
+
852
+ for (const flag of flags) {
853
+ if (facts[flag] !== prev[flag]) {
854
+ console.log(
855
+ `[feature-flags] ${flag}: ${prev[flag]} → ${facts[flag]}`,
856
+ );
857
+ }
858
+ }
859
+ },
860
+ },
861
+ },
862
+ });
863
+ ```
864
+
865
+ ## shopping-cart
866
+
867
+ ```typescript
868
+ // Example: shopping-cart
869
+ // Source: examples/shopping-cart/src/shopping-cart.ts
870
+ // Pure module file — no DOM wiring
871
+
872
+ /**
873
+ * Shopping Cart — Directive Modules
874
+ *
875
+ * Two modules:
876
+ * - cart: Items, coupons, checkout with cross-module auth dependency
877
+ * - auth: Simple authentication toggle for demo purposes
878
+ *
879
+ * Demonstrates cross-module constraints (`crossModuleDeps`),
880
+ * constraint ordering (`after`), priority-based resolution,
881
+ * and retry with exponential backoff.
882
+ */
883
+
884
+ import {
885
+ type ModuleSchema,
886
+ createModule,
887
+ createSystem,
888
+ t,
889
+ } from "@directive-run/core";
890
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
891
+ import { processCheckout, validateCoupon } from "./mock-api.js";
892
+
893
+ // ============================================================================
894
+ // Types
895
+ // ============================================================================
896
+
897
+ export interface CartItem {
898
+ id: string;
899
+ name: string;
900
+ price: number;
901
+ quantity: number;
902
+ maxStock: number;
903
+ image: string;
904
+ }
905
+
906
+ // ============================================================================
907
+ // Auth Module
908
+ // ============================================================================
909
+
910
+ export const authSchema = {
911
+ facts: {
912
+ isAuthenticated: t.boolean(),
913
+ userName: t.string(),
914
+ },
915
+ derivations: {
916
+ isAuthenticated: t.boolean(),
917
+ },
918
+ events: {
919
+ toggleAuth: {},
920
+ },
921
+ requirements: {},
922
+ } satisfies ModuleSchema;
923
+
924
+ export const authModule = createModule("auth", {
925
+ schema: authSchema,
926
+
927
+ init: (facts) => {
928
+ facts.isAuthenticated = true;
929
+ facts.userName = "Demo User";
930
+ },
931
+
932
+ derive: {
933
+ isAuthenticated: (facts) => facts.isAuthenticated,
934
+ },
935
+
936
+ events: {
937
+ toggleAuth: (facts) => {
938
+ facts.isAuthenticated = !facts.isAuthenticated;
939
+ if (!facts.isAuthenticated) {
940
+ facts.userName = "";
941
+ } else {
942
+ facts.userName = "Demo User";
943
+ }
944
+ },
945
+ },
946
+ });
947
+
948
+ // ============================================================================
949
+ // Cart Module
950
+ // ============================================================================
951
+
952
+ export const cartSchema = {
953
+ facts: {
954
+ items: t.array<CartItem>(),
955
+ couponCode: t.string(),
956
+ couponDiscount: t.number(),
957
+ couponStatus: t.string<"idle" | "checking" | "valid" | "invalid">(),
958
+ checkoutRequested: t.boolean(),
959
+ checkoutStatus: t.string<"idle" | "processing" | "complete" | "failed">(),
960
+ checkoutError: t.string(),
961
+ },
962
+ derivations: {
963
+ subtotal: t.number(),
964
+ itemCount: t.number(),
965
+ isEmpty: t.boolean(),
966
+ discount: t.number(),
967
+ tax: t.number(),
968
+ total: t.number(),
969
+ hasOverstockedItem: t.boolean(),
970
+ freeShipping: t.boolean(),
971
+ },
972
+ events: {
973
+ addItem: {
974
+ id: t.string(),
975
+ name: t.string(),
976
+ price: t.number(),
977
+ maxStock: t.number(),
978
+ image: t.string(),
979
+ },
980
+ removeItem: { id: t.string() },
981
+ updateQuantity: { id: t.string(), quantity: t.number() },
982
+ applyCoupon: { code: t.string() },
983
+ clearCoupon: {},
984
+ requestCheckout: {},
985
+ resetCheckout: {},
986
+ },
987
+ requirements: {
988
+ ADJUST_QUANTITY: {},
989
+ VALIDATE_COUPON: { code: t.string() },
990
+ PROCESS_CHECKOUT: {},
991
+ },
992
+ } satisfies ModuleSchema;
993
+
994
+ export const cartModule = createModule("cart", {
995
+ schema: cartSchema,
996
+
997
+ crossModuleDeps: { auth: authSchema },
998
+
999
+ init: (facts) => {
1000
+ facts.items = [
1001
+ {
1002
+ id: "headphones-1",
1003
+ name: "Wireless Headphones",
1004
+ price: 79.99,
1005
+ quantity: 1,
1006
+ maxStock: 5,
1007
+ image: "headphones",
1008
+ },
1009
+ {
1010
+ id: "keyboard-1",
1011
+ name: "Mechanical Keyboard",
1012
+ price: 129.99,
1013
+ quantity: 1,
1014
+ maxStock: 3,
1015
+ image: "keyboard",
1016
+ },
1017
+ {
1018
+ id: "hub-1",
1019
+ name: "USB-C Hub",
1020
+ price: 49.99,
1021
+ quantity: 2,
1022
+ maxStock: 10,
1023
+ image: "hub",
1024
+ },
1025
+ ];
1026
+ facts.couponCode = "";
1027
+ facts.couponDiscount = 0;
1028
+ facts.couponStatus = "idle";
1029
+ facts.checkoutRequested = false;
1030
+ facts.checkoutStatus = "idle";
1031
+ facts.checkoutError = "";
1032
+ },
1033
+
1034
+ // ============================================================================
1035
+ // Derivations
1036
+ // ============================================================================
1037
+
1038
+ derive: {
1039
+ subtotal: (facts) => {
1040
+ return facts.self.items.reduce(
1041
+ (sum: number, item: CartItem) => sum + item.price * item.quantity,
1042
+ 0,
1043
+ );
1044
+ },
1045
+
1046
+ itemCount: (facts) => {
1047
+ return facts.self.items.reduce(
1048
+ (sum: number, item: CartItem) => sum + item.quantity,
1049
+ 0,
1050
+ );
1051
+ },
1052
+
1053
+ isEmpty: (facts) => {
1054
+ return facts.self.items.length === 0;
1055
+ },
1056
+
1057
+ discount: (facts, derived) => {
1058
+ const sub = derived.subtotal;
1059
+
1060
+ return sub * (facts.self.couponDiscount / 100);
1061
+ },
1062
+
1063
+ tax: (_facts, derived) => {
1064
+ const sub = derived.subtotal;
1065
+ const disc = derived.discount;
1066
+
1067
+ return (sub - disc) * 0.08;
1068
+ },
1069
+
1070
+ total: (_facts, derived) => {
1071
+ const sub = derived.subtotal;
1072
+ const disc = derived.discount;
1073
+ const tx = derived.tax;
1074
+
1075
+ return sub - disc + tx;
1076
+ },
1077
+
1078
+ hasOverstockedItem: (facts) => {
1079
+ return facts.self.items.some(
1080
+ (item: CartItem) => item.quantity > item.maxStock,
1081
+ );
1082
+ },
1083
+
1084
+ freeShipping: (_facts, derived) => {
1085
+ const sub = derived.subtotal;
1086
+
1087
+ return sub >= 75;
1088
+ },
1089
+ },
1090
+
1091
+ // ============================================================================
1092
+ // Events
1093
+ // ============================================================================
1094
+
1095
+ events: {
1096
+ addItem: (facts, { id, name, price, maxStock, image }) => {
1097
+ const existing = facts.items.find((item: CartItem) => item.id === id);
1098
+ if (existing) {
1099
+ facts.items = facts.items.map((item: CartItem) =>
1100
+ item.id === id
1101
+ ? { ...item, quantity: Math.min(item.quantity + 1, item.maxStock) }
1102
+ : item,
1103
+ );
1104
+ } else {
1105
+ facts.items = [
1106
+ ...facts.items,
1107
+ { id, name, price, quantity: 1, maxStock, image },
1108
+ ];
1109
+ }
1110
+ },
1111
+
1112
+ removeItem: (facts, { id }) => {
1113
+ facts.items = facts.items.filter((item: CartItem) => item.id !== id);
1114
+ },
1115
+
1116
+ updateQuantity: (facts, { id, quantity }) => {
1117
+ if (quantity <= 0) {
1118
+ facts.items = facts.items.filter((item: CartItem) => item.id !== id);
1119
+
1120
+ return;
1121
+ }
1122
+
1123
+ facts.items = facts.items.map((item: CartItem) =>
1124
+ item.id === id ? { ...item, quantity } : item,
1125
+ );
1126
+ },
1127
+
1128
+ applyCoupon: (facts, { code }) => {
1129
+ facts.couponCode = code;
1130
+ facts.couponStatus = "idle";
1131
+ facts.couponDiscount = 0;
1132
+ },
1133
+
1134
+ clearCoupon: (facts) => {
1135
+ facts.couponCode = "";
1136
+ facts.couponDiscount = 0;
1137
+ facts.couponStatus = "idle";
1138
+ },
1139
+
1140
+ requestCheckout: (facts) => {
1141
+ facts.checkoutRequested = true;
1142
+ facts.checkoutStatus = "idle";
1143
+ facts.checkoutError = "";
1144
+ },
1145
+
1146
+ resetCheckout: (facts) => {
1147
+ facts.checkoutRequested = false;
1148
+ facts.checkoutStatus = "idle";
1149
+ facts.checkoutError = "";
1150
+ },
1151
+ },
1152
+
1153
+ // ============================================================================
1154
+ // Constraints
1155
+ // ============================================================================
1156
+
1157
+ constraints: {
1158
+ quantityLimit: {
1159
+ priority: 80,
1160
+ when: (facts) => {
1161
+ const hasOverstocked = facts.self.items.some(
1162
+ (item: CartItem) => item.quantity > item.maxStock,
1163
+ );
1164
+
1165
+ return hasOverstocked;
1166
+ },
1167
+ require: { type: "ADJUST_QUANTITY" },
1168
+ },
1169
+
1170
+ couponValidation: {
1171
+ priority: 70,
1172
+ when: (facts) => {
1173
+ return (
1174
+ facts.self.couponCode !== "" && facts.self.couponStatus === "idle"
1175
+ );
1176
+ },
1177
+ require: (facts) => ({
1178
+ type: "VALIDATE_COUPON",
1179
+ code: facts.self.couponCode,
1180
+ }),
1181
+ },
1182
+
1183
+ checkoutReady: {
1184
+ priority: 60,
1185
+ after: ["quantityLimit", "couponValidation"],
1186
+ when: (facts) => {
1187
+ const items = facts.self.items;
1188
+ const notEmpty = items.length > 0;
1189
+ const noOverstock = !items.some(
1190
+ (item: CartItem) => item.quantity > item.maxStock,
1191
+ );
1192
+
1193
+ return (
1194
+ facts.self.checkoutRequested === true &&
1195
+ notEmpty &&
1196
+ noOverstock &&
1197
+ facts.auth.isAuthenticated === true
1198
+ );
1199
+ },
1200
+ require: { type: "PROCESS_CHECKOUT" },
1201
+ },
1202
+ },
1203
+
1204
+ // ============================================================================
1205
+ // Resolvers
1206
+ // ============================================================================
1207
+
1208
+ resolvers: {
1209
+ adjustQuantity: {
1210
+ requirement: "ADJUST_QUANTITY",
1211
+ resolve: async (_req, context) => {
1212
+ context.facts.items = context.facts.items.map((item: CartItem) => {
1213
+ if (item.quantity > item.maxStock) {
1214
+ return { ...item, quantity: item.maxStock };
1215
+ }
1216
+
1217
+ return item;
1218
+ });
1219
+ },
1220
+ },
1221
+
1222
+ validateCoupon: {
1223
+ requirement: "VALIDATE_COUPON",
1224
+ key: (req) => `coupon-${req.code}`,
1225
+ resolve: async (req, context) => {
1226
+ context.facts.couponStatus = "checking";
1227
+
1228
+ const result = await validateCoupon(req.code);
1229
+
1230
+ if (result.valid) {
1231
+ context.facts.couponDiscount = result.discount;
1232
+ context.facts.couponStatus = "valid";
1233
+ } else {
1234
+ context.facts.couponDiscount = 0;
1235
+ context.facts.couponStatus = "invalid";
1236
+ }
1237
+ },
1238
+ },
1239
+
1240
+ processCheckout: {
1241
+ requirement: "PROCESS_CHECKOUT",
1242
+ retry: { attempts: 2, backoff: "exponential" },
1243
+ resolve: async (_req, context) => {
1244
+ context.facts.checkoutStatus = "processing";
1245
+
1246
+ try {
1247
+ const items = context.facts.items.map((item: CartItem) => ({
1248
+ id: item.id,
1249
+ name: item.name,
1250
+ quantity: item.quantity,
1251
+ price: item.price,
1252
+ }));
1253
+
1254
+ await processCheckout(items, context.facts.couponCode);
1255
+
1256
+ context.facts.checkoutStatus = "complete";
1257
+ context.facts.items = [];
1258
+ context.facts.couponCode = "";
1259
+ context.facts.couponDiscount = 0;
1260
+ context.facts.couponStatus = "idle";
1261
+ context.facts.checkoutRequested = false;
1262
+ } catch (err) {
1263
+ const msg = err instanceof Error ? err.message : "Checkout failed";
1264
+ context.facts.checkoutStatus = "failed";
1265
+ context.facts.checkoutError = msg;
1266
+ context.facts.checkoutRequested = false;
1267
+ throw err;
1268
+ }
1269
+ },
1270
+ },
1271
+ },
1272
+ });
1273
+
1274
+ // ============================================================================
1275
+ // System
1276
+ // ============================================================================
1277
+
1278
+ export const system = createSystem({
1279
+ modules: {
1280
+ cart: cartModule,
1281
+ auth: authModule,
1282
+ },
1283
+ plugins: [devtoolsPlugin({ name: "shopping-cart", panel: true })],
1284
+ history: { maxSnapshots: 50 },
1285
+ trace: true,
1286
+ });
1287
+ ```
1288
+
1289
+ ## form-wizard
1290
+
1291
+ ```typescript
1292
+ // Example: form-wizard
1293
+ // Source: examples/form-wizard/src/form-wizard.ts
1294
+ // Pure module file — no DOM wiring
1295
+
1296
+ /**
1297
+ * Form Wizard — Directive Modules
1298
+ *
1299
+ * Two-module system demonstrating multi-step form validation,
1300
+ * constraint-driven step advancement, cross-module async email
1301
+ * availability checking, and persistence of draft data.
1302
+ *
1303
+ * - wizard module: step navigation, field data, derivations for per-step
1304
+ * validity, constraints to advance/submit, resolvers for step transitions.
1305
+ * - validation module: cross-module email availability check using
1306
+ * crossModuleDeps on the wizard schema.
1307
+ */
1308
+
1309
+ import {
1310
+ type ModuleSchema,
1311
+ createModule,
1312
+ createSystem,
1313
+ t,
1314
+ } from "@directive-run/core";
1315
+ import { devtoolsPlugin, persistencePlugin } from "@directive-run/core/plugins";
1316
+
1317
+ // ============================================================================
1318
+ // Types
1319
+ // ============================================================================
1320
+
1321
+ export type PlanType = "free" | "pro" | "enterprise";
1322
+
1323
+ // ============================================================================
1324
+ // Wizard Schema
1325
+ // ============================================================================
1326
+
1327
+ export const wizardSchema = {
1328
+ facts: {
1329
+ currentStep: t.number(),
1330
+ totalSteps: t.number(),
1331
+ advanceRequested: t.boolean(),
1332
+ email: t.string(),
1333
+ password: t.string(),
1334
+ name: t.string(),
1335
+ company: t.string(),
1336
+ plan: t.string<PlanType>(),
1337
+ newsletter: t.boolean(),
1338
+ submitted: t.boolean(),
1339
+ },
1340
+ derivations: {
1341
+ step0Valid: t.boolean(),
1342
+ step1Valid: t.boolean(),
1343
+ step2Valid: t.boolean(),
1344
+ currentStepValid: t.boolean(),
1345
+ canAdvance: t.boolean(),
1346
+ canGoBack: t.boolean(),
1347
+ progress: t.number(),
1348
+ isLastStep: t.boolean(),
1349
+ },
1350
+ events: {
1351
+ requestAdvance: {},
1352
+ goBack: {},
1353
+ setField: { field: t.string(), value: t.object<unknown>() },
1354
+ reset: {},
1355
+ },
1356
+ requirements: {
1357
+ ADVANCE_STEP: {},
1358
+ SUBMIT_FORM: {},
1359
+ },
1360
+ } satisfies ModuleSchema;
1361
+
1362
+ // ============================================================================
1363
+ // Helpers
1364
+ // ============================================================================
1365
+
1366
+ /** Inline step validity check for use in constraints (which only receive facts). */
1367
+ function isStepValid(facts: Record<string, unknown>, step: number): boolean {
1368
+ if (step === 0) {
1369
+ return (
1370
+ (facts.email as string).includes("@") &&
1371
+ (facts.password as string).length >= 8
1372
+ );
1373
+ }
1374
+ if (step === 1) {
1375
+ return (facts.name as string).trim().length > 0;
1376
+ }
1377
+ if (step === 2) {
1378
+ return (facts.plan as string) !== "";
1379
+ }
1380
+
1381
+ return false;
1382
+ }
1383
+
1384
+ // ============================================================================
1385
+ // Wizard Module
1386
+ // ============================================================================
1387
+
1388
+ export const wizardModule = createModule("wizard", {
1389
+ schema: wizardSchema,
1390
+
1391
+ init: (facts) => {
1392
+ facts.currentStep = 0;
1393
+ facts.totalSteps = 3;
1394
+ facts.advanceRequested = false;
1395
+ facts.email = "";
1396
+ facts.password = "";
1397
+ facts.name = "";
1398
+ facts.company = "";
1399
+ facts.plan = "free";
1400
+ facts.newsletter = false;
1401
+ facts.submitted = false;
1402
+ },
1403
+
1404
+ // ============================================================================
1405
+ // Derivations
1406
+ // ============================================================================
1407
+
1408
+ derive: {
1409
+ step0Valid: (facts) => {
1410
+ return facts.email.includes("@") && facts.password.length >= 8;
1411
+ },
1412
+
1413
+ step1Valid: (facts) => {
1414
+ return facts.name.trim().length > 0;
1415
+ },
1416
+
1417
+ step2Valid: (facts) => {
1418
+ return facts.plan !== "";
1419
+ },
1420
+
1421
+ currentStepValid: (facts, derived) => {
1422
+ if (facts.currentStep === 0) {
1423
+ return derived.step0Valid;
1424
+ }
1425
+ if (facts.currentStep === 1) {
1426
+ return derived.step1Valid;
1427
+ }
1428
+ if (facts.currentStep === 2) {
1429
+ return derived.step2Valid;
1430
+ }
1431
+
1432
+ return false;
1433
+ },
1434
+
1435
+ canAdvance: (facts, derived) => {
1436
+ return (
1437
+ derived.currentStepValid && facts.currentStep < facts.totalSteps - 1
1438
+ );
1439
+ },
1440
+
1441
+ canGoBack: (facts) => {
1442
+ return facts.currentStep > 0;
1443
+ },
1444
+
1445
+ progress: (facts) => {
1446
+ return Math.round(((facts.currentStep + 1) / facts.totalSteps) * 100);
1447
+ },
1448
+
1449
+ isLastStep: (facts) => {
1450
+ return facts.currentStep === facts.totalSteps - 1;
1451
+ },
1452
+ },
1453
+
1454
+ // ============================================================================
1455
+ // Events
1456
+ // ============================================================================
1457
+
1458
+ events: {
1459
+ requestAdvance: (facts) => {
1460
+ facts.advanceRequested = true;
1461
+ },
1462
+
1463
+ goBack: (facts) => {
1464
+ if (facts.currentStep > 0) {
1465
+ facts.currentStep = facts.currentStep - 1;
1466
+ }
1467
+ },
1468
+
1469
+ setField: (facts, { field, value }) => {
1470
+ (facts as Record<string, unknown>)[field] = value;
1471
+ },
1472
+
1473
+ reset: (facts) => {
1474
+ facts.currentStep = 0;
1475
+ facts.advanceRequested = false;
1476
+ facts.email = "";
1477
+ facts.password = "";
1478
+ facts.name = "";
1479
+ facts.company = "";
1480
+ facts.plan = "free";
1481
+ facts.newsletter = false;
1482
+ facts.submitted = false;
1483
+ },
1484
+ },
1485
+
1486
+ // ============================================================================
1487
+ // Constraints
1488
+ // ============================================================================
1489
+
1490
+ constraints: {
1491
+ submit: {
1492
+ priority: 60,
1493
+ when: (facts) => {
1494
+ const isLastStep = facts.currentStep === facts.totalSteps - 1;
1495
+ const stepValid = isStepValid(facts, facts.currentStep);
1496
+
1497
+ return facts.advanceRequested && isLastStep && stepValid;
1498
+ },
1499
+ require: { type: "SUBMIT_FORM" },
1500
+ },
1501
+
1502
+ advance: {
1503
+ priority: 50,
1504
+ when: (facts) => {
1505
+ const isLastStep = facts.currentStep === facts.totalSteps - 1;
1506
+ const stepValid = isStepValid(facts, facts.currentStep);
1507
+
1508
+ return facts.advanceRequested && !isLastStep && stepValid;
1509
+ },
1510
+ require: { type: "ADVANCE_STEP" },
1511
+ },
1512
+ },
1513
+
1514
+ // ============================================================================
1515
+ // Resolvers
1516
+ // ============================================================================
1517
+
1518
+ resolvers: {
1519
+ advanceStep: {
1520
+ requirement: "ADVANCE_STEP",
1521
+ resolve: async (_req, context) => {
1522
+ context.facts.currentStep = context.facts.currentStep + 1;
1523
+ context.facts.advanceRequested = false;
1524
+ },
1525
+ },
1526
+
1527
+ submitForm: {
1528
+ requirement: "SUBMIT_FORM",
1529
+ timeout: 10000,
1530
+ resolve: async (_req, context) => {
1531
+ // Simulate API submission
1532
+ await new Promise((resolve) => setTimeout(resolve, 800));
1533
+ context.facts.submitted = true;
1534
+ context.facts.advanceRequested = false;
1535
+ },
1536
+ },
1537
+ },
1538
+ });
1539
+
1540
+ // ============================================================================
1541
+ // Validation Schema
1542
+ // ============================================================================
1543
+
1544
+ export const validationSchema = {
1545
+ facts: {
1546
+ emailAvailable: t.boolean(),
1547
+ checkingEmail: t.boolean(),
1548
+ emailChecked: t.string(),
1549
+ },
1550
+ derivations: {},
1551
+ events: {},
1552
+ requirements: {
1553
+ CHECK_EMAIL: { email: t.string() },
1554
+ },
1555
+ } satisfies ModuleSchema;
1556
+
1557
+ // ============================================================================
1558
+ // Validation Module
1559
+ // ============================================================================
1560
+
1561
+ export const validationModule = createModule("validation", {
1562
+ schema: validationSchema,
1563
+
1564
+ crossModuleDeps: { wizard: wizardSchema },
1565
+
1566
+ init: (facts) => {
1567
+ facts.emailAvailable = true;
1568
+ facts.checkingEmail = false;
1569
+ facts.emailChecked = "";
1570
+ },
1571
+
1572
+ // ============================================================================
1573
+ // Constraints
1574
+ // ============================================================================
1575
+
1576
+ constraints: {
1577
+ checkEmail: {
1578
+ when: (facts) => {
1579
+ const email = facts.wizard.email;
1580
+ const checked = facts.self.emailChecked;
1581
+
1582
+ return email.includes("@") && email !== checked;
1583
+ },
1584
+ require: (facts) => ({
1585
+ type: "CHECK_EMAIL",
1586
+ email: facts.wizard.email,
1587
+ }),
1588
+ },
1589
+ },
1590
+
1591
+ // ============================================================================
1592
+ // Resolvers
1593
+ // ============================================================================
1594
+
1595
+ resolvers: {
1596
+ checkEmail: {
1597
+ requirement: "CHECK_EMAIL",
1598
+ resolve: async (req, context) => {
1599
+ context.facts.checkingEmail = true;
1600
+
1601
+ try {
1602
+ // Simulate API availability check
1603
+ await new Promise((resolve) => setTimeout(resolve, 500));
1604
+ context.facts.emailAvailable = req.email !== "taken@test.com";
1605
+ context.facts.emailChecked = req.email;
1606
+ } finally {
1607
+ context.facts.checkingEmail = false;
1608
+ }
1609
+ },
1610
+ },
1611
+ },
1612
+ });
1613
+
1614
+ // ============================================================================
1615
+ // System
1616
+ // ============================================================================
1617
+
1618
+ export const system = createSystem({
1619
+ modules: {
1620
+ wizard: wizardModule,
1621
+ validation: validationModule,
1622
+ },
1623
+ trace: true,
1624
+ plugins: [
1625
+ devtoolsPlugin({ name: "form-wizard" }),
1626
+ persistencePlugin({
1627
+ storage: localStorage,
1628
+ key: "form-wizard-draft",
1629
+ include: [
1630
+ "wizard::email",
1631
+ "wizard::name",
1632
+ "wizard::company",
1633
+ "wizard::plan",
1634
+ "wizard::currentStep",
1635
+ ],
1636
+ }),
1637
+ ],
1638
+ });
1639
+ ```