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
@@ -5,13 +5,16 @@ description: Spawn and manage child processes using Effect's ChildProcess module
5
5
 
6
6
  # Child Process Execution with Effect v4
7
7
 
8
+ Targets **Effect 4.0.0**, checked against the `effect@4.0.0` source tag. Keep platform packages on the same version. The process API is marked `@stability unstable`, so check contracts again before a minor-version upgrade.
9
+
8
10
  ## Overview
9
11
 
10
- Node process-group cleanup waits for the leader and descendants. Without
12
+ On POSIX, Node process-group cleanup waits for the leader and descendants. Without
11
13
  `forceKillAfter`, cleanup waits up to one second without escalating. With it,
12
14
  the group receives SIGKILL at the deadline, then a final bounded wait. Native
13
15
  timers drive escalation even under TestClock. `exitCode` / `isRunning` describe
14
16
  the leader, not all descendants; do not use virtual time alone to prove OS cleanup.
17
+ On Windows, Node uses `taskkill` for tree termination and waits for the leader.
15
18
 
16
19
  The `ChildProcess` module provides type-safe, composable process execution with automatic resource cleanup via `Scope`. Commands are AST values — built first with `make` and `pipeTo`, then executed via the `ChildProcessSpawner` service.
17
20
 
@@ -23,12 +26,12 @@ The `ChildProcess` module provides type-safe, composable process execution with
23
26
  - Streaming output from long-running processes
24
27
  - Managing process lifecycles with scoped cleanup
25
28
 
26
- **Note:** This skill covers child process execution, NOT `@effect/cli` for building CLI applications.
29
+ **Note:** This skill covers child process execution; use `effect/cli` for building CLI applications.
27
30
 
28
31
  ## Import Pattern
29
32
 
30
33
  ```typescript
31
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
34
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
32
35
  ```
33
36
 
34
37
  Platform layer (Node.js):
@@ -41,8 +44,9 @@ import { NodeServices } from '@effect/platform-node';
41
44
 
42
45
  ### Template Literal Form
43
46
 
47
+ <!-- typecheck -->
44
48
  ```typescript
45
- import { ChildProcess } from 'effect/unstable/process';
49
+ import { ChildProcess } from 'effect/process';
46
50
 
47
51
  // Simple command — parsed by whitespace
48
52
  const cmd = ChildProcess.make`echo hello world`;
@@ -58,8 +62,9 @@ const cmd3 = ChildProcess.make`oxfmt ${files}`;
58
62
 
59
63
  ### Options + Template Literal Form
60
64
 
65
+ <!-- typecheck -->
61
66
  ```typescript
62
- import { ChildProcess } from 'effect/unstable/process';
67
+ import { ChildProcess } from 'effect/process';
63
68
 
64
69
  // Options object returns a tagged template function
65
70
  const cmd = ChildProcess.make({ cwd: '/tmp' })`ls -la`;
@@ -73,8 +78,9 @@ const cmd2 = ChildProcess.make({
73
78
 
74
79
  ### Array Form
75
80
 
81
+ <!-- typecheck -->
76
82
  ```typescript
77
- import { ChildProcess } from 'effect/unstable/process';
83
+ import { ChildProcess } from 'effect/process';
78
84
 
79
85
  // Explicit command + args array
80
86
  const cmd = ChildProcess.make('git', ['status'], {
@@ -99,8 +105,9 @@ const cmd3 = ChildProcess.make('node', {
99
105
 
100
106
  ### Command Options
101
107
 
108
+ <!-- typecheck -->
102
109
  ```typescript
103
- import { ChildProcess } from 'effect/unstable/process';
110
+ import { ChildProcess } from 'effect/process';
104
111
 
105
112
  const cmd = ChildProcess.make('node', ['script.js'], {
106
113
  // Working directory
@@ -141,8 +148,9 @@ The `fd3`/`fd4` names configure child-process stdio channels. Process handles ex
141
148
 
142
149
  ### Combinators
143
150
 
151
+ <!-- typecheck -->
144
152
  ```typescript
145
- import { ChildProcess } from 'effect/unstable/process';
153
+ import { ChildProcess } from 'effect/process';
146
154
 
147
155
  // Set cwd (applies to all commands in a pipeline)
148
156
  const cmd = ChildProcess.make`ls -la`.pipe(ChildProcess.setCwd('/tmp'));
@@ -165,7 +173,7 @@ Commands are `Effect` values: `yield*` on a command evaluates through its `Effec
165
173
 
166
174
  ```typescript
167
175
  import { Effect } from 'effect';
168
- import { ChildProcessSpawner } from 'effect/unstable/process';
176
+ import { ChildProcessSpawner } from 'effect/process';
169
177
 
170
178
  const program = Effect.gen(function* () {
171
179
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -175,9 +183,10 @@ const program = Effect.gen(function* () {
175
183
 
176
184
  ### Capture as String
177
185
 
186
+ <!-- typecheck -->
178
187
  ```typescript
179
188
  import { Effect, String } from 'effect';
180
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
189
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
181
190
 
182
191
  const program = Effect.gen(function* () {
183
192
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -190,39 +199,45 @@ const program = Effect.gen(function* () {
190
199
 
191
200
  ### Capture as Lines
192
201
 
202
+ <!-- typecheck -->
193
203
  ```typescript
194
204
  import { Effect } from 'effect';
195
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
205
+ import * as Arr from 'effect/Array';
206
+ import * as Str from 'effect/String';
207
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
196
208
 
197
209
  const program = Effect.gen(function* () {
198
210
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
199
211
  const files = yield* spawner.lines(
200
212
  ChildProcess.make('git', ['diff', '--name-only', 'main...HEAD'])
201
213
  );
202
- const tsFiles = files.filter((f) => f.endsWith('.ts'));
214
+ const tsFiles = Arr.filter(files, Str.endsWith('.ts'));
203
215
  });
204
216
  ```
205
217
 
206
218
  ### Progressive Output with Side Effects
207
219
 
220
+ <!-- typecheck -->
208
221
  ```typescript
209
222
  import { Effect, Stream } from 'effect';
210
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
223
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
211
224
 
212
225
  const program = Effect.gen(function* () {
213
226
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
214
- let output = '';
215
227
  const handle = yield* spawner.spawn(
216
228
  ChildProcess.make('bun', ['test'], {
217
229
  extendEnv: true,
218
230
  stdin: 'ignore'
219
231
  })
220
232
  );
221
- yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
222
- Effect.sync(() => {
223
- output += chunk;
224
- })
225
- ).pipe(Effect.forkScoped);
233
+ // Drain to EOF before leaving the scope, propagating read/progress failures.
234
+ const output = yield* handle.all.pipe(
235
+ Stream.decodeText(),
236
+ Stream.tap((chunk) => Effect.logDebug('Process output').pipe(
237
+ Effect.annotateLogs({ characters: chunk.length })
238
+ )),
239
+ Stream.mkString
240
+ );
226
241
  const exitCode = yield* handle.exitCode;
227
242
  return { output, exitCode };
228
243
  }).pipe(Effect.scoped);
@@ -230,29 +245,41 @@ const program = Effect.gen(function* () {
230
245
 
231
246
  Use this pattern when you need per-chunk side effects (progress reporting, streaming to UI) during command execution. `spawner.string`/`spawner.lines` cannot provide per-chunk callbacks.
232
247
 
248
+ Waiting for `handle.exitCode` alone does not drain output. If you fork a reader, join it before closing the scope and returning captured output. `Stream.mkString` buffers all text; for unbounded output, use `Stream.runForEach` without retaining it. Treat process output as potentially sensitive when choosing a progress callback.
249
+
233
250
  ### Get Exit Code
234
251
 
252
+ <!-- typecheck -->
235
253
  ```typescript
236
254
  import { Effect } from 'effect';
237
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
255
+ import * as Schema from 'effect/Schema';
256
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
257
+
258
+ class CommandFailed extends Schema.TaggedError<CommandFailed>()('CommandFailed', {
259
+ exitCode: Schema.Number
260
+ }) {}
238
261
 
239
262
  const program = Effect.gen(function* () {
240
263
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
241
264
  const exitCode = yield* spawner.exitCode(
242
- ChildProcess.make('test', ['-f', 'package.json'])
265
+ ChildProcess.make('test', ['-f', 'package.json'], {
266
+ stdin: 'ignore', stdout: 'ignore', stderr: 'ignore'
267
+ })
243
268
  );
244
269
  // exitCode: ExitCode (branded number)
245
270
  if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
246
- yield* Effect.fail(new Error('file not found'));
271
+ return yield* Effect.fail(new CommandFailed({ exitCode }));
247
272
  }
248
273
  });
249
274
  ```
250
275
 
276
+ `string`, `lines`, and stream helpers collect output; they do **not** reject a nonzero exit code. Use a scoped handle when success requires both captured output and a checked status. When using `exitCode` alone, inherit or ignore stdout/stderr so an unread pipe cannot fill and stall a noisy process. Output helpers default to stdout; pass `{ includeStderr: true }` to consume combined output, or configure stderr as `inherit`/`ignore` when only stdout is needed.
277
+
251
278
  ### Stream Output (Long-Running Processes)
252
279
 
253
280
  ```typescript
254
281
  import { Console, Effect, Stream } from 'effect';
255
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
282
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
256
283
 
257
284
  const program = Effect.gen(function* () {
258
285
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -273,9 +300,15 @@ const program = Effect.gen(function* () {
273
300
 
274
301
  Use `spawner.spawn` when you need the process handle for interactive control, streaming output while running, or checking exit codes.
275
302
 
303
+ <!-- typecheck -->
276
304
  ```typescript
277
305
  import { Console, Effect, Stream } from 'effect';
278
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
306
+ import * as Schema from 'effect/Schema';
307
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
308
+
309
+ class LintFailed extends Schema.TaggedError<LintFailed>()('LintFailed', {
310
+ exitCode: Schema.Number
311
+ }) {}
279
312
 
280
313
  const program = Effect.gen(function* () {
281
314
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -298,7 +331,7 @@ const program = Effect.gen(function* () {
298
331
  // Wait for exit
299
332
  const exitCode = yield* handle.exitCode;
300
333
  if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
301
- yield* Effect.fail(new Error(`lint failed: exit ${exitCode}`));
334
+ return yield* Effect.fail(new LintFailed({ exitCode }));
302
335
  }
303
336
  }).pipe(Effect.scoped); // <-- spawn requires Scope
304
337
  ```
@@ -310,7 +343,7 @@ const program = Effect.gen(function* () {
310
343
  | `pid` | `ProcessId` | Process identifier (branded number) |
311
344
  | `exitCode` | `Effect<ExitCode, PlatformError>` | Waits for exit, returns exit code |
312
345
  | `isRunning` | `Effect<boolean, PlatformError>` | Check if still running |
313
- | `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal; pass `{ forceKillAfter: '3 seconds' }` to ensure cleanup |
346
+ | `kill(options?)` | `Effect<void, PlatformError>` | Kill with signal; `forceKillAfter` bounds graceful waiting before escalation |
314
347
  | `stdin` | `Sink<void, Uint8Array, never, PlatformError>` | Write to process stdin |
315
348
  | `stdout` | `Stream<Uint8Array, PlatformError>` | Read process stdout |
316
349
  | `stderr` | `Stream<Uint8Array, PlatformError>` | Read process stderr |
@@ -320,9 +353,10 @@ const program = Effect.gen(function* () {
320
353
 
321
354
  ## Piping Commands
322
355
 
356
+ <!-- typecheck -->
323
357
  ```typescript
324
358
  import { Effect } from 'effect';
325
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
359
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
326
360
 
327
361
  const program = Effect.gen(function* () {
328
362
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -371,12 +405,18 @@ const program = Effect.gen(function* () {
371
405
  }).pipe(Effect.scoped, Effect.provide(NodeServices.layer));
372
406
  ```
373
407
 
374
- Or provide just the spawner:
408
+ Or compose the spawner with its FileSystem and Path dependencies:
375
409
 
376
410
  ```typescript
377
- import { NodeChildProcessSpawner } from '@effect/platform-node';
411
+ import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from '@effect/platform-node';
412
+ import { Effect, Layer } from 'effect';
413
+ import { ChildProcessSpawner } from 'effect/process';
378
414
 
379
- const program = myEffect.pipe(Effect.provide(NodeChildProcessSpawner.layer));
415
+ declare const myEffect: Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner>;
416
+ const SpawnerLayer = NodeChildProcessSpawner.layer.pipe(
417
+ Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
418
+ );
419
+ const program = myEffect.pipe(Effect.provide(SpawnerLayer));
380
420
  ```
381
421
 
382
422
  ## Complete Example: DevTools Service
@@ -392,7 +432,8 @@ import {
392
432
  Stream,
393
433
  String
394
434
  } from 'effect';
395
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
435
+ import * as Arr from 'effect/Array';
436
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
396
437
 
397
438
  class DevToolsError extends Schema.TaggedError<DevToolsError>()(
398
439
  'DevToolsError',
@@ -442,7 +483,7 @@ class DevTools extends Context.Service<
442
483
  .pipe(
443
484
  Effect.mapError((cause) => new DevToolsError({ cause }))
444
485
  );
445
- return files.filter((file) => file.endsWith('.ts'));
486
+ return Arr.filter(files, String.endsWith('.ts'));
446
487
  });
447
488
 
448
489
  const recentCommitSubjects = spawner
@@ -463,7 +504,7 @@ class DevTools extends Context.Service<
463
504
  const runLintFix = Effect.gen(function* () {
464
505
  const handle = yield* spawner
465
506
  .spawn(
466
- ChildProcess.make('bun', ['lint'], {
507
+ ChildProcess.make('bun', ['lint', '--fix'], {
467
508
  env: { FORCE_COLOR: '1' },
468
509
  extendEnv: true,
469
510
  stdin: 'ignore'
@@ -484,11 +525,9 @@ class DevTools extends Context.Service<
484
525
  Effect.mapError((cause) => new DevToolsError({ cause }))
485
526
  );
486
527
  if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
487
- return yield* new DevToolsError({
488
- cause: new Error(
489
- `bun lint failed with exit code ${exitCode}`
490
- )
491
- });
528
+ return yield* Effect.fail(new DevToolsError({
529
+ cause: { command: 'bun lint --fix', exitCode }
530
+ }));
492
531
  }
493
532
  }).pipe(Effect.scoped);
494
533
 
@@ -499,7 +538,9 @@ class DevTools extends Context.Service<
499
538
  runLintFix
500
539
  });
501
540
  })
502
- ).pipe(Layer.provide(NodeServices.layer));
541
+ );
542
+
543
+ static readonly defaultLayer = DevTools.layer.pipe(Layer.provide(NodeServices.layer));
503
544
  }
504
545
  ```
505
546
 
@@ -516,14 +557,14 @@ const program = Effect.gen(function* () {
516
557
  }).pipe(Effect.scoped);
517
558
  ```
518
559
 
519
- ### DON'T: Use `spawner.spawn` without scoping
560
+ ### DON'T: Run a scoped program without providing its Scope
520
561
 
521
562
  ```typescript
522
- // Process handle leaks — no scope to manage cleanup
563
+ // This definition is valid and retains Scope in its requirements.
523
564
  const program = Effect.gen(function* () {
524
565
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
525
566
  const handle = yield* spawner.spawn(ChildProcess.make`my-server`);
526
- // ❌ no Effect.scoped — process may leak
567
+ // The caller must own/provide Scope before running this program.
527
568
  });
528
569
  ```
529
570
 
@@ -571,22 +612,15 @@ class MyService extends Context.Service<
571
612
  >()('MyService') {}
572
613
  ```
573
614
 
574
- ### DO: Check `ExitCode` with the branded constructor
615
+ ### DO: Check the exit status explicitly
575
616
 
576
617
  ```typescript
577
618
  if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
578
- yield* Effect.fail(new Error(`command failed: ${exitCode}`));
619
+ return yield* Effect.fail(new CommandFailed({ exitCode }));
579
620
  }
580
621
  ```
581
622
 
582
- ### DON'T: Compare exit codes as raw numbers
583
-
584
- ```typescript
585
- // ❌ ExitCode is a branded type — direct number comparison may not work as expected
586
- if (exitCode !== 0) {
587
- /* ... */
588
- }
589
- ```
623
+ `ExitCode` is a branded **number**, not a boxed runtime value. Both `exitCode !== 0` and comparison with `ChildProcessSpawner.ExitCode(0)` work. The constructor is useful when supplying an ExitCode to a typed API; the brand does not change numeric comparison semantics.
590
624
 
591
625
  ### DO: Use `handle.all` for interleaved stdout+stderr
592
626
 
@@ -610,9 +644,10 @@ yield* Stream.merge(handle.stdout, handle.all).pipe(Stream.runCollect);
610
644
 
611
645
  Child process operations fail with `PlatformError`:
612
646
 
647
+ <!-- typecheck -->
613
648
  ```typescript
614
649
  import { Effect } from 'effect';
615
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
650
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
616
651
 
617
652
  const program = Effect.gen(function* () {
618
653
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
@@ -621,7 +656,9 @@ const program = Effect.gen(function* () {
621
656
  .string(ChildProcess.make`non-existent-command`)
622
657
  .pipe(
623
658
  Effect.catchTag('PlatformError', (error) =>
624
- Effect.succeed(`fallback: ${error.message}`)
659
+ error.reason._tag === 'NotFound'
660
+ ? Effect.succeed('Optional command is unavailable')
661
+ : Effect.fail(error)
625
662
  )
626
663
  );
627
664
  });
@@ -637,6 +674,7 @@ Keep process kill, timeout, and output-drain logic inside the process adapter th
637
674
 
638
675
  Use `Effect.callback` to convert an `AbortSignal` (or any event-driven API) into an Effect. The cleanup Effect removes the listener, and the already-aborted edge case is handled first:
639
676
 
677
+ <!-- typecheck -->
640
678
  ```typescript
641
679
  import { Effect } from 'effect';
642
680
 
@@ -651,16 +689,25 @@ const fromAbortSignal = (signal: AbortSignal) =>
651
689
 
652
690
  ### Abort / Timeout Multiplexing
653
691
 
654
- Combine `Effect.raceAll` with discriminated result types to handle exit, abort, and timeout in a single expression:
692
+ Combine `Effect.raceAll` with discriminated result types to handle successful exit, abort, and timeout in a single expression. `raceAll` waits for the first success, so turn exit observation into an `Exit` value to preserve a failed exit-code read rather than hiding it behind the timeout:
655
693
 
694
+ <!-- typecheck -->
656
695
  ```typescript
657
- import { Effect } from 'effect';
658
-
659
- const exit =
660
- yield*
661
- Effect.raceAll([
696
+ import { Duration, Effect } from 'effect';
697
+ import { ChildProcessSpawner } from 'effect/process';
698
+
699
+ declare const fromAbortSignal: (signal: AbortSignal) => Effect.Effect<void>;
700
+
701
+ // Call within the scope that owns the handle and its output reader.
702
+ const awaitOutcome = Effect.fn('Process.awaitOutcome')(function* (
703
+ handle: ChildProcessSpawner.ChildProcessHandle,
704
+ signal: AbortSignal,
705
+ timeout: Duration.Input
706
+ ) {
707
+ const exit = yield* Effect.raceAll([
662
708
  handle.exitCode.pipe(
663
- Effect.map((code) => ({ kind: 'exit' as const, code }))
709
+ Effect.exit,
710
+ Effect.map((result) => ({ kind: 'exit' as const, result }))
664
711
  ),
665
712
  fromAbortSignal(signal).pipe(
666
713
  Effect.map(() => ({ kind: 'abort' as const }))
@@ -669,11 +716,15 @@ const exit =
669
716
  Effect.map(() => ({ kind: 'timeout' as const }))
670
717
  )
671
718
  ]);
672
- if (exit.kind !== 'exit') {
673
- yield* handle.kill({ forceKillAfter: '3 seconds' });
674
- }
719
+ if (exit.kind !== 'exit') {
720
+ yield* handle.kill({ forceKillAfter: '3 seconds' });
721
+ }
722
+ return exit;
723
+ });
675
724
  ```
676
725
 
726
+ Handle `exit.result` with `Exit.match` when `kind === 'exit'`; it includes observation failures. This race only chooses the control outcome: drain/join any output reader separately within the owning scope before returning captured output.
727
+
677
728
  ## Related Skills
678
729
 
679
730
  - **effect-platform-abstraction**: FileSystem, Path, and other platform services
@@ -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