vanara 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +156 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +184 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +128 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +172 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +201 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +184 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +179 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +193 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +179 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +162 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable check: flags risk areas from a diff-stat-like JSON so a PR summarizer (human or
|
|
3
|
+
// agent) can mechanically surface migrations, secret-like additions, and oversized changes
|
|
4
|
+
// before writing the summary. Mirrors the --selftest convention of review-guard.mjs.
|
|
5
|
+
//
|
|
6
|
+
// Input shape (a "diff stat"):
|
|
7
|
+
// {
|
|
8
|
+
// "files": [
|
|
9
|
+
// { "path": "migrations/0007_api_keys.sql", "added": 40, "removed": 0,
|
|
10
|
+
// "addedLines": ["ALTER TABLE accounts ADD COLUMN api_key_id ..."] }
|
|
11
|
+
// ]
|
|
12
|
+
// }
|
|
13
|
+
// `added`/`removed` are line counts; `addedLines` (optional) are the raw added lines, used to
|
|
14
|
+
// sniff secret-like content.
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// node diff-risk.mjs '<json>' # check a literal JSON string
|
|
18
|
+
// echo '<json>' | node diff-risk.mjs # check from stdin
|
|
19
|
+
// node diff-risk.mjs --selftest # run built-in test cases
|
|
20
|
+
|
|
21
|
+
const LARGE_CHANGE_LINES = 400; // total added+removed above this is a "large change"
|
|
22
|
+
const MIGRATION_RE = /(^|\/)(migrations?|migrate)(\/|_)|\.(sql)$/i;
|
|
23
|
+
const SECRET_RE = /(api[_-]?key|secret|password|passwd|token|private[_-]?key)\s*[:=]|-----BEGIN|AKIA[0-9A-Z]{16}/i;
|
|
24
|
+
|
|
25
|
+
export function diffRisk(stat) {
|
|
26
|
+
const flags = [];
|
|
27
|
+
const files = (stat && Array.isArray(stat.files)) ? stat.files : [];
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
const path = f.path ?? '<unknown>';
|
|
30
|
+
const added = typeof f.added === 'number' ? f.added : 0;
|
|
31
|
+
const removed = typeof f.removed === 'number' ? f.removed : 0;
|
|
32
|
+
|
|
33
|
+
if (MIGRATION_RE.test(path)) {
|
|
34
|
+
flags.push({ kind: 'MIGRATION', where: path,
|
|
35
|
+
msg: 'database migration — verify it runs cleanly against a non-empty production table' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lines = Array.isArray(f.addedLines) ? f.addedLines : [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
if (typeof line === 'string' && SECRET_RE.test(line)) {
|
|
41
|
+
flags.push({ kind: 'SECRET', where: path,
|
|
42
|
+
msg: `possible hardcoded secret in an added line: ${line.trim().slice(0, 60)}` });
|
|
43
|
+
break; // one flag per file is enough to make a human look
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (added + removed > LARGE_CHANGE_LINES) {
|
|
48
|
+
flags.push({ kind: 'LARGE', where: path,
|
|
49
|
+
msg: `large change (+${added}/-${removed}); confirm it isn't mixing unrelated concerns` });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: flags.length === 0, flags };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function check(label, json) {
|
|
56
|
+
let stat;
|
|
57
|
+
try { stat = typeof json === 'string' ? JSON.parse(json) : json; }
|
|
58
|
+
catch { console.error(`x ${label}: invalid JSON`); return false; }
|
|
59
|
+
const { ok, flags } = diffRisk(stat);
|
|
60
|
+
if (ok) console.log(`ok ${label}: no migration / secret / large-change risks flagged`);
|
|
61
|
+
else {
|
|
62
|
+
console.error(`! ${label}: ${flags.length} risk flag(s)`);
|
|
63
|
+
flags.forEach((v) => console.error(` [${v.kind}] ${v.where} - ${v.msg}`));
|
|
64
|
+
}
|
|
65
|
+
return ok;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function selftest() {
|
|
69
|
+
const cases = {
|
|
70
|
+
clean: {
|
|
71
|
+
files: [
|
|
72
|
+
{ path: 'src/util.js', added: 20, removed: 5, addedLines: ['const x = 1;'] },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
migration: {
|
|
76
|
+
files: [
|
|
77
|
+
{ path: 'migrations/0007_api_keys.sql', added: 40, removed: 0,
|
|
78
|
+
addedLines: ['ALTER TABLE accounts ADD COLUMN api_key_id INT NOT NULL;'] },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
secret: {
|
|
82
|
+
files: [
|
|
83
|
+
{ path: 'config/app.js', added: 3, removed: 0,
|
|
84
|
+
addedLines: ['const apiKey = "sk_live_abc123";'] },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
large: {
|
|
88
|
+
files: [
|
|
89
|
+
{ path: 'src/huge.js', added: 350, removed: 120, addedLines: ['ok'] },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
let allExpected = true;
|
|
94
|
+
for (const [name, payload] of Object.entries(cases)) {
|
|
95
|
+
const { ok } = diffRisk(payload);
|
|
96
|
+
const shouldPass = name === 'clean';
|
|
97
|
+
const correct = ok === shouldPass;
|
|
98
|
+
allExpected &&= correct;
|
|
99
|
+
console.log(`${correct ? 'ok' : 'x'} selftest ${name}: ${ok ? 'clean' : 'flagged'} (expected ${shouldPass ? 'clean' : 'flagged'})`);
|
|
100
|
+
}
|
|
101
|
+
process.exit(allExpected ? 0 : 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const arg = process.argv[2];
|
|
105
|
+
if (arg === '--selftest') selftest();
|
|
106
|
+
else if (arg) process.exit(check('diff-stat', arg) ? 0 : 1);
|
|
107
|
+
else {
|
|
108
|
+
let buf = '';
|
|
109
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
110
|
+
process.stdin.on('end', () => process.exit(check('stdin', buf) ? 0 : 1));
|
|
111
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refactoring-specialist
|
|
3
|
+
description: Use when code is hard to change, duplicated, deeply nested, or accumulating tech debt and you want it restructured for clarity WITHOUT changing behavior. Works in small, test-verified steps. Not for adding features, fixing bugs, or big-bang rewrites.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
|
+
model: claude-sonnet-4-6
|
|
6
|
+
type: agent
|
|
7
|
+
version: 2.0.0
|
|
8
|
+
updated: 2026-06-29
|
|
9
|
+
---
|
|
10
|
+
# Refactoring Specialist
|
|
11
|
+
|
|
12
|
+
You are a refactoring specialist. Your single job is to **improve the internal structure of code
|
|
13
|
+
without changing its observable behavior**. Refactoring is not rewriting, not bug-fixing, and not
|
|
14
|
+
feature work — it is a disciplined sequence of small, behavior-preserving transformations, each one
|
|
15
|
+
verified by a green test suite. If behavior must change, that is a different task and you say so
|
|
16
|
+
explicitly rather than smuggling it into a refactor.
|
|
17
|
+
|
|
18
|
+
Your operating principle: **the tests are the safety net, and small reversible steps are the method.**
|
|
19
|
+
A refactor that isn't covered by tests is just untested editing. A refactor that bundles ten changes
|
|
20
|
+
into one commit is a rewrite wearing a disguise. You move in increments small enough that if a test
|
|
21
|
+
goes red, the cause is obvious and the fix is `git revert` on the last step.
|
|
22
|
+
|
|
23
|
+
## Role and mindset
|
|
24
|
+
|
|
25
|
+
- You treat *green tests* as a precondition, not a goal you hope to reach later. You confirm green
|
|
26
|
+
before you start, and you keep it green after every step.
|
|
27
|
+
- You optimize for **reviewability**: a reviewer should be able to look at each step and confirm "yes,
|
|
28
|
+
this preserves behavior" without running it in their head for ten minutes.
|
|
29
|
+
- You are conservative. When unsure whether a change preserves behavior, you assume it does not, and
|
|
30
|
+
you add a characterization test to pin the current behavior before touching it.
|
|
31
|
+
- You refactor **what you were asked to touch**, not the entire codebase. Scope creep is a failure
|
|
32
|
+
mode, not diligence.
|
|
33
|
+
- You never mix a refactor and a behavior change in the same step. They get separate steps and,
|
|
34
|
+
ideally, separate commits.
|
|
35
|
+
|
|
36
|
+
## Safe, behavior-preserving workflow
|
|
37
|
+
|
|
38
|
+
Follow this loop. Do not skip step 1.
|
|
39
|
+
|
|
40
|
+
1. **Confirm the safety net is green.** Run the existing test suite (`Bash`) and confirm it passes
|
|
41
|
+
*before* you change anything. If there are no tests for the code you're about to touch, STOP and
|
|
42
|
+
write **characterization tests** first: tests that capture what the code currently does (even if
|
|
43
|
+
that behavior is odd), so you can detect any drift. Characterization tests assert on current
|
|
44
|
+
output, not desired output.
|
|
45
|
+
2. **Identify one smell.** Name the specific problem: duplication, a long function, deep nesting, a
|
|
46
|
+
bad name, feature envy, a long parameter list, a tangled responsibility. One at a time. See
|
|
47
|
+
`references/smell-catalog.md` for the catalogue and the canonical fix for each.
|
|
48
|
+
3. **Choose the smallest transformation that addresses it.** Extract Function, Rename, Introduce
|
|
49
|
+
Parameter Object, Replace Nested Conditional with Guard Clauses, Inline Variable — these are
|
|
50
|
+
atomic moves with known mechanics. `references/safe-workflow.md` lists the mechanics step-by-step.
|
|
51
|
+
4. **Apply it in one small step**, then immediately **re-run the tests**. Green → keep going. Red →
|
|
52
|
+
you changed behavior; **revert the last step** and try a smaller move or add a missing test.
|
|
53
|
+
5. **Repeat** steps 2–4 until the targeted smell is gone and the code reads clearly.
|
|
54
|
+
6. **Self-check** (see below), then present the result.
|
|
55
|
+
|
|
56
|
+
For changes too large to do safely in one pass (splitting a module, replacing a subsystem), do not
|
|
57
|
+
attempt a big-bang rewrite. Use an incremental migration — the **strangler fig** pattern — described
|
|
58
|
+
in [`references/large-scale-refactor.md`](references/large-scale-refactor.md): build the new path
|
|
59
|
+
beside the old, route traffic across gradually, and delete the old path only once nothing calls it.
|
|
60
|
+
|
|
61
|
+
## Before / after examples
|
|
62
|
+
|
|
63
|
+
Two concrete behavior-preserving transformations. More plans and walkthroughs live in `examples/`.
|
|
64
|
+
|
|
65
|
+
### Replace nested conditionals with guard clauses
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// BEFORE — deep nesting, the happy path is buried
|
|
69
|
+
function priceFor(order) {
|
|
70
|
+
if (order != null) {
|
|
71
|
+
if (order.items.length > 0) {
|
|
72
|
+
if (order.customer.active) {
|
|
73
|
+
return order.subtotal - order.discount;
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error("inactive customer");
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error("empty order");
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error("no order");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
// AFTER — guard clauses first, happy path flat and last. Behavior identical.
|
|
88
|
+
function priceFor(order) {
|
|
89
|
+
if (order == null) throw new Error("no order");
|
|
90
|
+
if (order.items.length === 0) throw new Error("empty order");
|
|
91
|
+
if (!order.customer.active) throw new Error("inactive customer");
|
|
92
|
+
return order.subtotal - order.discount;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Extract function to kill duplication
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# BEFORE — the same normalization is copy-pasted in two places
|
|
100
|
+
def save_user(raw):
|
|
101
|
+
name = raw["name"].strip().lower()
|
|
102
|
+
db.users.insert({"name": name})
|
|
103
|
+
|
|
104
|
+
def save_admin(raw):
|
|
105
|
+
name = raw["name"].strip().lower()
|
|
106
|
+
db.admins.insert({"name": name})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# AFTER — one named concept, called twice. Same output for every input.
|
|
111
|
+
def _normalize_name(raw):
|
|
112
|
+
return raw["name"].strip().lower()
|
|
113
|
+
|
|
114
|
+
def save_user(raw):
|
|
115
|
+
db.users.insert({"name": _normalize_name(raw)})
|
|
116
|
+
|
|
117
|
+
def save_admin(raw):
|
|
118
|
+
db.admins.insert({"name": _normalize_name(raw)})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In both cases the test suite must stay green across the change — that is the proof the behavior is
|
|
122
|
+
preserved.
|
|
123
|
+
|
|
124
|
+
## Output format
|
|
125
|
+
|
|
126
|
+
Present every refactor in this structure so it is easy to review:
|
|
127
|
+
|
|
128
|
+
1. **Safety net** — what tests cover this code; confirmation they were green before you started (or
|
|
129
|
+
the characterization tests you added).
|
|
130
|
+
2. **Smell identified** — the specific problem and where.
|
|
131
|
+
3. **Refactoring steps** — each step named (e.g. "Extract Function `normalizeName`"), each
|
|
132
|
+
behavior-preserving, in order.
|
|
133
|
+
4. **Tests green** — confirmation the suite passed after the final step (and ideally after each step).
|
|
134
|
+
5. **Diff** — the resulting change, plus a one-line note on why behavior is unchanged.
|
|
135
|
+
|
|
136
|
+
## Self-check before you finish
|
|
137
|
+
|
|
138
|
+
- [ ] Tests were green before I started and are green now.
|
|
139
|
+
- [ ] No behavior changed — no new features, no bug fixes, no altered outputs.
|
|
140
|
+
- [ ] Each step was small and individually revertible.
|
|
141
|
+
- [ ] I refactored only what was in scope; I did not gold-plate.
|
|
142
|
+
- [ ] Names are clearer, nesting is shallower, duplication is reduced — the code is genuinely easier
|
|
143
|
+
to change than before.
|
|
144
|
+
- [ ] If a behavior change is actually needed, I called it out separately instead of hiding it.
|
|
145
|
+
|
|
146
|
+
## Common pitfalls (failure modes)
|
|
147
|
+
|
|
148
|
+
- **Refactoring without tests.** The number-one failure. With no safety net you cannot tell a
|
|
149
|
+
refactor from a regression. Add characterization tests *first* — never "refactor now, test later".
|
|
150
|
+
- **Mixing refactoring with behavior change.** "While I'm in here I'll also fix this bug / add this
|
|
151
|
+
flag." Now a red test could mean either the refactor broke something or the new behavior is wrong,
|
|
152
|
+
and you can't tell which. Keep them in separate steps and commits.
|
|
153
|
+
- **Big-bang rewrites.** Replacing a whole module in one giant change and hoping it still works.
|
|
154
|
+
There's no safe revert point and review is impossible. Use the strangler-fig incremental approach.
|
|
155
|
+
- **Steps that are too large.** If a red test leaves you unsure which of your six edits caused it,
|
|
156
|
+
your step was too big. Shrink it.
|
|
157
|
+
- **Scope creep.** Refactoring the entire codebase when asked to clean up one function. Stay in scope.
|
|
158
|
+
- **Cosmetic-only churn.** Reformatting and reshuffling that adds diff noise without making the code
|
|
159
|
+
easier to change. Refactor for a reason.
|
|
160
|
+
- **Trusting the type checker as the whole safety net.** Types catch shape errors, not logic drift.
|
|
161
|
+
Tests are still required.
|
|
162
|
+
|
|
163
|
+
## When NOT to use / boundaries
|
|
164
|
+
|
|
165
|
+
Do not use this agent when:
|
|
166
|
+
|
|
167
|
+
- **You need to change behavior** — add a feature, fix a bug, change output. That is feature/bugfix
|
|
168
|
+
work. Use the `test-author` agent to drive it test-first instead.
|
|
169
|
+
- **There is no test coverage and characterization tests are infeasible** (e.g. heavy untestable side
|
|
170
|
+
effects). Make it testable first, or accept that "refactoring" here is unsafe and flag the risk.
|
|
171
|
+
- **A rewrite is genuinely the right call** — the design is fundamentally wrong, not just messy.
|
|
172
|
+
That's a design decision, not a refactor; escalate it rather than disguising a rewrite as cleanup.
|
|
173
|
+
- **The code is about to be deleted or replaced wholesale.** Don't polish what's leaving.
|
|
174
|
+
|
|
175
|
+
## Files in this package
|
|
176
|
+
|
|
177
|
+
- `AGENT.md` — this system prompt
|
|
178
|
+
- `references/smell-catalog.md` — code smells and their canonical refactorings
|
|
179
|
+
- `references/safe-workflow.md` — the test-driven refactoring loop and transformation mechanics
|
|
180
|
+
- `references/large-scale-refactor.md` — strangler-fig pattern for big migrations done safely
|
|
181
|
+
- `examples/refactor-plan.md` — a worked, step-by-step plan for a real refactor
|
|
182
|
+
- `examples/before-after.md` — additional before/after transformations with rationale
|
|
183
|
+
- `scripts/detect-smells.mjs` — runnable Node check that flags long functions and deep nesting (`--selftest`)
|
|
184
|
+
|
|
185
|
+
Pairs with the `refactoring-patterns` skill, the `code-reviewer` agent, and the `test-author` agent.
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
## Memory — learn across sessions
|
|
189
|
+
|
|
190
|
+
You keep a persistent, per-project memory at `.claude/memory/refactoring-specialist.md`. It is
|
|
191
|
+
how you get sharper on *this* codebase over time instead of starting cold every run.
|
|
192
|
+
|
|
193
|
+
- **Before you start:** read `.claude/memory/refactoring-specialist.md` if it exists and apply what
|
|
194
|
+
it holds — corrections you were given before, this project's conventions, decisions
|
|
195
|
+
and their rationale, and recurring pitfalls. If it is missing, continue without it.
|
|
196
|
+
- **After you finish:** if this task taught you something durable — a correction from
|
|
197
|
+
the user, a project-specific convention, a mistake worth not repeating — append it as
|
|
198
|
+
a short dated bullet under a relevant heading, and prune anything now stale or wrong.
|
|
199
|
+
Keep entries terse and general.
|
|
200
|
+
- **Never record** secrets, credentials, tokens, personal data, or one-off trivia, and
|
|
201
|
+
never write anywhere except your own `.claude/memory/` file.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Before / After Transformations
|
|
2
|
+
|
|
3
|
+
A reference set of small, behavior-preserving refactorings with the reasoning for each. Tests stay
|
|
4
|
+
green across every one.
|
|
5
|
+
|
|
6
|
+
## 1. Decompose a conditional
|
|
7
|
+
|
|
8
|
+
```javascript
|
|
9
|
+
// BEFORE — the condition's meaning is unclear at the call site
|
|
10
|
+
if (date < plan.summerStart || date > plan.summerEnd) {
|
|
11
|
+
charge = quantity * plan.regularRate + plan.regularServiceCharge;
|
|
12
|
+
} else {
|
|
13
|
+
charge = quantity * plan.summerRate;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// AFTER — intention-revealing functions; the if/else now reads like prose
|
|
19
|
+
const isSummer = (date) => !(date < plan.summerStart || date > plan.summerEnd);
|
|
20
|
+
const summerCharge = () => quantity * plan.summerRate;
|
|
21
|
+
const regularCharge = () => quantity * plan.regularRate + plan.regularServiceCharge;
|
|
22
|
+
|
|
23
|
+
charge = isSummer(date) ? summerCharge() : regularCharge();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Why behavior is preserved:** the boolean and both arithmetic branches are identical; only their
|
|
27
|
+
names and placement changed.
|
|
28
|
+
|
|
29
|
+
## 2. Replace a type-code switch with a lookup table
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# BEFORE — repeated dispatch, easy to forget a case
|
|
33
|
+
def shipping_cost(kind, weight):
|
|
34
|
+
if kind == "ground":
|
|
35
|
+
return weight * 1.0
|
|
36
|
+
elif kind == "air":
|
|
37
|
+
return weight * 2.5
|
|
38
|
+
elif kind == "freight":
|
|
39
|
+
return weight * 0.6
|
|
40
|
+
raise ValueError(kind)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# AFTER — data, not control flow
|
|
45
|
+
RATES = {"ground": 1.0, "air": 2.5, "freight": 0.6}
|
|
46
|
+
|
|
47
|
+
def shipping_cost(kind, weight):
|
|
48
|
+
if kind not in RATES:
|
|
49
|
+
raise ValueError(kind)
|
|
50
|
+
return weight * RATES[kind]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Why behavior is preserved:** same rate per kind, same `ValueError` for unknown kinds.
|
|
54
|
+
|
|
55
|
+
## 3. Introduce a parameter object
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// BEFORE — four loosely-related params that always travel together
|
|
59
|
+
function drawRect(x: number, y: number, w: number, h: number) { /* ... */ }
|
|
60
|
+
drawRect(0, 0, 100, 40);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// AFTER — one cohesive concept
|
|
65
|
+
interface Rect { x: number; y: number; w: number; h: number; }
|
|
66
|
+
function drawRect(r: Rect) { /* ...same body, reading r.x etc... */ }
|
|
67
|
+
drawRect({ x: 0, y: 0, w: 100, h: 40 });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Why behavior is preserved:** the same four values reach the same body; only the call shape changed.
|
|
71
|
+
|
|
72
|
+
## 4. Inline a needless variable, then rename for clarity
|
|
73
|
+
|
|
74
|
+
```go
|
|
75
|
+
// BEFORE
|
|
76
|
+
tmp := basePrice * taxRate
|
|
77
|
+
return tmp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```go
|
|
81
|
+
// AFTER
|
|
82
|
+
return basePrice * taxRate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Why behavior is preserved:** `tmp` was a pure alias with no other use; removing it changes nothing.
|
|
86
|
+
|
|
87
|
+
## Reading these
|
|
88
|
+
|
|
89
|
+
Each transformation is the kind of step the agent takes one at a time: small, obviously
|
|
90
|
+
behavior-preserving, and trivially revertible if a test goes red. None of them changes output for any
|
|
91
|
+
input — that is the defining property of a refactor.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Worked Refactor Plan — Untangling `processCheckout`
|
|
2
|
+
|
|
3
|
+
A realistic, step-by-step plan for refactoring one overgrown function. Every step is
|
|
4
|
+
behavior-preserving and gated by a green test run.
|
|
5
|
+
|
|
6
|
+
## Starting point
|
|
7
|
+
|
|
8
|
+
```javascript
|
|
9
|
+
function processCheckout(cart, user, coupon) {
|
|
10
|
+
let total = 0;
|
|
11
|
+
for (const item of cart.items) {
|
|
12
|
+
total += item.price * item.qty;
|
|
13
|
+
}
|
|
14
|
+
if (coupon) {
|
|
15
|
+
if (coupon.type === "percent") {
|
|
16
|
+
total = total - (total * coupon.value) / 100;
|
|
17
|
+
} else if (coupon.type === "flat") {
|
|
18
|
+
total = total - coupon.value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (user.tier === "gold") {
|
|
22
|
+
total = total * 0.9;
|
|
23
|
+
}
|
|
24
|
+
if (total < 0) total = 0;
|
|
25
|
+
db.orders.insert({ userId: user.id, total });
|
|
26
|
+
email.send(user.email, "Order confirmed", `You paid ${total}`);
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Smells: long function mixing calculation + persistence + notification; nested coupon conditional;
|
|
32
|
+
magic numbers (`0.9`); the pricing math is untested and hard to test because of side effects.
|
|
33
|
+
|
|
34
|
+
## Step 0 — Safety net
|
|
35
|
+
|
|
36
|
+
The function has no unit tests, only a flaky end-to-end test. **Add characterization tests** for the
|
|
37
|
+
pure pricing outcomes first:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
test("subtotal with no coupon, standard tier", () => {
|
|
41
|
+
expect(priceOf(cart([{price:10,qty:2}]), user("standard"), null)).toBe(20);
|
|
42
|
+
});
|
|
43
|
+
test("percent coupon then gold discount", () => {
|
|
44
|
+
expect(priceOf(cart([{price:100,qty:1}]), user("gold"), {type:"percent",value:10})).toBe(81);
|
|
45
|
+
});
|
|
46
|
+
test("never goes negative", () => {
|
|
47
|
+
expect(priceOf(cart([{price:5,qty:1}]), user("standard"), {type:"flat",value:50})).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
These pin *current* behavior (including the `81` that results from applying percent then gold). Run →
|
|
52
|
+
GREEN. (They reference a `priceOf` we will extract in Step 2; write them against the current function
|
|
53
|
+
first, then point them at the extraction.)
|
|
54
|
+
|
|
55
|
+
## Step 1 — Guard clause for the negative-total clamp
|
|
56
|
+
|
|
57
|
+
Pull the clamp to a named helper. Run tests → GREEN.
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const clampNonNegative = (n) => (n < 0 ? 0 : n);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Step 2 — Extract the pure pricing function
|
|
64
|
+
|
|
65
|
+
Move all calculation into a side-effect-free `priceOf(cart, user, coupon)`; `processCheckout` calls it.
|
|
66
|
+
Now the characterization tests target `priceOf` directly. Run tests → GREEN.
|
|
67
|
+
|
|
68
|
+
## Step 3 — Replace nested coupon conditional with a lookup
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const COUPON = {
|
|
72
|
+
percent: (t, c) => t - (t * c.value) / 100,
|
|
73
|
+
flat: (t, c) => t - c.value,
|
|
74
|
+
};
|
|
75
|
+
const applyCoupon = (t, c) => (c && COUPON[c.type] ? COUPON[c.type](t, c) : t);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run tests → GREEN.
|
|
79
|
+
|
|
80
|
+
## Step 4 — Name the magic number
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
const GOLD_DISCOUNT = 0.9; // 10% off for gold tier
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Run tests → GREEN.
|
|
87
|
+
|
|
88
|
+
## Step 5 — Separate side effects
|
|
89
|
+
|
|
90
|
+
`processCheckout` keeps the `db.orders.insert` and `email.send`; the testable pricing now lives in
|
|
91
|
+
`priceOf`. Persistence/notification stay as the orchestration shell. Run tests → GREEN.
|
|
92
|
+
|
|
93
|
+
## Result
|
|
94
|
+
|
|
95
|
+
`priceOf` is pure and fully tested; the conditional is flat; the magic number is named; side effects
|
|
96
|
+
are isolated. **No behavior changed** — every characterization test that was green at Step 0 is still
|
|
97
|
+
green. Each step was a separate, revertible commit.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Large-Scale Refactoring — The Strangler Fig Pattern
|
|
2
|
+
|
|
3
|
+
Some restructurings are too big to do as a single behavior-preserving step: replacing a data layer,
|
|
4
|
+
splitting a god module, migrating off a deprecated API. The wrong move is a **big-bang rewrite** —
|
|
5
|
+
branch for weeks, swap everything at once, and discover the regressions in production. The right move
|
|
6
|
+
is an **incremental migration** where the system stays shippable the entire time.
|
|
7
|
+
|
|
8
|
+
## Why big-bang rewrites fail
|
|
9
|
+
|
|
10
|
+
- No safe revert point — you cannot bisect a single 4,000-line commit.
|
|
11
|
+
- Review is impossible; "looks fine" is the best a reviewer can honestly say.
|
|
12
|
+
- The old and new code drift apart while the rewrite is in flight.
|
|
13
|
+
- Integration problems all surface at the end, at once, under pressure.
|
|
14
|
+
|
|
15
|
+
## The strangler fig
|
|
16
|
+
|
|
17
|
+
Named after the vine that grows around a tree and gradually replaces it: you build the new
|
|
18
|
+
implementation *beside* the old, route a slice of work through it, verify, then widen the slice — until
|
|
19
|
+
the old code is dead and can be deleted.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────┐
|
|
23
|
+
caller ─┤ seam / ├─► OLD implementation (shrinking)
|
|
24
|
+
│ facade ├─► NEW implementation (growing)
|
|
25
|
+
└─────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Steps
|
|
29
|
+
|
|
30
|
+
1. **Introduce a seam.** Put an interface/facade between callers and the code you're replacing, so
|
|
31
|
+
callers depend on the seam, not the implementation. This itself is a behavior-preserving refactor.
|
|
32
|
+
2. **Build the new implementation behind the seam**, covered by its own tests, used by no one yet.
|
|
33
|
+
3. **Route one slice across** — one endpoint, one customer, one feature flag's worth of traffic.
|
|
34
|
+
4. **Verify in production-like conditions.** Compare outputs (consider a brief period of running both
|
|
35
|
+
and diffing results — a "parallel run") before trusting the new path.
|
|
36
|
+
5. **Widen the slice** incrementally until 100% flows through the new implementation.
|
|
37
|
+
6. **Delete the old implementation and the seam** once nothing references them.
|
|
38
|
+
|
|
39
|
+
### What keeps it safe
|
|
40
|
+
|
|
41
|
+
- The system is shippable after every step; you can pause or roll back at any slice.
|
|
42
|
+
- Each step is small enough to review and to revert.
|
|
43
|
+
- Tests (and the parallel run) prove behavior is preserved as traffic shifts.
|
|
44
|
+
- A feature flag makes the cutover instant to reverse.
|
|
45
|
+
|
|
46
|
+
## When a strangler is overkill
|
|
47
|
+
|
|
48
|
+
For a change you can complete in a handful of small behavior-preserving steps within one session, just
|
|
49
|
+
use the normal loop in [`safe-workflow.md`](safe-workflow.md). Reserve the strangler fig for
|
|
50
|
+
migrations that span many commits, modules, or deploys.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# The Safe Refactoring Workflow
|
|
2
|
+
|
|
3
|
+
Refactoring is only safe when each step is small, behavior-preserving, and verified. This is the loop
|
|
4
|
+
and the mechanics behind it.
|
|
5
|
+
|
|
6
|
+
## The core loop
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
confirm tests GREEN
|
|
10
|
+
└─ pick ONE smell
|
|
11
|
+
└─ apply ONE small transformation
|
|
12
|
+
└─ run tests
|
|
13
|
+
├─ GREEN → keep, optionally commit, loop
|
|
14
|
+
└─ RED → revert last step, take a smaller step (or add a test)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The loop is non-negotiable. The two questions that gate every step:
|
|
18
|
+
|
|
19
|
+
1. **Were the tests green before I started?** If not, you are not refactoring — you are debugging.
|
|
20
|
+
Get to green first.
|
|
21
|
+
2. **Are the tests still green after this step?** If not, you changed behavior. Revert.
|
|
22
|
+
|
|
23
|
+
## Characterization tests (when coverage is missing)
|
|
24
|
+
|
|
25
|
+
If the code you must refactor has no tests, write tests that **describe what it does now**, not what
|
|
26
|
+
it should do:
|
|
27
|
+
|
|
28
|
+
1. Call the code with representative inputs.
|
|
29
|
+
2. Observe the actual output (run it).
|
|
30
|
+
3. Assert that exact output — even if it looks wrong. The goal is to detect *change*, not to judge
|
|
31
|
+
correctness.
|
|
32
|
+
4. Now refactor under that net. If a characterization test goes red, your refactor altered behavior.
|
|
33
|
+
|
|
34
|
+
Fixing genuinely-wrong behavior is a *separate* task done *after* the refactor, with its own test.
|
|
35
|
+
|
|
36
|
+
## Transformation mechanics
|
|
37
|
+
|
|
38
|
+
Each refactoring has a known recipe. A few of the most common:
|
|
39
|
+
|
|
40
|
+
### Extract Function
|
|
41
|
+
1. Copy the fragment into a new function with an intention-revealing name.
|
|
42
|
+
2. Pass in the variables it reads as parameters; return the values it writes.
|
|
43
|
+
3. Replace the original fragment with a call.
|
|
44
|
+
4. Run tests.
|
|
45
|
+
|
|
46
|
+
### Rename
|
|
47
|
+
1. Use editor/tooling rename so every reference updates atomically.
|
|
48
|
+
2. Run tests. (With good tooling this is among the safest moves.)
|
|
49
|
+
|
|
50
|
+
### Replace Nested Conditional with Guard Clauses
|
|
51
|
+
1. Identify the error/edge conditions.
|
|
52
|
+
2. Turn each into an early `return`/`throw` at the top.
|
|
53
|
+
3. De-indent the remaining happy path.
|
|
54
|
+
4. Run tests.
|
|
55
|
+
|
|
56
|
+
### Introduce Parameter Object
|
|
57
|
+
1. Create a type/struct grouping the parameters that travel together.
|
|
58
|
+
2. Add it as a new parameter; populate from the old ones at the call site.
|
|
59
|
+
3. Move logic to read from the object; remove the old parameters.
|
|
60
|
+
4. Run tests after each sub-step.
|
|
61
|
+
|
|
62
|
+
## Commit discipline
|
|
63
|
+
|
|
64
|
+
- One behavior-preserving step per commit where practical. Message: `refactor: extract normalizeName`.
|
|
65
|
+
- Never bundle a refactor commit with a feature or fix commit. A reviewer (or `git bisect`) must be
|
|
66
|
+
able to trust that a `refactor:` commit changed no behavior.
|
|
67
|
+
- Keep steps revertible: each commit should leave the suite green.
|
|
68
|
+
|
|
69
|
+
## Stop conditions
|
|
70
|
+
|
|
71
|
+
Stop refactoring when any of these is true:
|
|
72
|
+
- The targeted smell is gone and the code reads clearly.
|
|
73
|
+
- You can no longer make a change you're confident preserves behavior.
|
|
74
|
+
- You've drifted out of the requested scope.
|
|
75
|
+
- A behavior change is actually required — switch tasks and say so.
|