@directive-run/claude-plugin 1.17.1 → 1.18.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/package.json +2 -2
- package/skills/building-ai-agents/ai-sources.md +406 -0
- package/skills/building-directive-systems/system-api.md +32 -2
- package/skills/getting-started-with-directive/core-patterns.md +3 -0
- package/skills/getting-started-with-directive/sitemap.md +1 -0
- package/skills/hardening-ai-systems/ai-security.md +57 -0
- package/skills/migrating-to-directive/anti-patterns.md +94 -1
- package/skills/migrating-to-directive/core-patterns.md +3 -0
- package/skills/reviewing-directive-code/anti-patterns.md +94 -1
- package/skills/reviewing-directive-code/core-patterns.md +3 -0
- package/skills/reviewing-directive-code/naming.md +6 -1
- package/skills/scaffolding-directive-modules/core-patterns.md +3 -0
- package/skills/scaffolding-directive-modules/naming.md +6 -1
- package/skills/writing-directive-modules/anti-patterns.md +94 -1
- package/skills/writing-directive-modules/core-patterns.md +3 -0
- package/skills/writing-directive-modules/naming.md +6 -1
- package/skills/writing-directive-modules/sources.md +424 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Covers `@directive-run/core` and `@directive-run/react` — hallucination-prone API patterns to avoid.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
21 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
|
|
6
6
|
|
|
7
7
|
## 1. Unnecessary Type Casting on Facts/Derivations
|
|
8
8
|
|
|
@@ -357,6 +357,97 @@ const system = createSystem({
|
|
|
357
357
|
});
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
+
## 20. Hand-Rolled External Subscriptions Inside React/`useEffect`
|
|
361
|
+
|
|
362
|
+
When wrapping an external event stream (Supabase realtime, WebSocket, polling timer, browser listener) into a Directive system, do NOT write a React `useEffect` that owns the subscription and dispatches `sys.events.X()` from the callback — declare a `source` on the module instead. The runtime owns the mount / unmount lifecycle and observability. The component collapses to fact reads.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// WRONG – subscription lives in component code; module never knows about it.
|
|
366
|
+
// On every remount you get duplicate channels; on unmount you can leak.
|
|
367
|
+
function Game({ system, gameId }: Props) {
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
const ch = supabase
|
|
370
|
+
.channel(`game:${gameId}`)
|
|
371
|
+
.on('postgres_changes', { event: 'UPDATE', table: 'games' }, (payload) => {
|
|
372
|
+
system.events.realtimeUpdate({ snapshot: mapRow(payload.new) });
|
|
373
|
+
})
|
|
374
|
+
.subscribe();
|
|
375
|
+
return () => { supabase.removeChannel(ch); };
|
|
376
|
+
}, [gameId]);
|
|
377
|
+
return <Board snapshot={useFact(system, 'snapshot')} />;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// CORRECT – declare a `source` on the module. The runtime owns the
|
|
381
|
+
// mount / unmount lifecycle and observability. The component collapses
|
|
382
|
+
// to fact reads.
|
|
383
|
+
const gameModule = createModule('game', {
|
|
384
|
+
schema: {
|
|
385
|
+
facts: { snapshot: t.object<GameSnapshot>().nullable() },
|
|
386
|
+
events: { realtimeUpdate: { snapshot: t.object<GameSnapshot>() } },
|
|
387
|
+
},
|
|
388
|
+
sources: {
|
|
389
|
+
realtime: {
|
|
390
|
+
attach: (publish) => {
|
|
391
|
+
const ch = supabase
|
|
392
|
+
.channel(`game:${gameId}`)
|
|
393
|
+
.on('postgres_changes', { event: 'UPDATE', table: 'games' },
|
|
394
|
+
(p) => publish('realtimeUpdate', { snapshot: mapRow(p.new) }))
|
|
395
|
+
.subscribe();
|
|
396
|
+
return () => { supabase.removeChannel(ch); };
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
on: { realtimeUpdate: (f, p) => { f.snapshot = p.snapshot; } },
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Why:** the hook-as-bridge pattern duplicates lifecycle code at every call site. Cleanup correctness drifts. `system.observe()` can't see the subscription. With a `source`, the engine attaches at `start()`, detaches at `stop()`, isolates failures, and emits `source.attach` / `.publish` / `.detach` / `.error` observation events for plugins.
|
|
405
|
+
|
|
406
|
+
**Don't subscribe in both an effect AND a source on the same channel** — the effect re-runs on fact changes, the source mounts once, you'll get 2× messages with silent duplicates. Pick one.
|
|
407
|
+
|
|
408
|
+
See [`sources.md`](./sources.md) for the full decision tree, recipes (Supabase / WebSocket / browser events / polling), and the typed-publish factory pattern.
|
|
409
|
+
|
|
410
|
+
## 21. Abbreviating Type Names (`*Def` instead of `*Definition`)
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// WRONG – `Def` is short for `Definition`. Same anti-pattern as `ctx`
|
|
414
|
+
// → `context` (entry #5). Source code reads better with spelled-out
|
|
415
|
+
// names; the minifier handles the bytes either way.
|
|
416
|
+
import type { ModuleDef, SourceDef, ResolverDef } from "@directive-run/core";
|
|
417
|
+
|
|
418
|
+
// CORRECT – use the spelled-out aliases. `*Def` stays canonical in 1.x
|
|
419
|
+
// for back-compat; consumers can migrate today.
|
|
420
|
+
import type {
|
|
421
|
+
ModuleDefinition,
|
|
422
|
+
SourceDefinition,
|
|
423
|
+
ResolverDefinition,
|
|
424
|
+
} from "@directive-run/core";
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
The `*Definition` aliases ship in 1.x via `export type { X as XDefinition }`
|
|
428
|
+
in `packages/core/src/core/types/index.ts`, so generic forwarding +
|
|
429
|
+
TS inference rules (mapped types, conditional distribution, tagged-
|
|
430
|
+
union discrimination, barrel re-exports) match the canonical `*Def`
|
|
431
|
+
form bit-for-bit. **2.0 swaps:** `*Definition` becomes canonical and
|
|
432
|
+
`*Def` becomes the deprecated alias. Start writing `*Definition` in
|
|
433
|
+
new code today so the 2.0 migration is a no-op.
|
|
434
|
+
|
|
435
|
+
Per RFC 0006, this applies to:
|
|
436
|
+
|
|
437
|
+
- `ModuleDef` → `ModuleDefinition`
|
|
438
|
+
- `ConstraintDef` / `ConstraintsDef` → `ConstraintDefinition` / `ConstraintsDefinition`
|
|
439
|
+
- `ResolverDef` / `ResolversDef` → `ResolverDefinition` / `ResolversDefinition`
|
|
440
|
+
- `DerivationDef` / `DerivationsDef` → `DerivationDefinition` / `DerivationsDefinition`
|
|
441
|
+
- `EffectDef` / `EffectsDef` → `EffectDefinition` / `EffectsDefinition`
|
|
442
|
+
- `EventsDef` → `EventsDefinition`
|
|
443
|
+
- `SourceDef` / `SourcesDef` → `SourceDefinition` / `SourcesDefinition`
|
|
444
|
+
- `SourcePublish` → `SourcePublishFn`, `SourceUnsubscribe` → `SourceUnsubscribeFn`
|
|
445
|
+
- Plus all `Typed*Def`, `CrossModule*Def`, `Dynamic*Def` variants.
|
|
446
|
+
|
|
447
|
+
`EffectCleanup`, `MetaAccessor`, `EventsAccessor`, `DeriveAccessor`,
|
|
448
|
+
`Snapshot` are explicitly kept as-is (each has reasoning recorded in
|
|
449
|
+
RFC 0006).
|
|
450
|
+
|
|
360
451
|
## Quick Reference Checklist
|
|
361
452
|
|
|
362
453
|
Before generating any Directive code, verify:
|
|
@@ -371,10 +462,12 @@ Before generating any Directive code, verify:
|
|
|
371
462
|
8. Multi-module uses `facts.self.*` for own facts
|
|
372
463
|
9. Imports from `@directive-run/core`, not deep paths
|
|
373
464
|
10. `await system.settle()` after `system.start()`
|
|
465
|
+
11. External event subscriptions live in `sources:`, not in `useEffect` or `onMount`
|
|
374
466
|
|
|
375
467
|
## See also
|
|
376
468
|
|
|
377
469
|
- [`naming.md`](./naming.md) — the strict canonical-term rules AND the alias map for cross-paradigm searches
|
|
470
|
+
- [`sources.md`](./sources.md) — the source primitive (the right answer for #20)
|
|
378
471
|
- [`constraints.md`](./constraints.md) — the constraint shape these anti-patterns reference (`facts` not in scope inside static `require:`, etc.)
|
|
379
472
|
- [`resolvers.md`](./resolvers.md) — the resolver shape these anti-patterns reference (return `void`, mutate `context.facts`, `(req, context)` not `(req, ctx)`)
|
|
380
473
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that exist and the hallucinated ones (`t.map`, `t.set`, `t.promise`, `t.date`) that don't
|
|
@@ -11,6 +11,8 @@ User wants to...
|
|
|
11
11
|
├── Store state → schema.facts + init()
|
|
12
12
|
├── Compute derived values → schema.derivations + derive
|
|
13
13
|
├── React to state changes → effects
|
|
14
|
+
├── Subscribe to an external event stream (WebSocket, Supabase realtime,
|
|
15
|
+
│ browser events, MCP server lifecycle) → sources
|
|
14
16
|
├── Trigger side effects when conditions are met → constraints + resolvers
|
|
15
17
|
├── Handle user actions → schema.events + events handlers
|
|
16
18
|
└── Coordinate multiple modules → createSystem({ modules: {} })
|
|
@@ -233,6 +235,7 @@ const typed = createModule("typed", {
|
|
|
233
235
|
|
|
234
236
|
- [`constraints.md`](./constraints.md) — the demand side of the constraint-resolver loop
|
|
235
237
|
- [`resolvers.md`](./resolvers.md) — the supply side that fulfills requirements
|
|
238
|
+
- [`sources.md`](./sources.md) — typed external event sources (the inbound dual of effects)
|
|
236
239
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that define fact and derivation types
|
|
237
240
|
- [`multi-module.md`](./multi-module.md) — when a single module isn't enough
|
|
238
241
|
- [`system-api.md`](./system-api.md) — `createSystem` and the runtime instance once your module is defined
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Covers `@directive-run/core` and `@directive-run/react` — hallucination-prone API patterns to avoid.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
21 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
|
|
6
6
|
|
|
7
7
|
## 1. Unnecessary Type Casting on Facts/Derivations
|
|
8
8
|
|
|
@@ -357,6 +357,97 @@ const system = createSystem({
|
|
|
357
357
|
});
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
+
## 20. Hand-Rolled External Subscriptions Inside React/`useEffect`
|
|
361
|
+
|
|
362
|
+
When wrapping an external event stream (Supabase realtime, WebSocket, polling timer, browser listener) into a Directive system, do NOT write a React `useEffect` that owns the subscription and dispatches `sys.events.X()` from the callback — declare a `source` on the module instead. The runtime owns the mount / unmount lifecycle and observability. The component collapses to fact reads.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// WRONG – subscription lives in component code; module never knows about it.
|
|
366
|
+
// On every remount you get duplicate channels; on unmount you can leak.
|
|
367
|
+
function Game({ system, gameId }: Props) {
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
const ch = supabase
|
|
370
|
+
.channel(`game:${gameId}`)
|
|
371
|
+
.on('postgres_changes', { event: 'UPDATE', table: 'games' }, (payload) => {
|
|
372
|
+
system.events.realtimeUpdate({ snapshot: mapRow(payload.new) });
|
|
373
|
+
})
|
|
374
|
+
.subscribe();
|
|
375
|
+
return () => { supabase.removeChannel(ch); };
|
|
376
|
+
}, [gameId]);
|
|
377
|
+
return <Board snapshot={useFact(system, 'snapshot')} />;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// CORRECT – declare a `source` on the module. The runtime owns the
|
|
381
|
+
// mount / unmount lifecycle and observability. The component collapses
|
|
382
|
+
// to fact reads.
|
|
383
|
+
const gameModule = createModule('game', {
|
|
384
|
+
schema: {
|
|
385
|
+
facts: { snapshot: t.object<GameSnapshot>().nullable() },
|
|
386
|
+
events: { realtimeUpdate: { snapshot: t.object<GameSnapshot>() } },
|
|
387
|
+
},
|
|
388
|
+
sources: {
|
|
389
|
+
realtime: {
|
|
390
|
+
attach: (publish) => {
|
|
391
|
+
const ch = supabase
|
|
392
|
+
.channel(`game:${gameId}`)
|
|
393
|
+
.on('postgres_changes', { event: 'UPDATE', table: 'games' },
|
|
394
|
+
(p) => publish('realtimeUpdate', { snapshot: mapRow(p.new) }))
|
|
395
|
+
.subscribe();
|
|
396
|
+
return () => { supabase.removeChannel(ch); };
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
on: { realtimeUpdate: (f, p) => { f.snapshot = p.snapshot; } },
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Why:** the hook-as-bridge pattern duplicates lifecycle code at every call site. Cleanup correctness drifts. `system.observe()` can't see the subscription. With a `source`, the engine attaches at `start()`, detaches at `stop()`, isolates failures, and emits `source.attach` / `.publish` / `.detach` / `.error` observation events for plugins.
|
|
405
|
+
|
|
406
|
+
**Don't subscribe in both an effect AND a source on the same channel** — the effect re-runs on fact changes, the source mounts once, you'll get 2× messages with silent duplicates. Pick one.
|
|
407
|
+
|
|
408
|
+
See [`sources.md`](./sources.md) for the full decision tree, recipes (Supabase / WebSocket / browser events / polling), and the typed-publish factory pattern.
|
|
409
|
+
|
|
410
|
+
## 21. Abbreviating Type Names (`*Def` instead of `*Definition`)
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// WRONG – `Def` is short for `Definition`. Same anti-pattern as `ctx`
|
|
414
|
+
// → `context` (entry #5). Source code reads better with spelled-out
|
|
415
|
+
// names; the minifier handles the bytes either way.
|
|
416
|
+
import type { ModuleDef, SourceDef, ResolverDef } from "@directive-run/core";
|
|
417
|
+
|
|
418
|
+
// CORRECT – use the spelled-out aliases. `*Def` stays canonical in 1.x
|
|
419
|
+
// for back-compat; consumers can migrate today.
|
|
420
|
+
import type {
|
|
421
|
+
ModuleDefinition,
|
|
422
|
+
SourceDefinition,
|
|
423
|
+
ResolverDefinition,
|
|
424
|
+
} from "@directive-run/core";
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
The `*Definition` aliases ship in 1.x via `export type { X as XDefinition }`
|
|
428
|
+
in `packages/core/src/core/types/index.ts`, so generic forwarding +
|
|
429
|
+
TS inference rules (mapped types, conditional distribution, tagged-
|
|
430
|
+
union discrimination, barrel re-exports) match the canonical `*Def`
|
|
431
|
+
form bit-for-bit. **2.0 swaps:** `*Definition` becomes canonical and
|
|
432
|
+
`*Def` becomes the deprecated alias. Start writing `*Definition` in
|
|
433
|
+
new code today so the 2.0 migration is a no-op.
|
|
434
|
+
|
|
435
|
+
Per RFC 0006, this applies to:
|
|
436
|
+
|
|
437
|
+
- `ModuleDef` → `ModuleDefinition`
|
|
438
|
+
- `ConstraintDef` / `ConstraintsDef` → `ConstraintDefinition` / `ConstraintsDefinition`
|
|
439
|
+
- `ResolverDef` / `ResolversDef` → `ResolverDefinition` / `ResolversDefinition`
|
|
440
|
+
- `DerivationDef` / `DerivationsDef` → `DerivationDefinition` / `DerivationsDefinition`
|
|
441
|
+
- `EffectDef` / `EffectsDef` → `EffectDefinition` / `EffectsDefinition`
|
|
442
|
+
- `EventsDef` → `EventsDefinition`
|
|
443
|
+
- `SourceDef` / `SourcesDef` → `SourceDefinition` / `SourcesDefinition`
|
|
444
|
+
- `SourcePublish` → `SourcePublishFn`, `SourceUnsubscribe` → `SourceUnsubscribeFn`
|
|
445
|
+
- Plus all `Typed*Def`, `CrossModule*Def`, `Dynamic*Def` variants.
|
|
446
|
+
|
|
447
|
+
`EffectCleanup`, `MetaAccessor`, `EventsAccessor`, `DeriveAccessor`,
|
|
448
|
+
`Snapshot` are explicitly kept as-is (each has reasoning recorded in
|
|
449
|
+
RFC 0006).
|
|
450
|
+
|
|
360
451
|
## Quick Reference Checklist
|
|
361
452
|
|
|
362
453
|
Before generating any Directive code, verify:
|
|
@@ -371,10 +462,12 @@ Before generating any Directive code, verify:
|
|
|
371
462
|
8. Multi-module uses `facts.self.*` for own facts
|
|
372
463
|
9. Imports from `@directive-run/core`, not deep paths
|
|
373
464
|
10. `await system.settle()` after `system.start()`
|
|
465
|
+
11. External event subscriptions live in `sources:`, not in `useEffect` or `onMount`
|
|
374
466
|
|
|
375
467
|
## See also
|
|
376
468
|
|
|
377
469
|
- [`naming.md`](./naming.md) — the strict canonical-term rules AND the alias map for cross-paradigm searches
|
|
470
|
+
- [`sources.md`](./sources.md) — the source primitive (the right answer for #20)
|
|
378
471
|
- [`constraints.md`](./constraints.md) — the constraint shape these anti-patterns reference (`facts` not in scope inside static `require:`, etc.)
|
|
379
472
|
- [`resolvers.md`](./resolvers.md) — the resolver shape these anti-patterns reference (return `void`, mutate `context.facts`, `(req, context)` not `(req, ctx)`)
|
|
380
473
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that exist and the hallucinated ones (`t.map`, `t.set`, `t.promise`, `t.date`) that don't
|
|
@@ -11,6 +11,8 @@ User wants to...
|
|
|
11
11
|
├── Store state → schema.facts + init()
|
|
12
12
|
├── Compute derived values → schema.derivations + derive
|
|
13
13
|
├── React to state changes → effects
|
|
14
|
+
├── Subscribe to an external event stream (WebSocket, Supabase realtime,
|
|
15
|
+
│ browser events, MCP server lifecycle) → sources
|
|
14
16
|
├── Trigger side effects when conditions are met → constraints + resolvers
|
|
15
17
|
├── Handle user actions → schema.events + events handlers
|
|
16
18
|
└── Coordinate multiple modules → createSystem({ modules: {} })
|
|
@@ -233,6 +235,7 @@ const typed = createModule("typed", {
|
|
|
233
235
|
|
|
234
236
|
- [`constraints.md`](./constraints.md) — the demand side of the constraint-resolver loop
|
|
235
237
|
- [`resolvers.md`](./resolvers.md) — the supply side that fulfills requirements
|
|
238
|
+
- [`sources.md`](./sources.md) — typed external event sources (the inbound dual of effects)
|
|
236
239
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that define fact and derivation types
|
|
237
240
|
- [`multi-module.md`](./multi-module.md) — when a single module isn't enough
|
|
238
241
|
- [`system-api.md`](./system-api.md) — `createSystem` and the runtime instance once your module is defined
|
|
@@ -16,7 +16,8 @@ This file is both the rules and the bridge. Search for the term you already know
|
|
|
16
16
|
| **thunk**, **saga**, **effect**, **middleware**, **listener** | Redux Toolkit, redux-saga, redux-observable, NgRx | **resolvers** | Async logic that mutates state in response to a requirement. Same role as a thunk; declared once, run by the runtime. |
|
|
17
17
|
| **rule**, **condition**, **guard**, **trigger**, **policy** | XState guards, Cerbos, OPA, business-rule engines | **constraints** | Declarative "when X is true, Y must happen." Constraints emit requirements; resolvers fulfill them. |
|
|
18
18
|
| **request**, **query**, **side-effect spec** | React Query / TanStack Query queries, Apollo queries | **requirements** | The object a constraint emits. The runtime dedupes them, dispatches to resolvers, tracks status. |
|
|
19
|
-
| **subscription**, **listener**, **reaction**, **watcher** | Zustand subscribe, MobX reactions, Redux subscriptions | **effects** |
|
|
19
|
+
| **subscription**, **listener**, **reaction**, **watcher** | Zustand subscribe, MobX reactions, Redux subscriptions | **effects** (for fact-change reactions) or **sources** (for external event streams) | Reacting to facts → **effects**; subscribing to an external stream that publishes INTO facts → **sources**. Both replace ad-hoc `subscribe()`/`useEffect` patterns. |
|
|
20
|
+
| **observable**, **subject**, **event source**, **WebSocket/SSE subscription** | RxJS, DOM `addEventListener`, EventSource, Supabase Realtime | **sources** | The inbound dual of effects. Mount-once external event stream the runtime owns. Declared on a module; auto-detached on `system.stop()`. |
|
|
20
21
|
| **slice**, **feature**, **domain**, **bounded context** | Redux Toolkit slices, NgRx feature modules | **modules** | Encapsulates facts + derivations + constraints + resolvers + events for one bounded slice of the system. |
|
|
21
22
|
| **store**, **container**, **context**, **app state** | Redux Store, Pinia, React Context, NgRx Store | **system** | The runtime that wires modules together, evaluates constraints, dispatches resolvers, and exposes observation. |
|
|
22
23
|
| **state machine**, **statechart**, **xstate machine** | XState | (closest: **module** + **constraints**) | Directive isn't a state machine — but if your XState `states` model business rules (not UI flow), constraints + facts express the same logic declaratively with no transition functions. |
|
|
@@ -54,6 +55,10 @@ Tagged union `{ type: "FETCH_USER", … }`. Constraints emit them; the runtime d
|
|
|
54
55
|
|
|
55
56
|
Declared via `effects: { name: { run: (facts, prev) => … } }`. Run on relevant fact changes. The auto-tracking dependency model means no `deps` array unless you specifically need a partial dep set.
|
|
56
57
|
|
|
58
|
+
### `sources` — typed external event subscriptions
|
|
59
|
+
|
|
60
|
+
The inbound dual of effects. Declared via `sources: { name: { attach: (publish) => cleanup } }`. The runtime mounts each source once at `system.start()` and unmounts at `system.stop()`. Inside `attach`, the source subscribes to an external event stream (WebSocket, Supabase realtime, browser events, MCP server lifecycle) and calls `publish(eventName, payload)` to dispatch into the system's event queue. Sources are the canonical replacement for `useEffect(() => subscribe(); return unsubscribe)` patterns. See [`sources.md`](./sources.md).
|
|
61
|
+
|
|
57
62
|
### `events` — typed mutation entry points
|
|
58
63
|
|
|
59
64
|
Declared via `events: { name: (facts, payload) => … }`. Called as `system.events.name(payload)`. The typed events accessor is the canonical way to dispatch — `system.dispatch({type, ...payload})` works but the typed accessor carries autocomplete.
|
|
@@ -11,6 +11,8 @@ User wants to...
|
|
|
11
11
|
├── Store state → schema.facts + init()
|
|
12
12
|
├── Compute derived values → schema.derivations + derive
|
|
13
13
|
├── React to state changes → effects
|
|
14
|
+
├── Subscribe to an external event stream (WebSocket, Supabase realtime,
|
|
15
|
+
│ browser events, MCP server lifecycle) → sources
|
|
14
16
|
├── Trigger side effects when conditions are met → constraints + resolvers
|
|
15
17
|
├── Handle user actions → schema.events + events handlers
|
|
16
18
|
└── Coordinate multiple modules → createSystem({ modules: {} })
|
|
@@ -233,6 +235,7 @@ const typed = createModule("typed", {
|
|
|
233
235
|
|
|
234
236
|
- [`constraints.md`](./constraints.md) — the demand side of the constraint-resolver loop
|
|
235
237
|
- [`resolvers.md`](./resolvers.md) — the supply side that fulfills requirements
|
|
238
|
+
- [`sources.md`](./sources.md) — typed external event sources (the inbound dual of effects)
|
|
236
239
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that define fact and derivation types
|
|
237
240
|
- [`multi-module.md`](./multi-module.md) — when a single module isn't enough
|
|
238
241
|
- [`system-api.md`](./system-api.md) — `createSystem` and the runtime instance once your module is defined
|
|
@@ -16,7 +16,8 @@ This file is both the rules and the bridge. Search for the term you already know
|
|
|
16
16
|
| **thunk**, **saga**, **effect**, **middleware**, **listener** | Redux Toolkit, redux-saga, redux-observable, NgRx | **resolvers** | Async logic that mutates state in response to a requirement. Same role as a thunk; declared once, run by the runtime. |
|
|
17
17
|
| **rule**, **condition**, **guard**, **trigger**, **policy** | XState guards, Cerbos, OPA, business-rule engines | **constraints** | Declarative "when X is true, Y must happen." Constraints emit requirements; resolvers fulfill them. |
|
|
18
18
|
| **request**, **query**, **side-effect spec** | React Query / TanStack Query queries, Apollo queries | **requirements** | The object a constraint emits. The runtime dedupes them, dispatches to resolvers, tracks status. |
|
|
19
|
-
| **subscription**, **listener**, **reaction**, **watcher** | Zustand subscribe, MobX reactions, Redux subscriptions | **effects** |
|
|
19
|
+
| **subscription**, **listener**, **reaction**, **watcher** | Zustand subscribe, MobX reactions, Redux subscriptions | **effects** (for fact-change reactions) or **sources** (for external event streams) | Reacting to facts → **effects**; subscribing to an external stream that publishes INTO facts → **sources**. Both replace ad-hoc `subscribe()`/`useEffect` patterns. |
|
|
20
|
+
| **observable**, **subject**, **event source**, **WebSocket/SSE subscription** | RxJS, DOM `addEventListener`, EventSource, Supabase Realtime | **sources** | The inbound dual of effects. Mount-once external event stream the runtime owns. Declared on a module; auto-detached on `system.stop()`. |
|
|
20
21
|
| **slice**, **feature**, **domain**, **bounded context** | Redux Toolkit slices, NgRx feature modules | **modules** | Encapsulates facts + derivations + constraints + resolvers + events for one bounded slice of the system. |
|
|
21
22
|
| **store**, **container**, **context**, **app state** | Redux Store, Pinia, React Context, NgRx Store | **system** | The runtime that wires modules together, evaluates constraints, dispatches resolvers, and exposes observation. |
|
|
22
23
|
| **state machine**, **statechart**, **xstate machine** | XState | (closest: **module** + **constraints**) | Directive isn't a state machine — but if your XState `states` model business rules (not UI flow), constraints + facts express the same logic declaratively with no transition functions. |
|
|
@@ -54,6 +55,10 @@ Tagged union `{ type: "FETCH_USER", … }`. Constraints emit them; the runtime d
|
|
|
54
55
|
|
|
55
56
|
Declared via `effects: { name: { run: (facts, prev) => … } }`. Run on relevant fact changes. The auto-tracking dependency model means no `deps` array unless you specifically need a partial dep set.
|
|
56
57
|
|
|
58
|
+
### `sources` — typed external event subscriptions
|
|
59
|
+
|
|
60
|
+
The inbound dual of effects. Declared via `sources: { name: { attach: (publish) => cleanup } }`. The runtime mounts each source once at `system.start()` and unmounts at `system.stop()`. Inside `attach`, the source subscribes to an external event stream (WebSocket, Supabase realtime, browser events, MCP server lifecycle) and calls `publish(eventName, payload)` to dispatch into the system's event queue. Sources are the canonical replacement for `useEffect(() => subscribe(); return unsubscribe)` patterns. See [`sources.md`](./sources.md).
|
|
61
|
+
|
|
57
62
|
### `events` — typed mutation entry points
|
|
58
63
|
|
|
59
64
|
Declared via `events: { name: (facts, payload) => … }`. Called as `system.events.name(payload)`. The typed events accessor is the canonical way to dispatch — `system.dispatch({type, ...payload})` works but the typed accessor carries autocomplete.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Covers `@directive-run/core` and `@directive-run/react` — hallucination-prone API patterns to avoid.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
21 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
|
|
6
6
|
|
|
7
7
|
## 1. Unnecessary Type Casting on Facts/Derivations
|
|
8
8
|
|
|
@@ -357,6 +357,97 @@ const system = createSystem({
|
|
|
357
357
|
});
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
+
## 20. Hand-Rolled External Subscriptions Inside React/`useEffect`
|
|
361
|
+
|
|
362
|
+
When wrapping an external event stream (Supabase realtime, WebSocket, polling timer, browser listener) into a Directive system, do NOT write a React `useEffect` that owns the subscription and dispatches `sys.events.X()` from the callback — declare a `source` on the module instead. The runtime owns the mount / unmount lifecycle and observability. The component collapses to fact reads.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// WRONG – subscription lives in component code; module never knows about it.
|
|
366
|
+
// On every remount you get duplicate channels; on unmount you can leak.
|
|
367
|
+
function Game({ system, gameId }: Props) {
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
const ch = supabase
|
|
370
|
+
.channel(`game:${gameId}`)
|
|
371
|
+
.on('postgres_changes', { event: 'UPDATE', table: 'games' }, (payload) => {
|
|
372
|
+
system.events.realtimeUpdate({ snapshot: mapRow(payload.new) });
|
|
373
|
+
})
|
|
374
|
+
.subscribe();
|
|
375
|
+
return () => { supabase.removeChannel(ch); };
|
|
376
|
+
}, [gameId]);
|
|
377
|
+
return <Board snapshot={useFact(system, 'snapshot')} />;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// CORRECT – declare a `source` on the module. The runtime owns the
|
|
381
|
+
// mount / unmount lifecycle and observability. The component collapses
|
|
382
|
+
// to fact reads.
|
|
383
|
+
const gameModule = createModule('game', {
|
|
384
|
+
schema: {
|
|
385
|
+
facts: { snapshot: t.object<GameSnapshot>().nullable() },
|
|
386
|
+
events: { realtimeUpdate: { snapshot: t.object<GameSnapshot>() } },
|
|
387
|
+
},
|
|
388
|
+
sources: {
|
|
389
|
+
realtime: {
|
|
390
|
+
attach: (publish) => {
|
|
391
|
+
const ch = supabase
|
|
392
|
+
.channel(`game:${gameId}`)
|
|
393
|
+
.on('postgres_changes', { event: 'UPDATE', table: 'games' },
|
|
394
|
+
(p) => publish('realtimeUpdate', { snapshot: mapRow(p.new) }))
|
|
395
|
+
.subscribe();
|
|
396
|
+
return () => { supabase.removeChannel(ch); };
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
on: { realtimeUpdate: (f, p) => { f.snapshot = p.snapshot; } },
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Why:** the hook-as-bridge pattern duplicates lifecycle code at every call site. Cleanup correctness drifts. `system.observe()` can't see the subscription. With a `source`, the engine attaches at `start()`, detaches at `stop()`, isolates failures, and emits `source.attach` / `.publish` / `.detach` / `.error` observation events for plugins.
|
|
405
|
+
|
|
406
|
+
**Don't subscribe in both an effect AND a source on the same channel** — the effect re-runs on fact changes, the source mounts once, you'll get 2× messages with silent duplicates. Pick one.
|
|
407
|
+
|
|
408
|
+
See [`sources.md`](./sources.md) for the full decision tree, recipes (Supabase / WebSocket / browser events / polling), and the typed-publish factory pattern.
|
|
409
|
+
|
|
410
|
+
## 21. Abbreviating Type Names (`*Def` instead of `*Definition`)
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// WRONG – `Def` is short for `Definition`. Same anti-pattern as `ctx`
|
|
414
|
+
// → `context` (entry #5). Source code reads better with spelled-out
|
|
415
|
+
// names; the minifier handles the bytes either way.
|
|
416
|
+
import type { ModuleDef, SourceDef, ResolverDef } from "@directive-run/core";
|
|
417
|
+
|
|
418
|
+
// CORRECT – use the spelled-out aliases. `*Def` stays canonical in 1.x
|
|
419
|
+
// for back-compat; consumers can migrate today.
|
|
420
|
+
import type {
|
|
421
|
+
ModuleDefinition,
|
|
422
|
+
SourceDefinition,
|
|
423
|
+
ResolverDefinition,
|
|
424
|
+
} from "@directive-run/core";
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
The `*Definition` aliases ship in 1.x via `export type { X as XDefinition }`
|
|
428
|
+
in `packages/core/src/core/types/index.ts`, so generic forwarding +
|
|
429
|
+
TS inference rules (mapped types, conditional distribution, tagged-
|
|
430
|
+
union discrimination, barrel re-exports) match the canonical `*Def`
|
|
431
|
+
form bit-for-bit. **2.0 swaps:** `*Definition` becomes canonical and
|
|
432
|
+
`*Def` becomes the deprecated alias. Start writing `*Definition` in
|
|
433
|
+
new code today so the 2.0 migration is a no-op.
|
|
434
|
+
|
|
435
|
+
Per RFC 0006, this applies to:
|
|
436
|
+
|
|
437
|
+
- `ModuleDef` → `ModuleDefinition`
|
|
438
|
+
- `ConstraintDef` / `ConstraintsDef` → `ConstraintDefinition` / `ConstraintsDefinition`
|
|
439
|
+
- `ResolverDef` / `ResolversDef` → `ResolverDefinition` / `ResolversDefinition`
|
|
440
|
+
- `DerivationDef` / `DerivationsDef` → `DerivationDefinition` / `DerivationsDefinition`
|
|
441
|
+
- `EffectDef` / `EffectsDef` → `EffectDefinition` / `EffectsDefinition`
|
|
442
|
+
- `EventsDef` → `EventsDefinition`
|
|
443
|
+
- `SourceDef` / `SourcesDef` → `SourceDefinition` / `SourcesDefinition`
|
|
444
|
+
- `SourcePublish` → `SourcePublishFn`, `SourceUnsubscribe` → `SourceUnsubscribeFn`
|
|
445
|
+
- Plus all `Typed*Def`, `CrossModule*Def`, `Dynamic*Def` variants.
|
|
446
|
+
|
|
447
|
+
`EffectCleanup`, `MetaAccessor`, `EventsAccessor`, `DeriveAccessor`,
|
|
448
|
+
`Snapshot` are explicitly kept as-is (each has reasoning recorded in
|
|
449
|
+
RFC 0006).
|
|
450
|
+
|
|
360
451
|
## Quick Reference Checklist
|
|
361
452
|
|
|
362
453
|
Before generating any Directive code, verify:
|
|
@@ -371,10 +462,12 @@ Before generating any Directive code, verify:
|
|
|
371
462
|
8. Multi-module uses `facts.self.*` for own facts
|
|
372
463
|
9. Imports from `@directive-run/core`, not deep paths
|
|
373
464
|
10. `await system.settle()` after `system.start()`
|
|
465
|
+
11. External event subscriptions live in `sources:`, not in `useEffect` or `onMount`
|
|
374
466
|
|
|
375
467
|
## See also
|
|
376
468
|
|
|
377
469
|
- [`naming.md`](./naming.md) — the strict canonical-term rules AND the alias map for cross-paradigm searches
|
|
470
|
+
- [`sources.md`](./sources.md) — the source primitive (the right answer for #20)
|
|
378
471
|
- [`constraints.md`](./constraints.md) — the constraint shape these anti-patterns reference (`facts` not in scope inside static `require:`, etc.)
|
|
379
472
|
- [`resolvers.md`](./resolvers.md) — the resolver shape these anti-patterns reference (return `void`, mutate `context.facts`, `(req, context)` not `(req, ctx)`)
|
|
380
473
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that exist and the hallucinated ones (`t.map`, `t.set`, `t.promise`, `t.date`) that don't
|
|
@@ -11,6 +11,8 @@ User wants to...
|
|
|
11
11
|
├── Store state → schema.facts + init()
|
|
12
12
|
├── Compute derived values → schema.derivations + derive
|
|
13
13
|
├── React to state changes → effects
|
|
14
|
+
├── Subscribe to an external event stream (WebSocket, Supabase realtime,
|
|
15
|
+
│ browser events, MCP server lifecycle) → sources
|
|
14
16
|
├── Trigger side effects when conditions are met → constraints + resolvers
|
|
15
17
|
├── Handle user actions → schema.events + events handlers
|
|
16
18
|
└── Coordinate multiple modules → createSystem({ modules: {} })
|
|
@@ -233,6 +235,7 @@ const typed = createModule("typed", {
|
|
|
233
235
|
|
|
234
236
|
- [`constraints.md`](./constraints.md) — the demand side of the constraint-resolver loop
|
|
235
237
|
- [`resolvers.md`](./resolvers.md) — the supply side that fulfills requirements
|
|
238
|
+
- [`sources.md`](./sources.md) — typed external event sources (the inbound dual of effects)
|
|
236
239
|
- [`schema-types.md`](./schema-types.md) — the `t.*()` builders that define fact and derivation types
|
|
237
240
|
- [`multi-module.md`](./multi-module.md) — when a single module isn't enough
|
|
238
241
|
- [`system-api.md`](./system-api.md) — `createSystem` and the runtime instance once your module is defined
|
|
@@ -16,7 +16,8 @@ This file is both the rules and the bridge. Search for the term you already know
|
|
|
16
16
|
| **thunk**, **saga**, **effect**, **middleware**, **listener** | Redux Toolkit, redux-saga, redux-observable, NgRx | **resolvers** | Async logic that mutates state in response to a requirement. Same role as a thunk; declared once, run by the runtime. |
|
|
17
17
|
| **rule**, **condition**, **guard**, **trigger**, **policy** | XState guards, Cerbos, OPA, business-rule engines | **constraints** | Declarative "when X is true, Y must happen." Constraints emit requirements; resolvers fulfill them. |
|
|
18
18
|
| **request**, **query**, **side-effect spec** | React Query / TanStack Query queries, Apollo queries | **requirements** | The object a constraint emits. The runtime dedupes them, dispatches to resolvers, tracks status. |
|
|
19
|
-
| **subscription**, **listener**, **reaction**, **watcher** | Zustand subscribe, MobX reactions, Redux subscriptions | **effects** |
|
|
19
|
+
| **subscription**, **listener**, **reaction**, **watcher** | Zustand subscribe, MobX reactions, Redux subscriptions | **effects** (for fact-change reactions) or **sources** (for external event streams) | Reacting to facts → **effects**; subscribing to an external stream that publishes INTO facts → **sources**. Both replace ad-hoc `subscribe()`/`useEffect` patterns. |
|
|
20
|
+
| **observable**, **subject**, **event source**, **WebSocket/SSE subscription** | RxJS, DOM `addEventListener`, EventSource, Supabase Realtime | **sources** | The inbound dual of effects. Mount-once external event stream the runtime owns. Declared on a module; auto-detached on `system.stop()`. |
|
|
20
21
|
| **slice**, **feature**, **domain**, **bounded context** | Redux Toolkit slices, NgRx feature modules | **modules** | Encapsulates facts + derivations + constraints + resolvers + events for one bounded slice of the system. |
|
|
21
22
|
| **store**, **container**, **context**, **app state** | Redux Store, Pinia, React Context, NgRx Store | **system** | The runtime that wires modules together, evaluates constraints, dispatches resolvers, and exposes observation. |
|
|
22
23
|
| **state machine**, **statechart**, **xstate machine** | XState | (closest: **module** + **constraints**) | Directive isn't a state machine — but if your XState `states` model business rules (not UI flow), constraints + facts express the same logic declaratively with no transition functions. |
|
|
@@ -54,6 +55,10 @@ Tagged union `{ type: "FETCH_USER", … }`. Constraints emit them; the runtime d
|
|
|
54
55
|
|
|
55
56
|
Declared via `effects: { name: { run: (facts, prev) => … } }`. Run on relevant fact changes. The auto-tracking dependency model means no `deps` array unless you specifically need a partial dep set.
|
|
56
57
|
|
|
58
|
+
### `sources` — typed external event subscriptions
|
|
59
|
+
|
|
60
|
+
The inbound dual of effects. Declared via `sources: { name: { attach: (publish) => cleanup } }`. The runtime mounts each source once at `system.start()` and unmounts at `system.stop()`. Inside `attach`, the source subscribes to an external event stream (WebSocket, Supabase realtime, browser events, MCP server lifecycle) and calls `publish(eventName, payload)` to dispatch into the system's event queue. Sources are the canonical replacement for `useEffect(() => subscribe(); return unsubscribe)` patterns. See [`sources.md`](./sources.md).
|
|
61
|
+
|
|
57
62
|
### `events` — typed mutation entry points
|
|
58
63
|
|
|
59
64
|
Declared via `events: { name: (facts, payload) => … }`. Called as `system.events.name(payload)`. The typed events accessor is the canonical way to dispatch — `system.dispatch({type, ...payload})` works but the typed accessor carries autocomplete.
|