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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
# Effect-First Development
|
|
2
|
+
|
|
3
|
+
This document defines the working model behind Effect-first code using the Effect v4 ecosystem.
|
|
4
|
+
|
|
5
|
+
## Definition
|
|
6
|
+
|
|
7
|
+
Effect-first development means domain code is written in Effect-native constructs first, and native JavaScript/TypeScript patterns only at explicit boundaries.
|
|
8
|
+
|
|
9
|
+
The goal is to make failure, absence, decoding, and dependency wiring explicit and typed.
|
|
10
|
+
|
|
11
|
+
## Primary References
|
|
12
|
+
|
|
13
|
+
- [Effect documentation](https://effect.website/docs)
|
|
14
|
+
- [Effect v4 GitHub](https://github.com/Effect-TS/effect)
|
|
15
|
+
- [Effect Schema docs](https://effect.website/docs/schema/introduction)
|
|
16
|
+
|
|
17
|
+
## Operating Model
|
|
18
|
+
|
|
19
|
+
Use three layers:
|
|
20
|
+
|
|
21
|
+
1. Boundary layer:
|
|
22
|
+
- Parse and decode unknown input with `Schema.decodeUnknown*`.
|
|
23
|
+
- Convert nullish values to `Option`.
|
|
24
|
+
- Convert throwable/rejecting APIs to typed Effect failures.
|
|
25
|
+
2. Domain layer:
|
|
26
|
+
- Use Effect modules (`Arr`, `Option`, `R`, `Schema`, `Str`, `HashMap`, `HashSet`) and typed services.
|
|
27
|
+
- Keep business logic pure, explicit, and exhaustive.
|
|
28
|
+
3. Runtime layer:
|
|
29
|
+
- Compose layers and run effects.
|
|
30
|
+
- Keep platform concerns (process, filesystem, env, network) outside core domain logic.
|
|
31
|
+
|
|
32
|
+
At boundaries:
|
|
33
|
+
|
|
34
|
+
- Keep transport handlers thin: decode input, read context, call application services, and map typed outcomes to transport responses.
|
|
35
|
+
- Put business rules in domain functions or services, not HTTP handlers or framework callbacks.
|
|
36
|
+
- Wrap HTTP clients, SDKs, CLIs, and other integrations in named adapter effects that own translation into domain values and errors.
|
|
37
|
+
- Decode persisted rows when they are not already trusted, refined domain values.
|
|
38
|
+
- Keep network and provider calls outside authoritative database transactions.
|
|
39
|
+
- Recover or retry only where the boundary has a truthful response. Retry only proven-idempotent operations, and keep exhausted failures visible unless a real fallback exists.
|
|
40
|
+
|
|
41
|
+
## Laws and Conventions
|
|
42
|
+
|
|
43
|
+
### EF-1: Errors are data, not side effects
|
|
44
|
+
|
|
45
|
+
- If logic can fail, return `Effect.Effect<A, E, R>` with a typed error `E`.
|
|
46
|
+
- Use `Schema.TaggedError` for public or cross-module failures.
|
|
47
|
+
- Do not `throw` or use `new Error(...)` in production domain logic.
|
|
48
|
+
- Do not use `try { } catch` blocks in Effect code; use `Effect.try` or `Effect.tryPromise` to capture throwable operations into the typed error channel.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { Effect } from 'effect';
|
|
54
|
+
import * as Option from 'effect/Option';
|
|
55
|
+
import * as Schema from 'effect/Schema';
|
|
56
|
+
|
|
57
|
+
class MissingConfigError extends Schema.TaggedError<MissingConfigError>()(
|
|
58
|
+
'MissingConfigError',
|
|
59
|
+
{ key: Schema.String },
|
|
60
|
+
{ description: 'Required configuration key is missing' }
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
const requireEnv = (key: string) =>
|
|
64
|
+
Effect.sync(() => process.env[key]).pipe(
|
|
65
|
+
Effect.flatMap((value) =>
|
|
66
|
+
Option.match(Option.fromNullishOr(value), {
|
|
67
|
+
onNone: () => Effect.fail(new MissingConfigError({ key })),
|
|
68
|
+
onSome: Effect.succeed
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### EF-2: Absence is `Option`
|
|
75
|
+
|
|
76
|
+
- Inside domain code, avoid `| null` and `| undefined`.
|
|
77
|
+
- Convert nullable values at boundaries via `Option.fromNullishOr`.
|
|
78
|
+
- Consume via `Option.map`, `Option.flatMap`, `Option.match`, `Option.getOrElse`.
|
|
79
|
+
- Do not use `Option.getOrThrow` — it defeats the purpose of `Option`. Always handle both cases explicitly.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { pipe } from 'effect';
|
|
85
|
+
import * as Option from 'effect/Option';
|
|
86
|
+
|
|
87
|
+
const toDisplayName = (rawName: string | null | undefined) =>
|
|
88
|
+
pipe(
|
|
89
|
+
Option.fromNullishOr(rawName),
|
|
90
|
+
Option.map((name) => name.trim()),
|
|
91
|
+
Option.filter((name) => name.length > 0),
|
|
92
|
+
Option.getOrElse(() => 'anonymous')
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### EF-3: Decode unknown input with `Schema`
|
|
97
|
+
|
|
98
|
+
- Unknown or external data must be decoded at the boundary.
|
|
99
|
+
- Prefer `Schema.decodeUnknownEffect` for effectful paths and `Schema.decodeUnknownSync` only where sync failure handling is explicit.
|
|
100
|
+
- Never use `JSON.parse` / `JSON.stringify`; use schema JSON codecs (`Schema.fromJsonString`, `Schema.decodeUnknown*`, `Schema.encode*`). For unknown JSON, use `Schema.fromJsonString(Schema.Unknown)`.
|
|
101
|
+
- Prefer `Schema.Class` over `Schema.Struct` for all decoded shapes — including HTTP response bodies, API payloads, and ephemeral wire formats, not just domain models. Named `Schema.Class` types enable `instanceof` discrimination (e.g., `Schema.Union([SuccessResponse, ErrorResponse])` then `if (parsed instanceof ErrorResponse)`), which is compile-time safe.
|
|
102
|
+
- Do not name schemas with a `Schema` suffix; schema constants should be named after the domain type.
|
|
103
|
+
- For non-class schemas, export type aliases with the same identifier name as the schema value.
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import * as Schema from 'effect/Schema';
|
|
109
|
+
|
|
110
|
+
export class CreateTaskInput extends Schema.Class<CreateTaskInput>(
|
|
111
|
+
'CreateTaskInput'
|
|
112
|
+
)({
|
|
113
|
+
id: Schema.String,
|
|
114
|
+
title: Schema.String,
|
|
115
|
+
priority: Schema.Int
|
|
116
|
+
}) {}
|
|
117
|
+
|
|
118
|
+
export const decodeCreateTaskInput =
|
|
119
|
+
Schema.decodeUnknownEffect(CreateTaskInput);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### EF-4: Canonical imports
|
|
123
|
+
|
|
124
|
+
- Required aliases:
|
|
125
|
+
- `import * as Arr from "effect/Array"`
|
|
126
|
+
- `import * as Option from "effect/Option"`
|
|
127
|
+
- `import * as P from "effect/Predicate"`
|
|
128
|
+
- `import * as R from "effect/Record"`
|
|
129
|
+
- `import * as Schema from "effect/Schema"`
|
|
130
|
+
- Prefer dedicated namespace imports for stable helper/data modules:
|
|
131
|
+
- `import * as Str from "effect/String"`
|
|
132
|
+
- `import * as Eq from "effect/Equal"`
|
|
133
|
+
- `import * as Bool from "effect/Boolean"`
|
|
134
|
+
- Reserve root imports from `"effect"` for core combinators/types such as `Effect`, `Match`, `pipe`, and `flow`.
|
|
135
|
+
- Keep unstable imports deliberate and local.
|
|
136
|
+
|
|
137
|
+
### EF-5: Effect modules over native collection helpers
|
|
138
|
+
|
|
139
|
+
- Use `Arr`, `R`, `Str`, `Eq`, `HashMap`, `HashSet`, `MutableHashMap`, `MutableHashSet`.
|
|
140
|
+
- Avoid domain usage of native `Object`, `Map`, `Set`, `Date`, and direct native string helpers.
|
|
141
|
+
- Do not use imperative `for` / `for...of` loops in domain code. Use `Arr.map`, `Arr.filter`, `Arr.filterMap`, or `Arr.reduce` for pure transformations. For effectful iteration, use `Effect.forEach` (which also supports concurrency).
|
|
142
|
+
- When behavior is unchanged, prefer the tersest helper form: direct helper refs over trivial wrapper lambdas, `flow(...)` over passthrough `pipe(...)` callbacks, and shared thunk helpers when already in scope.
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { pipe } from 'effect';
|
|
148
|
+
import * as Arr from 'effect/Array';
|
|
149
|
+
import * as Option from 'effect/Option';
|
|
150
|
+
|
|
151
|
+
const findActiveEmail = (
|
|
152
|
+
users: ReadonlyArray<{ readonly active: boolean; readonly email: string }>
|
|
153
|
+
) =>
|
|
154
|
+
pipe(
|
|
155
|
+
users,
|
|
156
|
+
Arr.findFirst((user) => user.active),
|
|
157
|
+
Option.map((user) => user.email)
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### EF-6: Predicate checks over raw runtime checks
|
|
162
|
+
|
|
163
|
+
- Prefer `P.isString`, `P.isNumber`, `P.isObject`, and predicate composition.
|
|
164
|
+
- Avoid raw `typeof`/ad-hoc runtime checks when a Predicate helper exists.
|
|
165
|
+
|
|
166
|
+
### EF-7: Branch with `Match` / `Arr.match`; model states with schema tagged unions
|
|
167
|
+
|
|
168
|
+
- Replace brittle if/else ladders with `Match`.
|
|
169
|
+
- For empty/non-empty array branching, prefer `Arr.match` over manual length checks.
|
|
170
|
+
- Do not use native `switch` statements for domain branching.
|
|
171
|
+
- Model domain states as schema tagged unions (see EF-13), then branch exhaustively.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { Match } from 'effect';
|
|
177
|
+
import * as Arr from 'effect/Array';
|
|
178
|
+
|
|
179
|
+
type SyncPhase = 'idle' | 'running' | 'failed';
|
|
180
|
+
|
|
181
|
+
const phaseLabel = (phase: SyncPhase) =>
|
|
182
|
+
Match.value(phase).pipe(
|
|
183
|
+
Match.when('idle', () => 'idle'),
|
|
184
|
+
Match.when('running', () => 'running'),
|
|
185
|
+
Match.when('failed', () => 'failed'),
|
|
186
|
+
Match.exhaustive
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const summarizeAttempts = (attempts: ReadonlyArray<number>) =>
|
|
190
|
+
Arr.match(attempts, {
|
|
191
|
+
onEmpty: () => 'no-attempts',
|
|
192
|
+
onNonEmpty: (values) => `attempts:${Arr.length(values)}`
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### EF-7b: Prefer `Bool.match` for booleans
|
|
197
|
+
|
|
198
|
+
- For boolean-driven branching, prefer `Bool.match` from `effect/Boolean` over ad-hoc `if/else`.
|
|
199
|
+
- This keeps control flow expression-oriented and consistent with Effect matching style.
|
|
200
|
+
|
|
201
|
+
### EF-8: Services use explicit tags + `Layer`
|
|
202
|
+
|
|
203
|
+
- Service identity comes from a unique string key.
|
|
204
|
+
- Honor a current Effect service-tag style already standardized by the project; otherwise default to `Context.Service`.
|
|
205
|
+
- Service constructors are explicit and layered.
|
|
206
|
+
- Dependency wiring happens in Layer composition, not hidden global state.
|
|
207
|
+
- Service identity must use a descriptive, unique string key.
|
|
208
|
+
- When no module convention exists, prefer file-local `Interface`, `Service`, `layer`, and `defaultLayer` exports plus an ES-module namespace projection such as `export * as Billing from "./billing.js"`.
|
|
209
|
+
- If an effectful helper hides dependencies, configuration, policy, or lifecycle state, promote it into its own service instead of leaving it as a static module helper.
|
|
210
|
+
- If a service starts owning busy/idle state, in-flight runner maps, cancellation handles, or registry/orchestration behavior, extract that coordinator concern into its own service.
|
|
211
|
+
- Do not `yield*` a `Ref`, `Deferred`, `Fiber`, or `Latch` directly — this was removed in v4. Use explicit method calls: `Ref.get(ref)`, `Deferred.await(deferred)`, `Fiber.join(fiber)`, `Latch.await(latch)`.
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { Context } from 'effect';
|
|
217
|
+
|
|
218
|
+
export class MyService extends Context.Service<
|
|
219
|
+
MyService,
|
|
220
|
+
{
|
|
221
|
+
readonly ping: () => string;
|
|
222
|
+
}
|
|
223
|
+
>()('MyService') {}
|
|
224
|
+
|
|
225
|
+
// At the owning module boundary:
|
|
226
|
+
export * as MyServiceModule from './my-service.js';
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### EF-9: Time/randomness should be effectful
|
|
230
|
+
|
|
231
|
+
- Prefer Effect runtime services such as `Clock` and `Random`.
|
|
232
|
+
- Avoid direct `Date.now()` and `Math.random()` in domain logic.
|
|
233
|
+
|
|
234
|
+
### EF-9b: Runtime HTTP uses Effect HTTP modules
|
|
235
|
+
|
|
236
|
+
- Do not use native `fetch` in runtime source.
|
|
237
|
+
- Compose requests/responses with `HttpClientRequest`, `HttpClientResponse`, `Headers`, `UrlParams`, `HttpMethod`, and `HttpBody`.
|
|
238
|
+
- Provide runtime client layers explicitly (`BunHttpClient.layer` from `@effect/platform-bun` for Bun runtimes).
|
|
239
|
+
- Native `fetch` is reserved for explicit low-level platform implementations that cannot use Effect HTTP. Isolate it in an adapter, lift it with `Effect.tryPromise`, propagate the supplied `AbortSignal`, classify status before decoding, and decode unknown bodies with `Schema`.
|
|
240
|
+
|
|
241
|
+
### EF-10: Tests stay effect-native
|
|
242
|
+
|
|
243
|
+
- Use `@effect/vitest` and `it.effect(...)` for effectful tests.
|
|
244
|
+
- Keep fixtures typed and schema-validated where useful.
|
|
245
|
+
|
|
246
|
+
### EF-11: Public APIs are documented
|
|
247
|
+
|
|
248
|
+
- Exported APIs in package/tooling source require JSDoc.
|
|
249
|
+
- Examples must remain docgen-clean.
|
|
250
|
+
- Runnable documentation examples may live in JSDoc, Markdown, or MDX; when the project uses `@effect/doctest`, keep all three doctest-clean.
|
|
251
|
+
|
|
252
|
+
### EF-12: Schema annotations are intentional
|
|
253
|
+
|
|
254
|
+
- Add schema annotations when they materially improve external docs, decode errors, introspection, or reusable schema helpers.
|
|
255
|
+
- Internal, local, or still-evolving schemas do not need annotations by default.
|
|
256
|
+
- When you do annotate, descriptions should encode intent, not repeat the symbol name.
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import * as Schema from 'effect/Schema';
|
|
262
|
+
|
|
263
|
+
export const Tenant = Schema.String;
|
|
264
|
+
|
|
265
|
+
export const TenantHeader = Tenant.annotate({
|
|
266
|
+
title: 'TenantHeader',
|
|
267
|
+
description: 'Tenant identifier read from the x-tenant header.'
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
export type Tenant = typeof Tenant.Type;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### EF-12b: Schema-first internal domain building blocks
|
|
274
|
+
|
|
275
|
+
- If an intermediate domain concept is named, reused, matched on, or structurally validated, model it as a schema first instead of an ad-hoc boolean helper.
|
|
276
|
+
- Prefer built-in schema constructors/checks such as `Schema.NonEmptyString`, `Schema.NonEmptyArray`, `Schema.TupleWithRest`, `Schema.Union`, `Schema.isPattern`, and `Schema.isIncludes` before reaching for `Schema.makeFilter`.
|
|
277
|
+
- Derive domain guards with `Schema.is(SomeSchema)`.
|
|
278
|
+
- If an internal literal domain needs type guards, use `Schema.is(Schema.Literal(...))`. For exhaustive matching over literals, use `Match`. For annotation-bearing schema values, use `Schema.Literal(...).annotate({...})`.
|
|
279
|
+
- Prefer named intermediate schemas; export and document them when reusable or when they materially clarify the module’s domain model, otherwise keep them module-local.
|
|
280
|
+
|
|
281
|
+
### EF-12c: Reusable schema checks carry metadata
|
|
282
|
+
|
|
283
|
+
- Reusable `Schema.makeFilter`, `Schema.makeFilterGroup`, and reusable built-in check blocks must include `identifier`, `title`, and `description`.
|
|
284
|
+
- Keep `message` focused on the user-facing decode failure.
|
|
285
|
+
- Tiny one-off test checks may stay lighter when the schema itself is not reusable.
|
|
286
|
+
|
|
287
|
+
### EF-13: Discriminated union schemas
|
|
288
|
+
|
|
289
|
+
- If schema properties are a union of literal strings (for example `kind`, `state`, `category`), compose class variants into a `Schema.Union` and finalize with `Schema.toTaggedUnion("<field>")`.
|
|
290
|
+
- Prefer `Schema.Class` for tagged union member schemas.
|
|
291
|
+
- Use `Schema.TaggedUnion` only for canonical `_tag` object-union construction.
|
|
292
|
+
- Reference: [Effect schema docs](packages/effect/SCHEMA.md:1891) (via effect_ref_read) and [toTaggedUnion notes](packages/effect/SCHEMA.md:1934) (via effect_ref_read).
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import * as Schema from 'effect/Schema';
|
|
298
|
+
|
|
299
|
+
export class ExternalJobCreated extends Schema.Class<ExternalJobCreated>(
|
|
300
|
+
'ExternalJobCreated'
|
|
301
|
+
)(
|
|
302
|
+
{
|
|
303
|
+
kind: Schema.tag('created'),
|
|
304
|
+
id: Schema.String
|
|
305
|
+
},
|
|
306
|
+
{ description: 'Created event from external job source.' }
|
|
307
|
+
) {}
|
|
308
|
+
|
|
309
|
+
export class ExternalJobCompleted extends Schema.Class<ExternalJobCompleted>(
|
|
310
|
+
'ExternalJobCompleted'
|
|
311
|
+
)(
|
|
312
|
+
{
|
|
313
|
+
kind: Schema.tag('completed'),
|
|
314
|
+
id: Schema.String,
|
|
315
|
+
at: Schema.String
|
|
316
|
+
},
|
|
317
|
+
{ description: 'Completed event from external job source.' }
|
|
318
|
+
) {}
|
|
319
|
+
|
|
320
|
+
export const ExternalJobEvent = Schema.Union([
|
|
321
|
+
ExternalJobCreated,
|
|
322
|
+
ExternalJobCompleted
|
|
323
|
+
])
|
|
324
|
+
.pipe(Schema.toTaggedUnion('kind'))
|
|
325
|
+
.annotate({
|
|
326
|
+
title: 'ExternalJobEvent',
|
|
327
|
+
description: 'External job event union discriminated by `kind`.'
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
export type ExternalJobEvent = typeof ExternalJobEvent.Type;
|
|
331
|
+
|
|
332
|
+
export const InternalJobEvent = Schema.TaggedUnion({
|
|
333
|
+
Created: { id: Schema.String },
|
|
334
|
+
Completed: { id: Schema.String, at: Schema.String }
|
|
335
|
+
}).annotate({
|
|
336
|
+
title: 'InternalJobEvent',
|
|
337
|
+
description: 'Canonical internal union discriminated by `_tag`.'
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### EF-14: Effect-returning functions use `Effect.fn` or `Effect.fnUntraced`
|
|
342
|
+
|
|
343
|
+
- Prefer `Effect.fn("Name")(...)` for reusable/public effectful functions.
|
|
344
|
+
- Use `Effect.fnUntraced(...)` for internal hot paths where tracing overhead is unnecessary.
|
|
345
|
+
- Reference: [Effect.fn docs](packages/effect/src/Effect.ts:12850) (via effect_ref_read) and [Effect.fnUntraced docs](packages/effect/src/Effect.ts:12821) (via effect_ref_read).
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { Effect } from 'effect';
|
|
351
|
+
import * as Schema from 'effect/Schema';
|
|
352
|
+
|
|
353
|
+
export const loadUser = Effect.fn('User.load')(function* (userId: string) {
|
|
354
|
+
yield* Effect.logDebug('loading user', userId);
|
|
355
|
+
return { userId };
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const parseInternal = Effect.fnUntraced(function* (input: string) {
|
|
359
|
+
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))(
|
|
360
|
+
input
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### EF-15: Effects must be observable
|
|
366
|
+
|
|
367
|
+
- Do not use `console.log`, `console.error`, or other `console.*` methods in Effect code. Use `Effect.logInfo`, `Effect.logError`, etc. for structured, testable logging.
|
|
368
|
+
- Instrument key workflows with logs, log annotations, spans, and metrics.
|
|
369
|
+
- Prefer built-in helpers:
|
|
370
|
+
- Logging: `Effect.logWithLevel`, `Effect.log`, `Effect.logFatal`, `Effect.logWarning`, `Effect.logError`, `Effect.logInfo`, `Effect.logDebug`, `Effect.logTrace`
|
|
371
|
+
- Logger/context: `Effect.withLogger`, `Effect.annotateLogs`, `Effect.annotateLogsScoped`, `Effect.withLogSpan`
|
|
372
|
+
- Metrics/tracking: `Effect.track`, `Effect.trackSuccesses`, `Effect.trackErrors`, `Effect.trackDefects`, `Effect.trackDuration`
|
|
373
|
+
- Tracing: `Effect.annotateSpans`, `Effect.annotateCurrentSpan`
|
|
374
|
+
|
|
375
|
+
Example:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { Effect } from 'effect';
|
|
379
|
+
import * as Metric from 'effect/Metric';
|
|
380
|
+
|
|
381
|
+
const durationMs = Metric.histogram('workflow_duration_ms', {
|
|
382
|
+
boundaries: Metric.boundariesFromIterable([10, 50, 100, 250, 500, 1000])
|
|
383
|
+
});
|
|
384
|
+
const failures = Metric.counter('workflow_failures_total');
|
|
385
|
+
|
|
386
|
+
const workflow = Effect.fn('Workflow.run')(function* (requestId: string) {
|
|
387
|
+
yield* Effect.annotateCurrentSpan('requestId', requestId);
|
|
388
|
+
yield* Effect.logInfo('workflow started');
|
|
389
|
+
return 'ok';
|
|
390
|
+
}).pipe(
|
|
391
|
+
Effect.withLogSpan('workflow.run'),
|
|
392
|
+
Effect.annotateLogs({ service: 'my-app' }),
|
|
393
|
+
Effect.trackDuration(durationMs),
|
|
394
|
+
Effect.trackErrors(failures)
|
|
395
|
+
);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### EF-16: Durations and windows use `effect/Duration`
|
|
399
|
+
|
|
400
|
+
- Model timeouts, intervals, and windows with `Duration`.
|
|
401
|
+
- Avoid magic number time values in domain logic.
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { Duration, Effect } from 'effect';
|
|
407
|
+
|
|
408
|
+
const timeout = Duration.seconds(30);
|
|
409
|
+
const pollInterval = Duration.millis(250);
|
|
410
|
+
|
|
411
|
+
const program = Effect.sleep(pollInterval).pipe(Effect.timeout(timeout));
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### EF-17: Nullable/nullish schema fields should decode to `Option`
|
|
415
|
+
|
|
416
|
+
- Use dedicated schema helpers for optional/null conversions:
|
|
417
|
+
- `Schema.OptionFromNullOr`
|
|
418
|
+
- `Schema.OptionFromNullishOr`
|
|
419
|
+
- `Schema.OptionFromOptionalKey`
|
|
420
|
+
- `Schema.OptionFromOptional`
|
|
421
|
+
- Reference: [Schema Option helpers](packages/effect/src/Schema.ts:5422) (via effect_ref_read) and [Schema optional field docs](packages/effect/SCHEMA.md:636) (via effect_ref_read).
|
|
422
|
+
|
|
423
|
+
Example:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import * as Schema from 'effect/Schema';
|
|
427
|
+
|
|
428
|
+
export class AccountInput extends Schema.Class<AccountInput>('AccountInput')({
|
|
429
|
+
nickname: Schema.OptionFromNullishOr(Schema.String),
|
|
430
|
+
bio: Schema.OptionFromNullOr(Schema.String),
|
|
431
|
+
phone: Schema.OptionFromOptionalKey(Schema.String),
|
|
432
|
+
timezone: Schema.OptionFromOptional(Schema.String)
|
|
433
|
+
}) {}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### EF-18: Exported helper APIs should be dual
|
|
437
|
+
|
|
438
|
+
- For reusable helper combinators, support both styles:
|
|
439
|
+
- Data-first: `fn(self, arg)`
|
|
440
|
+
- Data-last: `pipe(self, fn(arg))`
|
|
441
|
+
- Build these helpers with `dual` from `effect/Function`.
|
|
442
|
+
- Reference: [dual API](packages/effect/src/Function.ts:106) (via effect_ref_read).
|
|
443
|
+
|
|
444
|
+
Example:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
import { dual } from 'effect/Function';
|
|
448
|
+
import { pipe } from 'effect';
|
|
449
|
+
|
|
450
|
+
export const addPrefix: {
|
|
451
|
+
(prefix: string): (self: string) => string;
|
|
452
|
+
(self: string, prefix: string): string;
|
|
453
|
+
} = dual(2, (self: string, prefix: string) => `${prefix}${self}`);
|
|
454
|
+
|
|
455
|
+
const a = addPrefix('value', 'p:');
|
|
456
|
+
const b = pipe('value', addPrefix('p:'));
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### EF-19: JSON parse/stringify must use Schema
|
|
460
|
+
|
|
461
|
+
- Use `Schema.fromJsonString(Schema.Unknown)` for unknown JSON payloads. `Schema.UnknownFromJsonString` is internal as of beta.103.
|
|
462
|
+
- Use `Schema.fromJsonString(MySchema)` for typed JSON string boundaries.
|
|
463
|
+
- Avoid direct `JSON.parse` / `JSON.stringify` in Effect-first code.
|
|
464
|
+
- Reference: [`fromJsonString`](packages/effect/src/Schema.ts) in the Effect v4 source.
|
|
465
|
+
|
|
466
|
+
Example:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import * as Schema from 'effect/Schema';
|
|
470
|
+
|
|
471
|
+
export class User extends Schema.Class<User>('User')({
|
|
472
|
+
id: Schema.String,
|
|
473
|
+
name: Schema.String
|
|
474
|
+
}) {}
|
|
475
|
+
|
|
476
|
+
const UserJson = Schema.fromJsonString(User);
|
|
477
|
+
|
|
478
|
+
const decodeUserJson = Schema.decodeUnknownEffect(UserJson);
|
|
479
|
+
const encodeUserJson = Schema.encodeUnknownEffect(UserJson);
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### EF-20: Completion gate is strict
|
|
483
|
+
|
|
484
|
+
You are not done if these fail:
|
|
485
|
+
|
|
486
|
+
- `bun run check`
|
|
487
|
+
- `bun run lint`
|
|
488
|
+
- `bun run test`
|
|
489
|
+
|
|
490
|
+
### EF-21: Runtime execution stays at the boundary
|
|
491
|
+
|
|
492
|
+
- Application entrypoints and tests may execute effects with `Effect.run*`.
|
|
493
|
+
- Library and domain exports should return `Effect` values.
|
|
494
|
+
- Keep runtime execution in one place so wiring, logging, and lifecycle behavior stay auditable.
|
|
495
|
+
- Reference: [runPromise](packages/effect/src/Effect.ts:8423) (via effect_ref_read), [runSync](packages/effect/src/Effect.ts:8606) (via effect_ref_read), and [runFork](packages/effect/src/Effect.ts:8264) (via effect_ref_read).
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { Effect } from 'effect';
|
|
501
|
+
|
|
502
|
+
export const runJob = Effect.fn('Job.run')(function* (id: string) {
|
|
503
|
+
return { id };
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Runtime boundary only (for example, in main.ts):
|
|
507
|
+
// Effect.runPromise(runJob("job-1"))
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### EF-22: Promise boundaries must be lifted into Effect
|
|
511
|
+
|
|
512
|
+
- Use `Effect.tryPromise` for Promise APIs that may reject.
|
|
513
|
+
- Keep Promise rejection details in typed failure values.
|
|
514
|
+
- Domain APIs should return `Effect`, not raw `Promise`.
|
|
515
|
+
|
|
516
|
+
Example:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { Effect } from 'effect';
|
|
520
|
+
|
|
521
|
+
const readSdkValue = (client: ExternalSdk) =>
|
|
522
|
+
Effect.tryPromise({
|
|
523
|
+
try: (signal) => client.read({ signal }),
|
|
524
|
+
catch: (cause) => new SdkError({ operation: 'ExternalSdk.read', cause })
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### EF-23: Resource lifetime must be explicit and scoped
|
|
529
|
+
|
|
530
|
+
- Use `Effect.acquireUseRelease` for acquisition/use/release flows.
|
|
531
|
+
- Prefer `Effect.scoped` for helper composition that allocates resources.
|
|
532
|
+
- Do not manually open resources without an explicit finalization strategy.
|
|
533
|
+
- Reference: [acquireUseRelease](packages/effect/src/Effect.ts:6254) (via effect_ref_read) and [scoped](packages/effect/src/Effect.ts:6079) (via effect_ref_read).
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
import { Effect } from 'effect';
|
|
539
|
+
|
|
540
|
+
const withConnection = <A, E, R>(
|
|
541
|
+
use: (conn: Connection) => Effect.Effect<A, E, R>
|
|
542
|
+
) => Effect.acquireUseRelease(openConnection, use, closeConnection);
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### EF-24: Retry policy is declarative
|
|
546
|
+
|
|
547
|
+
- Encode retries with `Effect.retry` and `Schedule`.
|
|
548
|
+
- Avoid manual retry loops and ad-hoc mutable counters.
|
|
549
|
+
- Keep retry policy close to the failing effect.
|
|
550
|
+
- Retry only proven-idempotent operations at the narrowest boundary that can classify the failure.
|
|
551
|
+
- Let exhausted failures remain visible unless the boundary has a truthful fallback.
|
|
552
|
+
- Reference: [retry](packages/effect/src/Effect.ts:3978) (via effect_ref_read).
|
|
553
|
+
|
|
554
|
+
Example:
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
import { Effect, Schedule } from 'effect';
|
|
558
|
+
|
|
559
|
+
const resilientFetch = fetchRemote.pipe(Effect.retry(Schedule.recurs(3)));
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### EF-25: Timeouts are modeled outcomes
|
|
563
|
+
|
|
564
|
+
- Use `Effect.timeoutOption` when timeout should become `Option.None`.
|
|
565
|
+
- Use `Effect.timeoutOrElse` when timeout should produce a typed fallback effect.
|
|
566
|
+
- Avoid manually racing ad-hoc timers for business logic timeouts.
|
|
567
|
+
- Reference: [timeoutOption](packages/effect/src/Effect.ts:4421) (via effect_ref_read) and [timeoutOrElse](packages/effect/src/Effect.ts:4467) (via effect_ref_read).
|
|
568
|
+
|
|
569
|
+
Example:
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
import { Duration, Effect } from 'effect';
|
|
573
|
+
|
|
574
|
+
const lookupCachedOnTimeout = slowLookup.pipe(
|
|
575
|
+
Effect.timeoutOrElse({
|
|
576
|
+
duration: Duration.seconds(2),
|
|
577
|
+
onTimeout: () => Effect.succeed('cached-value')
|
|
578
|
+
})
|
|
579
|
+
);
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### EF-26: Structured concurrency is the default
|
|
583
|
+
|
|
584
|
+
- Prefer `Effect.forkChild` so lifecycle is supervised by parent scope.
|
|
585
|
+
- Use `Effect.forkDetach` only for explicit daemon semantics.
|
|
586
|
+
- Make fork intent explicit in code review and comments for detached work.
|
|
587
|
+
- Reference: [forkChild](packages/effect/src/Effect.ts:7978) (via effect_ref_read) and [forkDetach](packages/effect/src/Effect.ts:8121) (via effect_ref_read).
|
|
588
|
+
|
|
589
|
+
Example:
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
import { Effect, Fiber } from 'effect';
|
|
593
|
+
|
|
594
|
+
const runWithHeartbeat = Effect.fn('Worker.run')(function* () {
|
|
595
|
+
const heartbeat = yield* Effect.forkChild(heartbeatLoop);
|
|
596
|
+
const result = yield* doWork;
|
|
597
|
+
yield* Fiber.interrupt(heartbeat);
|
|
598
|
+
return result;
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### EF-27: Parallel fan-out needs explicit concurrency
|
|
603
|
+
|
|
604
|
+
- For non-trivial fan-out, set concurrency in `Effect.forEach`, `Effect.all`, or `Effect.validate`.
|
|
605
|
+
- Avoid implicit unbounded parallelism on large collections.
|
|
606
|
+
- Concurrency should be part of API intent for throughput-sensitive paths.
|
|
607
|
+
- Use an explicit number or `"unbounded"`. The `"inherit"` option and `Effect.withConcurrency` were removed in beta.102.
|
|
608
|
+
- Reference: `Effect.forEach`, `Effect.all`, and `Types.Concurrency` in the Effect v4 source.
|
|
609
|
+
|
|
610
|
+
Example:
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
import { Effect } from 'effect';
|
|
614
|
+
|
|
615
|
+
const hydrateUsers = (ids: ReadonlyArray<string>) =>
|
|
616
|
+
Effect.forEach(ids, fetchUser, { concurrency: 8 });
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### EF-28: Configuration is an effect, not a global read
|
|
620
|
+
|
|
621
|
+
- Use `Config` and `ConfigProvider` for configuration loading and parsing.
|
|
622
|
+
- Keep direct `process.env` access out of domain code.
|
|
623
|
+
- Layer/provide config sources explicitly for tests and non-default environments.
|
|
624
|
+
- Reference: [Config](packages/effect/src/Config.ts) (via effect_ref_read) and [ConfigProvider](packages/effect/src/ConfigProvider.ts:358) (via effect_ref_read).
|
|
625
|
+
|
|
626
|
+
Example:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
import { Config, Effect } from 'effect';
|
|
630
|
+
|
|
631
|
+
const loadPort = Effect.fn('Config.loadPort')(function* () {
|
|
632
|
+
return yield* Config.int('PORT');
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### EF-29: Secrets must stay redacted
|
|
637
|
+
|
|
638
|
+
- Use `Config.redacted` for secret config values.
|
|
639
|
+
- Use `Redacted.make` for sensitive values coming from non-config sources.
|
|
640
|
+
- Never log secret values after unwrapping.
|
|
641
|
+
- Reference: [Config.redacted](packages/effect/src/Config.ts:1161) (via effect_ref_read) and [Redacted](packages/effect/src/Redacted.ts) (via effect_ref_read).
|
|
642
|
+
|
|
643
|
+
Example:
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
import { Config, Effect } from 'effect';
|
|
647
|
+
|
|
648
|
+
const loadApiKey = Effect.fn('Config.loadApiKey')(function* () {
|
|
649
|
+
const apiKey = yield* Config.redacted('API_KEY');
|
|
650
|
+
yield* Effect.logDebug(`apiKey=${String(apiKey)}`);
|
|
651
|
+
return apiKey;
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### EF-30: Recovery should be precise, not blanket
|
|
656
|
+
|
|
657
|
+
- Prefer `Effect.catchTag` and `Effect.catchFilter` for targeted recovery.
|
|
658
|
+
- Do not hide unrelated failures behind broad fallback handlers.
|
|
659
|
+
- Keep recoverable error cases explicit in code.
|
|
660
|
+
|
|
661
|
+
Example:
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
import { Effect } from 'effect';
|
|
665
|
+
import * as Option from 'effect/Option';
|
|
666
|
+
|
|
667
|
+
const findUserOptional = (id: string) =>
|
|
668
|
+
findUser(id).pipe(
|
|
669
|
+
Effect.map(Option.some),
|
|
670
|
+
Effect.catchTag('UserNotFoundError', () =>
|
|
671
|
+
Effect.succeed(Option.none())
|
|
672
|
+
)
|
|
673
|
+
);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### EF-31: Separate expected failures from defects
|
|
677
|
+
|
|
678
|
+
- Use `Effect.fail` for expected business/domain failures.
|
|
679
|
+
- Reserve `Effect.die` / `Effect.orDie` for:
|
|
680
|
+
- Invariant violations and impossible states.
|
|
681
|
+
- Unrecoverable infrastructure failures where surfacing the error provides no actionable recovery path (e.g., the data directory is unwritable).
|
|
682
|
+
- Discarding irrelevant upstream error types: when a consuming service cannot meaningfully recover from a dependency's error type and the error is not part of the consumer's own contract, `Effect.orDie` is legitimate. For example, a config loader that depends on an auth service may use `yield* authSvc.all().pipe(Effect.orDie)` because auth failures during config loading are unrecoverable.
|
|
683
|
+
- Do not model normal user-facing errors as defects.
|
|
684
|
+
- Reference: [die](packages/effect/src/Effect.ts:1745) (via effect_ref_read) and [orDie](packages/effect/src/Effect.ts:3557) (via effect_ref_read).
|
|
685
|
+
|
|
686
|
+
Example:
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { Effect } from 'effect';
|
|
690
|
+
|
|
691
|
+
const validateInput = Effect.fn('Input.validate')(function* (value: string) {
|
|
692
|
+
if (value.length === 0) {
|
|
693
|
+
return yield* Effect.fail(
|
|
694
|
+
new ValidationError({ message: 'value must be non-empty' })
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (value === '__unreachable__') {
|
|
699
|
+
return yield* Effect.die('unreachable state');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return value;
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### EF-32: Layer memoization isolation must be intentional
|
|
707
|
+
|
|
708
|
+
- Understand that layer provisioning is shared by default.
|
|
709
|
+
- When isolation is required, use `Effect.provide(..., { local: true })` or `Layer.fresh`.
|
|
710
|
+
- Document why isolation is necessary for behavior-sensitive paths.
|
|
711
|
+
- Compose `defaultLayer` values directly by default.
|
|
712
|
+
- Use `Layer.suspend(() => ...)` only when import evaluation order or a real circular dependency requires deferred composition.
|
|
713
|
+
- Reference: [Effect.provide local option](packages/effect/src/Effect.ts:5592) (via effect_ref_read) and [Layer.fresh](packages/effect/src/Layer.ts:1621) (via effect_ref_read).
|
|
714
|
+
|
|
715
|
+
Example:
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
import { Effect, Layer } from 'effect';
|
|
719
|
+
|
|
720
|
+
const runIsolated = program.pipe(
|
|
721
|
+
Effect.provide(Layer.fresh(AppLayer), { local: true })
|
|
722
|
+
);
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### EF-33: Schema-first development for domain models
|
|
726
|
+
|
|
727
|
+
- If a data shape is decoded from external input, will be discriminated via `instanceof`, or participates in a `Schema.Union`, define it as `Schema.Class` first — regardless of whether it is a "domain model" or an ephemeral HTTP response shape.
|
|
728
|
+
- Prefer `Schema.Class` (or another schema constructor) over plain `type` / `interface` for property-based domain shapes.
|
|
729
|
+
- Derive runtime types from schema definitions instead of duplicating parallel `type` / `interface` models.
|
|
730
|
+
- Keep plain `type` / `interface` for cases schema cannot represent cleanly (complex type-level transforms, utility types, overload-only surfaces).
|
|
731
|
+
|
|
732
|
+
Example:
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
import * as Schema from 'effect/Schema';
|
|
736
|
+
|
|
737
|
+
// Prefer schema-first over plain interfaces for domain payloads.
|
|
738
|
+
export class CreateOrderInput extends Schema.Class<CreateOrderInput>(
|
|
739
|
+
'CreateOrderInput'
|
|
740
|
+
)(
|
|
741
|
+
{
|
|
742
|
+
orderId: Schema.String,
|
|
743
|
+
customerId: Schema.String
|
|
744
|
+
},
|
|
745
|
+
{ description: 'Input payload for creating an order.' }
|
|
746
|
+
) {}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### EF-34: Schema defaults over fallback object logic
|
|
750
|
+
|
|
751
|
+
- Put defaults in schema definitions, not in handler/service fallback object literals.
|
|
752
|
+
- Use `Schema.withConstructorDefault` for constructor-time defaults.
|
|
753
|
+
- Use `Schema.withDecodingDefault` / `Schema.withDecodingDefaultKey` for decode-time defaults.
|
|
754
|
+
- Constructor defaults may fail with `SchemaIssue.Issue`. Likewise, `Schema.makeEffect` returns validation failures directly as `SchemaIssue.Issue`, not wrapped in `Schema.SchemaError`.
|
|
755
|
+
|
|
756
|
+
Example:
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
import * as Effect from 'effect/Effect';
|
|
760
|
+
import * as Schema from 'effect/Schema';
|
|
761
|
+
|
|
762
|
+
export class VersionSyncOptions extends Schema.Class<VersionSyncOptions>(
|
|
763
|
+
'VersionSyncOptions'
|
|
764
|
+
)(
|
|
765
|
+
{
|
|
766
|
+
shouldCheck: Schema.Boolean.pipe(
|
|
767
|
+
Schema.withDecodingDefault(Effect.succeed(true)),
|
|
768
|
+
Schema.withConstructorDefault(Effect.succeed(true))
|
|
769
|
+
),
|
|
770
|
+
categories: Schema.Array(Schema.String).pipe(
|
|
771
|
+
Schema.withDecodingDefault(Effect.succeed([])),
|
|
772
|
+
Schema.withConstructorDefault(Effect.succeed([]))
|
|
773
|
+
)
|
|
774
|
+
},
|
|
775
|
+
{ description: 'Version sync options with schema-level defaults.' }
|
|
776
|
+
) {}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### EF-35: Schema-backed guards and internal domain modeling
|
|
780
|
+
|
|
781
|
+
- If a guard validates domain strings/paths/tags, define a branded schema and use `Schema.is(...)`.
|
|
782
|
+
- If a domain constraint is named, reused, matched on, or structurally validated, model it as a schema first rather than a forest of ad-hoc predicate helpers.
|
|
783
|
+
- Prefer built-in schema constructors/checks before `Schema.makeFilter`.
|
|
784
|
+
- Keep guard intent and reusable check intent in schema annotations and check metadata.
|
|
785
|
+
- For internal literal domains, use `Schema.is(Schema.Literal(...))` for type guards, `Match` for exhaustive matching, and `Schema.Literal(...).annotate({...})` for annotated schema values.
|
|
786
|
+
- Prefer named intermediate schemas; export them only when reusable or when they materially clarify the module's domain model.
|
|
787
|
+
- Propagate branded schema types through the persistence layer (e.g., ORM column types: `text().$type<AccessToken>()`) to enforce compile-time safety across the entire stack and prevent parameter-swapping bugs.
|
|
788
|
+
|
|
789
|
+
Example:
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
import { Match, pipe } from 'effect';
|
|
793
|
+
import * as Arr from 'effect/Array';
|
|
794
|
+
import * as P from 'effect/Predicate';
|
|
795
|
+
import * as Schema from 'effect/Schema';
|
|
796
|
+
import * as Str from 'effect/String';
|
|
797
|
+
|
|
798
|
+
type TopicKind = 'plain' | 'scoped';
|
|
799
|
+
|
|
800
|
+
const ContainsScopeSeparator = Schema.String.check(
|
|
801
|
+
Schema.isIncludes(':', {
|
|
802
|
+
identifier: 'ContainsScopeSeparatorCheck',
|
|
803
|
+
title: 'Contains Scope Separator',
|
|
804
|
+
description: 'A string that contains `:`.',
|
|
805
|
+
message: 'Topic text must contain :'
|
|
806
|
+
})
|
|
807
|
+
).pipe(
|
|
808
|
+
Schema.brand('ContainsScopeSeparator'),
|
|
809
|
+
Schema.annotate({
|
|
810
|
+
title: 'ContainsScopeSeparator',
|
|
811
|
+
description: 'A string that contains the topic scope separator `:`.'
|
|
812
|
+
})
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const isContainsScopeSeparator = Schema.is(ContainsScopeSeparator);
|
|
816
|
+
|
|
817
|
+
const TopicSegment = Schema.NonEmptyString.check(
|
|
818
|
+
Schema.makeFilter(P.not(isContainsScopeSeparator), {
|
|
819
|
+
identifier: 'TopicSegmentNoSeparatorCheck',
|
|
820
|
+
title: 'Topic Segment No Separator',
|
|
821
|
+
description: 'A topic segment that does not contain `:`.',
|
|
822
|
+
message: 'Topic segments must not contain :'
|
|
823
|
+
})
|
|
824
|
+
).pipe(
|
|
825
|
+
Schema.brand('TopicSegment'),
|
|
826
|
+
Schema.annotate({
|
|
827
|
+
title: 'TopicSegment',
|
|
828
|
+
description: 'A non-empty topic segment without the scope separator.'
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
const isTopicSegment = Schema.is(TopicSegment);
|
|
833
|
+
|
|
834
|
+
const splitNonEmpty =
|
|
835
|
+
(separator: string | RegExp) =>
|
|
836
|
+
(value: string): ReadonlyArray<string> =>
|
|
837
|
+
pipe(Str.split(separator)(value), Arr.filter(Str.isNonEmpty));
|
|
838
|
+
|
|
839
|
+
const classifyTopicKind = Match.type<string>().pipe(
|
|
840
|
+
Match.when(isContainsScopeSeparator, () => 'scoped' as const),
|
|
841
|
+
Match.orElse(() => 'plain' as const)
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const validateTopicSegments = (kind: TopicKind, value: string) =>
|
|
845
|
+
Match.value(kind).pipe(
|
|
846
|
+
Match.when('plain', () => isTopicSegment(value)),
|
|
847
|
+
Match.when('scoped', () =>
|
|
848
|
+
pipe(value, splitNonEmpty(':'), Arr.every(isTopicSegment))
|
|
849
|
+
),
|
|
850
|
+
Match.exhaustive
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
export const TopicName = Schema.NonEmptyString.check(
|
|
854
|
+
Schema.makeFilterGroup(
|
|
855
|
+
[
|
|
856
|
+
Schema.makeFilter(P.not(Str.endsWith(':')), {
|
|
857
|
+
identifier: 'TopicNameNoTrailingSeparatorCheck',
|
|
858
|
+
title: 'Topic Name No Trailing Separator',
|
|
859
|
+
description: 'A topic name that does not end with `:`.',
|
|
860
|
+
message: 'Topic names must not end with :'
|
|
861
|
+
}),
|
|
862
|
+
Schema.makeFilter(
|
|
863
|
+
(value: string) =>
|
|
864
|
+
validateTopicSegments(classifyTopicKind(value), value),
|
|
865
|
+
{
|
|
866
|
+
identifier: 'TopicNameSegmentsCheck',
|
|
867
|
+
title: 'Topic Name Segments',
|
|
868
|
+
description:
|
|
869
|
+
'A topic name whose segments are valid topic segments.',
|
|
870
|
+
message: 'Topic names must contain only valid segments'
|
|
871
|
+
}
|
|
872
|
+
)
|
|
873
|
+
],
|
|
874
|
+
{
|
|
875
|
+
identifier: 'TopicNameChecks',
|
|
876
|
+
title: 'Topic Name',
|
|
877
|
+
description: 'Checks for a plain or scoped topic name.'
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
).pipe(
|
|
881
|
+
Schema.brand('TopicName'),
|
|
882
|
+
Schema.annotate({
|
|
883
|
+
title: 'TopicName',
|
|
884
|
+
description:
|
|
885
|
+
'A topic name composed from valid plain or scoped segments.'
|
|
886
|
+
})
|
|
887
|
+
);
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Avoid this:
|
|
891
|
+
|
|
892
|
+
- A forest of `const hasX = ...`, `const isY = /.../.test(...)`, and unannotated predicate helpers when the named concepts can be expressed as schemas and reused with `Schema.is(...)`.
|
|
893
|
+
|
|
894
|
+
### EF-36: Prefer schema equivalence for domain comparisons
|
|
895
|
+
|
|
896
|
+
- For schema-modeled domain values, use `Schema.toEquivalence(schema)` instead of manual `===` / `!==`.
|
|
897
|
+
- This keeps comparison semantics aligned with schema intent and future schema changes.
|
|
898
|
+
|
|
899
|
+
Example:
|
|
900
|
+
|
|
901
|
+
```ts
|
|
902
|
+
import * as Schema from 'effect/Schema';
|
|
903
|
+
|
|
904
|
+
const stringArrayEq = Schema.toEquivalence(Schema.Array(Schema.String));
|
|
905
|
+
|
|
906
|
+
const arraysEqual = (
|
|
907
|
+
left: ReadonlyArray<string>,
|
|
908
|
+
right: ReadonlyArray<string>
|
|
909
|
+
) => stringArrayEq(left, right);
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### EF-37: Use schema transformations for deterministic conversions
|
|
913
|
+
|
|
914
|
+
- If conversion is deterministic and type-shaping (path normalization, filename conversion, tagged-string normalization), model it with `Schema.decodeTo(..., SchemaTransformation.transform(...))`.
|
|
915
|
+
- Prefer schema transformation helpers over ad-hoc conversion functions.
|
|
916
|
+
|
|
917
|
+
Example:
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
import { SchemaTransformation } from 'effect';
|
|
921
|
+
import * as Schema from 'effect/Schema';
|
|
922
|
+
import * as Str from 'effect/String';
|
|
923
|
+
|
|
924
|
+
const NativePathToPosixPath = Schema.String.pipe(
|
|
925
|
+
Schema.decodeTo(
|
|
926
|
+
Schema.String.check(Schema.isPattern(/^[^\\]*$/)).pipe(
|
|
927
|
+
Schema.brand('PosixPath')
|
|
928
|
+
),
|
|
929
|
+
SchemaTransformation.transform({
|
|
930
|
+
decode: (pathString) => Str.replaceAll('\\', '/')(pathString),
|
|
931
|
+
encode: (pathString) => pathString
|
|
932
|
+
})
|
|
933
|
+
)
|
|
934
|
+
);
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
### EF-38: Never use native array sort in Effect-first code
|
|
938
|
+
|
|
939
|
+
- Use `Arr.sort(values, order)` from `effect/Array`.
|
|
940
|
+
- Define ordering with `effect/Order` (`Order.String`, `Order.Number`, `Order.mapInput`, etc.).
|
|
941
|
+
- Do not call native `.sort()` directly on arrays.
|
|
942
|
+
|
|
943
|
+
Example:
|
|
944
|
+
|
|
945
|
+
```ts
|
|
946
|
+
import { Order } from 'effect';
|
|
947
|
+
import * as Arr from 'effect/Array';
|
|
948
|
+
|
|
949
|
+
const byName = Order.mapInput(
|
|
950
|
+
Order.String,
|
|
951
|
+
(item: { readonly name: string }) => item.name
|
|
952
|
+
);
|
|
953
|
+
const sorted = Arr.sort(items, byName);
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### EF-39: Avoid ad-hoc `String(...)` coercion for domain comparisons
|
|
957
|
+
|
|
958
|
+
- When unknown/scalar data must normalize to domain strings, model the conversion with schema transformations.
|
|
959
|
+
- Compare resulting values with `Schema.toEquivalence(Schema.String)` (or domain schema equivalence), not raw string equality.
|
|
960
|
+
|
|
961
|
+
Example:
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
import { SchemaTransformation } from 'effect';
|
|
965
|
+
import * as Schema from 'effect/Schema';
|
|
966
|
+
|
|
967
|
+
const UnknownToString = Schema.Unknown.pipe(
|
|
968
|
+
Schema.decodeTo(
|
|
969
|
+
Schema.String,
|
|
970
|
+
SchemaTransformation.transform({
|
|
971
|
+
decode: (value) => `${value}`,
|
|
972
|
+
encode: (value) => value
|
|
973
|
+
})
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
### EF-40: Use Effect.cached for memoization and deduplication
|
|
979
|
+
|
|
980
|
+
- Replace ad-hoc memoization (Promise-based `task` fields, `Fiber` tracking, mutable `result` caches) with `Effect.cached`.
|
|
981
|
+
- `Effect.cached(effect)` returns an Effect that runs `effect` at most once, sharing the result with all subsequent callers.
|
|
982
|
+
- For invalidatable caches, use `Effect.cachedInvalidateWithTTL(effect, Duration.infinity)` which returns a `[cachedEffect, invalidate]` tuple. Call `yield* invalidate` to force re-computation on next access.
|
|
983
|
+
- For time-based caches, use `Effect.cachedWithTTL(effect, duration)`.
|
|
984
|
+
- Prefer `Effect.cachedInvalidateWithTTL` with `Duration.infinity` over mutable `let` rebinding of cached effects.
|
|
985
|
+
|
|
986
|
+
Example:
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
import { Duration, Effect } from 'effect';
|
|
990
|
+
|
|
991
|
+
// One-shot lazy memoization
|
|
992
|
+
const cachedConfig = yield* Effect.cached(loadConfig());
|
|
993
|
+
|
|
994
|
+
// Manually invalidatable cache
|
|
995
|
+
const [cachedConfig, invalidate] =
|
|
996
|
+
yield*
|
|
997
|
+
Effect.cachedInvalidateWithTTL(
|
|
998
|
+
loadConfig().pipe(Effect.orElseSucceed(() => defaultConfig)),
|
|
999
|
+
Duration.infinity
|
|
1000
|
+
);
|
|
1001
|
+
// Later: yield* invalidate to force reload on next access
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
## Copy-Paste Templates
|
|
1005
|
+
|
|
1006
|
+
### Template: Tagged error
|
|
1007
|
+
|
|
1008
|
+
```ts
|
|
1009
|
+
import * as Schema from 'effect/Schema';
|
|
1010
|
+
|
|
1011
|
+
class DomainError extends Schema.TaggedError<DomainError>()(
|
|
1012
|
+
'DomainError',
|
|
1013
|
+
{
|
|
1014
|
+
message: Schema.String
|
|
1015
|
+
},
|
|
1016
|
+
{ description: 'Domain failure' }
|
|
1017
|
+
) {}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### Template: Safe nullable boundary conversion
|
|
1021
|
+
|
|
1022
|
+
```ts
|
|
1023
|
+
import { pipe } from 'effect';
|
|
1024
|
+
import * as Option from 'effect/Option';
|
|
1025
|
+
|
|
1026
|
+
const fromNullableName = (name: string | null | undefined) =>
|
|
1027
|
+
pipe(
|
|
1028
|
+
Option.fromNullishOr(name),
|
|
1029
|
+
Option.filter((value) => value.length > 0)
|
|
1030
|
+
);
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### Template: Decode unknown at API edge
|
|
1034
|
+
|
|
1035
|
+
```ts
|
|
1036
|
+
import * as Schema from 'effect/Schema';
|
|
1037
|
+
|
|
1038
|
+
export class Payload extends Schema.Class<Payload>('Payload')({
|
|
1039
|
+
query: Schema.String
|
|
1040
|
+
}) {}
|
|
1041
|
+
|
|
1042
|
+
const decodePayload = Schema.decodeUnknownEffect(Payload);
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### Template: Schema naming + type alias (no `Schema` suffix)
|
|
1046
|
+
|
|
1047
|
+
```ts
|
|
1048
|
+
import * as Schema from 'effect/Schema';
|
|
1049
|
+
|
|
1050
|
+
export const OrderId = Schema.String;
|
|
1051
|
+
export type OrderId = typeof OrderId.Type;
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Template: Schema-first replacement for interface
|
|
1055
|
+
|
|
1056
|
+
```ts
|
|
1057
|
+
import * as Schema from 'effect/Schema';
|
|
1058
|
+
|
|
1059
|
+
export class UserProfile extends Schema.Class<UserProfile>('UserProfile')(
|
|
1060
|
+
{
|
|
1061
|
+
id: Schema.String,
|
|
1062
|
+
displayName: Schema.String
|
|
1063
|
+
},
|
|
1064
|
+
{ description: 'User profile model used in domain workflows.' }
|
|
1065
|
+
) {}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### Template: Match over switch
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
import { Match } from 'effect';
|
|
1072
|
+
import * as Arr from 'effect/Array';
|
|
1073
|
+
|
|
1074
|
+
type Phase = 'draft' | 'running' | 'done';
|
|
1075
|
+
|
|
1076
|
+
const phaseLabel = (phase: Phase) =>
|
|
1077
|
+
Match.value(phase).pipe(
|
|
1078
|
+
Match.when('draft', () => 'draft'),
|
|
1079
|
+
Match.when('running', () => 'running'),
|
|
1080
|
+
Match.when('done', () => 'done'),
|
|
1081
|
+
Match.exhaustive
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
const summarize = (items: ReadonlyArray<string>) =>
|
|
1085
|
+
Arr.match(items, {
|
|
1086
|
+
onEmpty: () => 'none',
|
|
1087
|
+
onNonEmpty: (values) => `count:${Arr.length(values)}`
|
|
1088
|
+
});
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
### Template: Effect-returning function constructor
|
|
1092
|
+
|
|
1093
|
+
```ts
|
|
1094
|
+
import { Effect } from 'effect';
|
|
1095
|
+
|
|
1096
|
+
export const runTask = Effect.fn('Task.run')(function* (taskId: string) {
|
|
1097
|
+
yield* Effect.logInfo('run task', taskId);
|
|
1098
|
+
return taskId;
|
|
1099
|
+
});
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
### Template: Option schema from nullish/optional
|
|
1103
|
+
|
|
1104
|
+
```ts
|
|
1105
|
+
import * as Schema from 'effect/Schema';
|
|
1106
|
+
|
|
1107
|
+
export class Input extends Schema.Class<Input>('Input')({
|
|
1108
|
+
maybeName: Schema.OptionFromNullishOr(Schema.String),
|
|
1109
|
+
maybeEmail: Schema.OptionFromOptionalKey(Schema.String)
|
|
1110
|
+
}) {}
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
### Template: Dual helper (data-first + data-last)
|
|
1114
|
+
|
|
1115
|
+
```ts
|
|
1116
|
+
import { dual } from 'effect/Function';
|
|
1117
|
+
|
|
1118
|
+
export const rename: {
|
|
1119
|
+
(
|
|
1120
|
+
to: string
|
|
1121
|
+
): (self: { readonly name: string }) => { readonly name: string };
|
|
1122
|
+
(self: { readonly name: string }, to: string): { readonly name: string };
|
|
1123
|
+
} = dual(2, (self, to) => ({ ...self, name: to }));
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Template: JSON boundary without native JSON APIs
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
import * as Schema from 'effect/Schema';
|
|
1130
|
+
|
|
1131
|
+
export class Payload extends Schema.Class<Payload>('Payload')({
|
|
1132
|
+
query: Schema.String
|
|
1133
|
+
}) {}
|
|
1134
|
+
|
|
1135
|
+
const PayloadJson = Schema.fromJsonString(Payload);
|
|
1136
|
+
|
|
1137
|
+
export const decodePayloadJson = Schema.decodeUnknownEffect(PayloadJson);
|
|
1138
|
+
export const encodePayloadJson = Schema.encodeUnknownEffect(PayloadJson);
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
### Template: Runtime boundary execution
|
|
1142
|
+
|
|
1143
|
+
```ts
|
|
1144
|
+
import { Effect } from 'effect';
|
|
1145
|
+
|
|
1146
|
+
export const buildReport = Effect.fn('Report.build')(function* () {
|
|
1147
|
+
return 'ok';
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// runtime boundary only
|
|
1151
|
+
// Effect.runPromise(buildReport())
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### Template: Scoped resource helper
|
|
1155
|
+
|
|
1156
|
+
```ts
|
|
1157
|
+
import { Effect } from 'effect';
|
|
1158
|
+
|
|
1159
|
+
export const withResource = <A, E, R>(
|
|
1160
|
+
use: (resource: Resource) => Effect.Effect<A, E, R>
|
|
1161
|
+
) => Effect.acquireUseRelease(acquireResource, use, releaseResource);
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### Template: Retry + timeout
|
|
1165
|
+
|
|
1166
|
+
```ts
|
|
1167
|
+
import { Duration, Effect, Schedule } from 'effect';
|
|
1168
|
+
|
|
1169
|
+
export const resilientTask = task.pipe(
|
|
1170
|
+
Effect.retry(Schedule.recurs(3)),
|
|
1171
|
+
Effect.timeoutOption(Duration.seconds(5))
|
|
1172
|
+
);
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
### Template: Config + redacted secret
|
|
1176
|
+
|
|
1177
|
+
```ts
|
|
1178
|
+
import { Config, Effect } from 'effect';
|
|
1179
|
+
|
|
1180
|
+
export const loadConfig = Effect.fn('Config.load')(function* () {
|
|
1181
|
+
const port = yield* Config.int('PORT');
|
|
1182
|
+
const apiKey = yield* Config.redacted('API_KEY');
|
|
1183
|
+
return { port, apiKey };
|
|
1184
|
+
});
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
### Template: Isolated layer provide
|
|
1188
|
+
|
|
1189
|
+
```ts
|
|
1190
|
+
import { Effect, Layer } from 'effect';
|
|
1191
|
+
|
|
1192
|
+
export const runIsolated = program.pipe(
|
|
1193
|
+
Effect.provide(Layer.fresh(AppLayer), { local: true })
|
|
1194
|
+
);
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
## LLM Review Checklist
|
|
1198
|
+
|
|
1199
|
+
Use this before submitting code:
|
|
1200
|
+
|
|
1201
|
+
1. No `any`, no type assertions, no `@ts-ignore`, no non-null assertions.
|
|
1202
|
+
2. No untyped error throwing in domain logic.
|
|
1203
|
+
3. Nullish converted to `Option` at boundaries.
|
|
1204
|
+
4. Unknown input decoded with `Schema`.
|
|
1205
|
+
5. Canonical namespace imports (`Option`, `Schema`, `Arr`, `P`, `R`, etc.) present and used.
|
|
1206
|
+
6. No native `Object/Map/Set/Date/String` helpers in domain logic.
|
|
1207
|
+
7. Branching logic is exhaustive where appropriate (`Match.exhaustive`, schema `.match`, and `Arr.match` for array emptiness).
|
|
1208
|
+
8. No new schema constants end with `Schema`.
|
|
1209
|
+
9. For non-class schemas, new schema constants expose `export type X = typeof X.Type`.
|
|
1210
|
+
10. Schema annotations are used only where they materially improve docs, errors, or introspection.
|
|
1211
|
+
11. `Effect`-returning reusable functions are created with `Effect.fn`/`Effect.fnUntraced`.
|
|
1212
|
+
12. Critical flows include logs/spans/metrics instrumentation.
|
|
1213
|
+
13. Durations/time windows use `Duration` values.
|
|
1214
|
+
14. Nullish schema fields use `Schema.OptionFrom*` helpers when representing absence as `Option`.
|
|
1215
|
+
15. Exported helper combinators support dual API via `dual`.
|
|
1216
|
+
16. No `JSON.parse` / `JSON.stringify` in Effect-first domain paths.
|
|
1217
|
+
17. Prefer `Schema.Class` over `Schema.Struct` for all decoded shapes (domain models, HTTP responses, API payloads).
|
|
1218
|
+
18. Required verification commands are green.
|
|
1219
|
+
19. `Effect.run*` appears only in runtime boundaries (entrypoint/test harness).
|
|
1220
|
+
20. Promise-based APIs are lifted with `Effect.tryPromise`.
|
|
1221
|
+
21. Acquired resources use `Effect.acquireUseRelease` or `Effect.scoped`.
|
|
1222
|
+
22. Retries are declared with `Effect.retry` + `Schedule`.
|
|
1223
|
+
23. Timeouts use `Effect.timeoutOption` / `Effect.timeoutOrElse`.
|
|
1224
|
+
24. Forking intent is explicit (`forkChild` default; `forkDetach` justified).
|
|
1225
|
+
25. Large fan-out operations specify concurrency deliberately.
|
|
1226
|
+
26. Config values come from `Config` / `ConfigProvider`, not direct `process.env` in domain logic.
|
|
1227
|
+
27. Secrets are `Redacted` (`Config.redacted` / `Redacted.make`) and not logged raw.
|
|
1228
|
+
28. Recovery uses `catchTag` / `catchFilter` for targeted cases.
|
|
1229
|
+
29. Expected failures use `Effect.fail`; defects are reserved for invariants and discarding irrelevant upstream error types via `orDie`.
|
|
1230
|
+
30. Isolation-sensitive layer provisioning uses `{ local: true }` or `Layer.fresh`.
|
|
1231
|
+
31. All decoded shapes (domain models, HTTP responses, API payloads) are schema-first with `Schema.Class`; plain `type` / `interface` is used only when schema is not a practical fit.
|
|
1232
|
+
32. Literal-string discriminant unions use `Schema.Union` + `Schema.toTaggedUnion`. For exhaustive matching over literals, use `Match`. For type guards, use `Schema.is(Schema.Literal(...))`.
|
|
1233
|
+
33. Schema defaults use `Schema.withConstructorDefault` / `Schema.withDecodingDefault*`, not ad-hoc fallback objects in handlers/services.
|
|
1234
|
+
34. Named or reused domain constraints are modeled as schemas first; built-in schema constructors/checks are preferred before `Schema.makeFilter`.
|
|
1235
|
+
35. Guard helpers for domain strings/paths/tags come from branded schemas with `Schema.is(...)`, not ad-hoc `regex.test(...)` predicates.
|
|
1236
|
+
36. Reusable schema checks and filter groups carry `identifier`, `title`, and `description`.
|
|
1237
|
+
37. Intermediate schemas are exported only when reusable or materially clarifying; otherwise they stay module-local.
|
|
1238
|
+
38. Schema-modeled comparisons use `Schema.toEquivalence(...)` where practical.
|
|
1239
|
+
39. Deterministic format conversions use `Schema.decodeTo(..., SchemaTransformation.transform(...))`.
|
|
1240
|
+
40. Trivial helper wrapper lambdas are collapsed to direct helper refs where safe, and passthrough `pipe(...)` callbacks are expressed with `flow(...)`.
|
|
1241
|
+
41. Runtime source avoids `node:fs` / `node:path` / `node:child_process`; use Effect `FileSystem` / `Path` / process services.
|
|
1242
|
+
42. Runtime source avoids native `fetch`; HTTP boundaries use `effect/unstable/http` + platform layers (`BunHttpClient.layer`, etc.).
|
|
1243
|
+
43. Runtime sorting uses `Arr.sort` with explicit `Order`, not native `Array.prototype.sort`.
|
|
1244
|
+
44. Boolean branching prefers `Bool.match` over ad-hoc `if/else` when branching on booleans.
|
|
1245
|
+
45. HTTP request/response composition uses Effect HTTP modules (`HttpClientRequest`, `HttpClientResponse`, `Headers`, `UrlParams`, `HttpMethod`, `HttpBody`).
|
|
1246
|
+
46. Retried operations have proven idempotency, and exhausted failures remain visible unless a truthful fallback exists.
|
|
1247
|
+
47. Provider/network calls do not run inside authoritative database transactions.
|