@directive-run/knowledge 0.2.0 → 0.4.2

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 (54) hide show
  1. package/README.md +3 -3
  2. package/ai/ai-adapters.md +7 -7
  3. package/ai/ai-agents-streaming.md +8 -8
  4. package/ai/ai-budget-resilience.md +5 -5
  5. package/ai/ai-communication.md +1 -1
  6. package/ai/ai-guardrails-memory.md +7 -7
  7. package/ai/ai-mcp-rag.md +5 -5
  8. package/ai/ai-multi-agent.md +14 -14
  9. package/ai/ai-orchestrator.md +8 -8
  10. package/ai/ai-security.md +2 -2
  11. package/ai/ai-tasks.md +9 -9
  12. package/ai/ai-testing-evals.md +2 -2
  13. package/core/anti-patterns.md +39 -39
  14. package/core/constraints.md +15 -15
  15. package/core/core-patterns.md +9 -9
  16. package/core/error-boundaries.md +7 -7
  17. package/core/multi-module.md +16 -16
  18. package/core/naming.md +21 -21
  19. package/core/plugins.md +14 -14
  20. package/core/react-adapter.md +13 -13
  21. package/core/resolvers.md +14 -14
  22. package/core/schema-types.md +22 -22
  23. package/core/system-api.md +16 -16
  24. package/core/testing.md +5 -5
  25. package/core/time-travel.md +20 -20
  26. package/dist/index.cjs +6 -105
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.js +7 -97
  29. package/dist/index.js.map +1 -1
  30. package/examples/ab-testing.ts +18 -90
  31. package/examples/ai-checkpoint.ts +68 -87
  32. package/examples/ai-guardrails.ts +20 -70
  33. package/examples/auth-flow.ts +2 -2
  34. package/examples/batch-resolver.ts +19 -59
  35. package/examples/contact-form.ts +220 -69
  36. package/examples/counter.ts +77 -95
  37. package/examples/dashboard-loader.ts +37 -55
  38. package/examples/debounce-constraints.ts +0 -2
  39. package/examples/dynamic-modules.ts +17 -20
  40. package/examples/error-boundaries.ts +30 -81
  41. package/examples/newsletter.ts +22 -49
  42. package/examples/notifications.ts +24 -23
  43. package/examples/optimistic-updates.ts +36 -41
  44. package/examples/pagination.ts +2 -2
  45. package/examples/permissions.ts +22 -32
  46. package/examples/provider-routing.ts +26 -83
  47. package/examples/shopping-cart.ts +8 -8
  48. package/examples/sudoku.ts +55 -62
  49. package/examples/theme-locale.ts +4 -7
  50. package/examples/time-machine.ts +12 -90
  51. package/examples/topic-guard.ts +30 -38
  52. package/examples/url-sync.ts +8 -8
  53. package/examples/websocket.ts +5 -5
  54. package/package.json +3 -3
@@ -1,12 +1,19 @@
1
1
  // Example: contact-form
2
- // Source: examples/contact-form/src/main.ts
3
- // Extracted for AI rules — DOM wiring stripped
2
+ // Source: examples/contact-form/src/module.ts
3
+ // Pure module fileno DOM wiring
4
4
 
5
5
  /**
6
- * Contact Form — DOM Rendering & System Wiring
6
+ * Contact Form — Directive Module
7
7
  *
8
- * Creates the Directive system, subscribes to state changes,
9
- * renders the form and event timeline.
8
+ * Multi-field contact form showcasing validation, constraints, and resolvers:
9
+ * - Facts: name, email, subject, message, touched, status, errorMessage, etc.
10
+ * - Derivations: field errors, isValid, canSubmit, messageCharCount
11
+ * - Events: updateField, touchField, submit, reset
12
+ * - Constraints: submitForm, resetAfterSuccess
13
+ * - Resolvers: simulated async send, auto-reset after delay
14
+ * - Effects: logging status transitions
15
+ *
16
+ * Uses a simulated setTimeout instead of a real API so no account is needed.
10
17
  */
11
18
 
12
19
  import {
@@ -25,23 +32,35 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
25
32
  const RATE_LIMIT_MS = 10_000; // 10 seconds (shorter for demo)
26
33
 
27
34
  // ============================================================================
28
- // Timeline
35
+ // Types
29
36
  // ============================================================================
30
37
 
31
- interface TimelineEntry {
38
+ export interface TimelineEntry {
32
39
  time: number;
33
40
  event: string;
34
41
  detail: string;
35
42
  type: string;
36
43
  }
37
44
 
38
- const timeline: TimelineEntry[] = [];
45
+ // ============================================================================
46
+ // Timeline (external mutable array, same pattern as fraud-analysis)
47
+ // ============================================================================
48
+
49
+ export const timeline: TimelineEntry[] = [];
39
50
 
40
- function addTimelineEntry(event: string, detail: string, type: string) {
51
+ export function addTimelineEntry(
52
+ event: string,
53
+ detail: string,
54
+ type: string,
55
+ ): void {
41
56
  timeline.unshift({ time: Date.now(), event, detail, type });
42
57
  }
43
58
 
44
- function log(msg: string) {
59
+ // ============================================================================
60
+ // Logs helper
61
+ // ============================================================================
62
+
63
+ export function log(msg: string): void {
45
64
  console.log(`[contact-form] ${msg}`);
46
65
 
47
66
  // Classify and add to timeline
@@ -66,7 +85,7 @@ function log(msg: string) {
66
85
  // Schema
67
86
  // ============================================================================
68
87
 
69
- const schema = {
88
+ export const schema = {
70
89
  facts: {
71
90
  name: t.string(),
72
91
  email: t.string(),
@@ -103,74 +122,206 @@ const schema = {
103
122
  // Module
104
123
  // ============================================================================
105
124
 
125
+ const contactForm = createModule("contact-form", {
126
+ schema,
106
127
 
107
- // ============================================================================
108
- // System
109
- // ============================================================================
128
+ init: (facts) => {
129
+ facts.name = "";
130
+ facts.email = "";
131
+ facts.subject = "";
132
+ facts.message = "";
133
+ facts.touched = {};
134
+ facts.status = "idle";
135
+ facts.errorMessage = "";
136
+ facts.lastSubmittedAt = 0;
137
+ facts.submissionCount = 0;
138
+ },
110
139
 
111
- const system = createSystem({
112
- module: contactForm,
113
- debug: { runHistory: true },
114
- plugins: [devtoolsPlugin({ name: "contact-form" })],
115
- });
116
- system.start();
140
+ derive: {
141
+ nameError: (facts) => {
142
+ if (!facts.touched.name) {
143
+ return "";
144
+ }
145
+ if (!facts.name.trim()) {
146
+ return "Name is required";
147
+ }
148
+ if (facts.name.trim().length < 2) {
149
+ return "Name must be at least 2 characters";
150
+ }
117
151
 
118
- // ============================================================================
119
- // DOM References
120
- // ============================================================================
152
+ return "";
153
+ },
121
154
 
122
- // Form inputs
155
+ emailError: (facts) => {
156
+ if (!facts.touched.email) {
157
+ return "";
158
+ }
159
+ if (!facts.email.trim()) {
160
+ return "Email is required";
161
+ }
162
+ if (!EMAIL_REGEX.test(facts.email)) {
163
+ return "Enter a valid email address";
164
+ }
123
165
 
124
- // Timeline
166
+ return "";
167
+ },
125
168
 
126
- // ============================================================================
127
- // Input Handlers
128
- // ============================================================================
169
+ subjectError: (facts) => {
170
+ if (!facts.touched.subject) {
171
+ return "";
172
+ }
173
+ if (!facts.subject) {
174
+ return "Please select a subject";
175
+ }
129
176
 
130
- for (const [el, field] of [
131
- [nameInput, "name"],
132
- [emailInput, "email"],
133
- [subjectInput, "subject"],
134
- [messageInput, "message"],
135
- ] as const) {
136
- }
177
+ return "";
178
+ },
137
179
 
180
+ messageError: (facts) => {
181
+ if (!facts.touched.message) {
182
+ return "";
183
+ }
184
+ if (!facts.message.trim()) {
185
+ return "Message is required";
186
+ }
187
+ if (facts.message.trim().length < 10) {
188
+ return "Message must be at least 10 characters";
189
+ }
138
190
 
139
- // ============================================================================
140
- // Render
141
- // ============================================================================
191
+ return "";
192
+ },
142
193
 
143
- function escapeHtml(text: string): string {
194
+ isValid: (facts) =>
195
+ facts.name.trim().length >= 2 &&
196
+ EMAIL_REGEX.test(facts.email) &&
197
+ facts.subject !== "" &&
198
+ facts.message.trim().length >= 10,
144
199
 
145
- return div.innerHTML;
146
- }
200
+ canSubmit: (facts, derive) => {
201
+ if (!derive.isValid) {
202
+ return false;
203
+ }
204
+ if (facts.status !== "idle") {
205
+ return false;
206
+ }
207
+ if (
208
+ facts.lastSubmittedAt > 0 &&
209
+ Date.now() - facts.lastSubmittedAt < RATE_LIMIT_MS
210
+ ) {
211
+ return false;
212
+ }
213
+
214
+ return true;
215
+ },
147
216
 
217
+ messageCharCount: (facts) => facts.message.length,
218
+ },
219
+
220
+ events: {
221
+ updateField: (facts, { field, value }) => {
222
+ const key = field as "name" | "email" | "subject" | "message";
223
+ if (key in facts && typeof facts[key] === "string") {
224
+ (facts as unknown as Record<string, string>)[key] = value;
225
+ }
226
+ },
148
227
 
149
- // Subscribe to all relevant facts and derivations
150
- system.subscribe(
151
- [
152
- "name",
153
- "email",
154
- "subject",
155
- "message",
156
- "touched",
157
- "status",
158
- "errorMessage",
159
- "lastSubmittedAt",
160
- "submissionCount",
161
- "nameError",
162
- "emailError",
163
- "subjectError",
164
- "messageError",
165
- "isValid",
166
- "canSubmit",
167
- "messageCharCount",
168
- ],
169
- render,
170
- );
171
-
172
- // Initial render
173
- render();
174
- log("Contact form ready. Fill in all fields and submit.");
175
-
176
- // Signal to tests that initialization is complete
228
+ touchField: (facts, { field }) => {
229
+ facts.touched = { ...facts.touched, [field]: true };
230
+ },
231
+
232
+ submit: (facts) => {
233
+ facts.touched = { name: true, email: true, subject: true, message: true };
234
+ facts.status = "submitting";
235
+ },
236
+
237
+ reset: (facts) => {
238
+ facts.name = "";
239
+ facts.email = "";
240
+ facts.subject = "";
241
+ facts.message = "";
242
+ facts.touched = {};
243
+ facts.status = "idle";
244
+ facts.errorMessage = "";
245
+ },
246
+ },
247
+
248
+ constraints: {
249
+ submitForm: {
250
+ when: (facts) => facts.status === "submitting",
251
+ require: { type: "SEND_MESSAGE" },
252
+ },
253
+
254
+ resetAfterSuccess: {
255
+ when: (facts) => facts.status === "success",
256
+ require: { type: "RESET_AFTER_DELAY" },
257
+ },
258
+ },
259
+
260
+ resolvers: {
261
+ sendMessage: {
262
+ requirement: "SEND_MESSAGE",
263
+ resolve: async (req, context) => {
264
+ log(
265
+ `Sending: ${context.facts.name} <${context.facts.email}> [${context.facts.subject}]`,
266
+ );
267
+
268
+ await new Promise((resolve) => setTimeout(resolve, 1500));
269
+
270
+ if (Math.random() < 0.2) {
271
+ context.facts.status = "error";
272
+ context.facts.errorMessage =
273
+ "Simulated error — try again (20% failure rate for demo).";
274
+ log("Submission failed (simulated)");
275
+
276
+ return;
277
+ }
278
+
279
+ context.facts.status = "success";
280
+ context.facts.lastSubmittedAt = Date.now();
281
+ context.facts.submissionCount++;
282
+ log(`Submission #${context.facts.submissionCount} succeeded`);
283
+ },
284
+ },
285
+
286
+ resetAfterDelay: {
287
+ requirement: "RESET_AFTER_DELAY",
288
+ resolve: async (req, context) => {
289
+ log("Auto-resetting in 3 seconds...");
290
+ await new Promise((resolve) => setTimeout(resolve, 3000));
291
+ context.facts.name = "";
292
+ context.facts.email = "";
293
+ context.facts.subject = "";
294
+ context.facts.message = "";
295
+ context.facts.touched = {};
296
+ context.facts.status = "idle";
297
+ context.facts.errorMessage = "";
298
+ log("Form reset");
299
+ },
300
+ },
301
+ },
302
+
303
+ effects: {
304
+ logSubmission: {
305
+ deps: ["status", "submissionCount"],
306
+ run: (facts, prev) => {
307
+ if (!prev) {
308
+ return;
309
+ }
310
+
311
+ if (facts.status !== prev.status) {
312
+ log(`Status: ${prev.status} → ${facts.status}`);
313
+ }
314
+ },
315
+ },
316
+ },
317
+ });
318
+
319
+ // ============================================================================
320
+ // System
321
+ // ============================================================================
322
+
323
+ export const system = createSystem({
324
+ module: contactForm,
325
+ debug: { runHistory: true },
326
+ plugins: [devtoolsPlugin({ name: "contact-form" })],
327
+ });
@@ -1,12 +1,12 @@
1
1
  // Example: counter
2
- // Source: examples/counter/src/main.ts
3
- // Extracted for AI rules — DOM wiring stripped
2
+ // Source: examples/counter/src/module.ts
3
+ // Pure module fileno DOM wiring
4
4
 
5
5
  /**
6
- * Number Match — DOM Rendering & System Wiring
6
+ * Number Match — Directive Module
7
7
  *
8
- * Creates the Directive system, subscribes to state changes,
9
- * renders the game grid and event timeline.
8
+ * Types, schema, helpers, module definition, timeline, and system creation
9
+ * for a tile-matching game where pairs must add to 10.
10
10
  */
11
11
 
12
12
  import {
@@ -21,18 +21,22 @@ import { devtoolsPlugin } from "@directive-run/core/plugins";
21
21
  // Types
22
22
  // ============================================================================
23
23
 
24
- interface Tile {
24
+ export interface Tile {
25
25
  id: string;
26
26
  value: number;
27
27
  }
28
28
 
29
- interface TimelineEntry {
29
+ export interface TimelineEntry {
30
30
  time: number;
31
31
  event: string;
32
32
  detail: string;
33
33
  type: string;
34
34
  }
35
35
 
36
+ // ============================================================================
37
+ // Helpers
38
+ // ============================================================================
39
+
36
40
  // Create a pool of numbered tiles (1-9, four of each = 36 tiles)
37
41
  function createPool(): Tile[] {
38
42
  const tiles: Tile[] = [];
@@ -55,9 +59,9 @@ function createPool(): Tile[] {
55
59
  // Timeline
56
60
  // ============================================================================
57
61
 
58
- const timeline: TimelineEntry[] = [];
62
+ export const timeline: TimelineEntry[] = [];
59
63
 
60
- function log(msg: string) {
64
+ export function addLog(msg: string) {
61
65
  console.log(`[NumberMatch] ${msg}`);
62
66
 
63
67
  // Classify and add significant events to the timeline
@@ -101,15 +105,15 @@ function log(msg: string) {
101
105
  }
102
106
 
103
107
  // ============================================================================
104
- // Schema - same structure as eleven-up
108
+ // Schema
105
109
  // ============================================================================
106
110
 
107
- const schema = {
111
+ export const schema = {
108
112
  facts: {
109
- pool: t.object<Tile[]>(),
110
- table: t.object<Tile[]>(),
111
- removed: t.object<Tile[]>(),
112
- selected: t.object<string[]>(),
113
+ pool: t.array<Tile>(),
114
+ table: t.array<Tile>(),
115
+ removed: t.array<Tile>(),
116
+ selected: t.array<string>(),
113
117
  message: t.string(),
114
118
  moveCount: t.number(),
115
119
  gameOver: t.boolean(),
@@ -117,7 +121,7 @@ const schema = {
117
121
  derivations: {
118
122
  poolCount: t.number(),
119
123
  removedCount: t.number(),
120
- selectedTiles: t.object<Tile[]>(),
124
+ selectedTiles: t.array<Tile>(),
121
125
  hasValidMoves: t.boolean(),
122
126
  },
123
127
  events: {
@@ -127,7 +131,7 @@ const schema = {
127
131
  clearSelection: {},
128
132
  },
129
133
  requirements: {
130
- REMOVE_TILES: { tileIds: t.object<string[]>() },
134
+ REMOVE_TILES: { tileIds: t.array<string>() },
131
135
  REFILL_TABLE: { count: t.number() },
132
136
  END_GAME: { reason: t.string() },
133
137
  },
@@ -160,9 +164,12 @@ const numberMatch = createModule("number-match", {
160
164
  const nums = facts.table.map((t: Tile) => t.value);
161
165
  for (let i = 0; i < nums.length; i++) {
162
166
  for (let j = i + 1; j < nums.length; j++) {
163
- if (nums[i] + nums[j] === 10) return true;
167
+ if (nums[i] + nums[j] === 10) {
168
+ return true;
169
+ }
164
170
  }
165
171
  }
172
+
166
173
  return false;
167
174
  },
168
175
  },
@@ -181,7 +188,9 @@ const numberMatch = createModule("number-match", {
181
188
  selectTile: (facts, { tileId }) => {
182
189
  if (!facts.selected.includes(tileId) && !facts.gameOver) {
183
190
  facts.selected = [...facts.selected, tileId];
184
- log(`EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`);
191
+ addLog(
192
+ `EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`,
193
+ );
185
194
  }
186
195
  },
187
196
  deselectTile: (facts, { tileId }) => {
@@ -193,27 +202,34 @@ const numberMatch = createModule("number-match", {
193
202
  },
194
203
 
195
204
  // ============================================================================
196
- // Constraints - same pattern as eleven-up
205
+ // Constraints
197
206
  // ============================================================================
198
207
  constraints: {
199
208
  // When two selected tiles add to 10 -> remove them
200
209
  pairAddsTen: {
201
210
  priority: 100,
202
211
  when: (facts) => {
203
- if (facts.gameOver) return false;
212
+ if (facts.gameOver) {
213
+ return false;
214
+ }
204
215
  const selected = facts.table.filter((tile: Tile) =>
205
216
  facts.selected.includes(tile.id),
206
217
  );
207
- if (selected.length !== 2) return false;
218
+ if (selected.length !== 2) {
219
+ return false;
220
+ }
208
221
  const result = selected[0].value + selected[1].value === 10;
209
- if (result)
210
- log(
222
+ if (result) {
223
+ addLog(
211
224
  `CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
212
225
  );
226
+ }
227
+
213
228
  return result;
214
229
  },
215
230
  require: (facts) => {
216
- log("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
231
+ addLog("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
232
+
217
233
  return {
218
234
  type: "REMOVE_TILES",
219
235
  tileIds: [...facts.selected],
@@ -227,15 +243,18 @@ const numberMatch = createModule("number-match", {
227
243
  when: (facts) => {
228
244
  const result =
229
245
  !facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
230
- if (result)
231
- log(
246
+ if (result) {
247
+ addLog(
232
248
  `CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
233
249
  );
250
+ }
251
+
234
252
  return result;
235
253
  },
236
254
  require: (facts) => {
237
255
  const count = Math.min(9 - facts.table.length, facts.pool.length);
238
- log(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
256
+ addLog(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
257
+
239
258
  return { type: "REFILL_TABLE", count };
240
259
  },
241
260
  },
@@ -244,16 +263,25 @@ const numberMatch = createModule("number-match", {
244
263
  noMovesLeft: {
245
264
  priority: 190,
246
265
  when: (facts) => {
247
- if (facts.gameOver) return false;
248
- if (facts.table.length === 0) return false;
249
- if (facts.pool.length > 0) return false;
266
+ if (facts.gameOver) {
267
+ return false;
268
+ }
269
+ if (facts.table.length === 0) {
270
+ return false;
271
+ }
272
+ if (facts.pool.length > 0) {
273
+ return false;
274
+ }
250
275
  const nums = facts.table.map((t: Tile) => t.value);
251
276
  for (let i = 0; i < nums.length; i++) {
252
277
  for (let j = i + 1; j < nums.length; j++) {
253
- if (nums[i] + nums[j] === 10) return false;
278
+ if (nums[i] + nums[j] === 10) {
279
+ return false;
280
+ }
254
281
  }
255
282
  }
256
- log("CONSTRAINT noMovesLeft: TRUE");
283
+ addLog("CONSTRAINT noMovesLeft: TRUE");
284
+
257
285
  return true;
258
286
  },
259
287
  require: (facts) => ({
@@ -270,7 +298,10 @@ const numberMatch = createModule("number-match", {
270
298
  !facts.gameOver &&
271
299
  facts.table.length === 0 &&
272
300
  facts.pool.length === 0;
273
- if (result) log("CONSTRAINT allCleared: TRUE");
301
+ if (result) {
302
+ addLog("CONSTRAINT allCleared: TRUE");
303
+ }
304
+
274
305
  return result;
275
306
  },
276
307
  require: (facts) => ({
@@ -281,42 +312,42 @@ const numberMatch = createModule("number-match", {
281
312
  },
282
313
 
283
314
  // ============================================================================
284
- // Resolvers - same multi-fact mutation pattern as eleven-up
315
+ // Resolvers
285
316
  // ============================================================================
286
317
  resolvers: {
287
318
  removeTiles: {
288
319
  requirement: "REMOVE_TILES",
289
320
  resolve: async (req, context) => {
290
- log("RESOLVER removeTiles: START");
321
+ addLog("RESOLVER removeTiles: START");
291
322
  const tilesToRemove = context.facts.table.filter((tile: Tile) =>
292
323
  req.tileIds.includes(tile.id),
293
324
  );
294
325
 
295
326
  // Multiple fact mutations
296
- log("RESOLVER removeTiles: setting table");
327
+ addLog("RESOLVER removeTiles: setting table");
297
328
  context.facts.table = context.facts.table.filter(
298
329
  (tile: Tile) => !req.tileIds.includes(tile.id),
299
330
  );
300
- log("RESOLVER removeTiles: setting removed");
331
+ addLog("RESOLVER removeTiles: setting removed");
301
332
  context.facts.removed = [...context.facts.removed, ...tilesToRemove];
302
- log("RESOLVER removeTiles: clearing selected");
333
+ addLog("RESOLVER removeTiles: clearing selected");
303
334
  context.facts.selected = [];
304
- log("RESOLVER removeTiles: incrementing moveCount");
335
+ addLog("RESOLVER removeTiles: incrementing moveCount");
305
336
  context.facts.moveCount++;
306
- log("RESOLVER removeTiles: setting message");
337
+ addLog("RESOLVER removeTiles: setting message");
307
338
  context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
308
- log("RESOLVER removeTiles: DONE");
339
+ addLog("RESOLVER removeTiles: DONE");
309
340
  },
310
341
  },
311
342
 
312
343
  refillTable: {
313
344
  requirement: "REFILL_TABLE",
314
345
  resolve: async (req, context) => {
315
- log(`RESOLVER refillTable: START (count: ${req.count})`);
346
+ addLog(`RESOLVER refillTable: START (count: ${req.count})`);
316
347
  const newTiles = context.facts.pool.slice(0, req.count);
317
348
  context.facts.pool = context.facts.pool.slice(req.count);
318
349
  context.facts.table = [...context.facts.table, ...newTiles];
319
- log(
350
+ addLog(
320
351
  `RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
321
352
  );
322
353
  },
@@ -325,7 +356,7 @@ const numberMatch = createModule("number-match", {
325
356
  endGame: {
326
357
  requirement: "END_GAME",
327
358
  resolve: async (req, context) => {
328
- log(`RESOLVER endGame: ${req.reason}`);
359
+ addLog(`RESOLVER endGame: ${req.reason}`);
329
360
  context.facts.gameOver = true;
330
361
  context.facts.message = req.reason;
331
362
  },
@@ -337,57 +368,8 @@ const numberMatch = createModule("number-match", {
337
368
  // System
338
369
  // ============================================================================
339
370
 
340
- const system = createSystem({
371
+ export const system = createSystem({
341
372
  module: numberMatch,
342
373
  plugins: [devtoolsPlugin({ name: "number-match" })],
343
374
  debug: { timeTravel: true, runHistory: true },
344
375
  });
345
- system.start();
346
-
347
- // ============================================================================
348
- // DOM References
349
- // ============================================================================
350
-
351
- // Stats
352
-
353
- // Timeline
354
-
355
- // ============================================================================
356
- // Render
357
- // ============================================================================
358
-
359
- function escapeHtml(text: string): string {
360
-
361
- return div.innerHTML;
362
- }
363
-
364
-
365
- // ============================================================================
366
- // Subscribe
367
- // ============================================================================
368
-
369
- system.subscribe(
370
- [
371
- "table",
372
- "selected",
373
- "pool",
374
- "removed",
375
- "moveCount",
376
- "message",
377
- "gameOver",
378
- "poolCount",
379
- "removedCount",
380
- "selectedTiles",
381
- "hasValidMoves",
382
- ],
383
- render,
384
- );
385
-
386
- // Button handlers
387
-
388
-
389
- // Initial render
390
- render();
391
- log("Game started. Select two numbers that add to 10.");
392
-
393
- // Signal to tests that initialization is complete