@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,274 @@
|
|
|
1
|
+
# Constraints
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/core` — constraint definition: `when` / `require`, async constraints with `deps`, priority, data-form predicates.
|
|
4
|
+
|
|
5
|
+
Constraints declare WHEN something is needed. They are the demand side of the constraint-resolver pattern. Constraints evaluate conditions against facts and emit requirements that resolvers fulfill.
|
|
6
|
+
|
|
7
|
+
## Decision Tree: "Should this be a constraint?"
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Is this a condition that triggers work?
|
|
11
|
+
├── Yes, and the work is async/side-effectful → Constraint + Resolver
|
|
12
|
+
├── Yes, but the work is just a state derivation → Use derive instead
|
|
13
|
+
├── No, it's reacting to a change that already happened → Use effect
|
|
14
|
+
└── No, it's user-initiated → Use event handler
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Constraint Anatomy
|
|
18
|
+
|
|
19
|
+
A constraint has two parts: `when` (condition) and `require` (what's needed).
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
constraints: {
|
|
23
|
+
fetchUserWhenReady: {
|
|
24
|
+
// when() returns boolean – evaluated on every fact change
|
|
25
|
+
when: (facts) => facts.isAuthenticated && !facts.user,
|
|
26
|
+
|
|
27
|
+
// require – the requirement to emit when condition is true.
|
|
28
|
+
// Use the function form whenever you need to read facts —
|
|
29
|
+
// facts is NOT in scope inside the static object form.
|
|
30
|
+
require: (facts) => ({ type: "FETCH_USER", userId: facts.userId }),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Static vs Dynamic Requirements
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// Static requirement – same object every time
|
|
39
|
+
constraints: {
|
|
40
|
+
loadConfig: {
|
|
41
|
+
when: (facts) => facts.config === null,
|
|
42
|
+
require: { type: "LOAD_CONFIG" },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Dynamic requirement – function that reads facts
|
|
47
|
+
constraints: {
|
|
48
|
+
fetchUser: {
|
|
49
|
+
when: (facts) => facts.isAuthenticated && !facts.profile,
|
|
50
|
+
require: (facts) => ({ type: "FETCH_USER", userId: facts.userId }),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Multiple requirements – return an array
|
|
55
|
+
constraints: {
|
|
56
|
+
loadAll: {
|
|
57
|
+
when: (facts) => facts.phase === "init",
|
|
58
|
+
require: [
|
|
59
|
+
{ type: "LOAD_CONFIG" },
|
|
60
|
+
{ type: "LOAD_USER" },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Priority
|
|
67
|
+
|
|
68
|
+
Higher priority constraints are evaluated first. Use priority for conflict resolution when multiple constraints could fire simultaneously.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
constraints: {
|
|
72
|
+
normalTransition: {
|
|
73
|
+
priority: 50,
|
|
74
|
+
when: (facts) => facts.phase === "red" && facts.elapsed > 30,
|
|
75
|
+
require: { type: "TRANSITION", to: "green" },
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
emergencyOverride: {
|
|
79
|
+
priority: 100, // Evaluated before normalTransition
|
|
80
|
+
when: (facts) => facts.emergencyActive,
|
|
81
|
+
require: { type: "TRANSITION", to: "red" },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Default priority is 0. Higher numbers run first.
|
|
87
|
+
|
|
88
|
+
## Ordering with `after`
|
|
89
|
+
|
|
90
|
+
Use `after` to declare that a constraint should only be evaluated after another constraint's resolver completes. This is different from priority (evaluation order) -- `after` blocks evaluation entirely until the dependency is resolved.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
constraints: {
|
|
94
|
+
authenticate: {
|
|
95
|
+
when: (facts) => !facts.token,
|
|
96
|
+
require: { type: "AUTHENTICATE" },
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Only evaluate after authenticate's resolver completes
|
|
100
|
+
loadProfile: {
|
|
101
|
+
after: ["authenticate"],
|
|
102
|
+
when: (facts) => facts.token && !facts.profile,
|
|
103
|
+
require: { type: "LOAD_PROFILE" },
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Cross-module after reference
|
|
107
|
+
loadData: {
|
|
108
|
+
after: ["auth::authenticate"],
|
|
109
|
+
when: (facts) => facts.self.dataNeeded,
|
|
110
|
+
require: { type: "LOAD_DATA" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the dependency's `when()` returns false (no requirement emitted), the blocked constraint proceeds normally. If the dependency's resolver fails, the blocked constraint remains blocked.
|
|
116
|
+
|
|
117
|
+
## Async Constraints
|
|
118
|
+
|
|
119
|
+
For conditions that require async evaluation (e.g., remote validation). Async constraints MUST declare `deps` for dependency tracking.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
constraints: {
|
|
123
|
+
validateToken: {
|
|
124
|
+
async: true,
|
|
125
|
+
deps: ["token"], // REQUIRED for async constraints
|
|
126
|
+
|
|
127
|
+
when: async (facts) => {
|
|
128
|
+
const valid = await validateTokenRemotely(facts.token);
|
|
129
|
+
|
|
130
|
+
return valid;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
require: { type: "REFRESH_TOKEN" },
|
|
134
|
+
timeout: 5000, // Optional timeout in ms
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Why `deps` is Required for Async
|
|
140
|
+
|
|
141
|
+
Synchronous constraints use auto-tracking (proxy-based). Async constraints cannot be auto-tracked because the function is suspended across await boundaries. The `deps` array tells the engine which facts to watch.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// WRONG – async without deps, engine cannot track dependencies
|
|
145
|
+
constraints: {
|
|
146
|
+
check: {
|
|
147
|
+
async: true,
|
|
148
|
+
when: async (facts) => await validate(facts.token),
|
|
149
|
+
require: { type: "REFRESH" },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// CORRECT – deps tells the engine to re-evaluate when token changes
|
|
154
|
+
constraints: {
|
|
155
|
+
check: {
|
|
156
|
+
async: true,
|
|
157
|
+
deps: ["token"],
|
|
158
|
+
when: async (facts) => await validate(facts.token),
|
|
159
|
+
require: { type: "REFRESH" },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Disabling Constraints at Runtime
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const system = createSystem({ module: myModule });
|
|
168
|
+
system.start();
|
|
169
|
+
|
|
170
|
+
// Disable a constraint – it won't be evaluated
|
|
171
|
+
system.constraints.disable("fetchUserWhenReady");
|
|
172
|
+
|
|
173
|
+
// Check if disabled
|
|
174
|
+
system.constraints.isDisabled("fetchUserWhenReady"); // true
|
|
175
|
+
|
|
176
|
+
// Re-enable – triggers re-evaluation on next cycle
|
|
177
|
+
system.constraints.enable("fetchUserWhenReady");
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Common Mistakes
|
|
181
|
+
|
|
182
|
+
### Putting async logic in resolvers instead of constraints
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// WRONG – resolver checks conditions (constraint's job)
|
|
186
|
+
resolvers: {
|
|
187
|
+
fetchData: {
|
|
188
|
+
requirement: "FETCH",
|
|
189
|
+
resolve: async (req, context) => {
|
|
190
|
+
if (!context.facts.isAuthenticated) {
|
|
191
|
+
return; // Should be in constraint's when()
|
|
192
|
+
}
|
|
193
|
+
// ...
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// CORRECT – constraint declares when, resolver just does the work
|
|
199
|
+
constraints: {
|
|
200
|
+
fetchWhenAuth: {
|
|
201
|
+
when: (facts) => facts.isAuthenticated && !facts.data,
|
|
202
|
+
require: { type: "FETCH" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
resolvers: {
|
|
207
|
+
fetchData: {
|
|
208
|
+
requirement: "FETCH",
|
|
209
|
+
resolve: async (req, context) => {
|
|
210
|
+
const res = await fetch("/api/data");
|
|
211
|
+
context.facts.data = await res.json();
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### String literal for require
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// WRONG – require must be an object
|
|
221
|
+
require: "FETCH_DATA",
|
|
222
|
+
|
|
223
|
+
// CORRECT – object with type property
|
|
224
|
+
require: { type: "FETCH_DATA" },
|
|
225
|
+
|
|
226
|
+
// CORRECT – with payload
|
|
227
|
+
require: { type: "FETCH_DATA", endpoint: "/api/users" },
|
|
228
|
+
|
|
229
|
+
// CORRECT – dynamic from facts
|
|
230
|
+
require: (facts) => ({ type: "FETCH_DATA", userId: facts.currentUserId }),
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Returning null to conditionally skip
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// require can return null to suppress the requirement
|
|
237
|
+
constraints: {
|
|
238
|
+
conditionalFetch: {
|
|
239
|
+
when: (facts) => facts.needsUpdate,
|
|
240
|
+
require: (facts) => {
|
|
241
|
+
if (!facts.userId) {
|
|
242
|
+
return null; // No requirement emitted
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { type: "FETCH_USER", userId: facts.userId };
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Constraints vs Effects vs Derivations
|
|
252
|
+
|
|
253
|
+
| Feature | Purpose | Triggers |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| Constraint | Declare a need (emit requirement) | Fact changes, re-evaluated automatically |
|
|
256
|
+
| Resolver | Fulfill a need (async work) | Requirement emitted by constraint |
|
|
257
|
+
| Effect | React to changes (fire-and-forget) | Fact changes, runs after reconciliation |
|
|
258
|
+
| Derivation | Compute a value (synchronous, cached) | Fact changes, recomputed lazily |
|
|
259
|
+
|
|
260
|
+
### When to Use Which
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
"When X is true, the system needs Y" → Constraint
|
|
264
|
+
"Do Y" → Resolver
|
|
265
|
+
"Whenever X changes, log it" → Effect
|
|
266
|
+
"X is always facts.a + facts.b" → Derivation
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## See also
|
|
270
|
+
|
|
271
|
+
- [`resolvers.md`](./resolvers.md) — the other half of the constraint-resolver loop; the supply side that fulfills the requirements constraints emit
|
|
272
|
+
- [`core-patterns.md`](./core-patterns.md) — how constraints fit into the module shape alongside facts, derivations, events, and effects
|
|
273
|
+
- [`multi-module.md`](./multi-module.md) — `crossModuleDeps` for constraints that fire on facts owned by another module
|
|
274
|
+
- [`anti-patterns.md`](./anti-patterns.md) — the constraint-shaped mistakes AI assistants reliably make
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Error Boundaries
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/core` — error boundaries, recovery strategies, lifecycle hooks, and circuit breakers.
|
|
4
|
+
|
|
5
|
+
How to handle errors in Directive: recovery strategies, error boundaries, lifecycle hooks, and the circuit breaker pattern.
|
|
6
|
+
|
|
7
|
+
## Decision Tree: "How should errors be handled?"
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Where did the error occur?
|
|
11
|
+
├── Resolver (API call, async work)
|
|
12
|
+
│ ├── Transient failure (network, timeout) → retry / retry-later
|
|
13
|
+
│ ├── Permanent failure (404, auth) → skip or throw
|
|
14
|
+
│ └── Unknown → retry-later with maxRetries
|
|
15
|
+
│
|
|
16
|
+
├── Constraint (evaluation error)
|
|
17
|
+
│ ├── Bug in when() logic → throw (fix the code)
|
|
18
|
+
│ └── Unexpected data shape → skip (disable constraint)
|
|
19
|
+
│
|
|
20
|
+
├── Effect (side effect failed)
|
|
21
|
+
│ ├── Non-critical (logging, analytics) → skip
|
|
22
|
+
│ └── Critical (sync to external system) → retry-later
|
|
23
|
+
│
|
|
24
|
+
├── Derivation (computation error)
|
|
25
|
+
│ └── Usually a bug → throw (fix the code)
|
|
26
|
+
│
|
|
27
|
+
└── External service (repeated failures)
|
|
28
|
+
└── Circuit breaker pattern
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Recovery Strategies
|
|
32
|
+
|
|
33
|
+
Directive supports five recovery strategies:
|
|
34
|
+
|
|
35
|
+
| Strategy | Behavior |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `"skip"` | Swallow the error, continue processing |
|
|
38
|
+
| `"retry"` | Retry immediately (respects resolver retry policy) |
|
|
39
|
+
| `"retry-later"` | Retry after a delay with exponential backoff |
|
|
40
|
+
| `"disable"` | Disable the failing constraint/effect permanently |
|
|
41
|
+
| `"throw"` | Re-throw the error, halting the system |
|
|
42
|
+
|
|
43
|
+
## System-Level Error Boundary
|
|
44
|
+
|
|
45
|
+
Configure error handling for the entire system:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const system = createSystem({
|
|
49
|
+
module: myModule,
|
|
50
|
+
|
|
51
|
+
errorBoundary: {
|
|
52
|
+
// Per-subsystem strategies (string or function)
|
|
53
|
+
onConstraintError: "skip",
|
|
54
|
+
onResolverError: "retry-later",
|
|
55
|
+
onEffectError: "skip",
|
|
56
|
+
onDerivationError: "throw",
|
|
57
|
+
|
|
58
|
+
// Global error callback – fires for all errors
|
|
59
|
+
onError: (error) => {
|
|
60
|
+
// error is a DirectiveError with source tracking
|
|
61
|
+
console.error(`[${error.source}] ${error.sourceId}: ${error.message}`);
|
|
62
|
+
console.error("Recoverable:", error.recoverable);
|
|
63
|
+
console.error("Context:", error.context);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Configuration for retry-later strategy
|
|
67
|
+
retryLater: {
|
|
68
|
+
delayMs: 1000, // Initial delay (default: 1000)
|
|
69
|
+
maxRetries: 3, // Max retry attempts (default: 3)
|
|
70
|
+
backoffMultiplier: 2, // Multiply delay each retry (default: 2)
|
|
71
|
+
maxDelayMs: 30000, // Cap on delay growth (default: 30000)
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Dynamic Error Handling with Functions
|
|
78
|
+
|
|
79
|
+
Use functions instead of strings for conditional recovery:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
errorBoundary: {
|
|
83
|
+
onResolverError: (error, resolverId) => {
|
|
84
|
+
// Network errors – retry later
|
|
85
|
+
if (error.message.includes("NetworkError")) {
|
|
86
|
+
return "retry-later";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Auth errors – skip, don't retry
|
|
90
|
+
if (error.message.includes("401")) {
|
|
91
|
+
return "skip";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Everything else – throw
|
|
95
|
+
return "throw";
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
onConstraintError: (error, constraintId) => {
|
|
99
|
+
// Disable constraints that repeatedly fail
|
|
100
|
+
if (constraintId === "experimentalFeature") {
|
|
101
|
+
return "disable";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return "skip";
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
onEffectError: (error, effectId) => {
|
|
108
|
+
// Analytics can fail silently
|
|
109
|
+
if (effectId.startsWith("analytics")) {
|
|
110
|
+
return "skip";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return "throw";
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## DirectiveError
|
|
119
|
+
|
|
120
|
+
All errors passed to error boundary callbacks are `DirectiveError` instances with source tracking:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { DirectiveError } from "@directive-run/core";
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await system.settle();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err instanceof DirectiveError) {
|
|
129
|
+
err.source; // "constraint" | "resolver" | "effect" | "derivation" | "system"
|
|
130
|
+
err.sourceId; // e.g., "fetchUser" – the specific item that failed
|
|
131
|
+
err.recoverable; // boolean – whether recovery strategies apply
|
|
132
|
+
err.context; // arbitrary debug data (e.g., the requirement object)
|
|
133
|
+
err.message; // human-readable description
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Resolver-Level Error Handling
|
|
139
|
+
|
|
140
|
+
Resolvers have their own retry policy independent of the error boundary:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
resolvers: {
|
|
144
|
+
fetchData: {
|
|
145
|
+
requirement: "FETCH_DATA",
|
|
146
|
+
|
|
147
|
+
// Resolver-level retry policy
|
|
148
|
+
retry: {
|
|
149
|
+
attempts: 3,
|
|
150
|
+
backoff: "exponential",
|
|
151
|
+
initialDelay: 200,
|
|
152
|
+
maxDelay: 5000,
|
|
153
|
+
shouldRetry: (error, attempt) => {
|
|
154
|
+
// Only retry server errors, not client errors
|
|
155
|
+
if (error.message.includes("4")) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
resolve: async (req, context) => {
|
|
164
|
+
const res = await fetch("/api/data");
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
throw new Error(`HTTP ${res.status}`);
|
|
167
|
+
}
|
|
168
|
+
context.facts.data = await res.json();
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
When both resolver retry and error boundary are configured, the resolver retries first. If all retries fail, the error boundary strategy is applied.
|
|
175
|
+
|
|
176
|
+
## Lifecycle Hooks
|
|
177
|
+
|
|
178
|
+
Module-level hooks for lifecycle events:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const myModule = createModule("app", {
|
|
182
|
+
schema: { facts: { status: t.string() } },
|
|
183
|
+
|
|
184
|
+
hooks: {
|
|
185
|
+
onInit: (system) => {
|
|
186
|
+
console.log("Module initialized");
|
|
187
|
+
},
|
|
188
|
+
onStart: (system) => {
|
|
189
|
+
console.log("System started");
|
|
190
|
+
},
|
|
191
|
+
onStop: (system) => {
|
|
192
|
+
console.log("System stopped");
|
|
193
|
+
},
|
|
194
|
+
onError: (error, hookContext) => {
|
|
195
|
+
console.error("Module error:", error.message);
|
|
196
|
+
// error is a DirectiveError
|
|
197
|
+
// hookContext provides additional details
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
init: (facts) => {
|
|
202
|
+
facts.status = "ready";
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Circuit Breaker
|
|
208
|
+
|
|
209
|
+
For protecting against cascading failures from external services. The circuit breaker tracks failure rates and short-circuits requests when a threshold is exceeded.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { createCircuitBreaker } from "@directive-run/core/plugins";
|
|
213
|
+
|
|
214
|
+
const apiBreaker = createCircuitBreaker({
|
|
215
|
+
name: "external-api",
|
|
216
|
+
failureThreshold: 5, // Open after 5 failures (default: 5)
|
|
217
|
+
recoveryTimeMs: 30000, // Wait 30s before trying again (default: 30000)
|
|
218
|
+
halfOpenMaxRequests: 3, // Allow 3 trial requests in half-open (default: 3)
|
|
219
|
+
failureWindowMs: 60000, // Count failures within 60s window (default: 60000)
|
|
220
|
+
|
|
221
|
+
// Optional: classify which errors count as failures
|
|
222
|
+
isFailure: (error) => {
|
|
223
|
+
// Don't count 404s as circuit-breaking failures
|
|
224
|
+
if (error.message.includes("404")) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return true;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Optional: react to state changes
|
|
232
|
+
onStateChange: (from, to) => {
|
|
233
|
+
console.log(`Circuit: ${from} -> ${to}`);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Using Circuit Breaker in Resolvers
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
resolvers: {
|
|
242
|
+
fetchData: {
|
|
243
|
+
requirement: "FETCH_DATA",
|
|
244
|
+
resolve: async (req, context) => {
|
|
245
|
+
const data = await apiBreaker.execute(async () => {
|
|
246
|
+
const res = await fetch("/api/data");
|
|
247
|
+
|
|
248
|
+
return res.json();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
context.facts.data = data;
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Circuit Breaker + Constraints
|
|
258
|
+
|
|
259
|
+
Wire the circuit breaker state into constraints for automatic fallback:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
constraints: {
|
|
263
|
+
apiDown: {
|
|
264
|
+
when: () => apiBreaker.getState() === "OPEN",
|
|
265
|
+
require: { type: "USE_FALLBACK" },
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
fetchNormally: {
|
|
269
|
+
when: (facts) => apiBreaker.getState() !== "OPEN" && !facts.data,
|
|
270
|
+
require: { type: "FETCH_DATA" },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Circuit Breaker States
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
CLOSED → Normal operation, all requests pass through
|
|
279
|
+
↓ (failures >= threshold)
|
|
280
|
+
OPEN → All requests rejected immediately
|
|
281
|
+
↓ (after recoveryTimeMs)
|
|
282
|
+
HALF_OPEN → Limited trial requests allowed
|
|
283
|
+
↓ (trial succeeds) → CLOSED
|
|
284
|
+
↓ (trial fails) → OPEN
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Circuit Breaker API
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
apiBreaker.getState(); // "CLOSED" | "OPEN" | "HALF_OPEN"
|
|
291
|
+
apiBreaker.isAllowed(); // boolean – would a request be allowed?
|
|
292
|
+
apiBreaker.getStats(); // { totalRequests, totalFailures, recentFailures, ... }
|
|
293
|
+
apiBreaker.forceState("CLOSED"); // Force state (useful in tests)
|
|
294
|
+
apiBreaker.reset(); // Reset to CLOSED with cleared stats
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### CircuitBreakerOpenError
|
|
298
|
+
|
|
299
|
+
When the circuit is open, `execute()` throws a `CircuitBreakerOpenError`:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { CircuitBreakerOpenError } from "@directive-run/core/plugins";
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await apiBreaker.execute(() => fetch("/api"));
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof CircuitBreakerOpenError) {
|
|
308
|
+
error.code; // "CIRCUIT_OPEN"
|
|
309
|
+
error.retryAfterMs; // ms until circuit transitions to HALF_OPEN
|
|
310
|
+
error.state; // "OPEN" | "HALF_OPEN"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Error Handling Checklist
|
|
316
|
+
|
|
317
|
+
1. Set system-level `errorBoundary` with strategies for each subsystem
|
|
318
|
+
2. Add `retry` policy on resolvers that call external services
|
|
319
|
+
3. Use `shouldRetry` to avoid retrying permanent failures (4xx errors)
|
|
320
|
+
4. Use circuit breaker for services with known reliability issues
|
|
321
|
+
5. Wire circuit breaker state into constraints for automatic fallback
|
|
322
|
+
6. Add `onError` callback for logging/monitoring
|
|
323
|
+
7. Use `"throw"` for derivation errors (they indicate bugs)
|
|
324
|
+
8. Use `"skip"` for non-critical effects (logging, analytics)
|
|
325
|
+
|
|
326
|
+
## See also
|
|
327
|
+
|
|
328
|
+
- [`resolvers.md`](./resolvers.md) — retry policies and cancellation; the most common error source
|
|
329
|
+
- [`plugins.md`](./plugins.md) — `createCircuitBreaker` lives in `@directive-run/core/plugins`; pair it with error boundaries for cascading-failure isolation
|
|
330
|
+
- [`anti-patterns.md`](./anti-patterns.md) — common error-handling mistakes (catching the wrong type, swallowing recoverable errors)
|