@directive-run/knowledge 0.8.2 → 0.8.4

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.
package/LICENSE CHANGED
@@ -2,6 +2,11 @@ MIT License
2
2
 
3
3
  Copyright (c) 2026 Sizls LLC
4
4
 
5
+ This project is dual-licensed under MIT OR Apache-2.0. You may choose
6
+ either license. See LICENSE-APACHE for the Apache License, Version 2.0.
7
+
8
+ ---
9
+
5
10
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
11
  of this software and associated documentation files (the "Software"), to deal
7
12
  in the Software without restriction, including without limitation the rights
package/core/plugins.md CHANGED
@@ -28,7 +28,7 @@ const system = createSystem({
28
28
  module: myModule,
29
29
  plugins: [
30
30
  devtoolsPlugin(),
31
- loggingPlugin({ verbose: true }),
31
+ loggingPlugin({ level: "debug" }),
32
32
  persistencePlugin({
33
33
  key: "my-app-state",
34
34
  storage: localStorage,
@@ -46,25 +46,27 @@ Logs state changes, requirements, and resolutions to the console.
46
46
  ```typescript
47
47
  import { loggingPlugin } from "@directive-run/core/plugins";
48
48
 
49
- // Default – logs facts changes and resolver start/complete
49
+ // Default – logs facts changes and resolver start/complete at "info" level
50
50
  loggingPlugin()
51
51
 
52
- // Verbose – logs everything including derivation recomputation and constraint evaluation
53
- loggingPlugin({ verbose: true })
52
+ // Debug level – logs everything including derivation recomputation and constraint evaluation
53
+ loggingPlugin({ level: "debug" })
54
54
 
55
55
  // Custom filter – only log specific events
56
56
  loggingPlugin({
57
- filter: (event) => {
58
- // Only log resolver events
59
- if (event.type === "resolution:start" || event.type === "resolution:complete") {
60
- return true;
61
- }
57
+ filter: (event) => event.startsWith("resolver."),
58
+ })
62
59
 
63
- return false;
64
- },
60
+ // Custom logger and prefix
61
+ loggingPlugin({
62
+ level: "warn",
63
+ prefix: "[MyApp]",
64
+ logger: customLogger,
65
65
  })
66
66
  ```
67
67
 
68
+ Options: `level` ("debug" | "info" | "warn" | "error"), `filter` (predicate on event name string), `logger` (Console-compatible object), `prefix` (string, default "[Directive]").
69
+
68
70
  ## devtoolsPlugin
69
71
 
70
72
  Connects to the Directive DevTools browser extension for visual state inspection.
@@ -78,7 +80,9 @@ devtoolsPlugin()
78
80
  // With options
79
81
  devtoolsPlugin({
80
82
  name: "My App", // Name shown in DevTools
81
- maxAge: 50, // Max actions to keep in history
83
+ maxEvents: 1000, // Max trace events to retain (default: 1000)
84
+ trace: true, // Enable trace logging
85
+ panel: true, // Show floating debug panel (dev mode only)
82
86
  })
83
87
  ```
84
88
 
@@ -122,59 +126,56 @@ persistencePlugin({
122
126
  exclude: ["tempData", "sessionId"], // Everything except these
123
127
  })
124
128
 
125
- // Versioning – handle schema changes
129
+ // With callbacks
126
130
  persistencePlugin({
127
131
  key: "my-app",
128
132
  storage: localStorage,
129
- version: 2,
130
- migrate: (oldState, oldVersion) => {
131
- if (oldVersion === 1) {
132
- return {
133
- ...oldState,
134
- newField: "default",
135
- };
136
- }
137
-
138
- return oldState;
139
- },
133
+ debounce: 200, // Debounce saves (default: 100ms)
134
+ onRestore: (data) => console.log("Restored:", data),
135
+ onSave: (data) => console.log("Saved:", data),
136
+ onError: (err) => console.error("Persistence error:", err),
140
137
  })
141
138
  ```
142
139
 
143
140
  ## createCircuitBreaker
144
141
 
145
- Wraps resolvers with circuit breaker pattern for resilience.
142
+ Standalone utility (not a Plugin) that implements the circuit breaker pattern for resilience. Use it inside resolvers to protect against cascading failures from external services.
146
143
 
147
144
  ```typescript
148
145
  import { createCircuitBreaker } from "@directive-run/core/plugins";
149
146
 
150
- const system = createSystem({
151
- module: myModule,
152
- plugins: [
153
- createCircuitBreaker({
154
- // How many failures before opening the circuit
155
- failureThreshold: 5,
156
-
157
- // How long to wait before trying again (ms)
158
- resetTimeout: 30000,
159
-
160
- // Optional: only apply to specific resolver types
161
- include: ["FETCH_DATA", "SYNC_REMOTE"],
147
+ const breaker = createCircuitBreaker({
148
+ failureThreshold: 5, // Failures before opening (default: 5)
149
+ recoveryTimeMs: 30000, // Time before HALF_OPEN (default: 30000)
150
+ halfOpenMaxRequests: 3, // Requests allowed in HALF_OPEN (default: 3)
151
+ failureWindowMs: 60000, // Window for counting failures (default: 60000)
152
+ name: "api-breaker", // Name for metrics/errors
153
+ onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`),
154
+ });
162
155
 
163
- // Optional: callback when circuit opens
164
- onOpen: (resolverType) => {
165
- console.warn(`Circuit opened for ${resolverType}`);
166
- },
156
+ // Use inside a resolver
157
+ resolvers: {
158
+ fetchData: {
159
+ requirement: "FETCH_DATA",
160
+ resolve: async (req, context) => {
161
+ const result = await breaker.execute(async () => {
162
+ return await callExternalAPI();
163
+ });
164
+ context.facts.data = result;
165
+ },
166
+ },
167
+ },
167
168
 
168
- // Optional: callback when circuit closes
169
- onClose: (resolverType) => {
170
- console.log(`Circuit closed for ${resolverType}`);
171
- },
172
- }),
173
- ],
174
- });
169
+ // Wire circuit state into constraints
170
+ constraints: {
171
+ apiDown: {
172
+ when: () => breaker.getState() === "OPEN",
173
+ require: { type: "FALLBACK_RESPONSE" },
174
+ },
175
+ },
175
176
  ```
176
177
 
177
- Circuit breaker states: **Closed** (normal) -> **Open** (failing, rejects immediately) -> **Half-Open** (testing one request).
178
+ Circuit breaker states: **Closed** (normal) -> **Open** (failing, rejects immediately) -> **Half-Open** (testing limited requests).
178
179
 
179
180
  ## createObservability
180
181
 
@@ -212,14 +213,14 @@ const system = createSystem({
212
213
  Plugins hook into the system lifecycle. Use these to build custom plugins.
213
214
 
214
215
  ```typescript
215
- import type { DirectivePlugin } from "@directive-run/core";
216
+ import type { Plugin, ModuleSchema } from "@directive-run/core";
216
217
 
217
- const myPlugin: DirectivePlugin = {
218
+ const myPlugin: Plugin<ModuleSchema> = {
218
219
  name: "my-custom-plugin",
219
220
 
220
221
  // System lifecycle
221
222
  onInit: (system) => {
222
- // Called when system is created
223
+ // Called when system is created (only async hook)
223
224
  },
224
225
  onStart: (system) => {
225
226
  // Called when system.start() is invoked
@@ -227,32 +228,42 @@ const myPlugin: DirectivePlugin = {
227
228
  onStop: (system) => {
228
229
  // Called when system.stop() is invoked
229
230
  },
231
+ onDestroy: (system) => {
232
+ // Called when system.destroy() is invoked
233
+ },
230
234
 
231
- // State tracking
232
- onSnapshot: (snapshot) => {
233
- // Called after every fact mutation
234
- // snapshot.facts contains current state
235
- // snapshot.changedKeys lists what changed
235
+ // Fact tracking
236
+ onFactSet: (key, value, prev) => {
237
+ // Called when a single fact is set
238
+ },
239
+ onFactsBatch: (changes) => {
240
+ // Called after a batch of fact changes completes
236
241
  },
237
242
 
238
243
  // Requirement pipeline
239
- onRequirementEmitted: (requirement) => {
244
+ onRequirementCreated: (requirement) => {
240
245
  // Called when a constraint emits a requirement
241
- // requirement.type, requirement.id, payload
246
+ // requirement.type, requirement.id
247
+ },
248
+ onRequirementMet: (requirement, byResolver) => {
249
+ // Called when a requirement is fulfilled
242
250
  },
243
- onResolutionStart: (resolution) => {
251
+
252
+ // Resolver pipeline
253
+ onResolverStart: (resolverId, requirement) => {
244
254
  // Called when a resolver begins executing
245
- // resolution.resolverId, resolution.requirement
246
255
  },
247
- onResolutionComplete: (resolution) => {
248
- // Called when a resolver finishes (success or failure)
249
- // resolution.resolverId, resolution.duration, resolution.error?
256
+ onResolverComplete: (resolverId, requirement, duration) => {
257
+ // Called when a resolver finishes successfully
258
+ },
259
+ onResolverError: (resolverId, requirement, error) => {
260
+ // Called when a resolver fails (after all retries)
250
261
  },
251
262
 
252
263
  // Error handling
253
- onError: (error, context) => {
254
- // Called on any error in the system
255
- // context.source: "resolver" | "constraint" | "effect" | "derivation"
264
+ onError: (error) => {
265
+ // Called on any DirectiveError in the system
266
+ // error.source, error.message, error.context
256
267
  },
257
268
  };
258
269
 
@@ -277,7 +288,7 @@ const system = createSystem({
277
288
  const plugins = [];
278
289
  if (process.env.NODE_ENV === "development") {
279
290
  plugins.push(devtoolsPlugin());
280
- plugins.push(loggingPlugin({ verbose: true }));
291
+ plugins.push(loggingPlugin({ level: "debug" }));
281
292
  }
282
293
 
283
294
  const system = createSystem({
@@ -286,21 +297,20 @@ const system = createSystem({
286
297
  });
287
298
  ```
288
299
 
289
- ### Persistence without versioning
300
+ ### Persistence without filtering
290
301
 
291
302
  ```typescript
292
- // WRONG – schema changes break existing users
303
+ // WRONG – persists everything including transient state
293
304
  persistencePlugin({
294
305
  key: "app-state",
295
306
  storage: localStorage,
296
307
  })
297
308
 
298
- // CORRECT – version and migrate
309
+ // CORRECT – use include/exclude to control what's persisted
299
310
  persistencePlugin({
300
311
  key: "app-state",
301
312
  storage: localStorage,
302
- version: 1,
303
- migrate: (old, version) => old,
313
+ include: ["user", "preferences", "settings"],
304
314
  })
305
315
  ```
306
316
 
@@ -0,0 +1,44 @@
1
+ // Example: counter-react
2
+ // Source: examples/counter-react/src/module.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Shared counter module — same file used by all framework examples.
7
+ */
8
+
9
+ import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
10
+
11
+ const schema = {
12
+ facts: { count: t.number() },
13
+ derivations: { doubled: t.number(), isPositive: t.boolean() },
14
+ events: { increment: {}, decrement: {}, reset: {} },
15
+ requirements: { CLAMP_TO_ZERO: {} },
16
+ } satisfies ModuleSchema;
17
+
18
+ export const counterModule = createModule("counter", {
19
+ schema,
20
+ init: (facts) => { facts.count = 0; },
21
+ derive: {
22
+ doubled: (facts) => facts.count * 2,
23
+ isPositive: (facts) => facts.count > 0,
24
+ },
25
+ events: {
26
+ increment: (facts) => { facts.count += 1; },
27
+ decrement: (facts) => { facts.count -= 1; },
28
+ reset: (facts) => { facts.count = 0; },
29
+ },
30
+ constraints: {
31
+ noNegative: {
32
+ when: (facts) => facts.count < 0,
33
+ require: { type: "CLAMP_TO_ZERO" },
34
+ },
35
+ },
36
+ resolvers: {
37
+ clamp: {
38
+ requirement: "CLAMP_TO_ZERO",
39
+ resolve: async (req, context) => { context.facts.count = 0; },
40
+ },
41
+ },
42
+ });
43
+
44
+ export const system = createSystem({ module: counterModule });
@@ -0,0 +1,44 @@
1
+ // Example: counter-svelte
2
+ // Source: examples/counter-svelte/src/module.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Shared counter module — same file used by all framework examples.
7
+ */
8
+
9
+ import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
10
+
11
+ const schema = {
12
+ facts: { count: t.number() },
13
+ derivations: { doubled: t.number(), isPositive: t.boolean() },
14
+ events: { increment: {}, decrement: {}, reset: {} },
15
+ requirements: { CLAMP_TO_ZERO: {} },
16
+ } satisfies ModuleSchema;
17
+
18
+ export const counterModule = createModule("counter", {
19
+ schema,
20
+ init: (facts) => { facts.count = 0; },
21
+ derive: {
22
+ doubled: (facts) => facts.count * 2,
23
+ isPositive: (facts) => facts.count > 0,
24
+ },
25
+ events: {
26
+ increment: (facts) => { facts.count += 1; },
27
+ decrement: (facts) => { facts.count -= 1; },
28
+ reset: (facts) => { facts.count = 0; },
29
+ },
30
+ constraints: {
31
+ noNegative: {
32
+ when: (facts) => facts.count < 0,
33
+ require: { type: "CLAMP_TO_ZERO" },
34
+ },
35
+ },
36
+ resolvers: {
37
+ clamp: {
38
+ requirement: "CLAMP_TO_ZERO",
39
+ resolve: async (req, context) => { context.facts.count = 0; },
40
+ },
41
+ },
42
+ });
43
+
44
+ export const system = createSystem({ module: counterModule });
@@ -0,0 +1,44 @@
1
+ // Example: counter-vue
2
+ // Source: examples/counter-vue/src/module.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Shared counter module — same file used by all framework examples.
7
+ */
8
+
9
+ import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
10
+
11
+ const schema = {
12
+ facts: { count: t.number() },
13
+ derivations: { doubled: t.number(), isPositive: t.boolean() },
14
+ events: { increment: {}, decrement: {}, reset: {} },
15
+ requirements: { CLAMP_TO_ZERO: {} },
16
+ } satisfies ModuleSchema;
17
+
18
+ export const counterModule = createModule("counter", {
19
+ schema,
20
+ init: (facts) => { facts.count = 0; },
21
+ derive: {
22
+ doubled: (facts) => facts.count * 2,
23
+ isPositive: (facts) => facts.count > 0,
24
+ },
25
+ events: {
26
+ increment: (facts) => { facts.count += 1; },
27
+ decrement: (facts) => { facts.count -= 1; },
28
+ reset: (facts) => { facts.count = 0; },
29
+ },
30
+ constraints: {
31
+ noNegative: {
32
+ when: (facts) => facts.count < 0,
33
+ require: { type: "CLAMP_TO_ZERO" },
34
+ },
35
+ },
36
+ resolvers: {
37
+ clamp: {
38
+ requirement: "CLAMP_TO_ZERO",
39
+ resolve: async (req, context) => { context.facts.count = 0; },
40
+ },
41
+ },
42
+ });
43
+
44
+ export const system = createSystem({ module: counterModule });
@@ -3,374 +3,66 @@
3
3
  // Pure module file — no DOM wiring
4
4
 
5
5
  /**
6
- * Number Match — Directive Module
6
+ * CounterThe simplest Directive module.
7
7
  *
8
- * Types, schema, helpers, module definition, timeline, and system creation
9
- * for a tile-matching game where pairs must add to 10.
8
+ * Demonstrates: facts, events, derivations, one constraint, one resolver.
9
+ * Total: ~40 lines.
10
10
  */
11
11
 
12
- import {
13
- type ModuleSchema,
14
- createModule,
15
- createSystem,
16
- t,
17
- } from "@directive-run/core";
18
- import { devtoolsPlugin } from "@directive-run/core/plugins";
12
+ import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
19
13
 
20
- // ============================================================================
21
- // Types
22
- // ============================================================================
23
-
24
- export interface Tile {
25
- id: string;
26
- value: number;
27
- }
28
-
29
- export interface TimelineEntry {
30
- time: number;
31
- event: string;
32
- detail: string;
33
- type: string;
34
- }
35
-
36
- // ============================================================================
37
- // Helpers
38
- // ============================================================================
39
-
40
- // Create a pool of numbered tiles (1-9, four of each = 36 tiles)
41
- function createPool(): Tile[] {
42
- const tiles: Tile[] = [];
43
- let id = 0;
44
- for (let copy = 0; copy < 4; copy++) {
45
- for (let value = 1; value <= 9; value++) {
46
- tiles.push({ id: `t${id++}`, value });
47
- }
48
- }
49
- // Shuffle
50
- for (let i = tiles.length - 1; i > 0; i--) {
51
- const j = Math.floor(Math.random() * (i + 1));
52
- [tiles[i], tiles[j]] = [tiles[j], tiles[i]];
53
- }
54
-
55
- return tiles;
56
- }
57
-
58
- // ============================================================================
59
- // Timeline
60
- // ============================================================================
61
-
62
- export const timeline: TimelineEntry[] = [];
63
-
64
- export function addLog(msg: string) {
65
- console.log(`[NumberMatch] ${msg}`);
66
-
67
- // Classify and add significant events to the timeline
68
- let event = "";
69
- let detail = "";
70
- let type = "info";
71
-
72
- if (msg.startsWith("EVENT selectTile")) {
73
- event = "tile selected";
74
- const match = msg.match(/selectTile: (t\d+)/);
75
- detail = match ? match[1] : "";
76
- type = "selection";
77
- } else if (msg.includes("pairAddsTen: TRUE")) {
78
- event = "match found";
79
- const match = msg.match(/\((.+)\)/);
80
- detail = match ? match[1] : "";
81
- type = "match";
82
- } else if (msg === "RESOLVER removeTiles: DONE") {
83
- event = "tiles removed";
84
- detail = "";
85
- type = "match";
86
- } else if (msg.includes("refillTable: DONE")) {
87
- event = "refill";
88
- const match = msg.match(/table now: (\d+)/);
89
- detail = match ? `table: ${match[1]} tiles` : "";
90
- type = "refill";
91
- } else if (msg.startsWith("RESOLVER endGame:")) {
92
- event = "game over";
93
- detail = msg.replace("RESOLVER endGame: ", "");
94
- type = "gameover";
95
- } else if (msg.includes("New game") || msg.includes("Game started")) {
96
- event = "new game";
97
- detail = msg;
98
- type = "newgame";
99
- } else {
100
- // Skip verbose intermediate messages (RESOLVER steps, CONSTRAINT produce)
101
- return;
102
- }
103
-
104
- timeline.unshift({ time: Date.now(), event, detail, type });
105
- }
106
-
107
- // ============================================================================
108
- // Schema
109
- // ============================================================================
110
-
111
- export const schema = {
14
+ const schema = {
112
15
  facts: {
113
- pool: t.array<Tile>(),
114
- table: t.array<Tile>(),
115
- removed: t.array<Tile>(),
116
- selected: t.array<string>(),
117
- message: t.string(),
118
- moveCount: t.number(),
119
- gameOver: t.boolean(),
16
+ count: t.number(),
120
17
  },
121
18
  derivations: {
122
- poolCount: t.number(),
123
- removedCount: t.number(),
124
- selectedTiles: t.array<Tile>(),
125
- hasValidMoves: t.boolean(),
19
+ doubled: t.number(),
20
+ isPositive: t.boolean(),
126
21
  },
127
22
  events: {
128
- newGame: {},
129
- selectTile: { tileId: t.string() },
130
- deselectTile: { tileId: t.string() },
131
- clearSelection: {},
23
+ increment: {},
24
+ decrement: {},
25
+ reset: {},
132
26
  },
133
27
  requirements: {
134
- REMOVE_TILES: { tileIds: t.array<string>() },
135
- REFILL_TABLE: { count: t.number() },
136
- END_GAME: { reason: t.string() },
28
+ CLAMP_TO_ZERO: {},
137
29
  },
138
30
  } satisfies ModuleSchema;
139
31
 
140
- // ============================================================================
141
- // Module
142
- // ============================================================================
143
-
144
- const numberMatch = createModule("number-match", {
32
+ export const counterModule = createModule("counter", {
145
33
  schema,
146
34
 
147
35
  init: (facts) => {
148
- const pool = createPool();
149
- facts.pool = pool.slice(9);
150
- facts.table = pool.slice(0, 9);
151
- facts.removed = [];
152
- facts.selected = [];
153
- facts.message = "Select two numbers that add to 10";
154
- facts.moveCount = 0;
155
- facts.gameOver = false;
36
+ facts.count = 0;
156
37
  },
157
38
 
158
39
  derive: {
159
- poolCount: (facts) => facts.pool.length,
160
- removedCount: (facts) => facts.removed.length,
161
- selectedTiles: (facts) =>
162
- facts.table.filter((tile: Tile) => facts.selected.includes(tile.id)),
163
- hasValidMoves: (facts) => {
164
- const nums = facts.table.map((t: Tile) => t.value);
165
- for (let i = 0; i < nums.length; i++) {
166
- for (let j = i + 1; j < nums.length; j++) {
167
- if (nums[i] + nums[j] === 10) {
168
- return true;
169
- }
170
- }
171
- }
172
-
173
- return false;
174
- },
40
+ doubled: (facts) => facts.count * 2,
41
+ isPositive: (facts) => facts.count > 0,
175
42
  },
176
43
 
177
44
  events: {
178
- newGame: (facts) => {
179
- const pool = createPool();
180
- facts.pool = pool.slice(9);
181
- facts.table = pool.slice(0, 9);
182
- facts.removed = [];
183
- facts.selected = [];
184
- facts.message = "New game! Select two numbers that add to 10";
185
- facts.moveCount = 0;
186
- facts.gameOver = false;
187
- },
188
- selectTile: (facts, { tileId }) => {
189
- if (!facts.selected.includes(tileId) && !facts.gameOver) {
190
- facts.selected = [...facts.selected, tileId];
191
- addLog(
192
- `EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`,
193
- );
194
- }
195
- },
196
- deselectTile: (facts, { tileId }) => {
197
- facts.selected = facts.selected.filter((id: string) => id !== tileId);
198
- },
199
- clearSelection: (facts) => {
200
- facts.selected = [];
201
- },
45
+ increment: (facts) => { facts.count += 1; },
46
+ decrement: (facts) => { facts.count -= 1; },
47
+ reset: (facts) => { facts.count = 0; },
202
48
  },
203
49
 
204
- // ============================================================================
205
- // Constraints
206
- // ============================================================================
50
+ // When count goes negative, automatically fix it
207
51
  constraints: {
208
- // When two selected tiles add to 10 -> remove them
209
- pairAddsTen: {
210
- priority: 100,
211
- when: (facts) => {
212
- if (facts.gameOver) {
213
- return false;
214
- }
215
- const selected = facts.table.filter((tile: Tile) =>
216
- facts.selected.includes(tile.id),
217
- );
218
- if (selected.length !== 2) {
219
- return false;
220
- }
221
- const result = selected[0].value + selected[1].value === 10;
222
- if (result) {
223
- addLog(
224
- `CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
225
- );
226
- }
227
-
228
- return result;
229
- },
230
- require: (facts) => {
231
- addLog("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
232
-
233
- return {
234
- type: "REMOVE_TILES",
235
- tileIds: [...facts.selected],
236
- };
237
- },
238
- },
239
-
240
- // Refill table when tiles are removed
241
- refillTable: {
242
- priority: 50,
243
- when: (facts) => {
244
- const result =
245
- !facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
246
- if (result) {
247
- addLog(
248
- `CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
249
- );
250
- }
251
-
252
- return result;
253
- },
254
- require: (facts) => {
255
- const count = Math.min(9 - facts.table.length, facts.pool.length);
256
- addLog(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
257
-
258
- return { type: "REFILL_TABLE", count };
259
- },
260
- },
261
-
262
- // No moves left -> game over
263
- noMovesLeft: {
264
- priority: 190,
265
- when: (facts) => {
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
- }
275
- const nums = facts.table.map((t: Tile) => t.value);
276
- for (let i = 0; i < nums.length; i++) {
277
- for (let j = i + 1; j < nums.length; j++) {
278
- if (nums[i] + nums[j] === 10) {
279
- return false;
280
- }
281
- }
282
- }
283
- addLog("CONSTRAINT noMovesLeft: TRUE");
284
-
285
- return true;
286
- },
287
- require: (facts) => ({
288
- type: "END_GAME",
289
- reason: `Game over! Removed ${facts.removed.length} of 36 tiles.`,
290
- }),
291
- },
292
-
293
- // Win condition
294
- allCleared: {
295
- priority: 200,
296
- when: (facts) => {
297
- const result =
298
- !facts.gameOver &&
299
- facts.table.length === 0 &&
300
- facts.pool.length === 0;
301
- if (result) {
302
- addLog("CONSTRAINT allCleared: TRUE");
303
- }
304
-
305
- return result;
306
- },
307
- require: (facts) => ({
308
- type: "END_GAME",
309
- reason: `You win! Cleared all tiles in ${facts.moveCount} moves!`,
310
- }),
52
+ noNegative: {
53
+ when: (facts) => facts.count < 0,
54
+ require: { type: "CLAMP_TO_ZERO" },
311
55
  },
312
56
  },
313
57
 
314
- // ============================================================================
315
- // Resolvers
316
- // ============================================================================
317
58
  resolvers: {
318
- removeTiles: {
319
- requirement: "REMOVE_TILES",
320
- resolve: async (req, context) => {
321
- addLog("RESOLVER removeTiles: START");
322
- const tilesToRemove = context.facts.table.filter((tile: Tile) =>
323
- req.tileIds.includes(tile.id),
324
- );
325
-
326
- // Multiple fact mutations
327
- addLog("RESOLVER removeTiles: setting table");
328
- context.facts.table = context.facts.table.filter(
329
- (tile: Tile) => !req.tileIds.includes(tile.id),
330
- );
331
- addLog("RESOLVER removeTiles: setting removed");
332
- context.facts.removed = [...context.facts.removed, ...tilesToRemove];
333
- addLog("RESOLVER removeTiles: clearing selected");
334
- context.facts.selected = [];
335
- addLog("RESOLVER removeTiles: incrementing moveCount");
336
- context.facts.moveCount++;
337
- addLog("RESOLVER removeTiles: setting message");
338
- context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
339
- addLog("RESOLVER removeTiles: DONE");
340
- },
341
- },
342
-
343
- refillTable: {
344
- requirement: "REFILL_TABLE",
59
+ clamp: {
60
+ requirement: "CLAMP_TO_ZERO",
345
61
  resolve: async (req, context) => {
346
- addLog(`RESOLVER refillTable: START (count: ${req.count})`);
347
- const newTiles = context.facts.pool.slice(0, req.count);
348
- context.facts.pool = context.facts.pool.slice(req.count);
349
- context.facts.table = [...context.facts.table, ...newTiles];
350
- addLog(
351
- `RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
352
- );
353
- },
354
- },
355
-
356
- endGame: {
357
- requirement: "END_GAME",
358
- resolve: async (req, context) => {
359
- addLog(`RESOLVER endGame: ${req.reason}`);
360
- context.facts.gameOver = true;
361
- context.facts.message = req.reason;
62
+ context.facts.count = 0;
362
63
  },
363
64
  },
364
65
  },
365
66
  });
366
67
 
367
- // ============================================================================
368
- // System
369
- // ============================================================================
370
-
371
- export const system = createSystem({
372
- module: numberMatch,
373
- plugins: [devtoolsPlugin({ name: "number-match" })],
374
- history: true,
375
- trace: true,
376
- });
68
+ export const system = createSystem({ module: counterModule });
@@ -0,0 +1,376 @@
1
+ // Example: number-match
2
+ // Source: examples/number-match/src/module.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Number Match — Directive Module
7
+ *
8
+ * Types, schema, helpers, module definition, timeline, and system creation
9
+ * for a tile-matching game where pairs must add to 10.
10
+ */
11
+
12
+ import {
13
+ type ModuleSchema,
14
+ createModule,
15
+ createSystem,
16
+ t,
17
+ } from "@directive-run/core";
18
+ import { devtoolsPlugin } from "@directive-run/core/plugins";
19
+
20
+ // ============================================================================
21
+ // Types
22
+ // ============================================================================
23
+
24
+ export interface Tile {
25
+ id: string;
26
+ value: number;
27
+ }
28
+
29
+ export interface TimelineEntry {
30
+ time: number;
31
+ event: string;
32
+ detail: string;
33
+ type: string;
34
+ }
35
+
36
+ // ============================================================================
37
+ // Helpers
38
+ // ============================================================================
39
+
40
+ // Create a pool of numbered tiles (1-9, four of each = 36 tiles)
41
+ function createPool(): Tile[] {
42
+ const tiles: Tile[] = [];
43
+ let id = 0;
44
+ for (let copy = 0; copy < 4; copy++) {
45
+ for (let value = 1; value <= 9; value++) {
46
+ tiles.push({ id: `t${id++}`, value });
47
+ }
48
+ }
49
+ // Shuffle
50
+ for (let i = tiles.length - 1; i > 0; i--) {
51
+ const j = Math.floor(Math.random() * (i + 1));
52
+ [tiles[i], tiles[j]] = [tiles[j], tiles[i]];
53
+ }
54
+
55
+ return tiles;
56
+ }
57
+
58
+ // ============================================================================
59
+ // Timeline
60
+ // ============================================================================
61
+
62
+ export const timeline: TimelineEntry[] = [];
63
+
64
+ export function addLog(msg: string) {
65
+ console.log(`[NumberMatch] ${msg}`);
66
+
67
+ // Classify and add significant events to the timeline
68
+ let event = "";
69
+ let detail = "";
70
+ let type = "info";
71
+
72
+ if (msg.startsWith("EVENT selectTile")) {
73
+ event = "tile selected";
74
+ const match = msg.match(/selectTile: (t\d+)/);
75
+ detail = match ? match[1] : "";
76
+ type = "selection";
77
+ } else if (msg.includes("pairAddsTen: TRUE")) {
78
+ event = "match found";
79
+ const match = msg.match(/\((.+)\)/);
80
+ detail = match ? match[1] : "";
81
+ type = "match";
82
+ } else if (msg === "RESOLVER removeTiles: DONE") {
83
+ event = "tiles removed";
84
+ detail = "";
85
+ type = "match";
86
+ } else if (msg.includes("refillTable: DONE")) {
87
+ event = "refill";
88
+ const match = msg.match(/table now: (\d+)/);
89
+ detail = match ? `table: ${match[1]} tiles` : "";
90
+ type = "refill";
91
+ } else if (msg.startsWith("RESOLVER endGame:")) {
92
+ event = "game over";
93
+ detail = msg.replace("RESOLVER endGame: ", "");
94
+ type = "gameover";
95
+ } else if (msg.includes("New game") || msg.includes("Game started")) {
96
+ event = "new game";
97
+ detail = msg;
98
+ type = "newgame";
99
+ } else {
100
+ // Skip verbose intermediate messages (RESOLVER steps, CONSTRAINT produce)
101
+ return;
102
+ }
103
+
104
+ timeline.unshift({ time: Date.now(), event, detail, type });
105
+ }
106
+
107
+ // ============================================================================
108
+ // Schema
109
+ // ============================================================================
110
+
111
+ export const schema = {
112
+ facts: {
113
+ pool: t.array<Tile>(),
114
+ table: t.array<Tile>(),
115
+ removed: t.array<Tile>(),
116
+ selected: t.array<string>(),
117
+ message: t.string(),
118
+ moveCount: t.number(),
119
+ gameOver: t.boolean(),
120
+ },
121
+ derivations: {
122
+ poolCount: t.number(),
123
+ removedCount: t.number(),
124
+ selectedTiles: t.array<Tile>(),
125
+ hasValidMoves: t.boolean(),
126
+ },
127
+ events: {
128
+ newGame: {},
129
+ selectTile: { tileId: t.string() },
130
+ deselectTile: { tileId: t.string() },
131
+ clearSelection: {},
132
+ },
133
+ requirements: {
134
+ REMOVE_TILES: { tileIds: t.array<string>() },
135
+ REFILL_TABLE: { count: t.number() },
136
+ END_GAME: { reason: t.string() },
137
+ },
138
+ } satisfies ModuleSchema;
139
+
140
+ // ============================================================================
141
+ // Module
142
+ // ============================================================================
143
+
144
+ const numberMatch = createModule("number-match", {
145
+ schema,
146
+
147
+ init: (facts) => {
148
+ const pool = createPool();
149
+ facts.pool = pool.slice(9);
150
+ facts.table = pool.slice(0, 9);
151
+ facts.removed = [];
152
+ facts.selected = [];
153
+ facts.message = "Select two numbers that add to 10";
154
+ facts.moveCount = 0;
155
+ facts.gameOver = false;
156
+ },
157
+
158
+ derive: {
159
+ poolCount: (facts) => facts.pool.length,
160
+ removedCount: (facts) => facts.removed.length,
161
+ selectedTiles: (facts) =>
162
+ facts.table.filter((tile: Tile) => facts.selected.includes(tile.id)),
163
+ hasValidMoves: (facts) => {
164
+ const nums = facts.table.map((t: Tile) => t.value);
165
+ for (let i = 0; i < nums.length; i++) {
166
+ for (let j = i + 1; j < nums.length; j++) {
167
+ if (nums[i] + nums[j] === 10) {
168
+ return true;
169
+ }
170
+ }
171
+ }
172
+
173
+ return false;
174
+ },
175
+ },
176
+
177
+ events: {
178
+ newGame: (facts) => {
179
+ const pool = createPool();
180
+ facts.pool = pool.slice(9);
181
+ facts.table = pool.slice(0, 9);
182
+ facts.removed = [];
183
+ facts.selected = [];
184
+ facts.message = "New game! Select two numbers that add to 10";
185
+ facts.moveCount = 0;
186
+ facts.gameOver = false;
187
+ },
188
+ selectTile: (facts, { tileId }) => {
189
+ if (!facts.selected.includes(tileId) && !facts.gameOver) {
190
+ facts.selected = [...facts.selected, tileId];
191
+ addLog(
192
+ `EVENT selectTile: ${tileId}, selected now: [${facts.selected}]`,
193
+ );
194
+ }
195
+ },
196
+ deselectTile: (facts, { tileId }) => {
197
+ facts.selected = facts.selected.filter((id: string) => id !== tileId);
198
+ },
199
+ clearSelection: (facts) => {
200
+ facts.selected = [];
201
+ },
202
+ },
203
+
204
+ // ============================================================================
205
+ // Constraints
206
+ // ============================================================================
207
+ constraints: {
208
+ // When two selected tiles add to 10 -> remove them
209
+ pairAddsTen: {
210
+ priority: 100,
211
+ when: (facts) => {
212
+ if (facts.gameOver) {
213
+ return false;
214
+ }
215
+ const selected = facts.table.filter((tile: Tile) =>
216
+ facts.selected.includes(tile.id),
217
+ );
218
+ if (selected.length !== 2) {
219
+ return false;
220
+ }
221
+ const result = selected[0].value + selected[1].value === 10;
222
+ if (result) {
223
+ addLog(
224
+ `CONSTRAINT pairAddsTen: TRUE (${selected[0].value} + ${selected[1].value})`,
225
+ );
226
+ }
227
+
228
+ return result;
229
+ },
230
+ require: (facts) => {
231
+ addLog("CONSTRAINT pairAddsTen: producing REMOVE_TILES");
232
+
233
+ return {
234
+ type: "REMOVE_TILES",
235
+ tileIds: [...facts.selected],
236
+ };
237
+ },
238
+ },
239
+
240
+ // Refill table when tiles are removed
241
+ refillTable: {
242
+ priority: 50,
243
+ when: (facts) => {
244
+ const result =
245
+ !facts.gameOver && facts.table.length < 9 && facts.pool.length > 0;
246
+ if (result) {
247
+ addLog(
248
+ `CONSTRAINT refillTable: TRUE (table: ${facts.table.length}, pool: ${facts.pool.length})`,
249
+ );
250
+ }
251
+
252
+ return result;
253
+ },
254
+ require: (facts) => {
255
+ const count = Math.min(9 - facts.table.length, facts.pool.length);
256
+ addLog(`CONSTRAINT refillTable: producing REFILL_TABLE count=${count}`);
257
+
258
+ return { type: "REFILL_TABLE", count };
259
+ },
260
+ },
261
+
262
+ // No moves left -> game over
263
+ noMovesLeft: {
264
+ priority: 190,
265
+ when: (facts) => {
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
+ }
275
+ const nums = facts.table.map((t: Tile) => t.value);
276
+ for (let i = 0; i < nums.length; i++) {
277
+ for (let j = i + 1; j < nums.length; j++) {
278
+ if (nums[i] + nums[j] === 10) {
279
+ return false;
280
+ }
281
+ }
282
+ }
283
+ addLog("CONSTRAINT noMovesLeft: TRUE");
284
+
285
+ return true;
286
+ },
287
+ require: (facts) => ({
288
+ type: "END_GAME",
289
+ reason: `Game over! Removed ${facts.removed.length} of 36 tiles.`,
290
+ }),
291
+ },
292
+
293
+ // Win condition
294
+ allCleared: {
295
+ priority: 200,
296
+ when: (facts) => {
297
+ const result =
298
+ !facts.gameOver &&
299
+ facts.table.length === 0 &&
300
+ facts.pool.length === 0;
301
+ if (result) {
302
+ addLog("CONSTRAINT allCleared: TRUE");
303
+ }
304
+
305
+ return result;
306
+ },
307
+ require: (facts) => ({
308
+ type: "END_GAME",
309
+ reason: `You win! Cleared all tiles in ${facts.moveCount} moves!`,
310
+ }),
311
+ },
312
+ },
313
+
314
+ // ============================================================================
315
+ // Resolvers
316
+ // ============================================================================
317
+ resolvers: {
318
+ removeTiles: {
319
+ requirement: "REMOVE_TILES",
320
+ resolve: async (req, context) => {
321
+ addLog("RESOLVER removeTiles: START");
322
+ const tilesToRemove = context.facts.table.filter((tile: Tile) =>
323
+ req.tileIds.includes(tile.id),
324
+ );
325
+
326
+ // Multiple fact mutations
327
+ addLog("RESOLVER removeTiles: setting table");
328
+ context.facts.table = context.facts.table.filter(
329
+ (tile: Tile) => !req.tileIds.includes(tile.id),
330
+ );
331
+ addLog("RESOLVER removeTiles: setting removed");
332
+ context.facts.removed = [...context.facts.removed, ...tilesToRemove];
333
+ addLog("RESOLVER removeTiles: clearing selected");
334
+ context.facts.selected = [];
335
+ addLog("RESOLVER removeTiles: incrementing moveCount");
336
+ context.facts.moveCount++;
337
+ addLog("RESOLVER removeTiles: setting message");
338
+ context.facts.message = `Removed ${tilesToRemove[0].value} + ${tilesToRemove[1].value} = 10!`;
339
+ addLog("RESOLVER removeTiles: DONE");
340
+ },
341
+ },
342
+
343
+ refillTable: {
344
+ requirement: "REFILL_TABLE",
345
+ resolve: async (req, context) => {
346
+ addLog(`RESOLVER refillTable: START (count: ${req.count})`);
347
+ const newTiles = context.facts.pool.slice(0, req.count);
348
+ context.facts.pool = context.facts.pool.slice(req.count);
349
+ context.facts.table = [...context.facts.table, ...newTiles];
350
+ addLog(
351
+ `RESOLVER refillTable: DONE (table now: ${context.facts.table.length})`,
352
+ );
353
+ },
354
+ },
355
+
356
+ endGame: {
357
+ requirement: "END_GAME",
358
+ resolve: async (req, context) => {
359
+ addLog(`RESOLVER endGame: ${req.reason}`);
360
+ context.facts.gameOver = true;
361
+ context.facts.message = req.reason;
362
+ },
363
+ },
364
+ },
365
+ });
366
+
367
+ // ============================================================================
368
+ // System
369
+ // ============================================================================
370
+
371
+ export const system = createSystem({
372
+ module: numberMatch,
373
+ plugins: [devtoolsPlugin({ name: "number-match" })],
374
+ history: true,
375
+ trace: true,
376
+ });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@directive-run/knowledge",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Knowledge files, examples, and validation for Directive — the constraint-driven TypeScript runtime.",
5
- "license": "MIT",
5
+ "license": "(MIT OR Apache-2.0)",
6
6
  "author": "Jason Comes",
7
7
  "repository": {
8
8
  "type": "git",
@@ -50,8 +50,8 @@
50
50
  "tsx": "^4.19.2",
51
51
  "typescript": "^5.7.2",
52
52
  "vitest": "^3.0.0",
53
- "@directive-run/core": "0.8.2",
54
- "@directive-run/ai": "0.8.2"
53
+ "@directive-run/core": "0.8.4",
54
+ "@directive-run/ai": "0.8.4"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsx scripts/generate-api-skeleton.ts && tsx scripts/extract-examples.ts && tsup",