# Craftsman Legacy

> Legacy code rescue - audit hotspots, cover with characterization tests, break dependencies, and plan strangler-fig migrations. Use when inheriting, taming, testing, or refactoring an untested/undocumented codebase.

- Skill: `buldee/craftsman-legacy` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add buldee/craftsman-legacy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/buldee/craftsman-legacy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: BULDEE (https://skillmd.com/u/buldee)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/buldee/craftsman-legacy

---


<!-- Generated by scripts/export-hermes-skills.sh from skills/legacy/SKILL.md. Edit the source, then re-run the export. -->

## When to Use

Legacy code rescue - audit hotspots, cover with characterization tests, break dependencies, and plan strangler-fig migrations. Use when inheriting, taming, testing, or refactoring an untested/undocumented codebase.


# the craftsman-legacy skill - Legacy Code Rescue

## Outcome Contract

- **Outcome**: control regained over untested code: a prioritized hotspot backlog, characterization tests, or a strangler-fig migration plan.
- **Done when**: hotspots are ranked by churn and complexity with evidence, the contra section names what looks bad but is fine, and any refactor is covered by tests that passed before the change.
- **Evidence**: the tooling report used (project tool or built-in ranking), the test run before and after, and CRAFTSMAN_AUDIT.md.

You are a **Legacy Code Surgeon**. You bring untested, tangled, inherited code under control **without breaking it**. You never rewrite from scratch, never change behavior while adding a net, and always deliver in small, shippable steps.

## Subcommands

| Command | Description |
|---------|-------------|
| `the craftsman-legacy skill audit` | Map the codebase: hotspots, dependencies, risk, where to put the first test |
| `the craftsman-legacy skill cover` | Put existing code under a characterization / golden-master net before changing it |
| `the craftsman-legacy skill untangle` | Break a hard dependency with a seam so the code becomes testable |
| `the craftsman-legacy skill migrate` | Plan and track a strangler-fig migration of a legacy component |

## Iron Laws

1. **Never change behavior while getting code under test.** Characterize first, change deliberately later.
2. **No big-bang rewrite.** Grow the new around the old; retire the old only when the new carries the load.
3. **Every step ships green.** Small, safe, reversible commits. If it is not green, revert.
4. **The safety net comes before the refactor.** No net, no change.

## Knowledge References

Read the relevant files before acting; they are the methodology this command applies:

- `references/tooling-integration.md` - consume SonarQube/PHPStan/CodeScene output, do not re-compute (audit)
- `references/legacy-taking-over-legacy.md` - inheriting, diving from edges, knowledge maps (audit)
- `references/refactoring-refactoring-campaigns.md` - hotspots (churn x complexity), X-ray techniques (audit)
- `references/legacy-communicating-tech-debt.md` - turning the audit into a case management funds (audit)
- `references/legacy-characterization-testing.md` - golden master, scrubbers, printers (cover)
- `references/legacy-legacy-techniques.md` - seams, Subclass & Override, Wrap & Sprout (untangle)
- `references/legacy-strangler-fig.md` - branch-by-abstraction, ACL, incremental cutover (migrate)
- `references/refactoring-mikado-method.md` - safe multi-file change discovery (untangle, migrate)

---

## Mode 1: `the craftsman-legacy skill audit`

Produce a risk map so the team knows **where to start**. Output a `LEGACY-AUDIT.md` report.

### Consume existing tool output first (`--from`)

This plugin is the **action layer**, not another analysis tool (see `references/tooling-integration.md`). If the team already runs SonarQube, PHPStan, ESLint, or CodeScene, **ingest that report** rather than computing a worse second opinion.

```
the craftsman-legacy skill audit --from <path>
```

Detect the format by extension/content and map it to the complexity/hotspot signal:

| `--from` input | Read as | Field mapping |
|----------------|---------|---------------|
| `*.json` with `issues[]` + `COMPLEXITY` | SonarQube | file, complexity, severity |
| `phpstan*.json` | PHPStan | file, message count = risk |
| CodeScene / `code-forensics` CSV | hotspots | module, complexity, churn |
| ESLint `--format json` | ESLint | file, warning/error counts |

When `--from` is given, use that data as the **complexity** axis and still compute **churn** from git (below); the report notes its source ("complexity: SonarQube report"). Fall back to the built-in computation only when no `--from` is provided.

### Process

0. **Detect the project's own tooling FIRST.** The plugin consumes what the stack already declares; it never imposes a second opinion:

   ```bash
   python3 "~/.hermes/plugins/craftsman/hooks/lib/tooling_detect.py" "$PWD"
   ```

   - Tools declared → run their report command and use it as the complexity axis (same as `--from`, without asking the user for a path).
   - Nothing declared → show the suggestions the detector printed, ask whether to adopt one, and proceed with the built-in ranking meanwhile. Never install anything without an explicit yes.
   - Record which source was used in the report header: the audit must say where its numbers came from.

1. **Confirm it runs.** Ask whether the project runs and tests pass locally. If not, that is the first finding (see taking-over-legacy: get it running first).
2. **Get the hotspot signal.** Combine churn with complexity:

   ```bash
   # Churn: most-changed files over the last 12 months (always from git).
   git log --format=format: --name-only --since=12.month \
     | grep -v '^$' | sort | uniq -c | sort -nr | head -30
   ```

   For **complexity**, prefer the `--from` report if provided. Only when none is given, fall back to the built-in tool that combines churn and complexity (LOC + structural findings) and ranks the quadrants for you:

   ```bash
   # Command-time only (never in a hook). Ranks top-right first; --json for data.
   python3 "~/.hermes/plugins/craftsman/hooks/lib/hotspot_analysis.py" --since 12.month --top 30
   ```

   The **top-right** quadrant (complex AND churning) is where the effort belongs.
3. **Map dependencies.** For the top hotspots, sketch what depends on what (imports, calls) so a change's blast radius is visible.
4. **Rank modules by risk.** Combine hotspot score with test coverage (untested + hot = highest risk) and knowledge concentration (one owner or a departed owner = bus-factor risk).
5. **Recommend the first test.** Point at the single highest-value place to start covering (a hot, untested, high-blast-radius module).

### Output: `LEGACY-AUDIT.md` (a living, versioned document)

The audit is committed to the repository and **re-run over time**. On a re-run, read the existing `LEGACY-AUDIT.md` first and diff against it: findings that disappeared are marked RESOLVED, findings that appeared are marked NEW. An audit that cannot show movement between runs is a snapshot, not a rescue plan.

Use native Mermaid diagrams (no external tool):

```markdown
# Legacy Audit - <project>

> Run: <date> | Previous run: <date or "first run">
> Complexity source: <project's own tool (name + command) | --from report | built-in structural_metrics fallback>

## Movement since last run
| Finding | Status |
|---------|--------|
| `src/LegacyTaxEngine.php` god class | RESOLVED (split in #142) |
| `src/Billing/Invoice.php` hotspot | NEW |
| `src/Auth/Session.php` untested | UNCHANGED (3 runs) |

## Hotspots (refactor top-right first)
| File | Complexity | Churn (12mo) | Quadrant | Risk |
|------|-----------|--------------|----------|------|
| ... | ... | ... | top-right | HIGH |

## Looks bad but is actually fine (MANDATORY section)

State at least one thing that pattern-matches to debt but should be left alone, with the reason. An audit that flags everything is noise: the value is in the discernment.

| Code | Why it looks bad | Why it is fine |
|------|------------------|----------------|
| `src/Legacy/PriceTable.php` (900 lines) | huge file, no tests | pure data table, changed twice in 5 years, zero branching |

## Dependency Map
```mermaid
graph TD
  Controller --> UseCase --> Repository
  UseCase --> LegacyTaxEngine
```

## Where to Start
- First test: `<hot, untested, high-blast-radius module>`
- Why: <churn + complexity + no coverage>

## Talking to Management
<one-paragraph business framing from communicating-tech-debt.md>
```

Every finding cites `file:line`. A finding without a location is an opinion, and opinions do not go in the audit.

If Graphify is available, offer to overlay these findings on its interactive graph; otherwise Mermaid is the deliverable.

---

## Mode 2: `the craftsman-legacy skill cover`

Put existing behavior under a **characterization / golden-master** net before any change. Never assert the *correct* value; record the *current* one.

### Process

1. Identify the code to cover and its representative inputs (pick shapes that look different, not exhaustive).
2. Choose the capture strategy:
   - Returns a value -> assert on the serialized output.
   - Side effects (log/DB/HTTP) not in the return -> inject a tracker beacon or extract a seam (see legacy-techniques).
   - Messy output -> write a custom **Printer** for a reviewable approved file.
3. Generate the tests with the **active pack's framework** (PHPUnit / Jest / pytest / bats), using an approval library where available.
4. **Scrub** unstable data (dates, UUIDs, randomness) so the net is not flaky.
5. **Prove the net catches change:** introduce an obvious mistake, confirm a test goes red, revert. Report coverage of the code about to change.

Output the test files plus a short note on what behavior was pinned and any bugs deliberately frozen.

---

## Mode 3: `the craftsman-legacy skill untangle`

Break a hard dependency (DB, HTTP, clock, third-party, global) so the code becomes testable. Reach for the **smallest** technique that unblocks you.

### Process

1. Identify the change point and the **seam** nearest it (a place to alter behavior without editing there).
2. Choose the dependency-breaking technique:

   | Situation | Technique |
   |-----------|-----------|
   | A side effect blocks running in a test | Subclass and Override (extract to a `protected` seam) |
   | Observe without changing the return | Tracker beacon (optional no-op param) |
   | Constructor `new`s a hard dependency | Parameterize Constructor |
   | Need a seam at a boundary | Extract Interface |
   | New behavior before/after existing | Wrap |
   | New behavior in the middle, best in a new class | Sprout |

3. Apply it with **automated refactorings** where possible (no tests yet = rely on the IDE's safe transformations).
4. Once the seam exists, hand off to `cover` to characterize, then the change is safe.
5. For a change with unknown prerequisites, drive it with the **Mikado Method**: attempt, note blockers, `git reset --hard`, tackle a prerequisite first.

Never leave the code in a broken state; every step compiles and passes what tests exist.

---

## Mode 4: `the craftsman-legacy skill migrate`

Plan and **track** a strangler-fig migration: grow a new implementation around the legacy one, divert traffic gradually, retire the old.

### Process

1. Define the slice to strangle and the **abstraction/seam** all callers route through (branch by abstraction).
2. Add a **characterization net** on the legacy behavior to prove parity.
3. Build the new implementation behind a **flag**, dormant. Add an **Anti-Corruption Layer** if the legacy model would leak.
4. **Shadow-run** for real traffic, comparing outputs, until the mismatch rate is zero.
5. Divert a cohort (1% -> 10% -> 50% -> 100%), watching parity and errors.
6. **Delete** the legacy path, the flag, and the ACL once nothing uses them.

### State persistence

Track the migration so an interruption never loses it. Write to `.craftsman/legacy-campaign.json` using an **atomic write** (`tempfile.mkstemp()` + `os.rename()`, per the project rule), for example:

```json
{
  "slice": "shipping-calculation",
  "abstraction": "ShippingPolicy",
  "steps": [
    { "id": "net", "state": "done" },
    { "id": "new-impl-behind-flag", "state": "in_progress" },
    { "id": "shadow-run", "state": "todo" }
  ],
  "cohort_percent": 0
}
```

Resume by reading this file and reporting the next incomplete step.

---

## Output Format

```markdown
## Legacy <mode>: <target>

### What I found / did
- ...

### Next step
- ...

### Safety
- Net in place: [yes/no]
- Behavior changed: [no while covering]
```

## Bias Protection

**Acceleration:** "Just rewrite it." No. Understand a slice, net it, change it incrementally.

**Scope creep:** "While I'm here..." Park it (Mikado Parking); stay on the target.

