| name | building-directive-systems |
| description | Compose Directive modules into systems: single-module and multi-module createSystem() calls, namespaced fact/derive/event access, cross-module dependencies, initialFacts and hydration, plugins (logging, devtools, persistence, circuit breaker), React adapter hooks (useSelector, useEvent, useSystem, DirectiveProvider), and system lifecycle. Use when creating a system, connecting modules together, adding plugins, or integrating Directive with React. |
Building Directive Systems
Prerequisites
This skill applies when the project uses @directive-run/core. If not found in package.json, suggest installing it: npm install @directive-run/core.
When Claude Should Use This Skill
Auto-invoke when the user:
- Says "create a system", "compose modules", "add plugins", or "connect to React"
- Shows
createSystem() and asks to extend it with more modules or plugins
- Asks about
useSelector, useEvent, useSystem, or DirectiveProvider
- Asks how modules share state or read each other's facts
- Asks about system lifecycle, hydration, or dynamic module registration
Do NOT invoke when:
- Writing a module for the first time (start with
writing-directive-modules.md)
- Writing constraints/resolvers only (see
writing-directive-constraints.md)
- Writing tests (see
testing-directive-code.md)
Decision Tree: Single or Multi-Module?
How many state domains?
├── One → createSystem({ module: myModule })
│ Direct access: system.facts.count
│
└── Two or more → createSystem({ modules: { auth, cart, ui } })
Namespaced: system.facts.auth.token
Does module A need to read module B's state in constraints/effects/derivations?
├── No → No crossModuleDeps needed
└── Yes → Declare crossModuleDeps on the consuming module
Own facts at facts.self.*, other module at facts.otherModule.*
System Creation
Single module
import { createSystem } from "@directive-run/core";
import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
const system = createSystem({
module: myModule,
plugins: [loggingPlugin(), devtoolsPlugin()],
history: { maxSnapshots: 100 },
});
system.start();
await system.settle();
Multi-module
const system = createSystem({
modules: { auth: authModule, cart: cartModule, data: dataModule },
plugins: [loggingPlugin(), devtoolsPlugin()],
initialFacts: {
auth: { token: "restored-token" },
cart: { items: [] },
},
initOrder: "auto",
});
system.start();
await system.settle();
Hydration from async source (before start)
await system.hydrate(async () => {
const stored = localStorage.getItem("app-state");
return stored ? JSON.parse(stored) : {};
});
system.start();
Dynamic module registration at runtime
const chatModule = await import("./modules/chat");
system.registerModule("chat", chatModule.default);
Accessing System State
Single module – direct access
system.facts.count = 5;
const val = system.facts.count;
const loading = system.derive.isLoading;
system.events.increment();
system.events.setUser({ user: { id: "1", name: "Alice" } });
Multi-module – namespaced access (dot notation always)
system.facts.auth.token;
system.facts.cart.items;
system.derive.auth.isAdmin;
system.derive.cart.itemCount;
system.events.auth.login({ token: "abc" });
system.events.cart.addItem({ productId: "p1", qty: 1 });
system.facts["auth::token"];
system.facts["auth_token"];
system.facts.auth.token;
Subscribing and watching
const unsub = system.subscribe(["count", "isLoading"], () => {
console.log(system.facts.count, system.derive.isLoading);
});
system.subscribe(["auth.token", "cart.items"], () => { ... });
system.watch("count", (newVal, oldVal) => {
console.log(`Count: ${oldVal} -> ${newVal}`);
});
system.subscribeModule("cart", () => { ... });
await system.when((facts) => facts.phase === "done");
await system.when((facts) => facts.auth.isAuthenticated, { timeout: 5000 });
System lifecycle
system.start();
system.stop();
system.destroy();
Always call destroy() when a system is no longer needed (teardown, React unmount, test cleanup).
Cross-Module Dependencies
Export the schema separately so other modules can reference it for type safety.
export const authSchema = {
facts: {
token: t.string<string | null>(),
isAuthenticated: t.boolean(),
},
} as const;
export const authModule = createModule("auth", {
schema: authSchema,
init: (facts) => {
facts.token = null;
facts.isAuthenticated = false;
},
events: {
login: (facts, payload) => { facts.token = payload.token; facts.isAuthenticated = true; },
logout: (facts) => { facts.token = null; facts.isAuthenticated = false; },
},
});
import { authSchema } from "./auth";
const dataModule = createModule("data", {
schema: { facts: { items: t.array(t.string()), loaded: t.boolean() } },
crossModuleDeps: { auth: authSchema },
init: (facts) => { facts.items = []; facts.loaded = false; },
constraints: {
fetchWhenAuth: {
when: (facts) => facts.auth.isAuthenticated && !facts.self.loaded,
require: { type: "FETCH_ITEMS" },
},
},
effects: {
onAuthChange: {
run: (facts, prev) => {
if (prev && prev.auth.isAuthenticated && !facts.auth.isAuthenticated) {
console.log("User logged out");
}
},
},
},
});
Plugins
Built-in plugins
import {
loggingPlugin,
devtoolsPlugin,
persistencePlugin,
} from "@directive-run/core/plugins";
const plugins = [];
if (process.env.NODE_ENV === "development") {
plugins.push(loggingPlugin({ verbose: true }));
plugins.push(devtoolsPlugin({ name: "My App" }));
}
plugins.push(persistencePlugin({
key: "app-state",
storage: localStorage,
exclude: ["authToken", "isLoading"],
version: 1,
migrate: (old, version) => old,
}));
Plugin order matters – logging first captures all events including those from other plugins.
Custom plugin
import type { DirectivePlugin } from "@directive-run/core";
const myPlugin: DirectivePlugin = {
name: "my-plugin",
onInit: (system) => { ... },
onStart: (system) => { ... },
onStop: (system) => { ... },
onSnapshot: (snapshot) => {
},
onRequirementEmitted: (requirement) => { ... },
onResolutionStart: (resolution) => { ... },
onResolutionComplete: (resolution) => { ... },
onError: (error, context) => { ... },
};
React Adapter
Import from @directive-run/react.
Setup: create system outside React
export const system = createSystem({ module: counterModule });
useSelector – subscribe to state
Re-renders only when the selected value changes (shallow comparison).
import { useSelector, useEvent } from "@directive-run/react";
function Counter() {
const count = useSelector(system, (s) => s.facts.count);
const doubled = useSelector(system, (s) => s.derive.doubled);
const token = useSelector(system, (s) => s.facts.auth.token);
const itemCount = useSelector(system, (s) => s.derive.cart.itemCount);
const events = useEvent(system);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => events.increment()}>+1</button>
</div>
);
}
useEvent – dispatch events (stable reference)
const events = useEvent(system);
events.increment();
events.auth.login({ token: "abc" });
useSystem – lifecycle scoped to component
import { useSystem } from "@directive-run/react";
function GameBoard() {
const gameSystem = useSystem({
module: gameModule,
history: true,
});
const score = useSelector(gameSystem, (s) => s.facts.score);
return <div>Score: {score}</div>;
}
Use useSystem when the system's lifecycle matches a component's lifecycle (wizard, game board, modal form). For app-wide state, create outside React.
DirectiveProvider – share system via context
import { DirectiveProvider, useDirectiveContext } from "@directive-run/react";
function App() {
return (
<DirectiveProvider system={system}>
<Dashboard />
</DirectiveProvider>
);
}
function Dashboard() {
const system = useDirectiveContext();
const stats = useSelector(system, (s) => s.derive.dashboardStats);
return <div>{stats.totalUsers} users</div>;
}
Critical Anti-Patterns
1. Reading facts before settling
system.start();
console.log(system.facts.user);
system.start();
await system.settle();
console.log(system.facts.user);
2. Forgetting to start the system
const system = createSystem({ module: myModule });
console.log(system.facts.phase);
system.start();
await system.settle();
3. Missing crossModuleDeps
when: (facts) => facts.auth.isAuthenticated,
crossModuleDeps: { auth: authSchema },
when: (facts) => facts.auth.isAuthenticated,
4. Bare facts.* in cross-module context
when: (facts) => facts.loaded,
when: (facts) => facts.self.loaded,
5. Bracket notation for namespaced access
system.facts["auth::token"];
system.facts.auth.token;
6. Creating system inside a React component without useSystem
function Counter() {
const system = createSystem({ module: counterModule });
}
const system = createSystem({ module: counterModule });
7. The nonexistent useDirective hook
const { facts, derive, events } = useDirective(system);
const count = useSelector(system, (s) => s.facts.count);
const events = useEvent(system);
8. Selecting too much state in useSelector
const allFacts = useSelector(system, (s) => s.facts);
const name = useSelector(system, (s) => s.facts.userName);
9. devtoolsPlugin in production
const plugins = process.env.NODE_ENV === "development"
? [loggingPlugin(), devtoolsPlugin()]
: [];
10. persistencePlugin without version
persistencePlugin({ key: "app", storage: localStorage })
persistencePlugin({ key: "app", storage: localStorage, version: 1, migrate: (old) => old })
Inspecting System State
const inspection = system.inspect();
inspection.facts;
inspection.derivations;
inspection.requirements;
inspection.constraintDefs;
inspection.resolvers;
inspection.inflight;
inspection.unmet;
system.explain("req-123");
system.isSettled;
Reference Files
multi-module.md – crossModuleDeps, module schema export pattern, subscribe/watch in multi-module
system-api.md – full system API reference, read(), watch(), when(), lifecycle order
plugins.md – all built-in plugins with full config, custom plugin lifecycle hooks
react-adapter.md – all React hooks with examples, common mistakes, multi-module patterns