@luanpdd/kit-mcp 1.9.0 → 1.11.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/CHANGELOG.md +86 -0
- package/README.md +58 -0
- package/gates/ai-prompt-stability.md +120 -0
- package/gates/golden-signals-coverage.md +133 -0
- package/gates/legacy-refactor-safety.md +178 -0
- package/gates/observability-coverage.md +151 -0
- package/gates/postmortem-template-required.md +127 -0
- package/gates/prr-checklist-coverage.md +128 -0
- package/gates/release-pipeline-policy.md +132 -0
- package/kit/COMANDOS.md +15 -0
- package/kit/agents/ai-mutation-tester.md +298 -0
- package/kit/agents/cascading-failures-auditor.md +306 -0
- package/kit/agents/executor.md +13 -0
- package/kit/agents/golden-signals-instrumenter.md +241 -0
- package/kit/agents/legacy-characterizer.md +378 -0
- package/kit/agents/load-shedding-instrumenter.md +297 -0
- package/kit/agents/observability-coverage-auditor.md +325 -0
- package/kit/agents/omm-auditor.md +99 -0
- package/kit/agents/payload-capture-instrumenter.md +283 -0
- package/kit/agents/planner.md +29 -0
- package/kit/agents/postmortem-writer.md +282 -0
- package/kit/agents/prr-conductor.md +296 -0
- package/kit/agents/refactor-safety-auditor.md +414 -0
- package/kit/agents/release-pipeline-auditor.md +360 -0
- package/kit/agents/seam-finder.md +367 -0
- package/kit/agents/shotgun-surgery-detector.md +359 -0
- package/kit/agents/storytelling-analyst.md +309 -0
- package/kit/agents/supabase-architect.md +49 -0
- package/kit/agents/supabase-edge-fn-writer.md +114 -0
- package/kit/agents/supabase-migration-writer.md +80 -0
- package/kit/agents/supabase-storage-implementer.md +156 -0
- package/kit/agents/toil-auditor.md +277 -0
- package/kit/agents/verifier.md +30 -0
- package/kit/commands/auditar-cascading.md +111 -0
- package/kit/commands/auditar-marco.md +124 -1
- package/kit/commands/auditar-observabilidade-cobertura.md +183 -0
- package/kit/commands/auditar-refactor.md +219 -0
- package/kit/commands/auditar-release.md +109 -0
- package/kit/commands/auditar-toil.md +129 -0
- package/kit/commands/capturar-payloads.md +193 -0
- package/kit/commands/caracterizar-prompt.md +195 -0
- package/kit/commands/caracterizar.md +212 -0
- package/kit/commands/concluir-marco.md +95 -1
- package/kit/commands/detectar-duplicacao.md +197 -0
- package/kit/commands/discutir-fase.md +41 -0
- package/kit/commands/encontrar-seams.md +136 -0
- package/kit/commands/forense.md +103 -1
- package/kit/commands/golden-signals.md +142 -0
- package/kit/commands/legacy.md +263 -0
- package/kit/commands/load-shedding.md +117 -0
- package/kit/commands/observabilidade.md +2 -0
- package/kit/commands/postmortem.md +179 -0
- package/kit/commands/prr.md +205 -0
- package/kit/commands/refactor-seguro.md +321 -0
- package/kit/commands/risk-budget.md +220 -0
- package/kit/commands/sre.md +230 -0
- package/kit/commands/storytelling.md +179 -0
- package/kit/skills/_shared-legacy/glossary.md +389 -0
- package/kit/skills/_shared-sre/glossary.md +712 -0
- package/kit/skills/ai-prompt-characterization/SKILL.md +335 -0
- package/kit/skills/blameless-postmortems/SKILL.md +340 -0
- package/kit/skills/cascading-failures/SKILL.md +307 -0
- package/kit/skills/eliminating-toil/SKILL.md +243 -0
- package/kit/skills/event-based-slos/SKILL.md +22 -0
- package/kit/skills/four-golden-signals/SKILL.md +314 -0
- package/kit/skills/hermetic-builds/SKILL.md +323 -0
- package/kit/skills/legacy-api-only-applications/SKILL.md +358 -0
- package/kit/skills/legacy-characterization-tests/SKILL.md +330 -0
- package/kit/skills/legacy-effect-analysis/SKILL.md +331 -0
- package/kit/skills/legacy-extract-class/SKILL.md +203 -0
- package/kit/skills/legacy-monster-methods/SKILL.md +444 -0
- package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -0
- package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -0
- package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -0
- package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -0
- package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -0
- package/kit/skills/llm-as-dependency/SKILL.md +436 -0
- package/kit/skills/load-shedding-graceful-degradation/SKILL.md +396 -0
- package/kit/skills/pre-refactor-characterization/SKILL.md +421 -0
- package/kit/skills/production-readiness-review/SKILL.md +305 -0
- package/kit/skills/release-engineering/SKILL.md +367 -0
- package/kit/skills/retry-strategies/SKILL.md +372 -0
- package/kit/skills/sre-risk-management/SKILL.md +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: legacy-monster-methods
|
|
3
|
+
description: Use ao refatorar método > 100 linhas sem testes — scratch refactoring, single-goal editing, safe extraction (cap 22 Feathers). Padrões para domesticar bulleted vs snarled methods.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Legacy — Monster Methods
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando o user encontra método absurdamente longo (> 100 linhas) que precisa ser modificado. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "esse método tem [N] linhas, preciso mudar"
|
|
13
|
+
- "monster method", "método monstro", "método gigante"
|
|
14
|
+
- "scratch refactoring", "refactor de rascunho"
|
|
15
|
+
- "single-goal editing", "edição com objetivo único"
|
|
16
|
+
- "extract method em método sem testes"
|
|
17
|
+
- "cap 22 Feathers"
|
|
18
|
+
- "bulleted method", "snarled method"
|
|
19
|
+
|
|
20
|
+
Carrega quando `legacy-characterization-tests` é insuficiente isoladamente — método é grande demais para enumerar inputs, mas precisa ser refatorado para ficar testável.
|
|
21
|
+
|
|
22
|
+
## Regras absolutas
|
|
23
|
+
|
|
24
|
+
- **Monster method = > 100 linhas OU > 5 níveis de aninhamento.** Pelo menos 1 dos dois. Heurística empírica do livro.
|
|
25
|
+
- **Distinguir bulleted vs snarled.** Bulleted = linhas longas mas planas (mais fácil). Snarled = nesting profundo, control flow complexo (mais difícil). Estratégias diferentes.
|
|
26
|
+
- **Scratch refactoring é DESCARTÁVEL.** Branch lixo, refactor estético para entender, depois `git checkout main` e descarte tudo. Só conhecimento adquirido viaja para a refatoração real.
|
|
27
|
+
- **Single-goal editing.** UMA mudança por commit/PR. Renomear OR extrair OR mover, NUNCA os 3 juntos. Cada commit é mecânico, isolado, revisável.
|
|
28
|
+
- **Extract method em monster sem teste = SAFE EXTRACTION.** Apenas levantar bloco contíguo + capturar variáveis usadas como parâmetros. SEM mover lógica entre escopos. SEM mudar comportamento. Lê → executa → escreve idênticamente antes/depois.
|
|
29
|
+
- **Compilação verde após CADA commit.** Passo grande demais = você não está fazendo single-goal. Quebre menor.
|
|
30
|
+
- **Não introduza tests "no meio" do refactor.** Refactor preserva comportamento → mesmo set de tests roda verde antes/durante/depois. Test novo significa novo comportamento (PR diferente).
|
|
31
|
+
- **Pequenos testes em método extraído saem GRÁTIS.** Após extract method, o método extraído é menor, tipicamente puro. Test acumulado durante refactor incremental.
|
|
32
|
+
|
|
33
|
+
## Patterns canônicos
|
|
34
|
+
|
|
35
|
+
### Pattern 1: Bulleted vs Snarled — diagnóstico
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
BULLETED METHOD
|
|
39
|
+
===============
|
|
40
|
+
public function processOrder(order) {
|
|
41
|
+
validateOrder(order)
|
|
42
|
+
computeTotals(order)
|
|
43
|
+
applyDiscounts(order)
|
|
44
|
+
saveOrder(order)
|
|
45
|
+
notifyCustomer(order)
|
|
46
|
+
publishEvent(order)
|
|
47
|
+
updateAnalytics(order)
|
|
48
|
+
returnReceipt(order)
|
|
49
|
+
}
|
|
50
|
+
↑ 8 linhas, todas no mesmo nível. Cada linha é praticamente extract-method-ready.
|
|
51
|
+
|
|
52
|
+
SNARLED METHOD
|
|
53
|
+
==============
|
|
54
|
+
public function processOrder(order) {
|
|
55
|
+
if (order != null) {
|
|
56
|
+
if (order.items != null && order.items.length > 0) {
|
|
57
|
+
var total = 0
|
|
58
|
+
for (var item of order.items) {
|
|
59
|
+
if (item.discount) {
|
|
60
|
+
if (item.discount.type === 'percent') {
|
|
61
|
+
total += item.price * (1 - item.discount.value / 100) * item.qty
|
|
62
|
+
} else if (item.discount.type === 'fixed') {
|
|
63
|
+
total += (item.price - item.discount.value) * item.qty
|
|
64
|
+
} else {
|
|
65
|
+
total += item.price * item.qty
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
total += item.price * item.qty
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ... mais 80 linhas com nesting similar
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
↑ control flow profundamente aninhado. Extract method não é trivial — variáveis cruzam scopes.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Diagnóstico:**
|
|
79
|
+
- Conte linhas no mesmo nível de indentação. Se > 70% das linhas estão no nível 1-2 → BULLETED.
|
|
80
|
+
- Conte profundidade máxima de aninhamento. Se ≥ 5 → SNARLED.
|
|
81
|
+
|
|
82
|
+
**Estratégias divergem** — abaixo.
|
|
83
|
+
|
|
84
|
+
### Pattern 2: Scratch refactoring (cap 22)
|
|
85
|
+
|
|
86
|
+
Refactor descartável para ENTENDER o método antes de mudá-lo.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
1. Cria branch `scratch/<method-name>-explore`
|
|
90
|
+
2. Refatora à vontade — extract aleatório, rename especulativo,
|
|
91
|
+
reformata estética. Goal: tornar legível para você.
|
|
92
|
+
3. Lê o resultado, entende o que método faz.
|
|
93
|
+
4. NÃO commita nem PR-a essa branch.
|
|
94
|
+
5. `git checkout main`. Volta ao código original intocado.
|
|
95
|
+
6. AGORA você tem mental model. Refatoração real começa com
|
|
96
|
+
passos pequenos disciplinados (single-goal editing).
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Por que descartável:** scratch é estética + quebra contracts. Manter seria perigoso. Só conhecimento adquirido vai para a real.
|
|
100
|
+
|
|
101
|
+
**Quando vale o tempo:** método > 200 linhas + você nunca tinha mexido nele. < 100 linhas raramente vale (lê direto resolve).
|
|
102
|
+
|
|
103
|
+
### Pattern 3: Single-goal editing — atomic refactoring
|
|
104
|
+
|
|
105
|
+
Cada commit faz UMA coisa. Disciplina absoluta.
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
✓ COMMIT 1: rename method (mecânico, IDE-assisted)
|
|
109
|
+
- Antes: processOrder
|
|
110
|
+
- Depois: processOrderLegacy
|
|
111
|
+
- Diff: rename refactor automático
|
|
112
|
+
|
|
113
|
+
✓ COMMIT 2: extract method (mecânico)
|
|
114
|
+
- Antes: linhas 42-58 inline em processOrderLegacy
|
|
115
|
+
- Depois: extracted como computeTotals(items)
|
|
116
|
+
- Diff: 17 linhas → 1 chamada
|
|
117
|
+
|
|
118
|
+
✓ COMMIT 3: extract method (mecânico)
|
|
119
|
+
- Antes: linhas 60-85 inline
|
|
120
|
+
- Depois: extracted como applyDiscounts(items, discounts)
|
|
121
|
+
- Diff: 26 linhas → 1 chamada
|
|
122
|
+
|
|
123
|
+
✗ COMMIT NÃO-FEITO: extract + rename + add validation
|
|
124
|
+
Múltiplos goals num commit = no single-goal. Difícil revisar.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Princípio:** cada commit é REVERTIBLE individualmente. Se PR3 tem bug, revert PR3 só, não a sequência inteira.
|
|
128
|
+
|
|
129
|
+
### Pattern 4: Safe extraction em método sem testes
|
|
130
|
+
|
|
131
|
+
Extract method preservando comportamento sem ter test que valide.
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
SAFE EXTRACTION CHECKLIST
|
|
135
|
+
=========================
|
|
136
|
+
□ Bloco a extrair é CONTÍGUO (sem control flow saindo do meio)
|
|
137
|
+
□ Variáveis lidas dentro do bloco mas declaradas fora → parâmetros
|
|
138
|
+
□ Variáveis ESCRITAS dentro do bloco mas usadas fora → return values
|
|
139
|
+
□ Bloco não tem `return`, `throw`, `break`, `continue` que afete escopo externo
|
|
140
|
+
(se tem: extract não é seguro, escolha menor)
|
|
141
|
+
□ Type signatures preservadas (parâmetros e retorno bem-tipados)
|
|
142
|
+
□ Comportamento idêntico — antes do extract: lê X, computa Y, escreve Z;
|
|
143
|
+
depois: chama método extraído que lê X, computa Y, escreve Z
|
|
144
|
+
|
|
145
|
+
PROCESSO MECÂNICO
|
|
146
|
+
=================
|
|
147
|
+
1. Selecionar bloco contíguo (10-30 linhas tipicamente)
|
|
148
|
+
2. Identificar variáveis lidas vs escritas no bloco
|
|
149
|
+
3. Lidas-mas-não-escritas → parâmetros (in)
|
|
150
|
+
4. Escritas-mas-usadas-fora → adicionar a return value (out)
|
|
151
|
+
5. IDE: extract method (Cmd+Opt+M no IntelliJ; "Refactor: Extract Function" no VS Code)
|
|
152
|
+
6. Compilar — verde?
|
|
153
|
+
7. Run smoke (qualquer comando manual que rodava antes) — comportamento idêntico?
|
|
154
|
+
8. Commit. Próximo bloco.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Insight:** safe extraction é seguro mesmo SEM testes porque é PURELY MECHANICAL. IDE faz com 100% de fidelidade. Risco residual = bug do IDE (raríssimo) ou erro humano em copiar manualmente (não use cópia manual — use extract automation).
|
|
158
|
+
|
|
159
|
+
### Pattern 5: Domando bulleted method
|
|
160
|
+
|
|
161
|
+
Bulleted é fácil. Workflow:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
1. Cada linha do método é uma "frase" → cada uma vira método extraído.
|
|
165
|
+
Antes: processOrder() com 30 linhas no nível 1
|
|
166
|
+
Depois: processOrder() com 8 chamadas, cada uma para um helper
|
|
167
|
+
|
|
168
|
+
2. Após extract, helpers são MENORES → tests acumulam grátis:
|
|
169
|
+
public computeTotals(order) {
|
|
170
|
+
// 8 linhas — testável isolado agora
|
|
171
|
+
}
|
|
172
|
+
public applyDiscounts(order, codes) {
|
|
173
|
+
// 12 linhas — testável isolado agora
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
3. Tests acumulados cobrem comportamento UNIT:
|
|
177
|
+
test('computeTotals — com 1 item', () => { ... })
|
|
178
|
+
test('computeTotals — com items vazios', () => { ... })
|
|
179
|
+
|
|
180
|
+
4. processOrder() agora é orquestração — test acaba sendo
|
|
181
|
+
integration tipo:
|
|
182
|
+
test('processOrder — happy path orchestration', () => {
|
|
183
|
+
processOrder(typicalOrder)
|
|
184
|
+
expect(saved).toBeDefined()
|
|
185
|
+
expect(notifications).toHaveLength(1)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
5. Iterativamente, código vira pirâmide de tests bem definida.
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Esforço típico:** método 100 linhas bulleted → 6-8 commits, 1-2 dias para refactor + 5-10 testes acumulados grátis.
|
|
192
|
+
|
|
193
|
+
### Pattern 6: Domando snarled method
|
|
194
|
+
|
|
195
|
+
Snarled é difícil. Estratégia: APLAINAR primeiro, depois extrair.
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
PASSO 1 — Achatar via guard clauses (early return)
|
|
199
|
+
Antes: Depois:
|
|
200
|
+
if (order != null) { if (order == null) return ERROR
|
|
201
|
+
if (order.items != null) { if (order.items == null) return ERROR
|
|
202
|
+
if (order.items.length > 0) { if (order.items.length === 0) return EMPTY
|
|
203
|
+
// 80 linhas // 80 linhas (agora sem nesting)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
↑ 3 níveis viraram 0 níveis. Pure mechanical. Sem mudança comportamental.
|
|
208
|
+
|
|
209
|
+
PASSO 2 — Extract method em loop interno
|
|
210
|
+
Antes: Depois:
|
|
211
|
+
for (item of items) { for (item of items) {
|
|
212
|
+
if (item.discount) { total += computeItemTotal(item)
|
|
213
|
+
if (item.discount.type === 'percent') { ↑ 1 chamada
|
|
214
|
+
total += item.price * (1 - ...) * item.qty
|
|
215
|
+
} else if (...) {
|
|
216
|
+
...
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
total += item.price * item.qty
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function computeItemTotal(item) {
|
|
224
|
+
if (!item.discount) return item.price * item.qty
|
|
225
|
+
if (item.discount.type === 'percent')
|
|
226
|
+
return item.price * (1 - item.discount.value / 100) * item.qty
|
|
227
|
+
if (item.discount.type === 'fixed')
|
|
228
|
+
return (item.price - item.discount.value) * item.qty
|
|
229
|
+
return item.price * item.qty
|
|
230
|
+
}
|
|
231
|
+
↑ Loop de 12 linhas → 3 linhas + função pura testável.
|
|
232
|
+
|
|
233
|
+
PASSO 3 — Iterar até nível máximo de aninhamento ≤ 2.
|
|
234
|
+
|
|
235
|
+
PASSO 4 — Tests no método extraído (puro, fácil de testar):
|
|
236
|
+
test('computeItemTotal — sem desconto', ...)
|
|
237
|
+
test('computeItemTotal — desconto percentual', ...)
|
|
238
|
+
test('computeItemTotal — desconto fixo', ...)
|
|
239
|
+
test('computeItemTotal — discount.type desconhecido', ...)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Esforço típico:** método 150 linhas snarled → 12-20 commits, 3-7 dias para refactor + 15-25 testes acumulados.
|
|
243
|
+
|
|
244
|
+
### Pattern 7: Sequência canônica de tipos de single-goal commit
|
|
245
|
+
|
|
246
|
+
Em ordem do mais SEGURO ao mais ARRISCADO. Faça nessa ordem.
|
|
247
|
+
|
|
248
|
+
```text
|
|
249
|
+
SEGURO ↓
|
|
250
|
+
======================================================
|
|
251
|
+
1. RENAME (variable, method, class, file)
|
|
252
|
+
IDE-assisted, mecânico, comportamento idêntico
|
|
253
|
+
|
|
254
|
+
2. SAFE EXTRACTION (extract method/variable)
|
|
255
|
+
Selecione bloco contíguo, IDE extract, sem mover lógica entre scopes
|
|
256
|
+
|
|
257
|
+
3. MOVE METHOD (entre classes)
|
|
258
|
+
Apenas se método não usa state da origem (puro relativo à classe)
|
|
259
|
+
|
|
260
|
+
4. INTRODUCE PARAMETER OBJECT
|
|
261
|
+
Agrupar parâmetros relacionados em DTO. Mecânico.
|
|
262
|
+
|
|
263
|
+
5. INVERT DEPENDENCY (constructor injection)
|
|
264
|
+
New X() interno → recebe X externo. Quebra encapsulation, mas
|
|
265
|
+
comportamento permanece (se default-arg usado).
|
|
266
|
+
|
|
267
|
+
6. CHANGE METHOD SIGNATURE
|
|
268
|
+
Adicionar/remover parâmetro. Risk: callers podem passar wrong.
|
|
269
|
+
|
|
270
|
+
7. ALGORITHM REPLACEMENT
|
|
271
|
+
Mudar IMPLEMENTAÇÃO mantendo contrato. Risk médio — characterization
|
|
272
|
+
tests obrigatórios.
|
|
273
|
+
|
|
274
|
+
8. CONTRACT CHANGE
|
|
275
|
+
Mudar pre-condition/post-condition. Risk alto — todos os callers
|
|
276
|
+
precisam ser inspecionados.
|
|
277
|
+
↑ ARRISCADO
|
|
278
|
+
======================================================
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Stop em #5 ou #6 para refactor "limpa-e-vai". #7 e #8 = mudança comportamental, exigem characterization completa.
|
|
282
|
+
|
|
283
|
+
### Pattern 8: Effort budget de monster method
|
|
284
|
+
|
|
285
|
+
| Tamanho | Tipo | Esforço de refactor | Output esperado |
|
|
286
|
+
|---|---|---|---|
|
|
287
|
+
| 100-150 linhas, bulleted | extract methods + acumular tests | 1-2 dias | 5-10 helpers + 5-10 tests |
|
|
288
|
+
| 100-150 linhas, snarled | flatten + extract | 3-5 dias | 5-10 helpers + 10-15 tests |
|
|
289
|
+
| 150-300 linhas, bulleted | extract class após methods | 3-7 dias | 5-10 helpers + nova classe + 10-15 tests |
|
|
290
|
+
| 150-300 linhas, snarled | scratch refactor + flatten + extract | 1-2 semanas | classe + 15-25 tests |
|
|
291
|
+
| > 300 linhas | só com aprovação stakeholder, alocação dedicada | 2-4 semanas | reescrever via sprout class? |
|
|
292
|
+
|
|
293
|
+
**Heurística:** método > 300 linhas raramente vale refactor incremental. Considere sprout class — encapsular comportamento NOVO em classe nova, deixar legado intocado, eventually deprecate.
|
|
294
|
+
|
|
295
|
+
### Pattern 9: Cobertura emergente
|
|
296
|
+
|
|
297
|
+
Refactor de monster method NÃO precisa de characterization completa upfront se você usa safe extraction (mecânica). Mas teste se acumula:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
PRE-REFACTOR (T0)
|
|
301
|
+
Coverage: 0% (untested)
|
|
302
|
+
Confiança: baixa
|
|
303
|
+
|
|
304
|
+
DURANTE REFACTOR (T1-Tn)
|
|
305
|
+
Cada extract method → método extraído fica testável (puro/menor)
|
|
306
|
+
Adicione 1-3 unit tests do extracted antes de seguir
|
|
307
|
+
Coverage cresce 5-10% por extract
|
|
308
|
+
|
|
309
|
+
PÓS-REFACTOR (Tfinal)
|
|
310
|
+
Original 100 linhas → 1 método orquestrador + 8 helpers
|
|
311
|
+
Cobertura: 60-80% (helpers cobertos; orquestrador integration)
|
|
312
|
+
Confiança: alta
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Sem essa disciplina:** refactor termina, código mais limpo, mas cobertura ainda 0%. Próxima mudança volta ao mesmo dilema.
|
|
316
|
+
|
|
317
|
+
## Anti-patterns
|
|
318
|
+
|
|
319
|
+
### ANTI: refactor monstro em 1 PR
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
ANTI: PR de 1500 linhas — extracted 12 methods + renamed 8 variables
|
|
323
|
+
+ moved 3 fields + fixed 2 bugs + adicionou 25 tests.
|
|
324
|
+
|
|
325
|
+
PROBLEMA: PR não-revisável. Reviewer aprova "no fé". CI verde diz
|
|
326
|
+
pouco — branch coverage caiu. Revert é all-or-nothing.
|
|
327
|
+
|
|
328
|
+
CERTO: 12-25 commits/PRs em sequência. Cada um single-goal,
|
|
329
|
+
≤ 100 linhas, mecânico, revertível, com proof of correctness
|
|
330
|
+
(compila + roda).
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### ANTI: misturar refactor + bug fix + feature
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
ANTI: enquanto refatora, "ah esse if pode ser melhor", "esse loop
|
|
337
|
+
podia usar reduce", "ah aqui tem um bug, conserto".
|
|
338
|
+
|
|
339
|
+
PROBLEMA: você quebrou single-goal em ~5 lugares. Reviewer não consegue
|
|
340
|
+
identificar o que é refactor (preserva) vs bug fix (muda).
|
|
341
|
+
Se algo quebra, bisect aponta para PR mas não isola causa.
|
|
342
|
+
|
|
343
|
+
CERTO: anote bugs encontrados num arquivo `BUGS-FOUND.md`. Não
|
|
344
|
+
conserte agora. Após refactor terminar, faz PRs separados
|
|
345
|
+
para cada fix com test do comportamento correto.
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### ANTI: extract method + mover lógica
|
|
349
|
+
|
|
350
|
+
```text
|
|
351
|
+
ANTI: extract method, mas durante extraction "noto" que lógica é
|
|
352
|
+
melhor em outro escopo, então move pra lá durante o extract.
|
|
353
|
+
|
|
354
|
+
PROBLEMA: comportamento mudou. Não é mais SAFE extraction. Você
|
|
355
|
+
precisava de characterization mas pulou.
|
|
356
|
+
|
|
357
|
+
CERTO: 2 PRs sequenciais.
|
|
358
|
+
PR1 — extract method (idêntico, no mesmo escopo)
|
|
359
|
+
PR2 — move method (com test que valida em ambos os contextos)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### ANTI: scratch refactoring committed
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
ANTI: scratch ficou bom, mantenho-o em PR.
|
|
366
|
+
|
|
367
|
+
PROBLEMA: scratch fez mudanças não-mecânicas (estéticas, especulativas).
|
|
368
|
+
Sem characterization, não há prova de comportamento idêntico.
|
|
369
|
+
Você acabou de fazer "edit and pray" disfarçado de refactor.
|
|
370
|
+
|
|
371
|
+
CERTO: scratch é descartável. SEMPRE. Real refactor recomeça do
|
|
372
|
+
código original com passos disciplinados.
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### ANTI: extract APENAS para mais legibilidade, sem reduzir tamanho
|
|
376
|
+
|
|
377
|
+
```text
|
|
378
|
+
ANTI: extract de 1 linha para método com nome descritivo "para ficar
|
|
379
|
+
mais claro". Original tinha 200 linhas, agora tem 195 + 1 linha
|
|
380
|
+
em método novo.
|
|
381
|
+
|
|
382
|
+
PROBLEMA: 5 minutos para reviewer entender a chamada extra. Tamanho
|
|
383
|
+
do monster diminuiu 1%. Trade desfavorável.
|
|
384
|
+
|
|
385
|
+
CERTO: extract de 10-30 linhas mínimo. Bloco coeso e separável,
|
|
386
|
+
não single statement. Linha solta com nome longo é
|
|
387
|
+
refactoring teatral.
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### ANTI: tentar testar tudo upfront
|
|
391
|
+
|
|
392
|
+
```text
|
|
393
|
+
ANTI: "vou characterize completo das 200 linhas em todos os 30
|
|
394
|
+
inputs antes de tocar uma vírgula".
|
|
395
|
+
|
|
396
|
+
PROBLEMA: 200 linhas com 30 inputs = 1-2 semanas de characterization.
|
|
397
|
+
Stakeholder cancela. Refactor nunca acontece. Status quo
|
|
398
|
+
eterno.
|
|
399
|
+
|
|
400
|
+
CERTO: characterization MÍNIMA viável (5-10 inputs nos pontos óbvios).
|
|
401
|
+
Refactor mecânico (safe extraction, rename) que PRESERVA
|
|
402
|
+
comportamento. Acumula testes em helpers extraídos. Cobertura
|
|
403
|
+
emerge organicamente.
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Verificação
|
|
407
|
+
|
|
408
|
+
Antes de declarar refactor de monster method completo:
|
|
409
|
+
|
|
410
|
+
1. **Tipo identificado** — bulleted vs snarled
|
|
411
|
+
2. **Tamanho original < 100 linhas após refactor** — se ainda > 100, refactor não terminou
|
|
412
|
+
3. **Cada commit é single-goal** — rename OR extract OR move, nunca múltiplos
|
|
413
|
+
4. **Compilação verde a cada commit** — passos pequenos, mecânicos
|
|
414
|
+
5. **Smoke run após cada commit** — comportamento preservado
|
|
415
|
+
6. **Tests acumulados nos helpers extraídos** — coverage cresceu de 0% para ≥ 50%
|
|
416
|
+
7. **Bugs encontrados anotados, NÃO consertados durante refactor** — fix em PRs separados
|
|
417
|
+
8. **Sem scratch committed** — só conhecimento adquirido viajou
|
|
418
|
+
|
|
419
|
+
## Limiar de "pronto para feature change pós-refactor"
|
|
420
|
+
|
|
421
|
+
```text
|
|
422
|
+
Linhas do método principal: ≤ 100 (idealmente ≤ 50)
|
|
423
|
+
Profundidade máxima de aninhamento: ≤ 3
|
|
424
|
+
Helpers extraídos: 5-15 (cada ≤ 30 linhas)
|
|
425
|
+
Coverage do método principal: ≥ 50%
|
|
426
|
+
Coverage dos helpers: ≥ 70%
|
|
427
|
+
Bugs encontrados: anotados em BUGS-FOUND.md
|
|
428
|
+
PRs: cada single-goal, ≤ 100 linhas, revertíveis
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Atingidos? Agora a feature change pode acontecer com confiança normal de TDD.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Ver também
|
|
436
|
+
|
|
437
|
+
- [`_shared-legacy/glossary.md`](../_shared-legacy/glossary.md) — vocabulário (monster method, bulleted vs snarled, scratch, single-goal, safe extraction)
|
|
438
|
+
- [`legacy-characterization-tests`](../legacy-characterization-tests/SKILL.md) — para mudanças COMPORTAMENTAIS, characterization é obrigatório (não basta safe extraction)
|
|
439
|
+
- [`legacy-seams-and-test-harness`](../legacy-seams-and-test-harness/SKILL.md) — break-deps é pré-requisito quando helpers extraídos têm I/O
|
|
440
|
+
- [`legacy-effect-analysis`](../legacy-effect-analysis/SKILL.md) — sketch dentro do monster ajuda a escolher onde extrair
|
|
441
|
+
- [`legacy-sprout-wrap-techniques`](../legacy-sprout-wrap-techniques/SKILL.md) — quando monster > 300 linhas, sprout class para novo comportamento sem refatorar
|
|
442
|
+
- [`pre-refactor-characterization`](../pre-refactor-characterization/SKILL.md) — gate distingue safe extraction (livre) de behavioral change (requer characterization)
|
|
443
|
+
|
|
444
|
+
*Material-fonte: Working Effectively with Legacy Code — Feathers, 2004 — Cap 22: "I Need to Change a Monster Method and I Can't Write Tests for It".*
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: legacy-programming-by-difference
|
|
3
|
+
description: Use ao adicionar comportamento variante a código legado via subclassing/composition (cap 8 Feathers) — ponte temporária quando refactor estrutural ainda não cabe. Modernização 2026 — feature flags + variants A/B como aplicação direta.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Legacy — Programming by Difference
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando user precisa adicionar comportamento que coexiste com o existente (não substitui). Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "adicionar variante de [feature]"
|
|
13
|
+
- "feature flag para [behavior]"
|
|
14
|
+
- "comportamento alternativo para subset de users"
|
|
15
|
+
- "A/B test entre comportamentos"
|
|
16
|
+
- "programming by difference", "cap 8 Feathers"
|
|
17
|
+
- "subclass para mudar UMA coisa"
|
|
18
|
+
|
|
19
|
+
Carrega como atalho TDD em legacy quando refactor estrutural maior ainda não vale o custo.
|
|
20
|
+
|
|
21
|
+
## Regras absolutas
|
|
22
|
+
|
|
23
|
+
- **Programming by difference é PONTE TEMPORÁRIA**, não solução final. Funciona enquanto # variantes ≤ 3-4. Acima disso, refactor para strategy pattern ou similar.
|
|
24
|
+
- **Subclass-and-override** para herança; **composition** para frameworks/libs anti-herança. Padrão escolhido pela situação, não preferência teórica.
|
|
25
|
+
- **Cada variante é testada isoladamente.** Subclass tem suite própria. Composição tem fakes próprios.
|
|
26
|
+
- **Variants NÃO compartilham state mutável.** Estado é encapsulado dentro de cada variante. Compartilhamento via construtor (DI), nunca via globals.
|
|
27
|
+
- **Não bifurque o teste de cada variante.** Suite de characterization da base + suite de testes específicos da variante. Variante NÃO deve replicar tests da base.
|
|
28
|
+
- **Modernização 2026 — feature flags + A/B testing são aplicação direta.** GrowthBook, LaunchDarkly, Optimizely, Statsig — cada flag/variant é programming-by-difference em larga escala.
|
|
29
|
+
|
|
30
|
+
## Patterns canônicos
|
|
31
|
+
|
|
32
|
+
### Pattern 1: Subclass-and-override para variante simples
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// ANTES — comportamento único
|
|
36
|
+
class CheckoutFlow {
|
|
37
|
+
computeShipping(order: Order): number {
|
|
38
|
+
return order.weightKg * 5 // R$ 5/kg flat
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// DEPOIS — variante regional via subclass (cap 8 original)
|
|
43
|
+
class CheckoutFlow {
|
|
44
|
+
computeShipping(order: Order): number {
|
|
45
|
+
return order.weightKg * 5
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class CheckoutFlowNorthRegion extends CheckoutFlow {
|
|
50
|
+
override computeShipping(order: Order): number {
|
|
51
|
+
return order.weightKg * 8 // norte: frete maior
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Em tests
|
|
56
|
+
test('CheckoutFlowNorthRegion — shipping factor', () => {
|
|
57
|
+
const flow = new CheckoutFlowNorthRegion()
|
|
58
|
+
expect(flow.computeShipping({ weightKg: 2 } as Order)).toBe(16)
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Pattern 2: Composition variant (modernização — feature flags)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// Modernização 2026 — variant via DI (mais flexível que herança)
|
|
66
|
+
interface ShippingCalculator {
|
|
67
|
+
compute(order: Order): number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class FlatShippingCalc implements ShippingCalculator {
|
|
71
|
+
compute(order: Order): number { return order.weightKg * 5 }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class NorthShippingCalc implements ShippingCalculator {
|
|
75
|
+
compute(order: Order): number { return order.weightKg * 8 }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class TieredShippingCalc implements ShippingCalculator {
|
|
79
|
+
compute(order: Order): number {
|
|
80
|
+
if (order.weightKg < 1) return 5
|
|
81
|
+
if (order.weightKg < 5) return 12
|
|
82
|
+
return order.weightKg * 4
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class CheckoutFlow {
|
|
87
|
+
constructor(private shipping: ShippingCalculator = new FlatShippingCalc()) {}
|
|
88
|
+
computeTotal(order: Order): number {
|
|
89
|
+
return order.subtotal + this.shipping.compute(order)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Em produção — feature flag escolhe a variant
|
|
94
|
+
function getShippingCalcForUser(user: User): ShippingCalculator {
|
|
95
|
+
const variant = featureFlags.getVariant('shipping-strategy', user.id)
|
|
96
|
+
switch (variant) {
|
|
97
|
+
case 'flat': return new FlatShippingCalc()
|
|
98
|
+
case 'north': return new NorthShippingCalc()
|
|
99
|
+
case 'tiered': return new TieredShippingCalc()
|
|
100
|
+
default: return new FlatShippingCalc()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const flow = new CheckoutFlow(getShippingCalcForUser(user))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Modernização explícita:** programming-by-difference em 2004 = subclass-and-override; em 2026 = composition + feature flag service. Mesma essência (variante isolada, comportamento testado isolado), tooling diferente.
|
|
108
|
+
|
|
109
|
+
### Pattern 3: Quando preferir herança vs composição
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
HERANÇA (subclass-and-override)
|
|
113
|
+
================================
|
|
114
|
+
- Variante TEMPORÁRIA (vai ser removida em < 6 meses)
|
|
115
|
+
- Variante muda UMA coisa específica
|
|
116
|
+
- Classe base já tem método virtual/protected acessível
|
|
117
|
+
- Você tem confiança que a refatoração estrutural virá depois
|
|
118
|
+
|
|
119
|
+
COMPOSIÇÃO (DI)
|
|
120
|
+
================
|
|
121
|
+
- Variante PERMANENTE (parte do design)
|
|
122
|
+
- Múltiplas variantes (3+) coexistem
|
|
123
|
+
- Variantes podem ser combinadas (ortogonais)
|
|
124
|
+
- Classe base não tem métodos override-friendly
|
|
125
|
+
- Aplicação moderna com feature flags
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Pattern 4: TDD em legacy via programming-by-difference
|
|
129
|
+
|
|
130
|
+
Workflow canônico para inserir feature em código legacy sem testes:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
1. Identificar onde a NEW feature mudaria comportamento
|
|
134
|
+
2. Criar SUBCLASS (TestableLegacyClass extends LegacyClass)
|
|
135
|
+
3. Override método relevante; nova lógica é puramente NA SUBCLASSE
|
|
136
|
+
4. Test da SUBCLASS isolada (cap 8 — TDD by difference)
|
|
137
|
+
5. Em produção, feature flag escolhe Legacy ou TestableLegacy
|
|
138
|
+
6. Quando feature provada, EVOLVE — mover lógica para classe base com flag
|
|
139
|
+
|
|
140
|
+
Trade-off: legado segue untested no caminho default; mas NEW feature
|
|
141
|
+
é totalmente coberta no caminho variant.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Pattern 5: A/B test como programming-by-difference
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
Cenário moderno: testar 2 algoritmos de recomendação.
|
|
148
|
+
|
|
149
|
+
VARIANT A (atual): TF-IDF
|
|
150
|
+
VARIANT B (novo): Embedding similarity
|
|
151
|
+
|
|
152
|
+
Implementação:
|
|
153
|
+
interface Recommender { recommend(user, items): Recommendation[] }
|
|
154
|
+
class TFIDFRecommender implements Recommender { ... }
|
|
155
|
+
class EmbeddingRecommender implements Recommender { ... }
|
|
156
|
+
|
|
157
|
+
// Service layer
|
|
158
|
+
class FeedService {
|
|
159
|
+
constructor(private recommender: Recommender) {}
|
|
160
|
+
getFeed(user: User): Recommendation[] { return this.recommender.recommend(user, ...) }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Selector
|
|
164
|
+
function getRecommender(user: User): Recommender {
|
|
165
|
+
return featureFlags.getBoolean('use-embedding-recommender', user.id)
|
|
166
|
+
? new EmbeddingRecommender()
|
|
167
|
+
: new TFIDFRecommender()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Cada variant tem TESTES PRÓPRIOS isolados.
|
|
171
|
+
// Service layer testado uma vez com fake recommender.
|
|
172
|
+
// A/B test em prod compara métricas (CTR, dwell-time).
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Anti-patterns
|
|
176
|
+
|
|
177
|
+
### ANTI: subclassing uma vez = bom; subclassing 5 vezes = god hierarchy
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
ANTI: criar TenantA extends Base, TenantB extends Base, ... 8 subclasses
|
|
181
|
+
cada uma overriding 2-3 métodos.
|
|
182
|
+
|
|
183
|
+
PROBLEMA: hierarquia explode. Mudança na base afeta 8 subclasses.
|
|
184
|
+
Combinações (TenantA + featureX) ficam impossíveis sem
|
|
185
|
+
multiple inheritance.
|
|
186
|
+
|
|
187
|
+
CERTO: composition. Tenant é DTO/config, não subclass. Variantes
|
|
188
|
+
comportamentais são strategies injetadas. Combinações
|
|
189
|
+
ficam ortogonais.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### ANTI: variant compartilha state mutável com base
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
ANTI: SubClass.method() faz this.parentField = newValue (mutating
|
|
196
|
+
shared field).
|
|
197
|
+
|
|
198
|
+
PROBLEMA: state compartilhado quebra isolamento. Test variant
|
|
199
|
+
afeta state. Concurrent uso vira corrupção.
|
|
200
|
+
|
|
201
|
+
CERTO: variant tem state PRÓPRIO (fields novos na subclass) ou
|
|
202
|
+
recebe via DI. Base não é tocada.
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### ANTI: programming-by-difference como solução final
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
ANTI: depois de 1 ano, ainda tem 5 subclasses managing 5 user
|
|
209
|
+
segments. Nenhuma refatoração estrutural.
|
|
210
|
+
|
|
211
|
+
PROBLEMA: ponte virou ponto fixo. Hierarquia frágil. Onboarding
|
|
212
|
+
de 6º segment vira sprint inteira.
|
|
213
|
+
|
|
214
|
+
CERTO: programming-by-difference é PONTE. Após 3-6 meses, refatorar
|
|
215
|
+
para strategy pattern, plugin architecture, ou similar.
|
|
216
|
+
Hierarquia ≤ 3 subclasses; mais que isso = sinal de refactor.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### ANTI: feature flag sem deprecation
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
ANTI: feature flag X criada há 2 anos, ainda em prod, ambos paths
|
|
223
|
+
mantidos.
|
|
224
|
+
|
|
225
|
+
PROBLEMA: dead code maintenance dobrada. Bugs no path desligado
|
|
226
|
+
escapam meses. Quem leu por último = X-2-anos-atrás.
|
|
227
|
+
|
|
228
|
+
CERTO: feature flags têm DATA DE EXPIRAÇÃO. Após launch full +
|
|
229
|
+
30 dias safe, REMOVE flag (e variante perdedora). Cleanup
|
|
230
|
+
é parte da feature, não opcional.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Verificação
|
|
234
|
+
|
|
235
|
+
1. Subclass/variant testada isoladamente
|
|
236
|
+
2. Base class continua funcionando para usos default (compilação verde)
|
|
237
|
+
3. Variant não modifica state da base
|
|
238
|
+
4. Feature flag/seletor documentado se aplicável
|
|
239
|
+
5. Critério de remoção do flag/variant documentado (data ou métrica)
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Ver também
|
|
244
|
+
|
|
245
|
+
- [`_shared-legacy/glossary.md`](../_shared-legacy/glossary.md) — vocabulário (subclass-and-override, programming-by-difference)
|
|
246
|
+
- [`legacy-sprout-wrap-techniques`](../legacy-sprout-wrap-techniques/SKILL.md) — sprout method é caso especial (variant via composição)
|
|
247
|
+
- [`legacy-seams-and-test-harness`](../legacy-seams-and-test-harness/SKILL.md) — subclass-and-override é Pattern 3 lá
|
|
248
|
+
- [`legacy-extract-class`](../legacy-extract-class/SKILL.md) — quando 5+ variantes virou hierarquia, refactor para strategy pattern via extract class
|
|
249
|
+
- [`event-based-slos`](../event-based-slos/SKILL.md) (v1.9) — A/B test consume SLO events para validar variant superior
|
|
250
|
+
|
|
251
|
+
*Material-fonte: Working Effectively with Legacy Code — Feathers, 2004 — Cap 8: "How Do I Add a Feature?".*
|
|
252
|
+
*Modernização (2026):* Feature flags + A/B testing tooling como aplicação direta de programming-by-difference em escala.
|