@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,382 @@
|
|
|
1
|
+
# Anti-Patterns
|
|
2
|
+
|
|
3
|
+
20 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
|
|
4
|
+
|
|
5
|
+
## 1. Unnecessary Type Casting on Facts/Derivations
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// WRONG — schema already provides the type
|
|
9
|
+
const profile = system.facts.profile as ResourceState<Profile>;
|
|
10
|
+
|
|
11
|
+
// CORRECT — trust the schema
|
|
12
|
+
const profile = system.facts.profile;
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 2. Flat Schema (Missing facts Wrapper)
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// WRONG — facts must be nested under schema.facts
|
|
19
|
+
createModule("counter", {
|
|
20
|
+
schema: {
|
|
21
|
+
phase: t.string(),
|
|
22
|
+
count: t.number(),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// CORRECT
|
|
27
|
+
createModule("counter", {
|
|
28
|
+
schema: {
|
|
29
|
+
facts: {
|
|
30
|
+
phase: t.string(),
|
|
31
|
+
count: t.number(),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 3. Bare `facts.*` in Multi-Module Constraints
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// WRONG — multi-module constraints use facts.self for own module
|
|
41
|
+
constraints: {
|
|
42
|
+
checkItems: {
|
|
43
|
+
when: (facts) => facts.items.length > 0,
|
|
44
|
+
require: { type: "PROCESS" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// CORRECT — use facts.self.* for own module facts
|
|
49
|
+
constraints: {
|
|
50
|
+
checkItems: {
|
|
51
|
+
when: (facts) => facts.self.items.length > 0,
|
|
52
|
+
require: { type: "PROCESS" },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 4. Nonexistent Schema Builders
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// WRONG — t.map(), t.set(), t.promise() do not exist
|
|
61
|
+
schema: {
|
|
62
|
+
facts: {
|
|
63
|
+
cache: t.map<string, User>(),
|
|
64
|
+
tags: t.set<string>(),
|
|
65
|
+
pending: t.promise<Data>(),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// CORRECT — use t.object() with type parameter
|
|
70
|
+
schema: {
|
|
71
|
+
facts: {
|
|
72
|
+
cache: t.object<Map<string, User>>(),
|
|
73
|
+
tags: t.object<Set<string>>(),
|
|
74
|
+
pending: t.object<Promise<Data>>(),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 5. Abbreviating `context` to `ctx`
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// WRONG — never abbreviate context
|
|
83
|
+
resolve: async (req, ctx) => {
|
|
84
|
+
ctx.facts.status = "done";
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// CORRECT — always spell out context
|
|
88
|
+
resolve: async (req, context) => {
|
|
89
|
+
context.facts.status = "done";
|
|
90
|
+
},
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 6. Flat Module Config (No schema Wrapper)
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// WRONG — properties must be inside schema.facts
|
|
97
|
+
createModule("timer", {
|
|
98
|
+
phase: t.string(),
|
|
99
|
+
elapsed: t.number(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// CORRECT — wrap in schema: { facts: {} }
|
|
103
|
+
createModule("timer", {
|
|
104
|
+
schema: {
|
|
105
|
+
facts: {
|
|
106
|
+
phase: t.string(),
|
|
107
|
+
elapsed: t.number(),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 7. String-Based Event Dispatch
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// WRONG — events are not dispatched by string
|
|
117
|
+
system.dispatch("login", { token: "abc" });
|
|
118
|
+
|
|
119
|
+
// CORRECT — use the events accessor
|
|
120
|
+
system.events.login({ token: "abc" });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 8. Direct Array/Object Mutation
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// WRONG — proxy cannot detect in-place mutations
|
|
127
|
+
facts.items.push(item);
|
|
128
|
+
facts.config.theme = "dark";
|
|
129
|
+
|
|
130
|
+
// CORRECT — replace the entire value
|
|
131
|
+
facts.items = [...facts.items, item];
|
|
132
|
+
facts.config = { ...facts.config, theme: "dark" };
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 9. Nonexistent `useDirective` Hook
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// WRONG — there is no useDirective hook
|
|
139
|
+
const state = useDirective(system);
|
|
140
|
+
|
|
141
|
+
// CORRECT — use useSelector with a selector function
|
|
142
|
+
const count = useSelector(system, (s) => s.facts.count);
|
|
143
|
+
const isLoading = useSelector(system, (s) => s.derive.isLoading);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 10. Bracket Notation for Namespaced Facts
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// WRONG — internal separator is not part of the public API
|
|
150
|
+
const status = facts["auth::status"];
|
|
151
|
+
const token = facts["auth_token"];
|
|
152
|
+
|
|
153
|
+
// CORRECT — use dot notation through the namespace proxy
|
|
154
|
+
const status = facts.auth.status;
|
|
155
|
+
const token = facts.auth.token;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 11. Builder/Chaining API
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// WRONG — there is no builder pattern
|
|
162
|
+
const mod = module("counter")
|
|
163
|
+
.schema({ count: t.number() })
|
|
164
|
+
.build();
|
|
165
|
+
|
|
166
|
+
// CORRECT — use createModule with object syntax
|
|
167
|
+
const mod = createModule("counter", {
|
|
168
|
+
schema: {
|
|
169
|
+
facts: { count: t.number() },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 12. Returning Data from Resolvers
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// WRONG — resolvers return void, not data
|
|
178
|
+
resolve: async (req, context) => {
|
|
179
|
+
const user = await fetchUser(req.userId);
|
|
180
|
+
|
|
181
|
+
return user; // Return value is ignored
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// CORRECT — mutate context.facts to store results
|
|
185
|
+
resolve: async (req, context) => {
|
|
186
|
+
const user = await fetchUser(req.userId);
|
|
187
|
+
context.facts.user = user;
|
|
188
|
+
},
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 13. Async Logic in `init`
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// WRONG — init is synchronous, facts assignment only
|
|
195
|
+
init: async (facts) => {
|
|
196
|
+
const data = await fetch("/api/config");
|
|
197
|
+
facts.config = await data.json();
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// CORRECT — init sets defaults; use constraints/resolvers for async work
|
|
201
|
+
init: (facts) => {
|
|
202
|
+
facts.config = null;
|
|
203
|
+
facts.phase = "loading";
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
constraints: {
|
|
207
|
+
loadConfig: {
|
|
208
|
+
when: (facts) => facts.config === null,
|
|
209
|
+
require: { type: "LOAD_CONFIG" },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 14. Missing `settle()` After `start()`
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// WRONG — constraints fire on start, resolvers are async
|
|
218
|
+
system.start();
|
|
219
|
+
console.log(system.facts.data); // May still be null
|
|
220
|
+
|
|
221
|
+
// CORRECT — wait for resolvers to complete
|
|
222
|
+
system.start();
|
|
223
|
+
await system.settle();
|
|
224
|
+
console.log(system.facts.data); // Resolved
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 15. Missing `crossModuleDeps` Declaration
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// WRONG — accessing auth facts without declaring dependency
|
|
231
|
+
const dataModule = createModule("data", {
|
|
232
|
+
schema: { facts: { items: t.array(t.string()) } },
|
|
233
|
+
constraints: {
|
|
234
|
+
fetchWhenAuth: {
|
|
235
|
+
when: (facts) => facts.auth.isAuthenticated, // Type error
|
|
236
|
+
require: { type: "FETCH" },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// CORRECT — declare crossModuleDeps for type-safe cross-module access
|
|
242
|
+
const dataModule = createModule("data", {
|
|
243
|
+
schema: { facts: { items: t.array(t.string()) } },
|
|
244
|
+
crossModuleDeps: { auth: authSchema },
|
|
245
|
+
constraints: {
|
|
246
|
+
fetchWhenAuth: {
|
|
247
|
+
when: (facts) => facts.auth.isAuthenticated,
|
|
248
|
+
require: { type: "FETCH" },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## 16. String Literal for `require`
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// WRONG — require must be an object with type property
|
|
258
|
+
constraints: {
|
|
259
|
+
check: {
|
|
260
|
+
when: (facts) => facts.ready,
|
|
261
|
+
require: "FETCH_DATA",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// CORRECT — use object form with type
|
|
266
|
+
constraints: {
|
|
267
|
+
check: {
|
|
268
|
+
when: (facts) => facts.ready,
|
|
269
|
+
require: { type: "FETCH_DATA" },
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## 17. Passthrough Derivations
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// WRONG — derivation just returns a fact value unchanged
|
|
278
|
+
derive: {
|
|
279
|
+
count: (facts) => facts.count,
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// CORRECT — remove it, read the fact directly instead
|
|
283
|
+
// system.facts.count instead of system.derive.count
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## 18. Deep Import Paths
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// WRONG — internal module paths are not public API
|
|
290
|
+
import { createModule } from "@directive-run/core/module";
|
|
291
|
+
import { createSystem } from "@directive-run/core/system";
|
|
292
|
+
|
|
293
|
+
// CORRECT — import from package root
|
|
294
|
+
import { createModule, createSystem } from "@directive-run/core";
|
|
295
|
+
|
|
296
|
+
// Exception: plugins have their own entry point
|
|
297
|
+
import { loggingPlugin } from "@directive-run/core/plugins";
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 19. Async `when()` Without `deps`
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// WRONG — async constraints need explicit deps for tracking
|
|
304
|
+
constraints: {
|
|
305
|
+
validate: {
|
|
306
|
+
async: true,
|
|
307
|
+
when: async (facts) => {
|
|
308
|
+
const valid = await checkRemote(facts.token);
|
|
309
|
+
|
|
310
|
+
return valid;
|
|
311
|
+
},
|
|
312
|
+
require: { type: "REFRESH_TOKEN" },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// CORRECT — add deps array for async constraints
|
|
317
|
+
constraints: {
|
|
318
|
+
validate: {
|
|
319
|
+
async: true,
|
|
320
|
+
deps: ["token"],
|
|
321
|
+
when: async (facts) => {
|
|
322
|
+
const valid = await checkRemote(facts.token);
|
|
323
|
+
|
|
324
|
+
return valid;
|
|
325
|
+
},
|
|
326
|
+
require: { type: "REFRESH_TOKEN" },
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## 20. No Error Handling on Failing Resolvers
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
// WRONG — unhandled errors crash the system
|
|
335
|
+
resolvers: {
|
|
336
|
+
fetchData: {
|
|
337
|
+
requirement: "FETCH",
|
|
338
|
+
resolve: async (req, context) => {
|
|
339
|
+
const res = await fetch("/api/data");
|
|
340
|
+
context.facts.data = await res.json();
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// CORRECT — use retry policy and/or module error boundary
|
|
346
|
+
resolvers: {
|
|
347
|
+
fetchData: {
|
|
348
|
+
requirement: "FETCH",
|
|
349
|
+
retry: { attempts: 3, backoff: "exponential" },
|
|
350
|
+
resolve: async (req, context) => {
|
|
351
|
+
const res = await fetch("/api/data");
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
throw new Error(`HTTP ${res.status}`);
|
|
354
|
+
}
|
|
355
|
+
context.facts.data = await res.json();
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// Also set error boundary at system level
|
|
361
|
+
const system = createSystem({
|
|
362
|
+
module: myModule,
|
|
363
|
+
errorBoundary: {
|
|
364
|
+
onResolverError: "retry-later",
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Quick Reference Checklist
|
|
370
|
+
|
|
371
|
+
Before generating any Directive code, verify:
|
|
372
|
+
|
|
373
|
+
1. Schema is nested: `schema: { facts: { ... } }` (not flat)
|
|
374
|
+
2. No `as` casts when reading facts or derivations
|
|
375
|
+
3. Resolver params are `(req, context)` not `(req, ctx)`
|
|
376
|
+
4. `require` is an object `{ type: "..." }` not a string
|
|
377
|
+
5. `init()` is synchronous
|
|
378
|
+
6. Resolvers return void and mutate `context.facts`
|
|
379
|
+
7. Arrays/objects replaced, not mutated in place
|
|
380
|
+
8. Multi-module uses `facts.self.*` for own facts
|
|
381
|
+
9. Imports from `@directive-run/core`, not deep paths
|
|
382
|
+
10. `await system.settle()` after `system.start()`
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Constraints
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "Should this be a constraint?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Is this a condition that triggers work?
|
|
9
|
+
├── Yes, and the work is async/side-effectful → Constraint + Resolver
|
|
10
|
+
├── Yes, but the work is just a state derivation → Use derive instead
|
|
11
|
+
├── No, it's reacting to a change that already happened → Use effect
|
|
12
|
+
└── No, it's user-initiated → Use event handler
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic Constraint Anatomy
|
|
16
|
+
|
|
17
|
+
A constraint has two parts: `when` (condition) and `require` (what's needed).
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
constraints: {
|
|
21
|
+
fetchUserWhenReady: {
|
|
22
|
+
// when() returns boolean — evaluated on every fact change
|
|
23
|
+
when: (facts) => facts.isAuthenticated && !facts.user,
|
|
24
|
+
|
|
25
|
+
// require — the requirement to emit when condition is true
|
|
26
|
+
require: { type: "FETCH_USER", userId: facts.userId },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Static vs Dynamic Requirements
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Static requirement — same object every time
|
|
35
|
+
constraints: {
|
|
36
|
+
loadConfig: {
|
|
37
|
+
when: (facts) => facts.config === null,
|
|
38
|
+
require: { type: "LOAD_CONFIG" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Dynamic requirement — function that reads facts
|
|
43
|
+
constraints: {
|
|
44
|
+
fetchUser: {
|
|
45
|
+
when: (facts) => facts.isAuthenticated && !facts.profile,
|
|
46
|
+
require: (facts) => ({ type: "FETCH_USER", userId: facts.userId }),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Multiple requirements — return an array
|
|
51
|
+
constraints: {
|
|
52
|
+
loadAll: {
|
|
53
|
+
when: (facts) => facts.phase === "init",
|
|
54
|
+
require: [
|
|
55
|
+
{ type: "LOAD_CONFIG" },
|
|
56
|
+
{ type: "LOAD_USER" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Priority
|
|
63
|
+
|
|
64
|
+
Higher priority constraints are evaluated first. Use priority for conflict resolution when multiple constraints could fire simultaneously.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
constraints: {
|
|
68
|
+
normalTransition: {
|
|
69
|
+
priority: 50,
|
|
70
|
+
when: (facts) => facts.phase === "red" && facts.elapsed > 30,
|
|
71
|
+
require: { type: "TRANSITION", to: "green" },
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
emergencyOverride: {
|
|
75
|
+
priority: 100, // Evaluated before normalTransition
|
|
76
|
+
when: (facts) => facts.emergencyActive,
|
|
77
|
+
require: { type: "TRANSITION", to: "red" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Default priority is 0. Higher numbers run first.
|
|
83
|
+
|
|
84
|
+
## Ordering with `after`
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
constraints: {
|
|
90
|
+
authenticate: {
|
|
91
|
+
when: (facts) => !facts.token,
|
|
92
|
+
require: { type: "AUTHENTICATE" },
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// Only evaluate after authenticate's resolver completes
|
|
96
|
+
loadProfile: {
|
|
97
|
+
after: ["authenticate"],
|
|
98
|
+
when: (facts) => facts.token && !facts.profile,
|
|
99
|
+
require: { type: "LOAD_PROFILE" },
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Cross-module after reference
|
|
103
|
+
loadData: {
|
|
104
|
+
after: ["auth::authenticate"],
|
|
105
|
+
when: (facts) => facts.self.dataNeeded,
|
|
106
|
+
require: { type: "LOAD_DATA" },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
## Async Constraints
|
|
114
|
+
|
|
115
|
+
For conditions that require async evaluation (e.g., remote validation). Async constraints MUST declare `deps` for dependency tracking.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
constraints: {
|
|
119
|
+
validateToken: {
|
|
120
|
+
async: true,
|
|
121
|
+
deps: ["token"], // REQUIRED for async constraints
|
|
122
|
+
|
|
123
|
+
when: async (facts) => {
|
|
124
|
+
const valid = await validateTokenRemotely(facts.token);
|
|
125
|
+
|
|
126
|
+
return valid;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
require: { type: "REFRESH_TOKEN" },
|
|
130
|
+
timeout: 5000, // Optional timeout in ms
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Why `deps` is Required for Async
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// WRONG — async without deps, engine cannot track dependencies
|
|
141
|
+
constraints: {
|
|
142
|
+
check: {
|
|
143
|
+
async: true,
|
|
144
|
+
when: async (facts) => await validate(facts.token),
|
|
145
|
+
require: { type: "REFRESH" },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// CORRECT — deps tells the engine to re-evaluate when token changes
|
|
150
|
+
constraints: {
|
|
151
|
+
check: {
|
|
152
|
+
async: true,
|
|
153
|
+
deps: ["token"],
|
|
154
|
+
when: async (facts) => await validate(facts.token),
|
|
155
|
+
require: { type: "REFRESH" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Disabling Constraints at Runtime
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const system = createSystem({ module: myModule });
|
|
164
|
+
system.start();
|
|
165
|
+
|
|
166
|
+
// Disable a constraint — it won't be evaluated
|
|
167
|
+
system.constraints.disable("fetchUserWhenReady");
|
|
168
|
+
|
|
169
|
+
// Check if disabled
|
|
170
|
+
system.constraints.isDisabled("fetchUserWhenReady"); // true
|
|
171
|
+
|
|
172
|
+
// Re-enable — triggers re-evaluation on next cycle
|
|
173
|
+
system.constraints.enable("fetchUserWhenReady");
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Common Mistakes
|
|
177
|
+
|
|
178
|
+
### Putting async logic in resolvers instead of constraints
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// WRONG — resolver checks conditions (constraint's job)
|
|
182
|
+
resolvers: {
|
|
183
|
+
fetchData: {
|
|
184
|
+
requirement: "FETCH",
|
|
185
|
+
resolve: async (req, context) => {
|
|
186
|
+
if (!context.facts.isAuthenticated) {
|
|
187
|
+
return; // Should be in constraint's when()
|
|
188
|
+
}
|
|
189
|
+
// ...
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// CORRECT — constraint declares when, resolver just does the work
|
|
195
|
+
constraints: {
|
|
196
|
+
fetchWhenAuth: {
|
|
197
|
+
when: (facts) => facts.isAuthenticated && !facts.data,
|
|
198
|
+
require: { type: "FETCH" },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
resolvers: {
|
|
203
|
+
fetchData: {
|
|
204
|
+
requirement: "FETCH",
|
|
205
|
+
resolve: async (req, context) => {
|
|
206
|
+
const res = await fetch("/api/data");
|
|
207
|
+
context.facts.data = await res.json();
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### String literal for require
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// WRONG — require must be an object
|
|
217
|
+
require: "FETCH_DATA",
|
|
218
|
+
|
|
219
|
+
// CORRECT — object with type property
|
|
220
|
+
require: { type: "FETCH_DATA" },
|
|
221
|
+
|
|
222
|
+
// CORRECT — with payload
|
|
223
|
+
require: { type: "FETCH_DATA", endpoint: "/api/users" },
|
|
224
|
+
|
|
225
|
+
// CORRECT — dynamic from facts
|
|
226
|
+
require: (facts) => ({ type: "FETCH_DATA", userId: facts.currentUserId }),
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Returning null to conditionally skip
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// require can return null to suppress the requirement
|
|
233
|
+
constraints: {
|
|
234
|
+
conditionalFetch: {
|
|
235
|
+
when: (facts) => facts.needsUpdate,
|
|
236
|
+
require: (facts) => {
|
|
237
|
+
if (!facts.userId) {
|
|
238
|
+
return null; // No requirement emitted
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { type: "FETCH_USER", userId: facts.userId };
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Constraints vs Effects vs Derivations
|
|
248
|
+
|
|
249
|
+
| Feature | Purpose | Triggers |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| Constraint | Declare a need (emit requirement) | Fact changes, re-evaluated automatically |
|
|
252
|
+
| Resolver | Fulfill a need (async work) | Requirement emitted by constraint |
|
|
253
|
+
| Effect | React to changes (fire-and-forget) | Fact changes, runs after reconciliation |
|
|
254
|
+
| Derivation | Compute a value (synchronous, cached) | Fact changes, recomputed lazily |
|
|
255
|
+
|
|
256
|
+
### When to Use Which
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
"When X is true, the system needs Y" → Constraint
|
|
260
|
+
"Do Y" → Resolver
|
|
261
|
+
"Whenever X changes, log it" → Effect
|
|
262
|
+
"X is always facts.a + facts.b" → Derivation
|
|
263
|
+
```
|