@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.
@@ -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
- 19 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
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
- 19 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
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** | Side-effects that run when watched facts change. Cleaner than `subscribe()` callbacks because effects auto-track. |
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** | Side-effects that run when watched facts change. Cleaner than `subscribe()` callbacks because effects auto-track. |
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
- 19 most common mistakes when generating Directive code, ranked by AI hallucination frequency. Every code generation MUST be checked against this list.
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** | Side-effects that run when watched facts change. Cleaner than `subscribe()` callbacks because effects auto-track. |
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.