@deslop/coding-standards 0.1.1082 → 0.1.1083

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.
@@ -1,75 +1,71 @@
1
1
  ---
2
2
  name: engineering
3
- description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product and test code, including prototypes.'
3
+ description: 'Use before writing or changing code, tests, scripts or configuration, before choosing how to build a feature, and before proving or testing behavior.'
4
4
  ---
5
5
 
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
-
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.
12
-
13
- ## Simplicity
14
-
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.
6
+ # Engineering
7
+
8
+ Every file, flag, option, branch, abstraction and line answers one question: what breaks for the user without it? If nothing, remove it.
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. Fewer lines is never the goal: an abstraction stays only when it lowers what a reader must hold, never because it saves lines.
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 owned files. Scratch code follows no style rule.
13
+
14
+ ## Loop
15
+
16
+ - **Normalize.** Run the repository's fix command at the root before you analyze or check; a read-only reviewer never runs it.
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
+ - **Type early.** Check and test the package you change after each small batch of edits; the repository's full gate runs at the root before every push. Read an unfamiliar API's types in the installed version's source first. Fix the first error in each file first: one broken import turns into hundreds of implicit `any` errors downstream.
19
+ - **Disables** need an unavoidable conflict with an outside contract and a `--` reason naming it. Never disable ordering or weaken file configuration.
20
+
21
+ ## Mechanism
22
+
23
+ Start from a real caller and the outcome, not a pattern. Trace one successful operation and its strongest reachable failure through the existing modules: the authoritative state, write boundary, resource lifetime and completion signal.
24
+
25
+ - **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
+ - **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. Values, class lists and small expressions stay inline at each use, never in shared constants; reuse lives at the component, service or function level. Compose with Effect's own combinators, pipelines, Layers and Schema instead of writing a wrapper.
28
+ - **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
+ - **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
+ - **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.
31
+ - **Trusted data.** Decode once at the boundary with Effect Schema; never re-check what the schema, the type or every caller guarantees.
32
+ - **Whole values.** Pass whole values or spread them, named by role; project fields only to withhold one, such as a bearer token.
33
+ - **Inward.** Domain and service code never import transport types; domain schemas carry no HTTP status.
34
+ - **Building blocks.** A public interface offers services, Layers and functions a caller composes, never a bundle such as `serve(app)`.
35
+ - **No compatibility.** Rename a shape and every caller together: no alias, fallback, deprecated path or flag. Persisted production data is the exception: a migration converts the stored data, and the code reads only the new shape.
36
+ - **Refactors** keep observable behavior at every consumer: capture the current output through the real interface before you change structure and compare it after; type checks and lint are not that capture. Report any difference. Fix reachable bugs inside the scope and check every consumer of the changed output.
37
+ - **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
+ - **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
+ - **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. Read its locked source before you wrap it; extend through its public API, never a patch.
41
+ - **Dead code.** In the same change, delete every superseded file, export, type, branch, config, doc, test, dependency and empty directory.
35
42
 
36
43
  ```ts
37
44
  // good
38
- const sandboxGone = Effect.mapError(cause => SandboxError.make({cause, message: 'The sandbox is gone'}))
39
45
  const cycles = Graph.stronglyConnectedComponents(graph)
40
46
  upload({scope: input.scope, type: input.type}) // input also holds the bearer token
41
47
  // bad
42
48
  Schema.decodeEffect(LedgerDraft)(draft) // draft is already a LedgerDraft
49
+ function save(kind: 'note' | 'tag', value: Note | Tag) {} // one helper branching per caller: saveNote and saveTag
43
50
  import {glob} from 'glob' // Effect's FileSystem has glob
44
51
  const digits = BigDecimal.scale(BigDecimal.abs(amount), 2).value.toString().padStart(3, '0') // BigDecimal.format
45
52
  ```
46
53
 
47
54
  ## Patterns
48
55
 
49
- Keep the repository consistent by default, without a reminder, and change a standard everywhere at once.
56
+ The repository stays consistent without a reminder; a standard changes everywhere at once.
50
57
 
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.
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.
59
+ - **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 `CODING_STANDARDS.md`; when the user picks, rejects or corrects an uncovered choice, add a row with the user's words in the same change.
61
+ - **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
+ - **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.
56
63
 
57
64
  ## Effect
58
65
 
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.
66
+ **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.
60
67
 
61
- ```ts
62
- const prompt = Effect.fn('Ai.prompt')(function* (message: Prompt.UserMessage) {
63
- const input = yield* piUserMessage(message)
64
- yield* send(input)
65
- })
66
- const decodePart = Effect.fnUntraced(function* (part: Part) {
67
- return yield* Schema.decodeEffect(Event)(part)
68
- })
69
- const restart = setStatus('starting')
70
- ```
71
-
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.
68
+ **Errors.** One `Schema.TaggedError` per service. Map in `Effect.fn`'s pipeline argument, redact secrets, map SDK errors at the adapter.
73
69
 
74
70
  ```ts
75
71
  // good
@@ -84,15 +80,11 @@ const pages = Effect.tryPromise({
84
80
  try: () => client.pages.list()
85
81
  })
86
82
  // bad
87
- pipe(
88
- fs.readFileString(target),
89
- Effect.catch(() => Effect.succeed(''))
90
- )
91
83
  Effect.mapError(failure => new LedgerError({reason: failure.message})) // cause dropped
92
84
  load: (path: string) => Effect.Effect<void, PlatformError | Schema.SchemaError> // library failures leak
93
85
  ```
94
86
 
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`.
87
+ **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`.
96
88
 
97
89
  ```ts
98
90
  export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger/service/Ledger') {
@@ -107,22 +99,16 @@ export class Ledger extends Context.Service<Ledger, Ledger.Shape>()('@app/ledger
107
99
  export const Verbose = Context.Reference<boolean>('@app/ai/Verbose', {defaultValue: () => false})
108
100
  ```
109
101
 
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.
111
-
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.
113
-
114
- **Results.** Sort with `Order`, and return a fallible pure value as a `Result`.
115
-
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.
102
+ - **Primitives.** RPCs and services already trace. One scope owns each lifetime, with acquisition and release in one `Effect.acquireRelease`.
103
+ - **Results.** Sort with `Order`; a fallible pure value returns a `Result`.
117
104
 
118
105
  ## Types and Schema
119
106
 
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`.
107
+ - **Derive** types from their source, such as `ReturnType<typeof fromSdk>`; use schema types and indexed members in signatures. Annotate a return type only on a guard, assertion, recursive function or `Schema.suspend` thunk.
108
+ - **Schema pairs.** `export type X = typeof X.Type` directly before `export const X = Schema.Struct(...)`; reuse fields by spreading `.fields`. `Schema.optionalKey` for an absent field; `Schema.optional` only for an explicit `undefined`.
109
+ - **Construction.** `X.make` validates and throws: trusted values only. Input that can fail uses `X.makeEffect`, mapped to the domain error.
110
+ - **Error predicates** are static members of their tagged error; check a struct inline with `Schema.is`.
111
+ - **Opaque** SDK input stays `unknown` until decoded at its boundary.
126
112
 
127
113
  ```ts
128
114
  export type LedgerDraft = typeof LedgerDraft.Type
@@ -132,27 +118,21 @@ export const CallStep = Schema.Struct({
132
118
  do: Schema.Array(Schema.suspend((): Schema.Codec<Step> => Step)),
133
119
  kind: Schema.Literal('call')
134
120
  })
135
- pipe(
136
- Name.makeEffect(input),
137
- Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
138
- )
139
121
  ```
140
122
 
141
123
  ## Shape
142
124
 
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.
125
+ - **Linked state** moves in one pure updater, never a setter that calls another.
126
+ - **Files.** Kebab-case role names, one substantive file over near-empty fragments, `<name>.test.ts` beside its source.
127
+ - **Package surface.** Exports for imported subpaths, import aliases for internal paths. Extend the shared lint preset with generated ignores only.
128
+ - **Generated code** follows these rules as closely as it can; lint ignores it.
148
129
 
149
130
  ## React
150
131
 
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.
132
+ - **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 updates at once with `Atom.optimistic`.
134
+ - **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; `useEffect` only syncs external systems. Fix a rerender in the atom graph.
156
136
 
157
137
  ```tsx
158
138
  const appsRuntime = Atom.runtime(DualApps.layer)
@@ -163,52 +143,122 @@ function AppFrame(props: {appId: AppId}) {
163
143
  }
164
144
  ```
165
145
 
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:
146
+ ## Proof
147
+
148
+ - **Probe first.** Answer a question or check an assumption with inline code against the public interface or real data: `vp node -e '<script>'`, `vp env exec bun -e '<script>'`. Never a test file, worktree, seam or `/tmp` script for one question.
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. Never claim what you did not run. A green suite does not replace that run; zero collected tests or exit 0 alone prove nothing.
150
+ - **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
+ - **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
+ - **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.** `assert` from `@effect/vitest`. `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: an entered and release pair at the dependency. Expected failure: `Effect.flip`; both outcomes: `Effect.exit`; assert domain identity and preserved state.
154
+ - **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
+ - **Evidence.** Keep the exact command, result and revision of each claim; rerun only proof whose inputs changed.
156
+
157
+ ```ts
158
+ // good
159
+ it.layer(Layer.provideMerge(Ledger.layer, NodeServices.layer))(test => {
160
+ test.effect('keeps the previous entries when the file is malformed', () => program)
161
+ })
162
+ assert.containsSubset(error, {_tag: 'SandboxUnavailable', cause: killed})
163
+ // bad
164
+ it.effect('rejects an empty tag', () => run(ledger.add({tag: ''}))) // Schema.isNonEmpty already does
165
+ it.effect('forwards the prompt', () => run(agent.prompt(message))) // wiring that only reaches a double
166
+ it('shows the empty state', () => render(<Notes notes={[]} />)) // a UI test: open the screen in the browser
167
+ assert.strictEqual(Array.flatMap(provider.groups, group => group.actions).length, 75) // a pinned count
168
+ ```
169
+
170
+ ## Critique
171
+
172
+ Once it works, reread the diff as a reviewer who knows only the request, and act on each answer before you call it done:
173
+
174
+ 1. **Smaller?** Delete one part at a time (an edge-case branch, a check, an option, a helper, superseded code) and rerun the proof; keep only the parts whose removal breaks it.
175
+ 2. **Existing?** Does the repository, a dependency or Effect already do any part of it?
176
+ 3. **Consistent?** Does each recurring shape match its canonical example, and each name say its role?
177
+ 4. **In place?** Can each changed file be understood without the chat or a jump to another file?
178
+ 5. **Proved?** Which changed behavior or consumer has not run through its real interface?
179
+ 6. **Expert?** What would a top expert in this area change, and what did the request imply that the diff skips?
180
+
181
+ ## Lint rules
182
+
183
+ Lint rejects the right column without an autofix. Write the left column from the start.
184
+
185
+ | Write | Never | Rule |
186
+ | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
187
+ | An early `return`, `Boolean.match(x, {onFalse, onTrue})`, `Option.match` or a lookup record | `a ? b : c`, in JSX too | `no-ternary` |
188
+ | `options?.root ?? '.'` | `root !== undefined ? root : '.'` | `no-nullish-ternary`, `prefer-nullish-coalescing` |
189
+ | `pipe(Match.value(event), Match.tag('Text', text), Match.exhaustive)` | `switch` | `no-switch-statement` |
190
+ | `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` |
191
+ | `Array.filter` before the loop, an early `return` in a callback | `continue` | `no-continue` |
192
+ | At most 3 nested blocks, 6 nested callbacks and 3 nested `pipe` calls | Deeper nesting | `max-depth`, `max-nested-callbacks`, `no-deep-pipe` |
193
+ | `pipe(value, f, g)` | `value.pipe(f, g)` | `no-restricted-properties` |
194
+ | `const total = Array.reduce(values, 0, Number.sum)` | `let` with a loop | `no-let` |
195
+ | `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 |
196
+ | `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` |
197
+ | `Predicate.isNotNullish`, `Predicate.isObject`, `Predicate.isDate` passed directly | A hand-written guard `(v): v is T => v !== null` | `no-hand-written-guard` |
198
+ | `Array.isReadonlyArrayEmpty(items)`, `String.isNonEmpty(text)` | `items.length === 0`, `text.length > 0` | `no-native-emptiness-check` |
199
+ | `Array.ensure(value)` | `Array.isArray(value) ? value : [value]` | `no-array-wrap-ternary` |
200
+ | `Option.map(option, f)`, `Option.match`, `Option.getOrElse` | `option.value` after `Option.isSome` | `no-option-value-access` |
201
+ | `undefined`, `Option.getOrUndefined` | `null`, `Option.getOrNull`, except for an outside contract | `unicorn/no-null`, `no-restricted-properties` |
202
+ | `Number.parse(text)`, `Number.max`, `HashMap`, `HashSet`, `DateTime` | `parseInt`, `Math.max`, `Map`, `Set`, `Date` | `no-restricted-globals`, `no-restricted-properties`, `no-restricted-types` |
203
+ | `Clock`, `Config`, `Console`, `Random`, `Effect.sleep`, `HttpClient` | `Date.now()`, `process.env`, `console`, `setTimeout`, `fetch` | `effecttsgo/global-*`, `effecttsgo/process-env` |
204
+ | `FileSystem`, `Path`, `ChildProcess`, `Crypto`'s `randomUUIDv4` | `node:fs`, `node:path`, `node:child_process`, `crypto.randomUUID()` | `effecttsgo/node-builtin-import`, `effecttsgo/crypto-random-uuid` |
205
+ | `#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` | `import/no-relative-parent-imports`, `import/no-namespace`, `import/no-default-export`, `typescript/no-namespace` |
206
+ | `import {Folder} from 'lucide-react'` from the defining module | `export * from 'lucide-react'`, a file that only re-exports | `no-barrel-module` |
207
+ | Kebab-case file names | `LedgerView.tsx`, `ledger_view.ts` | `unicorn/filename-case` |
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
+ | The current API its deprecation names | A deprecated API | `typescript/no-deprecated` |
217
+ | Values decoded with Schema before use | Member access, calls, assignments, arguments or returns on `any` | `typescript/no-unsafe-*` |
218
+ | `function name() {}` for a named function; an arrow only in place | `const name = () => {}` | `func-style` |
219
+ | `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 |
220
+ | `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 |
221
+ | The source value where it is used | A one-line wrapper, alias or argument-free constant function | `no-trivial-indirection`, `no-constant-function` |
222
+ | `import {Schema} from 'effect'` | `import {Schema as S} from 'effect'` | `no-renamed-import` |
223
+ | Properties built with `Effect.fn` or `Effect.fnUntraced` | `fetch: url => Effect.tryPromise(...)` as a property arrow | `no-effect-property-arrow` |
224
+ | Inferred return and variable types, `satisfies`, `Array.empty<T>()` | `function f(): T`, `const items: T[] = []` | `no-return-type`, `no-redundant-variable-annotation` |
225
+ | `satisfies T`, `as const` | `as T`, `any`, `value!` | `consistent-type-assertions`, `no-explicit-any`, `no-non-null-assertion` |
226
+ | `useRef(initial)` for lifecycle state | `const [box] = useState(() => ({current: initial}))` with no setter | `no-fake-ref-state` |
227
+ | `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` |
228
+ | `T[]`, an optional property | `Array<T>`, `Promise<T>`, `x: T \| undefined` | `no-restricted-types` |
229
+ | `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`, `effecttsgo/unknown-in-effect-catch` |
230
+ | `Effect.mapError(cause => X.make({cause, message}))` | `Effect.mapError(failure => X.make({message}))`, `catch: failure => X.make({message})` | `preserve-error-cause` |
231
+ | 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` |
232
+ | `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` |
233
+ | `Effect.fn('Service.method')` for a function returning an Effect | A plain function returning `Effect.gen` | `effecttsgo/effect-fn-opportunity` |
234
+ | Layers provided once at the entry point; `Layer.provide` between layers | `Effect.provide(layer)` inside services or per call | `effecttsgo/strict-effect-provide` |
235
+ | `Layer.provideMerge` when one layer needs another | `Layer.mergeAll` of layers that depend on each other | `effecttsgo/layer-merge-all-with-dependencies` |
236
+ | One `pipe(value, f, g, h)` | `pipe(pipe(value, f), g)`, `pipe(value)` | `effecttsgo/unnecessary-pipe-chain`, `effecttsgo/unnecessary-pipe` |
237
+ | 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` |
238
+ | `return yield* NotFound.make({...})` | `yield* Effect.fail(NotFound.make({...}))` | `effecttsgo/unnecessary-fail-yieldable-error` |
239
+ | `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` |
240
+ | `Schema.fromJsonString(Protocol)` with the protocol's schema | `Schema.UnknownFromJsonString`, `Schema.fromJsonString(Schema.Unknown)` | `no-unvalidated-json-decode` |
241
+ | `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` |
242
+ | `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` |
243
+ | The Schema operation inline at its use | `const decode = Schema.decodeEffect(S)` stored for later | `no-stored-schema-operation` |
244
+ | `Schema.Struct`, `Schema.TaggedError`, `Schema.NonEmptyString` | `Schema.Class`, `Data.TaggedError`, `Error`, a rebuilt refinement | `no-restricted-properties`, `no-restricted-globals`, `no-reinvented-schema` |
245
+ | The failure mapped to the service's error | `Effect.ignore`, `Effect.orDie` outside tests | `no-discarded-failure` |
246
+ | `NodeRuntime.runMain(program)`; atoms in React | `Effect.runPromise`, `ManagedRuntime.make` | `no-restricted-properties` |
247
+ | `import {useState} from 'react'`; `const [open, setOpen] = useState(false)` | `React.useState`, `const state = useState(false)` | `no-restricted-properties`, `no-undestructured-use-state` |
248
+ | React Compiler memoization, a `ref` prop | `useCallback`, `useMemo`, `memo`, `forwardRef` | `no-restricted-imports` |
249
+ | Code the compiler accepts | `'use no memo'`, an inline disable of a React rule | `no-compiler-opt-out`, `react/rule-suppression` |
250
+ | `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` |
251
+ | `<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` |
252
+ | `<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` |
253
+ | `bg-card`, `border` | `bg-linear-to-b from-muted`, `bg-[radial-gradient(...)]` | `no-gradient` |
254
+ | `duration-200`, `zoom-in-95`, `starting:scale-95`, `motion-safe:animate-spin` | `duration-500`, `zoom-in`, `starting:scale-50`, an ungated `animate-bounce` | `motion-limits`, `react-doctor/no-ungated-tailwind-animation` |
255
+ | 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` |
256
+ | The UI package's primitive | A styled native `<button>` or `<input>` | `no-native-control` |
257
+ | `assert` from `@effect/vitest`, the failure's identity | `expect`, `vi`, an assertion on message wording | `no-restricted-imports`, `no-error-message-assertion` |
258
+ | One comment line for an outside constraint | A multiline comment | `no-multiline-comment` |
259
+ | Object keys in ascending order; a blank line starts a new group | Unsorted keys, a `sort-keys` disable | `sort-keys`, `no-unexplained-disable` |
260
+
261
+ Forms that satisfy every rule where the obvious fix trades one diagnostic for another:
212
262
 
213
263
  ```ts
214
264
  // A handler used twice: a function declaration, not a const arrow.