@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,1361 @@
1
+ # Examples
2
+
3
+ > Auto-generated from extracted examples. Do not edit manually.
4
+
5
+ ## auth-flow
6
+
7
+ ```typescript
8
+ // Example: auth-flow
9
+ // Source: examples/auth-flow/src/auth-flow.ts
10
+ // Pure module file — no DOM wiring
11
+
12
+ /**
13
+ * Auth Flow — Directive Module
14
+ *
15
+ * Demonstrates constraint `after` ordering, auto-tracked derivations
16
+ * driving constraints, resolvers with retry, effects for cleanup,
17
+ * and time-based reactivity (token expiry countdown).
18
+ */
19
+
20
+ import { type ModuleSchema, createModule, t } from "@directive-run/core";
21
+ import {
22
+ type User,
23
+ mockFetchUser,
24
+ mockLogin,
25
+ mockRefresh,
26
+ } from "./mock-auth.js";
27
+
28
+ // ============================================================================
29
+ // Types
30
+ // ============================================================================
31
+
32
+ export type AuthStatus =
33
+ | "idle"
34
+ | "authenticating"
35
+ | "authenticated"
36
+ | "refreshing"
37
+ | "expired";
38
+
39
+ export interface EventLogEntry {
40
+ timestamp: number;
41
+ event: string;
42
+ detail: string;
43
+ }
44
+
45
+ // ============================================================================
46
+ // Schema
47
+ // ============================================================================
48
+
49
+ export const authFlowSchema = {
50
+ facts: {
51
+ email: t.string(),
52
+ password: t.string(),
53
+ token: t.string(),
54
+ refreshToken: t.string(),
55
+ expiresAt: t.number(),
56
+ user: t.object<User | null>(),
57
+ status: t.string<AuthStatus>(),
58
+ loginRequested: t.boolean(),
59
+ now: t.number(),
60
+ tokenTTL: t.number(),
61
+ refreshBuffer: t.number(),
62
+ loginFailRate: t.number(),
63
+ refreshFailRate: t.number(),
64
+ eventLog: t.array<EventLogEntry>(),
65
+ },
66
+ derivations: {
67
+ isAuthenticated: t.boolean(),
68
+ isExpiringSoon: t.boolean(),
69
+ canRefresh: t.boolean(),
70
+ tokenTimeRemaining: t.number(),
71
+ canLogin: t.boolean(),
72
+ },
73
+ events: {
74
+ setEmail: { value: t.string() },
75
+ setPassword: { value: t.string() },
76
+ requestLogin: {},
77
+ logout: {},
78
+ forceExpire: {},
79
+ setTokenTTL: { value: t.number() },
80
+ setRefreshBuffer: { value: t.number() },
81
+ setLoginFailRate: { value: t.number() },
82
+ setRefreshFailRate: { value: t.number() },
83
+ tick: {},
84
+ },
85
+ requirements: {
86
+ LOGIN: { email: t.string(), password: t.string() },
87
+ REFRESH_TOKEN: { refreshToken: t.string() },
88
+ FETCH_USER: { token: t.string() },
89
+ },
90
+ } satisfies ModuleSchema;
91
+
92
+ // ============================================================================
93
+ // Helpers
94
+ // ============================================================================
95
+
96
+ function addLogEntry(facts: any, event: string, detail: string): void {
97
+ const log = [...facts.eventLog];
98
+ log.push({ timestamp: Date.now(), event, detail });
99
+ facts.eventLog = log;
100
+ }
101
+
102
+ // ============================================================================
103
+ // Module
104
+ // ============================================================================
105
+
106
+ export const authFlowModule = createModule("auth-flow", {
107
+ schema: authFlowSchema,
108
+
109
+ init: (facts) => {
110
+ facts.email = "alice@test.com";
111
+ facts.password = "password";
112
+ facts.token = "";
113
+ facts.refreshToken = "";
114
+ facts.expiresAt = 0;
115
+ facts.user = null;
116
+ facts.status = "idle";
117
+ facts.loginRequested = false;
118
+ facts.now = Date.now();
119
+ facts.tokenTTL = 30;
120
+ facts.refreshBuffer = 5;
121
+ facts.loginFailRate = 0;
122
+ facts.refreshFailRate = 0;
123
+ facts.eventLog = [];
124
+ },
125
+
126
+ // ============================================================================
127
+ // Derivations
128
+ // ============================================================================
129
+
130
+ derive: {
131
+ isAuthenticated: (facts) => facts.status === "authenticated",
132
+
133
+ isExpiringSoon: (facts) => {
134
+ if (facts.token === "") {
135
+ return false;
136
+ }
137
+
138
+ return facts.now > facts.expiresAt - facts.refreshBuffer * 1000;
139
+ },
140
+
141
+ canRefresh: (facts) => {
142
+ return facts.refreshToken !== "" && facts.status !== "refreshing";
143
+ },
144
+
145
+ tokenTimeRemaining: (facts) => {
146
+ if (facts.token === "") {
147
+ return 0;
148
+ }
149
+
150
+ return Math.max(0, Math.round((facts.expiresAt - facts.now) / 1000));
151
+ },
152
+
153
+ canLogin: (facts) => {
154
+ return (
155
+ facts.email.trim() !== "" &&
156
+ facts.password.trim() !== "" &&
157
+ (facts.status === "idle" || facts.status === "expired")
158
+ );
159
+ },
160
+ },
161
+
162
+ // ============================================================================
163
+ // Events
164
+ // ============================================================================
165
+
166
+ events: {
167
+ setEmail: (facts, { value }) => {
168
+ facts.email = value;
169
+ },
170
+
171
+ setPassword: (facts, { value }) => {
172
+ facts.password = value;
173
+ },
174
+
175
+ requestLogin: (facts) => {
176
+ facts.loginRequested = true;
177
+ facts.status = "authenticating";
178
+ facts.token = "";
179
+ facts.refreshToken = "";
180
+ facts.expiresAt = 0;
181
+ facts.user = null;
182
+ facts.eventLog = [];
183
+ },
184
+
185
+ logout: (facts) => {
186
+ facts.token = "";
187
+ facts.refreshToken = "";
188
+ facts.expiresAt = 0;
189
+ facts.user = null;
190
+ facts.status = "idle";
191
+ facts.loginRequested = false;
192
+ },
193
+
194
+ forceExpire: (facts) => {
195
+ facts.expiresAt = 0;
196
+ },
197
+
198
+ setTokenTTL: (facts, { value }) => {
199
+ facts.tokenTTL = value;
200
+ },
201
+
202
+ setRefreshBuffer: (facts, { value }) => {
203
+ facts.refreshBuffer = value;
204
+ },
205
+
206
+ setLoginFailRate: (facts, { value }) => {
207
+ facts.loginFailRate = value;
208
+ },
209
+
210
+ setRefreshFailRate: (facts, { value }) => {
211
+ facts.refreshFailRate = value;
212
+ },
213
+
214
+ tick: (facts) => {
215
+ facts.now = Date.now();
216
+ },
217
+ },
218
+
219
+ // ============================================================================
220
+ // Constraints
221
+ // ============================================================================
222
+
223
+ constraints: {
224
+ needsLogin: {
225
+ priority: 100,
226
+ when: (facts) => {
227
+ return facts.loginRequested && facts.status === "authenticating";
228
+ },
229
+ require: (facts) => ({
230
+ type: "LOGIN",
231
+ email: facts.email,
232
+ password: facts.password,
233
+ }),
234
+ },
235
+
236
+ refreshNeeded: {
237
+ priority: 90,
238
+ when: (facts) => {
239
+ const isExpiringSoon =
240
+ facts.token !== "" &&
241
+ facts.now > facts.expiresAt - facts.refreshBuffer * 1000;
242
+ const canRefresh =
243
+ facts.refreshToken !== "" && facts.status !== "refreshing";
244
+
245
+ return isExpiringSoon && canRefresh && facts.status === "authenticated";
246
+ },
247
+ require: (facts) => ({
248
+ type: "REFRESH_TOKEN",
249
+ refreshToken: facts.refreshToken,
250
+ }),
251
+ },
252
+
253
+ needsUser: {
254
+ priority: 80,
255
+ after: ["refreshNeeded"],
256
+ when: (facts) => {
257
+ return (
258
+ facts.token !== "" &&
259
+ facts.user === null &&
260
+ facts.status !== "authenticating"
261
+ );
262
+ },
263
+ require: (facts) => ({
264
+ type: "FETCH_USER",
265
+ token: facts.token,
266
+ }),
267
+ },
268
+ },
269
+
270
+ // ============================================================================
271
+ // Resolvers
272
+ // ============================================================================
273
+
274
+ resolvers: {
275
+ login: {
276
+ requirement: "LOGIN",
277
+ timeout: 10000,
278
+ resolve: async (req, context) => {
279
+ addLogEntry(context.facts, "login", "Authenticating...");
280
+
281
+ try {
282
+ const tokens = await mockLogin(
283
+ req.email,
284
+ req.password,
285
+ context.facts.loginFailRate,
286
+ context.facts.tokenTTL,
287
+ );
288
+ context.facts.token = tokens.token;
289
+ context.facts.refreshToken = tokens.refreshToken;
290
+ context.facts.expiresAt = Date.now() + tokens.expiresIn * 1000;
291
+ context.facts.status = "authenticated";
292
+ context.facts.user = null; // trigger needsUser constraint
293
+ addLogEntry(
294
+ context.facts,
295
+ "login-success",
296
+ `Token: ${tokens.token.slice(0, 12)}...`,
297
+ );
298
+ } catch (err) {
299
+ const msg = err instanceof Error ? err.message : "Unknown error";
300
+ context.facts.status = "idle";
301
+ context.facts.loginRequested = false;
302
+ addLogEntry(context.facts, "login-error", msg);
303
+ throw err;
304
+ }
305
+ },
306
+ },
307
+
308
+ refreshToken: {
309
+ requirement: "REFRESH_TOKEN",
310
+ retry: { attempts: 2, backoff: "exponential" },
311
+ timeout: 10000,
312
+ resolve: async (req, context) => {
313
+ context.facts.status = "refreshing";
314
+ addLogEntry(context.facts, "refresh", "Refreshing token...");
315
+
316
+ try {
317
+ const tokens = await mockRefresh(
318
+ req.refreshToken,
319
+ context.facts.refreshFailRate,
320
+ context.facts.tokenTTL,
321
+ );
322
+ context.facts.token = tokens.token;
323
+ context.facts.refreshToken = tokens.refreshToken;
324
+ context.facts.expiresAt = Date.now() + tokens.expiresIn * 1000;
325
+ context.facts.status = "authenticated";
326
+ addLogEntry(
327
+ context.facts,
328
+ "refresh-success",
329
+ `New token: ${tokens.token.slice(0, 12)}...`,
330
+ );
331
+ } catch (err) {
332
+ const msg = err instanceof Error ? err.message : "Unknown error";
333
+ context.facts.token = "";
334
+ context.facts.refreshToken = "";
335
+ context.facts.expiresAt = 0;
336
+ context.facts.status = "expired";
337
+ addLogEntry(context.facts, "refresh-error", msg);
338
+ throw err;
339
+ }
340
+ },
341
+ },
342
+
343
+ fetchUser: {
344
+ requirement: "FETCH_USER",
345
+ resolve: async (req, context) => {
346
+ addLogEntry(context.facts, "fetch-user", "Fetching user profile...");
347
+
348
+ try {
349
+ const user = await mockFetchUser(req.token);
350
+ context.facts.user = user;
351
+ addLogEntry(
352
+ context.facts,
353
+ "fetch-user-success",
354
+ `${user.name} (${user.role})`,
355
+ );
356
+ } catch (err) {
357
+ const msg = err instanceof Error ? err.message : "Unknown error";
358
+ addLogEntry(context.facts, "fetch-user-error", msg);
359
+ }
360
+ },
361
+ },
362
+ },
363
+
364
+ // ============================================================================
365
+ // Effects
366
+ // ============================================================================
367
+
368
+ effects: {
369
+ logStatusChange: {
370
+ deps: ["status"],
371
+ run: (facts, prev) => {
372
+ if (prev && prev.status !== facts.status) {
373
+ addLogEntry(facts, "status", `${prev.status} → ${facts.status}`);
374
+ }
375
+ },
376
+ },
377
+ },
378
+ });
379
+ ```
380
+
381
+ ## async-chains
382
+
383
+ ```typescript
384
+ // Example: async-chains
385
+ // Source: examples/async-chains/src/async-chains.ts
386
+ // Pure module file — no DOM wiring
387
+
388
+ /**
389
+ * Async Chains — Three Directive Modules
390
+ *
391
+ * Demonstrates cross-module constraint chaining with `after` ordering:
392
+ * Auth → Permissions → Dashboard
393
+ *
394
+ * Each step only fires after the previous step's resolver completes.
395
+ * Cross-module derivations drive the `when()` conditions.
396
+ */
397
+
398
+ import { type ModuleSchema, createModule, t } from "@directive-run/core";
399
+ import {
400
+ type DashboardWidget,
401
+ fetchDashboard,
402
+ fetchPermissions,
403
+ validateSession,
404
+ } from "./mock-api.js";
405
+
406
+ // ============================================================================
407
+ // Auth Module
408
+ // ============================================================================
409
+
410
+ export const authSchema = {
411
+ facts: {
412
+ token: t.string(),
413
+ status: t.string<"idle" | "validating" | "valid" | "expired">(),
414
+ userId: t.string(),
415
+ failRate: t.number(),
416
+ },
417
+ derivations: {
418
+ isValid: t.boolean(),
419
+ },
420
+ events: {
421
+ setToken: { value: t.string() },
422
+ setFailRate: { value: t.number() },
423
+ reset: {},
424
+ },
425
+ requirements: {
426
+ VALIDATE_SESSION: { token: t.string() },
427
+ },
428
+ } satisfies ModuleSchema;
429
+
430
+ export const authModule = createModule("auth", {
431
+ schema: authSchema,
432
+
433
+ init: (facts) => {
434
+ facts.token = "";
435
+ facts.status = "idle";
436
+ facts.userId = "";
437
+ facts.failRate = 0;
438
+ },
439
+
440
+ derive: {
441
+ isValid: (facts) => facts.status === "valid",
442
+ },
443
+
444
+ events: {
445
+ setToken: (facts, { value }) => {
446
+ facts.token = value;
447
+ facts.status = "idle";
448
+ facts.userId = "";
449
+ },
450
+ setFailRate: (facts, { value }) => {
451
+ facts.failRate = value;
452
+ },
453
+ reset: (facts) => {
454
+ facts.token = "";
455
+ facts.status = "idle";
456
+ facts.userId = "";
457
+ },
458
+ },
459
+
460
+ constraints: {
461
+ validateSession: {
462
+ when: (facts) => facts.token !== "" && facts.status === "idle",
463
+ require: (facts) => ({
464
+ type: "VALIDATE_SESSION",
465
+ token: facts.token,
466
+ }),
467
+ },
468
+ },
469
+
470
+ resolvers: {
471
+ validateSession: {
472
+ requirement: "VALIDATE_SESSION",
473
+ key: (req) => `validate-${req.token}`,
474
+ retry: {
475
+ attempts: 2,
476
+ backoff: "exponential",
477
+ initialDelay: 300,
478
+ },
479
+ resolve: async (req, context) => {
480
+ context.facts.status = "validating";
481
+
482
+ try {
483
+ const result = await validateSession(
484
+ req.token,
485
+ context.facts.failRate,
486
+ );
487
+ if (result.valid) {
488
+ context.facts.status = "valid";
489
+ context.facts.userId = result.userId;
490
+ } else {
491
+ context.facts.status = "expired";
492
+ }
493
+ } catch {
494
+ context.facts.status = "expired";
495
+ }
496
+ },
497
+ },
498
+ },
499
+ });
500
+
501
+ // ============================================================================
502
+ // Permissions Module
503
+ // ============================================================================
504
+
505
+ export const permissionsSchema = {
506
+ facts: {
507
+ role: t.string(),
508
+ permissions: t.array<string>(),
509
+ loaded: t.boolean(),
510
+ failRate: t.number(),
511
+ },
512
+ derivations: {
513
+ canEdit: t.boolean(),
514
+ canPublish: t.boolean(),
515
+ canManageUsers: t.boolean(),
516
+ },
517
+ events: {
518
+ setFailRate: { value: t.number() },
519
+ reset: {},
520
+ },
521
+ requirements: {
522
+ LOAD_PERMISSIONS: {},
523
+ },
524
+ } satisfies ModuleSchema;
525
+
526
+ export const permissionsModule = createModule("permissions", {
527
+ schema: permissionsSchema,
528
+
529
+ crossModuleDeps: { auth: authSchema },
530
+
531
+ init: (facts) => {
532
+ facts.role = "";
533
+ facts.permissions = [];
534
+ facts.loaded = false;
535
+ facts.failRate = 0;
536
+ },
537
+
538
+ derive: {
539
+ canEdit: (facts) => facts.self.permissions.includes("write"),
540
+ canPublish: (facts) =>
541
+ facts.self.permissions.includes("write") && facts.self.role !== "viewer",
542
+ canManageUsers: (facts) => facts.self.permissions.includes("manage-users"),
543
+ },
544
+
545
+ events: {
546
+ setFailRate: (facts, { value }) => {
547
+ facts.failRate = value;
548
+ },
549
+ reset: (facts) => {
550
+ facts.role = "";
551
+ facts.permissions = [];
552
+ facts.loaded = false;
553
+ },
554
+ },
555
+
556
+ constraints: {
557
+ loadPermissions: {
558
+ after: ["auth::validateSession"],
559
+ when: (facts) => {
560
+ // Use the fact directly — derivation values aren't available in the
561
+ // facts proxy passed to constraints (they live in the derive layer).
562
+ return facts.auth.status === "valid" && !facts.self.loaded;
563
+ },
564
+ require: { type: "LOAD_PERMISSIONS" },
565
+ },
566
+ },
567
+
568
+ resolvers: {
569
+ loadPermissions: {
570
+ requirement: "LOAD_PERMISSIONS",
571
+ retry: {
572
+ attempts: 2,
573
+ backoff: "exponential",
574
+ initialDelay: 200,
575
+ },
576
+ resolve: async (_req, context) => {
577
+ try {
578
+ const result = await fetchPermissions(context.facts.failRate);
579
+ context.facts.role = result.role;
580
+ context.facts.permissions = result.permissions;
581
+ context.facts.loaded = true;
582
+ } catch {
583
+ context.facts.loaded = false;
584
+ }
585
+ },
586
+ },
587
+ },
588
+ });
589
+
590
+ // ============================================================================
591
+ // Dashboard Module
592
+ // ============================================================================
593
+
594
+ export const dashboardSchema = {
595
+ facts: {
596
+ widgets: t.array<DashboardWidget>(),
597
+ loaded: t.boolean(),
598
+ failRate: t.number(),
599
+ },
600
+ derivations: {
601
+ widgetCount: t.number(),
602
+ },
603
+ events: {
604
+ setFailRate: { value: t.number() },
605
+ reset: {},
606
+ },
607
+ requirements: {
608
+ LOAD_DASHBOARD: { role: t.string() },
609
+ },
610
+ } satisfies ModuleSchema;
611
+
612
+ export const dashboardModule = createModule("dashboard", {
613
+ schema: dashboardSchema,
614
+
615
+ crossModuleDeps: { permissions: permissionsSchema },
616
+
617
+ init: (facts) => {
618
+ facts.widgets = [];
619
+ facts.loaded = false;
620
+ facts.failRate = 0;
621
+ },
622
+
623
+ derive: {
624
+ widgetCount: (facts) => facts.self.widgets.length,
625
+ },
626
+
627
+ events: {
628
+ setFailRate: (facts, { value }) => {
629
+ facts.failRate = value;
630
+ },
631
+ reset: (facts) => {
632
+ facts.widgets = [];
633
+ facts.loaded = false;
634
+ },
635
+ },
636
+
637
+ constraints: {
638
+ loadDashboard: {
639
+ after: ["permissions::loadPermissions"],
640
+ when: (facts) => {
641
+ return facts.permissions.role !== "" && !facts.self.loaded;
642
+ },
643
+ require: (facts) => ({
644
+ type: "LOAD_DASHBOARD",
645
+ role: facts.permissions.role,
646
+ }),
647
+ },
648
+ },
649
+
650
+ resolvers: {
651
+ loadDashboard: {
652
+ requirement: "LOAD_DASHBOARD",
653
+ key: (req) => `dashboard-${req.role}`,
654
+ retry: {
655
+ attempts: 2,
656
+ backoff: "exponential",
657
+ initialDelay: 300,
658
+ },
659
+ resolve: async (req, context) => {
660
+ try {
661
+ const result = await fetchDashboard(req.role, context.facts.failRate);
662
+ context.facts.widgets = result.widgets;
663
+ context.facts.loaded = true;
664
+ } catch {
665
+ context.facts.loaded = false;
666
+ }
667
+ },
668
+ },
669
+ },
670
+ });
671
+ ```
672
+
673
+ ## batch-resolver
674
+
675
+ ```typescript
676
+ // Example: batch-resolver
677
+ // Source: examples/batch-resolver/src/module.ts
678
+ // Pure module file — no DOM wiring
679
+
680
+ /**
681
+ * Batch Data Loader — Directive Module
682
+ *
683
+ * Types, schema, mock data, module definition, timeline, and system creation
684
+ * for a batched user profile loader with schema validation.
685
+ */
686
+
687
+ import {
688
+ type ModuleSchema,
689
+ createModule,
690
+ createSystem,
691
+ t,
692
+ } from "@directive-run/core";
693
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
694
+
695
+ // ============================================================================
696
+ // Types
697
+ // ============================================================================
698
+
699
+ export interface UserProfile {
700
+ id: number;
701
+ name: string;
702
+ email: string;
703
+ role: string;
704
+ }
705
+
706
+ export interface TimelineEntry {
707
+ time: number;
708
+ event: string;
709
+ detail: string;
710
+ type: "info" | "batch" | "error" | "success" | "validation";
711
+ }
712
+
713
+ // ============================================================================
714
+ // Mock Data
715
+ // ============================================================================
716
+
717
+ const MOCK_USERS: Record<number, UserProfile> = {
718
+ 1: { id: 1, name: "Alice Chen", email: "alice@acme.com", role: "Admin" },
719
+ 2: { id: 2, name: "Bob Smith", email: "bob@acme.com", role: "Editor" },
720
+ 3: { id: 3, name: "Carol Davis", email: "carol@acme.com", role: "Viewer" },
721
+ 4: { id: 4, name: "Dave Wilson", email: "dave@acme.com", role: "Editor" },
722
+ 5: { id: 5, name: "Eve Brown", email: "eve@acme.com", role: "Admin" },
723
+ 6: { id: 6, name: "Frank Lee", email: "frank@acme.com", role: "Viewer" },
724
+ 7: { id: 7, name: "Grace Kim", email: "grace@acme.com", role: "Editor" },
725
+ 8: { id: 8, name: "Hank Moore", email: "hank@acme.com", role: "Viewer" },
726
+ 9: { id: 9, name: "Iris Park", email: "iris@acme.com", role: "Admin" },
727
+ 10: { id: 10, name: "Jack Turner", email: "jack@acme.com", role: "Editor" },
728
+ 11: { id: 11, name: "Kate Adams", email: "kate@acme.com", role: "Viewer" },
729
+ 12: { id: 12, name: "Leo Garcia", email: "leo@acme.com", role: "Editor" },
730
+ 13: { id: 13, name: "Mia Jones", email: "mia@acme.com", role: "Admin" },
731
+ 14: { id: 14, name: "Nick White", email: "nick@acme.com", role: "Viewer" },
732
+ 15: { id: 15, name: "Olivia Hall", email: "olivia@acme.com", role: "Editor" },
733
+ 16: { id: 16, name: "Pete Clark", email: "pete@acme.com", role: "Viewer" },
734
+ 17: { id: 17, name: "Quinn Ross", email: "quinn@acme.com", role: "Admin" },
735
+ 18: { id: 18, name: "Rosa Martin", email: "rosa@acme.com", role: "Editor" },
736
+ 19: { id: 19, name: "Steve Young", email: "steve@acme.com", role: "Viewer" },
737
+ 20: { id: 20, name: "Tina Allen", email: "tina@acme.com", role: "Admin" },
738
+ };
739
+
740
+ // ============================================================================
741
+ // Timeline
742
+ // ============================================================================
743
+
744
+ export const timeline: TimelineEntry[] = [];
745
+
746
+ export function addTimeline(
747
+ event: string,
748
+ detail: string,
749
+ type: TimelineEntry["type"],
750
+ ) {
751
+ timeline.unshift({ time: Date.now(), event, detail, type });
752
+ if (timeline.length > 50) {
753
+ timeline.length = 50;
754
+ }
755
+ }
756
+
757
+ // ============================================================================
758
+ // Schema
759
+ // ============================================================================
760
+
761
+ export const schema = {
762
+ facts: {
763
+ users: t.array<UserProfile>(),
764
+ loadingIds: t.array<number>(),
765
+ batchCount: t.number(),
766
+ totalRequests: t.number(),
767
+ batchWindowMs: t.number(),
768
+ failItemId: t.number(),
769
+ validationErrors: t.array<string>(),
770
+ },
771
+ derivations: {
772
+ userCount: t.number(),
773
+ loadingCount: t.number(),
774
+ batchEfficiency: t.string(),
775
+ hasValidationErrors: t.boolean(),
776
+ },
777
+ events: {
778
+ loadUser: { id: t.number() },
779
+ loadRange: { start: t.number(), count: t.number() },
780
+ setBatchWindow: { value: t.number() },
781
+ setFailItemId: { value: t.number() },
782
+ injectSchemaError: {},
783
+ clearUsers: {},
784
+ resetAll: {},
785
+ },
786
+ requirements: {
787
+ LOAD_USER: { userId: t.number() },
788
+ },
789
+ } satisfies ModuleSchema;
790
+
791
+ // ============================================================================
792
+ // Module
793
+ // ============================================================================
794
+
795
+ const batchModule = createModule("batch-loader", {
796
+ schema,
797
+
798
+ init: (facts) => {
799
+ facts.users = [];
800
+ facts.loadingIds = [];
801
+ facts.batchCount = 0;
802
+ facts.totalRequests = 0;
803
+ facts.batchWindowMs = 50;
804
+ facts.failItemId = 0;
805
+ facts.validationErrors = [];
806
+ },
807
+
808
+ derive: {
809
+ userCount: (facts) => facts.users.length,
810
+ loadingCount: (facts) => facts.loadingIds.length,
811
+ batchEfficiency: (facts) => {
812
+ if (facts.totalRequests === 0) {
813
+ return "N/A";
814
+ }
815
+
816
+ return `${facts.batchCount} batches / ${facts.totalRequests} requests`;
817
+ },
818
+ hasValidationErrors: (facts) => facts.validationErrors.length > 0,
819
+ },
820
+
821
+ events: {
822
+ loadUser: (facts, { id }) => {
823
+ if (
824
+ !facts.loadingIds.includes(id) &&
825
+ !facts.users.find((u: UserProfile) => u.id === id)
826
+ ) {
827
+ facts.loadingIds = [...facts.loadingIds, id];
828
+ facts.totalRequests = facts.totalRequests + 1;
829
+ }
830
+ },
831
+ loadRange: (facts, { start, count }) => {
832
+ const newIds: number[] = [];
833
+ for (let i = start; i < start + count; i++) {
834
+ if (
835
+ !facts.loadingIds.includes(i) &&
836
+ !facts.users.find((u: UserProfile) => u.id === i)
837
+ ) {
838
+ newIds.push(i);
839
+ }
840
+ }
841
+ if (newIds.length > 0) {
842
+ facts.loadingIds = [...facts.loadingIds, ...newIds];
843
+ facts.totalRequests = facts.totalRequests + newIds.length;
844
+ }
845
+ },
846
+ setBatchWindow: (facts, { value }) => {
847
+ facts.batchWindowMs = value;
848
+ },
849
+ setFailItemId: (facts, { value }) => {
850
+ facts.failItemId = value;
851
+ },
852
+ injectSchemaError: (facts) => {
853
+ // Intentionally write a bad type to trigger validation
854
+ (facts as Record<string, unknown>).users = "not-an-array";
855
+ facts.validationErrors = [
856
+ ...facts.validationErrors,
857
+ "schema: expected array for 'users', got string",
858
+ ];
859
+ addTimeline(
860
+ "validation",
861
+ "schema error: expected array for 'users'",
862
+ "validation",
863
+ );
864
+ // Fix it immediately so the system keeps working
865
+ facts.users = [];
866
+ },
867
+ clearUsers: (facts) => {
868
+ facts.users = [];
869
+ },
870
+ resetAll: (facts) => {
871
+ facts.users = [];
872
+ facts.loadingIds = [];
873
+ facts.batchCount = 0;
874
+ facts.totalRequests = 0;
875
+ facts.failItemId = 0;
876
+ facts.validationErrors = [];
877
+ timeline.length = 0;
878
+ },
879
+ },
880
+
881
+ constraints: {
882
+ needsLoad: {
883
+ priority: 50,
884
+ when: (facts) => facts.loadingIds.length > 0,
885
+ require: (facts) => {
886
+ // Emit one requirement per loading ID — the batch resolver groups them
887
+ const id = facts.loadingIds[0];
888
+
889
+ return { type: "LOAD_USER", userId: id };
890
+ },
891
+ },
892
+ },
893
+
894
+ resolvers: {
895
+ loadUser: {
896
+ requirement: "LOAD_USER",
897
+ batch: {
898
+ enabled: true,
899
+ windowMs: 50,
900
+ },
901
+ resolveBatchWithResults: async (requirements, context) => {
902
+ const ids = requirements.map((r) => r.userId);
903
+ addTimeline(
904
+ "batch",
905
+ `batch formed: ${ids.length} items [${ids.join(", ")}]`,
906
+ "batch",
907
+ );
908
+ context.facts.batchCount = context.facts.batchCount + 1;
909
+
910
+ // Simulate API delay
911
+ await new Promise((resolve) =>
912
+ setTimeout(resolve, 150 + Math.random() * 100),
913
+ );
914
+
915
+ const failId = context.facts.failItemId;
916
+ const results = ids.map((id) => {
917
+ if (id === failId) {
918
+ addTimeline("error", `user ${id}: simulated failure`, "error");
919
+
920
+ return {
921
+ success: false as const,
922
+ error: new Error(`Failed to load user ${id}`),
923
+ };
924
+ }
925
+
926
+ const user = MOCK_USERS[id];
927
+ if (!user) {
928
+ addTimeline("error", `user ${id}: not found`, "error");
929
+
930
+ return {
931
+ success: false as const,
932
+ error: new Error(`User ${id} not found`),
933
+ };
934
+ }
935
+
936
+ return { success: true as const };
937
+ });
938
+
939
+ // Add successful users to facts
940
+ const successUsers = ids
941
+ .filter((id) => id !== failId && MOCK_USERS[id])
942
+ .map((id) => MOCK_USERS[id]!);
943
+
944
+ if (successUsers.length > 0) {
945
+ const existing = context.facts.users as UserProfile[];
946
+ context.facts.users = [...existing, ...successUsers];
947
+ }
948
+
949
+ // Remove all processed IDs from loading
950
+ const loadingIds = context.facts.loadingIds as number[];
951
+ context.facts.loadingIds = loadingIds.filter(
952
+ (lid: number) => !ids.includes(lid),
953
+ );
954
+
955
+ const successCount = results.filter((r) => r.success).length;
956
+ addTimeline(
957
+ "success",
958
+ `batch resolved: ${successCount}/${ids.length} success`,
959
+ "success",
960
+ );
961
+
962
+ return results;
963
+ },
964
+ },
965
+ },
966
+ });
967
+
968
+ // ============================================================================
969
+ // System
970
+ // ============================================================================
971
+
972
+ export const system = createSystem({
973
+ module: batchModule,
974
+ trace: true,
975
+ plugins: [devtoolsPlugin({ name: "batch-resolver" })],
976
+ });
977
+ ```
978
+
979
+ ## error-boundaries
980
+
981
+ ```typescript
982
+ // Example: error-boundaries
983
+ // Source: examples/error-boundaries/src/module.ts
984
+ // Pure module file — no DOM wiring
985
+
986
+ /**
987
+ * Resilient API Dashboard — Module Definition
988
+ *
989
+ * 3 simulated API services with configurable failure rates. Users inject errors
990
+ * and watch recovery strategies, circuit breaker state transitions, retry-later
991
+ * backoff, and performance metrics.
992
+ */
993
+
994
+ import {
995
+ type ModuleSchema,
996
+ type RecoveryStrategy,
997
+ createModule,
998
+ createSystem,
999
+ t,
1000
+ } from "@directive-run/core";
1001
+ import {
1002
+ type CircuitState,
1003
+ createCircuitBreaker,
1004
+ devtoolsPlugin,
1005
+ performancePlugin,
1006
+ } from "@directive-run/core/plugins";
1007
+
1008
+ // ============================================================================
1009
+ // Types
1010
+ // ============================================================================
1011
+
1012
+ export interface ServiceState {
1013
+ name: string;
1014
+ status: "idle" | "loading" | "success" | "error";
1015
+ lastResult: string;
1016
+ errorCount: number;
1017
+ successCount: number;
1018
+ lastError: string;
1019
+ }
1020
+
1021
+ export interface TimelineEntry {
1022
+ time: number;
1023
+ event: string;
1024
+ detail: string;
1025
+ type: "info" | "error" | "retry" | "circuit" | "recovery" | "success";
1026
+ }
1027
+
1028
+ // ============================================================================
1029
+ // Timeline
1030
+ // ============================================================================
1031
+
1032
+ export const timeline: TimelineEntry[] = [];
1033
+
1034
+ export function addTimeline(
1035
+ event: string,
1036
+ detail: string,
1037
+ type: TimelineEntry["type"],
1038
+ ) {
1039
+ timeline.unshift({ time: Date.now(), event, detail, type });
1040
+ if (timeline.length > 50) {
1041
+ timeline.length = 50;
1042
+ }
1043
+ }
1044
+
1045
+ // ============================================================================
1046
+ // Circuit Breakers (one per service)
1047
+ // ============================================================================
1048
+
1049
+ export const circuitBreakers = {
1050
+ users: createCircuitBreaker({
1051
+ name: "users-api",
1052
+ failureThreshold: 3,
1053
+ recoveryTimeMs: 5000,
1054
+ halfOpenMaxRequests: 2,
1055
+ onStateChange: (from, to) => {
1056
+ addTimeline("circuit", `users: ${from} → ${to}`, "circuit");
1057
+ },
1058
+ }),
1059
+ orders: createCircuitBreaker({
1060
+ name: "orders-api",
1061
+ failureThreshold: 3,
1062
+ recoveryTimeMs: 5000,
1063
+ halfOpenMaxRequests: 2,
1064
+ onStateChange: (from, to) => {
1065
+ addTimeline("circuit", `orders: ${from} → ${to}`, "circuit");
1066
+ },
1067
+ }),
1068
+ analytics: createCircuitBreaker({
1069
+ name: "analytics-api",
1070
+ failureThreshold: 3,
1071
+ recoveryTimeMs: 5000,
1072
+ halfOpenMaxRequests: 2,
1073
+ onStateChange: (from, to) => {
1074
+ addTimeline("circuit", `analytics: ${from} → ${to}`, "circuit");
1075
+ },
1076
+ }),
1077
+ };
1078
+
1079
+ // ============================================================================
1080
+ // Schema
1081
+ // ============================================================================
1082
+
1083
+ export const schema = {
1084
+ facts: {
1085
+ usersService: t.object<ServiceState>(),
1086
+ ordersService: t.object<ServiceState>(),
1087
+ analyticsService: t.object<ServiceState>(),
1088
+ strategy: t.string<RecoveryStrategy>(),
1089
+ usersFailRate: t.number(),
1090
+ ordersFailRate: t.number(),
1091
+ analyticsFailRate: t.number(),
1092
+ retryQueueCount: t.number(),
1093
+ totalErrors: t.number(),
1094
+ totalRecoveries: t.number(),
1095
+ },
1096
+ derivations: {
1097
+ usersCircuitState: t.string<CircuitState>(),
1098
+ ordersCircuitState: t.string<CircuitState>(),
1099
+ analyticsCircuitState: t.string<CircuitState>(),
1100
+ errorRate: t.number(),
1101
+ allServicesHealthy: t.boolean(),
1102
+ },
1103
+ events: {
1104
+ fetchUsers: {},
1105
+ fetchOrders: {},
1106
+ fetchAnalytics: {},
1107
+ fetchAll: {},
1108
+ setStrategy: { value: t.string<RecoveryStrategy>() },
1109
+ setUsersFailRate: { value: t.number() },
1110
+ setOrdersFailRate: { value: t.number() },
1111
+ setAnalyticsFailRate: { value: t.number() },
1112
+ resetAll: {},
1113
+ },
1114
+ requirements: {
1115
+ FETCH_SERVICE: { service: t.string(), failRate: t.number() },
1116
+ },
1117
+ } satisfies ModuleSchema;
1118
+
1119
+ // ============================================================================
1120
+ // Module
1121
+ // ============================================================================
1122
+
1123
+ const dashboardModule = createModule("dashboard", {
1124
+ schema,
1125
+
1126
+ init: (facts) => {
1127
+ const defaultService: ServiceState = {
1128
+ name: "",
1129
+ status: "idle",
1130
+ lastResult: "",
1131
+ errorCount: 0,
1132
+ successCount: 0,
1133
+ lastError: "",
1134
+ };
1135
+ facts.usersService = { ...defaultService, name: "Users API" };
1136
+ facts.ordersService = { ...defaultService, name: "Orders API" };
1137
+ facts.analyticsService = { ...defaultService, name: "Analytics API" };
1138
+ facts.strategy = "retry-later";
1139
+ facts.usersFailRate = 0;
1140
+ facts.ordersFailRate = 0;
1141
+ facts.analyticsFailRate = 0;
1142
+ facts.retryQueueCount = 0;
1143
+ facts.totalErrors = 0;
1144
+ facts.totalRecoveries = 0;
1145
+ },
1146
+
1147
+ derive: {
1148
+ usersCircuitState: () => circuitBreakers.users.getState(),
1149
+ ordersCircuitState: () => circuitBreakers.orders.getState(),
1150
+ analyticsCircuitState: () => circuitBreakers.analytics.getState(),
1151
+ errorRate: (facts) => {
1152
+ const total =
1153
+ facts.usersService.errorCount +
1154
+ facts.usersService.successCount +
1155
+ facts.ordersService.errorCount +
1156
+ facts.ordersService.successCount +
1157
+ facts.analyticsService.errorCount +
1158
+ facts.analyticsService.successCount;
1159
+
1160
+ if (total === 0) {
1161
+ return 0;
1162
+ }
1163
+
1164
+ const errors =
1165
+ facts.usersService.errorCount +
1166
+ facts.ordersService.errorCount +
1167
+ facts.analyticsService.errorCount;
1168
+
1169
+ return Math.round((errors / total) * 100);
1170
+ },
1171
+ allServicesHealthy: (facts) =>
1172
+ facts.usersService.status !== "error" &&
1173
+ facts.ordersService.status !== "error" &&
1174
+ facts.analyticsService.status !== "error",
1175
+ },
1176
+
1177
+ events: {
1178
+ fetchUsers: (facts) => {
1179
+ facts.usersService = { ...facts.usersService, status: "loading" };
1180
+ },
1181
+ fetchOrders: (facts) => {
1182
+ facts.ordersService = { ...facts.ordersService, status: "loading" };
1183
+ },
1184
+ fetchAnalytics: (facts) => {
1185
+ facts.analyticsService = { ...facts.analyticsService, status: "loading" };
1186
+ },
1187
+ fetchAll: (facts) => {
1188
+ facts.usersService = { ...facts.usersService, status: "loading" };
1189
+ facts.ordersService = { ...facts.ordersService, status: "loading" };
1190
+ facts.analyticsService = { ...facts.analyticsService, status: "loading" };
1191
+ },
1192
+ setStrategy: (facts, { value }) => {
1193
+ facts.strategy = value;
1194
+ },
1195
+ setUsersFailRate: (facts, { value }) => {
1196
+ facts.usersFailRate = value;
1197
+ },
1198
+ setOrdersFailRate: (facts, { value }) => {
1199
+ facts.ordersFailRate = value;
1200
+ },
1201
+ setAnalyticsFailRate: (facts, { value }) => {
1202
+ facts.analyticsFailRate = value;
1203
+ },
1204
+ resetAll: (facts) => {
1205
+ const defaultService: ServiceState = {
1206
+ name: "",
1207
+ status: "idle",
1208
+ lastResult: "",
1209
+ errorCount: 0,
1210
+ successCount: 0,
1211
+ lastError: "",
1212
+ };
1213
+ facts.usersService = { ...defaultService, name: "Users API" };
1214
+ facts.ordersService = { ...defaultService, name: "Orders API" };
1215
+ facts.analyticsService = { ...defaultService, name: "Analytics API" };
1216
+ facts.retryQueueCount = 0;
1217
+ facts.totalErrors = 0;
1218
+ facts.totalRecoveries = 0;
1219
+ circuitBreakers.users.reset();
1220
+ circuitBreakers.orders.reset();
1221
+ circuitBreakers.analytics.reset();
1222
+ timeline.length = 0;
1223
+ },
1224
+ },
1225
+
1226
+ constraints: {
1227
+ usersNeedsLoad: {
1228
+ priority: 50,
1229
+ when: (facts) => facts.usersService.status === "loading",
1230
+ require: (facts) => ({
1231
+ type: "FETCH_SERVICE",
1232
+ service: "users",
1233
+ failRate: facts.usersFailRate,
1234
+ }),
1235
+ },
1236
+ ordersNeedsLoad: {
1237
+ priority: 50,
1238
+ when: (facts) => facts.ordersService.status === "loading",
1239
+ require: (facts) => ({
1240
+ type: "FETCH_SERVICE",
1241
+ service: "orders",
1242
+ failRate: facts.ordersFailRate,
1243
+ }),
1244
+ },
1245
+ analyticsNeedsLoad: {
1246
+ priority: 50,
1247
+ when: (facts) => facts.analyticsService.status === "loading",
1248
+ require: (facts) => ({
1249
+ type: "FETCH_SERVICE",
1250
+ service: "analytics",
1251
+ failRate: facts.analyticsFailRate,
1252
+ }),
1253
+ },
1254
+ },
1255
+
1256
+ resolvers: {
1257
+ fetchService: {
1258
+ requirement: "FETCH_SERVICE",
1259
+ retry: { attempts: 2, backoff: "exponential", initialDelay: 200 },
1260
+ resolve: async (req, context) => {
1261
+ const { service, failRate } = req;
1262
+ const breaker =
1263
+ circuitBreakers[service as keyof typeof circuitBreakers];
1264
+ const serviceKey = `${service}Service` as
1265
+ | "usersService"
1266
+ | "ordersService"
1267
+ | "analyticsService";
1268
+
1269
+ try {
1270
+ await breaker.execute(async () => {
1271
+ // Simulate API call
1272
+ await new Promise((resolve) =>
1273
+ setTimeout(resolve, 200 + Math.random() * 300),
1274
+ );
1275
+
1276
+ if (Math.random() * 100 < failRate) {
1277
+ throw new Error(`${service} API: simulated failure`);
1278
+ }
1279
+ });
1280
+
1281
+ // Success
1282
+ const current = context.facts[serviceKey];
1283
+ context.facts[serviceKey] = {
1284
+ ...current,
1285
+ status: "success",
1286
+ lastResult: `Loaded at ${new Date().toLocaleTimeString()}`,
1287
+ successCount: current.successCount + 1,
1288
+ };
1289
+ addTimeline("success", `${service} fetched`, "success");
1290
+ } catch (error) {
1291
+ const current = context.facts[serviceKey];
1292
+ const msg = error instanceof Error ? error.message : String(error);
1293
+ context.facts[serviceKey] = {
1294
+ ...current,
1295
+ status: "error",
1296
+ lastError: msg,
1297
+ errorCount: current.errorCount + 1,
1298
+ };
1299
+ context.facts.totalErrors = context.facts.totalErrors + 1;
1300
+ addTimeline("error", `${service}: ${msg.slice(0, 60)}`, "error");
1301
+
1302
+ // Re-throw so the error boundary handles recovery
1303
+ throw error;
1304
+ }
1305
+ },
1306
+ },
1307
+ },
1308
+ });
1309
+
1310
+ // ============================================================================
1311
+ // Performance Plugin
1312
+ // ============================================================================
1313
+
1314
+ export const perf = performancePlugin({
1315
+ onSlowResolver: (id, ms) => {
1316
+ addTimeline("perf", `slow resolver: ${id} (${Math.round(ms)}ms)`, "info");
1317
+ },
1318
+ });
1319
+
1320
+ // ============================================================================
1321
+ // System
1322
+ // ============================================================================
1323
+
1324
+ let currentStrategy: RecoveryStrategy = "retry-later";
1325
+
1326
+ export const system = createSystem({
1327
+ module: dashboardModule,
1328
+ trace: true,
1329
+ plugins: [perf, devtoolsPlugin({ name: "error-boundaries" })],
1330
+ errorBoundary: {
1331
+ onResolverError: (_error, resolver) => {
1332
+ addTimeline(
1333
+ "recovery",
1334
+ `${resolver}: strategy=${currentStrategy}`,
1335
+ "recovery",
1336
+ );
1337
+
1338
+ return currentStrategy;
1339
+ },
1340
+ onConstraintError: "skip",
1341
+ onEffectError: "skip",
1342
+ retryLater: {
1343
+ delayMs: 1000,
1344
+ maxRetries: 3,
1345
+ backoffMultiplier: 2,
1346
+ },
1347
+ onError: (error) => {
1348
+ addTimeline("error", `boundary: ${error.message.slice(0, 60)}`, "error");
1349
+ },
1350
+ },
1351
+ });
1352
+
1353
+ // Track strategy changes to update error boundary (via re-dispatch)
1354
+ system.subscribe(["strategy"], () => {
1355
+ const newStrategy = system.facts.strategy;
1356
+ if (newStrategy !== currentStrategy) {
1357
+ currentStrategy = newStrategy;
1358
+ addTimeline("recovery", `strategy → ${newStrategy}`, "recovery");
1359
+ }
1360
+ });
1361
+ ```