opencode-effect-enforcer 0.2.4 → 0.2.6

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 (52) hide show
  1. package/README.md +42 -140
  2. package/docs/effect-4.0.0-rc.116-changelog.md +2654 -0
  3. package/docs/effect-4.0.0-rc.116.md +102 -0
  4. package/guidance/effect-first-development.md +23 -14
  5. package/guidance/progressive-disclosure-guidance.md +3 -3
  6. package/package.json +3 -3
  7. package/patterns/avoid-direct-tag-checks.md +1 -1
  8. package/patterns/avoid-process-env.md +4 -4
  9. package/patterns/context-tag-extends.md +4 -4
  10. package/patterns/prefer-redacted-config.md +10 -10
  11. package/patterns/require-effect-concurrency.md +1 -1
  12. package/skills/effect-ai-chat/SKILL.md +2 -2
  13. package/skills/effect-ai-language-model/SKILL.md +36 -4
  14. package/skills/effect-ai-prompt/SKILL.md +1 -1
  15. package/skills/effect-ai-provider/SKILL.md +23 -12
  16. package/skills/effect-ai-tool/SKILL.md +13 -0
  17. package/skills/effect-atom-rpc/SKILL.md +7 -1
  18. package/skills/effect-atom-state/SKILL.md +8 -2
  19. package/skills/effect-cache/SKILL.md +10 -1
  20. package/skills/effect-cli/SKILL.md +105 -94
  21. package/skills/effect-command-executor/SKILL.md +7 -1
  22. package/skills/effect-config/SKILL.md +67 -44
  23. package/skills/effect-domain-modeling/SKILL.md +3 -3
  24. package/skills/effect-error-handling/SKILL.md +2 -2
  25. package/skills/effect-fiber/SKILL.md +2 -2
  26. package/skills/effect-filesystem/SKILL.md +34 -4
  27. package/skills/effect-http-api/SKILL.md +17 -2
  28. package/skills/effect-http-client/SKILL.md +11 -2
  29. package/skills/effect-http-server/SKILL.md +28 -11
  30. package/skills/effect-layer-design/SKILL.md +6 -2
  31. package/skills/effect-mcp-server/SKILL.md +21 -4
  32. package/skills/effect-observability/SKILL.md +2 -2
  33. package/skills/effect-optics/SKILL.md +1 -1
  34. package/skills/effect-parallelization/SKILL.md +2 -2
  35. package/skills/effect-pattern-matching/SKILL.md +1 -1
  36. package/skills/effect-platform-abstraction/SKILL.md +3 -3
  37. package/skills/effect-rpc-api/SKILL.md +3 -3
  38. package/skills/effect-rpc-client/SKILL.md +14 -13
  39. package/skills/effect-rpc-cluster/SKILL.md +15 -12
  40. package/skills/effect-rpc-server/SKILL.md +11 -13
  41. package/skills/effect-scheduling/SKILL.md +7 -0
  42. package/skills/effect-schema-composition/SKILL.md +12 -4
  43. package/skills/effect-schema-v4/SKILL.md +75 -20
  44. package/skills/effect-scope/SKILL.md +4 -4
  45. package/skills/effect-socket/SKILL.md +161 -658
  46. package/skills/effect-sql/SKILL.md +50 -12
  47. package/skills/effect-stream/SKILL.md +19 -24
  48. package/skills/effect-testing/SKILL.md +35 -22
  49. package/skills/effect-workflow/SKILL.md +13 -2
  50. package/src/enforcer.ts +1 -1
  51. package/src/index.ts +1 -1
  52. package/src/skills.ts +4 -4
@@ -37,20 +37,20 @@ Never read `process.env` directly in Effect code. `Config` provides:
37
37
  Each constructor reads a single value and decodes it. The optional `name` parameter sets the root path segment for lookup. Omit it when the config is part of a larger `Config.schema`.
38
38
 
39
39
  ```ts
40
- Config.string('HOST'); // string
41
- Config.nonEmptyString('HOST'); // string (rejects "")
42
- Config.number('RATE'); // number (includes NaN, Infinity)
43
- Config.finite('RATE'); // number (rejects NaN, Infinity)
44
- Config.int('PORT'); // number (integers only)
45
- Config.boolean('DEBUG'); // boolean (accepts true/false, yes/no, on/off, 1/0, y/n)
46
- Config.port('PORT'); // number (integer in 1–65535)
47
- Config.url('CALLBACK_URL'); // URL
48
- Config.date('EXPIRES_AT'); // Date (rejects invalid dates)
49
- Config.duration('TIMEOUT'); // Duration (parses "10 seconds", "500 millis", "Infinity", "-Infinity")
50
- Config.logLevel('LOG_LEVEL'); // string (All|Fatal|Error|Warn|Info|Debug|Trace|None)
51
- Config.redacted('API_KEY'); // Redacted<string> (hidden from logs and toString)
52
- Config.literal('production', 'ENV'); // literal type (accepts only the given literal)
53
- Config.literals(['development', 'production'], 'ENV'); // accepts one of several literals
40
+ Config.String('HOST'); // string
41
+ Config.NonEmptyString('HOST'); // string (rejects "")
42
+ Config.Number('RATE'); // number (includes NaN, Infinity)
43
+ Config.Finite('RATE'); // number (rejects NaN, Infinity)
44
+ Config.Int('PORT'); // number (integers only)
45
+ Config.Boolean('DEBUG'); // boolean (accepts true/false, yes/no, on/off, 1/0, y/n)
46
+ Config.Port('PORT'); // number (integer in 1–65535)
47
+ Config.URL('CALLBACK_URL'); // URL
48
+ Config.Date('EXPIRES_AT'); // Date (rejects invalid dates)
49
+ Config.Duration('TIMEOUT'); // Duration (parses "10 seconds", "500 millis", "Infinity", "-Infinity")
50
+ Config.LogLevel('LOG_LEVEL'); // string (All|Fatal|Error|Warn|Info|Debug|Trace|None)
51
+ Config.Redacted('API_KEY'); // Redacted<string> (hidden from logs and toString)
52
+ Config.Literal('production', 'ENV'); // literal type (accepts only the given literal)
53
+ Config.Literals(['development', 'production'], 'ENV'); // accepts one of several literals
54
54
  ```
55
55
 
56
56
  ## Config Combinators
@@ -60,7 +60,7 @@ Config.literals(['development', 'production'], 'ENV'); // accepts one of several
60
60
  Only triggers when data is **missing**. Validation errors (wrong type, out of range) still propagate.
61
61
 
62
62
  ```ts
63
- const port = Config.int('PORT').pipe(Config.withDefault(3000));
63
+ const port = Config.Int('PORT').pipe(Config.withDefault(3000));
64
64
  ```
65
65
 
66
66
  ### `Config.option` — Optional Values
@@ -68,13 +68,13 @@ const port = Config.int('PORT').pipe(Config.withDefault(3000));
68
68
  Returns `Option.some(value)` on success, `Option.none()` when data is missing.
69
69
 
70
70
  ```ts
71
- const maybePort = Config.option(Config.int('PORT'));
71
+ const maybePort = Config.option(Config.Int('PORT'));
72
72
  ```
73
73
 
74
74
  ### `Config.map` — Transform a Value
75
75
 
76
76
  ```ts
77
- const upperHost = Config.string('HOST').pipe(
77
+ const upperHost = Config.String('HOST').pipe(
78
78
  Config.map((s) => s.toUpperCase())
79
79
  );
80
80
  ```
@@ -84,7 +84,7 @@ const upperHost = Config.string('HOST').pipe(
84
84
  Unlike `withDefault`, this catches **all** `ConfigError`s:
85
85
 
86
86
  ```ts
87
- const host = Config.string('HOST').pipe(
87
+ const host = Config.String('HOST').pipe(
88
88
  Config.orElse(() => Config.succeed('localhost'))
89
89
  );
90
90
  ```
@@ -96,13 +96,13 @@ Accepts a record or a tuple:
96
96
  ```ts
97
97
  // As a record
98
98
  const appConfig = Config.all({
99
- host: Config.string('host'),
100
- port: Config.int('port'),
101
- debug: Config.boolean('debug')
99
+ host: Config.String('host'),
100
+ port: Config.Int('port'),
101
+ debug: Config.Boolean('debug')
102
102
  });
103
103
 
104
104
  // As a tuple
105
- const pair = Config.all([Config.string('a'), Config.int('b')]);
105
+ const pair = Config.all([Config.String('a'), Config.Int('b')]);
106
106
  ```
107
107
 
108
108
  ### `Config.nested` — Scope Under a Prefix
@@ -111,8 +111,8 @@ Prepends a path segment to every key the inner config reads. With environment va
111
111
 
112
112
  ```ts
113
113
  const dbConfig = Config.all({
114
- host: Config.string('host'),
115
- port: Config.int('port')
114
+ host: Config.String('host'),
115
+ port: Config.Int('port')
116
116
  }).pipe(Config.nested('database'));
117
117
 
118
118
  // Reads from env: database_host, database_port
@@ -146,17 +146,40 @@ const ServerConfig = Config.schema(
146
146
  );
147
147
  ```
148
148
 
149
- ### Config Schemas for Use with `Config.schema`
149
+ ### Config constructors versus schemas
150
150
 
151
- | Schema | Type | Notes |
152
- | --------------------------- | -------------- | ------------------------------------------ |
153
- | `Config.Boolean` | `boolean` | Decodes `true/false/yes/no/on/off/1/0/y/n` |
154
- | `Schema.DurationFromString` | `Duration` | Decodes duration strings; accepts `"Infinity"` / `"-Infinity"` |
155
- | `Config.Port` | `number` | Integer in 1–65535 |
156
- | `Config.LogLevel` | `string` | One of the standard log level literals |
157
- | `Config.Record(key, value)` | `Record<K, V>` | Also parses flat `"k1=v1,k2=v2"` strings |
151
+ PascalCase names on `Config` construct configs, not schemas. Combine
152
+ `Config.Boolean`, `Config.Port`, and `Config.LogLevel` with `Config.all`; use
153
+ Schema values inside `Config.schema`. The specialized implementation schemas
154
+ are internal. `Config.mapEffect` is the effectful mapping combinator.
158
155
 
159
- Plain `Schema.Array` and `Schema.Record` load structural provider children. Use `Config.Array` and `Config.Record` when a flat separated scalar should also be accepted. Opaque encodings such as `Schema.Any`, `Schema.Unknown`, and `Schema.Json` are rejected when `Config.schema` is constructed; use a concrete shape or `Schema.fromJsonString(Schema.Json)` to read scalar JSON.
156
+ <!-- typecheck -->
157
+ ```ts
158
+ import { Config, ConfigProvider, Schema } from 'effect';
159
+
160
+ const settings = Config.all({
161
+ port: Config.Port('PORT'),
162
+ debug: Config.Boolean('DEBUG'),
163
+ exporters: Config.Array(Schema.String, 'EXPORTERS'),
164
+ labels: Config.Record(Schema.String, Schema.String, 'LABELS'),
165
+ limit: Config.ByteSize('LIMIT')
166
+ });
167
+ const parsed = settings.parse(ConfigProvider.fromUnknown({
168
+ PORT: '8080', DEBUG: 'yes', EXPORTERS: 'otlp,console',
169
+ LABELS: 'service.name=api', LIMIT: '64 KiB'
170
+ }));
171
+ ```
172
+
173
+ `Config.Array(value, path?, options?)` and `Config.Record(key, value, path?, options?)`
174
+ also accept an options object without a path. They read structural values or
175
+ separated strings. Plain `Schema.Array` / `Schema.Record` in `Config.schema` load
176
+ structural children. Opaque encodings such as `Schema.Any`, `Schema.Unknown`, and
177
+ `Schema.Json` are rejected; use a concrete shape or
178
+ `Schema.fromJsonString(Schema.Json)` for scalar JSON.
179
+
180
+ Environment/bracket array indices must be unpadded decimal integers from 0 through
181
+ 4294967294. Numeric-looking keys such as `01` remain object keys; use `[1]` for
182
+ array paths.
160
183
 
161
184
  Missing or unavailable representations are decoded as `undefined` before `Config.withDefault` and `Config.option` decide semantic absence. A successful decoded `undefined` or an explicitly present empty structure remains a real value and is not replaced by a default.
162
185
 
@@ -166,8 +189,8 @@ Missing or unavailable representations are decoded as `undefined` before `Config
166
189
 
167
190
  ```ts
168
191
  const program = Effect.gen(function* () {
169
- const host = yield* Config.string('HOST');
170
- const port = yield* Config.int('PORT');
192
+ const host = yield* Config.String('HOST');
193
+ const port = yield* Config.Int('PORT');
171
194
  console.log(`${host}:${port}`);
172
195
  });
173
196
  ```
@@ -175,7 +198,7 @@ const program = Effect.gen(function* () {
175
198
  ### 2. Call `.parse(provider)` directly — useful for testing
176
199
 
177
200
  ```ts
178
- const host = Config.string('HOST');
201
+ const host = Config.String('HOST');
179
202
  const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
180
203
  const result = Effect.runSync(host.parse(provider));
181
204
  // "localhost"
@@ -368,7 +391,7 @@ const TestLayer = ConfigProvider.layer(
368
391
  );
369
392
 
370
393
  const program = Effect.gen(function* () {
371
- const port = yield* Config.int('port');
394
+ const port = yield* Config.Int('port');
372
395
  return port;
373
396
  });
374
397
 
@@ -416,7 +439,7 @@ const DefaultsLayer = ConfigProvider.layerAdd(
416
439
  const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
417
440
 
418
441
  const program = Effect.gen(function* () {
419
- const host = yield* Config.string('HOST');
442
+ const host = yield* Config.String('HOST');
420
443
  return host;
421
444
  }).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider));
422
445
  ```
@@ -430,8 +453,8 @@ import { Config, ConfigProvider, Effect } from 'effect';
430
453
 
431
454
  // Pattern 1: .parse(provider) for direct testing
432
455
  const config = Config.all({
433
- host: Config.string('host'),
434
- port: Config.int('port')
456
+ host: Config.String('host'),
457
+ port: Config.Int('port')
435
458
  });
436
459
 
437
460
  const testProvider = ConfigProvider.fromUnknown({
@@ -451,7 +474,7 @@ const TestConfigLayer = ConfigProvider.layer(
451
474
  );
452
475
 
453
476
  const program = Effect.gen(function* () {
454
- const host = yield* Config.string('host').pipe(Config.nested('server'));
477
+ const host = yield* Config.String('host').pipe(Config.nested('server'));
455
478
  return host;
456
479
  });
457
480
 
@@ -466,7 +489,7 @@ Config operations fail with `ConfigError`, which wraps either:
466
489
  - **`SchemaError`** — data was found but didn't match the schema (wrong type, out of range, missing key)
467
490
 
468
491
  ```ts
469
- const program = Config.int('PORT')
492
+ const program = Config.Int('PORT')
470
493
  .parse(ConfigProvider.fromUnknown({ PORT: 'not-a-number' }))
471
494
  .pipe(
472
495
  Effect.tapError((error) =>
@@ -510,7 +533,7 @@ const DbConfig = Config.schema(
510
533
  const AppConfig = Config.all({
511
534
  server: ServerConfig,
512
535
  db: DbConfig,
513
- debug: Config.boolean('debug').pipe(Config.withDefault(false))
536
+ debug: Config.Boolean('debug').pipe(Config.withDefault(false))
514
537
  });
515
538
 
516
539
  // In production — just yield it, reads from process.env
@@ -551,7 +574,7 @@ debug=true
551
574
  const port = parseInt(process.env.PORT ?? '3000');
552
575
 
553
576
  // GOOD
554
- const port = Config.int('PORT').pipe(Config.withDefault(3000));
577
+ const port = Config.Int('PORT').pipe(Config.withDefault(3000));
555
578
  ```
556
579
 
557
580
  ### NEVER validate config manually
@@ -13,7 +13,7 @@ and `effect-typeclass-design` for reusable predicate/order APIs.
13
13
 
14
14
  ## Source Reference
15
15
 
16
- Baseline: **Effect 4.0.0-rc.112**. In the Effect source reference, consult
16
+ Baseline: **Effect 4.0.0-rc.116**. In the Effect source reference, consult
17
17
  `packages/effect/SCHEMA.md` and `packages/effect/src/{Schema,Match,DateTime,Order}.ts`.
18
18
  Verify the installed version and source tag before applying newer APIs.
19
19
 
@@ -159,7 +159,7 @@ message. Never claim the invariant is enforced solely because fields use
159
159
  `matchOrElse`. Use it when plain internal object variants are intentional.
160
160
  - `Data.TaggedEnum` is useful for trusted, non-schema types. It does not decode
161
161
  unknown input; do not duplicate a schema model with a parallel Data union.
162
- - Use `.match` for exhaustiveness. Use rc.112 `.matchOrElse(cases, fallback)` or
162
+ - Use `.match` for exhaustiveness. Use `.matchOrElse(cases, fallback)` or
163
163
  `.matchOrElse(value, cases, fallback)` when one fallback truthfully handles all
164
164
  other cases. The fallback from `toTaggedUnion` is narrowed to unmatched
165
165
  variants; direct `Schema.TaggedUnion.matchOrElse` types it as the full union.
@@ -228,7 +228,7 @@ see `effect-schema-composition` for transformation and recursion details.
228
228
  - Use `Schema.toEquivalence` for model comparisons. `Eq.equals` provides general
229
229
  structural equality in v4, but schema equivalence expresses model intent.
230
230
  - Compose `Order.mapInput` / `Order.combine` and sort with `Arr.sort`. Finite
231
- `Arr.groupBy` / `Iterable.groupBy` keys remain finite in rc.112, with optional
231
+ `Arr.groupBy` / `Iterable.groupBy` preserve finite keys, with optional
232
232
  properties: a particular group may not exist. Handle that absence explicitly.
233
233
  - Use `DateTime` for instants, `Duration` for intervals, and `DateTime.now` for
234
234
  effectful current time. Deterministic fixtures may use `DateTime.makeUnsafe`
@@ -84,7 +84,7 @@ Reference this for:
84
84
  | `Schema.TaggedErrorClass` | `Schema.TaggedError` |
85
85
  | `Schema.ErrorClass` | `Schema.Error` |
86
86
  | `Schema.Error` (instance schema) | `Schema.ErrorInstance` |
87
- | `Schema.ErrorReviver` | `Schema.ErrorInstanceReviver` |
87
+ | `Schema.ErrorReviver` | `SchemaRepresentation.ErrorInstanceReviver` |
88
88
  | `ParseError` | `Schema.SchemaError` |
89
89
 
90
90
  ## Core Error Handling Philosophy
@@ -592,7 +592,7 @@ const program2 = riskyOp().pipe(
592
592
  );
593
593
  ```
594
594
 
595
- > **Type preservation (beta.71):** When you omit `orElse`, the tags you do not handle stay in the error channel — `catchTag(['NotFound'], ...)` on `Effect<string, NotFound | Forbidden | ServerError>` yields `Effect<string, Forbidden | ServerError>`. Supplying `orElse` handles those remaining variants, so the resulting error channel reflects only what the fallback produces. A beta.71 fix ensures `catchTag` / `catchTags` / `catchIf` no longer silently drop the unhandled error types from the inferred type.
595
+ > **Type preservation:** When you omit `orElse`, unhandled tags stay in the error channel — `catchTag(['NotFound'], ...)` on `Effect<string, NotFound | Forbidden | ServerError>` yields `Effect<string, Forbidden | ServerError>`. Supplying `orElse` handles the remaining variants, so the resulting error channel reflects what the fallback produces.
596
596
 
597
597
  ### catchTags - Multiple Error Types
598
598
 
@@ -555,7 +555,7 @@ This is exactly what the FiberHandle/Map/Set runtime helpers wrap for you — pr
555
555
 
556
556
  ### Keep-alive and runMain
557
557
 
558
- In the current v4 runtime there is **no per-fiber keep-alive in the core runtime** (it existed earlier in v4 but was removed in beta.80; the `migration/fiber-keep-alive.md` doc predates the removal). A bare `Effect.runFork`/`Effect.runPromise` whose fiber is suspended on a pure Effect primitive (e.g. `Deferred.await`, `Effect.never`) does not by itself hold the Node.js process open.
558
+ There is **no per-fiber keep-alive in the core runtime**. A bare `Effect.runFork`/`Effect.runPromise` whose fiber is suspended on a pure Effect primitive (e.g. `Deferred.await`, `Effect.never`) does not by itself hold the Node.js process open.
559
559
 
560
560
  `Runtime.makeRunMain`-based runners — `NodeRuntime.runMain` from `@effect/platform-node`, `BunRuntime.runMain`, etc. — install a long-interval timer that keeps the process alive until the main fiber completes, and additionally provide SIGINT/SIGTERM handling (interrupting the root fiber gracefully), exit-code mapping (interruption-only causes → 130), and error reporting. Always use `runMain` for long-lived program entry points:
561
561
 
@@ -724,7 +724,7 @@ const handoff = Effect.gen(function* () {
724
724
  10. **Expecting `onlyIfMissing: true` to error when occupied** — it succeeds, returning a shared already-interrupted fiber while keeping the existing one. Check `Exit.hasInterrupts(yield* Fiber.await(fiber))` to detect the rejected start.
725
725
  11. **Calling `run` on a closed collection** — `FiberHandle.run`/`FiberMap.run` interrupt the *calling* fiber; `FiberSet.run` and all `runtime()` runners return a pre-interrupted fiber instead. Neither throws.
726
726
  12. **Assuming collection fibers are children of the caller** — they are root fibers created via `Effect.runForkWith` with the caller's context: they start immediately and survive the calling fiber; only the collection (scope close, replacement, remove/clear) interrupts them.
727
- 13. **Relying on `Effect.runFork`/`runPromise` to keep Node alive** — beta.80 removed the core fiber keep-alive; a fiber suspended on `Deferred.await`/`Effect.never` won't hold the process open. Use `NodeRuntime.runMain` (built on `Runtime.makeRunMain`).
727
+ 13. **Relying on `Effect.runFork`/`runPromise` to keep Node alive** — a fiber suspended on `Deferred.await`/`Effect.never` won't hold the process open. Use `NodeRuntime.runMain` (built on `Runtime.makeRunMain`).
728
728
  14. **Letting interruption leak through acquire/release** — wrap the whole sequence in `Effect.uninterruptibleMask` and `restore` only the use phase; pending interruption is delivered as soon as the region ends, so cleanup still runs exactly once.
729
729
  15. **Leaving collection type parameters off** — `FiberHandle.make()` defaults to `<unknown, unknown>`, making `join` surface `unknown` errors. Always pass them: `FiberHandle.make<A, E>()`, `FiberMap.make<K, A, E>()`, `FiberSet.make<A, E>()`.
730
730
  16. **Using v3 `FiberId` types** — v4 fiber ids are plain `number`s; `Cause.interruptors(cause)` and `Effect.onInterrupt` finalizers give you `ReadonlySet<number>`.
@@ -207,16 +207,46 @@ const useFileHandle = Effect.gen(function* () {
207
207
  Effect.gen(function* () {
208
208
  const file = yield* fs.open('data.txt', { flag: 'r' });
209
209
 
210
- // File methods return branded Size values for byte counts and offsets.
210
+ // Byte counts use ByteSize; seek positions are signed bigint inputs.
211
211
  const buffer = new Uint8Array(1024);
212
212
  const bytesRead = yield* file.read(buffer);
213
- const offset = yield* file.seek(FileSystem.Size(0), 'start');
213
+ const offset = yield* file.seek(0n, 'start');
214
214
  })
215
215
  );
216
216
  });
217
217
  ```
218
218
 
219
- `file.seek(offset, from)` accepts a `SizeInput`, supports `from: 'start' | 'current'`, and returns the new offset as `FileSystem.Size` (not `void` or a plain number). Open `File` handles no longer expose a `descriptor` property or `File.Descriptor` type as of beta.103; use the scoped handle operations (`read`, `readAlloc`, `write`, `writeAll`, `seek`, `stat`, `sync`, and `truncate`) instead.
219
+ `file.seek(offset, from)` accepts `bigint`, supports `from: 'start' | 'current'`,
220
+ and returns `Effect<bigint, PlatformError>`. A negative resulting position fails
221
+ without changing the cursor. Byte counts and `File.Info.size` use
222
+ `ByteSize.ByteSize`; size inputs use `ByteSize.Input`. Keep large offsets/counts
223
+ exact, and handle `Option.none()` for optional stat metadata outside the safe
224
+ integer range. Use scoped handle operations (`read`, `readAlloc`, `write`,
225
+ `writeAll`, `seek`, `stat`, `sync`, `truncate`) rather than raw descriptors.
226
+
227
+ ### Byte sizes and exact offsets
228
+
229
+ <!-- typecheck -->
230
+ ```ts
231
+ import { ByteSize, Effect, FileSystem } from 'effect';
232
+
233
+ const inspect = Effect.gen(function* () {
234
+ const fs = yield* FileSystem.FileSystem;
235
+ const file = yield* fs.open('data.bin');
236
+ const position = yield* file.seek(0n, 'start');
237
+ const chunk = yield* file.readAlloc(64 * 1024);
238
+ return { position, chunk };
239
+ }).pipe(Effect.scoped);
240
+ const bodyLimit = ByteSize.mebibytes(1);
241
+ const parsedLimit = ByteSize.fromString('1.5 MiB');
242
+ ```
243
+
244
+ `ByteSize.Input` string literals are canonical non-negative integers with recognized
245
+ units. Parse external strings or fractional quantities with `ByteSize.fromString`
246
+ (or the explicitly throwing `fromStringUnsafe` at a controlled boundary).
247
+ Do not convert large sizes/offsets to number and silently lose precision.
248
+ Buffer allocation lengths (`readAlloc`) and per-buffer read/write counts are
249
+ numbers. File stat sizes and streaming byte limits use ByteSize; seek uses bigint.
220
250
 
221
251
  ## Directory Operations
222
252
 
@@ -284,7 +314,7 @@ const getFileInfo = Effect.gen(function* () {
284
314
 
285
315
  yield* Console.log(`Type: ${info.type}`);
286
316
  // "File" | "Directory" | "SymbolicLink" | "BlockDevice" | "CharacterDevice" | "FIFO" | "Socket" | "Unknown"
287
- yield* Console.log(`Size: ${info.size}`); // FileSystem.Size (branded bigint)
317
+ yield* Console.log(`Size: ${info.size}`); // ByteSize.ByteSize
288
318
  yield* Console.log(`Modified: ${info.mtime}`); // Option<Date>
289
319
  yield* Console.log(`Accessed: ${info.atime}`); // Option<Date>
290
320
  yield* Console.log(`Created: ${info.birthtime}`); // Option<Date>
@@ -7,6 +7,21 @@ You are an Effect TypeScript expert specializing in the HttpApi module for build
7
7
 
8
8
  ## Effect Source Reference
9
9
 
10
+ Use `HttpApi.ParseOptions` annotations at API, group, or endpoint scope to
11
+ configure client and server codecs. `HttpApiBuilder.handler` defines a reusable
12
+ endpoint callback with inferred request, success, error, and service types.
13
+ Generated clients and AtomHttpApi calls accept per-call `sseOptions`.
14
+ SSE IDs may be absent: model them with `Schema.optional(Schema.String)`.
15
+ With excess-property errors, include the `event` field (default `message`) and
16
+ any `id`, including inherited IDs. `Sse.decodeSchema` / `ChannelSchema.decode`
17
+ accept parse options directly.
18
+
19
+ HTTP `QUERY` endpoints are supported. OpenAPI 3.1 represents them through
20
+ `x-oai-additionalOperations`, requiring consumer support for that extension;
21
+ the generator also accepts OpenAPI 3.2's native `query` operation.
22
+ Generated multipart binary fields use `File | Blob` and dedicated `*Multipart`
23
+ component exports. Keep generated imports aligned with the chosen transport.
24
+
10
25
  The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Browse and read files there directly to look up APIs, types, and implementations.
11
26
 
12
27
  Key reference files:
@@ -1128,7 +1143,7 @@ The `layerClient` second argument can also be an `Effect` returning the middlewa
1128
1143
  const AuthorizationClient = HttpApiMiddleware.layerClient(
1129
1144
  Authorization,
1130
1145
  Effect.gen(function* () {
1131
- const token = yield* Config.redacted('API_TOKEN');
1146
+ const token = yield* Config.Redacted('API_TOKEN');
1132
1147
  return ({ next, request }) =>
1133
1148
  next(
1134
1149
  HttpClientRequest.bearerToken(request, Redacted.value(token))
@@ -1170,7 +1185,7 @@ Top-level group endpoints are at the root: `buildUrl.health()`. With `disableCod
1170
1185
 
1171
1186
  ## OpenAPI Documentation
1172
1187
 
1173
- As of rc.112 built-in OpenAPI documentation responses are generated lazily on
1188
+ Built-in OpenAPI documentation responses are generated lazily on
1174
1189
  the first request, rather than while the route layer is built. A generation
1175
1190
  defect is not permanently cached: a later request retries generation. Include a
1176
1191
  documentation-route request in integration checks if generation must be verified;
@@ -9,6 +9,15 @@ In v4 there is no `@effect/platform` package — the HTTP client lives in the `e
9
9
 
10
10
  ## Effect Source Reference
11
11
 
12
+ Client recovery through `HttpClient.catch` must return an `HttpClientResponse`.
13
+ For a different success type, recover on the Effect returned by `execute`.
14
+ `response.url` includes query parameters, excludes the hash, and reflects the
15
+ final URL after redirects. Response schema decoders honor supplied parse options.
16
+ Use `Schema.Cookie`, `Schema.Cookies`, `Schema.Headers`, and `Schema.UrlParams`
17
+ for HTTP value schemas. Import Undici-specific APIs from
18
+ `@effect/platform-node/Undici`; the transport layer owns their initialization.
19
+ HTTP `QUERY` is supported; configure CORS `allowedMethods` explicitly if needed.
20
+
12
21
  The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — these modules are `unstable` and change between betas.
13
22
 
14
23
  Key files:
@@ -42,7 +51,7 @@ An `HttpClient.With<E, R>` is a pair of functions — `preprocess` (request →
42
51
  - Runtime application and provider integrations use `HttpClient`; do not call raw `fetch` from business or provider code.
43
52
  - A raw `fetch` call is permitted only in an explicitly named low-level platform adapter that owns transport interop and documents why an Effect transport cannot be used. Lift it with `Effect.tryPromise`, pass the supplied `AbortSignal` to fetch, and do not let `Request`, `Response`, rejected promises, or untyped payloads escape that adapter.
44
53
  - Give each upstream adapter a named service and named effects that own request construction, authentication, execution, status classification, schema decoding, and error mapping.
45
- - Read credentials with `Config.redacted` and attach them in a configured client transform; never pass raw secret strings through business workflows.
54
+ - Read credentials with `Config.Redacted` and attach them in a configured client transform; never pass raw secret strings through business workflows.
46
55
  - Classify status before decoding a success schema. Non-2xx error bodies often have a different shape and must not be decoded as successful payloads.
47
56
  - Decode external response data with `HttpClientResponse.schemaBodyJson`, `schemaJson`, or another `Schema` decoder. A successful JSON parse is not validation.
48
57
  - Preserve bounded diagnostic evidence such as provider request IDs, status, error codes, and retry metadata. Redact credentials, authorization headers, query secrets, private payload fields, and full bodies before logging or storing evidence.
@@ -233,7 +242,7 @@ request.pipe(
233
242
 
234
243
  `UrlParams.Input` accepts records, iterables of `[key, value]` tuples, or `URLSearchParams`. Values may be `string | number | bigint | boolean | null | undefined`; `undefined` entries are **skipped** (great for optional params), arrays produce repeated keys, and nested records render with bracket notation (`filter[name]=x`).
235
244
 
236
- For standalone `UrlParams` values (e.g. `response.urlParamsBody`) the module mirrors these combinators: `UrlParams.getFirst`/`getAll`, `set`/`append`/`setAll`/`appendAll`, `toRecord`. In schema pipelines, `UrlParams.schemaRecord` decodes params into a record (`schemaBodyUrlParams` wraps it) and `UrlParams.schemaJsonField(name, { reviver })` parses one field's value as JSON with an optional `JSON.parse` reviver.
245
+ For standalone `UrlParams` values (e.g. `response.urlParamsBody`) the module mirrors these combinators: `UrlParams.getFirst`/`getAll`, `set`/`append`/`setAll`/`appendAll`, `toRecord`. Schema values and record/JSON-field codecs live in `effect/Schema`; use `Schema.RecordFromUrlParams` and `Schema.JsonFromUrlParamsField(name, options?)`.
237
246
 
238
247
  ### Headers and auth
239
248
 
@@ -197,14 +197,14 @@ const byteStream = request.stream; // Stream<Uint8Array, HttpServerError> (singl
197
197
 
198
198
  Cap accepted body sizes with the `MaxBodySize` reference (re-exported from `HttpIncomingMessage`, default `undefined` = unlimited):
199
199
 
200
- On Node in rc.112, `remoteAddress` returns `Option.none()` after Node has cleared
200
+ On Node, `remoteAddress` returns `Option.none()` after Node has cleared
201
201
  the incoming message's socket. Preserve absence; do not dereference the native
202
202
  socket after request cleanup. The same behavior applies to Node client responses.
203
203
 
204
204
  ```ts
205
205
  import { FileSystem } from 'effect';
206
206
 
207
- someEffect.pipe(Effect.provideService(HttpServerRequest.MaxBodySize, FileSystem.Size(1024 * 1024)));
207
+ someEffect.pipe(Effect.provideService(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1)));
208
208
  ```
209
209
 
210
210
  ### Schema-validated decoding
@@ -624,7 +624,7 @@ Layer.launch(Main).pipe(BunRuntime.runMain);
624
624
 
625
625
  ```ts
626
626
  const server = yield* HttpServer.HttpServer;
627
- server.address; // { _tag: 'TcpAddress', hostname, port } | { _tag: 'UnixAddress', path }
627
+ server.address; // NetAddress.SocketAddress; narrow to InetAddress before reading port
628
628
  HttpServer.formatAddress(server.address); // 'http://0.0.0.0:3000'
629
629
  yield* HttpServer.logAddress; // log it
630
630
  someServerLayer.pipe(HttpServer.withLogAddress); // log on startup
@@ -654,7 +654,7 @@ yield* HttpServer.serveEffect(httpEffect);
654
654
 
655
655
  ## 8. WebSocket Upgrades
656
656
 
657
- In `@effect/platform-bun` rc.112, outgoing WebSocket messages are compressed when
657
+ In `@effect/platform-bun`, outgoing WebSocket messages are compressed when
658
658
  per-message deflate is configured **and negotiated**. The server option
659
659
  `websocket.compressionThreshold` sets the minimum byte size (default `1024`);
660
660
  smaller messages stay uncompressed. Configure it alongside
@@ -667,16 +667,18 @@ application payloads. See `packages/platform/bun/src/BunHttpServer.ts`.
667
667
  const WsRoute = HttpRouter.add('GET', '/ws', Effect.gen(function* () {
668
668
  const request = yield* HttpServerRequest.HttpServerRequest;
669
669
  const socket = yield* request.upgrade; // Effect<Socket.Socket, HttpServerError>
670
- const write = yield* socket.writer; // scoped — the request Scope keeps it alive
671
-
672
- // runs until the client disconnects; the handler receives each message
673
- yield* socket.runString((message) => write(`echo: ${message}`));
674
-
675
- return HttpServerResponse.empty(); // sent when the socket session ends
670
+ const writer = yield* socket.writer;
671
+ const pull = yield* Socket.readerString(socket);
672
+ yield* pull.pipe(
673
+ Effect.flatMap((batch) => Effect.forEach(batch, (message) => writer.write(`echo: ${message}`))),
674
+ Effect.forever,
675
+ Effect.catchReason('SocketError', 'SocketCloseError', () => Effect.void)
676
+ );
677
+ return HttpServerResponse.empty(); // the adapter does not write HTTP bytes after upgrade
676
678
  }));
677
679
  ```
678
680
 
679
- - `socket.run(handler)` for binary (`Uint8Array`) messages, `runString` for text, `runRaw` for both.
681
+ - Acquire `socket.reader` for raw batches or `Socket.readerBytes` / `readerString` for converted pulls. Every close is a typed failure; handle closure at the protocol boundary.
680
682
  - `HttpServerRequest.upgradeChannel()` exposes the socket as a `Channel` for pipeline-style use.
681
683
  - `request.upgrade` fails with `HttpServerError` (`RequestParseError` reason) when the request is not upgradeable — e.g. in plain web-handler adapters that lack upgrade support.
682
684
  - For socket combinators, close events, and client sockets see the `effect-socket` skill.
@@ -685,6 +687,21 @@ const WsRoute = HttpRouter.add('GET', '/ws', Effect.gen(function* () {
685
687
 
686
688
  ## 9. Static Files (`HttpStaticServer`)
687
689
 
690
+ `HttpServerResponse.file` calculates length from the file and requested range;
691
+ it has no `contentLength` override. Explicit content types/headers take priority;
692
+ web files then use nonempty `File.type`, then extension inference. Ranges clamp
693
+ to EOF and retain exact bigint offsets. HEAD and statuses 204/205/304 omit the
694
+ body and finalize request resources without starting omitted Effect streams.
695
+
696
+ `HttpRouter.toWebHandler` and the layer-based `HttpEffect` web handlers start
697
+ building their layers when created. Construction failures reject requests with
698
+ the build error. Own and dispose that handler lifetime explicitly.
699
+
700
+ Bound server addresses use `NetAddress.SocketAddress`. Use `NetAddress.formatIp`
701
+ for numeric IP display, `formatHost` for host/port socket APIs, and the server URL
702
+ helpers for IPv6-safe URLs. Bun/Deno listener layers can fail with `ServeError`
703
+ when address conversion fails.
704
+
688
705
  ```ts
689
706
  const StaticFiles = HttpStaticServer.layer({
690
707
  root: './public',
@@ -424,7 +424,7 @@ export const DatabaseTest = Layer.succeed(
424
424
 
425
425
  // Use in application
426
426
  const program = Effect.gen(function* () {
427
- const nodeEnv = yield* Config.string('NODE_ENV').pipe(
427
+ const nodeEnv = yield* Config.String('NODE_ENV').pipe(
428
428
  Config.withDefault('production')
429
429
  );
430
430
  yield* myProgram.pipe(
@@ -463,6 +463,10 @@ const program = Effect.all([
463
463
 
464
464
  ## Error Handling in Layers
465
465
 
466
+ `Layer.tapError` and `Layer.tapCause` observers must accept the complete source
467
+ error type. Preloading a LayerMap does not make future resource acquisition
468
+ infallible; its accessors retain the resource error channel.
469
+
466
470
  Handle construction errors:
467
471
 
468
472
  ```typescript
@@ -577,7 +581,7 @@ const layer = Layer.effect(Settings, Effect.gen(function* () {
577
581
  For refresh use `Effect.cachedInvalidateWithTTL(work, Duration.infinity)` and
578
582
  expose the returned invalidation effect. For keyed retention use `Cache`,
579
583
  `ScopedCache`, `RcMap`, or `LayerMap` according to resource lifetime (see
580
- `effect-cache`). `LayerMap.contextEffectOption` in rc.112 atomically retains an
584
+ `effect-cache`). `LayerMap.contextEffectOption` atomically retains an
581
585
  already-cached layer context without allocating a missing key.
582
586
 
583
587
  When the requirement is a restartable worker, latest-wins scheduling, or queued
@@ -53,14 +53,15 @@ Layer.mergeAll(
53
53
  )
54
54
  ```
55
55
 
56
- Every server runner requires a non-empty `protocols` option. Put the preferred fallback revision first; an exact initialization offer is selected when present, otherwise the first adapter is used where the transport permits fallback. In rc.112, `initialize` negotiates from its body even if the client sends an unsupported default `MCP-Protocol-Version` header. The header is checked only on subsequent requests; unsupported explicit versions there still return `400`. Do not reject the initialization request in custom middleware before body negotiation.
56
+ Every server runner requires a non-empty `protocols` option. Put the preferred fallback revision first; an exact initialization offer is selected when present, otherwise the first adapter is used where the transport permits fallback. `initialize` negotiates from its body even when the client sends an unsupported default protocol header. Subsequent requests validate `MCP-Protocol-Version`; unsupported explicit versions return `400`. Let initialization reach body negotiation.
57
57
 
58
58
  ## Protocol Revisions
59
59
 
60
- Effect ships four dated adapters:
60
+ Declare the protocol adapters the server supports:
61
61
 
62
62
  ```typescript
63
63
  const protocols = [
64
+ McpProtocol.v2026_07_28,
64
65
  McpProtocol.v2025_11_25,
65
66
  McpProtocol.v2025_06_18,
66
67
  McpProtocol.v2025_03_26,
@@ -71,6 +72,7 @@ const protocols = [
71
72
  - `2024-11-05` and `2025-03-26` are compatibility revisions.
72
73
  - `2025-06-18` supports form elicitation.
73
74
  - `2025-11-25` adds sampling with tools, independently advertised form/URL elicitation modes, descriptor icons, and elicitation-complete notifications.
75
+ - `2026-07-28` is available as `McpProtocol.v2026_07_28`.
74
76
  - Duplicate versions or an empty protocol declaration fail layer construction with `Cause.IllegalArgumentError`.
75
77
  - `v2024_11_05` over `layerHttp` uses Effect's single-endpoint Streamable HTTP compatibility transport. It does not recreate the historical two-endpoint HTTP+SSE transport, GET SSE, event resumption, session expiry, or client session termination.
76
78
 
@@ -78,6 +80,18 @@ Each part layer has type `Layer.Layer<never, never, ...>` — they register them
78
80
 
79
81
  ## Tools and Toolkit
80
82
 
83
+ Server options accept `instructions` for initialization/discovery. Prompt
84
+ registration accepts `title`; callbacks receive decoded prompt parameters.
85
+
86
+ `Tool.Strict` controls MCP input schema generation and argument validation.
87
+ Strict dynamic tools require Effect schemas; raw JSON Schema is rejected at
88
+ registration. Non-strict tools may use identified input schemas. Invalid arguments
89
+ produce `InvalidParams` before protocol 2025-11-25 and `isError: true` on newer
90
+ protocols. Declared handler failures return `isError: true` without
91
+ `structuredContent`; only JSON objects are valid structured content.
92
+ Declared failures are distinct from internal diagnostics. Defects and encoding
93
+ failures are logged/reported while client-facing messages stay generic.
94
+
81
95
  ### Defining Tools
82
96
 
83
97
  Tools are defined with `Tool.make` specifying a name, description, parameter schemas, and success schema:
@@ -362,7 +376,9 @@ Layer.mergeAll(/* parts */).pipe(
362
376
  );
363
377
  ```
364
378
 
365
- **Critical**: When using stdio transport, logs MUST go to stderr. Any stdout output interferes with protocol communication. Use `Logger.consolePretty({ stderr: true })` or `Logger.LogToStderr`.
379
+ For stdio transport, route logs to stderr with `Logger.LogToStderr`. Stdout is
380
+ reserved for protocol messages. `Logger.consolePretty` controls formatting;
381
+ the context reference controls its destination.
366
382
 
367
383
  ### HTTP Transport
368
384
 
@@ -525,7 +541,8 @@ const ServerLayer = Layer.mergeAll(
525
541
  })
526
542
  ),
527
543
  Layer.provide(NodeStdio.layer),
528
- Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })]))
544
+ Layer.provide(Logger.layer([Logger.consolePretty()])),
545
+ Layer.provide(Layer.succeed(Logger.LogToStderr)(true))
529
546
  );
530
547
 
531
548
  Layer.launch(ServerLayer).pipe(NodeRuntime.runMain);
@@ -207,7 +207,7 @@ import { Config, Effect, Layer, Logger } from 'effect';
207
207
 
208
208
  const LoggerLayer = Layer.unwrap(
209
209
  Effect.gen(function* () {
210
- const env = yield* Config.string('NODE_ENV').pipe(
210
+ const env = yield* Config.String('NODE_ENV').pipe(
211
211
  Config.withDefault('development')
212
212
  );
213
213
  if (env === 'production') {
@@ -652,7 +652,7 @@ const ProdObservability = Layer.mergeAll(
652
652
 
653
653
  const ObservabilityLayer = Layer.unwrap(
654
654
  Effect.gen(function* () {
655
- const env = yield* Config.string('NODE_ENV').pipe(
655
+ const env = yield* Config.String('NODE_ENV').pipe(
656
656
  Config.withDefault('development')
657
657
  );
658
658
  return env === 'production' ? ProdObservability : DevObservability;
@@ -328,7 +328,7 @@ const atKey = (key: string) =>
328
328
  );
329
329
  ```
330
330
 
331
- Since beta.105, all fallible optic operations use `SchemaIssue.Issue`, not `string`. Custom `makePrism` and `makeOptional` implementations must return structured issues. Issues do not format themselves through `toString`; use `SchemaIssue.makeFormatterDefault()` when a human-readable message is needed:
331
+ Fallible optic operations use `SchemaIssue.Issue`. Custom `makePrism` and `makeOptional` implementations return structured issues. Use `SchemaIssue.makeFormatterDefault()` for human-readable messages:
332
332
 
333
333
  ```ts
334
334
  const formatIssue = SchemaIssue.makeFormatterDefault();