hunter-party-ts/invariant-hunter-ts/SKILL.md
Audit TypeScript types for weak invariants — unnecessary casts, loose optionality, defensive `?.`/`??` masking missing guarantees, leaky discriminated unions, and runtime checks the type system should enforce. Use when: tightening domain models, reducing type assertions, increasing type coverage, reviewing discriminated unions, or establishing a type-safety baseline before refactoring.
npx skillsauth add skyosev/agent-skills invariant-hunter-tsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit TypeScript code to make the type system enforce invariants that are currently left to runtime, convention, or as
assertions. The goal: illegal states become unrepresentable, and consumers narrow via control flow without casts.
as assertions and type casts across a codebaseTypes are documentation that compiles. Encode invariants in the type system. If it cannot be encoded without excessive complexity, validate at runtime with a clear error.
Resolve at construction boundaries. Defaults and validation belong where data is created or enters the system — public API entry points, builders, factories. Downstream functions should require their inputs. If a caller "always passes X", make X required and push the default to the construction boundary.
Every ? is a branch. A ?: field means T | undefined — the consumer must handle the absent case. Only use
optional when the domain genuinely permits absence, not as a convenience for callers.
?. and ?? are symptoms. Optional chaining and nullish coalescing have legitimate uses (truly optional data,
external API responses), but each occurrence is a branch the reader must reason about. In non-boundary code, they
signal a type that is too loose. The fix is tightening the upstream type, not adding defensive access.
Discriminants: single source of truth. The discriminant field must be exhaustive. Redundant fields carrying the
same information (e.g., shape vs arch.kind) are a drift risk — eliminate one or derive it.
as must be justified. Type assertions should be minimized, but runtime-guarded casts are acceptable when
the cast immediately follows a runtime check and TypeScript's control flow analysis cannot correlate the
discriminants. Do not blindly refactor into verbose alternatives that harm ergonomics without improving safety.
Fail fast. When an invariant is violated, throw immediately — do not silently return a default or catch-and-log.
Invariant violations are programmer errors; they should crash loudly to surface bugs. Empty catches and broad
catch(e) { log(e) } blocks that only log without recovery or re-throw are never acceptable in non-boundary code.
(Try/catch at defined error boundaries — top-level handlers, middleware — with actual recovery logic is fine; see
Canonical Exceptions.)
Eliminate type-system bypasses. as any, as unknown as T, @ts-ignore, @ts-expect-error are escape hatches.
Each must be justified (why necessary), scoped (boundary layers only), and temporary (tracked as tech debt).
Not every finding requires action. Document these but do not flag as "must-fix":
| Pattern | When Acceptable |
| ------- | --------------- |
| Runtime-guarded as Extract<...> | Cast immediately follows a runtime check |
| Optional utility parameters | Helper accepting optional when domain type requires |
| ?? / ?. at true boundaries | External API responses, user input, config defaults |
| Try/catch at error boundaries | Top-level handlers, middleware with defined recovery |
| Type bypasses in boundary layers | JSON parsing, FFI, library workarounds — with comment |
as assertions and ! non-null assertions where narrowing should work or the type can be tightened.
Signals:
as not preceded by a runtime check validating the assertion! on a value that could be made non-optional at its sourceas unknown as T double-cast bypassesAction: Tighten the upstream type or add a runtime guard. If runtime-guarded, document as acceptable.
?: fields that are always present after construction, or mutually exclusive optional fields that should be a
discriminated union.
Signals:
?? default in every consumer?: throughoutAction: Make required at construction boundary. Replace mutually exclusive optionals with a discriminated union.
?. and ?? that compensate for a loose upstream type rather than handling genuine absence.
Signals:
?. on a property that is always present given the current context?? applying a default that was already resolved at the entry point?? to handle cases that should be separate functionsAction: Tighten the upstream type so the value is guaranteed present. Move defaults to construction boundaries.
Unions with redundant discriminants, missing exhaustiveness checks, or fields that leak across variants.
Signals:
type vs a boolean flag)switch over discriminant without default: assertNever(x)?: never guards)if/switchAction: Eliminate redundant discriminants. Add exhaustiveness guards. Apply ?: never to variant-exclusive fields.
Guards, assertions, and validations that could be compile-time guarantees.
Signals:
if (node.config) where config should be guaranteed by the discriminantstringNonEmptyArray<T> type would eliminateReadonly<T> would enforcesatisfies where shape conformance is intended but uncheckedboolean instead of using asserts param is T to narrow the caller's scopeas const would enforce literal types and immutabilityAction: Promote to type constraint. Use satisfies to validate shape at assignment without widening. Use assertion
functions (asserts x is T) for runtime guards that should narrow control flow. Use as const for fixed configuration
and lookup objects. If type complexity would be excessive, keep as runtime with documentation.
as any, @ts-ignore, double-casts, empty catch blocks, and catch-only-log patterns.
Signals:
as any or as unknown as T without justification comment@ts-ignore / @ts-expect-error without explanationcatch { } or catch(e) { console.log(e) } with no recovery logicreturn [], return null) on invalid input instead of throwingAction: Fix the underlying type issue. If bypass is necessary, add justification and track as tech debt. For catch blocks: remove if suppressing invariant violations, keep if at a defined error boundary with recovery.
Resolve audit surface. The prompt may specify the scope as:
main/master)BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo main)
SCOPE=$(git diff --name-only $(git merge-base HEAD $BASE)...HEAD)
Constrain all subsequent scans to the resolved surface.
Record tsconfig.json strictness flags (strict, strictNullChecks, exactOptionalPropertyTypes,
noUncheckedIndexedAccess). If key flags are off, note this prominently.
Scan for patterns:
EXCLUDE='--glob !**/*.test.* --glob !**/*.spec.* --glob !**/node_modules/**'
rg --pcre2 '\bas\s+(?!const\b)' --type ts $EXCLUDE # as assertions
rg --pcre2 '[a-zA-Z0-9_\]\)]\!(?!=)' --type ts $EXCLUDE # non-null assertions
rg ':\s*any\b|<any>|\bas\s+any\b' --type ts $EXCLUDE # any usage
rg '@ts-ignore|@ts-expect-error' --type ts # suppressions
rg '\?\.' --type ts $EXCLUDE # optional chaining
rg '\?\?' --type ts $EXCLUDE # nullish coalescing
rg 'as\s+(unknown|any)\s+as' --type ts $EXCLUDE # double-casts
rg -U 'catch\s*\([^)]*\)\s*\{\s*(//.*)?\s*\}' --type ts $EXCLUDE # empty catches
rg 'satisfies\s' --type ts $EXCLUDE # satisfies usage (adoption check)
rg 'asserts\s+\w+\s+is\s' --type ts $EXCLUDE # assertion functions
Produce counts by category, grouped by module/layer.
For each discriminated union:
if/switch without as? List bypass sites.switch statements have assertNever defaults??: never on other variants?For each ?: field in core types: Is absence meaningful, or always present after construction?
For each ?. / ?? in non-boundary code: Is the property guaranteed present in this context?
Classify each as: tighten type / move default to boundary / acceptable (see Canonical Exceptions).
For each runtime guard/assertion, classify:
For each catch block: classify as Remove (no recovery) / Keep (defined boundary) / Move (too broad). For each type bypass: verify justification, scoping, and tech debt tracking.
Save as YYYY-MM-DD-invariant-hunter-audit-{$LLM-name}.md in the project's docs folder (or project root if no docs folder exists).
# Invariant Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list}
## Compiler Context
- tsconfig: {path}
- `strict`: {on/off}, `strictNullChecks`: {on/off}
- `exactOptionalPropertyTypes`: {on/off}, `noUncheckedIndexedAccess`: {on/off}
## Baseline
| Category | Count |
| -------- | ----- |
| `as` assertions (non-const) | {n} |
| Non-null assertions `!` | {n} |
| `any` usage | {n} |
| `@ts-ignore` / `@ts-expect-error` | {n} |
| Double-cast bypasses | {n} |
| Empty/logging-only catch blocks | {n} |
| Optional fields in core types | {n} |
| `??` in non-boundary code | {n} |
| `?.` in non-boundary code | {n} |
## Discriminated Unions
### {UnionName}
- Discriminant: `{field}`
- Inference: {pass/fail}
- Redundancy: {none / {field} duplicates {other}}
- Exhaustiveness: {pass/fail}
- Never guards: {pass/fail}
## Optionality and Defensive Access
| # | Field/Expression | Location | Current | Proposed | Rationale |
| - | ---------------- | -------- | ------- | -------- | --------- |
| 1 | ... | file:line | optional | required | ... |
## Runtime → Type Promotions
| # | Invariant | Location | Current | Proposed | Complexity |
| - | --------- | -------- | ------- | -------- | ---------- |
| 1 | ... | file:line | runtime guard | type constraint | low/med/high |
## Error Handling and Bypasses
| # | Location | Pattern | Classification | Action |
| - | -------- | ------- | -------------- | ------ |
| 1 | file:line | `as any` | Remove | Fix type |
## Ergonomics
- Consumer branch count per variant: ...
- Boilerplate patterns: ...
- Extensibility: ...
## Recommendations (Priority Order)
1. **Must-fix**: {narrowing failures, forced casts, silent fallbacks masking bugs}
2. **Should-fix**: {defaults in wrong layer, always-present optionals, catch cleanup}
3. **Consider**: {ergonomic improvements, extensibility prep}
as any, @ts-ignore,
@ts-expect-error) and loose optionality from a type-enforcement perspective. Error-hunter owns how errors are
structured, propagated, caught, and converted — the design of the error handling strategy. If the finding is about
as any without justification, it belongs here. If the finding is about an empty catch block or missing
Error.cause chain, it belongs in error-hunter. Type-system bypass patterns that appear inside catch blocks
(e.g., catch (e) { throw e as any }) are invariant-hunter findings.file/path.ext:line with the exact code.noUnusedParameters; include cleanup.development
Transforms vague feature ideas into precise, codebase-grounded technical requirements. Use when requirements are ambiguous/incomplete, the user struggles to describe behavior, terminology is unclear, or multiple concepts are mixed. Output is a requirements spec—NOT an implementation plan.
tools
Audit TypeScript type definitions for design debt — duplicated shapes, missing derivations, over-engineered generics, under-constrained type parameters, reinvented utility types, and disorganized type architecture. Type structure and maintainability, not type enforcement. Use when: reviewing type definitions for maintainability, reducing type duplication, simplifying over-engineered type-level logic, or reorganizing type architecture after growth.
development
Audit TypeScript test code for quality gaps — missing coverage on critical paths, brittle tests coupled to implementation, over-mocking, assertion-free tests, missing edge cases, and duplicated test setup. Focuses on test effectiveness, not production code structure. Use when: reviewing TypeScript test suites for reliability, reducing false-positive test failures, improving coverage of critical business logic, or cleaning up test debt.
tools
Audit TypeScript class and interface design for SOLID violations — god classes, rigid extension points, broken substitutability, fat interfaces, and concrete dependency chains. Focuses on responsibility assignment and abstraction fitness. Use when: reviewing class hierarchies, preparing for extension with new variants, reducing coupling between services, or improving testability of class-heavy code.