@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,357 @@
1
+ # Resolvers
2
+
3
+ Resolvers fulfill requirements emitted by constraints. They are the supply side of the constraint-resolver pattern. Resolvers handle async work and mutate state through `context.facts`.
4
+
5
+ ## Decision Tree: "How should this resolver work?"
6
+
7
+ ```
8
+ Is the work async (API calls, timers)?
9
+ ├── Yes → Use resolve: async (req, context) => { ... }
10
+
11
+ │ Does it fail often?
12
+ │ ├── Yes → Add retry: { attempts: 3, backoff: "exponential" }
13
+ │ └── No → No retry needed
14
+
15
+ │ Are there many similar requirements at once?
16
+ │ ├── Yes → Add batch config to group them
17
+ │ └── No → Single resolve is fine
18
+
19
+ └── No → Reconsider — maybe this is an event handler or derivation
20
+ ```
21
+
22
+ ## Basic Resolver
23
+
24
+ ```typescript
25
+ resolvers: {
26
+ fetchUser: {
27
+ // Which requirement type this resolver handles
28
+ requirement: "FETCH_USER",
29
+
30
+ // Async function — req is the requirement, context has facts + signal
31
+ resolve: async (req, context) => {
32
+ const res = await fetch(`/api/users/${req.userId}`);
33
+ const user = await res.json();
34
+
35
+ // Mutate facts to store results — resolvers return void
36
+ context.facts.user = user;
37
+ context.facts.phase = "loaded";
38
+ },
39
+ },
40
+ },
41
+ ```
42
+
43
+ ## Resolver Context
44
+
45
+ The `context` object provides:
46
+
47
+ ```typescript
48
+ resolve: async (req, context) => {
49
+ // context.facts — mutable proxy to the module's facts
50
+ context.facts.status = "loading";
51
+
52
+ // context.signal — AbortSignal, cancelled when system stops or requirement removed
53
+ const res = await fetch("/api/data", { signal: context.signal });
54
+
55
+ // context.snapshot() — read-only snapshot for before/after comparisons
56
+ const before = context.snapshot();
57
+ context.facts.count += 1;
58
+ const after = context.snapshot();
59
+ console.log(`Count: ${before.count} -> ${after.count}`);
60
+ },
61
+ ```
62
+
63
+ ## Custom Deduplication Keys
64
+
65
+ By default, requirements are deduped by their full content. Use `key` for custom deduplication:
66
+
67
+ ```typescript
68
+ resolvers: {
69
+ fetchUser: {
70
+ requirement: "FETCH_USER",
71
+
72
+ // Custom key — only one inflight resolver per userId
73
+ key: (req) => `fetch-user-${req.userId}`,
74
+
75
+ resolve: async (req, context) => {
76
+ const user = await fetchUser(req.userId);
77
+ context.facts.user = user;
78
+ },
79
+ },
80
+ },
81
+ ```
82
+
83
+ Without `key`, two requirements `{ type: "FETCH_USER", userId: "1" }` are deduped because they are structurally identical. With `key`, you control exactly what counts as a duplicate.
84
+
85
+ ## Retry Policies
86
+
87
+ ```typescript
88
+ resolvers: {
89
+ fetchData: {
90
+ requirement: "FETCH_DATA",
91
+
92
+ retry: {
93
+ // Maximum number of attempts (including the first)
94
+ attempts: 3,
95
+
96
+ // Backoff strategy between retries
97
+ backoff: "exponential", // "none" | "linear" | "exponential"
98
+
99
+ // Initial delay in ms (default: 100)
100
+ initialDelay: 200,
101
+
102
+ // Maximum delay in ms (caps exponential growth)
103
+ maxDelay: 5000,
104
+
105
+ // Optional: only retry certain errors
106
+ shouldRetry: (error, attempt) => {
107
+ // Don't retry 4xx client errors
108
+ if (error.message.includes("404")) {
109
+ return false;
110
+ }
111
+
112
+ return true;
113
+ },
114
+ },
115
+
116
+ resolve: async (req, context) => {
117
+ const res = await fetch("/api/data");
118
+ if (!res.ok) {
119
+ throw new Error(`HTTP ${res.status}`);
120
+ }
121
+ context.facts.data = await res.json();
122
+ },
123
+ },
124
+ },
125
+ ```
126
+
127
+ ### Backoff Strategies
128
+
129
+ | Strategy | Delay Pattern |
130
+ |---|---|
131
+ | `"none"` | No delay between retries |
132
+ | `"linear"` | initialDelay, 2x, 3x, ... |
133
+ | `"exponential"` | initialDelay, 2x, 4x, 8x, ... (capped by maxDelay) |
134
+
135
+ ## Batch Resolution
136
+
137
+ Group similar requirements and resolve them together. Prevents N+1 problems.
138
+
139
+ ### All-or-Nothing Batch
140
+
141
+ ```typescript
142
+ resolvers: {
143
+ fetchUsers: {
144
+ requirement: "FETCH_USER",
145
+
146
+ batch: {
147
+ enabled: true,
148
+ windowMs: 50, // Collect requirements for 50ms
149
+ maxSize: 20, // Flush immediately at 20 items
150
+ },
151
+
152
+ // resolveBatch receives all collected requirements
153
+ resolveBatch: async (reqs, context) => {
154
+ const ids = reqs.map((req) => req.userId);
155
+ const users = await fetchUsersBatch(ids);
156
+
157
+ // Store results
158
+ context.facts.users = users;
159
+ },
160
+ },
161
+ },
162
+ ```
163
+
164
+ ### Batch with Per-Item Results
165
+
166
+ For partial success/failure handling:
167
+
168
+ ```typescript
169
+ resolvers: {
170
+ fetchUsers: {
171
+ requirement: "FETCH_USER",
172
+
173
+ batch: {
174
+ enabled: true,
175
+ windowMs: 50,
176
+ maxSize: 20,
177
+ timeoutMs: 10000, // Per-batch timeout
178
+ },
179
+
180
+ // Return results array matching input order
181
+ resolveBatchWithResults: async (reqs, context) => {
182
+ const results = await Promise.all(
183
+ reqs.map(async (req) => {
184
+ try {
185
+ const user = await fetchUser(req.userId);
186
+ context.facts.users = {
187
+ ...context.facts.users,
188
+ [req.userId]: user,
189
+ };
190
+
191
+ return { success: true };
192
+ } catch (error) {
193
+ return { success: false, error: error as Error };
194
+ }
195
+ }),
196
+ );
197
+
198
+ return results;
199
+ },
200
+ },
201
+ },
202
+ ```
203
+
204
+ Failed items from `resolveBatchWithResults` can be individually retried if a retry policy is configured.
205
+
206
+ ## Timeout
207
+
208
+ ```typescript
209
+ resolvers: {
210
+ fetchData: {
211
+ requirement: "FETCH_DATA",
212
+ timeout: 10000, // Abort after 10 seconds
213
+
214
+ resolve: async (req, context) => {
215
+ // context.signal is automatically aborted on timeout
216
+ const res = await fetch("/api/data", { signal: context.signal });
217
+ context.facts.data = await res.json();
218
+ },
219
+ },
220
+ },
221
+ ```
222
+
223
+ ## Waiting for Resolution
224
+
225
+ ```typescript
226
+ const system = createSystem({ module: myModule });
227
+ system.start();
228
+
229
+ // Wait for all resolvers to complete
230
+ await system.settle();
231
+
232
+ // Wait with timeout
233
+ await system.settle(5000); // Throws if not settled in 5s
234
+
235
+ // Check settlement state
236
+ system.isSettled; // boolean
237
+
238
+ // Subscribe to settlement changes
239
+ const unsub = system.onSettledChange(() => {
240
+ console.log("Settlement state:", system.isSettled);
241
+ });
242
+ ```
243
+
244
+ ## Inspecting Resolver Status
245
+
246
+ ```typescript
247
+ const inspection = system.inspect();
248
+
249
+ // All resolver definitions
250
+ inspection.resolverDefs;
251
+ // [{ id: "fetchUser", requirement: "FETCH_USER" }, ...]
252
+
253
+ // Current resolver statuses
254
+ inspection.resolvers;
255
+ // { fetchUser: { state: "success", completedAt: ..., duration: 150 } }
256
+
257
+ // Inflight resolvers
258
+ inspection.inflight;
259
+ // [{ id: "req-1", resolverId: "fetchData", startedAt: 1709000000 }]
260
+
261
+ // Unmet requirements (no resolver matched)
262
+ inspection.unmet;
263
+
264
+ // Explain why a specific requirement exists
265
+ const explanation = system.explain("req-123");
266
+ ```
267
+
268
+ ## Common Mistakes
269
+
270
+ ### Returning data from resolve
271
+
272
+ ```typescript
273
+ // WRONG — return value is ignored
274
+ resolve: async (req, context) => {
275
+ const user = await fetchUser(req.userId);
276
+
277
+ return user; // Ignored!
278
+ },
279
+
280
+ // CORRECT — mutate context.facts
281
+ resolve: async (req, context) => {
282
+ const user = await fetchUser(req.userId);
283
+ context.facts.user = user;
284
+ },
285
+ ```
286
+
287
+ ### Abbreviating context to ctx
288
+
289
+ ```typescript
290
+ // WRONG
291
+ resolve: async (req, ctx) => { /* ... */ },
292
+
293
+ // CORRECT
294
+ resolve: async (req, context) => { /* ... */ },
295
+ ```
296
+
297
+ ### Checking conditions in resolve (constraint's job)
298
+
299
+ ```typescript
300
+ // WRONG — condition checking belongs in constraint's when()
301
+ resolve: async (req, context) => {
302
+ if (!context.facts.isAuthenticated) {
303
+ return;
304
+ }
305
+ // ...
306
+ },
307
+
308
+ // CORRECT — let constraints handle conditions
309
+ // The resolver only runs when a requirement is emitted
310
+ resolve: async (req, context) => {
311
+ const data = await fetch("/api/data");
312
+ context.facts.data = await data.json();
313
+ },
314
+ ```
315
+
316
+ ### Forgetting error handling
317
+
318
+ ```typescript
319
+ // WRONG — unhandled errors with no recovery
320
+ resolvers: {
321
+ fetch: {
322
+ requirement: "FETCH",
323
+ resolve: async (req, context) => {
324
+ const res = await fetch("/api");
325
+ context.facts.data = await res.json();
326
+ },
327
+ },
328
+ },
329
+
330
+ // CORRECT — retry policy + error handling
331
+ resolvers: {
332
+ fetch: {
333
+ requirement: "FETCH",
334
+ retry: { attempts: 3, backoff: "exponential" },
335
+ resolve: async (req, context) => {
336
+ const res = await fetch("/api");
337
+ if (!res.ok) {
338
+ throw new Error(`HTTP ${res.status}`);
339
+ }
340
+ context.facts.data = await res.json();
341
+ },
342
+ },
343
+ },
344
+ ```
345
+
346
+ ### Missing settle() after start()
347
+
348
+ ```typescript
349
+ // WRONG — reading facts before resolvers finish
350
+ system.start();
351
+ console.log(system.facts.data); // Likely null
352
+
353
+ // CORRECT
354
+ system.start();
355
+ await system.settle();
356
+ console.log(system.facts.data); // Resolved value
357
+ ```
@@ -0,0 +1,262 @@
1
+ # Schema Types
2
+
3
+ The `t.*()` builders define fact types in module schemas. They provide runtime validation in dev mode and full TypeScript inference.
4
+
5
+ ## Decision Tree: "Which type builder do I use?"
6
+
7
+ ```
8
+ What kind of value?
9
+ ├── String → t.string() or t.string<"a" | "b">() for literal unions
10
+ ├── Number → t.number() with optional .min() / .max()
11
+ ├── Boolean → t.boolean()
12
+ ├── Array → t.array<ItemType>() with optional .of() / .nonEmpty()
13
+ ├── Object/Record → t.object<Shape>()
14
+ ├── Fixed set of string values → t.enum("a", "b", "c")
15
+ ├── Exact value → t.literal(42) or t.literal("active")
16
+ ├── Nullable → t.nullable(t.string())
17
+ ├── Optional → t.optional(t.number())
18
+ ├── Union → t.union(t.string(), t.number())
19
+ ├── Map/Set → t.object<Map<K,V>>() or t.object<Set<T>>()
20
+ ├── Date → t.object<Date>() or t.number() for timestamps
21
+ └── Unknown/any → t.object<unknown>()
22
+ ```
23
+
24
+ ## Primitive Types
25
+
26
+ ```typescript
27
+ import { createModule, t } from "@directive-run/core";
28
+
29
+ const myModule = createModule("example", {
30
+ schema: {
31
+ facts: {
32
+ // Basic string
33
+ name: t.string(),
34
+
35
+ // String literal union — full type safety
36
+ phase: t.string<"idle" | "loading" | "done">(),
37
+
38
+ // Number with validation
39
+ count: t.number(),
40
+ age: t.number().min(0).max(150),
41
+
42
+ // Boolean
43
+ isActive: t.boolean(),
44
+ },
45
+ },
46
+ init: (facts) => {
47
+ facts.name = "";
48
+ facts.phase = "idle";
49
+ facts.count = 0;
50
+ facts.age = 25;
51
+ facts.isActive = false;
52
+ },
53
+ });
54
+ ```
55
+
56
+ ## Complex Types
57
+
58
+ ```typescript
59
+ schema: {
60
+ facts: {
61
+ // Object with shape
62
+ user: t.object<{ id: string; name: string; email: string }>(),
63
+
64
+ // Nullable object
65
+ profile: t.object<{ bio: string; avatar: string } | null>(),
66
+
67
+ // Array
68
+ tags: t.array<string>(),
69
+ items: t.array<{ id: string; label: string }>().nonEmpty(),
70
+
71
+ // Record (key-value map)
72
+ scores: t.object<Record<string, number>>(),
73
+
74
+ // Nested complex type
75
+ config: t.object<{
76
+ theme: "light" | "dark";
77
+ notifications: { email: boolean; push: boolean };
78
+ }>(),
79
+ },
80
+ },
81
+ ```
82
+
83
+ ## Enum and Literal
84
+
85
+ ```typescript
86
+ schema: {
87
+ facts: {
88
+ // Enum — string literal union from values
89
+ status: t.enum("pending", "active", "archived"),
90
+ // TypeScript type: "pending" | "active" | "archived"
91
+
92
+ // Literal — exact match
93
+ version: t.literal(2),
94
+ mode: t.literal("strict"),
95
+ enabled: t.literal(true),
96
+ },
97
+ },
98
+ ```
99
+
100
+ ## Nullable and Optional
101
+
102
+ ```typescript
103
+ schema: {
104
+ facts: {
105
+ // Nullable — T | null
106
+ selectedItem: t.nullable(t.string()),
107
+
108
+ // Optional — T | undefined
109
+ nickname: t.optional(t.string()),
110
+
111
+ // Union — combine types
112
+ result: t.union(t.string(), t.number()),
113
+
114
+ // Nullable via object generic (also valid)
115
+ user: t.object<{ id: string } | null>(),
116
+ },
117
+ },
118
+ ```
119
+
120
+ ## Chainable Methods (Available on All Types)
121
+
122
+ ```typescript
123
+ schema: {
124
+ facts: {
125
+ // Default value — used if init doesn't set it
126
+ theme: t.string<"light" | "dark">().default("light"),
127
+
128
+ // Custom validation — runs in dev mode
129
+ email: t.string().validate((val) => val.includes("@")),
130
+
131
+ // Transform on set — runs when fact is mutated
132
+ slug: t.string().transform((val) => val.toLowerCase().replace(/\s+/g, "-")),
133
+
134
+ // Branded type — nominal typing
135
+ userId: t.string().brand<"UserId">(),
136
+
137
+ // Description — for docs and devtools
138
+ retryCount: t.number().describe("Number of failed attempts"),
139
+
140
+ // Refinement — predicate with error message
141
+ port: t.number().refine(
142
+ (n) => n >= 1 && n <= 65535,
143
+ "Port must be between 1 and 65535",
144
+ ),
145
+
146
+ // Chaining — combine multiple modifiers
147
+ score: t.number()
148
+ .min(0)
149
+ .max(100)
150
+ .default(0)
151
+ .describe("Player score"),
152
+ },
153
+ },
154
+ ```
155
+
156
+ ## Array-Specific Methods
157
+
158
+ ```typescript
159
+ schema: {
160
+ facts: {
161
+ // Basic array
162
+ items: t.array<string>(),
163
+
164
+ // Non-empty validation
165
+ requiredItems: t.array<string>().nonEmpty(),
166
+
167
+ // Length constraints
168
+ topFive: t.array<string>().maxLength(5),
169
+ atLeastThree: t.array<number>().minLength(3),
170
+
171
+ // Combined
172
+ tags: t.array<string>().nonEmpty().maxLength(10),
173
+ },
174
+ },
175
+ ```
176
+
177
+ ## Object-Specific Methods
178
+
179
+ ```typescript
180
+ schema: {
181
+ facts: {
182
+ // Non-null assertion
183
+ config: t.object<{ url: string }>().nonNull(),
184
+
185
+ // Required keys validation
186
+ settings: t.object<Record<string, unknown>>().hasKeys("apiUrl", "timeout"),
187
+ },
188
+ },
189
+ ```
190
+
191
+ ## Types That DO NOT Exist
192
+
193
+ These are common AI hallucinations. Do not use them.
194
+
195
+ ```typescript
196
+ // WRONG — t.map() does not exist
197
+ items: t.map<string, number>(),
198
+ // CORRECT
199
+ items: t.object<Map<string, number>>(),
200
+
201
+ // WRONG — t.set() does not exist
202
+ tags: t.set<string>(),
203
+ // CORRECT
204
+ tags: t.object<Set<string>>(),
205
+
206
+ // WRONG — t.date() does not exist
207
+ createdAt: t.date(),
208
+ // CORRECT
209
+ createdAt: t.object<Date>(),
210
+ // OR use timestamps
211
+ createdAt: t.number(), // Unix ms
212
+
213
+ // WRONG — t.tuple() does not exist
214
+ coords: t.tuple<[number, number]>(),
215
+ // CORRECT
216
+ coords: t.array<[number, number]>(),
217
+
218
+ // WRONG — t.record() does not exist
219
+ scores: t.record<string, number>(),
220
+ // CORRECT
221
+ scores: t.object<Record<string, number>>(),
222
+
223
+ // WRONG — t.promise() does not exist (facts are synchronous)
224
+ result: t.promise<string>(),
225
+
226
+ // WRONG — t.any() does not exist
227
+ data: t.any(),
228
+ // CORRECT
229
+ data: t.object<unknown>(),
230
+
231
+ // WRONG — t.void() does not exist (not a fact type)
232
+ // WRONG — t.function() does not exist (functions are not serializable)
233
+ ```
234
+
235
+ ## Type Assertion Alternative
236
+
237
+ For simple modules, you can skip `t.*()` and use TypeScript type assertions:
238
+
239
+ ```typescript
240
+ const myModule = createModule("simple", {
241
+ schema: {
242
+ facts: {} as {
243
+ count: number;
244
+ name: string;
245
+ items: string[];
246
+ },
247
+ derivations: {} as {
248
+ doubled: number;
249
+ },
250
+ },
251
+ init: (facts) => {
252
+ facts.count = 0;
253
+ facts.name = "";
254
+ facts.items = [];
255
+ },
256
+ derive: {
257
+ doubled: (facts) => facts.count * 2,
258
+ },
259
+ });
260
+ ```
261
+
262
+ This gives full TypeScript inference but skips runtime validation. Use `t.*()` when you want dev-mode validation, transforms, or self-documenting schemas.