ts-reviewer 1.0.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/README.md CHANGED
@@ -14,16 +14,17 @@ Three modes, one skill:
14
14
  | **fix** | Reads the report and applies fixes file-by-file with tsc/lint/test verification |
15
15
  | **auto** | Runs scan, asks you to confirm, fixes everything, deletes the report if clean |
16
16
 
17
- The review covers six domains, each with its own detailed checklist:
18
-
19
- | Domain | Examples |
20
- |---|---|
21
- | **Type Safety** | `any` abuse, unsafe casts, non-null assertions, missing exhaustive checks |
22
- | **Security** | Injection, prototype pollution, ReDoS, path traversal, hardcoded secrets |
23
- | **Async Patterns** | Floating promises, race conditions, missing error propagation, `forEach(async...)` |
24
- | **Modernization** | `enum` → `as const`, missing `satisfies`, `using` keyword, `import type` |
25
- | **Code Quality** | Dead code, complexity, duplication, hacky patterns, error handling |
26
- | **Config** | tsconfig.json strict flags, module resolution, deprecated options |
17
+ The review covers six domains by default, each with its own detailed checklist. Add `--arch` or `--full` to include architecture analysis:
18
+
19
+ | Domain | Examples | Default |
20
+ |---|---|---|
21
+ | **Type Safety** | `any` abuse, unsafe casts, non-null assertions, missing exhaustive checks | ✓ |
22
+ | **Security** | Injection, prototype pollution, ReDoS, path traversal, hardcoded secrets | ✓ |
23
+ | **Async Patterns** | Floating promises, race conditions, missing error propagation, `forEach(async...)` | ✓ |
24
+ | **Modernization** | `enum` → `as const`, missing `satisfies`, `using` keyword, `import type` | ✓ |
25
+ | **Code Quality** | Dead code, complexity, duplication, hacky patterns, error handling | ✓ |
26
+ | **Config** | tsconfig.json strict flags, module resolution, deprecated options | ✓ |
27
+ | **Architecture** | Shallow modules, scattered concepts, tight coupling, dependency seams, testability | `--arch` / `--full` |
27
28
 
28
29
  ## Installation
29
30
 
@@ -75,7 +76,29 @@ Find issues in this project
75
76
  Audit the codebase for security and type safety problems
76
77
  ```
77
78
 
78
- Claude will analyze the project and write a report to `code-smells.md` in the project root (or `.claude/code-smells.md` if that directory exists).
79
+ Claude will analyze the project and write a report to `code-smells.md` in the project root.
80
+
81
+ #### Domain flags
82
+
83
+ By default, only the six core domains run. Use flags to control which domains are active:
84
+
85
+ | Flag | What runs |
86
+ |---|---|
87
+ | *(none)* | Type Safety, Security, Async, Modernization, Code Quality, Config |
88
+ | `--arch` | Architecture only (shallow modules, coupling, dependency seams) |
89
+ | `--full` | All seven domains |
90
+
91
+ Examples:
92
+
93
+ ```
94
+ Review my TypeScript code --arch
95
+ ```
96
+ ```
97
+ Full audit --full
98
+ ```
99
+ ```
100
+ Review architecture of this project
101
+ ```
79
102
 
80
103
  ### Fix — apply fixes from the report
81
104
 
@@ -136,6 +159,14 @@ Issues on unchanged lines are listed separately as pre-existing tech debt — in
136
159
  | **Medium** | Tech debt — clean up when you're already editing that file |
137
160
  | **Low** | Style and conventions — improve when convenient |
138
161
 
162
+ Architecture findings use the same scale. Each candidate also carries a **Fixability** tag:
163
+
164
+ | Fixability | Meaning |
165
+ |---|---|
166
+ | `auto` | Applied automatically during fix mode |
167
+ | `needs-confirm` | Shown to you first — only applied after explicit approval |
168
+ | `report-only` | Left as documentation — never auto-applied |
169
+
139
170
  ## Project Structure
140
171
 
141
172
  ```
@@ -153,21 +184,22 @@ ts-reviewer/
153
184
  ├── modernization.md # Checklist: TS 5.9+ idioms, satisfies, using, as const
154
185
  ├── code-quality.md # Checklist: complexity, dead code, naming, duplication
155
186
  ├── tsconfig.md # Checklist: strict flags, module resolution, deprecated
187
+ ├── architecture.md # Checklist: shallow modules, coupling, seams, deepening
156
188
  └── fix-workflow.md # Complete fix protocol: tests, verification, rollback
157
189
  ```
158
190
 
159
- **SKILL.md** (349 lines) is the orchestrator — it routes between scan/fix/auto modes, defines scope detection, severity scale, and report format. It stays under the 500-line recommended limit for Claude skills.
191
+ **SKILL.md** is the orchestrator — it routes between scan/fix/auto modes, detects domain flags (`--arch`, `--full`), defines scope detection, severity scale, and report format.
160
192
 
161
- **Reference files** contain the detailed checklists and protocols. Each analysis agent reads only the reference file relevant to its domain, keeping context focused. The fix workflow is in its own reference file because it's a complex multi-step protocol.
193
+ **Reference files** contain the detailed checklists and protocols. Each analysis agent reads only the reference file relevant to its domain, keeping context focused. Architecture analysis is opt-in and loaded only when the domain is active.
162
194
 
163
195
  ## How It Works Under the Hood
164
196
 
165
197
  ### Scan mode
166
198
 
167
- 1. **Discovery** — maps the project, reads tsconfig.json, detects linter and test runner
199
+ 1. **Discovery** — detects domain flags, maps the project, reads tsconfig.json, detects linter and test runner. If architecture is active, also maps module relationships and checks for `docs/adr/`.
168
200
  2. **Diagnostics** — runs `tsc --noEmit`, linter, and LSP diagnostics (if available)
169
- 3. **Analysis** — six specialized passes (sub-agents in Claude Code, sequential in Claude.ai), each with its own checklist
170
- 4. **Report** — deduplicates, applies severity boost (scoped modes), consolidates recurring patterns, writes `code-smells.md`
201
+ 3. **Analysis** — specialized passes for each active domain (sub-agents in Claude Code, sequential in Claude.ai), each with its own checklist
202
+ 4. **Report** — deduplicates, applies severity boost (scoped modes), consolidates recurring patterns, writes `code-smells.md`. Architecture findings appear in a separate `## Architecture Opportunities` section at the end.
171
203
 
172
204
  ### Fix mode
173
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-reviewer",
3
- "version": "1.0.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": {
@@ -1,18 +1,12 @@
1
1
  ---
2
2
  name: ts-reviewer
3
3
  description: >
4
- Deep TypeScript code review and auto-fix tool. Three modes: scan (find issues),
5
- fix (apply fixes from scan report), auto (scan + fix + verify in one pass).
6
- Supports scoped review: full codebase, uncommitted changes, branch diff (PR review),
7
- or last N commits. Outputs a prioritized report and can auto-fix issues with
8
- regression tests and verification.
9
- Use this skill whenever the user asks to "review", "audit", "check", "lint", "find issues",
10
- "find problems", "find bugs", "fix issues", "fix code smells", "auto-fix",
11
- "review and fix", "clean up code", or "review code quality" in a TypeScript project.
12
- Also trigger when the user mentions "tech debt", "code health", "refactor candidates",
13
- "security audit", "modernize", "review my changes", "review my PR", "check what I changed",
14
- "review last commit", or "review uncommitted" in a TypeScript context. Works with pure
15
- TypeScript 5.9+ codebases — no framework-specific checks (React, Vue, Angular, etc.).
4
+ TypeScript code review and auto-fix. Modes: scan, fix, auto. Scopes: full codebase,
5
+ uncommitted, branch diff, last N commits. Trigger on: review, audit, check, lint,
6
+ find issues, find bugs, fix issues, fix code smells, auto-fix, review and fix,
7
+ clean up code, tech debt, code health, security audit, modernize, review my changes,
8
+ review my PR, review last commit. Architecture review: --arch, --full, review architecture,
9
+ find refactoring opportunities, full audit. Pure TypeScript 5.9+ only.
16
10
  ---
17
11
 
18
12
  # TypeScript Code Reviewer
@@ -33,6 +27,22 @@ This skill operates in three modes. Detect the mode from the user's request:
33
27
  **`fix`** — Read `code-smells.md` and apply fixes with verification.
34
28
  **`auto`** — Run scan, then fix, then re-scan to verify. Delete report if clean.
35
29
 
30
+ ## Domain Set
31
+
32
+ Controls which review domains are active. Detected from flags first, then natural-language phrases.
33
+
34
+ | Flag / phrase | Active domains |
35
+ |---|---|
36
+ | (none / default) | Type Safety, Security, Async Patterns, Modernization, Code Quality, Config, Boundary Validation, Error Handling, Dependency Hygiene |
37
+ | `--arch` / "review architecture" / "find refactoring opportunities" / "deepening review" | Architecture only |
38
+ | `--full` / "full audit" / "full review" / "review everything" | All 10 domains (default 9 + Architecture) |
39
+
40
+ Parse order: check for explicit `--arch` / `--full` / `--no-arch` flags in the user's message first.
41
+ Fall back to phrase detection. If no flag or phrase matches → default domain set (no architecture).
42
+
43
+ Architecture domain is **off** in default scans. It loads `references/architecture.md` and adds
44
+ an `## Architecture Opportunities` section to `code-smells.md` only when active.
45
+
36
46
  ## Report File Location
37
47
 
38
48
  - Always write to `code-smells.md` in the project root (not in `.claude/`)
@@ -94,30 +104,34 @@ If not specified, default to **full codebase**.
94
104
 
95
105
  ### Git commands per scope
96
106
 
97
- **`full`** — all `.ts` files:
107
+ **`full`** — all TypeScript files:
98
108
  ```bash
99
- npx glob '**/*.ts' --ignore '**/node_modules/**'
100
- # or: git ls-files '*.ts'
109
+ npx glob '**/*.{ts,mts,cts}' --ignore '**/node_modules/**'
110
+ # or: git ls-files '*.ts' '*.mts' '*.cts'
101
111
  ```
102
112
 
103
113
  **`uncommitted`** — staged + unstaged + untracked:
104
114
  ```bash
105
- git diff --name-only HEAD -- '*.ts'
106
- 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'
107
117
  ```
108
118
 
109
119
  **`branch`** — current branch vs base:
110
120
  ```bash
111
121
  BASE=$(git rev-parse --verify main 2>/dev/null && echo main || echo master)
112
- git diff --name-only "$BASE"...HEAD -- '*.ts'
113
- 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'
114
124
  ```
115
125
 
116
126
  **`commits:N`** — last N commits:
117
127
  ```bash
118
- git diff --name-only HEAD~N..HEAD -- '*.ts'
128
+ git diff --name-only HEAD~N..HEAD -- '*.ts' '*.mts' '*.cts'
119
129
  ```
120
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
+
121
135
  ### Context files (scoped reviews)
122
136
 
123
137
  Analysis scope = diff file list. Reading scope = wider. Always include as read-only:
@@ -133,7 +147,7 @@ Context files are NOT analyzed for issues.
133
147
 
134
148
  Collect changed hunks:
135
149
  ```bash
136
- git diff -U0 [appropriate range] -- '*.ts' | grep '^@@'
150
+ git diff -U0 [appropriate range] -- '*.ts' '*.mts' '*.cts' | grep '^@@'
137
151
  ```
138
152
 
139
153
  - Issue on **new/modified line**: boost severity +1 (Low→Medium, Medium→High, High→Highest)
@@ -145,21 +159,31 @@ git diff -U0 [appropriate range] -- '*.ts' | grep '^@@'
145
159
 
146
160
  ## Phase 1 — Discovery (scan mode)
147
161
 
148
- 1. **Detect scope mode** from user's request. Build file list.
162
+ 1. **Detect domain set** from flags and phrases (see Domain Set section above).
163
+
164
+ 2. **Detect scope mode** from user's request. Build file list.
149
165
  If scoped mode yields 0 files, ask whether to fall back to full.
150
166
 
151
- 2. **Map project tree** — full structure regardless of scope.
167
+ 3. **Map project tree** — full structure regardless of scope.
152
168
 
153
- 3. **Read `tsconfig.json`**. Load `references/tsconfig.md` and audit config flags.
169
+ 4. **Read `tsconfig.json`**. Load `references/tsconfig.md` and audit config flags.
154
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.
155
174
 
156
- 4. **Check for linter configs** — `eslint.config.*`, `.eslintrc.*`, `biome.json`, `deno.json`.
175
+ 5. **Check for linter configs** — `eslint.config.*`, `.eslintrc.*`, `biome.json`, `deno.json`.
157
176
 
158
- 5. **Read `package.json`** — TS version, dependencies, module type.
177
+ 6. **Read `package.json`** — TS version, dependencies, module type.
159
178
 
160
- 6. **Identify entry points** — `index.ts`, `main.ts`, exports in `package.json`.
179
+ 7. **Identify entry points** — `index.ts`, `main.ts`, exports in `package.json`.
161
180
 
162
- 7. **Collect context files** (scoped modes only).
181
+ 8. **Collect context files** (scoped modes only).
182
+
183
+ 9. **Architecture discovery** (only if Architecture is in active domain set):
184
+ - Map module relationships: note circular imports, deep relative imports, barrel-file cycles, feature slices, public entry points.
185
+ - Check if `docs/adr/` exists — if so, scan it for architectural decisions before proposing changes. Skip silently if absent.
186
+ - Do NOT require `CONTEXT.md` or any domain-doc files.
163
187
 
164
188
  Discovery summary:
165
189
  ```
@@ -183,7 +207,11 @@ Files in scope: <N> .ts files (+ <M> context files)
183
207
  npx tsc --noEmit 2>&1 | head -200
184
208
  ```
185
209
 
186
- 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
+
187
215
  In scoped modes: run on full project, report only errors in scoped files.
188
216
 
189
217
  ### 2b. Linter
@@ -191,7 +219,11 @@ In scoped modes: run on full project, report only errors in scoped files.
191
219
  ESLint: `npx eslint [files] --format json 2>/dev/null | head -500`
192
220
  Biome: `npx biome check [files] --reporter json 2>/dev/null | head -500`
193
221
 
194
- 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**
195
227
 
196
228
  ### 2c. LSP diagnostics (if available)
197
229
 
@@ -203,14 +235,20 @@ Query TypeScript LSP via MCP if accessible. Merge with compiler output, deduplic
203
235
 
204
236
  Read the corresponding reference file before each analysis pass.
205
237
 
238
+ Run only the agents whose domain is in the active domain set (see Domain Set section).
239
+
206
240
  | Agent | Reference file | Focus |
207
241
  |---|---|---|
208
242
  | Type Safety | `references/type-safety.md` | `any`, casts, `!`, exhaustiveness, generics |
209
243
  | Security | `references/security.md` | Injection, prototype pollution, ReDoS, path traversal |
210
244
  | Async Patterns | `references/async-patterns.md` | Floating promises, race conditions, error propagation |
211
245
  | Modernization | `references/modernization.md` | Outdated patterns vs TS 5.9+ idioms |
212
- | Code Quality | `references/code-quality.md` | Complexity, duplication, naming, dead code |
246
+ | Code Quality | `references/code-quality.md` | Complexity, duplication, naming, dead code, testability |
213
247
  | Config | `references/tsconfig.md` | tsconfig.json flags and module setup |
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 |
214
252
 
215
253
  ### Sub-agent template (Claude Code)
216
254
 
@@ -233,7 +271,7 @@ Output JSONL, one object per line:
233
271
  "fix": "Concrete recommendation with code example",
234
272
  "auto_fixable": true|false,
235
273
  "in_diff": true|false,
236
- "reference": "URL or docs ref"
274
+ "reference": "optional — omit unless allowed by the Evidence Protocol"
237
275
  }
238
276
  ```
239
277
 
@@ -254,8 +292,11 @@ Go through each domain one at a time. Same JSON structure.
254
292
 
255
293
  1. Apply severity boost (scoped modes only): `in_diff: true` -> boost +1 level.
256
294
  2. Deduplicate: same file + line + issue -> keep one.
257
- 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.
258
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.
259
300
 
260
301
  ### Report structure
261
302
 
@@ -269,7 +310,7 @@ Write to `code-smells.md`:
269
310
  **TypeScript version:** <version>
270
311
  **Scope:** Full / Uncommitted / Branch `x` vs `y` / Last N commits
271
312
  **Files analyzed:** N (+ M context)
272
- **Total issues:** N (X critical, Y high, Z medium, W low)
313
+ **Total issues:** N (X highest, Y high, Z medium, W low)
273
314
  **Severity-boosted:** N (scoped modes only)
274
315
 
275
316
  ## Summary
@@ -297,6 +338,23 @@ Write to `code-smells.md`:
297
338
  ## Recurring Patterns
298
339
  ## Config Issues
299
340
  ## Pre-existing Issues (scoped modes only)
341
+
342
+ ## Architecture Opportunities
343
+ (only when Architecture is in active domain set AND at least one candidate found — omit entirely otherwise)
344
+
345
+ ### TITLE — Severity
346
+
347
+ - **Files:** relative/path/a.ts, relative/path/b.ts
348
+ - **Problem:** why this causes friction now
349
+ - **Proposed deepening:** what would change
350
+ - **Interface shape:** rough sketch of new interface
351
+ - **Dependency category:** in-process | local-substitutable | remote-owned | true-external
352
+ - **Test strategy:** how tests improve
353
+ - **Benefits:** locality, leverage, test impact
354
+ - **Trade-offs:** what gets harder
355
+ - **Fixability:** auto | needs-confirm | report-only
356
+
357
+ ---
300
358
  ````
301
359
 
302
360
  ### Sorting
@@ -337,13 +395,43 @@ Key principles:
337
395
 
338
396
  In scoped modes: these are base severities before diff-aware boost.
339
397
 
398
+ Architecture findings use the same severity scale with domain-specific criteria.
399
+ See `references/architecture.md` → Severity Mapping for Architecture.
400
+
401
+ ---
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
+
340
428
  ---
341
429
 
342
430
  ## Important Guidelines
343
431
 
344
432
  - **No framework checks.** Pure TypeScript only.
345
- - **No false positives from intentional patterns.** `// @ts-expect-error` with explanation = Low.
346
- `// @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.
347
435
  - **Respect project conventions.** Don't flag consistent patterns unless harmful.
348
436
  - **Be concrete.** Every issue needs a snippet and a fix recommendation.
349
437
  "Consider refactoring" is not a valid fix.
@@ -0,0 +1,158 @@
1
+ # Architecture Review Checklist
2
+
3
+ Loaded only when architecture review is active (`--arch` or `--full` flag).
4
+
5
+ ## Glossary
6
+
7
+ Use these terms in all architecture findings. Don't substitute "component", "service", "API", "boundary".
8
+
9
+ - **Module** — anything with an interface and an implementation: function, class, package, feature slice.
10
+ - **Interface** — everything a caller must know: types, invariants, error modes, ordering, config. Not just the TypeScript `interface` keyword.
11
+ - **Implementation** — the code inside the module.
12
+ - **Depth** — leverage at the interface. Deep = large behaviour behind small interface. Shallow = interface nearly as complex as implementation.
13
+ - **Seam** — where the interface lives; a place behaviour can be altered without editing in place. Prefer over "boundary".
14
+ - **Adapter** — a concrete thing satisfying an interface at a seam.
15
+ - **Leverage** — what callers get from depth: more capability per unit of interface they must learn.
16
+ - **Locality** — what maintainers get from depth: change, bugs, and knowledge concentrated in one place.
17
+
18
+ ## Principles
19
+
20
+ - **Depth is a property of the interface, not the implementation.** A deep module can be internally complex — what matters is how small its interface is relative to what it hides.
21
+ - **Deletion test.** Imagine deleting the module. If complexity vanishes → it was a pass-through. If complexity reappears across callers → it was earning its keep.
22
+ - **The interface is the test surface.** Callers and tests cross the same seam. If tests must reach past the interface into internals, the module is the wrong shape.
23
+ - **One adapter = hypothetical seam. Two adapters = real seam.** Don't introduce a port unless at least two adapters are justified (production + test at minimum). Single-adapter seams are just indirection.
24
+
25
+ ## Smell Checklist
26
+
27
+ ### Shallow Modules
28
+
29
+ - Understanding one concept requires bouncing across many tiny modules. Severity: **Medium**.
30
+ - Module interface is nearly as complex as its implementation (shallow). Severity: **Medium**.
31
+ - Pass-through wrapper / manager / helper / service that adds no hidden complexity. Severity: **Medium**. Apply deletion test.
32
+ - Pure functions extracted only for testability, but real bugs hide in orchestration — no locality. Severity: **Medium**.
33
+
34
+ ### Coupling and Boundaries
35
+
36
+ - Tight coupling leaks across module interfaces (callers must know implementation details). Severity: **High**.
37
+ - Tests must reach into module internals or mock too many neighbors to test one thing. Severity: **High**.
38
+ - Public API exports implementation details, config, ordering constraints, or internal error modes that callers should not need to know. Severity: **Medium**.
39
+
40
+ ### Import and Module Structure
41
+
42
+ *Note: circular imports and deep relative imports are also flagged by the Code Quality domain in all scan modes. Architecture domain provides deeper refactoring guidance when Code Quality flags them.*
43
+
44
+ - Barrel-file cycles or circular import chains that can't be resolved by reordering. Severity: **High**.
45
+ - Deep relative imports (`../../../`) indicating modules not co-located with what they depend on. Severity: **Low**. Suggest path aliases or co-location.
46
+
47
+ ### Locality
48
+
49
+ - Shared utility module mixes unrelated domain concepts (a grab-bag util). Severity: **Medium**.
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
+
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
+
92
+ ## Dependency Classification
93
+
94
+ When proposing a deepening fix, classify the dependency to determine test strategy:
95
+
96
+ **1. In-process** — pure computation or in-memory state, no I/O.
97
+ - Strategy: merge modules, test through the deepened interface directly. No adapter needed.
98
+
99
+ **2. Local-substitutable** — DB, filesystem, etc. with usable local stand-ins (PGLite, in-memory filesystem).
100
+ - Strategy: deepen using the stand-in in tests. The seam is internal; no port at the external interface.
101
+
102
+ **3. Remote but owned** — internal services across a network boundary.
103
+ - Strategy: define a port (TypeScript interface) at the seam. Production adapter + in-memory adapter for tests.
104
+ - The deep module owns the logic; only the transport is injected.
105
+
106
+ **4. True external** — third-party services (Stripe, Twilio, etc.) you don't control.
107
+ - Strategy: inject a port; provide a mock adapter for tests.
108
+
109
+ **Rule:** don't introduce a port unless at least two adapters are justified. A single-adapter seam is indirection without benefit.
110
+
111
+ ## Candidate Report Format
112
+
113
+ Each architecture finding uses this format in the `## Architecture Opportunities` section:
114
+
115
+ ```md
116
+ ### TITLE — Severity
117
+
118
+ - **Files:** relative/path/a.ts, relative/path/b.ts
119
+ - **Problem:** why this causes friction now (not just a pattern name)
120
+ - **Proposed deepening:** plain-English description of what would change
121
+ - **Interface shape:** rough sketch of the new interface (types, methods, key invariants)
122
+ - **Dependency category:** in-process | local-substitutable | remote-owned | true-external
123
+ - **Test strategy:** how tests would improve (what tests survive, what gets deleted, what's new)
124
+ - **Benefits:** locality gained, leverage gained, test impact
125
+ - **Trade-offs:** what gets harder, what is genuinely uncertain
126
+ - **Fixability:** auto | needs-confirm | report-only
127
+ ```
128
+
129
+ ## Severity Mapping for Architecture
130
+
131
+ | Level | Architecture criteria |
132
+ |---|---|
133
+ | **Highest** | Architecture issue directly causing a security vulnerability, data loss, or production correctness bug |
134
+ | **High** | Circular imports or cross-module coupling that blocks reliable tests or causes recurring defects |
135
+ | **Medium** | Shallow modules, scattered domain logic, pass-through abstractions, hard-to-test orchestration |
136
+ | **Low** | Small interface leaks, minor naming drift between module interface and what it exposes |
137
+
138
+ ## Fixability Mapping
139
+
140
+ | Fixability | Meaning | Fix mode behavior |
141
+ |---|---|---|
142
+ | `auto` | Local change: import path cleanup, narrow circular-import break, local module merge with co-located tests | Applied in normal fix loop |
143
+ | `needs-confirm` | Interface change, dependency inversion, test replacement, feature-slice reorganization | Shown to user, NOT applied automatically |
144
+ | `report-only` | Broad migration, ambiguous domain model, change requiring product/domain decision | Never applied, left as documentation |
145
+
146
+ ## Naming Guidance
147
+
148
+ Use names that already exist in the codebase (function names, type names, file names, package names). Don't invent domain terms. If a deepened module needs a name, prefer a name already used in adjacent code.
149
+
150
+ ## Architecture Fix Rules
151
+
152
+ When applying `auto` fixability architecture findings:
153
+
154
+ - Write new tests at the deepened module's interface. Old unit tests on shallow modules that are now merged become redundant — delete them.
155
+ - Tests assert observable behavior through the interface, not internal state.
156
+ - Don't expose internal seams through the interface just because tests need them.
157
+ - Tests that have to change when the implementation changes are testing past the interface.
158
+ - For large architecture changes classified `needs-confirm`, show a diff/migration plan and wait for explicit go-ahead before touching code.
@@ -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