
Qa Refactoring
- 164 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
Helps with code review & quality tasks.
About
qa-refactoring is a Claude Code skill for code review & quality. It helps solo builders move faster with AI-assisted development.
- qa-refactoring
- Code Review & Quality
- AI-coding skill
Qa Refactoring by the numbers
- 164 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #365 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill qa-refactoringAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 164 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
What it does
Helps with code review & quality tasks.
Files
QA Refactoring Safety
Use this skill to refactor safely: preserve behavior, reduce risk, and keep CI green while improving maintainability and delivery speed.
Defaults: baseline first, smallest safe step next, and proof via tests/contracts/observability instead of intuition.
Quick Start (10 Minutes)
- If key context is missing, ask for: what must not change (invariants), risk level (money/auth/migrations/concurrency), deployment constraints, and the smallest boundary that can be protected by tests.
- Confirm baseline:
maingreen; reproduce the behavior you must preserve. - Choose a boundary: API surface, module boundary, DB boundary, or request handler.
- Add a safety net: characterization/contract/integration tests at that boundary.
- Refactor in micro-steps: one behavior-preserving change per commit/PR chunk.
- Prove: run the smallest relevant suite locally, then full CI; keep failures deterministic.
Core QA (Default)
Safe Refactor Loop (Behavior First)
- Establish baseline: get
maingreen; reproduce the behavior you must preserve. - Define invariants: inputs/outputs, error modes, permissions, data shape, performance budgets.
- Add a safety net: write characterization/contract/integration tests around the boundary you will touch.
- Create seams: introduce injection points/adapters to isolate side effects and external dependencies.
- Refactor in micro-steps: one behavior-preserving change at a time; keep diffs reviewable.
- Prove: run the smallest relevant suite locally, then full CI; keep failures debuggable and deterministic.
- Ship safely: use canary/dark launch/feature flags when refactors touch production-critical paths.
Risk Levels (Choose Safety Net)
| Risk | Examples | Minimum required safety net |
|---|---|---|
| Low | rename, extract method, formatting-only | unit tests + lint/type checks |
| Medium | moving logic across modules, dependency inversion | unit + integration/contract tests at boundary |
| High | auth/permission paths, concurrency, migrations, money/data-loss paths | integration + contract tests, observability checks, canary + rollback plan |
Test Strategy for Refactors
- Prefer contract and integration tests around boundaries to preserve behavior.
- Use snapshots/golden masters only when outputs are stable and reviewed (avoid "approve everything" loops).
- For invariants, consider property-based tests or table-driven cases (inputs, edge cases, error modes).
- Avoid making E2E/UI tests the primary safety net for refactors; keep most safety below the UI.
- For flaky areas: fix determinism first (seeds, time, ordering, network) before trusting results.
CI Economics and Debugging Ergonomics
- Keep refactor PRs small and reviewable; avoid refactor + feature in one PR.
- Require failure artifacts for tests guarding refactors (logs, trace IDs, deterministic seeds, repro steps).
- Reduce diff noise: isolate formatting-only changes (or apply formatting repo-wide once with buy-in).
- Keep
git bisectviable: avoid mixed "mechanical + semantic" changes unless necessary.
Do / Avoid
Do:
- Add missing tests before refactoring high-risk areas.
- Add guardrails (linters, type checks, contract checks, static analysis/security checks) so refactors don't silently break interfaces.
- Prefer "branch by abstraction" / adapters when you need to swap implementations safely.
Avoid:
- Combining large structural refactors with behavior changes.
- Using flaky E2E as the primary safety net for refactors.
Quick Reference
| Task | Tool/Pattern | Command/Approach | When to Use |
|---|---|---|---|
| Long method (>50 lines) | Extract Method | Split into smaller functions | Single method does too much |
| Large class (>300 lines) | Split Class | Create focused single-responsibility classes | God object doing too much |
| Duplicated code | Extract Function/Class | DRY principle | Same logic in multiple places |
| Complex conditionals | Replace Conditional with Polymorphism | Use inheritance/strategy pattern | Switch statements on type |
| Long parameter list | Introduce Parameter Object | Create DTO/config object | Functions with >3 parameters |
| Legacy code modernization | Characterization Tests + Strangler Fig | Write tests first, migrate incrementally | No tests, old codebase |
| Automated quality gates | ESLint, SonarQube, Prettier | npm run lint, CI/CD pipeline | Prevent quality regression |
| Technical debt tracking | SonarQube, CodeClimate | Track trends + hotspots | Prioritize refactoring work |
Decision Tree: Refactoring Strategy
Code issue: [Refactoring Scenario]
├─ Code Smells Detected?
│ ├─ Duplicated code? → Extract method/function
│ ├─ Long method (>50 lines)? → Extract smaller methods
│ ├─ Large class (>300 lines)? → Split into focused classes
│ ├─ Long parameter list? → Parameter object
│ └─ Feature envy? → Move method closer to data
│
├─ Legacy Code (No Tests)?
│ ├─ High risk? → Write characterization tests first
│ ├─ Large rewrite needed? → Strangler Fig (incremental migration)
│ ├─ Unknown behavior? → Characterization tests + small refactors
│ └─ Production system? → Canary deployments + monitoring
│
├─ Quality Standards?
│ ├─ New project? → Setup linter + formatter + quality gates
│ ├─ Existing project? → Add pre-commit hooks + CI checks
│ ├─ Complexity issues? → Set cyclomatic complexity limits (<10)
│ └─ Technical debt? → Track in register, 20% sprint capacityRelated Skills
- Debugging production issues: qa-debugging
- Code review process and checklists: software-code-review
- New architecture design from scratch: software-architecture-design
- Test strategy and coverage planning: qa-testing-strategy
Scope Boundaries (Handoffs)
- Pure test flake cleanup (timers, ordering, retries):
../qa-debugging/SKILL.md - Pure performance tuning (SQL, indexing, query plans):
../data-sql-optimization/SKILL.md - Architecture redesign decisions (service boundaries, eventing):
../software-architecture-design/SKILL.md
Operational Deep Dives
Shared Foundation
- ../software-clean-code-standard/references/clean-code-standard.md - Canonical clean code rules (
CC-*) for citation - Legacy playbook: ../software-clean-code-standard/references/code-quality-operational-playbook.md -
RULE-01–RULE-13, decision trees, and operational procedures - ../software-clean-code-standard/references/refactoring-operational-checklist.md - Refactoring smell-to-action mapping, safe refactoring guardrails
- ../software-clean-code-standard/references/working-effectively-with-legacy-code-operational-checklist.md - Seams, characterization tests, incremental migration patterns
Skill-Specific
See references/operational-patterns.md for detailed refactoring catalogs, automated quality gates, technical debt playbooks, and legacy modernization steps.
Templates
Use copy-paste templates in assets/ for checklists and quality-gate configs:
- Refactoring: assets/process/refactoring-checklist.md, assets/process/code-review-quality.md
- Technical debt: assets/tracking/tech-debt-register.md
- Quality gates: assets/quality-gates/javascript/eslint-config.js, assets/quality-gates/platform-agnostic/sonarqube-setup.md
Resources
Use deep-dive guides in references/ (load only what you need):
- Operational Patterns: references/operational-patterns.md - Core refactoring catalogs, quality gates, and legacy modernization
- Refactoring Catalog: references/refactoring-catalog.md
- Code Smells Guide: references/code-smells-guide.md
- Technical Debt Management: references/tech-debt-management.md
- Legacy Code Modernization: references/legacy-code-strategies.md
- Characterization Testing: references/characterization-testing.md - Golden master and approval testing patterns
- Strangler Fig Migration: references/strangler-fig-migration.md - Incremental legacy migration strategies
- Automated Refactoring Tools: references/automated-refactoring-tools.md - Codemods, AST transforms, and IDE refactoring
Optional: AI / Automation
Do:
- Use AI to propose mechanical refactors (rename/extract/move) only when you can prove behavior preservation via tests and contracts.
- Use AI to summarize diffs and risk hotspots; verify by running targeted characterization tests.
- Prefer tool-assisted refactors (IDE/compiler-aware, codemods) over freeform text edits when available.
Avoid:
- Accepting refactors that change behavior without an explicit requirement and regression tests.
- Letting AI "fix tests" by weakening assertions to make CI green.
See data/sources.json for curated external references.
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Code Review Quality Checklist
Copy-paste checklist for reviewing code quality, maintainability, and technical debt.
---
Quick Review (5 minutes)
Fast pass for obvious issues:
- [ ] Linter passes - no warnings
- [ ] Tests pass - all green
- [ ] Builds successfully - no errors
- [ ] No debug code - console.log, debugger, print statements removed
- [ ] No commented-out code
- [ ] No TODOs for critical functionality
- [ ] Formatting consistent - follows project style
---
Comprehensive Review (30 minutes)
Code Smells
Bloaters
- [ ] No long methods (>20 lines)
- If found: Request Extract Method refactoring
- [ ] No large classes (>300 lines)
- If found: Request Extract Class refactoring
- [ ] No long parameter lists (>3 parameters)
- If found: Suggest Introduce Parameter Object
- [ ] No primitive obsession (string/number for domain concepts)
- If found: Suggest value objects (Email, Money, etc.)
- [ ] No data clumps (same group of variables repeated)
- If found: Suggest Extract Class
Object-Orientation Abusers
- [ ] No switch statements on type codes
- If found: Suggest Replace Conditional with Polymorphism
- [ ] No temporary fields (fields only used sometimes)
- If found: Suggest Extract Class or Remove Field
- [ ] No refused bequest (subclass not using parent's methods)
- If found: Suggest Replace Inheritance with Delegation
Change Preventers
- [ ] No divergent change (class changed for many reasons)
- If found: Violates Single Responsibility Principle
- [ ] No shotgun surgery (change requires updates in many classes)
- If found: Suggest Move Method or Inline Class
Dispensables
- [ ] No unnecessary comments (code should be self-explanatory)
- [ ] No duplicate code
- If found: Suggest Extract Method or Extract Class
- [ ] No lazy classes (classes doing too little)
- If found: Suggest Inline Class or Remove Class
- [ ] No dead code (unused variables, methods, classes)
- If found: Request removal
- [ ] No speculative generality ("in case we need it")
- If found: Suggest YAGNI, remove unused abstractions
Couplers
- [ ] No feature envy (method uses more of another class)
- If found: Suggest Move Method
- [ ] No inappropriate intimacy (classes too dependent)
- If found: Suggest Extract Class or Hide Delegate
- [ ] No message chains (a.getB().getC().getD())
- If found: Violates Law of Demeter, suggest Hide Delegate
- [ ] No middle man (class just delegates to another)
- If found: Suggest Remove Middle Man or Inline Class
---
Code Quality Metrics
Complexity
- [ ] Cyclomatic complexity < 10 per method
- Tool: ESLint
complexityrule, SonarQube - If >10: Request simplification
- [ ] Cognitive complexity < 15 per method
- Measures understandability
- If >15: Hard to understand, request refactoring
- [ ] Nesting depth < 3 levels
- If deeper: Use guard clauses or Extract Method
Size Metrics
- [ ] Method length < 20 lines
- If longer: Suggest Extract Method
- [ ] Class length < 300 lines
- If longer: Suggest Extract Class
- [ ] File length < 500 lines
- If longer: Consider splitting into modules
- [ ] Line length < 120 characters
- If longer: Break into multiple lines
Maintainability
- [ ] No magic numbers - all constants named
// BAD: Bad
if (age > 18) { /* ... */ }
// GOOD: Good
const LEGAL_ADULT_AGE = 18;
if (age > LEGAL_ADULT_AGE) { /* ... */ }- [ ] Meaningful names - variables, methods, classes
// BAD: Bad
const x = users.filter(u => u.a > 18);
// GOOD: Good
const adultUsers = users.filter(user => user.age > 18);- [ ] No abbreviations unless well-known (e.g., URL, HTTP)
- [ ] Consistent naming across codebase
---
Design Principles
SOLID Principles
- [ ] Single Responsibility Principle
- Each class/method has one reason to change
- If violated: Class does too much, suggest splitting
- [ ] Open/Closed Principle
- Open for extension, closed for modification
- If violated: Suggest strategy pattern or inheritance
- [ ] Liskov Substitution Principle
- Subtypes should be substitutable for base types
- If violated: Check subclass overrides
- [ ] Interface Segregation Principle
- Many specific interfaces > one general interface
- If violated: Clients forced to depend on unused methods
- [ ] Dependency Inversion Principle
- Depend on abstractions, not concretions
- If violated: Hard-coded dependencies, suggest injection
DRY (Don't Repeat Yourself)
- [ ] No duplicate code in same file
- [ ] No duplicate code across files
- [ ] No duplicate logic with slight variations
- If found: Extract shared logic, parameterize differences
YAGNI (You Aren't Gonna Need It)
- [ ] No premature abstractions
- [ ] No unused parameters "for future use"
- [ ] No overly complex designs for simple problems
- [ ] No feature flags for features not coming
KISS (Keep It Simple, Stupid)
- [ ] Simplest solution that works
- [ ] No unnecessary complexity
- [ ] No over-engineering
---
Testing
Test Coverage
- [ ] New code has tests
- [ ] Unit tests for business logic
- [ ] Integration tests for external dependencies
- [ ] E2E tests for critical user flows
- [ ] Test coverage > 80% for new code
- Critical paths: 100%
- Business logic: 90%+
- Overall: 80%+
Test Quality
- [ ] Tests are independent - can run in any order
- [ ] Tests are deterministic - same result every time
- [ ] Tests are fast - unit tests < 1s each
- [ ] Tests have clear names - describe what they test
// BAD: Bad
it('test1', () => { /* ... */ });
// GOOD: Good
it('should return 400 when email is invalid', () => { /* ... */ });- [ ] Tests use AAA pattern - Arrange, Act, Assert
- [ ] No test duplication - use helper functions
- [ ] Mocks used appropriately - only for external dependencies
---
Security
Common Vulnerabilities
- [ ] No SQL injection - use parameterized queries
// BAD: Bad
db.query(`SELECT * FROM users WHERE id = ${userId}`);
// GOOD: Good
db.query('SELECT * FROM users WHERE id = ?', [userId]);- [ ] No XSS vulnerabilities - escape user input
// BAD: Bad
element.innerHTML = userInput;
// GOOD: Good
element.textContent = userInput;
// or use DOMPurify for HTML- [ ] No command injection - validate inputs
- [ ] No hardcoded secrets - use environment variables
- [ ] No sensitive data in logs
- [ ] No weak crypto - use industry standards (AES-256, bcrypt)
- [ ] Input validation on all user inputs
- [ ] Authentication/authorization implemented correctly
---
Performance
Potential Issues
- [ ] No N+1 queries - use eager loading
// BAD: Bad
const users = await User.findAll();
for (const user of users) {
user.orders = await Order.findByUserId(user.id); // N queries
}
// GOOD: Good
const users = await User.findAll({ include: [Order] }); // 1 query- [ ] No unnecessary database calls - cache if appropriate
- [ ] No memory leaks - clean up listeners, intervals
- [ ] Efficient algorithms - not O(n²) when O(n) possible
- [ ] No blocking operations in async code
- [ ] Proper indexing for database queries
---
Error Handling
- [ ] Errors handled appropriately
- Don't swallow errors silently
- Log errors with context
- Return meaningful error messages
- [ ] No catch-all handlers without re-throwing
// BAD: Bad
try {
await doSomething();
} catch (error) {
console.log(error); // Swallowed!
}
// GOOD: Good
try {
await doSomething();
} catch (error) {
logger.error('Failed to do something', { error });
throw new ApplicationError('Operation failed', error);
}- [ ] Specific error types - not generic Error
- [ ] Error messages are user-friendly (for user-facing errors)
- [ ] Stack traces preserved when re-throwing
---
Documentation
- [ ] Public APIs documented - JSDoc, TSDoc, etc.
/**
* Calculates user's total order value.
*
* @param userId - The user's unique identifier
* @param startDate - Filter orders from this date
* @param endDate - Filter orders until this date
* @returns Total order value in cents
* @throws {UserNotFoundError} If user doesn't exist
*/
async function calculateTotalOrders(
userId: string,
startDate: Date,
endDate: Date
): Promise<number> {
// ...
}- [ ] Complex logic explained - why, not what
// BAD: Bad
// Multiply by 1.1
const price = basePrice * 1.1;
// GOOD: Good
// Apply 10% VAT as required by EU regulations
const VAT_RATE = 1.1;
const price = basePrice * VAT_RATE;- [ ] README updated if architecture changed
- [ ] Breaking changes documented
---
Automated Checks
Use these tools to automate quality checks:
Linters
- [ ] ESLint (JavaScript/TypeScript)
{
"extends": ["eslint:recommended"],
"rules": {
"complexity": ["error", 10],
"max-lines": ["error", 300],
"max-lines-per-function": ["error", 20],
"max-params": ["error", 3],
"max-depth": ["error", 3]
}
}- [ ] Pylint (Python)
- [ ] RuboCop (Ruby)
- [ ] Clippy (Rust)
Code Quality Tools
- [ ] SonarQube - comprehensive analysis
- [ ] CodeClimate - maintainability metrics
- [ ] Embold - anti-pattern detection
Security Scanners
- [ ] npm audit / yarn audit (JavaScript)
- [ ] Snyk - dependency vulnerabilities
- [ ] OWASP Dependency-Check
---
Review Comments Template
For Code Smells
**Code Smell: Long Method**
This method is 50 lines long, which makes it hard to understand and test.
Suggestion: Extract the validation logic into a separate `validateInput()` method and the calculation logic into `calculateTotal()`.
References:
- [Refactoring: Extract Method](https://refactoring.guru/extract-method)
- See references/refactoring-catalog.mdFor Complexity Issues
**High Complexity: 15**
This method has cyclomatic complexity of 15, which is above our threshold of 10.
Suggestion: Break down the nested conditionals using guard clauses, or use the Strategy pattern if this is polymorphic behavior.
Tool output:eslint: complexity: Method 'processOrder' has complexity 15 (max 10)
For Missing Tests
**Missing Test Coverage**
The new `PaymentProcessor` class has 0% test coverage.
Suggestion: Add unit tests covering:
- [ ] Happy path (successful payment)
- [ ] Error handling (failed payment)
- [ ] Edge cases (zero amount, negative amount)
Target coverage: 80%+---
Approval Criteria
Code is approved when ALL of these are true:
- [ ] No critical issues (security, bugs)
- [ ] All automated checks pass (linter, tests, build)
- [ ] No major code smells (God objects, high complexity)
- [ ] Test coverage sufficient (>80% for new code)
- [ ] Documentation adequate
- [ ] Performance acceptable
- [ ] Follows team conventions
---
Technical Debt Assessment
If code has quality issues but must be merged:
Document Technical Debt
**Technical Debt Created**
Issue: UserService class is 450 lines (>300 line limit)
Reason: Time-sensitive feature needed for demo
Impact: High (difficult to maintain)
Effort to fix: 1 day
Plan: Refactor in sprint 23 (TD-042)
Priority: P1 (high impact, low effort)Track in Debt Register
Add to technical debt register:
- ID: TD-XXX
- Description: Issue summary
- Type: Reckless/Prudent, Deliberate/Inadvertent
- Impact: High/Medium/Low
- Effort: Days to fix
- Priority: P1/P2/P3/P4
- Owner: Who will fix it
- Target sprint: When it will be addressed
---
Summary Template
## Code Review Summary
**Overall**: [Approve / Request Changes / Reject]
**Positives**:
- [check] Good test coverage (85%)
- [check] Clean separation of concerns
- [check] Clear naming conventions
**Issues Found**:
- [FAIL] 3 methods exceed complexity threshold
- [WARNING] 1 missing error handler
- [WARNING] 2 minor code smells
**Action Items**:
1. Reduce complexity in `processOrder()` (Priority: High)
2. Add error handling in `validateInput()` (Priority: High)
3. Extract duplicated validation logic (Priority: Low)
**Technical Debt**:
- None created [check]
**Estimated Fix Time**: 2 hoursRefactor Safety Checklist (Characterization + Incremental Steps)
Copy-paste checklist for safe refactoring that preserves behavior and reduces regression risk.
Core
Pre-Refactoring Checklist
Before starting any refactoring:
- [ ] Safety net exists for code being refactored
- [ ] If tests already exist: identify which tests guard the behavior
- [ ] If tests are missing: add characterization tests for current behavior (Feathers: https://michaelfeathers.silvrback.com/characterization-testing)
- [ ] Flaky tests addressed (fix or quarantine with owner + expiry)
- [ ] Version control is up to date
- [ ] All changes committed
- [ ] Working on feature branch
- [ ] Branch is up to date with main
- [ ] Baseline metrics recorded
- [ ] Current lines of code
- [ ] Cyclomatic complexity
- [ ] Code coverage percentage
- [ ] SonarQube debt ratio (if available)
- [ ] Refactoring scope is defined
- [ ] Specific files/classes identified
- [ ] Clear goal stated (e.g., "reduce complexity")
- [ ] Time-boxed (e.g., "2 hours max")
- [ ] PR plan is safe
- [ ] Refactor-only PR (no feature changes mixed in)
- [ ] Rollback strategy defined (revertable commits)
- [ ] Stakeholders informed
- [ ] Team aware of refactoring session
- [ ] No conflicting work on same files
---
During Refactoring Checklist
Every 15-30 Minutes
- [ ] Run tests after each small change
- [ ] Commit working code with descriptive message
- [ ] Verify behavior hasn't changed
- [ ] No new failing tests
- [ ] No performance degradation
- [ ] Same output for same input
Code Quality Checks
Method-Level Refactoring
- [ ] Method length < 20 lines
- [ ] Method complexity < 10 (cyclomatic)
- [ ] Single responsibility - method does one thing
- [ ] Meaningful name - describes what it does
- [ ] Parameters < 4 (use parameter objects if more)
- [ ] No side effects unless clearly named (e.g.,
saveAndNotify) - [ ] Comments removed if code is self-explanatory
- [ ] Magic numbers extracted to named constants
Class-Level Refactoring
- [ ] Class length < 300 lines
- [ ] Single responsibility - one reason to change
- [ ] Low coupling - minimal dependencies on other classes
- [ ] High cohesion - related functionality grouped
- [ ] Meaningful name - describes purpose
- [ ] No God objects - not doing too much
- [ ] Fields encapsulated - private with getters/setters if needed
- [ ] No duplicate code within class
Code Smell Removal
- [ ] Duplicate code extracted to shared method
- [ ] Long parameter lists replaced with parameter objects
- [ ] Large classes split into focused classes
- [ ] Switch statements replaced with polymorphism (if appropriate)
- [ ] Primitive obsession replaced with value objects
- [ ] Feature envy fixed by moving method to proper class
- [ ] Temporary fields eliminated
- [ ] Dead code removed
---
Post-Refactoring Checklist
Verification
- [ ] All tests pass
- [ ] Unit tests: [check]
- [ ] Integration tests: [check]
- [ ] E2E tests: [check]
- [ ] Code coverage maintained or improved
- [ ] Before: ____%
- [ ] After: ____%
- [ ] Performance unchanged or improved
- [ ] Run performance benchmarks
- [ ] Check memory usage
- [ ] Verify response times
- [ ] Linter passes with no new warnings
- [ ] Type checker passes (if applicable)
Metrics Improvement
- [ ] Complexity reduced
- [ ] Before: _____
- [ ] After: _____
- [ ] Lines of code (should decrease or stay same)
- [ ] Before: _____
- [ ] After: _____
- [ ] Code duplication reduced
- [ ] Before: ____%
- [ ] After: ____%
- [ ] Technical debt reduced (SonarQube)
- [ ] Before: _____
- [ ] After: _____
Documentation
- [ ] Commit message describes refactoring
- Example: "Refactor UserService: extract validation logic, reduce complexity from 25 to 8"
- [ ] PR description explains changes
- Why refactoring was needed
- What changed
- Metrics before/after
- [ ] Code comments updated if needed
- [ ] README updated if architecture changed
- [ ] Technical debt register updated
Code Review Preparation
- [ ] Self-review completed
- [ ] Check diff for unintended changes
- [ ] Verify no debug code left
- [ ] Ensure consistent formatting
- [ ] Tests demonstrate refactoring didn't break functionality
- [ ] Screenshots/metrics show improvement
- [ ] Reviewers assigned
---
Specific Refactoring Patterns
Extract Method
- [ ] Identified code block that can be grouped
- [ ] Created method with descriptive name
- [ ] Moved code to new method
- [ ] Replaced original code with method call
- [ ] Verified tests still pass
- [ ] No duplicate code created
Rename Variable/Method/Class
- [ ] New name is more descriptive
- [ ] New name follows naming conventions
- [ ] All references updated
- [ ] Tests still pass
- [ ] Documentation updated
Extract Class
- [ ] Identified cohesive group of methods/fields
- [ ] Created new class with clear responsibility
- [ ] Moved methods/fields to new class
- [ ] Updated original class to use new class
- [ ] Tests still pass
- [ ] Both classes have single responsibility
Replace Conditional with Polymorphism
- [ ] Identified type-based conditional logic
- [ ] Created interface or abstract class
- [ ] Created concrete subclasses for each type
- [ ] Moved type-specific logic to subclasses
- [ ] Replaced conditional with polymorphic call
- [ ] Tests still pass
Introduce Parameter Object
- [ ] Identified related parameters (3+)
- [ ] Created parameter object class
- [ ] Updated method signature
- [ ] Updated all call sites
- [ ] Tests still pass
- [ ] Code is more readable
---
Emergency Rollback Checklist
If refactoring causes issues:
- [ ] Stop immediately - don't add more changes
- [ ] Identify issue
- Which test is failing?
- What behavior changed?
- [ ] Options:
- [ ] Quick fix (< 15 minutes)
- [ ] Revert last commit
- [ ] Revert entire refactoring branch
- [ ] After rollback:
- [ ] Understand what went wrong
- [ ] Plan safer approach
- [ ] Add more tests before trying again
---
Boy Scout Rule Checklist
When touching existing code (not dedicated refactoring session):
- [ ] Leave code better than you found it
- [ ] Fix at least one code smell
- [ ] Add at least one test if missing
- [ ] Improve at least one variable name
- [ ] Extract at least one magic number to constant
- [ ] Remove at least one comment by making code self-explanatory
- [ ] Changes are small and safe
- [ ] Changes don't delay feature delivery
---
Optional: AI / Automation
Do:
- Use AI to propose mechanical refactors (rename/extract/move) and lint fixes; verify behavior with tests and contracts.
- Use AI to summarize diffs and highlight risky areas; validate by running characterization and integration tests.
Avoid:
- Accepting AI refactors that change behavior without explicit requirements and regression tests.
- Letting AI "fix CI" by weakening assertions or deleting tests.
Team Refactoring Session Checklist
For organized team refactoring days:
Before Session
- [ ] Goal identified (e.g., "reduce UserService complexity")
- [ ] Time allocated (e.g., "4-hour session")
- [ ] Team available - no meetings scheduled
- [ ] Branch created for refactoring
- [ ] Baseline metrics captured
- [ ] Areas prioritized by impact
During Session
- [ ] Pair/mob programming - not solo refactoring
- [ ] Small commits every 15-30 minutes
- [ ] Tests run after each commit
- [ ] Progress tracked on board
- [ ] Breaks taken every 90 minutes
After Session
- [ ] Metrics compared to baseline
- [ ] PR created with before/after stats
- [ ] Team demo of improvements
- [ ] Retrospective - what worked, what didn't
- [ ] Next session planned if needed
---
Refactoring Safety Levels
Use this to assess risk:
Level 1: Safe (No Tests Required)
- Rename variable (IDE refactoring)
- Extract constant
- Reorder method parameters (with IDE)
- Format code
Level 2: Low Risk (Basic Tests)
- Extract method
- Inline variable
- Rename method/class (with IDE)
- Add parameter
Level 3: Medium Risk (Good Test Coverage)
- Move method to another class
- Extract class
- Split conditional
- Replace conditional with polymorphism
Level 4: High Risk (Extensive Tests Required)
- Change class hierarchy
- Modify algorithm
- Change data structure
- Refactor across multiple files
Rule: Never attempt Level 3-4 refactoring without 80%+ test coverage.
---
Quick Refactoring Wins Checklist
15-minute improvements anyone can do:
- [ ] Remove unused imports
- [ ] Remove commented-out code
- [ ] Fix spelling in variable names
- [ ] Extract magic numbers to constants
- [ ] Add missing braces to single-line conditionals
- [ ] Break long lines (>120 characters)
- [ ] Add whitespace for readability
- [ ] Remove unnecessary else after return
- [ ] Replace var with const/let (JavaScript)
- [ ] Add missing error handling
---
Summary Checklist
Before marking refactoring as complete:
- [ ] All tests pass [check]
- [ ] Code coverage maintained/improved [check]
- [ ] Metrics improved [check]
- [ ] Code is more readable [check]
- [ ] No new bugs introduced [check]
- [ ] Team reviewed and approved [check]
- [ ] Documentation updated [check]
- [ ] Committed and pushed [check]
Time spent: _____ hours Value delivered: [Improved maintainability / Reduced complexity / Enabled feature X]
---
Template for Refactoring Commit Message
Refactor [Component]: [Brief description]
What changed:
- [Change 1]
- [Change 2]
- [Change 3]
Why:
- [Reason 1]
- [Reason 2]
Metrics:
- Complexity: [Before] → [After]
- Lines of code: [Before] → [After]
- Test coverage: [Before] → [After]
Tests: All passing [check]Example:
Refactor UserService: Extract validation and reduce complexity
What changed:
- Extracted email validation to EmailValidator class
- Extracted password validation to PasswordValidator class
- Split UserService into UserService and UserRepository
- Reduced method lengths from 50+ to < 20 lines
Why:
- UserService was 800 lines (God object)
- Cyclomatic complexity was 35 (very high risk)
- Mixed concerns (validation, persistence, business logic)
Metrics:
- Complexity: 35 → 8
- Lines of code: 800 → 250
- Test coverage: 45% → 82%
Tests: All passing [check]// ESLint Configuration for Code Quality & Refactoring
// Copy this to your project as .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
es2022: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended', // If using TypeScript
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
plugins: [
'@typescript-eslint',
],
rules: {
// ============================================
// CODE QUALITY RULES
// ============================================
// Complexity Rules (prevent code smells)
'complexity': ['error', 10], // Max cyclomatic complexity
'max-depth': ['error', 3], // Max nesting depth
'max-lines': ['error', {
max: 300, // Max lines per file
skipBlankLines: true,
skipComments: true,
}],
'max-lines-per-function': ['error', {
max: 50, // Max lines per function
skipBlankLines: true,
skipComments: true,
}],
'max-nested-callbacks': ['error', 3], // Max callback nesting
'max-params': ['error', 4], // Max function parameters
'max-statements': ['error', 15], // Max statements per function
// Naming Conventions
'camelcase': ['error', {
properties: 'never',
ignoreDestructuring: true,
}],
'id-length': ['error', {
min: 2, // Min variable name length
exceptions: ['i', 'j', 'k', 'x', 'y', 'z', '_'], // Loop counters allowed
}],
'new-cap': ['error', { // Constructor names must start with capital
newIsCap: true,
capIsNew: false,
}],
// Code Smell Prevention
'no-duplicate-imports': 'error',
'no-else-return': ['error', {
allowElseIf: false, // Enforce guard clauses
}],
'no-lonely-if': 'error', // Merge nested if statements
'no-magic-numbers': ['warn', {
ignore: [-1, 0, 1, 2], // Common numbers allowed
ignoreArrayIndexes: true,
enforceConst: true,
}],
'no-negated-condition': 'warn', // Avoid negated conditions
'no-nested-ternary': 'error', // No nested ternaries
'no-param-reassign': 'error', // Don't modify parameters
'no-return-assign': 'error', // No assignment in return
'no-unneeded-ternary': 'error', // Use boolean instead
'no-unused-expressions': 'error',
'no-unused-vars': ['error', {
argsIgnorePattern: '^_', // Allow unused args starting with _
varsIgnorePattern': '^_',
}],
'no-useless-return': 'error',
'no-var': 'error', // Use const/let instead
'prefer-const': 'error', // Use const when possible
'prefer-template': 'error', // Use template literals
// Function Quality
'consistent-return': 'error', // All paths must return same type
'default-case': 'warn', // Switch statements need default
'default-param-last': 'error', // Default params at end
'func-style': ['error', 'declaration', { // Use function declarations
allowArrowFunctions: true,
}],
'no-console': 'warn', // No console.log in production
'no-debugger': 'error', // No debugger statements
'no-empty-function': 'error',
'no-loop-func': 'error', // No functions in loops
'no-shadow': 'error', // No variable shadowing
'require-await': 'error', // Async functions must have await
// Best Practices
'curly': ['error', 'all'], // Always use braces
'dot-notation': 'error', // Use dot notation when possible
'eqeqeq': ['error', 'always'], // Use === instead of ==
'guard-for-in': 'error', // Check hasOwnProperty in for-in
'no-alert': 'error', // No alert() calls
'no-eval': 'error', // No eval()
'no-implied-eval': 'error', // No setTimeout with string
'no-multi-assign': 'error', // No a = b = c
'no-new': 'error', // No new for side effects
'no-throw-literal': 'error', // Throw Error objects
'prefer-arrow-callback': 'error', // Use arrow functions for callbacks
'prefer-promise-reject-errors': 'error', // Reject with Error objects
'radix': 'error', // parseInt with radix
'yoda': 'error', // No yoda conditions
// Error Handling
'no-catch-shadow': 'off', // Deprecated in ESLint 8
'no-empty': ['error', {
allowEmptyCatch: false, // No empty catch blocks
}],
'no-ex-assign': 'error', // No reassigning exception
'no-throw-literal': 'error', // Throw Error objects only
// ============================================
// TYPESCRIPT-SPECIFIC RULES
// ============================================
'@typescript-eslint/no-explicit-any': 'warn', // Avoid any
'@typescript-eslint/explicit-function-return-type': ['warn', {
allowExpressions: true,
allowTypedFunctionExpressions: true,
}],
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/no-floating-promises': 'error', // Handle promises
// ============================================
// CODE STYLE (optional - use Prettier instead)
// ============================================
'indent': ['error', 2], // 2-space indentation
'linebreak-style': ['error', 'unix'], // Unix line endings
'quotes': ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: true,
}],
'semi': ['error', 'always'], // Always use semicolons
'comma-dangle': ['error', 'always-multiline'], // Trailing commas
'max-len': ['error', {
code: 120, // Max line length
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
}],
},
// ============================================
// OVERRIDES FOR SPECIFIC FILES
// ============================================
overrides: [
{
// Test files can be longer and more complex
files: ['**/*.test.js', '**/*.test.ts', '**/*.spec.js', '**/*.spec.ts'],
rules: {
'max-lines': 'off',
'max-lines-per-function': 'off',
'max-statements': 'off',
'no-magic-numbers': 'off',
},
},
{
// Configuration files can use require
files: ['.eslintrc.js', '*.config.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
},
],
};
// ============================================
// USAGE INSTRUCTIONS
// ============================================
/*
1. Install dependencies:
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
2. Copy this file to your project root as .eslintrc.js
3. Add scripts to package.json:
{
"scripts": {
"lint": "eslint . --ext .js,.ts",
"lint:fix": "eslint . --ext .js,.ts --fix"
}
}
4. Run linter:
npm run lint # Check for issues
npm run lint:fix # Auto-fix issues
5. Integrate with VS Code:
Install "ESLint" extension
Add to settings.json:
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
6. Add to CI/CD:
- name: Lint
run: npm run lint
7. Configure with Husky pre-commit hook:
npx husky add .husky/pre-commit "npm run lint"
*/
// ============================================
// ALTERNATIVE: EXTEND POPULAR CONFIGS
// ============================================
/*
Instead of custom rules, you can extend popular configs:
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'airbnb-base', // Airbnb style guide
// or
'standard', // JavaScript Standard Style
// or
'google', // Google style guide
],
rules: {
// Override specific rules if needed
'max-len': ['error', { code: 120 }],
},
};
Install:
npm install --save-dev eslint-config-airbnb-base
npm install --save-dev eslint-config-standard
npm install --save-dev eslint-config-google
*/
// ============================================
// INTEGRATION WITH PRETTIER
// ============================================
/*
Use ESLint for code quality, Prettier for formatting:
1. Install:
npm install --save-dev prettier eslint-config-prettier
2. Update extends:
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier', // Must be last to override formatting rules
],
3. Create .prettierrc:
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120
}
4. Update package.json:
{
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
*/
SonarQube Setup Guide
Complete guide to setting up SonarQube for code quality and technical debt management.
Updated: January 2026 SonarQube Version: 2025.1 LTA (Long Term Active)
---
Overview
SonarQube is an open-source platform for continuous inspection of code quality. It performs automatic reviews with static analysis to detect:
- Bugs
- Code smells
- Security vulnerabilities
- Technical debt
- Code coverage gaps
2025 LTA Features:
- Sonar way for AI Code: New built-in quality gate qualified for AI Code Assurance
- Zero New Issues: Stricter enforcement option to ensure no new issues enter codebase
- Fudge Factor: Relaxed conditions for small changes (<20 lines) to avoid over-enforcement
- Quality Gate Recommendations: Automatic suggestions when better configurations exist
---
Installation Options
Option 1: SonarCloud (Hosted - Easiest)
Best for: Public repositories, small teams, quick setup
Setup: 1. Go to https://sonarcloud.io 2. Sign in with GitHub/Bitbucket/Azure DevOps 3. Import your repository 4. Follow integration steps below
Pricing:
- Free for public repositories
- Paid for private repositories ($10/month+)
---
Option 2: Docker (Local Development)
Best for: Local development, private projects
# Start SonarQube
docker run -d --name sonarqube \
-p 9000:9000 \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
sonarqube:latest
# Wait 2-3 minutes for startup
# Access at http://localhost:9000
# Default credentials: admin/admin (change immediately)---
Option 3: Server Installation
Best for: Enterprise, on-premise deployment
1. Requirements:
- Java 11 or 17
- PostgreSQL (recommended) or MySQL
- 2GB RAM minimum (4GB recommended)
2. Download:
wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-9.9.0.65466.zip
unzip sonarqube-9.9.0.65466.zip
cd sonarqube-9.9.0.654663. Configure Database (conf/sonar.properties):
sonar.jdbc.username=sonarqube
sonar.jdbc.password=mypassword
sonar.jdbc.url=jdbc:postgresql://localhost/sonarqube4. Start Server:
bin/linux-x86-64/sonar.sh start5. Access: http://localhost:9000
---
Project Configuration
Step 1: Create Project
In SonarQube UI: 1. Click "Create Project" 2. Enter project key (e.g., my-company_my-app) 3. Set project name and visibility 4. Generate token (save it!)
---
Step 2: Configure Project Properties
Create sonar-project.properties in project root:
# ============================================
# PROJECT IDENTIFICATION
# ============================================
sonar.projectKey=my-company_my-app
sonar.projectName=My Application
sonar.projectVersion=1.0.0
# ============================================
# SOURCE CODE LOCATION
# ============================================
# Source directories (comma-separated)
sonar.sources=src
# Test directories
sonar.tests=src/**/*.test.js,src/**/*.spec.js
# Exclusions (files to ignore)
sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/*.test.js
# Test coverage exclusions
sonar.coverage.exclusions=**/*.test.js,**/*.spec.js,**/mocks/**
# ============================================
# LANGUAGE CONFIGURATION
# ============================================
# Source encoding
sonar.sourceEncoding=UTF-8
# JavaScript/TypeScript
sonar.javascript.file.suffixes=.js,.jsx
sonar.typescript.file.suffixes=.ts,.tsx
# Python
# sonar.python.version=3.9
# Java
# sonar.java.binaries=target/classes
# ============================================
# CODE COVERAGE
# ============================================
# JavaScript/TypeScript with Jest
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=coverage/test-reporter.xml
# Python with pytest-cov
# sonar.python.coverage.reportPaths=coverage.xml
# Java with JaCoCo
# sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
# ============================================
# QUALITY GATE SETTINGS
# ============================================
# Wait for quality gate result
sonar.qualitygate.wait=true
sonar.qualitygate.timeout=300
# ============================================
# QUALITY THRESHOLDS
# ============================================
# Code coverage minimum
sonar.coverage.threshold=80
# Duplicate code maximum (%)
sonar.cpd.exclusions=**/test/**
sonar.duplications.exclusions=**/test/**
# ============================================
# ANALYSIS PARAMETERS
# ============================================
# Branch name
sonar.branch.name=main
# Pull request analysis (if applicable)
# sonar.pullrequest.key=123
# sonar.pullrequest.branch=feature-branch
# sonar.pullrequest.base=main---
CI/CD Integration
GitHub Actions
Create .github/workflows/sonarqube.yml:
name: SonarQube Analysis
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
sonarqube:
name: SonarQube Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history for better analysis
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: SonarQube Quality Gate Check
uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
scanMetadataReportFile: .scannerwork/report-task.txt
- name: Fail if Quality Gate failed
if: steps.sonarqube-quality-gate-check.outputs.quality-gate-status == 'FAILED'
run: exit 1Setup Secrets: 1. Go to repository Settings → Secrets 2. Add SONAR_TOKEN (from SonarQube) 3. Add SONAR_HOST_URL (e.g., https://sonarcloud.io or http://localhost:9000)
---
GitLab CI
Create .gitlab-ci.yml:
stages:
- test
- sonarqube
test:
stage: test
script:
- npm ci
- npm run test:coverage
artifacts:
paths:
- coverage/
expire_in: 1 day
sonarqube:
stage: sonarqube
image: sonarsource/sonar-scanner-cli:latest
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- sonar-scanner
-Dsonar.qualitygate.wait=true
-Dsonar.projectKey=$CI_PROJECT_PATH_SLUG
-Dsonar.sources=src
-Dsonar.host.url=$SONAR_HOST_URL
-Dsonar.login=$SONAR_TOKEN
allow_failure: false
only:
- main
- merge_requests---
Jenkins
Install SonarQube Scanner plugin, then add to Jenkinsfile:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Test') {
steps {
sh 'npm ci'
sh 'npm run test:coverage'
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh 'sonar-scanner'
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
}---
Quality Gates (2025 LTA)
Quality Gates define minimum quality standards for code to pass.
Reference: SonarQube 2025 LTA Quality Gates Documentation
Built-in Quality Gates
SonarQube 2025 LTA provides two built-in quality gates:
| Quality Gate | Use Case |
|---|---|
| Sonar way | Default for all projects (recommended) |
| Sonar way for AI Code | Projects containing AI-generated code (stricter) |
Sonar way (Default)
Conditions (fail if any breached on new code):
- Number of issues > 0 (enforces zero new issues)
- Reliability Rating worse than A
- Security Rating worse than A
- Maintainability Rating worse than A
- Security Hotspots Reviewed < 100%
- Coverage < 80% (configurable)
- Duplicated Lines (%) > 3% (configurable)
Sonar way for AI Code
Additional conditions for AI-generated code:
- All conditions from Sonar way
- Stricter enforcement on security and reliability
- Enhanced detection of AI-generated anti-patterns
- Required for AI Code Assurance qualification
When to use: Projects using GitHub Copilot, Cursor, Claude Code, or other AI coding assistants should consider this quality gate.
Zero New Issues Strategy (Recommended 2025)
The most effective quality gate strategy is zero new issues:
Condition: Number of issues > 0 → FAIL
Why: Prevents ALL technical debt from entering new code.
Rating conditions (A) still allow some issues to slip through.Custom Quality Gate
Create in SonarQube UI:
1. Quality Gates → Create 2. Start from "Sonar way" template (auto-copied) 3. Add/modify conditions:
Conditions on New Code:
- Number of issues > 0 (strictest, recommended)
- Coverage < 80%
- Duplicated Lines (%) > 3%
- Security Hotspots Reviewed < 100%
Conditions on Overall Code (legacy projects):
- Technical Debt Ratio > 5%
- Code Smells > 100
- Bugs > 0
- Vulnerabilities > 0Fudge Factor
For small changes (<20 lines), SonarQube relaxes coverage and duplication checks to avoid over-enforcement. This is enabled by default.
Behavior: Coverage and duplication conditions are ignored until new code reaches 20+ lines.
---
Analyzing Results
Metrics Explained
Reliability:
- Bugs: Code that will likely fail in production
- Reliability Rating: A (0 bugs) to E (many bugs)
Security:
- Vulnerabilities: Security flaws
- Security Hotspots: Security-sensitive code to review
- Security Rating: A (0 vulnerabilities) to E (many)
Maintainability:
- Code Smells: Maintainability issues
- Technical Debt: Time to fix all code smells
- Maintainability Rating: A (<5% debt ratio) to E (>50%)
Coverage:
- Coverage: % of code covered by tests
- Line Coverage: % of lines executed
- Branch Coverage: % of branches (if/else) covered
Duplication:
- Duplicated Lines: % of duplicated code
- Duplicated Blocks: Number of duplicate blocks
Size:
- Lines of Code: Total lines (excluding comments/blank lines)
- Statements: Number of statements
- Functions: Number of functions
- Classes: Number of classes
---
Best Practices
1. Focus on New Code
Why: Can't fix everything at once
How:
- Set strict quality gates for new code
- Allow legacy code to have more issues
- Gradually improve old code with Boy Scout Rule
2. Fix Blocker/Critical Issues First
Priority Order: 1. Blocker bugs/vulnerabilities 2. Critical bugs/vulnerabilities 3. Major bugs 4. Code smells (by debt)
3. Aim for "Clean as You Code"
Principle: All new code meets quality standards
Practice:
- Quality gate on new code only
- Fix issues before merging
- Don't accumulate new debt
4. Regular Debt Reduction
Schedule:
- 20% of sprint capacity for debt
- Monthly "debt day"
- Quarterly refactoring sprints
5. Monitor Trends
Watch for:
- Increasing debt ratio
- Decreasing coverage
- Rising bug count
- Growing duplications
---
Troubleshooting
Analysis Fails
Issue: Analysis doesn't complete
Solutions:
- Check SonarQube logs
- Verify token permissions
- Ensure correct project key
- Check network connectivity
No Coverage Data
Issue: Coverage shows 0%
Solutions:
- Verify test script generates coverage
- Check
sonar.javascript.lcov.reportPathspath - Ensure coverage file exists before scan
- Run tests before SonarQube scan
Quality Gate Always Passes
Issue: Even bad code passes
Solutions:
- Check quality gate configuration
- Verify conditions are set
- Ensure quality gate is assigned to project
- Check for exclusions hiding issues
---
Advanced Configuration
Multi-Module Projects
# Parent module
sonar.projectKey=my-company_my-monorepo
sonar.modules=module1,module2,module3
# Module 1
module1.sonar.projectName=Module 1
module1.sonar.sources=packages/module1/src
module1.sonar.tests=packages/module1/tests
# Module 2
module2.sonar.projectName=Module 2
module2.sonar.sources=packages/module2/src
module2.sonar.tests=packages/module2/testsBranch Analysis
# Long-lived branches
sonar.branch.name=develop
sonar.branch.target=main
# Pull request analysis
sonar.pullrequest.key=123
sonar.pullrequest.branch=feature-xyz
sonar.pullrequest.base=mainCustom Rules
Create custom rules using SonarQube plugin API (Java): 1. Create Maven project 2. Implement JavaCheck interface 3. Build JAR 4. Upload to SonarQube
---
Maintenance
Regular Tasks
Weekly:
- Review new issues
- Check quality gate status
- Monitor coverage trends
Monthly:
- Update quality gates
- Review custom rules
- Clean up old branches
Quarterly:
- SonarQube version upgrade
- Plugin updates
- Performance tuning
---
Resources
- Official Docs (2025 LTA): SonarQube Server 2025.1
- Quality Gates Guide: Quality Gates Documentation
- SonarCloud: sonarcloud.io
- Rules Reference: rules.sonarsource.com
- Community: community.sonarsource.com
- GitHub: SonarSource/sonarqube
---
Quick Reference
Common Commands
# Local analysis
sonar-scanner
# With custom properties
sonar-scanner -Dsonar.projectKey=my-project
# With token
sonar-scanner -Dsonar.login=my-token
# Verbose output
sonar-scanner -XDocker Commands
# Start
docker run -d --name sonarqube -p 9000:9000 sonarqube:latest
# Stop
docker stop sonarqube
# Restart
docker restart sonarqube
# Logs
docker logs -f sonarqube
# Remove
docker rm -f sonarqubeTechnical Debt Register
Track and prioritize technical debt items. Copy this template to your project.
---
Active Technical Debt
| ID | Description | Type | Impact | Effort | Priority | Created | Owner | Target | Status |
|---|---|---|---|---|---|---|---|---|---|
| TD-001 | Refactor UserService (600+ lines, complexity 25) | Prudent Deliberate | High | 2d | P1 | 2025-11-01 | Alice | Sprint 24 | In Progress |
| TD-002 | Add tests for PaymentProcessor (0% coverage) | Reckless Inadvertent | High | 3d | P1 | 2025-10-15 | Bob | Sprint 24 | Backlog |
| TD-003 | Extract shared validation logic (duplicated 5x) | Prudent Inadvertent | Medium | 1d | P2 | 2025-11-10 | Charlie | Sprint 25 | Backlog |
| TD-004 | Update deprecated API endpoints (3 remaining) | Prudent Deliberate | Medium | 2d | P2 | 2025-09-01 | Dave | Sprint 26 | Backlog |
| TD-005 | Reduce OrderProcessor complexity (18) | Reckless Inadvertent | Low | 4h | P3 | 2025-11-15 | Eve | Sprint 27 | Backlog |
---
Debt Classification
Type (Technical Debt Quadrant)
Reckless Deliberate: "We don't have time for design"
- Most dangerous, avoid creating
- Example: Copy-pasting code to ship fast
Prudent Deliberate: "We must ship now and deal with consequences"
- Acceptable short-term
- Example: Shipping MVP with known limitations
Reckless Inadvertent: "What's layering?"
- Due to lack of knowledge
- Example: Not using design patterns
Prudent Inadvertent: "Now we know how we should have done it"
- Normal learning process
- Example: Realizing better architecture after implementation
---
Prioritization
Priority Matrix
High Impact
│
P1 = Do Now │ P2 = Plan
─────────────┼─────────────
P3 = Maybe │ P4 = Skip
│
Low Impact
Low Effort → High EffortP1: High impact, low effort → Address immediately P2: High impact, high effort → Plan and schedule P3: Low impact, low effort → Fix if time available P4: Low impact, high effort → Defer or reconsider
Impact Assessment
High Impact:
- Blocks new features
- Causes frequent bugs
- Slows team velocity
- Security risk
Medium Impact:
- Makes changes difficult
- Reduces code quality
- Increases maintenance time
Low Impact:
- Minor inconvenience
- Aesthetic issue
- Minimal effect on development
Effort Estimation
Days or hours to fix:
- Include time for testing
- Include time for documentation
- Include time for review
---
Template for New Debt Items
## TD-XXX: [Brief Description]
**Created**: YYYY-MM-DD
**Owner**: [Name]
**Status**: Backlog
### Description
[Detailed explanation of the technical debt]
### Type
[Reckless/Prudent] [Deliberate/Inadvertent]
### Why It Was Created
[Context: Why did we take this shortcut?]
### Impact
**Level**: High / Medium / Low
**Effects**:
- [Effect 1]
- [Effect 2]
**Business Impact**:
- [How does this affect business? Slower features? More bugs?]
### Effort to Fix
**Estimated**: X days / hours
**Tasks**:
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
### Priority
**Priority**: P1 / P2 / P3 / P4
**Reasoning**:
[Why this priority? Impact vs. effort trade-off]
### Target
**Sprint**: Sprint XX
**Deadline**: YYYY-MM-DD (if applicable)
### Related Items
- Related to TD-XXX
- Blocks Feature-YYY
- Depends on TD-ZZZ
### Metrics
**Before**:
- Lines of code: XXX
- Complexity: YY
- Test coverage: ZZ%
- Debt ratio: AA%
**After (Expected)**:
- Lines of code: XXX
- Complexity: YY
- Test coverage: ZZ%
- Debt ratio: AA%
### Notes
[Additional context, links to discussions, etc.]---
Example: Complete Debt Item
## TD-001: Refactor UserService
**Created**: 2025-11-01
**Owner**: Alice
**Status**: In Progress
### Description
UserService class has grown to 600+ lines with cyclomatic complexity of 25. It handles authentication, validation, database operations, and email notifications—violating Single Responsibility Principle.
### Type
Prudent Deliberate
### Why It Was Created
Started as simple user CRUD. Features added incrementally over 2 years without refactoring. Shipped quickly to meet deadlines.
### Impact
**Level**: High
**Effects**:
- 3 bugs in last month due to complexity
- New features take 2x longer (must understand entire class)
- Difficult to test (mocking 5+ dependencies)
- Onboarding new developers takes extra 2 days
**Business Impact**:
- Slower feature delivery (estimated 15% velocity reduction)
- Higher bug rate (3 production incidents in Q4)
- Increased hiring friction (candidates mention code quality in interviews)
### Effort to Fix
**Estimated**: 2 days
**Tasks**:
- [x] Extract AuthenticationService
- [ ] Extract ValidationService
- [ ] Extract UserRepository
- [ ] Extract EmailService
- [ ] Add unit tests (target: 85% coverage)
- [ ] Update integration tests
- [ ] Update documentation
### Priority
**Priority**: P1 (Do Now)
**Reasoning**:
- High impact (blocks features, causes bugs)
- Relatively low effort (2 days)
- Quick win for team morale
### Target
**Sprint**: Sprint 24
**Deadline**: 2025-11-30
### Related Items
- Related to TD-003 (validation logic duplication)
- Blocks Feature-045 (OAuth integration)
- Mentioned in SEC-12 (security audit finding)
### Metrics
**Before**:
- Lines of code: 600
- Complexity: 25
- Test coverage: 45%
- Debt ratio: 18%
**After (Expected)**:
- Lines of code: ~250
- Complexity: <10
- Test coverage: 85%
- Debt ratio: 12%
### Notes
- Discussed in retrospective 2025-10-28
- Stakeholders aware: Will delay Feature-046 by 2 days
- SonarQube report: [link]
- Refactoring plan: [link to design doc]---
Tracking Metrics
Overall Debt Metrics
Current State (updated weekly):
Total Debt Items: 12
├── P1 (Critical): 2
├── P2 (High): 4
├── P3 (Medium): 5
└── P4 (Low): 1
By Status:
├── In Progress: 2
├── Backlog: 8
├── Blocked: 1
└── Completed this quarter: 6
Technical Debt Ratio: 14% (SonarQube)
Target: <10%
Estimated Total Effort: 24 days
Sprint Capacity for Debt: 2 days/sprint (20%)Trend Chart
Debt Ratio Over Time:
Q1 2025: 22% ████████████████████████ ↓
Q2 2025: 18% ████████████████████ ↓
Q3 2025: 14% ████████████████ ↓
Q4 2025: 12% █████████████ (target: <10%)---
Sprint Planning
Debt Allocation
Rule: Allocate 20% of sprint capacity to debt reduction
Example (2-week sprint):
- Total capacity: 10 days
- Feature work: 8 days (80%)
- Debt reduction: 2 days (20%)
Sprint N Planning
Debt Items for Sprint N:
- [ ] TD-001: UserService refactoring (2d) - P1
- [ ] TD-005: OrderProcessor complexity (4h) - P3
Total Debt Work: 2.5 days (25% of sprint)
Rationale: Catching up on P1 items, will return to 20% next sprint
---
Completed Debt (Archive)
| ID | Description | Priority | Completed | Time Spent | Owner |
|---|---|---|---|---|---|
| TD-010 | Remove deprecated API v1 | P2 | 2025-10-15 | 1d | Dave |
| TD-011 | Add tests for AuthService | P1 | 2025-10-10 | 2d | Bob |
| TD-012 | Extract payment logic | P2 | 2025-09-20 | 3d | Alice |
---
Prevention Strategies
Code Review Checklist
When reviewing PRs, check for new debt:
- [ ] No methods >20 lines
- [ ] No classes >300 lines
- [ ] No complexity >10
- [ ] Test coverage >80%
- [ ] No duplicate code
If debt found: 1. Option 1: Fix before merge (preferred) 2. Option 2: Create debt item, get approval, merge with plan
Quality Gates (CI/CD)
Prevent debt with automated checks:
- Linter: Max complexity 10
- SonarQube: Quality gate must pass
- Test coverage: Must be >80%
- Build fails if debt exceeds thresholds
---
Communication
To Team
Weekly Update (standup):
- "We reduced debt ratio from 14% to 12% this week"
- "Completed TD-001, UserService is now maintainable"
- "3 P1 items remaining, focusing on those next sprint"
To Stakeholders
Monthly Report:
Technical Debt Update - November 2025
Progress:
[check] Completed 3 debt items (TD-001, TD-005, TD-008)
[check] Debt ratio reduced: 18% → 14%
[check] Team velocity increased: 15 → 17 story points/sprint
Impact:
[check] Bug rate decreased: 8 bugs/month → 4 bugs/month
[check] Feature delivery faster: -20% time for new features
[check] Developer satisfaction improved: 3.2 → 4.1 (out of 5)
Investment:
[check] 6 days spent on debt this month (20% of capacity)
[check] ROI: 4 hours/week saved in maintenance
Next Month:
→ Target debt ratio: 12%
→ Focus on P1/P2 items (4 remaining)
→ Continue 20% allocation---
Retrospective Questions
Include in sprint retrospectives:
1. What new debt did we create this sprint?
- Was it intentional (Prudent Deliberate)?
- Did we document it?
2. Did we meet our 20% debt reduction target?
- If not, why?
- What blocked us?
3. Which debt items caused problems this sprint?
- Did lack of tests slow us down?
- Did complexity cause bugs?
4. Are we preventing new debt effectively?
- Are quality gates working?
- Are code reviews catching issues?
---
ROI Calculator
Use this to justify debt reduction to stakeholders:
Cost of Debt (per week):
- Bug fixes: 8 hours × $100/hour = $800
- Slow feature delivery: 10 hours × $100/hour = $1000
- Developer frustration: -10% productivity = $500
Total weekly cost: $2300
Investment to Fix:
- 2 days refactoring × $800/day = $1600
ROI:
- Payback period: 0.7 weeks
- Annual savings: $119,600
- ROI: 7475%
Break-even: Less than 1 week!---
Tools Integration
Jira
Create "Technical Debt" issue type:
- Fields: ID, Type, Impact, Effort, Priority
- Labels: debt, P1, P2, P3, P4
- Dashboard: Debt burndown chart
GitHub
Use labels for tracking:
technical-debtdebt-p1,debt-p2,debt-p3debt-security,debt-performance
SonarQube
Link debt items to SonarQube issues:
- Export debt metrics
- Track debt ratio over time
- Set quality gates
---
References
- Technical Debt Quadrant: https://martinfowler.com/bliki/TechnicalDebtQuadrant.html
- Managing Technical Debt: references/tech-debt-management.md
{
"metadata": {
"skill": "qa-refactoring",
"updated": "2026-01-23",
"version": "2.1",
"total_sources": 16,
"description": "Primary references for safe refactoring, legacy code characterization tests, and CI quality gates. AI sources are optional."
},
"categories": {
"refactoring_foundations": [
{
"name": "Martin Fowler - Refactoring (Book)",
"url": "https://martinfowler.com/books/refactoring.html",
"description": "Canonical refactoring reference and catalog entry point.",
"add_as_web_search": false,
"optional": false
},
{
"name": "Refactoring.Guru - Refactoring Catalog",
"url": "https://refactoring.guru/refactoring",
"description": "Refactoring patterns with examples; useful for mapping smells to moves.",
"add_as_web_search": false,
"optional": false
},
{
"name": "Refactoring.Guru - Code Smells",
"url": "https://refactoring.guru/refactoring/smells",
"description": "Code smell identification and remediation hints.",
"add_as_web_search": false,
"optional": false
}
],
"legacy_code_safety": [
{
"name": "Michael Feathers - Characterization Testing",
"url": "https://michaelfeathers.silvrback.com/characterization-testing",
"description": "Core technique for adding a safety net before refactoring legacy code.",
"add_as_web_search": false,
"optional": false
},
{
"name": "Martin Fowler - Strangler Fig Application",
"url": "https://martinfowler.com/bliki/StranglerFigApplication.html",
"description": "Incremental modernization pattern for legacy systems.",
"add_as_web_search": false,
"optional": false
},
{
"name": "Working Effectively with Legacy Code (Book)",
"url": "https://www.oreilly.com/library/view/working-effectively-with/0131177052/",
"description": "Seams, characterization tests, and incremental refactoring strategies.",
"add_as_web_search": false,
"optional": false
},
{
"name": "2026 Legacy Modernization Report",
"url": "https://devoxsoftware.com/blog/the-2026-legacy-modernization-report-research-insights-and-strategic-roadmap/",
"description": "Industry research on AI-driven modernization, characterization testing trends, and enterprise migration patterns.",
"add_as_web_search": true,
"optional": false
}
],
"quality_gates": [
{
"name": "ESLint Documentation",
"url": "https://eslint.org/docs/latest/",
"description": "Lint rules and configuration for JavaScript/TypeScript quality gates.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Prettier Documentation",
"url": "https://prettier.io/docs/en/",
"description": "Code formatting to reduce diff noise and review friction.",
"add_as_web_search": true,
"optional": false
},
{
"name": "SonarQube Server 2025 LTA - Quality Gates",
"url": "https://docs.sonarsource.com/sonarqube-server/2025.1/instance-administration/analysis-functions/quality-gates",
"description": "Latest 2025 LTA quality gate configuration including Sonar way for AI Code.",
"add_as_web_search": true,
"optional": false
}
],
"technical_debt": [
{
"name": "Gartner - Technical Debt Management",
"url": "https://www.gartner.com/en/infrastructure-and-it-operations-leaders/topics/technical-debt",
"description": "Analyst recommendations for structured debt reduction and portfolio management.",
"add_as_web_search": true,
"optional": false
},
{
"name": "TechDebt 2026 Conference",
"url": "https://conf.researchr.org/series/TechDebt",
"description": "Academic and industry research on technical debt identification and management strategies.",
"add_as_web_search": true,
"optional": true
}
],
"ci_workflows": [
{
"name": "GitHub Actions - Automating Builds and Tests",
"url": "https://docs.github.com/en/actions/automating-builds-and-tests",
"description": "CI workflow building blocks (matrices, caching) for refactor safety.",
"add_as_web_search": true,
"optional": false
}
],
"optional_ai_automation": [
{
"name": "GitHub Copilot",
"url": "https://github.com/features/copilot",
"description": "Optional: AI-assisted refactor suggestions; require tests/contracts to prove behavior preservation.",
"add_as_web_search": true,
"optional": true
},
{
"name": "JetBrains AI",
"url": "https://www.jetbrains.com/ai/",
"description": "Optional: IDE assistance for refactors; treat as a draft generator, not an oracle.",
"add_as_web_search": true,
"optional": true
},
{
"name": "Qodo (CodiumAI)",
"url": "https://www.qodo.ai/",
"description": "Optional: AI-driven code integrity with test-driven refactoring and multi-language support.",
"add_as_web_search": true,
"optional": true
}
]
}
}
Automated Refactoring Tools
Codemods, AST transforms, and IDE refactoring automation for safe, large-scale code changes. Move beyond manual find-and-replace.
Contents
- Codemod Frameworks
- AST Manipulation Basics
- Writing Custom Codemods
- IDE Refactoring Features
- Large-Scale Refactoring
- Safety Verification
- Codemod Testing Strategies
- Migration Codemods for Framework Upgrades
- Codemod Composition Patterns
- Related Resources
---
Codemod Frameworks
Framework Overview
| Framework | Language | AST Library | Maintained By | Best For |
|---|---|---|---|---|
| jscodeshift | JavaScript/TypeScript | recast + ast-types | Meta | React/JS migrations |
| ts-morph | TypeScript | TypeScript compiler | Community | TS-specific transforms |
| libCST | Python | libCST (concrete syntax tree) | Meta/Instagram | Python code transforms |
| Scalafix | Scala | Scalameta | Community | Scala migrations |
| Rector | PHP | php-parser | Community | PHP framework upgrades |
| Grit | Multi-language | Tree-sitter | Grit.io | Pattern-based, polyglot |
| Semgrep | Multi-language | Tree-sitter | Semgrep Inc | Pattern matching + autofix |
| OpenRewrite | Java/Kotlin | Custom | Moderne | Java framework migrations |
Installation
# jscodeshift (JavaScript/TypeScript)
npm install -g jscodeshift
# ts-morph (TypeScript)
npm install ts-morph
# libCST (Python)
pip install libcst
# Rector (PHP)
composer require rector/rector --dev
# OpenRewrite (Java - via Maven)
# Add to pom.xml as plugin
# Grit
npm install -g @getgrit/cli
# Semgrep
pip install semgrep---
AST Manipulation Basics
Understanding ASTs (Abstract Syntax Trees) is the foundation for writing codemods.
What Is an AST?
// Source code:
const total = price * quantity;
// AST (simplified):
{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "total" },
"init": {
"type": "BinaryExpression",
"operator": "*",
"left": { "type": "Identifier", "name": "price" },
"right": { "type": "Identifier", "name": "quantity" }
}
}]
}AST Explorer
Use astexplorer.net to visualize ASTs interactively. Select the parser that matches your codemod framework:
| Codemod Tool | AST Explorer Parser |
|---|---|
| jscodeshift | recast |
| ts-morph | TypeScript |
| libCST | Python (CST) |
| Babel | @babel/parser |
CST vs AST
| Property | AST (Abstract Syntax Tree) | CST (Concrete Syntax Tree) |
|---|---|---|
| Whitespace | Discarded | Preserved |
| Comments | Usually discarded | Preserved |
| Formatting | Lost | Preserved |
| Best for | Analysis, linting | Refactoring (preserves style) |
| Libraries | babel, typescript, tree-sitter | recast, libCST, ts-morph |
For refactoring, prefer CST-based tools (recast, libCST) to preserve formatting and comments.
---
Writing Custom Codemods
jscodeshift: Rename a Function
// codemod: rename-function.js
// Renames all calls from `oldFunctionName` to `newFunctionName`
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Find all function calls to `oldFunctionName`
root
.find(j.CallExpression, {
callee: { type: "Identifier", name: "oldFunctionName" },
})
.forEach((path) => {
path.node.callee.name = "newFunctionName";
});
// Also rename the import if it exists
root
.find(j.ImportSpecifier, {
imported: { name: "oldFunctionName" },
})
.forEach((path) => {
path.node.imported.name = "newFunctionName";
// Update local binding too
if (path.node.local && path.node.local.name === "oldFunctionName") {
path.node.local.name = "newFunctionName";
}
});
return root.toSource({ quote: "single" });
};
// Run:
// jscodeshift -t rename-function.js src/**/*.jsjscodeshift: Migrate API Pattern
// codemod: migrate-fetch-to-axios.js
// Transform: fetch(url, { method: 'POST', body: JSON.stringify(data) })
// Into: axios.post(url, data)
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
let needsAxiosImport = false;
root
.find(j.CallExpression, { callee: { name: "fetch" } })
.forEach((path) => {
const args = path.node.arguments;
if (args.length < 2) return;
const url = args[0];
const options = args[1];
if (options.type !== "ObjectExpression") return;
const methodProp = options.properties.find(
(p) => p.key.name === "method" || p.key.value === "method"
);
const bodyProp = options.properties.find(
(p) => p.key.name === "body" || p.key.value === "body"
);
if (!methodProp) return;
const method = methodProp.value.value?.toLowerCase();
if (!method) return;
needsAxiosImport = true;
// Build axios call
let axiosArgs = [url];
if (bodyProp && bodyProp.value.type === "CallExpression") {
// Extract data from JSON.stringify(data)
if (
bodyProp.value.callee.object?.name === "JSON" &&
bodyProp.value.callee.property?.name === "stringify"
) {
axiosArgs.push(bodyProp.value.arguments[0]);
}
}
// Replace fetch() with axios.method()
j(path).replaceWith(
j.callExpression(
j.memberExpression(j.identifier("axios"), j.identifier(method)),
axiosArgs
)
);
});
// Add axios import if needed
if (needsAxiosImport) {
const axiosImport = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier("axios"))],
j.literal("axios")
);
const body = root.find(j.Program).get("body");
body.unshift(axiosImport);
}
return root.toSource();
};libCST: Python Codemod
"""
Codemod: Migrate from unittest assertions to pytest assertions.
Transform: self.assertEqual(a, b) → assert a == b
"""
import libcst as cst
import libcst.matchers as m
class UnittestToPytestTransformer(cst.CSTTransformer):
"""Transform unittest-style assertions to pytest-style."""
ASSERTION_MAP = {
"assertEqual": "==",
"assertNotEqual": "!=",
"assertTrue": None, # Special handling
"assertFalse": None,
"assertIs": "is",
"assertIsNot": "is not",
"assertIn": "in",
"assertNotIn": "not in",
"assertIsNone": None,
"assertIsNotNone": None,
"assertGreater": ">",
"assertGreaterEqual": ">=",
"assertLess": "<",
"assertLessEqual": "<=",
}
def leave_Expr(
self, original_node: cst.Expr, updated_node: cst.Expr
) -> cst.BaseStatement:
# Match self.assertXxx(...) calls
if not m.matches(
updated_node.value,
m.Call(func=m.Attribute(value=m.Name("self"))),
):
return updated_node
call = updated_node.value
method_name = call.func.attr.value
if method_name not in self.ASSERTION_MAP:
return updated_node
args = [arg.value for arg in call.args]
operator = self.ASSERTION_MAP[method_name]
# Binary comparison: assertEqual(a, b) → assert a == b
if operator and len(args) >= 2:
comparison = cst.Comparison(
left=args[0],
comparisons=[
cst.ComparisonTarget(
operator=self._get_cst_operator(operator),
comparator=args[1],
)
],
)
return updated_node.with_changes(
value=cst.Assert(test=comparison)
)
# assertTrue(x) → assert x
if method_name == "assertTrue" and len(args) >= 1:
return updated_node.with_changes(
value=cst.Assert(test=args[0])
)
# assertFalse(x) → assert not x
if method_name == "assertFalse" and len(args) >= 1:
return updated_node.with_changes(
value=cst.Assert(test=cst.UnaryOperation(
operator=cst.Not(),
expression=args[0],
))
)
return updated_node
def _get_cst_operator(self, op_str: str):
ops = {
"==": cst.Equal(),
"!=": cst.NotEqual(),
">": cst.GreaterThan(),
">=": cst.GreaterThanEqual(),
"<": cst.LessThan(),
"<=": cst.LessThanEqual(),
"in": cst.In(),
"not in": cst.NotIn(),
"is": cst.Is(),
"is not": cst.IsNot(),
}
return ops[op_str]
# Run the codemod
def run_codemod(file_path: str):
with open(file_path) as f:
source = f.read()
tree = cst.parse_module(source)
modified = tree.visit(UnittestToPytestTransformer())
with open(file_path, "w") as f:
f.write(modified.code)---
IDE Refactoring Features
VS Code Refactoring
| Refactoring | Keyboard Shortcut | Scope |
|---|---|---|
| Rename symbol | F2 | Project-wide |
| Extract function | Ctrl+Shift+R | Selection |
| Extract variable | Ctrl+Shift+R | Selection |
| Move to new file | Quick Fix menu | Single symbol |
| Inline variable | Quick Fix menu | Single variable |
| Convert to template literal | Quick Fix menu | String concatenation |
IntelliJ IDEA Refactoring
| Refactoring | Keyboard Shortcut | Scope |
|---|---|---|
| Rename | Shift+F6 | Project-wide with usages |
| Extract method | Ctrl+Alt+M | Selection |
| Extract variable | Ctrl+Alt+V | Expression |
| Extract interface | Refactor menu | Class |
| Inline | Ctrl+Alt+N | Variable, method, or class |
| Move | F6 | Class, file, or package |
| Change signature | Ctrl+F6 | Method parameters |
| Pull members up | Refactor menu | Inheritance |
| Push members down | Refactor menu | Inheritance |
| Safe delete | Alt+Delete | Verify no usages before deleting |
When IDE Refactoring Is Sufficient
Number of files to change?
├── 1-5 files → IDE refactoring (rename, extract, inline)
├── 5-50 files → IDE refactoring OR simple codemod
├── 50-500 files → Codemod required
└── 500+ files → Codemod + staged rollout---
Large-Scale Refactoring
Meta's Approach to Large-Scale Codemods
Meta (Facebook) runs codemods across millions of files. Their approach:
1. Write the codemod -- Transform the code pattern 2. Test on a sample -- Run on 100 files, review manually 3. Dry-run at scale -- Generate diffs for all files without writing 4. Human review -- Sample review of generated diffs 5. Apply in batches -- Commit in groups of 100-500 files 6. CI validation -- Full test suite runs on each batch
Batch Execution Pattern
#!/bin/bash
# run-codemod-batched.sh
# Run a codemod in batches with CI validation
CODEMOD="$1"
BATCH_SIZE=100
FILES=$(find src -name "*.ts" -type f)
TOTAL=$(echo "$FILES" | wc -l)
BATCH=0
echo "Running codemod: $CODEMOD"
echo "Total files: $TOTAL"
echo "Batch size: $BATCH_SIZE"
echo "$FILES" | while mapfile -t -n $BATCH_SIZE batch && [ ${#batch[@]} -gt 0 ]; do
BATCH=$((BATCH + 1))
echo ""
echo "=== Batch $BATCH (${#batch[@]} files) ==="
# Apply codemod to this batch
jscodeshift -t "$CODEMOD" "${batch[@]}"
# Run tests
echo "Running tests..."
if ! npm test 2>/dev/null; then
echo "TESTS FAILED in batch $BATCH. Reverting..."
git checkout -- "${batch[@]}"
echo "Reverted. Investigate and fix codemod."
exit 1
fi
# Commit batch
git add "${batch[@]}"
git commit -m "codemod: $(basename "$CODEMOD" .js) (batch $BATCH)"
echo "Batch $BATCH committed successfully."
done
echo ""
echo "All batches complete."Dry-Run and Diff Generation
# jscodeshift: dry-run mode (prints to stdout, doesn't modify files)
jscodeshift -t my-codemod.js --dry --print src/
# Generate diff without applying
jscodeshift -t my-codemod.js --dry src/ 2>&1 | tee codemod-preview.diff
# Count affected files
jscodeshift -t my-codemod.js --dry src/ 2>&1 | grep "^Modified" | wc -l
# Semgrep: autofix with diff preview
semgrep --config my-rules.yaml --autofix --dryrun src/---
Safety Verification
Post-Codemod Verification Checklist
- [ ] TypeScript compiles --
tsc --noEmitpasses - [ ] Linter passes --
eslint .or equivalent - [ ] Unit tests pass -- Full test suite green
- [ ] Integration tests pass -- API contract tests green
- [ ] No behavior change -- Characterization tests green
- [ ] No import cycles --
madge --circular src/ - [ ] No dead code introduced --
ts-pruneorvulture - [ ] Bundle size stable -- Size diff within 5%
- [ ] Manual review of sample -- Spot-check 10 transformed files
Automated Verification Pipeline
# .github/workflows/codemod-verify.yaml
name: Codemod Verification
on:
pull_request:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Type check
run: npx tsc --noEmit
- name: Lint
run: npx eslint src/ --max-warnings=0
- name: Unit tests
run: npm test
- name: Check for circular imports
run: npx madge --circular src/
- name: Bundle size check
run: |
npm run build
CURRENT_SIZE=$(du -sb dist/ | cut -f1)
echo "Bundle size: $CURRENT_SIZE bytes"
# Compare against baseline
BASELINE=$(cat .bundle-size-baseline 2>/dev/null || echo 0)
DIFF=$((CURRENT_SIZE - BASELINE))
if [ $DIFF -gt 50000 ]; then
echo "::warning::Bundle size increased by $DIFF bytes"
fi---
Codemod Testing Strategies
Unit Testing Codemods
// __tests__/rename-function-codemod.test.js
const { applyTransform } = require("jscodeshift/dist/testUtils");
const transform = require("../rename-function");
describe("rename-function codemod", () => {
it("renames function calls", () => {
const input = `
import { oldFunctionName } from './utils';
const result = oldFunctionName(arg1, arg2);
`;
const expected = `
import { newFunctionName } from './utils';
const result = newFunctionName(arg1, arg2);
`;
const output = applyTransform(transform, {}, { source: input });
expect(output.trim()).toBe(expected.trim());
});
it("handles already-renamed code (idempotent)", () => {
const input = `
import { newFunctionName } from './utils';
const result = newFunctionName(arg1);
`;
const output = applyTransform(transform, {}, { source: input });
expect(output.trim()).toBe(input.trim());
});
it("does not rename unrelated functions", () => {
const input = `
const result = unrelatedFunction(arg);
`;
const output = applyTransform(transform, {}, { source: input });
expect(output.trim()).toBe(input.trim());
});
it("handles no matches gracefully", () => {
const input = `console.log("hello");`;
const output = applyTransform(transform, {}, { source: input });
expect(output.trim()).toBe(input.trim());
});
});Testing Python Codemods
"""
Test suite for Python codemods using libCST.
"""
import pytest
import libcst as cst
from codemods.unittest_to_pytest import UnittestToPytestTransformer
def apply_codemod(source: str) -> str:
tree = cst.parse_module(source)
modified = tree.visit(UnittestToPytestTransformer())
return modified.code
def test_assertEqual_transforms():
input_code = 'self.assertEqual(result, 42)'
expected = 'assert result == 42'
assert apply_codemod(input_code).strip() == expected
def test_assertTrue_transforms():
input_code = 'self.assertTrue(is_valid)'
expected = 'assert is_valid'
assert apply_codemod(input_code).strip() == expected
def test_assertFalse_transforms():
input_code = 'self.assertFalse(is_deleted)'
expected = 'assert not is_deleted'
assert apply_codemod(input_code).strip() == expected
def test_preserves_non_assertion_code():
input_code = 'result = calculate(x, y)'
assert apply_codemod(input_code).strip() == input_code
def test_idempotent_on_already_transformed():
input_code = 'assert result == 42'
assert apply_codemod(input_code).strip() == input_code
def test_preserves_comments():
input_code = '# Check the result\nself.assertEqual(result, 42)'
output = apply_codemod(input_code)
assert '# Check the result' in output
assert 'assert result == 42' in outputTest Fixtures Pattern
codemods/
__tests__/
__fixtures__/
rename-function/
input.js # Before codemod
output.js # Expected after codemod
migrate-api/
input.ts
output.ts
rename-function.test.js
migrate-api.test.js// Generic fixture-based test runner
const fs = require("fs");
const path = require("path");
const { applyTransform } = require("jscodeshift/dist/testUtils");
function testFixture(codemodName, fixtureName) {
const fixtureDir = path.join(__dirname, "__fixtures__", codemodName);
const input = fs.readFileSync(path.join(fixtureDir, `${fixtureName}.input.js`), "utf8");
const expected = fs.readFileSync(path.join(fixtureDir, `${fixtureName}.output.js`), "utf8");
const transform = require(`../${codemodName}`);
const output = applyTransform(transform, {}, { source: input });
expect(output.trim()).toBe(expected.trim());
}---
Migration Codemods for Framework Upgrades
React Class to Functional Components
// codemod: class-to-functional.js (simplified)
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
root
.find(j.ClassDeclaration, {
superClass: { name: "Component" },
})
.forEach((path) => {
const className = path.node.id.name;
const renderMethod = path.node.body.body.find(
(m) => m.type === "ClassMethod" && m.key.name === "render"
);
if (!renderMethod) return;
// Create functional component
const funcComponent = j.variableDeclaration("const", [
j.variableDeclarator(
j.identifier(className),
j.arrowFunctionExpression(
[j.identifier("props")],
renderMethod.body
)
),
]);
j(path).replaceWith(funcComponent);
});
return root.toSource();
};Common Framework Migration Codemods
| Migration | Tool | Notes |
|---|---|---|
| React class → functional | jscodeshift | Meta provides official codemods |
| Vue 2 → Vue 3 | @vue/compat + codemods | Vue CLI migration helper |
| Angular upgrade | ng update | Built-in schematics |
| Express 4 → 5 | Custom jscodeshift | Middleware signature changes |
| Jest 28 → 29 | jest-codemods | Official migration toolkit |
| Python 2 → 3 | 2to3, futurize | Built into Python stdlib |
| jQuery → vanilla JS | jscodeshift | Community codemods available |
| Moment.js → date-fns | Custom codemod | API surface changes |
| Enzyme → Testing Library | codemod-missing-await | Community codemod |
Finding Existing Codemods
# Search npm for codemods
npm search codemod react
npm search jscodeshift migration
# Search GitHub for codemods
gh search repos "codemod jscodeshift" --sort stars
# React codemods (official)
npx @codemod-com/cli react/19/replace-string-ref
# Semgrep registry
semgrep --config "p/react-best-practices" src/---
Codemod Composition Patterns
Sequential Composition
Run codemods in order, where each builds on the previous one.
#!/bin/bash
# run-migration.sh: Compose multiple codemods sequentially
echo "Step 1: Rename imports"
jscodeshift -t codemods/01-rename-imports.js src/
echo "Step 2: Update function signatures"
jscodeshift -t codemods/02-update-signatures.js src/
echo "Step 3: Migrate API calls"
jscodeshift -t codemods/03-migrate-api.js src/
echo "Step 4: Clean up unused imports"
jscodeshift -t codemods/04-remove-unused-imports.js src/
echo "Step 5: Run formatter"
npx prettier --write src/
echo "Step 6: Verify"
npx tsc --noEmit && npm testPipeline Composition (libCST)
"""
Compose multiple libCST transformers into a single pass.
More efficient than running each transformer separately.
"""
import libcst as cst
from typing import Sequence
def compose_transformers(
source: str,
transformers: Sequence[cst.CSTTransformer],
) -> str:
"""Apply multiple transformers in a single parse-transform-print cycle."""
tree = cst.parse_module(source)
for transformer in transformers:
tree = tree.visit(transformer)
return tree.code
# Usage
from codemods.rename_imports import RenameImportsTransformer
from codemods.update_signatures import UpdateSignaturesTransformer
from codemods.migrate_api import MigrateAPITransformer
result = compose_transformers(
source=open("src/service.py").read(),
transformers=[
RenameImportsTransformer(),
UpdateSignaturesTransformer(),
MigrateAPITransformer(),
],
)Conditional Composition
// codemod-runner.js: Apply codemods conditionally based on file analysis
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Only apply React migration if file uses React
const hasReactImport = root.find(j.ImportDeclaration, {
source: { value: "react" },
}).length > 0;
if (hasReactImport) {
// Apply React-specific transforms
applyReactMigration(root, j);
}
// Only apply API migration if file imports the old API client
const hasOldClient = root.find(j.ImportDeclaration, {
source: { value: "@company/old-api-client" },
}).length > 0;
if (hasOldClient) {
applyAPIMigration(root, j);
}
return root.toSource();
};---
Related Resources
- Characterization Testing - Verify behavior after automated refactoring
- Code Smells Guide - Identify patterns to target with codemods
- Refactoring Catalog - Manual refactoring techniques
- Strangler Fig Migration - Incremental system replacement
- Tech Debt Management - Prioritizing what to codemod
- Operational Patterns - CI/CD integration for refactoring
- SKILL.md - Parent skill overview
Characterization Testing
Golden master and approval testing techniques for preserving behavior during refactoring. Based on Michael Feathers' Working Effectively with Legacy Code.
Contents
- Characterization Test Theory
- Golden Master Pattern
- Approval Testing Libraries
- When to Use Characterization Tests
- Generating Tests from Logs
- Maintaining Golden Masters
- Transitioning to Unit Tests
- Workflow Integration
- Related Resources
---
Characterization Test Theory
A characterization test documents what code actually does, not what it should do. The goal is to capture current behavior so you can refactor with confidence, even when you do not fully understand the code.
Key Principles (Feathers)
1. The code is the specification -- existing behavior IS the requirement until proven otherwise 2. Test what IS, not what SHOULD BE -- do not fix bugs while characterizing 3. Cover the boundary you will change -- test the API surface, module boundary, or function you plan to refactor 4. Use the tests as a safety net -- if a characterization test fails after refactoring, you changed behavior
When Characterization Tests Are Necessary
Is the code under test well-understood?
├── Yes → Does it have adequate unit tests?
│ ├── Yes → Refactor directly, existing tests protect you
│ └── No → Write unit tests first if feasible
└── No → Is it risky to change without tests?
├── Yes → Write characterization tests
└── No → Consider the risk tolerance
├── High risk (money, auth, data) → Write characterization tests
└── Low risk → Acceptable to refactor with integration tests onlyThe Characterization Test Workflow
1. Pick the boundary you'll refactor
2. Write tests that call the code and record actual outputs
3. Assert that outputs match the recorded values
4. Run the full characterization suite → all green (by definition)
5. Refactor the internals
6. Run the suite again → if anything fails, you changed behavior
7. Investigate: was the behavior change intentional?
- Yes → Update the golden master
- No → Revert the refactoring step---
Golden Master Pattern
The golden master pattern captures a snapshot of current output and compares future runs against it.
Basic Golden Master Implementation
"""
Golden master testing: capture output, store as reference,
compare future runs against the reference.
"""
import json
import hashlib
from pathlib import Path
from typing import Any
GOLDEN_DIR = Path("tests/golden_masters")
def capture_golden_master(test_name: str, output: Any) -> Path:
"""Capture current output as the golden master."""
GOLDEN_DIR.mkdir(parents=True, exist_ok=True)
path = GOLDEN_DIR / f"{test_name}.golden.json"
with open(path, "w") as f:
json.dump(output, f, indent=2, sort_keys=True, default=str)
print(f"Golden master captured: {path}")
return path
def assert_matches_golden_master(test_name: str, actual: Any):
"""Compare current output against the golden master."""
path = GOLDEN_DIR / f"{test_name}.golden.json"
if not path.exists():
# First run: capture the golden master
capture_golden_master(test_name, actual)
return
with open(path) as f:
expected = json.load(f)
actual_normalized = json.loads(json.dumps(actual, sort_keys=True, default=str))
assert actual_normalized == expected, (
f"Output does not match golden master for {test_name}.\n"
f"To update the golden master, delete {path} and re-run.\n"
f"Diff:\n{_diff(expected, actual_normalized)}"
)
def _diff(expected: Any, actual: Any, path: str = "$") -> str:
"""Generate a human-readable diff."""
diffs = []
if isinstance(expected, dict) and isinstance(actual, dict):
all_keys = set(expected.keys()) | set(actual.keys())
for key in sorted(all_keys):
if key not in expected:
diffs.append(f" ADDED {path}.{key}: {actual[key]}")
elif key not in actual:
diffs.append(f" REMOVED {path}.{key}: {expected[key]}")
elif expected[key] != actual[key]:
diffs.append(f" CHANGED {path}.{key}: {expected[key]} → {actual[key]}")
elif expected != actual:
diffs.append(f" CHANGED {path}: {expected} → {actual}")
return "\n".join(diffs) if diffs else " (no differences)"
# Usage in tests
import pytest
def test_order_total_calculation():
"""Characterization test for the legacy order calculator."""
from legacy_app.orders import calculate_order_total
order = {
"items": [
{"sku": "WIDGET-A", "qty": 3, "unit_price": 9.99},
{"sku": "GADGET-B", "qty": 1, "unit_price": 24.50},
],
"coupon": "SAVE10",
"shipping": "standard",
}
result = calculate_order_total(order)
assert_matches_golden_master("order_total_basic", result)
def test_order_total_edge_cases():
"""Characterization: empty cart, zero quantities, negative discounts."""
from legacy_app.orders import calculate_order_total
edge_cases = [
{"items": [], "coupon": None, "shipping": "standard"},
{"items": [{"sku": "X", "qty": 0, "unit_price": 10.0}], "coupon": None, "shipping": "express"},
{"items": [{"sku": "X", "qty": 1, "unit_price": 0.0}], "coupon": "SAVE10", "shipping": "standard"},
]
results = [calculate_order_total(case) for case in edge_cases]
assert_matches_golden_master("order_total_edge_cases", results)Golden Master for Data Transformations
"""
Characterize a data transformation pipeline.
Useful for ETL code, report generators, CSV processors.
"""
import csv
import io
def test_csv_export_golden_master():
"""Capture the exact CSV output of the legacy export."""
from legacy_app.reports import generate_monthly_report
report = generate_monthly_report(month=1, year=2026)
# Capture the full output including headers, formatting, precision
output = io.StringIO()
writer = csv.writer(output)
for row in report:
writer.writerow(row)
assert_matches_golden_master(
"monthly_report_jan_2026",
output.getvalue()
)---
Approval Testing Libraries
Approval testing automates the golden master workflow with built-in diff tools and approval commands.
Python: approvaltests
"""
Using the approvaltests library for Python.
pip install approvaltests
"""
from approvaltests import verify, verify_all
from approvaltests.reporters import GenericDiffReporterFactory
def test_pricing_engine():
"""Approval test: captures output, shows diff on failure."""
from legacy_app.pricing import calculate_price
scenarios = [
("Basic item", calculate_price(item="widget", qty=1)),
("Bulk discount", calculate_price(item="widget", qty=100)),
("Premium item", calculate_price(item="premium-widget", qty=1)),
("Zero quantity", calculate_price(item="widget", qty=0)),
]
# verify_all generates a formatted string and compares to approved file
verify_all(
"Pricing Scenarios",
scenarios,
lambda s: f"{s[0]}: ${s[1]:.2f}"
)
# First run:
# Creates test_pricing_engine.received.txt
# Fails (no approved file yet)
# Review the .received.txt file
# Rename to test_pricing_engine.approved.txt to approve
# Subsequent runs:
# Compares output against .approved.txt
# If different: shows diff and fails
# If same: passes silentlyJava: ApprovalTests.Java
import org.approvaltests.Approvals;
import org.approvaltests.combinations.CombinationApprovals;
import org.junit.jupiter.api.Test;
class PricingEngineTest {
@Test
void testPricingCombinations() {
// Test all combinations of inputs
CombinationApprovals.verifyAllCombinations(
this::calculatePrice,
new String[]{"widget", "premium-widget", "service"}, // items
new Integer[]{0, 1, 10, 100} // quantities
);
}
private String calculatePrice(String item, Integer qty) {
double price = LegacyPricingEngine.calculate(item, qty);
return String.format("$%.2f", price);
}
}JavaScript: Jest Snapshots
// Jest has built-in snapshot testing (approval-style)
const { processOrder } = require("../legacy/orderProcessor");
describe("Order Processor (characterization)", () => {
test("standard order output", () => {
const order = {
items: [
{ sku: "WIDGET-A", qty: 3, price: 9.99 },
{ sku: "GADGET-B", qty: 1, price: 24.50 },
],
coupon: "SAVE10",
};
const result = processOrder(order);
// First run: creates __snapshots__/orderProcessor.test.js.snap
// Subsequent runs: compares against snapshot
expect(result).toMatchSnapshot();
});
test("edge cases", () => {
const cases = [
{ items: [], coupon: null },
{ items: [{ sku: "X", qty: 0, price: 10 }], coupon: null },
{ items: [{ sku: "X", qty: -1, price: 10 }], coupon: "INVALID" },
];
cases.forEach((testCase, index) => {
expect(processOrder(testCase)).toMatchSnapshot(`edge-case-${index}`);
});
});
});
// Update snapshots: npx jest --updateSnapshotLibrary Comparison
| Library | Language | Diff Tool | CI Support | Combination Testing |
|---|---|---|---|---|
| approvaltests | Python | System diff, custom | Yes | verify_all |
| ApprovalTests.Java | Java | IntelliJ, custom | Yes | CombinationApprovals |
| Jest snapshots | JavaScript | Built-in | Yes | Manual loops |
| verify (Rust) | Rust | insta crate | Yes | Manual |
| ApprovalTests.Net | C# | VS, Beyond Compare | Yes | CombinationApprovals |
| SnapshotTesting | Swift | Xcode | Yes | Manual |
---
When to Use Characterization Tests
Good Fit
| Scenario | Why Characterization Tests Work |
|---|---|
| Untested legacy code | No existing safety net; characterization creates one fast |
| Complex algorithms | Behavior is hard to specify; easier to capture than describe |
| Data transformations | Output format is the contract; golden master verifies it |
| Before strangler migration | Prove the new system matches the old one |
| Regulatory/compliance code | Must prove behavior did not change |
| Third-party integration wrappers | Capture expected responses for offline testing |
Poor Fit
| Scenario | Better Alternative |
|---|---|
| Nondeterministic output (timestamps, random IDs) | Mock or normalize before comparing |
| UI rendering | Visual regression testing (Chromatic, Percy) |
| Performance characteristics | Benchmark tests |
| Well-understood code with clear specs | Write proper unit tests instead |
| Code that is known to be buggy | Fix bugs first, then characterize |
Handling Nondeterminism
"""
Normalize nondeterministic values before golden master comparison.
"""
import re
from datetime import datetime
def normalize_for_golden_master(output: dict) -> dict:
"""Remove or normalize nondeterministic fields."""
normalized = json.loads(json.dumps(output))
# Replace timestamps with placeholder
if "created_at" in normalized:
normalized["created_at"] = "<TIMESTAMP>"
# Replace UUIDs with placeholder
if "id" in normalized:
normalized["id"] = "<UUID>"
# Normalize floating point precision
if "total" in normalized:
normalized["total"] = round(normalized["total"], 2)
return normalized
def normalize_string_output(text: str) -> str:
"""Normalize nondeterministic values in string output."""
# Replace UUIDs
text = re.sub(
r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
'<UUID>', text
)
# Replace ISO timestamps
text = re.sub(
r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?',
'<TIMESTAMP>', text
)
return text---
Generating Tests from Logs
Use production or staging logs to generate realistic characterization test cases.
Log-to-Test Generator
"""
Generate characterization tests from request/response logs.
Input: structured logs with request + response pairs.
Output: pytest test file with golden master assertions.
"""
import json
from pathlib import Path
def generate_tests_from_logs(log_file: str, output_dir: str, max_tests: int = 50):
"""Parse request/response logs into test cases."""
test_cases = []
with open(log_file) as f:
for line in f:
entry = json.loads(line)
if entry.get("type") != "http_request":
continue
test_cases.append({
"method": entry["http"]["method"],
"path": entry["http"]["path"],
"request_body": entry.get("request_body"),
"response_status": entry["http"]["status_code"],
"response_body": entry.get("response_body"),
})
if len(test_cases) >= max_tests:
break
# Generate test file
output = Path(output_dir) / "test_characterization_generated.py"
with open(output, "w") as f:
f.write('"""Auto-generated characterization tests from production logs."""\n')
f.write("import pytest\n")
f.write("import requests\n\n")
f.write('BASE_URL = "http://localhost:8000"\n\n')
for i, tc in enumerate(test_cases):
f.write(f"def test_case_{i:04d}_{tc['method'].lower()}_{tc['path'].replace('/', '_').strip('_')}():\n")
f.write(f' """Characterized from production log entry."""\n')
f.write(f' response = requests.{tc["method"].lower()}(\n')
f.write(f' f"{{BASE_URL}}{tc["path"]}",\n')
if tc["request_body"]:
f.write(f" json={json.dumps(tc['request_body'])},\n")
f.write(f" )\n")
f.write(f" assert response.status_code == {tc['response_status']}\n")
if tc["response_body"]:
f.write(f" assert response.json() == {json.dumps(tc['response_body'])}\n")
f.write("\n\n")
print(f"Generated {len(test_cases)} test cases in {output}")
# Usage
generate_tests_from_logs(
"logs/api-access-2026-01.jsonl",
"tests/characterization/",
max_tests=100
)Sampling Strategy for Log-Based Test Generation
- [ ] Include at least one example per endpoint
- [ ] Include examples for each HTTP status code returned
- [ ] Prioritize endpoints that will be refactored
- [ ] Include edge cases (empty bodies, large payloads, special characters)
- [ ] Normalize nondeterministic fields (timestamps, IDs) before storing as golden masters
- [ ] Limit to 50-200 tests to keep the suite fast
---
Maintaining Golden Masters
Golden Master Lifecycle
| Phase | Action | Who |
|---|---|---|
| Capture | Run test for the first time, review and approve output | Developer starting refactoring |
| Protect | Golden masters committed to Git, fail CI on mismatch | CI/CD pipeline |
| Update | Intentional behavior change requires re-approval | Developer + reviewer |
| Retire | Replace with unit tests after refactoring complete | Developer |
Update Workflow
#!/bin/bash
# update-golden-masters.sh
# Use when behavior change is intentional
echo "WARNING: This will overwrite golden masters with current output."
echo "Only run this after confirming the behavior change is intentional."
read -p "Continue? (y/N) " confirm
if [ "$confirm" != "y" ]; then
echo "Aborted."
exit 0
fi
# For pytest + custom golden master
find tests/golden_masters -name "*.golden.json" -delete
pytest tests/characterization/ -x
# For Jest snapshots
# npx jest --updateSnapshot
# For approvaltests
# mv tests/*.received.txt tests/*.approved.txt
echo "Golden masters updated. Review changes with: git diff tests/"CI Protection
# .github/workflows/characterization-tests.yaml
name: Characterization Tests
on:
pull_request:
paths:
- "src/**"
- "tests/characterization/**"
- "tests/golden_masters/**"
jobs:
characterization:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run characterization tests
run: pytest tests/characterization/ -v
- name: Check for unapproved golden master changes
run: |
if git diff --name-only | grep -q "golden_masters"; then
echo "::error::Golden master files were modified during test run."
echo "::error::This means behavior changed. Review carefully."
git diff tests/golden_masters/
exit 1
fi---
Transitioning to Unit Tests
Characterization tests are temporary scaffolding. Replace them with proper unit tests as you understand the code better.
Transition Process
Phase 1: Characterize
- Write golden master tests around the boundary
- Cover happy paths and edge cases
- All tests pass (they capture current behavior)
Phase 2: Refactor
- Extract methods, introduce seams, simplify
- Characterization tests catch any behavior changes
- Keep refactoring steps small
Phase 3: Understand
- As you refactor, you learn what the code actually does
- Document discovered behavior as comments or specs
- Identify bugs vs features in the current behavior
Phase 4: Replace
- Write unit tests for the refactored code
- Each unit test replaces part of the characterization test
- Unit tests test intent; characterization tests test behavior
- Delete characterization tests when fully covered
Phase 5: Clean up
- Remove golden master files
- Remove characterization test infrastructure
- Update CI to run only unit/integration testsReplacement Checklist
- [ ] Every characterization test has a corresponding unit test
- [ ] Unit tests cover the same edge cases
- [ ] Unit tests are faster than characterization tests
- [ ] Golden master files deleted from repository
- [ ] CI updated to exclude characterization test directory
- [ ] Documentation updated with discovered behavior notes
---
Workflow Integration
Refactoring with Characterization Tests: Step by Step
# 1. Create the characterization test branch
git checkout -b refactor/order-calculator
# 2. Write characterization tests
pytest tests/characterization/test_order_calculator.py -v
# All pass (capturing current behavior)
# 3. Commit the golden masters
git add tests/characterization/ tests/golden_masters/
git commit -m "Add characterization tests for order calculator"
# 4. Refactor in small steps
# ... make changes ...
pytest tests/characterization/test_order_calculator.py -v
# If any fail: you changed behavior. Investigate.
# 5. After refactoring is complete, write unit tests
pytest tests/unit/test_order_calculator.py -v
# 6. Verify unit tests cover characterization tests
pytest tests/ --cov=src/orders/calculator.py
# Coverage should be equal or better
# 7. Remove characterization tests
git rm tests/characterization/test_order_calculator.py
git rm tests/golden_masters/order_calculator_*.golden.json
git commit -m "Replace characterization tests with unit tests for order calculator"---
Related Resources
- Legacy Code Strategies - Broader strategies for working with legacy code
- Code Smells Guide - Identifying what to refactor
- Refactoring Catalog - Specific refactoring techniques
- Strangler Fig Migration - Incremental migration using characterization tests
- Automated Refactoring Tools - Tool-assisted refactoring
- Tech Debt Management - Prioritizing refactoring work
- SKILL.md - Parent skill overview
Technical Debt Management
Comprehensive guide to identifying, measuring, tracking, and managing technical debt.
Contents
- What is Technical Debt?
- Technical Debt Quadrant
- Measuring Technical Debt
- 8 Key Metrics for Technical Debt
- Technical Debt Register
- Managing Technical Debt
- Debt Prevention
- Communicating Technical Debt
- Technical Debt in Agile
- Tools and Platforms
- Case Study: Reducing Debt by 50%
- Best Practices Summary
- References
---
What is Technical Debt?
Definition: Technical debt is the implied cost of future rework caused by choosing an easy (limited) solution now instead of a better approach that would take longer.
Origin: Coined by Ward Cunningham in 1992, comparing shortcuts in code to financial debt that accrues interest.
---
Technical Debt Quadrant
Martin Fowler's classification framework:
Reckless | Prudent
─────────────────────
Deliberate │ "We don't have │ "We must ship
│ time for design"│ now and deal
│ │ with consequences"
├─────────────────────────
Inadvertent │ "What's │ "Now we know
│ layering?" │ how we should
│ │ have done it"Quadrant Details
1. Reckless Deliberate (Avoid)
- Knowingly taking shortcuts without plan to fix
- "We don't have time for design"
- Dangerous and accumulates quickly
2. Prudent Deliberate (Acceptable short-term)
- Strategic decision to ship fast
- "We must ship now and deal with consequences later"
- Document decision and plan to address
3. Reckless Inadvertent (Fix through training)
- Lack of knowledge or skills
- "What's layering? What's dependency injection?"
- Address through education and mentoring
4. Prudent Inadvertent (Normal learning)
- Learning from experience
- "Now we know how we should have done it"
- Part of normal development process
---
Measuring Technical Debt
SonarQube Metrics
Technical Debt Ratio (TDR):
TDR = (Remediation Cost / Development Cost) × 100
Example:
Remediation Cost: 50 hours (to fix all issues)
Development Cost: 500 hours (total project time)
TDR: 10%
Thresholds:
< 5%: Excellent
5-10%: Good
10-20%: Needs attention
> 20%: CriticalKey Metrics:
- Code Smells: Maintainability issues
- Bugs: Reliability issues
- Vulnerabilities: Security issues
- Coverage: Test coverage percentage
- Duplications: Duplicate code blocks
- Complexity: Cyclomatic complexity
SonarQube Quality Gates
# sonar-project.properties
sonar.projectKey=my-project
sonar.organization=my-org
# Quality Gate thresholds
sonar.qualitygate.wait=true
# Code coverage
sonar.coverage.threshold=80
# Duplications
sonar.duplications.threshold=3
# Complexity
sonar.complexity.threshold=10
# Ratings (A-E scale)
sonar.maintainability.rating=A
sonar.reliability.rating=A
sonar.security.rating=A
# Technical debt
sonar.techdebt.threshold=5 # 5% maximum TDR---
8 Key Metrics for Technical Debt
1. Technical Debt Ratio (TDR)
Percentage of development time spent fixing debt.
2. Code Churn
Rate of code changes over time. High churn indicates instability.
Code Churn = (Lines Added + Lines Deleted) / Total Lines3. Cycle Time
Time from commit to deployment. Longer cycles suggest debt.
4. Defect Density
Number of bugs per lines of code.
Defect Density = Total Defects / KLOC (thousands of lines of code)5. Code Duplication
Percentage of duplicated code blocks.
6. Cyclomatic Complexity
Number of independent paths through code. Higher = more complex.
Thresholds:
- 1-10: Simple, low risk
- 11-20: Moderate, medium risk
- 21-50: Complex, high risk
- 50+: Very high risk, untestable
7. Code Coverage
Percentage of code covered by tests.
Targets:
- Critical paths: 100%
- Business logic: 90%+
- Overall: 80%+
8. Failed Builds
Frequency of CI/CD failures indicates quality issues.
---
Technical Debt Register
Track and prioritize technical debt items.
Template
| ID | Description | Type | Impact | Effort | Priority | Created | Owner | Status |
|---|---|---|---|---|---|---|---|---|
| TD-001 | Refactor UserService (600 lines) | Prudent Deliberate | High | 2 days | P1 | 2025-10-01 | Alice | In Progress |
| TD-002 | Add tests for PaymentProcessor | Reckless Inadvertent | Medium | 3 days | P2 | 2025-09-15 | Bob | Backlog |
| TD-003 | Extract shared validation logic | Prudent Inadvertent | Low | 1 day | P3 | 2025-11-01 | Charlie | Backlog |
| TD-004 | Remove deprecated API endpoints | Deliberate | Medium | 1 day | P2 | 2025-08-20 | Dave | Completed |
Prioritization Matrix
High Impact
│
P1 = Do Now │ P2 = Plan
─────────────┼─────────────
P3 = Maybe │ P4 = Skip
│
Low Impact
Low Effort → High EffortPriority Levels:
- P1: High impact, low effort → Do immediately
- P2: High impact, high effort → Plan and schedule
- P3: Low impact, low effort → Maybe do if time
- P4: Low impact, high effort → Skip or reconsider
---
Managing Technical Debt
Boy Scout Rule
"Leave the code better than you found it."
When touching a file:
- [ ] Fix at least one code smell
- [ ] Add missing tests
- [ ] Improve naming
- [ ] Extract duplicated code
- [ ] Add documentation
20% Time Rule
Allocate 20% of sprint capacity to debt reduction:
- 80%: New features
- 20%: Refactoring, testing, documentation
Debt Reduction Strategies
1. Incremental Refactoring
- Small, safe changes
- One pattern at a time
- Run tests after each change
2. Feature Freeze Sprints
- Dedicate sprint to debt reduction
- No new features
- Focus on quality improvements
3. Debt Day
- One day per week for debt
- Rotate team members
- Track progress
4. Opportunistic Refactoring
- Refactor while working on features
- Make code better for new feature
- Don't break existing functionality
---
Debt Prevention
Code Review Checklist
- [ ] No duplicate code
- [ ] Methods < 20 lines
- [ ] Classes < 300 lines
- [ ] Functions have < 4 parameters
- [ ] No magic numbers
- [ ] Meaningful names
- [ ] Tests included
- [ ] Documentation added
Automated Quality Gates
Pre-commit Hooks:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write",
"jest --bail --findRelatedTests"
]
}
}CI/CD Gates:
name: Quality Check
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Lint
run: npm run lint
- name: Test
run: npm run test:coverage
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
- name: Quality Gate
run: |
if [ $SONAR_QUALITY_GATE == "ERROR" ]; then
exit 1
fi---
Communicating Technical Debt
To Non-Technical Stakeholders
Don't say: "We have high cyclomatic complexity in the UserService class."
Do say: "Our user management code is becoming difficult to maintain, which will slow down new features and increase bug risk. We need 3 days to simplify it."
Business Impact Framework
Connect debt to business outcomes:
| Technical Issue | Business Impact |
|---|---|
| High complexity | Slower feature delivery |
| Low test coverage | More production bugs |
| Code duplication | Inconsistent behavior |
| Legacy code | Hard to hire developers |
| Security debt | Risk of data breach |
ROI Calculation
Cost of debt:
- Bug fix time: $500/week
- Slow feature delivery: $2000/week
- Developer frustration: $1000/week
Total: $3500/week
Investment to fix:
- 2 weeks refactoring: $10,000
ROI:
Payback period: 3 weeks
Annual savings: $182,000---
Technical Debt in Agile
Scrum Integration
Product Backlog:
- Technical debt items as user stories
- Prioritize with business features
- Estimate using story points
Sprint Planning:
- Include debt reduction tasks
- Balance features and debt
- Track velocity impact
Retrospectives:
- Discuss new debt created
- Review debt reduction progress
- Adjust 20% allocation
Debt Story Template
As a developer
I want to refactor the UserService class
So that it's easier to maintain and extend
Acceptance Criteria:
- [ ] UserService < 300 lines
- [ ] Extract authentication logic
- [ ] Extract validation logic
- [ ] Test coverage > 80%
- [ ] Cyclomatic complexity < 10
Definition of Done:
- [ ] Code reviewed
- [ ] Tests pass
- [ ] Documentation updated
- [ ] SonarQube metrics improved---
Tools and Platforms
Static Analysis Tools
| Tool | Language | Key Features |
|---|---|---|
| SonarQube | Multi-language | Comprehensive analysis, quality gates |
| CodeClimate | Multi-language | Maintainability metrics, GitHub integration |
| ESLint | JavaScript/TS | Linting, custom rules |
| Pylint | Python | Code analysis, PEP 8 compliance |
| RuboCop | Ruby | Style enforcement, security checks |
Debt Tracking Tools
| Tool | Key Features |
|---|---|
| Jira | Debt as issues, custom fields, roadmaps |
| Linear | Modern interface, issue tracking |
| Stepsize | Dedicated debt tracking, metrics |
| GitHub Projects | Simple, integrated with code |
Modern AI Tools (2025)
| Tool | Key Features |
|---|---|
| GitHub Copilot | Real-time refactoring suggestions |
| Embold | Preventive refactoring, anti-patterns |
| CodiumAI | Test-driven refactoring |
| ReSharper | AI-enhanced .NET refactoring |
| IntelliJ IDEA | AI Assistant for refactoring |
---
Case Study: Reducing Debt by 50%
Company: Tech Startup, 300k LOC codebase
Initial State:
- TDR: 25% (Critical)
- Test coverage: 40%
- Build time: 30 minutes
- Deploy frequency: Weekly
Strategy (6-month plan):
Month 1-2: Measurement
- Install SonarQube
- Create debt register
- Baseline metrics
Month 3-4: Quick Wins
- Remove dead code
- Fix obvious code smells
- Add missing tests
Month 5-6: Strategic Refactoring
- Refactor high-churn files
- Extract shared logic
- Improve architecture
Results:
- TDR: 12% (Good)
- Test coverage: 75%
- Build time: 10 minutes
- Deploy frequency: Daily
- Developer satisfaction: +40%
- Bug rate: -60%
Investment: $120,000 (2 developers × 6 months) Annual Savings: $300,000 (faster delivery, fewer bugs) ROI: 250%
---
Best Practices Summary
1. Measure and track debt continuously 2. Allocate 20% of time to debt reduction 3. Boy Scout Rule - always improve code you touch 4. Automate quality gates to prevent new debt 5. Communicate business impact to stakeholders 6. Prioritize by impact and effort 7. Incremental refactoring over big bang rewrites 8. Include debt in sprint planning 9. Use tools for detection and tracking 10. Make debt visible to entire team
---
References
- Managing Technical Debt - Philippe Kruchten
- SonarSource - Measuring Technical Debt - https://www.sonarsource.com/learn/
- Martin Fowler - Technical Debt Quadrant - https://martinfowler.com/bliki/TechnicalDebtQuadrant.html
- Stepsize - Technical Debt Metrics - https://www.stepsize.com/blog/
- CircleCI - Technical Debt Management - https://circleci.com/blog/