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,682 @@
1
+ ---
2
+ name: effect-scope
3
+ description: Manage resource lifecycles with Effect Scope — Effect.acquireRelease/addFinalizer/scoped/scopedWith, onExit/ensuring finalizers, manual Scope.make/close/fork, ScopedRef, and scope ownership in Layers and fibers. Use when acquiring anything that needs guaranteed cleanup (file handles, connections, event listeners, temp files), when Scope appears in an effect's R, or when resources leak or are released too early.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in resource lifecycle management with `Scope`, finalizers, and scoped effects.
7
+
8
+ ## Effect Source Reference
9
+
10
+ The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — v4 differs substantially from v3 and from most training data.
11
+
12
+ Key files:
13
+
14
+ - `packages/effect/src/Scope.ts` — `Scope`/`Closeable` interfaces, state model, `make`, `fork`, `close`, `provide`, `use`, `addFinalizer(Exit)`
15
+ - `packages/effect/src/Effect.ts` — `acquireRelease`, `acquireDisposable`, `acquireUseRelease`, `addFinalizer`, `scoped`, `scopedWith`, `scope`, the `onExit`/`ensuring`/`onError`/`onInterrupt` family, `forkScoped`/`forkIn` (grep; the file is huge)
16
+ - `packages/effect/src/internal/effect.ts` — runtime semantics: finalizer ordering, close behavior, `scoped` implementation (search for `scopeClose`, `scopeCloseFinalizers`)
17
+ - `packages/effect/src/ScopedRef.ts` — resource-backed mutable reference with release-on-replace
18
+ - `packages/effect/src/Layer.ts` — how layers fork and refcount scopes (`fromBuild`, `memoMapBuild`)
19
+ - `packages/effect/test/Scope.test.ts` + `test/ScopedRef.test.ts` — edge-case semantics
20
+ - `ai-docs/src/01_effect/04_resources/` — runnable lessons: `acquireRelease` inside a Layer, background tasks via `Layer.effectDiscard` + `forkScoped`, `LayerMap`
21
+ - `migration/scope.md` + `migration/v3-to-v4.md` — v3 → v4 renames (`Scope.extend` → `Scope.provide`, `Layer.scoped` → `Layer.effect`)
22
+
23
+ ## Core Model
24
+
25
+ A `Scope` is a lifetime boundary. Acquiring a resource registers a cleanup effect (a *finalizer*) on the scope; closing the scope runs all registered finalizers with the `Exit` value that ended the work. `Scope` appears in an effect's requirements `R` like any other service:
26
+
27
+ ```ts
28
+ // Acquiring puts Scope into R…
29
+ const acquireRelease: <A, E, R, R2>(
30
+ acquire: Effect.Effect<A, E, R>,
31
+ release: (a: A, exit: Exit.Exit<unknown, unknown>) => Effect.Effect<unknown, never, R2>,
32
+ options?: { readonly interruptible?: boolean }
33
+ ) => Effect.Effect<A, E, R | R2 | Scope.Scope>;
34
+
35
+ // …and eliminating Scope decides the resource's lifetime
36
+ const scoped: <A, E, R>(
37
+ self: Effect.Effect<A, E, R>
38
+ ) => Effect.Effect<A, E, Exclude<R, Scope.Scope>>;
39
+ ```
40
+
41
+ `Scope.Scope` in `R` reads as: "this effect registers cleanup on whoever provides the scope." The provider decides how long the resource lives. The three providers, from most to least common:
42
+
43
+ 1. **`Effect.scoped(effect)`** — lifetime is exactly this effect. A fresh scope opens before it runs and closes (running finalizers) when it exits, whether by success, failure, or interruption.
44
+ 2. **`Layer.effect(Tag, effect)`** — lifetime is the layer. Finalizers run when the layer's scope closes, normally at application or `ManagedRuntime` shutdown. In v4 `Layer.effect` *is* the scoped constructor; `Layer.scoped` no longer exists.
45
+ 3. **`Scope.provide(effect, scope)` / `Scope.use(effect, scope)`** — lifetime is caller-managed. `provide` injects a scope without closing it; `use` injects a `Closeable` and closes it when the effect exits.
46
+
47
+ `Scope` must be fully eliminated before the run boundary: `Effect.runPromise`/`runFork` require `Effect<A, E, never>`.
48
+
49
+ The `Scope` object itself:
50
+
51
+ ```ts
52
+ interface Scope {
53
+ readonly strategy: 'sequential' | 'parallel';
54
+ state: State.Open | State.Closed | State.Empty;
55
+ }
56
+ interface Closeable extends Scope {} // can be passed to Scope.close
57
+ ```
58
+
59
+ `Scope.Scope` is also the `Context` tag for the current scope, so `yield* Scope.Scope` and `yield* Effect.scope` both return it.
60
+
61
+ Imports used throughout this skill:
62
+
63
+ ```ts
64
+ import { Effect, Exit, Layer, Scope, ScopedRef } from 'effect';
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 1. Acquiring Resources
70
+
71
+ ### Effect.acquireRelease
72
+
73
+ The workhorse. Acquire a value; the release finalizer is registered on the current scope and is guaranteed to run when that scope closes. The finalizer receives the `Exit` the scope was closed with.
74
+
75
+ ```ts
76
+ interface Conn {
77
+ readonly query: (sql: string) => Effect.Effect<string>;
78
+ readonly close: () => void;
79
+ }
80
+
81
+ declare const connect: Effect.Effect<Conn, ConnError>;
82
+
83
+ const connection = Effect.acquireRelease(
84
+ connect,
85
+ (conn, exit) =>
86
+ Effect.sync(() => {
87
+ console.log(Exit.isSuccess(exit) ? 'clean close' : 'close after failure');
88
+ conn.close();
89
+ })
90
+ );
91
+ // Effect<Conn, ConnError, Scope.Scope>
92
+
93
+ const program = Effect.scoped(
94
+ Effect.gen(function* () {
95
+ const conn = yield* connection;
96
+ return yield* conn.query('SELECT 1');
97
+ })
98
+ );
99
+ // Effect<string, ConnError> — Scope eliminated, conn closed on exit
100
+ ```
101
+
102
+ Semantics to rely on:
103
+
104
+ - **Acquisition is uninterruptible by default.** Pass `{ interruptible: true }` as the third argument to allow the acquire effect to be interrupted (there is no separate `acquireReleaseInterruptible` in v4).
105
+ - **`release` cannot fail at the type level** — its error channel is `never`. Catch and convert errors inside the release effect.
106
+ - **The release's services (`R2`) are captured from the context at acquisition time**, so release can use services even though the scope may close in a different context.
107
+ - If acquisition fails, no finalizer is registered.
108
+
109
+ ### Effect.acquireDisposable (v4-only)
110
+
111
+ For resources implementing the JS disposal protocols (`Symbol.dispose` / `Symbol.asyncDispose`) — no explicit release function needed:
112
+
113
+ ```ts
114
+ import sqlite from 'node:sqlite';
115
+
116
+ const db = Effect.acquireDisposable(
117
+ Effect.sync(() => new sqlite.DatabaseSync(':memory:'))
118
+ );
119
+ // Effect<DatabaseSync, never, Scope.Scope> — disposed when the scope closes
120
+ ```
121
+
122
+ ### Effect.acquireUseRelease
123
+
124
+ Bracketing without `Scope`: acquire, use, release in one effect. Use when the resource's lifetime is exactly one callback and you don't want `Scope` in `R` at all.
125
+
126
+ ```ts
127
+ const result = Effect.acquireUseRelease(
128
+ connect, // acquire (uninterruptible)
129
+ (conn) => conn.query('SELECT * FROM users'), // use (interruptible)
130
+ (conn, exit) => Effect.sync(() => conn.close()) // release (uninterruptible)
131
+ );
132
+ // Effect<string, ConnError> — no Scope requirement; a fallible release would add its error here (E3 in the signature)
133
+ ```
134
+
135
+ - `release` receives the `Exit` of the **use** step (`Exit<A, E2>`), not of the whole program.
136
+ - Unlike `acquireRelease`, the release here *can* fail (`E3`), and a release failure fails the whole effect even when `use` succeeded. If both `use` and `release` fail, their causes are combined rather than one replacing the other. Catch inside release if that is not desired.
137
+ - Release runs as soon as `use` finishes — choose `acquireRelease` + `Scope` when the resource must outlive a single callback.
138
+
139
+ ---
140
+
141
+ ## 2. Finalizers
142
+
143
+ ### Effect.addFinalizer — register cleanup on the current scope
144
+
145
+ When there is no acquired value, just cleanup to schedule. The callback receives the `Exit` used to close the scope:
146
+
147
+ ```ts
148
+ const program = Effect.scoped(
149
+ Effect.gen(function* () {
150
+ yield* Effect.addFinalizer((exit) =>
151
+ Effect.log(Exit.isSuccess(exit) ? 'completed' : 'failed or interrupted')
152
+ );
153
+ yield* Effect.log('working...');
154
+ })
155
+ );
156
+ ```
157
+
158
+ - Requires `Scope.Scope` in `R`; finalizer error channel is `never`.
159
+ - **The finalizer's services are captured at registration time** from the surrounding context.
160
+ - Registering on an already-closed scope runs the finalizer **immediately** with the stored exit (it is not silently dropped).
161
+
162
+ ### onExit / ensuring / onError / onInterrupt — attach cleanup to one effect
163
+
164
+ These do **not** use `Scope`; they guard a single effect and run as soon as it settles:
165
+
166
+ ```ts
167
+ // Run on every completion, observing the Exit
168
+ task.pipe(Effect.onExit((exit) => Effect.log(`done: ${exit._tag}`)));
169
+
170
+ // Run on every completion, ignoring the Exit (ensuring = onExit that ignores its input)
171
+ task.pipe(Effect.ensuring(Effect.log('always runs')));
172
+
173
+ // Run only on failure, observing the Cause
174
+ task.pipe(Effect.onError((cause) => Effect.log('failed')));
175
+
176
+ // Run only on interruption; receives the set of interrupting fiber ids
177
+ task.pipe(Effect.onInterrupt((interruptors) => Effect.log('interrupted')));
178
+ ```
179
+
180
+ Selective variants: `Effect.onExitIf(self, predicate, f)`, `Effect.onExitFilter(self, filter, f)`, `Effect.onErrorIf(self, predicate, f)`, `Effect.onErrorFilter(self, filter, f)` (predicate/filter argument comes before the finalizer; all are dual).
181
+
182
+ Semantics:
183
+
184
+ - Finalizers attached with `onExit`/`ensuring` run in an **uninterruptible region** (unless you reach for the low-level `Effect.onExitPrimitive(self, f, interruptible)`, which also allows returning `undefined` to skip finalization).
185
+ - If an `onExit` finalizer fails (`f` may have an error channel `XE`), its error joins the result error channel. If both the source and finalizer fail, their causes are combined rather than one replacing the other.
186
+ - `Effect.ensuring` deliberately requires `Effect<X, never, R1>` as its finalizer, so typed finalizer errors must be handled before attachment. A finalizer defect can still occur at runtime and is combined with an existing source failure.
187
+ - These only fire if the effect **starts** executing.
188
+
189
+ Choosing between them:
190
+
191
+ | Need | Use |
192
+ |---|---|
193
+ | Cleanup tied to a resource value | `Effect.acquireRelease` |
194
+ | Cleanup tied to the surrounding scope's lifetime | `Effect.addFinalizer` |
195
+ | Cleanup tied to one effect's completion | `Effect.ensuring` / `Effect.onExit` |
196
+ | Bracket acquire/use/release with no `Scope` in `R` | `Effect.acquireUseRelease` |
197
+
198
+ ---
199
+
200
+ ## 3. Eliminating Scope
201
+
202
+ ### Effect.scoped
203
+
204
+ Creates a fresh scope, runs the effect with it, closes the scope with the effect's `Exit`:
205
+
206
+ ```ts
207
+ const safe = Effect.scoped(
208
+ Effect.gen(function* () {
209
+ const conn = yield* connection; // Effect<..., Scope.Scope>
210
+ return yield* conn.query('SELECT 1');
211
+ })
212
+ );
213
+ // Scope removed from R; finalizers run when the gen block exits
214
+ ```
215
+
216
+ Implementation detail that matters: `Effect.scoped` adds the fresh scope to the fiber's context, **shadowing any outer scope** for everything inside — `Effect.scope`, `acquireRelease`, and `addFinalizer` inside the block all see the inner scope. The close runs in the effect's finalization (uninterruptible) region.
217
+
218
+ Place `Effect.scoped` as close to the use site as correctness allows: everything inside it holds the resource open.
219
+
220
+ ### Effect.scopedWith
221
+
222
+ Like `scoped`, but hands you the scope explicitly instead of putting it in context. Useful when you want to register finalizers on the managed scope manually, or pass it to scope-accepting APIs without affecting the ambient `Scope` service:
223
+
224
+ ```ts
225
+ const program = Effect.scopedWith((scope) =>
226
+ Effect.gen(function* () {
227
+ yield* Scope.addFinalizer(scope, Effect.log('closing'));
228
+ yield* doWork;
229
+ })
230
+ );
231
+ // Effect<A, E, R> — note: does NOT remove Scope from R, the scope is only the callback argument
232
+ ```
233
+
234
+ ### Scope.provide and Scope.use
235
+
236
+ When the caller owns a scope:
237
+
238
+ ```ts
239
+ const manual = Effect.gen(function* () {
240
+ const scope = yield* Scope.make(); // Effect<Scope.Closeable>
241
+
242
+ // provide: inject the scope, do NOT close it — caller closes later
243
+ const conn = yield* connection.pipe(Scope.provide(scope));
244
+ // ... use conn across multiple steps/effects ...
245
+ yield* Scope.close(scope, Exit.void);
246
+ });
247
+
248
+ // use: inject a Closeable AND close it when the effect exits (with the same Exit).
249
+ // Create the scope per execution — a scope baked into a shared const is closed
250
+ // after the first run, and a closed scope releases acquisitions immediately (mistake #8).
251
+ const once = Effect.suspend(() =>
252
+ connection.pipe(
253
+ Effect.flatMap((conn) => conn.query('SELECT 1')),
254
+ Scope.use(Scope.makeUnsafe())
255
+ )
256
+ );
257
+ ```
258
+
259
+ Both are dual (data-first and data-last). `Scope.provide` was called `Scope.extend` in v3.
260
+
261
+ ### Effect.scope
262
+
263
+ `Effect.scope: Effect<Scope, never, Scope>` returns the current scope from the context — it is exactly the `Scope.Scope` service access. Use it to capture an outer scope before entering a narrower one (see section 5).
264
+
265
+ ---
266
+
267
+ ## 4. Manual Scopes: make, close, finalizer ordering
268
+
269
+ ```ts
270
+ const lifecycle = Effect.gen(function* () {
271
+ const scope = yield* Scope.make(); // default strategy 'sequential'
272
+
273
+ yield* Scope.addFinalizer(scope, Effect.log('cleanup 1'));
274
+ yield* Scope.addFinalizer(scope, Effect.log('cleanup 2'));
275
+ yield* Scope.addFinalizerExit(scope, (exit) =>
276
+ Effect.log(`cleanup 3, closed with ${exit._tag}`)
277
+ );
278
+
279
+ yield* Scope.close(scope, Exit.void);
280
+ // Output: cleanup 3, cleanup 2, cleanup 1 (reverse registration order)
281
+ });
282
+ ```
283
+
284
+ Constructors and operations (all verified against `Scope.ts`):
285
+
286
+ | API | Signature | Notes |
287
+ |---|---|---|
288
+ | `Scope.make` | `(strategy?: 'sequential' \| 'parallel') => Effect<Closeable>` | default `'sequential'` |
289
+ | `Scope.makeUnsafe` | `(strategy?) => Closeable` | synchronous |
290
+ | `Scope.addFinalizer` | `(scope, finalizer: Effect<unknown>) => Effect<void>` | exit-blind |
291
+ | `Scope.addFinalizerExit` | `(scope, (exit) => Effect<unknown>) => Effect<void>` | exit-aware |
292
+ | `Scope.close` | `(scope, exit: Exit<A, E>) => Effect<void>` | idempotent |
293
+ | `Scope.closeUnsafe` | `(scope, exit) => Effect<void> \| undefined` | low-level; **you must run the returned effect** or finalizers are skipped |
294
+ | `Scope.fork` | `(scope, strategy?) => Effect<Closeable>` | child scope, see section 5 |
295
+ | `Scope.forkUnsafe` | `(scope, strategy?) => Closeable` | synchronous |
296
+ | `Scope.provide` / `Scope.use` | dual | see section 3 |
297
+
298
+ Close semantics (from `internal/effect.ts` `scopeCloseFinalizers`):
299
+
300
+ - Finalizers run in **reverse registration order** (LIFO).
301
+ - `'sequential'` (default): one at a time, each awaited. `'parallel'`: all started concurrently, then awaited together.
302
+ - **Every finalizer always runs** — a failing finalizer does not prevent the others. All failures are collected and combined into a single `Cause`; `Scope.close` then fails with that combined cause.
303
+ - When scoped work and scope finalization both fail, the work cause and combined finalizer cause are merged.
304
+ - Closing an already-closed scope is a no-op.
305
+ - The scope transitions to `Closed` *before* finalizers run, so finalizers registered from inside finalizers execute immediately.
306
+ - You can inspect `scope.state._tag` (`'Empty' | 'Open' | 'Closed'`) and `scope.strategy` directly.
307
+
308
+ Manual scopes are warranted when a resource's lifetime does not align with any effect's lexical extent — for example a connection cached between requests, a resource handed off to another fiber, or interop with non-Effect lifecycle callbacks (`Scope.makeUnsafe` + `Scope.closeUnsafe` from a `dispose()` method).
309
+
310
+ ---
311
+
312
+ ## 5. Extending and Splitting Lifetimes
313
+
314
+ ### Acquire into an outer scope
315
+
316
+ Inside `Effect.scoped`, the ambient scope is the inner one. To make a resource outlive the block, capture the outer scope first and `Scope.provide` it:
317
+
318
+ ```ts
319
+ const program = Effect.gen(function* () {
320
+ const outer = yield* Effect.scope; // capture before entering the inner scope
321
+
322
+ yield* Effect.scoped(
323
+ Effect.gen(function* () {
324
+ const shortLived = yield* connection; // closed when this block exits
325
+ const longLived = yield* connection.pipe(Scope.provide(outer)); // survives the block
326
+ // ...
327
+ })
328
+ );
329
+ });
330
+ ```
331
+
332
+ ### Child scopes with Scope.fork
333
+
334
+ `Scope.fork(parent)` creates a `Closeable` child registered with the parent:
335
+
336
+ - Closing the **parent** closes the child with the same `Exit`.
337
+ - Closing the **child** first detaches it from the parent (the parent no longer tracks it).
338
+ - Forking from an **already-closed** parent returns an already-closed child — finalizers added to it run immediately.
339
+
340
+ This is the splitting primitive: hand part of a lifetime to other code while keeping an upper bound.
341
+
342
+ ```ts
343
+ // Hand a resource's lifetime to a background fiber, bounded by the parent scope
344
+ const handOff = Effect.gen(function* () {
345
+ const parent = yield* Effect.scope;
346
+ const child = yield* Scope.fork(parent);
347
+ const resource = yield* connection.pipe(Scope.provide(child));
348
+ yield* processInBackground(resource).pipe(
349
+ Effect.onExit((exit) => Scope.close(child, exit)),
350
+ Effect.forkIn(child)
351
+ );
352
+ // Worker finishes first: onExit closes `child`, releasing the resource.
353
+ // Parent closes first: it closes `child`, releasing the resource AND
354
+ // interrupting the worker — forkIn ties the fiber to the child scope.
355
+ // (forkDetach would NOT be interrupted; it would keep running against
356
+ // a released resource.)
357
+ });
358
+ ```
359
+
360
+ Per-item scopes inside a long-running loop — do not let per-job resources pile up on the daemon's scope:
361
+
362
+ ```ts
363
+ const worker = Effect.gen(function* () {
364
+ while (true) {
365
+ const job = yield* nextJob;
366
+ // fresh lifetime per job; finalizers run when handleJob exits
367
+ yield* Effect.scoped(handleJob(job));
368
+ }
369
+ });
370
+ ```
371
+
372
+ Layers use a similar mechanism internally: each layer builds inside its own scope whose closure is tied to its consumers' scopes via a refcounted finalizer (see section 6).
373
+
374
+ ---
375
+
376
+ ## 6. Layers Own Scopes
377
+
378
+ `Layer.effect` builds the service inside the **layer's scope**: any `acquireRelease`/`addFinalizer` performed during construction is released when the layer is torn down (app shutdown, `ManagedRuntime.disposeEffect`, test end). The `Scope` requirement is eliminated by the constructor itself:
379
+
380
+ ```ts
381
+ import { Context } from 'effect';
382
+
383
+ class Db extends Context.Service<Db, {
384
+ readonly query: (sql: string) => Effect.Effect<string>;
385
+ }>()('app/Db') {
386
+ static readonly layer = Layer.effect(
387
+ Db,
388
+ Effect.gen(function* () {
389
+ const conn = yield* Effect.acquireRelease(
390
+ connect,
391
+ (conn) => Effect.sync(() => conn.close())
392
+ );
393
+ return Db.of({ query: (sql) => conn.query(sql) });
394
+ })
395
+ );
396
+ // Layer<Db, ConnError> — Scope consumed by the layer
397
+ }
398
+ ```
399
+
400
+ Key facts (verified in `Layer.ts`):
401
+
402
+ - **v3 `Layer.scoped` and `Layer.scopedDiscard` are gone.** `Layer.effect` / `Layer.effectDiscard` already do `Exclude<R, Scope.Scope>` — there is nothing extra to call.
403
+ - `Layer.effect` is callable both ways: `Layer.effect(Tag, effect)` or curried `Layer.effect(Tag)(effect)`.
404
+ - `Layer.effectDiscard(effect)` runs construction side effects in the layer scope, providing no service — the canonical home for background tasks (see section 7).
405
+ - **Memoized layers refcount their scope.** Each layer builds once per `MemoMap` in its own scope; every consumer's scope registers a finalizer that decrements an observer count, and the layer's finalizers run only when the **last** consumer's scope closes. In v4 the `MemoMap` is shared across `Effect.provide` calls.
406
+ - `Layer.build(layer)` returns `Effect<Context<ROut>, E, RIn | Scope.Scope>` — the resources bind to the ambient scope. `Layer.buildWithScope(layer, scope)` uses an explicit one. `Layer.launch(layer)` builds the layer and sleeps forever inside `Effect.scoped` — for apps that *are* a layer.
407
+
408
+ Layer composition, `Layer.fresh`, and `provide` semantics are covered by the `effect-layer-design` skill; `ManagedRuntime` scope handling by the `effect-managed-runtime` skill.
409
+
410
+ ---
411
+
412
+ ## 7. Fibers and Scopes
413
+
414
+ Fibers can be bound to a scope: the fiber is interrupted when the scope closes. Two operators (details and supervision strategy live in the `effect-fiber` skill):
415
+
416
+ ```ts
417
+ // Fork tied to the current scope (adds Scope to R)
418
+ const daemon = Effect.scoped(
419
+ Effect.gen(function* () {
420
+ const fiber = yield* heartbeat.pipe(Effect.forkScoped);
421
+ // or start immediately rather than deferred:
422
+ yield* heartbeat.pipe(Effect.forkScoped({ startImmediately: true }));
423
+ yield* mainWork;
424
+ }) // heartbeat interrupted when the scope closes
425
+ );
426
+
427
+ // Fork into an explicit scope
428
+ const inScope = Effect.gen(function* () {
429
+ const scope = yield* Scope.make();
430
+ const fiber = yield* Effect.forkIn(task, scope, { startImmediately: true });
431
+ // ...
432
+ yield* Scope.close(scope, Exit.void); // interrupts the fiber
433
+ });
434
+ ```
435
+
436
+ Options for all fork variants: `{ startImmediately?: boolean, uninterruptible?: boolean | 'inherit' }`. Remember the v4 renames: `Effect.fork` → `Effect.forkChild`, `Effect.forkDaemon` → `Effect.forkDetach`.
437
+
438
+ The standard background-task-in-a-layer pattern combines both worlds — the layer scope bounds the fiber:
439
+
440
+ ```ts
441
+ const Heartbeat = Layer.effectDiscard(
442
+ Effect.gen(function* () {
443
+ yield* Effect.gen(function* () {
444
+ while (true) {
445
+ yield* Effect.sleep('5 seconds');
446
+ yield* Effect.logInfo('heartbeat');
447
+ }
448
+ }).pipe(
449
+ Effect.onInterrupt(() => Effect.logInfo('heartbeat stopped: layer closed')),
450
+ Effect.forkScoped
451
+ );
452
+ })
453
+ );
454
+ ```
455
+
456
+ `FiberSet`/`FiberMap`/`FiberHandle` constructors also require `Scope.Scope` and interrupt their fibers on close — see the `effect-fiber` skill.
457
+
458
+ ---
459
+
460
+ ## 8. ScopedRef — a resource-backed mutable reference
461
+
462
+ `ScopedRef<A>` holds a current value together with the scope that owns it. Replacing the value acquires the replacement in a fresh scope and **releases the previous value's resources**. Ideal for rotating connections, refreshed clients, reloaded credentials.
463
+
464
+ ```ts
465
+ // Initial value WITHOUT resources: make takes a THUNK (LazyArg), not a value
466
+ const ref = yield* ScopedRef.make(() => initialClient);
467
+ // Effect<ScopedRef<Client>, never, Scope.Scope>
468
+
469
+ // Initial value WITH resources: fromAcquire tracks acquisition in the ref's scope
470
+ const ref2 = yield* ScopedRef.fromAcquire(
471
+ Effect.acquireRelease(connect, (conn) => Effect.sync(() => conn.close()))
472
+ );
473
+ // Effect<ScopedRef<Conn>, ConnError, Scope.Scope>
474
+
475
+ // Read
476
+ const conn = yield* ScopedRef.get(ref2); // Effect<Conn>
477
+ const now = ScopedRef.getUnsafe(ref2); // synchronous
478
+
479
+ // Replace: releases the old value's scope, acquires the new value
480
+ yield* ScopedRef.set(
481
+ ref2,
482
+ Effect.acquireRelease(connect, (conn) => Effect.sync(() => conn.close()))
483
+ );
484
+ // Effect<void, ConnError> — the acquire's Scope requirement is absorbed by the ref
485
+ ```
486
+
487
+ Semantics (verified in `ScopedRef.ts` and its tests):
488
+
489
+ - Constructing requires `Scope.Scope`: when the *outer* scope closes, the currently-held value's scope is closed too.
490
+ - `set` is **synchronized** (internal semaphore — one replacement at a time) and **uninterruptible**.
491
+ - `set` acquires the replacement first. If acquisition fails, its new scope is closed, the error propagates, and the current value remains alive and unchanged.
492
+ - After successful acquisition, `set` closes the old scope before installing the replacement. If the old finalizer defects, the replacement scope is also closed and the reference is not switched, preventing the newly acquired resource from leaking.
493
+ - `ScopedRef.set` is dual: `ref.pipe(ScopedRef.set(acquire))` also works.
494
+
495
+ For keyed collections of scoped resources, see `LayerMap` (`ai-docs/src/01_effect/04_resources/30_layer-map.ts`); for capacity-managed pools, see `Pool` (`packages/effect/src/Pool.ts` — `Pool.make` returns a scoped pool whose `Pool.get(pool)` is itself scoped per item). `ScopedCache` is covered by the `effect-cache` skill.
496
+
497
+ ---
498
+
499
+ ## 9. Scope-Aware Utilities
500
+
501
+ Small APIs that bind other lifetimes to the current scope:
502
+
503
+ ```ts
504
+ // AbortSignal aborted when the scope closes — for fetch()-style cancellation
505
+ const signal = yield* Effect.abortSignal; // Effect<AbortSignal, never, Scope.Scope>
506
+
507
+ // Tracing span ended when the scope closes
508
+ const span = yield* Effect.makeSpanScoped('operation'); // Effect<Span, never, Scope.Scope>
509
+ yield* task.pipe(Effect.withSpanScoped('task')); // wraps, span ends at scope close
510
+
511
+ // Log annotations that last until the scope closes, then restore
512
+ yield* Effect.annotateLogsScoped({ requestId: 'req-123' });
513
+ ```
514
+
515
+ Other modules lean on `Scope` the same way — recognize the signature `Effect<X, E, R | Scope>` as "lifetime managed by your scope":
516
+
517
+ - `FileSystem.open`, `makeTempDirectoryScoped`, `makeTempFileScoped` (see the `effect-filesystem` skill)
518
+ - `Stream.scoped` and `Stream.callback` resource handling (see the `effect-stream` skill)
519
+ - HTTP request and connection scopes (see the `effect-http-server` and `effect-http-client` skills)
520
+
521
+ In tests, `it.effect` from `@effect/vitest` wraps every test body in `Effect.scoped`, so `acquireRelease`/`addFinalizer` fixtures clean up per test with no extra wiring.
522
+
523
+ ---
524
+
525
+ ## Key Patterns
526
+
527
+ ### File handle, bounded lifetime
528
+
529
+ ```ts
530
+ import { Effect, FileSystem } from 'effect';
531
+ import { NodeFileSystem } from '@effect/platform-node';
532
+
533
+ const writeLog = (line: string) =>
534
+ Effect.scoped(
535
+ Effect.gen(function* () {
536
+ const fs = yield* FileSystem.FileSystem;
537
+ const file = yield* fs.open('/var/log/app.log', { flag: 'a' }); // Effect<File, PlatformError, Scope>
538
+ yield* file.writeAll(new TextEncoder().encode(line + '\n'));
539
+ })
540
+ ).pipe(Effect.provide(NodeFileSystem.layer));
541
+ // file closed on success, failure, or interruption
542
+ ```
543
+
544
+ ### Service layer owning ordered resources
545
+
546
+ Multiple acquisitions in one layer constructor release in LIFO order at layer teardown — dependents detach before what they depend on (the single-resource version is in section 6; layer architecture in the `effect-layer-design` skill):
547
+
548
+ ```ts
549
+ import { Context, Effect, Layer } from 'effect';
550
+
551
+ interface PgPool {
552
+ readonly query: (sql: string) => Effect.Effect<string>;
553
+ readonly end: () => Promise<void>;
554
+ readonly on: (event: 'error', cb: (err: unknown) => void) => void;
555
+ readonly off: (event: 'error', cb: (err: unknown) => void) => void;
556
+ }
557
+ declare const createPool: Effect.Effect<PgPool, ConnError>;
558
+
559
+ class Pg extends Context.Service<Pg, {
560
+ readonly query: (sql: string) => Effect.Effect<string>;
561
+ }>()('app/Pg') {
562
+ static readonly layer = Layer.effect(
563
+ Pg,
564
+ Effect.gen(function* () {
565
+ const pool = yield* Effect.acquireRelease(
566
+ createPool,
567
+ (pool) => Effect.promise(() => pool.end())
568
+ );
569
+ const onError = (err: unknown) => console.error('pg pool error', err);
570
+ yield* Effect.acquireRelease(
571
+ Effect.sync(() => pool.on('error', onError)),
572
+ () => Effect.sync(() => pool.off('error', onError))
573
+ );
574
+ return Pg.of({ query: (sql) => pool.query(sql) });
575
+ })
576
+ );
577
+ }
578
+ // Layer teardown (LIFO): listener detached first, THEN pool.end() —
579
+ // nothing ever touches the pool after it has been destroyed.
580
+ ```
581
+
582
+ ### Event-listener registration
583
+
584
+ Pair register/unregister with `acquireRelease`; the listener lives as long as the scope:
585
+
586
+ ```ts
587
+ const onResize = (handler: () => void) =>
588
+ Effect.acquireRelease(
589
+ Effect.sync(() => window.addEventListener('resize', handler)),
590
+ () => Effect.sync(() => window.removeEventListener('resize', handler))
591
+ );
592
+
593
+ // As a stream of events, use Stream.callback + acquireRelease inside
594
+ // (full treatment in the effect-stream skill)
595
+ ```
596
+
597
+ ### Test fixture with automatic teardown
598
+
599
+ ```ts
600
+ import { Effect } from 'effect';
601
+ import { expect, it } from '@effect/vitest';
602
+
603
+ const testDb = Effect.acquireRelease(
604
+ openTestDatabase, // create temp schema, seed data
605
+ (db, exit) => db.drop // always dropped, even on assertion failure
606
+ );
607
+
608
+ it.effect('queries seeded data', () =>
609
+ Effect.gen(function* () {
610
+ const db = yield* testDb; // it.effect provides a per-test scope
611
+ const rows = yield* db.query('SELECT count(*) FROM users');
612
+ expect(rows).toBe(3);
613
+ }));
614
+ ```
615
+
616
+ ### Rotating credentials with ScopedRef
617
+
618
+ ```ts
619
+ const makeClient = (token: string) =>
620
+ Effect.acquireRelease(
621
+ Effect.sync(() => createClient(token)),
622
+ (client) => Effect.sync(() => client.destroy())
623
+ );
624
+
625
+ const program = Effect.scoped(
626
+ Effect.gen(function* () {
627
+ const token = yield* fetchToken;
628
+ const clientRef = yield* ScopedRef.fromAcquire(makeClient(token));
629
+
630
+ // refresh loop: each set destroys the previous client
631
+ yield* Effect.gen(function* () {
632
+ while (true) {
633
+ yield* Effect.sleep('55 minutes');
634
+ const fresh = yield* fetchToken;
635
+ yield* ScopedRef.set(clientRef, makeClient(fresh));
636
+ }
637
+ }).pipe(Effect.forkScoped);
638
+
639
+ yield* serveRequests(clientRef); // readers: ScopedRef.get(clientRef)
640
+ })
641
+ );
642
+ ```
643
+
644
+ ### Caller-managed scope for non-Effect lifecycles
645
+
646
+ ```ts
647
+ // Interop: an OO container with start/stop hooks owning Effect resources
648
+ class Plugin {
649
+ private readonly scope = Scope.makeUnsafe();
650
+
651
+ start() {
652
+ return Effect.runPromise(
653
+ startResources.pipe(Scope.provide(this.scope)) // register, don't close
654
+ );
655
+ }
656
+
657
+ stop() {
658
+ return Effect.runPromise(Scope.close(this.scope, Exit.void));
659
+ }
660
+ }
661
+ ```
662
+
663
+ ---
664
+
665
+ ## Common Mistakes
666
+
667
+ 1. **`Scope.extend` does not exist in v4** — it was renamed `Scope.provide`. Same behavior (inject without closing), dual API: `Scope.provide(effect, scope)` or `effect.pipe(Scope.provide(scope))`.
668
+ 2. **`Layer.scoped` / `Layer.scopedDiscard` do not exist in v4** — `Layer.effect` / `Layer.effectDiscard` already eliminate `Scope` from the construction effect. Writing `Layer.scoped(Tag, ...)` is a v3 habit that no longer compiles.
669
+ 3. **Forgetting `Effect.scoped` and "fixing" the leftover `Scope` in `R` at the app root.** It compiles once some outer scope exists, but every resource then lives until *that* scope closes — a leak in long-running apps. Eliminate `Scope` at the narrowest correct boundary.
670
+ 4. **Expecting `Effect.scoped` to accept a scope argument.** It always creates a fresh scope. To supply your own, use `Scope.use` (closes it) or `Scope.provide` (doesn't).
671
+ 5. **Assuming `Scope.provide` closes the scope.** It only injects. Without a later `Scope.close`, finalizers never run. `Scope.use` is the inject-and-close variant.
672
+ 6. **Looking for `Effect.acquireReleaseInterruptible`** — gone. Acquisition is uninterruptible by default; opt out with `Effect.acquireRelease(acquire, release, { interruptible: true })`.
673
+ 7. **`ScopedRef.make` takes a thunk**: `ScopedRef.make(() => 0)`, not `ScopedRef.make(0)`. And use `fromAcquire` when the initial value acquires resources — `make` does not track acquisition.
674
+ 8. **Assuming finalizers added to a closed scope are ignored.** `Scope.addFinalizer*` (and `Effect.addFinalizer` via it) runs the finalizer immediately with the scope's stored exit. The same applies to `Scope.fork` on a closed parent: the child is born closed.
675
+ 9. **Relying on registration-order cleanup.** Finalizers run in *reverse* registration order (LIFO), sequentially by default. Parallel finalization is opt-in per scope: `Scope.make('parallel')` — there is no v3-style `Effect.parallelFinalizers` combinator in v4.
676
+ 10. **Assuming one failing finalizer aborts the rest.** All finalizers run on close; their failures are combined into a single `Cause` that `Scope.close` fails with.
677
+ 11. **Using `acquireUseRelease` and swallowing release errors unknowingly — or the opposite.** Its release *can* fail; a release failure fails the whole effect after successful `use`, and combines with the use cause after failed `use`. Conversely `acquireRelease`'s release is typed `never` — convert errors inside it.
678
+ 12. **Acquiring per-request resources in a `Layer.effect` constructor.** Layer finalizers run when the layer scope closes (shutdown), not per call. Acquire per-request resources inside the request handler under `Effect.scoped`, or fork a child scope per item (section 5).
679
+ 13. **v3 fork names**: `Effect.fork` → `Effect.forkChild`, `Effect.forkDaemon` → `Effect.forkDetach`. `forkScoped`/`forkIn` keep their names and now accept `{ startImmediately?, uninterruptible? }`.
680
+ 14. **Calling `Scope.closeUnsafe` and dropping the result.** It returns `Effect | undefined`; ignoring the returned effect skips every finalizer. Use `Scope.close` unless you are writing low-level machinery.
681
+ 15. **Expecting interruption to skip cleanup.** Finalizers receive `Exit.failCause` with an interrupt cause and still run (uninterruptibly). Use exit-aware finalizers (`addFinalizer`, `acquireRelease`'s `(a, exit) =>`) to branch on success/failure/interrupt — don't split cleanup across `onError` + success paths.
682
+ 16. **Splitting acquire and `Effect.addFinalizer` into separate yields.** Interruption between the two steps leaks the resource. `acquireRelease` wraps acquisition and finalizer registration in a single `uninterruptibleMask` precisely to close this window — use it whenever cleanup is tied to an acquired value.