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,580 @@
1
+ ---
2
+ name: effect-config
3
+ description: Load and validate typed configuration with Config and ConfigProvider. Use this skill when reading environment variables, building structured config, providing test config, or working with .env files, JSON config, and custom config sources.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in typed configuration loading, validation, and provider composition.
7
+
8
+ ## Effect Source Reference
9
+
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.
12
+
13
+ Reference these files for Config/ConfigProvider details:
14
+
15
+ - `packages/effect/CONFIG.md` — primary guide
16
+ - `packages/effect/src/Config.ts` — Config API source
17
+ - `packages/effect/src/ConfigProvider.ts` — ConfigProvider API source
18
+
19
+ ## Core Imports
20
+
21
+ ```ts
22
+ import { Config, ConfigProvider, Effect, Schema } from 'effect';
23
+ ```
24
+
25
+ ## Why Not `process.env`
26
+
27
+ Never read `process.env` directly in Effect code. `Config` provides:
28
+
29
+ 1. **Type safety** — primitives decode strings into `number`, `boolean`, `Date`, `Duration`, etc.
30
+ 2. **Validation** — invalid values produce structured `ConfigError` with clear messages
31
+ 3. **Composability** — nest, combine, transform, and default configs declaratively
32
+ 4. **Testability** — swap providers without mocking `process.env`
33
+ 5. **Schema integration** — use `Config.schema` with `Schema.Struct` for complex shapes
34
+
35
+ ## Config Primitives
36
+
37
+ Each constructor reads a single value and decodes it. The optional `name` parameter sets the root path segment for lookup. Omit it when the config is part of a larger `Config.schema`.
38
+
39
+ ```ts
40
+ Config.string('HOST'); // string
41
+ Config.nonEmptyString('HOST'); // string (rejects "")
42
+ Config.number('RATE'); // number (includes NaN, Infinity)
43
+ Config.finite('RATE'); // number (rejects NaN, Infinity)
44
+ Config.int('PORT'); // number (integers only)
45
+ Config.boolean('DEBUG'); // boolean (accepts true/false, yes/no, on/off, 1/0, y/n)
46
+ Config.port('PORT'); // number (integer in 1–65535)
47
+ Config.url('CALLBACK_URL'); // URL
48
+ Config.date('EXPIRES_AT'); // Date (rejects invalid dates)
49
+ Config.duration('TIMEOUT'); // Duration (parses "10 seconds", "500 millis", "Infinity", "-Infinity")
50
+ Config.logLevel('LOG_LEVEL'); // string (All|Fatal|Error|Warn|Info|Debug|Trace|None)
51
+ Config.redacted('API_KEY'); // Redacted<string> (hidden from logs and toString)
52
+ Config.literal('production', 'ENV'); // literal type (accepts only the given literal)
53
+ Config.literals(['development', 'production'], 'ENV'); // accepts one of several literals
54
+ ```
55
+
56
+ ## Config Combinators
57
+
58
+ ### `Config.withDefault` — Fallback for Missing Keys
59
+
60
+ Only triggers when data is **missing**. Validation errors (wrong type, out of range) still propagate.
61
+
62
+ ```ts
63
+ const port = Config.int('PORT').pipe(Config.withDefault(3000));
64
+ ```
65
+
66
+ ### `Config.option` — Optional Values
67
+
68
+ Returns `Option.some(value)` on success, `Option.none()` when data is missing.
69
+
70
+ ```ts
71
+ const maybePort = Config.option(Config.int('PORT'));
72
+ ```
73
+
74
+ ### `Config.map` — Transform a Value
75
+
76
+ ```ts
77
+ const upperHost = Config.string('HOST').pipe(
78
+ Config.map((s) => s.toUpperCase())
79
+ );
80
+ ```
81
+
82
+ ### `Config.orElse` — Fallback on Any Error
83
+
84
+ Unlike `withDefault`, this catches **all** `ConfigError`s:
85
+
86
+ ```ts
87
+ const host = Config.string('HOST').pipe(
88
+ Config.orElse(() => Config.succeed('localhost'))
89
+ );
90
+ ```
91
+
92
+ ### `Config.all` — Combine Multiple Configs
93
+
94
+ Accepts a record or a tuple:
95
+
96
+ ```ts
97
+ // As a record
98
+ const appConfig = Config.all({
99
+ host: Config.string('host'),
100
+ port: Config.int('port'),
101
+ debug: Config.boolean('debug')
102
+ });
103
+
104
+ // As a tuple
105
+ const pair = Config.all([Config.string('a'), Config.int('b')]);
106
+ ```
107
+
108
+ ### `Config.nested` — Scope Under a Prefix
109
+
110
+ Prepends a path segment to every key the inner config reads. With environment variables, nesting uses `_` as separator.
111
+
112
+ ```ts
113
+ const dbConfig = Config.all({
114
+ host: Config.string('host'),
115
+ port: Config.int('port')
116
+ }).pipe(Config.nested('database'));
117
+
118
+ // Reads from env: database_host, database_port
119
+ // Or from JSON: { database: { host: "...", port: 5432 } }
120
+ ```
121
+
122
+ ## Config.schema — Structured Config from Schema
123
+
124
+ For larger configs, use `Config.schema` with a concrete `StringTree` shape. The schema's canonical encoded shape determines whether the provider loads a scalar, object, array, or each member of a mixed-shape union.
125
+
126
+ ```ts
127
+ const AppConfig = Config.schema(
128
+ Schema.Struct({
129
+ host: Schema.String,
130
+ port: Schema.Int,
131
+ debug: Schema.Boolean
132
+ })
133
+ );
134
+ ```
135
+
136
+ With an optional name parameter for nesting:
137
+
138
+ ```ts
139
+ const ServerConfig = Config.schema(
140
+ Schema.Struct({
141
+ host: Schema.String,
142
+ port: Schema.Int,
143
+ logLevel: Schema.Literals(['debug', 'info', 'warn', 'error'])
144
+ }),
145
+ 'server' // reads from server_host, server_port, server_logLevel in env
146
+ );
147
+ ```
148
+
149
+ ### Config Schemas for Use with `Config.schema`
150
+
151
+ | Schema | Type | Notes |
152
+ | --------------------------- | -------------- | ------------------------------------------ |
153
+ | `Config.Boolean` | `boolean` | Decodes `true/false/yes/no/on/off/1/0/y/n` |
154
+ | `Schema.DurationFromString` | `Duration` | Decodes duration strings; accepts `"Infinity"` / `"-Infinity"` |
155
+ | `Config.Port` | `number` | Integer in 1–65535 |
156
+ | `Config.LogLevel` | `string` | One of the standard log level literals |
157
+ | `Config.Record(key, value)` | `Record<K, V>` | Also parses flat `"k1=v1,k2=v2"` strings |
158
+
159
+ Plain `Schema.Array` and `Schema.Record` load structural provider children. Use `Config.Array` and `Config.Record` when a flat separated scalar should also be accepted. Opaque encodings such as `Schema.Any`, `Schema.Unknown`, and `Schema.Json` are rejected when `Config.schema` is constructed; use a concrete shape or `Schema.fromJsonString(Schema.Json)` to read scalar JSON.
160
+
161
+ Missing or unavailable representations are decoded as `undefined` before `Config.withDefault` and `Config.option` decide semantic absence. A successful decoded `undefined` or an explicitly present empty structure remains a real value and is not replaced by a default.
162
+
163
+ ## Two Ways to Run a Config
164
+
165
+ ### 1. Yield in `Effect.gen` — uses current ConfigProvider from service map
166
+
167
+ ```ts
168
+ const program = Effect.gen(function* () {
169
+ const host = yield* Config.string('HOST');
170
+ const port = yield* Config.int('PORT');
171
+ console.log(`${host}:${port}`);
172
+ });
173
+ ```
174
+
175
+ ### 2. Call `.parse(provider)` directly — useful for testing
176
+
177
+ ```ts
178
+ const host = Config.string('HOST');
179
+ const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
180
+ const result = Effect.runSync(host.parse(provider));
181
+ // "localhost"
182
+ ```
183
+
184
+ ## ConfigProvider Sources
185
+
186
+ ### `ConfigProvider.fromEnv` — Environment Variables (Default)
187
+
188
+ The default provider. Path segments are joined with `_` for lookup. Env var names are split on `_` to build a tree, so `DATABASE_HOST=localhost` is accessible at both `["DATABASE_HOST"]` (flat) and `["DATABASE", "HOST"]` (nested).
189
+
190
+ ```ts
191
+ // Default — reads from process.env (merged with import.meta.env when available)
192
+ // No explicit provision needed; this is the default ConfigProvider.
193
+
194
+ // For testing, pass an explicit env object:
195
+ const provider = ConfigProvider.fromEnv({
196
+ env: {
197
+ DATABASE_HOST: 'localhost',
198
+ DATABASE_PORT: '5432'
199
+ }
200
+ });
201
+ ```
202
+
203
+ Empty strings are treated as missing by default. Pass `{ preserveEmptyStrings: true }` when an empty string is an explicit value.
204
+
205
+ ### `ConfigProvider.fromEnvRecord` — Explicit Environment Records
206
+
207
+ Use `fromEnvRecord` when the environment record is supplied explicitly, especially in restricted runtimes where `fromEnv` cannot perform automatic environment detection. Unlike the `env` option of `fromEnv`, the record may contain `undefined` values; those entries are ignored.
208
+
209
+ ```ts
210
+ const provider = ConfigProvider.fromEnvRecord({
211
+ HOST: 'localhost',
212
+ PORT: '3000',
213
+ OPTIONAL_VALUE: undefined
214
+ });
215
+ ```
216
+
217
+ ### `ConfigProvider.fromUnknown` — Plain JS Objects
218
+
219
+ Ideal for testing or embedding config in code. Supports nested objects and arrays. Primitive values are automatically stringified.
220
+
221
+ ```ts
222
+ const provider = ConfigProvider.fromUnknown({
223
+ database: {
224
+ host: 'localhost',
225
+ port: 5432,
226
+ credentials: {
227
+ username: 'admin',
228
+ password: 'secret'
229
+ }
230
+ },
231
+ servers: ['server1', 'server2', 'server3']
232
+ });
233
+ ```
234
+
235
+ ### `ConfigProvider.fromDotEnvContents` — Parse `.env` Strings
236
+
237
+ Supports `export` prefixes, single/double/backtick quoting, inline comments, and escaped newlines.
238
+
239
+ ```ts
240
+ const contents = `
241
+ # Database settings
242
+ HOST=localhost
243
+ PORT=3000
244
+ SECRET="my-secret-value"
245
+ `;
246
+
247
+ const provider = ConfigProvider.fromDotEnvContents(contents);
248
+
249
+ // With variable expansion:
250
+ const provider2 = ConfigProvider.fromDotEnvContents(
251
+ `PASSWORD=secret\nDB_PASS=$PASSWORD`,
252
+ {
253
+ expandVariables: true
254
+ }
255
+ );
256
+ ```
257
+
258
+ ### `ConfigProvider.fromDotEnv` — Load `.env` Files
259
+
260
+ Reads a `.env` file from disk. Returns an Effect (requires `FileSystem` in context).
261
+
262
+ ```ts
263
+ const program = Effect.gen(function* () {
264
+ const provider = yield* ConfigProvider.fromDotEnv();
265
+ // or: yield* ConfigProvider.fromDotEnv({ path: "/custom/.env" })
266
+ return provider;
267
+ });
268
+ ```
269
+
270
+ ### `ConfigProvider.fromDir` — Directory Trees (Kubernetes ConfigMap/Secret)
271
+
272
+ Reads config from a file-system tree where each file is a leaf and each directory is a container. Requires `Path` and `FileSystem` in context.
273
+
274
+ ```
275
+ /etc/myapp/
276
+ database/
277
+ host # contains "localhost"
278
+ port # contains "5432"
279
+ api_key # contains "sk-abc123"
280
+ ```
281
+
282
+ ```ts
283
+ const program = Effect.gen(function* () {
284
+ const provider = yield* ConfigProvider.fromDir({ rootPath: '/etc/myapp' });
285
+ return provider;
286
+ });
287
+ ```
288
+
289
+ ### `ConfigProvider.make` — Custom Sources
290
+
291
+ Build a provider from any backing store. Return `undefined` for "not found". Only fail with `SourceError` for actual I/O errors.
292
+
293
+ ```ts
294
+ const data: Record<string, string> = {
295
+ host: 'localhost',
296
+ port: '5432'
297
+ };
298
+
299
+ const provider = ConfigProvider.make((path) => {
300
+ const key = path.join('.');
301
+ const value = data[key];
302
+ return Effect.succeed(
303
+ value !== undefined ? ConfigProvider.makeValue(value) : undefined
304
+ );
305
+ });
306
+ ```
307
+
308
+ ## ConfigProvider Combinators
309
+
310
+ ### `ConfigProvider.orElse` — Fallback Sources
311
+
312
+ Falls back to a second provider when the first returns `undefined` (path not found). Does **not** catch `SourceError`.
313
+
314
+ ```ts
315
+ const envProvider = ConfigProvider.fromEnv({
316
+ env: { HOST: 'prod.example.com' }
317
+ });
318
+ const defaults = ConfigProvider.fromUnknown({
319
+ HOST: 'localhost',
320
+ PORT: '3000'
321
+ });
322
+
323
+ const combined = ConfigProvider.orElse(envProvider, defaults);
324
+ ```
325
+
326
+ 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.
327
+
328
+ ### `ConfigProvider.nested` — Prefix All Lookups
329
+
330
+ Prepends path segments so that all lookups are scoped:
331
+
332
+ ```ts
333
+ const provider = ConfigProvider.fromEnv({
334
+ env: { APP_HOST: 'localhost', APP_PORT: '3000' }
335
+ });
336
+
337
+ // Lookups for ["HOST"] now resolve to ["APP", "HOST"]
338
+ const scoped = ConfigProvider.nested(provider, 'APP');
339
+ ```
340
+
341
+ ### `ConfigProvider.constantCase` — CamelCase to SCREAMING_SNAKE_CASE
342
+
343
+ Bridges camelCase schema keys to environment variable naming:
344
+
345
+ ```ts
346
+ const provider = ConfigProvider.fromEnv({
347
+ env: { DATABASE_HOST: 'localhost' }
348
+ }).pipe(ConfigProvider.constantCase);
349
+
350
+ // path ["databaseHost"] now resolves to ["DATABASE_HOST"]
351
+ ```
352
+
353
+ ### `ConfigProvider.mapInput` — Arbitrary Path Transforms
354
+
355
+ ```ts
356
+ const upper = ConfigProvider.mapInput(provider, (path) =>
357
+ path.map((seg) => (typeof seg === 'string' ? seg.toUpperCase() : seg))
358
+ );
359
+ ```
360
+
361
+ ## Installing a Provider
362
+
363
+ ### `ConfigProvider.layer` — Replace the Active Provider
364
+
365
+ ```ts
366
+ const TestLayer = ConfigProvider.layer(
367
+ ConfigProvider.fromUnknown({ port: 8080 })
368
+ );
369
+
370
+ const program = Effect.gen(function* () {
371
+ const port = yield* Config.int('port');
372
+ return port;
373
+ });
374
+
375
+ Effect.runSync(Effect.provide(program, TestLayer)); // 8080
376
+ ```
377
+
378
+ ## Config-Backed Layer Constructors
379
+
380
+ Library-style services should usually expose a concrete `layer(options)` for direct use and tests, plus `layerConfig(config)` when callers need runtime configuration. Type the latter with `Config.Wrap<Options>` and decode it once with `Config.unwrap`.
381
+
382
+ `Config.Wrap<Options>` accepts either one `Config<Options>` or a recursively wrapped object whose leaves are `Config` values. It does not accept raw concrete option values.
383
+
384
+ ```ts
385
+ export const layer = (options: ClientOptions) =>
386
+ Layer.effect(Client.Service, makeClient(options));
387
+
388
+ export const layerConfig = (config: Config.Wrap<ClientOptions>) =>
389
+ Layer.effect(
390
+ Client.Service,
391
+ Config.unwrap(config).pipe(
392
+ Effect.flatMap(makeClient),
393
+ Effect.map(Client.Service.of)
394
+ )
395
+ );
396
+ ```
397
+
398
+ Use `layer(options)` when options are already decoded. Use `layerConfig(...)` only at a configuration boundary; do not repeatedly read configuration inside business operations.
399
+
400
+ ### `ConfigProvider.layerAdd` — Add Without Replacing
401
+
402
+ By default the new provider is a **fallback**:
403
+
404
+ ```ts
405
+ // process.env is tried first; defaults is the fallback
406
+ const DefaultsLayer = ConfigProvider.layerAdd(
407
+ ConfigProvider.fromUnknown({ HOST: 'localhost', PORT: '3000' })
408
+ );
409
+
410
+ // Set { asPrimary: true } to make the new provider the primary source instead
411
+ ```
412
+
413
+ ### `Effect.provideService` — One-Off Override
414
+
415
+ ```ts
416
+ const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
417
+
418
+ const program = Effect.gen(function* () {
419
+ const host = yield* Config.string('HOST');
420
+ return host;
421
+ }).pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider));
422
+ ```
423
+
424
+ ## Testing Patterns
425
+
426
+ Always use `ConfigProvider.fromUnknown` or `ConfigProvider.fromEnvRecord({...})` in tests for deterministic, hermetic config:
427
+
428
+ ```ts
429
+ import { Config, ConfigProvider, Effect } from 'effect';
430
+
431
+ // Pattern 1: .parse(provider) for direct testing
432
+ const config = Config.all({
433
+ host: Config.string('host'),
434
+ port: Config.int('port')
435
+ });
436
+
437
+ const testProvider = ConfigProvider.fromUnknown({
438
+ host: 'localhost',
439
+ port: 5432
440
+ });
441
+
442
+ const result = Effect.runSync(config.parse(testProvider));
443
+ // { host: "localhost", port: 5432 }
444
+
445
+ // Pattern 2: ConfigProvider.layer for program-level tests
446
+ const TestConfigLayer = ConfigProvider.layer(
447
+ ConfigProvider.fromUnknown({
448
+ server: { host: 'localhost', port: 3000 },
449
+ debug: true
450
+ })
451
+ );
452
+
453
+ const program = Effect.gen(function* () {
454
+ const host = yield* Config.string('host').pipe(Config.nested('server'));
455
+ return host;
456
+ });
457
+
458
+ Effect.runSync(Effect.provide(program, TestConfigLayer));
459
+ ```
460
+
461
+ ## Error Handling
462
+
463
+ Config operations fail with `ConfigError`, which wraps either:
464
+
465
+ - **`SourceError`** — the provider could not read data (I/O failure, permission error)
466
+ - **`SchemaError`** — data was found but didn't match the schema (wrong type, out of range, missing key)
467
+
468
+ ```ts
469
+ const program = Config.int('PORT')
470
+ .parse(ConfigProvider.fromUnknown({ PORT: 'not-a-number' }))
471
+ .pipe(
472
+ Effect.tapError((error) =>
473
+ Effect.sync(() => {
474
+ if (error.cause._tag === 'SchemaError') {
475
+ console.log('Validation failed:', error.message);
476
+ } else {
477
+ console.log('Source error:', error.message);
478
+ }
479
+ })
480
+ )
481
+ );
482
+ ```
483
+
484
+ **Important**: `Config.withDefault` and `Config.option` only recover from **missing-data** errors. Validation errors still propagate.
485
+
486
+ ## Practical Example: Full Application Config
487
+
488
+ ```ts
489
+ import { Config, ConfigProvider, Effect, Schema } from 'effect';
490
+
491
+ // Define structured config sections with Config.schema
492
+ const ServerConfig = Config.schema(
493
+ Schema.Struct({
494
+ host: Schema.String,
495
+ port: Schema.Int,
496
+ logLevel: Schema.Literals(['debug', 'info', 'warn', 'error'])
497
+ }),
498
+ 'server'
499
+ );
500
+
501
+ const DbConfig = Config.schema(
502
+ Schema.Struct({
503
+ url: Schema.String,
504
+ poolSize: Schema.Int
505
+ }),
506
+ 'db'
507
+ );
508
+
509
+ // Combine with primitive configs
510
+ const AppConfig = Config.all({
511
+ server: ServerConfig,
512
+ db: DbConfig,
513
+ debug: Config.boolean('debug').pipe(Config.withDefault(false))
514
+ });
515
+
516
+ // In production — just yield it, reads from process.env
517
+ const program = Effect.gen(function* () {
518
+ const config = yield* AppConfig;
519
+ console.log(config);
520
+ });
521
+
522
+ // For testing — provide a specific provider
523
+ const testProvider = ConfigProvider.fromUnknown({
524
+ server: { host: 'localhost', port: 3000, logLevel: 'debug' },
525
+ db: { url: 'postgres://localhost/testdb', poolSize: 5 },
526
+ debug: true
527
+ });
528
+
529
+ Effect.runSync(
530
+ program.pipe(Effect.provide(ConfigProvider.layer(testProvider)))
531
+ );
532
+ ```
533
+
534
+ With environment variables, the same config reads:
535
+
536
+ ```
537
+ server_host=localhost
538
+ server_port=3000
539
+ server_logLevel=debug
540
+ db_url=postgres://localhost/mydb
541
+ db_poolSize=10
542
+ debug=true
543
+ ```
544
+
545
+ ## Anti-Patterns
546
+
547
+ ### NEVER read process.env directly
548
+
549
+ ```ts
550
+ // BAD
551
+ const port = parseInt(process.env.PORT ?? '3000');
552
+
553
+ // GOOD
554
+ const port = Config.int('PORT').pipe(Config.withDefault(3000));
555
+ ```
556
+
557
+ ### NEVER validate config manually
558
+
559
+ ```ts
560
+ // BAD
561
+ const raw = process.env.LOG_LEVEL;
562
+ if (!['debug', 'info', 'warn', 'error'].includes(raw)) throw new Error('...');
563
+
564
+ // GOOD
565
+ const logLevel = Config.schema(
566
+ Schema.Literals(['debug', 'info', 'warn', 'error']),
567
+ 'LOG_LEVEL'
568
+ );
569
+ ```
570
+
571
+ ### NEVER mock process.env in tests
572
+
573
+ ```ts
574
+ // BAD
575
+ process.env.HOST = 'localhost';
576
+
577
+ // GOOD
578
+ const provider = ConfigProvider.fromUnknown({ HOST: 'localhost' });
579
+ Effect.runSync(config.parse(provider));
580
+ ```