@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,486 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-directive-constraints
|
|
3
|
+
description: "Write Directive constraints, resolvers, and error boundaries: when/require patterns, static vs dynamic requirements, async constraints with explicit deps, priority and ordering, resolver retry policies, deduplication keys, batch resolution, and system error boundaries. Use when asked to add constraint-resolver pairs, handle errors/retries in resolvers, or configure recovery strategies."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing Directive Constraints
|
|
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 "add a constraint", "write a resolver", "handle errors in the resolver", or "add retry"
|
|
16
|
+
- Asks "when X happens, do Y" where Y is async work
|
|
17
|
+
- Shows existing constraints/resolvers and asks to extend or fix them
|
|
18
|
+
- Asks about error handling, recovery strategies, circuit breakers, or retry policies
|
|
19
|
+
- Asks about requirement deduplication, batching, or ordering
|
|
20
|
+
|
|
21
|
+
**Do NOT invoke when:**
|
|
22
|
+
- Scaffolding an entire module from scratch (start with `writing-directive-modules.md`)
|
|
23
|
+
- Composing multiple modules (see `building-directive-systems.md`)
|
|
24
|
+
- Writing tests (see `testing-directive-code.md`)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The Core Mental Model
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Constraint declares WHEN something is needed → emits a requirement
|
|
32
|
+
Resolver fulfills WHAT to do about it → mutates facts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Never put condition checks in a resolver. Never do async work in a constraint's `when()`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Decision Tree: Constraint or Something Else?
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Is this "when X is true, the system needs Y"?
|
|
43
|
+
├── Yes, and Y is async/side-effectful → Constraint + Resolver
|
|
44
|
+
├── Yes, but Y is just a derived value → Use derive instead
|
|
45
|
+
├── No, it reacts to a change that happened → Use effect
|
|
46
|
+
└── No, it's user-initiated → Use event handler
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Constraint Anatomy
|
|
52
|
+
|
|
53
|
+
### Static vs dynamic requirements
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
constraints: {
|
|
57
|
+
// Static – same object every time
|
|
58
|
+
loadConfig: {
|
|
59
|
+
when: (facts) => facts.config === null,
|
|
60
|
+
require: { type: "LOAD_CONFIG" },
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Dynamic – function reads facts
|
|
64
|
+
fetchUser: {
|
|
65
|
+
when: (facts) => facts.isAuthenticated && !facts.profile,
|
|
66
|
+
require: (facts) => ({ type: "FETCH_USER", userId: facts.userId }),
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Multiple requirements at once
|
|
70
|
+
loadAll: {
|
|
71
|
+
when: (facts) => facts.phase === "init",
|
|
72
|
+
require: [
|
|
73
|
+
{ type: "LOAD_CONFIG" },
|
|
74
|
+
{ type: "LOAD_USER" },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Suppress conditionally – return null from require
|
|
79
|
+
conditionalFetch: {
|
|
80
|
+
when: (facts) => facts.needsUpdate,
|
|
81
|
+
require: (facts) => {
|
|
82
|
+
if (!facts.userId) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { type: "FETCH_USER", userId: facts.userId };
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Priority – conflict resolution
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
constraints: {
|
|
96
|
+
normalTransition: {
|
|
97
|
+
priority: 50, // Default is 0; higher runs first
|
|
98
|
+
when: (facts) => facts.phase === "red" && facts.elapsed > 30,
|
|
99
|
+
require: { type: "TRANSITION", to: "green" },
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
emergencyOverride: {
|
|
103
|
+
priority: 100, // Evaluated before normalTransition
|
|
104
|
+
when: (facts) => facts.emergencyActive,
|
|
105
|
+
require: { type: "TRANSITION", to: "red" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Ordering with `after` – sequencing constraints
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
constraints: {
|
|
114
|
+
authenticate: {
|
|
115
|
+
when: (facts) => !facts.token,
|
|
116
|
+
require: { type: "AUTHENTICATE" },
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Only evaluates after authenticate's resolver completes
|
|
120
|
+
loadProfile: {
|
|
121
|
+
after: ["authenticate"],
|
|
122
|
+
when: (facts) => facts.token && !facts.profile,
|
|
123
|
+
require: { type: "LOAD_PROFILE" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`after` blocks evaluation entirely until the named constraint's resolver finishes. If the dependency's `when()` returns false, the blocked constraint proceeds normally.
|
|
129
|
+
|
|
130
|
+
### Async constraints – MUST declare `deps`
|
|
131
|
+
|
|
132
|
+
Synchronous constraints auto-track deps via proxy. Async constraints cannot (suspended across await), so you must declare `deps` explicitly.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
constraints: {
|
|
136
|
+
validateToken: {
|
|
137
|
+
async: true,
|
|
138
|
+
deps: ["token"], // Re-evaluate when token changes
|
|
139
|
+
when: async (facts) => {
|
|
140
|
+
const valid = await validateTokenRemotely(facts.token);
|
|
141
|
+
|
|
142
|
+
return valid;
|
|
143
|
+
},
|
|
144
|
+
require: { type: "REFRESH_TOKEN" },
|
|
145
|
+
timeout: 5000, // Optional: abort if check takes too long
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Disabling constraints at runtime
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
system.constraints.disable("fetchWhenReady");
|
|
154
|
+
system.constraints.isDisabled("fetchWhenReady"); // true
|
|
155
|
+
system.constraints.enable("fetchWhenReady");
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Resolver Anatomy
|
|
161
|
+
|
|
162
|
+
### Basic resolver
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
resolvers: {
|
|
166
|
+
fetchUser: {
|
|
167
|
+
requirement: "FETCH_USER",
|
|
168
|
+
resolve: async (req, context) => {
|
|
169
|
+
// req – the requirement object emitted by the constraint
|
|
170
|
+
// context.facts – mutable proxy to module facts
|
|
171
|
+
// context.signal – AbortSignal (cancelled on stop or timeout)
|
|
172
|
+
// context.snapshot() – read-only snapshot for before/after
|
|
173
|
+
const user = await fetch(`/api/users/${req.userId}`, {
|
|
174
|
+
signal: context.signal,
|
|
175
|
+
}).then((r) => r.json());
|
|
176
|
+
context.facts.user = user;
|
|
177
|
+
context.facts.phase = "loaded";
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Custom deduplication key
|
|
184
|
+
|
|
185
|
+
Without `key`, requirements are deduped by structural equality. With `key`, you control dedup:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
resolvers: {
|
|
189
|
+
fetchUser: {
|
|
190
|
+
requirement: "FETCH_USER",
|
|
191
|
+
key: (req) => `fetch-user-${req.userId}`, // One inflight per userId
|
|
192
|
+
resolve: async (req, context) => {
|
|
193
|
+
context.facts.user = await fetchUser(req.userId);
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Retry policy
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
resolvers: {
|
|
203
|
+
fetchData: {
|
|
204
|
+
requirement: "FETCH_DATA",
|
|
205
|
+
retry: {
|
|
206
|
+
attempts: 3,
|
|
207
|
+
backoff: "exponential", // "none" | "linear" | "exponential"
|
|
208
|
+
initialDelay: 200,
|
|
209
|
+
maxDelay: 5000,
|
|
210
|
+
shouldRetry: (error, attempt) => {
|
|
211
|
+
if (error.message.includes("404")) {
|
|
212
|
+
return false; // Don't retry permanent failures
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
resolve: async (req, context) => {
|
|
219
|
+
const res = await fetch("/api/data");
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
throw new Error(`HTTP ${res.status}`);
|
|
222
|
+
}
|
|
223
|
+
context.facts.data = await res.json();
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Batch resolution – prevent N+1
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
resolvers: {
|
|
233
|
+
fetchUsers: {
|
|
234
|
+
requirement: "FETCH_USER",
|
|
235
|
+
batch: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
windowMs: 50, // Collect requirements for 50ms
|
|
238
|
+
maxSize: 20, // Flush immediately at 20 items
|
|
239
|
+
},
|
|
240
|
+
resolveBatch: async (reqs, context) => {
|
|
241
|
+
const ids = reqs.map((req) => req.userId);
|
|
242
|
+
context.facts.users = await fetchUsersBatch(ids);
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
For per-item success/failure, use `resolveBatchWithResults` and return a results array matching input order.
|
|
249
|
+
|
|
250
|
+
### Timeout
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
resolvers: {
|
|
254
|
+
fetchData: {
|
|
255
|
+
requirement: "FETCH_DATA",
|
|
256
|
+
timeout: 10000, // context.signal aborted after 10s
|
|
257
|
+
resolve: async (req, context) => {
|
|
258
|
+
const res = await fetch("/api/data", { signal: context.signal });
|
|
259
|
+
context.facts.data = await res.json();
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Error Boundaries
|
|
268
|
+
|
|
269
|
+
### System-level error boundary
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const system = createSystem({
|
|
273
|
+
module: myModule,
|
|
274
|
+
errorBoundary: {
|
|
275
|
+
onConstraintError: "skip",
|
|
276
|
+
onResolverError: "retry-later",
|
|
277
|
+
onEffectError: "skip",
|
|
278
|
+
onDerivationError: "throw", // Derivation errors are bugs – throw always
|
|
279
|
+
onError: (error) => {
|
|
280
|
+
console.error(`[${error.source}] ${error.sourceId}: ${error.message}`);
|
|
281
|
+
},
|
|
282
|
+
retryLater: {
|
|
283
|
+
delayMs: 1000,
|
|
284
|
+
maxRetries: 3,
|
|
285
|
+
backoffMultiplier: 2,
|
|
286
|
+
maxDelayMs: 30000,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Recovery strategies:
|
|
293
|
+
|
|
294
|
+
| Strategy | Behavior |
|
|
295
|
+
|----------|----------|
|
|
296
|
+
| `"skip"` | Swallow the error, continue |
|
|
297
|
+
| `"retry"` | Retry immediately |
|
|
298
|
+
| `"retry-later"` | Retry after exponential backoff delay |
|
|
299
|
+
| `"disable"` | Disable the failing constraint/effect permanently |
|
|
300
|
+
| `"throw"` | Re-throw, halting the system |
|
|
301
|
+
|
|
302
|
+
### Dynamic recovery with functions
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
errorBoundary: {
|
|
306
|
+
onResolverError: (error, resolverId) => {
|
|
307
|
+
if (error.message.includes("NetworkError")) {
|
|
308
|
+
return "retry-later";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (error.message.includes("401")) {
|
|
312
|
+
return "skip";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return "throw";
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### DirectiveError properties
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { DirectiveError } from "@directive-run/core";
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await system.settle();
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (err instanceof DirectiveError) {
|
|
329
|
+
err.source; // "constraint" | "resolver" | "effect" | "derivation" | "system"
|
|
330
|
+
err.sourceId; // e.g., "fetchUser"
|
|
331
|
+
err.recoverable; // boolean
|
|
332
|
+
err.context; // debug data (e.g., the requirement object)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Module-level lifecycle hooks
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
hooks: {
|
|
341
|
+
onInit: (system) => { ... },
|
|
342
|
+
onStart: (system) => { ... },
|
|
343
|
+
onStop: (system) => { ... },
|
|
344
|
+
onError: (error, hookContext) => { ... },
|
|
345
|
+
},
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Circuit breaker
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { createCircuitBreaker } from "@directive-run/core/plugins";
|
|
352
|
+
|
|
353
|
+
const apiBreaker = createCircuitBreaker({
|
|
354
|
+
name: "external-api",
|
|
355
|
+
failureThreshold: 5,
|
|
356
|
+
recoveryTimeMs: 30000,
|
|
357
|
+
halfOpenMaxRequests: 3,
|
|
358
|
+
isFailure: (error) => !error.message.includes("404"),
|
|
359
|
+
onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Use in resolvers
|
|
363
|
+
resolvers: {
|
|
364
|
+
fetchData: {
|
|
365
|
+
requirement: "FETCH_DATA",
|
|
366
|
+
resolve: async (req, context) => {
|
|
367
|
+
const data = await apiBreaker.execute(async () => {
|
|
368
|
+
return fetch("/api/data").then((r) => r.json());
|
|
369
|
+
});
|
|
370
|
+
context.facts.data = data;
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// Wire into constraints for automatic fallback
|
|
376
|
+
constraints: {
|
|
377
|
+
apiDown: {
|
|
378
|
+
when: () => apiBreaker.getState() === "OPEN",
|
|
379
|
+
require: { type: "USE_FALLBACK" },
|
|
380
|
+
},
|
|
381
|
+
fetchNormally: {
|
|
382
|
+
when: (facts) => apiBreaker.getState() !== "OPEN" && !facts.data,
|
|
383
|
+
require: { type: "FETCH_DATA" },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Critical Anti-Patterns
|
|
391
|
+
|
|
392
|
+
### 1. Async logic in constraint when()
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// WRONG – when() must be synchronous (unless async: true + deps)
|
|
396
|
+
when: async (facts) => {
|
|
397
|
+
return await validate(facts.token);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// CORRECT – either use async: true with deps, or put validation in resolver
|
|
401
|
+
when: (facts) => Boolean(facts.token),
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 2. Condition checking inside a resolver
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// WRONG – resolver should not check conditions (that's the constraint's job)
|
|
408
|
+
resolve: async (req, context) => {
|
|
409
|
+
if (!context.facts.isAuthenticated) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// ...
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// CORRECT – let the constraint's when() gate this
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### 3. Async constraint without deps
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
// WRONG – engine cannot track dependencies
|
|
422
|
+
{ async: true, when: async (facts) => await validate(facts.token) }
|
|
423
|
+
|
|
424
|
+
// CORRECT
|
|
425
|
+
{ async: true, deps: ["token"], when: async (facts) => await validate(facts.token) }
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### 4. String literal for require
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// WRONG
|
|
432
|
+
require: "FETCH_DATA"
|
|
433
|
+
|
|
434
|
+
// CORRECT
|
|
435
|
+
require: { type: "FETCH_DATA" }
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### 5. Returning data from a resolver
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// WRONG – return value ignored
|
|
442
|
+
resolve: async (req, context) => { return await fetchUser(req.userId); }
|
|
443
|
+
|
|
444
|
+
// CORRECT
|
|
445
|
+
resolve: async (req, context) => { context.facts.user = await fetchUser(req.userId); }
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 6. Resolver parameter naming
|
|
449
|
+
Always use `(req, context)` – never `(req, ctx)` or `(request, context)`.
|
|
450
|
+
|
|
451
|
+
### 7. No error handling on network resolvers
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// WRONG
|
|
455
|
+
resolve: async (req, context) => {
|
|
456
|
+
context.facts.data = await fetch("/api").then((r) => r.json());
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// CORRECT – add retry and check response status
|
|
460
|
+
resolve: async (req, context) => {
|
|
461
|
+
const res = await fetch("/api");
|
|
462
|
+
if (!res.ok) {
|
|
463
|
+
throw new Error(`HTTP ${res.status}`);
|
|
464
|
+
}
|
|
465
|
+
context.facts.data = await res.json();
|
|
466
|
+
},
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Constraint vs Effect vs Derivation
|
|
472
|
+
|
|
473
|
+
| Feature | Purpose | When to use |
|
|
474
|
+
|---------|---------|-------------|
|
|
475
|
+
| Constraint | Declare a need (emit requirement) | "When X, the system needs Y" |
|
|
476
|
+
| Resolver | Fulfill a need (async work) | "Do Y" |
|
|
477
|
+
| Effect | React to changes (fire-and-forget) | "Whenever X changes, log it" |
|
|
478
|
+
| Derivation | Compute a value (sync, cached) | "X is always facts.a + facts.b" |
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Reference Files
|
|
483
|
+
|
|
484
|
+
- `constraints.md` – full constraint API, async constraints, disabling, common mistakes
|
|
485
|
+
- `resolvers.md` – full resolver API, context object, batch resolution, inspecting resolver status
|
|
486
|
+
- `error-boundaries.md` – all recovery strategies, DirectiveError API, circuit breaker patterns
|