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,490 @@
1
+ # Effect, and the Near-Inexpressible Majesty of Layers
2
+
3
+ The precise reasons for my transfixion upon Effect's `Layer` type are difficult to compress. Now, I could flail pedagogically toward some vague zenith shouting "testability," "beauty," "dependency injection," or even "fractality!" and yet, for all my perspiring, you'd still calmly inform me that this is, has always been, and shall remain ever after an Arby's, whilst covertly spamming the silent sub-counter alarm that occasions my arrest.
4
+
5
+ This _incompressibility_ is an unfortunate aspect of all Effect curricula. This graph, prominently featured on the library's website, does a fine job of illustrating my conundrum.
6
+
7
+ > **[Graph: Complexity at Scale]** A line graph on a dark background with the x-axis running from "START" to "SCALE." Two lines are plotted. The red line, labeled "WITHOUT EFFECT," begins low and roughly flat, then curves sharply upward in an exponential growth pattern as scale increases. The green line, labeled "WITH EFFECT," starts at approximately the same level but remains nearly flat across the entire range, showing only minimal growth in complexity even at large scale.
8
+
9
+ _Do you even scale, bro?_
10
+
11
+ A certain number of concepts must first be unfurled at the prospective user's feet, and reckoned with individually, before they're able to comprehend the surprising power compacted into their gestalt — this explains the characteristic months-long incubation period between initial exposure and full-bore Effect evangelism. My usual tactic is to plead for a suspension of disbelief, to ask that they trust me as they would their own Effect-pilled grandmother, and then swiftly clap on the floodlights and turn a terrible mirror upon them!
12
+
13
+ The average TypeScript practitioner has habituated itself to a degree of suffering and uncertainty that becomes retrospectively sadomasochistic once it has sufficiently glimpsed the effectful alternative. The initial chapter of Effect Institute attempts to gently expose the listener, in their vulnerable, semi-ASMR-tenderized condition, to their many unwitting self-flagellations.
14
+
15
+ In this vein — though textually, as I'm presently traveling by aeroplane — let's countenance, most fluorescently and unflatteringly, the current state of defining and testing services in JavaScript.
16
+
17
+ ## Without Effect
18
+
19
+ Imagine the following scenario:
20
+
21
+ You're a prompt engineer at a respectable online pharmaceutical startup. One day, Claude, to whom you've long since handed unilateral control over your home automation system, suddenly begins strobing your lights whilst blasting that part of Bill Withers's "Lovely Day" where he just goes "daaaaaaaaayyyyyyyy" interminably out of every possible sound-emitting orifice in your tenement. You know well by now that there's a freshly minted Jira ticket with your name on it.
22
+
23
+ You slump off your modular sleeping platform and heave it back into its recess, simultaneously causing a desktop to extrude from the opposite wall of your tiny, squalid, loud, flashing room. You groggily survey the in-desk readout to discover that your manager's Claude has selected you as the federally mandated Human-in-the-Loop for a new "surge pricing" feature (_As a customer with a rare and luxurious disease, I want pricing to vary with demand such that my medication is safeguarded from the underemployed_). You ssh into a very distant place.
24
+
25
+ ### Claude's Code
26
+
27
+ In a matter of milliseconds, you're connected, and a commit is already waiting for you.
28
+
29
+ You begin your review.
30
+
31
+ ```bash
32
+ commit 9f8e7d6c5b4a3
33
+ Author: Claude <claude@anthropic.com>
34
+ Date: Mon Jan 27 04:32:00 2031 -0500
35
+
36
+ feat(pricing): implement surge pricing for high-demand medications [PHARM-5485]
37
+
38
+ Approved-By-HITL: MISSING
39
+ ```
40
+
41
+ Your Claude has added a new `feature-flags.ts` file. This exports an `isEnabled` function that simply fetches the given feature flag's state from an approved vendor. So far, so good.
42
+
43
+ ```typescript
44
+ // feature-flags.ts
45
+ export const isEnabled = async (flag: string): Promise<boolean> => {
46
+ const res = await fetch(`https://api.flags.com/flags/${flag}`, {
47
+ headers: { Authorization: process.env.FLAGS_API_KEY! }
48
+ });
49
+ const data = await res.json();
50
+ return data.enabled;
51
+ };
52
+ ```
53
+
54
+ You press your thumb into a concavity near the right edge of the slab-screen. There's a pinch as a dram of blood is extracted, whereupon the still-blaring "-aaaaaaaaaayyyyy" dips to a marginally less deafening volume.
55
+
56
+ "Hunk one complete. Twenty-four hunks remaining."
57
+
58
+ Next up, the `pricing` module uses `isEnabled` to determine whether or not surge pricing should be applied and, if so, applies a modest markup.
59
+
60
+ ```typescript
61
+ // pricing.ts
62
+ import { isEnabled } from './feature-flags';
63
+
64
+ export const getPrice = async (basePrice: number): Promise<number> => {
65
+ if (await isEnabled('surge-pricing')) {
66
+ return basePrice * 6.5; // 550% markup
67
+ }
68
+ return basePrice;
69
+ };
70
+ ```
71
+
72
+ You automatically offer your thumb to the concavity. A second driblet of blood is drawn out of you and into the hemobiometrical machine.
73
+
74
+ "Hunk two complete. Twenty-three hunks remaining."
75
+
76
+ The next hunk reveals a test file. You lean in, curious how Claude intends to probe a function with a network dependency. A cursory glance across the buffer tightens your throat and wicks the moisture from your tongue. There is mocking afoot.
77
+
78
+ ```typescript
79
+ // pricing.test.ts
80
+ import { vi, describe, it, expect } from 'vitest';
81
+
82
+ vi.mock('./feature-flags', () => ({ isEnabled: vi.fn() }));
83
+
84
+ import { isEnabled } from './feature-flags';
85
+ import { getPrice } from './pricing';
86
+
87
+ describe('getPrice', () => {
88
+ it('applies surge pricing when enabled', async () => {
89
+ vi.mocked(isEnabled).mockResolvedValue(true);
90
+ expect(await getPrice(100)).toBe(650);
91
+ });
92
+
93
+ it('returns base price when surge pricing is disabled', async () => {
94
+ vi.mocked(isEnabled).mockResolvedValue(false);
95
+ expect(await getPrice(100)).toBe(100);
96
+ });
97
+ });
98
+ ```
99
+
100
+ Claude has ingested this pattern countless times throughout his debauched pre-training data bender. Yet prevalence should not be mistaken for soundness. As the man himself might put it: this isn't just a footgun — it's a foot armada! You're absolutely fucked.
101
+
102
+ And how is it that you are, in this regard, fucked? Let us enumerate.
103
+
104
+ Beyond the perverted mechanisms by which these mocking libraries operate, they're inherently limited, especially with regard to type-safety. For instance, what if you misspell the file name? What if you rename the file in the future, and forget to update the call to `mock`? Well, nothing would happen; the mock would miss its target and you'd be none the wiser.
105
+
106
+ ```typescript
107
+ vi.mock('./future-flaps', () => ({ isEnabled: vi.fn() }));
108
+ ```
109
+
110
+ What if you fat-finger the function name? Then you'd be mocking an imaginary `isEnable` function, while `isEnabled` runs for real in your tests.
111
+
112
+ ```typescript
113
+ vi.mock('./feature-flags', () => ({ isEnable: vi.fn() }));
114
+ ```
115
+
116
+ What if your mock returns the wrong type? The mocking API is entirely untethered from the type system. If you return a `string` where a `boolean` is expected, it'll type-check all the same.
117
+
118
+ ```typescript
119
+ vi.mocked(isEnabled).mockResolvedValue('sure');
120
+ ```
121
+
122
+ Further, the signature of `getPrice` gives no indication whatsoever of its dependence upon feature flags. Nothing at the type-level apprises us of this relationship.
123
+
124
+ ```typescript
125
+ export const getPrice = async (basePrice: number): Promise<number> // ???
126
+ ```
127
+
128
+ Wait, do I hear a scoff? Is that the telltale clink of a monocle hitting your teacup? Do you mutter into the mahogany cuff of your caviar sport coat, "Blasphemy! This is not the domain of a type checker. Types are trifling, decorative things like Shetland ponies and ceramic frogs, to be placed about the codebase for our amusement!"
129
+
130
+ I disagree, my friend. Especially in this age of the agent managerial class, types force both ourselves and our ephemeral, jaggedly-doltish factotums to deal with reality as it stands. They let us validate the fundamental cohesion of our programs long before they run amok in production. They're a form of documentation that cannot drift, as they are themselves the very source of truth worth documenting. In our feature flag scenario, a Claude couldn't know these implicit, inter-module dependencies without first reading each and every file in full, clogging its precious, small context with mostly irrelevant junk.
131
+
132
+ Try to hold in your mind these many inadequacies as we consider the effectful model.
133
+
134
+ ## With Effect
135
+
136
+ Effect affords a beautifully regular and self-similar architecture. Its approach to dependency injection, in particular, makes testing even the most complicated async code nearly trivial.
137
+
138
+ There is, of course, the aforementioned obstacle of syntactic and conceptual overload (if you know nothing of Effect, I'd recommend working through the first two chapters of my little Institute). By the end of this, you should fully understand `Context.Service`, `Layer`, and our definition of _service_.
139
+
140
+ ### Defining a Service
141
+
142
+ Think of `Context.Service` as an effectful `interface`. Here, we define a `FeatureFlags` service, composed of a type, a string key, and the shape of the interface it represents.
143
+
144
+ In plain TypeScript, you might write:
145
+
146
+ ```typescript
147
+ interface FeatureFlags {
148
+ readonly isEnabled: (flag: string) => Promise<boolean>;
149
+ }
150
+ ```
151
+
152
+ The Effect equivalent:
153
+
154
+ ```typescript
155
+ class FeatureFlags extends Context.Service<
156
+ FeatureFlags,
157
+ {
158
+ readonly isEnabled: (flag: string) => Effect.Effect<boolean>;
159
+ }
160
+ >()('FeatureFlags') {}
161
+ ```
162
+
163
+ Behold, our abstract service definition, sans implementation. I admit there is some unpleasant syntactic overgrowth (e.g., `FeatureFlags` in triplicate). Alas, for the behavior we desire, this is unavoidable in TypeScript, our otherwise most gracious host language. If you could forestall your recoiling until you've seen the full picture, I don't believe you'll regret it.
164
+
165
+ ### Implementing a Service
166
+
167
+ Think of a `Layer` as an effectful constructor for our service. Here we define a test implementation of `FeatureFlags`, parameterized by its enabled flag names.
168
+
169
+ ```typescript
170
+ const featureFlagsTestLayer = (
171
+ ...enabled: string[]
172
+ ): Layer.Layer<FeatureFlags> =>
173
+ Layer.succeed(
174
+ FeatureFlags,
175
+ FeatureFlags.of({
176
+ isEnabled: (flag) => Effect.succeed(enabled.includes(flag))
177
+ })
178
+ );
179
+ ```
180
+
181
+ Our layer is defined as a function so that we can provide different sets of flags on a per-test basis. `Layer.succeed` is used because we're directly providing a concrete implementation (this mirrors `Effect.succeed`). We must pass our `FeatureFlags` tag as the first argument for reasons we'll get into later; for now, please accept this as part of the ritual.
182
+
183
+ ### Defining a Second Service
184
+
185
+ Next, let's define `Pricing` as a service. Once more, we extend `Context.Service` and specify the interface:
186
+
187
+ ```typescript
188
+ class Pricing extends Context.Service<
189
+ Pricing,
190
+ {
191
+ readonly getPrice: (basePrice: number) => Effect.Effect<number>;
192
+ }
193
+ >()('Pricing') {}
194
+ ```
195
+
196
+ Most applications naturally decompose into services. This is the self-similar, fractal quality I alluded to earlier. To begin reaping the benefits of this pattern, let's implement a layer (read: an effectful constructor) for our `Pricing` service.
197
+
198
+ ```typescript
199
+ // 1. Define the layer
200
+ const pricingLayer = Layer.effect(
201
+ Pricing,
202
+ Effect.gen(function* () {
203
+ // 2. Grab hold of our transitive dependencies
204
+ const flags = yield* FeatureFlags;
205
+
206
+ // 3. Close over these dependencies in our implementation
207
+ const getPrice = Effect.fn(function* (basePrice: number) {
208
+ const surging = yield* flags.isEnabled('surge-pricing');
209
+ return surging ? basePrice * 6.5 : basePrice;
210
+ });
211
+
212
+ // 4. Return the implementation
213
+ return Pricing.of({ getPrice });
214
+ })
215
+ );
216
+ ```
217
+
218
+ This time we use `Layer.effect` because our implementation needs to acquire another service at construction time. Inside `Effect.gen`, we write `yield* FeatureFlags` to obtain an instance of the `FeatureFlags` interface.
219
+
220
+ Let's look at the type of our `pricingLayer`:
221
+
222
+ ```typescript
223
+ Layer.Layer<Pricing, never, FeatureFlags>;
224
+ // ^^^^^^^^ what it provides
225
+ // ^^^^^ how it can fail
226
+ // ^^^^^^^^^^^^^^ what it requires
227
+ ```
228
+
229
+ As a consequence of our plucking `FeatureFlags` out of the air with `yield*`, it is tracked at the type-level. It is now impossible to run a program that uses this layer without also providing an implementation of `FeatureFlags`. Let's take a brief aside to see what this means in practice.
230
+
231
+ ### An Interlude on the Introduction and Elimination of Requirements
232
+
233
+ If this type-level tracking of requirements feels unfamiliar, consider that you already do this every day in TypeScript. A function parameter is an unsatisfied requirement, and calling a function with an argument eliminates it. All of your intuitions around function types should map cleanly onto Effects and Layers.
234
+
235
+ Just as a function's parameters signify its requirements, an Effect's third type parameter serves the same purpose.
236
+
237
+ ```typescript
238
+ const useConfigFunction: (config: Config) => string = ...
239
+ // ^^^^^^ ^^^^^^
240
+ // requires returns
241
+
242
+ const useConfigEffect: Effect.Effect<string, never, Config> = ...
243
+ // ^^^^^^ ^^^^^^
244
+ // returns requires
245
+ ```
246
+
247
+ Calling a function with an argument eliminates the requirement; providing a Layer does the same for an Effect.
248
+
249
+ ```typescript
250
+ const config: Config = ...
251
+ const result: string = useConfigFunction(config)
252
+ // ^^^^^^
253
+ // fully satisfied
254
+
255
+ const configLayer: Layer.Layer<Config> = ...
256
+ const effect: Effect.Effect<string, never, never> = useConfigEffect.pipe(Effect.provide(configLayer))
257
+ // ^^^^^
258
+ // fully satisfied
259
+ ```
260
+
261
+ One difference worth noting is that calling a function executes it immediately, whereas `Effect.provide` merely satisfies the requirement without running anything. The more honest functional analog would be closing over the argument in a thunk, which would likewise remain inert until explicitly invoked:
262
+
263
+ ```typescript
264
+ const thunk: () => string = () => useConfigFunction(config);
265
+
266
+ const effect: Effect.Effect<string> = useConfigEffect.pipe(
267
+ Effect.provide(configLayer)
268
+ );
269
+ ```
270
+
271
+ Let's put this all together. First, we'll define a simple `Random` service by extending `Context.Service` to specify our interface:
272
+
273
+ ```typescript
274
+ class Random extends Context.Service<
275
+ Random,
276
+ {
277
+ readonly nextNumber: Effect.Effect<number>;
278
+ }
279
+ >()('Random') {}
280
+ ```
281
+
282
+ Next, we give it an implementation. Actually, let's give it two: a real one that delegates to `Math.random`, and a fixed one for testing that always returns whatever number you give it.
283
+
284
+ ```typescript
285
+ const randomLayer = Layer.succeed(
286
+ Random,
287
+ Random.of({
288
+ nextNumber: Effect.sync(() => Math.random())
289
+ })
290
+ );
291
+
292
+ const fixedRandomLayer = (n: number) =>
293
+ Layer.succeed(
294
+ Random,
295
+ Random.of({
296
+ nextNumber: Effect.succeed(n)
297
+ })
298
+ );
299
+ ```
300
+
301
+ Finally, we define a program that uses the `Random` service:
302
+
303
+ ```typescript
304
+ const coinFlip: Effect.Effect<string, never, Random> = Effect.gen(function* () {
305
+ const random = yield* Random;
306
+ const n = yield* random.nextNumber;
307
+ return n > 0.5 ? 'heads' : 'tails';
308
+ });
309
+ ```
310
+
311
+ Our type, as expected, indicates that `coinFlip` requires `Random`. If we naively attempt to run it without providing that dependency, the compiler protests.
312
+
313
+ ```typescript
314
+ Effect.runPromise(coinFlip);
315
+ // Type Error: Missing 'Random' in the expected Effect context.
316
+ ```
317
+
318
+ To satisfy the requirement, we use `Effect.provide` and pass in our `Math.random`-backed implementation.
319
+
320
+ ```typescript
321
+ const effect = coinFlip.pipe(Effect.provide(randomLayer));
322
+ // effect: Effect.Effect<string, never, never>
323
+ // ^^^^^ ready to run!
324
+ ```
325
+
326
+ Now that we have provided all of our dependencies, we can run it.
327
+
328
+ ```typescript
329
+ Effect.runPromise(effect).then(console.log);
330
+ // => "heads" or "tails"
331
+ ```
332
+
333
+ Or, if we want deterministic behavior for testing, we provide the fixed layer instead:
334
+
335
+ ```typescript
336
+ const testable = coinFlip.pipe(Effect.provide(fixedRandomLayer(1)));
337
+
338
+ Effect.runPromise(testable).then(console.log);
339
+ // => "heads" (always, since 1 > 0.5)
340
+ ```
341
+
342
+ And to complete our tangent, we should cover what happens when a layer for a service depends upon another service, just like our `pricingLayer` depends on `FeatureFlags`.
343
+
344
+ We could, just for fun, define an implementation of `Random` that asks the user to input a number via a terminal prompt. Let's implement this with Effect's `Terminal` service:
345
+
346
+ ```typescript
347
+ import { Terminal } from 'effect';
348
+
349
+ const terminalRandomLayer = Layer.effect(
350
+ Random,
351
+ Effect.gen(function* () {
352
+ const terminal = yield* Terminal.Terminal;
353
+
354
+ const nextNumber = Effect.gen(function* () {
355
+ yield* terminal.display('Enter a number: ');
356
+ const input = yield* terminal.readLine;
357
+ const n = parseFloat(input);
358
+ if (isNaN(n)) {
359
+ return yield* Effect.fail('Invalid number');
360
+ }
361
+ return n;
362
+ }).pipe(Effect.eventually); // keep asking until valid
363
+
364
+ return Random.of({ nextNumber });
365
+ })
366
+ );
367
+ // terminalRandomLayer: Layer.Layer<Random, never, Terminal.Terminal>
368
+ // ^^^^^^^^^^^^^^^^^
369
+ // depends on Terminal!
370
+ ```
371
+
372
+ Because we `yield* Terminal.Terminal` inside the layer, our `terminalRandomLayer` now requires `Terminal`, which is reflected at the type-level. To use it, we must provide a `Terminal` implementation to our `terminalRandomLayer`.
373
+
374
+ ```typescript
375
+ import { NodeTerminal } from '@effect/platform-node';
376
+
377
+ const appLayer: Layer.Layer<Random> = terminalRandomLayer.pipe(
378
+ Layer.provide(NodeTerminal.layer)
379
+ );
380
+ ```
381
+
382
+ And then we can provide this composed layer to our `coinFlip` program:
383
+
384
+ ```typescript
385
+ const program = coinFlip.pipe(
386
+ Effect.replicateEffect(3),
387
+ Effect.provide(appLayer)
388
+ );
389
+ ```
390
+
391
+ Running this program prompts us three times and returns an array of results:
392
+
393
+ ```text
394
+ $ bun main.ts
395
+ Enter a number: 0.8
396
+ Enter a number: 0.2
397
+ Enter a number: 0.6
398
+ [ "heads", "tails", "heads" ]
399
+ ```
400
+
401
+ ### Don't Worry, it's Almost Over
402
+
403
+ With that out of the way, let's return to testing our pharmaceutical surge pricing feature.
404
+
405
+ Recall that `pricingLayer` depends on `FeatureFlags`. So, to test `Pricing` in isolation, we'll start by defining a helper function that returns a fully satisfied `Layer.Layer<Pricing>` by providing our in-memory `FeatureFlags` implementation to our `pricingLayer`:
406
+
407
+ ```typescript
408
+ const testLayer = (...enabled: string[]): Layer.Layer<Pricing> =>
409
+ pricingLayer.pipe(Layer.provide(featureFlagsTestLayer(...enabled)));
410
+ ```
411
+
412
+ This will let us vary the enabled feature flags per test. Now, to actually write the tests, we'll make use of the `@effect/vitest` module.
413
+
414
+ ```typescript
415
+ describe('getPrice', () => {
416
+ it.effect('applies surge pricing when enabled', () =>
417
+ Effect.gen(function* () {
418
+ const pricing = yield* Pricing;
419
+ const price = yield* pricing.getPrice(100);
420
+ expect(price).toBe(650);
421
+ }).pipe(Effect.provide(testLayer('surge-pricing')))
422
+ );
423
+
424
+ it.effect('returns base price when disabled', () =>
425
+ Effect.gen(function* () {
426
+ const pricing = yield* Pricing;
427
+ const price = yield* pricing.getPrice(100);
428
+ expect(price).toBe(100);
429
+ }).pipe(Effect.provide(testLayer()))
430
+ );
431
+ });
432
+ ```
433
+
434
+ Here we `yield* Pricing` and use the returned service to get the price. In the first test, we check that surge pricing applies a hefty markup. In the second, we verify the base price is returned when the flag is disabled.
435
+
436
+ All of our tests pass, and everything is type-safe:
437
+
438
+ ```text
439
+ ✓ pricing.test.ts (2 tests) 4ms
440
+ ✓ getPrice > applies surge pricing when enabled
441
+ ✓ getPrice > returns base price when disabled
442
+ ```
443
+
444
+ We cannot forget to provide an implementation of `FeatureFlags`, and we cannot misspell a service or a function because we're relying on the actual services instead of some diabolical import swizzling. Furthermore, it is trivial (and fun!) to design curated test implementations of our services, rather than artlessly slapping together brittle and ad hoc mocks.
445
+
446
+ ## Under the Hood
447
+
448
+ Now that you're fully convinced of the supremacy of Effect and its attendant abstractions, you are free to go forth and rewrite all of your projects and sequester yourself from all effect-less nonbelievers. However, before I release you, I'm obliged to explain a bit of how this all works internally.
449
+
450
+ The Effect runtime, most famous for running Effects, implicitly provides a `Context` to every `Effect`. This `Context` is basically a map from `string` keys to `any` values.
451
+
452
+ ```typescript
453
+ // A simplified mental model
454
+ type Context = Map<string, any>;
455
+ ```
456
+
457
+ When we defined `FeatureFlags` with `Context.Service<FeatureFlags>()('FeatureFlags')`, that `'FeatureFlags'` string is what serves as the key in this map. Later, when we implemented our service with `Layer.succeed` and `Layer.effect`, we needed to provide the same key to these functions because a `Layer` is (modulo a few details) an `Effect` that returns a `Context` map.
458
+
459
+ ```typescript
460
+ // A very simplified implementation of Layer.succeed
461
+ const succeed = (tag, impl) =>
462
+ Effect.sync(() => {
463
+ // e.g., [["FeatureFlags", { isEnabled: ... }]]
464
+ return new Map([[tag.key, impl]]);
465
+ });
466
+
467
+ // A very simplified implementation of Layer.effect
468
+ const effect = (tag, make) =>
469
+ Effect.gen(function* () {
470
+ const impl = yield* make;
471
+ // e.g., [["FeatureFlags", { isEnabled: ... }]]
472
+ return new Map([[tag.key, impl]]);
473
+ });
474
+ ```
475
+
476
+ Then, when we finally `yield* FeatureFlags`, what we're really doing is just indexing into that ambient, runtime-provided `Context` with our tag's key.
477
+
478
+ ```typescript
479
+ // A simplified mental model of yield* FeatureFlags
480
+ const context = yield* Effect.context();
481
+ const flags = context.get(FeatureFlags.key)!; // => { isEnabled: ... }
482
+ ```
483
+
484
+ This might sound unsafe, but it isn't, because the whole system is correct by construction. Whenever you `yield*` a tag, its type propagates to the surrounding Effect's requirements. Conversely, when you `provide` a layer, its output's type is eliminated from the underlying Effect's requirements. Layer constructors, like `Layer.succeed` and `Layer.effect`, are constrained by the type of the provided tag, forcing the implementation to match the expected interface. If your program type-checks, every lookup is guaranteed to succeed with the correct type.
485
+
486
+ ## The End
487
+
488
+ There is something deeply calming, to me at least, about the regularity of it all. Much of your app can be expressed as interlocking services, with their interfaces specified with `Tag`s and implemented with `Layer`s. Once a handful of novel concepts are absorbed — a process which I hope to have accelerated here — you'll have a design primitive that can be composed into arbitrarily complex applications, as trees of services with explicit, transitive dependencies, all while remaining perfectly type-safe and eminently testable.
489
+
490
+ The resulting uniformity is a useful property, both for us humans and our robot friends. The question of application structure is resoundingly answered, and we're freed up to concern ourselves with more interesting and meaningful problems. Take care, and check out Effect Institute.
@@ -0,0 +1,109 @@
1
+ # Parse, don’t validate
2
+
3
+ Alexis King’s mantra for type-driven design, condensed.
4
+
5
+ ## Partial functions to total functions
6
+
7
+ Consider `head :: [a] -> a` — return the first element of a list. It can’t be implemented totally; `[]` has no element to return. Two ways to fix it.
8
+
9
+ ### Weaken the result
10
+
11
+ ```haskell
12
+ head :: [a] -> Maybe a
13
+ head (x:_) = Just x
14
+ head [] = Nothing
15
+ ```
16
+
17
+ Easy to implement, but every call site must handle `Nothing` — even when the caller has already proved the list non-empty:
18
+
19
+ ```haskell
20
+ getConfigurationDirectories :: IO [FilePath]
21
+ getConfigurationDirectories = do
22
+ configDirsString <- getEnv "CONFIG_DIRS"
23
+ let configDirsList = split ',' configDirsString
24
+ when (null configDirsList) $
25
+ throwIO $ userError "CONFIG_DIRS cannot be empty"
26
+ pure configDirsList
27
+
28
+ main :: IO ()
29
+ main = do
30
+ configDirs <- getConfigurationDirectories
31
+ case head configDirs of
32
+ Just cacheDir -> initializeCache cacheDir
33
+ Nothing -> error "should never happen; already checked configDirs is non-empty"
34
+ ```
35
+
36
+ That `Nothing` branch is dead code we can’t statically prove dead. If `getConfigurationDirectories` ever stops checking, the “impossible” silently becomes possible.
37
+
38
+ ### Strengthen the argument
39
+
40
+ Instead of `[a]`, take `NonEmpty a`:
41
+
42
+ ```haskell
43
+ head :: NonEmpty a -> a
44
+ head (x:|_) = x
45
+ ```
46
+
47
+ `head` is now total. The non-empty check happens exactly once, at the boundary:
48
+
49
+ ```haskell
50
+ getConfigurationDirectories :: IO (NonEmpty FilePath)
51
+ getConfigurationDirectories = do
52
+ configDirsString <- getEnv "CONFIG_DIRS"
53
+ let configDirsList = split ',' configDirsString
54
+ case nonEmpty configDirsList of
55
+ Just nonEmptyConfigDirsList -> pure nonEmptyConfigDirsList
56
+ Nothing -> throwIO $ userError "CONFIG_DIRS cannot be empty"
57
+
58
+ main :: IO ()
59
+ main = do
60
+ configDirs <- getConfigurationDirectories
61
+ initializeCache (head configDirs)
62
+ ```
63
+
64
+ If the upstream check is removed, the return type changes and `main` stops type-checking. The knowledge is preserved in the type system.
65
+
66
+ ## Parsing vs validation
67
+
68
+ Two almost-identical functions distinguished only by their return type:
69
+
70
+ ```haskell
71
+ validateNonEmpty :: [a] -> IO ()
72
+ validateNonEmpty (_:_) = pure ()
73
+ validateNonEmpty [] = throwIO $ userError "list cannot be empty"
74
+
75
+ parseNonEmpty :: [a] -> IO (NonEmpty a)
76
+ parseNonEmpty (x:xs) = pure (x:|xs)
77
+ parseNonEmpty [] = throwIO $ userError "list cannot be empty"
78
+ ```
79
+
80
+ `validateNonEmpty` checks then throws the knowledge away. `parseNonEmpty` returns a refined type that carries the proof forward. **A parser is just a function from less-structured input to more-structured output, with a notion of failure.** Once parsing is done, downstream code never has to re-check.
81
+
82
+ ## The danger of validation: shotgun parsing
83
+
84
+ From *The Seven Turrets of Babel: A Taxonomy of LangSec Errors*:
85
+
86
+ > Shotgun parsing is a programming antipattern whereby parsing and input-validating code is mixed with and spread across processing code—throwing a cloud of checks at the input, and hoping, without any systematic justification, that one or another would catch all the “bad” cases.
87
+ >
88
+ > Shotgun parsing necessarily deprives the program of the ability to reject invalid input instead of processing it. Late-discovered errors in an input stream will result in some portion of invalid input having been processed, with the consequence that program state is difficult to accurately predict.
89
+
90
+ Validation-based code can’t tell you whether all the checks really happened up front, so every call site must assume failure is possible everywhere. Parsing stratifies the program: invalid input is rejected in one phase; execution can’t fail for the same reason.
91
+
92
+ ## In practice
93
+
94
+ Focus on the datatypes.
95
+
96
+ 1. **Use a data structure that makes illegal states unrepresentable.** If `[(k, v)]` allows duplicate keys and you don’t want them, take `Map k v` instead. Refactor upward until you reach either the value’s origin or a point where the looser shape is genuinely needed; insert the parsing step there.
97
+ 2. **Push the burden of proof upward as far as possible, but no further.** Parse at the system boundary. When a branch later needs a more precise shape, parse the moment that branch is selected. Use sum types so the datatype reflects control flow.
98
+
99
+ Write functions on the data representation you *wish* you had, not the one you were given.
100
+
101
+ Additional rules of thumb:
102
+
103
+ - Let datatypes drive your code, not the reverse. Don’t reach for a `Bool` field because it’s convenient — pick the representation that makes the invariant obvious, and let the type checker chase down the call sites.
104
+ - Treat functions that return `m ()` with suspicion. If the only point of the effect is raising an error, there’s usually a return value worth preserving.
105
+ - Parse in multiple passes when you need to. Context-sensitive parsing — using already-parsed input to decide how to parse the rest — is fine; that’s not shotgun parsing.
106
+ - Avoid denormalized representations, especially mutable ones; duplicated state is trivially representable illegal state. Where denormalization is unavoidable, keep it behind an abstraction boundary owned by one small module.
107
+ - When the type system can’t directly express the invariant (e.g. “this integer is in range”), use an abstract `newtype` with a smart constructor so a validator presents a parser-shaped API.
108
+
109
+ Treat `error "impossible"` calls and undocumented invariants as radioactive: handle with care, and at minimum leave a comment recording the invariant.
@@ -0,0 +1,38 @@
1
+ # Agent Rules
2
+
3
+ Load all relevant skills before writing or planning any code. Effect is a massive ecosystem — without loading skills you will write outdated v3 code or miss high-leverage libraries. Load AT LEAST 4 `effect-*` skills before any Effect work.
4
+
5
+ When skills leave any ambiguity, or when you encounter unfamiliar APIs during implementation, read the OpenCode `effect` reference at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Treat this reference as the source of truth over `node_modules`, stale external docs, or memory.
6
+
7
+ ## Skill routing
8
+
9
+ Load every branch that the task crosses:
10
+
11
+ - Schemas, brands, variants, optionality, or decoding: `effect-schema-v4`, `effect-schema-composition`, and `effect-domain-modeling`.
12
+ - Services, layers, runtime wiring, or scoped lifetimes: `effect-service-implementation`, `effect-layer-design`, `effect-scope`, and `effect-fiber`.
13
+ - Configuration or secrets: `effect-config`.
14
+ - Retry, repeat, polling, backoff, pacing, or recurrence: `effect-scheduling` plus the relevant error, HTTP, or testing skill.
15
+ - Memoization, keyed caches, or request batching: `effect-cache` and `effect-batching`.
16
+ - Streams, queues, pubsubs, pagination, or backpressure: `effect-stream` and the relevant concurrency skill.
17
+ - Outgoing HTTP: `effect-http-client` plus the relevant platform-layer skill.
18
+ - Effect tests, virtual time, or concurrent synchronization: `effect-testing` and `effect-concurrency-testing`.
19
+
20
+ ## Start here
21
+
22
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/LLMS.md` — generated task-oriented guide for Effect v4, with links to examples.
23
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/ai-docs/src/` — source examples behind `LLMS.md`, organized by topic; use when you need the full runnable snippet.
24
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/src/` — actual Effect source code for any module; use this when docs and skills disagree.
25
+
26
+ ## Major user-facing guides
27
+
28
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/SCHEMA.md` — full Schema reference.
29
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/HTTPAPI.md` — HttpApi, HttpApiClient, HttpApiBuilder, middleware, security, and OpenAPI docs.
30
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/CONFIG.md` — `Config` and `ConfigProvider` guide.
31
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/MCP.md` — MCP server resources, prompts, tools, and transports.
32
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/effect/OPTIC.md` — `Optic` guide for lenses, prisms, optionals, traversals, and schema isos.
33
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/packages/vitest/README.md` — `@effect/vitest` testing guide.
34
+ - `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/cookbooks/schedule.md` — Schedule cookbook; read before designing retries, repeats, polling, backoff, jitter, timeouts, or recurrence limits.
35
+
36
+ Do not use migration notes, Effect-repo contributor patterns, or in-repo specs as general application guidance. Read those only when the task is explicitly about migrating old Effect code or contributing to the Effect repository itself.
37
+
38
+ Do not guess at Effect v4 APIs. If uncertain, read the reference first.