@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,630 @@
1
+ // Example: sudoku
2
+ // Source: examples/sudoku/src/sudoku.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Sudoku – Directive Module
7
+ *
8
+ * Constraint-driven Sudoku game. Sudoku IS a constraint satisfaction problem:
9
+ * no duplicates in rows, columns, or 3x3 boxes. The game rules map directly
10
+ * to Directive's constraint→resolver flow.
11
+ *
12
+ * Also demonstrates temporal constraints (countdown timer) and runtime
13
+ * reconfiguration (difficulty modes) – patterns not shown in checkers.
14
+ *
15
+ * Pure Sudoku logic lives in rules.ts; puzzle generation in generator.ts.
16
+ */
17
+
18
+ import { type ModuleSchema, createModule, t } from "@directive-run/core";
19
+ import { generatePuzzle } from "./generator.js";
20
+ import {
21
+ type Conflict,
22
+ type Difficulty,
23
+ type Grid,
24
+ MAX_HINTS,
25
+ TIMER_CRITICAL_THRESHOLD,
26
+ TIMER_DURATIONS,
27
+ TIMER_EFFECT_CRITICAL,
28
+ TIMER_EFFECT_WARNING,
29
+ TIMER_WARNING_THRESHOLD,
30
+ createEmptyNotes,
31
+ findConflicts,
32
+ getCandidates,
33
+ getPeers,
34
+ isBoardComplete,
35
+ toRowCol,
36
+ } from "./rules.js";
37
+
38
+ // ============================================================================
39
+ // Schema
40
+ // ============================================================================
41
+
42
+ export const sudokuSchema = {
43
+ facts: {
44
+ grid: t.object<Grid>(),
45
+ solution: t.object<Grid>(),
46
+ givens: t.object<Set<number>>(),
47
+ selectedIndex: t.object<number | null>(),
48
+ difficulty: t.object<Difficulty>(),
49
+ timerRemaining: t.number(),
50
+ timerRunning: t.boolean(),
51
+ gameOver: t.boolean(),
52
+ won: t.boolean(),
53
+ message: t.string(),
54
+ notesMode: t.boolean(),
55
+ notes: t.object<Set<number>[]>(),
56
+ hintsUsed: t.number(),
57
+ errorsCount: t.number(),
58
+ hintRequested: t.boolean(),
59
+ },
60
+ derivations: {
61
+ conflicts: t.object<Conflict[]>(),
62
+ conflictIndices: t.object<Set<number>>(),
63
+ hasConflicts: t.boolean(),
64
+ filledCount: t.number(),
65
+ progress: t.number(),
66
+ isComplete: t.boolean(),
67
+ isSolved: t.boolean(),
68
+ selectedPeers: t.object<number[]>(),
69
+ highlightValue: t.number(),
70
+ sameValueIndices: t.object<Set<number>>(),
71
+ candidates: t.object<number[]>(),
72
+ timerDisplay: t.string(),
73
+ timerUrgency: t.object<"normal" | "warning" | "critical">(),
74
+ },
75
+ events: {
76
+ newGame: { difficulty: t.object<Difficulty>() },
77
+ selectCell: { index: t.number() },
78
+ inputNumber: { value: t.number() },
79
+ toggleNote: { value: t.number() },
80
+ toggleNotesMode: {},
81
+ requestHint: {},
82
+ tick: {},
83
+ },
84
+ requirements: {
85
+ SHOW_CONFLICT: {
86
+ index: t.number(),
87
+ value: t.number(),
88
+ row: t.number(),
89
+ col: t.number(),
90
+ },
91
+ GAME_WON: {
92
+ timeLeft: t.number(),
93
+ hintsUsed: t.number(),
94
+ errors: t.number(),
95
+ },
96
+ GAME_OVER: {
97
+ reason: t.string(),
98
+ },
99
+ REVEAL_HINT: {
100
+ index: t.number(),
101
+ value: t.number(),
102
+ },
103
+ },
104
+ } satisfies ModuleSchema;
105
+
106
+ // ============================================================================
107
+ // Module
108
+ // ============================================================================
109
+
110
+ export const sudokuGame = createModule("sudoku", {
111
+ schema: sudokuSchema,
112
+ snapshotEvents: ["inputNumber", "toggleNote", "requestHint", "newGame"],
113
+
114
+ init: (facts) => {
115
+ const { puzzle, solution } = generatePuzzle("easy");
116
+ const givens = new Set<number>();
117
+ for (let i = 0; i < 81; i++) {
118
+ if (puzzle[i] !== 0) {
119
+ givens.add(i);
120
+ }
121
+ }
122
+
123
+ facts.grid = puzzle;
124
+ facts.solution = solution;
125
+ facts.givens = givens;
126
+ facts.selectedIndex = null;
127
+ facts.difficulty = "easy";
128
+ facts.timerRemaining = TIMER_DURATIONS.easy;
129
+ facts.timerRunning = true;
130
+ facts.gameOver = false;
131
+ facts.won = false;
132
+ facts.message =
133
+ "Fill in the grid. No duplicates in rows, columns, or boxes.";
134
+ facts.notesMode = false;
135
+ facts.notes = createEmptyNotes();
136
+ facts.hintsUsed = 0;
137
+ facts.errorsCount = 0;
138
+ facts.hintRequested = false;
139
+ },
140
+
141
+ // ============================================================================
142
+ // Derivations
143
+ // ============================================================================
144
+
145
+ derive: {
146
+ conflicts: (facts) => {
147
+ return findConflicts(facts.grid as Grid);
148
+ },
149
+
150
+ conflictIndices: (facts, derive) => {
151
+ const indices = new Set<number>();
152
+ const givens = facts.givens as Set<number>;
153
+ for (const c of derive.conflicts as Conflict[]) {
154
+ // Only highlight player-placed cells, not givens
155
+ if (!givens.has(c.index)) {
156
+ indices.add(c.index);
157
+ }
158
+ }
159
+
160
+ return indices;
161
+ },
162
+
163
+ hasConflicts: (_facts, derive) => {
164
+ return (derive.conflicts as Conflict[]).length > 0;
165
+ },
166
+
167
+ filledCount: (facts) => {
168
+ let count = 0;
169
+ const grid = facts.grid as Grid;
170
+ for (let i = 0; i < 81; i++) {
171
+ if (grid[i] !== 0) {
172
+ count++;
173
+ }
174
+ }
175
+
176
+ return count;
177
+ },
178
+
179
+ progress: (_facts, derive) => {
180
+ return Math.round(((derive.filledCount as number) / 81) * 100);
181
+ },
182
+
183
+ isComplete: (facts) => {
184
+ return isBoardComplete(facts.grid as Grid);
185
+ },
186
+
187
+ isSolved: (_facts, derive) => {
188
+ return (
189
+ (derive.isComplete as boolean) && !(derive.hasConflicts as boolean)
190
+ );
191
+ },
192
+
193
+ selectedPeers: (facts) => {
194
+ const sel = facts.selectedIndex as number | null;
195
+ if (sel === null) {
196
+ return [];
197
+ }
198
+
199
+ return getPeers(sel);
200
+ },
201
+
202
+ highlightValue: (facts) => {
203
+ const sel = facts.selectedIndex as number | null;
204
+ if (sel === null) {
205
+ return 0;
206
+ }
207
+
208
+ return (facts.grid as Grid)[sel];
209
+ },
210
+
211
+ sameValueIndices: (facts, derive) => {
212
+ const val = derive.highlightValue as number;
213
+ if (val === 0) {
214
+ return new Set<number>();
215
+ }
216
+
217
+ const indices = new Set<number>();
218
+ const grid = facts.grid as Grid;
219
+ for (let i = 0; i < 81; i++) {
220
+ if (grid[i] === val) {
221
+ indices.add(i);
222
+ }
223
+ }
224
+
225
+ return indices;
226
+ },
227
+
228
+ candidates: (facts) => {
229
+ const sel = facts.selectedIndex as number | null;
230
+ if (sel === null) {
231
+ return [];
232
+ }
233
+
234
+ return getCandidates(facts.grid as Grid, sel);
235
+ },
236
+
237
+ timerDisplay: (facts) => {
238
+ const remaining = facts.timerRemaining as number;
239
+ const mins = Math.max(0, Math.floor(remaining / 60));
240
+ const secs = Math.max(0, remaining % 60);
241
+
242
+ return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
243
+ },
244
+
245
+ timerUrgency: (facts) => {
246
+ const remaining = facts.timerRemaining as number;
247
+ if (remaining <= TIMER_CRITICAL_THRESHOLD) {
248
+ return "critical";
249
+ }
250
+ if (remaining <= TIMER_WARNING_THRESHOLD) {
251
+ return "warning";
252
+ }
253
+
254
+ return "normal";
255
+ },
256
+ },
257
+
258
+ // ============================================================================
259
+ // Events
260
+ // ============================================================================
261
+
262
+ events: {
263
+ newGame: (facts, { difficulty }) => {
264
+ const { puzzle, solution } = generatePuzzle(difficulty);
265
+ const givens = new Set<number>();
266
+ for (let i = 0; i < 81; i++) {
267
+ if (puzzle[i] !== 0) {
268
+ givens.add(i);
269
+ }
270
+ }
271
+
272
+ facts.grid = puzzle;
273
+ facts.solution = solution;
274
+ facts.givens = givens;
275
+ facts.selectedIndex = null;
276
+ facts.difficulty = difficulty;
277
+ facts.timerRemaining = TIMER_DURATIONS[difficulty];
278
+ facts.timerRunning = true;
279
+ facts.gameOver = false;
280
+ facts.won = false;
281
+ facts.message =
282
+ "Fill in the grid. No duplicates in rows, columns, or boxes.";
283
+ facts.notesMode = false;
284
+ facts.notes = createEmptyNotes();
285
+ facts.hintsUsed = 0;
286
+ facts.errorsCount = 0;
287
+ facts.hintRequested = false;
288
+ },
289
+
290
+ selectCell: (facts, { index }) => {
291
+ if (facts.gameOver) {
292
+ return;
293
+ }
294
+ facts.selectedIndex = index;
295
+ },
296
+
297
+ inputNumber: (facts, { value }) => {
298
+ if (facts.gameOver) {
299
+ return;
300
+ }
301
+
302
+ const sel = facts.selectedIndex as number | null;
303
+ if (sel === null) {
304
+ return;
305
+ }
306
+
307
+ const givens = facts.givens as Set<number>;
308
+ if (givens.has(sel)) {
309
+ facts.message = "That cell is locked.";
310
+
311
+ return;
312
+ }
313
+
314
+ if (facts.notesMode && value !== 0) {
315
+ // In notes mode, toggle the pencil mark instead
316
+ const notes = [...(facts.notes as Set<number>[])];
317
+ notes[sel] = new Set(notes[sel]);
318
+ if (notes[sel].has(value)) {
319
+ notes[sel].delete(value);
320
+ } else {
321
+ notes[sel].add(value);
322
+ }
323
+ facts.notes = notes;
324
+ facts.message = "";
325
+
326
+ return;
327
+ }
328
+
329
+ // Place or clear a number
330
+ const grid = [...(facts.grid as Grid)];
331
+ grid[sel] = value;
332
+ facts.grid = grid;
333
+
334
+ // Clear notes for this cell when placing a number
335
+ if (value !== 0) {
336
+ const notes = [...(facts.notes as Set<number>[])];
337
+ notes[sel] = new Set();
338
+ // Also clear this value from peer notes
339
+ for (const peer of getPeers(sel)) {
340
+ if (notes[peer].has(value)) {
341
+ notes[peer] = new Set(notes[peer]);
342
+ notes[peer].delete(value);
343
+ }
344
+ }
345
+ facts.notes = notes;
346
+ }
347
+
348
+ facts.message = "";
349
+ },
350
+
351
+ toggleNote: (facts, { value }) => {
352
+ if (facts.gameOver) {
353
+ return;
354
+ }
355
+
356
+ const sel = facts.selectedIndex as number | null;
357
+ if (sel === null) {
358
+ return;
359
+ }
360
+
361
+ const givens = facts.givens as Set<number>;
362
+ if (givens.has(sel)) {
363
+ return;
364
+ }
365
+
366
+ // Only allow notes on empty cells
367
+ if ((facts.grid as Grid)[sel] !== 0) {
368
+ return;
369
+ }
370
+
371
+ const notes = [...(facts.notes as Set<number>[])];
372
+ notes[sel] = new Set(notes[sel]);
373
+ if (notes[sel].has(value)) {
374
+ notes[sel].delete(value);
375
+ } else {
376
+ notes[sel].add(value);
377
+ }
378
+ facts.notes = notes;
379
+ },
380
+
381
+ toggleNotesMode: (facts) => {
382
+ facts.notesMode = !(facts.notesMode as boolean);
383
+ },
384
+
385
+ requestHint: (facts) => {
386
+ if (facts.gameOver) {
387
+ return;
388
+ }
389
+ if (facts.hintsUsed >= MAX_HINTS) {
390
+ facts.message = "No hints remaining.";
391
+
392
+ return;
393
+ }
394
+
395
+ const sel = facts.selectedIndex as number | null;
396
+ if (sel === null) {
397
+ facts.message = "Select a cell first.";
398
+
399
+ return;
400
+ }
401
+
402
+ const givens = facts.givens as Set<number>;
403
+ if (givens.has(sel)) {
404
+ facts.message = "That cell is already filled.";
405
+
406
+ return;
407
+ }
408
+
409
+ if ((facts.grid as Grid)[sel] !== 0) {
410
+ facts.message = "Clear the cell first, or select an empty cell.";
411
+
412
+ return;
413
+ }
414
+
415
+ // Signal the hintAvailable constraint to fire
416
+ facts.hintRequested = true;
417
+ },
418
+
419
+ tick: (facts) => {
420
+ if (!facts.timerRunning || facts.gameOver) {
421
+ return;
422
+ }
423
+ facts.timerRemaining = Math.max(0, (facts.timerRemaining as number) - 1);
424
+ },
425
+ },
426
+
427
+ // ============================================================================
428
+ // Constraints – The Showcase
429
+ // ============================================================================
430
+
431
+ constraints: {
432
+ // Highest priority: timer expiry ends the game immediately
433
+ timerExpired: {
434
+ priority: 200,
435
+ when: (facts) => {
436
+ if (facts.gameOver) {
437
+ return false;
438
+ }
439
+
440
+ return (facts.timerRemaining as number) <= 0;
441
+ },
442
+ require: () => ({
443
+ type: "GAME_OVER",
444
+ reason: "Time's up!",
445
+ }),
446
+ },
447
+
448
+ // Detect conflicts on player-placed cells
449
+ detectConflict: {
450
+ priority: 100,
451
+ when: (facts) => {
452
+ if (facts.gameOver) {
453
+ return false;
454
+ }
455
+ const conflicts = findConflicts(facts.grid as Grid);
456
+ const givens = facts.givens as Set<number>;
457
+
458
+ return conflicts.some((c) => !givens.has(c.index));
459
+ },
460
+ require: (facts) => {
461
+ const conflicts = findConflicts(facts.grid as Grid);
462
+ const givens = facts.givens as Set<number>;
463
+ const playerConflict = conflicts.find((c) => !givens.has(c.index));
464
+ const idx = playerConflict?.index ?? 0;
465
+ const { row, col } = toRowCol(idx);
466
+
467
+ return {
468
+ type: "SHOW_CONFLICT",
469
+ index: idx,
470
+ value: playerConflict?.value ?? 0,
471
+ row: row + 1,
472
+ col: col + 1,
473
+ };
474
+ },
475
+ },
476
+
477
+ // Puzzle solved: all cells filled with no conflicts
478
+ puzzleSolved: {
479
+ priority: 90,
480
+ when: (facts) => {
481
+ if (facts.gameOver) {
482
+ return false;
483
+ }
484
+
485
+ return (
486
+ isBoardComplete(facts.grid as Grid) &&
487
+ findConflicts(facts.grid as Grid).length === 0
488
+ );
489
+ },
490
+ require: (facts) => ({
491
+ type: "GAME_WON",
492
+ timeLeft: facts.timerRemaining as number,
493
+ hintsUsed: facts.hintsUsed as number,
494
+ errors: facts.errorsCount as number,
495
+ }),
496
+ },
497
+
498
+ // Hint available: player requested a hint on an empty cell
499
+ hintAvailable: {
500
+ priority: 70,
501
+ when: (facts) => {
502
+ if (facts.gameOver) {
503
+ return false;
504
+ }
505
+ if (!facts.hintRequested) {
506
+ return false;
507
+ }
508
+
509
+ const sel = facts.selectedIndex as number | null;
510
+ if (sel === null) {
511
+ return false;
512
+ }
513
+
514
+ return (facts.grid as Grid)[sel] === 0;
515
+ },
516
+ require: (facts) => {
517
+ const sel = facts.selectedIndex as number;
518
+ const solution = facts.solution as Grid;
519
+
520
+ return {
521
+ type: "REVEAL_HINT",
522
+ index: sel,
523
+ value: solution[sel],
524
+ };
525
+ },
526
+ },
527
+ },
528
+
529
+ // ============================================================================
530
+ // Resolvers
531
+ // ============================================================================
532
+
533
+ resolvers: {
534
+ showConflict: {
535
+ requirement: "SHOW_CONFLICT",
536
+ resolve: async (req, context) => {
537
+ context.facts.errorsCount = (context.facts.errorsCount as number) + 1;
538
+ context.facts.message = `Conflict at row ${req.row}, column ${req.col} – duplicate ${req.value}.`;
539
+ },
540
+ },
541
+
542
+ gameWon: {
543
+ requirement: "GAME_WON",
544
+ resolve: async (req, context) => {
545
+ context.facts.timerRunning = false;
546
+ context.facts.gameOver = true;
547
+ context.facts.won = true;
548
+
549
+ const mins = Math.floor(
550
+ (TIMER_DURATIONS[context.facts.difficulty as Difficulty] -
551
+ req.timeLeft) /
552
+ 60,
553
+ );
554
+ const secs =
555
+ (TIMER_DURATIONS[context.facts.difficulty as Difficulty] -
556
+ req.timeLeft) %
557
+ 60;
558
+ context.facts.message = `Solved in ${mins}m ${secs}s! Hints: ${req.hintsUsed}, Errors: ${req.errors}`;
559
+ },
560
+ },
561
+
562
+ gameOver: {
563
+ requirement: "GAME_OVER",
564
+ resolve: async (req, context) => {
565
+ context.facts.timerRunning = false;
566
+ context.facts.gameOver = true;
567
+ context.facts.won = false;
568
+ context.facts.message = req.reason;
569
+ },
570
+ },
571
+
572
+ revealHint: {
573
+ requirement: "REVEAL_HINT",
574
+ resolve: async (req, context) => {
575
+ const grid = [...(context.facts.grid as Grid)];
576
+ grid[req.index] = req.value;
577
+ context.facts.grid = grid;
578
+
579
+ // Clear notes for the hinted cell and remove value from peer notes
580
+ const notes = [...(context.facts.notes as Set<number>[])];
581
+ notes[req.index] = new Set();
582
+ for (const peer of getPeers(req.index)) {
583
+ if (notes[peer].has(req.value)) {
584
+ notes[peer] = new Set(notes[peer]);
585
+ notes[peer].delete(req.value);
586
+ }
587
+ }
588
+ context.facts.notes = notes;
589
+
590
+ context.facts.hintRequested = false;
591
+ context.facts.hintsUsed = (context.facts.hintsUsed as number) + 1;
592
+ context.facts.message = `Hint revealed! ${MAX_HINTS - (context.facts.hintsUsed as number)} remaining.`;
593
+ },
594
+ },
595
+ },
596
+
597
+ // ============================================================================
598
+ // Effects
599
+ // ============================================================================
600
+
601
+ effects: {
602
+ timerWarning: {
603
+ deps: ["timerRemaining"],
604
+ run: (facts) => {
605
+ const remaining = facts.timerRemaining as number;
606
+ if (remaining === TIMER_EFFECT_WARNING) {
607
+ console.log("[Sudoku] 1 minute remaining!");
608
+ }
609
+ if (remaining === TIMER_EFFECT_CRITICAL) {
610
+ console.log("[Sudoku] 30 seconds remaining!");
611
+ }
612
+ },
613
+ },
614
+
615
+ gameResult: {
616
+ deps: ["gameOver"],
617
+ run: (facts) => {
618
+ if (facts.gameOver) {
619
+ if (facts.won) {
620
+ console.log(
621
+ `[Sudoku] Puzzle solved! Difficulty: ${facts.difficulty}, Hints: ${facts.hintsUsed}, Errors: ${facts.errorsCount}`,
622
+ );
623
+ } else {
624
+ console.log(`[Sudoku] Game over: ${facts.message}`);
625
+ }
626
+ }
627
+ },
628
+ },
629
+ },
630
+ });