unguard 0.15.2 → 0.16.1

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
@@ -2,45 +2,140 @@
2
2
 
3
3
  Unguard your code. Defend against overdefensive AI-generated code.
4
4
 
5
- Type-aware static analysis powered by the TypeScript compiler API.
5
+ Type-aware static analysis powered by the TypeScript compiler itself.
6
6
 
7
7
  If `??` is on a non-nullable type, you don't need it.
8
8
 
9
9
  If `?.` is on a guaranteed object, it's noise.
10
10
 
11
- unguard proves it with types.
11
+ unguard proves it with types, and complains.
12
12
 
13
- ## Install
13
+ ![unguard scanning a file and citing the types that prove each guard dead](https://raw.githubusercontent.com/TwoAbove/unguard/main/demo/demo.gif)
14
+
15
+ ## Quick start
14
16
 
15
17
  ```bash
16
- npm install -g unguard
18
+ npx unguard
17
19
  ```
18
20
 
19
- or simply
21
+ No config, no setup. unguard finds your tsconfig, builds a real TypeScript program, and reports only what the types prove. (`npm install -g unguard` if you prefer a global install.)
20
22
 
21
- ```bash
22
- npx unguard
23
+ ## What it catches
24
+
25
+ AI assistants pad code with guards against states the type system already rules out. Each one reads like caution, and in aggregate, they bury the real contracts. unguard reads the types and proves which guards need to be unguarded.
26
+
27
+ **Dead fallbacks** - the type says the value is always there:
28
+
29
+ ```typescript
30
+ const attempts = job.attempts ?? 1; // attempts: number
31
+ ```
32
+
33
+ ```txt
34
+ 10:20 warning Nullish coalescing (??) fallback on a non-nullable type is dead
35
+ code; remove the fallback or fix the type upstream
36
+ ```
37
+
38
+ **Dead narrowing** - the diagnostic cites the type that decides the condition:
39
+
40
+ ```typescript
41
+ if (typeof job.id !== "string") { // id: string
42
+ throw new Error("job is missing an id");
43
+ }
44
+ ```
45
+
46
+ ```txt
47
+ 12:7 warning typeof comparison is always false: `job.id` is `string`, and
48
+ typeof always yields "string" for that type
49
+ ```
50
+
51
+ **Fabricated types** - external data asserted into shape instead of validated:
52
+
53
+ ```typescript
54
+ const payload = JSON.parse(job.payload) as Payload;
55
+ ```
56
+
57
+ ```txt
58
+ 17:21 error Casting `any`/`unknown` to a concrete type without runtime
59
+ validation fabricates structure; validate first or narrow with
60
+ a type guard
61
+ ```
62
+
63
+ **Swallowed errors** - failure silently becomes a normal-looking value:
64
+
65
+ ```typescript
66
+ try {
67
+ return { id: job.id, attempts, payload };
68
+ } catch {
69
+ return null;
70
+ }
71
+ ```
72
+
73
+ ```txt
74
+ 19:5 warning Catch swallows the error: it neither throws nor returns a value
75
+ referencing the caught error. Propagate via throw, or model
76
+ failure into the return type carrying the original error
77
+ ```
78
+
79
+ Plus more type-system evasions (`as any`, `x as unknown as T`, `@ts-ignore`), cross-file analysis (exports nobody imports, optional parameters no call site ever passes, arguments that are the same literal everywhere, dead overloads, copy-pasted functions), and more - the full catalog is in [Current Rules](#current-rules).
80
+
81
+ ## Why not just typescript-eslint?
82
+
83
+ Keep typescript-eslint - unguard is a complement to it, not a replacement. Its `no-unnecessary-condition` overlaps with a slice of `no-dead-narrowing`, and that's roughly where the overlap ends. Unguard, on the other hand, provides more:
84
+
85
+ - **Whole-project analysis.** unguard indexes every call site, import, and export across your tsconfig groups (and is monorepo-aware). Lint rules see one file at a time while unguard can prove an optional parameter is never passed, an argument is always the same literal, an export is never imported.
86
+ - **Defensive-pattern focus.** Swallowed catches detected structurally (does any expression in the catch reference the error?), `throw new Error(e.message)` losing the cause, unvalidated casts of external data, defaults that widen interface contracts.
87
+ - **Tiers split by proof.** `scan` runs only rules whose findings the checker demonstrates - it's designed to gate CI without false-positive fatigue. Everything debatable lives in `audit`.
88
+ - **Zero config.** `npx unguard` on any TypeScript repo.
89
+
90
+ ## For AI coding agents
91
+
92
+ unguard is built for the verify loop. Findings are proofs with the evidence in the message, which is exactly what an agent needs to fix code instead of arguing with a style warning. Add to your `CLAUDE.md` / `AGENTS.md` / rules file:
93
+
94
+ ```markdown
95
+ After changing TypeScript, run `npx unguard` and fix every finding.
96
+ Diagnostics cite the type that proves the defect - fix the code (or the type),
97
+ don't add suppressions. If a guard is genuinely intentional, annotate it:
98
+ `// @unguard <rule-id> <reason>`.
99
+ ```
100
+
101
+ And gate CI:
102
+
103
+ ```yaml
104
+ - name: unguard
105
+ run: npx unguard --fail-on=error
23
106
  ```
24
107
 
25
108
  ## Usage
26
109
 
27
110
  ```bash
28
- unguard src # scan files/directories
29
- unguard src --config ./unguard.config.json # load config
30
- unguard src --ignore '**/*.gen.ts' # add ignore globs
31
- unguard src --filter no-any-cast # run a single rule
32
- unguard src --rule duplicate-*=warning # override rule severity/policy
33
- unguard src --rule category:cross-file=warning
34
- unguard src --rule tag:safety=error
35
- unguard src --severity=error,warning # show errors+warnings
36
- unguard src --fail-on=error # fail only on errors
37
- unguard src --format=flat # one-line-per-diagnostic, grepable
38
- unguard src --format=flat | grep error
39
- unguard src --concurrency 1 # disable worker-thread parallelism
40
- unguard src --no-cache # bypass on-disk diagnostic cache
111
+ unguard # scan files/directories
112
+ unguard audit # run heuristic rules as review prompts
113
+ unguard --config ./unguard.config.json # load config
114
+ unguard --ignore '**/*.gen.ts' # add ignore globs
115
+ unguard --filter no-any-cast # run a single rule
116
+ unguard --rule duplicate-*=warning # override rule severity/policy
117
+ unguard --rule category:cross-file=warning
118
+ unguard --rule tag:safety=error
119
+ unguard --severity=error,warning # show errors+warnings
120
+ unguard --fail-on=error # fail only on errors
121
+ unguard --format=flat # one-line-per-diagnostic, grepable
122
+ unguard --format=flat | grep error
123
+ unguard --format=json # machine-readable report
124
+ unguard --fix # apply auto-fixes, report what remains
125
+ unguard baseline # record current issues as the baseline
126
+ unguard --no-baseline # ignore unguard.baseline.json for this scan
127
+ unguard --concurrency 1 # disable worker-thread parallelism, can be slow for large codebases
128
+ unguard --no-cache # bypass on-disk diagnostic cache
41
129
  ```
42
130
 
43
- Add `unguard` to your lint check, especially if code is written by AI.
131
+ ### Two tiers: `scan` and `audit`
132
+
133
+ Every rule is classified by confidence:
134
+
135
+ - **proven** - the checker or AST demonstrates the defect; every finding demands a fix (or an explicit `@unguard` annotation). `unguard scan` runs these and is meant to gate CI or commits.
136
+ - **heuristic** - strong pattern evidence, but a correct alternative reading exists: deliberate duplication, API surface for external consumers, intentionally optional params. `unguard audit` runs these and exits 0 unless `--fail-on` is passed explicitly, so findings surface for review without blocking anything.
137
+
138
+ `--filter <rule>` and explicit `rules` selections bypass the tiers - a rule requested by name runs in either command.
44
139
 
45
140
  ### Config
46
141
 
@@ -58,6 +153,15 @@ Add `unguard` to your lint check, especially if code is written by AI.
58
153
  "no-ts-ignore": "error",
59
154
  "prefer-*": "off"
60
155
  },
156
+ "overrides": [
157
+ {
158
+ "files": ["tests/**", "**/*.test.ts"],
159
+ "rules": {
160
+ "no-non-null-assertion": "off",
161
+ "duplicate-*": "off"
162
+ }
163
+ }
164
+ ],
61
165
  "failOn": "error",
62
166
  "concurrency": 4,
63
167
  "cache": true
@@ -71,6 +175,9 @@ Selectors support:
71
175
  - wildcard: `duplicate-*`
72
176
  - category: `category:cross-file`
73
177
  - tag: `tag:safety`
178
+ - confidence tier: `confidence:proven`, `confidence:heuristic`
179
+
180
+ `overrides` entries apply a rule policy only to diagnostics in files matching the listed globs (gitignore syntax, relative to the working directory). Later entries win; `off` drops the diagnostic. Typical use: relaxing rules in test directories, where non-null assertions and duplication are often deliberate.
74
181
 
75
182
  ### Ignore behavior
76
183
 
@@ -97,6 +204,8 @@ unguard src --fail-on=error
97
204
 
98
205
  `--severity` filters display only. `--fail-on` evaluates all diagnostics after rule policy.
99
206
 
207
+ `unguard audit` defaults to `--fail-on=none` and exits 0 regardless of findings. Pass `--fail-on` explicitly to make audit gate.
208
+
100
209
  ### Output formats
101
210
 
102
211
  **Grouped** (default) -- diagnostics grouped by file:
@@ -112,84 +221,124 @@ src/lib/probe.ts
112
221
  src/lib/probe.ts:37:4 warning [no-swallowed-catch] Catch swallows the error...
113
222
  ```
114
223
 
224
+ **JSON** (`--format=json`) -- machine-readable, for CI integrations:
225
+
226
+ ```json
227
+ {
228
+ "diagnostics": [
229
+ {
230
+ "file": "src/lib/probe.ts",
231
+ "line": 37,
232
+ "column": 4,
233
+ "severity": "warning",
234
+ "ruleId": "no-swallowed-catch",
235
+ "message": "Catch swallows the error...",
236
+ "fixable": false
237
+ }
238
+ ],
239
+ "fileCount": 1,
240
+ "exitCode": 1
241
+ }
242
+ ```
243
+
244
+ ### Auto-fix
245
+
246
+ `--fix` applies fixes for diagnostics whose replacement is provably semantics-preserving (dead `??` fallbacks, dead `?.`, no-op `await`, redundant casts, `cond ? true : false`, and similar), then reports what remains. The exit code reflects only the remaining diagnostics.
247
+
248
+ ### Baseline
249
+
250
+ Adopting unguard in an existing codebase? `unguard baseline src` records every current issue in `unguard.baseline.json`. Subsequent scans suppress a `(file, rule)` group as long as its count stays at or below the recorded number - new issues anywhere still fail, and fixing old ones ratchets the allowance down on the next `unguard baseline`. Use `--no-baseline` to see the full picture.
251
+
115
252
  ## Current Rules
116
253
 
254
+ The **Tier** column says which command runs the rule: `scan` (proven) or `audit` (heuristic).
255
+
117
256
  ### Type system evasion
118
257
 
119
- | Rule | Severity | What it catches |
120
- | ---- | -------- | --------------- |
121
- | `no-any-cast` | error | `x as any` |
122
- | `no-explicit-any-annotation` | error | `param: any`, `const x: any` |
123
- | `no-inline-type-assertion` | error | `x as { ... }`, `<{ ... }>x` |
124
- | `no-type-assertion` | error | `x as unknown as T` |
125
- | `no-ts-ignore` | error | `@ts-ignore` / `@ts-expect-error` |
258
+ | Rule | Severity | Tier | What it catches |
259
+ | ---- | -------- | ---- | --------------- |
260
+ | `no-any-cast` | error | scan | `x as any` -- erases type safety for everything downstream of the cast |
261
+ | `no-explicit-any-annotation` | error | scan | `param: any`, `const x: any` -- an `any` annotation where a real type (or `unknown`) belongs |
262
+ | `no-inline-type-assertion` | error | scan | `x as { ... }`, `<{ ... }>x` -- asserting into an anonymous inline shape; name the type or fix it upstream |
263
+ | `no-type-assertion` | error | scan | `x as unknown as T` -- the double assertion that can connect any two types |
264
+ | `no-ts-ignore` | error | scan | `@ts-ignore` -- silences the checker unconditionally |
265
+ | `no-ts-expect-error` | warning | scan | `@ts-expect-error` -- self-expiring, but still an evasion |
266
+ | `no-never-cast` | warning | scan | `x as never` -- silences the checker completely, usually to force past an exhaustiveness error |
267
+ | `no-redundant-cast` | error | scan | `x as T` where `x` already has type `T` |
268
+ | `no-unvalidated-cast` | error | scan | `JSON.parse(...) as T`, `await fetch(...).json() as T` -- casting external data into a shape nothing has checked |
269
+ | `redundant-narrowing-then-cast` | warning | scan | `if (typeof x === "string") { (x as string).length }` -- the `if` already narrowed `x`, so the cast adds nothing |
270
+ | `return-type-widens-via-destructure` | warning | scan | `const [x] = await db...returning(); return x;` in a function declared to return `T` -- an array element is really `T \| undefined`, so the return type hides the empty case |
126
271
 
127
272
  ### Defensive code (type-aware)
128
273
 
129
274
  These rules use the TypeScript type checker. Non-nullable types suppress the diagnostic; nullable types are flagged.
130
275
 
131
- | Rule | Severity | What it catches |
132
- | ---- | -------- | --------------- |
133
- | `no-optional-property-access` | warning | `obj?.prop` on a non-nullable type |
134
- | `no-optional-element-access` | warning | `obj?.[key]` on a non-nullable type |
135
- | `no-optional-call` | warning | `fn?.()` on a non-nullable type |
136
- | `no-nullish-coalescing` | warning | `x ?? fallback` on a non-nullable type |
137
- | `no-logical-or-fallback` | warning | `map.get(k) \|\| fallback`, `count \|\| 1` -- `\|\|` swallows `0` and `""`; use `??` |
138
- | `no-null-ternary-normalization` | warning | `x == null ? fallback : x` |
139
- | `no-coalesce-then-guard` | warning | `const x = a ?? null; if (x == null)` -- guard re-checks the same partition the `??` just created |
140
- | `no-await-coalesce` | warning | `await fn() ?? fallback` -- fuses the call's failure mode into the fallback (skips `Map.get`, `Array.find`, and structural optionals) |
141
- | `no-non-null-assertion` | warning | `x!` on a nullable type without a local narrowing guard |
142
- | `no-double-negation-coercion` | info | `!!value` |
143
- | `no-redundant-existence-guard` | warning | `obj && obj.prop` on a non-nullable type |
276
+ Nullability-driven rules require `strictNullChecks` (or `strict`). Without it the checker erases `null`/`undefined` from every type, so these rules are skipped with a warning rather than reporting every load-bearing guard as dead code.
277
+
278
+ | Rule | Severity | Tier | What it catches |
279
+ | ---- | -------- | ---- | --------------- |
280
+ | `no-optional-property-access` | warning | scan | `obj?.prop` on a non-nullable type |
281
+ | `no-optional-element-access` | warning | scan | `obj?.[key]` on a non-nullable type |
282
+ | `no-optional-call` | warning | scan | `fn?.()` on a non-nullable type |
283
+ | `no-nullish-coalescing` | warning | scan | `x ?? fallback` on a non-nullable type |
284
+ | `no-logical-or-fallback` | warning | scan | `map.get(k) \|\| fallback`, `count \|\| 1` -- `\|\|` swallows `0` and `""`; use `??` |
285
+ | `no-null-ternary-normalization` | warning | scan | `x == null ? fallback : x` -- a hand-rolled `??`: dead if the type is non-nullable, and a sign the type needs fixing if it isn't |
286
+ | `no-coalesce-then-guard` | warning | scan | `const x = a ?? null; if (x == null)` -- the `if` re-asks the question the `??` just answered |
287
+ | `no-await-coalesce` | warning | audit | `await fn() ?? fallback` -- defaulting away a call's nullable result hides why it can be empty; check it and branch instead (built-in lookups like `Map.get` and `Array.find` are exempt) |
288
+ | `no-non-null-assertion` | warning | scan | `x!` on a nullable type without a local narrowing guard |
289
+ | `no-double-negation-coercion` | warning | scan | `!!value` inside an `if`/`while`/ternary test -- the construct already coerces (`const flag = !!x` and other places that need an actual boolean are fine) |
290
+ | `no-redundant-existence-guard` | warning | scan | `obj && obj.prop` on a non-nullable type |
291
+ | `no-dead-narrowing` | warning | scan | Conditions statically decided by the operand's declared type: truthiness checks that can never fail, `typeof` comparisons that can never (or must always) match, always-true `instanceof`, type-predicate calls whose argument already has the asserted type. Exempt: ambient globals (`typeof document === "undefined"` probes the environment, not the type) and, without `noUncheckedIndexedAccess`, truthiness checks (index reads erase `undefined`, so the guard may be load-bearing) |
292
+ | `no-coalesce-undefined` | warning | scan | `x ?? undefined` where `x` can be `undefined` but never `null` -- an identity no-op |
293
+ | `redundant-boolean-branch` | warning | scan | `cond ? true : false`, `if (cond) return true; return false;` -- the condition is already boolean. The inverted form is flagged only when the replacement is a readable `!cond`; compound conditions are never rewritten into `!(a && b)` |
294
+ | `no-useless-await` | warning | scan | `await x` where `x`'s type has no `then` -- a no-op that suggests asynchrony that isn't there |
295
+ | `redundant-destructure-default` | warning | scan | `const { a = fallback } = obj` where `obj.a` is required and non-undefined -- the default can never apply |
144
296
 
145
297
  ### Error handling
146
298
 
147
- | Rule | Severity | What it catches |
148
- | ---- | -------- | --------------- |
149
- | `no-swallowed-catch` | warning | `catch (e) {}`, `catch (e) { log(e); return fallback }`, `.catch(() => fallback)` -- error neither rethrown nor surfaced in the return value |
150
- | `no-error-rewrap` | error | `throw new Error(e.message)` without `{ cause: e }` |
299
+ | Rule | Severity | Tier | What it catches |
300
+ | ---- | -------- | ---- | --------------- |
301
+ | `no-swallowed-catch` | warning | scan | `catch (e) {}`, `.catch(() => fallback)` -- error is neither rethrown, returned, nor passed into a handling call |
302
+ | `no-error-rewrap` | error | scan | `throw new Error(e.message)` without `{ cause: e }` |
151
303
 
152
304
  ### Interface design
153
305
 
154
- | Rule | Severity | What it catches |
155
- | ---- | -------- | --------------- |
156
- | `no-inline-param-type` | warning | `(params: { id: string; ... })` inline object type on parameter |
157
- | `prefer-default-param-value` | info | Optional param reassigned with `??` in the body |
158
- | `prefer-required-param-with-guard` | info | `arg?: T` followed by `if (!arg) throw` |
159
- | `no-defaulted-required-port-arg` | warning | `class C implements I { method(arg = x) }` where `I.method(arg)` is required -- the default widens the interface contract |
160
- | `repeated-literal-property` | warning | Same literal value repeated across object properties -- likely a missed constant |
161
- | `repeated-return-shape` | warning | Multiple functions return object literals with the same property names -- extract a shared return type |
162
-
163
- ### State management
164
-
165
- | Rule | Severity | What it catches |
166
- | ---- | -------- | --------------- |
167
- | `no-module-state-write` | warning | Function mutates a module-scope binding (`count++`, `state.ready = ...`, `cache.set(...)`) |
306
+ | Rule | Severity | Tier | What it catches |
307
+ | ---- | -------- | ---- | --------------- |
308
+ | `no-inline-param-type` | warning | audit | `(params: { id: string; ... })` -- an object type spelled out in the signature; name it so call sites and other signatures can share it |
309
+ | `prefer-type-predicate` | warning | audit | `(x: unknown): boolean` whose body is `typeof`/`instanceof`/`in` checks -- returning `x is T` would let callers narrow |
310
+ | `optional-param-coerced-in-body` | warning | scan | Optional param forced non-optional in the body (`x = x ?? def`, `x ??= def`, or `if (!x) throw`) |
311
+ | `no-defaulted-required-port-arg` | warning | scan | `class C implements I { method(arg = x) }` where `I.method(arg)` is required -- the implementation quietly makes a required argument optional |
312
+ | `repeated-literal-property` | warning | audit | Same literal value repeated across object properties -- likely a missed constant |
313
+ | `repeated-return-shape` | warning | audit | Multiple functions return object literals with the same property names -- extract a shared return type |
314
+ | `trivial-type-alias` | info | audit | `type Foo = Bar;` -- a second name for an existing type with no change (info: an alias marking a domain boundary is often deliberate) |
168
315
 
169
316
  ### Cross-file analysis
170
317
 
171
- | Rule | Severity | What it catches |
172
- | ---- | -------- | --------------- |
173
- | `duplicate-type-declaration` | warning | Same type shape in multiple files |
174
- | `duplicate-type-name` | warning | Same exported type name, different shapes |
175
- | `duplicate-function-declaration` | warning | Same function body in multiple files |
176
- | `duplicate-function-name` | warning | Same exported function name, different bodies |
177
- | `duplicate-constant-declaration` | warning | Same constant value in multiple files |
178
- | `duplicate-inline-type-in-params` | warning | Same inline `{ ... }` param type repeated across signatures |
179
- | `duplicate-file` | warning | File with identical content to another file |
180
- | `near-duplicate-function` | warning | Function bodies that match after renaming params and literals -- likely a copy-paste |
181
- | `duplicate-statement-sequence` | info | Repeated block of statements across functions or files |
182
- | `trivial-wrapper` | info | Function that delegates to another without transformation |
183
- | `unused-export` | info | Exported function with no usages in the project |
184
- | `optional-arg-always-used` | warning | Optional param provided at every call site |
185
- | `explicit-null-arg` | warning | `fn(null)` / `fn(undefined)` to project functions |
186
- | `dead-overload` | warning | Overload signature with zero matching project call sites |
318
+ | Rule | Severity | Tier | What it catches |
319
+ | ---- | -------- | ---- | --------------- |
320
+ | `duplicate-type-declaration` | warning | audit | Same type shape in multiple files |
321
+ | `duplicate-type-name` | warning | audit | Same exported type name, different shapes |
322
+ | `duplicate-function-declaration` | warning | audit | Same function body in multiple files |
323
+ | `duplicate-function-name` | warning | audit | Same exported function name, different bodies |
324
+ | `duplicate-constant-declaration` | info | audit | Same constant value in multiple files (info: coincidental value equality is common) |
325
+ | `duplicate-inline-type-in-params` | warning | audit | Same inline `{ ... }` param type repeated across signatures |
326
+ | `duplicate-file` | warning | audit | File with identical content to another file |
327
+ | `near-duplicate-function` | warning | audit | Function bodies that match after renaming params and literals -- likely a copy-paste |
328
+ | `duplicate-statement-sequence` | warning | audit | Repeated block of statements across functions or files (identical text; lookup tables that vary only by literal data are not flagged) |
329
+ | `trivial-wrapper` | warning | audit | Function that delegates to another without transformation (skipped when the wrapper specializes a type predicate, introduces generics, reorders args, or partially applies) |
330
+ | `unused-export` | warning | audit | Exported function, type, or constant with no usages in the project. Imports resolve through the checker (path aliases, workspace packages) and usage is merged across tsconfig groups, so an export consumed by a sibling monorepo package counts as used. Consumers outside the scanned tree, such as a published package's API surface, are invisible to the analysis - hence audit tier |
331
+ | `optional-arg-always-used` | warning | audit | Optional param provided at every call site -- make it required |
332
+ | `optional-arg-never-used` | warning | audit | Optional param never provided at any call site -- remove it, inline the default |
333
+ | `constant-argument` | warning | audit | Parameter receives the same literal at every call site -- inline the value |
334
+ | `explicit-null-arg` | warning | audit | `fn(null)` / `fn(undefined)` passed to a project function -- the parameter invites nullish values; redesign it so callers can omit the argument |
335
+ | `dead-overload` | warning | audit | Overload signature with zero matching project call sites |
187
336
 
188
337
  ### Imports
189
338
 
190
- | Rule | Severity | What it catches |
191
- | ---- | -------- | --------------- |
192
- | `no-dynamic-import` | error | `import("./module")` |
339
+ | Rule | Severity | Tier | What it catches |
340
+ | ---- | -------- | ---- | --------------- |
341
+ | `no-dynamic-import` | warning | audit | `import("./module")` -- breaks static analysis (warning, not error: code-splitting and lazy loading are legitimate) |
193
342
 
194
343
  ## Annotations
195
344
 
@@ -207,8 +356,8 @@ file.ts:2:31 error Explicit `any` annotation ... (intentional escape hatch for u
207
356
  For `warning` and `info` diagnostics, you can explicitly mark a finding as intentional with `@unguard <rule-id>` on the same line or immediately above:
208
357
 
209
358
  ```typescript
210
- // @unguard no-module-state-write module cache is intentional in this adapter
211
- cache.set(user.id, user);
359
+ // @unguard no-nullish-coalescing intentional default for legacy callers
360
+ const port = config.port ?? 3000;
212
361
  ```
213
362
 
214
363
  `@unguard` never suppresses `error` diagnostics.
@@ -221,6 +370,7 @@ import { executeScan, scan } from "unguard";
221
370
  const result = await scan({ paths: ["src/"] }); // raw diagnostics
222
371
  const execution = await executeScan({
223
372
  paths: ["src"],
373
+ mode: "scan", // or "audit" for the heuristic tier
224
374
  ignore: ["**/*.gen.ts"],
225
375
  rulePolicy: {
226
376
  "duplicate-*": "warning",
@@ -228,6 +378,7 @@ const execution = await executeScan({
228
378
  "tag:safety": "error",
229
379
  "prefer-*": "off",
230
380
  },
381
+ overrides: [{ files: ["tests/**"], rules: { "no-non-null-assertion": "off" } }],
231
382
  showSeverities: ["error", "warning"],
232
383
  failOn: "error",
233
384
  });
@@ -245,7 +396,7 @@ run, if every file's content hash and the active rule set are unchanged,
245
396
  unguard returns cached diagnostics without building a TypeScript program.
246
397
  The cache invalidates automatically on:
247
398
 
248
- - file content changes (mtime-only changes are ignored `git checkout` and `git stash` stay cache hits)
399
+ - file content changes (mtime-only changes are ignored - `git checkout` and `git stash` stay cache hits)
249
400
  - changes to active rules or rule severities
250
401
  - changes to scan paths, ignore globs, or `failOn`
251
402
  - unguard version upgrades