@patdown/packs 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +28 -0
- package/anti-slop/README.md +31 -0
- package/anti-slop/decode-at-the-boundary-instead-of-runtime-typeof.md +19 -0
- package/anti-slop/do-not-chain-type-assertions.md +16 -0
- package/anti-slop/do-not-copy-growing-reducer-accumulators.md +16 -0
- package/anti-slop/do-not-discard-known-type-evidence.md +16 -0
- package/anti-slop/do-not-hide-omission-behind-empty-object-spreads.md +18 -0
- package/anti-slop/do-not-pass-unknown-through-function-contracts.md +18 -0
- package/anti-slop/do-not-put-shape-in-symbol-names.md +17 -0
- package/anti-slop/do-not-widen-then-assert.md +21 -0
- package/anti-slop/prefer-named-owner-types-over-escape-hatch-dictionaries.md +18 -0
- package/anti-slop/prefer-one-pass-over-filter-then-map.md +16 -0
- package/anti-slop/prefer-typed-access-over-reflect-get-and-apply.md +16 -0
- package/anti-slop/replace-dependencies-through-real-seams.md +20 -0
- package/anti-slop/require-a-safety-comment-for-type-assertions.md +15 -0
- package/effect/README.md +37 -0
- package/effect/address-effect-diagnostics-instead-of-dodging-them.md +16 -0
- package/effect/keep-service-method-requirements-empty.md +17 -0
- package/effect/prefer-effect-aware-tests-and-layers.md +21 -0
- package/effect/prefer-effect-cache-over-hand-rolled-maps.md +16 -0
- package/effect/prefer-effect-fn-for-named-effectful-work.md +24 -0
- package/effect/prefer-effect-for-uncertain-io-and-boundaries.md +18 -0
- package/effect/prefer-effect-httpclient-for-outgoing-http.md +16 -0
- package/effect/prefer-match-over-manual-tag-branching.md +16 -0
- package/effect/prefer-schedule-and-testclock-over-date-and-sleep.md +16 -0
- package/effect/prefer-schema-and-tagged-errors-at-effect-boundaries.md +16 -0
- package/effect/prefer-schema-struct-and-branded-domain-values.md +20 -0
- package/effect/prefer-stream-for-multi-value-effectful-sources.md +21 -0
- package/effect/use-context-service-and-layer-instead-of-hidden-singletons.md +16 -0
- package/effect/yield-tagged-errors-and-recover-with-catchtag.md +20 -0
- package/package.json +54 -0
- package/typescript/README.md +16 -0
- package/typescript/do-not-launder-types-with-casts.md +16 -0
- package/typescript/keep-branded-and-precise-types-through-boundaries.md +16 -0
- package/typescript/prefer-discriminated-unions-over-flag-bags.md +15 -0
- package/typescript/prefer-named-object-parameters-for-multi-arg-functions.md +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Zero-Clause BSD
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for
|
|
5
|
+
any purpose with or without fee is hereby granted.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
8
|
+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
|
9
|
+
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
|
10
|
+
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
|
11
|
+
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
12
|
+
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
13
|
+
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @patdown/packs
|
|
2
|
+
|
|
3
|
+
Optional fuzzy rule packs for [patdown](https://github.com/tyler-dot-earth/patdown). Content only: the CLI still just runs rules.
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add -D @patdown/packs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then point `--rules` at a pack directory or one rule file:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx patdown --rules ./node_modules/@patdown/packs/typescript
|
|
15
|
+
npx patdown --rules ./node_modules/@patdown/packs/typescript/do-not-launder-types-with-casts.md
|
|
16
|
+
npx patdown --rules ./node_modules/@patdown/packs/effect --files-from changed.txt --verbose
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Subpath imports resolve the same files:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import typescriptPack from '@patdown/packs/typescript'
|
|
23
|
+
import launder from '@patdown/packs/typescript/do-not-launder-types-with-casts.md'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`@patdown/rules` is the parser library. These are markdown files, not that package.
|
|
27
|
+
|
|
28
|
+
Source of truth in the repo is [`packs/`](../../packs/README.md). This package copies that tree at pack time.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# anti-slop pack
|
|
2
|
+
|
|
3
|
+
Fuzzy markdown counterpart to **[Dillon Mulroy](https://github.com/dmmulroy)'s [anti-slop](https://github.com/dmmulroy/anti-slop)** generic Oxlint rules. Credit to Mulroy for the ruleset and the “reject low-evidence TypeScript” framing; this pack rephrases those generic rules for a judge instead of an AST linter.
|
|
4
|
+
|
|
5
|
+
Use it when you want a **judge-backed** check for the same type-laundering patterns static anti-slop rejects. Keep Oxlint anti-slop for exact AST hits; use this pack for paraphrases, multi-step laundering, and “looks fine to the linter” refactors.
|
|
6
|
+
|
|
7
|
+
Effect-specific anti-slop stays in [`../effect`](../effect/) (or Oxlint's `anti-slop-effect` group), not here. Spacing autofix (`require-readable-spacing`) stays a formatter concern and is omitted.
|
|
8
|
+
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
| Rule | Static cousin |
|
|
12
|
+
|---|---|
|
|
13
|
+
| [Do not chain type assertions](do-not-chain-type-assertions.md) | `no-chained-type-assertions` |
|
|
14
|
+
| [Do not widen then assert](do-not-widen-then-assert.md) | `no-widen-then-assert` |
|
|
15
|
+
| [Do not discard known type evidence](do-not-discard-known-type-evidence.md) | `no-known-value-widening` |
|
|
16
|
+
| [Require a SAFETY comment for type assertions](require-a-safety-comment-for-type-assertions.md) | `require-safety-comment-for-type-assertion` |
|
|
17
|
+
| [Do not pass unknown through function contracts](do-not-pass-unknown-through-function-contracts.md) | `no-unknown-parameters`, `no-unknown-returns`, `no-unknown-type-aliases` |
|
|
18
|
+
| [Prefer named owner types over escape-hatch dictionaries](prefer-named-owner-types-over-escape-hatch-dictionaries.md) | `no-unsafe-dictionary-type`, `no-object-parameters` |
|
|
19
|
+
| [Do not hide omission behind empty-object spreads](do-not-hide-omission-behind-empty-object-spreads.md) | `no-conditional-empty-object-spread` |
|
|
20
|
+
| [Prefer one pass over filter-then-map](prefer-one-pass-over-filter-then-map.md) | `no-array-filter-map` |
|
|
21
|
+
| [Do not copy growing reducer accumulators](do-not-copy-growing-reducer-accumulators.md) | `no-reduce-accumulator-copy` |
|
|
22
|
+
| [Decode at the boundary instead of runtime typeof](decode-at-the-boundary-instead-of-runtime-typeof.md) | `no-runtime-typeof` |
|
|
23
|
+
| [Prefer typed access over Reflect get and apply](prefer-typed-access-over-reflect-get-and-apply.md) | `no-reflect-get`, `no-reflect-apply` |
|
|
24
|
+
| [Replace dependencies through real seams](replace-dependencies-through-real-seams.md) | `no-module-mocking` |
|
|
25
|
+
| [Do not put shape in symbol names](do-not-put-shape-in-symbol-names.md) | `no-shape-in-symbol-names` |
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx patdown --rules ./node_modules/@patdown/packs/anti-slop --files-from changed.txt --verbose
|
|
31
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Decode at the boundary instead of runtime typeof
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use ad hoc `typeof` narrowing to invent domain meaning for external values. Decode into a meaningful type at the I/O boundary (Schema, zod, etc.), then trust that type downstream.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
function port(raw: unknown) {
|
|
11
|
+
if (typeof raw === 'number') return raw
|
|
12
|
+
if (typeof raw === 'string') return Number(raw)
|
|
13
|
+
throw new Error('bad port')
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Exceptions
|
|
18
|
+
|
|
19
|
+
Existence probes against the string `'undefined'` (feature detection) are fine. User-defined type predicates that are themselves the boundary decode may inspect `typeof` when necessary.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Do not chain type assertions
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not nest or chain `as` / angle-bracket assertions to invent a type the value does not have. One unjustified assertion is already weak; a chain (`as unknown as T`, `as object as T`) is almost always laundering.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const user = input as unknown as User
|
|
11
|
+
const value = <Config>(<unknown>raw)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
A chain made only of `as const` is fine. A single assertion next to a non-empty `SAFETY:` comment that states a real invariant is fine when the surrounding code establishes that invariant.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Do not copy growing reducer accumulators
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Inside `reduce` / `reduceRight`, do not copy the accumulator on every iteration with `Object.assign({}, acc, …)`, `Array.from(acc)`, `concat`, `slice`, or similar. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline / `flatMap`. Pair with static `oxc/no-accumulating-spread` for spread copies.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
items.reduce((acc, item) => Object.assign({}, acc, { [item.id]: item }), {})
|
|
11
|
+
items.reduce((acc, item) => acc.concat([item]), [])
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Mutating a fresh local accumulator (`acc.push(item); return acc`) is fine. Copying individual input items (not the accumulator) is fine. Bounded one-shot copies outside a growing loop are fine when ownership is clear.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Do not discard known type evidence
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not force a value that already has useful type evidence into an explicit broad or anonymous target (`unknown`, `object`, `{}`, open `Record<string, …>`, or a hand-written anonymous object type) when that annotation's only job is to throw away keys, brands, or precision. Prefer inference, `satisfies`, or a named owner type.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const handlers: Record<string, Handler> = { start: startHandler }
|
|
11
|
+
const payload: object = { id: userId }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Empty dictionary accumulators you intentionally grow, finite-key `Record` targets that match a real wire contract, and `satisfies Record<string, Handler>` (which keeps the known keys) are fine.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Do not hide omission behind empty-object spreads
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use conditional empty-object spreads to pretend a property was always part of the object when the honest model is “property present or absent”. Prefer building the object in branches or using optional fields explicitly. Omission is not the same as assigning `undefined`.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
...(name === undefined ? {} : { name }),
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Exceptions
|
|
17
|
+
|
|
18
|
+
Merging option bags from a library that documents empty-object spreads as the API is fine. Spreading a real partial object (`...options`) is fine.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Do not pass unknown through function contracts
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use `unknown` (or a Promise of `unknown`, or an alias that only hides `unknown`) as a standing parameter or return contract. Decode unknown input at the I/O boundary into a named type, then pass that type through the rest of the program.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
function handle(body: unknown): Promise<unknown> {
|
|
11
|
+
return save(body as User)
|
|
12
|
+
}
|
|
13
|
+
type Payload = unknown
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Exceptions
|
|
17
|
+
|
|
18
|
+
An explicit `cause: unknown` catch parameter is fine. The exact subject of a user-defined type predicate may be `unknown`. Dynamic import / host JSON boundaries may decode from `unknown` once, then return a named type.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Do not put shape in symbol names
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use the substring `shape` (any casing) in names you own—variables, functions, types, private fields, JSX identifiers. Prefer domain words (`fields`, `schema`, `layout`, `form`). Static member access on third-party APIs you cannot rename (for example Zod's `.shape`) is fine.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
type UserShape = { id: string }
|
|
11
|
+
function getShape(input: Form) {}
|
|
12
|
+
const formShape = input
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Exceptions
|
|
16
|
+
|
|
17
|
+
Reading `schema.shape` (or similar) from a library API you do not control is fine. Comments and strings are out of scope for this fuzzy rule unless they rename a symbol.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Do not widen then assert
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not annotate or assign a known value to a broad type (`unknown`, `any`, `object`, `{}`, or a wide record) and later assert it back to a narrower type. Keep the precise type from initialization through use, or parse once at the boundary into the narrow type.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const value: unknown = getConfig()
|
|
11
|
+
const port = (value as Config).port
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
const row = { id: userId } as Record<string, unknown>
|
|
16
|
+
const id = row.id as UserId
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Exceptions
|
|
20
|
+
|
|
21
|
+
A single decode step from `unknown` through Schema (or another parser) into a named type is fine. Interop that truly requires `unknown` at a library boundary is fine when you decode immediately afterward.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Prefer named owner types over escape-hatch dictionaries
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use `Record<string, unknown>`, `Record<string, any>`, `{ [key: string]: object }`, bare `object`, or similar escape-hatch dictionaries as a standing contract for application data or function inputs. Prefer a named schema/type for the closed set of fields you actually read, and parse at the boundary.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
function handle(body: Record<string, unknown>) {
|
|
11
|
+
return body['email'] as string
|
|
12
|
+
}
|
|
13
|
+
function take(value: object) {}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Exceptions
|
|
17
|
+
|
|
18
|
+
Open-ended user metadata that is only stored and returned opaque is fine when callers never dig fields out via cast. Generic constraints such as `T extends Record<string, unknown>` are fine. JSON AST types in parsers are fine.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer one pass over filter-then-map
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not chain adjacent eager `.filter(…).map(…)` or `.map(…).filter(…)` passes over arrays when a single transformation (or a lazy iterator pipeline) expresses the same work. Prefer `.values().filter(…).map(…).toArray()` where supported, or one `flatMap` / local reducer that preserves callback order and filtering semantics.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const emails = users.filter((u) => u.active).map((u) => u.email)
|
|
11
|
+
const found = users.map(lookup).filter((v) => v !== undefined)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Lazy iterator pipelines are fine. Separated filter and map with meaningful work between them are fine. Cases where intermediate arrays are intentional and documented are fine.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer typed access over Reflect get and apply
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use global `Reflect.get` or `Reflect.apply` to reach properties or call functions when typed property access or a normal call would do. Dynamic input should be parsed into a domain type (or modeled behind an interface), not probed with Reflect.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const value = Reflect.get(object, key)
|
|
11
|
+
Reflect.apply(fn, thisArg, args)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Polyfill / proxy implementation code that must speak Reflect for correctness is fine. Test harnesses that intentionally exercise Reflect behavior are fine.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Replace dependencies through real seams
|
|
2
|
+
globs: `**/*.test.ts`
|
|
3
|
+
globs: `**/*.test.tsx`
|
|
4
|
+
globs: `**/*.spec.ts`
|
|
5
|
+
globs: `**/*.spec.tsx`
|
|
6
|
+
globs: `**/tests/**/*.ts`
|
|
7
|
+
globs: `**/tests/**/*.tsx`
|
|
8
|
+
|
|
9
|
+
Do not use Vitest/Jest `mock`, `doMock`, or `unstable_mockModule` to replace modules. Tests should swap dependencies through real interfaces (Effect Layers, constructor injection, test doubles you own).
|
|
10
|
+
|
|
11
|
+
## Not allowed
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
vi.mock('./db')
|
|
15
|
+
jest.unstable_mockModule('./db', () => ({ query: vi.fn() }))
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Exceptions
|
|
19
|
+
|
|
20
|
+
Temporary characterization tests against an unseamed third-party module may mock when introducing a seam in the same change set—and should delete the mock once the seam exists.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Require a SAFETY comment for type assertions
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Every non-`const` type assertion needs a nearby comment that states the invariant being trusted (default marker `SAFETY:`). An assertion without that justification is a violation—especially when the cast is the only thing making the types line up.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const user = body as User
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Exceptions
|
|
14
|
+
|
|
15
|
+
`as const` is fine. A nearby `SAFETY: …` (or project-configured marker) that names a real invariant is fine. Test fixtures may cast when the fake's incomplete object is intentional and commented.
|
package/effect/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# effect pack
|
|
2
|
+
|
|
3
|
+
Effect **v4** production defaults for services, schemas, errors, time, HTTP, and tests.
|
|
4
|
+
|
|
5
|
+
Sources (Effect **v4** only; do not copy Effect v2/v3 blog patterns):
|
|
6
|
+
|
|
7
|
+
- [effect.solutions](https://www.effect.solutions/) — field manual (Basics, Services & Layers, Error Handling, …)
|
|
8
|
+
- [Kit Langton's Effect skill](https://github.com/kitlangton/skills/tree/main/skills/effect) — branch references (schema, services, config, schedule, cache, stream, HTTP, tests)
|
|
9
|
+
- [OpenCode Effect skill (`beta`)](https://github.com/anomalyco/opencode/blob/beta/.opencode/skills/effect/SKILL.md) — short v4 house style from an actively developed Effect-heavy agent codebase ([repo](https://github.com/anomalyco/opencode/tree/beta); their docs also treat `v2` as the default integration branch)
|
|
10
|
+
- Optional Oxlint [anti-slop-effect](https://github.com/dmmulroy/anti-slop) group for `_tag` / Match / service-constructor policy
|
|
11
|
+
|
|
12
|
+
**Modeling default here:** `Schema.Struct` + same-name `interface`, and `Schema.TaggedErrorClass` for errors. Prefer that over `Schema.Class` / `TaggedClass` as the everyday record style (effect.solutions demos Class in places; treat that as optional when you need instance methods, not the pack default).
|
|
13
|
+
|
|
14
|
+
## Rules
|
|
15
|
+
|
|
16
|
+
| Rule | Theme |
|
|
17
|
+
|---|---|
|
|
18
|
+
| [Prefer Effect for uncertain IO and boundaries](prefer-effect-for-uncertain-io-and-boundaries.md) | When to use Effect |
|
|
19
|
+
| [Use Context.Service and Layer instead of hidden singletons](use-context-service-and-layer-instead-of-hidden-singletons.md) | DI / config |
|
|
20
|
+
| [Prefer Effect.fn for named effectful work](prefer-effect-fn-for-named-effectful-work.md) | Basics / tracing |
|
|
21
|
+
| [Keep service method requirements empty](keep-service-method-requirements-empty.md) | Services & Layers |
|
|
22
|
+
| [Prefer Schema.Struct and branded domain values](prefer-schema-struct-and-branded-domain-values.md) | Data modeling |
|
|
23
|
+
| [Prefer Schema and tagged errors at Effect boundaries](prefer-schema-and-tagged-errors-at-effect-boundaries.md) | Decode + errors |
|
|
24
|
+
| [Yield tagged errors and recover with catchTag](yield-tagged-errors-and-recover-with-catchtag.md) | Error handling |
|
|
25
|
+
| [Prefer Match over manual tag branching](prefer-match-over-manual-tag-branching.md) | Variants / anti-slop-effect |
|
|
26
|
+
| [Prefer Schedule and TestClock over Date and sleep](prefer-schedule-and-testclock-over-date-and-sleep.md) | Time / retries |
|
|
27
|
+
| [Prefer Effect HttpClient for outgoing HTTP](prefer-effect-httpclient-for-outgoing-http.md) | HTTP |
|
|
28
|
+
| [Prefer effect Cache over hand-rolled maps](prefer-effect-cache-over-hand-rolled-maps.md) | Caching |
|
|
29
|
+
| [Prefer Stream for multi-value effectful sources](prefer-stream-for-multi-value-effectful-sources.md) | Streams |
|
|
30
|
+
| [Prefer Effect-aware tests and layers](prefer-effect-aware-tests-and-layers.md) | Testing |
|
|
31
|
+
| [Address Effect diagnostics instead of dodging them](address-effect-diagnostics-instead-of-dodging-them.md) | Language service |
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx patdown --rules ./node_modules/@patdown/packs/effect --files-from changed.txt --verbose
|
|
37
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Address Effect diagnostics instead of dodging them
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Use Effect compiler and language-service suggestions to improve the implementation. Do not silence diagnostics or add indirection that only exists to evade a check.
|
|
6
|
+
|
|
7
|
+
Clear violations include:
|
|
8
|
+
|
|
9
|
+
- A zero-argument service method that only returns an Effect when an Effect-valued member would do. Effects are already lazy.
|
|
10
|
+
- Synchronous schema decoding inside an Effect workflow when an effectful decoder would preserve the typed error channel.
|
|
11
|
+
- Using an unknown-input schema decoder when the input already has the schema's encoded type.
|
|
12
|
+
- Suppressing an Effect diagnostic without a specific explanation of why it does not apply.
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Unknown-input decoding at untyped boundaries (dynamic imports, host JSON) is fine. A diagnostic suppression with a comment that names the invariant is fine when that invariant is real.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Keep service method requirements empty
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
`Context.Service` method signatures should not require services in `R`. Acquire dependencies in `Layer.effect` / layer construction and close them over inside methods so callers see `Effect<A, E>` (or `R = never`), not a leak of `HttpClient`, `FileSystem`, etc. Unique service ids (`@app/Users`) and `readonly` method fields are part of the same contract.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
class Users extends Context.Service<Users, {
|
|
11
|
+
readonly find: (id: UserId) => Effect.Effect<User, E, HttpClient.HttpClient>
|
|
12
|
+
}>()('@app/Users') {}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Exceptions
|
|
16
|
+
|
|
17
|
+
Framework request-scoped values that are intentionally leakable (documented with `@effect-leakable-service` / `@effect-expect-leaking`) are fine. Layers themselves may require services; only the **provided service methods** should not.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Prefer Effect-aware tests and layers
|
|
2
|
+
globs: `**/*.test.ts`
|
|
3
|
+
globs: `**/*.test.tsx`
|
|
4
|
+
globs: `**/tests/**/*.ts`
|
|
5
|
+
globs: `**/tests/**/*.tsx`
|
|
6
|
+
|
|
7
|
+
Test Effect programs with `@effect/vitest` (`it.effect`), explicit test Layers, and `TestClock` / Deferred / Queue synchronization. Do not `Effect.runSync` / `runPromise` inside ordinary `async` tests just to avoid Effect test tooling, and do not sleep to wait for fibers.
|
|
8
|
+
|
|
9
|
+
## Not allowed
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
it('loads', async () => {
|
|
13
|
+
const user = await Effect.runPromise(loadUser(id))
|
|
14
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
15
|
+
expect(user.id).toBe(id)
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Exceptions
|
|
20
|
+
|
|
21
|
+
Pure non-Effect unit tests stay on plain Vitest. Contract tests against a real HTTP server may use async/await at the outer edge while the app under test remains Effectful.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer effect Cache over hand-rolled maps
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
For keyed lookup caches with TTL and in-flight dedupe, prefer `Cache.make` / `Cache.makeWith` (or `Effect.cached` / `cachedWithTTL` for a single effect) over a module-level `Map` plus manual prune timers and promise dedupe. Use `Request` / `RequestResolver` only when a real batch endpoint exists.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const cache = new Map<string, { value: User; expires: number }>()
|
|
11
|
+
const inflight = new Map<string, Promise<User>>()
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Tiny process-local memo tables with clear ownership and no TTL/dedupe requirements are fine. Caches owned by an external library are out of scope.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Prefer Effect.fn for named effectful work
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
In Effect v4 modules, define public and non-trivial service methods with `Effect.fn("Domain.operation")` (generator or effect-returning) so call sites get tracing spans. Use `Effect.fnUntraced` only for internal helpers where span metadata is intentionally unnecessary. Prefer `Effect.gen` for sequencing over nested `flatMap` chains.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const get = (id: UserId) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
return yield* repo.find(id)
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
function load() {
|
|
18
|
+
return fetchUser.pipe(Effect.flatMap(processUser))
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Exceptions
|
|
23
|
+
|
|
24
|
+
Trivial one-liners that only `yield*` another named effect may stay unnamed. Test-only helpers may be untraced. Non-Effect pure functions are out of scope.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Prefer Effect for uncertain IO and boundaries
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
In modules that already use Effect, wrap external IO and untrusted input with Effect APIs instead of bare `async`/`await`, raw `fetch`, or unchecked JSON. Prefer `Effect.tryPromise`, `HttpClient`, `Schema.decodeUnknownEffect`, and tagged errors over try/catch that returns `any` or swallows failures.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
async function load() {
|
|
11
|
+
const res = await fetch(url)
|
|
12
|
+
return (await res.json()) as User
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Exceptions
|
|
17
|
+
|
|
18
|
+
Trivial pure helpers, type-only files, and one-line glue that does not perform IO are fine. Test setup that intentionally uses platform APIs outside an Effect is fine when the code under test is still Effectful.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer Effect HttpClient for outgoing HTTP
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
In Effect applications, prefer Effect `HttpClient` (and schema body decoders) over raw `fetch` / SDK defaults when you need typed errors, retries (`HttpClient.retryTransient`), and layer-provided clients. Keep business logic free of transport details.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const res = yield* Effect.tryPromise(() => fetch(url))
|
|
11
|
+
const json = (yield* Effect.tryPromise(() => res.json())) as User
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Tiny scripts, non-Effect modules, and vendor SDKs wrapped once behind a service method are fine. Streaming uploads/downloads may use Stream/platform APIs instead of `res.json()`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer Match over manual tag branching
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
For tagged unions and Effect tagged errors, prefer `Match` / `Predicate.isTagged` / tagged-enum `$match` over manual `if (value._tag === 'X')` chains or `switch (value._tag)`. Construct tagged values with their Schema / `Data.taggedEnum` constructors instead of writing `{ _tag: 'X', ... }` literals.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
if (result._tag === 'Success') return result.value
|
|
11
|
+
const err = { _tag: 'NotFound', id } as NotFound
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
`Match.when` / pattern objects that mention `_tag` as part of Match itself are fine. Test fixtures may build tagged literals when no constructor exists yet.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer Schedule and TestClock over Date and sleep
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
In Effect programs and tests, use `Clock` / `TestClock` and `Schedule` (`retry`, `repeat`, spaced/exponential policies) instead of `Date.now`, `new Date()` for control flow, or `Effect.sleep` / real timers to wait for concurrency. Timeouts and retries belong in `.pipe(Effect.timeout, Effect.retry(schedule))`.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const started = Date.now()
|
|
11
|
+
yield* Effect.sleep('1 second') // in a test waiting for a fiber
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Formatting a timestamp for display/logs may use clock time you already read. One-shot demos/scripts outside the Effect runtime are out of scope.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prefer Schema and tagged errors at Effect boundaries
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Decode unknown input with Schema effectful decoders (`Schema.decodeUnknownEffect`, schema body decoders). Model expected failures as tagged errors (`Schema.TaggedErrorClass` / `Data.TaggedError`). Do not use `as` to skip schema validation. Prefer Struct+interface records (see the Struct/brands rule); do not reach for Schema.Class just to hold fields.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const body = JSON.parse(text) as CreateUser
|
|
11
|
+
yield* Effect.fail(new NotFound({ id }))
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Trusted internal construction may use `schema.make`. Unknown-input decoding at dynamic import or other untyped host boundaries is appropriate. Tests may construct tagged errors directly.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Prefer Schema.Struct and branded domain values
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
For ordinary Effect v4 records, prefer `Schema.Struct(...)` plus a same-name `interface` over `Schema.Class` / `TaggedClass` as the default. Brand domain primitives (`UserId`, `Email`, …) so same-shaped strings cannot be mixed. Model closed variants as tagged structs/unions (or `Data.TaggedEnum` for internal workflow state) and match exhaustively.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
export class User extends Schema.Class<User>('User')({
|
|
11
|
+
id: Schema.String,
|
|
12
|
+
email: Schema.String,
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
function find(userId: string, postId: string) {}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Exceptions
|
|
19
|
+
|
|
20
|
+
`Schema.Class` is fine when you truly need instance methods/getters on the value. Wire DTOs that intentionally use plain strings before branding at the decode boundary are fine. Non-Effect TypeScript models outside Schema are out of scope for this rule.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Prefer Stream for multi-value effectful sources
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Prefer `Stream` (and Queue/PubSub-backed streams) for effectful sources that emit many values over time and need pull, backpressure, interruption, or transformation—SSE, uploads, logs, model tokens, pagination—rather than ad-hoc async iterators glued with bare Promises.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
async function* pages() {
|
|
11
|
+
while (true) {
|
|
12
|
+
const batch = await fetchPage()
|
|
13
|
+
if (batch.length === 0) return
|
|
14
|
+
yield batch
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Exceptions
|
|
20
|
+
|
|
21
|
+
Single-shot Effects that return one array are fine. Node streams bridged once at an adapter boundary into `Stream` are fine.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Use Context.Service and Layer instead of hidden singletons
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Application dependencies that call networks, disks, clocks, or process env should be `Context.Service` values provided by `Layer`, not module-level mutable singletons or direct `process.env` reads in business logic. Reading config through `Config` / `ConfigProvider` inside a layer is required when the value varies by environment.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
export const db = new Pool(process.env.DATABASE_URL)
|
|
11
|
+
export function getUser(id: string) { return db.query(id) }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Process entrypoints may read env while building layers. True constants (literal URLs for public docs, fixed algorithm names) may stay as module constants. Scripts and one-off tools are fine.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Yield tagged errors and recover with catchTag
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Define expected failures with `Schema.TaggedErrorClass` (or `Data.TaggedError`). In `Effect.gen`, yield the error value directly (`yield* new NotFound({ id })`) instead of `yield* Effect.fail(new NotFound(...))`. Recover with `Effect.catchTag` / `catchTags` / Match on tags—not a broad catch that switches on `error._tag` by hand.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
yield* Effect.fail(new NotFound({ id }))
|
|
11
|
+
program.pipe(
|
|
12
|
+
Effect.catchAll((error) =>
|
|
13
|
+
error._tag === 'NotFound' ? fallback : Effect.fail(error),
|
|
14
|
+
),
|
|
15
|
+
)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Exceptions
|
|
19
|
+
|
|
20
|
+
Defects and truly unknown failures may use cause-level handlers. Mapping a foreign error into a tagged domain error at an adapter boundary is fine.
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patdown/packs",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Optional fuzzy rule packs and individual rule files for patdown",
|
|
5
|
+
"homepage": "https://github.com/tyler-dot-earth/patdown",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/tyler-dot-earth/patdown/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "0BSD",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/tyler-dot-earth/patdown.git",
|
|
13
|
+
"directory": "packages/patdown-packs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md",
|
|
18
|
+
"anti-slop",
|
|
19
|
+
"effect",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./README.md",
|
|
25
|
+
"./anti-slop": "./anti-slop/README.md",
|
|
26
|
+
"./anti-slop/*": "./anti-slop/*",
|
|
27
|
+
"./effect": "./effect/README.md",
|
|
28
|
+
"./effect/*": "./effect/*",
|
|
29
|
+
"./typescript": "./typescript/README.md",
|
|
30
|
+
"./typescript/*": "./typescript/*"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"oxfmt": "^0.65.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.22.2"
|
|
40
|
+
},
|
|
41
|
+
"patdown": {
|
|
42
|
+
"packageKind": "content-package"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node ../../scripts/warn-direct-package-task.mjs build && node ./scripts/sync-patdown-packs.mjs",
|
|
46
|
+
"check": "node ../../scripts/warn-direct-package-task.mjs check && pnpm run lint && pnpm run format:check && pnpm run typecheck && pnpm run test",
|
|
47
|
+
"clean": "rm -rf anti-slop effect typescript LICENSE",
|
|
48
|
+
"format": "oxfmt --write package.json",
|
|
49
|
+
"format:check": "node ../../scripts/warn-direct-package-task.mjs format:check && oxfmt --check package.json",
|
|
50
|
+
"lint": "node ../../scripts/warn-direct-package-task.mjs lint && printf 'content package: no TypeScript to lint.\\n'",
|
|
51
|
+
"test": "node ../../scripts/warn-direct-package-task.mjs test && node --test tests/sync-patdown-packs.test.mjs",
|
|
52
|
+
"typecheck": "node ../../scripts/warn-direct-package-task.mjs typecheck && printf 'content package: no TypeScript to typecheck.\\n'"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# typescript pack
|
|
2
|
+
|
|
3
|
+
TypeScript type-safety crimes and type laundering.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- [Prefer discriminated unions over flag bags](prefer-discriminated-unions-over-flag-bags.md)
|
|
8
|
+
- [Do not launder types with casts](do-not-launder-types-with-casts.md)
|
|
9
|
+
- [Keep branded and precise types through boundaries](keep-branded-and-precise-types-through-boundaries.md)
|
|
10
|
+
- [Prefer named object parameters for multi-arg functions](prefer-named-object-parameters-for-multi-arg-functions.md)
|
|
11
|
+
|
|
12
|
+
## Use
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx patdown --rules ./node_modules/@patdown/packs/typescript
|
|
16
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Do not launder types with casts
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not use `as`, non-null assertions, or chained assertions to make a value look like a narrower type after discarding evidence. Validate at the boundary or keep the precise type. A cast whose only job is to silence the type checker is a violation.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const id = raw as UserId
|
|
11
|
+
const value = (data as unknown as Config).port!
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
`as const` is fine. A cast next to a `SAFETY:` comment that names the invariant is fine when the surrounding code actually establishes that invariant. Test fixtures that build incomplete objects for a fake may cast when the fake's contract is intentional.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Keep branded and precise types through boundaries
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Do not strip branded or precise types by re-annotating them as `string`, `number`, `object`, `unknown`, or an anonymous structural type when the branded/precise type is still available. Prefer deriving with `Pick`, `Omit`, `Parameters`, `ReturnType`, or the schema's type.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
function save(userId: string) { /* userId was UserId one call up */ }
|
|
11
|
+
const row: { id: string } = brandedUser
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
|
|
16
|
+
Encoding to JSON/storage where the wire type is truly a string is fine at the encoder boundary. Public API surfaces that intentionally accept plain strings and brand inside are fine.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Prefer discriminated unions over flag bags
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Model related states as a tagged union so invalid combinations cannot compile. A single object with several optional fields or booleans that only make sense together is a violation when those fields represent mutually exclusive outcomes.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
type LoadState = { loading: boolean; user?: User; error?: string }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Exceptions
|
|
14
|
+
|
|
15
|
+
Independent optional fields that can all be present at once are fine. Transport DTOs that mirror a wire format before decoding into a union are fine when the next step narrows them.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Prefer named object parameters for multi-arg functions
|
|
2
|
+
globs: `**/*.ts`
|
|
3
|
+
globs: `**/*.tsx`
|
|
4
|
+
|
|
5
|
+
Exported or cross-module functions with two or more parameters of similar or swappable types should take one named object instead of positional args. Swappable strings, numbers, or ids in adjacent positions are the main violation.
|
|
6
|
+
|
|
7
|
+
## Not allowed
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
export function sendEmail(subject: string, body: string) {}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Exceptions
|
|
14
|
+
|
|
15
|
+
Hot numeric kernels, well-known callbacks (`(err, value) => …`), constructors matching a platform API, and single-parameter functions are fine. Methods that already use a destructured object parameter are fine.
|