@drkmttr/adhere 0.6.1 → 0.7.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 (36) hide show
  1. package/README.md +59 -31
  2. package/package.json +7 -7
  3. package/presets/effect/basics/external-calls-are-resilient.md +6 -2
  4. package/presets/effect/basics/fn-for-named-effects.md +10 -2
  5. package/presets/effect/basics/gen-for-sequencing.md +10 -2
  6. package/presets/effect/basics/instrument-with-pipe.md +9 -2
  7. package/presets/effect/cli/handlers-delegate.md +13 -2
  8. package/presets/effect/config/business-logic-depends-on-config-service.md +9 -2
  9. package/presets/effect/config/secrets-are-redacted.md +6 -2
  10. package/presets/effect/config/tests-provide-values-directly.md +8 -2
  11. package/presets/effect/config/validate-with-schema.md +9 -2
  12. package/presets/effect/data/brand-meaningful-primitives.md +11 -2
  13. package/presets/effect/data/decode-json-with-schema.md +6 -2
  14. package/presets/effect/data/records-are-schema-classes.md +9 -2
  15. package/presets/effect/data/variants-are-tagged-unions.md +9 -2
  16. package/presets/effect/errors/catch-defects-at-boundaries-only.md +6 -2
  17. package/presets/effect/errors/defects-for-bugs.md +13 -2
  18. package/presets/effect/errors/domain-errors-are-tagged.md +6 -2
  19. package/presets/effect/errors/wrap-external-errors.md +7 -2
  20. package/presets/effect/platform/os-access-through-platform-services.md +8 -2
  21. package/presets/effect/services/fresh-layer-per-test.md +9 -2
  22. package/presets/effect/services/memoize-parameterized-layers.md +9 -2
  23. package/presets/effect/services/methods-have-no-requirements.md +9 -2
  24. package/presets/effect/services/no-mutable-state.md +9 -2
  25. package/presets/effect/services/provide-at-entry.md +7 -2
  26. package/presets/effect/services/test-layers-are-in-memory.md +12 -2
  27. package/presets/effect/testing/test-clock-for-time.md +11 -2
  28. package/presets/effect/testing/test-random.md +9 -2
  29. package/skills/adhere/SKILL.md +24 -17
  30. package/src/cli.ts +2 -2
  31. package/src/config.ts +67 -13
  32. package/src/init.ts +10 -8
  33. package/src/markdown.ts +59 -25
  34. package/src/services/Jev.ts +45 -31
  35. package/src/workflows/audit.ts +3 -5
  36. package/src/workflows/format.ts +7 -3
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # adhere
2
2
 
3
3
  A linter for rules a normal linter cannot check. A rule is a one-sentence
4
- description like "business logic should live in services", plus correct
5
- reference code / code to avoid / or both. For each
6
- source file, adhere asks [Jev](https://typesafe.ai) (TypeSafe AI's System One
4
+ description in RFC 2119's words, like "business logic must live in services",
5
+ with code that must be written that way, code that must never be, or both. For
6
+ each source file, adhere asks [Jev](https://typesafe.ai) (TypeSafe AI's System One
7
7
  model) whether the file breaks each rule, gets a calibrated probability per
8
8
  rule, and reports the ones above a threshold with the line Jev points at. The
9
9
  report uses the same frame as `vp lint`.
@@ -61,8 +61,8 @@ Found 1 error.
61
61
  ```
62
62
 
63
63
  The header is the rule id, Jev's probability, and the rule's description. The
64
- hint is the rule's reference; a rule with only code to avoid shows that code,
65
- labeled `avoid:`, instead. On a terminal, the report is in color and the code
64
+ hint is the rule's code that must be written; a rule with only code that must
65
+ never be written shows that code, labeled `never:`, instead. On a terminal, the report is in color and the code
66
66
  in it is highlighted. The exit code is 0 when nothing is reported and 1 when
67
67
  something is. It is also 1 when the run refuses, for example on an invalid
68
68
  config or rule file, a missing API key, or an unknown command or flag; a
@@ -73,7 +73,7 @@ refusal prints its reason.
73
73
  ```sh
74
74
  adhere lint # audit the working directory
75
75
  adhere lint --preset effect # add a built-in rule set; the config becomes optional
76
- adhere lint --threshold 0.8 # replace the config's threshold; per-rule thresholds still apply
76
+ adhere lint --threshold 0.9 # replace the config's threshold; per-rule thresholds still apply
77
77
  adhere lint --yes # send the requests without asking first
78
78
  adhere validate # load the config and rules, ask Jev whether any contradict
79
79
  adhere init [--force] # scaffold .adhere/config.ts and two example rules
@@ -161,17 +161,17 @@ import type { Config } from "@drkmttr/adhere";
161
161
 
162
162
  export default {
163
163
  model: "jev-latest", // optional, default "jev-latest"
164
- threshold: 0.7, // optional, default 0.7
164
+ threshold: 0.8, // optional, default 0.8
165
165
  presets: ["effect"], // optional, built-in rule sets
166
166
  rules: {
167
167
  "data/brand-meaningful-primitives": {
168
168
  description:
169
- "A primitive with semantic meaning, such as an id, email, URL, port, or count, should be a branded schema.",
170
- reference: `
169
+ "A primitive with semantic meaning, such as an id, email, URL, port, or count, must be a branded schema.",
170
+ must: `
171
171
  const UserId = Schema.String.pipe(Schema.brand("UserId"))
172
172
  type UserId = typeof UserId.Type
173
173
  `,
174
- avoid: "type UserId = string", // optional: what a violation looks like
174
+ never: "type UserId = string", // optional: what a violation looks like
175
175
  threshold: 0.8, // optional per-rule override
176
176
  },
177
177
  },
@@ -196,44 +196,54 @@ config and the cache (neither is read as a rule). The path without `.md` is the
196
196
  rule id, so `.adhere/data/brand-ports.md` is `data/brand-ports`. When that
197
197
  directory exists, it is read without any config.
198
198
 
199
- A file is front matter, then a body. The first fenced code block in the body
200
- is the reference; prose around it renders on GitHub and is ignored. Without a
201
- fence, the whole body is the reference. A fence tagged `avoid` after its
202
- language holds code to avoid instead: what a violation looks like.
199
+ A file is front matter, then a body. The body's code goes in fences tagged,
200
+ after the language, with RFC 2119's words: `must` for code that must be
201
+ written, and `never` for code that must never be, which is what a violation
202
+ looks like. Prose around them renders on GitHub and is ignored. An untagged
203
+ fence is code that must be written, and without a fence the whole body is.
204
+ `avoid`, the tag before 0.7, reads as `never`.
203
205
 
204
206
  ````md
205
207
  ---
206
- description: A port is a branded, range-checked integer, not a bare number.
208
+ description: A port must be a branded, range-checked integer, never a bare number.
207
209
  threshold: 0.8
208
210
  ---
209
211
 
210
212
  Why: a bare `number` accepts 70000 and -1.
211
213
 
212
- ```ts
214
+ ```ts must
213
215
  const Port = Schema.Int.pipe(
214
216
  Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
215
217
  Schema.brand("Port"),
216
218
  );
217
219
  ```
218
220
 
219
- Not this:
220
-
221
- ```ts avoid
221
+ ```ts never
222
222
  const port: number = Number(process.env.PORT);
223
223
  ```
224
224
  ````
225
225
 
226
- `description` is required, with a reference, code to avoid, or both.
226
+ `description` is required, with code under `must`, `never`, or both.
227
227
  `threshold` is optional. A file that fails validation refuses the run with its
228
228
  path in the message. `loadRules(directory)` from the package root does the same
229
229
  load for your own tooling.
230
230
 
231
- With a reference, Jev is asked whether the file diverges from it, and code to
232
- avoid, when there is some, is its example of diverging. With only code to
233
- avoid, Jev is asked whether the file contains it. That suits a rule with no
234
- single correct form to show, such as a hand-rolled retry loop or an error
235
- caught and dropped. Neither goes in the other's place: code to avoid in the
236
- reference reads to Jev as the pattern to follow.
231
+ Jev reads the code under the same words, and is asked whether the file
232
+ diverges from the pattern `must` shows, with `never` as an example of
233
+ diverging. Write the description in them too, so the rule and its code agree:
234
+ "must" and "never", as above.
235
+
236
+ A rule with only code that must never be written suits a rule with no single
237
+ correct form to show, such as a hand-rolled retry loop or an error caught and
238
+ dropped. Neither goes in the other's place: code that must never be written,
239
+ under `must`, reads to Jev as the pattern to follow.
240
+
241
+ A rule that is a guideline rather than a requirement says "should" instead:
242
+ in its description, and in fences tagged `should` and `should not` (`should`
243
+ and `shouldNot` inline in a config). Jev then reads the code under `should`
244
+ and `should_not`. A rule is one or the other, so it cannot mix `must` or
245
+ `never` with `should` or `should not`. In a config, `reference` and `avoid`,
246
+ the names before 0.7, read as `must` and `never`.
237
247
 
238
248
  Nested `.adhere/` directories are also discovered, except under
239
249
  `node_modules/`, `dist/`, and the other skipped directories (above). A rule in
@@ -260,6 +270,22 @@ export default { presets: ["effect"], rules: "./docs/adhere" } satisfies Config;
260
270
 
261
271
  Rules read through `rules` apply project-wide.
262
272
 
273
+ ### Rule writing tips
274
+
275
+ We've evaluated different ways of giving Jev a rule, to catch the most
276
+ violations with the fewest false positives. In general:
277
+
278
+ - Say "must" for what code must do and "never" for what it must not, in the
279
+ description and as the fence tags.
280
+ - Give one example of each: one `must` block and one `never` block. Three of
281
+ each did no better, and several of one kind alone did worse.
282
+ - For a guideline rather than a requirement, say "should" and "should not"
283
+ the same way.
284
+
285
+ On the Effect preset, rules written this way cut the findings Jev got wrong at
286
+ the default threshold from 6 to 1, and caught as many violations. For more
287
+ about the evaluations, see [the eval](eval/README.md).
288
+
263
289
  ### Presets
264
290
 
265
291
  A preset has the shape of a config without `presets`: `rules`, and optionally
@@ -273,7 +299,7 @@ case the config file is optional.
273
299
  ### Precedence
274
300
 
275
301
  Highest first: `--threshold` on the command line, the config file, presets in
276
- order (a later preset wins), then the defaults `jev-latest` and `0.7`. A rule
302
+ order (a later preset wins), then the defaults `jev-latest` and `0.8`. A rule
277
303
  in `rules` replaces a preset rule with the same id. A rule's own `threshold`
278
304
  beats all of the above for that rule.
279
305
 
@@ -281,10 +307,12 @@ beats all of the above for that rule.
281
307
 
282
308
  1. One Jev request per file. The state is the file's numbered lines and
283
309
  nothing else. Each rule is one `noul` (yes/no probability) question that
284
- carries the rule's description, reference, and code to avoid: whether the
285
- file diverges from the reference or, for a rule with only code to avoid,
286
- whether it contains that code. Criteria for each answer draw the line at
287
- the rule's scope, so a file with no code the rule is about is a no. Since
310
+ carries the rule's description and its code under its words, `must` and
311
+ `never` (or a guideline's `should` and `should_not`): whether the file
312
+ diverges from the pattern `must` shows, with `never` as an example of
313
+ diverging, or, for a rule with only code that must never be written,
314
+ whether the file contains that code. Criteria for each answer draw the line
315
+ at the rule's scope, so a file with no code the rule is about is a no. Since
288
316
  no rule sits in the shared state, a rule's probability depends only on the
289
317
  file and that rule, not on which other rules share the request. Every
290
318
  question shares the state cost of the request.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@drkmttr/adhere",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
- "description": "Lints a repository against reference code in .adhere/, judged by Jev.",
5
+ "description": "Lints a repository against the rules in .adhere/, judged by Jev.",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/darkmatter/adhere.git"
@@ -67,10 +67,10 @@
67
67
  }
68
68
  },
69
69
  "optionalDependencies": {
70
- "@drkmttr/adhere-darwin-arm64": "0.6.1",
71
- "@drkmttr/adhere-darwin-x64": "0.6.1",
72
- "@drkmttr/adhere-linux-arm64": "0.6.1",
73
- "@drkmttr/adhere-linux-x64": "0.6.1",
74
- "@drkmttr/adhere-win32-x64": "0.6.1"
70
+ "@drkmttr/adhere-darwin-arm64": "0.7.0",
71
+ "@drkmttr/adhere-darwin-x64": "0.7.0",
72
+ "@drkmttr/adhere-linux-arm64": "0.7.0",
73
+ "@drkmttr/adhere-linux-x64": "0.7.0",
74
+ "@drkmttr/adhere-win32-x64": "0.7.0"
75
75
  }
76
76
  }
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A call over the network, such as an HTTP request, a database query, or a third-party API, carries a timeout and a retry schedule. Calls through the platform FileSystem and Path services are local and are not in scope.
2
+ description: A call over the network, such as an HTTP request, a database query, or a third-party API, must carry a timeout and a retry schedule. Calls through the platform FileSystem and Path services are local and are not in scope.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const retryPolicy = Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)));
7
7
 
8
8
  const resilientCall = HttpClient.get("https://api.example.com/users").pipe(
@@ -11,3 +11,7 @@ const resilientCall = HttpClient.get("https://api.example.com/users").pipe(
11
11
  Effect.timeout("10 seconds"),
12
12
  );
13
13
  ```
14
+
15
+ ```ts never
16
+ const response = yield* HttpClient.get(`https://api.example.com/users/${id}`);
17
+ ```
@@ -1,11 +1,19 @@
1
1
  ---
2
- description: A named function that returns an Effect should be defined with Effect.fn so the call site is traced.
2
+ description: A named function that returns an Effect must be defined with Effect.fn so the call site is traced. An Effect held in a constant, such as `const program = Effect.gen(...)`, is not a function and is not in scope.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const processUser = Effect.fn("processUser")(function* (userId: string) {
7
7
  yield* Effect.logInfo(`Processing user ${userId}`);
8
8
  const user = yield* getUser(userId);
9
9
  return yield* processData(user);
10
10
  });
11
11
  ```
12
+
13
+ ```ts never
14
+ const loadUser = (id: string) =>
15
+ Effect.gen(function* () {
16
+ const repo = yield* UserRepo;
17
+ return yield* repo.find(id);
18
+ });
19
+ ```
@@ -1,11 +1,19 @@
1
1
  ---
2
- description: Sequential effectful steps should be written with Effect.gen and yield*, not nested flatMap or callback chains.
2
+ description: Sequential effectful steps must be written with Effect.gen and yield*, never as nested flatMap or callback chains.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const program = Effect.gen(function* () {
7
7
  const data = yield* fetchData;
8
8
  yield* Effect.logInfo(`Processing data: ${data}`);
9
9
  return yield* processData(data);
10
10
  });
11
11
  ```
12
+
13
+ ```ts never
14
+ const page = fetchUser(id).pipe(
15
+ Effect.flatMap((user) =>
16
+ fetchOrders(user.id).pipe(Effect.flatMap((orders) => render(user, orders))),
17
+ ),
18
+ );
19
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Timeouts, retries, logging, and spans should be attached with .pipe, not written into the body of the effect.
2
+ description: Timeouts, retries, logging, and spans must be attached with .pipe, never written into the body of the effect.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const program = fetchData.pipe(
7
7
  Effect.timeout("5 seconds"),
8
8
  Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3)))),
@@ -10,3 +10,10 @@ const program = fetchData.pipe(
10
10
  Effect.withSpan("fetchData"),
11
11
  );
12
12
  ```
13
+
14
+ ```ts never
15
+ const report = Effect.gen(function* () {
16
+ const rows = yield* Effect.timeout(loadRows, "5 seconds");
17
+ return yield* Effect.withSpan(summarize(rows), "summarize");
18
+ });
19
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A command handler parses input and calls a service. Business logic lives in the service.
2
+ description: A command handler must only parse input and call a service. Business logic must live in the service, never in the handler.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const addCommand = Command.make("add", { text }, ({ text }) =>
7
7
  Effect.gen(function* () {
8
8
  const repo = yield* TaskRepo;
@@ -11,3 +11,14 @@ const addCommand = Command.make("add", { text }, ({ text }) =>
11
11
  }),
12
12
  ).pipe(Command.withDescription("Add a new task"));
13
13
  ```
14
+
15
+ ```ts never
16
+ Command.make("rename", { from, to }, ({ from, to }) =>
17
+ Effect.gen(function* () {
18
+ const fs = yield* FileSystem.FileSystem;
19
+ const text = yield* fs.readFileString(from);
20
+ yield* fs.writeFileString(to, text.replaceAll("draft", "final"));
21
+ yield* fs.remove(from);
22
+ }),
23
+ );
24
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Business logic depends on a config service. Config primitives should be read only inside that service's layer.
2
+ description: Business logic must depend on a config service. Config primitives must be read only inside that service's layer, never in business logic.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  class ApiConfig extends Context.Service<
7
7
  ApiConfig,
8
8
  {
@@ -20,3 +20,10 @@ class ApiConfig extends Context.Service<
20
20
  );
21
21
  }
22
22
  ```
23
+
24
+ ```ts never
25
+ const sendInvoice = Effect.fn("sendInvoice")(function* (invoice: Invoice) {
26
+ const apiUrl = yield* Config.string("BILLING_API_URL");
27
+ return yield* post(apiUrl, invoice);
28
+ });
29
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A token, password, or key should be read with Config.redacted.
2
+ description: A token, password, or key must be read with Config.redacted, never with Config.string.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const program = Effect.gen(function* () {
7
7
  const apiKey = yield* Config.redacted("API_KEY");
8
8
 
@@ -13,3 +13,7 @@ const program = Effect.gen(function* () {
13
13
  return headers;
14
14
  });
15
15
  ```
16
+
17
+ ```ts never
18
+ const token = yield* Config.string("GITHUB_TOKEN");
19
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A test supplies config with Layer.succeed on the config service, not by setting environment variables or a ConfigProvider.
2
+ description: A test must supply config with Layer.succeed on the config service, never by setting environment variables or a ConfigProvider.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  Effect.runPromise(
7
7
  program.pipe(
8
8
  Effect.provide(
@@ -14,3 +14,9 @@ Effect.runPromise(
14
14
  ),
15
15
  );
16
16
  ```
17
+
18
+ ```ts never
19
+ beforeAll(() => {
20
+ process.env.DATABASE_URL = "postgres://localhost/test";
21
+ });
22
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A config value with constraints should be read with Config.schema, not mapped and checked by hand.
2
+ description: A config value with constraints must be read with Config.schema, never mapped and checked by hand.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const Port = Schema.NumberFromString.pipe(
7
7
  Schema.check(Schema.isInt()),
8
8
  Schema.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
@@ -16,3 +16,10 @@ const program = Effect.gen(function* () {
16
16
  return { port, env };
17
17
  });
18
18
  ```
19
+
20
+ ```ts never
21
+ const timeoutMs = yield* Config.number("TIMEOUT_MS");
22
+ if (timeoutMs <= 0 || timeoutMs > 60_000) {
23
+ return yield* Effect.die("TIMEOUT_MS out of range");
24
+ }
25
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A primitive with semantic meaning, such as an id, email, URL, port, or count, should be a branded schema.
2
+ description: A primitive with semantic meaning, such as an id, email, URL, port, or count, must be a branded schema, never a bare string or number.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  export const UserId = Schema.String.pipe(Schema.brand("UserId"));
7
7
  export type UserId = typeof UserId.Type;
8
8
 
@@ -15,3 +15,12 @@ export const Port = Schema.Int.pipe(
15
15
  );
16
16
  export type Port = typeof Port.Type;
17
17
  ```
18
+
19
+ ```ts never
20
+ type AccountId = string;
21
+
22
+ const Session = Schema.Struct({
23
+ accountId: Schema.String,
24
+ retries: Schema.Number,
25
+ });
26
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: JSON crossing a boundary should be decoded with Schema.fromJsonString, not JSON.parse followed by a cast.
2
+ description: JSON crossing a boundary must be decoded with Schema.fromJsonString, never with JSON.parse followed by a cast.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const MoveFromJson = Schema.fromJsonString(Move);
7
7
 
8
8
  const program = Effect.gen(function* () {
@@ -12,3 +12,7 @@ const program = Effect.gen(function* () {
12
12
  return json;
13
13
  });
14
14
  ```
15
+
16
+ ```ts never
17
+ const settings = JSON.parse(text) as Settings;
18
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Domain records should be defined with Schema.Class, not with a plain interface or type alias.
2
+ description: Domain records must be defined with Schema.Class, never with a plain interface or type alias.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  export class User extends Schema.Class<User>("User")({
7
7
  id: UserId,
8
8
  name: Schema.String,
@@ -14,3 +14,10 @@ export class User extends Schema.Class<User>("User")({
14
14
  }
15
15
  }
16
16
  ```
17
+
18
+ ```ts never
19
+ interface Invoice {
20
+ readonly id: InvoiceId;
21
+ readonly total: Money;
22
+ }
23
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Structured variants should be Schema.TaggedClass in a Schema.Union and matched with Match.tag and Match.exhaustive.
2
+ description: Structured variants must be Schema.TaggedClass in a Schema.Union, matched with Match.tag and Match.exhaustive, never a plain union checked with a switch.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  export class Success extends Schema.TaggedClass<Success>("Success")("Success", {
7
7
  value: Schema.Number,
8
8
  }) {}
@@ -21,3 +21,10 @@ const renderResult = (result: Result) =>
21
21
  Match.exhaustive,
22
22
  );
23
23
  ```
24
+
25
+ ```ts never
26
+ type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
27
+
28
+ const area = (shape: Shape) =>
29
+ shape.kind === "circle" ? Math.PI * shape.radius ** 2 : shape.side ** 2;
30
+ ```
@@ -1,11 +1,15 @@
1
1
  ---
2
- description: Defects should be caught only at a system boundary for logging or shutdown, never in business logic.
2
+ description: Defects must be caught only at a system boundary for logging or shutdown, never in business logic.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  // At app entry: if config fails, nothing can proceed
7
7
  const main = Effect.gen(function* () {
8
8
  const config = yield* loadConfig.pipe(Effect.orDie);
9
9
  yield* Effect.log(`Starting on port ${config.port}`);
10
10
  });
11
11
  ```
12
+
13
+ ```ts never
14
+ const price = computePrice(cart).pipe(Effect.catchDefect(() => Effect.succeed(0)));
15
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A typed error should be for a failure the caller can handle. A bug or invariant violation should be a defect.
2
+ description: A typed error must be for a failure the caller can handle. A bug or invariant violation must be a defect, never a typed error.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const recovered: Effect.Effect<string, ValidationError> = program.pipe(
7
7
  Effect.catchTag("HttpError", (error) =>
8
8
  Effect.gen(function* () {
@@ -17,3 +17,14 @@ const main = Effect.gen(function* () {
17
17
  yield* Effect.log(`Starting on port ${config.port}`);
18
18
  });
19
19
  ```
20
+
21
+ ```ts never
22
+ class CacheCorrupted extends Schema.TaggedError<CacheCorrupted>()("CacheCorrupted", {
23
+ key: Schema.String,
24
+ }) {}
25
+
26
+ const entry = yield* cache.get(key);
27
+ if (entry !== undefined && entry.key !== key) {
28
+ return yield* new CacheCorrupted({ key });
29
+ }
30
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: A domain failure should be a Schema.TaggedError with its own tag.
2
+ description: A domain failure must be a Schema.TaggedError with its own tag, never a bare Error or a plain object.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
7
7
  field: Schema.String,
8
8
  message: Schema.String,
@@ -13,3 +13,7 @@ class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError",
13
13
  id: Schema.String,
14
14
  }) {}
15
15
  ```
16
+
17
+ ```ts never
18
+ return yield* Effect.fail(new Error("Card declined"));
19
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: An error from an external library should be wrapped in a tagged error with a Schema.Defect field, not passed through raw.
2
+ description: An error from an external library must be wrapped in a tagged error with a Schema.Defect field, never passed through raw.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
7
7
  endpoint: Schema.String,
8
8
  statusCode: Schema.Number,
@@ -22,3 +22,8 @@ const fetchUser = (id: string) =>
22
22
  ),
23
23
  );
24
24
  ```
25
+
26
+ ```ts never
27
+ const charge = (amount: number) =>
28
+ Effect.tryPromise(() => stripe.charges.create({ amount, currency: "usd" }));
29
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: File, path, process, terminal, and HTTP access goes through Effect's platform services (FileSystem, Path, ChildProcess, Terminal, HttpClient, KeyValueStore), acquired with yield*, not through node: builtins, Bun globals, or fetch. The Bun implementations should be provided once at the entry point with BunServices.layer.
2
+ description: File, path, process, terminal, and HTTP access must go through Effect's platform services (FileSystem, Path, ChildProcess, Terminal, HttpClient, KeyValueStore), acquired with yield*, never through node: builtins, Bun globals, or fetch. The Bun implementations must be provided once at the entry point with BunServices.layer.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  import { Effect, FileSystem, Path } from "effect";
7
7
  import { HttpClient, HttpClientResponse } from "effect/unstable/http";
8
8
  import { BunRuntime, BunServices } from "@effect/platform-bun";
@@ -18,3 +18,9 @@ const program = Effect.gen(function* () {
18
18
 
19
19
  program.pipe(Effect.provide(BunServices.layer), BunRuntime.runMain);
20
20
  ```
21
+
22
+ ```ts never
23
+ import { readFileSync } from "node:fs";
24
+
25
+ const settings = readFileSync("settings.json", "utf8");
26
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Each it.effect provides its own layer. it.layer should be used only to share an expensive resource across a suite.
2
+ description: Each it.effect must provide its own layer. it.layer must be used only to share an expensive resource across a suite, never a cheap one.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  it.effect("starts at zero", () =>
7
7
  Effect.gen(function* () {
8
8
  const counter = yield* Counter;
@@ -18,3 +18,10 @@ it.effect("increments without leaking", () =>
18
18
  }).pipe(Effect.provide(Counter.layer)),
19
19
  );
20
20
  ```
21
+
22
+ ```ts never
23
+ it.layer(Counter.layer)("Counter", (it) => {
24
+ it.effect("starts at zero", () => Effect.gen(function* () { /* ... */ }));
25
+ it.effect("increments", () => Effect.gen(function* () { /* ... */ }));
26
+ });
27
+ ```
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: The result of a parameterized layer constructor should be stored in a module constant before it is used in more than one place.
2
+ description: The result of a parameterized layer constructor must be stored in a module constant before it is used in more than one place, never constructed again for each use.
3
3
  ---
4
4
 
5
- ```ts
5
+ ```ts must
6
6
  const postgresLayer = Postgres.layer({ url: "postgres://localhost/mydb", poolSize: 10 });
7
7
 
8
8
  const goodAppLayer = Layer.merge(
@@ -10,3 +10,10 @@ const goodAppLayer = Layer.merge(
10
10
  OrderRepo.layer.pipe(Layer.provide(postgresLayer)),
11
11
  );
12
12
  ```
13
+
14
+ ```ts never
15
+ const Live = Layer.merge(
16
+ Sessions.layer.pipe(Layer.provide(Redis.layer({ host: "localhost" }))),
17
+ RateLimits.layer.pipe(Layer.provide(Redis.layer({ host: "localhost" }))),
18
+ );
19
+ ```