@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,315 @@
|
|
|
1
|
+
# Multi-Module Systems
|
|
2
|
+
|
|
3
|
+
How to compose multiple modules into a namespaced system with cross-module type safety.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "Single or Multi-Module?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
How many state domains?
|
|
9
|
+
├── One → createSystem({ module: myModule })
|
|
10
|
+
│ Direct access: system.facts.count
|
|
11
|
+
│
|
|
12
|
+
└── Two or more → createSystem({ modules: { auth, cart, ui } })
|
|
13
|
+
Namespaced: system.facts.auth.token
|
|
14
|
+
|
|
15
|
+
Does module A need to read module B's state?
|
|
16
|
+
├── No → No crossModuleDeps needed
|
|
17
|
+
│
|
|
18
|
+
└── Yes → Declare crossModuleDeps on the consuming module
|
|
19
|
+
Own facts at facts.self.*, other at facts.otherModule.*
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Creating a Multi-Module System
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createModule, createSystem, t } from "@directive-run/core";
|
|
26
|
+
|
|
27
|
+
const authModule = createModule("auth", {
|
|
28
|
+
schema: {
|
|
29
|
+
facts: {
|
|
30
|
+
token: t.string<string | null>(),
|
|
31
|
+
isAuthenticated: t.boolean(),
|
|
32
|
+
},
|
|
33
|
+
events: {
|
|
34
|
+
login: { token: t.string() },
|
|
35
|
+
logout: {},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
init: (facts) => {
|
|
39
|
+
facts.token = null;
|
|
40
|
+
facts.isAuthenticated = false;
|
|
41
|
+
},
|
|
42
|
+
events: {
|
|
43
|
+
login: (facts, payload) => {
|
|
44
|
+
facts.token = payload.token;
|
|
45
|
+
facts.isAuthenticated = true;
|
|
46
|
+
},
|
|
47
|
+
logout: (facts) => {
|
|
48
|
+
facts.token = null;
|
|
49
|
+
facts.isAuthenticated = false;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const cartModule = createModule("cart", {
|
|
55
|
+
schema: {
|
|
56
|
+
facts: {
|
|
57
|
+
items: t.array(t.object<{ id: string; qty: number }>()),
|
|
58
|
+
},
|
|
59
|
+
derivations: {
|
|
60
|
+
itemCount: t.number(),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
init: (facts) => {
|
|
64
|
+
facts.items = [];
|
|
65
|
+
},
|
|
66
|
+
derive: {
|
|
67
|
+
itemCount: (facts) => facts.items.length,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Multi-module system — namespaced access
|
|
72
|
+
const system = createSystem({
|
|
73
|
+
modules: { auth: authModule, cart: cartModule },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
system.start();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Accessing Namespaced State
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Facts — namespaced under module name
|
|
83
|
+
system.facts.auth.token;
|
|
84
|
+
system.facts.auth.isAuthenticated;
|
|
85
|
+
system.facts.cart.items;
|
|
86
|
+
|
|
87
|
+
// Derivations — namespaced under module name
|
|
88
|
+
system.derive.cart.itemCount;
|
|
89
|
+
|
|
90
|
+
// Events — namespaced under module name
|
|
91
|
+
system.events.auth.login({ token: "abc123" });
|
|
92
|
+
system.events.auth.logout();
|
|
93
|
+
|
|
94
|
+
// Subscribe — use "namespace.key" format
|
|
95
|
+
system.subscribe(["auth.token", "cart.items"], () => {
|
|
96
|
+
console.log("auth or cart changed");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Watch — use "namespace.key" format
|
|
100
|
+
system.watch("auth.isAuthenticated", (newVal, oldVal) => {
|
|
101
|
+
console.log(`Auth: ${oldVal} -> ${newVal}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Subscribe to all keys in a module
|
|
105
|
+
system.subscribeModule("cart", () => {
|
|
106
|
+
console.log("anything in cart changed");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Wait for condition — facts are namespaced
|
|
110
|
+
await system.when((facts) => facts.auth.isAuthenticated);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Cross-Module Dependencies
|
|
114
|
+
|
|
115
|
+
When a module needs to read another module's facts in its constraints, effects, or derivations, declare `crossModuleDeps`.
|
|
116
|
+
|
|
117
|
+
### Extracting the Schema
|
|
118
|
+
|
|
119
|
+
Export the schema separately from the module so other modules can reference it:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// modules/auth.ts
|
|
123
|
+
export const authSchema = {
|
|
124
|
+
facts: {
|
|
125
|
+
token: t.string<string | null>(),
|
|
126
|
+
isAuthenticated: t.boolean(),
|
|
127
|
+
},
|
|
128
|
+
events: {
|
|
129
|
+
login: { token: t.string() },
|
|
130
|
+
logout: {},
|
|
131
|
+
},
|
|
132
|
+
} as const;
|
|
133
|
+
|
|
134
|
+
export const authModule = createModule("auth", {
|
|
135
|
+
schema: authSchema,
|
|
136
|
+
init: (facts) => {
|
|
137
|
+
facts.token = null;
|
|
138
|
+
facts.isAuthenticated = false;
|
|
139
|
+
},
|
|
140
|
+
// ...
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Declaring crossModuleDeps
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// modules/data.ts
|
|
148
|
+
import { authSchema } from "./auth";
|
|
149
|
+
|
|
150
|
+
const dataModule = createModule("data", {
|
|
151
|
+
schema: {
|
|
152
|
+
facts: {
|
|
153
|
+
items: t.array(t.string()),
|
|
154
|
+
loaded: t.boolean(),
|
|
155
|
+
},
|
|
156
|
+
requirements: {
|
|
157
|
+
FETCH_ITEMS: {},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Declare cross-module dependency
|
|
162
|
+
crossModuleDeps: { auth: authSchema },
|
|
163
|
+
|
|
164
|
+
init: (facts) => {
|
|
165
|
+
facts.items = [];
|
|
166
|
+
facts.loaded = false;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// CORRECT — facts.self for own module, facts.auth for cross-module
|
|
170
|
+
constraints: {
|
|
171
|
+
fetchWhenAuth: {
|
|
172
|
+
when: (facts) => facts.auth.isAuthenticated && !facts.self.loaded,
|
|
173
|
+
require: { type: "FETCH_ITEMS" },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
resolvers: {
|
|
178
|
+
fetchItems: {
|
|
179
|
+
requirement: "FETCH_ITEMS",
|
|
180
|
+
resolve: async (req, context) => {
|
|
181
|
+
const res = await fetch("/api/items");
|
|
182
|
+
context.facts.items = await res.json();
|
|
183
|
+
context.facts.loaded = true;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Effects also get cross-module facts
|
|
189
|
+
effects: {
|
|
190
|
+
onAuthChange: {
|
|
191
|
+
run: (facts, prev) => {
|
|
192
|
+
if (prev && prev.auth.isAuthenticated && !facts.auth.isAuthenticated) {
|
|
193
|
+
console.log("User logged out, clearing data");
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Common Mistakes
|
|
202
|
+
|
|
203
|
+
### Using bare `facts.*` instead of `facts.self.*`
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// WRONG — in cross-module context, bare facts has no self-module properties
|
|
207
|
+
constraints: {
|
|
208
|
+
check: {
|
|
209
|
+
when: (facts) => facts.loaded, // TypeScript error
|
|
210
|
+
require: { type: "FETCH" },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// CORRECT — use facts.self for own module
|
|
215
|
+
constraints: {
|
|
216
|
+
check: {
|
|
217
|
+
when: (facts) => facts.self.loaded,
|
|
218
|
+
require: { type: "FETCH" },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Bracket notation for internal keys
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// WRONG — the :: separator is internal, never use it directly
|
|
227
|
+
system.facts["auth::token"];
|
|
228
|
+
system.read("auth::status");
|
|
229
|
+
|
|
230
|
+
// CORRECT — dot notation through namespace proxy
|
|
231
|
+
system.facts.auth.token;
|
|
232
|
+
system.read("auth.status");
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Forgetting crossModuleDeps
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// WRONG — facts.auth is untyped without crossModuleDeps
|
|
239
|
+
const dataModule = createModule("data", {
|
|
240
|
+
schema: { facts: { items: t.array(t.string()) } },
|
|
241
|
+
constraints: {
|
|
242
|
+
check: {
|
|
243
|
+
when: (facts) => facts.auth.isAuthenticated, // No type info
|
|
244
|
+
require: { type: "FETCH" },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// CORRECT — declare the dependency
|
|
250
|
+
const dataModule = createModule("data", {
|
|
251
|
+
schema: { facts: { items: t.array(t.string()) } },
|
|
252
|
+
crossModuleDeps: { auth: authSchema },
|
|
253
|
+
constraints: {
|
|
254
|
+
check: {
|
|
255
|
+
when: (facts) => facts.auth.isAuthenticated, // Fully typed
|
|
256
|
+
require: { type: "FETCH" },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## System Configuration Options
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const system = createSystem({
|
|
266
|
+
modules: { auth: authModule, cart: cartModule, data: dataModule },
|
|
267
|
+
|
|
268
|
+
// Initial facts — applied after init(), before first reconciliation
|
|
269
|
+
initialFacts: {
|
|
270
|
+
auth: { token: "restored-token" },
|
|
271
|
+
cart: { items: cachedItems },
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// Init order — control module initialization sequence
|
|
275
|
+
initOrder: "auto", // Sort by crossModuleDeps topology (default)
|
|
276
|
+
// initOrder: "declaration", // Use object key order
|
|
277
|
+
// initOrder: ["auth", "data", "cart"], // Explicit order
|
|
278
|
+
|
|
279
|
+
plugins: [loggingPlugin()],
|
|
280
|
+
debug: { timeTravel: true },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Hydrate from async source (call before start)
|
|
284
|
+
await system.hydrate(async () => {
|
|
285
|
+
const stored = localStorage.getItem("app-state");
|
|
286
|
+
|
|
287
|
+
return stored ? JSON.parse(stored) : {};
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
system.start();
|
|
291
|
+
await system.settle();
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Dynamic Module Registration
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// Lazy-load and register a module at runtime
|
|
298
|
+
const chatModule = await import("./modules/chat");
|
|
299
|
+
system.registerModule("chat", chatModule.default);
|
|
300
|
+
|
|
301
|
+
// Now accessible at system.facts.chat.*, system.events.chat.*, etc.
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Cross-Module Events
|
|
305
|
+
|
|
306
|
+
Events are namespaced at the system level but dispatched through the events accessor:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Multi-module events
|
|
310
|
+
system.events.auth.login({ token: "abc" });
|
|
311
|
+
system.events.cart.addItem({ id: "item-1", qty: 1 });
|
|
312
|
+
|
|
313
|
+
// dispatch() also works with type discriminator
|
|
314
|
+
system.dispatch({ type: "login", token: "abc" });
|
|
315
|
+
```
|
package/core/naming.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Naming Conventions
|
|
2
|
+
|
|
3
|
+
Directive naming rules that AI coding assistants must follow. These are non-negotiable project conventions.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "What do I call this?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Resolver parameter names?
|
|
9
|
+
→ (req, context) req = requirement, NEVER "request"
|
|
10
|
+
→ NEVER (req, ctx) context is NEVER abbreviated
|
|
11
|
+
|
|
12
|
+
Computed values?
|
|
13
|
+
→ "derivations" / derive NEVER "computed", "selectors", "getters"
|
|
14
|
+
→ system.derive.myValue NEVER system.computed.myValue
|
|
15
|
+
|
|
16
|
+
State values?
|
|
17
|
+
→ "facts" NEVER "state", "store", "atoms"
|
|
18
|
+
→ system.facts.count NEVER system.state.count
|
|
19
|
+
|
|
20
|
+
Conditional triggers?
|
|
21
|
+
→ "constraints" NEVER "rules", "conditions", "triggers"
|
|
22
|
+
→ when() + require() NEVER if/then, trigger/action
|
|
23
|
+
|
|
24
|
+
Fulfillment logic?
|
|
25
|
+
→ "resolvers" NEVER "handlers", "actions", "reducers"
|
|
26
|
+
→ resolve(req, context) NEVER handle(req, ctx)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Parameter Naming
|
|
30
|
+
|
|
31
|
+
### `req` = requirement (NOT request)
|
|
32
|
+
|
|
33
|
+
The `req` parameter in resolvers and constraint `key()` functions is short for **requirement** -- the object emitted by a constraint's `require` property.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// CORRECT — req is a requirement
|
|
37
|
+
resolvers: {
|
|
38
|
+
fetchUser: {
|
|
39
|
+
requirement: "FETCH_USER",
|
|
40
|
+
key: (req) => `fetch-${req.userId}`,
|
|
41
|
+
resolve: async (req, context) => {
|
|
42
|
+
// req.type === "FETCH_USER"
|
|
43
|
+
// req.userId from the requirement payload
|
|
44
|
+
const user = await fetchUser(req.userId);
|
|
45
|
+
context.facts.user = user;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// WRONG — never use "request" or "r"
|
|
51
|
+
resolve: async (request, context) => { /* ... */ },
|
|
52
|
+
resolve: async (r, context) => { /* ... */ },
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `context` is Never Abbreviated
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// CORRECT
|
|
59
|
+
resolve: async (req, context) => {
|
|
60
|
+
context.facts.status = "loaded";
|
|
61
|
+
context.signal; // AbortSignal
|
|
62
|
+
context.snapshot(); // facts snapshot
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// WRONG — never abbreviate to ctx
|
|
66
|
+
resolve: async (req, ctx) => { /* ... */ },
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Return Style
|
|
70
|
+
|
|
71
|
+
### Always Use Braces
|
|
72
|
+
|
|
73
|
+
No single-line returns. Always wrap in braces.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// WRONG
|
|
77
|
+
derive: {
|
|
78
|
+
isReady: (facts) => facts.phase === "ready",
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
constraints: {
|
|
82
|
+
check: {
|
|
83
|
+
when: (facts) => facts.count > 10,
|
|
84
|
+
require: { type: "PROCESS" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Wait -- the above IS correct for one-line arrow expressions.
|
|
89
|
+
// The brace rule applies to if/return blocks:
|
|
90
|
+
|
|
91
|
+
// WRONG — single-line if return
|
|
92
|
+
if (facts.user) return "ready";
|
|
93
|
+
|
|
94
|
+
// CORRECT — always use braces
|
|
95
|
+
if (facts.user) {
|
|
96
|
+
return "ready";
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Blank Line Before `return`
|
|
101
|
+
|
|
102
|
+
Add a blank line before `return` when there is code above it. Skip the blank line when `return` is the first statement in a block.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// CORRECT — blank line before return when code precedes it
|
|
106
|
+
function getStatus(facts) {
|
|
107
|
+
const phase = facts.phase;
|
|
108
|
+
const hasUser = facts.user !== null;
|
|
109
|
+
|
|
110
|
+
return phase === "ready" && hasUser;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// CORRECT — no blank line when return is first statement
|
|
114
|
+
function isReady(facts) {
|
|
115
|
+
return facts.phase === "ready";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// CORRECT — blank line after brace-style return block
|
|
119
|
+
function process(facts) {
|
|
120
|
+
if (!facts.ready) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = computeResult(facts);
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// WRONG — no blank line before return after code
|
|
130
|
+
function getStatus(facts) {
|
|
131
|
+
const phase = facts.phase;
|
|
132
|
+
return phase === "ready"; // Missing blank line
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Multi-Line Code Formatting
|
|
137
|
+
|
|
138
|
+
Never put properties or statements on a single line inside braces. Always expand to one item per line with proper indentation. This applies everywhere: schema definitions, init functions, events, effects, requirement types, and any other object or block.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// WRONG — properties crammed on one line
|
|
142
|
+
schema: {
|
|
143
|
+
facts: { phase: t.string(), count: t.number() },
|
|
144
|
+
requirements: { FETCH_USER: { id: t.string() }, RESET: {} },
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// CORRECT — one property per line, always expanded
|
|
148
|
+
schema: {
|
|
149
|
+
facts: {
|
|
150
|
+
phase: t.string(),
|
|
151
|
+
count: t.number(),
|
|
152
|
+
},
|
|
153
|
+
requirements: {
|
|
154
|
+
FETCH_USER: {
|
|
155
|
+
id: t.string(),
|
|
156
|
+
},
|
|
157
|
+
RESET: {},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// WRONG — statements crammed on one line
|
|
162
|
+
init: (facts) => { facts.phase = "idle"; facts.count = 0; },
|
|
163
|
+
|
|
164
|
+
// CORRECT — one statement per line
|
|
165
|
+
init: (facts) => {
|
|
166
|
+
facts.phase = "idle";
|
|
167
|
+
facts.count = 0;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// WRONG
|
|
171
|
+
events: { reset: (facts) => { facts.count = 0; facts.phase = "idle"; } },
|
|
172
|
+
|
|
173
|
+
// CORRECT
|
|
174
|
+
events: {
|
|
175
|
+
reset: (facts) => {
|
|
176
|
+
facts.count = 0;
|
|
177
|
+
facts.phase = "idle";
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Single-expression arrows (no braces) are fine on one line. Empty objects `{}` are fine inline.
|
|
183
|
+
```typescript
|
|
184
|
+
// OK — single expression, no braces
|
|
185
|
+
derive: {
|
|
186
|
+
isReady: (facts) => facts.phase === "ready",
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// OK — empty object
|
|
190
|
+
RESET: {},
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Multi-Module Naming
|
|
194
|
+
|
|
195
|
+
### `facts.self.*` for Own Module
|
|
196
|
+
|
|
197
|
+
In multi-module systems, constraints, effects, and derivations with `crossModuleDeps` receive namespaced facts. Own module facts are always at `facts.self.*`.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// CORRECT
|
|
201
|
+
constraints: {
|
|
202
|
+
loadWhenAuth: {
|
|
203
|
+
when: (facts) => facts.auth.isAuthenticated && !facts.self.loaded,
|
|
204
|
+
require: { type: "LOAD_DATA" },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// WRONG — bare facts.* in multi-module context
|
|
209
|
+
constraints: {
|
|
210
|
+
loadWhenAuth: {
|
|
211
|
+
when: (facts) => facts.isAuthenticated && !facts.loaded,
|
|
212
|
+
require: { type: "LOAD_DATA" },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### System-Level Access Uses Dot Notation
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// CORRECT — dot notation through namespace proxy
|
|
221
|
+
system.facts.auth.token;
|
|
222
|
+
system.facts.cart.items;
|
|
223
|
+
system.derive.auth.isLoggedIn;
|
|
224
|
+
system.events.auth.login({ token: "..." });
|
|
225
|
+
|
|
226
|
+
// WRONG — bracket notation with internal separator
|
|
227
|
+
system.facts["auth::token"];
|
|
228
|
+
system.facts["auth_token"];
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Type Casting Rules
|
|
232
|
+
|
|
233
|
+
### Never Cast When Reading
|
|
234
|
+
|
|
235
|
+
The schema provides all types. Do not add `as` casts when reading facts or derivations from the system.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// CORRECT — schema provides the type
|
|
239
|
+
const profile = system.facts.profile;
|
|
240
|
+
const isReady = system.derive.isReady;
|
|
241
|
+
|
|
242
|
+
// WRONG — unnecessary cast
|
|
243
|
+
const profile = system.facts.profile as UserProfile;
|
|
244
|
+
const isReady = system.derive.isReady as boolean;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Cast Only in Schema Definition
|
|
248
|
+
|
|
249
|
+
Type assertions are only valid in schema definition using the `{} as {}` pattern:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// CORRECT — cast in schema definition
|
|
253
|
+
schema: {
|
|
254
|
+
facts: {} as { profile: UserProfile; settings: AppSettings },
|
|
255
|
+
derivations: {} as { displayName: string },
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// OR use t.* builders (preferred)
|
|
259
|
+
schema: {
|
|
260
|
+
facts: {
|
|
261
|
+
profile: t.object<UserProfile>(),
|
|
262
|
+
settings: t.object<AppSettings>(),
|
|
263
|
+
},
|
|
264
|
+
derivations: {
|
|
265
|
+
displayName: t.string(),
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Terminology Quick Reference
|
|
271
|
+
|
|
272
|
+
| Directive Term | NEVER Use |
|
|
273
|
+
|---|---|
|
|
274
|
+
| facts | state, store, atoms, signals |
|
|
275
|
+
| derivations / derive | computed, selectors, getters, memos |
|
|
276
|
+
| constraints | rules, conditions, triggers, guards |
|
|
277
|
+
| resolvers | handlers, actions, reducers, sagas |
|
|
278
|
+
| requirements | requests, commands, intents |
|
|
279
|
+
| effects | watchers, subscriptions, reactions |
|
|
280
|
+
| module | slice, feature, domain |
|
|
281
|
+
| system | store, container, context |
|
|
282
|
+
| `req` (parameter) | request, r, requirement (spelled out) |
|
|
283
|
+
| `context` (parameter) | ctx, c, resolverContext |
|