@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- 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.
|