@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,327 @@
|
|
|
1
|
+
# Multi-Module Systems
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/core` — namespaced multi-module systems, cross-module deps, `facts.self.*`.
|
|
4
|
+
|
|
5
|
+
How to compose multiple modules into a namespaced system with cross-module type safety.
|
|
6
|
+
|
|
7
|
+
## Decision Tree: "Single or Multi-Module?"
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
How many state domains?
|
|
11
|
+
├── One → createSystem({ module: myModule })
|
|
12
|
+
│ Direct access: system.facts.count
|
|
13
|
+
│
|
|
14
|
+
└── Two or more → createSystem({ modules: { auth, cart, ui } })
|
|
15
|
+
Namespaced: system.facts.auth.token
|
|
16
|
+
|
|
17
|
+
Does module A need to read module B's state?
|
|
18
|
+
├── No → No crossModuleDeps needed
|
|
19
|
+
│
|
|
20
|
+
└── Yes → Declare crossModuleDeps on the consuming module
|
|
21
|
+
Own facts at facts.self.*, other at facts.otherModule.*
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Creating a Multi-Module System
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createModule, createSystem, t } from "@directive-run/core";
|
|
28
|
+
|
|
29
|
+
const authModule = createModule("auth", {
|
|
30
|
+
schema: {
|
|
31
|
+
facts: {
|
|
32
|
+
token: t.string<string | null>(),
|
|
33
|
+
isAuthenticated: t.boolean(),
|
|
34
|
+
},
|
|
35
|
+
events: {
|
|
36
|
+
login: { token: t.string() },
|
|
37
|
+
logout: {},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
init: (facts) => {
|
|
41
|
+
facts.token = null;
|
|
42
|
+
facts.isAuthenticated = false;
|
|
43
|
+
},
|
|
44
|
+
events: {
|
|
45
|
+
login: (facts, payload) => {
|
|
46
|
+
facts.token = payload.token;
|
|
47
|
+
facts.isAuthenticated = true;
|
|
48
|
+
},
|
|
49
|
+
logout: (facts) => {
|
|
50
|
+
facts.token = null;
|
|
51
|
+
facts.isAuthenticated = false;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const cartModule = createModule("cart", {
|
|
57
|
+
schema: {
|
|
58
|
+
facts: {
|
|
59
|
+
items: t.array(t.object<{ id: string; qty: number }>()),
|
|
60
|
+
},
|
|
61
|
+
derivations: {
|
|
62
|
+
itemCount: t.number(),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
init: (facts) => {
|
|
66
|
+
facts.items = [];
|
|
67
|
+
},
|
|
68
|
+
derive: {
|
|
69
|
+
itemCount: (facts) => facts.items.length,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Multi-module system – namespaced access
|
|
74
|
+
const system = createSystem({
|
|
75
|
+
modules: { auth: authModule, cart: cartModule },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
system.start();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Accessing Namespaced State
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Facts – namespaced under module name
|
|
85
|
+
system.facts.auth.token;
|
|
86
|
+
system.facts.auth.isAuthenticated;
|
|
87
|
+
system.facts.cart.items;
|
|
88
|
+
|
|
89
|
+
// Derivations – namespaced under module name
|
|
90
|
+
system.derive.cart.itemCount;
|
|
91
|
+
|
|
92
|
+
// Events – namespaced under module name
|
|
93
|
+
system.events.auth.login({ token: "abc123" });
|
|
94
|
+
system.events.auth.logout();
|
|
95
|
+
|
|
96
|
+
// Subscribe – use "namespace.key" format
|
|
97
|
+
system.subscribe(["auth.token", "cart.items"], () => {
|
|
98
|
+
console.log("auth or cart changed");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Watch – use "namespace.key" format
|
|
102
|
+
system.watch("auth.isAuthenticated", (newVal, oldVal) => {
|
|
103
|
+
console.log(`Auth: ${oldVal} -> ${newVal}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Subscribe to all keys in a module
|
|
107
|
+
system.subscribeModule("cart", () => {
|
|
108
|
+
console.log("anything in cart changed");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Wait for condition – facts are namespaced
|
|
112
|
+
await system.when((facts) => facts.auth.isAuthenticated);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Cross-Module Dependencies
|
|
116
|
+
|
|
117
|
+
When a module needs to read another module's facts in its constraints, effects, or derivations, declare `crossModuleDeps`.
|
|
118
|
+
|
|
119
|
+
### Extracting the Schema
|
|
120
|
+
|
|
121
|
+
Export the schema separately from the module so other modules can reference it:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// modules/auth.ts
|
|
125
|
+
export const authSchema = {
|
|
126
|
+
facts: {
|
|
127
|
+
token: t.string<string | null>(),
|
|
128
|
+
isAuthenticated: t.boolean(),
|
|
129
|
+
},
|
|
130
|
+
events: {
|
|
131
|
+
login: { token: t.string() },
|
|
132
|
+
logout: {},
|
|
133
|
+
},
|
|
134
|
+
} as const;
|
|
135
|
+
|
|
136
|
+
export const authModule = createModule("auth", {
|
|
137
|
+
schema: authSchema,
|
|
138
|
+
init: (facts) => {
|
|
139
|
+
facts.token = null;
|
|
140
|
+
facts.isAuthenticated = false;
|
|
141
|
+
},
|
|
142
|
+
// ...
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Declaring crossModuleDeps
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// modules/data.ts
|
|
150
|
+
import { authSchema } from "./auth";
|
|
151
|
+
|
|
152
|
+
const dataModule = createModule("data", {
|
|
153
|
+
schema: {
|
|
154
|
+
facts: {
|
|
155
|
+
items: t.array(t.string()),
|
|
156
|
+
loaded: t.boolean(),
|
|
157
|
+
},
|
|
158
|
+
requirements: {
|
|
159
|
+
FETCH_ITEMS: {},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Declare cross-module dependency
|
|
164
|
+
crossModuleDeps: { auth: authSchema },
|
|
165
|
+
|
|
166
|
+
init: (facts) => {
|
|
167
|
+
facts.items = [];
|
|
168
|
+
facts.loaded = false;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// CORRECT – facts.self for own module, facts.auth for cross-module
|
|
172
|
+
constraints: {
|
|
173
|
+
fetchWhenAuth: {
|
|
174
|
+
when: (facts) => facts.auth.isAuthenticated && !facts.self.loaded,
|
|
175
|
+
require: { type: "FETCH_ITEMS" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
resolvers: {
|
|
180
|
+
fetchItems: {
|
|
181
|
+
requirement: "FETCH_ITEMS",
|
|
182
|
+
resolve: async (req, context) => {
|
|
183
|
+
const res = await fetch("/api/items");
|
|
184
|
+
context.facts.items = await res.json();
|
|
185
|
+
context.facts.loaded = true;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Effects also get cross-module facts
|
|
191
|
+
effects: {
|
|
192
|
+
onAuthChange: {
|
|
193
|
+
run: (facts, prev) => {
|
|
194
|
+
if (prev && prev.auth.isAuthenticated && !facts.auth.isAuthenticated) {
|
|
195
|
+
console.log("User logged out, clearing data");
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Common Mistakes
|
|
204
|
+
|
|
205
|
+
### Using bare `facts.*` instead of `facts.self.*`
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// WRONG – in cross-module context, bare facts has no self-module properties
|
|
209
|
+
constraints: {
|
|
210
|
+
check: {
|
|
211
|
+
when: (facts) => facts.loaded, // TypeScript error
|
|
212
|
+
require: { type: "FETCH" },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// CORRECT – use facts.self for own module
|
|
217
|
+
constraints: {
|
|
218
|
+
check: {
|
|
219
|
+
when: (facts) => facts.self.loaded,
|
|
220
|
+
require: { type: "FETCH" },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Bracket notation for internal keys
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// WRONG – the :: separator is internal, never use it directly
|
|
229
|
+
system.facts["auth::token"];
|
|
230
|
+
system.read("auth::status");
|
|
231
|
+
|
|
232
|
+
// CORRECT – dot notation through namespace proxy
|
|
233
|
+
system.facts.auth.token;
|
|
234
|
+
system.read("auth.status");
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Forgetting crossModuleDeps
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// WRONG – facts.auth is untyped without crossModuleDeps
|
|
241
|
+
const dataModule = createModule("data", {
|
|
242
|
+
schema: { facts: { items: t.array(t.string()) } },
|
|
243
|
+
constraints: {
|
|
244
|
+
check: {
|
|
245
|
+
when: (facts) => facts.auth.isAuthenticated, // No type info
|
|
246
|
+
require: { type: "FETCH" },
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// CORRECT – declare the dependency
|
|
252
|
+
const dataModule = createModule("data", {
|
|
253
|
+
schema: { facts: { items: t.array(t.string()) } },
|
|
254
|
+
crossModuleDeps: { auth: authSchema },
|
|
255
|
+
constraints: {
|
|
256
|
+
check: {
|
|
257
|
+
when: (facts) => facts.auth.isAuthenticated, // Fully typed
|
|
258
|
+
require: { type: "FETCH" },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## System Configuration Options
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const system = createSystem({
|
|
268
|
+
modules: { auth: authModule, cart: cartModule, data: dataModule },
|
|
269
|
+
|
|
270
|
+
// Initial facts – applied after init(), before first reconciliation
|
|
271
|
+
initialFacts: {
|
|
272
|
+
auth: { token: "restored-token" },
|
|
273
|
+
cart: { items: cachedItems },
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// Init order – control module initialization sequence
|
|
277
|
+
initOrder: "auto", // Sort by crossModuleDeps topology (default)
|
|
278
|
+
// initOrder: "declaration", // Use object key order
|
|
279
|
+
// initOrder: ["auth", "data", "cart"], // Explicit order
|
|
280
|
+
|
|
281
|
+
plugins: [loggingPlugin()],
|
|
282
|
+
history: true,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Hydrate from async source (call before start)
|
|
286
|
+
await system.hydrate(async () => {
|
|
287
|
+
const stored = localStorage.getItem("app-state");
|
|
288
|
+
|
|
289
|
+
return stored ? JSON.parse(stored) : {};
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
system.start();
|
|
293
|
+
await system.settle();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Dynamic Module Registration
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Lazy-load and register a module at runtime
|
|
300
|
+
const chatModule = await import("./modules/chat");
|
|
301
|
+
system.registerModule("chat", chatModule.default);
|
|
302
|
+
|
|
303
|
+
// Now accessible at system.facts.chat.*, system.events.chat.*, etc.
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Cross-Module Events
|
|
307
|
+
|
|
308
|
+
Events are namespaced at the system level. **Prefer the typed events accessor** — it carries autocomplete and per-event payload typing.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Multi-module events — canonical form
|
|
312
|
+
system.events.auth.login({ token: "abc" });
|
|
313
|
+
system.events.cart.addItem({ id: "item-1", qty: 1 });
|
|
314
|
+
|
|
315
|
+
// dispatch() is supported when you need to forward a programmatically-built
|
|
316
|
+
// event (e.g., replaying a serialized event). The string form
|
|
317
|
+
// (system.dispatch("login", payload)) does NOT exist — only the single-arg
|
|
318
|
+
// object form is valid.
|
|
319
|
+
system.dispatch({ type: "login", token: "abc" });
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## See also
|
|
323
|
+
|
|
324
|
+
- [`core-patterns.md`](./core-patterns.md) — start here for the single-module shape before going namespaced
|
|
325
|
+
- [`system-api.md`](./system-api.md) — `createSystem({ modules: { ... } })` and the namespaced facts proxy
|
|
326
|
+
- [`constraints.md`](./constraints.md) — `crossModuleDeps` for constraints that fire on facts owned by another module
|
|
327
|
+
- [`naming.md`](./naming.md) — `facts.self.*` convention; dot-notation vs bracket-notation rules
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Plugins
|
|
2
|
+
|
|
3
|
+
> Covers `@directive-run/core/plugins` — plugin authoring + built-ins (logging, devtools, persistence, audit-ledger, observability, OTLP).
|
|
4
|
+
|
|
5
|
+
Plugins extend Directive systems with cross-cutting functionality like logging, persistence, devtools, and resilience patterns.
|
|
6
|
+
|
|
7
|
+
## Decision Tree: "Which plugin do I need?"
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
What do you want?
|
|
11
|
+
├── See state changes in console → loggingPlugin()
|
|
12
|
+
├── Connect to browser DevTools → devtoolsPlugin()
|
|
13
|
+
├── Persist state across reloads → persistencePlugin(config)
|
|
14
|
+
├── Protect resolvers from cascading failures → createCircuitBreaker(config)
|
|
15
|
+
├── Track metrics, traces, alerts → createObservability(config)
|
|
16
|
+
└── Custom cross-cutting behavior → Write a custom plugin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Using Built-In Plugins
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { createSystem } from "@directive-run/core";
|
|
23
|
+
import {
|
|
24
|
+
devtoolsPlugin,
|
|
25
|
+
loggingPlugin,
|
|
26
|
+
persistencePlugin,
|
|
27
|
+
} from "@directive-run/core/plugins";
|
|
28
|
+
|
|
29
|
+
const system = createSystem({
|
|
30
|
+
module: myModule,
|
|
31
|
+
plugins: [
|
|
32
|
+
devtoolsPlugin(),
|
|
33
|
+
loggingPlugin({ level: "debug" }),
|
|
34
|
+
persistencePlugin({
|
|
35
|
+
key: "my-app-state",
|
|
36
|
+
storage: localStorage,
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Plugins are applied in order. Place logging first to capture all events including those from other plugins.
|
|
43
|
+
|
|
44
|
+
## loggingPlugin
|
|
45
|
+
|
|
46
|
+
Logs state changes, requirements, and resolutions to the console.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { loggingPlugin } from "@directive-run/core/plugins";
|
|
50
|
+
|
|
51
|
+
// Default – logs facts changes and resolver start/complete at "info" level
|
|
52
|
+
loggingPlugin()
|
|
53
|
+
|
|
54
|
+
// Debug level – logs everything including derivation recomputation and constraint evaluation
|
|
55
|
+
loggingPlugin({ level: "debug" })
|
|
56
|
+
|
|
57
|
+
// Custom filter – only log specific events
|
|
58
|
+
loggingPlugin({
|
|
59
|
+
filter: (event) => event.startsWith("resolver."),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Custom logger and prefix
|
|
63
|
+
loggingPlugin({
|
|
64
|
+
level: "warn",
|
|
65
|
+
prefix: "[MyApp]",
|
|
66
|
+
logger: customLogger,
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Options: `level` ("debug" | "info" | "warn" | "error"), `filter` (predicate on event name string), `logger` (Console-compatible object), `prefix` (string, default "[Directive]").
|
|
71
|
+
|
|
72
|
+
## devtoolsPlugin
|
|
73
|
+
|
|
74
|
+
Connects to the Directive DevTools browser extension for visual state inspection.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
78
|
+
|
|
79
|
+
// Default
|
|
80
|
+
devtoolsPlugin()
|
|
81
|
+
|
|
82
|
+
// With options
|
|
83
|
+
devtoolsPlugin({
|
|
84
|
+
name: "My App", // Name shown in DevTools
|
|
85
|
+
maxEvents: 1000, // Max trace events to retain (default: 1000)
|
|
86
|
+
trace: true, // Enable trace logging
|
|
87
|
+
panel: true, // Show floating debug panel (dev mode only)
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Only enable in development. The plugin is a no-op if the DevTools extension is not installed.
|
|
92
|
+
|
|
93
|
+
## persistencePlugin
|
|
94
|
+
|
|
95
|
+
Persists fact state to storage. Restores on system creation.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { persistencePlugin } from "@directive-run/core/plugins";
|
|
99
|
+
|
|
100
|
+
// localStorage (default)
|
|
101
|
+
persistencePlugin({
|
|
102
|
+
key: "my-app-state",
|
|
103
|
+
storage: localStorage,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// sessionStorage – cleared when tab closes
|
|
107
|
+
persistencePlugin({
|
|
108
|
+
key: "session-state",
|
|
109
|
+
storage: sessionStorage,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Custom storage adapter
|
|
113
|
+
persistencePlugin({
|
|
114
|
+
key: "my-app",
|
|
115
|
+
storage: {
|
|
116
|
+
getItem: (key) => myCustomStore.get(key),
|
|
117
|
+
setItem: (key, value) => myCustomStore.set(key, value),
|
|
118
|
+
removeItem: (key) => myCustomStore.delete(key),
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Selective persistence – only persist certain facts
|
|
123
|
+
persistencePlugin({
|
|
124
|
+
key: "my-app",
|
|
125
|
+
storage: localStorage,
|
|
126
|
+
include: ["user", "preferences"], // Only these facts
|
|
127
|
+
// OR
|
|
128
|
+
exclude: ["tempData", "sessionId"], // Everything except these
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// With callbacks
|
|
132
|
+
persistencePlugin({
|
|
133
|
+
key: "my-app",
|
|
134
|
+
storage: localStorage,
|
|
135
|
+
debounce: 200, // Debounce saves (default: 100ms)
|
|
136
|
+
onRestore: (data) => console.log("Restored:", data),
|
|
137
|
+
onSave: (data) => console.log("Saved:", data),
|
|
138
|
+
onError: (err) => console.error("Persistence error:", err),
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## createCircuitBreaker
|
|
143
|
+
|
|
144
|
+
Standalone utility (not a Plugin) that implements the circuit breaker pattern for resilience. Use it inside resolvers to protect against cascading failures from external services.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { createCircuitBreaker } from "@directive-run/core/plugins";
|
|
148
|
+
|
|
149
|
+
const breaker = createCircuitBreaker({
|
|
150
|
+
failureThreshold: 5, // Failures before opening (default: 5)
|
|
151
|
+
recoveryTimeMs: 30000, // Time before HALF_OPEN (default: 30000)
|
|
152
|
+
halfOpenMaxRequests: 3, // Requests allowed in HALF_OPEN (default: 3)
|
|
153
|
+
failureWindowMs: 60000, // Window for counting failures (default: 60000)
|
|
154
|
+
name: "api-breaker", // Name for metrics/errors
|
|
155
|
+
onStateChange: (from, to) => console.log(`Circuit: ${from} -> ${to}`),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Use inside a resolver
|
|
159
|
+
resolvers: {
|
|
160
|
+
fetchData: {
|
|
161
|
+
requirement: "FETCH_DATA",
|
|
162
|
+
resolve: async (req, context) => {
|
|
163
|
+
const result = await breaker.execute(async () => {
|
|
164
|
+
return await callExternalAPI();
|
|
165
|
+
});
|
|
166
|
+
context.facts.data = result;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Wire circuit state into constraints
|
|
172
|
+
constraints: {
|
|
173
|
+
apiDown: {
|
|
174
|
+
when: () => breaker.getState() === "OPEN",
|
|
175
|
+
require: { type: "FALLBACK_RESPONSE" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Circuit breaker states: **Closed** (normal) -> **Open** (failing, rejects immediately) -> **Half-Open** (testing limited requests).
|
|
181
|
+
|
|
182
|
+
## createObservability
|
|
183
|
+
|
|
184
|
+
Metrics, tracing, and alerting for production systems.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { createObservability } from "@directive-run/core/plugins";
|
|
188
|
+
|
|
189
|
+
const system = createSystem({
|
|
190
|
+
module: myModule,
|
|
191
|
+
plugins: [
|
|
192
|
+
createObservability({
|
|
193
|
+
metrics: {
|
|
194
|
+
enabled: true,
|
|
195
|
+
// Track resolver duration, constraint evaluation count, etc.
|
|
196
|
+
},
|
|
197
|
+
tracing: {
|
|
198
|
+
enabled: true,
|
|
199
|
+
// Trace resolver execution with spans
|
|
200
|
+
},
|
|
201
|
+
alerts: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
onAlert: (alert) => {
|
|
204
|
+
// Send to monitoring service
|
|
205
|
+
sendToDatadog(alert);
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Plugin Lifecycle Hooks
|
|
214
|
+
|
|
215
|
+
Plugins hook into the system lifecycle. Use these to build custom plugins.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import type { Plugin, ModuleSchema } from "@directive-run/core";
|
|
219
|
+
|
|
220
|
+
const myPlugin: Plugin<ModuleSchema> = {
|
|
221
|
+
name: "my-custom-plugin",
|
|
222
|
+
|
|
223
|
+
// System lifecycle
|
|
224
|
+
onInit: (system) => {
|
|
225
|
+
// Called when system is created (only async hook)
|
|
226
|
+
},
|
|
227
|
+
onStart: (system) => {
|
|
228
|
+
// Called when system.start() is invoked
|
|
229
|
+
},
|
|
230
|
+
onStop: (system) => {
|
|
231
|
+
// Called when system.stop() is invoked
|
|
232
|
+
},
|
|
233
|
+
onDestroy: (system) => {
|
|
234
|
+
// Called when system.destroy() is invoked
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// Fact tracking
|
|
238
|
+
onFactSet: (key, value, prev) => {
|
|
239
|
+
// Called when a single fact is set
|
|
240
|
+
},
|
|
241
|
+
onFactsBatch: (changes) => {
|
|
242
|
+
// Called after a batch of fact changes completes
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Requirement pipeline
|
|
246
|
+
onRequirementCreated: (requirement) => {
|
|
247
|
+
// Called when a constraint emits a requirement
|
|
248
|
+
// requirement.type, requirement.id
|
|
249
|
+
},
|
|
250
|
+
onRequirementMet: (requirement, byResolver) => {
|
|
251
|
+
// Called when a requirement is fulfilled
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// Resolver pipeline
|
|
255
|
+
onResolverStart: (resolverId, requirement) => {
|
|
256
|
+
// Called when a resolver begins executing
|
|
257
|
+
},
|
|
258
|
+
onResolverComplete: (resolverId, requirement, duration) => {
|
|
259
|
+
// Called when a resolver finishes successfully
|
|
260
|
+
},
|
|
261
|
+
onResolverError: (resolverId, requirement, error) => {
|
|
262
|
+
// Called when a resolver fails (after all retries)
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// Error handling
|
|
266
|
+
onError: (error) => {
|
|
267
|
+
// Called on any DirectiveError in the system
|
|
268
|
+
// error.source, error.message, error.context
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const system = createSystem({
|
|
273
|
+
module: myModule,
|
|
274
|
+
plugins: [myPlugin],
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Common Mistakes
|
|
279
|
+
|
|
280
|
+
### Enabling devtools in production
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// WRONG – devtools overhead in production
|
|
284
|
+
const system = createSystem({
|
|
285
|
+
module: myModule,
|
|
286
|
+
plugins: [devtoolsPlugin()],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// CORRECT – conditional on environment
|
|
290
|
+
const plugins = [];
|
|
291
|
+
if (process.env.NODE_ENV === "development") {
|
|
292
|
+
plugins.push(devtoolsPlugin());
|
|
293
|
+
plugins.push(loggingPlugin({ level: "debug" }));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const system = createSystem({
|
|
297
|
+
module: myModule,
|
|
298
|
+
plugins,
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Persistence without filtering
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// WRONG – persists everything including transient state
|
|
306
|
+
persistencePlugin({
|
|
307
|
+
key: "app-state",
|
|
308
|
+
storage: localStorage,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// CORRECT – use include/exclude to control what's persisted
|
|
312
|
+
persistencePlugin({
|
|
313
|
+
key: "app-state",
|
|
314
|
+
storage: localStorage,
|
|
315
|
+
include: ["user", "preferences", "settings"],
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Plugin order matters
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// WRONG – logging misses events from persistence restore
|
|
323
|
+
const system = createSystem({
|
|
324
|
+
module: myModule,
|
|
325
|
+
plugins: [
|
|
326
|
+
persistencePlugin({ key: "app", storage: localStorage }),
|
|
327
|
+
loggingPlugin(), // Misses restore events
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// CORRECT – logging first to capture everything
|
|
332
|
+
const system = createSystem({
|
|
333
|
+
module: myModule,
|
|
334
|
+
plugins: [
|
|
335
|
+
loggingPlugin(),
|
|
336
|
+
persistencePlugin({ key: "app", storage: localStorage }),
|
|
337
|
+
],
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Persisting sensitive or transient data
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// WRONG – persists auth tokens and loading state
|
|
345
|
+
persistencePlugin({
|
|
346
|
+
key: "app",
|
|
347
|
+
storage: localStorage,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// CORRECT – exclude sensitive and transient facts
|
|
351
|
+
persistencePlugin({
|
|
352
|
+
key: "app",
|
|
353
|
+
storage: localStorage,
|
|
354
|
+
exclude: ["authToken", "isLoading", "error"],
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## See also
|
|
359
|
+
|
|
360
|
+
- [`system-api.md`](./system-api.md) — `plugins:` option on `createSystem` and the lifecycle hooks plugins observe
|
|
361
|
+
- [`error-boundaries.md`](./error-boundaries.md) — `createCircuitBreaker` and the resilience plugins that pair with error boundaries
|