opencode-effect-enforcer 0.2.8 → 0.4.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 (74) hide show
  1. package/README.md +6 -5
  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 +6 -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-decision-model/SKILL.md +301 -0
  28. package/skills/effect-ai-decision-model/openrouter.md +70 -0
  29. package/skills/effect-ai-language-model/SKILL.md +53 -21
  30. package/skills/effect-ai-prompt/SKILL.md +25 -14
  31. package/skills/effect-ai-provider/SKILL.md +53 -22
  32. package/skills/effect-ai-streaming/SKILL.md +27 -12
  33. package/skills/effect-ai-tool/SKILL.md +37 -28
  34. package/skills/effect-atom-rpc/SKILL.md +57 -36
  35. package/skills/effect-atom-state/SKILL.md +57 -19
  36. package/skills/effect-batching/SKILL.md +5 -3
  37. package/skills/effect-cache/SKILL.md +19 -7
  38. package/skills/effect-cli/SKILL.md +17 -8
  39. package/skills/effect-command-executor/SKILL.md +115 -64
  40. package/skills/effect-concurrency-testing/SKILL.md +26 -6
  41. package/skills/effect-config/SKILL.md +53 -2
  42. package/skills/effect-context-witness/SKILL.md +6 -6
  43. package/skills/effect-domain-modeling/SKILL.md +8 -1
  44. package/skills/effect-error-handling/SKILL.md +15 -2
  45. package/skills/effect-fiber/SKILL.md +20 -25
  46. package/skills/effect-filesystem/SKILL.md +69 -57
  47. package/skills/effect-http-api/SKILL.md +72 -22
  48. package/skills/effect-http-client/SKILL.md +25 -21
  49. package/skills/effect-http-server/SKILL.md +51 -21
  50. package/skills/effect-incremental-migration/SKILL.md +17 -8
  51. package/skills/effect-layer-design/SKILL.md +8 -0
  52. package/skills/effect-managed-runtime/SKILL.md +6 -0
  53. package/skills/effect-mcp-server/SKILL.md +64 -24
  54. package/skills/effect-observability/SKILL.md +61 -15
  55. package/skills/effect-parallelization/SKILL.md +24 -7
  56. package/skills/effect-path/SKILL.md +8 -2
  57. package/skills/effect-platform-abstraction/SKILL.md +88 -66
  58. package/skills/effect-platform-layers/SKILL.md +68 -67
  59. package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
  60. package/skills/effect-react-composition/SKILL.md +19 -6
  61. package/skills/effect-rpc-api/SKILL.md +24 -24
  62. package/skills/effect-rpc-client/SKILL.md +33 -28
  63. package/skills/effect-rpc-cluster/SKILL.md +122 -78
  64. package/skills/effect-rpc-server/SKILL.md +56 -20
  65. package/skills/effect-scheduling/SKILL.md +29 -1
  66. package/skills/effect-schema-composition/SKILL.md +31 -13
  67. package/skills/effect-schema-v4/SKILL.md +94 -10
  68. package/skills/effect-scope/SKILL.md +13 -5
  69. package/skills/effect-service-implementation/SKILL.md +1 -1
  70. package/skills/effect-socket/SKILL.md +52 -8
  71. package/skills/effect-sql/SKILL.md +67 -33
  72. package/skills/effect-stream/SKILL.md +50 -5
  73. package/skills/effect-testing/SKILL.md +91 -2
  74. package/skills/effect-workflow/SKILL.md +76 -39
@@ -16,12 +16,15 @@ This skill provides patterns for testing Effect's concurrency primitives: fibers
16
16
  | Simple fiber yield | `Effect.yieldNow` |
17
17
  | Wait for subscriber ready | `Deferred.make()` + `Deferred.await` |
18
18
  | Wait for stream element | `Latch.make()` + `Stream.tap(() => latch.open)` |
19
- | Passive subscription registration | explicit readiness signal if possible; otherwise a tiny one-tick yield/sleep fallback |
19
+ | Passive subscription registration | acquire `PubSub.subscribe` before publishing, or expose a readiness signal |
20
20
  | Time-dependent behavior | `TestClock.adjust` |
21
21
  | Verify events published | `PubSub.subscribe` + `PubSub.takeUpTo` |
22
22
  | Check fiber status | `fiber.pollUnsafe()` |
23
23
 
24
- For `Stream.fromPubSub` subscription registration, prefer an explicit readiness signal when you control the stream. If the API offers no readiness hook and you only need registration to settle before publishing, a tiny `Effect.yieldNow()` or very short sleep is an acceptable last resort. Avoid broad polling or arbitrary delays.
24
+ For `Stream.fromPubSub` registration, signal readiness after acquiring the actual
25
+ subscription, not merely before starting the stream. A scheduler yield is not a
26
+ general readiness guarantee; a sleep under `it.effect` also needs `TestClock`
27
+ advancement. Prefer a subscription-first test seam to arbitrary delays.
25
28
 
26
29
  ## Fiber Coordination Patterns
27
30
 
@@ -39,7 +42,7 @@ it.effect('fiber polling with yieldNow', () =>
39
42
 
40
43
  const fiber = yield* latch.await.pipe(Effect.forkChild);
41
44
 
42
- yield* Effect.yieldNow();
45
+ yield* Effect.yieldNow;
43
46
 
44
47
  expect(fiber.pollUnsafe()).toBeUndefined();
45
48
 
@@ -67,7 +70,7 @@ it.effect('latch coordination', () =>
67
70
  return 'completed';
68
71
  }).pipe(Effect.forkChild);
69
72
 
70
- yield* Effect.yieldNow();
73
+ yield* Effect.yieldNow;
71
74
  expect(fiber.pollUnsafe()).toBeUndefined();
72
75
 
73
76
  yield* latch.open;
@@ -436,6 +439,23 @@ it.effect('should run finalizers', () =>
436
439
 
437
440
  ## Interruption Testing
438
441
 
442
+ Test cancellation at the resource-owning boundary, using a `Deferred` to prove
443
+ work has started before interrupting it:
444
+
445
+ - Shared `ScopedCache` lookup: cancel one of two waiters and let the other
446
+ finish; then cancel the last waiter on another pending lookup and verify its
447
+ resource finalizer ran and a later `get` reacquires.
448
+ - `race` / `raceFirst` / iterable variants: assert loser cleanup is complete when
449
+ the race returns, including cancellation while contenders are starting.
450
+ - `Scope.close`: gate a finalizer, interrupt the closing fiber, release the gate,
451
+ and verify remaining finalizers still run.
452
+ - `ManagedRuntime.dispose`: verify request cleanup can use layer services before
453
+ those services are released.
454
+ - Queue batches: offer fewer than `takeN` requests, verify the taker remains
455
+ pending, then `end` or `fail` and assert the short final batch precedes the
456
+ terminal outcome. For PubSub, test the `end` value with `take`, not `takeUpTo`,
457
+ including a late subscriber and a full dropping buffer.
458
+
439
459
  ### Testing Fiber Interruption
440
460
 
441
461
  ```typescript
@@ -518,7 +538,7 @@ Effect.gen(function* () {
518
538
  // GOOD - Use yieldNow for simple yielding
519
539
  Effect.gen(function* () {
520
540
  const fiber = yield* someEffect.pipe(Effect.forkChild);
521
- yield* Effect.yieldNow();
541
+ yield* Effect.yieldNow;
522
542
  yield* Fiber.join(fiber);
523
543
  });
524
544
  ```
@@ -538,7 +558,7 @@ while (fiber.pollUnsafe() === undefined) {
538
558
  // GOOD - Yield between polls or use Fiber.await
539
559
  Effect.gen(function* () {
540
560
  while (fiber.pollUnsafe() === undefined) {
541
- yield* Effect.yieldNow();
561
+ yield* Effect.yieldNow;
542
562
  }
543
563
  });
544
564
 
@@ -89,9 +89,36 @@ const host = Config.String('HOST').pipe(
89
89
  );
90
90
  ```
91
91
 
92
+ The fallback result replaces the original failure. If the fallback is absent,
93
+ an outer `withDefault` or `option` can recover it even when the primary input
94
+ was invalid. If the fallback fails, its error propagates. `Config.fail(error)`
95
+ has type `Config<never>` and does not widen a composed result to `unknown`.
96
+
97
+ ### `Config.flatMap` — Select a Dependent Config
98
+
99
+ Use `map` for a pure value transformation, `mapEffect` for a transformation that
100
+ may fail with `ConfigError`, and `flatMap` when the parsed value selects another
101
+ `Config`. The selected config uses the same provider and nesting prefix; the
102
+ callback runs only after successful resolution of the first config.
103
+
104
+ <!-- typecheck -->
105
+ ```ts
106
+ import { Config, ConfigProvider } from 'effect';
107
+
108
+ const host = Config.Literals(['development', 'production'], 'MODE').pipe(
109
+ Config.flatMap((mode) => Config.NonEmptyString(
110
+ mode === 'development' ? 'DEV_HOST' : 'PROD_HOST'
111
+ )),
112
+ Config.nested('APP')
113
+ );
114
+ const parsed = host.parse(ConfigProvider.fromUnknown({
115
+ APP: { MODE: 'production', PROD_HOST: 'api.example.com' }
116
+ }));
117
+ ```
118
+
92
119
  ### `Config.all` — Combine Multiple Configs
93
120
 
94
- Accepts a record or a tuple:
121
+ Accepts a record, tuple, or iterable of configs:
95
122
 
96
123
  ```ts
97
124
  // As a record
@@ -105,6 +132,28 @@ const appConfig = Config.all({
105
132
  const pair = Config.all([Config.String('a'), Config.Int('b')]);
106
133
  ```
107
134
 
135
+ A group is absent when any child is absent and no child fails, even if other
136
+ children have supplied values. `Config.option(group)` then returns `None`;
137
+ `Config.withDefault(group, fallback)` replaces the **whole group**. Put defaults
138
+ on individual children to retain supplied siblings. Validation and source
139
+ errors still propagate. Use `Config.schema(Schema.Struct(...))` when an existing
140
+ but incomplete object must fail validation rather than default.
141
+
142
+ <!-- typecheck -->
143
+ ```ts
144
+ import { Config, ConfigProvider, Effect } from 'effect';
145
+
146
+ const group = Config.all({
147
+ host: Config.String('HOST'),
148
+ port: Config.Int('PORT')
149
+ }).pipe(Config.withDefault({ host: 'localhost', port: 3000 }));
150
+
151
+ const result = Effect.runSync(group.parse(
152
+ ConfigProvider.fromUnknown({ HOST: 'supplied.example.com' })
153
+ ));
154
+ // { host: 'localhost', port: 3000 } — the supplied host is replaced too.
155
+ ```
156
+
108
157
  ### `Config.nested` — Scope Under a Prefix
109
158
 
110
159
  Prepends a path segment to every key the inner config reads. With environment variables, nesting uses `_` as separator.
@@ -346,7 +395,9 @@ const defaults = ConfigProvider.fromUnknown({
346
395
  const combined = ConfigProvider.orElse(envProvider, defaults);
347
396
  ```
348
397
 
349
- At the `Config` level, `Config.orElse` preserves evidence that the primary branch read provider input. Consequently, an outer `Config.withDefault` or `Config.option` does not hide a partially supplied `Config.all` group.
398
+ At the `Config` level, `Config.orElse` handles missing data and all `ConfigError`s;
399
+ its fallback determines whether an outer default or option can recover. Provider
400
+ fallback only chooses a source and does not perform that error recovery.
350
401
 
351
402
  ### `ConfigProvider.nested` — Prefix All Lookups
352
403
 
@@ -36,7 +36,7 @@ export const PaymentIntent = Schema.Struct({
36
36
  Field **removed from schema**, only injected in code:
37
37
 
38
38
  ```typescript
39
- import { Schema, Context, Effect, Logger } from 'effect';
39
+ import { Schema, Context, Effect } from 'effect';
40
40
 
41
41
  declare const generateId: () => string;
42
42
 
@@ -56,7 +56,7 @@ const createPaymentIntent = (amount: bigint) =>
56
56
 
57
57
  // Use serial in business logic, logging, etc.
58
58
  // but it's not part of the persisted data
59
- yield* Logger.info(`Creating payment intent ${serial}`);
59
+ yield* Effect.logInfo(`Creating payment intent ${serial}`);
60
60
 
61
61
  return PaymentIntent.make({ id: generateId(), amount });
62
62
  });
@@ -177,7 +177,7 @@ Good fits:
177
177
  Witnesses are trivial to provide:
178
178
 
179
179
  ```typescript
180
- import { Effect } from 'effect';
180
+ import { Context, Effect } from 'effect';
181
181
 
182
182
  declare const myProgram: Effect.Effect<unknown, never, Serial>;
183
183
  declare class Serial extends Context.Service<Serial, string>()('Serial') {}
@@ -188,7 +188,7 @@ const test = myProgram.pipe(Effect.provideService(Serial, 'test-serial-123'));
188
188
  Capabilities need implementation:
189
189
 
190
190
  ```typescript
191
- import { Effect } from 'effect';
191
+ import { Context, Effect } from 'effect';
192
192
 
193
193
  declare const myProgram: Effect.Effect<unknown, never, SerialService>;
194
194
  declare class SerialService extends Context.Service<
@@ -217,7 +217,7 @@ const test = myProgram.pipe(
217
217
  - **Yes** → Keep in schema
218
218
 
219
219
  ```typescript
220
- import { Schema, Context, Effect, Logger, Clock } from 'effect';
220
+ import { Schema, Context, Effect, Clock } from 'effect';
221
221
 
222
222
  declare const LineItem: Schema.Schema<any>;
223
223
  declare const generateId: () => string;
@@ -245,7 +245,7 @@ const createOrder = (items: Array<Schema.Schema.Type<typeof LineItem>>) =>
245
245
  const requestId = yield* RequestId; // For logging
246
246
  const timestamp = yield* Clock.currentTimeMillis; // For timestamp
247
247
 
248
- yield* Logger.info({
248
+ yield* Effect.logInfo({
249
249
  message: 'Creating order',
250
250
  correlationId, // Used for tracing
251
251
  requestId, // Used for logging
@@ -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