@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,1386 @@
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
+ ## auth-flow
90
+
91
+ ```typescript
92
+ // Example: auth-flow
93
+ // Source: examples/auth-flow/src/auth-flow.ts
94
+ // Pure module file — no DOM wiring
95
+
96
+ /**
97
+ * Auth Flow — Directive Module
98
+ *
99
+ * Demonstrates constraint `after` ordering, auto-tracked derivations
100
+ * driving constraints, resolvers with retry, effects for cleanup,
101
+ * and time-based reactivity (token expiry countdown).
102
+ */
103
+
104
+ import { type ModuleSchema, createModule, t } from "@directive-run/core";
105
+ import {
106
+ type User,
107
+ mockFetchUser,
108
+ mockLogin,
109
+ mockRefresh,
110
+ } from "./mock-auth.js";
111
+
112
+ // ============================================================================
113
+ // Types
114
+ // ============================================================================
115
+
116
+ export type AuthStatus =
117
+ | "idle"
118
+ | "authenticating"
119
+ | "authenticated"
120
+ | "refreshing"
121
+ | "expired";
122
+
123
+ export interface EventLogEntry {
124
+ timestamp: number;
125
+ event: string;
126
+ detail: string;
127
+ }
128
+
129
+ // ============================================================================
130
+ // Schema
131
+ // ============================================================================
132
+
133
+ export const authFlowSchema = {
134
+ facts: {
135
+ email: t.string(),
136
+ password: t.string(),
137
+ token: t.string(),
138
+ refreshToken: t.string(),
139
+ expiresAt: t.number(),
140
+ user: t.object<User | null>(),
141
+ status: t.string<AuthStatus>(),
142
+ loginRequested: t.boolean(),
143
+ now: t.number(),
144
+ tokenTTL: t.number(),
145
+ refreshBuffer: t.number(),
146
+ loginFailRate: t.number(),
147
+ refreshFailRate: t.number(),
148
+ eventLog: t.array<EventLogEntry>(),
149
+ },
150
+ derivations: {
151
+ isAuthenticated: t.boolean(),
152
+ isExpiringSoon: t.boolean(),
153
+ canRefresh: t.boolean(),
154
+ tokenTimeRemaining: t.number(),
155
+ canLogin: t.boolean(),
156
+ },
157
+ events: {
158
+ setEmail: { value: t.string() },
159
+ setPassword: { value: t.string() },
160
+ requestLogin: {},
161
+ logout: {},
162
+ forceExpire: {},
163
+ setTokenTTL: { value: t.number() },
164
+ setRefreshBuffer: { value: t.number() },
165
+ setLoginFailRate: { value: t.number() },
166
+ setRefreshFailRate: { value: t.number() },
167
+ tick: {},
168
+ },
169
+ requirements: {
170
+ LOGIN: { email: t.string(), password: t.string() },
171
+ REFRESH_TOKEN: { refreshToken: t.string() },
172
+ FETCH_USER: { token: t.string() },
173
+ },
174
+ } satisfies ModuleSchema;
175
+
176
+ // ============================================================================
177
+ // Helpers
178
+ // ============================================================================
179
+
180
+ function addLogEntry(facts: any, event: string, detail: string): void {
181
+ const log = [...facts.eventLog];
182
+ log.push({ timestamp: Date.now(), event, detail });
183
+ facts.eventLog = log;
184
+ }
185
+
186
+ // ============================================================================
187
+ // Module
188
+ // ============================================================================
189
+
190
+ export const authFlowModule = createModule("auth-flow", {
191
+ schema: authFlowSchema,
192
+
193
+ init: (facts) => {
194
+ facts.email = "alice@test.com";
195
+ facts.password = "password";
196
+ facts.token = "";
197
+ facts.refreshToken = "";
198
+ facts.expiresAt = 0;
199
+ facts.user = null;
200
+ facts.status = "idle";
201
+ facts.loginRequested = false;
202
+ facts.now = Date.now();
203
+ facts.tokenTTL = 30;
204
+ facts.refreshBuffer = 5;
205
+ facts.loginFailRate = 0;
206
+ facts.refreshFailRate = 0;
207
+ facts.eventLog = [];
208
+ },
209
+
210
+ // ============================================================================
211
+ // Derivations
212
+ // ============================================================================
213
+
214
+ derive: {
215
+ isAuthenticated: (facts) => facts.status === "authenticated",
216
+
217
+ isExpiringSoon: (facts) => {
218
+ if (facts.token === "") {
219
+ return false;
220
+ }
221
+
222
+ return facts.now > facts.expiresAt - facts.refreshBuffer * 1000;
223
+ },
224
+
225
+ canRefresh: (facts) => {
226
+ return facts.refreshToken !== "" && facts.status !== "refreshing";
227
+ },
228
+
229
+ tokenTimeRemaining: (facts) => {
230
+ if (facts.token === "") {
231
+ return 0;
232
+ }
233
+
234
+ return Math.max(0, Math.round((facts.expiresAt - facts.now) / 1000));
235
+ },
236
+
237
+ canLogin: (facts) => {
238
+ return (
239
+ facts.email.trim() !== "" &&
240
+ facts.password.trim() !== "" &&
241
+ (facts.status === "idle" || facts.status === "expired")
242
+ );
243
+ },
244
+ },
245
+
246
+ // ============================================================================
247
+ // Events
248
+ // ============================================================================
249
+
250
+ events: {
251
+ setEmail: (facts, { value }) => {
252
+ facts.email = value;
253
+ },
254
+
255
+ setPassword: (facts, { value }) => {
256
+ facts.password = value;
257
+ },
258
+
259
+ requestLogin: (facts) => {
260
+ facts.loginRequested = true;
261
+ facts.status = "authenticating";
262
+ facts.token = "";
263
+ facts.refreshToken = "";
264
+ facts.expiresAt = 0;
265
+ facts.user = null;
266
+ facts.eventLog = [];
267
+ },
268
+
269
+ logout: (facts) => {
270
+ facts.token = "";
271
+ facts.refreshToken = "";
272
+ facts.expiresAt = 0;
273
+ facts.user = null;
274
+ facts.status = "idle";
275
+ facts.loginRequested = false;
276
+ },
277
+
278
+ forceExpire: (facts) => {
279
+ facts.expiresAt = 0;
280
+ },
281
+
282
+ setTokenTTL: (facts, { value }) => {
283
+ facts.tokenTTL = value;
284
+ },
285
+
286
+ setRefreshBuffer: (facts, { value }) => {
287
+ facts.refreshBuffer = value;
288
+ },
289
+
290
+ setLoginFailRate: (facts, { value }) => {
291
+ facts.loginFailRate = value;
292
+ },
293
+
294
+ setRefreshFailRate: (facts, { value }) => {
295
+ facts.refreshFailRate = value;
296
+ },
297
+
298
+ tick: (facts) => {
299
+ facts.now = Date.now();
300
+ },
301
+ },
302
+
303
+ // ============================================================================
304
+ // Constraints
305
+ // ============================================================================
306
+
307
+ constraints: {
308
+ needsLogin: {
309
+ priority: 100,
310
+ when: (facts) => {
311
+ return facts.loginRequested && facts.status === "authenticating";
312
+ },
313
+ require: (facts) => ({
314
+ type: "LOGIN",
315
+ email: facts.email,
316
+ password: facts.password,
317
+ }),
318
+ },
319
+
320
+ refreshNeeded: {
321
+ priority: 90,
322
+ when: (facts) => {
323
+ const isExpiringSoon =
324
+ facts.token !== "" &&
325
+ facts.now > facts.expiresAt - facts.refreshBuffer * 1000;
326
+ const canRefresh =
327
+ facts.refreshToken !== "" && facts.status !== "refreshing";
328
+
329
+ return isExpiringSoon && canRefresh && facts.status === "authenticated";
330
+ },
331
+ require: (facts) => ({
332
+ type: "REFRESH_TOKEN",
333
+ refreshToken: facts.refreshToken,
334
+ }),
335
+ },
336
+
337
+ needsUser: {
338
+ priority: 80,
339
+ after: ["refreshNeeded"],
340
+ when: (facts) => {
341
+ return (
342
+ facts.token !== "" &&
343
+ facts.user === null &&
344
+ facts.status !== "authenticating"
345
+ );
346
+ },
347
+ require: (facts) => ({
348
+ type: "FETCH_USER",
349
+ token: facts.token,
350
+ }),
351
+ },
352
+ },
353
+
354
+ // ============================================================================
355
+ // Resolvers
356
+ // ============================================================================
357
+
358
+ resolvers: {
359
+ login: {
360
+ requirement: "LOGIN",
361
+ timeout: 10000,
362
+ resolve: async (req, context) => {
363
+ addLogEntry(context.facts, "login", "Authenticating...");
364
+
365
+ try {
366
+ const tokens = await mockLogin(
367
+ req.email,
368
+ req.password,
369
+ context.facts.loginFailRate,
370
+ context.facts.tokenTTL,
371
+ );
372
+ context.facts.token = tokens.token;
373
+ context.facts.refreshToken = tokens.refreshToken;
374
+ context.facts.expiresAt = Date.now() + tokens.expiresIn * 1000;
375
+ context.facts.status = "authenticated";
376
+ context.facts.user = null; // trigger needsUser constraint
377
+ addLogEntry(
378
+ context.facts,
379
+ "login-success",
380
+ `Token: ${tokens.token.slice(0, 12)}...`,
381
+ );
382
+ } catch (err) {
383
+ const msg = err instanceof Error ? err.message : "Unknown error";
384
+ context.facts.status = "idle";
385
+ context.facts.loginRequested = false;
386
+ addLogEntry(context.facts, "login-error", msg);
387
+ throw err;
388
+ }
389
+ },
390
+ },
391
+
392
+ refreshToken: {
393
+ requirement: "REFRESH_TOKEN",
394
+ retry: { attempts: 2, backoff: "exponential" },
395
+ timeout: 10000,
396
+ resolve: async (req, context) => {
397
+ context.facts.status = "refreshing";
398
+ addLogEntry(context.facts, "refresh", "Refreshing token...");
399
+
400
+ try {
401
+ const tokens = await mockRefresh(
402
+ req.refreshToken,
403
+ context.facts.refreshFailRate,
404
+ context.facts.tokenTTL,
405
+ );
406
+ context.facts.token = tokens.token;
407
+ context.facts.refreshToken = tokens.refreshToken;
408
+ context.facts.expiresAt = Date.now() + tokens.expiresIn * 1000;
409
+ context.facts.status = "authenticated";
410
+ addLogEntry(
411
+ context.facts,
412
+ "refresh-success",
413
+ `New token: ${tokens.token.slice(0, 12)}...`,
414
+ );
415
+ } catch (err) {
416
+ const msg = err instanceof Error ? err.message : "Unknown error";
417
+ context.facts.token = "";
418
+ context.facts.refreshToken = "";
419
+ context.facts.expiresAt = 0;
420
+ context.facts.status = "expired";
421
+ addLogEntry(context.facts, "refresh-error", msg);
422
+ throw err;
423
+ }
424
+ },
425
+ },
426
+
427
+ fetchUser: {
428
+ requirement: "FETCH_USER",
429
+ resolve: async (req, context) => {
430
+ addLogEntry(context.facts, "fetch-user", "Fetching user profile...");
431
+
432
+ try {
433
+ const user = await mockFetchUser(req.token);
434
+ context.facts.user = user;
435
+ addLogEntry(
436
+ context.facts,
437
+ "fetch-user-success",
438
+ `${user.name} (${user.role})`,
439
+ );
440
+ } catch (err) {
441
+ const msg = err instanceof Error ? err.message : "Unknown error";
442
+ addLogEntry(context.facts, "fetch-user-error", msg);
443
+ }
444
+ },
445
+ },
446
+ },
447
+
448
+ // ============================================================================
449
+ // Effects
450
+ // ============================================================================
451
+
452
+ effects: {
453
+ logStatusChange: {
454
+ deps: ["status"],
455
+ run: (facts, prev) => {
456
+ if (prev && prev.status !== facts.status) {
457
+ addLogEntry(facts, "status", `${prev.status} → ${facts.status}`);
458
+ }
459
+ },
460
+ },
461
+ },
462
+ });
463
+ ```
464
+
465
+ ## shopping-cart
466
+
467
+ ```typescript
468
+ // Example: shopping-cart
469
+ // Source: examples/shopping-cart/src/shopping-cart.ts
470
+ // Pure module file — no DOM wiring
471
+
472
+ /**
473
+ * Shopping Cart — Directive Modules
474
+ *
475
+ * Two modules:
476
+ * - cart: Items, coupons, checkout with cross-module auth dependency
477
+ * - auth: Simple authentication toggle for demo purposes
478
+ *
479
+ * Demonstrates cross-module constraints (`crossModuleDeps`),
480
+ * constraint ordering (`after`), priority-based resolution,
481
+ * and retry with exponential backoff.
482
+ */
483
+
484
+ import {
485
+ type ModuleSchema,
486
+ createModule,
487
+ createSystem,
488
+ t,
489
+ } from "@directive-run/core";
490
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
491
+ import { processCheckout, validateCoupon } from "./mock-api.js";
492
+
493
+ // ============================================================================
494
+ // Types
495
+ // ============================================================================
496
+
497
+ export interface CartItem {
498
+ id: string;
499
+ name: string;
500
+ price: number;
501
+ quantity: number;
502
+ maxStock: number;
503
+ image: string;
504
+ }
505
+
506
+ // ============================================================================
507
+ // Auth Module
508
+ // ============================================================================
509
+
510
+ export const authSchema = {
511
+ facts: {
512
+ isAuthenticated: t.boolean(),
513
+ userName: t.string(),
514
+ },
515
+ derivations: {
516
+ isAuthenticated: t.boolean(),
517
+ },
518
+ events: {
519
+ toggleAuth: {},
520
+ },
521
+ requirements: {},
522
+ } satisfies ModuleSchema;
523
+
524
+ export const authModule = createModule("auth", {
525
+ schema: authSchema,
526
+
527
+ init: (facts) => {
528
+ facts.isAuthenticated = true;
529
+ facts.userName = "Demo User";
530
+ },
531
+
532
+ derive: {
533
+ isAuthenticated: (facts) => facts.isAuthenticated,
534
+ },
535
+
536
+ events: {
537
+ toggleAuth: (facts) => {
538
+ facts.isAuthenticated = !facts.isAuthenticated;
539
+ if (!facts.isAuthenticated) {
540
+ facts.userName = "";
541
+ } else {
542
+ facts.userName = "Demo User";
543
+ }
544
+ },
545
+ },
546
+ });
547
+
548
+ // ============================================================================
549
+ // Cart Module
550
+ // ============================================================================
551
+
552
+ export const cartSchema = {
553
+ facts: {
554
+ items: t.array<CartItem>(),
555
+ couponCode: t.string(),
556
+ couponDiscount: t.number(),
557
+ couponStatus: t.string<"idle" | "checking" | "valid" | "invalid">(),
558
+ checkoutRequested: t.boolean(),
559
+ checkoutStatus: t.string<"idle" | "processing" | "complete" | "failed">(),
560
+ checkoutError: t.string(),
561
+ },
562
+ derivations: {
563
+ subtotal: t.number(),
564
+ itemCount: t.number(),
565
+ isEmpty: t.boolean(),
566
+ discount: t.number(),
567
+ tax: t.number(),
568
+ total: t.number(),
569
+ hasOverstockedItem: t.boolean(),
570
+ freeShipping: t.boolean(),
571
+ },
572
+ events: {
573
+ addItem: {
574
+ id: t.string(),
575
+ name: t.string(),
576
+ price: t.number(),
577
+ maxStock: t.number(),
578
+ image: t.string(),
579
+ },
580
+ removeItem: { id: t.string() },
581
+ updateQuantity: { id: t.string(), quantity: t.number() },
582
+ applyCoupon: { code: t.string() },
583
+ clearCoupon: {},
584
+ requestCheckout: {},
585
+ resetCheckout: {},
586
+ },
587
+ requirements: {
588
+ ADJUST_QUANTITY: {},
589
+ VALIDATE_COUPON: { code: t.string() },
590
+ PROCESS_CHECKOUT: {},
591
+ },
592
+ } satisfies ModuleSchema;
593
+
594
+ export const cartModule = createModule("cart", {
595
+ schema: cartSchema,
596
+
597
+ crossModuleDeps: { auth: authSchema },
598
+
599
+ init: (facts) => {
600
+ facts.items = [
601
+ {
602
+ id: "headphones-1",
603
+ name: "Wireless Headphones",
604
+ price: 79.99,
605
+ quantity: 1,
606
+ maxStock: 5,
607
+ image: "headphones",
608
+ },
609
+ {
610
+ id: "keyboard-1",
611
+ name: "Mechanical Keyboard",
612
+ price: 129.99,
613
+ quantity: 1,
614
+ maxStock: 3,
615
+ image: "keyboard",
616
+ },
617
+ {
618
+ id: "hub-1",
619
+ name: "USB-C Hub",
620
+ price: 49.99,
621
+ quantity: 2,
622
+ maxStock: 10,
623
+ image: "hub",
624
+ },
625
+ ];
626
+ facts.couponCode = "";
627
+ facts.couponDiscount = 0;
628
+ facts.couponStatus = "idle";
629
+ facts.checkoutRequested = false;
630
+ facts.checkoutStatus = "idle";
631
+ facts.checkoutError = "";
632
+ },
633
+
634
+ // ============================================================================
635
+ // Derivations
636
+ // ============================================================================
637
+
638
+ derive: {
639
+ subtotal: (facts) => {
640
+ return facts.self.items.reduce(
641
+ (sum: number, item: CartItem) => sum + item.price * item.quantity,
642
+ 0,
643
+ );
644
+ },
645
+
646
+ itemCount: (facts) => {
647
+ return facts.self.items.reduce(
648
+ (sum: number, item: CartItem) => sum + item.quantity,
649
+ 0,
650
+ );
651
+ },
652
+
653
+ isEmpty: (facts) => {
654
+ return facts.self.items.length === 0;
655
+ },
656
+
657
+ discount: (facts, derived) => {
658
+ const sub = derived.subtotal;
659
+
660
+ return sub * (facts.self.couponDiscount / 100);
661
+ },
662
+
663
+ tax: (_facts, derived) => {
664
+ const sub = derived.subtotal;
665
+ const disc = derived.discount;
666
+
667
+ return (sub - disc) * 0.08;
668
+ },
669
+
670
+ total: (_facts, derived) => {
671
+ const sub = derived.subtotal;
672
+ const disc = derived.discount;
673
+ const tx = derived.tax;
674
+
675
+ return sub - disc + tx;
676
+ },
677
+
678
+ hasOverstockedItem: (facts) => {
679
+ return facts.self.items.some(
680
+ (item: CartItem) => item.quantity > item.maxStock,
681
+ );
682
+ },
683
+
684
+ freeShipping: (_facts, derived) => {
685
+ const sub = derived.subtotal;
686
+
687
+ return sub >= 75;
688
+ },
689
+ },
690
+
691
+ // ============================================================================
692
+ // Events
693
+ // ============================================================================
694
+
695
+ events: {
696
+ addItem: (facts, { id, name, price, maxStock, image }) => {
697
+ const existing = facts.items.find((item: CartItem) => item.id === id);
698
+ if (existing) {
699
+ facts.items = facts.items.map((item: CartItem) =>
700
+ item.id === id
701
+ ? { ...item, quantity: Math.min(item.quantity + 1, item.maxStock) }
702
+ : item,
703
+ );
704
+ } else {
705
+ facts.items = [
706
+ ...facts.items,
707
+ { id, name, price, quantity: 1, maxStock, image },
708
+ ];
709
+ }
710
+ },
711
+
712
+ removeItem: (facts, { id }) => {
713
+ facts.items = facts.items.filter((item: CartItem) => item.id !== id);
714
+ },
715
+
716
+ updateQuantity: (facts, { id, quantity }) => {
717
+ if (quantity <= 0) {
718
+ facts.items = facts.items.filter((item: CartItem) => item.id !== id);
719
+
720
+ return;
721
+ }
722
+
723
+ facts.items = facts.items.map((item: CartItem) =>
724
+ item.id === id ? { ...item, quantity } : item,
725
+ );
726
+ },
727
+
728
+ applyCoupon: (facts, { code }) => {
729
+ facts.couponCode = code;
730
+ facts.couponStatus = "idle";
731
+ facts.couponDiscount = 0;
732
+ },
733
+
734
+ clearCoupon: (facts) => {
735
+ facts.couponCode = "";
736
+ facts.couponDiscount = 0;
737
+ facts.couponStatus = "idle";
738
+ },
739
+
740
+ requestCheckout: (facts) => {
741
+ facts.checkoutRequested = true;
742
+ facts.checkoutStatus = "idle";
743
+ facts.checkoutError = "";
744
+ },
745
+
746
+ resetCheckout: (facts) => {
747
+ facts.checkoutRequested = false;
748
+ facts.checkoutStatus = "idle";
749
+ facts.checkoutError = "";
750
+ },
751
+ },
752
+
753
+ // ============================================================================
754
+ // Constraints
755
+ // ============================================================================
756
+
757
+ constraints: {
758
+ quantityLimit: {
759
+ priority: 80,
760
+ when: (facts) => {
761
+ const hasOverstocked = facts.self.items.some(
762
+ (item: CartItem) => item.quantity > item.maxStock,
763
+ );
764
+
765
+ return hasOverstocked;
766
+ },
767
+ require: { type: "ADJUST_QUANTITY" },
768
+ },
769
+
770
+ couponValidation: {
771
+ priority: 70,
772
+ when: (facts) => {
773
+ return (
774
+ facts.self.couponCode !== "" && facts.self.couponStatus === "idle"
775
+ );
776
+ },
777
+ require: (facts) => ({
778
+ type: "VALIDATE_COUPON",
779
+ code: facts.self.couponCode,
780
+ }),
781
+ },
782
+
783
+ checkoutReady: {
784
+ priority: 60,
785
+ after: ["quantityLimit", "couponValidation"],
786
+ when: (facts) => {
787
+ const items = facts.self.items;
788
+ const notEmpty = items.length > 0;
789
+ const noOverstock = !items.some(
790
+ (item: CartItem) => item.quantity > item.maxStock,
791
+ );
792
+
793
+ return (
794
+ facts.self.checkoutRequested === true &&
795
+ notEmpty &&
796
+ noOverstock &&
797
+ facts.auth.isAuthenticated === true
798
+ );
799
+ },
800
+ require: { type: "PROCESS_CHECKOUT" },
801
+ },
802
+ },
803
+
804
+ // ============================================================================
805
+ // Resolvers
806
+ // ============================================================================
807
+
808
+ resolvers: {
809
+ adjustQuantity: {
810
+ requirement: "ADJUST_QUANTITY",
811
+ resolve: async (_req, context) => {
812
+ context.facts.items = context.facts.items.map((item: CartItem) => {
813
+ if (item.quantity > item.maxStock) {
814
+ return { ...item, quantity: item.maxStock };
815
+ }
816
+
817
+ return item;
818
+ });
819
+ },
820
+ },
821
+
822
+ validateCoupon: {
823
+ requirement: "VALIDATE_COUPON",
824
+ key: (req) => `coupon-${req.code}`,
825
+ resolve: async (req, context) => {
826
+ context.facts.couponStatus = "checking";
827
+
828
+ const result = await validateCoupon(req.code);
829
+
830
+ if (result.valid) {
831
+ context.facts.couponDiscount = result.discount;
832
+ context.facts.couponStatus = "valid";
833
+ } else {
834
+ context.facts.couponDiscount = 0;
835
+ context.facts.couponStatus = "invalid";
836
+ }
837
+ },
838
+ },
839
+
840
+ processCheckout: {
841
+ requirement: "PROCESS_CHECKOUT",
842
+ retry: { attempts: 2, backoff: "exponential" },
843
+ resolve: async (_req, context) => {
844
+ context.facts.checkoutStatus = "processing";
845
+
846
+ try {
847
+ const items = context.facts.items.map((item: CartItem) => ({
848
+ id: item.id,
849
+ name: item.name,
850
+ quantity: item.quantity,
851
+ price: item.price,
852
+ }));
853
+
854
+ await processCheckout(items, context.facts.couponCode);
855
+
856
+ context.facts.checkoutStatus = "complete";
857
+ context.facts.items = [];
858
+ context.facts.couponCode = "";
859
+ context.facts.couponDiscount = 0;
860
+ context.facts.couponStatus = "idle";
861
+ context.facts.checkoutRequested = false;
862
+ } catch (err) {
863
+ const msg = err instanceof Error ? err.message : "Checkout failed";
864
+ context.facts.checkoutStatus = "failed";
865
+ context.facts.checkoutError = msg;
866
+ context.facts.checkoutRequested = false;
867
+ throw err;
868
+ }
869
+ },
870
+ },
871
+ },
872
+ });
873
+
874
+ // ============================================================================
875
+ // System
876
+ // ============================================================================
877
+
878
+ export const system = createSystem({
879
+ modules: {
880
+ cart: cartModule,
881
+ auth: authModule,
882
+ },
883
+ plugins: [devtoolsPlugin({ name: "shopping-cart", panel: true })],
884
+ history: { maxSnapshots: 50 },
885
+ trace: true,
886
+ });
887
+ ```
888
+
889
+ ## dashboard-loader
890
+
891
+ ```typescript
892
+ // Example: dashboard-loader
893
+ // Source: examples/dashboard-loader/src/dashboard-loader.ts
894
+ // Pure module file — no DOM wiring
895
+
896
+ /**
897
+ * Dashboard Loader — Directive Module
898
+ *
899
+ * Demonstrates loading & error states with concurrent resource fetching,
900
+ * configurable delays/failure rates, retry with exponential backoff,
901
+ * and combined status derivations.
902
+ */
903
+
904
+ import { type ModuleSchema, createModule, t } from "@directive-run/core";
905
+ import {
906
+ type Permissions,
907
+ type Preferences,
908
+ type Profile,
909
+ fetchMockPermissions,
910
+ fetchMockPreferences,
911
+ fetchMockProfile,
912
+ } from "./mock-api.js";
913
+
914
+ // ============================================================================
915
+ // Types
916
+ // ============================================================================
917
+
918
+ export type ResourceStatus = "idle" | "loading" | "success" | "error";
919
+
920
+ export interface ResourceState<T> {
921
+ data: T | null;
922
+ status: ResourceStatus;
923
+ error: string | null;
924
+ attempts: number;
925
+ startedAt: number | null;
926
+ completedAt: number | null;
927
+ }
928
+
929
+ export interface EventLogEntry {
930
+ timestamp: number;
931
+ event: string;
932
+ resource: string;
933
+ detail: string;
934
+ }
935
+
936
+ function makeIdleResource<T>(): ResourceState<T> {
937
+ return {
938
+ data: null,
939
+ status: "idle",
940
+ error: null,
941
+ attempts: 0,
942
+ startedAt: null,
943
+ completedAt: null,
944
+ };
945
+ }
946
+
947
+ // ============================================================================
948
+ // Schema
949
+ // ============================================================================
950
+
951
+ export const dashboardLoaderSchema = {
952
+ facts: {
953
+ userId: t.string(),
954
+ profile: t.object<ResourceState<Profile>>(),
955
+ preferences: t.object<ResourceState<Preferences>>(),
956
+ permissions: t.object<ResourceState<Permissions>>(),
957
+ profileDelay: t.number(),
958
+ preferencesDelay: t.number(),
959
+ permissionsDelay: t.number(),
960
+ profileFailRate: t.number(),
961
+ preferencesFailRate: t.number(),
962
+ permissionsFailRate: t.number(),
963
+ loadRequested: t.boolean(),
964
+ eventLog: t.array<EventLogEntry>(),
965
+ },
966
+ derivations: {
967
+ loadedCount: t.number(),
968
+ totalResources: t.number(),
969
+ allLoaded: t.boolean(),
970
+ anyError: t.boolean(),
971
+ anyLoading: t.boolean(),
972
+ combinedStatus: t.string(),
973
+ canStart: t.boolean(),
974
+ },
975
+ events: {
976
+ setUserId: { value: t.string() },
977
+ start: {},
978
+ retryResource: { resource: t.string() },
979
+ reloadAll: {},
980
+ setDelay: { resource: t.string(), value: t.number() },
981
+ setFailRate: { resource: t.string(), value: t.number() },
982
+ },
983
+ requirements: {
984
+ FETCH_PROFILE: { userId: t.string() },
985
+ FETCH_PREFERENCES: { userId: t.string() },
986
+ FETCH_PERMISSIONS: { userId: t.string() },
987
+ },
988
+ } satisfies ModuleSchema;
989
+
990
+ // ============================================================================
991
+ // Helpers
992
+ // ============================================================================
993
+
994
+ function addLogEntry(
995
+ facts: any,
996
+ event: string,
997
+ resource: string,
998
+ detail: string,
999
+ ): void {
1000
+ const log = [...facts.eventLog];
1001
+ log.push({ timestamp: Date.now(), event, resource, detail });
1002
+ facts.eventLog = log;
1003
+ }
1004
+
1005
+ // ============================================================================
1006
+ // Module
1007
+ // ============================================================================
1008
+
1009
+ export const dashboardLoaderModule = createModule("dashboard-loader", {
1010
+ schema: dashboardLoaderSchema,
1011
+
1012
+ init: (facts) => {
1013
+ facts.userId = "";
1014
+ facts.profile = makeIdleResource<Profile>();
1015
+ facts.preferences = makeIdleResource<Preferences>();
1016
+ facts.permissions = makeIdleResource<Permissions>();
1017
+ facts.profileDelay = 1000;
1018
+ facts.preferencesDelay = 1500;
1019
+ facts.permissionsDelay = 2000;
1020
+ facts.profileFailRate = 0;
1021
+ facts.preferencesFailRate = 0;
1022
+ facts.permissionsFailRate = 0;
1023
+ facts.loadRequested = false;
1024
+ facts.eventLog = [];
1025
+ },
1026
+
1027
+ // ============================================================================
1028
+ // Derivations
1029
+ // ============================================================================
1030
+
1031
+ derive: {
1032
+ loadedCount: (facts) => {
1033
+ const resources = [
1034
+ facts.profile,
1035
+ facts.preferences,
1036
+ facts.permissions,
1037
+ ] as ResourceState<unknown>[];
1038
+
1039
+ return resources.filter((r) => r.status === "success").length;
1040
+ },
1041
+
1042
+ totalResources: () => 3,
1043
+
1044
+ allLoaded: (facts) => {
1045
+ const resources = [
1046
+ facts.profile,
1047
+ facts.preferences,
1048
+ facts.permissions,
1049
+ ] as ResourceState<unknown>[];
1050
+
1051
+ return resources.every((r) => r.status === "success");
1052
+ },
1053
+
1054
+ anyError: (facts) => {
1055
+ const resources = [
1056
+ facts.profile,
1057
+ facts.preferences,
1058
+ facts.permissions,
1059
+ ] as ResourceState<unknown>[];
1060
+
1061
+ return resources.some((r) => r.status === "error");
1062
+ },
1063
+
1064
+ anyLoading: (facts) => {
1065
+ const resources = [
1066
+ facts.profile,
1067
+ facts.preferences,
1068
+ facts.permissions,
1069
+ ] as ResourceState<unknown>[];
1070
+
1071
+ return resources.some((r) => r.status === "loading");
1072
+ },
1073
+
1074
+ combinedStatus: (facts, derived) => {
1075
+ const loaded = derived.loadedCount;
1076
+ const anyErr = derived.anyError;
1077
+ const anyLoad = derived.anyLoading;
1078
+ const allIdle = [
1079
+ facts.profile,
1080
+ facts.preferences,
1081
+ facts.permissions,
1082
+ ].every((r: any) => r.status === "idle");
1083
+
1084
+ if (allIdle) {
1085
+ return "Not started";
1086
+ }
1087
+
1088
+ const errCount = [
1089
+ facts.profile,
1090
+ facts.preferences,
1091
+ facts.permissions,
1092
+ ].filter((r: any) => r.status === "error").length;
1093
+
1094
+ if (anyLoad) {
1095
+ return `Loading ${loaded} of 3...`;
1096
+ }
1097
+
1098
+ if (anyErr && loaded > 0) {
1099
+ return `${errCount} failed, ${loaded} loaded`;
1100
+ }
1101
+
1102
+ if (anyErr) {
1103
+ return `${errCount} failed`;
1104
+ }
1105
+
1106
+ return "All loaded";
1107
+ },
1108
+
1109
+ canStart: (facts) => {
1110
+ const id = facts.userId.trim();
1111
+ const allIdle = [
1112
+ facts.profile,
1113
+ facts.preferences,
1114
+ facts.permissions,
1115
+ ].every((r: any) => r.status === "idle");
1116
+
1117
+ return id.length > 0 && allIdle;
1118
+ },
1119
+ },
1120
+
1121
+ // ============================================================================
1122
+ // Events
1123
+ // ============================================================================
1124
+
1125
+ events: {
1126
+ setUserId: (facts, { value }) => {
1127
+ facts.userId = value;
1128
+ },
1129
+
1130
+ start: (facts) => {
1131
+ const id = facts.userId.trim();
1132
+ if (id.length === 0) {
1133
+ return;
1134
+ }
1135
+
1136
+ // Reset all resources to idle so constraints re-fire
1137
+ facts.profile = makeIdleResource<Profile>();
1138
+ facts.preferences = makeIdleResource<Preferences>();
1139
+ facts.permissions = makeIdleResource<Permissions>();
1140
+ facts.loadRequested = true;
1141
+ facts.eventLog = [];
1142
+ },
1143
+
1144
+ retryResource: (facts, { resource }) => {
1145
+ const res = (facts as any)[resource] as ResourceState<unknown>;
1146
+ if (!res || res.status !== "error") {
1147
+ return;
1148
+ }
1149
+
1150
+ (facts as any)[resource] = {
1151
+ ...res,
1152
+ status: "idle",
1153
+ error: null,
1154
+ };
1155
+ },
1156
+
1157
+ reloadAll: (facts) => {
1158
+ facts.profile = makeIdleResource<Profile>();
1159
+ facts.preferences = makeIdleResource<Preferences>();
1160
+ facts.permissions = makeIdleResource<Permissions>();
1161
+ facts.eventLog = [];
1162
+ },
1163
+
1164
+ setDelay: (facts, { resource, value }) => {
1165
+ const key = `${resource}Delay` as keyof typeof facts;
1166
+ if (key in facts) {
1167
+ (facts as any)[key] = value;
1168
+ }
1169
+ },
1170
+
1171
+ setFailRate: (facts, { resource, value }) => {
1172
+ const key = `${resource}FailRate` as keyof typeof facts;
1173
+ if (key in facts) {
1174
+ (facts as any)[key] = value;
1175
+ }
1176
+ },
1177
+ },
1178
+
1179
+ // ============================================================================
1180
+ // Constraints
1181
+ // ============================================================================
1182
+
1183
+ constraints: {
1184
+ needsProfile: {
1185
+ priority: 100,
1186
+ when: (facts) => {
1187
+ const id = facts.userId.trim();
1188
+ const profile = facts.profile;
1189
+
1190
+ return facts.loadRequested && id !== "" && profile.status === "idle";
1191
+ },
1192
+ require: (facts) => ({
1193
+ type: "FETCH_PROFILE",
1194
+ userId: facts.userId.trim(),
1195
+ }),
1196
+ },
1197
+
1198
+ needsPreferences: {
1199
+ priority: 90,
1200
+ when: (facts) => {
1201
+ const id = facts.userId.trim();
1202
+ const prefs = facts.preferences;
1203
+
1204
+ return facts.loadRequested && id !== "" && prefs.status === "idle";
1205
+ },
1206
+ require: (facts) => ({
1207
+ type: "FETCH_PREFERENCES",
1208
+ userId: facts.userId.trim(),
1209
+ }),
1210
+ },
1211
+
1212
+ needsPermissions: {
1213
+ priority: 80,
1214
+ when: (facts) => {
1215
+ const id = facts.userId.trim();
1216
+ const perms = facts.permissions;
1217
+
1218
+ return facts.loadRequested && id !== "" && perms.status === "idle";
1219
+ },
1220
+ require: (facts) => ({
1221
+ type: "FETCH_PERMISSIONS",
1222
+ userId: facts.userId.trim(),
1223
+ }),
1224
+ },
1225
+ },
1226
+
1227
+ // ============================================================================
1228
+ // Resolvers
1229
+ // ============================================================================
1230
+
1231
+ resolvers: {
1232
+ fetchProfile: {
1233
+ requirement: "FETCH_PROFILE",
1234
+ retry: { attempts: 3, backoff: "exponential" },
1235
+ timeout: 10000,
1236
+ resolve: async (req, context) => {
1237
+ const prev = context.facts.profile;
1238
+ context.facts.profile = {
1239
+ ...prev,
1240
+ status: "loading",
1241
+ attempts: prev.attempts + 1,
1242
+ startedAt: prev.startedAt ?? Date.now(),
1243
+ };
1244
+ addLogEntry(
1245
+ context.facts,
1246
+ "loading",
1247
+ "profile",
1248
+ `Attempt ${prev.attempts + 1}`,
1249
+ );
1250
+
1251
+ try {
1252
+ const data = await fetchMockProfile(
1253
+ req.userId,
1254
+ context.facts.profileDelay,
1255
+ context.facts.profileFailRate,
1256
+ );
1257
+ context.facts.profile = {
1258
+ data,
1259
+ status: "success",
1260
+ error: null,
1261
+ attempts: context.facts.profile.attempts,
1262
+ startedAt: context.facts.profile.startedAt,
1263
+ completedAt: Date.now(),
1264
+ };
1265
+ addLogEntry(context.facts, "success", "profile", data.name);
1266
+ } catch (err) {
1267
+ const msg = err instanceof Error ? err.message : "Unknown error";
1268
+ context.facts.profile = {
1269
+ ...context.facts.profile,
1270
+ status: "error",
1271
+ error: msg,
1272
+ completedAt: Date.now(),
1273
+ };
1274
+ addLogEntry(context.facts, "error", "profile", msg);
1275
+ throw err;
1276
+ }
1277
+ },
1278
+ },
1279
+
1280
+ fetchPreferences: {
1281
+ requirement: "FETCH_PREFERENCES",
1282
+ retry: { attempts: 2, backoff: "exponential" },
1283
+ resolve: async (req, context) => {
1284
+ const prev = context.facts.preferences;
1285
+ context.facts.preferences = {
1286
+ ...prev,
1287
+ status: "loading",
1288
+ attempts: prev.attempts + 1,
1289
+ startedAt: prev.startedAt ?? Date.now(),
1290
+ };
1291
+ addLogEntry(
1292
+ context.facts,
1293
+ "loading",
1294
+ "preferences",
1295
+ `Attempt ${prev.attempts + 1}`,
1296
+ );
1297
+
1298
+ try {
1299
+ const data = await fetchMockPreferences(
1300
+ req.userId,
1301
+ context.facts.preferencesDelay,
1302
+ context.facts.preferencesFailRate,
1303
+ );
1304
+ context.facts.preferences = {
1305
+ data,
1306
+ status: "success",
1307
+ error: null,
1308
+ attempts: context.facts.preferences.attempts,
1309
+ startedAt: context.facts.preferences.startedAt,
1310
+ completedAt: Date.now(),
1311
+ };
1312
+ addLogEntry(
1313
+ context.facts,
1314
+ "success",
1315
+ "preferences",
1316
+ `${data.theme} / ${data.locale}`,
1317
+ );
1318
+ } catch (err) {
1319
+ const msg = err instanceof Error ? err.message : "Unknown error";
1320
+ context.facts.preferences = {
1321
+ ...context.facts.preferences,
1322
+ status: "error",
1323
+ error: msg,
1324
+ completedAt: Date.now(),
1325
+ };
1326
+ addLogEntry(context.facts, "error", "preferences", msg);
1327
+ throw err;
1328
+ }
1329
+ },
1330
+ },
1331
+
1332
+ fetchPermissions: {
1333
+ requirement: "FETCH_PERMISSIONS",
1334
+ retry: { attempts: 3, backoff: "exponential" },
1335
+ timeout: 15000,
1336
+ resolve: async (req, context) => {
1337
+ const prev = context.facts.permissions;
1338
+ context.facts.permissions = {
1339
+ ...prev,
1340
+ status: "loading",
1341
+ attempts: prev.attempts + 1,
1342
+ startedAt: prev.startedAt ?? Date.now(),
1343
+ };
1344
+ addLogEntry(
1345
+ context.facts,
1346
+ "loading",
1347
+ "permissions",
1348
+ `Attempt ${prev.attempts + 1}`,
1349
+ );
1350
+
1351
+ try {
1352
+ const data = await fetchMockPermissions(
1353
+ req.userId,
1354
+ context.facts.permissionsDelay,
1355
+ context.facts.permissionsFailRate,
1356
+ );
1357
+ context.facts.permissions = {
1358
+ data,
1359
+ status: "success",
1360
+ error: null,
1361
+ attempts: context.facts.permissions.attempts,
1362
+ startedAt: context.facts.permissions.startedAt,
1363
+ completedAt: Date.now(),
1364
+ };
1365
+ addLogEntry(
1366
+ context.facts,
1367
+ "success",
1368
+ "permissions",
1369
+ `${data.role} (${data.features.join(", ")})`,
1370
+ );
1371
+ } catch (err) {
1372
+ const msg = err instanceof Error ? err.message : "Unknown error";
1373
+ context.facts.permissions = {
1374
+ ...context.facts.permissions,
1375
+ status: "error",
1376
+ error: msg,
1377
+ completedAt: Date.now(),
1378
+ };
1379
+ addLogEntry(context.facts, "error", "permissions", msg);
1380
+ throw err;
1381
+ }
1382
+ },
1383
+ },
1384
+ },
1385
+ });
1386
+ ```