codeforge-dev 1.5.7 → 1.7.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/.devcontainer/.env +2 -1
- package/.devcontainer/CHANGELOG.md +55 -9
- package/.devcontainer/CLAUDE.md +65 -15
- package/.devcontainer/README.md +67 -6
- package/.devcontainer/config/keybindings.json +5 -0
- package/.devcontainer/config/main-system-prompt.md +63 -2
- package/.devcontainer/config/settings.json +25 -6
- package/.devcontainer/devcontainer.json +23 -7
- package/.devcontainer/features/README.md +21 -7
- package/.devcontainer/features/ccburn/README.md +60 -0
- package/.devcontainer/features/ccburn/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccburn/install.sh +174 -0
- package/.devcontainer/features/ccstatusline/README.md +22 -21
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +1 -1
- package/.devcontainer/features/ccstatusline/install.sh +48 -16
- package/.devcontainer/features/claude-code/config/settings.json +60 -24
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +1 -1
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +21 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +7 -10
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/REVIEW-RUBRIC.md +440 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +190 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/bash-exec.md +173 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +155 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +248 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +233 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +235 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +125 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/git-archaeologist.md +242 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +195 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/perf-profiler.md +265 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +209 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/researcher.md +195 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/security-auditor.md +289 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +284 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/statusline-config.md +188 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +12 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/guard-readonly-bash.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/redirect-builtin-agents.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/skill-suggester.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/syntax-validator.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/verify-no-regression.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/verify-tests-pass.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/guard-readonly-bash.py +611 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/redirect-builtin-agents.py +83 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +85 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/syntax-validator.py +9 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/verify-no-regression.py +221 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/verify-tests-pass.py +176 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/claude-agent-sdk/SKILL.md +599 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/claude-agent-sdk/references/sdk-typescript-reference.md +954 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/git-forensics/SKILL.md +276 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/git-forensics/references/advanced-commands.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/git-forensics/references/investigation-playbooks.md +319 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/performance-profiling/SKILL.md +341 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/performance-profiling/references/interpreting-results.md +235 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/performance-profiling/references/tool-commands.md +395 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/refactoring-patterns/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/refactoring-patterns/references/safe-transformations.md +247 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/refactoring-patterns/references/smell-catalog.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/security-checklist/SKILL.md +277 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/security-checklist/references/owasp-patterns.md +269 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/security-checklist/references/secrets-patterns.md +253 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +288 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/references/criteria-patterns.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/references/ears-templates.md +239 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +40 -39
- package/.devcontainer/scripts/setup-aliases.sh +10 -20
- package/.devcontainer/scripts/setup-config.sh +2 -0
- package/.devcontainer/scripts/setup-plugins.sh +38 -46
- package/.devcontainer/scripts/setup-projects.sh +175 -0
- package/.devcontainer/scripts/setup-symlink-claude.sh +36 -0
- package/.devcontainer/scripts/setup-update-claude.sh +11 -8
- package/.devcontainer/scripts/setup.sh +4 -2
- package/package.json +1 -1
- package/.devcontainer/scripts/setup-irie-claude.sh +0 -32
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refactoring-patterns
|
|
3
|
+
description: >-
|
|
4
|
+
This skill should be used when the user asks to "refactor this code",
|
|
5
|
+
"clean up this function", "extract a method", "reduce code duplication",
|
|
6
|
+
"fix code smells", "simplify this class", "break up this large function",
|
|
7
|
+
"apply design patterns", "remove dead code", "improve code structure",
|
|
8
|
+
or discusses code smells, refactoring techniques, extract function,
|
|
9
|
+
inline variable, feature envy, god class, or behavior-preserving
|
|
10
|
+
transformations.
|
|
11
|
+
version: 0.1.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Refactoring Patterns
|
|
15
|
+
|
|
16
|
+
## Mental Model
|
|
17
|
+
|
|
18
|
+
Refactoring is **behavior-preserving transformation** -- you change the structure of code without changing what it does. The key word is *preserving*. Every refactoring step must leave the system in a working state. If you can't verify the behavior hasn't changed, you're not refactoring -- you're rewriting.
|
|
19
|
+
|
|
20
|
+
Tests are the safety net. Before touching any code, confirm that tests pass. After every transformation, run tests again. If there are no tests, write characterization tests first -- tests that capture the current behavior, even if that behavior is buggy. Fix bugs separately from refactoring; mixing the two makes both harder to verify.
|
|
21
|
+
|
|
22
|
+
The refactoring cycle is:
|
|
23
|
+
1. **Identify** a code smell (a surface indicator of a deeper structural problem)
|
|
24
|
+
2. **Choose** the appropriate transformation
|
|
25
|
+
3. **Apply** the transformation in the smallest possible step
|
|
26
|
+
4. **Verify** tests still pass
|
|
27
|
+
5. **Commit** before moving to the next smell
|
|
28
|
+
|
|
29
|
+
Small steps are critical. A refactoring that touches 15 files in one commit is a rewrite wearing a disguise. Each commit should be independently revertable.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Code Smell Catalog
|
|
34
|
+
|
|
35
|
+
Code smells are heuristics, not rules. A method with 30 lines might be perfectly clear; a method with 8 lines might hide a design problem. Use these as starting points for investigation, not automatic triggers for refactoring.
|
|
36
|
+
|
|
37
|
+
### Long Method
|
|
38
|
+
**Detection:** Method exceeds ~20 lines, or you need to scroll to see the whole thing, or you find yourself writing comments to separate logical sections.
|
|
39
|
+
**Root cause:** The method has accumulated responsibilities over time.
|
|
40
|
+
**Fix:** Extract Function -- pull each commented section or logical block into a named function.
|
|
41
|
+
|
|
42
|
+
### Feature Envy
|
|
43
|
+
**Detection:** A method accesses data from another object more than its own. Count the dots -- `order.customer.address.city` suggests the method belongs elsewhere.
|
|
44
|
+
**Root cause:** Behavior is in the wrong class.
|
|
45
|
+
**Fix:** Move Method to the class whose data it uses most.
|
|
46
|
+
|
|
47
|
+
### Data Clump
|
|
48
|
+
**Detection:** The same group of parameters appears together in multiple function signatures (`x, y, z` or `host, port, protocol`).
|
|
49
|
+
**Root cause:** A missing abstraction -- these values form a concept that deserves its own type.
|
|
50
|
+
**Fix:** Introduce Parameter Object or extract a dataclass/type.
|
|
51
|
+
|
|
52
|
+
### Primitive Obsession
|
|
53
|
+
**Detection:** Using `str` for email, `int` for currency, `dict` for structured data. Business rules are scattered across validation checks.
|
|
54
|
+
**Root cause:** Domain concepts are represented as raw primitives instead of types.
|
|
55
|
+
**Fix:** Replace Primitive with Value Object -- create a type that encapsulates the value and its constraints.
|
|
56
|
+
|
|
57
|
+
### God Class
|
|
58
|
+
**Detection:** A class with 500+ lines, 20+ methods, or a name like `Manager`, `Handler`, `Processor`, `Utils`. It knows about everything and everything depends on it.
|
|
59
|
+
**Root cause:** Single Responsibility Principle violation accumulated over time.
|
|
60
|
+
**Fix:** Extract Class -- identify clusters of related fields and methods, pull each cluster into its own class.
|
|
61
|
+
|
|
62
|
+
### Shotgun Surgery
|
|
63
|
+
**Detection:** A single conceptual change requires editing 5+ files in the same way. Adding a new status enum value means updating the model, serializer, validator, template, and two tests.
|
|
64
|
+
**Root cause:** A concept is spread across too many places without a central abstraction.
|
|
65
|
+
**Fix:** Move Method / Move Field to consolidate the scattered logic, or introduce a registry/dispatch pattern.
|
|
66
|
+
|
|
67
|
+
### Divergent Change
|
|
68
|
+
**Detection:** One class changes for multiple unrelated reasons -- the `User` class changes when auth logic changes AND when profile rendering changes.
|
|
69
|
+
**Root cause:** The class has multiple responsibilities that change at different rates.
|
|
70
|
+
**Fix:** Extract Class to separate the responsibilities.
|
|
71
|
+
|
|
72
|
+
### Message Chain
|
|
73
|
+
**Detection:** A chain of calls like `a.getB().getC().getD().doSomething()`. Each link in the chain is a coupling point.
|
|
74
|
+
**Root cause:** Client code knows too much about the object graph.
|
|
75
|
+
**Fix:** Hide Delegate -- provide a method on the first object that reaches through the chain internally.
|
|
76
|
+
|
|
77
|
+
> **Deep dive:** See `references/smell-catalog.md` for the full catalog with before/after code examples for each smell.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Safe Refactoring Steps
|
|
82
|
+
|
|
83
|
+
Each transformation below is mechanical and behavior-preserving when applied correctly. Follow the steps exactly.
|
|
84
|
+
|
|
85
|
+
### Extract Function
|
|
86
|
+
**When:** A block of code inside a method can be grouped under a descriptive name.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dataclasses import dataclass
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Order:
|
|
93
|
+
items: list
|
|
94
|
+
total: float
|
|
95
|
+
customer: "Customer"
|
|
96
|
+
|
|
97
|
+
# Before
|
|
98
|
+
def process_order(order: Order) -> None:
|
|
99
|
+
# validate
|
|
100
|
+
if not order.items:
|
|
101
|
+
raise ValueError("Empty order")
|
|
102
|
+
if order.total < 0:
|
|
103
|
+
raise ValueError("Negative total")
|
|
104
|
+
# apply discount
|
|
105
|
+
if order.customer.is_premium:
|
|
106
|
+
order.total *= 0.9
|
|
107
|
+
|
|
108
|
+
# After
|
|
109
|
+
def process_order(order: Order) -> None:
|
|
110
|
+
validate_order(order)
|
|
111
|
+
apply_premium_discount(order)
|
|
112
|
+
|
|
113
|
+
def validate_order(order: Order) -> None:
|
|
114
|
+
if not order.items:
|
|
115
|
+
raise ValueError("Empty order")
|
|
116
|
+
if order.total < 0:
|
|
117
|
+
raise ValueError("Negative total")
|
|
118
|
+
|
|
119
|
+
def apply_premium_discount(order: Order) -> None:
|
|
120
|
+
if order.customer.is_premium:
|
|
121
|
+
order.total *= 0.9
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Inline Variable
|
|
125
|
+
**When:** A temporary variable adds no clarity and is used only once.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Before
|
|
129
|
+
const basePrice = order.quantity * order.itemPrice;
|
|
130
|
+
return basePrice;
|
|
131
|
+
|
|
132
|
+
// After
|
|
133
|
+
return order.quantity * order.itemPrice;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Replace Conditional with Polymorphism
|
|
137
|
+
**When:** A switch or if-chain selects behavior based on a type field.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import math
|
|
141
|
+
from dataclasses import dataclass
|
|
142
|
+
|
|
143
|
+
# Before
|
|
144
|
+
def calculate_area(shape):
|
|
145
|
+
if shape.type == "circle":
|
|
146
|
+
return math.pi * shape.radius ** 2
|
|
147
|
+
elif shape.type == "rectangle":
|
|
148
|
+
return shape.width * shape.height
|
|
149
|
+
|
|
150
|
+
# After
|
|
151
|
+
@dataclass
|
|
152
|
+
class Circle:
|
|
153
|
+
radius: float
|
|
154
|
+
|
|
155
|
+
def area(self) -> float:
|
|
156
|
+
return math.pi * self.radius ** 2
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class Rectangle:
|
|
160
|
+
width: float
|
|
161
|
+
height: float
|
|
162
|
+
|
|
163
|
+
def area(self) -> float:
|
|
164
|
+
return self.width * self.height
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Introduce Parameter Object
|
|
168
|
+
**When:** Multiple parameters travel together across function boundaries.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from dataclasses import dataclass
|
|
172
|
+
|
|
173
|
+
# Before
|
|
174
|
+
def search(query: str, page: int, per_page: int, sort_by: str, order: str): ...
|
|
175
|
+
def count(query: str, sort_by: str, order: str): ...
|
|
176
|
+
|
|
177
|
+
# After
|
|
178
|
+
@dataclass
|
|
179
|
+
class SearchParams:
|
|
180
|
+
query: str
|
|
181
|
+
page: int = 1
|
|
182
|
+
per_page: int = 20
|
|
183
|
+
sort_by: str = "relevance"
|
|
184
|
+
order: str = "desc"
|
|
185
|
+
|
|
186
|
+
def search(params: SearchParams): ...
|
|
187
|
+
def count(params: SearchParams): ...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Pull Up Method
|
|
191
|
+
**When:** A method in a subclass is identical across all subclasses.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# Before: duplicated method in both subclasses
|
|
195
|
+
class SalariedEmployee(Employee):
|
|
196
|
+
def annual_cost(self) -> float:
|
|
197
|
+
return self.salary * 1.3 # 30% benefits overhead
|
|
198
|
+
|
|
199
|
+
class HourlyEmployee(Employee):
|
|
200
|
+
def annual_cost(self) -> float:
|
|
201
|
+
return self.salary * 1.3 # identical logic
|
|
202
|
+
|
|
203
|
+
# After: pulled up to parent
|
|
204
|
+
class Employee:
|
|
205
|
+
def annual_cost(self) -> float:
|
|
206
|
+
return self.salary * 1.3
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Push Down Method
|
|
210
|
+
**When:** A method in the parent is only relevant to one subclass.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
# Before: method in parent used by only one subclass
|
|
214
|
+
class Employee:
|
|
215
|
+
def commission_rate(self) -> float: # only SalesRep uses this
|
|
216
|
+
return 0.05
|
|
217
|
+
|
|
218
|
+
# After: pushed down to the relevant subclass
|
|
219
|
+
class SalesRep(Employee):
|
|
220
|
+
def commission_rate(self) -> float:
|
|
221
|
+
return 0.05
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
> **Deep dive:** See `references/safe-transformations.md` for step-by-step transformation recipes with pre-conditions, mechanical steps, and post-conditions.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Language-Specific Patterns
|
|
229
|
+
|
|
230
|
+
### Python
|
|
231
|
+
|
|
232
|
+
**Dataclasses over raw dicts:**
|
|
233
|
+
```python
|
|
234
|
+
from dataclasses import dataclass
|
|
235
|
+
|
|
236
|
+
# Before: dict with implicit schema
|
|
237
|
+
user = {"name": "Alice", "email": "alice@example.com", "role": "admin"}
|
|
238
|
+
|
|
239
|
+
# After: explicit, typed, with defaults
|
|
240
|
+
@dataclass
|
|
241
|
+
class User:
|
|
242
|
+
name: str
|
|
243
|
+
email: str
|
|
244
|
+
role: str = "viewer"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Comprehensions over manual loops:**
|
|
248
|
+
```python
|
|
249
|
+
# Before
|
|
250
|
+
result = []
|
|
251
|
+
for item in items:
|
|
252
|
+
if item.is_active():
|
|
253
|
+
result.append(item.name)
|
|
254
|
+
|
|
255
|
+
# After
|
|
256
|
+
result = [item.name for item in items if item.is_active()]
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Context managers over try/finally:**
|
|
260
|
+
```python
|
|
261
|
+
# Before
|
|
262
|
+
f = open("data.txt")
|
|
263
|
+
try:
|
|
264
|
+
data = f.read()
|
|
265
|
+
finally:
|
|
266
|
+
f.close()
|
|
267
|
+
|
|
268
|
+
# After
|
|
269
|
+
with open("data.txt") as f:
|
|
270
|
+
data = f.read()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### TypeScript
|
|
274
|
+
|
|
275
|
+
**Type narrowing over type assertions:**
|
|
276
|
+
```typescript
|
|
277
|
+
// Before: unsafe assertion
|
|
278
|
+
const user = data as User;
|
|
279
|
+
|
|
280
|
+
// After: runtime narrowing
|
|
281
|
+
function isUser(data: unknown): data is User {
|
|
282
|
+
return typeof data === "object" && data !== null && "email" in data;
|
|
283
|
+
}
|
|
284
|
+
if (isUser(data)) {
|
|
285
|
+
console.log(data.email); // safely narrowed
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Discriminated unions over type fields:**
|
|
290
|
+
```typescript
|
|
291
|
+
// Before: stringly-typed
|
|
292
|
+
interface Shape { type: string; radius?: number; width?: number; height?: number; }
|
|
293
|
+
|
|
294
|
+
// After: discriminated union
|
|
295
|
+
type Shape =
|
|
296
|
+
| { kind: "circle"; radius: number }
|
|
297
|
+
| { kind: "rectangle"; width: number; height: number };
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Verification Protocol
|
|
303
|
+
|
|
304
|
+
Every refactoring session follows this protocol:
|
|
305
|
+
|
|
306
|
+
1. **Run all tests** before starting. If tests fail, fix them first -- don't refactor broken code.
|
|
307
|
+
2. **Make one transformation** at a time. Extract one function, rename one variable, move one method.
|
|
308
|
+
3. **Run tests** after every transformation. If tests fail, revert immediately -- don't debug a failed refactoring.
|
|
309
|
+
4. **Commit** each passing transformation. Atomic commits make bisecting easy if something breaks later.
|
|
310
|
+
5. **Review the diff** before pushing. Does each change preserve behavior? Are there unintended side effects?
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# The refactoring loop
|
|
314
|
+
git stash # save any unrelated work
|
|
315
|
+
pytest # green? proceed. red? fix first.
|
|
316
|
+
|
|
317
|
+
# ... make ONE transformation ...
|
|
318
|
+
|
|
319
|
+
pytest # green? commit. red? git checkout -- .
|
|
320
|
+
git add -p # stage only the refactoring
|
|
321
|
+
git commit -m "refactor: extract validate_order from process_order"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Ambiguity Policy
|
|
327
|
+
|
|
328
|
+
These defaults apply when the user does not specify a preference. State the assumption when making a choice:
|
|
329
|
+
|
|
330
|
+
- **Scope:** Default to the smallest transformation that addresses the identified smell. Do not chain multiple refactorings without testing between each step.
|
|
331
|
+
- **Test requirement:** If no tests exist, write characterization tests before refactoring. Do not refactor untested code.
|
|
332
|
+
- **Naming:** When extracting a function, name it after what it does, not how it does it. Prefer verbs (`validate_order`) over nouns (`order_validation`).
|
|
333
|
+
- **Depth:** Default to one level of extraction. If the extracted function itself has smells, address them in a separate commit.
|
|
334
|
+
- **Language idioms:** Prefer language-native patterns (Python dataclasses over hand-rolled `__init__`, TypeScript discriminated unions over type assertions).
|
|
335
|
+
- **Commit granularity:** One refactoring transformation per commit. Multiple files in one commit is fine if they're part of the same transformation.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Reference Files
|
|
340
|
+
|
|
341
|
+
| File | Contents |
|
|
342
|
+
|------|----------|
|
|
343
|
+
| `references/smell-catalog.md` | Full catalog of code smells with detection heuristics, before/after code examples, and recommended transformations |
|
|
344
|
+
| `references/safe-transformations.md` | Step-by-step transformation recipes with pre-conditions, mechanical steps, post-conditions, and rollback instructions |
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Safe Transformation Recipes
|
|
2
|
+
|
|
3
|
+
Step-by-step recipes for behavior-preserving refactorings. Each recipe has pre-conditions, mechanical steps, and post-conditions.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Extract Function](#extract-function)
|
|
8
|
+
- [Inline Variable](#inline-variable)
|
|
9
|
+
- [Replace Conditional with Polymorphism](#replace-conditional-with-polymorphism)
|
|
10
|
+
- [Introduce Parameter Object](#introduce-parameter-object)
|
|
11
|
+
- [Move Method](#move-method)
|
|
12
|
+
- [Pull Up Method](#pull-up-method)
|
|
13
|
+
- [Push Down Method](#push-down-method)
|
|
14
|
+
- [Rollback Protocol](#rollback-protocol)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Extract Function
|
|
19
|
+
|
|
20
|
+
### Pre-conditions
|
|
21
|
+
- [ ] Tests pass before starting
|
|
22
|
+
- [ ] The code block to extract has clear inputs and outputs
|
|
23
|
+
- [ ] The block does not modify local variables used later in the parent function (or you handle the return)
|
|
24
|
+
|
|
25
|
+
### Steps
|
|
26
|
+
|
|
27
|
+
1. **Identify** the block of code to extract. Note all variables it reads (inputs) and all variables it modifies (outputs).
|
|
28
|
+
2. **Create** a new function with a descriptive name. Use the inputs as parameters.
|
|
29
|
+
3. **Copy** the code block into the new function.
|
|
30
|
+
4. **Return** any outputs that the parent function needs.
|
|
31
|
+
5. **Replace** the original block with a call to the new function.
|
|
32
|
+
6. **Run tests.**
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# Step 1: Identify inputs (items, tax_rate) and outputs (total)
|
|
36
|
+
def process(items, tax_rate):
|
|
37
|
+
# --- begin extract ---
|
|
38
|
+
subtotal = sum(i.price for i in items)
|
|
39
|
+
total = subtotal * (1 + tax_rate)
|
|
40
|
+
# --- end extract ---
|
|
41
|
+
save(total)
|
|
42
|
+
|
|
43
|
+
# Steps 2-5: Extract
|
|
44
|
+
def calculate_total(items, tax_rate):
|
|
45
|
+
subtotal = sum(i.price for i in items)
|
|
46
|
+
return subtotal * (1 + tax_rate)
|
|
47
|
+
|
|
48
|
+
def process(items, tax_rate):
|
|
49
|
+
total = calculate_total(items, tax_rate)
|
|
50
|
+
save(total)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Post-conditions
|
|
54
|
+
- [ ] Tests still pass
|
|
55
|
+
- [ ] The new function has no side effects that differ from the original block
|
|
56
|
+
- [ ] The parent function produces identical results
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Inline Variable
|
|
61
|
+
|
|
62
|
+
### Pre-conditions
|
|
63
|
+
- [ ] The variable is assigned once and used once
|
|
64
|
+
- [ ] The variable name does not add meaningful clarity over the expression itself
|
|
65
|
+
- [ ] The expression has no side effects (calling it once vs. zero times must be equivalent)
|
|
66
|
+
|
|
67
|
+
### Steps
|
|
68
|
+
|
|
69
|
+
1. **Verify** the variable is used exactly once after assignment.
|
|
70
|
+
2. **Replace** the variable reference with the expression.
|
|
71
|
+
3. **Remove** the variable assignment.
|
|
72
|
+
4. **Run tests.**
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Before
|
|
76
|
+
const isEligible = user.age >= 18 && user.hasVerifiedEmail;
|
|
77
|
+
if (isEligible) { ... }
|
|
78
|
+
|
|
79
|
+
// After (only if isEligible doesn't add clarity)
|
|
80
|
+
if (user.age >= 18 && user.hasVerifiedEmail) { ... }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Post-conditions
|
|
84
|
+
- [ ] Tests still pass
|
|
85
|
+
- [ ] No change in behavior (expression was pure)
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Replace Conditional with Polymorphism
|
|
90
|
+
|
|
91
|
+
### Pre-conditions
|
|
92
|
+
- [ ] A switch/if-chain selects behavior based on a type indicator
|
|
93
|
+
- [ ] The type indicator has a stable set of values (not unbounded user input)
|
|
94
|
+
- [ ] Each branch has enough logic to justify its own method/class
|
|
95
|
+
|
|
96
|
+
### Steps
|
|
97
|
+
|
|
98
|
+
1. **Create** a base class or interface with the polymorphic method.
|
|
99
|
+
2. **Create** a subclass for each branch of the conditional.
|
|
100
|
+
3. **Move** the branch logic into the corresponding subclass method.
|
|
101
|
+
4. **Replace** the conditional with a method call on the object.
|
|
102
|
+
5. **Update** object creation to instantiate the correct subclass.
|
|
103
|
+
6. **Run tests.**
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# Before
|
|
107
|
+
def calculate_pay(employee):
|
|
108
|
+
if employee.type == "hourly":
|
|
109
|
+
return employee.hours * employee.rate
|
|
110
|
+
elif employee.type == "salaried":
|
|
111
|
+
return employee.annual_salary / 26
|
|
112
|
+
elif employee.type == "contractor":
|
|
113
|
+
return employee.hours * employee.rate * 1.2
|
|
114
|
+
|
|
115
|
+
# After
|
|
116
|
+
class HourlyEmployee:
|
|
117
|
+
def calculate_pay(self):
|
|
118
|
+
return self.hours * self.rate
|
|
119
|
+
|
|
120
|
+
class SalariedEmployee:
|
|
121
|
+
def calculate_pay(self):
|
|
122
|
+
return self.annual_salary / 26
|
|
123
|
+
|
|
124
|
+
class Contractor:
|
|
125
|
+
def calculate_pay(self):
|
|
126
|
+
return self.hours * self.rate * 1.2
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Post-conditions
|
|
130
|
+
- [ ] Tests still pass
|
|
131
|
+
- [ ] Adding a new type requires adding a new class, not modifying existing code
|
|
132
|
+
- [ ] The conditional is completely removed
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Introduce Parameter Object
|
|
137
|
+
|
|
138
|
+
### Pre-conditions
|
|
139
|
+
- [ ] 3+ parameters appear together in multiple function signatures
|
|
140
|
+
- [ ] The parameters have a logical relationship (they represent a concept)
|
|
141
|
+
|
|
142
|
+
### Steps
|
|
143
|
+
|
|
144
|
+
1. **Create** a class/dataclass/type with the grouped parameters as fields.
|
|
145
|
+
2. **Add** the new type as a parameter to ONE function.
|
|
146
|
+
3. **Update** callers of that function to construct the object.
|
|
147
|
+
4. **Run tests.**
|
|
148
|
+
5. **Repeat** for the remaining functions, one at a time.
|
|
149
|
+
6. **Move** any logic that operates on the grouped parameters into the new class.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from dataclasses import dataclass
|
|
153
|
+
from datetime import datetime
|
|
154
|
+
|
|
155
|
+
# Step 1: Create the type
|
|
156
|
+
@dataclass
|
|
157
|
+
class DateRange:
|
|
158
|
+
start: datetime
|
|
159
|
+
end: datetime
|
|
160
|
+
|
|
161
|
+
def contains(self, dt: datetime) -> bool:
|
|
162
|
+
return self.start <= dt <= self.end
|
|
163
|
+
|
|
164
|
+
def days(self) -> int:
|
|
165
|
+
return (self.end - self.start).days
|
|
166
|
+
|
|
167
|
+
# Steps 2-5: Update functions one at a time
|
|
168
|
+
def query_logs(date_range: DateRange, level: str) -> list[dict]: ...
|
|
169
|
+
def generate_report(date_range: DateRange, format: str) -> str: ...
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Post-conditions
|
|
173
|
+
- [ ] Tests still pass
|
|
174
|
+
- [ ] Each function that previously took the scattered parameters now takes the object
|
|
175
|
+
- [ ] Behavior that belongs to the grouped parameters has migrated into the new class
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Move Method
|
|
180
|
+
|
|
181
|
+
### Pre-conditions
|
|
182
|
+
- [ ] A method accesses more data from another class than its own (Feature Envy)
|
|
183
|
+
- [ ] Moving the method would reduce coupling between the two classes
|
|
184
|
+
|
|
185
|
+
### Steps
|
|
186
|
+
|
|
187
|
+
1. **Copy** the method to the target class.
|
|
188
|
+
2. **Adjust** the method to use `self` for the target class's data.
|
|
189
|
+
3. **Add** parameters for any data it still needs from the source class.
|
|
190
|
+
4. **Update** callers to call the method on the target object.
|
|
191
|
+
5. **Remove** the method from the source class (or delegate to the target).
|
|
192
|
+
6. **Run tests.**
|
|
193
|
+
|
|
194
|
+
### Post-conditions
|
|
195
|
+
- [ ] Tests still pass
|
|
196
|
+
- [ ] The method no longer envies another class's data
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Pull Up Method
|
|
201
|
+
|
|
202
|
+
### Pre-conditions
|
|
203
|
+
- [ ] Two or more subclasses have identical (or near-identical) methods
|
|
204
|
+
- [ ] The method uses only fields/methods available in the parent class
|
|
205
|
+
|
|
206
|
+
### Steps
|
|
207
|
+
|
|
208
|
+
1. **Verify** the methods are semantically identical (not just textually similar).
|
|
209
|
+
2. **Ensure** any referenced fields exist in the parent class (move them up if needed).
|
|
210
|
+
3. **Copy** the method to the parent class.
|
|
211
|
+
4. **Remove** the method from all subclasses.
|
|
212
|
+
5. **Run tests.**
|
|
213
|
+
|
|
214
|
+
### Post-conditions
|
|
215
|
+
- [ ] Tests still pass
|
|
216
|
+
- [ ] No duplication of the method across subclasses
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Push Down Method
|
|
221
|
+
|
|
222
|
+
### Pre-conditions
|
|
223
|
+
- [ ] A method in the parent class is only relevant to one subclass
|
|
224
|
+
- [ ] Other subclasses don't call or override the method
|
|
225
|
+
|
|
226
|
+
### Steps
|
|
227
|
+
|
|
228
|
+
1. **Copy** the method to the relevant subclass.
|
|
229
|
+
2. **Remove** the method from the parent class.
|
|
230
|
+
3. **Run tests.** If other subclasses call the method, compilation/runtime will fail -- that's a signal this transformation is wrong.
|
|
231
|
+
|
|
232
|
+
### Post-conditions
|
|
233
|
+
- [ ] Tests still pass
|
|
234
|
+
- [ ] The parent class is simpler with one fewer responsibility
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Rollback Protocol
|
|
239
|
+
|
|
240
|
+
If tests fail after any transformation:
|
|
241
|
+
|
|
242
|
+
1. **Do not debug.** The transformation was incorrect or incomplete.
|
|
243
|
+
2. **Revert immediately:** `git checkout -- .` (if uncommitted) or `git revert HEAD` (if committed).
|
|
244
|
+
3. **Analyze** why it failed: missing parameter, side effect, implicit dependency.
|
|
245
|
+
4. **Retry** with a smaller transformation or different approach.
|
|
246
|
+
|
|
247
|
+
The cost of reverting is zero. The cost of debugging a broken refactoring compounds with every additional change.
|