opencode-effect-enforcer 0.2.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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,514 @@
1
+ ---
2
+ name: effect-platform-layers
3
+ description: Structure Effect platform layer provision for cross-platform applications using Effect platform abstractions.
4
+ ---
5
+
6
+ # Platform Layers
7
+
8
+ Master Effect platform layer provision for cross-platform applications. Use this skill when structuring applications that use Effect platform abstractions to ensure portability across Node.js and Bun environments.
9
+
10
+ ## The Golden Rule
11
+
12
+ **Application code uses abstract interfaces. Platform-specific layers are provided either at the program entry point or inside a runtime-facing adapter module's `defaultLayer`.**
13
+
14
+ ```typescript
15
+ // Application code - platform agnostic
16
+ import { Effect, FileSystem, Path, pipe } from 'effect';
17
+
18
+ const readConfig = Effect.gen(function* () {
19
+ const fs = yield* FileSystem.FileSystem;
20
+ const path = yield* Path.Path;
21
+ const configPath = path.join('config', 'app.json');
22
+ return yield* fs.readFileString(configPath);
23
+ });
24
+
25
+ // Entry point - platform specific
26
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
27
+
28
+ declare const program: Effect.Effect<void, never, never>;
29
+
30
+ pipe(program, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
31
+ ```
32
+
33
+ ```typescript
34
+ // WRONG - platform-specific imports in application code
35
+ import { readFileSync } from 'fs'; // Ties code to Node.js
36
+ import { FileSystem } from '@effect/platform-node'; // Platform-specific
37
+ ```
38
+
39
+ Runtime-facing adapter modules may own their platform wiring directly:
40
+
41
+ ```typescript
42
+ export const defaultLayer = layer.pipe(
43
+ Layer.provide(NodeFileSystem.layer),
44
+ Layer.provide(NodePath.layer)
45
+ );
46
+ ```
47
+
48
+ The important boundary is that downstream callers still depend on the abstract service, not on Node/Bun modules.
49
+
50
+ For HTTP provider adapters, the abstract dependency is `HttpClient.HttpClient`. Keep it visible on the adapter's raw layer and name the adapter after the upstream it owns:
51
+
52
+ ```typescript
53
+ import { Context, Effect, Layer } from 'effect';
54
+ import { FetchHttpClient, HttpClient } from 'effect/unstable/http';
55
+
56
+ export class ProviderGateway extends Context.Service<ProviderGateway, {
57
+ readonly health: Effect.Effect<void>;
58
+ }>()('app/ProviderGateway') {}
59
+
60
+ const makeProviderGateway = Effect.gen(function* () {
61
+ yield* HttpClient.HttpClient;
62
+ return ProviderGateway.of({ health: Effect.void });
63
+ });
64
+
65
+ export const layer: Layer.Layer<ProviderGateway, never, HttpClient.HttpClient> =
66
+ Layer.effect(ProviderGateway, makeProviderGateway);
67
+
68
+ export const defaultLayer: Layer.Layer<ProviderGateway> = layer.pipe(
69
+ Layer.provide(FetchHttpClient.layer)
70
+ );
71
+ ```
72
+
73
+ Use `layer` when the application or test owns transport selection. Use `defaultLayer` only when this runtime-facing adapter intentionally owns the default transport. Do not provide the transport inside `layer`, because that erases the dependency graph and prevents straightforward substitution.
74
+
75
+ ## Platform Import Patterns
76
+
77
+ ### Node.js
78
+
79
+ ```typescript
80
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
81
+ import { Effect, pipe } from 'effect';
82
+
83
+ declare const program: Effect.Effect<void, never, never>;
84
+
85
+ pipe(program, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
86
+ ```
87
+
88
+ ### Bun
89
+
90
+ ```typescript
91
+ import { BunServices, BunRuntime } from '@effect/platform-bun';
92
+ import { Effect, pipe } from 'effect';
93
+
94
+ declare const program: Effect.Effect<void, never, never>;
95
+
96
+ pipe(program, Effect.provide(BunServices.layer), BunRuntime.runMain);
97
+ ```
98
+
99
+ ### Browser
100
+
101
+ ```typescript
102
+ import { BrowserRuntime } from '@effect/platform-browser';
103
+ import { Effect, pipe } from 'effect';
104
+
105
+ declare const program: Effect.Effect<void, never, never>;
106
+
107
+ pipe(program, BrowserRuntime.runMain);
108
+ ```
109
+
110
+ `BrowserRuntime.runMain` keeps the main fiber alive when a `pagehide` event is persisted for the browser back/forward cache. It interrupts the fiber on non-persisted `pagehide`, when the document is actually being discarded. Browser teardown is best-effort, so asynchronous finalizers are not guaranteed to finish before the page disappears.
111
+
112
+ ## Context Layer Services
113
+
114
+ Each platform context (`NodeServices.layer`, `BunServices.layer`) provides these services:
115
+
116
+ | Service | Tag | Description | Import from |
117
+ | ----------------------- | ----------------------------------------- | --------------------------------------------------- | ------------------------- |
118
+ | **FileSystem** | `FileSystem.FileSystem` | File I/O operations (read, write, stat, etc.) | `effect` |
119
+ | **Path** | `Path.Path` | Path manipulation (join, normalize, relative, etc.) | `effect` |
120
+ | **Stdio** | `Stdio.Stdio` | Standard I/O streams (stdin, stdout, stderr) | `effect` |
121
+ | **Terminal** | `Terminal.Terminal` | Terminal/console I/O with ANSI support | `effect` |
122
+ | **Crypto** | `Crypto.Crypto` | Cryptographic random bytes, UUIDs, and digests | `effect` |
123
+ | **ChildProcessSpawner** | `ChildProcessSpawner.ChildProcessSpawner` | Spawn and manage child processes | `effect/unstable/process` |
124
+
125
+ `Crypto.Crypto` is included in the Node/Bun aggregate layers; browser applications can provide `BrowserCrypto.layer` when they need the crypto service. These aggregate layers are core service bundles: they do **not** provide specialized integrations such as HTTP clients/servers, sockets, workers, or Redis. For sockets, import `Socket.Socket` / `SocketServer.SocketServer` from `effect/unstable/socket` and provide socket-specific layers such as `NodeSocket.layerWebSocket(...)`, `NodeSocket.layerNet(...)`, `BunSocket.layerWebSocket(...)`, `BrowserSocket.layerWebSocket(...)`, or Node/Bun socket-server layers as appropriate.
126
+
127
+ Runtime application/provider HTTP belongs behind Effect `HttpClient`, not raw `fetch`. Only a named low-level platform transport adapter may use fetch directly, with a documented justification and full ownership of interruption, status-before-decode, schema decoding, and typed errors. Provider adapters also own redacted diagnostic evidence and retry exhaustion; provider calls run outside database transactions, and retries apply only to proven-idempotent operations. In particular, do not decorate a shared client with automatic retry when it can execute ordinary non-idempotent POST/PATCH requests.
128
+
129
+ `Migrator.fromFileSystem` now requires both `FileSystem.FileSystem` and `Path.Path`. `NodeServices.layer` and `BunServices.layer` already satisfy both. If a migration runtime provides only an individual file-system layer, add the matching host path layer too; on Windows, core `Path.layer` is not a substitute for a platform-aware path implementation because it uses POSIX semantics.
130
+
131
+ ### Redis Layers
132
+
133
+ Redis is deliberately outside the aggregate platform layers. `NodeRedis.layer` and `NodeRedis.layerConfig` use `redis` (node-redis), with a supported peer range of `redis >=5.0.0 <7.0.0`, and accept node-redis `RedisClientOptions`. When migrating from `ioredis`:
134
+
135
+ - Move host, port, TLS, and reconnect settings under `socket`.
136
+ - Rename `db` to `database`.
137
+ - Use node-redis camel-cased commands such as `hLen` and `lRange` on `NodeRedis.NodeRedis.client`.
138
+ - Use `sendCommand` for arbitrary raw commands.
139
+ - Do not force a RESP protocol unless required; protocol selection follows the installed node-redis default.
140
+
141
+ The Node layer connects while it is built and therefore can fail with `Redis.RedisError`. The initial connection fails fast by default; supplying `socket.reconnectStrategy` opts into caller-defined initial retry behavior. After the client first becomes ready, the built-in strategy uses node-redis exponential backoff and stops on socket timeouts. Scope finalization calls `close()`, which waits for in-flight and blocking commands and can delay layer shutdown.
142
+
143
+ ### Usage Example
144
+
145
+ ```typescript
146
+ import { Console, Crypto, Effect, FileSystem, Path, Stream, Terminal } from 'effect';
147
+ import { ChildProcess, ChildProcessSpawner } from 'effect/unstable/process';
148
+
149
+ const buildProject = Effect.gen(function* () {
150
+ const fs = yield* FileSystem.FileSystem;
151
+ const path = yield* Path.Path;
152
+ const terminal = yield* Terminal.Terminal;
153
+ const crypto = yield* Crypto.Crypto;
154
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
155
+
156
+ // Use Path for cross-platform paths
157
+ const outDir = path.join('dist', 'bundle');
158
+
159
+ // Use FileSystem for I/O
160
+ yield* fs.makeDirectory(outDir, { recursive: true });
161
+
162
+ // Use Terminal dimensions and Crypto for output metadata
163
+ const columns = yield* terminal.columns;
164
+ const rows = yield* terminal.rows;
165
+ const buildId = yield* crypto.randomUUIDv7;
166
+ yield* terminal.display(`Building project ${buildId} (${columns}x${rows})...\n`);
167
+
168
+ // Use ChildProcessSpawner for processes
169
+ const handle = yield* spawner.spawn(
170
+ ChildProcess.make('npm', ['run', 'build'])
171
+ );
172
+ yield* handle.all.pipe(
173
+ Stream.decodeText(),
174
+ Stream.splitLines,
175
+ Stream.runForEach((line) => Console.log(`[build] ${line}`))
176
+ );
177
+ return yield* handle.exitCode;
178
+ });
179
+ ```
180
+
181
+ ## Layer Composition Patterns
182
+
183
+ ### Basic Provision
184
+
185
+ ```typescript
186
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
187
+ import { Effect, pipe } from 'effect';
188
+
189
+ declare const program: Effect.Effect<void, never, never>;
190
+
191
+ // Single platform context provides all services
192
+ pipe(program, Effect.provide(NodeServices.layer), NodeRuntime.runMain);
193
+ ```
194
+
195
+ ### Adding Custom Services
196
+
197
+ ```typescript
198
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
199
+ import { Effect, Layer, pipe } from 'effect';
200
+
201
+ declare const DatabaseLive: Layer.Layer<never, never, never>;
202
+ declare const ConfigServiceLive: Layer.Layer<never, never, never>;
203
+ declare const LoggerLive: Layer.Layer<never, never, never>;
204
+ declare const program: Effect.Effect<void, never, never>;
205
+
206
+ const AppLayer = Layer.mergeAll(DatabaseLive, ConfigServiceLive, LoggerLive);
207
+
208
+ pipe(
209
+ program,
210
+ Effect.provide(AppLayer),
211
+ Effect.provide(NodeServices.layer), // Platform services last
212
+ NodeRuntime.runMain
213
+ );
214
+ ```
215
+
216
+ ### Overriding Platform Services
217
+
218
+ ```typescript
219
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
220
+ import { Effect, FileSystem, Layer, pipe } from 'effect';
221
+
222
+ declare const program: Effect.Effect<void, never, never>;
223
+
224
+ // Custom FileSystem implementation
225
+ const CustomFS = Layer.succeed(FileSystem.FileSystem, {
226
+ /* custom implementation */
227
+ } as FileSystem.FileSystem);
228
+
229
+ pipe(
230
+ program,
231
+ Effect.provide(NodeServices.layer),
232
+ Effect.provide(CustomFS), // Override after platform layer
233
+ NodeRuntime.runMain
234
+ );
235
+ ```
236
+
237
+ ## Testing with Mock Layers
238
+
239
+ **CRITICAL**: Prefer mock abstract services for unit tests. For runtime-adapter or layer-composition tests, it is acceptable to provide the real Node/Bun layers directly when that is the behavior under review.
240
+
241
+ ### Mocking FileSystem
242
+
243
+ ```typescript
244
+ import { Effect, FileSystem, Layer } from 'effect';
245
+ import { expect, test } from 'vitest';
246
+
247
+ declare const readConfig: Effect.Effect<string, never, FileSystem.FileSystem>;
248
+
249
+ // Use FileSystem.makeNoop for testing — provides default "NotFound" stubs
250
+ // for all methods, then override only the ones you need
251
+ const MockFileSystem = Layer.succeed(
252
+ FileSystem.FileSystem,
253
+ FileSystem.makeNoop({
254
+ readFileString: (path) => Effect.succeed(`mock content for ${path}`),
255
+ exists: (path) => Effect.succeed(true)
256
+ })
257
+ );
258
+
259
+ test('should read config', () =>
260
+ Effect.gen(function* () {
261
+ const result = yield* readConfig;
262
+ expect(result).toContain('mock content');
263
+ }).pipe(Effect.provide(MockFileSystem), Effect.runPromise));
264
+ ```
265
+
266
+ ### Mocking Multiple Services
267
+
268
+ ```typescript
269
+ import { Effect, FileSystem, Layer, Path, Terminal } from 'effect';
270
+ import { test } from 'vitest';
271
+
272
+ declare const program: Effect.Effect<
273
+ void,
274
+ never,
275
+ FileSystem.FileSystem | Path.Path | Terminal.Terminal
276
+ >;
277
+
278
+ const TestContext = Layer.mergeAll(
279
+ Layer.succeed(FileSystem.FileSystem, {
280
+ readFileString: () => Effect.succeed('test')
281
+ // ...
282
+ } as FileSystem.FileSystem),
283
+
284
+ Layer.succeed(Path.Path, {
285
+ join: (...parts) => parts.join('/'),
286
+ normalize: (path) => path
287
+ // ...
288
+ } as Path.Path),
289
+
290
+ Layer.succeed(
291
+ Terminal.Terminal,
292
+ Terminal.make({
293
+ columns: Effect.succeed(80),
294
+ rows: Effect.succeed(24),
295
+ readInput: Effect.dieMessage('readInput not used in this test'),
296
+ readLine: Effect.succeed('test input'),
297
+ display: () => Effect.void
298
+ })
299
+ )
300
+ );
301
+
302
+ test('integration test', () =>
303
+ program.pipe(Effect.provide(TestContext), Effect.runPromise));
304
+ ```
305
+
306
+ ### Using layerNoop for Convenient Test Layers
307
+
308
+ ```typescript
309
+ import { Effect, FileSystem } from 'effect';
310
+ import { test } from 'vitest';
311
+
312
+ // FileSystem.layerNoop wraps makeNoop in a Layer for convenience
313
+ const TestFS = FileSystem.layerNoop({
314
+ readFileString: () => Effect.succeed('test content'),
315
+ writeFileString: () => Effect.void,
316
+ exists: () => Effect.succeed(true)
317
+ });
318
+
319
+ test('with layerNoop', () =>
320
+ Effect.gen(function* () {
321
+ const fs = yield* FileSystem.FileSystem;
322
+ yield* fs.writeFileString('test.txt', 'content');
323
+ }).pipe(Effect.provide(TestFS), Effect.runPromise));
324
+ ```
325
+
326
+ ## Architecture Patterns
327
+
328
+ ### Layered Application Structure
329
+
330
+ ```
331
+ src/
332
+ ├── domain/ # Pure domain logic (no platform deps)
333
+ ├── services/ # Business services (uses abstract platform)
334
+ ├── infrastructure/ # Platform adapters (if needed)
335
+ └── main/
336
+ ├── main.ts # Entry point with NodeServices
337
+ └── main.test.ts # Tests with mock contexts
338
+ ```
339
+
340
+ ### Service Implementation
341
+
342
+ ```typescript
343
+ // services/ConfigService.ts
344
+ import { Effect, FileSystem, Layer, Path, Schema, Context } from 'effect';
345
+
346
+ interface Config {
347
+ readonly name: string;
348
+ readonly version: string;
349
+ }
350
+
351
+ declare const ConfigSchema: Schema.Schema<Config>;
352
+
353
+ class ConfigError extends Schema.TaggedError<ConfigError>()(
354
+ 'ConfigError',
355
+ {
356
+ message: Schema.String
357
+ }
358
+ ) {}
359
+
360
+ export class ConfigService extends Context.Service<
361
+ ConfigService,
362
+ {
363
+ readonly load: Effect.Effect<Config, ConfigError>;
364
+ save(config: Config): Effect.Effect<void, ConfigError>;
365
+ }
366
+ >()('ConfigService') {}
367
+
368
+ export const ConfigServiceLive = Layer.effect(
369
+ ConfigService,
370
+ Effect.gen(function* () {
371
+ const fs = yield* FileSystem.FileSystem;
372
+ const path = yield* Path.Path;
373
+
374
+ const load = Effect.gen(function* () {
375
+ const configPath = path.join('config', 'app.json');
376
+ const content = yield* fs.readFileString(configPath);
377
+ return yield* Schema.decode(ConfigSchema)(JSON.parse(content));
378
+ });
379
+
380
+ const save = (config: Config) =>
381
+ Effect.gen(function* () {
382
+ const configPath = path.join('config', 'app.json');
383
+ const content = JSON.stringify(config, null, 2);
384
+ yield* fs.writeFileString(configPath, content);
385
+ });
386
+
387
+ return { load, save };
388
+ })
389
+ );
390
+ ```
391
+
392
+ ### Entry Point
393
+
394
+ ```typescript
395
+ // main/main.ts
396
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
397
+ import { Effect, Layer, pipe } from 'effect';
398
+ import { ConfigService, ConfigServiceLive } from '../services/ConfigService.js';
399
+
400
+ const MainLayer = Layer.mergeAll(
401
+ ConfigServiceLive
402
+ // ... other services
403
+ );
404
+
405
+ const program = Effect.gen(function* () {
406
+ const config = yield* ConfigService;
407
+ yield* config.load;
408
+ // ... application logic
409
+ });
410
+
411
+ pipe(
412
+ program,
413
+ Effect.provide(MainLayer),
414
+ Effect.provide(NodeServices.layer),
415
+ NodeRuntime.runMain
416
+ );
417
+ ```
418
+
419
+ ## Common Patterns
420
+
421
+ ### Conditional Platform Loading
422
+
423
+ ```typescript
424
+ import { NodeServices, NodeRuntime } from '@effect/platform-node';
425
+ import { BunServices } from '@effect/platform-bun';
426
+ import { Effect, pipe } from 'effect';
427
+
428
+ declare const program: Effect.Effect<void, never, never>;
429
+
430
+ const PlatformContext =
431
+ process.env.RUNTIME === 'bun' ? BunServices.layer : NodeServices.layer;
432
+
433
+ pipe(
434
+ program,
435
+ Effect.provide(PlatformContext),
436
+ NodeRuntime.runMain // Runtime matches context
437
+ );
438
+ ```
439
+
440
+ ### Scoped Platform Resources
441
+
442
+ ```typescript
443
+ import { Effect, FileSystem, Path } from 'effect';
444
+
445
+ const withTempDirectory = Effect.gen(function* () {
446
+ const fs = yield* FileSystem.FileSystem;
447
+ const path = yield* Path.Path;
448
+
449
+ const tempDir = yield* Effect.acquireRelease(
450
+ Effect.gen(function* () {
451
+ const dir = path.join('temp', `${Date.now()}`);
452
+ yield* fs.makeDirectory(dir, { recursive: true });
453
+ return dir;
454
+ }),
455
+ (dir) => fs.remove(dir, { recursive: true })
456
+ );
457
+
458
+ return tempDir;
459
+ });
460
+ ```
461
+
462
+ ## Anti-Patterns
463
+
464
+ ### Platform-Specific Imports in Application Code
465
+
466
+ ```typescript
467
+ // WRONG - ties application to Node.js
468
+ import * as fs from 'fs';
469
+ import * as path from 'path';
470
+
471
+ const readConfig = () => {
472
+ const content = fs.readFileSync(path.join('config', 'app.json'), 'utf8');
473
+ return JSON.parse(content);
474
+ };
475
+ ```
476
+
477
+ ### Direct Platform Module Usage
478
+
479
+ ```typescript
480
+ // WRONG - bypasses Effect abstractions
481
+ import { FileSystem } from '@effect/platform-node';
482
+ import { Effect } from 'effect';
483
+
484
+ const program = Effect.gen(function* () {
485
+ const fs = yield* FileSystem.FileSystem;
486
+ // ...
487
+ });
488
+ ```
489
+
490
+ ### Providing Platform Layers in Application Code
491
+
492
+ ```typescript
493
+ // WRONG - application code should not know about platform
494
+ import { NodeServices } from '@effect/platform-node';
495
+ import { Effect } from 'effect';
496
+
497
+ declare const program: Effect.Effect<void, never, never>;
498
+
499
+ export const myService = program.pipe(
500
+ Effect.provide(NodeServices.layer) // Should be at entry point only
501
+ );
502
+ ```
503
+
504
+ ## Key Principles
505
+
506
+ 1. **Import abstractions, provide implementations**: Application code imports from `effect` (e.g. `FileSystem`, `Path`, `Terminal`), entry points provide platform-specific contexts
507
+ 2. **One platform layer per runtime**: Use exactly one of `NodeServices.layer` or `BunServices.layer`
508
+ 3. **Platform layer last**: Provide custom services first, platform context last
509
+ 4. **Mock in tests**: Use `Layer.succeed` with mock implementations, never import platform-specific modules in tests
510
+ 5. **Entry point decides platform**: Only `main.ts` (or equivalent entry) should import platform-specific modules
511
+ 6. **Keep HTTP transport requirements visible**: Provider adapter `layer` requires `HttpClient.HttpClient`; an optional `defaultLayer` may provide the chosen transport
512
+ 7. **Make adapters own the boundary**: Named adapters classify status before schema decoding, map typed failures, retain redacted evidence, and expose retry exhaustion
513
+ 8. **Do not retry by accident**: Restrict retrying/rate-limited clients to proven-idempotent operations; non-idempotent calls need an explicit provider guarantee or idempotency key
514
+ 9. **Do not hold transactions across providers**: Complete network calls before opening the database transaction used to persist their result