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
@@ -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.116**. In the Effect source reference, consult
16
+ Baseline: **Effect 4.0.0**. 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
 
@@ -38,6 +38,13 @@ when encoded values or service requirements matter; do not erase them with `any`
38
38
 
39
39
  ## Complete Model: Task Lifecycle
40
40
 
41
+ `Schema.brand` is a type-only distinction with no runtime AST metadata or extra
42
+ checks. Each call takes one concrete string literal (not a widened string,
43
+ union, or open template); compose brands with repeated calls. A brand does not
44
+ prove a domain invariant unless the underlying schema checks it. Representations
45
+ and generated schema code omit brands, so reapply them after rebuilding a model.
46
+ `Schema.fromBrand` preserves constructor checks and requires its sole brand key.
47
+
41
48
  This module demonstrates brands, class variants, exhaustive/partial matching,
42
49
  schema guards, equivalence, orders, dual helpers, and legal transitions.
43
50
 
@@ -89,7 +89,7 @@ Reference this for:
89
89
 
90
90
  ## Core Error Handling Philosophy
91
91
 
92
- Effect distinguishes between two types of failures:
92
+ Effect distinguishes expected errors from defects:
93
93
 
94
94
  1. **Expected Errors (Error Channel)** - Business logic failures that should be handled
95
95
  - Type-safe and tracked in the effect signature: `Effect<A, E, R>`
@@ -101,6 +101,12 @@ Effect distinguishes between two types of failures:
101
101
  - Result from programming mistakes (null refs, unhandled cases, assertions)
102
102
  - Usually should NOT be caught; use catchDefect only at boundaries
103
103
 
104
+ Interruption is cancellation, a separate `Cause` reason rather than a typed
105
+ business error. A cause can retain several reasons: if work fails and its
106
+ finalizer throws, both the original failure and the finalizer defect remain.
107
+ Inspect the full `Cause` at supervision boundaries rather than assuming one
108
+ reason or discarding cancellation during cleanup.
109
+
104
110
  ### Runtime Adapter Boundaries and Invariants
105
111
 
106
112
  Do not force every impossible or adapter-internal failure into a tagged error just to satisfy a blanket rule.
@@ -1047,6 +1053,13 @@ The `instanceof` guard prevents double-wrapping when an upstream operation alrea
1047
1053
 
1048
1054
  ## Error Recovery Patterns
1049
1055
 
1056
+ For per-item typed outcomes, `Effect.partition(items, work, { concurrency })`
1057
+ returns `[successes, failures]`, in that order. Both arrays preserve input order.
1058
+ `Effect.all(..., { mode: 'result' })` instead preserves the input structure with
1059
+ one `Result` per slot. Their `never` error channel means typed failures are
1060
+ collected; defects and interruption still propagate. Use `Effect.validate` when
1061
+ any item failure must fail the entire batch with all typed errors.
1062
+
1050
1063
  Retry timing and recurrence design belong in the dedicated effect-scheduling skill. This skill determines which failures are typed and recoverable; scheduling determines whether, when, and how often an idempotent operation is retried.
1051
1064
 
1052
1065
  ### Translate at Service Boundaries
@@ -1388,7 +1401,7 @@ Model ambiguous HTTP responses (where the body structure differs for success vs
1388
1401
 
1389
1402
  ```typescript
1390
1403
  import { Effect, Schema } from 'effect';
1391
- import { HttpClientResponse } from 'effect/unstable/http';
1404
+ import { HttpClientResponse } from 'effect/http';
1392
1405
 
1393
1406
  class TokenSuccess extends Schema.Class<TokenSuccess>('TokenSuccess')({
1394
1407
  access_token: AccessToken,
@@ -346,7 +346,7 @@ const detached = Effect.gen(function* () {
346
346
 
347
347
  Whichever lifetime you pick, **a forked fiber's failure is observed by nobody unless you arrange it**: `join`/`await` the fiber, supervise it via `FiberHandle`/`FiberMap`/`FiberSet` `join`, or attach `Effect.catchCause`/`Effect.onError` plus logging inside the forked effect. v4 removed `Effect.forkWithErrorHandler`, and the runtime does not log unhandled fiber failures.
348
348
 
349
- `Effect.awaitAllChildren` only waits for children forked while the wrapped effect runs — children that existed beforehand are not awaited.
349
+ `Effect.awaitAllChildren` only waits for children forked while the wrapped effect runs — children that existed beforehand are not awaited. The child wait respects the surrounding interruptibility. If interrupted while waiting after the wrapped effect failed, the resulting cause retains both the original failure and interruption; an enclosing uninterruptible region keeps the wait uninterruptible.
350
350
 
351
351
  `forkIn`/`forkScoped` register an interruption finalizer on the scope and remove it when the fiber completes on its own. Forking into an already-closed scope interrupts the new fiber immediately. The same applies to `Fiber.runIn(fiber, scope)`, which only registers the finalizer — it does not wait for the fiber.
352
352
 
@@ -380,7 +380,7 @@ const program = Effect.gen(function* () {
380
380
  }).pipe(Effect.scoped);
381
381
  ```
382
382
 
383
- `run` options: `{ onlyIfMissing?: boolean; propagateInterruption?: boolean }`. The type also accepts `startImmediately`, but it is a no-op — collection fibers are forked via `Effect.runForkWith` and always start synchronously (see "How collection fibers relate to the caller" in section 8).
383
+ `run` options: `{ onlyIfMissing?: boolean; propagateInterruption?: boolean; startImmediately?: boolean }`. Startup is immediate by default; `startImmediately: false` defers it (see "How collection fibers relate to the caller" in section 8).
384
384
 
385
385
  ### join vs awaitEmpty
386
386
 
@@ -462,7 +462,7 @@ const program = Effect.gen(function* () {
462
462
  }).pipe(Effect.scoped);
463
463
  ```
464
464
 
465
- `run` options are the same as FiberHandle's: `{ onlyIfMissing?, propagateInterruption? }` (`startImmediately` is in the type but is a no-op here too). `set`/`setUnsafe` install existing fibers under a key with `{ onlyIfMissing?, propagateInterruption? }`.
465
+ `run` options are the same as FiberHandle's: `{ onlyIfMissing?, propagateInterruption?, startImmediately? }`. Startup defaults to immediate; `false` defers it. `set`/`setUnsafe` install existing fibers under a key with `{ onlyIfMissing?, propagateInterruption? }`.
466
466
 
467
467
  Runtime helpers take the key as the first runner argument:
468
468
 
@@ -482,7 +482,7 @@ Closed-map behavior matches FiberHandle: `FiberMap.run` interrupts the caller; r
482
482
 
483
483
  ## 8. FiberSet — Grow-Only Fiber Collections
484
484
 
485
- `FiberSet<A, E>` tracks an unkeyed set of fibers. No replacement semantics — every `run`/`add` grows the set; completed fibers remove themselves; scope close interrupts all. Nothing limits admission: unbounded `run` calls on a production ingest path are a hazard — gate them with a `Semaphore` (see the bounded pattern under Key Patterns).
485
+ `FiberSet<A, E>` tracks an unkeyed set of fibers. No replacement semantics — every `run`/`add` grows the set; completed fibers remove themselves; scope close interrupts all. Nothing limits admission. For a finite work list, prefer bounded `Effect.forEach`; for ongoing ingestion, use a bounded queue with a fixed set of workers.
486
486
 
487
487
  ```ts
488
488
  const program = Effect.gen(function* () {
@@ -504,7 +504,7 @@ const program = Effect.gen(function* () {
504
504
  }).pipe(Effect.scoped);
505
505
  ```
506
506
 
507
- `run` options: `{ propagateInterruption? }` (no `onlyIfMissing` — there is no key; `startImmediately` is in the type but is a no-op). On a closed set, `FiberSet.run` returns an already-interrupted fiber (it does **not** interrupt the caller, unlike FiberHandle/FiberMap).
507
+ `run` options: `{ propagateInterruption?, startImmediately? }` (no `onlyIfMissing` — there is no key). Startup defaults to immediate; `false` defers it. On a closed set, `FiberSet.run` returns an already-interrupted fiber (it does **not** interrupt the caller, unlike FiberHandle/FiberMap).
508
508
 
509
509
  Runtime helpers mirror the others in shape. `FiberSet.runtime` and `runtimePromise` forward `propagateInterruption` when registering the managed fiber:
510
510
 
@@ -519,11 +519,14 @@ const runPromise2 = yield* FiberSet.makeRuntimePromise();
519
519
 
520
520
  ### How collection fibers relate to the caller
521
521
 
522
- Fibers forked via `FiberHandle/FiberMap/FiberSet.run` (and the runtime runners) are created with `Effect.runForkWith(parent.context)` — they are **root fibers carrying the caller's services, not children of the calling fiber**. Consequences:
522
+ Fibers forked via `FiberHandle/FiberMap/FiberSet.run` carry the caller's context but are **detached from its child lifetime** and owned by the collection. Consequences:
523
523
 
524
- - They start executing **immediately and synchronously** up to their first suspension (no lazy start, unlike `Effect.forkChild`).
524
+ - `run` starts them **immediately** by default; pass `{ startImmediately: false }` to defer startup. This differs from `Effect.forkChild`, whose default is deferred.
525
525
  - The calling fiber's completion does not interrupt them — only key replacement, `remove`/`clear`, or the collection's scope close does.
526
526
 
527
+ The captured `runtime()` runners instead use `Effect.runForkWith` at the callback
528
+ boundary. Those runners start synchronously and do not expose `startImmediately`.
529
+
527
530
  ---
528
531
 
529
532
  ## 9. Running Fibers at the Program Edge
@@ -647,27 +650,19 @@ const webhookProcessor = Effect.gen(function* () {
647
650
 
648
651
  To fail fast when any background task crashes, run the service's main loop against `FiberSet.join(tasks)` (it fails with the first non-interruption failure).
649
652
 
650
- ### Bounded background task set (FiberSet + Semaphore)
653
+ ### Bounded work lists
651
654
 
652
- `FiberSet` never applies backpressure on its own. Bound admission by taking a semaphore permit before `run` and releasing it when the task fiber settles:
655
+ `FiberSet` never applies backpressure on its own. For a work list, let
656
+ `Effect.forEach` own both admission and cleanup. A manual `Semaphore.take`
657
+ followed by `FiberSet.run` has an interruption gap, and a closed collection or a
658
+ fiber interrupted before startup may never install the intended release handler.
653
659
 
654
660
  ```ts
655
661
  const boundedProcessor = Effect.gen(function* () {
656
- const tasks = yield* FiberSet.make<void, WebhookError>();
657
- const permits = yield* Semaphore.make(16); // at most 16 in flight
658
-
659
- for (const event of events) {
660
- yield* Semaphore.take(permits, 1); // waits while 16 tasks are in flight
661
- yield* FiberSet.run(
662
- tasks,
663
- handleWebhook(event).pipe(
664
- // runs on success, failure, AND interruption — permits never leak
665
- Effect.ensuring(Semaphore.release(permits, 1))
666
- )
667
- );
668
- }
669
-
670
- yield* FiberSet.awaitEmpty(tasks);
662
+ yield* Effect.forEach(events, handleWebhook, {
663
+ concurrency: 16,
664
+ discard: true
665
+ });
671
666
  }).pipe(Effect.scoped);
672
667
  ```
673
668
 
@@ -723,7 +718,7 @@ const handoff = Effect.gen(function* () {
723
718
  9. **Expecting external interruption to fail `join`** — by default it doesn't. Pass `{ propagateInterruption: true }` to `run`/`set`/`add`; the collection's own internal interruptions (replacement, `clear`, scope close) never fail `join` either way.
724
719
  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
720
  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
- 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.
721
+ 12. **Assuming collection fibers are children of the caller** — they carry the caller's context but survive its completion; the collection owns their lifetime. `run` starts immediately by default and honors `startImmediately: false`; callback runtime runners start synchronously.
727
722
  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
723
  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
724
  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>()`.
@@ -7,6 +7,8 @@ description: Use Effect FileSystem for platform-abstract file I/O with Node.js/B
7
7
 
8
8
  Use `effect` FileSystem for platform-abstract file I/O. Stock layers are provided for Node.js and Bun; `@effect/platform-browser` does not provide a FileSystem layer, so browser code needs a custom/injected implementation.
9
9
 
10
+ Targets **Effect 4.0.0**. Match platform package versions to `effect`; verify APIs against the `effect@4.0.0` source tag rather than unreleased `main`.
11
+
10
12
  ## Basic Pattern
11
13
 
12
14
  ```typescript
@@ -195,6 +197,7 @@ const removeDir = Effect.gen(function* () {
195
197
 
196
198
  ### Open File Handle
197
199
 
200
+ <!-- typecheck -->
198
201
  ```typescript
199
202
  import { FileSystem } from 'effect';
200
203
  import { Effect } from 'effect';
@@ -265,36 +268,38 @@ const createDir = Effect.gen(function* () {
265
268
  });
266
269
  ```
267
270
 
268
- ### Make Temp Directory
271
+ ### Make Temp Directory with Explicit Ownership
269
272
 
273
+ <!-- typecheck -->
270
274
  ```typescript
271
- import { FileSystem } from 'effect';
272
- import { Effect } from 'effect';
275
+ import { Effect, FileSystem, Path } from 'effect';
273
276
 
274
277
  const useTempDir = Effect.gen(function* () {
275
278
  const fs = yield* FileSystem.FileSystem;
276
- const tempPath = yield* fs.makeTempDirectory();
277
-
278
- // Use tempPath
279
- yield* fs.writeFileString(`${tempPath}/temp-file.txt`, 'data');
280
-
281
- // Manual cleanup required
282
- yield* fs.remove(tempPath, { recursive: true });
279
+ const path = yield* Path.Path;
280
+ return yield* Effect.acquireUseRelease(
281
+ fs.makeTempDirectory(),
282
+ (tempPath) => fs.writeFileString(path.join(tempPath, 'temp-file.txt'), 'data'),
283
+ (tempPath) => fs.remove(tempPath, { recursive: true }).pipe(Effect.orDie)
284
+ );
283
285
  });
284
286
  ```
285
287
 
288
+ Release runs on success, failure, and interruption. Cleanup failure is surfaced as a defect here; prefer the built-in scoped variant for ordinary temporary work.
289
+
286
290
  ### Make Temp Directory (Scoped)
287
291
 
292
+ <!-- typecheck -->
288
293
  ```typescript
289
- import { FileSystem } from 'effect';
290
- import { Effect } from 'effect';
294
+ import { Effect, FileSystem, Path } from 'effect';
291
295
 
292
296
  const useScopedTempDir = Effect.gen(function* () {
293
297
  const fs = yield* FileSystem.FileSystem;
298
+ const path = yield* Path.Path;
294
299
  const tempPath = yield* fs.makeTempDirectoryScoped();
295
300
 
296
301
  // Use tempPath within scope
297
- yield* fs.writeFileString(`${tempPath}/temp-file.txt`, 'data');
302
+ yield* fs.writeFileString(path.join(tempPath, 'temp-file.txt'), 'data');
298
303
 
299
304
  // Automatically cleaned up when scope exits
300
305
  }).pipe(Effect.scoped);
@@ -304,6 +309,7 @@ const useScopedTempDir = Effect.gen(function* () {
304
309
 
305
310
  ### Stat (File Info)
306
311
 
312
+ <!-- typecheck -->
307
313
  ```typescript
308
314
  import { FileSystem } from 'effect';
309
315
  import { Effect, Console } from 'effect';
@@ -395,14 +401,15 @@ const changeOwner = Effect.gen(function* () {
395
401
 
396
402
  ### Update Times (utimes)
397
403
 
404
+ <!-- typecheck -->
398
405
  ```typescript
399
- import { FileSystem } from 'effect';
400
- import { Effect } from 'effect';
406
+ import { Clock, Effect, FileSystem } from 'effect';
401
407
 
402
408
  const updateTimes = Effect.gen(function* () {
403
409
  const fs = yield* FileSystem.FileSystem;
404
- const now = new Date();
405
- yield* fs.utimes('file.txt', now, now); // atime, mtime
410
+ const nowMillis = yield* Clock.currentTimeMillis;
411
+ // Numeric utimes inputs are epoch seconds, not milliseconds.
412
+ yield* fs.utimes('file.txt', nowMillis / 1000, nowMillis / 1000);
406
413
  });
407
414
  ```
408
415
 
@@ -436,6 +443,7 @@ const createSymlink = Effect.gen(function* () {
436
443
 
437
444
  ### Watch Files/Directories
438
445
 
446
+ <!-- typecheck -->
439
447
  ```typescript
440
448
  import { FileSystem } from 'effect';
441
449
  import { Effect, Stream, Console, pipe } from 'effect';
@@ -466,28 +474,31 @@ Directory watching is non-recursive by default. Pass `{ recursive: true }` to in
466
474
 
467
475
  ## Size Helpers
468
476
 
477
+ <!-- typecheck -->
469
478
  ```typescript
470
- import { FileSystem } from 'effect';
471
- import { Effect } from 'effect';
472
-
473
- const { Size, KiB, MiB, GiB, TiB, PiB } = FileSystem;
479
+ import { ByteSize, Effect, FileSystem } from 'effect';
480
+ import * as Schema from 'effect/Schema';
474
481
 
475
482
  // Create size values
476
- const oneKb = Size(1024);
477
- const tenKb = KiB(10);
478
- const oneMb = MiB(1);
479
- const fiveGb = GiB(5);
480
- const oneTb = TiB(1);
481
- const onePb = PiB(1);
483
+ const oneKb = ByteSize.bytes(1024);
484
+ const tenKb = ByteSize.kibibytes(10);
485
+ const oneMb = ByteSize.mebibytes(1);
486
+ const fiveGb = ByteSize.gibibytes(5);
487
+ const oneTb = ByteSize.tebibytes(1);
488
+ const onePb = ByteSize.pebibytes(1);
489
+
490
+ class FileTooLarge extends Schema.TaggedError<FileTooLarge>()('FileTooLarge', {
491
+ path: Schema.String
492
+ }) {}
482
493
 
483
494
  // Use with file operations
484
495
  const checkFileSize = Effect.gen(function* () {
485
496
  const fs = yield* FileSystem.FileSystem;
486
497
  const info = yield* fs.stat('large-file.bin');
487
498
 
488
- const maxSize = MiB(100);
499
+ const maxSize = ByteSize.mebibytes(100);
489
500
  if (info.size > maxSize) {
490
- yield* Effect.fail(new Error('File too large'));
501
+ return yield* Effect.fail(new FileTooLarge({ path: 'large-file.bin' }));
491
502
  }
492
503
  });
493
504
  ```
@@ -496,7 +507,7 @@ const checkFileSize = Effect.gen(function* () {
496
507
 
497
508
  ### SystemErrorTag Values
498
509
 
499
- FileSystem operations fail with `PlatformError` containing a `SystemErrorTag`:
510
+ FileSystem operations fail with `PlatformError`. Its `reason` is `BadArgument` or `SystemError`. In **4.0.0**, `SystemError._tag` is the normalized category below; match `error.reason._tag` directly (there is no `reason.tag` field):
500
511
 
501
512
  - `AlreadyExists` - File/directory already exists
502
513
  - `BadResource` - Invalid file descriptor or handle
@@ -510,8 +521,11 @@ FileSystem operations fail with `PlatformError` containing a `SystemErrorTag`:
510
521
  - `WouldBlock` - Operation would block
511
522
  - `WriteZero` - Write operation wrote zero bytes
512
523
 
524
+ For an unknown error at a boundary, `PlatformError.isPlatformError(value)` narrows it to the wrapper before inspecting `reason`.
525
+
513
526
  ### Error Handling Pattern
514
527
 
528
+ <!-- typecheck -->
515
529
  ```typescript
516
530
  import { FileSystem } from 'effect';
517
531
  import { Effect, pipe } from 'effect';
@@ -525,11 +539,7 @@ const readConfigWithFallback = pipe(
525
539
  if (error.reason._tag === 'NotFound') {
526
540
  return Effect.succeed('{}');
527
541
  }
528
- if (error.reason._tag === 'PermissionDenied') {
529
- return Effect.fail(
530
- new Error('Cannot read config: permission denied')
531
- );
532
- }
542
+ // Permission and other failures remain visible to the caller.
533
543
  return Effect.fail(error);
534
544
  })
535
545
  );
@@ -537,6 +547,7 @@ const readConfigWithFallback = pipe(
537
547
 
538
548
  ### Typed Error Recovery
539
549
 
550
+ <!-- typecheck -->
540
551
  ```typescript
541
552
  import { FileSystem } from 'effect';
542
553
  import { Effect, Schema, pipe } from 'effect';
@@ -548,50 +559,51 @@ class ConfigNotFound extends Schema.TaggedError<ConfigNotFound>()(
548
559
  }
549
560
  ) {}
550
561
 
551
- class ConfigInvalid extends Schema.TaggedError<ConfigInvalid>()(
552
- 'ConfigInvalid',
562
+ class ConfigReadFailed extends Schema.TaggedError<ConfigReadFailed>()(
563
+ 'ConfigReadFailed',
553
564
  {
554
565
  path: Schema.String,
555
566
  reason: Schema.String
556
567
  }
557
568
  ) {}
558
569
 
559
- const readConfig = (path: string) =>
560
- Effect.gen(function* () {
561
- const fs = yield* FileSystem.FileSystem;
570
+ const readConfig = Effect.fn('Config.read')(function* (path: string) {
571
+ const fs = yield* FileSystem.FileSystem;
562
572
 
563
- const content = yield* pipe(
564
- fs.readFileString(path),
565
- Effect.mapError((error) =>
566
- error.reason._tag === 'NotFound'
567
- ? new ConfigNotFound({ path })
568
- : new ConfigInvalid({ path, reason: error.message })
569
- )
570
- );
573
+ const content = yield* pipe(
574
+ fs.readFileString(path),
575
+ Effect.mapError((error) =>
576
+ error.reason._tag === 'NotFound'
577
+ ? new ConfigNotFound({ path })
578
+ : new ConfigReadFailed({ path, reason: error.message })
579
+ )
580
+ );
571
581
 
572
- return content;
573
- });
582
+ return content;
583
+ });
574
584
  ```
575
585
 
576
586
  ## Scoped Resources Pattern
577
587
 
588
+ <!-- typecheck -->
578
589
  ```typescript
579
- import { FileSystem } from 'effect';
580
- import { Effect } from 'effect';
590
+ import { Effect, FileSystem, Path } from 'effect';
591
+ import * as Str from 'effect/String';
581
592
 
582
593
  const processInTempDir = Effect.gen(function* () {
583
594
  const fs = yield* FileSystem.FileSystem;
595
+ const path = yield* Path.Path;
584
596
 
585
597
  // Create temp directory with automatic cleanup
586
598
  const tempDir = yield* fs.makeTempDirectoryScoped();
587
599
 
588
600
  // Do work in temp directory
589
- const inputPath = `${tempDir}/input.txt`;
590
- const outputPath = `${tempDir}/output.txt`;
601
+ const inputPath = path.join(tempDir, 'input.txt');
602
+ const outputPath = path.join(tempDir, 'output.txt');
591
603
 
592
604
  yield* fs.writeFileString(inputPath, 'data');
593
605
  const content = yield* fs.readFileString(inputPath);
594
- yield* fs.writeFileString(outputPath, content.toUpperCase());
606
+ yield* fs.writeFileString(outputPath, Str.toUpperCase(content));
595
607
 
596
608
  const result = yield* fs.readFileString(outputPath);
597
609
 
@@ -638,10 +650,10 @@ Effect.runPromise(runnable);
638
650
 
639
651
  - Import from `effect`
640
652
  - Use `yield* FileSystem.FileSystem` for service injection
641
- - Provide platform layer at entry point only
653
+ - Provide platform layers at entry points or runtime-facing adapter `defaultLayer` exports
642
654
  - Use scoped temp directories with `makeTempDirectoryScoped`
643
655
  - Handle `PlatformError` with `catchTag("PlatformError", ...)`
644
- - Use size helpers: `Size()`, `KiB()`, `MiB()`, `GiB()`, `TiB()`, `PiB()`
656
+ - Use `ByteSize.bytes`, `ByteSize.kibibytes`, `ByteSize.mebibytes`, and other ByteSize constructors; the old FileSystem size helpers are removed
645
657
  - Stream large files with `stream()` and `sink()`
646
658
 
647
659
  ## DON'T