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
@@ -8,14 +8,14 @@ description: Use Effect platform abstractions for cross-platform file I/O, proce
8
8
  ## Effect Source Reference
9
9
 
10
10
  The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
11
- Browse and read files there directly to look up APIs, types, and implementations.
11
+ This guide targets **Effect 4.0.0**. Inspect the `effect@4.0.0` tag when the checkout is ahead. Keep `effect` and every `@effect/*` package on the same version. APIs explicitly tagged `@stability unstable` can change in minor releases, including third-party Redis/client options.
12
12
 
13
13
  Reference this for:
14
14
 
15
15
  - FileSystem source: `packages/effect/src/FileSystem.ts`
16
16
  - Path source: `packages/effect/src/Path.ts`
17
17
  - Crypto source: `packages/effect/src/Crypto.ts`
18
- - Socket source: `packages/effect/src/unstable/socket/`
18
+ - Socket source: `packages/effect/src/socket/`
19
19
  - Platform layers: `packages/platform/node/`, `packages/platform/bun/`, and `packages/platform/browser/`
20
20
  - Migration guide: `MIGRATION.md`
21
21
  - Effect source: `packages/effect/src/`
@@ -83,6 +83,7 @@ const program = Effect.gen(function* () {
83
83
 
84
84
  Easy to mock and stub services:
85
85
 
86
+ <!-- typecheck -->
86
87
  ```typescript
87
88
  import { Effect, FileSystem, Layer } from 'effect';
88
89
 
@@ -90,7 +91,7 @@ declare const myProgram: Effect.Effect<void, never, FileSystem.FileSystem>;
90
91
 
91
92
  const TestFileSystem = Layer.succeed(
92
93
  FileSystem.FileSystem,
93
- FileSystem.make({
94
+ FileSystem.makeNoop({
94
95
  readFile: () => Effect.succeed(new Uint8Array())
95
96
  })
96
97
  );
@@ -102,11 +103,12 @@ const test = myProgram.pipe(Effect.provide(TestFileSystem));
102
103
 
103
104
  Integrates naturally with Effect's service system:
104
105
 
106
+ <!-- typecheck -->
105
107
  ```typescript
106
- import { Effect, FileSystem, Layer, Path, Context } from 'effect';
108
+ import { Effect, FileSystem, Layer, Path, PlatformError, Context } from 'effect';
107
109
 
108
110
  interface ConfigService {
109
- readonly load: (name: string) => Effect.Effect<string>;
111
+ readonly load: (name: string) => Effect.Effect<string, PlatformError.PlatformError>;
110
112
  }
111
113
 
112
114
  const ConfigService = Context.Service<ConfigService>('ConfigService');
@@ -118,11 +120,10 @@ const ConfigServiceLive = Layer.effect(
118
120
  const path = yield* Path.Path;
119
121
 
120
122
  return {
121
- load: (name: string) =>
122
- Effect.gen(function* () {
123
- const configPath = path.join('configs', name);
124
- return yield* fs.readFileString(configPath);
125
- })
123
+ load: Effect.fn('Config.load')(function* (name: string) {
124
+ const configPath = path.join('configs', name);
125
+ return yield* fs.readFileString(configPath);
126
+ })
126
127
  };
127
128
  })
128
129
  );
@@ -155,21 +156,22 @@ const content = await file.text();
155
156
 
156
157
  **Correct Pattern - FileSystem Service:**
157
158
 
159
+ <!-- typecheck -->
158
160
  ```typescript
159
161
  import { Effect, FileSystem } from 'effect';
160
162
 
161
163
  // ✅ CORRECT - Cross-platform, type-safe, testable
162
- const readFile = (path: string) =>
163
- Effect.gen(function* () {
164
- const fs = yield* FileSystem.FileSystem;
165
- return yield* fs.readFileString(path);
166
- });
164
+ const readFile = Effect.fn('File.read')(function* (path: string) {
165
+ const fs = yield* FileSystem.FileSystem;
166
+ return yield* fs.readFileString(path);
167
+ });
167
168
 
168
169
  // Effect<string, PlatformError, FileSystem>
169
170
  ```
170
171
 
171
172
  **Common Operations:**
172
173
 
174
+ <!-- typecheck -->
173
175
  ```typescript
174
176
  import { Effect, FileSystem } from 'effect';
175
177
 
@@ -210,6 +212,7 @@ const fileOperations = Effect.gen(function* () {
210
212
 
211
213
  **Streaming Files:**
212
214
 
215
+ <!-- typecheck -->
213
216
  ```typescript
214
217
  import { Effect, FileSystem, Stream } from 'effect';
215
218
 
@@ -224,7 +227,7 @@ const processLargeFile = Effect.gen(function* () {
224
227
 
225
228
  // Process stream
226
229
  yield* stream.pipe(
227
- Stream.mapEffect((chunk) => processChunk(chunk)),
230
+ Stream.tap(processChunk),
228
231
  Stream.run(fs.sink('output.txt'))
229
232
  );
230
233
  });
@@ -252,6 +255,7 @@ const joined = path.join('src', 'components', 'Button.tsx');
252
255
 
253
256
  **Correct Pattern - Path Service:**
254
257
 
258
+ <!-- typecheck -->
255
259
  ```typescript
256
260
  import { Effect, Path } from 'effect';
257
261
 
@@ -281,6 +285,7 @@ const buildPath = (filename: string) =>
281
285
 
282
286
  **Path Operations:**
283
287
 
288
+ <!-- typecheck -->
284
289
  ```typescript
285
290
  import { Effect, Path } from 'effect';
286
291
 
@@ -315,7 +320,7 @@ In Effect v4, `Migrator.fromFileSystem` requires both `FileSystem.FileSystem` an
315
320
 
316
321
  ### ChildProcess - Process Execution
317
322
 
318
- The `ChildProcess` and `ChildProcessSpawner` services enable safe process spawning.
323
+ `ChildProcess` builds command values; the `ChildProcessSpawner` service executes them with scoped resource ownership. Output helpers do not reject nonzero exit codes: use a handle and check `exitCode` when status determines success. Drain combined output with `handle.all`, or consume stdout/stderr concurrently, before leaving the scope.
319
324
 
320
325
  **Anti-Pattern - Direct child_process:**
321
326
 
@@ -341,8 +346,9 @@ const output = await new Response(proc.stdout).text();
341
346
 
342
347
  **Correct Pattern - ChildProcess + ChildProcessSpawner:**
343
348
 
349
+ <!-- typecheck -->
344
350
  ```typescript
345
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
351
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
346
352
  import { Effect, Stream } from 'effect';
347
353
 
348
354
  // ✅ CORRECT - Cross-platform command execution
@@ -358,8 +364,9 @@ const runCommand = Effect.gen(function* () {
358
364
 
359
365
  **Advanced ChildProcess Usage:**
360
366
 
367
+ <!-- typecheck -->
361
368
  ```typescript
362
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
369
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
363
370
  import { Console, Effect, Stream } from 'effect';
364
371
 
365
372
  const commandExamples = Effect.gen(function* () {
@@ -398,7 +405,7 @@ const commandExamples = Effect.gen(function* () {
398
405
  const exitCode = yield* handle.exitCode;
399
406
 
400
407
  return exitCode;
401
- });
408
+ }).pipe(Effect.scoped);
402
409
  ```
403
410
 
404
411
  ### Terminal - Terminal I/O
@@ -428,6 +435,7 @@ const input = prompt('Enter name:');
428
435
 
429
436
  **Correct Pattern - Terminal Service:**
430
437
 
438
+ <!-- typecheck -->
431
439
  ```typescript
432
440
  import { Effect, Terminal } from 'effect';
433
441
 
@@ -451,6 +459,7 @@ const interactiveProgram = Effect.gen(function* () {
451
459
 
452
460
  **For Simple Logging - Use Console or Effect.log:**
453
461
 
462
+ <!-- typecheck -->
454
463
  ```typescript
455
464
  import { Console, Effect } from 'effect';
456
465
 
@@ -480,6 +489,7 @@ const structuredLog = Effect.gen(function* () {
480
489
 
481
490
  The `Crypto.Crypto` service provides platform-backed cryptographic random bytes, UUIDv4/v7 generation, and message digests. Prefer it over `globalThis.crypto`, `crypto.randomUUID()`, or ad-hoc randomness when code should stay platform-abstract and testable.
482
491
 
492
+ <!-- typecheck -->
483
493
  ```typescript
484
494
  import { Crypto, Effect } from 'effect';
485
495
 
@@ -522,8 +532,9 @@ const result = await axios.get('https://api.example.com/data');
522
532
 
523
533
  **Correct Pattern - HttpClient Service:**
524
534
 
535
+ <!-- typecheck -->
525
536
  ```typescript
526
- import { HttpClient, HttpClientResponse } from 'effect/unstable/http';
537
+ import { HttpClient, HttpClientResponse } from 'effect/http';
527
538
  import { Effect, Schema } from 'effect';
528
539
 
529
540
  // ✅ CORRECT - Integrated with Effect type system
@@ -545,12 +556,13 @@ Name the adapter service and its effects after the upstream operation. The adapt
545
556
 
546
557
  **Advanced HTTP Operations:**
547
558
 
559
+ <!-- typecheck -->
548
560
  ```typescript
549
561
  import {
550
562
  HttpClient,
551
563
  HttpClientRequest,
552
564
  HttpClientResponse
553
- } from 'effect/unstable/http';
565
+ } from 'effect/http';
554
566
  import { Effect, Schema, Schedule } from 'effect';
555
567
 
556
568
  class User extends Schema.Class<User>('User')({
@@ -597,20 +609,9 @@ const httpExamples = Effect.gen(function* () {
597
609
  // Error handling — all HttpClient errors are "HttpClientError" with a reason field
598
610
  const safeRequest = client.get('https://api.example.com/data').pipe(
599
611
  Effect.flatMap(HttpClientResponse.filterStatusOk),
600
- Effect.catchTag('HttpClientError', (error) => {
601
- switch (error.reason._tag) {
602
- case 'TransportError':
603
- return Effect.succeed({ error: 'Network error' });
604
- case 'StatusCodeError':
605
- return Effect.succeed({
606
- error: `HTTP ${error.response?.status}`
607
- });
608
- default:
609
- return Effect.succeed({
610
- error: `Client error: ${error.reason._tag}`
611
- });
612
- }
613
- })
612
+ Effect.tapError((error) => Effect.logWarning('Provider request failed').pipe(
613
+ Effect.annotateLogs({ reason: error.reason._tag })
614
+ ))
614
615
  );
615
616
 
616
617
  // Retries with backoff: GET is idempotent and attempts are bounded.
@@ -618,7 +619,9 @@ const httpExamples = Effect.gen(function* () {
618
619
  Effect.flatMap(HttpClientResponse.filterStatusOk),
619
620
  Effect.retry({
620
621
  times: 3,
621
- schedule: Schedule.exponential('100 millis')
622
+ schedule: Schedule.exponential('100 millis'),
623
+ while: (error) => error.reason._tag === 'TransportError' ||
624
+ (error.reason._tag === 'StatusCodeError' && error.reason.response.status >= 500)
622
625
  }),
623
626
  Effect.tapError((error) =>
624
627
  Effect.logError('Provider read exhausted retries').pipe(
@@ -665,8 +668,9 @@ const value2 = fs.readFileSync('.cache/key', 'utf-8');
665
668
 
666
669
  **Correct Pattern - KeyValueStore Service:**
667
670
 
671
+ <!-- typecheck -->
668
672
  ```typescript
669
- import { KeyValueStore } from 'effect/unstable/persistence';
673
+ import { KeyValueStore } from 'effect/persistence';
670
674
  import { Effect, Schema } from 'effect';
671
675
 
672
676
  // ✅ CORRECT - Works on all platforms
@@ -697,8 +701,9 @@ const cacheData = Effect.gen(function* () {
697
701
 
698
702
  **Schema-Based Store:**
699
703
 
704
+ <!-- typecheck -->
700
705
  ```typescript
701
- import { KeyValueStore } from 'effect/unstable/persistence';
706
+ import { KeyValueStore } from 'effect/persistence';
702
707
  import { Effect, Schema } from 'effect';
703
708
 
704
709
  class User extends Schema.Class<User>('User')({
@@ -727,12 +732,14 @@ const typedStore = Effect.gen(function* () {
727
732
 
728
733
  ### Redis - Commands and Scoped Subscriptions
729
734
 
730
- The portable `Redis.Redis` service in `effect/unstable/persistence` provides `send`, cached script evaluation, and scoped pub/sub. Platform-specific layers provide the client: `NodeRedis.layer(...)`, `DenoRedis.layer(...)`, or `BunRedis.layer(...)`. These are specialized layers and are not included in `NodeServices.layer` or `BunServices.layer`.
735
+ The portable `Redis.Redis` service in `effect/persistence` provides `send`, cached script evaluation, and scoped pub/sub. Platform-specific layers provide the client: `NodeRedis.layer(...)`, `DenoRedis.layer(...)`, or `BunRedis.layer(...)`. These are specialized layers and are not included in `NodeServices.layer` or `BunServices.layer`.
736
+
737
+ The platform Redis APIs expose third-party options and are marked `@stability unstable`; inspect the installed package's contracts before minor-version upgrades.
731
738
 
732
739
  ```typescript
733
740
  import { NodeRedis } from '@effect/platform-node';
734
741
  import { Effect, Queue } from 'effect';
735
- import { Redis } from 'effect/unstable/persistence';
742
+ import { Redis } from 'effect/persistence';
736
743
 
737
744
  const RedisLayer = NodeRedis.layer({
738
745
  database: 1,
@@ -748,9 +755,9 @@ const receiveOne = Effect.gen(function* () {
748
755
 
749
756
  `redis.subscribe(channel)` requires `Scope` and returns a `Queue.Dequeue<RedisMessage, RedisError>`. Closing the scope shuts down the queue and releases the dedicated subscriber. Node and Deno subscribers reconnect and re-subscribe after interruptions, which can leave message-delivery gaps; Bun subscriptions do not reconnect, so a dropped connection fails the dequeue and callers must subscribe again.
750
757
 
751
- ### CLI Arguments - effect/unstable/cli
758
+ ### CLI Arguments - effect/cli
752
759
 
753
- For CLI applications, use `effect/unstable/cli` instead of direct `process.argv`.
760
+ For CLI applications, use `effect/cli` instead of direct `process.argv`.
754
761
 
755
762
  **Anti-Pattern - Direct process.argv:**
756
763
 
@@ -770,10 +777,10 @@ declare const yargs: (args: string[]) => { argv: Record<string, unknown> };
770
777
  const argv = yargs(process.argv.slice(2)).argv;
771
778
  ```
772
779
 
773
- **Correct Pattern - effect/unstable/cli:**
780
+ **Correct Pattern - effect/cli:**
774
781
 
775
782
  ```typescript
776
- import { Argument, Command as CliCommand, Flag } from 'effect/unstable/cli';
783
+ import { Argument, Command as CliCommand, Flag } from 'effect/cli';
777
784
  import { NodeServices, NodeRuntime } from '@effect/platform-node';
778
785
  import { Console, Effect } from 'effect';
779
786
 
@@ -816,20 +823,20 @@ Complete reference table of platform abstractions:
816
823
  | ------------------------- | -------------------------------------- | ---------------------------- | ----------------------------- |
817
824
  | **File I/O** | `FileSystem.FileSystem` | `fs`, `Bun.file` | `effect` |
818
825
  | **Path Operations** | `Path.Path` | `path`, string concat | `effect` |
819
- | **Process Spawning** | `ChildProcess` + `ChildProcessSpawner` | `child_process`, `Bun.spawn` | `effect/unstable/process` |
826
+ | **Process Spawning** | `ChildProcess` + `ChildProcessSpawner` | `child_process`, `Bun.spawn` | `effect/process` |
820
827
  | **Terminal I/O** | `Terminal.Terminal` | `process.stdin/stdout` | `effect` |
821
828
  | **Console Logging** | `Console.log` or `Effect.log` | `console.log` | `effect` |
822
829
  | **Crypto** | `Crypto.Crypto` | `globalThis.crypto`, `crypto.randomUUID()` | `effect` |
823
- | **HTTP Client** | `HttpClient.HttpClient` | `fetch`, `axios` | `effect/unstable/http` |
824
- | **HTTP Server** | `HttpServer.HttpServer` | `http.createServer` | `effect/unstable/http` |
825
- | **Sockets** | `Socket.Socket` / `SocketServer.SocketServer` | raw TCP/WebSocket APIs | `effect/unstable/socket` |
826
- | **Key-Value Store** | `KeyValueStore.KeyValueStore` | `localStorage`, manual files | `effect/unstable/persistence` |
827
- | **Redis** | `Redis.Redis` | direct Redis clients | `effect/unstable/persistence` |
828
- | **CLI Arguments** | `Argument` + `Flag` + `Command` | `process.argv`, `yargs` | `effect/unstable/cli` |
830
+ | **HTTP Client** | `HttpClient.HttpClient` | `fetch`, `axios` | `effect/http` |
831
+ | **HTTP Server** | `HttpServer.HttpServer` | `http.createServer` | `effect/http` |
832
+ | **Sockets** | `Socket.Socket` / `SocketServer.SocketServer` | raw TCP/WebSocket APIs | `effect/socket` |
833
+ | **Key-Value Store** | `KeyValueStore.KeyValueStore` | `localStorage`, manual files | `effect/persistence` |
834
+ | **Redis** | `Redis.Redis` | direct Redis clients | `effect/persistence` |
835
+ | **CLI Arguments** | `Argument` + `Flag` + `Command` | `process.argv`, `yargs` | `effect/cli` |
829
836
  | **Environment Variables** | `Config` from effect | `process.env` | `effect` |
830
837
  | **Streams** | `Stream` | Node streams, ReadableStream | `effect` |
831
838
 
832
- Socket services live in `effect/unstable/socket`. Use `Socket.Socket` for scoped bidirectional string/binary frame transports and `SocketServer.SocketServer` for accepting connections. Provide service-specific layers such as `BrowserSocket.layerWebSocket(url)`, `NodeSocket.layerWebSocket(url)`, `NodeSocket.layerNet(options)`, `BunSocket.layerWebSocket(url)`, or Node/Bun socket-server layers; `NodeServices.layer` and `BunServices.layer` do not provide sockets.
839
+ Socket services live in `effect/socket`. Use `Socket.Socket` for scoped bidirectional string/binary frame transports and `SocketServer.SocketServer` for accepting connections. Provide service-specific layers such as `BrowserSocket.layerWebSocket(url)`, `NodeSocket.layerWebSocket(url)`, `NodeSocket.layerNet(options)`, `BunSocket.layerWebSocket(url)`, or Node/Bun socket-server layers; `NodeServices.layer` and `BunServices.layer` do not provide sockets.
833
840
 
834
841
  ## Setting Up Platform-Specific Layers
835
842
 
@@ -871,7 +878,7 @@ program.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
871
878
  import { NodeServices, NodeRuntime } from '@effect/platform-node';
872
879
  import { BunServices, BunRuntime } from '@effect/platform-bun';
873
880
  import { Console, Effect, FileSystem, Path, Schema } from 'effect';
874
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
881
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
875
882
 
876
883
  class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
877
884
  'FileProcessorConfig'
@@ -881,6 +888,11 @@ class FileProcessorConfig extends Schema.Class<FileProcessorConfig>(
881
888
  compress: Schema.Boolean
882
889
  }) {}
883
890
 
891
+ class CompressionFailed extends Schema.TaggedError<CompressionFailed>()('CompressionFailed', {
892
+ file: Schema.String,
893
+ exitCode: Schema.Number
894
+ }) {}
895
+
884
896
  const processFiles = Effect.gen(function* () {
885
897
  const fs = yield* FileSystem.FileSystem;
886
898
  const path = yield* Path.Path;
@@ -913,9 +925,14 @@ const processFiles = Effect.gen(function* () {
913
925
 
914
926
  // Optionally compress
915
927
  if (config.compress) {
916
- yield* spawner.string(
917
- ChildProcess.make('gzip', [outputPath])
928
+ const exitCode = yield* spawner.exitCode(
929
+ ChildProcess.make('gzip', [outputPath], {
930
+ stdin: 'ignore', stdout: 'ignore', stderr: 'inherit'
931
+ })
918
932
  );
933
+ if (exitCode !== 0) {
934
+ return yield* Effect.fail(new CompressionFailed({ file, exitCode }));
935
+ }
919
936
  }
920
937
 
921
938
  yield* Console.log(`Processed: ${file}`);
@@ -937,8 +954,9 @@ processFiles.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
937
954
 
938
955
  One major benefit of platform abstractions is testability:
939
956
 
957
+ <!-- typecheck -->
940
958
  ```typescript
941
- import { Effect, FileSystem, Layer } from 'effect';
959
+ import { Effect, FileSystem, Layer, PlatformError } from 'effect';
942
960
 
943
961
  declare const myFileProcessor: Effect.Effect<
944
962
  void,
@@ -950,14 +968,16 @@ declare const myFileProcessor: Effect.Effect<
950
968
  const TestFileSystem = Layer.succeed(
951
969
  FileSystem.FileSystem,
952
970
  FileSystem.makeNoop({
953
- readFile: (path) => {
971
+ readFileString: (path) => {
954
972
  if (path === 'config.json') {
955
- const data = JSON.stringify({ key: 'value' });
956
- return Effect.succeed(new TextEncoder().encode(data));
973
+ return Effect.succeed('{"key":"value"}');
957
974
  }
958
- return Effect.fail(new Error('File not found'));
975
+ return Effect.fail(PlatformError.systemError({
976
+ _tag: 'NotFound', module: 'FileSystem', method: 'readFileString',
977
+ pathOrDescriptor: path
978
+ }));
959
979
  },
960
- exists: (path) => Effect.succeed(true)
980
+ exists: (path) => Effect.succeed(path === 'config.json')
961
981
  })
962
982
  );
963
983
 
@@ -967,6 +987,8 @@ const testProgram = myFileProcessor.pipe(Effect.provide(TestFileSystem));
967
987
  Effect.runPromise(testProgram);
968
988
  ```
969
989
 
990
+ `makeNoop` does not derive `readFileString` from an overridden `readFile`; override the methods your program calls. Most missing operations fail with NotFound, `exists` defaults to false, `remove` succeeds, and directory/temp creation defects unless overridden. `make` is for implementing the full set of core operations and deriving conveniences, not for partial test doubles.
991
+
970
992
  ## Quality Checklist
971
993
 
972
994
  Before completing code that uses platform operations:
@@ -975,7 +997,7 @@ Before completing code that uses platform operations:
975
997
  - [ ] All path operations use `Path.Path` service
976
998
  - [ ] Process spawning uses `ChildProcess` + `ChildProcessSpawner`
977
999
  - [ ] Console output uses `Console.log` or `Effect.log` (not `console.log`)
978
- - [ ] CLI arguments parsed with `effect/unstable/cli` (not `process.argv`)
1000
+ - [ ] CLI arguments parsed with `effect/cli` (not `process.argv`)
979
1001
  - [ ] HTTP requests use `HttpClient.HttpClient` (not `fetch`/`axios`)
980
1002
  - [ ] Any raw `fetch` is isolated in a named low-level platform adapter with documented justification
981
1003
  - [ ] HTTP status is classified before success-body schema decoding
@@ -1026,7 +1048,7 @@ const program = Effect.gen(function* () {
1026
1048
  return yield* fs.readFileString('file.txt');
1027
1049
  });
1028
1050
 
1029
- Effect.runPromise(program); // Runtime error!
1051
+ Effect.runPromise(program); // Type error: FileSystem requirement is unsatisfied
1030
1052
 
1031
1053
  // ✅ CORRECT - Provide platform layer
1032
1054
  program.pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
@@ -1093,7 +1115,7 @@ import {
1093
1115
  HttpClient,
1094
1116
  HttpClientRequest,
1095
1117
  HttpClientResponse
1096
- } from 'effect/unstable/http';
1118
+ } from 'effect/http';
1097
1119
 
1098
1120
  // Before (fetch)
1099
1121
  declare const fetch: (
@@ -1140,7 +1162,7 @@ This POST is intentionally not retried. Add retry only if the provider offers a
1140
1162
 
1141
1163
  ```typescript
1142
1164
  import { Effect } from 'effect';
1143
- import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
1165
+ import { ChildProcess, ChildProcessSpawner } from 'effect/process';
1144
1166
  import { exec } from 'child_process';
1145
1167
  import { promisify } from 'util';
1146
1168