@deslop/coding-standards 0.1.1083 → 0.1.1086
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 +5 -78
- package/dist/{MutableList-Cpkiw3Cl.mjs → MutableList-CnvD7fcZ.mjs} +49 -14
- package/dist/install.js +18 -30
- package/dist/oxlint.d.ts +30 -25
- package/dist/oxlint.js +582 -362
- package/package.json +3 -3
- package/skills/design/SKILL.md +12 -15
- package/skills/engineering/SKILL.md +115 -110
- package/skills/review/SKILL.md +5 -6
- package/tsconfig.json +0 -2
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: engineering
|
|
3
|
-
description:
|
|
3
|
+
description: "Use before writing or changing code, tests, scripts or configuration, before choosing how to build a feature, before a repository's fix, check, lint or test command, and before proving or testing behavior."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Engineering
|
|
7
7
|
|
|
8
8
|
Every file, flag, option, branch, abstraction and line answers one question: what breaks for the user without it? If nothing, remove it.
|
|
9
9
|
|
|
10
|
-
Agents write and maintain the code; people review it from the PR body, the proof and a scan of the diff. Write for that reader: idiomatic, effectful, testable code that reads in place, explicit and self-contained like Tailwind classes, so editing one part cannot break another.
|
|
10
|
+
Agents write and maintain the code; people review it from the PR body, the proof and a scan of the diff. Write for that reader: idiomatic, effectful, testable code that reads in place, explicit and self-contained like Tailwind classes, so editing one part cannot break another. An abstraction stays only when it lowers what a reader must hold, never because it saves lines.
|
|
11
11
|
|
|
12
|
-
Oxlint and Oxfmt check every change and stay authoritative; this skill holds what they cannot check. Apply it with the repository's `CODING_STANDARDS.md`; a cleanup request applies every rule to every line of the
|
|
12
|
+
Oxlint and Oxfmt check every change and stay authoritative; this skill holds what they cannot check. Apply it with the repository's standards: its `AGENTS.md` and the files it links, such as a `CODING_STANDARDS.md`; a cleanup request applies every rule to every line of the files the task touches. Scratch code follows no style rule.
|
|
13
13
|
|
|
14
14
|
## Loop
|
|
15
15
|
|
|
16
|
-
- **
|
|
17
|
-
- **First pass.** Write the forms in [Lint rules](#lint-rules) from the start. Fix a diagnostic at its source with the form its message names; never a cast, a weakened rule or a rewrite that trades one diagnostic for another.
|
|
18
|
-
- **
|
|
16
|
+
- **Verify** at the root once the whole change is in, and again after each round of fixes: the repository's fix command (lint fixes, then format), then its check, then its tests, chained in one command so a failure stops the rest; fix what fails and rerun the chain until it passes. Never a package-level or per-edit check: one root run costs fewer steps than many partial ones, and the cache replays unchanged work. Fix the first error in each file first: one broken import turns into hundreds of implicit `any` errors downstream. A read-only reviewer never runs fix.
|
|
17
|
+
- **First pass.** Write the forms in [Lint rules](#lint-rules) from the start. Fix a diagnostic at its source with the form its message or row names; never a cast, a weakened rule or a rewrite that trades one diagnostic for another.
|
|
18
|
+
- **Fix the code, not the rule.** A diagnostic whose named form does not fit points at the model upstream: change the model, not the line that trips the rule. A diagnostic on code the rules allow is a rule bug: fix the rule with a rejected and an accepted case.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// good: absence is an optional value, cleared by "All"
|
|
22
|
+
const [kind, setKind] = useState<Kind>()
|
|
23
|
+
// bad: a sentinel that only exists because `undefined` is banned as a type
|
|
24
|
+
const [kind, setKind] = useState<Kind | ''>('')
|
|
25
|
+
```
|
|
26
|
+
|
|
19
27
|
- **Disables** need an unavoidable conflict with an outside contract and a `--` reason naming it. Never disable ordering or weaken file configuration.
|
|
20
28
|
|
|
21
29
|
## Mechanism
|
|
@@ -24,7 +32,7 @@ Start from a real caller and the outcome, not a pattern. Trace one successful op
|
|
|
24
32
|
|
|
25
33
|
- **Direct.** The most direct code that meets today's request: extend the canonical example per [Patterns](#patterns), else a function or an existing service method. A new service only for a distinct capability or lifetime; reimplementing a library's job is a new mechanism. Keep the direct version unless a probe proves a concrete contract rules it out.
|
|
26
34
|
- **Callers.** Every function, parameter, option, export, Layer, check and config value has a caller that varies it. Handle reachable inputs only: no futureproofing or impossible cases. Explicit variants for genuinely different behaviors; no boolean combinations, registries, generic configuration or factories for hypothetical callers.
|
|
27
|
-
- **Reuse.** Search existing code, installed modules and Effect's modules by the domain concept, not only the request's words, before you write traversal, state, configuration or platform code; replace dependencies Effect covers. Identical logic twice (a computation, a query, an Effect pipeline) is one function in its owner's `lib/utils`. Similar code stays separate: three similar lines beat an abstraction, and a shared function never branches on its caller.
|
|
35
|
+
- **Reuse.** Search existing code, installed modules and Effect's modules by the domain concept, not only the request's words, before you write traversal, state, configuration or platform code; replace dependencies Effect covers. Before a hand-written type, guard, table, wrapper or dependency, list the owning module's exports in its locked source checkout per explore: `Schema.Literals([...]).pick([...])` replaces a second literal list, `Schema.TaggedUnion` a hand-written matcher. Identical logic twice (a computation, a query, an Effect pipeline) is one function in its owner's `lib/utils`. Similar code stays separate: three similar lines beat an abstraction, and a shared function never branches on its caller. Class lists and small expressions stay inline at each use, never in shared constants. Compose with Effect's own combinators, pipelines, Layers and Schema instead of writing a wrapper.
|
|
28
36
|
- **One owner.** Derive values from their owner; never mirror server state, request state, caches or pending results in another store. A persisted value has one write boundary, a domain enum one schema, a cache key the full identity it caches. No duplicate, subset or superset fields, methods, options or exports; remove states no caller produces.
|
|
29
37
|
- **Lifetime.** Finite work stays in its request scope; a stream serves progress without a job system. Background ownership only when work must outlive its caller.
|
|
30
38
|
- **Failure.** Failures flow through the error channel with their cause; never turn one into empty success. No catch, retry, fallback, idempotency or defensive check without a contract that needs it.
|
|
@@ -37,7 +45,7 @@ Start from a real caller and the outcome, not a pattern. Trace one successful op
|
|
|
37
45
|
- **Clarity.** Pure functions over immutable data, effects only inside Effect. Code reads top to bottom; rename or split instead of narrating; a comment only for an outside constraint the code cannot express.
|
|
38
46
|
- **Performance** only when the request needs it: locate the cost with a profiler or trace, measure one real action before and after with run count and variation, and treat a gap within the variation as no change.
|
|
39
47
|
- **Domain.** Implement the domain's definition, such as a cycle for recursion, never a syntactic proxy.
|
|
40
|
-
- **Dependencies.** A new one is `latest`; pin only what a consumer requires.
|
|
48
|
+
- **Dependencies.** A new one is `latest`; pin only what a consumer requires. Extend it through its public API, never a patch.
|
|
41
49
|
- **Dead code.** In the same change, delete every superseded file, export, type, branch, config, doc, test, dependency and empty directory.
|
|
42
50
|
|
|
43
51
|
```ts
|
|
@@ -45,19 +53,17 @@ Start from a real caller and the outcome, not a pattern. Trace one successful op
|
|
|
45
53
|
const cycles = Graph.stronglyConnectedComponents(graph)
|
|
46
54
|
upload({scope: input.scope, type: input.type}) // input also holds the bearer token
|
|
47
55
|
// bad
|
|
48
|
-
Schema.decodeEffect(LedgerDraft)(draft) // draft is already a LedgerDraft
|
|
56
|
+
Schema.decodeEffect(LedgerDraft)(draft) // draft is already a decoded LedgerDraft
|
|
49
57
|
function save(kind: 'note' | 'tag', value: Note | Tag) {} // one helper branching per caller: saveNote and saveTag
|
|
50
58
|
import {glob} from 'glob' // Effect's FileSystem has glob
|
|
51
|
-
const
|
|
59
|
+
const unique = Array.fromIterable(HashSet.fromIterable(items)) // Array.dedupe(items)
|
|
52
60
|
```
|
|
53
61
|
|
|
54
62
|
## Patterns
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- **Canonical example.** Before you write a recurring shape (page layout, list, form, empty or loading state, dialog, service, route, error mapping, file layout, name), find its canonical example: the Patterns row in `CODING_STANDARDS.md`, else the variant most consumers use, the newest on a tie. Copy its structure, components, names, copy and states.
|
|
64
|
+
- **Canonical example.** Before you write a recurring shape (page layout, list, form, empty or loading state, dialog, service, route, error mapping, file layout, name), find its canonical example: the Patterns row in the repository's standards, else the variant most consumers use, the newest on a tie. Copy its structure, components, names, copy and states.
|
|
59
65
|
- **Register.** A shape two consumers share without a row gets one: pattern, owner and example, a search listing its consumers, and what enforces it.
|
|
60
|
-
- **Taste.** The user's taste decides layout, file organization, boundaries, interfaces and names. Follow the Taste rows of
|
|
66
|
+
- **Taste.** The user's taste decides layout, file organization, boundaries, interfaces and names. Follow the Taste rows of the repository's standards; when the user picks, rejects or corrects an uncovered choice, add a row with the user's words in the same change.
|
|
61
67
|
- **Pattern change.** The user's change to a pattern, style or name changes the standard: update its owner (component, template, generator, lint rule, row), migrate every consumer its search lists, delete the old variant and search again. Another repository or a published contract needs the user's approval first.
|
|
62
68
|
- **Enforce.** The same correction twice becomes impossible at the highest level that works: an owner component or type, then a lint rule naming the replacement.
|
|
63
69
|
|
|
@@ -65,7 +71,7 @@ The repository stays consistent without a reminder; a standard changes everywher
|
|
|
65
71
|
|
|
66
72
|
**Functions.** `Effect.fn('Service.method')` for a traced function with arguments, `Effect.fnUntraced` for an internal helper, `Effect.gen` for statements without arguments, a single effect directly. `Effect.fn.Return` only for a generator that calls itself.
|
|
67
73
|
|
|
68
|
-
**Errors.** One `Schema.TaggedError` per service. Map in `Effect.fn`'s pipeline argument, redact secrets, map SDK errors at the adapter.
|
|
74
|
+
**Errors.** One `Schema.TaggedError` per service. Map in `Effect.fn`'s pipeline argument, redact secrets, map SDK errors at the adapter so no library failure leaks into a service signature.
|
|
69
75
|
|
|
70
76
|
```ts
|
|
71
77
|
// good
|
|
@@ -75,13 +81,8 @@ const prompt = Effect.fn('Ai.prompt')(
|
|
|
75
81
|
},
|
|
76
82
|
Effect.mapError(cause => AiError.make({cause, message: 'Cannot prompt the agent'}))
|
|
77
83
|
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try: () => client.pages.list()
|
|
81
|
-
})
|
|
82
|
-
// bad
|
|
83
|
-
Effect.mapError(failure => new LedgerError({reason: failure.message})) // cause dropped
|
|
84
|
-
load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError> // library failures leak
|
|
84
|
+
// bad: library failures leak
|
|
85
|
+
load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError>
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
**Services.** Inline a sole implementation in its Layer; name layers only for several backends. `Context.Reference` for a default; build the shape with `Service.of` or `satisfies`.
|
|
@@ -99,13 +100,14 @@ export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger
|
|
|
99
100
|
export const Verbose = Context.Reference<boolean>('@app/ai/Verbose', {defaultValue: () => false})
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
- **
|
|
103
|
-
- **
|
|
103
|
+
- **Tracing.** RPC handlers and `Effect.fn` already trace: add no span.
|
|
104
|
+
- **Lifetimes.** One scope owns each lifetime, with acquisition and release in one `Effect.acquireRelease`.
|
|
105
|
+
- **Results.** A fallible pure value returns a `Result`.
|
|
104
106
|
|
|
105
107
|
## Types and Schema
|
|
106
108
|
|
|
107
|
-
- **Derive** types from their source,
|
|
108
|
-
- **
|
|
109
|
+
- **Derive** types from their source: the library's exported type first, else `ReturnType<typeof fromSdk>` in the signature that needs it, never a private alias; use schema types and indexed members in signatures.
|
|
110
|
+
- **Optionality.** `Schema.optionalKey` for an absent field; `Schema.optional` only for an explicit `undefined`. Reuse fields by spreading `.fields`.
|
|
109
111
|
- **Construction.** `X.make` validates and throws: trusted values only. Input that can fail uses `X.makeEffect`, mapped to the domain error.
|
|
110
112
|
- **Error predicates** are static members of their tagged error; check a struct inline with `Schema.is`.
|
|
111
113
|
- **Opaque** SDK input stays `unknown` until decoded at its boundary.
|
|
@@ -113,6 +115,7 @@ export const Verbose = Context.Reference<boolean>('@app/ai/Verbose', {defaultVal
|
|
|
113
115
|
```ts
|
|
114
116
|
export type LedgerDraft = typeof LedgerDraft.Type
|
|
115
117
|
export const LedgerDraft = Schema.Struct({...LedgerEntry.fields, id: Schema.optionalKey(Schema.NonEmptyString)})
|
|
118
|
+
// recursive: Schema.suspend needs the type written by hand, readonly as Schema.Array decodes it
|
|
116
119
|
export type CallStep = {readonly do: readonly Step[]; readonly kind: 'call'}
|
|
117
120
|
export const CallStep = Schema.Struct({
|
|
118
121
|
do: Schema.Array(Schema.suspend((): Schema.Codec<Step> => Step)),
|
|
@@ -123,16 +126,16 @@ export const CallStep = Schema.Struct({
|
|
|
123
126
|
## Shape
|
|
124
127
|
|
|
125
128
|
- **Linked state** moves in one pure updater, never a setter that calls another.
|
|
126
|
-
- **Files.**
|
|
127
|
-
- **Package surface.**
|
|
129
|
+
- **Files.** One substantive file over near-empty fragments, named by role (`home`, `utils`), never by kind or app; `<name>.test.ts` beside its source.
|
|
130
|
+
- **Package surface.** Extend the shared lint preset with generated ignores only.
|
|
128
131
|
- **Generated code** follows these rules as closely as it can; lint ignores it.
|
|
129
132
|
|
|
130
133
|
## React
|
|
131
134
|
|
|
132
135
|
- **Atoms.** Data, shared state and every Effect a component needs live in atoms; components read them with hooks and never run an Effect. Import `Atom` and `AsyncResult` from `effect/reactivity`, hooks from `@effect/atom-react`. Supply services through `Atom.runtime(layer)` and build reads and writes with its `atom` and `fn`; tie writes to reads with reactivity keys. Never wrap a write that already has an owner, such as an existing mutation, in a second state holder.
|
|
133
|
-
- **Outcomes.** Read with `useAtomSuspense`; the screen's owner wraps it in one `Suspense` and one error boundary with retry. Components never branch on loading or failure. A write's pending and failure state come from its atom; a reversible write
|
|
136
|
+
- **Outcomes.** Read with `useAtomSuspense`; the screen's owner wraps it in one `Suspense` and one error boundary with retry. Components never branch on loading or failure. A write's pending and failure state come from its atom; a reversible write uses `Atom.optimistic`.
|
|
134
137
|
- **Ownership.** A component owning atoms, writes or a lifetime gets its own file.
|
|
135
|
-
- **Pure render.** Derive display values in render; event handlers own actions
|
|
138
|
+
- **Pure render.** Derive display values in render; event handlers own actions. Fix a rerender in the atom graph.
|
|
136
139
|
|
|
137
140
|
```tsx
|
|
138
141
|
const appsRuntime = Atom.runtime(DualApps.layer)
|
|
@@ -145,12 +148,12 @@ function AppFrame(props: {appId: AppId}) {
|
|
|
145
148
|
|
|
146
149
|
## Proof
|
|
147
150
|
|
|
148
|
-
- **Probe first.** Answer a question or check an assumption with inline code against the public interface or real data
|
|
149
|
-
- **Done** means each changed behavior and each of its consumers ran through its real interface on the final code: a screen in the browser, an API through a request, a CLI by running it; for a stack, on every layer's head.
|
|
151
|
+
- **Probe first.** Answer a question or check an assumption with inline code against the public interface or real data, run by the repository's script runner, such as `vp node -e '<script>'` or `bun -e '<script>'`. Never a test file, seam, worktree or clone for one question.
|
|
152
|
+
- **Done** means each changed behavior and each of its consumers ran through its real interface on the final code: a screen in the browser, an API through a request, a CLI by running it; for a stack, on every layer's head. A green suite does not replace that run; zero collected tests or exit 0 alone prove nothing.
|
|
150
153
|
- **Bugs.** Reproduce before you fix, then shrink the reproduction until every remaining input, setting and step is needed. One hypothesis per probe. After two failed fixes, write down the premise they share and test it before a third. Rerun the original scenario after the fix.
|
|
151
154
|
- **Tests** only for complex logic you own, such as parsing, state transitions, concurrency, scheduling, money or data conversion: a black box through its public interface, expected values from the contract, never the implementation. One representative input per behavior; reachable distinctions only (empty, duplicate, concurrent, dependency failure).
|
|
152
155
|
- **No tests** for UI (the browser proves a screen); external libraries, APIs or SDKs; what types or schemas guarantee; inputs the architecture makes impossible; simple code (a filter, a mapping, wiring, a constant); an assumption (a probe answers it); a fixed bug (no regression tests); anything that changes when the implementation changes. Delete touched tests that break these rules.
|
|
153
|
-
- **Effect tests.** `
|
|
156
|
+
- **Effect tests.** `it.effect` runs virtual services, `it.live` real ones, `it.layer` caches a block's context. `TestClock` and `TestConsole` come from `effect/testing`: fork, await a `Deferred` at the dependency boundary, adjust time, join. Concurrency: block the dependency on a `Deferred`, assert the entered state, then release it. Expected failure: `Effect.flip`; both outcomes: `Effect.exit`; assert domain identity and preserved state.
|
|
154
157
|
- **Boundaries.** Replace dependencies through Layers or public parameters, never module mocks. A claim about an SDK, process, filesystem, HTTP, browser or package needs the real boundary. `FileSystem.layerNoop` is not storage; `RpcTest` bypasses serialization. A public tool is proved packed, not from source.
|
|
155
158
|
- **Evidence.** Keep the exact command, result and revision of each claim; rerun only proof whose inputs changed.
|
|
156
159
|
|
|
@@ -180,83 +183,85 @@ Once it works, reread the diff as a reviewer who knows only the request, and act
|
|
|
180
183
|
|
|
181
184
|
## Lint rules
|
|
182
185
|
|
|
183
|
-
Lint rejects the
|
|
184
|
-
|
|
185
|
-
| Write
|
|
186
|
-
|
|
|
187
|
-
| An early `return`, `Boolean.match(x, {onFalse, onTrue})`, `Option.match`
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
|
|
|
191
|
-
| `
|
|
192
|
-
|
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `Array.
|
|
196
|
-
| `Predicate.isString(v)`, `Predicate.hasProperty(node, 'tag')`, `Predicate.isNotNullish
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
| A
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
| `
|
|
215
|
-
|
|
|
216
|
-
| The
|
|
217
|
-
|
|
|
218
|
-
| `
|
|
219
|
-
|
|
|
220
|
-
|
|
|
221
|
-
| The
|
|
222
|
-
| `
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
| `satisfies
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
|
|
|
232
|
-
| `Effect.
|
|
233
|
-
| `Effect.
|
|
234
|
-
|
|
|
235
|
-
| `
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
| `
|
|
239
|
-
|
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
|
|
|
244
|
-
| `
|
|
245
|
-
| The
|
|
246
|
-
| `
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
186
|
+
Lint rejects the Never column; write the Write column from the start. The fix command rewrites the rows whose rule has an autofix.
|
|
187
|
+
|
|
188
|
+
| Write | Never | Rule |
|
|
189
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
+
| An early `return`, `Boolean.match(x, {onFalse, onTrue})`, `Option.match`, a lookup record; `options?.root ?? '.'` for a default; `Array.ensure(value)` | `a ? b : c`, in JSX too | `no-ternary` |
|
|
191
|
+
| `selected === true` on a value that can be `undefined`; `ref.current === null`, `editor === undefined` | `if (selected)` on an optional, `if (!ref.current)`, `if (editor)` | `strict-boolean-expressions` |
|
|
192
|
+
| `Array.filter` before the loop, an early `return` in a callback | `continue` | `no-continue` |
|
|
193
|
+
| At most 3 nested blocks, 6 nested callbacks and 3 nested `pipe` calls; a named function for a deeper branch | Deeper nesting | `max-depth`, `max-nested-callbacks`, `no-deep-pipe` |
|
|
194
|
+
| `pipe(value, f, g)` | `value.pipe(f, g)` | `no-restricted-properties` |
|
|
195
|
+
| `const total = Array.reduce(values, 0, Number.sum)` | `let` with a loop | `no-let` |
|
|
196
|
+
| `return consume(String.trim(text))` | `const trimmed = String.trim(text); return consume(trimmed)` | `no-single-use-value` |
|
|
197
|
+
| `Array.join(parts, ', ')`, `Array.findFirst(items, f)`, `String.split(text, ',')`, `Record.keys(record)` | `parts.join(', ')`, `items.find(f)`, `text.split(',')`, `Object.keys(record)` | `effecttsgo/prefer-effect-array`, `effecttsgo/prefer-effect-record`, `no-native-method-call`; the `Path` service's `path.join` and a third-party object's own method stay |
|
|
198
|
+
| `Array.sort(entries, Order.mapInput(Order.Number, (entry: Entry) => entry.tokens))` | `entries.sort((left, right) => left.tokens - right.tokens)` | `no-native-method-call` |
|
|
199
|
+
| `Predicate.isString(v)`, `Predicate.hasProperty(node, 'tag')`, `Predicate.isNotNullish` passed directly | `typeof`, `'tag' in node`, `v !== null && v !== undefined`, a hand-written guard `(v): v is T => v !== null` | `no-typeof`, `no-in-operator`, `no-double-nullish-check`, `no-hand-written-guard` |
|
|
200
|
+
| `Array.isReadonlyArrayEmpty(items)`, `String.isNonEmpty(text)` | `items.length === 0`, `text.length > 0` | `no-native-emptiness-check` |
|
|
201
|
+
| `Option.map(option, f)`, `Option.match`, `Option.getOrElse` | `option.value` after `Option.isSome` | `no-option-value-access` |
|
|
202
|
+
| `undefined`, `Option.getOrUndefined`; `null` only in `.tsx`, such as `useRef<HTMLDivElement>(null)`, and for an outside contract | `null`, `Option.getOrNull` | `unicorn/no-null`, `no-restricted-properties` |
|
|
203
|
+
| `Number.parse(text)`, `Number.max`, `HashMap`, `HashSet`, `DateTime` | `parseInt`, `Math.max`, `Map`, `Set`, `Date` | `no-restricted-globals`, `no-restricted-properties`, `no-restricted-types` |
|
|
204
|
+
| `Clock`, `Config`, `Console`, `Random`, `Effect.sleep`, `HttpClient` | `Date.now()`, `process.env`, `console`, `setTimeout`, `fetch` | `effecttsgo/global-*`, `effecttsgo/process-env` |
|
|
205
|
+
| `FileSystem`, `Path`, `ChildProcess`, `Crypto`'s `randomUUIDv4` | `node:fs`, `node:path`, `node:child_process`, `crypto.randomUUID()` | `effecttsgo/node-builtin-import`, `effecttsgo/crypto-random-uuid` |
|
|
206
|
+
| `#lib/utils` through the package's `imports` map, named imports, named exports | `../lib/utils`, `import * as utils from '#lib/utils'`, `export default` outside config files, a `namespace` outside a `declare` | `import/no-relative-parent-imports`, `import/no-namespace`, `import/no-default-export`, `typescript/no-namespace` |
|
|
207
|
+
| `import {Folder} from 'lucide-react'` from the defining module | `export * from 'lucide-react'`, a file that only re-exports | `no-barrel-module` |
|
|
208
|
+
| A name an outer scope does not use | An inner `entry` shadowing an outer `entry` | `no-shadow` |
|
|
209
|
+
| A new value | Assigning to a parameter or its properties | `no-param-reassign` |
|
|
210
|
+
| `/pattern/u` | A regular expression without the `u` or `v` flag | `require-unicode-regexp` |
|
|
211
|
+
| Strings, numbers and booleans in a template literal | An object, `Option` or `unknown` value interpolated | `typescript/restrict-template-expressions`, `typescript/no-base-to-string` |
|
|
212
|
+
| A check only where the type allows both outcomes | `?.`, `??` or a condition on a value whose type rules one outcome out | `typescript/no-unnecessary-condition` |
|
|
213
|
+
| A type parameter that links two positions | A type parameter used once, `string \| 'id'`, `void` in a union or parameter | `typescript/no-unnecessary-type-parameters`, `typescript/no-redundant-type-constituents`, `typescript/no-invalid-void-type` |
|
|
214
|
+
| `items => service.load(items)` | `service.load` passed unbound | `typescript/unbound-method` |
|
|
215
|
+
| An Effect (`Effect.fn`, `Effect.tryPromise`); in `.tsx` only, an `async` handler that awaits | An `async` function in `.ts`, `async` without `await`, `void promise`, an unhandled promise | `effecttsgo/async-function`, `typescript/require-await`, `no-void`, `typescript/no-floating-promises` |
|
|
216
|
+
| Values decoded with Schema before use | Member access, calls, assignments, arguments or returns on `any` | `typescript/no-unsafe-*` |
|
|
217
|
+
| `function name() {}` for a named function; an arrow only in place | `const name = () => {}` | `func-style` |
|
|
218
|
+
| `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 |
|
|
219
|
+
| The source value where it is used | A one-line wrapper, alias or argument-free constant function | `no-trivial-indirection`, `no-constant-function` |
|
|
220
|
+
| `props.note.title` in place; an alias only for a value a check narrows and a callback reads | `const note = props.note`, `export const server = extra.server` | `no-property-alias` |
|
|
221
|
+
| `const MAX_WHISPER_SECONDS = 2 * 60 * 60` directly under the imports; a one-line literal read once in place | `const maxSize = 10` below a function, `const pattern = /x/u` read once | `module-constant` |
|
|
222
|
+
| Definitions above their first use | A `const` read above its declaration | `no-use-before-define` |
|
|
223
|
+
| The element where it renders; a component such as `function DeleteButton(props)` when several places render it | `const remove = <Button label="Delete" />` | `no-stored-jsx` |
|
|
224
|
+
| The library's own type, such as `import type {AnyNode} from 'domhandler'`, else `ReturnType<typeof make>` in the signature that needs it | `type Options = Parameters<typeof make>[0]` kept as a module alias | `no-floating-derived-type` |
|
|
225
|
+
| `export type Kind = typeof Kind.Type` above `export const Kind = Schema.Literals([...])`; `Kind.pick([...])` for a subset | `(typeof kinds)[number]['kind']`, `keyof typeof table` | `no-type-from-value` |
|
|
226
|
+
| `import {Schema} from 'effect'` | `import {Schema as S} from 'effect'` | `no-renamed-import` |
|
|
227
|
+
| Properties built with `Effect.fn` or `Effect.fnUntraced` | `fetch: url => Effect.tryPromise(...)` as a property arrow | `no-effect-property-arrow` |
|
|
228
|
+
| Inferred return and variable types, `satisfies`, `Array.empty<T>()` | `function f(): T`, `const items: T[] = []` | `no-return-type`, `no-redundant-variable-annotation` |
|
|
229
|
+
| `{values: string[]}`; `readonly` only where the fix adds it, on service shapes | `readonly string[]`, `Readonly<T>` | `no-readonly-type-syntax` |
|
|
230
|
+
| `satisfies T`, `as const` | `as T`, `any`, `value!` | `typescript/consistent-type-assertions`, `typescript/no-explicit-any`, `typescript/no-non-null-assertion` |
|
|
231
|
+
| `useRef(initial)` for lifecycle state | `const [box] = useState(() => ({current: initial}))` with no setter | `no-fake-ref-state` |
|
|
232
|
+
| `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` |
|
|
233
|
+
| `T[]`, an optional property | `Array<T>`, `Promise<T>`, `x: T \| undefined` | `typescript/no-restricted-types` |
|
|
234
|
+
| `Effect.tryPromise({catch: cause => X.make({cause, message}), try})`, each failure typed by its tagged error | `unknown` or `any` in the error or requirements channel, a `catch` returning the raw cause | `effecttsgo/any-unknown-in-error-context` |
|
|
235
|
+
| `Effect.mapError(cause => X.make({cause, message}))` | `Effect.mapError(failure => X.make({message}))`, `catch: failure => X.make({message})` | `preserve-error-cause` |
|
|
236
|
+
| The failure in the error channel; `Effect.option` for a value that is optional by design | `Effect.orElseSucceed(() => [])`, `Effect.catch(() => Effect.void)`, `Effect.succeed('')` in a catch | `no-empty-recovery` |
|
|
237
|
+
| `Effect.forEach(items, load, {concurrency: 'unbounded'})`; `{concurrency: 1}` when order or a resource matters | `Effect.forEach(items, load)`, `Effect.all([a, b])`, `Effect.all(effects, {discard: true})` | `explicit-effect-concurrency` |
|
|
238
|
+
| `Effect.fn('Service.method')` for a function returning an Effect | A plain function returning `Effect.gen` | `effecttsgo/effect-fn-opportunity` |
|
|
239
|
+
| Layers provided once at the entry point; `Layer.provide` between layers | `Effect.provide(layer)` inside services or per call | `effecttsgo/strict-effect-provide` |
|
|
240
|
+
| `Layer.provideMerge` when one layer needs another | `Layer.mergeAll` of layers that depend on each other | `effecttsgo/layer-merge-all-with-dependencies` |
|
|
241
|
+
| One `pipe(value, f, g, h)` | `pipe(pipe(value, f), g)`, `pipe(value)` | `effecttsgo/unnecessary-pipe-chain`, `effecttsgo/unnecessary-pipe` |
|
|
242
|
+
| Service keys `@<package>/<path>/<Name>`; an error's tag is its class name | A service key that does not match the package, path and class name | `effecttsgo/deterministic-keys` |
|
|
243
|
+
| `return yield* NotFound.make({...})` | `yield* Effect.fail(NotFound.make({...}))` | `effecttsgo/unnecessary-fail-yieldable-error` |
|
|
244
|
+
| `Schema.decodeEffect(S)(encoded)` on an `S.Encoded` value, `Schema.fromJsonString(S)` for JSON text | `Schema.decodeUnknownEffect(S)` on typed input, `JSON.parse` in an Effect | `effecttsgo/prefer-typed-schema-decoder`, `effecttsgo/prefer-schema-over-json` |
|
|
245
|
+
| `Schema.fromJsonString(Protocol)` with the protocol's schema | `Schema.UnknownFromJsonString`, `Schema.fromJsonString(Schema.Unknown)` | `no-unvalidated-json-decode` |
|
|
246
|
+
| `export type X = typeof X.Type` directly before `export const X`; an inferred schema | A type elsewhere, `Schema.Schema<X>` restated, an unannotated `suspend` thunk | `schema-type-pair` |
|
|
247
|
+
| `X.make(...)`, `Schema.is(X)(value)`, `Schema.decodeEffect` inside an Effect, `Schema.Finite` for numbers | `new X(...)` on a schema class, `instanceof X`, `Schema.decodeSync` in a generator, `Schema.Number` | `effecttsgo/new-schema-class`, `effecttsgo/instance-of-schema`, `effecttsgo/schema-sync-in-effect`, `effecttsgo/schema-number` |
|
|
248
|
+
| The Schema operation inline at its use | `const decode = Schema.decodeEffect(S)` stored for later | `no-stored-schema-operation` |
|
|
249
|
+
| `Schema.Struct`, `Schema.TaggedError`, `Schema.NonEmptyString`, `Schema.Literals([...])` | `Schema.Class`, `Data.TaggedError`, `Error`, a rebuilt refinement, `Schema.Union` of literals | `no-restricted-properties`, `no-restricted-globals`, `no-reinvented-schema`, `effecttsgo/schema-union-of-literals` |
|
|
250
|
+
| The failure mapped to the service's error; best-effort work as `Effect.ignore({log: 'Warn', message})` | A bare `Effect.ignore` or `Effect.ignoreCause`, `Effect.orDie` outside tests | `no-discarded-failure` |
|
|
251
|
+
| `NodeRuntime.runMain(program)`; atoms in React | `Effect.runPromise`, `ManagedRuntime.make` | `no-restricted-properties` |
|
|
252
|
+
| `import {useState} from 'react'`; `const [open, setOpen] = useState(false)` | `React.useState`, `const state = useState(false)` | `no-restricted-properties`, `no-undestructured-use-state` |
|
|
253
|
+
| React Compiler memoization, a `ref` prop | `useCallback`, `useMemo`, `memo`, `forwardRef` | `no-restricted-imports` |
|
|
254
|
+
| Code the compiler accepts | `'use no memo'`, an inline disable of a React rule | `no-compiler-opt-out`, `react/rule-suppression` |
|
|
255
|
+
| `const countAtom = Atom.make(0)` at module level, `Atom.family(id => ...)` per prop | `Atom.make` inside a component or hook, `useState(() => Atom.make(0))` | `no-component-atom-allocation` |
|
|
256
|
+
| `<Editor key={draft.id} />` to reset a draft; a value derived in render | A `useEffect` that copies a prop into state | `react-doctor/no-adjust-state-on-prop-change`, `react-doctor/no-derived-state` |
|
|
257
|
+
| `<Label htmlFor="name">`, `aria-label` on a field without a visible label, a `<Button>` for a click | A placeholder as the only label, `<div onClick>` | `jsx-a11y/*`, `react-doctor/no-placeholder-only-field` |
|
|
258
|
+
| `bg-card`, `border` | `bg-linear-to-b from-muted`, `bg-[radial-gradient(...)]` | `no-gradient` |
|
|
259
|
+
| `duration-200`, `zoom-in-95`, `starting:scale-95`, `motion-safe:animate-spin` | `duration-300`, `zoom-in`, `starting:scale-50`, an ungated `animate-bounce` | `motion-limits`, `react-doctor/no-ungated-tailwind-animation` |
|
|
260
|
+
| Tailwind's scale and the theme's tokens: `w-64`, `text-sm`, `bg-primary`; `className` on a primitive for layout only | `w-[312px]`, `text-blue-500`, `style={{...}}`, an unknown class, a primitive restyled | `shadcn/no-arbitrary-values`, `shadcn/no-raw-colors`, `shadcn/no-inline-styles`, `shadcn/no-unknown-classes`, `shadcn/no-restyle` |
|
|
261
|
+
| The UI package's primitive | A styled native `<button>` or `<input>` | `no-native-control` |
|
|
262
|
+
| `assert` from `@effect/vitest` on the failure's identity; a screen proved in the browser | `expect`, `vi`, an assertion on message wording, `@testing-library/*`, `jsdom` | `no-restricted-imports`, `no-error-message-assertion` |
|
|
263
|
+
| One comment line for an outside constraint | A multiline comment | `no-multiline-comment` |
|
|
264
|
+
| Object keys in ascending order; a blank line starts a new group | Unsorted keys, a `sort-keys` disable | `sort-keys`, `no-unexplained-disable` |
|
|
260
265
|
|
|
261
266
|
Forms that satisfy every rule where the obvious fix trades one diagnostic for another:
|
|
262
267
|
|
package/skills/review/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: 'Use for any review, audit, critique or review round of code, a PR,
|
|
|
5
5
|
|
|
6
6
|
# Review
|
|
7
7
|
|
|
8
|
-
Do what a short seller does to a position: build the strongest case that the work is wrong from raw evidence (the code, real runs, data), never from the author's summary or rationale.
|
|
8
|
+
Do what a short seller does to a position: build the strongest case that the work is wrong from raw evidence (the code, real runs, data), never from the author's summary or rationale. Load every skill the work needed (engineering for code, design for visible output, and the other skills the checkout has for the task) with the repository's `AGENTS.md` files and the standards they link; last, answer each loaded skill's Critique against the change, as its author should have: a question the change fails is a finding.
|
|
9
9
|
|
|
10
10
|
## Findings
|
|
11
11
|
|
|
@@ -17,20 +17,19 @@ Each finding: severity, `path:line`, the reaching input, actual and required out
|
|
|
17
17
|
| P2 | Wrong owner, boundary or organization, rule violation, duplicated or dead code, or a mechanism, option or element the request does not need |
|
|
18
18
|
| P3 | Naming, wording or style |
|
|
19
19
|
|
|
20
|
-
- **
|
|
20
|
+
- **One pass.** Report everything the first round can find: every class once, with every instance's `path:line`, across every layer, so no later round discovers what this one could have. A later round checks each earlier fix and reads only the commits since; it never repeats a finding the author answered.
|
|
21
21
|
- **Rank honestly.** Never inflate a P3 to keep it alive; never pad a report with P3 to look thorough. Return P1 first; "Clean" with the covered scope when nothing reaches P2.
|
|
22
|
-
- **Not findings:** taste without a rule, hypothetical inputs no caller sends, defects the branch neither made nor relies on, what lint reports
|
|
22
|
+
- **Not findings:** taste without a rule, hypothetical inputs no caller sends, defects the branch neither made nor relies on, what lint already reports.
|
|
23
23
|
- **Probe** a suspicion inline per engineering's Proof; never start a service or test suite in a read-only review.
|
|
24
24
|
|
|
25
25
|
## Code
|
|
26
26
|
|
|
27
27
|
- **Architecture first.** Before the lines, rebuild the change from the request alone: who owns each piece of state, the boundaries, the file and module organization, the data flow and the mechanism. The first pass usually gets this wrong, so attack it: wherever the change's shape differs from the simplest idiomatic one (fewer parts, the existing pattern, Effect's own building blocks), report it with that shape as the fix, even when every line is correct.
|
|
28
|
-
- **Depth.** Read every hand-written changed line; for generated output, the generator change and a sample. Trace each contract through its real consumers, failure paths, concurrency and resource ownership against the merge base.
|
|
28
|
+
- **Depth.** Read every hand-written changed line; for generated output, the generator change and a sample. Trace each contract through its real consumers, failure paths, concurrency and resource ownership against the merge base.
|
|
29
29
|
- **Simplicity.** Name each part the request does not need: a script, flag, option, wrapper, fallback, reimplemented library job, UI control or test, and each abstraction (helper, shared constant, generic, factory) that saves lines without lowering what a reader must hold. Name each block that duplicates code already in the repository, with both locations.
|
|
30
30
|
- **Requirements.** Map each of the user's requirements to the hunk and the proof that meet it; a requirement with neither is a finding, even when no line changed for it.
|
|
31
|
-
- **Production data.** A changed persisted shape without a migration that converts existing data is P1.
|
|
32
31
|
- **Beyond the diff.** A contradiction with an unchanged file, a stale reference the change leaves, or a false assumption it rests on.
|
|
33
|
-
- **
|
|
32
|
+
- **Head.** Read the pushed head, never the author's working tree, which keeps changing: `gh pr diff <n>`, `glab mr diff <n>`, `git show <head>:<path>`. Judge a stack as one change and report what crosses layers: duplication, a contract one layer breaks for another, code in the wrong layer.
|
|
34
33
|
- **Browser.** Inspect the existing preview, captures, console and requests without changing page state; name the smallest missing check for the author.
|
|
35
34
|
|
|
36
35
|
## Cleanup audit
|
package/tsconfig.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"strict": true,
|
|
4
|
-
"noUnusedLocals": true,
|
|
5
4
|
"noImplicitReturns": true,
|
|
6
5
|
"noErrorTruncation": true,
|
|
7
6
|
"allowUnusedLabels": false,
|
|
8
|
-
"noUnusedParameters": true,
|
|
9
7
|
"noImplicitOverride": true,
|
|
10
8
|
"allowUnreachableCode": false,
|
|
11
9
|
"noUncheckedIndexedAccess": true,
|