opencode-effect-enforcer 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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,384 @@
1
+ ---
2
+ name: effect-pubsub-event-bus
3
+ description: Build typed event buses using Effect PubSub and Stream. Use this skill when implementing publish/subscribe communication between services, replacing callback-based event systems, or building reactive service architectures with typed event streams.
4
+ ---
5
+
6
+ # PubSub Event Bus with Effect v4
7
+
8
+ ## Overview
9
+
10
+ Effect's `PubSub` module provides a typed, composable publish/subscribe primitive. Combined with `Stream.fromPubSub` and `Effect.forkScoped`, it replaces imperative callback-based event buses with a fully typed, stream-based subscription model where cleanup is automatic.
11
+
12
+ **When to use this skill:**
13
+
14
+ - Building intra-service communication (event bus, message broker)
15
+ - Replacing callback-based event systems with typed streams
16
+ - Implementing reactive patterns where services subscribe to domain events
17
+ - Managing per-instance event channels with scoped cleanup
18
+
19
+ ## Import Pattern
20
+
21
+ ```typescript
22
+ import { Effect, PubSub, Stream } from 'effect';
23
+ ```
24
+
25
+ ## Core Pattern: Typed Event Bus Service
26
+
27
+ Define events as a discriminated union, then build a Bus service that publishes and subscribes using `PubSub`:
28
+
29
+ ```typescript
30
+ import { Effect, Layer, PubSub, Schema, Context, Stream } from 'effect';
31
+
32
+ // ─── Event Definitions ──────────────────────────────────────
33
+
34
+ class FileChanged extends Schema.TaggedClass<FileChanged>()('FileChanged', {
35
+ path: Schema.String,
36
+ kind: Schema.Literals(['created', 'modified', 'deleted'])
37
+ }) {}
38
+
39
+ class ConfigReloaded extends Schema.TaggedClass<ConfigReloaded>()(
40
+ 'ConfigReloaded',
41
+ {
42
+ source: Schema.String
43
+ }
44
+ ) {}
45
+
46
+ type BusEvent = FileChanged | ConfigReloaded;
47
+
48
+ // ─── Bus Service ─────────────────────────────────────────────
49
+
50
+ export namespace Bus {
51
+ export interface Interface {
52
+ readonly publish: (event: BusEvent) => Effect.Effect<void>;
53
+ readonly subscribe: <T extends BusEvent>(
54
+ eventClass: new (...args: ReadonlyArray<never>) => T
55
+ ) => Stream.Stream<T>;
56
+ readonly subscribeAll: Stream.Stream<BusEvent>;
57
+ }
58
+
59
+ export class Service extends Context.Service<Service, Interface>()(
60
+ '@app/Bus'
61
+ ) {}
62
+
63
+ export const layer = Layer.effect(
64
+ Service,
65
+ Effect.gen(function* () {
66
+ const pubsub = yield* PubSub.unbounded<BusEvent>();
67
+
68
+ // Cleanup: shutdown PubSub when scope closes
69
+ yield* Effect.addFinalizer(() => PubSub.shutdown(pubsub));
70
+
71
+ const publish = Effect.fn('Bus.publish')(function* (
72
+ event: BusEvent
73
+ ) {
74
+ yield* PubSub.publish(pubsub, event);
75
+ });
76
+
77
+ const subscribe = <T extends BusEvent>(
78
+ eventClass: new (...args: ReadonlyArray<never>) => T
79
+ ): Stream.Stream<T> =>
80
+ Stream.fromPubSub(pubsub).pipe(
81
+ Stream.filter((evt): evt is T => evt instanceof eventClass)
82
+ );
83
+
84
+ const subscribeAll = Stream.fromPubSub(pubsub);
85
+
86
+ return Service.of({ publish, subscribe, subscribeAll });
87
+ })
88
+ );
89
+ }
90
+ ```
91
+
92
+ ## Subscribing to Events
93
+
94
+ Consumers use `Stream.fromPubSub` (via the Bus service) + `Effect.forkScoped` to register subscriptions that are automatically cleaned up when the scope closes:
95
+
96
+ ```typescript
97
+ import { Effect, Stream } from 'effect';
98
+
99
+ const setupFileWatcher = Effect.gen(function* () {
100
+ const bus = yield* Bus.Service;
101
+
102
+ // Fork a scoped fiber that processes FileChanged events
103
+ yield* bus.subscribe(FileChanged).pipe(
104
+ Stream.filter((evt) => evt.path.endsWith('.ts')),
105
+ Stream.runForEach((evt) =>
106
+ Effect.gen(function* () {
107
+ yield* Effect.logInfo(
108
+ `File changed: ${evt.path} (${evt.kind})`
109
+ );
110
+ yield* reloadModule(evt.path);
111
+ })
112
+ ),
113
+ Effect.forkScoped
114
+ );
115
+ });
116
+ ```
117
+
118
+ Key points:
119
+
120
+ - **`Effect.forkScoped`** ties the subscription fiber's lifetime to the enclosing scope — no explicit unsubscribe needed
121
+ - **Stream combinators** (`filter`, `map`, `debounce`, `groupBy`) compose naturally before `runForEach`
122
+ - **No `acquireRelease` bookkeeping** — the stream and its fiber are cleaned up automatically
123
+
124
+ ### Why `forkScoped` and not `forkChild`
125
+
126
+ `Effect.forkScoped` ties the fiber to the `Scope` lifecycle, while `Effect.forkChild` ties it to the parent fiber. For subscriptions registered during service construction (inside `Layer.effect`), `forkScoped` is correct because the subscription must live as long as the layer's scope, not the constructing fiber.
127
+
128
+ ## Publishing Events
129
+
130
+ Publishing is straightforward — call `PubSub.publish` or use the Bus service:
131
+
132
+ ```typescript
133
+ const onFileChange = Effect.fn('Watcher.onFileChange')(function* (
134
+ path: string,
135
+ kind: 'created' | 'modified' | 'deleted'
136
+ ) {
137
+ const bus = yield* Bus.Service;
138
+ yield* bus.publish(new FileChanged({ path, kind }));
139
+ });
140
+ ```
141
+
142
+ ## Pattern: Per-Type PubSub Channels
143
+
144
+ For high-throughput systems, maintain a `Map` of per-type PubSub channels to avoid filtering overhead on the wildcard channel:
145
+
146
+ ```typescript
147
+ import { Effect, PubSub, Stream } from 'effect';
148
+
149
+ interface Payload {
150
+ readonly type: string;
151
+ readonly data: unknown;
152
+ }
153
+
154
+ const make = Effect.gen(function* () {
155
+ const wildcard = yield* PubSub.unbounded<Payload>();
156
+ const typed = new Map<string, PubSub.PubSub<Payload>>();
157
+
158
+ yield* Effect.addFinalizer(() =>
159
+ Effect.gen(function* () {
160
+ yield* PubSub.shutdown(wildcard);
161
+ for (const ps of typed.values()) {
162
+ yield* PubSub.shutdown(ps);
163
+ }
164
+ })
165
+ );
166
+
167
+ const getOrCreate = (type: string) =>
168
+ Effect.gen(function* () {
169
+ const existing = typed.get(type);
170
+ if (existing) return existing;
171
+ const ps = yield* PubSub.unbounded<Payload>();
172
+ typed.set(type, ps);
173
+ return ps;
174
+ });
175
+
176
+ const publish = Effect.fn('Bus.publish')(function* (event: Payload) {
177
+ yield* PubSub.publish(wildcard, event);
178
+ const ps = typed.get(event.type);
179
+ if (ps) yield* PubSub.publish(ps, event);
180
+ });
181
+
182
+ const subscribe = (type: string): Stream.Stream<Payload> =>
183
+ Stream.unwrap(
184
+ getOrCreate(type).pipe(Effect.map((ps) => Stream.fromPubSub(ps)))
185
+ );
186
+
187
+ const subscribeAll = Stream.fromPubSub(wildcard);
188
+
189
+ return { publish, subscribe, subscribeAll };
190
+ });
191
+ ```
192
+
193
+ ## Pattern: Graceful Shutdown Event
194
+
195
+ Publish a final event before shutting down PubSub channels so subscribers can perform cleanup:
196
+
197
+ ```typescript
198
+ yield*
199
+ Effect.addFinalizer(() =>
200
+ Effect.gen(function* () {
201
+ // Notify all subscribers that the bus is shutting down
202
+ yield* PubSub.publish(
203
+ wildcard,
204
+ new InstanceDisposed({ reason: 'scope-closed' })
205
+ );
206
+ // Then shut down the channel
207
+ yield* PubSub.shutdown(wildcard);
208
+ })
209
+ );
210
+ ```
211
+
212
+ Subscribers can detect this event and perform teardown:
213
+
214
+ ```typescript
215
+ yield*
216
+ bus.subscribeAll.pipe(
217
+ Stream.takeUntil((evt) => evt instanceof InstanceDisposed),
218
+ Stream.runForEach(handleEvent),
219
+ Effect.forkScoped
220
+ );
221
+ ```
222
+
223
+ ## Redis Pub/Sub
224
+
225
+ For cross-process pub/sub, use the portable `Redis.Redis` service rather than modeling Redis as an in-memory `PubSub`. `redis.subscribe(channel)` is scoped and returns a dequeue whose values contain both `channel` and `message`.
226
+
227
+ ```typescript
228
+ import { Effect, Stream } from 'effect';
229
+ import { Redis } from 'effect/unstable/persistence';
230
+
231
+ declare const handleRedisMessage: (
232
+ channel: string,
233
+ message: string
234
+ ) => Effect.Effect<void>;
235
+
236
+ const consumeRedisEvents = Effect.gen(function* () {
237
+ const redis = yield* Redis.Redis;
238
+ const subscription = yield* redis.subscribe('domain-events');
239
+
240
+ yield* Stream.fromQueue(subscription).pipe(
241
+ Stream.runForEach(({ channel, message }) =>
242
+ handleRedisMessage(channel, message)
243
+ )
244
+ );
245
+ }).pipe(Effect.scoped);
246
+ ```
247
+
248
+ The subscription uses a dedicated client and is released when the enclosing scope closes. Node and Deno reconnect and re-subscribe after connection interruptions; messages published during recovery can be lost. Bun disables subscriber auto-reconnect: a dropped connection fails the dequeue with `RedisError`, and the caller must create a new scoped subscription. In every runtime, queue failure is observable by `Queue` and `Stream.fromQueue` consumers.
249
+
250
+ ## Testing PubSub Services
251
+
252
+ Testing PubSub subscriptions requires specific choreography:
253
+
254
+ 1. Fork the consumer fiber
255
+ 2. Wait for subscriber readiness explicitly when possible; otherwise use a tiny registration barrier
256
+ 3. Publish events
257
+ 4. Gate on a `Deferred` for synchronization
258
+
259
+ ```typescript
260
+ import { Deferred, Effect, PubSub, Stream } from 'effect';
261
+
262
+ it.effect('should receive published events', () =>
263
+ Effect.gen(function* () {
264
+ const bus = yield* Bus.Service;
265
+ const received: Array<string> = [];
266
+ const done = yield* Deferred.make<void>();
267
+
268
+ // 1. Fork the consumer
269
+ yield* bus.subscribe(FileChanged).pipe(
270
+ Stream.runForEach((evt) =>
271
+ Effect.gen(function* () {
272
+ received.push(evt.path);
273
+ if (received.length === 2) {
274
+ yield* Deferred.succeed(done, undefined);
275
+ }
276
+ })
277
+ ),
278
+ Effect.forkScoped
279
+ );
280
+
281
+ // 2. Registration barrier
282
+ yield* Effect.sleep('10 millis');
283
+
284
+ // 3. Publish events
285
+ yield* bus.publish(new FileChanged({ path: 'a.ts', kind: 'modified' }));
286
+ yield* bus.publish(new FileChanged({ path: 'b.ts', kind: 'created' }));
287
+
288
+ // 4. Wait for events to be received
289
+ yield* Deferred.await(done);
290
+
291
+ expect(received).toEqual(['a.ts', 'b.ts']);
292
+ }).pipe(Effect.provide(Bus.layer))
293
+ );
294
+ ```
295
+
296
+ **Notes:**
297
+
298
+ - The `runForEach` handler is effectful, so complete the gate with `yield* Deferred.succeed(done, undefined)`. There is no `Deferred.unsafeDone` in v4; reach for the low-level `Deferred.doneUnsafe(done, Effect.void)` only inside a truly synchronous callback that has no surrounding effect.
299
+ - The tiny sleep above is an acceptable fallback for `Stream.fromPubSub` registration when no explicit readiness hook exists. If you control the consumer stream, prefer a readiness `Deferred` or latch instead.
300
+ - To drain a `PubSub` subscription for assertions, prefer `PubSub.takeUpTo(sub, n)`: it returns immediately with whatever is buffered (possibly an empty array). `PubSub.takeAll(sub)` **suspends when the subscription is empty** and returns a `NonEmptyArray`, so it cannot be used to assert “no more events” — it would hang waiting for one.
301
+
302
+ ## PubSub Configuration
303
+
304
+ ### Bounded vs Unbounded
305
+
306
+ ```typescript
307
+ // Unbounded — no capacity limit / backpressure for active subscribers
308
+ const ps = yield* PubSub.unbounded<Event>();
309
+
310
+ // Bounded — applies backpressure when full
311
+ const ps = yield* PubSub.bounded<Event>(1024);
312
+
313
+ // Sliding — drops oldest events when full
314
+ const ps = yield* PubSub.sliding<Event>(1024);
315
+
316
+ // Dropping — drops newest events when full
317
+ const ps = yield* PubSub.dropping<Event>(1024);
318
+
319
+ // Optional replay buffer: late subscribers first receive the most recent N values
320
+ const withReplay = yield* PubSub.unbounded<Event>({ replay: 10 });
321
+ const boundedReplay = yield* PubSub.bounded<Event>({ capacity: 1024, replay: 10 });
322
+ ```
323
+
324
+ Choose based on your use case:
325
+
326
+ - **`unbounded`** — no capacity limit or backpressure; nothing is dropped for subscribers that are already attached
327
+ - **`bounded`** — when backpressure is acceptable and memory must be bounded
328
+ - **`sliding`** — when the latest events matter most (metrics, status updates)
329
+ - **`dropping`** — when burst absorption is needed but current events take priority
330
+
331
+ **PubSub is not an event log.** Messages are delivered to *active* subscribers only. A subscriber that attaches after a value was published does not see that value unless a `replay` buffer is configured, and `replay` only retains the most recent N values — it is bounded, recent-only, and not durable storage. If you need every consumer to observe the full history, subscribe before publishing (see the testing choreography above) or persist events separately.
332
+
333
+ ## DO / DON'T
334
+
335
+ ### DO: Use `Stream.fromPubSub` + `forkScoped` for subscriptions
336
+
337
+ ```typescript
338
+ yield*
339
+ Stream.fromPubSub(pubsub).pipe(
340
+ Stream.filter(isRelevant),
341
+ Stream.runForEach(handle),
342
+ Effect.forkScoped
343
+ );
344
+ ```
345
+
346
+ ### DON'T: Use `PubSub.subscribe` with manual cleanup
347
+
348
+ ```typescript
349
+ // ❌ Overly complex — manual subscription management
350
+ const sub = yield* PubSub.subscribe(pubsub);
351
+ yield* Effect.acquireRelease(Effect.succeed(sub), (s) => Queue.shutdown(s));
352
+ ```
353
+
354
+ ### DO: Shut down PubSub in finalizers
355
+
356
+ ```typescript
357
+ yield* Effect.addFinalizer(() => PubSub.shutdown(pubsub));
358
+ ```
359
+
360
+ ### DON'T: Leave PubSub channels open
361
+
362
+ ```typescript
363
+ // ❌ Resource leak — subscribers may hang indefinitely
364
+ const pubsub = yield* PubSub.unbounded<Event>();
365
+ // No shutdown registered
366
+ ```
367
+
368
+ ### DO: Use `Stream.takeUntil` for shutdown-aware subscriptions
369
+
370
+ ```typescript
371
+ yield*
372
+ stream.pipe(
373
+ Stream.takeUntil((evt) => evt instanceof ShutdownEvent),
374
+ Stream.runForEach(handle),
375
+ Effect.forkScoped
376
+ );
377
+ ```
378
+
379
+ ## Related Skills
380
+
381
+ - **effect-service-implementation**: Service declaration patterns
382
+ - **effect-layer-design**: Layer composition and dependency management
383
+ - **effect-stream**: Stream processing patterns
384
+ - **effect-testing**: Testing Effect programs with @effect/vitest