@directive-run/claude-plugin 1.16.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/.claude-plugin/plugin.json +6 -0
- package/LICENSE +26 -0
- package/README.md +113 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/skills/README.md +9 -0
- package/skills/building-ai-agents/SKILL.md +414 -0
- package/skills/building-ai-agents/ai-adapters.md +257 -0
- package/skills/building-ai-agents/ai-agents-streaming.md +307 -0
- package/skills/building-ai-agents/ai-communication.md +304 -0
- package/skills/building-ai-agents/api-skeleton.md +5 -0
- package/skills/building-ai-agents/examples.md +849 -0
- package/skills/building-ai-orchestrators/SKILL.md +401 -0
- package/skills/building-ai-orchestrators/ai-multi-agent.md +375 -0
- package/skills/building-ai-orchestrators/ai-orchestrator.md +466 -0
- package/skills/building-ai-orchestrators/ai-tasks.md +268 -0
- package/skills/building-ai-orchestrators/api-skeleton.md +5 -0
- package/skills/building-ai-orchestrators/examples.md +1609 -0
- package/skills/building-directive-systems/SKILL.md +499 -0
- package/skills/building-directive-systems/api-skeleton.md +5 -0
- package/skills/building-directive-systems/examples.md +4149 -0
- package/skills/building-directive-systems/multi-module.md +327 -0
- package/skills/building-directive-systems/plugins.md +361 -0
- package/skills/building-directive-systems/react-adapter.md +376 -0
- package/skills/building-directive-systems/system-api.md +463 -0
- package/skills/getting-started-with-directive/SKILL.md +198 -0
- package/skills/getting-started-with-directive/api-skeleton.md +5 -0
- package/skills/getting-started-with-directive/core-patterns.md +240 -0
- package/skills/getting-started-with-directive/examples.md +87 -0
- package/skills/getting-started-with-directive/sitemap.md +202 -0
- package/skills/hardening-ai-systems/SKILL.md +377 -0
- package/skills/hardening-ai-systems/ai-budget-resilience.md +408 -0
- package/skills/hardening-ai-systems/ai-guardrails-memory.md +370 -0
- package/skills/hardening-ai-systems/ai-security.md +400 -0
- package/skills/hardening-ai-systems/api-skeleton.md +5 -0
- package/skills/hardening-ai-systems/examples.md +628 -0
- package/skills/migrating-to-directive/SKILL.md +249 -0
- package/skills/migrating-to-directive/anti-patterns.md +382 -0
- package/skills/migrating-to-directive/api-skeleton.md +5 -0
- package/skills/migrating-to-directive/core-patterns.md +240 -0
- package/skills/migrating-to-directive/examples.md +511 -0
- package/skills/migrating-to-directive/schema-types.md +273 -0
- package/skills/reviewing-directive-code/SKILL.md +232 -0
- package/skills/reviewing-directive-code/anti-patterns.md +382 -0
- package/skills/reviewing-directive-code/api-skeleton.md +5 -0
- package/skills/reviewing-directive-code/core-patterns.md +240 -0
- package/skills/reviewing-directive-code/examples.md +463 -0
- package/skills/reviewing-directive-code/naming.md +321 -0
- package/skills/scaffolding-directive-modules/SKILL.md +218 -0
- package/skills/scaffolding-directive-modules/api-skeleton.md +5 -0
- package/skills/scaffolding-directive-modules/core-patterns.md +240 -0
- package/skills/scaffolding-directive-modules/examples.md +1386 -0
- package/skills/scaffolding-directive-modules/naming.md +321 -0
- package/skills/scaffolding-directive-modules/schema-types.md +273 -0
- package/skills/testing-ai-systems/SKILL.md +398 -0
- package/skills/testing-ai-systems/ai-debug-observability.md +329 -0
- package/skills/testing-ai-systems/ai-mcp-rag.md +293 -0
- package/skills/testing-ai-systems/ai-testing-evals.md +485 -0
- package/skills/testing-ai-systems/api-skeleton.md +5 -0
- package/skills/testing-ai-systems/examples.md +1261 -0
- package/skills/testing-directive-code/SKILL.md +479 -0
- package/skills/testing-directive-code/api-skeleton.md +5 -0
- package/skills/testing-directive-code/examples.md +156 -0
- package/skills/testing-directive-code/history.md +351 -0
- package/skills/testing-directive-code/testing.md +412 -0
- package/skills/writing-directive-constraints/SKILL.md +486 -0
- package/skills/writing-directive-constraints/api-skeleton.md +5 -0
- package/skills/writing-directive-constraints/constraints.md +274 -0
- package/skills/writing-directive-constraints/error-boundaries.md +330 -0
- package/skills/writing-directive-constraints/examples.md +1361 -0
- package/skills/writing-directive-constraints/resolvers.md +367 -0
- package/skills/writing-directive-modules/SKILL.md +342 -0
- package/skills/writing-directive-modules/anti-patterns.md +382 -0
- package/skills/writing-directive-modules/api-skeleton.md +5 -0
- package/skills/writing-directive-modules/core-patterns.md +240 -0
- package/skills/writing-directive-modules/examples.md +1639 -0
- package/skills/writing-directive-modules/naming.md +321 -0
- package/skills/writing-directive-modules/schema-types.md +273 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Resolvers
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/core` — resolver definition: retry policies, batching, cancellation, custom dedup keys, `owns` binding.
|
|
4
|
+
|
|
5
|
+
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`.
|
|
6
|
+
|
|
7
|
+
## Decision Tree: "How should this resolver work?"
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Is the work async (API calls, timers)?
|
|
11
|
+
├── Yes → Use resolve: async (req, context) => { ... }
|
|
12
|
+
│
|
|
13
|
+
│ Does it fail often?
|
|
14
|
+
│ ├── Yes → Add retry: { attempts: 3, backoff: "exponential" }
|
|
15
|
+
│ └── No → No retry needed
|
|
16
|
+
│
|
|
17
|
+
│ Are there many similar requirements at once?
|
|
18
|
+
│ ├── Yes → Add batch config to group them
|
|
19
|
+
│ └── No → Single resolve is fine
|
|
20
|
+
│
|
|
21
|
+
└── No → Reconsider – maybe this is an event handler or derivation
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Basic Resolver
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
resolvers: {
|
|
28
|
+
fetchUser: {
|
|
29
|
+
// Which requirement type this resolver handles
|
|
30
|
+
requirement: "FETCH_USER",
|
|
31
|
+
|
|
32
|
+
// Async function – req is the requirement, context has facts + signal
|
|
33
|
+
resolve: async (req, context) => {
|
|
34
|
+
const res = await fetch(`/api/users/${req.userId}`);
|
|
35
|
+
const user = await res.json();
|
|
36
|
+
|
|
37
|
+
// Mutate facts to store results – resolvers return void
|
|
38
|
+
context.facts.user = user;
|
|
39
|
+
context.facts.phase = "loaded";
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Resolver Context
|
|
46
|
+
|
|
47
|
+
The `context` object provides:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
resolve: async (req, context) => {
|
|
51
|
+
// context.facts – mutable proxy to the module's facts
|
|
52
|
+
context.facts.status = "loading";
|
|
53
|
+
|
|
54
|
+
// context.signal – AbortSignal, cancelled when system stops or requirement removed
|
|
55
|
+
const res = await fetch("/api/data", { signal: context.signal });
|
|
56
|
+
|
|
57
|
+
// context.snapshot() – read-only snapshot for before/after comparisons
|
|
58
|
+
const before = context.snapshot();
|
|
59
|
+
context.facts.count += 1;
|
|
60
|
+
const after = context.snapshot();
|
|
61
|
+
console.log(`Count: ${before.count} -> ${after.count}`);
|
|
62
|
+
},
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Custom Deduplication Keys
|
|
66
|
+
|
|
67
|
+
By default, requirements are deduped by their full content. Use `key` for custom deduplication:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
resolvers: {
|
|
71
|
+
fetchUser: {
|
|
72
|
+
requirement: "FETCH_USER",
|
|
73
|
+
|
|
74
|
+
// Custom key – only one inflight resolver per userId
|
|
75
|
+
key: (req) => `fetch-user-${req.userId}`,
|
|
76
|
+
|
|
77
|
+
resolve: async (req, context) => {
|
|
78
|
+
const user = await fetchUser(req.userId);
|
|
79
|
+
context.facts.user = user;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
## Retry Policies
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
resolvers: {
|
|
91
|
+
fetchData: {
|
|
92
|
+
requirement: "FETCH_DATA",
|
|
93
|
+
|
|
94
|
+
retry: {
|
|
95
|
+
// Maximum number of attempts (including the first)
|
|
96
|
+
attempts: 3,
|
|
97
|
+
|
|
98
|
+
// Backoff strategy between retries
|
|
99
|
+
backoff: "exponential", // "none" | "linear" | "exponential"
|
|
100
|
+
|
|
101
|
+
// Initial delay in ms (default: 100)
|
|
102
|
+
initialDelay: 200,
|
|
103
|
+
|
|
104
|
+
// Maximum delay in ms (caps exponential growth)
|
|
105
|
+
maxDelay: 5000,
|
|
106
|
+
|
|
107
|
+
// Optional: only retry certain errors
|
|
108
|
+
shouldRetry: (error, attempt) => {
|
|
109
|
+
// Don't retry 4xx client errors
|
|
110
|
+
if (error.message.includes("404")) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
resolve: async (req, context) => {
|
|
119
|
+
const res = await fetch("/api/data");
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new Error(`HTTP ${res.status}`);
|
|
122
|
+
}
|
|
123
|
+
context.facts.data = await res.json();
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Backoff Strategies
|
|
130
|
+
|
|
131
|
+
| Strategy | Delay Pattern |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `"none"` | No delay between retries |
|
|
134
|
+
| `"linear"` | initialDelay, 2x, 3x, ... |
|
|
135
|
+
| `"exponential"` | initialDelay, 2x, 4x, 8x, ... (capped by maxDelay) |
|
|
136
|
+
|
|
137
|
+
## Batch Resolution
|
|
138
|
+
|
|
139
|
+
Group similar requirements and resolve them together. Prevents N+1 problems.
|
|
140
|
+
|
|
141
|
+
### All-or-Nothing Batch
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
resolvers: {
|
|
145
|
+
fetchUsers: {
|
|
146
|
+
requirement: "FETCH_USER",
|
|
147
|
+
|
|
148
|
+
batch: {
|
|
149
|
+
enabled: true,
|
|
150
|
+
windowMs: 50, // Collect requirements for 50ms
|
|
151
|
+
maxSize: 20, // Flush immediately at 20 items
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// resolveBatch receives all collected requirements
|
|
155
|
+
resolveBatch: async (reqs, context) => {
|
|
156
|
+
const ids = reqs.map((req) => req.userId);
|
|
157
|
+
const users = await fetchUsersBatch(ids);
|
|
158
|
+
|
|
159
|
+
// Store results
|
|
160
|
+
context.facts.users = users;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Batch with Per-Item Results
|
|
167
|
+
|
|
168
|
+
For partial success/failure handling:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
resolvers: {
|
|
172
|
+
fetchUsers: {
|
|
173
|
+
requirement: "FETCH_USER",
|
|
174
|
+
|
|
175
|
+
batch: {
|
|
176
|
+
enabled: true,
|
|
177
|
+
windowMs: 50,
|
|
178
|
+
maxSize: 20,
|
|
179
|
+
timeoutMs: 10000, // Per-batch timeout
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Return results array matching input order
|
|
183
|
+
resolveBatchWithResults: async (reqs, context) => {
|
|
184
|
+
const results = await Promise.all(
|
|
185
|
+
reqs.map(async (req) => {
|
|
186
|
+
try {
|
|
187
|
+
const user = await fetchUser(req.userId);
|
|
188
|
+
context.facts.users = {
|
|
189
|
+
...context.facts.users,
|
|
190
|
+
[req.userId]: user,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return { success: true };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return { success: false, error: error as Error };
|
|
196
|
+
}
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return results;
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Failed items from `resolveBatchWithResults` can be individually retried if a retry policy is configured.
|
|
207
|
+
|
|
208
|
+
## Timeout
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
resolvers: {
|
|
212
|
+
fetchData: {
|
|
213
|
+
requirement: "FETCH_DATA",
|
|
214
|
+
timeout: 10000, // Abort after 10 seconds
|
|
215
|
+
|
|
216
|
+
resolve: async (req, context) => {
|
|
217
|
+
// context.signal is automatically aborted on timeout
|
|
218
|
+
const res = await fetch("/api/data", { signal: context.signal });
|
|
219
|
+
context.facts.data = await res.json();
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Waiting for Resolution
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const system = createSystem({ module: myModule });
|
|
229
|
+
system.start();
|
|
230
|
+
|
|
231
|
+
// Wait for all resolvers to complete
|
|
232
|
+
await system.settle();
|
|
233
|
+
|
|
234
|
+
// Wait with timeout
|
|
235
|
+
await system.settle(5000); // Throws if not settled in 5s
|
|
236
|
+
|
|
237
|
+
// Check settlement state
|
|
238
|
+
system.isSettled; // boolean
|
|
239
|
+
|
|
240
|
+
// Subscribe to settlement changes
|
|
241
|
+
const unsub = system.onSettledChange(() => {
|
|
242
|
+
console.log("Settlement state:", system.isSettled);
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Inspecting Resolver Status
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const inspection = system.inspect();
|
|
250
|
+
|
|
251
|
+
// All resolver definitions
|
|
252
|
+
inspection.resolverDefs;
|
|
253
|
+
// [{ id: "fetchUser", requirement: "FETCH_USER" }, ...]
|
|
254
|
+
|
|
255
|
+
// Current resolver statuses
|
|
256
|
+
inspection.resolvers;
|
|
257
|
+
// { fetchUser: { state: "success", completedAt: ..., duration: 150 } }
|
|
258
|
+
|
|
259
|
+
// Inflight resolvers
|
|
260
|
+
inspection.inflight;
|
|
261
|
+
// [{ id: "req-1", resolverId: "fetchData", startedAt: 1709000000 }]
|
|
262
|
+
|
|
263
|
+
// Unmet requirements (no resolver matched)
|
|
264
|
+
inspection.unmet;
|
|
265
|
+
|
|
266
|
+
// Explain why a specific requirement exists
|
|
267
|
+
const explanation = system.explain("req-123");
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Common Mistakes
|
|
271
|
+
|
|
272
|
+
### Returning data from resolve
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// WRONG – return value is ignored
|
|
276
|
+
resolve: async (req, context) => {
|
|
277
|
+
const user = await fetchUser(req.userId);
|
|
278
|
+
|
|
279
|
+
return user; // Ignored!
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// CORRECT – mutate context.facts
|
|
283
|
+
resolve: async (req, context) => {
|
|
284
|
+
const user = await fetchUser(req.userId);
|
|
285
|
+
context.facts.user = user;
|
|
286
|
+
},
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Abbreviating context to ctx
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// WRONG
|
|
293
|
+
resolve: async (req, ctx) => { /* ... */ },
|
|
294
|
+
|
|
295
|
+
// CORRECT
|
|
296
|
+
resolve: async (req, context) => { /* ... */ },
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Checking conditions in resolve (constraint's job)
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// WRONG – condition checking belongs in constraint's when()
|
|
303
|
+
resolve: async (req, context) => {
|
|
304
|
+
if (!context.facts.isAuthenticated) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// ...
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// CORRECT – let constraints handle conditions
|
|
311
|
+
// The resolver only runs when a requirement is emitted
|
|
312
|
+
resolve: async (req, context) => {
|
|
313
|
+
const data = await fetch("/api/data");
|
|
314
|
+
context.facts.data = await data.json();
|
|
315
|
+
},
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Forgetting error handling
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// WRONG – unhandled errors with no recovery
|
|
322
|
+
resolvers: {
|
|
323
|
+
fetch: {
|
|
324
|
+
requirement: "FETCH",
|
|
325
|
+
resolve: async (req, context) => {
|
|
326
|
+
const res = await fetch("/api");
|
|
327
|
+
context.facts.data = await res.json();
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// CORRECT – retry policy + error handling
|
|
333
|
+
resolvers: {
|
|
334
|
+
fetch: {
|
|
335
|
+
requirement: "FETCH",
|
|
336
|
+
retry: { attempts: 3, backoff: "exponential" },
|
|
337
|
+
resolve: async (req, context) => {
|
|
338
|
+
const res = await fetch("/api");
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
throw new Error(`HTTP ${res.status}`);
|
|
341
|
+
}
|
|
342
|
+
context.facts.data = await res.json();
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Missing settle() after start()
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// WRONG – reading facts before resolvers finish
|
|
352
|
+
system.start();
|
|
353
|
+
console.log(system.facts.data); // Likely null
|
|
354
|
+
|
|
355
|
+
// CORRECT
|
|
356
|
+
system.start();
|
|
357
|
+
await system.settle();
|
|
358
|
+
console.log(system.facts.data); // Resolved value
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## See also
|
|
362
|
+
|
|
363
|
+
- [`constraints.md`](./constraints.md) — the demand side of the loop; what emits the requirements this resolver fulfills
|
|
364
|
+
- [`core-patterns.md`](./core-patterns.md) — how resolvers fit into the module shape
|
|
365
|
+
- [`error-boundaries.md`](./error-boundaries.md) — recovery strategies when a resolver throws (retry, fallback, circuit breaker)
|
|
366
|
+
- [`testing.md`](./testing.md) — mock resolvers via `mocks.resolvers` and the programmatic `mockResolver(type)` helper
|
|
367
|
+
- [`anti-patterns.md`](./anti-patterns.md) — the resolver-shaped mistakes (returning data from `resolve`, abbreviating `context` to `ctx`)
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-directive-modules
|
|
3
|
+
description: "Create and structure Directive modules with schema definitions, init functions, derivations, effects, events, constraints, and resolvers using the correct t.* type builders and naming conventions. Use when asked to build a Directive module, add facts/derivations/resolvers to an existing module, or scaffold any stateful domain with Directive."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing Directive Modules
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
This skill applies when the project uses `@directive-run/core`. If not found in `package.json`, suggest installing it: `npm install @directive-run/core`.
|
|
11
|
+
|
|
12
|
+
## When Claude Should Use This Skill
|
|
13
|
+
|
|
14
|
+
**Auto-invoke when the user:**
|
|
15
|
+
- Says "create a module", "add a fact", "build with Directive", or "scaffold state"
|
|
16
|
+
- Asks to model a domain (auth, cart, user profile, game state, etc.) in Directive
|
|
17
|
+
- Shows a `createModule()` call and asks to extend it
|
|
18
|
+
- Asks about schema types, derivations, effects, resolvers, or events in Directive context
|
|
19
|
+
|
|
20
|
+
**Do NOT invoke when:**
|
|
21
|
+
- Working on system composition (see `building-directive-systems.md`)
|
|
22
|
+
- Writing tests (see `testing-directive-code.md`)
|
|
23
|
+
- Asking about constraints/resolver interaction patterns only (see `writing-directive-constraints.md`)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Module Shape – Canonical Form
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createModule, t } from "@directive-run/core";
|
|
31
|
+
|
|
32
|
+
const myModule = createModule("name", {
|
|
33
|
+
schema: {
|
|
34
|
+
facts: {
|
|
35
|
+
phase: t.string<"idle" | "loading" | "done">(),
|
|
36
|
+
count: t.number(),
|
|
37
|
+
user: t.object<{ id: string; name: string } | null>(),
|
|
38
|
+
},
|
|
39
|
+
derivations: { // Optional – declare types here, define in derive
|
|
40
|
+
isLoading: t.boolean(),
|
|
41
|
+
displayName: t.string(),
|
|
42
|
+
},
|
|
43
|
+
events: { // Optional – declare event payloads here
|
|
44
|
+
increment: {},
|
|
45
|
+
setUser: { user: t.object<{ id: string; name: string }>() },
|
|
46
|
+
},
|
|
47
|
+
requirements: { // Optional – declare requirement payloads here
|
|
48
|
+
FETCH_USER: { userId: t.string() },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
init: (facts) => { // Synchronous. Sets all facts to initial values.
|
|
53
|
+
facts.phase = "idle";
|
|
54
|
+
facts.count = 0;
|
|
55
|
+
facts.user = null;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
derive: { // Auto-tracked – no manual deps needed
|
|
59
|
+
isLoading: (facts) => facts.phase === "loading",
|
|
60
|
+
displayName: (facts) => {
|
|
61
|
+
if (!facts.user) {
|
|
62
|
+
return "Guest";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return facts.user.name;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
effects: { // Fire-and-forget side effects
|
|
70
|
+
logPhase: {
|
|
71
|
+
run: (facts, prev) => {
|
|
72
|
+
if (prev?.phase !== facts.phase) {
|
|
73
|
+
console.log(`Phase: ${facts.phase}`);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
events: { // Synchronous fact mutations driven by user actions
|
|
80
|
+
increment: (facts) => {
|
|
81
|
+
facts.count += 1;
|
|
82
|
+
},
|
|
83
|
+
setUser: (facts, payload) => {
|
|
84
|
+
facts.user = payload.user;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
constraints: { // WHEN conditions are true, REQUIRE something
|
|
89
|
+
fetchWhenReady: {
|
|
90
|
+
when: (facts) => facts.phase === "idle" && facts.count > 0,
|
|
91
|
+
require: (facts) => ({ type: "FETCH_USER", userId: "user-1" }),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
resolvers: { // HOW to fulfill requirements
|
|
96
|
+
fetchUser: {
|
|
97
|
+
requirement: "FETCH_USER",
|
|
98
|
+
resolve: async (req, context) => {
|
|
99
|
+
context.facts.phase = "loading";
|
|
100
|
+
const res = await fetch(`/api/users/${req.userId}`);
|
|
101
|
+
context.facts.user = await res.json();
|
|
102
|
+
context.facts.phase = "done";
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Decision Tree: Where Does This Logic Go?
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
What does this code do?
|
|
115
|
+
├── Store a value that changes over time
|
|
116
|
+
│ └── schema.facts + init()
|
|
117
|
+
├── Compute a value from other facts (synchronous, cached)
|
|
118
|
+
│ └── schema.derivations + derive
|
|
119
|
+
├── React to fact changes (side effect, fire-and-forget)
|
|
120
|
+
│ └── effects
|
|
121
|
+
├── Respond to a user action
|
|
122
|
+
│ └── schema.events + events
|
|
123
|
+
├── Declare "when X is true, the system needs Y"
|
|
124
|
+
│ └── constraints
|
|
125
|
+
└── Fulfill a requirement with async work
|
|
126
|
+
└── resolvers
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Schema Type Builders
|
|
132
|
+
|
|
133
|
+
### Which builder to use
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
Value type?
|
|
137
|
+
├── string (possibly with union) → t.string() or t.string<"a" | "b">()
|
|
138
|
+
├── number (possibly with bounds) → t.number() or t.number().min(0).max(100)
|
|
139
|
+
├── boolean → t.boolean()
|
|
140
|
+
├── object / record → t.object<Shape>()
|
|
141
|
+
├── array → t.array<ItemType>()
|
|
142
|
+
├── string literal union → t.enum("a", "b", "c")
|
|
143
|
+
├── exact value → t.literal(42)
|
|
144
|
+
├── T | null → t.nullable(t.string()) or t.object<T | null>()
|
|
145
|
+
├── T | undefined → t.optional(t.string())
|
|
146
|
+
└── Map / Set / Date → t.object<Map<K,V>>() / t.object<Set<T>>() / t.object<Date>()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Chainable modifiers (available on all types)
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
t.string().default("light") // Default if init doesn't set it
|
|
153
|
+
t.string().validate((v) => v.includes("@")) // Dev-mode validation
|
|
154
|
+
t.string().transform((v) => v.trim()) // Runs on every set
|
|
155
|
+
t.string().brand<"UserId">() // Nominal typing
|
|
156
|
+
t.number().min(0).max(100).describe("Player score")
|
|
157
|
+
t.array<string>().nonEmpty().maxLength(10)
|
|
158
|
+
t.object<{ url: string }>().nonNull()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Type assertion alternative (for simple schemas)
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
schema: {
|
|
165
|
+
facts: {} as { count: number; name: string },
|
|
166
|
+
derivations: {} as { doubled: number },
|
|
167
|
+
},
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Gives full TypeScript inference, skips runtime validation.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Key Patterns
|
|
175
|
+
|
|
176
|
+
### Derivation composition (derivation depending on another)
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
derive: {
|
|
180
|
+
isRed: (facts) => facts.phase === "red",
|
|
181
|
+
status: (facts, derived) => ({ // Second param accesses other derivations
|
|
182
|
+
phase: facts.phase,
|
|
183
|
+
isRed: derived.isRed,
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Dynamic requirement from facts
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
constraints: {
|
|
192
|
+
fetchUser: {
|
|
193
|
+
when: (facts) => facts.isAuthenticated && !facts.profile,
|
|
194
|
+
require: (facts) => ({ type: "FETCH_USER", userId: facts.userId }),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Returning null from require to suppress
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
require: (facts) => {
|
|
203
|
+
if (!facts.userId) {
|
|
204
|
+
return null; // No requirement emitted
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { type: "FETCH_USER", userId: facts.userId };
|
|
208
|
+
},
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Array/object mutation – always replace, never mutate in place
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// CORRECT
|
|
215
|
+
facts.items = [...facts.items, newItem];
|
|
216
|
+
facts.config = { ...facts.config, theme: "dark" };
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Critical Anti-Patterns
|
|
222
|
+
|
|
223
|
+
### 1. Missing facts wrapper in schema
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// WRONG
|
|
227
|
+
schema: { phase: t.string(), count: t.number() }
|
|
228
|
+
|
|
229
|
+
// CORRECT
|
|
230
|
+
schema: { facts: { phase: t.string(), count: t.number() } }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 2. Nonexistent type builders
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// WRONG – these do not exist
|
|
237
|
+
t.map<string, User>()
|
|
238
|
+
t.set<string>()
|
|
239
|
+
t.date()
|
|
240
|
+
t.record<string, number>()
|
|
241
|
+
t.any()
|
|
242
|
+
|
|
243
|
+
// CORRECT
|
|
244
|
+
t.object<Map<string, User>>()
|
|
245
|
+
t.object<Set<string>>()
|
|
246
|
+
t.object<Date>()
|
|
247
|
+
t.object<Record<string, number>>()
|
|
248
|
+
t.object<unknown>()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 3. Async init
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// WRONG – init is synchronous
|
|
255
|
+
init: async (facts) => { facts.config = await fetch("/api/config").then(r => r.json()); }
|
|
256
|
+
|
|
257
|
+
// CORRECT – init sets defaults; async work goes in constraints/resolvers
|
|
258
|
+
init: (facts) => { facts.config = null; }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 4. Resolver parameter naming
|
|
262
|
+
Always use `(req, context)` – never `(req, ctx)` or `(request, context)`.
|
|
263
|
+
|
|
264
|
+
### 5. Resolver returning data
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// WRONG – return value is ignored
|
|
268
|
+
resolve: async (req, context) => { return await fetchUser(req.userId); }
|
|
269
|
+
|
|
270
|
+
// CORRECT – mutate context.facts
|
|
271
|
+
resolve: async (req, context) => { context.facts.user = await fetchUser(req.userId); }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 6. Unnecessary type casting
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// WRONG
|
|
278
|
+
const profile = system.facts.profile as UserProfile;
|
|
279
|
+
|
|
280
|
+
// CORRECT – schema provides the type
|
|
281
|
+
const profile = system.facts.profile;
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 7. Passthrough derivations
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// WRONG – derivation just returns a fact unchanged
|
|
288
|
+
derive: { count: (facts) => facts.count }
|
|
289
|
+
|
|
290
|
+
// CORRECT – read the fact directly: system.facts.count
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 8. String require instead of object
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// WRONG
|
|
297
|
+
require: "FETCH_DATA"
|
|
298
|
+
|
|
299
|
+
// CORRECT
|
|
300
|
+
require: { type: "FETCH_DATA" }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 9. Accessing facts.self in single-module context
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// facts.self.* is only for multi-module cross-module deps contexts
|
|
307
|
+
// In single-module resolvers: use context.facts.fieldName directly
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 10. Deep imports
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// WRONG
|
|
314
|
+
import { createModule } from "@directive-run/core/module";
|
|
315
|
+
|
|
316
|
+
// CORRECT
|
|
317
|
+
import { createModule } from "@directive-run/core";
|
|
318
|
+
// Exception: plugins use @directive-run/core/plugins
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Terminology Quick Reference
|
|
324
|
+
|
|
325
|
+
| Use | Never Use |
|
|
326
|
+
|-----|-----------|
|
|
327
|
+
| `facts` | state, store, atoms |
|
|
328
|
+
| `derivations` / `derive` | computed, selectors, getters, memos |
|
|
329
|
+
| `constraints` | rules, conditions, triggers |
|
|
330
|
+
| `resolvers` | handlers, actions, reducers |
|
|
331
|
+
| `requirements` | requests, commands |
|
|
332
|
+
| `effects` | watchers, subscriptions, reactions |
|
|
333
|
+
| `(req, context)` | `(req, ctx)` or `(request, context)` |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Reference Files
|
|
338
|
+
|
|
339
|
+
- `core-patterns.md` – decision trees, full module example, system creation patterns
|
|
340
|
+
- `schema-types.md` – complete t.* builder reference, chainable methods, nonexistent types
|
|
341
|
+
- `naming.md` – req/context rules, return style, blank-line conventions, multi-module naming
|
|
342
|
+
- `anti-patterns.md` – 20 ranked anti-patterns with correct/wrong examples
|