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,362 @@
1
+ ---
2
+ name: effect-incremental-migration
3
+ description: Incrementally migrate existing async/Promise-based modules to Effect services while preserving backward compatibility. Use this skill when effectifying an existing module, replacing async facades with Effect services, or maintaining dual async/Effect APIs during migration.
4
+ ---
5
+
6
+ # Incremental Migration Skill
7
+
8
+ This skill provides a step-by-step template for converting existing async/Promise-based modules to Effect services. Preserve backward compatibility only where you still have non-Effect callers. The main goal is to move Effect callers onto `yield* SomeService.Service` early so dependency edges become explicit in the code and in the layer graph.
9
+
10
+ ## Effect Source Reference
11
+
12
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
13
+ Browse and read files there directly to look up APIs, types, and implementations.
14
+
15
+ Reference this for:
16
+
17
+ - Context source: `packages/effect/src/Context.ts`
18
+ - Layer source: `packages/effect/src/Layer.ts`
19
+ - ManagedRuntime source: `packages/effect/src/ManagedRuntime.ts`
20
+ - Migration guide: `MIGRATION.md`
21
+ - Effect source: `packages/effect/src/`
22
+
23
+ ## The 7-Step Migration Template
24
+
25
+ ### Step 1: Define the Service Interface
26
+
27
+ Extract a named `Interface` type with Effect-returning methods. Keep parameter and return types identical to the original module — only swap `Promise<T>` for `Effect.Effect<T, E>`.
28
+
29
+ ```typescript
30
+ import type { Effect } from 'effect';
31
+
32
+ export namespace MyModule {
33
+ export interface Interface {
34
+ readonly get: (id: string) => Effect.Effect<Item, MyModuleError>;
35
+ readonly list: Effect.Effect<ReadonlyArray<Item>>;
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### Step 2: Declare the Service Class
41
+
42
+ Empty class body — no `make:`, no `static readonly layer`. The interface and service class live in the same namespace.
43
+
44
+ ```typescript
45
+ import { Context } from 'effect';
46
+ import type { Effect } from 'effect';
47
+
48
+ export namespace MyModule {
49
+ export interface Interface {
50
+ readonly get: (id: string) => Effect.Effect<Item, MyModuleError>;
51
+ readonly list: Effect.Effect<ReadonlyArray<Item>>;
52
+ }
53
+
54
+ export class Service extends Context.Service<Service, Interface>()(
55
+ '@app/MyModule'
56
+ ) {}
57
+ }
58
+ ```
59
+
60
+ ### Step 3: Build the Raw Layer
61
+
62
+ Construct the service inside `Layer.effect`, capturing dependencies via `yield*`. Use `Effect.fn` for traced methods.
63
+
64
+ ```typescript
65
+ import { Effect, Layer, Context } from 'effect';
66
+
67
+ export namespace MyModule {
68
+ export interface Interface {
69
+ readonly get: (id: string) => Effect.Effect<Item, MyModuleError>;
70
+ readonly list: Effect.Effect<ReadonlyArray<Item>>;
71
+ }
72
+
73
+ export class Service extends Context.Service<Service, Interface>()(
74
+ '@app/MyModule'
75
+ ) {}
76
+
77
+ export const layer = Layer.effect(
78
+ Service,
79
+ Effect.gen(function* () {
80
+ const config = yield* Config.Service;
81
+ const db = yield* Database.Service;
82
+
83
+ const get = Effect.fn('MyModule.get')(function* (id: string) {
84
+ const cfg = yield* config.get();
85
+ return yield* db.findById(cfg.table, id);
86
+ });
87
+
88
+ const list = Effect.fn('MyModule.list')(function* () {
89
+ const cfg = yield* config.get();
90
+ return yield* db.listAll(cfg.table);
91
+ });
92
+
93
+ return Service.of({ get, list });
94
+ })
95
+ );
96
+ }
97
+ ```
98
+
99
+ The layer's type exposes unsatisfied requirements (`Config.Service | Database.Service`). This is intentional — the raw layer declares its true dependency graph.
100
+
101
+ ### Step 4: Build the Wired Default Layer
102
+
103
+ Compose the wired `defaultLayer` directly in the normal case. Use `Layer.suspend(() => ...)` only if module evaluation order or a real circular import requires deferral.
104
+
105
+ ```typescript
106
+ import { Layer } from 'effect';
107
+
108
+ export namespace MyModule {
109
+ // ... Interface, Service, layer above ...
110
+
111
+ export const defaultLayer = layer.pipe(
112
+ Layer.provide(Config.defaultLayer),
113
+ Layer.provide(Database.defaultLayer)
114
+ );
115
+ }
116
+ ```
117
+
118
+ If the module really needs deferred composition:
119
+
120
+ ```typescript
121
+ export const defaultLayer = Layer.suspend(() =>
122
+ layer.pipe(Layer.provide(Config.defaultLayer))
123
+ );
124
+ ```
125
+
126
+ ### Step 5: Create the Runtime Bridge
127
+
128
+ A shared `memoMap` ensures layers are deduplicated across all per-service runtimes. Define the bridge utility once and reuse it across migrated modules.
129
+
130
+ ```typescript
131
+ import { Layer, ManagedRuntime } from 'effect';
132
+ import type { Effect, Context } from 'effect';
133
+
134
+ const memoMap = Layer.makeMemoMapUnsafe();
135
+
136
+ export function makeRuntime<I, S, E>(
137
+ service: Context.Service<I, S>,
138
+ layer: Layer.Layer<I, E>
139
+ ) {
140
+ let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined;
141
+ const getRuntime = () => (rt ??= ManagedRuntime.make(layer, { memoMap }));
142
+ return {
143
+ runPromise: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) =>
144
+ getRuntime().runPromise(service.use(fn)),
145
+ runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) =>
146
+ getRuntime().runSync(service.use(fn))
147
+ };
148
+ }
149
+ ```
150
+
151
+ Then in the module namespace, create the bridge from the service and its default layer:
152
+
153
+ ```typescript
154
+ const { runPromise } = makeRuntime(MyModule.Service, MyModule.defaultLayer);
155
+ ```
156
+
157
+ > **Memo-map nuance:** Keep the bridge `memoMap` shared (the root `Layer.makeMemoMapUnsafe()` above) so every per-service runtime reuses the same layer allocations. Do not `Layer.forkMemoMap` it unless a specific child runtime intentionally needs isolated allocations — a forked memo map can read the parent's existing allocations but builds new ones in isolation, which defeats the deduplication this bridge exists to provide.
158
+
159
+ ### Step 6: Keep Boundary Facades Only When Still Needed
160
+
161
+ If non-Effect callers still exist, wrap each service method in a thin `async` function that delegates to `runPromise`. Do not keep facades as the primary API once Effect callers can `yield*` the service directly.
162
+
163
+ ```typescript
164
+ export namespace MyModule {
165
+ // ... Interface, Service, layer, defaultLayer above ...
166
+
167
+ export async function get(id: string): Promise<Item> {
168
+ return runPromise((svc) => svc.get(id));
169
+ }
170
+
171
+ export async function list(): Promise<ReadonlyArray<Item>> {
172
+ return runPromise((svc) => svc.list);
173
+ }
174
+ }
175
+ ```
176
+
177
+ These facades are boundary shims. Do not add new internal Effect callers that go through them.
178
+
179
+ ### Step 7: Update Effect Callers Immediately
180
+
181
+ As soon as the service exists, replace `Effect.promise(() => facade())` calls in Effect code with direct service yields:
182
+
183
+ ```typescript
184
+ // Before: calling through async facade
185
+ const item = yield* Effect.promise(() => MyModule.get(id));
186
+
187
+ // After: yielding the service directly
188
+ const myModule = yield* MyModule.Service;
189
+ const item = yield* myModule.get(id);
190
+ ```
191
+
192
+ When replacing `Effect.promise(() => facade())` with direct service yields, errors that previously flowed as defects become typed channel errors. Update any `catchDefect` handlers to `catch` or `catchTag`.
193
+
194
+ This is the real migration milestone: the dependency graph becomes visible at the call site, and reviewers no longer need to remember which helper hides which runtime requirements.
195
+
196
+ **Common migration transformation — `Promise.all` fan-out to `Effect.forEach`:**
197
+
198
+ When migrating callers that use `Promise.all(items.map(async (x) => ...))`, replace with `Effect.forEach`:
199
+
200
+ ```typescript
201
+ // Before: wrapped Promise.all fan-out
202
+ const results =
203
+ yield*
204
+ Effect.promise(() =>
205
+ Promise.all(items.map(async (item) => processItem(item)))
206
+ );
207
+
208
+ // After: Effect.forEach with explicit concurrency
209
+ const results =
210
+ yield*
211
+ Effect.forEach(items, (item) => processItem(item), {
212
+ concurrency: 'unbounded'
213
+ });
214
+ ```
215
+
216
+ This transformation eliminates the `Effect.promise` wrapper entirely and gives explicit control over concurrency.
217
+
218
+ ### Step 8: Prune Dead Facades Aggressively
219
+
220
+ Once Effect callers have been updated to yield the service directly, the async facade functions from Step 6 become dead code. Remove them in a dedicated cleanup commit:
221
+
222
+ 1. Search for callers of each facade function (grep for the function name across the codebase)
223
+ 2. Verify no remaining callers exist outside of Effect service code
224
+ 3. Delete the facade functions and the runtime bridge (`runPromise`)
225
+ 4. If the runtime bridge was the last consumer of `defaultLayer`, the `makeRuntime` call can also be removed
226
+
227
+ **Prune in a separate commit.** Facade pruning is a pure deletion — it should be reviewable independently from the migration work that preceded it. This makes it easy to verify that no callers were missed.
228
+
229
+ ## Complete Before/After Example
230
+
231
+ ### Before: Plain Async Module
232
+
233
+ ```typescript
234
+ import { loadConfig } from './config';
235
+
236
+ export namespace Items {
237
+ let cachedConfig: Config | undefined;
238
+
239
+ async function getConfig(): Promise<Config> {
240
+ return (cachedConfig ??= await loadConfig());
241
+ }
242
+
243
+ export async function get(id: string): Promise<Item> {
244
+ const cfg = await getConfig();
245
+ const res = await fetch(`${cfg.apiUrl}/items/${id}`);
246
+ if (!res.ok) throw new Error(`Item ${id} not found`);
247
+ return res.json();
248
+ }
249
+
250
+ export async function list(): Promise<ReadonlyArray<Item>> {
251
+ const cfg = await getConfig();
252
+ const res = await fetch(`${cfg.apiUrl}/items`);
253
+ return res.json();
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### After: Effect Service with Backward-Compatible Facades
259
+
260
+ ```typescript
261
+ import { Effect, Layer, Schema, Context } from 'effect';
262
+ import { makeRuntime } from './runtime-bridge';
263
+
264
+ class ItemsError extends Schema.TaggedError<ItemsError>()('ItemsError', {
265
+ message: Schema.String
266
+ }) {}
267
+
268
+ export namespace Items {
269
+ // Step 1: Interface
270
+ export interface Interface {
271
+ readonly get: (id: string) => Effect.Effect<Item, ItemsError>;
272
+ readonly list: Effect.Effect<ReadonlyArray<Item>>;
273
+ }
274
+
275
+ // Step 2: Service class
276
+ export class Service extends Context.Service<Service, Interface>()(
277
+ '@app/Items'
278
+ ) {}
279
+
280
+ // Step 3: Raw layer
281
+ export const layer = Layer.effect(
282
+ Service,
283
+ Effect.gen(function* () {
284
+ const config = yield* AppConfig.Service;
285
+
286
+ const get = Effect.fn('Items.get')(function* (id: string) {
287
+ const cfg = yield* config.load();
288
+ const res = yield* Effect.tryPromise({
289
+ try: () => fetch(`${cfg.apiUrl}/items/${id}`),
290
+ catch: () =>
291
+ new ItemsError({ message: `Item ${id} not found` })
292
+ });
293
+ return yield* Effect.tryPromise({
294
+ try: () => res.json() as Promise<Item>,
295
+ catch: () =>
296
+ new ItemsError({ message: 'Failed to parse item' })
297
+ });
298
+ });
299
+
300
+ const list = Effect.fn('Items.list')(function* () {
301
+ const cfg = yield* config.load();
302
+ const res = yield* Effect.tryPromise({
303
+ try: () => fetch(`${cfg.apiUrl}/items`),
304
+ catch: () =>
305
+ new ItemsError({ message: 'Failed to list items' })
306
+ });
307
+ return yield* Effect.tryPromise({
308
+ try: () => res.json() as Promise<ReadonlyArray<Item>>,
309
+ catch: () =>
310
+ new ItemsError({ message: 'Failed to parse items' })
311
+ });
312
+ });
313
+
314
+ return Service.of({ get, list });
315
+ })
316
+ );
317
+
318
+ // Step 4: Default layer
319
+ export const defaultLayer = layer.pipe(
320
+ Layer.provide(AppConfig.defaultLayer)
321
+ );
322
+
323
+ // Step 5: Runtime bridge
324
+ const { runPromise } = makeRuntime(Service, defaultLayer);
325
+
326
+ // Step 6: Async facades (remove once all callers migrate)
327
+ export async function get(id: string): Promise<Item> {
328
+ return runPromise((svc) => svc.get(id));
329
+ }
330
+
331
+ export async function list(): Promise<ReadonlyArray<Item>> {
332
+ return runPromise((svc) => svc.list);
333
+ }
334
+ }
335
+
336
+ // Step 7: Callers migrate from facade to service
337
+ // const item = yield* Effect.promise(() => Items.get(id));
338
+ // becomes:
339
+ // const items = yield* Items.Service;
340
+ // const item = yield* items.get(id);
341
+ ```
342
+
343
+ ## Migration Checklist
344
+
345
+ - [ ] Interface type extracted with Effect-returning methods
346
+ - [ ] Service class declared with `Context.Service`
347
+ - [ ] Layer built with `Layer.effect` capturing dependencies via `yield*`
348
+ - [ ] Effect callers migrate to `yield* SomeService.Service` as early as possible
349
+ - [ ] Default layer composes directly unless deferred evaluation is truly required
350
+ - [ ] Runtime bridge created with shared `memoMap`
351
+ - [ ] Async facades kept only for remaining non-Effect boundaries
352
+ - [ ] Callers in Effect.gen blocks updated to yield service directly
353
+ - [ ] `Promise.all(items.map(...))` patterns replaced with `Effect.forEach`
354
+ - [ ] Former `catchDefect` handlers updated to `catch`/`catchTag` for now-typed errors
355
+ - [ ] Dead facade functions pruned in a separate commit once all callers migrated
356
+
357
+ ## Related Skills
358
+
359
+ - `effect-service-implementation` — service declaration patterns and capability design
360
+ - `effect-layer-design` — layer composition, merging, and dependency management
361
+ - `effect-managed-runtime` — ManagedRuntime lifecycle and `memoMap` usage
362
+ - `effect-error-handling` — typed errors, `catchTag`, and error channel design