@deslop/coding-standards 0.1.1077 → 0.1.1081

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.
@@ -3,341 +3,76 @@ name: engineering
3
3
  description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product and test code, including prototypes.'
4
4
  ---
5
5
 
6
- **Scope.** Apply these rules and the repository's `CODING_STANDARDS.md` when present. Aim for a structurally correct first pass with few lint errors, not a promise of zero diagnostics. Oxlint remains the authoritative check; instructions do not replace it. Oxfmt owns whitespace, indentation and other mechanical formatting. Use automatic formatting and available fixes instead of spending reasoning on those details; fix remaining structural diagnostics at their source without a full rewrite or weakened rules. A refactor or cleanup request applies every rule to every line of the owned files. Scratch code outside the repository follows no style rule. Create a missing standards file only to hold a [Patterns](#patterns) or Taste row.
6
+ Oxlint and Oxfmt check every change and stay authoritative. This skill holds what they cannot check, and the forms they require without an autofix, to reduce first-pass corrections. Apply it with the repository's `CODING_STANDARDS.md`. A refactor or cleanup request applies every rule to every line of the owned files. Scratch code outside the repository follows no style rule.
7
7
 
8
- **Before checking.** Run Oxfmt and the repository's deterministic lint autofix before manual code analysis, independent review or a check-only lint pass. Use the owned scope and preserve unrelated changes. Run both even when generated code looks correct; automatic fixes need no model iteration. Analyze the remaining diagnostics and behavior only after normalization. For a read-only review, the author prepares the normalized diff; the reviewer never writes the reviewed checkout.
8
+ - **Normalize.** Run the repository's fix command, or Oxfmt and the deterministic lint autofix, on the owned scope before you analyze code, review it or run a check-only lint. Automatic fixes need no model iteration; analyze only the diagnostics that remain. For a read-only review, the author normalizes and the reviewer never writes the checkout.
9
+ - **First pass.** Write the [Lint forms](#lint-forms) from the start. Fix a remaining diagnostic at its source with the form its message or the table names. Never use a cast, a weakened rule or a rewrite that trades one diagnostic for another.
10
+ - **Type early.** Type errors cause most fix loops: run the repository's check after each small batch of edits, not once after the whole feature. Read the types of an API you have not used in the installed version's source before you call it. Fix the first error in each file first: one unresolved type or import turns into hundreds of implicit `any` errors, such as TS7006 and TS7031, in every consumer.
11
+ - **Disables.** An inline disable needs an unavoidable conflict with an outside contract and a `--` reason that names it. Never disable ordering or weaken file configuration. Report dependency-only diagnostics; rename filename violations.
9
12
 
10
13
  ## Simplicity
11
14
 
12
- **Architecture.** Use architecture for a new boundary, state owner, background lifetime, dependency or storage contract. Reuse the existing shape for routine changes; do not turn a small feature into a framework.
13
-
14
- **MVP.** Write the most direct code that meets the request today. Apply YAGNI to every file, function, branch, parameter, option, export, Layer, check, config value, library and abstraction; configure only what callers vary. Write functional, explicit code: pure functions over immutable data, effects only inside Effect, and Effect modules instead of hand-written helpers.
15
-
16
- **Plan the shape.** Before writing, choose the file tree, entrypoints and the smallest mechanism per feature, checked against existing repository rules and the canonical examples from [Patterns](#patterns).
17
-
18
- **Clarity.** Code, names and files read top to bottom. Rename or split code instead of narrating it.
19
-
20
- **Comments.** Default to no comments. Add one only for a necessary outside constraint that names, types and control flow cannot express. Keep it to one line. Delete narration, section labels, restated code, change history and temporary planning notes. Required diagnostic directives retain their reason.
21
-
22
- **Reuse first.** Check existing code, installed modules and platform capabilities before adding code. Extend the canonical example: match its permissions, errors, refresh and tests, and reuse its helpers. Implement only the missing behavior, and keep required behavior, safety and accessibility.
23
-
24
- **Inward dependencies.** Domain and service code never import HTTP, RPC, or other transport types, and domain schemas carry no transport annotation such as an HTTP status.
25
-
26
- **Building blocks.** Design each new public interface from a realistic caller operation and its ordering, failure and lifetime obligations; hide useful complexity, not forwarded names. Offer services, Layers and functions that the caller composes like any Effect module, never a bundle such as `serve(app)`.
27
-
28
- **Behavior kept.** A refactor keeps observable behavior at every consumer; printed diagnostics are not a contract. Accept only a difference that no consumer observes or that a rule here causes, such as an error that now keeps its cause, and report each one. Any other behavior change needs the request.
29
-
30
- **Reachable bugs.** Fix a bug that real input reaches inside the request's scope, and check every consumer of the changed output. For a hard bug, use one reproduction and one hypothesis per probe; remove edits from disproven hypotheses and rerun the original scenario. Delete handling only for input the contract excludes; keep and report code that looks intentional.
31
-
32
- **Performance.** Optimize only when the request or a contract needs it. Measure one real action on realistic input before and after, where the symptom shows: spans for backend calls, React commits for rerenders, a browser profile for main-thread work. Report run count and variation; a difference within noise is unproved.
33
-
34
- **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
35
-
36
- **Happy path.** Failures flow through the error channel: no catch, retry, fallback or defensive check unless the request or a contract needs it. Map an expected failure to the service's error; never `Effect.ignore` it or turn it into a defect with `Effect.orDie`.
37
-
38
- **Trusted data.** Validate and transform once at the boundary with Effect Schema, and carry narrowed values forward. Never re-check what the schema, the declared type, an earlier filter or every caller guarantees.
39
-
40
- ```ts
41
- // good
42
- const add = Effect.fn('Ledger.add')(function* (draft: LedgerDraft) {
43
- yield* Ref.update(entries, Array.append(draft))
44
- })
45
- // bad
46
- Schema.decodeEffect(LedgerDraft)(draft) // draft is already a LedgerDraft
47
- variable.init?.type === 'TSSatisfiesExpression' // init was already narrowed to non-null
48
- ```
49
-
50
- **Effect first.** Search the cloned Effect modules before you hand-write traversal, state, configuration or platform operations. Replace dependencies that Effect or its platform adapters cover.
15
+ - **MVP.** Write the most direct code that meets the request today. Apply YAGNI to every file, function, branch, parameter, option, export, Layer, check, config value, library and abstraction; configure only what callers vary. Write pure functions over immutable data, effects only inside Effect.
16
+ - **Plan the shape.** Before writing, choose the file tree, entrypoints and the smallest mechanism per feature from the canonical examples in [Patterns](#patterns). Use architecture for a new boundary, state owner, background lifetime, dependency or storage contract.
17
+ - **Clarity.** Code, names and files read top to bottom. Rename or split code instead of narrating it. Write a comment only for an outside constraint that names, types and control flow cannot express.
18
+ - **Reuse first.** Check existing code, installed modules and platform capabilities before you add code. Extend the canonical example: match its permissions, errors, refresh and tests, and reuse its helpers.
19
+ - **Effect first.** Search the cloned Effect modules before you hand-write traversal, state, configuration or platform operations, and replace dependencies that Effect or its platform adapters cover.
20
+ - **Inward dependencies.** Domain and service code never import HTTP, RPC or other transport types, and domain schemas carry no transport annotation such as an HTTP status.
21
+ - **Building blocks.** Design a new public interface from a realistic caller operation and its ordering, failure and lifetime obligations. Offer services, Layers and functions that the caller composes like any Effect module, never a bundle such as `serve(app)`.
22
+ - **Behavior kept.** A refactor keeps observable behavior at every consumer; printed diagnostics are not a contract. Accept only a difference that no consumer observes or that a rule here causes, such as an error that now keeps its cause, and report it; any other behavior change needs the request.
23
+ - **Reachable bugs.** Fix a bug that real input reaches inside the request's scope, and check every consumer of the changed output. Delete handling only for input the contract excludes; keep and report code that looks intentional.
24
+ - **Performance.** Optimize only when the request or a contract needs it. Measure one real action on realistic input before and after, where the symptom shows: spans for backend calls, React commits for rerenders, a browser profile for main-thread work. Report run count and variation; a difference within noise is unproved.
25
+ - **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
26
+ - **Happy path.** Failures flow through the error channel: no catch, retry, fallback or defensive check unless the request or a contract needs it.
27
+ - **Trusted data.** Decode once at the boundary with Effect Schema and carry the narrowed value forward. Never re-check what the schema, the declared type, an earlier filter or every caller guarantees.
28
+ - **Whole values.** Pass whole values or spread them, named by their role. Project fields only to keep data from a recipient that must not get it, such as a bearer token. When a name shadows an outer one, rename by role; never add an underscore or a number.
29
+ - **Options.** Use Option where it composes with other Option operators; keep a plain optional value otherwise.
30
+ - **Once.** One fact has one owner: extract repeated logic once, give a domain enum one schema, and give a cache or atom family key the full identity of what it caches.
31
+ - **One way.** No duplicate, subset or superset fields, methods, options or exports. Store and send canonical data, derive display values, expose each state once and remove states no caller produces.
32
+ - **No compatibility.** Rename the shape and every caller together. No alias, fallback, deprecated path or flag unless a documented or user-named contract requires it.
33
+ - **Dependencies.** Declare a new dependency as `latest`; pin only versions a consumer requires. Read the locked version's upstream source before you add a wrapper or workaround, and extend it through its public API, never a patch.
34
+ - **Dead code.** In the same change, delete every superseded or unused file, folder, export, type, branch, config, validation, doc, test and dependency, and the empty directories that remain. Update docs and generated bundles that describe changed behavior.
51
35
 
52
36
  ```ts
53
37
  // good
38
+ const sandboxGone = Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
54
39
  const cycles = Graph.stronglyConnectedComponents(graph)
55
- BigDecimal.format(amount)
56
- Duration.parts(DateTime.distance(oldest.timestamp, newest.timestamp)).days
57
- fs.glob('**/.env*.example', {exclude: ['**/node_modules'], root: config.cwd})
58
- // bad
59
- const digits = BigDecimal.scale(BigDecimal.abs(amount), 2).value.toString().padStart(3, '0')
60
- Math.floor(Duration.toDays(DateTime.distance(oldest.timestamp, newest.timestamp)))
61
- import {glob} from 'glob' // a dependency Effect's FileSystem replaces
62
- ```
63
-
64
- **Whole values.** Pass whole values or spread them, named by their role. Do not destructure parameters, callbacks or loop bindings, except labeled tuple parameters, React state and component refs; read other tuples by index. Project fields only to exclude data the recipient must not get. A leading underscore marks only an unread parameter; never add an underscore or a number to avoid shadowing.
65
-
66
- ```ts
67
- // good
68
- server => ({server: {...server, forwardConsole: true, warmup}})
69
- for (const schema of schemas) report(schema.name)
70
- Record.map(groups, entries => entries.length)
71
40
  upload({scope: input.scope, type: input.type}) // input also holds the bearer token
72
- submission => onSubmit(submission.value)
73
41
  // bad
74
- ({host, port}) => ({server: {forwardConsole: true, host, port, warmup}})
75
- server: {forwardConsole: true, host: config.host, port: config.port, warmup}
76
- for (const {name} of schemas) report(name)
77
- Array.map(Record.toEntries(groups), ([key, entries]) => entries.length)
78
- _input => onSubmit(_input.value) // or input2: an outer input is shadowed
79
- ```
80
-
81
- **Dependencies.** Declare a new dependency as `latest`; pin only versions a consumer requires. Use the locked version's API, and read its upstream source before you add a wrapper or workaround. Extend a dependency through its public API; never patch its source. Library APIs do not define coding policy.
82
-
83
- **Once.** Extract repeated logic once; one fact has one owner. A domain enum, such as a status, has one schema that others reuse. A cache or atom family key carries the full identity of what it caches.
84
-
85
- ```ts
86
- // good
87
- const sandboxGone = Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
88
- // bad — the same mapping written in three handlers
89
- pipe(
90
- sandbox.exec(input),
91
- Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
92
- )
93
- pipe(
94
- sandbox.read(path),
95
- Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
96
- )
42
+ Schema.decodeEffect(LedgerDraft)(draft) // draft is already a LedgerDraft
43
+ import {glob} from 'glob' // Effect's FileSystem has glob
44
+ const digits = BigDecimal.scale(BigDecimal.abs(amount), 2).value.toString().padStart(3, '0') // BigDecimal.format
97
45
  ```
98
46
 
99
- **Canonical data.** Store and send canonical data, derive display values, expose each state once and remove states no caller produces.
100
-
101
- **One way.** No duplicate, subset or superset fields, methods, options or exports.
102
-
103
- **No compatibility.** Rename the shape and every caller together. No alias, fallback, deprecated path or flag unless a documented or user-named contract requires it.
104
-
105
- **Dead code.** Delete every superseded or unused file, folder, export, type, branch, config, validation, doc, test and dependency in the same change. Update docs and generated bundles that describe changed behavior in the same commit.
106
-
107
47
  ## Patterns
108
48
 
109
49
  Keep the repository consistent by default, without a reminder, and change a standard everywhere at once.
110
50
 
111
- **Canonical example.** Before writing a recurring shape, such as a page layout, list, form, empty or loading state, dialog, service, route, error mapping, file layout or name, find its canonical example. Use the row in the Patterns section of `CODING_STANDARDS.md` first. Otherwise, use the variant that most current consumers use, and the newest one on a tie; the nearest file is not the example when it uses a minority variant. Copy its structure, components, names, visible terms, copy and states. Depart from it only on request, and never write a recurring shape from scratch when an example exists.
112
-
113
- **Register.** When a task settles a shape that two or more consumers share and the Patterns section has no row for it, add one: the pattern, its owner and canonical example, a search that lists its consumers, and what enforces it.
114
-
115
- **Taste.** Read the Taste section of `CODING_STANDARDS.md` before choosing a layout, file organization, interface or name, and follow its rows like Patterns rows. When the user picks a variant, rejects one or corrects a taste choice that no rule covers, add a row in the same change: the area, the choice, the rejected alternative and his reason in his words. A row that recurs across repositories moves into the owning skill.
116
-
117
- **Pattern change.** A user change to a pattern, style, name or convention changes the repository standard, unless he limits its scope. In the same change, update its owner: the shared component, template, generator, lint rule and Patterns row. List every consumer with the row's search, migrate each one, delete the superseded variant and its exemptions, then run the search for the old form again. Report each consumer kept on purpose with its reason. A migration that crosses another repository or a published contract needs the user's approval first.
118
-
119
- **Enforce.** When the same correction happens twice, make the mistake impossible at the highest level that works: an owner component, export or type that removes the wrong form, then a lint rule whose message names the replacement, then a test. Keep a prose rule only for a judgment that no check can make. Prove a new check against the real past mistake and an accepted control.
120
-
121
- ## Lint pairs
122
-
123
- Lint enforces these forms without an autofix; each example satisfies every rule involved.
124
-
125
- **Fallback.** A default is `??`; never a ternary on the same value.
126
-
127
- ```ts
128
- // good
129
- const root = options?.root ?? '.'
130
- // bad — no-negated-condition flips it, then prefer-nullish-coalescing flags it
131
- const root = options?.root !== undefined ? options.root : '.'
132
- ```
133
-
134
- **Conditions.** A boolean is tested bare; `=== true` only on a value that can be undefined.
135
-
136
- ```tsx
137
- // good
138
- if (enabled) return
139
- aria-current={props.selected === true ? 'page' : undefined}
140
- // bad
141
- if (enabled === true) return // enabled is a boolean
142
- if (props.selected) return // props.selected can be undefined
143
- ```
144
-
145
- **Flat.** Use early-return statements in Effect.fn. No nested ternaries or combinators deeper than equivalent statements. Match and combinators serve single expressions; Boolean.match selects a value, except one-line JSX-attribute ternaries.
146
-
147
- ```ts
148
- // good
149
- const kill = Effect.fn('Sandbox.kill')(function* (sandbox?: Sandbox) {
150
- if (sandbox?.status !== 'running') return
151
-
152
- yield* sandbox.kill
153
- })
154
- Boolean.match(ignoreCase, {onFalse: () => undefined, onTrue: () => 'i'})
155
- // bad
156
- const type = event.type === 'text-delta' ? 'text' : event.type === 'reasoning-delta' ? 'reasoning' : 'unknown'
157
- const flags = ignoreCase ? 'i' : undefined // "instead of a ternary the Boolean.match would be cleaner"
158
- // bad
159
- Array.appendAll(
160
- base,
161
- Boolean.match(hasTools, {
162
- onFalse: () => [],
163
- onTrue: () =>
164
- pipe(
165
- tools,
166
- Array.map(tool => pipe(tool, Option.toArray))
167
- )
168
- })
169
- )
170
- ```
171
-
172
- **Choices.** `Match` with `Match.exhaustive` for a union, never `switch`.
173
-
174
- **Options.** Use Option operators, never `.value`. Use Option where it composes; keep plain optional values otherwise.
175
-
176
- ```ts
177
- // good
178
- Option.map(Number.parse(event.target.value), field.handleChange)
179
- Option.map(Array.head(parts), part => part.text)
180
- // bad
181
- if (Option.isSome(open)) open.value.append(event.delta)
182
- const parsed = Option.getOrUndefined(Number.parse(event.target.value))
183
- ```
184
-
185
- **Handlers.** A handler used once is an arrow in place; a handler used twice is a `function`.
186
-
187
- **Accumulators.** No `let`: a fold or an Effect collection operation that says what it selects.
188
-
189
- ```ts
190
- // good
191
- const total = Array.reduce(values, 0, Number.sum)
192
- pipe(teamId, String.split(','), Array.map(String.trim), Array.findFirst(String.isNonEmpty))
193
- Array.filterMap(items, item =>
194
- Boolean.match(supports(item), {onFalse: () => Result.failVoid, onTrue: () => Result.succeed(item.value)})
195
- )
196
- // bad
197
- let total = 0
198
- for (const value of values) total += value
199
- const [first] = teamId
200
- .split(',')
201
- .map(value => value.trim())
202
- .filter(value => value.length > 0)
203
- items.filter(item => supports(item)).map(item => item.value)
204
- ```
205
-
206
- **Natives.** Use Effect modules instead of globals and native methods. Third-party-owned methods stay, with an inline diagnostic disable and reason when needed.
207
-
208
- ```ts
209
- // good
210
- Number.parse(input.count)
211
- Predicate.isString(value)
212
- Predicate.isNotNullish(annotation)
213
- Array.isReadonlyArrayEmpty(node.arguments)
214
- String.isNonEmpty(event.delta)
215
- Array.get(items, 0)
216
- const serve = Effect.gen(function* () {
217
- const start = yield* Clock.currentTimeMillis
218
- const host = yield* pipe(Config.String('HOST'), Config.withDefault('0.0.0.0'))
219
- const password = yield* Config.Redacted('SMTP_PASS')
220
- yield* Effect.sleep(Duration.millis(delay))
221
- return {host, password, start}
222
- })
223
- // bad
224
- parseInt(input.count)
225
- typeof value === 'string'
226
- annotation !== null && annotation !== undefined
227
- node.arguments.length === 0
228
- Effect.promise(() => new Promise(resolve => setTimeout(resolve, delay)))
229
- process.env.HOST ?? '0.0.0.0'
230
- ```
231
-
232
- **Pipe.** A standalone `pipe`, nested at most three deep; never the `.pipe` method.
233
-
234
- **Failures.** Yield a yieldable domain error directly in a generator: `return yield* E.make(...)`. Keep `Effect.fail(error)` when the error is not yieldable; outside a generator, use `Effect.fail`. Do not infer yieldability from factory-call syntax.
235
-
236
- ```ts
237
- // good
238
- const text = Effect.fn('Ai.text')(function* (response: AiResponse) {
239
- if (response.status === 'refused') return yield* AiError.make({message: 'The model refused the prompt'})
240
-
241
- return response.text
242
- })
243
- // bad
244
- const text = Effect.fn('Ai.text')(function* (response: AiResponse) {
245
- if (response.status === 'refused') yield* Effect.fail(AiError.make({message: 'The model refused the prompt'}))
246
- if (response.status === 'refused') throw new Error('The model refused the prompt')
247
- return response.text
248
- })
249
- ```
250
-
251
- **Casts.** No `as`, `any`, dropped generic, loosened exported type or known shape erased into `unknown`, even to make a symptom go away; `satisfies` and `as const` are the only assertions. Opaque SDK and framework inputs remain `unknown` until narrowed or decoded at their boundary; never fabricate a shape to avoid that type.
252
-
253
- ```ts
254
- // good
255
- const params = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))(part.params)
256
- return Effect.succeed([{text: part.text, type: 'text'} satisfies TextContent])
257
- // bad — a cast satisfies one rule and the next flags it
258
- const params = part.params as Record<string, unknown>
259
- const params: unknown = part.params // erases an already-known shape
260
- ```
261
-
262
- **Decoders.** `Schema.decodeUnknownEffect` only for `unknown` input; typed input uses `Schema.decodeEffect`, and JSON text goes through `Schema.fromJsonString`.
263
-
264
- ```ts
265
- // good
266
- const load = Effect.fn('Plugin.load')(function* (path: string) {
267
- const endpoint = yield* Schema.decodeEffect(Endpoint)(config.endpoint)
268
- const manifest = yield* Schema.decodeEffect(Schema.fromJsonString(PackageManifest))(yield* fs.readFileString(path))
269
- return {endpoint, manifest}
270
- })
271
- // bad
272
- Schema.decodeUnknownEffect(Endpoint)(config.endpoint) // config.endpoint is a string
273
- JSON.parse(text)
274
- ```
275
-
276
- **Imports.** Keep module names; only generated shadcn aliases differ. Separate type imports; packages, blank line, then # aliases. Every export has a consumer.
277
-
278
- ```ts
279
- // good
280
- import {Effect, Schema} from 'effect'
281
- // bad — each line is a diagnostic
282
- import {Schema as S} from 'effect'
283
-
284
- import type {Plugin} from 'vite'
285
- import {type Plugin, createServer} from 'vite'
286
-
287
- import type {PortfolioState} from '#rpcs/contracts.ts'
288
- export const EntryKind = Schema.Literals(['income', 'expense']) // no importer
289
- ```
290
-
291
- **Readonly.** Required on service-shape properties. Allowed on class members and handwritten `Schema.suspend` cycle types; otherwise forbidden.
292
-
293
- **Disables.** Fix a diagnostic with the shown idiom, never a cast, a weakened rule or a rewrite that trades one diagnostic for another, or reproduce a rule defect. Inline disables require an unavoidable conflict and a `--` reason; never disable ordering or weaken file configuration. Report dependency-only diagnostics; rename filename violations.
294
-
295
- ```ts
296
- // good
297
- // oxlint-disable-next-line typescript/consistent-type-definitions -- TanStack Router augments this interface by name.
298
- // @effect-diagnostics-next-line nodeBuiltinImport:off -- Vite's plugin API hands over a Node server.
299
- // bad
300
- "exactOptionalPropertyTypes": false
301
- // oxlint-disable-next-line typescript/consistent-type-assertions
302
- // oxlint-disable-next-line sort-keys -- keeps the printed order
303
- // bad
304
- files: ['packages/components-web/src/form.tsx', 'packages/components-web/src/rich-text-area.tsx'],
305
- ```
51
+ - **Canonical example.** Before you write a recurring shape, such as a page layout, list, form, empty or loading state, dialog, service, route, error mapping, file layout or name, find its canonical example: the row in the Patterns section of `CODING_STANDARDS.md`, else the variant that most current consumers use, the newest one on a tie. Copy its structure, components, names, visible terms, copy and states. Depart from it only on request.
52
+ - **Register.** When a task settles a shape that two or more consumers share and no Patterns row covers it, add one: the pattern, its owner and canonical example, a search that lists its consumers, and what enforces it.
53
+ - **Taste.** The user's taste decides layout, visual design, file organization, module boundaries, public interfaces and names. Read the Taste section of `CODING_STANDARDS.md` first and follow its rows like Patterns rows; decide a choice that no row settles from the closest rows. When the user picks, rejects or corrects a choice that no rule covers, add a row in the same change: the area, the choice, the rejected alternative and his words. Create a missing `CODING_STANDARDS.md` only to hold a Patterns or Taste row. A row that recurs across repositories moves into the owning skill.
54
+ - **Pattern change.** A user change to a pattern, style, name or convention changes the repository standard unless he limits its scope. In the same change, update its owner (shared component, template, generator, lint rule and Patterns row), migrate every consumer that the row's search lists, delete the superseded variant and its exemptions, and run the search again. Report each consumer kept on purpose. A migration across another repository or a published contract needs the user's approval first.
55
+ - **Enforce.** When the same correction happens twice, make the mistake impossible at the highest level that works: an owner component, export or type, then a lint rule whose message names the replacement, then a test. Keep a prose rule only for a judgment that no check can make. Prove a new check against the real past mistake and an accepted control.
306
56
 
307
57
  ## Effect
308
58
 
309
- **Functions.** Use Effect.fn('Name.method') for a traced function with arguments and Effect.fnUntraced for an internal helper. Use Effect.gen for an effect with several statements and no arguments, and a single effect directly. Use Effect.fn.Return for an explicit return type.
59
+ **Functions.** Use `Effect.fn('Service.method')` for a traced function with arguments, `Effect.fnUntraced` for an internal helper, `Effect.gen` for several statements without arguments, and a single effect directly. Use `Effect.fn.Return` only for a generator that calls itself.
310
60
 
311
61
  ```ts
312
- // good
313
- const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage): Effect.fn.Return<void, AiError> {
62
+ const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage) {
314
63
  const input = yield* piUserMessage(message)
315
64
  yield* send(input)
316
65
  })
317
66
  const decodePart = Effect.fnUntraced(function* (part: Part) {
318
67
  return yield* Schema.decodeEffect(Event)(part)
319
68
  })
320
- const stop = Effect.gen(function* () {
321
- yield* setStatus('stopping')
322
- yield* kill
323
- })
324
69
  const restart = setStatus('starting')
325
- // bad
326
- fetch: url => Effect.tryPromise(() => fetch(url))
327
- const decodePart = Effect.fn('decodePart')(function* (part: Part) {}) // a span on an internal helper
328
- const stop = Effect.gen(function* () {
329
- yield* setStatus('stopping')
330
- }) // one statement
331
70
  ```
332
71
 
333
- **Errors.** One domain error per service; preserve cause, redact secrets, map in Effect.fn's pipeline argument. Map SDK unknown errors at the adapter.
72
+ **Errors.** One `Schema.TaggedError` per service. Map in `Effect.fn`'s pipeline argument, keep the cause, redact secrets, and map SDK errors at the adapter, so library failures never leak from the service.
334
73
 
335
74
  ```ts
336
75
  // good
337
- export class AiError extends Schema.TaggedError<AiError>()('AiError', {
338
- cause: Schema.optional(Schema.Defect()),
339
- message: Schema.String
340
- }) {}
341
76
  const prompt = Effect.fn('Ai.prompt')(
342
77
  function* (message: Prompt.UserMessage) {
343
78
  yield* send(message)
@@ -348,23 +83,18 @@ const pages = Effect.tryPromise({
348
83
  catch: cause => NotionError.make({cause, message: 'Cannot list pages'}),
349
84
  try: () => client.pages.list()
350
85
  })
351
- Effect.mapError(cause => WorkspaceError.make({cause: Redacted.make(cause), operation: 'exec input'}))
352
- Effect.tapError(() => cleanup)
353
86
  // bad
354
87
  pipe(
355
88
  fs.readFileString(target),
356
89
  Effect.catch(() => Effect.succeed(''))
357
90
  )
358
- pipe(loadConfig, Effect.retry(Schedule.recurs(3)))
359
- Effect.mapError(failure => new LedgerError({reason: failure.message})) // cause dropped, new
360
- Effect.catch(error => pipe(cleanup, Effect.andThen(Effect.fail(error)))) // fails again with the error it caught
361
- load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError> // library failures leak from the service
91
+ Effect.mapError(failure => new LedgerError({reason: failure.message})) // cause dropped
92
+ load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError> // library failures leak
362
93
  ```
363
94
 
364
- **Services.** Inline a sole implementation in its Layer; name layers for multiple backends. Defaults use Context.Reference. Implement with Service.of or satisfies; service methods are readonly properties.
95
+ **Services.** Inline a sole implementation in its Layer; name layers only for several backends. Use `Context.Reference` for a default. Build the shape with `Service.of` or `satisfies`.
365
96
 
366
97
  ```ts
367
- // good
368
98
  export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger/service/Ledger') {
369
99
  static readonly layer = Layer.effect(
370
100
  this,
@@ -374,243 +104,124 @@ export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger
374
104
  })
375
105
  )
376
106
  }
377
- export class Ai extends Context.Service<Ai, Ai.Agent>()('@app/ai/service/Ai') {
378
- static layerPi(config: Pi.Config) {
379
- return Layer.effect(this, makePi(config))
380
- }
381
- }
382
107
  export const Verbose = Context.Reference<boolean>('@app/ai/Verbose', {defaultValue: () => false})
383
- // bad
384
- class Logger extends Context.Service<Logger>()('Logger', {make: Effect.succeed(service)}) {}
385
- static readonly layer = Layer.effect(Ledger, makeLedger) // a make with one caller
386
- query(sql: string): Effect.Effect<Rows> // a method signature instead of a readonly property
387
108
  ```
388
109
 
389
- **Primitives.** Use Effect's primitives directly; RPCs and services already trace, and one scope owns each lifetime. Run independent input and output with Effect.all and explicit concurrency; keep order-dependent, transactional or resource-conflicting work sequential.
110
+ **Primitives.** Use Effect's primitives directly: RPCs and services already trace, and one scope owns each lifetime, with acquisition and release in one `Effect.acquireRelease`. Run independent input and output with `Effect.all` or `Effect.forEach` and explicit concurrency; keep order-dependent, transactional or conflicting work sequential.
390
111
 
391
- ```ts
392
- // good
393
- const connect = Effect.gen(function* () {
394
- const state = yield* SubscriptionRef.make(PortfolioState.make({}))
395
- const socket = yield* Effect.acquireRelease(openSocket, closeSocket)
396
- return {socket, state}
397
- })
398
- // bad
399
- class State {
400
- update(f: (s: PortfolioState) => PortfolioState) {
401
- return SubscriptionRef.update(this.ref, f)
402
- }
403
- }
404
- const traced = Effect.withSpan('Notes.create')(notes.create(input))
405
- Effect.addFinalizer(() => closeSocket(socket)) // acquisition and release with different owners
406
- ```
112
+ **Failures.** Yield a yieldable domain error directly in a generator: `return yield* E.make(...)`. Use `Effect.fail(error)` for an error that is not yieldable, and for every error outside a generator. Factory-call syntax does not show yieldability; the type does.
407
113
 
408
- **Runtime.** The owned runtime runs the program. Bridge to a Promise once, where an external contract demands it. React is no such contract: atoms run its Effects.
114
+ **Results.** Sort with `Order`, and return a fallible pure value as a `Result`.
409
115
 
410
- ```ts
411
- // good
412
- NodeRuntime.runMain(program)
413
- health: flow(rpc.health, Effect.runPromiseWith(Context.empty())),
414
- // bad
415
- const runtime = ManagedRuntime.make(AppLayer)
416
- const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect)
417
- ```
418
-
419
- **Sorting.** `Order` for sorting, `Result` for a fallible pure value.
420
-
421
- ```ts
422
- // good
423
- Array.sort(
424
- entries,
425
- Order.mapInput(Order.Number, entry => entry.tokens)
426
- )
427
- Result.fromOption(Array.head(parts), () => AiError.make({message: 'The model returned no parts'}))
428
- // bad
429
- entries.sort((left, right) => left.tokens - right.tokens)
430
- ```
116
+ **Runtime.** The entry point runs `NodeRuntime.runMain(program)`. Bridge to a Promise once, where an outside contract demands it, with `Effect.runPromise` and a disable that names the contract. React is no such contract: atoms run its Effects.
431
117
 
432
118
  ## Types and Schema
433
119
 
434
- **Inference.** Infer types. Annotate recursion or branches TypeScript cannot unite; use schema types and indexed members in signatures. Keep intentional widening, not repeated inferred generics.
435
-
436
- ```ts
437
- // good
438
- const balances = Array.reduce(decoded, HashMap.empty<Tag, BigDecimal.BigDecimal>(), sumByTag)
439
- const label = MutableRef.make('') // the initializer infers string, not just ''
440
- const [status, setStatus] = useState<'idle' | 'running'>('idle') // intentional widening
441
- function visit(node: Node): Result {
442
- return visit(node.parent)
443
- }
444
- function settle(amount: LedgerEntry['amount']) {
445
- return BigDecimal.format(amount)
446
- }
447
- export type Sandbox = ReturnType<typeof fromSdk>
448
- // bad
449
- const Input = Schema.Struct({value: Schema.String}) satisfies Schema.Schema<Input>
450
- const label = MutableRef.make<string>('') // repeats the inferred type
451
- export type SandboxLike = {id: string; renew: (seconds: number) => Promise<unknown>} // a copy of an inferred shape
452
- ```
453
-
454
- **Schema pairs.** Type pair immediately before every schema. Structs use Schema.Struct, never Schema.Class; reuse fields by spreading. Use Schema.optionalKey for absent fields; Schema.optional allows explicit undefined too, so reserve it for that contract.
120
+ - **Inference.** Infer types, and check a returned object with `satisfies`. Type an empty or one-element array with `Array.empty<T>()` or `Array.of<T>(...)`. Never drop a generic or loosen an exported type to silence an error. Annotate a return type only on a type guard, an assertion function, a recursive function or a recursive `Schema.suspend` thunk. Use schema types and indexed members, such as `LedgerEntry['amount']`, in signatures. Keep an intentional widening, such as `useState<'idle' | 'running'>('idle')`. Derive a type from its source, such as `ReturnType<typeof fromSdk>`; never copy an inferred shape by hand.
121
+ - **Schema pairs.** Write `export type X = typeof X.Type` directly before `export const X = Schema.Struct(...)`; reuse fields by spreading `.fields`. Use `Schema.optionalKey` for an absent field; reserve `Schema.optional` for an explicit `undefined`.
122
+ - **Construction.** `X.make` validates synchronously and throws: use it for trusted values. For input that can fail a refinement, use `X.makeEffect` and map its issue to the owning domain error.
123
+ - **Cycles.** Handwrite each recursive-cycle type and annotate only its `Schema.suspend` thunk; other types stay inferred, and a union follows its members.
124
+ - **Error predicates.** A predicate is a static member of its tagged error; check a struct inline with `Schema.is`.
125
+ - **Casts.** Opaque SDK and framework inputs stay `unknown` until decoded at their boundary; never fabricate a shape or erase a known shape into `unknown`.
455
126
 
456
127
  ```ts
457
- // good
458
128
  export type LedgerDraft = typeof LedgerDraft.Type
459
129
  export const LedgerDraft = Schema.Struct({...LedgerEntry.fields, id: Schema.optionalKey(Schema.NonEmptyString)})
460
- RunEventCursor.make(value.toString())
461
- trails: pipe(Schema.Array(PortfolioTrail), Schema.withConstructorDefault(Effect.succeed([])))
462
- // bad
463
- export type EntryDraft = {amount: EntryAmount; id?: EntryId; tag: string} // a hand-written shape beside the schema
464
- // bad
465
- new AiError({message: 'The model returned no text'}) // the schema has make
466
- function cursorFromBigInt(value: bigint) {
467
- return RunEventCursor.make(value.toString())
468
- }
469
- payload: Schema.Struct(pipe(PortfolioVisitor.fields, Struct.pick(['color', 'id', 'name', 'x', 'y']))) // the schema itself fits
470
- ```
471
-
472
- **Construction.** `schema.make` applies defaults and validates synchronously, throwing on failure: use it for trusted construction. For reachable fallible construction, use `schema.makeEffect` and map its `SchemaIssue.Issue` to the owning domain error. Effect decoders report `SchemaError`.
473
-
474
- ```ts
475
- // good — reachable constructor failure stays in the error channel
476
- pipe(
477
- Name.makeEffect(input),
478
- Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
479
- )
480
- // bad — input can fail a refinement even though its TypeScript type is string
481
- Name.make(input)
482
- ```
483
-
484
- **Cycles.** Handwrite each recursive-cycle type; annotate only Schema.suspend thunks. Noncycle types stay inferred; unions follow members.
485
-
486
- ```ts
487
- // good
488
- export type CodeStep = typeof CodeStep.Type
489
- export const CodeStep = Schema.Struct({code: Schema.String, kind: Schema.Literal('code')})
490
130
  export type CallStep = {readonly do: readonly Step[]; readonly kind: 'call'}
491
131
  export const CallStep = Schema.Struct({
492
132
  do: Schema.Array(Schema.suspend((): Schema.Codec<Step> => Step)),
493
133
  kind: Schema.Literal('call')
494
134
  })
495
- export type Step = CallStep | CodeStep
496
- export const Step = Schema.Union([CallStep, CodeStep])
497
- ```
498
-
499
- **Error predicates.** A predicate is a static member of its tagged error; a struct is checked inline with `Schema.is`.
500
-
501
- ```ts
502
- // good
503
- class PathNotFound extends Schema.TaggedError<PathNotFound>()('PathNotFound', {
504
- cause: Schema.optional(Schema.Defect())
505
- }) {
506
- static matches = (error: Cause.UnknownError) => Schema.is(FileNotFound)(error.cause)
507
- }
508
- // bad
509
- function isPathNotFound(error: unknown) {
510
- return Schema.is(FileNotFound)(error)
511
- }
135
+ pipe(
136
+ Name.makeEffect(input),
137
+ Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
138
+ )
512
139
  ```
513
140
 
514
141
  ## Shape
515
142
 
516
- **Inline.** Inline forwarding wrappers, temporaries and constants. Keep a helper only for logic that is reused or deserves its own name. Argument-free functions are named values.
517
-
518
- ```ts
519
- // good
520
- function randomIndex(length: number) {
521
- return Random.Random.defaultValue().nextIntUnsafe() % length
522
- }
523
- const isApiUrl = Predicate.compose(String.isString, Predicate.or(Equal.equals('/api'), String.startsWith('/api/'))) // a predicate, reused
524
- static generateText = generateTextPi
525
- const notFound = HttpServerResponse.empty({status: 404})
526
- // bad
527
- const random = Random.Random.defaultValue()
528
- function isBackendRequest(request: IncomingMessage) {
529
- return isApiUrl(request.url)
530
- } // "this is just a wrapper / factory function"
531
- Ref.set(entries, Array.copy(decoded)) // decoded is already an array
532
- const runPromise = Effect.runPromiseWith(Context.empty()) // "wrapper function, tmp variable ... that can be inlined"
533
- ```
534
-
535
- **Linked state.** Linked state moves in one pure updater, never a setter that calls another.
536
-
537
- ```ts
538
- // good
539
- setState(current => {
540
- const draft = {...current.draft, files}
541
- return {draft, owner: ownerAfter(draft)}
542
- })
543
- // bad
544
- setDraft(current => {
545
- setOwner(ownerAfter(current))
546
- return {...current, files}
547
- })
548
- ```
549
-
550
- **File tree.** Use explicit kebab-case role names and one naming scheme. Prefer one substantive file to near-empty fragments; colocate <name>.test.ts; no barrels.
551
-
552
- **Package surface.** Declare exports for imported subpaths and import aliases for internal paths. Use the shared lint preset with generated ignores only.
553
-
554
- ```ts
555
- // good
556
- "exports": {"./plugin": "./src/generated/plugin.ts"}
557
- export {oxlint as default} from '@deslop/coding-standards'
558
- // bad — consumers import two names from a barrel of everything
559
- export * from './generated/Client.ts'
560
- const warned = ['sort-keys', 'typescript/no-restricted-types'] // a package rule list
561
- "typecheck": "tsc --noEmit" // type-aware lint already checks types
562
- ```
563
-
564
- **Generated code.** A generator emits code that follows these rules as closely as it can; lint ignores its output.
143
+ - **Inline.** Inline forwarding wrappers, temporaries, constants and property aliases; keep an alias only where a callback needs its narrowed type. Inline a small helper with one caller; a helper that several files share lives in the package's `lib/utils`. An argument-free function is a named value.
144
+ - **Linked state.** Linked state moves in one pure updater, never a setter that calls another.
145
+ - **File tree.** Use explicit kebab-case role names and one naming scheme. Prefer one substantive file to near-empty fragments; colocate `<name>.test.ts`; no barrels.
146
+ - **Package surface.** Declare exports for imported subpaths and import aliases for internal paths. Extend the shared lint preset with generated ignores only; add no package rule list or separate typecheck script.
147
+ - **Generated code.** A generator emits code that follows these rules as closely as it can; lint ignores its output.
565
148
 
566
149
  ## React
567
150
 
568
- **Components.** React Compiler memoizes. Destructure state at its declaration, receive component refs through props, and keep only a component's own interaction state in React.
569
-
570
- ```tsx
571
- // good
572
- const editorRef = useRef<Lexical.LexicalEditor>(null)
573
- const [showShortcuts, setShowShortcuts] = useState(false)
574
- const moveRpc = useAtomSet(RpcClient.mutation('portfolio.move'))
575
- // bad
576
- const handle = useCallback(() => setOpen(true), [])
577
- const state = useState(false)
578
- ```
579
-
580
- **Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves that code to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)`, and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them, so never pass them to `Atom.fn`.
151
+ - **Atoms.** Data, shared state and every Effect a component needs live in atoms; a component reads them with hooks and never runs an Effect. A task that changes a React Query hook, a fetching Effect or an Effect runner in React code moves it to an atom. Declare atoms at module level or in `Atom.family`, never during render. Import `Atom` and `AsyncResult` from `effect/reactivity` and the hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)` and build every read and write with that runtime's `atom` and `fn`. Tie each write to the reads it changes with reactivity keys; only a runtime's `fn` honors them.
152
+ - **Outcomes.** Read data with `useAtomSuspense`. The route or section that owns a screen wraps it in one `Suspense` boundary and one error boundary with a retry. Components never branch on loading or failure, and never turn a failure into an empty value. Take a write's pending and failure state from its atom's result. A reversible write updates the screen at once through `Atom.optimistic` and `Atom.optimisticFn`.
153
+ - **Ownership.** A component that owns atoms, writes or a lifetime gets its own file; a small presentational helper stays beside its one user. Reset an editable draft through a `key` on its identity, never a `useEffect` that copies props.
154
+ - **Pure render.** Derive display values during render, and never mutate them or run an RPC, write or subscription there. Event handlers own user actions; `useEffect` synchronizes only external systems.
155
+ - **State identity.** Keep one owner per value, stable domain keys for list items and deliberate reset boundaries. Fix an observed rerender by deriving the value in the existing atom graph; add no manual memoization or compiler escape directives.
581
156
 
582
157
  ```tsx
583
- // good
584
158
  const appsRuntime = Atom.runtime(DualApps.layer)
585
159
  const appAtom = Atom.family((id: AppId) => pipe(appsRuntime.atom(loadApp(id)), Atom.withReactivity(['apps'])))
586
160
  const saveAppAtom = appsRuntime.fn((draft: AppDraft) => saveApp(draft), {reactivityKeys: ['apps']})
587
- const app = useAtomValue(appAtom(appId))
588
- const [saveResult, save] = useAtom(saveAppAtom)
589
- // bad
590
- useQuery({queryFn: () => Effect.runPromiseWith(Context.empty())(loadApp(appId))})
591
- onClick={() => Effect.runPromise(saveApp(draft))}
592
- useAtomValue(Atom.make(loadApp(appId))) // a new atom on every render
593
- ```
594
-
595
- **Outcomes.** Read data with `useAtomSuspense`. The route or section that owns a screen wraps it in one `Suspense` boundary for loading. One error boundary there shows the failure with a retry. Components never branch on loading or failure, and never turn a failure into an empty value. Take a write's pending and failure state from its atom's result, never from copied React state. A reversible write updates the screen at once through `Atom.optimistic` and `Atom.optimisticFn`, which roll back on failure.
596
-
597
- ```tsx
598
- // good
599
161
  function AppFrame(props: {appId: AppId}) {
600
162
  return <Frame app={useAtomSuspense(appAtom(props.appId)).value} />
601
163
  }
602
- <ErrorBoundary fallback={<LoadError onRetry={refresh} />}>
603
- <Suspense fallback={<AppFrameSkeleton />}>
604
- <AppFrame appId={appId} />
605
- </Suspense>
606
- </ErrorBoundary>
607
- <Button disabled={saveResult.waiting} onClick={() => save(draft)}>Save</Button>
608
- // bad
609
- app.data ? <AppFrame app={app.data} /> : <Spinner /> // a failed load spins forever
610
- const members = query.data ?? [] // a failed load looks like no members
611
- const [pending, setPending] = useState(false) // copies the write atom's waiting state
612
164
  ```
613
165
 
614
- - **Ownership.** Give a component that owns its own atoms, writes or lifetime its own file; keep small presentational helpers beside their one user. Reset an editable draft through a `key` on its identity, never a `useEffect` that copies props.
615
- - **Pure render.** Derive display values from props and subscribed state during render, and never mutate them or run an RPC, write or subscription there. Event handlers own user actions; `useEffect` synchronizes only external systems, never copied state or calculations.
616
- - **State identity.** Keep one owner per value, stable domain keys for list items and deliberate reset boundaries. For an observed rerender problem, derive the value in the existing atom graph instead of subscribing to unrelated changing state. Compiler memoization never guarantees correctness or lifetime; add no manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
166
+ ## Lint forms
167
+
168
+ Lint rejects the right column and has no autofix for it. Write the left column from the start.
169
+
170
+ | Write | Never | Rule |
171
+ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
172
+ | An early `return`, `Boolean.match(x, {onFalse, onTrue})`, `Option.match` or a lookup record | `a ? b : c`, in JSX too | `no-ternary` |
173
+ | `options?.root ?? '.'` | `root !== undefined ? root : '.'` | `no-nullish-ternary`, `prefer-nullish-coalescing` |
174
+ | `pipe(Match.value(event), Match.tag('Text', text), Match.exhaustive)` | `switch` | `no-switch-statement` |
175
+ | `selected === true` on a value that can be `undefined` | `if (selected)` on an optional | `strict-boolean-expressions` |
176
+ | `Array.filter` before the loop, an early `return` in a callback | `continue` | `no-continue` |
177
+ | At most 3 nested blocks, 6 nested callbacks and 3 nested `pipe` calls | Deeper nesting | `max-depth`, `max-nested-callbacks`, `no-deep-pipe` |
178
+ | `pipe(value, f, g)` | `value.pipe(f, g)` | `no-restricted-properties` |
179
+ | `const total = Array.reduce(values, 0, Number.sum)` | `let` with a loop | `no-let` |
180
+ | `Array.join(parts, ', ')`, `Array.map(items, f)`, `String.split(text, ',')`, `Record.keys(record)` | `parts.join(', ')`, `items.map(f)`, `text.split(',')`, `Object.keys(record)` | `no-native-method-call`, `no-restricted-globals`; the `Path` service's `path.join` stays |
181
+ | `Predicate.isString(v)`, `Predicate.hasProperty(node, 'tag')`, `Predicate.isNotNullish(v)` | `typeof`, `'tag' in node`, `v !== null && v !== undefined` | `no-typeof`, `no-in-operator`, `no-double-nullish-check` |
182
+ | `Array.isReadonlyArrayEmpty(items)`, `String.isNonEmpty(text)` | `items.length === 0`, `text.length > 0` | `no-native-emptiness-check` |
183
+ | `Array.ensure(value)` | `Array.isArray(value) ? value : [value]` | `no-array-wrap-ternary` |
184
+ | `Option.map(option, f)`, `Option.match`, `Option.getOrElse` | `option.value` after `Option.isSome` | `no-option-value-access` |
185
+ | `undefined`, `Option.getOrUndefined` | `null`, `Option.getOrNull`, except for an outside contract | `unicorn/no-null`, `no-restricted-properties` |
186
+ | `Number.parse(text)`, `Number.max`, `HashMap`, `HashSet`, `DateTime` | `parseInt`, `Math.max`, `Map`, `Set`, `Date` | `no-restricted-globals`, `no-restricted-properties`, `no-restricted-types` |
187
+ | `Clock`, `Config`, `Console`, `Random`, `Effect.sleep`, `HttpClient` | `Date.now()`, `process.env`, `console`, `setTimeout`, `fetch` | `effecttsgo/global-*`, `effecttsgo/process-env` |
188
+ | `function name() {}` for a named function; an arrow only in place | `const name = () => {}` | `func-style` |
189
+ | `server => ({...server, warmup})`, `for (const schema of schemas)`, `pair => pair[0]` | `({host, port}) =>`, `for (const {name} of schemas)` | `no-destructured-parameter`; labeled tuples, React state and refs can destructure |
190
+ | `Array.sort(entries, Order.mapInput(Order.Number, (entry: Entry) => entry.tokens))` | `entries.sort((left, right) => left.tokens - right.tokens)` | `no-native-method-call`; a third-party object's own method stays |
191
+ | The source value where it is used | A one-line wrapper, alias or argument-free constant function | `no-trivial-indirection`, `no-constant-function` |
192
+ | `import {Schema} from 'effect'` | `import {Schema as S} from 'effect'` | `no-renamed-import` |
193
+ | Properties built with `Effect.fn` or `Effect.fnUntraced` | `fetch: url => Effect.tryPromise(...)` as a property arrow | `no-effect-property-arrow` |
194
+ | Inferred return and variable types, `satisfies`, `Array.empty<T>()` | `function f(): T`, `const items: T[] = []` | `no-return-type`, `no-redundant-variable-annotation` |
195
+ | `satisfies T`, `as const` | `as T`, `any`, `value!` | `consistent-type-assertions`, `no-explicit-any`, `no-non-null-assertion` |
196
+ | `Ref.make(0)`, `useState(false)`, `useRef<HTMLDivElement>(null)` | `Ref.make<number>(0)`, `useRef<HTMLDivElement \| null>(null)` | `no-redundant-state-type`, `no-redundant-use-ref-null-type` |
197
+ | `T[]`, an optional property | `Array<T>`, `Promise<T>`, `x: T \| undefined` | `no-restricted-types` |
198
+ | `return yield* NotFound.make({...})` | `yield* Effect.fail(NotFound.make({...}))` | `effecttsgo/unnecessary-fail-yieldable-error` |
199
+ | `Schema.decodeEffect(S)(typed)`, `Schema.fromJsonString(S)` for JSON text | `Schema.decodeUnknownEffect` on typed input, `JSON.parse` in an Effect | `effecttsgo/prefer-typed-schema-decoder`, `prefer-schema-over-json` |
200
+ | The Schema operation inline at its use | `const decode = Schema.decodeEffect(S)` stored for later | `no-stored-schema-operation` |
201
+ | `Schema.Struct`, `Schema.TaggedError`, `Schema.NonEmptyString` | `Schema.Class`, `Data.TaggedError`, `Error`, a rebuilt refinement | `no-restricted-properties`, `no-restricted-globals`, `no-reinvented-schema` |
202
+ | The failure mapped to the service's error | `Effect.ignore`, `Effect.orDie` outside tests | `no-discarded-failure` |
203
+ | `NodeRuntime.runMain(program)`; atoms in React | `Effect.runPromise`, `ManagedRuntime.make` | `no-restricted-properties` |
204
+ | `import {useState} from 'react'`; `const [open, setOpen] = useState(false)` | `React.useState`, `const state = useState(false)` | `no-restricted-properties`, `no-undestructured-use-state` |
205
+ | React Compiler memoization, a `ref` prop | `useCallback`, `useMemo`, `memo`, `forwardRef` | `no-restricted-imports` |
206
+ | The UI package's primitive | A styled native `<button>` or `<input>` | `no-native-control` |
207
+ | `assert` from `@effect/vitest`, the failure's identity | `expect`, `vi`, an assertion on message wording | `no-restricted-imports`, `no-error-message-assertion` |
208
+ | One comment line for an outside constraint | A multiline comment | `no-multiline-comment` |
209
+ | Object keys in ascending order; a blank line starts a new group | Unsorted keys, a `sort-keys` disable | `sort-keys`, `no-unexplained-disable` |
210
+
211
+ These forms satisfy every rule involved, where the obvious fix trades one diagnostic for another:
212
+
213
+ ```ts
214
+ // A handler used twice: a function declaration, not a const arrow.
215
+ function label(entry: Entry) {
216
+ return Boolean.match(entry.done, {onFalse: () => 'Running', onTrue: () => 'Done'})
217
+ }
218
+ // Two nested loops: one loop over the pairs, not a sixth nested callback.
219
+ Effect.forEach(Array.cartesian(cases, harnesses), pair => prepareRun(pair[0], pair[1]), {concurrency: 'unbounded'})
220
+ // Cleanup on failure: its own failure flows, without Effect.orDie or Effect.ignore.
221
+ pipe(
222
+ prepareRuns,
223
+ Effect.onExitIf(Exit.isFailure, () => fs.remove(directory, {recursive: true}))
224
+ )
225
+ // A choice inside a deep pipeline: a named function, not a nested Boolean.match.
226
+ Array.appendAll(base, toolParts(tools, hasTools))
227
+ ```