@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.
- package/README.md +1 -1
- package/dist/{MutableRef-KfyWrSvW.mjs → MutableList-BCF17jPH.mjs} +536 -65
- package/dist/install.js +1611 -1752
- package/dist/oxlint.d.ts +12 -18
- package/dist/oxlint.js +631 -226
- package/package.json +1 -1
- package/skills/architecture/EVALS.md +1 -1
- package/skills/architecture/SKILL.md +6 -8
- package/skills/design/EVALS.md +2 -2
- package/skills/design/SKILL.md +5 -9
- package/skills/engineering/SKILL.md +126 -515
- package/skills/review/SKILL.md +3 -3
- package/skills/testing/SKILL.md +3 -3
|
@@ -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
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
13
|
-
|
|
14
|
-
**
|
|
15
|
-
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
**
|
|
19
|
-
|
|
20
|
-
**
|
|
21
|
-
|
|
22
|
-
**
|
|
23
|
-
|
|
24
|
-
**
|
|
25
|
-
|
|
26
|
-
**
|
|
27
|
-
|
|
28
|
-
**
|
|
29
|
-
|
|
30
|
-
**
|
|
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
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
**
|
|
114
|
-
|
|
115
|
-
**
|
|
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('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
**
|
|
114
|
+
**Results.** Sort with `Order`, and return a fallible pure value as a `Result`.
|
|
409
115
|
|
|
410
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
**
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
+
```
|