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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-reviewer",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Install the TypeScript Code Reviewer skill for Claude Code, Codex, or Antigravity",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 7 domains (default 6 + Architecture) |
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 `.ts` files:
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
- Compiler errors -> **Highest**. Warnings -> **High**.
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: `error` -> **High**, `warning` -> **Medium**, `info` -> **Low**.
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
- | Architecture | `references/architecture.md` | Shallow modules, scattered concepts, coupling, dependency seams, testability |
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": "URL or docs ref"
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 critical, Y high, Z medium, W low)
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.** `// @ts-expect-error` with explanation = Low.
386
- `// @ts-ignore` (legacy) = Medium. Without explanation = Medium.
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
- - `catch(e)` without typing as `unknown` (requires `useUnknownInCatchVariables`).
17
- Severity: **Medium**.
18
- - Empty catch blocks: `catch(e) {}`. Severity: **High** (unless commented).
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(...))` if order doesn't matter.
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
- - Throwing raw strings. Severity: **Medium**. Fix: `throw new Error(...)`.
31
- - Custom errors not extending `Error`. Severity: **Medium**.
32
- - Error messages without context. Severity: **Low**.
33
- - Missing `cause` chaining (ES2022):
34
- `catch(e) { throw new Error("msg", { cause: e }); }`. Severity: **Low**.
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
- - Barrel files causing circular dependencies. Severity: **Low** to **Medium**.
67
- - Circular imports. Severity: **High**.
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 .claude/ts-reviewer-baseline.log
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 .claude/ts-reviewer-postfix.log
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. **Revert individual fixes with `git checkout -- <file>`** before the fix was applied,
344
- then re-apply only the safe fixes. Use `git diff` to identify what changed in a file.
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
- ## Enums -> const Objects or Union Types (Medium)
7
-
8
- ```typescript
9
- // OUTDATED
10
- enum Direction { Up, Down, Left, Right }
11
- // MODERN
12
- const Direction = { Up: 'up', Down: 'down', Left: 'left', Right: 'right' } as const;
13
- type Direction = (typeof Direction)[keyof typeof Direction];
14
- // Or simple union: type Direction = 'up' | 'down' | 'left' | 'right';
15
- ```
16
- Exception: `const enum` in library code for inlining.
17
- Note: `const enum` is incompatible with `isolatedModules` (used by esbuild, SWC, Babel).
18
- If the project uses a transpiler with `isolatedModules`, flag `const enum` as **Medium**
19
- and recommend `as const` objects instead.
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 `as Type` or explicit type annotation where `satisfies` preserves literal types:
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
- ## `accessor` Keyword (Low)
101
+ ## Redundant get/set Pairs (Low)
66
102
 
67
- Flag get/set pairs that simply read/write a backing field — use `accessor` instead.
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
- ## `Promise` Constructor -> async/await (Low)
108
+ ## Promise Constructor Anti-Pattern
70
109
 
71
- Flag `new Promise()` wrapping an already-async function or another promise.
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
- ## `@ts-expect-error` vs `@ts-ignore` (Low)
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
- Flag `// @ts-ignore` without explanation — recommend `// @ts-expect-error` instead.
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 for secrets using `===`. Severity: **High**.
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.