Add features to tested code using TDD or Programming by Difference. Use whenever a developer is adding a feature to a class that already has tests (or can be...
---
name: tdd-and-programming-by-difference
description: "Add features to tested code using TDD or Programming by Difference. Use whenever a developer is adding a feature to a class that already has tests (or can be brought under test) — 'how do I add this feature', 'test-driven development for legacy', 'red green refactor', 'TDD cycle', 'add behavior with tests', 'inheritance to add feature', 'programming by difference', 'subclass to add behavior', 'Liskov substitution', 'LSP violation'. Triggers for 'I just got this class under test, now I need to add X'."
version: 1.0.0
homepage: https://github.com/bookforge-ai/bookforge-skills/tree/main/books/working-effectively-with-legacy-code/skills/tdd-and-programming-by-difference
metadata: {"openclaw":{"emoji":"📚","homepage":"https://github.com/bookforge-ai/bookforge-skills"}}
status: draft
source-books:
- id: working-effectively-with-legacy-code
title: "Working Effectively with Legacy Code"
authors: ["Michael C. Feathers"]
chapters: [8]
domain: software-engineering
tags: [legacy-code, testing, tdd, refactoring, software-engineering, feature-addition]
depends-on:
- legacy-code-change-algorithm
- safe-legacy-editing-discipline
- characterization-test-writing
execution:
tier: 1
mode: full
inputs:
- type: codebase
description: "Class that is under test (or testable) + feature specification"
tools-required: [Read, Edit, Bash]
tools-optional: [Grep]
mcps-required: []
environment: "Codebase with working test framework."
discovery:
goal: "Add a feature to tested code using the appropriate technique (TDD vs Programming by Difference)."
tasks:
- "Confirm the class IS under test"
- "Decide TDD vs Programming by Difference"
- "Execute the chosen approach"
- "For PbD: normalize the inheritance (push up or push down) once feature works"
- "Detect LSP violations"
audience:
roles: [software-engineer, backend-developer]
experience: intermediate
when_to_use:
triggers:
- "Adding a feature to a class that has tests"
- "Feature should coexist with existing class behavior"
- "Prefer tests-first workflow"
prerequisites:
- skill: characterization-test-writing
why: "If class is not yet under test, characterize first"
not_for:
- "Class is NOT testable — use legacy-code-addition-techniques (Sprout/Wrap) instead"
environment:
codebase_required: true
codebase_helpful: true
works_offline: true
quality:
scores: {with_skill: null, baseline: null, delta: null}
tested_at: null
eval_count: null
assertion_count: 11
iterations_needed: null
---
# TDD and Programming by Difference
## When to Use
The class is already under test — or can be brought under test with minimal effort. You need to add a new feature and you want to do it safely, with tests driving the design from the start.
This skill covers two complementary techniques:
- **Test-Driven Development (TDD):** the default. Language-agnostic. Add the feature directly to the class by writing one failing test at a time, implementing just enough to pass, then removing duplication.
- **Programming by Difference (PbD):** object-oriented only. Create a subclass, write tests against it, implement the feature there, then normalize the inheritance hierarchy. Useful when you want to defer the integration decision until you can see the feature working.
If the class is **not yet under test**, do not start here. Run `characterization-test-writing` first to establish a safety net, then return. If the class cannot be brought under test at all, use `legacy-code-addition-techniques` (Sprout Method or Wrap Method) instead.
## Context and Input Gathering
Before choosing an approach, collect:
1. **The target class** — confirm it has passing tests or can be brought into a test harness quickly.
2. **The feature specification** — what the new behavior should do, including edge cases you can identify now.
3. **The language** — PbD requires an object-oriented language with single-class inheritance. TDD works in any language.
4. **Whether modifying the existing class is feasible** — if the class is shared, frozen, or owned by another team, PbD may be the safer path.
## Process
### Step 1: Confirm the Class Is Under Test
Run the existing test suite for the target class. All tests must pass before you write a single new line of feature code. A failing test baseline is a signal: stop, diagnose, and fix before proceeding.
If no tests exist for this class, invoke `characterization-test-writing` now. Come back here when you have a passing baseline.
### Step 2: Decide — TDD or Programming by Difference
Use this decision rule:
**Use TDD (default) when:**
- You can modify the existing class directly.
- The feature belongs in the class — it is not a variant, an override, or an alternative behavior mode.
- The language does not require an OO inheritance model (procedural, functional, etc.).
**Use Programming by Difference when:**
- You cannot or do not want to modify the existing class directly (shared, frozen, or complex dependency on existing behavior).
- You want to prove the feature works in isolation before deciding where it lives in the hierarchy.
- The integration direction is unclear — you need to see the feature first, then decide: push it up (shared behavior) or push it down (specialized subclass).
When uncertain, choose TDD. PbD adds a normalization step that TDD avoids.
### Step 3 (TDD Path): Run the 5-Step Cycle
For each slice of the feature, execute this cycle in full before moving to the next slice:
**1. Write a failing test case.**
Write a test for the method or behavior you need. Do not write more than one new test at a time. The test must fail when you run it now — if it passes immediately, the test is wrong or the feature already exists.
**2. Get it to compile.**
Add the minimum code required for the test to compile: method signature, return type, stub body. Return a nonsense value (NaN, -1, empty string, null) so the test fails at assertion, not at compilation.
**3. Make it pass.**
Implement only enough code to make this specific test pass. Do not generalize. Do not add untested behavior. Write the simplest implementation that works.
**4. Remove duplication.**
Look for structural duplication between the new code and existing code. Extract shared behavior into a helper method, a shared base, or a well-named private. Do not skip this step — duplication left in place becomes a maintenance burden and obscures design intent. The tests you now have make this safe.
**5. Repeat.**
Move to the next test case. Each iteration narrows in on the full behavior — edge cases, error cases, boundary conditions.
**InstrumentCalculator example — two full iterations:**
*Iteration 1: base case*
```java
// Step 1: Failing test
public void testFirstMoment() {
InstrumentCalculator calculator = new InstrumentCalculator();
calculator.addElement(1.0);
calculator.addElement(2.0);
assertEquals(-0.5, calculator.firstMomentAbout(2.0), TOLERANCE);
}
// Step 2: Stub — compiles, fails assertion
public double firstMomentAbout(double point) {
return Double.NaN;
}
// Step 3: Implementation that passes
public double firstMomentAbout(double point) {
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}
// Step 4: No duplication yet — proceed
```
*Iteration 2: edge case — empty elements list*
```java
// Step 1: New failing test for the zero-division case
public void testFirstMoment_noElements() {
try {
new InstrumentCalculator().firstMomentAbout(0.0);
fail("expected InvalidBasisException");
} catch (InvalidBasisException e) { }
}
// Step 2: Update declaration to compile
public double firstMomentAbout(double point) throws InvalidBasisException { ... }
// Step 3: Add guard clause to pass
public double firstMomentAbout(double point) throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
// ... existing loop ...
}
// Step 4: When secondMomentAbout arrives, extract nthMomentAbout(point, n)
// to remove the loop duplication between the two methods.
```
> **Why the duplication step is non-negotiable:** Skipping it leaves clones in the codebase. Future developers find `firstMomentAbout` and `secondMomentAbout` side by side, both with the same loop, and change one but not the other. The TDD cycle's power comes from the entire loop — removing duplication is the step that makes the code better, not just tested.
### Step 4 (Programming by Difference Path): Create a Testing Subclass
Instead of modifying the existing class, create a subclass whose sole purpose is to introduce the new feature. Write tests against the subclass, not the parent.
```java
// Existing tested class — do not touch yet
public class MessageForwarder {
private InternetAddress getFromAddress(Message message) { ... }
// ...
}
// New subclass introduces the feature
public class AnonymousMessageForwarder extends MessageForwarder {
@Override
protected InternetAddress getFromAddress(Message message)
throws MessagingException {
return new InternetAddress("anon-members@" + listAddress);
}
}
// Test drives against the subclass
public void testAnonymous() throws Exception {
MessageForwarder forwarder = new AnonymousMessageForwarder();
forwarder.forwardMessage(makeFakeMessage());
assertEquals("anon-members@" + forwarder.getDomain(),
expectedMessage.getFrom()[0].toString());
}
```
The test passes. The feature is proven. Now you have a decision to make (Step 5).
### Step 5 (PbD Path): Normalize the Inheritance
Once the feature tests are passing, decide how to integrate:
**Option A — Push behavior UP (shared by all subclasses):**
The feature belongs in the parent class. Refactor the parent to include the logic (e.g., via a configuration flag or a new abstract method), verify all tests still pass, and delete the subclass.
```java
// Move anonymous logic into MessageForwarder via configuration
private InternetAddress getFromAddress(Message message) throws MessagingException {
if (configuration.getProperty("anonymous").equals("true")) {
return new InternetAddress("anon-members@" + domain);
}
Address[] from = message.getFrom();
if (from != null && from.length > 0)
return new InternetAddress(from[0].toString());
return new InternetAddress(getDefaultFrom());
}
// AnonymousMessageForwarder is now deletable — tests pass without the override
```
**Option B — Push behavior DOWN (specialized subclass):**
The behavior is genuinely specific to one variant. Make the parent method abstract, force subclasses to provide their own implementation. No subclass should override a concrete method — that is the normalized form.
```java
// Normalized hierarchy: parent is abstract, subclasses provide implementations
public abstract class MessageForwarder {
protected abstract InternetAddress getFromAddress(Message message)
throws MessagingException;
}
public class StandardMessageForwarder extends MessageForwarder {
protected InternetAddress getFromAddress(Message message) { ... }
}
public class AnonymousMessageForwarder extends MessageForwarder {
protected InternetAddress getFromAddress(Message message) { ... }
}
```
> **What is a normalized hierarchy?** A hierarchy in which no class has a method that overrides a concrete method it inherited. Every method is either implemented once (in the class that owns it) or abstract (delegated to all subclasses). In a normalized hierarchy, "How does this class do X?" has a single, unambiguous answer.
### Step 6: Watch for LSP Violations
During PbD normalization, check whether the subclass changes method semantics in a way that breaks callers who expect the parent's behavior.
**Classic LSP violation pattern:**
```java
// Parent: setWidth(3) and setHeight(4) gives area = 12
Rectangle r = new Square();
r.setWidth(3);
r.setHeight(4);
// Actual area: 16 — Square.setWidth() also changed height to maintain squareness
// Caller expected 12. LSP is violated.
```
**Red flags during normalization:**
1. The subclass override changes a return value's *meaning*, not just its value.
2. A caller that holds a reference typed as the parent class would behave differently if it received a subclass instance.
3. The override removes a guarantee the parent's contract provided (e.g., parent never throws, subclass throws).
**Rules of thumb to avoid LSP violations:**
- Prefer not to override concrete methods. If you must, call `super.method()` inside the override wherever possible.
- If the override changes semantics rather than extends them, use composition (delegate to a collaborator object) instead of inheritance.
If you detect an LSP violation during normalization, do not suppress it. Restructure: either use composition, make the parent abstract, or move the feature out of the inheritance hierarchy entirely.
### Step 7: Integrate
After normalization (PbD) or after the last duplication-removal pass (TDD):
1. Run the full test suite for the class. All tests — old and new — must pass.
2. Search for callers of any methods you modified or added. Verify callers compile and behave correctly with the new behavior.
3. Delete any scaffolding: temporary subclasses used only during PbD, helper stubs added to make tests compile.
## Inputs
| Input | Required | Description |
|-------|----------|-------------|
| Target class with tests | Yes | The class to extend — must have a passing test baseline |
| Feature specification | Yes | What the new behavior should do, including known edge cases |
| Language + OO capability | Yes | Determines whether PbD is available |
| Integration intent | No | Whether the new behavior is shared or specialized — informs normalization direction |
## Outputs
| Output | Description |
|--------|-------------|
| Feature code | New methods or overrides implementing the specified behavior |
| Passing tests | New test cases covering the feature (base case, edge cases, error cases) |
| Normalized hierarchy | For PbD: inheritance resolved to push-up or push-down form with no LSP violations |
| `chosen-approach.md` | Brief record of why TDD or PbD was selected and how normalization was resolved (create in the working directory or PR description) |
## Key Principles
**TDD is the default — PbD only when integration decision is deferred.**
TDD adds the feature in place with full test support. PbD defers the integration decision to after the feature is proven. Do not use PbD because inheritance "seems easier" — it is only appropriate when you genuinely cannot or should not modify the existing class directly.
**The duplication-removal step is a first-class step, not a suggestion.**
Every TDD cycle must complete through step 4. Leaving duplication behind — two methods with the same loop, two classes with the same logic — creates a compound maintenance burden. The tests make removal safe. Do it now, not "later."
**PbD's test subclass is scaffolding — normalize the inheritance after the feature works.**
A subclass that overrides a concrete method is a temporary construction. Once you have a passing test, decide where the behavior really belongs (parent or subclass) and restructure. Do not ship a normalized-by-accident hierarchy.
**LSP violation = subclass changes method semantics in a way that breaks callers.**
Overriding a concrete method silently redirects callers who are unaware of the subclass. If a caller typed as `Parent p = new ChildInstance()` would receive different behavior from `Parent p = new ParentInstance()` in a way they cannot predict or handle, you have an LSP violation. Detect it during normalization; fix it before shipping.
## Examples
### Example 1: TDD on InstrumentCalculator
**Task:** Add `firstMomentAbout(double point)` to a class with no methods yet.
Iteration 1 writes `testFirstMoment()` asserting −0.5 for inputs [1.0, 2.0] about point 2.0. The stub returns `NaN` (compiles, fails). The implementation adds the numerator loop and returns `numerator / elements.size()`. No duplication yet — proceed.
Iteration 2 writes `testFirstMoment_noElements()` expecting `InvalidBasisException`. The stub adds the declaration but no guard — the test throws `ArithmeticException` (wrong exception, still fails). Adding the guard `if (elements.size() == 0) throw new InvalidBasisException(...)` makes it pass.
When `secondMomentAbout` is needed, copy the loop, change the accumulator to `Math.pow(element - point, 2.0)`, get the test passing, then extract `nthMomentAbout(double point, double n)` as a shared private. Both public methods delegate to it. Duplication removed.
### Example 2: PbD on a Tested PaymentProcessor
**Task:** Add `TrialUser` behavior — 50% discount on everything — without modifying `PaymentProcessor` directly (it is owned by another team and used by 40 callers).
1. Subclass: `TrialPaymentProcessor extends PaymentProcessor`. Override `calculateCharge(double basePrice)` to return `basePrice * 0.5`.
2. Write tests against `TrialPaymentProcessor`. Tests pass.
3. Normalize: Does the discount belong in the parent? No — it is specific to trial users. Push DOWN: make `calculateCharge` abstract in `PaymentProcessor`. Create `StandardPaymentProcessor` for the existing logic. `TrialPaymentProcessor` provides the discounted implementation.
4. Check LSP: callers using `PaymentProcessor` references receive either standard or trial logic — both return a valid charge amount of the same type. No semantic breach. No violation.
### Example 3: LSP Violation Detected and Restructured
**Task:** Add `PremiumEmployee` as a PbD subclass of `Employee`. Override `pay()` to return a struct with base + bonus, whereas `Employee.pay()` returns a plain salary `double`.
This violates LSP: callers typed as `Employee e = new PremiumEmployee()` would receive a struct where they expected a double, breaking code that assigns the result to a `double` variable.
**Fix:** Do not override `pay()`. Instead, add a new `payWithBonus()` method on `PremiumEmployee`. Or restructure `Employee.pay()` to return a `PayResult` object in all cases, with a `bonus` field that is 0.0 for standard employees. Either approach preserves substitutability.
## References
- `references/legacy-code-fundamentals.md` — core definitions: seams, normalized hierarchy, LSP
## License
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) — BookForge Skills, derived from "Working Effectively with Legacy Code" by Michael C. Feathers. Skill interpretation, structure, and synthesis are original BookForge content.
## Related BookForge Skills
**Direct dependencies (run before this skill):**
- `legacy-code-change-algorithm` — this skill executes steps 4–5 of that algorithm (write tests, then make the change)
- `safe-legacy-editing-discipline` — guides careful editing once tests are in place
- `characterization-test-writing` — required prerequisite if the class is not yet under test
**Invoke instead if class is not testable:**
- `legacy-code-addition-techniques` — Sprout Method, Sprout Class, Wrap Method for code that cannot be brought under test
**Cross-reference:**
- `unit-test-quality-checker` — validates that the new tests written in this skill meet quality thresholds
don't have the plugin yet? install it then click "run inline in claude" again.
add a feature to a class that already has passing tests (or can be brought under test quickly) using either test-driven development or programming by difference. use this skill when you need to extend tested code safely, letting tests drive design. choose TDD by default: write one failing test at a time, implement just enough to pass, remove duplication, and repeat. choose programming by difference when you cannot modify the existing class directly or when you need to defer the integration decision until the feature is proven. both approaches preserve the safety net of existing tests while adding new behavior.
| input | required | type | description | setup |
|---|---|---|---|---|
| target class with passing tests | yes | codebase artifact | the class you plan to extend, must have a working test suite that passes before you start | run characterization-test-writing first if no tests exist; confirm all tests pass via bash before proceeding |
| feature specification | yes | document or tickets | what the new behavior should do, including edge cases, error conditions, and boundaries you can identify now | gather from requirements, acceptance criteria, or ticket description; list preconditions and postconditions |
| language and OO capability | yes | environment context | the programming language and whether it supports single-class inheritance (required only for programming by difference) | TDD works in any language; PbD requires OO inheritance; document language choice in working notes |
| integration intent (optional) | no | context | whether the new behavior is shared across the hierarchy or specialized to one variant; informs normalization direction for PbD | clarify early: "is this feature for everyone or just this user type?" |
| test framework | yes | external tool | the testing framework already in use (JUnit, pytest, RSpec, Jest, etc.) | confirm framework is installed and tests run via single command (e.g., bash -c "gradle test" or bash -c "npm test") |
| IDE or text editor | yes | tool | code authoring environment | any editor that can open, edit, and save source files; bash access for running tests |
1a. input: target class file path, name of the test class.
1b. execute: run the full test suite for the target class. use the test command appropriate to your framework (gradle test, pytest, npm test, etc.).
1c. output: test run report showing all existing tests passing (0 failures, 0 errors).
1d. gate: if any test fails, do not proceed. diagnose and fix the failure first. a failing baseline test is a signal that something is unstable. a passing baseline is the safety net you depend on.
1e. decision point: if no tests exist for this class, stop here and run characterization-test-writing to establish a baseline. return to this step once you have passing tests.
2a. input: feature specification, class ownership, language constraints.
2b. decision logic:
use TDD (default) if:
use programming by difference if:
2c. output: decision record. write one sentence in your working notes or PR description: "chosen: TDD because
2d. default: if uncertain, choose TDD. PbD adds a normalization step; TDD is simpler.
repeat this cycle for each atomic slice of the feature (one test case per iteration):
3a-i: write a failing test case.
3a-ii: get the code to compile.
3a-iii: make the test pass.
3a-iv: remove duplication.
3a-v: repeat for the next test case.
edge cases to cover during iteration:
3b-i. input: the target class (untouched), feature specification.
3b-ii. action: create a new subclass of the target class. the subclass's sole purpose is to introduce the new feature via method override(s). do not modify the parent class.
example:
// existing parent - do not touch
public class MessageForwarder {
protected InternetAddress getFromAddress(Message message) { ... }
}
// new subclass
public class AnonymousMessageForwarder extends MessageForwarder {
@Override
protected InternetAddress getFromAddress(Message message) {
return new InternetAddress("anon@" + listAddress);
}
}
3b-iii. output: subclass file created; parent class unchanged.
3b-iv. now run the TDD cycle (step 3a) against the subclass, not the parent. write tests for the subclass, implement overrides in the subclass. the parent's tests continue to pass independently.
once all subclass tests are passing, decide where the feature really belongs:
4a. push behavior up (if shared):
example:
// parent refactored: everyone uses anonymity setting
public class MessageForwarder {
protected InternetAddress getFromAddress(Message message) {
if (configuration.isAnonymous()) {
return new InternetAddress("anon@" + domain);
}
return new InternetAddress(message.getFrom()[0].toString());
}
}
// temporary subclass is now unnecessary - delete it
4b. push behavior down (if specialized):
example:
// parent: abstract
public abstract class MessageForwarder {
protected abstract InternetAddress getFromAddress(Message message);
}
// standard variant
public class StandardMessageForwarder extends MessageForwarder {
protected InternetAddress getFromAddress(Message message) {
return new InternetAddress(message.getFrom()[0].toString());
}
}
// anonymous variant
public class AnonymousMessageForwarder extends MessageForwarder {
protected InternetAddress getFromAddress(Message message) {
return new InternetAddress("anon@" + domain);
}
}
4c. normalized hierarchy definition: a hierarchy is normalized when no class has a method that overrides a concrete method inherited from its parent. every method is either implemented once or delegated to all subclasses via an abstract contract. in normalized code, "how does this class do X?" has a single, unambiguous answer.
4d. output: inheritance restructured; parent and all subclasses have updated method signatures; all tests (new and old) pass.
5a. liskov substitution principle (LSP) violation definition: a subclass violates LSP if a caller typed as the parent class receives different semantics when given a subclass instance, in a way the caller cannot predict or handle.
5b. red flags during normalization:
Parent p = new ChildInstance() would behave incorrectly if the method semantics changed.5c. classic violation: Rectangle and Square. Rectangle.setWidth(3); setHeight(4) gives area 12. Square.setWidth(3); setHeight(4) gives area 16 (Square maintains squareness). callers expect 12 and get 16. LSP violated.
5d. rules to avoid violations:
super.method() inside the override wherever possible.5e. action: if you detect an LSP violation during normalization, do not suppress it. restructure: use composition, make the parent abstract, or move the feature out of inheritance entirely. fix it before shipping.
example violation fix:
// violation: override changes return type semantics
public class PremiumEmployee extends Employee {
@Override
public double pay() {
return payWithBonus(); // returns struct, not double - violation
}
}
// fix option 1: composition
public class PremiumEmployee {
private Employee delegate;
public double pay() { return delegate.pay(); }
public PayResult payWithBonus() { /* return struct */ }
}
// fix option 2: abstract parent
public abstract class Employee {
public abstract PayResult pay();
}
public class StandardEmployee extends Employee {
public PayResult pay() { return new PayResult(salary, 0); }
}
public class PremiumEmployee extends Employee {
public PayResult pay() { return new PayResult(salary, bonus); }
}
5f. output: LSP verified; no violations remain. or violations fixed and integration restructured.
6a. input: completed feature (all tests passing for TDD; normalized hierarchy for PbD).
6b. action: run the full test suite for the target class and all related classes. all tests (old and new) must pass.
6c. search for callers: grep or search the codebase for all callers of methods you added or modified. review each call site. verify that callers compile and that the new behavior is compatible with how the method is called.
6d. delete scaffolding: if you used PbD, remove any temporary subclasses that were used only during development. if you created temporary helper methods during TDD and later removed them, confirm they are gone.
6e. output: full test suite passing, all callers verified, code ready for review and merge.
decision 1: class has no tests?
characterization-test-writing to establish a safety baseline. return to step 1 once baseline is passing.decision 2: use TDD or programming by difference?
decision 3 (TDD): is the test failing?
decision 4 (PbD): is there duplication between the subclass override and parent implementation?
decision 5 (PbD normalization): does the feature belong in the parent or in the subclass?
decision 6 (LSP check): does the subclass override change method semantics in a way that breaks callers?
decision 7 (edge cases): have you covered boundary conditions, error cases, and alternative inputs?
decision 8 (rate limits, timeouts, network failures): does the feature depend on external services or resources?
decision 9 (auth expiry, token refresh): if the feature uses API keys or authentication tokens, do they expire?
decision 10 (empty result sets): does the feature handle empty inputs, null returns, or zero-length collections?
| output | data format | location | success criteria |
|---|---|---|---|
| feature code (new methods or overrides) | source code in target language | target class file (TDD) or subclass file (PbD) | methods compile, signatures match test expectations, no syntax errors |
| passing tests | test code in target test framework | test file (same suite as target class) | all test methods execute, assertions pass, 0 failures, 0 errors |
| passing baseline + new tests | full test suite run | build output (console, CI log, test report) | all old tests still pass (regression check); all new tests pass (feature check) |
| normalized hierarchy (PbD only) | restructured class hierarchy | source code directory | no class overrides a concrete method inherited from parent; no LSP violations; abstract contracts are explicit |
| caller verification | grep/search results or code review notes | working directory or PR description | all call sites of modified/added methods reviewed; all callers compile and behave correctly; integration is sound |
| chosen-approach.md | markdown file | working directory or PR description | one sentence per approach explaining why TDD or PbD was chosen; for PbD, one sentence explaining normalization direction (push up or push down) and LSP decision |
the skill worked if: