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,395 @@
1
+ ---
2
+ name: effect-managed-runtime
3
+ description: Bridge Effect services into non-Effect frameworks (Hono, Express, Fastify, Lambda, Workers) using ManagedRuntime. Use this skill when you need to run Effects outside of runMain — in HTTP handlers, serverless functions, or any embedded context where Effect doesn't own the process lifecycle.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in integrating Effect services into external frameworks using ManagedRuntime.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
+ Browse and read files there directly to look up APIs, types, and implementations.
12
+
13
+ Reference this for:
14
+
15
+ - `packages/effect/src/ManagedRuntime.ts` — full API surface
16
+ - `ai-docs/src/03_integration/10_managed-runtime.ts` — Hono integration example
17
+
18
+ ## When to Use ManagedRuntime
19
+
20
+ Use ManagedRuntime when Effect does **not** own the process lifecycle. This is the bridge pattern — your domain logic lives in Effect services and layers, but the outer framework (Hono, Express, Fastify, Koa, AWS Lambda, Cloudflare Workers, etc.) controls the HTTP server, routing, and process lifecycle.
21
+
22
+ Typical scenarios:
23
+
24
+ - **Web frameworks**: Hono, Express, Fastify, Koa handlers that call Effect services
25
+ - **Serverless functions**: AWS Lambda, Cloudflare Workers, Vercel Edge Functions
26
+ - **Embedded contexts**: Running Effect inside a larger non-Effect application
27
+ - **Tests**: Creating a runtime with test layers for integration testing
28
+
29
+ Do NOT use ManagedRuntime when:
30
+
31
+ - Effect owns the entire process → use `NodeRuntime.runMain` / `BunRuntime.runMain`
32
+ - You have a long-running Effect service that IS the application → use `Layer.launch`
33
+
34
+ ## Core API
35
+
36
+ ### Creating a ManagedRuntime
37
+
38
+ ```ts
39
+ import { ManagedRuntime, Layer } from 'effect';
40
+
41
+ // ManagedRuntime.make takes a Layer and returns a ManagedRuntime
42
+ // The layer's requirements must be fully satisfied (no remaining R)
43
+ const runtime = ManagedRuntime.make(MyService.layer);
44
+ ```
45
+
46
+ **Signature:**
47
+
48
+ ```ts
49
+ ManagedRuntime.make<R, ER>(
50
+ layer: Layer.Layer<R, ER, never>,
51
+ options?: { readonly memoMap?: Layer.MemoMap | undefined }
52
+ ): ManagedRuntime<R, ER>
53
+ ```
54
+
55
+ The `ManagedRuntime<R, ER>` type parameters:
56
+
57
+ - `R` — the services provided by the runtime (available to effects you run)
58
+ - `ER` — errors that can occur during layer construction
59
+
60
+ ### Shared MemoMap
61
+
62
+ When using multiple ManagedRuntime instances in the same application, share a MemoMap so that memoized layers (the default) are built only once:
63
+
64
+ ```ts
65
+ import { Layer, ManagedRuntime } from 'effect';
66
+
67
+ // Create a global memo map shared across all runtimes
68
+ const appMemoMap = Layer.makeMemoMapUnsafe();
69
+
70
+ const runtime1 = ManagedRuntime.make(ServiceA.layer, { memoMap: appMemoMap });
71
+ const runtime2 = ManagedRuntime.make(ServiceB.layer, { memoMap: appMemoMap });
72
+ ```
73
+
74
+ If you only have a single ManagedRuntime, you can omit the `memoMap` option — one is created automatically.
75
+
76
+ ## Running Effects
77
+
78
+ A ManagedRuntime provides all the standard run methods. The runtime automatically provides the services from its layer to every effect you run.
79
+
80
+ ### `runtime.runPromise(effect)` — Async execution (most common)
81
+
82
+ ```ts
83
+ const result = await runtime.runPromise(
84
+ MyService.use((svc) => svc.doSomething(input))
85
+ );
86
+ ```
87
+
88
+ Returns a `Promise<A>`. Rejects with the first error or exception. Use this in async HTTP handlers, Lambda handlers, etc.
89
+
90
+ ### `runtime.runSync(effect)` — Synchronous execution
91
+
92
+ ```ts
93
+ const result = runtime.runSync(MyService.use((svc) => svc.computeSync(input)));
94
+ ```
95
+
96
+ Throws if the effect is async or fails. Use sparingly — only when you are certain the effect is synchronous.
97
+
98
+ ### `runtime.runFork(effect)` — Fire and forget
99
+
100
+ ```ts
101
+ const fiber = runtime.runFork(MyService.use((svc) => svc.backgroundTask()));
102
+ ```
103
+
104
+ Returns a `Fiber<A, E | ER>`. The effect runs in the background. Use for fire-and-forget work.
105
+
106
+ ### `runtime.runCallback(effect)` — Callback-style execution
107
+
108
+ ```ts
109
+ const cancel = runtime.runCallback(effect, {
110
+ onExit: (exit) => {
111
+ // handle exit
112
+ }
113
+ });
114
+ // cancel() to interrupt
115
+ ```
116
+
117
+ Use for callback-only APIs (e.g., some Node.js patterns).
118
+
119
+ ### `runtime.runPromiseExit(effect)` / `runtime.runSyncExit(effect)`
120
+
121
+ Return `Exit<A, E | ER>` instead of throwing — useful when you want to inspect failures structurally.
122
+
123
+ ## Lifecycle Management
124
+
125
+ ManagedRuntime **owns the scope** of the layers it builds. When you dispose the runtime, all resources acquired during layer construction (database pools, HTTP clients, file handles, etc.) are released.
126
+
127
+ ### Disposing the runtime
128
+
129
+ ```ts
130
+ // Async dispose (returns Promise<void>)
131
+ await runtime.dispose();
132
+
133
+ // Effect-based dispose (composable)
134
+ yield* runtime.disposeEffect;
135
+ ```
136
+
137
+ `ManagedRuntime` also implements `Symbol.asyncDispose`, so environments with explicit resource management can dispose it automatically:
138
+
139
+ ```ts
140
+ await using runtime = ManagedRuntime.make(AppLayer);
141
+ const result = await runtime.runPromise(program);
142
+ ```
143
+
144
+ ### Shutdown hook pattern
145
+
146
+ ```ts
147
+ const shutdown = () => {
148
+ void runtime.dispose();
149
+ };
150
+ process.once('SIGINT', shutdown);
151
+ process.once('SIGTERM', shutdown);
152
+ ```
153
+
154
+ **Critical**: Always dispose ManagedRuntime on shutdown. Unlike `runMain` which handles this automatically, ManagedRuntime requires explicit lifecycle management.
155
+
156
+ ## Integration Patterns
157
+
158
+ ### Hono
159
+
160
+ ```ts
161
+ import { Effect, Layer, ManagedRuntime, Ref, Schema, Context } from 'effect';
162
+ import { Hono } from 'hono';
163
+
164
+ class Todo extends Schema.Class<Todo>('Todo')({
165
+ id: Schema.Number,
166
+ title: Schema.String,
167
+ completed: Schema.Boolean
168
+ }) {}
169
+
170
+ class CreateTodoPayload extends Schema.Class<CreateTodoPayload>(
171
+ 'CreateTodoPayload'
172
+ )({
173
+ title: Schema.String
174
+ }) {}
175
+
176
+ class TodoNotFound extends Schema.TaggedError<TodoNotFound>()(
177
+ 'TodoNotFound',
178
+ {
179
+ id: Schema.Number
180
+ }
181
+ ) {}
182
+
183
+ class TodoRepo extends Context.Service<
184
+ TodoRepo,
185
+ {
186
+ readonly getAll: Effect.Effect<ReadonlyArray<Todo>>;
187
+ getById(id: number): Effect.Effect<Todo, TodoNotFound>;
188
+ create(payload: CreateTodoPayload): Effect.Effect<Todo>;
189
+ }
190
+ >()('app/TodoRepo') {
191
+ static readonly layer = Layer.effect(
192
+ TodoRepo,
193
+ Effect.gen(function* () {
194
+ const store = new Map<number, Todo>();
195
+ const nextId = yield* Ref.make(1);
196
+
197
+ const getAll = Effect.gen(function* () {
198
+ return Array.from(store.values());
199
+ }).pipe(Effect.withSpan('TodoRepo.getAll'));
200
+
201
+ const getById = Effect.fn('TodoRepo.getById')(function* (
202
+ id: number
203
+ ) {
204
+ const todo = store.get(id);
205
+ if (todo === undefined) {
206
+ return yield* new TodoNotFound({ id });
207
+ }
208
+ return todo;
209
+ });
210
+
211
+ const create = Effect.fn('TodoRepo.create')(function* (
212
+ payload: CreateTodoPayload
213
+ ) {
214
+ const id = yield* Ref.getAndUpdate(
215
+ nextId,
216
+ (current) => current + 1
217
+ );
218
+ const todo = new Todo({
219
+ id,
220
+ title: payload.title,
221
+ completed: false
222
+ });
223
+ store.set(id, todo);
224
+ return todo;
225
+ });
226
+
227
+ return TodoRepo.of({ getAll, getById, create });
228
+ })
229
+ );
230
+ }
231
+
232
+ // Shared memo map + ManagedRuntime
233
+ const appMemoMap = Layer.makeMemoMapUnsafe();
234
+ const runtime = ManagedRuntime.make(TodoRepo.layer, { memoMap: appMemoMap });
235
+
236
+ const app = new Hono();
237
+
238
+ app.get('/todos', async (c) => {
239
+ const todos = await runtime.runPromise(TodoRepo.use((repo) => repo.getAll));
240
+ return c.json(todos);
241
+ });
242
+
243
+ app.get('/todos/:id', async (c) => {
244
+ const id = Number(c.req.param('id'));
245
+ if (!Number.isFinite(id)) {
246
+ return c.json({ message: 'Todo id must be a number' }, 400);
247
+ }
248
+ const todo = await runtime.runPromise(
249
+ TodoRepo.use((repo) => repo.getById(id)).pipe(
250
+ Effect.catchTag('TodoNotFound', () => Effect.succeed(null))
251
+ )
252
+ );
253
+ if (todo === null) {
254
+ return c.json({ message: 'Todo not found' }, 404);
255
+ }
256
+ return c.json(todo);
257
+ });
258
+
259
+ app.post('/todos', async (c) => {
260
+ const body = await c.req.json();
261
+ const payload = Schema.decodeUnknownSync(CreateTodoPayload)(body);
262
+ const todo = await runtime.runPromise(
263
+ TodoRepo.use((repo) => repo.create(payload))
264
+ );
265
+ return c.json(todo, 201);
266
+ });
267
+
268
+ // Shutdown
269
+ const shutdown = () => {
270
+ void runtime.dispose();
271
+ };
272
+ process.once('SIGINT', shutdown);
273
+ process.once('SIGTERM', shutdown);
274
+ ```
275
+
276
+ ### Express
277
+
278
+ ```ts
279
+ import express from 'express';
280
+
281
+ const app = express();
282
+ app.use(express.json());
283
+
284
+ const runtime = ManagedRuntime.make(AppLayer);
285
+
286
+ app.get('/users/:id', async (req, res) => {
287
+ try {
288
+ const user = await runtime.runPromise(
289
+ UserService.use((svc) => svc.getById(req.params.id))
290
+ );
291
+ res.json(user);
292
+ } catch (error) {
293
+ res.status(500).json({ message: 'Internal error' });
294
+ }
295
+ });
296
+
297
+ // Graceful shutdown
298
+ const server = app.listen(3000);
299
+ process.once('SIGTERM', () => {
300
+ server.close(() => {
301
+ void runtime.dispose();
302
+ });
303
+ });
304
+ ```
305
+
306
+ ### AWS Lambda
307
+
308
+ ```ts
309
+ import { ManagedRuntime } from 'effect';
310
+
311
+ // Runtime created at module scope — reused across warm invocations
312
+ const runtime = ManagedRuntime.make(AppLayer);
313
+
314
+ export const handler = async (event: APIGatewayEvent) => {
315
+ const result = await runtime.runPromise(
316
+ MyService.use((svc) => svc.handleRequest(event))
317
+ );
318
+ return {
319
+ statusCode: 200,
320
+ body: JSON.stringify(result)
321
+ };
322
+ };
323
+
324
+ // Keep the module-scoped runtime alive for warm reuse.
325
+ // Do not dispose after every invocation; dispose only when the runtime is
326
+ // no longer needed or if your host exposes a real shutdown/lifecycle hook.
327
+ ```
328
+
329
+ ### Cloudflare Workers
330
+
331
+ ```ts
332
+ import { ManagedRuntime } from 'effect';
333
+
334
+ // Module-level runtime for warm reuse while the Worker isolate stays alive
335
+ const runtime = ManagedRuntime.make(AppLayer);
336
+
337
+ export default {
338
+ async fetch(request: Request): Promise<Response> {
339
+ const result = await runtime.runPromise(
340
+ MyService.use((svc) => svc.handle(request))
341
+ );
342
+ return new Response(JSON.stringify(result), {
343
+ headers: { 'content-type': 'application/json' }
344
+ });
345
+ }
346
+ };
347
+ ```
348
+
349
+ ## ManagedRuntime vs Layer.launch
350
+
351
+ | | ManagedRuntime | Layer.launch |
352
+ | ------------------------- | -------------------------------------- | -------------------------------------------- |
353
+ | **Who owns the process?** | External framework | Effect |
354
+ | **Use case** | Bridge to Hono/Express/Lambda/etc. | Effect IS the application |
355
+ | **How to run effects** | `runtime.runPromise(effect)` | Effects run inside the layer graph |
356
+ | **Lifecycle** | Manual dispose on shutdown | Automatic — runs until interrupted |
357
+ | **Typical shape** | Web handler calls `runtime.runPromise` | `Layer.launch(pipe(...layers))` in `runMain` |
358
+
359
+ Use `Layer.launch` when Effect is the entire application — it builds the layer, runs it, and tears it down when the process exits. Use `ManagedRuntime` when you need to call into Effect from non-Effect code.
360
+
361
+ ## ManagedRuntime vs runMain
362
+
363
+ | | ManagedRuntime | runMain |
364
+ | ------------------------- | ------------------------------------- | ----------------------- |
365
+ | **Context** | Embedded in another framework | Effect owns the process |
366
+ | **Signal handling** | Manual (you wire SIGINT/SIGTERM) | Automatic |
367
+ | **Scope management** | Manual dispose | Automatic |
368
+ | **Multiple entry points** | Yes — many handlers share one runtime | Single entry point |
369
+
370
+ `runMain` is for CLI apps and standalone Effect services. ManagedRuntime is for when your Effect code lives inside a larger system.
371
+
372
+ ## Type-Level Utilities
373
+
374
+ Extract service and error types from a ManagedRuntime:
375
+
376
+ ```ts
377
+ type Services = ManagedRuntime.ManagedRuntime.Services<typeof runtime>;
378
+ type Error = ManagedRuntime.ManagedRuntime.Error<typeof runtime>;
379
+ ```
380
+
381
+ ## Guard
382
+
383
+ ```ts
384
+ import { ManagedRuntime } from 'effect';
385
+
386
+ ManagedRuntime.isManagedRuntime(value); // type guard
387
+ ```
388
+
389
+ ## Common Mistakes
390
+
391
+ 1. **Forgetting to dispose** — Leaks resources. Wire up shutdown hooks for long-running hosts; in serverless, keep warm runtimes alive and dispose only when the runtime is no longer needed or the host exposes a real shutdown/lifecycle hook.
392
+ 2. **Creating a runtime per request** — Expensive. Create once at module/app scope, share across handlers.
393
+ 3. **Not sharing MemoMap** — If you have multiple runtimes, layers won't be deduplicated without a shared MemoMap.
394
+ 4. **Using runSync for async effects** — Will throw. Use `runPromise` for anything that might be async.
395
+ 5. **Catching errors outside Effect** — Prefer `Effect.catchTag` / `Effect.catchTags` inside the effect pipeline before calling `runPromise`, so errors are handled structurally rather than as untyped promise rejections.