opencode-effect-enforcer 0.2.2 → 0.2.4

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 (47) hide show
  1. package/README.md +38 -10
  2. package/docs/effect-4.0.0-rc.112.md +316 -0
  3. package/guidance/effect-first-development.md +30 -17
  4. package/guidance/progressive-disclosure-guidance.md +13 -0
  5. package/package.json +3 -2
  6. package/patterns/avoid-direct-tag-checks.md +8 -2
  7. package/patterns/avoid-react-hooks.md +18 -37
  8. package/patterns/effect-run-in-body.md +1 -1
  9. package/patterns/require-effect-concurrency.md +11 -0
  10. package/patterns/use-console-service.md +6 -1
  11. package/skills/effect-ai-language-model/SKILL.md +10 -16
  12. package/skills/effect-ai-prompt/SKILL.md +36 -2
  13. package/skills/effect-ai-provider/SKILL.md +13 -0
  14. package/skills/effect-ai-streaming/SKILL.md +81 -108
  15. package/skills/effect-ai-tool/SKILL.md +50 -87
  16. package/skills/effect-atom-rpc/SKILL.md +9 -2
  17. package/skills/effect-atom-state/SKILL.md +5 -0
  18. package/skills/effect-cache/SKILL.md +32 -0
  19. package/skills/effect-cli/SKILL.md +22 -3
  20. package/skills/effect-concurrency-testing/SKILL.md +7 -9
  21. package/skills/effect-domain-modeling/SKILL.md +208 -1169
  22. package/skills/effect-domain-predicates/SKILL.md +5 -6
  23. package/skills/effect-error-handling/SKILL.md +5 -4
  24. package/skills/effect-http-api/SKILL.md +12 -1
  25. package/skills/effect-http-client/SKILL.md +1 -1
  26. package/skills/effect-http-server/SKILL.md +14 -3
  27. package/skills/effect-layer-design/SKILL.md +22 -56
  28. package/skills/effect-mcp-server/SKILL.md +1 -1
  29. package/skills/effect-pattern-matching/SKILL.md +44 -11
  30. package/skills/effect-platform-abstraction/SKILL.md +1 -1
  31. package/skills/effect-platform-layers/SKILL.md +1 -1
  32. package/skills/effect-rpc-api/SKILL.md +8 -1
  33. package/skills/effect-rpc-client/SKILL.md +20 -6
  34. package/skills/effect-rpc-cluster/SKILL.md +44 -14
  35. package/skills/effect-rpc-server/SKILL.md +32 -5
  36. package/skills/effect-scheduling/SKILL.md +1 -1
  37. package/skills/effect-schema-composition/SKILL.md +69 -15
  38. package/skills/effect-schema-v4/SKILL.md +43 -1
  39. package/skills/effect-scope/SKILL.md +30 -0
  40. package/skills/effect-service-implementation/SKILL.md +10 -4
  41. package/skills/effect-socket/SKILL.md +5 -5
  42. package/skills/effect-sql/SKILL.md +22 -0
  43. package/skills/effect-stream/SKILL.md +32 -1
  44. package/skills/effect-testing/SKILL.md +39 -31
  45. package/skills/effect-workflow/SKILL.md +6 -0
  46. package/patterns/vm-in-wrong-file.md +0 -51
  47. package/skills/effect-react-vm/SKILL.md +0 -675
@@ -22,8 +22,8 @@ Key files:
22
22
  - `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
23
23
  - `packages/effect/src/unstable/rpc/RpcTest.ts` — in-process test client
24
24
  - `packages/effect/src/unstable/rpc/RpcSchema.ts` — `ClientAbort` cause annotation, stream schema markers
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
- - `packages/platform-browser/test/fixtures/rpc-worker.ts` — minimal worker-side server entrypoint
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
+ - `packages/platform/browser/test/fixtures/rpc-worker.ts` — minimal worker-side server entrypoint
27
27
 
28
28
  ## Core Model
29
29
 
@@ -349,7 +349,33 @@ Provide exactly one `RpcSerialization` layer. The load-bearing property is `incl
349
349
  | `RpcSerialization.layerNdjson` | `application/ndjson` | yes | newline-delimited JSON |
350
350
  | `RpcSerialization.layerJsonRpc({ contentType? })` | `application/json` | no | JSON-RPC 2.0 interop |
351
351
  | `RpcSerialization.layerNdJsonRpc({ contentType? })` | `application/json-rpc` | yes | JSON-RPC 2.0, newline-framed |
352
- | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes | binary, smallest; msgpackr `useRecords: true` |
352
+ | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes | msgpackr `useRecords: true`; schema payloads still use JSON codecs |
353
+ | `RpcSerialization.layerSchemaBinary(options?)` | `application/vnd.effect.rpc+schema-binary` | yes | schema-derived binary payloads and envelopes |
354
+
355
+ ### Schema-aware serialization (rc.112)
356
+
357
+ `RpcSerialization.RpcSerialization`, `RpcClient.Protocol`, and
358
+ `RpcServer.Protocol` now require `codecFor: RpcSerialization.CodecFor`:
359
+
360
+ ```ts
361
+ type CodecFor = <S extends Schema.Top>(schema: S) =>
362
+ Schema.Codec<S['Type'], unknown, S['DecodingServices'], S['EncodingServices']>;
363
+ ```
364
+
365
+ Forward the selected serialization's `codecFor` when implementing a protocol.
366
+ This selects codecs for payloads, successes, errors, defects, and stream elements;
367
+ envelope framing remains the serialization's responsibility. Existing JSON,
368
+ NDJSON, JSON-RPC, and MsgPack wire formats keep their JSON-compatible schema
369
+ codecs. Workers supply `Schema.toCodecJson` themselves over structured clone and
370
+ still need no serialization layer. Cluster network traffic follows the protocol
371
+ codec; cluster persistence continues to use JSON.
372
+
373
+ `layerSchemaBinary({ maxFrameSize?, fingerprintPayloads? })` must be selected on
374
+ both peers. The default maximum frame size is 16 MiB. Envelopes use fingerprints
375
+ and a connection-local string dictionary. Payload fingerprints default to `false`
376
+ to allow compatible schema evolution; enable them for strict layout agreement.
377
+ This is an Effect RPC format, not generic MsgPack or JSON-RPC interoperability.
378
+ See `effect-schema-composition` for binary layout/ownership constraints.
353
379
 
354
380
  Rules, verified against the protocol implementations and the e2e matrix:
355
381
 
@@ -608,7 +634,7 @@ it.effect('GetUser', () =>
608
634
 
609
635
  ### Transport integration tests
610
636
 
611
- For exercising a real transport in-process, use `NodeHttpServer.layerTest` (provides both `HttpServer` and `HttpClient` on a random port) under your normal server+client layers — `packages/platform-node/test/RpcServer.test.ts` is the template; it runs one shared e2e suite against http/ws/tcp with the serializations each transport supports (plain json only over ws).
637
+ For exercising a real transport in-process, use `NodeHttpServer.layerTest` (provides both `HttpServer` and `HttpClient` on a random port) under your normal server+client layers — `packages/platform/node/test/RpcServer.test.ts` is the template. Framed formats support byte streams; unframed HTTP buffers the response. Verify the selected transport/serialization pair rather than assuming every pairing streams.
612
638
 
613
639
  ### Unit-testing one handler
614
640
 
@@ -628,6 +654,7 @@ const myProtocol = RpcServer.Protocol.make((writeRequest) =>
628
654
  const disconnects = yield* Queue.make<number>();
629
655
  // wire your transport: on inbound data → writeRequest(clientId, message)
630
656
  return {
657
+ codecFor: Schema.toCodecJson, // or the selected serialization.codecFor
631
658
  disconnects,
632
659
  send: (clientId, response, _transferables) => sendToTransport(clientId, response),
633
660
  end: (clientId) => Effect.void,
@@ -752,7 +779,7 @@ const ServerLayer = RpcServer.layer(StoreRpcs, { concurrency: 1 }).pipe(
752
779
  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.
753
780
  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.
754
781
  4. **`layerJson` on a raw TCP socket server.** No framing — decode breaks when messages span chunks. Sockets need `layerNdjson`, `layerNdJsonRpc()`, or `layerMsgPack`. (WebSocket is fine with `layerJson` — ws frames messages itself.)
755
- 5. **Streaming rpcs over `layerProtocolHttp` + `layerJson` and wondering why chunks arrive all at once.** Unframed HTTP buffers the whole response until every request in the call finishes. Use a framed serialization to get a chunked streaming response, and remember HTTP has no acks → no backpressure either way.
782
+ 5. **Streaming rpcs over `layerProtocolHttp` + `layerJson` and wondering why chunks arrive all at once.** Unframed HTTP buffers the whole response until every request in the call finishes. Framed HTTP streams incrementally with a bounded response queue (`streamBufferSize`, default 16). HTTP has no RPC acknowledgment protocol, but the bounded queue still backpressures producers; full-duplex transports additionally support RPC acks.
756
783
  6. **Leaving `disableFatalDefects: false` in production.** One handler `die` then nukes every in-flight request on that connection with a connection-level defect. Set `true` to confine defects to the failing request.
757
784
  7. **Assuming `concurrency` is per-client.** It is one semaphore per server instance shared by all clients. Use `Rpc.fork` to exempt cheap read handlers instead of raising the global limit.
758
785
  8. **Treating `Rpc.fork`/`Rpc.uninterruptible` as rpc options.** They wrap the handler's *returned* Effect/Stream: `db.get(id).pipe(Rpc.fork)`. There is no `{ fork: true }` key on `Rpc.make`.
@@ -44,7 +44,7 @@ const retryPolicy = Schedule.exponential('100 millis').pipe(
44
44
  const result = request.pipe(
45
45
  Effect.retryOrElse(retryPolicy, (error, scheduleOutput) =>
46
46
  Effect.logError('request retries exhausted', error).pipe(
47
- Effect.zipRight(fallback(error, scheduleOutput))
47
+ Effect.andThen(fallback(error, scheduleOutput))
48
48
  )
49
49
  )
50
50
  );
@@ -24,19 +24,21 @@ Reference this for:
24
24
 
25
25
  ### The Schema Type
26
26
 
27
- Every schema in Effect has the type signature `Schema<Type, Encoded, Context>` where:
27
+ Use `Schema.Schema<Type>` when only the decoded type matters, or
28
+ `Schema.Codec<Type, Encoded, DecodingServices, EncodingServices>` to retain the
29
+ full codec contract:
28
30
 
29
31
  - **Type**: The validated, decoded output type (what you get after successful decoding)
30
32
  - **Encoded**: The raw input type (what you provide for decoding)
31
- - **Context**: External dependencies required for encoding/decoding (often `never`)
33
+ - **DecodingServices**: Services needed to decode (default `never`)
34
+ - **EncodingServices**: Services needed to encode (default `never`)
32
35
 
33
36
  **Example:**
34
37
 
35
38
  ```typescript
36
39
  import { Schema } from 'effect';
37
40
 
38
- // Schema<number, string, never>
39
- // ^Type ^Encoded ^Context
41
+ // Schema.Codec<number, string, never, never>
40
42
  const NumberFromString = Schema.NumberFromString;
41
43
  ```
42
44
 
@@ -123,7 +125,7 @@ const PositiveInt = Schema.Number.check(
123
125
  Schema.isGreaterThan(0)
124
126
  );
125
127
 
126
- // Type: Schema<number, number, never>
128
+ // Type: Schema.Codec<number, number, never, never>
127
129
  // Both Type and Encoded are `number`
128
130
  ```
129
131
 
@@ -404,8 +406,8 @@ Schema.String.pipe(
404
406
  );
405
407
 
406
408
  // Pre-built transformation schemas
407
- Schema.Trimmed; // Schema<string, string> — trimmed string
408
- Schema.NonEmptyString; // Schema<string, string> — non-empty
409
+ Schema.Trimmed; // checks an already-trimmed string; does not trim input
410
+ Schema.NonEmptyString; // checks a non-empty string
409
411
  ```
410
412
 
411
413
  ### Number Transformations
@@ -453,7 +455,7 @@ function split(separator: string) {
453
455
  Schema.decodeTo(
454
456
  Schema.Array(Schema.String),
455
457
  SchemaTransformation.transform({
456
- decode: (s) => s.split(separator) as ReadonlyArray<string>,
458
+ decode: (s): ReadonlyArray<string> => s.split(separator),
457
459
  encode: (as) => as.join(separator)
458
460
  })
459
461
  )
@@ -463,6 +465,58 @@ function split(separator: string) {
463
465
 
464
466
  ## Custom Transformations
465
467
 
468
+ ### Schema-derived binary boundaries (rc.112)
469
+
470
+ Use `SchemaBinary.toCodec(schema)` from `effect/unstable/encoding` for a compact
471
+ `Uint8Array` representation. It derives the wire layout from the schema's
472
+ **encoded side**, preserving transformations, checks, and decoding/encoding
473
+ services. Use public Schema encode/decode adapters; `toCodecDirect` and the
474
+ module's internal fast-path functions are not application APIs.
475
+
476
+ <!-- typecheck -->
477
+ ```ts
478
+ import { Effect } from 'effect';
479
+ import * as Schema from 'effect/Schema';
480
+ import { SchemaBinary } from 'effect/unstable/encoding';
481
+
482
+ class Reading extends Schema.Class<Reading>('Reading')({
483
+ id: Schema.String,
484
+ value: Schema.NumberFromString
485
+ }) {}
486
+
487
+ const ReadingBinary = SchemaBinary.toCodec(Reading);
488
+ const roundTrip = Effect.gen(function* () {
489
+ const bytes = yield* Schema.encodeEffect(ReadingBinary)(
490
+ new Reading({ id: 'sensor-1', value: 12 })
491
+ );
492
+ return yield* Schema.decodeUnknownEffect(ReadingBinary)(bytes);
493
+ });
494
+ ```
495
+
496
+ One codec call handles one complete frame. For arbitrary stream chunks, use
497
+ `SchemaBinary.parser`, `encode` / `decode` Channels, or `duplex` (see
498
+ `effect-stream`). Encoded bytes are arena-backed views; copy with `bytes.slice()`
499
+ when independent ownership is required. Default mode supports compatible schema
500
+ evolution; `{ fingerprint: true }` uses positional layouts and an 8-byte layout
501
+ hash, requiring matching schema definitions. A fingerprint is a compatibility
502
+ check, not authentication or encryption.
503
+
504
+ The connection-scoped `encoder` / `parser` pair accepts `{ dictionary: true }`
505
+ for repeated strings. Both peers must use the same schema/options and process
506
+ frames in order; dictionary frames do not stand alone. This mode throws during
507
+ construction if the binary layer cannot fully validate that schema itself.
508
+ `parser.feed` lifts the synchronous parser into Effect; it does not make schema
509
+ transformations asynchronous or add their services. Use codec adapters or
510
+ `encode` / `decode` channels for async/service-dependent transformations. A parser
511
+ is spent after failure; call `end` at EOF to detect an incomplete trailing frame.
512
+ Use `maxFrameSize` to bound buffered frames. `SchemaBinary.fieldId` can assign
513
+ stable IDs to fields for compatible evolution; changing established IDs is a
514
+ wire-contract change.
515
+
516
+ Use JSON codecs for JSON contracts and binary codecs only when the transport
517
+ contract permits them. RPC selects a payload codec through `codecFor`; use
518
+ `RpcSerialization.layerSchemaBinary()` instead of manually wrapping RPC envelopes.
519
+
466
520
  ### SchemaTransformation.transform — Simple Transformations
467
521
 
468
522
  Use `SchemaTransformation.transform` when the transformation always succeeds:
@@ -542,7 +596,7 @@ const OptionFromNonEmptyString = Schema.optionalKey(Schema.String).pipe(
542
596
  import { Effect, Schema } from 'effect';
543
597
 
544
598
  declare const self: Effect.Effect<unknown, unknown, unknown>;
545
- declare const schema: Schema.Schema<unknown, unknown, never>;
599
+ declare const schema: Schema.Codec<unknown, unknown>;
546
600
  declare const toError: (e: unknown) => unknown;
547
601
 
548
602
  // Streamlined
@@ -748,9 +802,9 @@ const DogWithBreed3 = Schema.Struct({
748
802
  ```typescript
749
803
  import { Schema, SchemaTransformation } from 'effect';
750
804
 
751
- const ReadonlySetFromArray = <A, I, R>(
752
- itemSchema: Schema.Schema<A, I, R>
753
- ): Schema.Schema<ReadonlySet<A>, ReadonlyArray<I>, R> =>
805
+ const ReadonlySetFromArray = <A, I, RD, RE>(
806
+ itemSchema: Schema.Codec<A, I, RD, RE>
807
+ ): Schema.Codec<ReadonlySet<A>, ReadonlyArray<I>, RD, RE> =>
754
808
  Schema.Array(itemSchema).pipe(
755
809
  Schema.decodeTo(
756
810
  Schema.ReadonlySet(Schema.toType(itemSchema)),
@@ -762,7 +816,7 @@ const ReadonlySetFromArray = <A, I, R>(
762
816
  );
763
817
 
764
818
  const schema = ReadonlySetFromArray(Schema.String);
765
- // Schema<ReadonlySet<string>, readonly string[], never>
819
+ // Schema.Codec<ReadonlySet<string>, readonly string[], never, never>
766
820
  ```
767
821
 
768
822
  ### Multi-Stage Transformations
@@ -967,9 +1021,9 @@ When creating schemas, ensure:
967
1021
  - `SchemaGetter` is imported from `effect/SchemaGetter` or `{ SchemaGetter } from "effect"`
968
1022
  - `SchemaIssue` is imported from `effect/SchemaIssue` or `{ SchemaIssue } from "effect"`
969
1023
  - `Struct` is imported from `{ Struct } from "effect"` for `mapFields` operations
970
- - Schema API signature: `Schema<Type, Encoded, Context>`
1024
+ - Type-only schema: `Schema.Schema<Type>`; full codec: `Schema.Codec<Type, Encoded, DecodingServices, EncodingServices>`
971
1025
  - All schemas return `readonly` types by default
972
- - Use `Schema.revealCodec(schema)` to view any schema as `Schema<Type, Encoded, Context>`
1026
+ - Use `Schema.revealCodec(schema)` to expose its full `Schema.Codec` contract
973
1027
  - Use `Schema.toType(schema)` to get the type-side schema (replaces v3 `Schema.typeSchema`)
974
1028
  - Access struct fields with `.fields` property
975
1029
  - Filters preserve schema type — `.check()` on a `Schema.Struct` returns a `Schema.Struct`
@@ -20,6 +20,14 @@ Reference this for:
20
20
 
21
21
  ## 1. Key Renames (find-and-replace safe)
22
22
 
23
+ ### Current type model (rc.112)
24
+
25
+ `Schema.Schema<T>` describes only the decoded type. Preserve wire types and
26
+ services with `Schema.Codec<T, E, RD, RE>`: decoded Type, Encoded representation,
27
+ DecodingServices, and EncodingServices. A three-argument `Schema.Schema<A, I, R>`
28
+ is not a v4 type. Prefer inference or `S extends Schema.Constraint` in generic
29
+ schema helpers so concrete schema operations are retained.
30
+
23
31
  | v3 | v4 | Notes |
24
32
  | ----------------------------- | ----------------------------------- | ----------------------------------------- |
25
33
  | `annotations(ann)` | `annotate(ann)` | |
@@ -374,7 +382,7 @@ const fallback = SchemaGetter.withDefault(Effect.succeed('viewer'));
374
382
 
375
383
  ### Later v4 Updates
376
384
 
377
- - `Schema.makeEffect(input, options?)` on schemas and schema-backed classes returns an `Effect` that fails directly with `SchemaIssue.Issue`, not `Schema.SchemaError`.
385
+ - `schema.makeEffect(input, options?)` on schemas and schema-backed classes returns an `Effect` that fails directly with `SchemaIssue.Issue`, not `Schema.SchemaError`.
378
386
  - `Schema.resolveInto` was renamed to `Schema.resolveAnnotations`.
379
387
  - `Schema.resolveAnnotationsKey(schema)` returns key-level annotations.
380
388
  - `Schema.annotateEncoded({...})` annotates the encoded side of a transformed schema; use `Schema.annotate({...})` for the decoded Type side.
@@ -485,6 +493,40 @@ The input is `{ ast, occurrences, identifier }`; return a name to extract that c
485
493
 
486
494
  The standalone `SchemaError` root module was removed. Parser adapters such as `Schema.decodeUnknownEffect` fail with `Schema.SchemaError`, which contains the structured `issue`; narrow unknown failures with `Schema.isSchemaError`. By contrast, schema/class `makeEffect` and constructor defaults fail directly with `SchemaIssue.Issue`.
487
495
 
496
+ In rc.112, `SchemaError` skips stack-frame capture for lower construction cost.
497
+ Use its `issue` and formatter/message for validation diagnostics; a captured
498
+ parser-error stack is not a diagnostic contract. Synchronous parser optimizations
499
+ do not change the choice between constructors, sync decoders, and Effect decoders.
500
+
501
+ ### Partial tagged-union matching (rc.112)
502
+
503
+ `Schema.TaggedUnion(...)` and `Schema.Union([...]).pipe(Schema.toTaggedUnion(tag))`
504
+ now expose `matchOrElse(value, cases, fallback)` and its curried
505
+ `matchOrElse(cases, fallback)(value)` form. Use exhaustive `.match` for closed
506
+ business decisions. Use `.matchOrElse` when all remaining cases genuinely share
507
+ one behavior. The `toTaggedUnion` helper narrows the fallback to omitted variants;
508
+ the direct `TaggedUnion` overload types its fallback as the full union.
509
+ Neither matcher decodes unknown input. See `effect-pattern-matching` for a checked example.
510
+
511
+ ### JSON Schema import, conversion, and Standard Schema (rc.112)
512
+
513
+ - `SchemaRepresentation.fromJsonSchemaDocument` rejects unsupported references,
514
+ validation keywords, object/array `const` or `enum` values, and intersections
515
+ it cannot represent faithfully. Do not discard the failing constraint to make
516
+ an import succeed.
517
+ - A keyword such as `minLength` does not imply `type: 'string'`. Constraints beside
518
+ `const`, `enum`, and `$ref` are applied. Imported `oneOf` remains `oneOf` on export,
519
+ and tuple imports preserve `minItems` even when `prefixItems` alone is insufficient.
520
+ - `JsonSchema` dialect conversion preserves custom keywords and representable
521
+ conditionals, contains, dependencies, identifiers, and tuples; it relocates
522
+ local references and throws for unsupported conversions. These synchronous
523
+ schema-tooling failures need an explicit `Effect.try` boundary when actionable.
524
+ - Import vendored V1 interoperability types from `effect/StandardSchema`, for
525
+ example `StandardSchemaV1` and `StandardJSONSchemaV1`. Continue to adapt Effect
526
+ schemas with `Schema.toStandardSchemaV1`; the new module is not a schema builder.
527
+ - Binary encoding is available from `effect/unstable/encoding` as `SchemaBinary`.
528
+ See `effect-schema-composition` for codecs and `effect-stream` for framing.
529
+
488
530
  ## 8. New Modules
489
531
 
490
532
  ### SchemaTransformation
@@ -305,6 +305,11 @@ Close semantics (from `internal/effect.ts` `scopeCloseFinalizers`):
305
305
  - The scope transitions to `Closed` *before* finalizers run, so finalizers registered from inside finalizers execute immediately.
306
306
  - You can inspect `scope.state._tag` (`'Empty' | 'Open' | 'Closed'`) and `scope.strategy` directly.
307
307
 
308
+ In rc.112, `Scope.State.Open` stores `finalizerKey` / `finalizer` inline and
309
+ allocates its optional `finalizers` map only for additional entries. Avoid
310
+ constructing or mutating that representation; use `Scope.addFinalizer*` and
311
+ `Scope.close`. LIFO order and failure-preserving cleanup still apply.
312
+
308
313
  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
314
 
310
315
  ---
@@ -494,6 +499,31 @@ Semantics (verified in `ScopedRef.ts` and its tests):
494
499
 
495
500
  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
501
 
502
+ ### Callback-scoped pool checkout (rc.112)
503
+
504
+ Prefer `Pool.use(pool, use)` when one callback owns the entire borrow. It returns
505
+ the item on success, failure, or interruption without adding `Scope` to the
506
+ caller's requirements. Pool construction still needs an owning scope. Use
507
+ `Pool.get` when the caller deliberately owns a longer checkout scope.
508
+
509
+ <!-- typecheck -->
510
+ ```ts
511
+ import { Effect, Pool } from 'effect';
512
+
513
+ const program = Effect.gen(function* () {
514
+ const pool = yield* Pool.make({
515
+ acquire: Effect.succeed('connection'),
516
+ size: 2
517
+ });
518
+ return yield* Pool.use(pool, (connection) => Effect.succeed(connection.length));
519
+ }).pipe(Effect.scoped);
520
+ ```
521
+
522
+ `Effect.scoped(Pool.get(pool))` releases the checkout before the returned item is
523
+ used outside that effect. Put the use inside the scope or use `Pool.use`.
524
+ `Pool.State` / `Pool.PoolItem` changed in rc.112 (incremental usage and intrusive
525
+ FIFO tracking); use the public checkout/invalidation APIs rather than their fields.
526
+
497
527
  ---
498
528
 
499
529
  ## 9. Scope-Aware Utilities
@@ -344,7 +344,10 @@ This reduces cognitive load in the parent service and makes race-sensitive behav
344
344
 
345
345
  ## Pattern: No Requirement Leakage
346
346
 
347
- Service methods should **never** have requirements in their return type:
347
+ Capture stable implementation dependencies at construction. Preserve intentional
348
+ call-time requirements such as `Scope`, transactions, request context, and
349
+ schema decoding/encoding services in method return types; capturing those in an
350
+ application-lifetime layer would give them the wrong lifetime or authority.
348
351
 
349
352
  ```typescript
350
353
  // database.ts
@@ -389,7 +392,8 @@ Dependencies are handled by:
389
392
  1. **`Layer.effect` closure** — services captured at construction time via `yield*`
390
393
  2. **`Layer.provide`** — wires dependency layers into `defaultLayer`
391
394
 
392
- Both keep the method signatures clean (`R = never`).
395
+ Both remove stable infrastructure dependencies from method signatures. `R = never`
396
+ is appropriate only when the operation has no intentional caller-supplied context.
393
397
 
394
398
  ## Pattern: Whole-Function Transforms
395
399
 
@@ -556,7 +560,9 @@ const TestWebhook = Layer.mock(PaymentWebhookGateway.Service)({
556
560
  });
557
561
  ```
558
562
 
559
- `Layer.mock(Service)({...})` is shorthand for `Layer.succeed(Service, Service.of({...}))` — use whichever reads more clearly in context.
563
+ `Layer.mock(Service)({...})` accepts a partial implementation and supplies defecting
564
+ stubs for unimplemented members. Use a complete `Layer.succeed(Service,
565
+ Service.of({...}))` fake when every method must be implemented at compile time.
560
566
 
561
567
  For a reusable stateful fake, expose a separate test-control service and provide the same implementation under both tags with `Layer.effectContext`. Production code sees only the production interface; tests can inspect state and trigger transitions deterministically.
562
568
 
@@ -643,7 +649,7 @@ Tag identifiers should include the domain name according to project convention:
643
649
  - [ ] When using class-style `Context.Service`, the shape is its type parameter and the class body is empty
644
650
  - [ ] Service methods use `Effect.fn("Domain.methodName")` with the projected domain prefix
645
651
  - [ ] Service represents single capability
646
- - [ ] All operations have Requirements = never (no R parameter)
652
+ - [ ] Stable dependencies are captured; intentional caller-scoped requirements remain explicit in R
647
653
  - [ ] Dependencies captured in `Layer.effect` closure via `yield*`; wired via `Layer.provide` on `defaultLayer`
648
654
  - [ ] The project tag's implementation constructor is used; for `Context.Service`, use `Service.of({...})`
649
655
  - [ ] Tagged with a descriptive, unique identifier under project convention
@@ -15,11 +15,11 @@ Key files:
15
15
 
16
16
  - `packages/effect/src/unstable/socket/Socket.ts` — `Socket` interface and service tag, `make`, `CloseEvent`, the `SocketError` taxonomy, channel adapters (`toChannel`, `toChannelString`, `toChannelMap`, `makeChannel`), WebSocket constructors (`makeWebSocket`, `fromWebSocket`, `layerWebSocket`, `WebSocketConstructor`), `fromTransformStream`
17
17
  - `packages/effect/src/unstable/socket/SocketServer.ts` — `SocketServer` service contract, `Address` (`TcpAddress` / `UnixAddress`), `SocketServerError`
18
- - `packages/platform-node-shared/src/NodeSocket.ts` — `makeNet`, `fromDuplex`, `makeNetChannel`, `layerNet`, `NetSocket` service, `NodeWS` (`ws` re-export)
19
- - `packages/platform-node-shared/src/NodeSocketServer.ts` — TCP/Unix server `make`/`layer`, WebSocket server `makeWebSocket`/`layerWebSocket`, `IncomingMessage` service
20
- - `packages/platform-node/src/NodeSocket.ts` — re-exports shared module; adds `layerWebSocketConstructor`, `layerWebSocketConstructorWS`, `layerWebSocket`
21
- - `packages/platform-bun/src/BunSocket.ts` — same shared re-export; Bun-global WebSocket constructor layers
22
- - `packages/platform-node/test/NodeSocket.test.ts` — loopback echo server, WebSocket client semantics, transform-stream sockets
18
+ - `packages/platform/node-shared/src/NodeSocket.ts` — `makeNet`, `fromDuplex`, `makeNetChannel`, `layerNet`, `NetSocket` service, `NodeWS` (`ws` re-export)
19
+ - `packages/platform/node-shared/src/NodeSocketServer.ts` — TCP/Unix server `make`/`layer`, WebSocket server `makeWebSocket`/`layerWebSocket`, `IncomingMessage` service
20
+ - `packages/platform/node/src/NodeSocket.ts` — re-exports shared module; adds `layerWebSocketConstructor`, `layerWebSocketConstructorWS`, `layerWebSocket`
21
+ - `packages/platform/bun/src/BunSocket.ts` — same shared re-export; Bun-global WebSocket constructor layers
22
+ - `packages/platform/node/test/NodeSocket.test.ts` — loopback echo server, WebSocket client semantics, transform-stream sockets
23
23
  - `packages/effect/src/unstable/devtools/DevToolsClient.ts` — production example of NDJSON-framed request/response over a `Socket`
24
24
  - `packages/effect/src/unstable/rpc/RpcClient.ts` (`makeProtocolSocket`) — production example of socket reconnect with retry schedules
25
25
 
@@ -606,6 +606,28 @@ const main = program.pipe(Effect.provide(DatabaseLayer));
606
606
 
607
607
  ### PgClient-Specific Features
608
608
 
609
+ ### Low-level PostgreSQL codecs (rc.112)
610
+
611
+ `@effect/sql-pg` adds public `PgProtocol`, `PgTypes`, and `PgAuth` modules.
612
+ These are building blocks for protocol adapters; `PgClient` still uses `pg`.
613
+
614
+ - `PgProtocol`: PostgreSQL 3.0 frontend encoding and incremental backend-frame
615
+ parsing. Stateful parser failures are terminal and synchronous; lift a parser
616
+ boundary with `Effect.try` and preserve the protocol error.
617
+ - `PgTypes`: binary scalar and one-dimensional array OID codecs returning typed
618
+ `Result` failures. Internal field-reader throwing fast paths are not the public
619
+ application API.
620
+ - `PgAuth`: MD5 and SCRAM-SHA-256 authentication codecs with typed `Result`
621
+ failures. Treat authentication material as secrets.
622
+ - Encoded frames/decoded byte fields are buffer views. Copy bytes that must
623
+ outlive the owning message. Read `packages/sql/pg/src/{PgProtocol,PgTypes,PgAuth}.ts`
624
+ for exact signatures before implementing an adapter.
625
+
626
+ Driver releases also update production dependencies for D1, mysql2, and PGlite;
627
+ keep the Effect-family package versions aligned when upgrading those adapters.
628
+
629
+ ### PgClient JSON and Notifications
630
+
609
631
  ```ts
610
632
  const pg = yield* PgClient;
611
633
 
@@ -355,7 +355,7 @@ stream.pipe(
355
355
 
356
356
  ---
357
357
 
358
- ## 4. Encoding & Decoding (NDJSON / Msgpack)
358
+ ## 4. Encoding & Decoding (NDJSON / Msgpack / SchemaBinary)
359
359
 
360
360
  Use `Stream.pipeThroughChannel` with codec channels from `effect/unstable/encoding`.
361
361
 
@@ -363,6 +363,37 @@ Use `Stream.pipeThroughChannel` with codec channels from `effect/unstable/encodi
363
363
  import { Ndjson, Msgpack } from 'effect/unstable/encoding';
364
364
  ```
365
365
 
366
+ ### Schema-derived binary frames (rc.112)
367
+
368
+ <!-- typecheck -->
369
+ ```ts
370
+ import { Stream } from 'effect';
371
+ import * as Schema from 'effect/Schema';
372
+ import { SchemaBinary } from 'effect/unstable/encoding';
373
+
374
+ class Reading extends Schema.Class<Reading>('Reading')({
375
+ id: Schema.String,
376
+ value: Schema.Number
377
+ }) {}
378
+
379
+ const roundTrip = Stream.make(new Reading({ id: 'sensor-1', value: 21 })).pipe(
380
+ Stream.pipeThroughChannel(SchemaBinary.encode(Reading)()),
381
+ Stream.pipeThroughChannel(SchemaBinary.decode(Reading, { maxFrameSize: 1024 })()),
382
+ Stream.runCollect
383
+ );
384
+ ```
385
+
386
+ `encode(schema)()` and `decode(schema)()` are channel factories; use them with
387
+ `Stream.pipeThroughChannel`. Input chunks can split or concatenate frames.
388
+ The decoder retains completed values before a later failure and fails with
389
+ `Schema.SchemaError` on an incomplete final frame. Decoding/encoding services
390
+ from schema transformations remain in the channel requirements. `maxFrameSize`
391
+ limits decoding only. Keep paired schemas/options compatible, and copy any
392
+ arena-backed encoded bytes that must survive later writes. For synchronous
393
+ framing/dictionaries use `SchemaBinary.encoder` and `parser`; `duplex` adapts a
394
+ bidirectional channel. See `effect-schema-composition` for codec layout,
395
+ fingerprints, and ownership. Use `RpcSerialization.layerSchemaBinary` for RPC.
396
+
366
397
  ### Text decoding (split multi-byte characters)
367
398
 
368
399
  To turn a byte stream into text, use `Stream.decodeText` (or `Channel.decodeText`) rather than hand-rolling `new TextDecoder().decode(chunk)` per chunk. These helpers decode with streaming enabled, so multi-byte UTF-8 characters split across `Uint8Array` chunk boundaries are reassembled correctly; per-chunk `TextDecoder` calls would corrupt characters that straddle a boundary.
@@ -14,13 +14,18 @@ Browse and read files there directly to look up APIs, types, and implementations
14
14
 
15
15
  Reference this for:
16
16
 
17
- - Testing utilities: `packages/effect/src/Testing.ts`
17
+ - Testing utilities: `packages/effect/src/testing/`
18
18
  - @effect/vitest source: `packages/vitest/`
19
19
  - Migration guide: `MIGRATION.md`
20
20
  - Effect source: `packages/effect/src/`
21
21
 
22
22
  ## Framework Selection
23
23
 
24
+ Keep `@effect/vitest` aligned with the Effect release. The rc.112 adapter's
25
+ Vitest peer range is `>=4.1.0 <5.0.0`; do not install it into a Vitest 3 project
26
+ without migrating that project's test framework. Plain compiler/inventory tests
27
+ can keep using their existing Vitest runner without the adapter.
28
+
24
29
  **CRITICAL**: Choose the correct testing framework based on the code being tested.
25
30
 
26
31
  ### Use @effect/vitest for Effect Code
@@ -168,13 +173,13 @@ import {
168
173
  assertFalse,
169
174
  assertSome, // For Option.Some
170
175
  assertNone, // For Option.None
171
- assertSuccess, // For Either.Right / Exit.Success
172
- assertFailure // For Either.Left / Exit.Failure
176
+ assertSuccess, // For Result.Success / Exit.Success
177
+ assertFailure // For Result.Failure / Exit.Failure
173
178
  } from '@effect/vitest/utils';
174
- import { Effect, Option, Either } from 'effect';
179
+ import { Effect, Option, Result } from 'effect';
175
180
 
176
181
  declare const someOptionalEffect: Effect.Effect<Option.Option<number>>;
177
- declare const someEitherEffect: Effect.Effect<Either.Either<number, Error>>;
182
+ declare const someResultEffect: Effect.Effect<Result.Result<number, Error>>;
178
183
  declare const expectedValue: number;
179
184
 
180
185
  it.effect('with effect assertions', () =>
@@ -182,8 +187,8 @@ it.effect('with effect assertions', () =>
182
187
  const option = yield* someOptionalEffect;
183
188
  assertSome(option, expectedValue);
184
189
 
185
- const either = yield* someEitherEffect;
186
- assertSuccess(either, expectedValue);
190
+ const result = yield* someResultEffect;
191
+ assertSuccess(result, expectedValue);
187
192
  })
188
193
  );
189
194
  ```
@@ -223,7 +228,10 @@ const TestUserService = Layer.mock(UserService)({
223
228
  });
224
229
  ```
225
230
 
226
- `Layer.mock(Service)({...})` is shorthand for `Layer.succeed(Service, Service.of({...}))` — use whichever reads more clearly in context.
231
+ `Layer.mock` accepts a partial implementation and supplies defecting stubs for
232
+ omitted methods. It is not equivalent to a complete, compiler-checked
233
+ `Layer.succeed(Service, Service.of({...}))`. Prefer complete fakes for reusable
234
+ test services; use partial mocks only when omitted operations should defect.
227
235
 
228
236
  ### First-Class Controllable Test Services
229
237
 
@@ -809,7 +817,7 @@ import { Effect, Logger } from 'effect';
809
817
  it.effect('logs visible', () =>
810
818
  Effect.gen(function* () {
811
819
  yield* Effect.log('This will appear');
812
- }).pipe(Effect.provide(Logger.pretty))
820
+ }).pipe(Effect.provide(Logger.layer([Logger.consolePretty()])))
813
821
  );
814
822
 
815
823
  // Use it.live only when the live console itself is under test.
@@ -857,23 +865,23 @@ describe('UserService', () => {
857
865
  });
858
866
  ```
859
867
 
860
- ### Testing STM Operations
868
+ ### Testing Transactions
869
+
870
+ In v4, transactional collections use `TxRef` and `Effect.tx`; there is no
871
+ separate `STM` effect type or `STM.commit`. Test rollback as well as success:
861
872
 
862
873
  ```typescript
863
874
  import { it, expect } from '@effect/vitest';
864
- import { Effect, STM, TRef } from 'effect';
875
+ import { Effect, TxRef } from 'effect';
865
876
 
866
- it.effect('should handle concurrent updates', () =>
877
+ it.effect('rolls back a failed transaction', () =>
867
878
  Effect.gen(function* () {
868
- const counter = yield* TRef.make(0);
869
-
870
- const increment = STM.updateAndGet(counter, (n) => n + 1);
871
-
872
- yield* STM.commit(increment);
873
- yield* STM.commit(increment);
874
-
875
- const final = yield* STM.commit(TRef.get(counter));
876
- expect(final).toBe(2);
879
+ const counter = yield* TxRef.make(0);
880
+ yield* Effect.gen(function* () {
881
+ yield* TxRef.set(counter, 1);
882
+ return yield* Effect.fail('cancel');
883
+ }).pipe(Effect.tx, Effect.flip);
884
+ expect(yield* TxRef.get(counter)).toBe(0);
877
885
  })
878
886
  );
879
887
  ```
@@ -882,14 +890,14 @@ it.effect('should handle concurrent updates', () =>
882
890
 
883
891
  ```typescript
884
892
  import { it, expect } from '@effect/vitest';
885
- import { Effect, STM } from 'effect';
893
+ import { Effect } from 'effect';
886
894
 
887
895
  declare const GCounter: {
888
896
  make: (id: string) => Effect.Effect<unknown>;
889
- increment: (counter: unknown, value: number) => STM.STM<void>;
890
- query: (counter: unknown) => STM.STM<unknown>;
891
- merge: (counter: unknown, state: unknown) => STM.STM<void>;
892
- value: (counter: unknown) => STM.STM<number>;
897
+ increment: (counter: unknown, value: number) => Effect.Effect<void>;
898
+ query: (counter: unknown) => Effect.Effect<unknown>;
899
+ merge: (counter: unknown, state: unknown) => Effect.Effect<void>;
900
+ value: (counter: unknown) => Effect.Effect<number>;
893
901
  };
894
902
 
895
903
  declare const ReplicaId: (id: string) => string;
@@ -899,13 +907,13 @@ it.effect('should merge states correctly', () =>
899
907
  const counter1 = yield* GCounter.make(ReplicaId('replica-1'));
900
908
  const counter2 = yield* GCounter.make(ReplicaId('replica-2'));
901
909
 
902
- yield* STM.commit(GCounter.increment(counter1, 10));
903
- yield* STM.commit(GCounter.increment(counter2, 20));
910
+ yield* Effect.tx(GCounter.increment(counter1, 10));
911
+ yield* Effect.tx(GCounter.increment(counter2, 20));
904
912
 
905
- const state2 = yield* STM.commit(GCounter.query(counter2));
906
- yield* STM.commit(GCounter.merge(counter1, state2));
913
+ const state2 = yield* Effect.tx(GCounter.query(counter2));
914
+ yield* Effect.tx(GCounter.merge(counter1, state2));
907
915
 
908
- const result = yield* STM.commit(GCounter.value(counter1));
916
+ const result = yield* Effect.tx(GCounter.value(counter1));
909
917
  expect(result).toBe(30);
910
918
  })
911
919
  );
@@ -125,6 +125,12 @@ yield* SendEmail.resume(executionId);
125
125
 
126
126
  ### Deterministic Execution ID
127
127
 
128
+ As of rc.112, generated `WorkflowProxy.toRpcGroup` `<Name>Discard` RPCs and
129
+ `WorkflowProxy.toHttpApiGroup` discard HTTP endpoints also return the execution
130
+ ID (`Schema.String`). Consumers can persist that ID to poll/resume the workflow.
131
+ Update generated client response types and tests that expected `void`; ordinary
132
+ RPC `discard: true` call options still discard the response and cannot return it.
133
+
128
134
  The execution ID is computed as a hash of `"${name}-${idempotencyKey(payload)}"`. This means executing the same workflow with the same payload is idempotent — it returns the existing execution rather than starting a new one.
129
135
 
130
136
  ```ts