vanara 0.1.0 → 0.2.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 +140 -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 +168 -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 +112 -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 +156 -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 +185 -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 +168 -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 +163 -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 +177 -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 +163 -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 +146 -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,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable check: lints a README for the required sections and a runnable
|
|
3
|
+
// quickstart code block. Demonstrates an *executed*, verifiable example asset.
|
|
4
|
+
// Zero dependencies — Node built-ins only.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node lint-readme.mjs README.md # lint a file
|
|
8
|
+
// node lint-readme.mjs --selftest # run built-in test cases (exit 0/1)
|
|
9
|
+
// echo '<markdown>' | node lint-readme.mjs # lint from stdin
|
|
10
|
+
//
|
|
11
|
+
// A README passes when it has a title (# H1), a one-line description, every
|
|
12
|
+
// required section heading, and at least one fenced code block (the quickstart).
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
|
|
16
|
+
// Each rule: [label, predicate(markdown) -> boolean]. Heading matches are
|
|
17
|
+
// case-insensitive and accept common synonyms.
|
|
18
|
+
const REQUIRED_SECTIONS = [
|
|
19
|
+
['Install', /^#{1,6}\s+(install|installation|setup|getting started)\b/im],
|
|
20
|
+
['Usage', /^#{1,6}\s+(usage|quick ?start|getting started|examples?)\b/im],
|
|
21
|
+
['License', /^#{1,6}\s+licen[sc]e\b/im],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const RULES = [
|
|
25
|
+
['has a top-level title (# H1)', (md) => /^#\s+\S+/m.test(md)],
|
|
26
|
+
['has a one-line description under the title', (md) => hasDescription(md)],
|
|
27
|
+
['has at least one fenced code block (quickstart)', (md) => /```[\s\S]*?```/.test(md)],
|
|
28
|
+
...REQUIRED_SECTIONS.map(([name, re]) => [`has a "${name}" section`, (md) => re.test(md)]),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// A description is a non-empty, non-heading, non-badge line appearing after the
|
|
32
|
+
// H1 title within the first several lines.
|
|
33
|
+
function hasDescription(md) {
|
|
34
|
+
const lines = md.split(/\r?\n/);
|
|
35
|
+
const titleIdx = lines.findIndex((l) => /^#\s+\S+/.test(l));
|
|
36
|
+
if (titleIdx === -1) return false;
|
|
37
|
+
for (let i = titleIdx + 1; i < Math.min(lines.length, titleIdx + 8); i++) {
|
|
38
|
+
const line = lines[i].trim();
|
|
39
|
+
if (!line) continue;
|
|
40
|
+
if (line.startsWith('#')) return false; // hit next heading first
|
|
41
|
+
if (line.startsWith('<!--')) continue; // skip comments
|
|
42
|
+
if (/^[[!]/.test(line) && /\]\(/.test(line)) continue; // skip badge/link-only lines
|
|
43
|
+
return true; // found prose
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function lintReadme(md) {
|
|
49
|
+
const failures = RULES.filter(([, fn]) => !safe(fn, md)).map(([msg]) => msg);
|
|
50
|
+
return { ok: failures.length === 0, failures };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safe(fn, md) { try { return fn(md); } catch { return false; } }
|
|
54
|
+
|
|
55
|
+
function lint(label, md) {
|
|
56
|
+
const { ok, failures } = lintReadme(md);
|
|
57
|
+
if (ok) console.log(`✓ ${label}: README has all required sections`);
|
|
58
|
+
else { console.error(`✗ ${label}:`); failures.forEach((f) => console.error(` - ${f}`)); }
|
|
59
|
+
return ok;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const COMPLETE = `# Acme
|
|
63
|
+
|
|
64
|
+
A fast CLI for doing the thing.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
\`\`\`bash
|
|
68
|
+
npm install acme
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
acme run
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
MIT
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const INCOMPLETE = `# Acme
|
|
81
|
+
|
|
82
|
+
Some text but no fenced code block and no license section.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
Run npm install acme.
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
function selftest() {
|
|
89
|
+
const cases = {
|
|
90
|
+
valid_complete: COMPLETE,
|
|
91
|
+
invalid_incomplete: INCOMPLETE,
|
|
92
|
+
};
|
|
93
|
+
let allExpected = true;
|
|
94
|
+
for (const [name, md] of Object.entries(cases)) {
|
|
95
|
+
const { ok } = lintReadme(md);
|
|
96
|
+
const shouldPass = name.startsWith('valid');
|
|
97
|
+
const correct = ok === shouldPass;
|
|
98
|
+
allExpected &&= correct;
|
|
99
|
+
console.log(
|
|
100
|
+
`${correct ? '✓' : '✗'} selftest ${name}: ${ok ? 'pass' : 'fail'} (expected ${shouldPass ? 'pass' : 'fail'})`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
process.exit(allExpected ? 0 : 1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const arg = process.argv[2];
|
|
107
|
+
if (arg === '--selftest') selftest();
|
|
108
|
+
else if (arg) {
|
|
109
|
+
let md;
|
|
110
|
+
try { md = readFileSync(arg, 'utf8'); }
|
|
111
|
+
catch { console.error(`✗ cannot read file: ${arg}`); process.exit(1); }
|
|
112
|
+
process.exit(lint(arg, md) ? 0 : 1);
|
|
113
|
+
} else {
|
|
114
|
+
let buf = '';
|
|
115
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
116
|
+
process.stdin.on('end', () => process.exit(lint('stdin', buf) ? 0 : 1));
|
|
117
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refactoring-patterns
|
|
3
|
+
description: Improve code structure without changing behavior — the discipline of small, named, test-backed moves. Extract function/variable, inline, rename, replace conditional with polymorphism, introduce parameter object, guard clauses. Recognize smells, refactor safely, avoid big-bang rewrites.
|
|
4
|
+
type: skill
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
updated: 2026-06-29
|
|
7
|
+
---
|
|
8
|
+
# Refactoring Patterns
|
|
9
|
+
|
|
10
|
+
Refactoring is **changing the internal structure of code without changing its observable behavior**. It
|
|
11
|
+
is not "cleanup whenever," and it is not rewriting. It is a disciplined sequence of small, *named*,
|
|
12
|
+
behavior-preserving moves, each verified by tests, each committable on its own. Done right it is nearly
|
|
13
|
+
risk-free; done wrong — without tests, mixed with feature work, or as a big-bang rewrite — it is one of
|
|
14
|
+
the most reliable ways to ship a regression.
|
|
15
|
+
|
|
16
|
+
This skill is the deep reference for that discipline: the safety net that makes it possible, the named
|
|
17
|
+
moves themselves, the smells that signal where to apply them, and the trade-offs of when *not* to. Heavy
|
|
18
|
+
detail lives in `references/`; copy-paste before/after material in `examples/`; a runnable smell detector
|
|
19
|
+
in `scripts/`.
|
|
20
|
+
|
|
21
|
+
## The mental model
|
|
22
|
+
|
|
23
|
+
Two hats, never worn at once (Kent Beck's rule): you are either **adding behavior** (new tests go red,
|
|
24
|
+
then green) *or* **refactoring** (all tests stay green the whole time). Switching hats mid-edit is the
|
|
25
|
+
root cause of most refactoring disasters, because when a test breaks you can no longer tell whether your
|
|
26
|
+
*restructuring* was wrong or your *new feature* was wrong.
|
|
27
|
+
|
|
28
|
+
| Question | Refactoring answer |
|
|
29
|
+
|---|---|
|
|
30
|
+
| What changes? | structure only — names, shape, location |
|
|
31
|
+
| What stays identical? | observable behavior, the public contract, test results |
|
|
32
|
+
| How do I stay safe? | tests green before, green after, green between every step |
|
|
33
|
+
| How big is a step? | small enough that a broken test points at one change |
|
|
34
|
+
| When do I commit? | after each move that leaves the suite green |
|
|
35
|
+
|
|
36
|
+
## 1. Tests are the safety net (non-negotiable)
|
|
37
|
+
|
|
38
|
+
You cannot refactor code you cannot verify. Before touching structure, ensure a **green** test suite
|
|
39
|
+
exercises the behavior you're about to move. If coverage is missing, write **characterization tests**
|
|
40
|
+
first — tests that pin down what the code *currently* does (even if that's arguably wrong), so any
|
|
41
|
+
behavior drift during refactoring surfaces immediately. See `references/safe-workflow.md` for the
|
|
42
|
+
red-green-refactor loop, characterization testing, and the strangler-fig pattern for large systems.
|
|
43
|
+
|
|
44
|
+
The loop:
|
|
45
|
+
|
|
46
|
+
1. Run the suite — confirm green.
|
|
47
|
+
2. Apply **one** named refactoring.
|
|
48
|
+
3. Run the suite — confirm still green.
|
|
49
|
+
4. Commit.
|
|
50
|
+
5. Repeat.
|
|
51
|
+
|
|
52
|
+
If step 3 goes red, you have exactly one small change to undo. That is the entire value proposition.
|
|
53
|
+
|
|
54
|
+
## 2. Smells: knowing *where* to refactor
|
|
55
|
+
|
|
56
|
+
Refactoring is demand-driven — you don't refactor everything, you refactor what a **code smell** is
|
|
57
|
+
pointing at. The catalogue in `references/code-smells.md` is the full list; the high-frequency ones:
|
|
58
|
+
|
|
59
|
+
- **Long method / long function** — does too much; the body has comment-delimited "paragraphs."
|
|
60
|
+
- **Large class / god object** — too many fields and responsibilities; low cohesion.
|
|
61
|
+
- **Feature envy** — a method that reaches into another object's data more than its own.
|
|
62
|
+
- **Primitive obsession** — strings/ints standing in for real concepts (`string currency`, `int cents`).
|
|
63
|
+
- **Shotgun surgery** — one conceptual change forces edits in many scattered places.
|
|
64
|
+
- **Duplicated code** — the same logic in three spots (the rule of three: extract on the third copy).
|
|
65
|
+
|
|
66
|
+
## 3. The named moves
|
|
67
|
+
|
|
68
|
+
Each refactoring has a **name** and a **mechanics** (a precise step sequence). Naming them lets a team
|
|
69
|
+
say "extract a function here" and share an exact, low-risk procedure. The full catalogue with mechanics
|
|
70
|
+
is in `references/refactoring-catalog.md`. The core set:
|
|
71
|
+
|
|
72
|
+
- **Extract Function** — pull a coherent block into its own named function.
|
|
73
|
+
- **Inline Function/Variable** — the inverse, when the indirection earns nothing.
|
|
74
|
+
- **Rename** — the highest-value, lowest-risk move; a precise name deletes a comment.
|
|
75
|
+
- **Extract Variable** — name a sub-expression to explain it.
|
|
76
|
+
- **Replace Conditional with Polymorphism** — swap a type-switch for dispatch on a type.
|
|
77
|
+
- **Introduce Parameter Object** — bundle a clump of args that always travel together.
|
|
78
|
+
- **Replace Magic Literal with Constant**, **Decompose Conditional**, **Replace Nested Conditional with
|
|
79
|
+
Guard Clauses**, **Move Function/Field**, **Separate Query from Modifier**.
|
|
80
|
+
|
|
81
|
+
### Before / after: Extract Function + guard clauses
|
|
82
|
+
|
|
83
|
+
A long function doing validation, calculation, and formatting at once:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
// before — one function, three jobs, deep nesting
|
|
87
|
+
function invoiceLine(item) {
|
|
88
|
+
if (item) {
|
|
89
|
+
if (item.qty > 0) {
|
|
90
|
+
let total = item.qty * item.price;
|
|
91
|
+
if (item.taxable) { total = total * 1.2; }
|
|
92
|
+
return `${item.name}: $${total.toFixed(2)}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return "invalid";
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// after — guard clauses flatten nesting; intent-named helpers
|
|
101
|
+
function invoiceLine(item) {
|
|
102
|
+
if (!isValid(item)) return "invalid";
|
|
103
|
+
return format(item.name, totalFor(item));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isValid = (i) => i && i.qty > 0;
|
|
107
|
+
const totalFor = (i) => i.taxable ? i.qty * i.price * 1.2 : i.qty * i.price;
|
|
108
|
+
const format = (name, total) => `${name}: $${total.toFixed(2)}`;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Same inputs, same outputs — tests stay green — but each piece now has one job and a name.
|
|
112
|
+
|
|
113
|
+
### Before / after: Replace Conditional with Polymorphism
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// before — the switch will grow with every new type; shotgun surgery waiting to happen
|
|
117
|
+
function area(shape: Shape): number {
|
|
118
|
+
switch (shape.kind) {
|
|
119
|
+
case "circle": return Math.PI * shape.r ** 2;
|
|
120
|
+
case "square": return shape.side ** 2;
|
|
121
|
+
default: throw new Error("unknown shape");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// after — each type owns its behavior; adding a shape is additive, not invasive
|
|
128
|
+
interface Shape { area(): number; }
|
|
129
|
+
class Circle implements Shape { constructor(private r: number) {} area() { return Math.PI * this.r ** 2; } }
|
|
130
|
+
class Square implements Shape { constructor(private side: number) {} area() { return this.side ** 2; } }
|
|
131
|
+
// new shapes add a class; no existing code is touched (Open/Closed)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
See `examples/` for both of these as standalone, runnable files.
|
|
135
|
+
|
|
136
|
+
## 4. Common pitfalls (failure modes)
|
|
137
|
+
|
|
138
|
+
- **Refactoring without tests.** The cardinal sin. With no safety net you are *rewriting and hoping*.
|
|
139
|
+
Write characterization tests first or do not start.
|
|
140
|
+
- **Wearing both hats.** Mixing a refactor with a behavior change in the same commit. When something
|
|
141
|
+
breaks you can't bisect *which* intent caused it, and reviewers can't see the real change in the diff.
|
|
142
|
+
Keep refactor commits and feature commits separate.
|
|
143
|
+
- **Big-bang rewrites.** "Let's just rewrite it cleanly" discards hard-won, battle-tested edge-case
|
|
144
|
+
knowledge and ships with no incremental safety. Prefer the **strangler fig**: grow the new structure
|
|
145
|
+
around the old, route traffic over piece by piece, delete the old when nothing calls it.
|
|
146
|
+
- **Steps too big.** If a "step" touches 30 files before you re-run tests, a red bar tells you nothing
|
|
147
|
+
useful. Shrink the step.
|
|
148
|
+
- **Refactoring on a feature branch for weeks.** Long-lived refactor branches rot against `main` and
|
|
149
|
+
produce merge nightmares. Land small refactors continuously.
|
|
150
|
+
- **Speculative generality** — extracting abstractions "for the future" no caller needs yet. Refactor
|
|
151
|
+
toward concrete, present demands (YAGNI).
|
|
152
|
+
|
|
153
|
+
## 5. When NOT to refactor / trade-offs
|
|
154
|
+
|
|
155
|
+
- **No tests and no time to write them** for code you don't understand — refactoring here is gambling.
|
|
156
|
+
Add characterization tests first, or leave it alone.
|
|
157
|
+
- **Code you're about to delete** — don't polish a corpse.
|
|
158
|
+
- **Stable code nobody touches and nobody complains about** — clean for clean's sake has no payoff;
|
|
159
|
+
refactor when you're *already* in the area for a feature or fix (the "campsite rule").
|
|
160
|
+
- **A hard deadline shipping today** — note the debt, ship, refactor next iteration. Be honest that this
|
|
161
|
+
is borrowing, not free.
|
|
162
|
+
- **Refactor vs. rewrite:** rewrite only when the design is fundamentally wrong for current
|
|
163
|
+
requirements *and* you can carve off a strangler-fig seam. Otherwise incremental refactoring is almost
|
|
164
|
+
always cheaper and safer than a rewrite.
|
|
165
|
+
|
|
166
|
+
## Files in this package
|
|
167
|
+
|
|
168
|
+
- `references/code-smells.md` — the smell catalogue (long method, large class, feature envy, primitive
|
|
169
|
+
obsession, shotgun surgery, and more) with the move each one calls for.
|
|
170
|
+
- `references/refactoring-catalog.md` — named refactorings with their step-by-step mechanics.
|
|
171
|
+
- `references/safe-workflow.md` — red-green-refactor, characterization tests, strangler fig, the two hats.
|
|
172
|
+
- `examples/extract-function.before-after.js` — long function → extracted, named helpers.
|
|
173
|
+
- `examples/replace-conditional-with-polymorphism.before-after.ts` — type-switch → dispatch.
|
|
174
|
+
- `scripts/detect-smells.mjs` — runnable Node scanner that flags long functions and deep nesting
|
|
175
|
+
(`--selftest` for built-in cases).
|
|
176
|
+
|
|
177
|
+
Pairs with the `error-handling-patterns` skill (refactor toward explicit error handling), the
|
|
178
|
+
`testing-patterns` skill (the safety net), and the `code-reviewer` and `refactor-cleaner` agents.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Extract Function (+ guard clauses + extract variable)
|
|
2
|
+
// ------------------------------------------------------
|
|
3
|
+
// A single long function doing validation, calculation, tax, and formatting,
|
|
4
|
+
// refactored into small named pieces. Behavior is identical — the demo at the
|
|
5
|
+
// bottom proves before === after for the same inputs.
|
|
6
|
+
//
|
|
7
|
+
// Run: node extract-function.before-after.js
|
|
8
|
+
|
|
9
|
+
// ---- BEFORE: one function, several jobs, nested conditionals -------------------
|
|
10
|
+
function invoiceLineBefore(item) {
|
|
11
|
+
if (item) {
|
|
12
|
+
if (item.qty > 0) {
|
|
13
|
+
let total = item.qty * item.price;
|
|
14
|
+
if (item.taxable) {
|
|
15
|
+
total = total * 1.2; // what's 1.2? magic number
|
|
16
|
+
}
|
|
17
|
+
return item.name + ": $" + total.toFixed(2);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return "invalid";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- AFTER: guard clause + extracted, intent-named helpers ---------------------
|
|
24
|
+
const TAX_RATE = 0.2; // Replace Magic Literal with Symbolic Constant
|
|
25
|
+
|
|
26
|
+
const isValidItem = (item) => Boolean(item) && item.qty > 0; // Decompose Conditional
|
|
27
|
+
|
|
28
|
+
function lineTotal(item) {
|
|
29
|
+
const subtotal = item.qty * item.price; // Extract Variable
|
|
30
|
+
return item.taxable ? subtotal * (1 + TAX_RATE) : subtotal;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const formatLine = (name, total) => `${name}: $${total.toFixed(2)}`;
|
|
34
|
+
|
|
35
|
+
function invoiceLineAfter(item) {
|
|
36
|
+
if (!isValidItem(item)) return "invalid"; // guard clause flattens nesting
|
|
37
|
+
return formatLine(item.name, lineTotal(item));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- Proof the refactoring preserved behavior ---------------------------------
|
|
41
|
+
const cases = [
|
|
42
|
+
{ name: "Widget", qty: 3, price: 10, taxable: true },
|
|
43
|
+
{ name: "Gadget", qty: 2, price: 5, taxable: false },
|
|
44
|
+
null,
|
|
45
|
+
{ name: "Empty", qty: 0, price: 99, taxable: true },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
let allMatch = true;
|
|
49
|
+
for (const c of cases) {
|
|
50
|
+
const before = invoiceLineBefore(c);
|
|
51
|
+
const after = invoiceLineAfter(c);
|
|
52
|
+
const ok = before === after;
|
|
53
|
+
allMatch = allMatch && ok;
|
|
54
|
+
console.log(`${ok ? "✓" : "✗"} ${JSON.stringify(c)} -> before:"${before}" after:"${after}"`);
|
|
55
|
+
}
|
|
56
|
+
console.log(allMatch ? "\nAll outputs identical — behavior preserved." : "\nMISMATCH — refactor changed behavior!");
|
|
57
|
+
process.exit(allMatch ? 0 : 1);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Replace Conditional with Polymorphism
|
|
2
|
+
// -------------------------------------
|
|
3
|
+
// BEFORE: a type-code switch repeated across multiple functions. Every new shape
|
|
4
|
+
// forces edits to area() AND name() AND any future per-type function — the
|
|
5
|
+
// "repeated switch" / shotgun-surgery smell.
|
|
6
|
+
//
|
|
7
|
+
// AFTER: each type owns its behavior behind a common interface. Adding a shape is
|
|
8
|
+
// purely additive (Open/Closed); no existing function is touched.
|
|
9
|
+
//
|
|
10
|
+
// This is illustrative TypeScript. Compile with `tsc` or read as reference.
|
|
11
|
+
|
|
12
|
+
// ===== BEFORE =================================================================
|
|
13
|
+
type ShapeData =
|
|
14
|
+
| { kind: "circle"; r: number }
|
|
15
|
+
| { kind: "square"; side: number }
|
|
16
|
+
| { kind: "rect"; w: number; h: number };
|
|
17
|
+
|
|
18
|
+
function areaBefore(s: ShapeData): number {
|
|
19
|
+
switch (s.kind) {
|
|
20
|
+
case "circle": return Math.PI * s.r ** 2;
|
|
21
|
+
case "square": return s.side ** 2;
|
|
22
|
+
case "rect": return s.w * s.h;
|
|
23
|
+
default: throw new Error("unknown shape");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nameBefore(s: ShapeData): string {
|
|
28
|
+
switch (s.kind) { // the SAME switch, duplicated — change one, hunt the rest
|
|
29
|
+
case "circle": return "Circle";
|
|
30
|
+
case "square": return "Square";
|
|
31
|
+
case "rect": return "Rectangle";
|
|
32
|
+
default: throw new Error("unknown shape");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ===== AFTER ==================================================================
|
|
37
|
+
interface Shape {
|
|
38
|
+
area(): number;
|
|
39
|
+
name(): string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class Circle implements Shape {
|
|
43
|
+
constructor(private readonly r: number) {}
|
|
44
|
+
area(): number { return Math.PI * this.r ** 2; }
|
|
45
|
+
name(): string { return "Circle"; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class Square implements Shape {
|
|
49
|
+
constructor(private readonly side: number) {}
|
|
50
|
+
area(): number { return this.side ** 2; }
|
|
51
|
+
name(): string { return "Square"; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class Rectangle implements Shape {
|
|
55
|
+
constructor(private readonly w: number, private readonly h: number) {}
|
|
56
|
+
area(): number { return this.w * this.h; }
|
|
57
|
+
name(): string { return "Rectangle"; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Adding a Triangle = add one class. areaBefore/nameBefore would each need a new
|
|
61
|
+
// case; here, no existing class changes. That is the payoff.
|
|
62
|
+
|
|
63
|
+
export { areaBefore, nameBefore, Shape, Circle, Square, Rectangle };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Code Smells — and the move each one calls for
|
|
2
|
+
|
|
3
|
+
A *smell* is a surface symptom that usually (not always) points at a deeper structural problem. Smells
|
|
4
|
+
tell you **where** to refactor; the named refactoring tells you **how**. Use judgment — a smell is a
|
|
5
|
+
prompt to look, not an automatic mandate to change.
|
|
6
|
+
|
|
7
|
+
## Bloaters
|
|
8
|
+
|
|
9
|
+
### Long method / long function
|
|
10
|
+
**Symptom:** the body has comment-delimited "paragraphs," scrolls off-screen, mixes levels of
|
|
11
|
+
abstraction (high-level orchestration next to bit-twiddling).
|
|
12
|
+
**Move:** *Extract Function* on each paragraph; *Replace Temp with Query*; *Decompose Conditional*.
|
|
13
|
+
**Heuristic:** if you feel the urge to write a comment explaining a block, extract that block and let the
|
|
14
|
+
function name be the comment.
|
|
15
|
+
|
|
16
|
+
### Large class / god object
|
|
17
|
+
**Symptom:** dozens of fields, many unrelated responsibilities, low cohesion (fields used by disjoint
|
|
18
|
+
subsets of methods).
|
|
19
|
+
**Move:** *Extract Class* / *Extract Subclass*; group fields that change together into their own type.
|
|
20
|
+
|
|
21
|
+
### Primitive obsession
|
|
22
|
+
**Symptom:** `string currency`, `int cents`, `string` for a phone number — primitives standing in for
|
|
23
|
+
domain concepts, with validation/formatting scattered around every use site.
|
|
24
|
+
**Move:** *Replace Primitive with Object* (a `Money`, `PhoneNumber`, `EmailAddress` value type that owns
|
|
25
|
+
its validation and behavior). Kills duplicated validation and impossible states.
|
|
26
|
+
|
|
27
|
+
### Long parameter list
|
|
28
|
+
**Symptom:** five-plus parameters, or the same clump of args passed together everywhere.
|
|
29
|
+
**Move:** *Introduce Parameter Object* / *Preserve Whole Object*.
|
|
30
|
+
|
|
31
|
+
### Data clumps
|
|
32
|
+
**Symptom:** the same three or four data items travel together through many signatures
|
|
33
|
+
(`x, y, width, height`).
|
|
34
|
+
**Move:** *Introduce Parameter Object* — they want to be a `Rect`.
|
|
35
|
+
|
|
36
|
+
## Change-preventers (these make change expensive)
|
|
37
|
+
|
|
38
|
+
### Shotgun surgery
|
|
39
|
+
**Symptom:** one conceptual change (e.g. "add a new tax rule") forces small edits in many scattered
|
|
40
|
+
files/classes.
|
|
41
|
+
**Move:** *Move Function/Field* to pull the scattered logic into one place; *Inline Class* if a class
|
|
42
|
+
adds no value.
|
|
43
|
+
|
|
44
|
+
### Divergent change
|
|
45
|
+
**Symptom:** the opposite — one class is changed for many *different* reasons (a new report format *and*
|
|
46
|
+
a new DB column both touch it).
|
|
47
|
+
**Move:** *Extract Class* so each axis of change lives in its own type.
|
|
48
|
+
|
|
49
|
+
## Couplers
|
|
50
|
+
|
|
51
|
+
### Feature envy
|
|
52
|
+
**Symptom:** a method spends more time reading another object's fields than its own.
|
|
53
|
+
**Move:** *Move Function* to the class whose data it envies; *Extract Function* then move the part that
|
|
54
|
+
envies.
|
|
55
|
+
|
|
56
|
+
### Inappropriate intimacy
|
|
57
|
+
**Symptom:** two classes reach deep into each other's private parts.
|
|
58
|
+
**Move:** *Move Function/Field*, *Hide Delegate*, or merge if they're really one concept.
|
|
59
|
+
|
|
60
|
+
### Message chains
|
|
61
|
+
**Symptom:** `a.getB().getC().getD().doThing()` — the caller is coupled to the whole navigation path.
|
|
62
|
+
**Move:** *Hide Delegate* (ask the first object to do the work).
|
|
63
|
+
|
|
64
|
+
## Dispensables
|
|
65
|
+
|
|
66
|
+
### Duplicated code
|
|
67
|
+
**Symptom:** the same (or near-same) logic in multiple places.
|
|
68
|
+
**Move:** *Extract Function* and call it from both. **Rule of three:** duplication is acceptable twice;
|
|
69
|
+
on the third occurrence, extract. Extracting too early (on the second) risks a wrong abstraction that
|
|
70
|
+
couples things that only *coincidentally* looked alike.
|
|
71
|
+
|
|
72
|
+
### Comments (as deodorant)
|
|
73
|
+
**Symptom:** a comment exists to explain *what* confusing code does.
|
|
74
|
+
**Move:** *Extract Function* / *Rename* so the code self-documents; keep comments for *why*, not *what*.
|
|
75
|
+
|
|
76
|
+
### Dead code / speculative generality
|
|
77
|
+
**Symptom:** unused params, unreachable branches, abstractions with a single implementation "for the
|
|
78
|
+
future."
|
|
79
|
+
**Move:** delete it (YAGNI). Pairs with the `refactor-cleaner` agent.
|
|
80
|
+
|
|
81
|
+
## Conditionals
|
|
82
|
+
|
|
83
|
+
### Complex / nested conditional
|
|
84
|
+
**Symptom:** deep `if/else` pyramids; a `switch` on a type code that recurs in several places.
|
|
85
|
+
**Move:** *Replace Nested Conditional with Guard Clauses*; *Decompose Conditional* (extract the
|
|
86
|
+
condition and each leg into named functions); *Replace Conditional with Polymorphism* when the switch is
|
|
87
|
+
on a type.
|
|
88
|
+
|
|
89
|
+
### Repeated switch on a type
|
|
90
|
+
**Symptom:** the same `switch (kind)` appears in `area()`, `perimeter()`, `draw()`…
|
|
91
|
+
**Move:** *Replace Conditional with Polymorphism* — each type owns all its behaviors; new types become
|
|
92
|
+
additive (Open/Closed).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Refactoring Catalog — named moves with mechanics
|
|
2
|
+
|
|
3
|
+
Each refactoring is a *named* procedure with explicit **mechanics** — a small step sequence designed so
|
|
4
|
+
the suite stays green throughout. Run tests after every numbered step where noted. Most modern IDEs
|
|
5
|
+
automate the safe versions (Rename, Extract Function/Variable, Inline, Move); prefer the automated
|
|
6
|
+
version when available because it preserves references mechanically.
|
|
7
|
+
|
|
8
|
+
## Extract Function
|
|
9
|
+
**When:** a fragment of a function can be grouped and named for its intent (long method, duplication,
|
|
10
|
+
comment-as-deodorant).
|
|
11
|
+
**Mechanics:**
|
|
12
|
+
1. Create a new function named for *what it does*, not how.
|
|
13
|
+
2. Copy the extracted code into it.
|
|
14
|
+
3. Pass in any local variables it reads as parameters; return any it writes.
|
|
15
|
+
4. Replace the original fragment with a call.
|
|
16
|
+
5. Run tests.
|
|
17
|
+
|
|
18
|
+
## Inline Function / Inline Variable
|
|
19
|
+
**When:** the body is as clear as the name, or a layer of indirection earns nothing.
|
|
20
|
+
**Mechanics:** verify it isn't polymorphically overridden; replace each call with the body; remove the
|
|
21
|
+
declaration; run tests. (The inverse of Extract — use it to undo a premature extraction.)
|
|
22
|
+
|
|
23
|
+
## Rename (Variable / Function / Field)
|
|
24
|
+
**When:** a name no longer reveals intent. The highest value-to-risk move there is.
|
|
25
|
+
**Mechanics:** prefer the IDE's rename (updates all references atomically). If manual, change the
|
|
26
|
+
declaration, find every reference, update, run tests. Renaming a published API element requires a
|
|
27
|
+
deprecation step — keep the old name delegating for one release.
|
|
28
|
+
|
|
29
|
+
## Extract Variable
|
|
30
|
+
**When:** a sub-expression is hard to read or repeated within an expression.
|
|
31
|
+
**Mechanics:** introduce a well-named `const` for the sub-expression; replace occurrences; run tests.
|
|
32
|
+
Makes complex booleans and arithmetic self-explaining.
|
|
33
|
+
|
|
34
|
+
## Replace Nested Conditional with Guard Clauses
|
|
35
|
+
**When:** deep `if` nesting where some branches are exceptional/early-exit cases.
|
|
36
|
+
**Mechanics:** for each "this isn't the normal path" check, return (or throw) early at the top; the happy
|
|
37
|
+
path then drops out of the nesting. Run tests after each guard.
|
|
38
|
+
|
|
39
|
+
## Decompose Conditional
|
|
40
|
+
**When:** a complex `if (cond) { ... } else { ... }` where the condition and legs are all hard to read.
|
|
41
|
+
**Mechanics:** *Extract Function* on the condition (name it `isEligible(...)`), and on each leg. The
|
|
42
|
+
conditional becomes `if (isEligible(x)) return discounted(x); return standard(x);`.
|
|
43
|
+
|
|
44
|
+
## Replace Conditional with Polymorphism
|
|
45
|
+
**When:** the same `switch`/`if`-on-type appears in multiple methods (repeated switch smell).
|
|
46
|
+
**Mechanics:**
|
|
47
|
+
1. Ensure an inheritance/interface structure exists (create it via *Extract Class* / *Replace Type Code
|
|
48
|
+
with Subclasses* if needed).
|
|
49
|
+
2. Move one `switch` into the superclass/interface as a method.
|
|
50
|
+
3. For each leg, override the method in the matching subclass with that leg's body.
|
|
51
|
+
4. Remove the leg from the superclass; leave the default/abstract.
|
|
52
|
+
5. Run tests after each leg moved. Repeat per method that switched on the same type.
|
|
53
|
+
|
|
54
|
+
## Introduce Parameter Object
|
|
55
|
+
**When:** a clump of arguments (data clump) recurs across signatures.
|
|
56
|
+
**Mechanics:** create a class/record for the clump; add it as a parameter; one call site at a time, route
|
|
57
|
+
the individual args through the object; remove the originals once all sites are migrated; run tests
|
|
58
|
+
between sites.
|
|
59
|
+
|
|
60
|
+
## Replace Magic Literal with Symbolic Constant
|
|
61
|
+
**When:** an unexplained literal (`0.2`, `86400`, `"ACTIVE"`) carries meaning.
|
|
62
|
+
**Mechanics:** declare a named constant; replace each occurrence; run tests. Search for the literal value
|
|
63
|
+
elsewhere — duplicated magic numbers often hide.
|
|
64
|
+
|
|
65
|
+
## Move Function / Move Field
|
|
66
|
+
**When:** feature envy or shotgun surgery — the element lives in the wrong class.
|
|
67
|
+
**Mechanics:** check what it references; create it in the target; adjust references; turn the original
|
|
68
|
+
into a delegating call (or remove if no longer needed); update callers; run tests.
|
|
69
|
+
|
|
70
|
+
## Separate Query from Modifier
|
|
71
|
+
**When:** a function both returns a value *and* has a side effect (surprising to callers).
|
|
72
|
+
**Mechanics:** create a pure query that returns the value; leave the modifier returning void; update
|
|
73
|
+
callers to call both where they truly need the side effect; run tests.
|
|
74
|
+
|
|
75
|
+
## Mechanics discipline
|
|
76
|
+
The point of writing mechanics down is that under pressure you follow the *steps*, not your intuition.
|
|
77
|
+
Small steps + green tests after each = you can always revert exactly one move. If a step list feels too
|
|
78
|
+
small, that's the point — speed comes from never having to debug.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Safe Refactoring Workflow
|
|
2
|
+
|
|
3
|
+
Refactoring is only "safe" because of the process around it. The moves themselves are mechanical; the
|
|
4
|
+
discipline is what prevents regressions.
|
|
5
|
+
|
|
6
|
+
## The two hats (Kent Beck)
|
|
7
|
+
|
|
8
|
+
At any moment you are wearing exactly one hat:
|
|
9
|
+
|
|
10
|
+
- **Adding behavior** — you write a new failing test, then make it pass. The structure may get a little
|
|
11
|
+
worse; that's fine.
|
|
12
|
+
- **Refactoring** — you change structure while **all tests stay green**. You add no tests (except as a
|
|
13
|
+
safety net before starting) and you change no behavior.
|
|
14
|
+
|
|
15
|
+
Never wear both at once. The instant a test breaks, the hat you're wearing tells you what kind of mistake
|
|
16
|
+
you made. Mixing them destroys that signal and pollutes the diff so reviewers can't see the real change.
|
|
17
|
+
|
|
18
|
+
## Red-Green-Refactor
|
|
19
|
+
|
|
20
|
+
The TDD loop bakes refactoring in as a first-class step:
|
|
21
|
+
|
|
22
|
+
1. **Red** — write a test for the next behavior; it fails.
|
|
23
|
+
2. **Green** — write the *simplest* code that passes, even if ugly.
|
|
24
|
+
3. **Refactor** — now, with a green bar, clean up the code you just wrote *and* the surrounding code your
|
|
25
|
+
change touched. Tests stay green.
|
|
26
|
+
|
|
27
|
+
The refactor step is where design actually happens. Skipping it is how "temporary" green code calcifies
|
|
28
|
+
into permanent debt.
|
|
29
|
+
|
|
30
|
+
## Characterization tests (the legacy-code entry point)
|
|
31
|
+
|
|
32
|
+
When you must refactor code that has **no tests** and you're unsure what it does:
|
|
33
|
+
|
|
34
|
+
1. Write a test that calls the code and asserts *whatever it currently returns* — even if that output
|
|
35
|
+
looks wrong. You're not asserting correctness; you're **pinning current behavior**.
|
|
36
|
+
2. A fast way to discover the expected value: assert something obviously false, run it, and copy the
|
|
37
|
+
actual value from the failure message into the assertion.
|
|
38
|
+
3. Build up enough of these to cover the branches you're about to touch.
|
|
39
|
+
4. Now refactor. Any behavior drift breaks a characterization test immediately.
|
|
40
|
+
|
|
41
|
+
Characterization tests document the *as-is* contract. If you later decide some pinned behavior was a bug,
|
|
42
|
+
that's a **separate, behavior-changing** task (other hat) with its own test update and commit.
|
|
43
|
+
|
|
44
|
+
## Strangler fig (refactoring at system scale)
|
|
45
|
+
|
|
46
|
+
Big-bang rewrites fail because they discard embedded edge-case knowledge and offer no incremental safety.
|
|
47
|
+
The strangler fig grows the new structure *around* the old, like the vine that envelops a tree:
|
|
48
|
+
|
|
49
|
+
1. Put a **seam** (interface/facade/router) in front of the thing you want to replace.
|
|
50
|
+
2. Build the new implementation behind the seam for **one** slice of functionality.
|
|
51
|
+
3. Route that slice's traffic to the new code; keep everything else on the old.
|
|
52
|
+
4. Verify in production (metrics, parity checks); migrate the next slice.
|
|
53
|
+
5. When nothing routes to the old code, delete it.
|
|
54
|
+
|
|
55
|
+
At every step the system is fully working and releasable. You can stop, pause, or roll back a single
|
|
56
|
+
slice — none of which a rewrite allows.
|
|
57
|
+
|
|
58
|
+
## Commit hygiene
|
|
59
|
+
|
|
60
|
+
- **One refactoring move per commit** where practical; at minimum, never mix a refactor with a feature in
|
|
61
|
+
one commit. A reviewer should be able to read a commit message of `refactor: extract totalFor()` and
|
|
62
|
+
trust the behavior is unchanged.
|
|
63
|
+
- Land refactors **continuously** onto `main`. Long-lived refactor branches rot against everyone else's
|
|
64
|
+
work and create merge hell.
|
|
65
|
+
- Keep the suite **fast**. If running tests after every step is painful, you'll stop doing it — and the
|
|
66
|
+
whole safety net collapses. Invest in test speed first.
|
|
67
|
+
|
|
68
|
+
## Pre-flight checklist
|
|
69
|
+
|
|
70
|
+
- [ ] Green test suite covering the behavior I'm about to move (or characterization tests written).
|
|
71
|
+
- [ ] I know which hat I'm wearing right now.
|
|
72
|
+
- [ ] My next step is small enough that a red bar points at one change.
|
|
73
|
+
- [ ] I will run the suite after this step.
|
|
74
|
+
- [ ] My commit contains only refactoring, no behavior change.
|