@directive-run/knowledge 0.2.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/ai/ai-adapters.md +250 -0
  4. package/ai/ai-agents-streaming.md +269 -0
  5. package/ai/ai-budget-resilience.md +235 -0
  6. package/ai/ai-communication.md +281 -0
  7. package/ai/ai-debug-observability.md +243 -0
  8. package/ai/ai-guardrails-memory.md +332 -0
  9. package/ai/ai-mcp-rag.md +288 -0
  10. package/ai/ai-multi-agent.md +274 -0
  11. package/ai/ai-orchestrator.md +227 -0
  12. package/ai/ai-security.md +293 -0
  13. package/ai/ai-tasks.md +261 -0
  14. package/ai/ai-testing-evals.md +378 -0
  15. package/api-skeleton.md +5 -0
  16. package/core/anti-patterns.md +382 -0
  17. package/core/constraints.md +263 -0
  18. package/core/core-patterns.md +228 -0
  19. package/core/error-boundaries.md +322 -0
  20. package/core/multi-module.md +315 -0
  21. package/core/naming.md +283 -0
  22. package/core/plugins.md +344 -0
  23. package/core/react-adapter.md +262 -0
  24. package/core/resolvers.md +357 -0
  25. package/core/schema-types.md +262 -0
  26. package/core/system-api.md +271 -0
  27. package/core/testing.md +257 -0
  28. package/core/time-travel.md +238 -0
  29. package/dist/index.cjs +111 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +10 -0
  32. package/dist/index.d.ts +10 -0
  33. package/dist/index.js +102 -0
  34. package/dist/index.js.map +1 -0
  35. package/examples/ab-testing.ts +385 -0
  36. package/examples/ai-checkpoint.ts +509 -0
  37. package/examples/ai-guardrails.ts +319 -0
  38. package/examples/ai-orchestrator.ts +589 -0
  39. package/examples/async-chains.ts +287 -0
  40. package/examples/auth-flow.ts +371 -0
  41. package/examples/batch-resolver.ts +341 -0
  42. package/examples/checkers.ts +589 -0
  43. package/examples/contact-form.ts +176 -0
  44. package/examples/counter.ts +393 -0
  45. package/examples/dashboard-loader.ts +512 -0
  46. package/examples/debounce-constraints.ts +105 -0
  47. package/examples/dynamic-modules.ts +293 -0
  48. package/examples/error-boundaries.ts +430 -0
  49. package/examples/feature-flags.ts +220 -0
  50. package/examples/form-wizard.ts +347 -0
  51. package/examples/fraud-analysis.ts +663 -0
  52. package/examples/goal-heist.ts +341 -0
  53. package/examples/multi-module.ts +57 -0
  54. package/examples/newsletter.ts +241 -0
  55. package/examples/notifications.ts +210 -0
  56. package/examples/optimistic-updates.ts +317 -0
  57. package/examples/pagination.ts +260 -0
  58. package/examples/permissions.ts +337 -0
  59. package/examples/provider-routing.ts +403 -0
  60. package/examples/server.ts +316 -0
  61. package/examples/shopping-cart.ts +422 -0
  62. package/examples/sudoku.ts +630 -0
  63. package/examples/theme-locale.ts +204 -0
  64. package/examples/time-machine.ts +225 -0
  65. package/examples/topic-guard.ts +306 -0
  66. package/examples/url-sync.ts +333 -0
  67. package/examples/websocket.ts +404 -0
  68. package/package.json +65 -0
@@ -0,0 +1,663 @@
1
+ // Example: fraud-analysis
2
+ // Source: examples/fraud-analysis/src/fraud-analysis.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Fraud Case Analysis — Directive Module
7
+ *
8
+ * Multi-stage fraud detection pipeline showcasing every major Directive feature:
9
+ * - 6 constraints with priority + `after` ordering (including competing constraints)
10
+ * - 6 resolvers with retry policies and custom dedup keys
11
+ * - 3 effects with explicit deps
12
+ * - 9 derivations with composition
13
+ * - Local PII detection + checkpoint store
14
+ * - DevTools panel with time-travel debugging
15
+ */
16
+
17
+ import {
18
+ type ModuleSchema,
19
+ createModule,
20
+ createSystem,
21
+ t,
22
+ } from "@directive-run/core";
23
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
24
+ import { InMemoryCheckpointStore } from "./checkpoint.js";
25
+ import { detectPII, redactPII } from "./pii.js";
26
+
27
+ import {
28
+ type CheckpointEntry,
29
+ type Disposition,
30
+ type FlagEvent,
31
+ type FraudCase,
32
+ type PipelineStage,
33
+ type Severity,
34
+ type TimelineEntry,
35
+ getMockEnrichment,
36
+ } from "./mock-data.js";
37
+
38
+ // ============================================================================
39
+ // Timeline (external mutable array, same pattern as ai-checkpoint)
40
+ // ============================================================================
41
+
42
+ export const timeline: TimelineEntry[] = [];
43
+
44
+ export function addTimeline(
45
+ type: TimelineEntry["type"],
46
+ message: string,
47
+ ): void {
48
+ timeline.push({
49
+ time: new Date().toLocaleTimeString(),
50
+ type,
51
+ message,
52
+ });
53
+ }
54
+
55
+ // ============================================================================
56
+ // Checkpoint Store
57
+ // ============================================================================
58
+
59
+ export const checkpointStore = new InMemoryCheckpointStore();
60
+
61
+ // ============================================================================
62
+ // Analysis Helpers
63
+ // ============================================================================
64
+
65
+ interface AnalysisResult {
66
+ riskScore: number;
67
+ severity: Severity;
68
+ disposition: Disposition;
69
+ analysisNotes: string;
70
+ }
71
+
72
+ /** Deterministic risk scoring formula */
73
+ function analyzeWithFormula(fraudCase: FraudCase): AnalysisResult {
74
+ const avgSignalRisk =
75
+ fraudCase.signals.length > 0
76
+ ? fraudCase.signals.reduce((sum, s) => sum + s.risk, 0) /
77
+ fraudCase.signals.length
78
+ : 50;
79
+
80
+ const totalAmount = fraudCase.events.reduce((sum, e) => sum + e.amount, 0);
81
+ const amountFactor = Math.min(totalAmount / 10000, 1) * 30;
82
+ const eventFactor = Math.min(fraudCase.events.length / 10, 1) * 20;
83
+ const piiFactor = fraudCase.events.some((e) => e.piiFound) ? 15 : 0;
84
+
85
+ const riskScore = Math.min(
86
+ 100,
87
+ Math.round(avgSignalRisk * 0.5 + amountFactor + eventFactor + piiFactor),
88
+ );
89
+
90
+ let severity: Severity = "low";
91
+ if (riskScore >= 80) {
92
+ severity = "critical";
93
+ } else if (riskScore >= 60) {
94
+ severity = "high";
95
+ } else if (riskScore >= 40) {
96
+ severity = "medium";
97
+ }
98
+
99
+ let disposition: Disposition = "pending";
100
+ let notes = `Risk: ${riskScore}/100. Signals: ${fraudCase.signals.map((s) => s.source).join(", ")}.`;
101
+
102
+ if (riskScore <= 30) {
103
+ disposition = "cleared";
104
+ notes += " Auto-cleared: low risk.";
105
+ } else if (riskScore <= 50) {
106
+ disposition = "flagged";
107
+ notes += " Flagged for monitoring.";
108
+ }
109
+
110
+ return { riskScore, severity, disposition, analysisNotes: notes };
111
+ }
112
+
113
+ // ============================================================================
114
+ // Schema
115
+ // ============================================================================
116
+
117
+ export const fraudSchema = {
118
+ facts: {
119
+ stage: t.string<PipelineStage>(),
120
+ flagEvents: t.array<FlagEvent>(),
121
+ cases: t.array<FraudCase>(),
122
+ isRunning: t.boolean(),
123
+ totalEventsProcessed: t.number(),
124
+ totalPiiDetections: t.number(),
125
+ analysisBudget: t.number(),
126
+ maxAnalysisBudget: t.number(),
127
+ riskThreshold: t.number(),
128
+ lastError: t.string(),
129
+ checkpoints: t.array<CheckpointEntry>(),
130
+ selectedScenario: t.string(),
131
+ },
132
+ derivations: {
133
+ ungroupedCount: t.number(),
134
+ caseCount: t.number(),
135
+ criticalCaseCount: t.number(),
136
+ pendingAnalysisCount: t.number(),
137
+ needsHumanReview: t.boolean(),
138
+ budgetExhausted: t.boolean(),
139
+ completionPercentage: t.number(),
140
+ averageRiskScore: t.number(),
141
+ dispositionSummary: t.object<Record<string, number>>(),
142
+ },
143
+ events: {
144
+ ingestEvents: { events: t.array<FlagEvent>() },
145
+ setRiskThreshold: { value: t.number() },
146
+ setBudget: { value: t.number() },
147
+ selectScenario: { key: t.string() },
148
+ reset: {},
149
+ },
150
+ requirements: {
151
+ NORMALIZE_EVENTS: {},
152
+ GROUP_EVENTS: {},
153
+ ENRICH_CASE: { caseId: t.string() },
154
+ ANALYZE_CASE: { caseId: t.string() },
155
+ HUMAN_REVIEW: { caseId: t.string() },
156
+ ESCALATE: { caseId: t.string() },
157
+ },
158
+ } satisfies ModuleSchema;
159
+
160
+ // ============================================================================
161
+ // Module
162
+ // ============================================================================
163
+
164
+ export const fraudAnalysisModule = createModule("fraud", {
165
+ schema: fraudSchema,
166
+
167
+ init: (facts) => {
168
+ facts.stage = "idle";
169
+ facts.flagEvents = [];
170
+ facts.cases = [];
171
+ facts.isRunning = false;
172
+ facts.totalEventsProcessed = 0;
173
+ facts.totalPiiDetections = 0;
174
+ facts.analysisBudget = 300;
175
+ facts.maxAnalysisBudget = 300;
176
+ facts.riskThreshold = 70;
177
+ facts.lastError = "";
178
+ facts.checkpoints = [];
179
+ facts.selectedScenario = "card-skimming";
180
+ },
181
+
182
+ // ============================================================================
183
+ // Derivations (9)
184
+ // ============================================================================
185
+
186
+ derive: {
187
+ ungroupedCount: (facts) => {
188
+ return facts.flagEvents.filter((e) => !e.grouped).length;
189
+ },
190
+
191
+ caseCount: (facts) => {
192
+ return facts.cases.length;
193
+ },
194
+
195
+ criticalCaseCount: (facts) => {
196
+ return facts.cases.filter((c) => c.severity === "critical").length;
197
+ },
198
+
199
+ pendingAnalysisCount: (facts) => {
200
+ return facts.cases.filter((c) => c.enriched && !c.analyzed).length;
201
+ },
202
+
203
+ needsHumanReview: (facts) => {
204
+ return facts.cases.some(
205
+ (c) => c.riskScore > facts.riskThreshold && c.disposition === "pending",
206
+ );
207
+ },
208
+
209
+ budgetExhausted: (facts) => {
210
+ return facts.analysisBudget <= 0;
211
+ },
212
+
213
+ completionPercentage: (facts) => {
214
+ const stages: PipelineStage[] = [
215
+ "idle",
216
+ "ingesting",
217
+ "normalizing",
218
+ "grouping",
219
+ "enriching",
220
+ "analyzing",
221
+ "complete",
222
+ ];
223
+ const idx = stages.indexOf(facts.stage);
224
+ if (idx < 0) {
225
+ return 0;
226
+ }
227
+
228
+ return Math.round((idx / (stages.length - 1)) * 100);
229
+ },
230
+
231
+ averageRiskScore: (facts) => {
232
+ if (facts.cases.length === 0) {
233
+ return 0;
234
+ }
235
+
236
+ const sum = facts.cases.reduce((acc, c) => acc + c.riskScore, 0);
237
+
238
+ return Math.round(sum / facts.cases.length);
239
+ },
240
+
241
+ // Composition: derives from cases (same source as caseCount)
242
+ dispositionSummary: (facts) => {
243
+ const summary: Record<string, number> = {};
244
+ for (const c of facts.cases) {
245
+ summary[c.disposition] = (summary[c.disposition] || 0) + 1;
246
+ }
247
+
248
+ return summary;
249
+ },
250
+ },
251
+
252
+ // ============================================================================
253
+ // Events
254
+ // ============================================================================
255
+
256
+ events: {
257
+ ingestEvents: (facts, { events }) => {
258
+ facts.flagEvents = [...facts.flagEvents, ...events];
259
+ facts.totalEventsProcessed = facts.totalEventsProcessed + events.length;
260
+ facts.stage = "ingesting";
261
+ facts.isRunning = true;
262
+ facts.lastError = "";
263
+ },
264
+
265
+ setRiskThreshold: (facts, { value }) => {
266
+ facts.riskThreshold = Math.max(50, Math.min(90, value));
267
+ },
268
+
269
+ setBudget: (facts, { value }) => {
270
+ facts.analysisBudget = Math.max(0, Math.min(500, value));
271
+ facts.maxAnalysisBudget = Math.max(facts.maxAnalysisBudget, value);
272
+ },
273
+
274
+ selectScenario: (facts, { key }) => {
275
+ facts.selectedScenario = key;
276
+ },
277
+
278
+ reset: (facts) => {
279
+ facts.stage = "idle";
280
+ facts.flagEvents = [];
281
+ facts.cases = [];
282
+ facts.isRunning = false;
283
+ facts.totalEventsProcessed = 0;
284
+ facts.totalPiiDetections = 0;
285
+ facts.lastError = "";
286
+ facts.checkpoints = [];
287
+ timeline.length = 0;
288
+ },
289
+ },
290
+
291
+ // ============================================================================
292
+ // Constraints (6 with priority + after ordering)
293
+ // ============================================================================
294
+
295
+ constraints: {
296
+ normalizeNeeded: {
297
+ priority: 100,
298
+ when: (facts) => {
299
+ return facts.stage === "ingesting" && facts.flagEvents.length > 0;
300
+ },
301
+ require: { type: "NORMALIZE_EVENTS" },
302
+ },
303
+
304
+ groupingNeeded: {
305
+ priority: 90,
306
+ after: ["normalizeNeeded"],
307
+ when: (facts) => {
308
+ return facts.flagEvents.some((e) => !e.grouped);
309
+ },
310
+ require: { type: "GROUP_EVENTS" },
311
+ },
312
+
313
+ enrichmentNeeded: {
314
+ priority: 80,
315
+ after: ["groupingNeeded"],
316
+ when: (facts) => {
317
+ return facts.cases.some((c) => !c.enriched && c.signals.length < 3);
318
+ },
319
+ require: (facts) => {
320
+ const target = facts.cases.find(
321
+ (c) => !c.enriched && c.signals.length < 3,
322
+ );
323
+
324
+ return { type: "ENRICH_CASE", caseId: target?.id ?? "" };
325
+ },
326
+ },
327
+
328
+ analysisNeeded: {
329
+ priority: 70,
330
+ after: ["enrichmentNeeded"],
331
+ when: (facts) => {
332
+ return (
333
+ facts.analysisBudget > 0 &&
334
+ facts.cases.some((c) => c.enriched && !c.analyzed)
335
+ );
336
+ },
337
+ require: (facts) => {
338
+ const target = facts.cases.find((c) => c.enriched && !c.analyzed);
339
+
340
+ return { type: "ANALYZE_CASE", caseId: target?.id ?? "" };
341
+ },
342
+ },
343
+
344
+ humanReviewNeeded: {
345
+ priority: 65,
346
+ after: ["analysisNeeded"],
347
+ when: (facts) => {
348
+ return facts.cases.some(
349
+ (c) =>
350
+ c.analyzed &&
351
+ c.riskScore > facts.riskThreshold &&
352
+ c.disposition === "pending",
353
+ );
354
+ },
355
+ require: (facts) => {
356
+ const target = facts.cases.find(
357
+ (c) =>
358
+ c.analyzed &&
359
+ c.riskScore > facts.riskThreshold &&
360
+ c.disposition === "pending",
361
+ );
362
+
363
+ return { type: "HUMAN_REVIEW", caseId: target?.id ?? "" };
364
+ },
365
+ },
366
+
367
+ budgetEscalation: {
368
+ priority: 60,
369
+ when: (facts) => {
370
+ return (
371
+ facts.analysisBudget <= 0 &&
372
+ facts.cases.some(
373
+ (c) => c.enriched && !c.analyzed && c.disposition === "pending",
374
+ )
375
+ );
376
+ },
377
+ require: (facts) => {
378
+ const target = facts.cases.find(
379
+ (c) => c.enriched && !c.analyzed && c.disposition === "pending",
380
+ );
381
+
382
+ return { type: "ESCALATE", caseId: target?.id ?? "" };
383
+ },
384
+ },
385
+ },
386
+
387
+ // ============================================================================
388
+ // Resolvers (6)
389
+ // ============================================================================
390
+
391
+ resolvers: {
392
+ normalizeEvents: {
393
+ requirement: "NORMALIZE_EVENTS",
394
+ resolve: async (_req, context) => {
395
+ addTimeline("stage", "normalizing events");
396
+
397
+ const events = [...context.facts.flagEvents];
398
+ let piiCount = 0;
399
+
400
+ for (let i = 0; i < events.length; i++) {
401
+ const event = events[i];
402
+
403
+ // Run PII detection on merchant + memo fields
404
+ const merchantResult = await detectPII(event.merchant, {
405
+ types: ["credit_card", "bank_account", "ssn"],
406
+ });
407
+ const memoResult = await detectPII(event.memo, {
408
+ types: ["credit_card", "bank_account", "ssn"],
409
+ });
410
+
411
+ const hasPii = merchantResult.detected || memoResult.detected;
412
+ if (hasPii) {
413
+ piiCount++;
414
+ }
415
+
416
+ events[i] = {
417
+ ...event,
418
+ piiFound: hasPii,
419
+ redactedMerchant: merchantResult.detected
420
+ ? redactPII(event.merchant, merchantResult.items, "typed")
421
+ : event.merchant,
422
+ redactedMemo: memoResult.detected
423
+ ? redactPII(event.memo, memoResult.items, "typed")
424
+ : event.memo,
425
+ };
426
+ }
427
+
428
+ // Simulate processing delay (before fact mutations to avoid
429
+ // mid-resolver reconcile canceling this resolver)
430
+ await delay(300);
431
+
432
+ // All fact mutations at the end — no more awaits after this
433
+ context.facts.stage = "normalizing";
434
+ context.facts.flagEvents = events;
435
+ context.facts.totalPiiDetections =
436
+ context.facts.totalPiiDetections + piiCount;
437
+ },
438
+ },
439
+
440
+ groupEvents: {
441
+ requirement: "GROUP_EVENTS",
442
+ resolve: async (_req, context) => {
443
+ addTimeline("stage", "grouping events into cases");
444
+
445
+ const events = [...context.facts.flagEvents];
446
+ const existingCases = [...context.facts.cases];
447
+
448
+ // Group by accountId
449
+ const groups = new Map<string, FlagEvent[]>();
450
+ for (const event of events) {
451
+ if (event.grouped) {
452
+ continue;
453
+ }
454
+
455
+ const existing = groups.get(event.accountId) ?? [];
456
+ existing.push(event);
457
+ groups.set(event.accountId, existing);
458
+ }
459
+
460
+ // Create cases from groups
461
+ let caseNum = existingCases.length;
462
+ for (const [accountId, groupEvents] of groups) {
463
+ caseNum++;
464
+ const newCase: FraudCase = {
465
+ id: `case-${String(caseNum).padStart(3, "0")}`,
466
+ accountId,
467
+ events: groupEvents,
468
+ signals: [],
469
+ enriched: false,
470
+ analyzed: false,
471
+ riskScore: 0,
472
+ severity: "low",
473
+ disposition: "pending",
474
+ };
475
+ existingCases.push(newCase);
476
+ }
477
+
478
+ // Mark all events as grouped
479
+ const markedEvents = events.map((e) => ({ ...e, grouped: true }));
480
+
481
+ await delay(200);
482
+
483
+ // All fact mutations at the end — no more awaits after this
484
+ context.facts.stage = "grouping";
485
+ context.facts.flagEvents = markedEvents;
486
+ context.facts.cases = existingCases;
487
+ },
488
+ },
489
+
490
+ enrichCase: {
491
+ requirement: "ENRICH_CASE",
492
+ key: (req) => `enrich-${req.caseId}`,
493
+ retry: { attempts: 2, backoff: "exponential" },
494
+ resolve: async (req, context) => {
495
+ addTimeline("stage", `enriching ${req.caseId}`);
496
+
497
+ const cases = [...context.facts.cases];
498
+ const idx = cases.findIndex((c) => c.id === req.caseId);
499
+ if (idx < 0) {
500
+ return;
501
+ }
502
+
503
+ const signals = getMockEnrichment(cases[idx].accountId);
504
+
505
+ // Simulate API call
506
+ await delay(400);
507
+
508
+ // All fact mutations at the end — no more awaits after this
509
+ cases[idx] = {
510
+ ...cases[idx],
511
+ signals,
512
+ enriched: true,
513
+ };
514
+ context.facts.stage = "enriching";
515
+ context.facts.cases = cases;
516
+ },
517
+ },
518
+
519
+ analyzeCase: {
520
+ requirement: "ANALYZE_CASE",
521
+ key: (req) => `analyze-${req.caseId}`,
522
+ retry: { attempts: 1, backoff: "none" },
523
+ resolve: async (req, context) => {
524
+ addTimeline("stage", `analyzing ${req.caseId}`);
525
+
526
+ const cases = [...context.facts.cases];
527
+ const idx = cases.findIndex((c) => c.id === req.caseId);
528
+ if (idx < 0) {
529
+ return;
530
+ }
531
+
532
+ const fraudCase = cases[idx];
533
+
534
+ // Consume budget
535
+ const cost = 25 + Math.floor(fraudCase.events.length * 5);
536
+
537
+ // Deterministic analysis
538
+ await delay(500);
539
+ const result = analyzeWithFormula(fraudCase);
540
+ if (
541
+ result.disposition === "pending" &&
542
+ result.riskScore <= context.facts.riskThreshold
543
+ ) {
544
+ result.disposition = "flagged";
545
+ result.analysisNotes +=
546
+ " Auto-flagged: below human review threshold.";
547
+ }
548
+
549
+ // All fact mutations at the end — no more awaits after this
550
+ cases[idx] = { ...fraudCase, ...result, analyzed: true };
551
+ context.facts.stage = "analyzing";
552
+ context.facts.analysisBudget = Math.max(
553
+ 0,
554
+ context.facts.analysisBudget - cost,
555
+ );
556
+ context.facts.cases = cases;
557
+ },
558
+ },
559
+
560
+ humanReview: {
561
+ requirement: "HUMAN_REVIEW",
562
+ resolve: async (req, context) => {
563
+ addTimeline("info", `${req.caseId} sent to human review`);
564
+
565
+ const cases = [...context.facts.cases];
566
+ const idx = cases.findIndex((c) => c.id === req.caseId);
567
+ if (idx < 0) {
568
+ return;
569
+ }
570
+
571
+ await delay(100);
572
+
573
+ cases[idx] = {
574
+ ...cases[idx],
575
+ disposition: "human_review",
576
+ dispositionReason: "Risk score exceeds threshold",
577
+ };
578
+ context.facts.cases = cases;
579
+ },
580
+ },
581
+
582
+ escalate: {
583
+ requirement: "ESCALATE",
584
+ resolve: async (req, context) => {
585
+ addTimeline("info", `${req.caseId} escalated (budget exhausted)`);
586
+
587
+ const cases = [...context.facts.cases];
588
+ const idx = cases.findIndex((c) => c.id === req.caseId);
589
+ if (idx < 0) {
590
+ return;
591
+ }
592
+
593
+ await delay(100);
594
+
595
+ cases[idx] = {
596
+ ...cases[idx],
597
+ disposition: "escalated",
598
+ dispositionReason: "Analysis budget exhausted",
599
+ };
600
+ context.facts.cases = cases;
601
+ },
602
+ },
603
+ },
604
+
605
+ // ============================================================================
606
+ // Effects (3)
607
+ // ============================================================================
608
+
609
+ effects: {
610
+ logStageChange: {
611
+ deps: ["stage"],
612
+ run: (facts, prev) => {
613
+ if (prev && prev.stage !== facts.stage) {
614
+ addTimeline("stage", `${prev.stage} → ${facts.stage}`);
615
+ }
616
+ },
617
+ },
618
+
619
+ logPiiDetection: {
620
+ deps: ["totalPiiDetections"],
621
+ run: (facts, prev) => {
622
+ if (prev && facts.totalPiiDetections !== prev.totalPiiDetections) {
623
+ addTimeline(
624
+ "pii",
625
+ `PII guardrail fired (${facts.totalPiiDetections} total detections)`,
626
+ );
627
+ }
628
+ },
629
+ },
630
+
631
+ logBudgetWarning: {
632
+ deps: ["analysisBudget"],
633
+ run: (facts, prev) => {
634
+ if (prev && prev.analysisBudget > 0 && facts.analysisBudget <= 0) {
635
+ addTimeline("budget", "analysis budget exhausted");
636
+ }
637
+ },
638
+ },
639
+ },
640
+ });
641
+
642
+ // ============================================================================
643
+ // System
644
+ // ============================================================================
645
+
646
+ export const system = createSystem({
647
+ module: fraudAnalysisModule,
648
+ plugins: [devtoolsPlugin({ name: "fraud-analysis", panel: true })],
649
+ debug: {
650
+ timeTravel: true,
651
+ maxSnapshots: 50,
652
+ runHistory: true,
653
+ maxRuns: 100,
654
+ },
655
+ });
656
+
657
+ // ============================================================================
658
+ // Helpers
659
+ // ============================================================================
660
+
661
+ export function delay(ms: number): Promise<void> {
662
+ return new Promise((resolve) => setTimeout(resolve, ms));
663
+ }