skills/refactor-architect/SKILL.md
Plans large-scale refactoring campaigns across codebases. Analyzes dependency graphs, calculates blast radius, designs migration strategies, and creates incremental plans that keep the system deployable at every step. The strategist to the refactoring-surgeon's operator.
npx skillsauth add curiositech/windags-skills refactor-architectInstall 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.
Plans and coordinates large-scale refactoring campaigns. Analyzes dependency graphs to determine safe execution order, designs migration strategies that keep the system deployable at every step, calculates blast radius of proposed changes, and creates incremental plans that multiple developers or agents can execute in parallel.
Use this skill when:
Do NOT use this skill for:
Before touching anything, map what depends on what.
// dependency-analysis.ts — Extract import graph for blast radius calculation
import { readFileSync } from 'node:fs';
import { globSync } from 'glob';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import path from 'node:path';
interface DepGraph {
/** file -> files it imports */
imports: Map<string, Set<string>>;
/** file -> files that import it */
importedBy: Map<string, Set<string>>;
}
function buildDepGraph(rootDir: string, pattern = '**/*.{ts,tsx}'): DepGraph {
const files = globSync(pattern, { cwd: rootDir, absolute: true });
const imports = new Map<string, Set<string>>();
const importedBy = new Map<string, Set<string>>();
for (const file of files) {
const source = readFileSync(file, 'utf8');
const deps = new Set<string>();
try {
const ast = parse(source, {
sourceType: 'module',
plugins: ['typescript', 'jsx'],
});
traverse(ast, {
ImportDeclaration({ node }) {
const resolved = resolveImport(node.source.value, file, rootDir);
if (resolved) deps.add(resolved);
},
CallExpression({ node }) {
if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
const arg = node.arguments[0];
if (arg?.type === 'StringLiteral') {
const resolved = resolveImport(arg.value, file, rootDir);
if (resolved) deps.add(resolved);
}
}
},
});
} catch {
// Parse errors are noted but don't block analysis
}
imports.set(file, deps);
for (const dep of deps) {
if (!importedBy.has(dep)) importedBy.set(dep, new Set());
importedBy.get(dep)!.add(file);
}
}
return { imports, importedBy };
}
function resolveImport(specifier: string, fromFile: string, rootDir: string): string | null {
if (specifier.startsWith('.')) {
const dir = path.dirname(fromFile);
// Try common extensions
for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx']) {
const candidate = path.resolve(dir, specifier + ext);
try { readFileSync(candidate); return candidate; } catch { /* try next */ }
}
}
return null; // External package, not in our graph
}
Given a set of files you intend to change, determine everything downstream:
function calculateBlastRadius(graph: DepGraph, changedFiles: string[]): BlastRadius {
const directlyAffected = new Set<string>();
const transitivelyAffected = new Set<string>();
const queue = [...changedFiles];
const visited = new Set<string>();
// BFS through importedBy graph
while (queue.length > 0) {
const file = queue.shift()!;
if (visited.has(file)) continue;
visited.add(file);
const dependents = graph.importedBy.get(file) || new Set();
for (const dep of dependents) {
if (changedFiles.includes(file)) {
directlyAffected.add(dep);
} else {
transitivelyAffected.add(dep);
}
queue.push(dep);
}
}
return {
changedFiles,
directlyAffected: [...directlyAffected],
transitivelyAffected: [...transitivelyAffected],
totalAffected: visited.size - changedFiles.length,
riskLevel: categorizeRisk(visited.size),
};
}
interface BlastRadius {
changedFiles: string[];
directlyAffected: string[];
transitivelyAffected: string[];
totalAffected: number;
riskLevel: 'low' | 'medium' | 'high' | 'critical';
}
function categorizeRisk(affectedCount: number): BlastRadius['riskLevel'] {
if (affectedCount <= 5) return 'low';
if (affectedCount <= 20) return 'medium';
if (affectedCount <= 50) return 'high';
return 'critical';
}
Blast radius report format:
=== Blast Radius Report ===
Target: Rename UserService.getUser() -> UserService.findUser()
Changed files (1):
src/services/user-service.ts
Directly affected (8):
src/routes/user-routes.ts
src/routes/admin-routes.ts
src/middleware/auth.ts
src/controllers/user-controller.ts
src/controllers/admin-controller.ts
src/jobs/user-sync.ts
src/jobs/cleanup.ts
tests/services/user-service.test.ts
Transitively affected (12):
src/routes/index.ts
src/app.ts
tests/routes/user-routes.test.ts
tests/routes/admin-routes.test.ts
...
Risk level: HIGH (21 files affected)
Recommendation: Split into 3 PRs over 2 days.
PR 1: Add findUser() as alias, deprecate getUser() [0 breakage]
PR 2: Migrate all callers to findUser() [0 breakage]
PR 3: Remove getUser() [0 breakage if PR 2 is complete]
Topological sort of the dependency graph gives you the order in which files can be safely modified without breaking intermediate states:
function computeSafeRefactoringOrder(
graph: DepGraph,
affectedFiles: string[]
): RefactoringWave[] {
// Build sub-graph of only affected files
const inDegree = new Map<string, number>();
const subEdges = new Map<string, Set<string>>();
for (const file of affectedFiles) {
inDegree.set(file, 0);
subEdges.set(file, new Set());
}
for (const file of affectedFiles) {
const deps = graph.imports.get(file) || new Set();
for (const dep of deps) {
if (affectedFiles.includes(dep)) {
subEdges.get(dep)!.add(file);
inDegree.set(file, (inDegree.get(file) || 0) + 1);
}
}
}
// Kahn's algorithm — group into waves (same as DAG wave computation)
const waves: RefactoringWave[] = [];
let remaining = new Set(affectedFiles);
while (remaining.size > 0) {
const wave = [...remaining].filter(f => (inDegree.get(f) || 0) === 0);
if (wave.length === 0) {
// Cycle detected — these files have circular dependencies
waves.push({ files: [...remaining], parallel: false, note: 'CIRCULAR — manual ordering required' });
break;
}
waves.push({ files: wave, parallel: true, note: `Wave ${waves.length + 1}: safe to modify in parallel` });
for (const file of wave) {
remaining.delete(file);
for (const dependent of subEdges.get(file) || []) {
inDegree.set(dependent, (inDegree.get(dependent) || 0) - 1);
}
}
}
return waves;
}
interface RefactoringWave {
files: string[];
parallel: boolean;
note: string;
}
Output example:
=== Refactoring Order (4 waves) ===
Wave 1 (parallel-safe):
src/types/user.ts — Change the type definition first
src/types/admin.ts — Also independent
Wave 2 (parallel-safe):
src/services/user-service.ts — Update service (depends on types)
src/services/admin-service.ts — Update service (depends on types)
Wave 3 (parallel-safe):
src/controllers/user-controller.ts — Update consumers of services
src/controllers/admin-controller.ts
src/middleware/auth.ts
src/jobs/user-sync.ts
Wave 4 (parallel-safe):
src/routes/user-routes.ts — Update route wiring (depends on controllers)
src/routes/admin-routes.ts
tests/**/*.test.ts — Update tests last (they verify the new state)
The safest pattern for API changes. Three phases, each independently deployable:
Phase 1: EXPAND — Add the new alongside the old
- New method/field/endpoint exists
- Old method/field/endpoint still works
- All tests pass, nothing breaks
Phase 2: MIGRATE — Move consumers to the new
- Update all callers one by one
- Each caller migration is a separate commit
- Old method/field/endpoint still works (for safety)
Phase 3: CONTRACT — Remove the old
- Delete deprecated method/field/endpoint
- Remove any shim/adapter code
- Final cleanup
For replacing an entire module or subsystem:
1. Identify the boundary (imports into and out of the module)
2. Create the new module with the same public interface
3. Route traffic/calls through a facade that delegates:
- Initially 100% to old module
- Gradually shift to new module (feature flag or config)
- Monitor for errors at each percentage
4. When new module handles 100%, remove old module and facade
For replacing a dependency that is deeply woven into the codebase:
1. Create an abstraction layer (interface) in front of the old dependency
2. Make all existing code use the abstraction instead of the dependency directly
3. Create a second implementation of the abstraction using the new dependency
4. Switch the implementation (DI container, factory, config flag)
5. Remove the old implementation and the abstraction (if only one impl remains)
For schema changes that require coordinated app changes:
Step 1: Deploy app that WRITES to both old and new schema
Step 2: Backfill — migrate existing data to new schema
Step 3: Deploy app that READS from new schema
Step 4: Deploy app that stops WRITING to old schema
Step 5: Drop old columns/tables
NEVER: Change schema and app code in the same deployment.
When the refactoring is large enough to parallelize across agents or developers:
# refactoring-plan.yaml
campaign: rename-getUser-to-findUser
strategy: expand-contract
estimated_waves: 4
estimated_duration: 2 days
phases:
- name: expand
description: Add findUser() as an alias for getUser()
files:
- src/services/user-service.ts
verification: npm test
pr_title: "refactor: add findUser() alias (expand phase)"
- name: migrate-wave-1
description: Migrate controllers and middleware
parallel: true
files:
- src/controllers/user-controller.ts
- src/controllers/admin-controller.ts
- src/middleware/auth.ts
verification: npm test
depends_on: expand
pr_title: "refactor: migrate controllers to findUser()"
- name: migrate-wave-2
description: Migrate background jobs and tests
parallel: true
files:
- src/jobs/user-sync.ts
- src/jobs/cleanup.ts
- tests/services/user-service.test.ts
- tests/controllers/*.test.ts
verification: npm test
depends_on: migrate-wave-1
pr_title: "refactor: migrate jobs and tests to findUser()"
- name: contract
description: Remove deprecated getUser()
files:
- src/services/user-service.ts
verification: |
npm test
grep -r "getUser" src/ --include="*.ts" && exit 1 || exit 0
depends_on: migrate-wave-2
pr_title: "refactor: remove deprecated getUser()"
When reorganizing modules (moving files, splitting packages), the import graph must be systematically updated. Approach:
# 1. Find all imports of the file being moved
grep -rn "from ['\"].*old-path" src/ --include="*.ts" --include="*.tsx"
# 2. Generate a sed script for the rename
find src -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
"s|from ['\"]\.\.\/services\/user-service['\"]|from '../services/identity/user-service'|g"
# 3. Verify no broken imports
npx tsc --noEmit 2>&1 | grep "Cannot find module" || echo "All imports resolved"
# 4. Verify no circular dependencies introduced
npx madge --circular src/
Automated verification after each wave:
tsc --noEmit -- type checker confirms all imports resolvenpx madge --circular src/ -- no new circular dependenciesnpm test -- behavior is unchangedgit diff --stat -- confirms only expected files changed| Factor | Expand-Contract | Big Bang | |--------|----------------|----------| | Change scope | > 10 files | < 5 files, all in one module | | Deployment risk | Low (each phase is safe) | High (all or nothing) | | Team coordination | Multiple developers/agents | Single developer | | Rollback cost | Trivial (revert one phase) | Painful (revert everything) | | Duration | Days to weeks | Hours | | When to use | Production systems, shared APIs | Internal tools, early-stage code |
npx madge --circular src/npx madge --circular src/ should show one fewer cycleSymptom: "Let's just rename this interface everywhere" without knowing what "everywhere" means Why wrong: You will miss transitive consumers, break CI for other teams, and spend days debugging Fix: Build or generate the dependency graph first. Every refactoring plan starts with blast radius.
Symptom: A 3000-line PR titled "refactor: modernize user module" Why wrong: Unreviewable, unrevertable, blocks the main branch for days Fix: Split by wave. Each wave is a PR. Each PR is reviewable in 15 minutes.
Symptom: Renaming a function and updating all callers in one commit Why wrong: If any caller was missed (dynamic imports, config files, scripts), the deploy breaks Fix: Always add the new name alongside the old, migrate callers, then remove the old name.
Symptom: "We'll run tests after the whole refactoring is done"
Why wrong: When tests fail, you don't know which wave introduced the failure
Fix: npm test after every wave. If tests fail, fix before moving to the next wave.
Symptom: Modifying a service's public API and its internal logic in the same PR Why wrong: If a bug appears, you can't tell if it's from the API change or the logic change Fix: Separate PRs. First change the interface shape (with the old behavior). Then change the behavior.
Symptom: Renaming an API endpoint without checking who calls it Fix: Check API access logs, search for the endpoint in other repositories, notify consumers before the contract phase.
Symptom: A 50-page refactoring document that becomes stale after the first wave Fix: Plan 2-3 waves ahead. After each wave, reassess. The dependency graph may change as you refactor.
tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.