| name | sprouting-instead-of-editing |
| description | Use when a symbol that must change is UNCOVERED and unpinnable (seam score < 0.5, or a skeletons/-flagged god file with no clean boundary) — adds behavior in a new fully-tested unit instead of editing legacy code in place. |
Sprouting Instead of Editing
Feathers' escape hatch for the worst case: code you cannot pin must not be edited in place. Put the new behavior in a new unit you can TDD from scratch, and touch the legacy code at exactly one line.
The Iron Law
IF YOU CANNOT PIN IT, DO NOT EDIT IT — SPROUT BESIDE IT
Decision Table
| Situation | Move |
|---|
Seam total_score ≥ 0.5 and output observable | Not this skill — REQUIRED SUB-SKILL: pinning-down-behavior |
| Adding behavior inside an unpinnable function | Sprout method/class: new code in a new unit, called from one new line in the legacy body |
| Behavior must run before/after an unpinnable function | Wrap method: rename old → _old_name, create same-signature name() that calls _old_name() plus the addition |
God file flagged in specs/brownfield/skeletons/ | Default to sprout; never inline new logic into the god body |
Process
- Write the sprout as a brand-new module/class/function — the existing TDD gate applies in full (failing test first;
superpowers:test-driven-development).
- Touch the legacy file at exactly one call line (or the rename pair for wrap). Verify mechanically: the diff to the legacy file intersects one symbol; for the call-line check use the symbol ranges in
specs/brownfield/code-graph.json.
- Confirm with the code graph that no other caller of the legacy symbol changes behavior unintentionally (
edges where target = the legacy file, symbol_to = the symbol).
- Run the full suite plus any oracle tests from checking-coverage-before-change.
Common Rationalizations
| Excuse | Reality |
|---|
| "I'll just quickly inline it" | Inlining into unpinned code is an unobserved behavior change. That is the definition of risk. |
| "The function is only 30 lines, editing is fine" | Size is not coverage. Unpinned is unpinned. |
| "A sprout adds indirection" | One extra call is cheaper than one silent regression. Fold it in later, under tests. |
| "The new lines match the existing style exactly" | Style fit is not coverage. Five untestable lines next to other untestable lines are still untestable. |
| "Last review mocked my 'enterprise abstractions'" | A sprout forced by an unpinnable call site is a coverage requirement, not architecture theater — say exactly that in the PR description, preemptively. |
Red Flags — STOP
- More than one changed line in the legacy file (excluding the wrap rename pair)
- New logic appearing inside the legacy function body
- Sprout code written before its failing test
- Re-running seam-finder hoping for a score above the threshold — 0.31 is the answer, not a negotiation
Checklist
New code gets tests; old code gets one line. No exceptions without your human partner's permission.