opencode-effect-enforcer 0.2.8 → 0.3.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 (72) hide show
  1. package/README.md +3 -3
  2. package/docs/effect-4.0.0-changelog.md +3213 -0
  3. package/docs/effect-4.0.0.md +110 -0
  4. package/guidance/effect-first-development.md +8 -6
  5. package/guidance/progressive-disclosure-guidance.md +15 -7
  6. package/package.json +2 -2
  7. package/patterns/avoid-any.md +2 -2
  8. package/patterns/avoid-direct-json.md +6 -6
  9. package/patterns/avoid-native-fetch.md +8 -6
  10. package/patterns/avoid-node-imports.md +2 -2
  11. package/patterns/avoid-non-null-assertion.md +2 -2
  12. package/patterns/avoid-object-type.md +2 -2
  13. package/patterns/avoid-platform-coupling.md +1 -1
  14. package/patterns/avoid-process-env.md +3 -4
  15. package/patterns/avoid-ts-ignore.md +1 -1
  16. package/patterns/context-tag-extends.md +11 -8
  17. package/patterns/effect-promise-vs-trypromise.md +6 -7
  18. package/patterns/prefer-arr-sort.md +1 -1
  19. package/patterns/prefer-effect-fn.md +21 -65
  20. package/patterns/prefer-schema-class.md +3 -3
  21. package/patterns/throw-in-effect-gen.md +1 -1
  22. package/patterns/use-clock-service.md +4 -0
  23. package/patterns/use-command-executor-service.md +2 -2
  24. package/patterns/use-http-client-service.md +8 -6
  25. package/patterns/use-random-service.md +6 -7
  26. package/skills/effect-ai-chat/SKILL.md +13 -7
  27. package/skills/effect-ai-language-model/SKILL.md +50 -21
  28. package/skills/effect-ai-prompt/SKILL.md +25 -14
  29. package/skills/effect-ai-provider/SKILL.md +50 -22
  30. package/skills/effect-ai-streaming/SKILL.md +27 -12
  31. package/skills/effect-ai-tool/SKILL.md +37 -28
  32. package/skills/effect-atom-rpc/SKILL.md +57 -36
  33. package/skills/effect-atom-state/SKILL.md +57 -19
  34. package/skills/effect-batching/SKILL.md +5 -3
  35. package/skills/effect-cache/SKILL.md +19 -7
  36. package/skills/effect-cli/SKILL.md +17 -8
  37. package/skills/effect-command-executor/SKILL.md +115 -64
  38. package/skills/effect-concurrency-testing/SKILL.md +26 -6
  39. package/skills/effect-config/SKILL.md +53 -2
  40. package/skills/effect-context-witness/SKILL.md +6 -6
  41. package/skills/effect-domain-modeling/SKILL.md +8 -1
  42. package/skills/effect-error-handling/SKILL.md +15 -2
  43. package/skills/effect-fiber/SKILL.md +20 -25
  44. package/skills/effect-filesystem/SKILL.md +69 -57
  45. package/skills/effect-http-api/SKILL.md +72 -22
  46. package/skills/effect-http-client/SKILL.md +25 -21
  47. package/skills/effect-http-server/SKILL.md +51 -21
  48. package/skills/effect-incremental-migration/SKILL.md +17 -8
  49. package/skills/effect-layer-design/SKILL.md +8 -0
  50. package/skills/effect-managed-runtime/SKILL.md +6 -0
  51. package/skills/effect-mcp-server/SKILL.md +64 -24
  52. package/skills/effect-observability/SKILL.md +61 -15
  53. package/skills/effect-parallelization/SKILL.md +24 -7
  54. package/skills/effect-path/SKILL.md +8 -2
  55. package/skills/effect-platform-abstraction/SKILL.md +88 -66
  56. package/skills/effect-platform-layers/SKILL.md +68 -67
  57. package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
  58. package/skills/effect-react-composition/SKILL.md +19 -6
  59. package/skills/effect-rpc-api/SKILL.md +24 -24
  60. package/skills/effect-rpc-client/SKILL.md +33 -28
  61. package/skills/effect-rpc-cluster/SKILL.md +122 -78
  62. package/skills/effect-rpc-server/SKILL.md +56 -20
  63. package/skills/effect-scheduling/SKILL.md +29 -1
  64. package/skills/effect-schema-composition/SKILL.md +31 -13
  65. package/skills/effect-schema-v4/SKILL.md +94 -10
  66. package/skills/effect-scope/SKILL.md +13 -5
  67. package/skills/effect-service-implementation/SKILL.md +1 -1
  68. package/skills/effect-socket/SKILL.md +52 -8
  69. package/skills/effect-sql/SKILL.md +67 -33
  70. package/skills/effect-stream/SKILL.md +50 -5
  71. package/skills/effect-testing/SKILL.md +91 -2
  72. package/skills/effect-workflow/SKILL.md +76 -39
@@ -3,25 +3,25 @@ name: effect-rpc-server
3
3
  description: Serve RpcGroup contracts with Effect's RpcServer — handler layers (RpcGroup.toLayer/toLayerHandler), protocol layers (HTTP, WebSocket, TCP, stdio, worker), RpcSerialization, server middleware, streaming results, interruption and shutdown semantics, RpcTest. Use when implementing the server side of an Effect RPC API, mounting RPC on an HttpRouter or existing HTTP app, choosing a wire format, implementing RpcMiddleware, handling client aborts, or testing RPC handlers.
4
4
  ---
5
5
 
6
- You are an Effect TypeScript expert specializing in serving RPC groups with `RpcServer` from `effect/unstable/rpc`.
6
+ You are an Effect TypeScript expert specializing in serving RPC groups with `RpcServer` from `effect/rpc`.
7
7
 
8
- Everything ships from the `effect` package under `effect/unstable/rpc` — there is no `@effect/rpc` package in v4. This skill covers the **server side**: implementing handlers, transports, serialization, middleware, and lifecycle. For defining `Rpc`/`RpcGroup` contracts see the `effect-rpc-api` skill; for building clients see the `effect-rpc-client` skill; for cluster entities see the `effect-rpc-cluster` skill; for `HttpRouter`/`HttpServer` fundamentals see the `effect-http-server` skill.
8
+ Everything ships from the `effect` package under `effect/rpc` — there is no `@effect/rpc` package in v4. This skill covers the **server side**: implementing handlers, transports, serialization, middleware, and lifecycle. For defining `Rpc`/`RpcGroup` contracts see the `effect-rpc-api` skill; for building clients see the `effect-rpc-client` skill; for cluster entities see the `effect-rpc-cluster` skill; for `HttpRouter`/`HttpServer` fundamentals see the `effect-http-server` skill.
9
9
 
10
10
  ## Effect Source Reference
11
11
 
12
- The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — these modules change between betas.
12
+ The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read the `effect@4.0.0` tag for this skill; main may be newer. These APIs remain `@stability unstable` and may break in minor releases. Keep Effect-family packages on the same release.
13
13
 
14
14
  Key files:
15
15
 
16
- - `packages/effect/src/unstable/rpc/RpcServer.ts` — `make`, `makeNoSerialization`, `layer`, `layerHttp`, every `layerProtocol*` / `makeProtocol*`, `toHttpEffect*`, the `Protocol` service
17
- - `packages/effect/src/unstable/rpc/RpcGroup.ts` — `toLayer`, `toLayerHandler`, `toHandlers`, `accessHandler`, `of`, handler type derivation
18
- - `packages/effect/src/unstable/rpc/Rpc.ts` — `ServerClient`, `Handler`, `ToHandlerFn`, `ResultFrom`, `fork`, `uninterruptible`, `ServicesServer`
19
- - `packages/effect/src/unstable/rpc/RpcMiddleware.ts` — `Service` constructor, server middleware function shape, `layerClient`
20
- - `packages/effect/src/unstable/rpc/RpcSerialization.ts` — JSON, NDJSON, JSON-RPC, and SchemaBinary parsers and layers
21
- - `packages/effect/src/unstable/rpc/RpcMessage.ts` — the wire vocabulary (`Request`, `Ack`, `Interrupt`, `Eof`, `Chunk`, `Exit`, `Defect`, `ClientEnd`)
22
- - `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
23
- - `packages/effect/src/unstable/rpc/RpcTest.ts` — in-process test client
24
- - `packages/effect/src/unstable/rpc/RpcSchema.ts` — `ClientAbort` cause annotation, stream schema markers
16
+ - `packages/effect/src/rpc/RpcServer.ts` — `make`, `makeNoSerialization`, `layer`, `layerHttp`, every `layerProtocol*` / `makeProtocol*`, `toHttpEffect*`, the `Protocol` service
17
+ - `packages/effect/src/rpc/RpcGroup.ts` — `toLayer`, `toLayerHandler`, `toHandlers`, `accessHandler`, `of`, handler type derivation
18
+ - `packages/effect/src/rpc/Rpc.ts` — `ServerClient`, `Handler`, `ToHandlerFn`, `ResultFrom`, `fork`, `uninterruptible`, `ServicesServer`
19
+ - `packages/effect/src/rpc/RpcMiddleware.ts` — `Service` constructor, server middleware function shape, `layerClient`
20
+ - `packages/effect/src/rpc/RpcSerialization.ts` — JSON, NDJSON, JSON-RPC, and SchemaBinary parsers and layers
21
+ - `packages/effect/src/rpc/RpcMessage.ts` — the wire vocabulary (`Request`, `Ack`, `Interrupt`, `Eof`, `Chunk`, `Exit`, `Defect`, `ClientEnd`)
22
+ - `packages/effect/src/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
23
+ - `packages/effect/src/rpc/RpcTest.ts` — in-process test client
24
+ - `packages/effect/src/rpc/RpcSchema.ts` — `ClientAbort` cause annotation, stream schema markers
25
25
  - `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — the best end-to-end reference for real wiring across http/ws/tcp transports and every serialization
26
26
  - `packages/platform/browser/test/fixtures/rpc-worker.ts` — minimal worker-side server entrypoint
27
27
 
@@ -60,7 +60,7 @@ Imports used throughout:
60
60
 
61
61
  ```ts
62
62
  import { Cause, Context, Deferred, Effect, Layer, Queue, Schema, Stream } from 'effect';
63
- import { Headers, HttpRouter } from 'effect/unstable/http';
63
+ import { Headers, HttpRouter } from 'effect/http';
64
64
  import {
65
65
  Rpc,
66
66
  RpcGroup,
@@ -71,7 +71,7 @@ import {
71
71
  RpcServer,
72
72
  RpcTest,
73
73
  RpcWorker
74
- } from 'effect/unstable/rpc';
74
+ } from 'effect/rpc';
75
75
  ```
76
76
 
77
77
  Running example group (definition details belong to the `effect-rpc-api` skill):
@@ -255,6 +255,14 @@ const ServerLayer = RpcServer.layerHttp({
255
255
 
256
256
  The protocol layers register routes on `HttpRouter`; `HttpRouter.serve` provides the router and turns it into an HTTP app:
257
257
 
258
+ Keep the protocol/server composition inside the app passed to `serve` (or
259
+ `toWebHandler`/`toHttpEffect`). `serve` and `toHttpEffect` build a fresh router in a
260
+ forked layer memo map; `toWebHandler` builds separately by default but uses any
261
+ explicit `memoMap` as supplied. Do not pre-provide `HttpRouter.layer` to the RPC route layer;
262
+ that registers on a router the entrypoint does not serve. If sibling servers need
263
+ the same stateful handler dependencies, provide those services outside their
264
+ entrypoints; layers first built inside an app are private to that app.
265
+
258
266
  ```ts
259
267
  import { createServer } from 'node:http';
260
268
  import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
@@ -268,6 +276,27 @@ NodeRuntime.runMain(Layer.launch(Main));
268
276
 
269
277
  See the `effect-http-server` skill for `HttpRouter.serve` options (middleware, `disableLogger`, `disableListenLog`) and adding sibling routes.
270
278
 
279
+ The same route composition can be built without any platform dependency:
280
+
281
+ <!-- typecheck -->
282
+ ```ts
283
+ import { Effect, Layer } from 'effect';
284
+ import * as Schema from 'effect/Schema';
285
+ import { HttpRouter } from 'effect/http';
286
+ import { Rpc, RpcGroup, RpcSerialization, RpcServer } from 'effect/rpc';
287
+
288
+ const Pings = RpcGroup.make(Rpc.make('Ping', { success: Schema.String }));
289
+ const Handlers = Pings.toLayer({ Ping: () => Effect.succeed('pong') });
290
+ const Routes = RpcServer.layerHttp({
291
+ group: Pings,
292
+ path: '/rpc',
293
+ protocol: 'http'
294
+ }).pipe(Layer.provide([Handlers, RpcSerialization.layerNdjson]));
295
+
296
+ // Keep this construction scope alive while using the returned HTTP effect.
297
+ const makeHttpEffect = HttpRouter.toHttpEffect(Routes);
298
+ ```
299
+
271
300
  ---
272
301
 
273
302
  ## 3. Protocol Layers — Picking a Transport
@@ -386,6 +415,12 @@ Rules, verified against the protocol implementations and the e2e matrix:
386
415
 
387
416
  Client and server must use the **same** serialization.
388
417
 
418
+ NDJSON skips malformed lines so later frames still decode; JSON-RPC skips
419
+ non-object messages and checks notification method types before interpreting
420
+ internal messages. Continue validating procedure payloads through the contract.
421
+ Custom encoded-interrupt consumers must allow `fiberId: null` as well as
422
+ `undefined`, matching JSON-encoded `RpcMessage.ExitEncoded` values.
423
+
389
424
  ---
390
425
 
391
426
  ## 5. Embedding in an Existing HTTP App
@@ -512,7 +547,7 @@ StreamUsers: Effect.fnUntraced(function* (payload) {
512
547
  Semantics, verified against `RpcServer.makeNoSerialization`:
513
548
 
514
549
  - The server batches available values into `Chunk` messages (`Stream.runForEachArray` / `Queue.takeAll`), so one message can carry many elements.
515
- - **Backpressure** exists only on ack-supporting transports (everything except plain HTTP): after writing a chunk the server waits for the client's `Ack` before pulling more. Over plain HTTP the producer is never throttled.
550
+ - **RPC acknowledgment backpressure** exists on ack-supporting transports: after writing a chunk the server waits for the client's `Ack` before pulling more. Framed HTTP has no RPC acks but still backpressures producers through its bounded response queue (`streamBufferSize`, default 16).
516
551
  - **Ending**: a `Stream` ending ends the response; for the queue form call `Queue.end(queue)` — `Cause.Done` is the end-of-stream signal, and `Queue.end` only typechecks when the queue's error channel includes it, so create the queue as `Queue.bounded<User, Cause.Done>(16)`. The client then sees the stream complete with a final void `Exit`.
517
552
  - **Failures**: failing the stream with the rpc's declared error fails the client's stream with that typed error.
518
553
  - The client consumes the result as a `Stream` by default or a `Queue.Dequeue` with `{ asQueue: true }` — see the `effect-rpc-client` skill.
@@ -545,6 +580,7 @@ Never: () =>
545
580
 
546
581
  - **HTTP**: if the request's scope closes before responses finish (client went away), the protocol sends an `Interrupt` for every request id from that HTTP call.
547
582
  - **Sockets/WebSockets**: a closed connection is pushed to the protocol's `disconnects` queue; the server interrupts all of that client's in-flight fibers.
583
+ - **HTTP WebSocket upgrades**: Node/Bun scope cleanup sends `1000` on success, `1001` for interruption-only exits, or `1011` on failure, preserving an explicit close code already sent. The HTTP request scope retains the handler failure through error-response handling and observation middleware.
548
584
  - An `Interrupt` for an unknown/finished request id is answered with `Exit.interrupt()` — interruption is idempotent.
549
585
 
550
586
  ### `Rpc.uninterruptible`
@@ -572,7 +608,7 @@ Worker entrypoint (browser shown; for Node use `NodeWorkerRunner.layer` from `@e
572
608
  // worker.ts
573
609
  import { BrowserWorkerRunner } from '@effect/platform-browser';
574
610
  import { Effect, Layer } from 'effect';
575
- import { RpcServer } from 'effect/unstable/rpc';
611
+ import { RpcServer } from 'effect/rpc';
576
612
 
577
613
  const MainLive = RpcServer.layer(UserRpcs).pipe(
578
614
  Layer.provide(UsersLive),
@@ -609,7 +645,7 @@ Wires `RpcServer.makeNoSerialization` to a no-serialization client. Requests, st
609
645
 
610
646
  ```ts
611
647
  import { assert, it } from '@effect/vitest';
612
- import { RpcClient } from 'effect/unstable/rpc';
648
+ import { RpcClient } from 'effect/rpc';
613
649
 
614
650
  class UsersClient extends Context.Service<
615
651
  UsersClient,
@@ -684,8 +720,8 @@ The decoded core with no transport at all: you push `FromClient<Rpcs>` messages
684
720
  import { createServer } from 'node:http';
685
721
  import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
686
722
  import { Effect, Layer } from 'effect';
687
- import { HttpRouter } from 'effect/unstable/http';
688
- import { RpcSerialization, RpcServer } from 'effect/unstable/rpc';
723
+ import { HttpRouter } from 'effect/http';
724
+ import { RpcSerialization, RpcServer } from 'effect/rpc';
689
725
  import { SecureRpcs } from '../domain/rpc.ts';
690
726
 
691
727
  const UsersLive = SecureRpcs.toLayer(
@@ -773,7 +809,7 @@ const ServerLayer = RpcServer.layer(StoreRpcs, { concurrency: 1 }).pipe(
773
809
 
774
810
  ## Common Mistakes
775
811
 
776
- 1. **Importing from `@effect/rpc`.** v3 habit; the package does not exist in v4. Everything is `effect/unstable/rpc` (and platform layers come from `@effect/platform-node` / `-bun` / `-browser`).
812
+ 1. **Importing from `@effect/rpc`.** v3 habit; the package does not exist in v4. Everything is `effect/rpc` (and platform layers come from `@effect/platform-node` / `-bun` / `-browser`).
777
813
  2. **Forgetting `protocol: 'http'` on `layerHttp`.** The default is `'websocket'` — your `POST /rpc` curl returns 404 and only a `GET` upgrade route exists. Also note `layerHttp` takes `{ group, path, ... }` as one options bag, while `layer(group, options)` takes the group positionally.
778
814
  3. **Providing handlers/middleware but no `Protocol` or `RpcSerialization`.** `RpcServer.layer` requires all of: handler layer(s), middleware implementation layers, a `layerProtocol*`, and (for non-worker protocols) a `RpcSerialization.layer*`. Missing ones surface as unresolved layer requirements.
779
815
  4. **`layerJson` on a raw TCP socket server.** Sockets need `layerNdjson`, `layerNdJsonRpc()`, or `layerSchemaBinary()` for framing. WebSocket supplies its own framing and supports `layerJson`.
@@ -7,7 +7,11 @@ You are an Effect TypeScript expert specializing in `Schedule`, retry, repeat, p
7
7
 
8
8
  ## Source Of Truth
9
9
 
10
- Verify APIs against `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/src/Schedule.ts` and `Effect.ts`. In Effect v4, `Schedule.concat` is current, `Schedule.tapInput` is absent, and `Schedule.tap` receives full metadata.
10
+ Verify APIs against `packages/effect/src/Schedule.ts` and `Effect.ts` at the
11
+ installed release tag in `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
12
+ The `effect@4.0.0` tag has no `cookbooks/schedule.md`; its runnable schedule guide
13
+ is `ai-docs/src/06_schedule/10_schedules.ts`. In Effect v4, `Schedule.concat` is
14
+ current, `Schedule.tapInput` is absent, and `Schedule.tap` receives full metadata.
11
15
 
12
16
  ## Semantics
13
17
 
@@ -22,12 +26,14 @@ incorrect delay, retryAfter, and resetAfter values.
22
26
  - `Effect.repeat` reruns successes. A typed failure stops repetition unless the pass handles it first.
23
27
  - The source effect runs once before the schedule is stepped.
24
28
  - `Schedule.recurs(3)` permits three recurrences after the initial evaluation: at most four evaluations total.
29
+ - `Schedule.once` permits one immediate recurrence and outputs `void` on both recurrence and completion. `Effect.repeat(task, Schedule.once)` evaluates `task` twice, not once.
25
30
  - Schedules can require services and fail; schedule errors join the resulting effect's error channel.
26
31
  - Retry only the narrowest idempotent operation. Never retry non-idempotent writes unless an idempotency key, transaction, or equivalent guarantee makes replay safe.
27
32
 
28
33
  ## Policy Chooser
29
34
 
30
35
  - Counter only: `Schedule.recurs(n)`.
36
+ - One immediate recurrence, no counter output: `Schedule.once` (a value, not a function).
31
37
  - Delay after each completed run: `Schedule.spaced(duration)`.
32
38
  - Cadence aligned to time boundaries: `Schedule.fixed(interval)`; slow work may make the next run immediate, and missed ticks are not replayed.
33
39
  - Backoff: `Schedule.exponential(base)` or `Schedule.fibonacci(base)`.
@@ -75,6 +81,28 @@ const numericDates = mixed.pipe(
75
81
 
76
82
  ## Polling And Item Failure Policy
77
83
 
84
+ ### Bounded repetition and refinement
85
+
86
+ With predicate-only `Effect.repeat`, an `until` refinement narrows the final
87
+ result to the matched type; a `while` refinement excludes the continuing type.
88
+ Adding `times` or `schedule` preserves the **full source result type** because
89
+ the bound can stop repetition before the predicate does. This also applies when
90
+ `times` is optional in the options type. Check the returned value before using
91
+ fields that only exist on the desired terminal variant.
92
+
93
+ <!-- typecheck -->
94
+ ```ts
95
+ import { Effect, Schedule } from 'effect';
96
+ import * as Option from 'effect/Option';
97
+
98
+ declare const poll: Effect.Effect<Option.Option<string>>;
99
+
100
+ const bounded: Effect.Effect<Option.Option<string>> = poll.pipe(
101
+ Effect.repeat({ until: Option.isSome, times: 3 })
102
+ );
103
+ const twice = Effect.repeat(Effect.succeed('tick'), Schedule.once);
104
+ ```
105
+
78
106
  Use `Effect.repeat(pass, Schedule.spaced(...))` for a worker that emits no meaningful values. Use `Stream.fromEffectSchedule` when each result is part of a stream pipeline.
79
107
 
80
108
  Decide failures at the correct granularity:
@@ -164,13 +164,13 @@ import { Schema } from 'effect';
164
164
  Schema.String.check(Schema.isMaxLength(5));
165
165
  Schema.String.check(Schema.isMinLength(5));
166
166
  Schema.String.check(Schema.isNonEmpty()); // non-empty string
167
- Schema.String.check(Schema.isLengthBetween(2, 4));
167
+ Schema.String.check(Schema.isBetweenLength(2, 4));
168
168
 
169
169
  // Pattern matching
170
170
  Schema.String.check(Schema.isPattern(/^[a-z]+$/));
171
- Schema.String.check(Schema.isStartsWith('prefix'));
172
- Schema.String.check(Schema.isEndsWith('suffix'));
173
- Schema.String.check(Schema.isIncludes('substring'));
171
+ Schema.String.check(Schema.isStartingWith('prefix'));
172
+ Schema.String.check(Schema.isEndingWith('suffix'));
173
+ Schema.String.check(Schema.isIncluding('substring'));
174
174
 
175
175
  // Case and whitespace validation
176
176
  Schema.String.check(Schema.isTrimmed()); // No leading/trailing whitespace
@@ -185,6 +185,16 @@ Schema.String.check(Schema.isBase64());
185
185
  Schema.String.check(Schema.isBase64Url());
186
186
  ```
187
187
 
188
+ For string length, choose the unit deliberately: `isMinLength`, `isMaxLength`,
189
+ and `isBetweenLength` count UTF-16 code units; `isMinCodePoints`,
190
+ `isMaxCodePoints`, and `isBetweenCodePoints` count Unicode code points, not
191
+ grapheme clusters. The latter match JSON Schema length semantics and guide
192
+ native Arbitrary generation. Cardinality bounds must be finite.
193
+
194
+ JSON Schema export can approximate runtime checks. Use the Effect decoder as
195
+ the final authority; see `effect-schema-v4` for exact/approximate check exporters,
196
+ Unicode patterns, and `oneOf` fallback semantics.
197
+
188
198
  ### Number Filters
189
199
 
190
200
  ```typescript
@@ -220,7 +230,7 @@ import { Schema } from 'effect';
220
230
 
221
231
  Schema.Array(Schema.Number).check(Schema.isMinLength(2));
222
232
  Schema.Array(Schema.Number).check(Schema.isMaxLength(5));
223
- Schema.Array(Schema.Number).check(Schema.isLengthBetween(2, 5));
233
+ Schema.Array(Schema.Number).check(Schema.isBetweenLength(2, 5));
224
234
  ```
225
235
 
226
236
  ### Combining Multiple Filters
@@ -329,6 +339,7 @@ const MyForm = Schema.Struct({
329
339
 
330
340
  Use `SchemaGetter.checkEffect` for async validation inside a `Schema.decode` transformation:
331
341
 
342
+ <!-- typecheck -->
332
343
  ```typescript
333
344
  import {
334
345
  Effect,
@@ -345,14 +356,14 @@ async function validateUsername(username: string) {
345
356
 
346
357
  const ValidUsername = Schema.String.pipe(
347
358
  Schema.decode({
348
- decode: SchemaGetter.checkEffect((username) =>
359
+ decode: SchemaGetter.checkEffect((username, options) =>
349
360
  Effect.promise(() =>
350
361
  validateUsername(username).then((valid) =>
351
362
  valid
352
363
  ? undefined
353
- : new SchemaIssue.InvalidValue(Option.some(username), {
354
- title: 'Invalid username'
355
- })
364
+ : new SchemaIssue.InvalidValue(
365
+ { message: 'Invalid username' }, username, options
366
+ )
356
367
  )
357
368
  )
358
369
  ),
@@ -467,7 +478,7 @@ function split(separator: string) {
467
478
 
468
479
  ### Schema-derived binary boundaries
469
480
 
470
- Use `SchemaBinary.toCodec(schema)` from `effect/unstable/encoding` for a compact
481
+ Use `SchemaBinary.toCodec(schema)` from `effect/encoding` for a compact
471
482
  `Uint8Array` representation. It derives the wire layout from the schema's
472
483
  **encoded side**, preserving transformations, checks, and decoding/encoding
473
484
  services. Use public Schema encode/decode adapters; `toCodecDirect` and the
@@ -477,7 +488,7 @@ module's internal fast-path functions are not application APIs.
477
488
  ```ts
478
489
  import { Effect } from 'effect';
479
490
  import * as Schema from 'effect/Schema';
480
- import { SchemaBinary } from 'effect/unstable/encoding';
491
+ import { SchemaBinary } from 'effect/encoding';
481
492
 
482
493
  class Reading extends Schema.Class<Reading>('Reading')({
483
494
  id: Schema.String,
@@ -539,6 +550,7 @@ const BooleanFromString = Schema.Literals(['on', 'off']).pipe(
539
550
 
540
551
  Use `SchemaTransformation.transformEffect` when transformation might fail:
541
552
 
553
+ <!-- typecheck -->
542
554
  ```typescript
543
555
  import {
544
556
  Effect,
@@ -551,11 +563,11 @@ import {
551
563
 
552
564
  const NumberFromString = Schema.String.pipe(
553
565
  Schema.decodeTo(Schema.Number, {
554
- decode: SchemaGetter.transformEffect((s) =>
566
+ decode: SchemaGetter.transformEffect((s, options) =>
555
567
  Option.match(Number.parse(s), {
556
568
  onNone: () =>
557
569
  Effect.fail(
558
- new SchemaIssue.InvalidValue(Option.some(s))
570
+ new SchemaIssue.InvalidValue({ expected: 'a number' }, s, options)
559
571
  ),
560
572
  onSome: (n) => Effect.succeed(n)
561
573
  })
@@ -945,6 +957,12 @@ const AuthToken = Schema.TemplateLiteralParser(authTemplate.parts);
945
957
 
946
958
  ### Branded Types
947
959
 
960
+ Brands are type-only: `Schema.brand` does not add checks or change the runtime
961
+ AST. Pass a single concrete string literal and apply it repeatedly to compose
962
+ brands. `Schema.fromBrand` additionally applies a constructor's checks, and its
963
+ identifier must be that constructor's sole brand key. Representation round trips
964
+ preserve checks, but not nominal brands; reapply brands after rebuilding.
965
+
948
966
  ```typescript
949
967
  import { Schema } from 'effect';
950
968
 
@@ -35,7 +35,7 @@ schema helpers so concrete schema operations are retained.
35
35
  | `typeSchema(schema)` | `toType(schema)` | |
36
36
  | `asSchema(schema)` | `revealCodec(schema)` | |
37
37
  | `equivalence()` | `toEquivalence()` | |
38
- | `arbitrary()` | `Arbitrary.schema(schema)` | Native `effect/unstable/arbitrary`; fast-check bridge removed |
38
+ | `arbitrary()` | `Arbitrary.schema(schema)` | Native `effect/Arbitrary`; fast-check bridge removed |
39
39
  | `pretty()` | `toFormatter()` | |
40
40
  | `parseJson()` | `fromJsonString(Schema.Unknown)` | Public unknown-JSON codec |
41
41
  | `parseJson(schema)` | `fromJsonString(schema)` | With-schema version |
@@ -145,10 +145,35 @@ Schema.Number.check(Schema.isGreaterThan(0));
145
145
  | `finite` | `isFinite()` |
146
146
  | `minLength(n)` | `isMinLength(n)` |
147
147
  | `maxLength(n)` | `isMaxLength(n)` |
148
- | `length(n)` | `isLengthBetween(n, n)` |
148
+ | `length(n)` | `isBetweenLength(n, n)` |
149
149
  | `pattern(regex)` | `isPattern(regex)` |
150
150
  | `nonEmptyString` | `isNonEmpty()` |
151
151
 
152
+ Use `isBetweenLength`, `isBetweenCodePoints`, `isBetweenSize`, and
153
+ `isBetweenProperties` for range checks, and `isStartingWith`, `isEndingWith`,
154
+ and `isIncluding` for string checks. Their `SchemaRepresentation.*Reviver`
155
+ exports and persisted `effect/schema/...` check IDs use these same names;
156
+ update persisted representations as well as source calls.
157
+
158
+ ### String length semantics
159
+
160
+ `isMinLength`, `isMaxLength`, and `isBetweenLength` count UTF-16 code units on
161
+ strings and elements on arrays. Use `isMinCodePoints`, `isMaxCodePoints`, and
162
+ `isBetweenCodePoints` for Unicode code point counts, matching JSON Schema length
163
+ keywords. Code points are not grapheme clusters; these checks do not normalize
164
+ strings, and unpaired surrogates each count as one code point. Cardinality bounds
165
+ must be finite; they are rounded down and clamped to zero.
166
+
167
+ <!-- typecheck -->
168
+ ```ts
169
+ import * as Schema from 'effect/Schema';
170
+
171
+ const OneCodePoint = Schema.String.check(Schema.isBetweenCodePoints(1, 1));
172
+ const OneCodeUnit = Schema.String.check(Schema.isBetweenLength(1, 1));
173
+ Schema.is(OneCodePoint)('😀'); // true
174
+ Schema.is(OneCodeUnit)('😀'); // false
175
+ ```
176
+
152
177
  ### Removed Filters (no v4 equivalent)
153
178
 
154
179
  `positive`, `negative`, `nonNegative`, `nonPositive` — build these yourself:
@@ -252,11 +277,11 @@ import {
252
277
 
253
278
  const NumberFromString = Schema.String.pipe(
254
279
  Schema.decodeTo(Schema.Number, {
255
- decode: SchemaGetter.transformEffect((s) =>
280
+ decode: SchemaGetter.transformEffect((s, options) =>
256
281
  Option.match(Number.parse(s), {
257
282
  onNone: () =>
258
283
  Effect.fail(
259
- new SchemaIssue.InvalidValue(Option.some(s))
284
+ new SchemaIssue.InvalidValue({ expected: 'a number' }, s, options)
260
285
  ),
261
286
  onSome: (n) => Effect.succeed(n)
262
287
  })
@@ -278,6 +303,35 @@ Schema.Literal(0).transform('a');
278
303
  Schema.Literals([0, 1]).transform(['a', 'b']);
279
304
  ```
280
305
 
306
+ ### Brands are type-only
307
+
308
+ `Schema.brand('UserId')` adds a nominal TypeScript distinction, not a runtime
309
+ check or AST annotation. Apply checks before branding. The identifier must be
310
+ one concrete string literal: widened strings, unions, and open template literal
311
+ types are rejected. Compose distinct brands by applying `brand` repeatedly.
312
+ For an enum key, pass the enum member rather than its underlying string value.
313
+
314
+ `Schema.fromBrand(identifier, constructor)` requires the constructor's sole
315
+ concrete brand key and applies its checks. Compose distinct constructors through
316
+ repeated `fromBrand` calls; use `Schema.Union` for alternatives.
317
+
318
+ Representations and generated schema code do not retain type-only brands.
319
+ Reapply branding after rebuilding a schema when the nominal type is required;
320
+ checks supplied by `fromBrand` remain represented. Use an explicit `identifier`
321
+ annotation for reference naming instead of relying on the brand name.
322
+
323
+ <!-- typecheck -->
324
+ ```ts
325
+ import * as Schema from 'effect/Schema';
326
+
327
+ const UserId = Schema.NonEmptyString.pipe(Schema.brand('UserId'));
328
+ const TenantUserId = UserId.pipe(Schema.brand('TenantScoped'));
329
+ type TenantUserId = typeof TenantUserId.Type;
330
+
331
+ const id: TenantUserId = Schema.decodeUnknownSync(TenantUserId)('user-1');
332
+ // Both brands are static distinctions; runtime validation is NonEmptyString.
333
+ ```
334
+
281
335
  ## 5. Schema.Data Removal
282
336
 
283
337
  `Schema.Data` is **removed** in v4. No replacement needed — `Equal.equals` now does deep structural comparison on plain objects by default.
@@ -387,7 +441,7 @@ const fallback = SchemaGetter.withDefault(Effect.succeed('viewer'));
387
441
  - `Schema.resolveAnnotationsKey(schema)` returns key-level annotations.
388
442
  - `Schema.annotateEncoded({...})` annotates the encoded side of a transformed schema; use `Schema.annotate({...})` for the decoded Type side.
389
443
  - Schemas are directly extendable as classes.
390
- - Derive native generators with `Arbitrary.schema(schema)` from `effect/unstable/arbitrary`. See `effect-testing` for sampling, bounded generation, shrinking, and replay.
444
+ - Derive native generators with `Arbitrary.schema(schema)` from `effect/Arbitrary`. See `effect-testing` for sampling, bounded generation, shrinking, and replay.
391
445
  - New built-in schemas:
392
446
  - `Schema.DateFromString`
393
447
  - `Schema.BigIntFromString`
@@ -404,7 +458,7 @@ const fallback = SchemaGetter.withDefault(Effect.succeed('viewer'));
404
458
 
405
459
  ```ts
406
460
  import { Effect, Schema } from 'effect';
407
- import { Arbitrary } from 'effect/unstable/arbitrary';
461
+ import * as Arbitrary from 'effect/Arbitrary';
408
462
 
409
463
  class UserName extends Schema.NonEmptyString {
410
464
  static readonly decodeUnknownSync = Schema.decodeUnknownSync(this);
@@ -509,13 +563,40 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
509
563
 
510
564
  ### JSON Schema import, conversion, and Standard Schema
511
565
 
566
+ - `Schema.toJsonSchemaDocument(schema)` describes the encoded side of
567
+ `Schema.toCodecJson(schema)`. Decode matching JSON through that codec: for
568
+ example, its optional string field accepts JSON `null` as `undefined`, while
569
+ the original `Schema.optional(Schema.String)` rejects `null`.
570
+ - Export is best-effort, and successful JSON Schema validation is not proof that
571
+ Effect decoding will succeed. Known approximate branches cause `oneOf` to
572
+ export as `anyOf`; unions with only exact branches retain `oneOf`. Approximation propagates
573
+ through nested schemas, check dependencies, and recursive references.
574
+ - Custom check `toJsonSchema` callbacks return a fragment for exact semantics,
575
+ `[fragment, true]` for a safe, looser approximation, or `[{}, true]` to omit a
576
+ constraint. The compiler trusts that declaration. Approximate record-key
577
+ patterns cannot select `patternProperties` values; with excess-property mode
578
+ `"error"`, generated `propertyNames` and permissive candidate value schemas
579
+ still leave exact key/value associations to the Effect decoder.
580
+ - String code-unit minima export as `Math.ceil(minimum / 2)` code points, and
581
+ maxima use the same numeric bound but count code points. Code-point checks
582
+ export exact string bounds. Direct `isPattern` exports omit patterns unless
583
+ the regex has `u` and only optional `d`, `g`, or `y` flags; sticky patterns are
584
+ anchored at the start. Casing, safe integers, size, uniqueness, and non-number
585
+ ranges may also have looser or omitted exported constraints.
512
586
  - `SchemaRepresentation.fromJsonSchemaDocument` rejects unsupported references,
513
587
  validation keywords, object/array `const` or `enum` values, and intersections
514
588
  it cannot represent faithfully. Do not discard the failing constraint to make
515
589
  an import succeed.
516
590
  - A keyword such as `minLength` does not imply `type: 'string'`. Constraints beside
517
- `const`, `enum`, and `$ref` are applied. Imported `oneOf` remains `oneOf` on export,
518
- and tuple imports preserve `minItems` even when `prefixItems` alone is insufficient.
591
+ `const`, `enum`, and `$ref` are applied. Imports preserve `oneOf` mode; re-export
592
+ follows the approximation rules above. Tuple imports preserve `minItems` even
593
+ when `prefixItems` alone is insufficient.
594
+ - Imports assume a valid Draft 2020-12 document, without meta-schema validation,
595
+ and JSON-compatible instance values. Imported string lengths count code points;
596
+ `patterns: "apply"` compiles in ECMAScript Unicode (`u`) mode and rejects patterns
597
+ invalid in that mode with their source path. Patterns default to rejection;
598
+ ignoring them can also reject valid inputs by creating overlapping `oneOf`
599
+ branches. Import/re-export is not a lossless equivalence guarantee.
519
600
  - `JsonSchema` dialect conversion preserves custom keywords and representable
520
601
  conditionals, contains, dependencies, identifiers, and tuples; it relocates
521
602
  local references and throws for unsupported conversions. These synchronous
@@ -523,7 +604,7 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
523
604
  - Import vendored V1 interoperability types from `effect/StandardSchema`, for
524
605
  example `StandardSchemaV1` and `StandardJSONSchemaV1`. Continue to adapt Effect
525
606
  schemas with `Schema.toStandardSchemaV1`; the new module is not a schema builder.
526
- - Binary encoding is available from `effect/unstable/encoding` as `SchemaBinary`.
607
+ - Binary encoding is available from `effect/encoding` as `SchemaBinary`.
527
608
  See `effect-schema-composition` for codecs and `effect-stream` for framing.
528
609
 
529
610
  ## Parsing and compilation contracts
@@ -540,6 +621,9 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
540
621
  - Declared fields may be inherited and are copied to own output properties;
541
622
  dynamic record keys remain own-only and `__proto__` remains own-only. Enforce
542
623
  ownership at the boundary when the protocol requires own declared fields.
624
+ Excess-property checks ignore non-enumerable own properties; dynamic string
625
+ and symbol index signatures skip them too. Declare a field explicitly or make
626
+ it enumerable when it must survive decoding or encoding.
543
627
  - Class `make`, `makeOption`, and `makeEffect` preserve existing instances. Use
544
628
  `new MyClass(fields)` for a distinct instance. Class equivalence now compares
545
629
  declared fields, excluding unrelated runtime properties.
@@ -562,7 +646,7 @@ Neither matcher decodes unknown input. See `effect-pattern-matching` for a check
562
646
  `$id` are rejected; flatten references or explicitly choose to ignore constraints.
563
647
 
564
648
  Experimental JIT/AOT compilation uses the existing `SchemaParser` APIs. Opt in
565
- globally with `effect/unstable/schema/SchemaJITCompiler/enable` or selectively with
649
+ globally with `effect/schema/SchemaJITCompiler/enable` or selectively with
566
650
  `SchemaJITCompiler.enable(ast)`. AOT's `SchemaAOTCompiler/Build` discovers direct
567
651
  schema exports from explicit loaders and writes a self-installing module via
568
652
  FileSystem/Path. Choose prepared operations explicitly: omitted operations use
@@ -51,6 +51,7 @@ The `Scope` object itself:
51
51
  ```ts
52
52
  interface Scope {
53
53
  readonly strategy: 'sequential' | 'parallel';
54
+ readonly parent: Scope | undefined;
54
55
  state: State.Open | State.Closed | State.Empty;
55
56
  }
56
57
  interface Closeable extends Scope {} // can be passed to Scope.close
@@ -58,6 +59,11 @@ interface Closeable extends Scope {} // can be passed to Scope.close
58
59
 
59
60
  `Scope.Scope` is also the `Context` tag for the current scope, so `yield* Scope.Scope` and `yield* Effect.scope` both return it.
60
61
 
62
+ `Scope.close` and `Scope.closeUnsafe` require **`Scope.Closeable`**, obtained from
63
+ `Scope.make` / `Scope.fork` (or their unsafe constructors). An ambient
64
+ `Scope.Scope` grants finalizer registration, not authority to close its owner.
65
+ Keep owned scope fields typed `Scope.Closeable`; do not cast a borrowed scope.
66
+
61
67
  Imports used throughout this skill:
62
68
 
63
69
  ```ts
@@ -181,7 +187,7 @@ Selective variants: `Effect.onExitIf(self, predicate, f)`, `Effect.onExitFilter(
181
187
 
182
188
  Semantics:
183
189
 
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).
190
+ - Finalizers attached with `onExit`/`ensuring` run in an **uninterruptible region** by default. Use public combinators rather than internal runtime primitives.
185
191
  - 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
192
  - `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
193
  - These only fire if the effect **starts** executing.
@@ -289,8 +295,8 @@ Constructors and operations (all verified against `Scope.ts`):
289
295
  | `Scope.makeUnsafe` | `(strategy?) => Closeable` | synchronous |
290
296
  | `Scope.addFinalizer` | `(scope, finalizer: Effect<unknown>) => Effect<void>` | exit-blind |
291
297
  | `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 |
298
+ | `Scope.close` | `(scope: Closeable, exit: Exit<A, E>) => Effect<void>` | idempotent; finalizes uninterruptibly by default |
299
+ | `Scope.closeUnsafe` | `(scope: Closeable, exit) => Effect<void> \| undefined` | low-level; **run the returned effect uninterruptibly** or cleanup can be skipped |
294
300
  | `Scope.fork` | `(scope, strategy?) => Effect<Closeable>` | child scope, see section 5 |
295
301
  | `Scope.forkUnsafe` | `(scope, strategy?) => Closeable` | synchronous |
296
302
  | `Scope.provide` / `Scope.use` | dual | see section 3 |
@@ -299,6 +305,7 @@ Close semantics (from `internal/effect.ts` `scopeCloseFinalizers`):
299
305
 
300
306
  - Finalizers run in **reverse registration order** (LIFO).
301
307
  - `'sequential'` (default): one at a time, each awaited. `'parallel'`: all started concurrently, then awaited together.
308
+ - `Scope.close` runs finalizers uninterruptibly by default: interruption of the closing fiber waits for cleanup. A finalizer can explicitly restore interruptibility; avoid doing so unless abandoning that cleanup is intentional.
302
309
  - **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
310
  - When scoped work and scope finalization both fail, the work cause and combined finalizer cause are merged.
304
311
  - Closing an already-closed scope is a no-op.
@@ -339,7 +346,7 @@ const program = Effect.gen(function* () {
339
346
  `Scope.fork(parent)` creates a `Closeable` child registered with the parent:
340
347
 
341
348
  - Closing the **parent** closes the child with the same `Exit`.
342
- - Closing the **child** first detaches it from the parent (the parent no longer tracks it).
349
+ - Closing the **child** first detaches it from the parent before cleanup runs, even if cleanup throws or is interrupted. The readonly `child.parent` still identifies its parent; it is not a liveness indicator.
343
350
  - Forking from an **already-closed** parent returns an already-closed child — finalizers added to it run immediately.
344
351
 
345
352
  This is the splitting primitive: hand part of a lifetime to other code while keeping an upper bound.
@@ -492,6 +499,7 @@ yield* ScopedRef.set(
492
499
  Semantics (verified in `ScopedRef.ts` and its tests):
493
500
 
494
501
  - Constructing requires `Scope.Scope`: when the *outer* scope closes, the currently-held value's scope is closed too.
502
+ - The owner also closes in-flight replacement scopes. `make`, `fromAcquire`, and `set` interrupt if their owning scope has closed; they cannot return or install a resource from a closed generation. Replacements retain the reference's original position in the owner's finalizer order.
495
503
  - `set` is **synchronized** (internal semaphore — one replacement at a time) and **uninterruptible**.
496
504
  - `set` acquires the replacement first. If acquisition fails, its new scope is closed, the error propagates, and the current value remains alive and unchanged.
497
505
  - 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.
@@ -707,6 +715,6 @@ class Plugin {
707
715
  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.
708
716
  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).
709
717
  13. **v3 fork names**: `Effect.fork` → `Effect.forkChild`, `Effect.forkDaemon` → `Effect.forkDetach`. `forkScoped`/`forkIn` keep their names and now accept `{ startImmediately?, uninterruptible? }`.
710
- 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.
718
+ 14. **Calling `Scope.closeUnsafe` and dropping or interrupting the result.** It requires `Closeable` and returns `Effect | undefined`; run a returned effect uninterruptibly. The scope is already marked closed, so retrying close cannot recover skipped finalizers. Prefer `Scope.close`.
711
719
  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.
712
720
  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.
@@ -291,7 +291,7 @@ Promote it into its own service when any of these are true:
291
291
 
292
292
  ```typescript
293
293
  // git.ts
294
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
294
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
295
295
  import { Context, Effect, Layer } from 'effect';
296
296
 
297
297
  export interface Interface {