@directive-run/knowledge 0.2.0 → 0.5.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 (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -3
  3. package/ai/ai-adapters.md +7 -7
  4. package/ai/ai-agents-streaming.md +8 -8
  5. package/ai/ai-budget-resilience.md +5 -5
  6. package/ai/ai-communication.md +1 -1
  7. package/ai/ai-guardrails-memory.md +7 -7
  8. package/ai/ai-mcp-rag.md +5 -5
  9. package/ai/ai-multi-agent.md +14 -14
  10. package/ai/ai-orchestrator.md +8 -8
  11. package/ai/ai-security.md +2 -2
  12. package/ai/ai-tasks.md +9 -9
  13. package/ai/ai-testing-evals.md +2 -2
  14. package/core/anti-patterns.md +39 -39
  15. package/core/constraints.md +15 -15
  16. package/core/core-patterns.md +9 -9
  17. package/core/error-boundaries.md +7 -7
  18. package/core/multi-module.md +16 -16
  19. package/core/naming.md +21 -21
  20. package/core/plugins.md +14 -14
  21. package/core/react-adapter.md +13 -13
  22. package/core/resolvers.md +14 -14
  23. package/core/schema-types.md +22 -22
  24. package/core/system-api.md +16 -16
  25. package/core/testing.md +5 -5
  26. package/core/time-travel.md +20 -20
  27. package/dist/index.cjs +6 -105
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +7 -97
  30. package/dist/index.js.map +1 -1
  31. package/examples/ab-testing.ts +18 -90
  32. package/examples/ai-checkpoint.ts +68 -87
  33. package/examples/ai-guardrails.ts +20 -70
  34. package/examples/auth-flow.ts +2 -2
  35. package/examples/batch-resolver.ts +19 -59
  36. package/examples/contact-form.ts +220 -69
  37. package/examples/counter.ts +77 -95
  38. package/examples/dashboard-loader.ts +38 -56
  39. package/examples/debounce-constraints.ts +0 -2
  40. package/examples/dynamic-modules.ts +17 -20
  41. package/examples/error-boundaries.ts +30 -81
  42. package/examples/form-wizard.ts +6 -6
  43. package/examples/newsletter.ts +24 -51
  44. package/examples/notifications.ts +24 -23
  45. package/examples/optimistic-updates.ts +36 -41
  46. package/examples/pagination.ts +2 -2
  47. package/examples/permissions.ts +22 -32
  48. package/examples/provider-routing.ts +26 -83
  49. package/examples/shopping-cart.ts +12 -12
  50. package/examples/sudoku.ts +60 -67
  51. package/examples/theme-locale.ts +4 -7
  52. package/examples/time-machine.ts +12 -90
  53. package/examples/topic-guard.ts +31 -39
  54. package/examples/url-sync.ts +8 -8
  55. package/examples/websocket.ts +5 -5
  56. package/package.json +3 -3
@@ -1,9 +1,9 @@
1
1
  // Example: provider-routing
2
- // Source: examples/provider-routing/src/main.ts
3
- // Extracted for AI rules — DOM wiring stripped
2
+ // Source: examples/provider-routing/src/module.ts
3
+ // Pure module fileno DOM wiring
4
4
 
5
5
  /**
6
- * Smart Provider Router — Constraint-Based Provider Routing & Fallback
6
+ * Smart Provider Router — Module Definition
7
7
  *
8
8
  * 3 mock providers (OpenAI, Anthropic, Ollama). Constraint router selects based
9
9
  * on cost, error rates, circuit state. Provider fallback chain.
@@ -25,7 +25,7 @@ import {
25
25
  // Types
26
26
  // ============================================================================
27
27
 
28
- interface ProviderStats {
28
+ export interface ProviderStats {
29
29
  name: string;
30
30
  callCount: number;
31
31
  errorCount: number;
@@ -34,7 +34,7 @@ interface ProviderStats {
34
34
  circuitState: CircuitState;
35
35
  }
36
36
 
37
- interface TimelineEntry {
37
+ export interface TimelineEntry {
38
38
  time: number;
39
39
  event: string;
40
40
  detail: string;
@@ -57,13 +57,14 @@ const providerErrors: Record<string, boolean> = {
57
57
  ollama: false,
58
58
  };
59
59
 
60
- const circuitBreakers = {
60
+ export const circuitBreakers = {
61
61
  openai: createCircuitBreaker({
62
62
  name: "openai",
63
63
  failureThreshold: 3,
64
64
  recoveryTimeMs: 5000,
65
65
  halfOpenMaxRequests: 2,
66
66
  onStateChange: (from, to) =>
67
+ addTimeline("circuit", `openai: ${from} → ${to}`, "circuit"),
67
68
  }),
68
69
  anthropic: createCircuitBreaker({
69
70
  name: "anthropic",
@@ -71,6 +72,7 @@ const circuitBreakers = {
71
72
  recoveryTimeMs: 5000,
72
73
  halfOpenMaxRequests: 2,
73
74
  onStateChange: (from, to) =>
75
+ addTimeline("circuit", `anthropic: ${from} → ${to}`, "circuit"),
74
76
  }),
75
77
  ollama: createCircuitBreaker({
76
78
  name: "ollama",
@@ -78,6 +80,7 @@ const circuitBreakers = {
78
80
  recoveryTimeMs: 5000,
79
81
  halfOpenMaxRequests: 2,
80
82
  onStateChange: (from, to) =>
83
+ addTimeline("circuit", `ollama: ${from} → ${to}`, "circuit"),
81
84
  }),
82
85
  };
83
86
 
@@ -85,9 +88,9 @@ const circuitBreakers = {
85
88
  // Timeline
86
89
  // ============================================================================
87
90
 
88
- const timeline: TimelineEntry[] = [];
91
+ export const timeline: TimelineEntry[] = [];
89
92
 
90
- function addTimeline(
93
+ export function addTimeline(
91
94
  event: string,
92
95
  detail: string,
93
96
  type: TimelineEntry["type"],
@@ -102,7 +105,7 @@ function addTimeline(
102
105
  // Schema
103
106
  // ============================================================================
104
107
 
105
- const schema = {
108
+ export const schema = {
106
109
  facts: {
107
110
  openaiStats: t.object<ProviderStats>(),
108
111
  anthropicStats: t.object<ProviderStats>(),
@@ -216,20 +219,19 @@ const routerModule = createModule("router", {
216
219
  // System
217
220
  // ============================================================================
218
221
 
219
- const system = createSystem({
222
+ export const system = createSystem({
220
223
  module: routerModule,
221
224
  debug: { runHistory: true },
222
225
  plugins: [devtoolsPlugin({ name: "provider-routing" })],
223
226
  });
224
- system.start();
225
227
 
226
228
  // ============================================================================
227
229
  // Routing Logic
228
230
  // ============================================================================
229
231
 
230
232
  function selectProvider(): string | null {
231
- const budget = system.facts.budgetRemaining as number;
232
- const preferCheapest = system.facts.preferCheapest as boolean;
233
+ const budget = system.facts.budgetRemaining;
234
+ const preferCheapest = system.facts.preferCheapest;
233
235
 
234
236
  // Collect available providers (circuit breaker allows + within budget)
235
237
  const available: { id: string; cost: number }[] = [];
@@ -280,7 +282,7 @@ async function executeProvider(providerId: string): Promise<boolean> {
280
282
  }
281
283
  });
282
284
 
283
- const stats = system.facts[statsKey] as ProviderStats;
285
+ const stats = system.facts[statsKey];
284
286
  const cost = config.costPer1k;
285
287
  const latency = config.baseLatency + Math.random() * 100;
286
288
  system.facts[statsKey] = {
@@ -294,35 +296,38 @@ async function executeProvider(providerId: string): Promise<boolean> {
294
296
  circuitState: breaker.getState(),
295
297
  };
296
298
  system.facts.budgetRemaining =
297
- Math.round(((system.facts.budgetRemaining as number) - cost) * 1000) /
298
- 1000;
299
+ Math.round((system.facts.budgetRemaining - cost) * 1000) / 1000;
299
300
  system.facts.lastError = "";
301
+ addTimeline("success", `${config.name}: ok ($${cost})`, "success");
300
302
 
301
303
  return true;
302
304
  } catch (err) {
303
- const stats = system.facts[statsKey] as ProviderStats;
305
+ const stats = system.facts[statsKey];
304
306
  system.facts[statsKey] = {
305
307
  ...stats,
306
308
  errorCount: stats.errorCount + 1,
307
309
  circuitState: breaker.getState(),
308
310
  };
309
311
  system.facts.lastError = err instanceof Error ? err.message : String(err);
312
+ addTimeline("error", `${config.name}: ${system.facts.lastError}`, "error");
310
313
 
311
314
  return false;
312
315
  }
313
316
  }
314
317
 
315
- async function sendRequest() {
316
- system.facts.totalRequests = (system.facts.totalRequests as number) + 1;
318
+ export async function sendRequest(): Promise<void> {
319
+ system.facts.totalRequests = system.facts.totalRequests + 1;
317
320
 
318
321
  const providerId = selectProvider();
319
322
  if (!providerId) {
320
323
  system.facts.lastError = "All providers unavailable or over budget";
324
+ addTimeline("error", "no available providers", "error");
321
325
 
322
326
  return;
323
327
  }
324
328
 
325
329
  system.facts.lastProvider = providerId;
330
+ addTimeline("route", `→ ${providerId}`, "route");
326
331
 
327
332
  const success = await executeProvider(providerId);
328
333
  if (success) {
@@ -332,72 +337,10 @@ async function sendRequest() {
332
337
  // Primary failed — try fallback
333
338
  const fallbackId = selectProvider();
334
339
  if (fallbackId && fallbackId !== providerId) {
340
+ addTimeline("fallback", `falling back to ${fallbackId}`, "fallback");
335
341
  system.facts.lastProvider = fallbackId;
336
342
  await executeProvider(fallbackId);
337
343
  }
338
344
  }
339
345
 
340
- // ============================================================================
341
- // DOM References
342
- // ============================================================================
343
-
344
-
345
- // ============================================================================
346
- // Render
347
- // ============================================================================
348
-
349
- function escapeHtml(text: string): string {
350
-
351
- return div.innerHTML;
352
- }
353
-
354
- function circuitBadge(state: CircuitState): string {
355
- const cls =
356
- state === "CLOSED" ? "closed" : state === "OPEN" ? "open" : "half-open";
357
-
358
- return `<span class="pr-circuit-badge ${cls}">${state}</span>`;
359
- }
360
-
361
- stats: ProviderStats,
362
- state: CircuitState,
363
- ): void {
364
- ${circuitBadge(state)}
365
- <span style="font-size:0.55rem;color:var(--brand-text-dim)">${stats.callCount} calls, ${stats.errorCount} err, $${stats.totalCost}</span>
366
- `;
367
- }
368
-
369
-
370
- // ============================================================================
371
- // Subscribe
372
- // ============================================================================
373
-
374
- const allKeys = [
375
- ...Object.keys(schema.facts),
376
- ...Object.keys(schema.derivations),
377
- ];
378
- system.subscribe(allKeys, render);
379
-
380
- setInterval(render, 1000);
381
-
382
- // ============================================================================
383
- // Controls
384
- // ============================================================================
385
-
386
-
387
- for (const id of ["openai", "anthropic", "ollama"]) {
388
- system.events.toggleProviderError({ provider: id });
389
- });
390
- }
391
-
392
- "input",
393
- (e) => {
394
- system.events.setBudget({ value });
395
- },
396
- );
397
-
398
-
399
- // ============================================================================
400
- // Initial Render
401
- // ============================================================================
402
-
403
- render();
346
+ export { providerErrors };
@@ -187,23 +187,23 @@ export const cartModule = createModule("cart", {
187
187
  return facts.self.items.length === 0;
188
188
  },
189
189
 
190
- discount: (facts, derive) => {
191
- const sub = derive.subtotal as number;
190
+ discount: (facts, derived) => {
191
+ const sub = derived.subtotal;
192
192
 
193
193
  return sub * (facts.self.couponDiscount / 100);
194
194
  },
195
195
 
196
- tax: (facts, derive) => {
197
- const sub = derive.subtotal as number;
198
- const disc = derive.discount as number;
196
+ tax: (facts, derived) => {
197
+ const sub = derived.subtotal;
198
+ const disc = derived.discount;
199
199
 
200
200
  return (sub - disc) * 0.08;
201
201
  },
202
202
 
203
- total: (_facts, derive) => {
204
- const sub = derive.subtotal as number;
205
- const disc = derive.discount as number;
206
- const tx = derive.tax as number;
203
+ total: (_facts, derived) => {
204
+ const sub = derived.subtotal;
205
+ const disc = derived.discount;
206
+ const tx = derived.tax;
207
207
 
208
208
  return sub - disc + tx;
209
209
  },
@@ -214,8 +214,8 @@ export const cartModule = createModule("cart", {
214
214
  );
215
215
  },
216
216
 
217
- freeShipping: (_facts, derive) => {
218
- const sub = derive.subtotal as number;
217
+ freeShipping: (_facts, derived) => {
218
+ const sub = derived.subtotal;
219
219
 
220
220
  return sub >= 75;
221
221
  },
@@ -317,7 +317,7 @@ export const cartModule = createModule("cart", {
317
317
  priority: 60,
318
318
  after: ["quantityLimit", "couponValidation"],
319
319
  when: (facts) => {
320
- const items = facts.self.items as CartItem[];
320
+ const items = facts.self.items;
321
321
  const notEmpty = items.length > 0;
322
322
  const noOverstock = !items.some(
323
323
  (item: CartItem) => item.quantity > item.maxStock,
@@ -52,23 +52,23 @@ export const sudokuSchema = {
52
52
  won: t.boolean(),
53
53
  message: t.string(),
54
54
  notesMode: t.boolean(),
55
- notes: t.object<Set<number>[]>(),
55
+ notes: t.array<Set<number>>(),
56
56
  hintsUsed: t.number(),
57
57
  errorsCount: t.number(),
58
58
  hintRequested: t.boolean(),
59
59
  },
60
60
  derivations: {
61
- conflicts: t.object<Conflict[]>(),
61
+ conflicts: t.array<Conflict>(),
62
62
  conflictIndices: t.object<Set<number>>(),
63
63
  hasConflicts: t.boolean(),
64
64
  filledCount: t.number(),
65
65
  progress: t.number(),
66
66
  isComplete: t.boolean(),
67
67
  isSolved: t.boolean(),
68
- selectedPeers: t.object<number[]>(),
68
+ selectedPeers: t.array<number>(),
69
69
  highlightValue: t.number(),
70
70
  sameValueIndices: t.object<Set<number>>(),
71
- candidates: t.object<number[]>(),
71
+ candidates: t.array<number>(),
72
72
  timerDisplay: t.string(),
73
73
  timerUrgency: t.object<"normal" | "warning" | "critical">(),
74
74
  },
@@ -144,13 +144,13 @@ export const sudokuGame = createModule("sudoku", {
144
144
 
145
145
  derive: {
146
146
  conflicts: (facts) => {
147
- return findConflicts(facts.grid as Grid);
147
+ return findConflicts(facts.grid);
148
148
  },
149
149
 
150
- conflictIndices: (facts, derive) => {
150
+ conflictIndices: (facts, derived) => {
151
151
  const indices = new Set<number>();
152
- const givens = facts.givens as Set<number>;
153
- for (const c of derive.conflicts as Conflict[]) {
152
+ const givens = facts.givens;
153
+ for (const c of derived.conflicts) {
154
154
  // Only highlight player-placed cells, not givens
155
155
  if (!givens.has(c.index)) {
156
156
  indices.add(c.index);
@@ -160,13 +160,13 @@ export const sudokuGame = createModule("sudoku", {
160
160
  return indices;
161
161
  },
162
162
 
163
- hasConflicts: (_facts, derive) => {
164
- return (derive.conflicts as Conflict[]).length > 0;
163
+ hasConflicts: (_facts, derived) => {
164
+ return derived.conflicts.length > 0;
165
165
  },
166
166
 
167
167
  filledCount: (facts) => {
168
168
  let count = 0;
169
- const grid = facts.grid as Grid;
169
+ const grid = facts.grid;
170
170
  for (let i = 0; i < 81; i++) {
171
171
  if (grid[i] !== 0) {
172
172
  count++;
@@ -176,22 +176,20 @@ export const sudokuGame = createModule("sudoku", {
176
176
  return count;
177
177
  },
178
178
 
179
- progress: (_facts, derive) => {
180
- return Math.round(((derive.filledCount as number) / 81) * 100);
179
+ progress: (_facts, derived) => {
180
+ return Math.round((derived.filledCount / 81) * 100);
181
181
  },
182
182
 
183
183
  isComplete: (facts) => {
184
- return isBoardComplete(facts.grid as Grid);
184
+ return isBoardComplete(facts.grid);
185
185
  },
186
186
 
187
- isSolved: (_facts, derive) => {
188
- return (
189
- (derive.isComplete as boolean) && !(derive.hasConflicts as boolean)
190
- );
187
+ isSolved: (_facts, derived) => {
188
+ return derived.isComplete && !derived.hasConflicts;
191
189
  },
192
190
 
193
191
  selectedPeers: (facts) => {
194
- const sel = facts.selectedIndex as number | null;
192
+ const sel = facts.selectedIndex;
195
193
  if (sel === null) {
196
194
  return [];
197
195
  }
@@ -200,22 +198,22 @@ export const sudokuGame = createModule("sudoku", {
200
198
  },
201
199
 
202
200
  highlightValue: (facts) => {
203
- const sel = facts.selectedIndex as number | null;
201
+ const sel = facts.selectedIndex;
204
202
  if (sel === null) {
205
203
  return 0;
206
204
  }
207
205
 
208
- return (facts.grid as Grid)[sel];
206
+ return facts.grid[sel];
209
207
  },
210
208
 
211
- sameValueIndices: (facts, derive) => {
212
- const val = derive.highlightValue as number;
209
+ sameValueIndices: (facts, derived) => {
210
+ const val = derived.highlightValue;
213
211
  if (val === 0) {
214
212
  return new Set<number>();
215
213
  }
216
214
 
217
215
  const indices = new Set<number>();
218
- const grid = facts.grid as Grid;
216
+ const grid = facts.grid;
219
217
  for (let i = 0; i < 81; i++) {
220
218
  if (grid[i] === val) {
221
219
  indices.add(i);
@@ -226,16 +224,16 @@ export const sudokuGame = createModule("sudoku", {
226
224
  },
227
225
 
228
226
  candidates: (facts) => {
229
- const sel = facts.selectedIndex as number | null;
227
+ const sel = facts.selectedIndex;
230
228
  if (sel === null) {
231
229
  return [];
232
230
  }
233
231
 
234
- return getCandidates(facts.grid as Grid, sel);
232
+ return getCandidates(facts.grid, sel);
235
233
  },
236
234
 
237
235
  timerDisplay: (facts) => {
238
- const remaining = facts.timerRemaining as number;
236
+ const remaining = facts.timerRemaining;
239
237
  const mins = Math.max(0, Math.floor(remaining / 60));
240
238
  const secs = Math.max(0, remaining % 60);
241
239
 
@@ -243,7 +241,7 @@ export const sudokuGame = createModule("sudoku", {
243
241
  },
244
242
 
245
243
  timerUrgency: (facts) => {
246
- const remaining = facts.timerRemaining as number;
244
+ const remaining = facts.timerRemaining;
247
245
  if (remaining <= TIMER_CRITICAL_THRESHOLD) {
248
246
  return "critical";
249
247
  }
@@ -299,12 +297,12 @@ export const sudokuGame = createModule("sudoku", {
299
297
  return;
300
298
  }
301
299
 
302
- const sel = facts.selectedIndex as number | null;
300
+ const sel = facts.selectedIndex;
303
301
  if (sel === null) {
304
302
  return;
305
303
  }
306
304
 
307
- const givens = facts.givens as Set<number>;
305
+ const givens = facts.givens;
308
306
  if (givens.has(sel)) {
309
307
  facts.message = "That cell is locked.";
310
308
 
@@ -313,7 +311,7 @@ export const sudokuGame = createModule("sudoku", {
313
311
 
314
312
  if (facts.notesMode && value !== 0) {
315
313
  // In notes mode, toggle the pencil mark instead
316
- const notes = [...(facts.notes as Set<number>[])];
314
+ const notes = [...facts.notes];
317
315
  notes[sel] = new Set(notes[sel]);
318
316
  if (notes[sel].has(value)) {
319
317
  notes[sel].delete(value);
@@ -327,13 +325,13 @@ export const sudokuGame = createModule("sudoku", {
327
325
  }
328
326
 
329
327
  // Place or clear a number
330
- const grid = [...(facts.grid as Grid)];
328
+ const grid = [...facts.grid];
331
329
  grid[sel] = value;
332
330
  facts.grid = grid;
333
331
 
334
332
  // Clear notes for this cell when placing a number
335
333
  if (value !== 0) {
336
- const notes = [...(facts.notes as Set<number>[])];
334
+ const notes = [...facts.notes];
337
335
  notes[sel] = new Set();
338
336
  // Also clear this value from peer notes
339
337
  for (const peer of getPeers(sel)) {
@@ -353,22 +351,22 @@ export const sudokuGame = createModule("sudoku", {
353
351
  return;
354
352
  }
355
353
 
356
- const sel = facts.selectedIndex as number | null;
354
+ const sel = facts.selectedIndex;
357
355
  if (sel === null) {
358
356
  return;
359
357
  }
360
358
 
361
- const givens = facts.givens as Set<number>;
359
+ const givens = facts.givens;
362
360
  if (givens.has(sel)) {
363
361
  return;
364
362
  }
365
363
 
366
364
  // Only allow notes on empty cells
367
- if ((facts.grid as Grid)[sel] !== 0) {
365
+ if (facts.grid[sel] !== 0) {
368
366
  return;
369
367
  }
370
368
 
371
- const notes = [...(facts.notes as Set<number>[])];
369
+ const notes = [...facts.notes];
372
370
  notes[sel] = new Set(notes[sel]);
373
371
  if (notes[sel].has(value)) {
374
372
  notes[sel].delete(value);
@@ -379,7 +377,7 @@ export const sudokuGame = createModule("sudoku", {
379
377
  },
380
378
 
381
379
  toggleNotesMode: (facts) => {
382
- facts.notesMode = !(facts.notesMode as boolean);
380
+ facts.notesMode = !facts.notesMode;
383
381
  },
384
382
 
385
383
  requestHint: (facts) => {
@@ -392,21 +390,21 @@ export const sudokuGame = createModule("sudoku", {
392
390
  return;
393
391
  }
394
392
 
395
- const sel = facts.selectedIndex as number | null;
393
+ const sel = facts.selectedIndex;
396
394
  if (sel === null) {
397
395
  facts.message = "Select a cell first.";
398
396
 
399
397
  return;
400
398
  }
401
399
 
402
- const givens = facts.givens as Set<number>;
400
+ const givens = facts.givens;
403
401
  if (givens.has(sel)) {
404
402
  facts.message = "That cell is already filled.";
405
403
 
406
404
  return;
407
405
  }
408
406
 
409
- if ((facts.grid as Grid)[sel] !== 0) {
407
+ if (facts.grid[sel] !== 0) {
410
408
  facts.message = "Clear the cell first, or select an empty cell.";
411
409
 
412
410
  return;
@@ -420,7 +418,7 @@ export const sudokuGame = createModule("sudoku", {
420
418
  if (!facts.timerRunning || facts.gameOver) {
421
419
  return;
422
420
  }
423
- facts.timerRemaining = Math.max(0, (facts.timerRemaining as number) - 1);
421
+ facts.timerRemaining = Math.max(0, facts.timerRemaining - 1);
424
422
  },
425
423
  },
426
424
 
@@ -437,7 +435,7 @@ export const sudokuGame = createModule("sudoku", {
437
435
  return false;
438
436
  }
439
437
 
440
- return (facts.timerRemaining as number) <= 0;
438
+ return facts.timerRemaining <= 0;
441
439
  },
442
440
  require: () => ({
443
441
  type: "GAME_OVER",
@@ -452,14 +450,14 @@ export const sudokuGame = createModule("sudoku", {
452
450
  if (facts.gameOver) {
453
451
  return false;
454
452
  }
455
- const conflicts = findConflicts(facts.grid as Grid);
456
- const givens = facts.givens as Set<number>;
453
+ const conflicts = findConflicts(facts.grid);
454
+ const givens = facts.givens;
457
455
 
458
456
  return conflicts.some((c) => !givens.has(c.index));
459
457
  },
460
458
  require: (facts) => {
461
- const conflicts = findConflicts(facts.grid as Grid);
462
- const givens = facts.givens as Set<number>;
459
+ const conflicts = findConflicts(facts.grid);
460
+ const givens = facts.givens;
463
461
  const playerConflict = conflicts.find((c) => !givens.has(c.index));
464
462
  const idx = playerConflict?.index ?? 0;
465
463
  const { row, col } = toRowCol(idx);
@@ -483,15 +481,14 @@ export const sudokuGame = createModule("sudoku", {
483
481
  }
484
482
 
485
483
  return (
486
- isBoardComplete(facts.grid as Grid) &&
487
- findConflicts(facts.grid as Grid).length === 0
484
+ isBoardComplete(facts.grid) && findConflicts(facts.grid).length === 0
488
485
  );
489
486
  },
490
487
  require: (facts) => ({
491
488
  type: "GAME_WON",
492
- timeLeft: facts.timerRemaining as number,
493
- hintsUsed: facts.hintsUsed as number,
494
- errors: facts.errorsCount as number,
489
+ timeLeft: facts.timerRemaining,
490
+ hintsUsed: facts.hintsUsed,
491
+ errors: facts.errorsCount,
495
492
  }),
496
493
  },
497
494
 
@@ -506,16 +503,16 @@ export const sudokuGame = createModule("sudoku", {
506
503
  return false;
507
504
  }
508
505
 
509
- const sel = facts.selectedIndex as number | null;
506
+ const sel = facts.selectedIndex;
510
507
  if (sel === null) {
511
508
  return false;
512
509
  }
513
510
 
514
- return (facts.grid as Grid)[sel] === 0;
511
+ return facts.grid[sel] === 0;
515
512
  },
516
513
  require: (facts) => {
517
514
  const sel = facts.selectedIndex as number;
518
- const solution = facts.solution as Grid;
515
+ const solution = facts.solution;
519
516
 
520
517
  return {
521
518
  type: "REVEAL_HINT",
@@ -534,7 +531,7 @@ export const sudokuGame = createModule("sudoku", {
534
531
  showConflict: {
535
532
  requirement: "SHOW_CONFLICT",
536
533
  resolve: async (req, context) => {
537
- context.facts.errorsCount = (context.facts.errorsCount as number) + 1;
534
+ context.facts.errorsCount = context.facts.errorsCount + 1;
538
535
  context.facts.message = `Conflict at row ${req.row}, column ${req.col} – duplicate ${req.value}.`;
539
536
  },
540
537
  },
@@ -547,14 +544,10 @@ export const sudokuGame = createModule("sudoku", {
547
544
  context.facts.won = true;
548
545
 
549
546
  const mins = Math.floor(
550
- (TIMER_DURATIONS[context.facts.difficulty as Difficulty] -
551
- req.timeLeft) /
552
- 60,
547
+ (TIMER_DURATIONS[context.facts.difficulty] - req.timeLeft) / 60,
553
548
  );
554
549
  const secs =
555
- (TIMER_DURATIONS[context.facts.difficulty as Difficulty] -
556
- req.timeLeft) %
557
- 60;
550
+ (TIMER_DURATIONS[context.facts.difficulty] - req.timeLeft) % 60;
558
551
  context.facts.message = `Solved in ${mins}m ${secs}s! Hints: ${req.hintsUsed}, Errors: ${req.errors}`;
559
552
  },
560
553
  },
@@ -572,12 +565,12 @@ export const sudokuGame = createModule("sudoku", {
572
565
  revealHint: {
573
566
  requirement: "REVEAL_HINT",
574
567
  resolve: async (req, context) => {
575
- const grid = [...(context.facts.grid as Grid)];
568
+ const grid = [...context.facts.grid];
576
569
  grid[req.index] = req.value;
577
570
  context.facts.grid = grid;
578
571
 
579
572
  // Clear notes for the hinted cell and remove value from peer notes
580
- const notes = [...(context.facts.notes as Set<number>[])];
573
+ const notes = [...context.facts.notes];
581
574
  notes[req.index] = new Set();
582
575
  for (const peer of getPeers(req.index)) {
583
576
  if (notes[peer].has(req.value)) {
@@ -588,8 +581,8 @@ export const sudokuGame = createModule("sudoku", {
588
581
  context.facts.notes = notes;
589
582
 
590
583
  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.`;
584
+ context.facts.hintsUsed = context.facts.hintsUsed + 1;
585
+ context.facts.message = `Hint revealed! ${MAX_HINTS - context.facts.hintsUsed} remaining.`;
593
586
  },
594
587
  },
595
588
  },
@@ -602,7 +595,7 @@ export const sudokuGame = createModule("sudoku", {
602
595
  timerWarning: {
603
596
  deps: ["timerRemaining"],
604
597
  run: (facts) => {
605
- const remaining = facts.timerRemaining as number;
598
+ const remaining = facts.timerRemaining;
606
599
  if (remaining === TIMER_EFFECT_WARNING) {
607
600
  console.log("[Sudoku] 1 minute remaining!");
608
601
  }