ts-reviewer 1.1.0 → 1.2.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/package.json +1 -1
- package/ts-reviewer/SKILL.md +68 -20
- package/ts-reviewer/references/architecture.md +40 -0
- package/ts-reviewer/references/async-patterns.md +50 -8
- package/ts-reviewer/references/boundary-validation.md +56 -0
- package/ts-reviewer/references/code-quality.md +42 -7
- package/ts-reviewer/references/dependency-hygiene.md +47 -0
- package/ts-reviewer/references/error-handling.md +49 -0
- package/ts-reviewer/references/fix-workflow.md +17 -5
- package/ts-reviewer/references/modernization.md +80 -27
- package/ts-reviewer/references/security.md +45 -2
- package/ts-reviewer/references/tsconfig.md +35 -2
- package/ts-reviewer/references/type-safety.md +48 -1
package/package.json
CHANGED
package/ts-reviewer/SKILL.md
CHANGED
|
@@ -33,9 +33,9 @@ Controls which review domains are active. Detected from flags first, then natura
|
|
|
33
33
|
|
|
34
34
|
| Flag / phrase | Active domains |
|
|
35
35
|
|---|---|
|
|
36
|
-
| (none / default) | Type Safety, Security, Async Patterns, Modernization, Code Quality, Config |
|
|
36
|
+
| (none / default) | Type Safety, Security, Async Patterns, Modernization, Code Quality, Config, Boundary Validation, Error Handling, Dependency Hygiene |
|
|
37
37
|
| `--arch` / "review architecture" / "find refactoring opportunities" / "deepening review" | Architecture only |
|
|
38
|
-
| `--full` / "full audit" / "full review" / "review everything" | All
|
|
38
|
+
| `--full` / "full audit" / "full review" / "review everything" | All 10 domains (default 9 + Architecture) |
|
|
39
39
|
|
|
40
40
|
Parse order: check for explicit `--arch` / `--full` / `--no-arch` flags in the user's message first.
|
|
41
41
|
Fall back to phrase detection. If no flag or phrase matches → default domain set (no architecture).
|
|
@@ -104,30 +104,34 @@ If not specified, default to **full codebase**.
|
|
|
104
104
|
|
|
105
105
|
### Git commands per scope
|
|
106
106
|
|
|
107
|
-
**`full`** — all
|
|
107
|
+
**`full`** — all TypeScript files:
|
|
108
108
|
```bash
|
|
109
|
-
npx glob '**/*.ts' --ignore '**/node_modules/**'
|
|
110
|
-
# or: git ls-files '*.ts'
|
|
109
|
+
npx glob '**/*.{ts,mts,cts}' --ignore '**/node_modules/**'
|
|
110
|
+
# or: git ls-files '*.ts' '*.mts' '*.cts'
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
**`uncommitted`** — staged + unstaged + untracked:
|
|
114
114
|
```bash
|
|
115
|
-
git diff --name-only HEAD -- '*.ts'
|
|
116
|
-
git ls-files --others --exclude-standard -- '*.ts'
|
|
115
|
+
git diff --name-only HEAD -- '*.ts' '*.mts' '*.cts'
|
|
116
|
+
git ls-files --others --exclude-standard -- '*.ts' '*.mts' '*.cts'
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
**`branch`** — current branch vs base:
|
|
120
120
|
```bash
|
|
121
121
|
BASE=$(git rev-parse --verify main 2>/dev/null && echo main || echo master)
|
|
122
|
-
git diff --name-only "$BASE"...HEAD -- '*.ts'
|
|
123
|
-
git diff --name-only HEAD -- '*.ts'
|
|
122
|
+
git diff --name-only "$BASE"...HEAD -- '*.ts' '*.mts' '*.cts'
|
|
123
|
+
git diff --name-only HEAD -- '*.ts' '*.mts' '*.cts'
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
**`commits:N`** — last N commits:
|
|
127
127
|
```bash
|
|
128
|
-
git diff --name-only HEAD~N..HEAD -- '*.ts'
|
|
128
|
+
git diff --name-only HEAD~N..HEAD -- '*.ts' '*.mts' '*.cts'
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
**File type policy:** `.mts`/`.cts` are reviewed like `.ts`. `.d.ts` files are reviewed
|
|
132
|
+
only by the Type Safety and Config domains (declarations have no runtime behavior).
|
|
133
|
+
`.tsx` is out of scope — pure TypeScript only, no frameworks.
|
|
134
|
+
|
|
131
135
|
### Context files (scoped reviews)
|
|
132
136
|
|
|
133
137
|
Analysis scope = diff file list. Reading scope = wider. Always include as read-only:
|
|
@@ -143,7 +147,7 @@ Context files are NOT analyzed for issues.
|
|
|
143
147
|
|
|
144
148
|
Collect changed hunks:
|
|
145
149
|
```bash
|
|
146
|
-
git diff -U0 [appropriate range] -- '*.ts' | grep '^@@'
|
|
150
|
+
git diff -U0 [appropriate range] -- '*.ts' '*.mts' '*.cts' | grep '^@@'
|
|
147
151
|
```
|
|
148
152
|
|
|
149
153
|
- Issue on **new/modified line**: boost severity +1 (Low→Medium, Medium→High, High→Highest)
|
|
@@ -164,6 +168,9 @@ git diff -U0 [appropriate range] -- '*.ts' | grep '^@@'
|
|
|
164
168
|
|
|
165
169
|
4. **Read `tsconfig.json`**. Load `references/tsconfig.md` and audit config flags.
|
|
166
170
|
In scoped modes: only flag config if `tsconfig.json` is in diff or if full review.
|
|
171
|
+
Monorepos: detect workspaces (`workspaces` in package.json, `pnpm-workspace.yaml`) and
|
|
172
|
+
multiple tsconfigs (`tsconfig.build.json`, per-package configs, `references`). Audit the
|
|
173
|
+
config that actually governs the files in scope; note which config applies in the summary.
|
|
167
174
|
|
|
168
175
|
5. **Check for linter configs** — `eslint.config.*`, `.eslintrc.*`, `biome.json`, `deno.json`.
|
|
169
176
|
|
|
@@ -200,7 +207,11 @@ Files in scope: <N> .ts files (+ <M> context files)
|
|
|
200
207
|
npx tsc --noEmit 2>&1 | head -200
|
|
201
208
|
```
|
|
202
209
|
|
|
203
|
-
|
|
210
|
+
All output lines are errors (tsc emits no warnings). Triage each error:
|
|
211
|
+
- Indicates a runtime hazard (null/undefined access, wrong argument shape,
|
|
212
|
+
missing property) -> **Highest**
|
|
213
|
+
- Hygiene errors (unused locals, unreachable code, implicit any on internals) -> **High**
|
|
214
|
+
|
|
204
215
|
In scoped modes: run on full project, report only errors in scoped files.
|
|
205
216
|
|
|
206
217
|
### 2b. Linter
|
|
@@ -208,7 +219,11 @@ In scoped modes: run on full project, report only errors in scoped files.
|
|
|
208
219
|
ESLint: `npx eslint [files] --format json 2>/dev/null | head -500`
|
|
209
220
|
Biome: `npx biome check [files] --reporter json 2>/dev/null | head -500`
|
|
210
221
|
|
|
211
|
-
Map
|
|
222
|
+
Map linter severities conservatively — projects configure stylistic rules as `error`:
|
|
223
|
+
- Rule matches a checklist item in a reference file -> use the checklist severity
|
|
224
|
+
- Correctness-class rule (`no-floating-promises`, `no-misused-promises`,
|
|
225
|
+
`no-unsafe-*`) -> **High**
|
|
226
|
+
- Anything else: `error` -> **Medium**, `warning` -> **Low**, `info` -> **Low**
|
|
212
227
|
|
|
213
228
|
### 2c. LSP diagnostics (if available)
|
|
214
229
|
|
|
@@ -228,9 +243,12 @@ Run only the agents whose domain is in the active domain set (see Domain Set sec
|
|
|
228
243
|
| Security | `references/security.md` | Injection, prototype pollution, ReDoS, path traversal |
|
|
229
244
|
| Async Patterns | `references/async-patterns.md` | Floating promises, race conditions, error propagation |
|
|
230
245
|
| Modernization | `references/modernization.md` | Outdated patterns vs TS 5.9+ idioms |
|
|
231
|
-
| Code Quality | `references/code-quality.md` | Complexity, duplication, naming, dead code |
|
|
246
|
+
| Code Quality | `references/code-quality.md` | Complexity, duplication, naming, dead code, testability |
|
|
232
247
|
| Config | `references/tsconfig.md` | tsconfig.json flags and module setup |
|
|
233
|
-
|
|
|
248
|
+
| Boundary Validation | `references/boundary-validation.md` | Runtime validation at system edges, DTO/domain separation |
|
|
249
|
+
| Error Handling | `references/error-handling.md` | Silent failures, throw hygiene, failure design |
|
|
250
|
+
| Dependency Hygiene | `references/dependency-hygiene.md` | package.json, versions, lockfiles, supply chain |
|
|
251
|
+
| Architecture | `references/architecture.md` | Shallow modules, scattered concepts, coupling, dependency seams, layering |
|
|
234
252
|
|
|
235
253
|
### Sub-agent template (Claude Code)
|
|
236
254
|
|
|
@@ -253,7 +271,7 @@ Output JSONL, one object per line:
|
|
|
253
271
|
"fix": "Concrete recommendation with code example",
|
|
254
272
|
"auto_fixable": true|false,
|
|
255
273
|
"in_diff": true|false,
|
|
256
|
-
"reference": "
|
|
274
|
+
"reference": "optional — omit unless allowed by the Evidence Protocol"
|
|
257
275
|
}
|
|
258
276
|
```
|
|
259
277
|
|
|
@@ -274,8 +292,11 @@ Go through each domain one at a time. Same JSON structure.
|
|
|
274
292
|
|
|
275
293
|
1. Apply severity boost (scoped modes only): `in_diff: true` -> boost +1 level.
|
|
276
294
|
2. Deduplicate: same file + line + issue -> keep one.
|
|
277
|
-
3. Merge overlapping: keep higher severity, note overlap.
|
|
295
|
+
3. Merge overlapping: keep higher severity, note overlap. If two domains flag the same
|
|
296
|
+
file + line, merge into one entry attributing both categories — never emit twice.
|
|
278
297
|
4. Consolidate patterns: 3+ identical issues -> one "Recurring Pattern" entry.
|
|
298
|
+
5. Noise budget: if a single domain produces > 25 Medium/Low findings, keep its top 15
|
|
299
|
+
by severity and impact, consolidate the rest into Recurring Pattern entries with counts.
|
|
279
300
|
|
|
280
301
|
### Report structure
|
|
281
302
|
|
|
@@ -289,7 +310,7 @@ Write to `code-smells.md`:
|
|
|
289
310
|
**TypeScript version:** <version>
|
|
290
311
|
**Scope:** Full / Uncommitted / Branch `x` vs `y` / Last N commits
|
|
291
312
|
**Files analyzed:** N (+ M context)
|
|
292
|
-
**Total issues:** N (X
|
|
313
|
+
**Total issues:** N (X highest, Y high, Z medium, W low)
|
|
293
314
|
**Severity-boosted:** N (scoped modes only)
|
|
294
315
|
|
|
295
316
|
## Summary
|
|
@@ -379,11 +400,38 @@ See `references/architecture.md` → Severity Mapping for Architecture.
|
|
|
379
400
|
|
|
380
401
|
---
|
|
381
402
|
|
|
403
|
+
## Evidence Protocol
|
|
404
|
+
|
|
405
|
+
Every finding must survive verification before it enters the report:
|
|
406
|
+
|
|
407
|
+
1. **Re-read before reporting.** After drafting a finding, re-read the exact lines in the
|
|
408
|
+
current file state. The `snippet` must appear verbatim at the stated `line` (±2 lines).
|
|
409
|
+
If it doesn't, re-locate or drop the finding.
|
|
410
|
+
2. **Check the surrounding context.** If a guard, validation, type narrowing, or comment
|
|
411
|
+
within the enclosing function/module already handles the case, the finding is a false
|
|
412
|
+
positive — drop it, don't downgrade it.
|
|
413
|
+
3. **Confidence gate.** Report only findings you can defend with the code in front of you.
|
|
414
|
+
If a finding depends on runtime behavior you cannot see (external input actually reaching
|
|
415
|
+
a sink, concurrent callers actually existing), either verify the data flow by reading the
|
|
416
|
+
callers, or mark the finding's problem statement with "if <condition>" and cap severity
|
|
417
|
+
at Medium.
|
|
418
|
+
4. **References must be real.** The `reference` field is optional. Only cite
|
|
419
|
+
typescriptlang.org, developer.mozilla.org, nodejs.org, or a file path inside this skill.
|
|
420
|
+
Never construct URLs from memory for anything else — omit the field instead.
|
|
421
|
+
5. **Convention override.** If a flagged non-High pattern appears consistently 5+ times
|
|
422
|
+
across the codebase, treat it as an intentional convention: downgrade one severity level
|
|
423
|
+
and report it once as a Recurring Pattern, not per occurrence.
|
|
424
|
+
6. **Version gate.** Every finding that recommends newer syntax or APIs must state the
|
|
425
|
+
minimum TS/runtime version required, verified against the project's actual TS version,
|
|
426
|
+
`target`/`lib`, and runtime. Never recommend a feature the project cannot use.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
382
430
|
## Important Guidelines
|
|
383
431
|
|
|
384
432
|
- **No framework checks.** Pure TypeScript only.
|
|
385
|
-
- **No false positives from intentional patterns.**
|
|
386
|
-
|
|
433
|
+
- **No false positives from intentional patterns.** Suppression-directive severities are
|
|
434
|
+
owned by `references/type-safety.md` (Suppression Directives) — follow it, don't improvise.
|
|
387
435
|
- **Respect project conventions.** Don't flag consistent patterns unless harmful.
|
|
388
436
|
- **Be concrete.** Every issue needs a snippet and a fix recommendation.
|
|
389
437
|
"Consider refactoring" is not a valid fix.
|
|
@@ -49,6 +49,46 @@ Use these terms in all architecture findings. Don't substitute "component", "ser
|
|
|
49
49
|
- Shared utility module mixes unrelated domain concepts (a grab-bag util). Severity: **Medium**.
|
|
50
50
|
- Feature logic split by technical layer (e.g. controller / service / repo per feature) in a way that destroys locality — change to one feature requires touching 4+ files. Severity: **Medium**.
|
|
51
51
|
|
|
52
|
+
### Under-Engineering
|
|
53
|
+
|
|
54
|
+
Depth cuts both ways — flag missing abstraction where it already hurts:
|
|
55
|
+
|
|
56
|
+
- Identical business rule (same constants/branching) implemented in 2+ modules.
|
|
57
|
+
Severity: **Medium**. Under-abstraction; deepen one module to own the rule.
|
|
58
|
+
- One module accreting unrelated concerns (imports from many unrelated domains, exports
|
|
59
|
+
serving disjoint caller groups). Severity: **Medium**. Split along caller groups.
|
|
60
|
+
- Event/string-keyed indirection between two modules that only ever talk to each other —
|
|
61
|
+
a direct typed call would be simpler and checkable. Severity: **Low**.
|
|
62
|
+
|
|
63
|
+
## Dependency Direction and Layering
|
|
64
|
+
|
|
65
|
+
Direction matters more than layer count. Detect and flag:
|
|
66
|
+
|
|
67
|
+
- Domain/computation modules importing IO directly (`node:fs`, `node:http`, DB clients,
|
|
68
|
+
`fetch`) when the dependency classification says the IO belongs behind a seam.
|
|
69
|
+
Severity: **Medium**. The domain owns logic; transport/storage is injected or
|
|
70
|
+
isolated at the edge.
|
|
71
|
+
- Shared/leaf modules (`utils/`, `types/`, `core/`) importing from feature modules —
|
|
72
|
+
inverted direction; the "shared" code now depends on a specific feature.
|
|
73
|
+
Severity: **High** — this is how import cycles start.
|
|
74
|
+
- Wire/DTO types from an external API imported deep into domain modules rather than
|
|
75
|
+
mapped to domain types at the boundary. Severity: **Medium**.
|
|
76
|
+
Cross-reference: `references/boundary-validation.md`.
|
|
77
|
+
|
|
78
|
+
## Detection Heuristics
|
|
79
|
+
|
|
80
|
+
Run these — don't guess from file names:
|
|
81
|
+
|
|
82
|
+
- Grep feature-module imports inside `shared|common|core|utils` directories
|
|
83
|
+
(inverted dependency direction).
|
|
84
|
+
- For each module, list its importers. A module whose every export has exactly one
|
|
85
|
+
importer is a pass-through candidate — apply the deletion test.
|
|
86
|
+
- An interface/type re-exported through 3+ files unchanged marks a pass-through chain.
|
|
87
|
+
- A utility file imported by the majority of modules is a grab-bag candidate — check
|
|
88
|
+
whether its exports share a domain concept.
|
|
89
|
+
- Change-set locality: in `git log --name-only`, do commits touching one feature
|
|
90
|
+
consistently touch 4+ directories? That feature's logic is scattered.
|
|
91
|
+
|
|
52
92
|
## Dependency Classification
|
|
53
93
|
|
|
54
94
|
When proposing a deepening fix, classify the dependency to determine test strategy:
|
|
@@ -13,19 +13,59 @@
|
|
|
13
13
|
|
|
14
14
|
## Error Handling
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- `catch` that only logs without rethrowing. Severity: **Medium**.
|
|
20
|
-
- `Promise.allSettled()` ignoring `'rejected'` entries. Severity: **Medium**.
|
|
16
|
+
All catch/throw/failure-design checks live in `references/error-handling.md` — do not
|
|
17
|
+
duplicate them here. This domain only flags errors *lost to the async machinery*
|
|
18
|
+
(floating promises above, allSettled below).
|
|
21
19
|
|
|
22
20
|
## Race Conditions
|
|
23
21
|
|
|
24
22
|
- Multiple async ops modifying shared state without coordination.
|
|
25
|
-
Severity: **High**.
|
|
23
|
+
Severity: **High**. Fix: serialize via a promise chain / async mutex, or make the
|
|
24
|
+
operations idempotent so ordering doesn't matter.
|
|
26
25
|
- `Promise.race()` where losing promise's side effects still execute.
|
|
27
26
|
Severity: **Medium**.
|
|
28
27
|
- Read-modify-write with `await` in the middle. Severity: **High** in concurrent contexts.
|
|
28
|
+
- Overlapping requests writing to the same variable/state where the earlier response
|
|
29
|
+
can arrive last (last-write-wins clobbering). Severity: **High** in concurrent contexts.
|
|
30
|
+
Fix: request-sequence token check before applying the result, or abort the previous
|
|
31
|
+
request via AbortController.
|
|
32
|
+
- Lazily-initialized async singleton where concurrent first callers each run the
|
|
33
|
+
initializer. Severity: **Medium**. Fix: memoize the *promise*, not the resolved value:
|
|
34
|
+
`init ??= doInit(); return init;`
|
|
35
|
+
|
|
36
|
+
## Timeouts
|
|
37
|
+
|
|
38
|
+
- `await` on network/IO (fetch, DB call, queue op) with no timeout and no AbortSignal.
|
|
39
|
+
Severity: **Medium** (internal tools), **High** (request-handling / server paths —
|
|
40
|
+
a hung dependency hangs every caller).
|
|
41
|
+
Fix: `fetch(url, { signal: AbortSignal.timeout(5000) })`, or `Promise.race` with a
|
|
42
|
+
timer for APIs without signal support (clear the timer in `finally`).
|
|
43
|
+
- Timeout implemented with `Promise.race` but the losing operation keeps running and
|
|
44
|
+
holds resources (sockets, locks). Severity: **Medium**. Fix: abort the operation,
|
|
45
|
+
don't just abandon the promise.
|
|
46
|
+
|
|
47
|
+
## Retries
|
|
48
|
+
|
|
49
|
+
- Retry loop without a max-attempt cap. Severity: **High** (infinite loop under
|
|
50
|
+
persistent failure).
|
|
51
|
+
- Retries without backoff (tight loop hammering a failing dependency). Severity: **Medium**.
|
|
52
|
+
Fix: exponential backoff with jitter.
|
|
53
|
+
- Retrying a non-idempotent operation (payment, email send, resource creation).
|
|
54
|
+
Severity: **Highest** — duplicate side effects. Fix: idempotency key or check-then-act
|
|
55
|
+
on the server side; otherwise don't retry.
|
|
56
|
+
|
|
57
|
+
## Concurrency Limits
|
|
58
|
+
|
|
59
|
+
- `Promise.all(items.map(asyncFn))` where `items` is unbounded (user data, file lists,
|
|
60
|
+
API pages) and `asyncFn` does network/disk work. Severity: **Medium**.
|
|
61
|
+
Fix: process in chunks or use a concurrency limiter. Do NOT recommend converting
|
|
62
|
+
sequential awaits to unbounded `Promise.all` — recommend it only with an explicit
|
|
63
|
+
concurrency cap when the collection can exceed ~10 items.
|
|
64
|
+
- `Promise.all` where one rejection should not discard sibling results, or where
|
|
65
|
+
siblings' later rejections become unhandled. Severity: **Medium**.
|
|
66
|
+
Fix: `Promise.allSettled` + explicit handling of `rejected` entries.
|
|
67
|
+
- `Promise.allSettled()` results used without checking `status === 'rejected'` entries.
|
|
68
|
+
Severity: **Medium**.
|
|
29
69
|
|
|
30
70
|
## Cancellation
|
|
31
71
|
|
|
@@ -47,8 +87,10 @@
|
|
|
47
87
|
- `async function() { return await bar(); }` — unnecessary await (except in try/catch).
|
|
48
88
|
Severity: **Low**.
|
|
49
89
|
- Mixing `await` and `.then()` chains in same function. Severity: **Low**.
|
|
50
|
-
- Sequential `await` in loop when iterations are independent
|
|
51
|
-
Severity: **Medium**. Fix: `Promise.all(items.map(...))
|
|
90
|
+
- Sequential `await` in loop when iterations are independent AND the collection is small
|
|
91
|
+
and bounded (< ~10 known items). Severity: **Medium**. Fix: `Promise.all(items.map(...))`.
|
|
92
|
+
If the collection is unbounded or does network/disk work, require a concurrency cap
|
|
93
|
+
instead — see Concurrency Limits.
|
|
52
94
|
|
|
53
95
|
## Async Iterators
|
|
54
96
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Boundary Validation Checklist
|
|
2
|
+
|
|
3
|
+
Runtime boundaries and data contracts: the places where untyped data from outside the
|
|
4
|
+
process becomes typed. Design rule — **parse, don't validate**: one place where external
|
|
5
|
+
data is checked and converted into a typed value, after which no re-checking anywhere.
|
|
6
|
+
The compiler only knows what happens inside the process; every claim about outside data
|
|
7
|
+
is a promise the code must earn at the boundary.
|
|
8
|
+
|
|
9
|
+
## Lying to the Compiler at Boundaries
|
|
10
|
+
|
|
11
|
+
- `as T` (or a typed variable annotation) on `JSON.parse(...)`, `await res.json()`,
|
|
12
|
+
`process.env.X`, CLI args, queue/socket messages, file contents.
|
|
13
|
+
Severity: **High** — the canonical unverified-boundary pattern; the type is a wish.
|
|
14
|
+
Fix: validate at the boundary with a schema (Zod/Valibot/ArkType/ajv) or a hand-written
|
|
15
|
+
guard, then use the *inferred* type from the schema.
|
|
16
|
+
- `fetch` wrapper generic `get<T>(url): Promise<T>` that casts internally — moves the
|
|
17
|
+
lie into a helper every caller trusts. Severity: **High**.
|
|
18
|
+
- Validation library already in dependencies while boundaries still cast.
|
|
19
|
+
Severity: **Medium** — the tool exists; wire it in.
|
|
20
|
+
|
|
21
|
+
## Environment and Config
|
|
22
|
+
|
|
23
|
+
- `process.env.X` read scattered through the codebase (with `!` or `as string`).
|
|
24
|
+
Severity: **Medium**. Fix: validate all env vars once at startup into a typed,
|
|
25
|
+
frozen config object; everything else imports the config.
|
|
26
|
+
- Missing-required-config discovered deep at first use instead of at startup.
|
|
27
|
+
Severity: **Medium** — fail fast at boot with a clear message.
|
|
28
|
+
|
|
29
|
+
## DTO / Domain Separation
|
|
30
|
+
|
|
31
|
+
- External wire types (API response shapes, DB row types, third-party SDK types)
|
|
32
|
+
imported into deep domain modules and used as the domain model. Severity: **Medium** —
|
|
33
|
+
couples core logic to a contract someone else can change; renames/nullability ripple
|
|
34
|
+
everywhere. Fix: map wire types to domain types at the boundary module; domain code
|
|
35
|
+
never imports wire types.
|
|
36
|
+
- Internal entities returned directly as API responses (response shaping by leaking the
|
|
37
|
+
storage/domain object). Severity: **Medium** — leaks fields added later, couples wire
|
|
38
|
+
format to storage. Fix: explicit response DTO + mapping at the edge.
|
|
39
|
+
|
|
40
|
+
## Contract Visibility
|
|
41
|
+
|
|
42
|
+
- Public API boundary (exported package function, HTTP handler, message consumer) whose
|
|
43
|
+
input constraints exist only as runtime checks deep inside — not visible in the
|
|
44
|
+
signature or schema. Severity: **Medium**. Fix: schema/type at the boundary is the
|
|
45
|
+
contract; derive both the static type and the runtime validation from it.
|
|
46
|
+
- Two independent definitions of the same contract (a TypeScript type AND a separate
|
|
47
|
+
schema, maintained by hand in parallel). Severity: **Medium** — they will drift.
|
|
48
|
+
Fix: derive the type from the schema (`z.infer`) or generate both from one source.
|
|
49
|
+
|
|
50
|
+
## Non-Findings
|
|
51
|
+
|
|
52
|
+
- Do NOT flag missing validation on data that never leaves the process (internal
|
|
53
|
+
function calls, module-to-module) — that's what static types are for. Boundary
|
|
54
|
+
validation at every layer is over-engineering, not safety.
|
|
55
|
+
- Do NOT demand a specific validation library; hand-written guards are fine when the
|
|
56
|
+
shape is small. Flag the missing check, not the missing dependency.
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
- Commented-out code blocks (> 2-3 lines). Severity: **Low**. Fix: delete (VCS has history).
|
|
15
15
|
- Unused private class members. Severity: **Low**.
|
|
16
16
|
- Exported symbols never imported anywhere. Severity: **Medium**.
|
|
17
|
+
Exception: symbols re-exported from package entry points or the `exports` map are the
|
|
18
|
+
public API surface of a library, not dead code — do not flag.
|
|
17
19
|
- Empty files or import-only files. Severity: **Low**.
|
|
18
20
|
- Defined but never called functions. Severity: **Medium**.
|
|
19
21
|
|
|
@@ -23,15 +25,48 @@
|
|
|
23
25
|
- Single-letter variables outside trivial loops. Severity: **Low**.
|
|
24
26
|
- Inconsistent conventions (mixing camelCase/snake_case). Severity: **Low**.
|
|
25
27
|
- Booleans without `is`/`has`/`should`/`can` prefix. Severity: **Low**.
|
|
28
|
+
Report once per codebase as a Recurring Pattern, never per variable.
|
|
26
29
|
- Opaque abbreviations (`usr`, `msg`, `cfg`). Severity: **Low**.
|
|
27
30
|
|
|
28
31
|
## Error Handling
|
|
29
32
|
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
All error-handling checks live in `references/error-handling.md` — do not duplicate here.
|
|
34
|
+
|
|
35
|
+
## Debug Artifacts
|
|
36
|
+
|
|
37
|
+
- `debugger;` statement in committed code. Severity: **High** — blocks execution under
|
|
38
|
+
devtools; never ship.
|
|
39
|
+
- Leftover `console.log`/`console.debug` from debugging sessions (dumps of local
|
|
40
|
+
variables, "here", "test") in non-CLI code. Severity: **Low**; **Medium** in library
|
|
41
|
+
code. Don't flag intentional logging or CLI output.
|
|
42
|
+
- Committed `.only` / `.skip` in test files. Severity: **High** — `.only` silently
|
|
43
|
+
disables the rest of the suite.
|
|
44
|
+
|
|
45
|
+
## Import-time Side Effects
|
|
46
|
+
|
|
47
|
+
- Module top-level code performing IO, registrations, or global mutation while the
|
|
48
|
+
module also exports pure logic. Severity: **Medium** — makes the module untestable
|
|
49
|
+
and load-order-dependent. Fix: move behind an explicit `init()` or the entry point.
|
|
50
|
+
- Singleton constructed at module scope and imported everywhere. Severity: **Medium** —
|
|
51
|
+
hidden shared state; nothing can substitute it in tests.
|
|
52
|
+
|
|
53
|
+
## Testability
|
|
54
|
+
|
|
55
|
+
- `Date.now()` / `new Date()` / `Math.random()` inline in business logic.
|
|
56
|
+
Severity: **Medium** — untestable nondeterminism. Fix: inject a clock/rng, or accept
|
|
57
|
+
the value as a parameter with a default.
|
|
58
|
+
- Logic reachable only through static call chains that cannot be substituted in tests.
|
|
59
|
+
Severity: **Low**; escalate to **Medium** if tests already work around it with
|
|
60
|
+
module-mocking hacks.
|
|
61
|
+
|
|
62
|
+
## Speculative Abstraction
|
|
63
|
+
|
|
64
|
+
Lightweight over-engineering checks, active in default scans. For the full framework
|
|
65
|
+
(deletion test, seams, depth) load `references/architecture.md` (`--arch` / `--full`).
|
|
66
|
+
|
|
67
|
+
- Interface with exactly one implementation and no test double using it. Severity: **Low**.
|
|
68
|
+
- Factory/builder for a class constructed in exactly one place. Severity: **Low**.
|
|
69
|
+
- Config option/parameter whose value is identical at every call site. Severity: **Low**.
|
|
35
70
|
|
|
36
71
|
## Duplication
|
|
37
72
|
|
|
@@ -63,8 +98,8 @@
|
|
|
63
98
|
|
|
64
99
|
## Module Structure
|
|
65
100
|
|
|
66
|
-
-
|
|
67
|
-
|
|
101
|
+
- Circular imports (including cycles through barrel files). Severity: **High**.
|
|
102
|
+
A barrel file that causes no cycle is not a finding by itself.
|
|
68
103
|
- Deep relative imports (`../../../`). Severity: **Low**. Fix: path aliases.
|
|
69
104
|
|
|
70
105
|
For deeper architectural refactors (shallow modules, dependency seams, module deepening, coupling),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Dependency Hygiene Checklist
|
|
2
|
+
|
|
3
|
+
Audits `package.json`, lockfiles, and the dependency graph. `package.json` is already
|
|
4
|
+
read in Phase 1 — reuse it. Run the cheap machine checks; don't guess.
|
|
5
|
+
|
|
6
|
+
## Machine Checks (run these)
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm audit --json 2>/dev/null | head -100 # or: pnpm audit / yarn npm audit / bun audit
|
|
10
|
+
npm outdated 2>/dev/null | head -50
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Known critical/high advisories in production dependencies. Severity: **High**
|
|
14
|
+
(Highest if the vulnerable API is actually called in scoped files).
|
|
15
|
+
- Advisories in devDependencies only. Severity: **Low** — build-time exposure.
|
|
16
|
+
- For unused-dependency detection, recommend `knip` or `depcheck` in the report rather
|
|
17
|
+
than guessing from grep — imports via configs and bins produce false positives.
|
|
18
|
+
|
|
19
|
+
## package.json Structure
|
|
20
|
+
|
|
21
|
+
- No lockfile committed (`package-lock.json`/`pnpm-lock.yaml`/`yarn.lock`/`bun.lock`).
|
|
22
|
+
Severity: **High** — unreproducible installs.
|
|
23
|
+
- Wildcard or `latest` version ranges in dependencies. Severity: **High**.
|
|
24
|
+
- Runtime packages in `devDependencies` (or types/build tools in `dependencies`).
|
|
25
|
+
Severity: **Medium** — breaks production installs / bloats them.
|
|
26
|
+
- Missing `engines.node` when the code uses version-gated APIs (`AbortSignal.timeout`,
|
|
27
|
+
`structuredClone`, `using`). Severity: **Low**.
|
|
28
|
+
- Library packages: missing or inconsistent `exports` map vs `main`/`types`
|
|
29
|
+
(deep imports unblocked, types not resolving under `nodenext`). Severity: **Medium**.
|
|
30
|
+
|
|
31
|
+
## Dependency Choice
|
|
32
|
+
|
|
33
|
+
- Duplicate-purpose dependencies (two HTTP clients, two date libs, lodash + ramda).
|
|
34
|
+
Severity: **Medium** — pick one; note which is less used.
|
|
35
|
+
- Dependency whose installed version is deprecated on npm, or package abandoned with a
|
|
36
|
+
well-known maintained successor. Severity: **Medium**. Only claim deprecation when
|
|
37
|
+
`npm outdated`/`npm audit` output or the package's own warnings show it — do not
|
|
38
|
+
assert abandonment from memory.
|
|
39
|
+
- Trivial dependency replaceable by a few lines or a built-in (left-pad class:
|
|
40
|
+
`is-odd`, `mkdirp` on Node 10+, `rimraf` vs `fs.rm`). Severity: **Low**.
|
|
41
|
+
|
|
42
|
+
## Non-Findings
|
|
43
|
+
|
|
44
|
+
- Do NOT flag version ranges (`^`/`~`) with a lockfile present — that's the normal model.
|
|
45
|
+
- Do NOT recommend adding dependencies to fix findings from other domains unless the
|
|
46
|
+
checklist item explicitly names one.
|
|
47
|
+
- Do NOT flag a dependency as outdated for being behind by a patch/minor version.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Error Handling Checklist
|
|
2
|
+
|
|
3
|
+
Owns all error-handling checks. `code-quality.md` and `async-patterns.md` link here.
|
|
4
|
+
Errors lost specifically to async machinery (floating promises) stay in async-patterns.md.
|
|
5
|
+
|
|
6
|
+
## Silent Failures
|
|
7
|
+
|
|
8
|
+
- Empty catch block `catch (e) {}` without an explanatory comment. Severity: **High**.
|
|
9
|
+
- `catch` that only logs and continues, in a code path whose caller assumes success.
|
|
10
|
+
Severity: **Medium** (High if the swallowed error leaves state partially mutated).
|
|
11
|
+
- `Promise.allSettled()` results used without checking `status === 'rejected'` entries.
|
|
12
|
+
Severity: **Medium**.
|
|
13
|
+
- Fire-and-forget cleanup (`void cleanup()`) whose failure corrupts subsequent runs.
|
|
14
|
+
Severity: **Medium**.
|
|
15
|
+
|
|
16
|
+
## Throw Hygiene
|
|
17
|
+
|
|
18
|
+
- Throwing non-Error values (strings, objects). Severity: **Medium**.
|
|
19
|
+
Fix: `throw new Error(...)` — stack traces and `instanceof` depend on it.
|
|
20
|
+
- Custom error classes not extending `Error`. Severity: **Medium**.
|
|
21
|
+
- Rethrow that discards the original: `catch (e) { throw new Error(msg) }`.
|
|
22
|
+
Severity: **Medium**. Fix: `throw new Error(msg, { cause: e })` (ES2022).
|
|
23
|
+
- Error messages without operational context (what operation, what input id).
|
|
24
|
+
Severity: **Low**.
|
|
25
|
+
|
|
26
|
+
## Catch Discipline
|
|
27
|
+
|
|
28
|
+
- Broad `catch` around a large block treating programmer errors (TypeError,
|
|
29
|
+
ReferenceError) the same as expected failures — hides bugs as handled conditions.
|
|
30
|
+
Severity: **Medium**. Fix: narrow the try to the failing operation; re-throw
|
|
31
|
+
unexpected error types.
|
|
32
|
+
- `catch (e)` where `e` is used as if typed (`e.message` without narrowing) —
|
|
33
|
+
see type-safety.md `unknown` Discipline. If the project lacks
|
|
34
|
+
`useUnknownInCatchVariables`, flag the config (tsconfig.md), not every catch site.
|
|
35
|
+
|
|
36
|
+
## Failure Design (public APIs and module seams)
|
|
37
|
+
|
|
38
|
+
- Expected, recoverable outcomes (not-found, validation failure, conflict) signaled by
|
|
39
|
+
`throw` so every caller needs try/catch for normal control flow. Severity: **Medium**.
|
|
40
|
+
Fix: return a discriminated result: `{ ok: true, value } | { ok: false, error }` —
|
|
41
|
+
exhaustiveness-checkable, contract visible in the signature. Keep `throw` for
|
|
42
|
+
unexpected/unrecoverable failures.
|
|
43
|
+
- Exported function's failure modes not derivable from its signature or docs — callers
|
|
44
|
+
can't discriminate errors except by message string matching. Severity: **Medium**.
|
|
45
|
+
Fix: typed error classes or result unions; never promise message-string stability.
|
|
46
|
+
- Matching on `e.message` content to branch behavior. Severity: **High** — breaks on
|
|
47
|
+
any wording change.
|
|
48
|
+
- `process.exit()` inside library/domain code. Severity: **High** — only entry points
|
|
49
|
+
may decide to terminate.
|
|
@@ -35,7 +35,7 @@ Check for test runner in this order:
|
|
|
35
35
|
|
|
36
36
|
| Signal | Runner | Command |
|
|
37
37
|
|---|---|---|
|
|
38
|
-
| `package.json` has `"scripts": { "test": "..." }` | npm script | `npm test` |
|
|
38
|
+
| `package.json` has `"scripts": { "test": "..." }` AND the script is not the npm placeholder (`echo "Error: no test specified" && exit 1`) | npm script | `npm test` (use `pnpm test` / `yarn test` if a pnpm/yarn lockfile is present) |
|
|
39
39
|
| `vitest.config.*` exists | vitest | `npx vitest run` |
|
|
40
40
|
| `jest.config.*` or `"jest"` in package.json | jest | `npx jest` |
|
|
41
41
|
| `*.test.ts` / `*.spec.ts` + `"mocha"` in devDeps | mocha | `npx mocha` |
|
|
@@ -57,8 +57,11 @@ Also detect the test file naming convention:
|
|
|
57
57
|
|
|
58
58
|
Run the full test suite BEFORE any changes:
|
|
59
59
|
```bash
|
|
60
|
-
<test_command> 2>&1 | tee
|
|
60
|
+
<test_command> 2>&1 | tee "$TMPDIR/ts-reviewer-baseline.log"
|
|
61
61
|
```
|
|
62
|
+
Write logs to the OS temp directory (or the project root if temp is unavailable —
|
|
63
|
+
recommend gitignoring them). Do NOT write to `.claude/` — the skill must work in
|
|
64
|
+
non-Claude environments and the directory may not exist.
|
|
62
65
|
|
|
63
66
|
Record:
|
|
64
67
|
- Total tests: N
|
|
@@ -112,6 +115,8 @@ Regression test conventions:
|
|
|
112
115
|
});
|
|
113
116
|
```
|
|
114
117
|
- Keep tests focused and minimal — test the specific fix, not the entire function
|
|
118
|
+
- Tell the user these files are candidates to rename and merge into their existing
|
|
119
|
+
suites — the `.reviewer-fixes` naming is a handoff convention, not a permanent home
|
|
115
120
|
|
|
116
121
|
### 4c. Run tsc after each file
|
|
117
122
|
|
|
@@ -119,6 +124,9 @@ Regression test conventions:
|
|
|
119
124
|
npx tsc --noEmit 2>&1
|
|
120
125
|
```
|
|
121
126
|
|
|
127
|
+
On large repos (> ~30 files in the work plan) where a full typecheck is slow, use
|
|
128
|
+
`npx tsc --noEmit --incremental` or batch the check every 3-5 files instead of every file.
|
|
129
|
+
|
|
122
130
|
If `tsc` reports NEW errors in the file we just fixed or in files affected by our changes:
|
|
123
131
|
- Analyze the errors
|
|
124
132
|
- Fix them immediately (they are likely caused by our refactoring — e.g., type changes
|
|
@@ -151,7 +159,7 @@ If linter reports errors in files we changed:
|
|
|
151
159
|
|
|
152
160
|
Run the full test suite:
|
|
153
161
|
```bash
|
|
154
|
-
<test_command> 2>&1 | tee
|
|
162
|
+
<test_command> 2>&1 | tee "$TMPDIR/ts-reviewer-postfix.log"
|
|
155
163
|
```
|
|
156
164
|
|
|
157
165
|
Compare with baseline:
|
|
@@ -340,8 +348,12 @@ When applying an `auto` architecture fix (module merge, import path cleanup, nar
|
|
|
340
348
|
Fix exactly what the report says, nothing more.
|
|
341
349
|
6. **If uncertain, skip.** If a fix is ambiguous or risky, mark it as
|
|
342
350
|
`[SKIPPED: requires manual review]` and move on. Better to skip than to break.
|
|
343
|
-
7. **
|
|
344
|
-
|
|
351
|
+
7. **Snapshot before touching, restore to revert.** Before applying the first fix to a
|
|
352
|
+
file, save its exact current content (copy to a temp/scratch directory, keyed by
|
|
353
|
+
path). To revert a fix, restore the file from that snapshot and re-apply only the
|
|
354
|
+
fixes that were verified safe. NEVER use `git checkout -- <file>` or `git restore` —
|
|
355
|
+
the user may have uncommitted changes in the same file, and those commands destroy
|
|
356
|
+
them along with the fix.
|
|
345
357
|
|
|
346
358
|
---
|
|
347
359
|
|
|
@@ -3,20 +3,30 @@
|
|
|
3
3
|
Flag outdated patterns when a modern TypeScript equivalent exists.
|
|
4
4
|
All items here are **Medium** or **Low** severity.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
**Version gate (applies to every item):** before flagging, confirm the project's TS
|
|
7
|
+
version, `target`/`lib`, and runtime support the recommended replacement. Every finding
|
|
8
|
+
must state the minimum version required. Never recommend a feature the project cannot use.
|
|
9
|
+
|
|
10
|
+
## Enums (Medium/Low — scope carefully)
|
|
11
|
+
|
|
12
|
+
Do NOT blanket-flag every `enum`. Flag these specific cases:
|
|
13
|
+
|
|
14
|
+
- **Numeric enums** (implicit or explicit numeric values). Severity: **Medium**.
|
|
15
|
+
Reverse mappings pollute the object; any number was assignable pre-TS5 and the
|
|
16
|
+
runtime object invites misuse. Fix: string enum, `as const` object, or union:
|
|
17
|
+
```typescript
|
|
18
|
+
const Direction = { Up: 'up', Down: 'down', Left: 'left', Right: 'right' } as const;
|
|
19
|
+
type Direction = (typeof Direction)[keyof typeof Direction];
|
|
20
|
+
// Or simple union: type Direction = 'up' | 'down' | 'left' | 'right';
|
|
21
|
+
```
|
|
22
|
+
- **`const enum` when the project uses `isolatedModules`** or transpiles with
|
|
23
|
+
esbuild/SWC/Babel. Severity: **Medium**. Fix: `as const` object.
|
|
24
|
+
Exception: `const enum` in library code for inlining, compiled with tsc only.
|
|
25
|
+
- **Any enum when `erasableSyntaxOnly` is set** or the project runs TS natively via
|
|
26
|
+
Node type-stripping. Severity: **Medium** — enums are non-erasable syntax.
|
|
27
|
+
- **String enums in a codebase that uses them consistently:** Severity: **Low**,
|
|
28
|
+
report once as a Recurring Pattern with the `as const` alternative — this is a
|
|
29
|
+
style preference, not a defect.
|
|
20
30
|
|
|
21
31
|
## `namespace` -> ES Modules (Medium)
|
|
22
32
|
|
|
@@ -30,13 +40,39 @@ Exception: `/// <reference types="..." />` in global `.d.ts` files.
|
|
|
30
40
|
|
|
31
41
|
## `satisfies` Operator (Medium)
|
|
32
42
|
|
|
33
|
-
Flag
|
|
43
|
+
Flag only when the widening actually matters — NOT every annotated constant:
|
|
44
|
+
- `as Type` on an object literal (silently allows excess/missing property mismatches
|
|
45
|
+
that `satisfies` would catch), or
|
|
46
|
+
- an explicit annotation where the literal types are consumed downstream
|
|
47
|
+
(keys used as a union, values used as literal types).
|
|
48
|
+
|
|
34
49
|
```typescript
|
|
35
|
-
// BEFORE: loses literal type
|
|
50
|
+
// BEFORE: loses literal type that IS used downstream
|
|
36
51
|
const config: Config = { timeout: 5000 };
|
|
37
52
|
// MODERN: validates AND keeps literal type
|
|
38
53
|
const config = { timeout: 5000 } satisfies Config;
|
|
39
54
|
```
|
|
55
|
+
An annotated constant whose literal types are never used is fine — do not flag.
|
|
56
|
+
|
|
57
|
+
## `??` and `?.` (Medium/Low)
|
|
58
|
+
|
|
59
|
+
- `x || fallback` where `x` can legitimately be `0`, `''`, or `false`.
|
|
60
|
+
Severity: **Medium** — behavior bug, not style: valid falsy values are replaced.
|
|
61
|
+
Fix: `x ?? fallback`.
|
|
62
|
+
- `x || fallback` where `x` is only ever `T | null | undefined` (falsy non-nullish
|
|
63
|
+
impossible per its type). Severity: **Low**. Fix: `??` for intent clarity.
|
|
64
|
+
- `a && a.b && a.b.c` chains. Severity: **Low**. Fix: `a?.b?.c`.
|
|
65
|
+
- `x !== null && x !== undefined ? x : y`. Severity: **Low**. Fix: `x ?? y`.
|
|
66
|
+
- `if (!x) x = y` / `obj.prop = obj.prop ?? init` self-assignment.
|
|
67
|
+
Severity: **Low**. Fix: `x ??= y` (also `||=`, `&&=` where semantics match).
|
|
68
|
+
|
|
69
|
+
## Mutation-Safe Array Methods (Medium)
|
|
70
|
+
|
|
71
|
+
- `.sort()`, `.reverse()`, `.splice()` called on a function parameter, shared array, or
|
|
72
|
+
anything not created locally in the same scope. Severity: **Medium** — mutates the
|
|
73
|
+
caller's data. Fix: `toSorted()`, `toReversed()`, `toSpliced()` (ES2023) or copy first.
|
|
74
|
+
- `JSON.parse(JSON.stringify(x))` for deep cloning. Severity: **Low** — silently drops
|
|
75
|
+
`undefined`, functions; `Date` becomes string. Fix: `structuredClone(x)`.
|
|
40
76
|
|
|
41
77
|
## Explicit Resource Management — `using` (Medium)
|
|
42
78
|
|
|
@@ -62,16 +98,16 @@ function createRoute<const T extends string>(path: T) { ... }
|
|
|
62
98
|
Flag type-only imports missing `import type` or inline `type` keyword.
|
|
63
99
|
If `verbatimModuleSyntax` is enabled, the compiler enforces this — don't double-flag.
|
|
64
100
|
|
|
65
|
-
##
|
|
101
|
+
## Redundant get/set Pairs (Low)
|
|
66
102
|
|
|
67
|
-
Flag get/set
|
|
103
|
+
Flag a get/set pair that only reads/writes a private backing field with no validation,
|
|
104
|
+
transformation, or side effect. Fix: replace with a plain public field (or a `readonly`
|
|
105
|
+
field + method if only mutation needs control). Mention the `accessor` keyword only if
|
|
106
|
+
the class already uses standard decorators that require it.
|
|
68
107
|
|
|
69
|
-
##
|
|
108
|
+
## Promise Constructor Anti-Pattern
|
|
70
109
|
|
|
71
|
-
|
|
72
|
-
The promise constructor anti-pattern adds unnecessary nesting.
|
|
73
|
-
Fix: use `async/await` directly.
|
|
74
|
-
Exception: wrapping callback-based APIs — this is the correct use of the constructor.
|
|
110
|
+
Covered in `references/async-patterns.md` (Promise Anti-Patterns) — do not duplicate here.
|
|
75
111
|
|
|
76
112
|
## `Object.keys()` / `Object.entries()` Typing (Low)
|
|
77
113
|
|
|
@@ -87,8 +123,25 @@ when `moduleResolution` is `nodenext`.
|
|
|
87
123
|
- Custom `Awaited<T>` — built-in since TS 4.5.
|
|
88
124
|
- Custom `NoInfer<T>` — built-in since TS 5.4.
|
|
89
125
|
|
|
90
|
-
##
|
|
126
|
+
## Modern Runtime APIs (Low, version-gated)
|
|
127
|
+
|
|
128
|
+
Flag the outdated idiom when the target/runtime supports the modern one:
|
|
129
|
+
|
|
130
|
+
- `arr[arr.length - 1]` -> `arr.at(-1)` (ES2022).
|
|
131
|
+
- `Object.prototype.hasOwnProperty.call(obj, k)` -> `Object.hasOwn(obj, k)` (ES2022).
|
|
132
|
+
- `str.replace(/x/g, y)` with a literal pattern -> `str.replaceAll('x', y)` (ES2021).
|
|
133
|
+
- Manual reduce-into-groups -> `Object.groupBy` / `Map.groupBy` (ES2024).
|
|
134
|
+
- Array spread + filter/map chains over large iterables -> iterator helpers
|
|
135
|
+
(`.map`, `.filter`, `.take` on iterators, ES2025).
|
|
136
|
+
- Manual set arithmetic loops -> `Set.prototype.union` / `intersection` /
|
|
137
|
+
`difference` (ES2025).
|
|
138
|
+
- Manual timeout-promise wiring -> `AbortSignal.timeout(ms)`; combining signals ->
|
|
139
|
+
`AbortSignal.any([...])` (Node 20+).
|
|
140
|
+
- Bare builtin imports (`import fs from 'fs'`) -> `node:` prefix
|
|
141
|
+
(`import fs from 'node:fs'`) in Node-targeted code.
|
|
142
|
+
- `require('./data.json')` / untyped JSON loading in ESM -> import attributes:
|
|
143
|
+
`import data from './data.json' with { type: 'json' }` (Node 20.10+/TS 5.3+).
|
|
144
|
+
|
|
145
|
+
## Suppression Directives
|
|
91
146
|
|
|
92
|
-
|
|
93
|
-
`@ts-expect-error` is preferred because it errors if the suppressed error disappears,
|
|
94
|
-
preventing stale suppressions from silently hiding future regressions.
|
|
147
|
+
Covered in `references/type-safety.md` (Suppression Directives) — do not duplicate here.
|
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
For pure TypeScript code — no framework-specific issues.
|
|
4
4
|
Focus on patterns dangerous regardless of runtime (Node.js, Deno, Bun, browser).
|
|
5
5
|
|
|
6
|
+
## Trust Boundaries — read first
|
|
7
|
+
|
|
8
|
+
Every item below assumes the dangerous value is *attacker-influenced*. Before flagging,
|
|
9
|
+
trace where the value comes from:
|
|
10
|
+
|
|
11
|
+
**Untrusted sources:** network request bodies/headers/URLs, CLI arguments, environment
|
|
12
|
+
variables in multi-tenant contexts, file contents from user-writable paths, database
|
|
13
|
+
fields that were ever written from user input, messages from queues/sockets, anything
|
|
14
|
+
returned by a third-party API.
|
|
15
|
+
|
|
16
|
+
**Trusted sources:** literals, values from this codebase's own constants/config files,
|
|
17
|
+
values already validated by a schema at the boundary (note where).
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Value provably static/internal -> downgrade the finding to **Medium** and say why it
|
|
21
|
+
still matters (fragile pattern), or drop it if the sink is safe by construction.
|
|
22
|
+
- Taint survives transformations: concatenation, template literals, `JSON.parse`,
|
|
23
|
+
property access on a parsed object all preserve untrustedness.
|
|
24
|
+
- If you cannot trace the source within the files available, report at the listed
|
|
25
|
+
severity but state the assumption: "assumes `x` can carry external input".
|
|
26
|
+
|
|
6
27
|
## Injection
|
|
7
28
|
|
|
8
29
|
- **`eval()` and `new Function()`** — executing dynamic strings.
|
|
@@ -16,6 +37,21 @@ Focus on patterns dangerous regardless of runtime (Node.js, Deno, Bun, browser).
|
|
|
16
37
|
- **RegExp from user input** — `new RegExp(userInput)`.
|
|
17
38
|
Severity: **High** (ReDoS + injection). Fix: escape input or use static regex.
|
|
18
39
|
|
|
40
|
+
## SSRF
|
|
41
|
+
|
|
42
|
+
- `fetch()` / `http.request()` / HTTP client call with a URL built from external input,
|
|
43
|
+
without a protocol + host allowlist. Severity: **High**. Fix: parse with `new URL()`,
|
|
44
|
+
check protocol is http(s) and host against an explicit allowlist; reject redirects to
|
|
45
|
+
internal ranges.
|
|
46
|
+
|
|
47
|
+
## DOM Sinks (browser code)
|
|
48
|
+
|
|
49
|
+
- `element.innerHTML = x`, `insertAdjacentHTML`, `document.write` where `x` has any
|
|
50
|
+
non-literal part. Severity: **High**. Fix: `textContent` for text; a sanitizer
|
|
51
|
+
(DOMPurify) only when HTML is genuinely required.
|
|
52
|
+
- `location.href = x` / `window.open(x)` from external input — `javascript:` URL risk.
|
|
53
|
+
Severity: **Medium**. Fix: validate protocol via `new URL()`.
|
|
54
|
+
|
|
19
55
|
## Prototype Pollution
|
|
20
56
|
|
|
21
57
|
- `Object.assign(target, untrustedSource)` where source may contain `__proto__`.
|
|
@@ -55,8 +91,15 @@ Focus on patterns dangerous regardless of runtime (Node.js, Deno, Bun, browser).
|
|
|
55
91
|
|
|
56
92
|
## Timing Attacks
|
|
57
93
|
|
|
58
|
-
- String comparison
|
|
59
|
-
Fix: `crypto.timingSafeEqual()`.
|
|
94
|
+
- String comparison using `===` where both sides are secrets/tokens/MACs/password hashes
|
|
95
|
+
being verified. Severity: **High**. Fix: `crypto.timingSafeEqual()`.
|
|
96
|
+
Do NOT flag ordinary string comparisons that merely involve variables named "token" —
|
|
97
|
+
only comparisons whose result reveals secret equality to an attacker.
|
|
98
|
+
|
|
99
|
+
## Unsafe Memory
|
|
100
|
+
|
|
101
|
+
- `Buffer.allocUnsafe()` where the buffer is not immediately and fully overwritten —
|
|
102
|
+
leaks previous memory contents. Severity: **High**. Fix: `Buffer.alloc()`.
|
|
60
103
|
|
|
61
104
|
## Denial of Service
|
|
62
105
|
|
|
@@ -22,13 +22,43 @@ If `strict: true` set but a sub-flag explicitly OFF — flag as that flag's seve
|
|
|
22
22
|
| Flag | Recommended | Severity | Why |
|
|
23
23
|
|---|---|---|---|
|
|
24
24
|
| `noUncheckedIndexedAccess` | `true` | **Medium** | obj[key] returns T\|undefined instead of T |
|
|
25
|
-
| `exactOptionalPropertyTypes` | `true` | **Medium** | Prevents `{ key: undefined }` from satisfying optional `{ key?: string }` |
|
|
26
25
|
| `noFallthroughCasesInSwitch` | `true` | **Medium** | Prevents accidental fall-through |
|
|
27
26
|
| `noImplicitReturns` | `true` | **Medium** | Not all code paths return a value |
|
|
28
27
|
| `noImplicitOverride` | `true` | **Low** | Requires override keyword |
|
|
29
|
-
| `noPropertyAccessFromIndexSignature` | `true` | **Low** | Forces bracket notation |
|
|
30
28
|
| `allowUnreachableCode` | `false` | **Medium** | Should not be true |
|
|
31
29
|
|
|
30
|
+
## Target and Lib
|
|
31
|
+
|
|
32
|
+
| Check | Severity | Why |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `target` two+ ES versions below minimum runtime in `engines`/docs | **Medium** | Pointless downleveling: slower output, loses native syntax |
|
|
35
|
+
| `lib` includes `dom` in a Node-only project (or vice versa) | **Low** | Wrong globals available at compile time |
|
|
36
|
+
| No `target` set (defaults to ES5 pre-5.0 configs) | **Medium** | Almost never intended in 2026 |
|
|
37
|
+
|
|
38
|
+
## Legacy Decorators
|
|
39
|
+
|
|
40
|
+
- `experimentalDecorators: true` (with or without `emitDecoratorMetadata`) and no
|
|
41
|
+
dependency that requires legacy decorators. Severity: **Medium**.
|
|
42
|
+
Fix: migrate to standard (TC39) decorators, remove both flags.
|
|
43
|
+
|
|
44
|
+
## Project Structure (monorepos / large codebases)
|
|
45
|
+
|
|
46
|
+
- Multiple packages with independent tsconfigs but no `references` / `composite` —
|
|
47
|
+
cross-package type changes silently skip rechecking dependents. Severity: **Low**;
|
|
48
|
+
suggest project references only when packages actually import each other's source.
|
|
49
|
+
- Build config drift: `tsconfig.build.json` excludes files that `tsconfig.json`
|
|
50
|
+
typechecks (or vice versa) so `tsc --noEmit` and the build see different code.
|
|
51
|
+
Severity: **Medium**.
|
|
52
|
+
|
|
53
|
+
## Explicit Non-Findings
|
|
54
|
+
|
|
55
|
+
Do NOT flag:
|
|
56
|
+
- `skipLibCheck: true` — standard practice; checking node_modules types is rarely useful.
|
|
57
|
+
- Missing `exactOptionalPropertyTypes` / `noPropertyAccessFromIndexSignature` in an
|
|
58
|
+
existing codebase — these are opt-in strictness with real migration cost.
|
|
59
|
+
Mention once as **Low** suggestions only for greenfield projects (few source files,
|
|
60
|
+
young git history).
|
|
61
|
+
|
|
32
62
|
## Module System
|
|
33
63
|
|
|
34
64
|
| Setting | Recommendation | Severity |
|
|
@@ -37,6 +67,7 @@ If `strict: true` set but a sub-flag explicitly OFF — flag as that flag's seve
|
|
|
37
67
|
| `moduleResolution` | `"nodenext"` / `"bundler"` | **Medium** (avoid legacy `"node"` / `"node10"`) |
|
|
38
68
|
| `verbatimModuleSyntax` | `true` | **Medium** (enforces `import type`, removes unused imports) |
|
|
39
69
|
| `isolatedModules` | `true` | **Medium** (required by esbuild, SWC, Babel, TS transpile mode) |
|
|
70
|
+
| `erasableSyntaxOnly` | `true` if the project runs TS via Node type-stripping | **Medium** when type-stripping is used without it (enums/namespaces/parameter properties would fail at runtime) |
|
|
40
71
|
|
|
41
72
|
## Path Configuration
|
|
42
73
|
|
|
@@ -57,3 +88,5 @@ Flag if present:
|
|
|
57
88
|
- No linter configured at all: **Medium** (recommend adding one).
|
|
58
89
|
- TypeScript parser not configured in ESLint: **Medium**.
|
|
59
90
|
- Type-aware rules not enabled (e.g., `no-floating-promises`): **Medium**.
|
|
91
|
+
For ESLint, actionable check: config extends `typescript-eslint` `recommendedTypeChecked`
|
|
92
|
+
(or `strictTypeChecked`) and sets `parserOptions.projectService` (or `project`).
|
|
@@ -27,12 +27,42 @@
|
|
|
27
27
|
- `as Type` that narrows a wider type without validation.
|
|
28
28
|
Severity: **High** — a runtime mismatch is a bug waiting to happen.
|
|
29
29
|
Fix: use a type guard, `satisfies`, or a validation function (e.g., Zod, io-ts, hand-written).
|
|
30
|
-
- `as unknown as Type` — double cast is almost always a red flag.
|
|
30
|
+
- `as unknown as Type` or `as any as Type` — double cast is almost always a red flag.
|
|
31
31
|
Severity: **High**.
|
|
32
32
|
- `<Type>value` (angle-bracket cast) — same issue as `as`, plus it conflicts with JSX.
|
|
33
33
|
Severity: **Medium** (prefer `as` syntax, but still flag the underlying safety issue).
|
|
34
34
|
- `as const` used correctly is NOT an issue — do not flag it.
|
|
35
35
|
|
|
36
|
+
## `unknown` Discipline
|
|
37
|
+
|
|
38
|
+
- `unknown` narrowed with `as` instead of a runtime check. Severity: **High** — this is
|
|
39
|
+
`any` with extra steps. Fix: `typeof` / `instanceof` / `in` guards, or schema validation.
|
|
40
|
+
- `catch (e)` handled via `(e as Error).message`. Severity: **Medium**.
|
|
41
|
+
Fix: `e instanceof Error ? e.message : String(e)`.
|
|
42
|
+
- Public API returning `unknown` where a generic or a discriminated result type is
|
|
43
|
+
derivable — forces every caller to cast. Severity: **Medium**.
|
|
44
|
+
|
|
45
|
+
## Structural Typing Traps
|
|
46
|
+
|
|
47
|
+
- `{}` as a type annotation — means "any non-nullish value", not "empty object" or
|
|
48
|
+
"plain object". Severity: **Medium**. Fix: `Record<string, unknown>`, `object`, or a
|
|
49
|
+
concrete shape.
|
|
50
|
+
- Boxed primitive types `String`, `Number`, `Boolean`, `Object` in annotations.
|
|
51
|
+
Severity: **Medium**. Fix: lowercase primitives.
|
|
52
|
+
- Method shorthand in interfaces intended as strict callbacks:
|
|
53
|
+
`interface H { handle(e: E): void }` is bivariant even under `strictFunctionTypes`;
|
|
54
|
+
property syntax `handle: (e: E) => void` is checked contravariantly.
|
|
55
|
+
Severity: **Low** internal, **Medium** on public APIs where wrong-argument
|
|
56
|
+
implementations would compile.
|
|
57
|
+
|
|
58
|
+
## Branded Types (suggestion-level)
|
|
59
|
+
|
|
60
|
+
- Multiple domain identifiers sharing one primitive type (`userId: string`,
|
|
61
|
+
`orderId: string`) passed across module boundaries — mix-ups compile silently.
|
|
62
|
+
Severity: **Low** (suggest, don't insist). Fix: brand the types:
|
|
63
|
+
`type UserId = string & { readonly __brand: 'UserId' }` plus a constructor function.
|
|
64
|
+
Only flag when the codebase shows 3+ same-primitive IDs crossing function boundaries.
|
|
65
|
+
|
|
36
66
|
## Non-null Assertions (`!`)
|
|
37
67
|
|
|
38
68
|
- `value!` where `value` could genuinely be `null | undefined` at runtime.
|
|
@@ -69,6 +99,10 @@
|
|
|
69
99
|
- Union types that should be discriminated but aren't (no shared literal field).
|
|
70
100
|
Severity: **Medium**.
|
|
71
101
|
- Discriminant field is `string` instead of a literal type. Severity: **Medium**.
|
|
102
|
+
- Boolean flag soup modeling mutually exclusive states
|
|
103
|
+
(`{ loading: boolean; error?: E; data?: T }` allows impossible combinations).
|
|
104
|
+
Severity: **Medium**. Fix: discriminated union —
|
|
105
|
+
`{ status: 'loading' } | { status: 'error'; error: E } | { status: 'ready'; data: T }`.
|
|
72
106
|
|
|
73
107
|
## Index Signatures
|
|
74
108
|
|
|
@@ -99,3 +133,16 @@
|
|
|
99
133
|
`Parameters`, `Awaited`, `NoInfer`). Severity: **Low**.
|
|
100
134
|
- `Omit` with a key that doesn't exist in the source type — TS silently allows this,
|
|
101
135
|
which may indicate a typo or stale code. Severity: **Low**.
|
|
136
|
+
|
|
137
|
+
## Readonly Posture
|
|
138
|
+
|
|
139
|
+
- Mutable arrays/objects in exported signatures where the function never mutates them —
|
|
140
|
+
`readonly T[]` / `Readonly<T>` communicates the contract and accepts more inputs.
|
|
141
|
+
Severity: **Low**. Public APIs only; don't flag internals.
|
|
142
|
+
|
|
143
|
+
## Type-level Complexity
|
|
144
|
+
|
|
145
|
+
- Conditional type > 2 levels deep, or mapped type with nested `infer`, used in only one
|
|
146
|
+
place. Severity: **Low**. Fix: inline or simplify to a union/overloads unless the
|
|
147
|
+
type-level machinery removes real duplication. Clever types are a maintenance cost —
|
|
148
|
+
the next reader must decode them.
|