| name | refactoring |
| description | BACKWARD view over rules/ — routes linter and review failures to the rule whose Fix pattern owns the repair. Use when linter fails with complexity issues (cyclomatic, cognitive, maintainability) or when code feels hard to read/maintain. Also the skill for removing a `// eslint-disable-next-line` directive or a package-level global (R8): "drop the suppression", "remove the global", "make the linter pass without suppressions" route here, one green step per commit. Also runs PREPARATORY mode: reshape code an approved plan touches, before the first RED, so the feature lands add-only. Applies storifying, type extraction, function extraction, conditional-dispatch, and mutation-discipline patterns via rules/R1-R8 and R10-R12. |
| allowed-tools | Skill(ts-react-linter-driven-development:code-designing), Skill(ts-react-linter-driven-development:testing), Skill(ts-react-linter-driven-development:pre-commit-review) |
| Linter failure | Route |
|---|---|
sonarjs/cognitive-complexity / sonarjs/cyclomatic-complexity |
../../rules/R3-storifying.md |
sonarjs/max-lines-per-function / sonarjs/nested-control-flow / sonarjs/no-nested-conditional / sonarjs/no-nested-functions / sonarjs/expression-complexity / sonarjs/elseif-without-else |
../../rules/R3-storifying.md |
react/no-unstable-nested-components |
../../rules/R3-storifying.md — extract the component; place it per ../../rules/R4-helper-placement.md |
max-params |
../../rules/R1-primitive-obsession.md (Introduce Parameter Object — a readonly props/options type, scored) |
A tuple return ([T, boolean], [Data, Error]); a Record<string, string>, string[] or Map of primitives crossing a function boundary or nested in a type (review-only) |
../../rules/R1-primitive-obsession.md (Name the Container — Q7 nested, Q8 flat across a boundary; the receivers' lookups, filters and loops are the methods of the type that does not exist yet) |
sonarjs/no-duplicate-string on an enum-shaped literal; no-magic-numbers on a domain value; sonarjs/max-union-size |
../../rules/R1-primitive-obsession.md (Name enum strings, or a named type) |
sonarjs/no-identical-functions; duplicated code (review-only) |
../../rules/R1-primitive-obsession.md (extract shared type/logic); duplicated switch/if-chains on the same kind/type discriminator → ../../rules/R11-conditional-dispatch.md |
sonarjs/max-lines; scripts/ldd-lint.sh file-length and package-size findings |
../../rules/R5-vertical-slice.md — mechanics in <file_and_package_routing>, sed -n '/^## File and package routing/,/^## Preparatory mode/p' over this skill's reference.md |
react/no-multi-comp |
../../rules/R5-vertical-slice.md — one component per file; the page folder decides where the second goes |
@typescript-eslint/no-explicit-any / @typescript-eslint/no-unsafe-* / @typescript-eslint/no-non-null-assertion / @typescript-eslint/no-unnecessary-condition at a boundary |
../../rules/R2-self-validating-types.md (parse at the boundary — a guard, not an assertion) |
@typescript-eslint/switch-exhaustiveness-check (when configured; review-only otherwise) / sonarjs/no-nested-switch / sonarjs/max-switch-cases / sonarjs/no-small-switch |
../../rules/R11-conditional-dispatch.md — handle the case at the single dispatch site and close it with default: return assertNever(x); a second switch appearing is the R11 violation itself |
Three or more boolean props on one component; an isLoading/isError/isEmpty triplet (review-only) |
../../rules/R11-conditional-dispatch.md — Split Flag Argument when the branches share little; a status union when they are mutually exclusive |
react-hooks/exhaustive-deps / react-hooks/set-state-in-effect / @typescript-eslint/no-floating-promises / @typescript-eslint/no-misused-promises / promise/catch-or-return |
../../rules/R10-concurrency-safety.md (no-floating-promises is mechanical when the fix is await, or void only at an entry point for genuinely fire-and-forget work; otherwise R10) |
react/no-array-index-key when the item has no id field (review the type) |
../../rules/R1-primitive-obsession.md — the list element has no identity, which is a missing type, not a key problem |
jsx-a11y/click-events-have-key-events / jsx-a11y/no-static-element-interactions / jsx-a11y/interactive-supports-focus when the honest fix is role + tabIndex + onKeyDown on a non-interactive element |
Escalate — a design question for the component (a button, a Link), not a markup fix |
import/no-mutable-exports; no-restricted-syntax on import.meta.env outside the config module (when configured); import-time side effects (review-only) |
../../rules/R8-no-globals.md |
no-param-reassign / sonarjs/prefer-read-only-props / react/no-direct-mutation-state; an in-place .sort()/.splice() on query data (review-only) |
../../rules/R12-mutation-discipline.md |
A vi.mock of an internal module; an interface with one implementation (review-only) |
../../rules/R6-test-only-interfaces.md |
unused-imports/no-unused-imports, unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars, simple-import-sort/*, import/order, import/no-duplicates, @typescript-eslint/consistent-type-imports, curly, arrow-body-style, no-console, prefer-const, eqeqeq, no-plusplus, react/jsx-* ordering and react/jsx-no-leaked-render, react/no-array-index-key when the item has an id (key={item.id}), react/forbid-dom-props / react/forbid-component-props on an inline style (the SCSS module class), the jsx-a11y families click-events-have-key-events, no-static-element-interactions, interactive-supports-focus, label-has-associated-control, aria-props, aria-proptypes, role-has-required-aria-props, alt-text, img-redundant-alt when the fix is one attribute or a button for a div onClick, Prettier |
Mechanical — fix directly (import the type, add the alt/htmlFor/role, wrap the && render in a boolean, delete the unused symbol, sort the imports). Enum-shaped sonarjs/no-duplicate-string literals and === 'READY' comparisons → R1's "Name enum strings" move. An @ts-expect-error on return undefined is R1 Q4 — never a suppression. |
Fowler's preparatory refactoring — reshape code an approved plan is about to touch,
before the first RED, so the feature lands add-only. Invoked by @linter-driven-development
(Phase 1.5, or RED friction) or /tsr-ldd-prepare with a DESIGN PLAN, the
touch-point files and findings that already passed the four PREPARE gates; this mode
re-runs none of them. The trigger is the plan, not the linter; characterization tests
come before motion; the stop is the landing shape (add-only), not lint; prep lands in
its own commits. In full, printed in the loop's opening lint call (step 2) when this
mode is the one invoked, before any move: sed -n '/^## Preparatory mode/,/^## Stopping criteria/p' <this skill dir>/reference.md.
changed_files=$({ git diff --name-only; git diff --cached --name-only; git ls-files --others --exclude-standard; } | sort -u)
[ -n "$changed_files" ] && printf '%s\n' "$changed_files" | xargs grep -nE 'eslint-disable|@ts-(expect-error|ignore|nocheck)|prettier-ignore' 2>/dev/null
Any hit → remove the directive and fix properly. Genuine false positives belong in
eslint.config.* — a rule turned off for a named file glob — or in tsconfig*.json,
with user approval, never unilaterally.
An eslint-disable or @ts-expect-error that was already in a touched file is the
same hit when it names a rule routed this session and sits on a function, component
or type this session changed, or on a module-level declaration in a touched module
(import/no-mutable-exports → R8 in a globals request; react-hooks/exhaustive-deps
→ R10; an @ts-expect-error on return undefined → R1): it suppresses the rule it
names, so route it as a finding of that rule and delete it with the fix.
"Pre-existing" and "unrelated" are not verdicts for those — a request to make the
linter pass without suppressions is met when the touched functions and the touched
modules' top-level declarations carry none of the routed rules' directives, not when
the one directive the request named is gone and its neighbours keep their // TODO.
A directive elsewhere in a touched file, or one naming a rule this session never
routed — sonarjs/cognitive-complexity → R3 on the function whose one global read
was just replaced — is a BROADER CONTEXT line under the Stop check block: reported
with its rule, not fixed in this session, not silent.
- Gates. Linter 0 issues; tests green; functions <50 LOC, nesting ≤2; no red-zone
packages. Line
1 gates: the four measurements. - Detection re-run. Re-run the detection commands of every rule routed this
session over the touched files (R8: the touched packages). A hit in a function or
type this session changed, or an R8 declaration in a touched package, is fixed —
routed again, "pre-existing" is no verdict; a hit anywhere else in a touched file, or
of a rule never routed, is reported as one
BROADER CONTEXTline (file:line — rule and question — what stands). Line2 re-run: per routed rule,0 hits, the anchor routed again, orn reported. - The noun check. For each concept the touched code handles: does it have a named
box? A slice walked with flags is a collection type over it (R1); a container of
primitives handed across a boundary and looked up or filtered by its receivers is
a type with no name (R1, Name the Container); an optional
collaborator is a Null Object default; a value parsed twice has one constructor; a
repeated predicate is a method. Score each with R1's scorecard: ≥4 apply; 2–3 apply
or record the judgment call; 0–1 leave. Line
3 nouns: each candidate with score and verdict —none scored ≥2when nothing qualified, never a blank. - The comment critic. Spawn one
ts-react-linter-driven-development:comment-critic(Agent tool, foreground) over the touched files, its spawn prompt carrying, as absolute paths,../../rules/R9-repo-brain.mdwithsed -n '/^### Comment policy/,/^### Edge conventions/p',../documentation/reference.mdwithsed -n '/^## Comment Value Toolbox/,/^## Frontmatter Templates/p', and../../examples/private-comment-noise.md, and the scope stated as every comment in each touched file. Apply its TRIM / REWRITE / DELETE verdicts; routeDELETE → route R3back to step 3. Line4 critic: verdicts applied and routed. - STOP, reading the over-engineering signs as a check on step 3: a one-method type
that merely unwraps, a function that only calls another, more layers than concepts
— undo that move. Line
5 STOP: none of the signs, or the move undone. - Commit. Tests and lint green and the tree dirty →
git commitwith the STATUS summary — one commit per green step when deployable steps were asked for (R8: one island and its caller per commit). Inside the workflow, Phase 5 commits the slice; a standalone invocation commits here, before the report. Line6 commit: the hash and message,Phase 5 commits the slice, ortree clean, nothing to commit.
The six lines are the block, and the block goes in the message that ends the turn, whichever path invoked this skill; a caller that summarises this work carries it and its BROADER CONTEXT lines verbatim. Prose narrating the steps in place of the six lines is the block missing, and a missing block means the refactoring did not finish.
``` REFACTORING APPLIEDFailures Routed:
- [linter] → [rule] → [move applied]: [what changed]
Types Created (R1 verdict): [Type] — [why juicy] → @testing invoked Types Rejected (not juicy): [Type] — [cheaper alternative used]
Metrics: cyclomatic [before]→[after], LOC [before]→[after], nesting [before]→[after] Files Modified: [file] (+X, -Y)
Stop check: 1 gates lint 0 · tests green · max LOC [n] · max nesting [n] 2 re-run [R3, R1, R8]: [0 hits / file.ts:NN still — routed again] 3 nouns [Region (score 5) → Replace Primitive with Domain Type applied; Tags (score 2) → recorded] 4 critic [n] verdicts applied · [n] DELETE → routed R3 5 STOP [none of the over-engineering signs / undone: ] 6 commit [abc1234 "" / Phase 5 commits the slice / tree clean, nothing to commit]
BROADER CONTEXT [file.ts:NN — R3 Q1 — // eslint-disable-next-line on an eight-linter function this session never opened / none]
STATUS: [linter green / still failing: N issues / escalated to @code-designing]
Every `Stop check` line renders, in this order, opening with its number and keyword
exactly as shown — `1 gates` … `6 commit` — with its result on the same line; a missing
line means the step did not run and the STATUS is not final. `BROADER CONTEXT` lists
every hit step 2 reported rather than fixed, or `none`. Who invokes this skill and what
it invokes: `sed -n '/^## Integration/,/^## Multi-rule procedures/p' <this skill dir>/reference.md`.
</output_format>