Install
openclaw skills install bookforge-change-effect-analysisTrace the blast radius of a legacy code change and produce a test placement plan with pinch points. Use whenever a developer needs to decide WHERE to write tests for a pending change — 'what should I test?', 'where to test?', 'how far do the effects propagate?', 'what else could this break?', 'how to find test points', 'pinch point', 'effect sketch', 'impact analysis', 'blast radius', 'interception point', 'high-leverage test'. Triggers for 'I need to make many changes', 'many classes affected', 'cluster of related changes', 'cross-class refactoring'.
openclaw skills install bookforge-change-effect-analysisYou are about to change a method or class in a legacy codebase and you need to decide where to write tests. Specifically:
legacy-code-change-algorithm and need a deeper procedureThis skill does not determine whether the change is safe to make. It determines where to place the tests that will make it safe.
Before executing this skill, confirm:
Change point: Which method(s) or class(es) will change? -> Check prompt for: class names, method names, file paths -> If still missing, ask: "Which specific method or class needs to change?"
Change description: What is being modified and why? -> If still missing, ask: "What should the code do differently after the change?"
Language and codebase access: What language? Can the files be read? -> Check environment for: file extensions, project structure -> If unknown: proceed — the analysis is language-agnostic, though language-specific firewalls affect where you stop
SUFFICIENT: Change point named + read access to files + change description available
PROCEED WITH DEFAULTS: Change point named + rough file area identified
MUST ASK: No change point at all — this analysis cannot begin without knowing what changes
ACTION: Locate the exact method(s) or variable(s) being modified. Record each as a change point entry.
WHY: Every downstream effect traces from a specific origin. Vague change points ("we're changing the billing area") produce incomplete effect sketches. Precise change points ("we're modifying Invoice.calculateShipping()") let you trace outward systematically.
HOW:
ARTIFACT: Start effect-sketch.md:
## Change Points
- [ClassName.methodName] — what changes: [brief description]
- [ClassName.fieldName] — what changes: [brief description]
ACTION: For each change point that is a method, determine if it has a return value. If yes, trace who uses that return value.
WHY: Return values are the most visible propagation mechanism. When a method's return value changes, every caller that uses that value is affected — and each of those callers may propagate the effect further through their own return values.
HOW:
effect-sketch.md: changeMethod → callerA → callerBSTOP condition: If the same method appears twice in your trace chain, you have a cycle. Mark it and do not recurse further.
ARTIFACT: Add to effect-sketch.md:
## Mechanism 1: Return Value Chain
changeMethod() → [callerA(), callerB()] → [callerA: used in callerX()] → ...
ACTION: For each change point method, check whether it modifies state on any object it receives as a parameter.
WHY: Parameter mutation is sneaky because it does not appear in the method's return type. A method that accepts a List, an Order, or any mutable object and modifies its state will cause effects to propagate silently back through the caller. You cannot detect this from the signature alone — you must read the body.
HOW:
order.setStatus(...), list.add(...), buf.append(...)).effect-sketch.md.Language note: In Java and C#, object parameters are passed by reference — the handle can be used to mutate the object. Primitive parameters (int, double) cannot be mutated. In C++, check whether parameters use const — but also check if the type uses mutable internally.
ARTIFACT: Add to effect-sketch.md:
## Mechanism 2: Parameter Mutation
changeMethod(order) — mutates order.status → callers that use order after call: [callerA, callerB]
ACTION: Check whether any method in your change points reads or writes global variables, static fields, or singleton state.
WHY: Global and static data is the sneakiest propagation mechanism. It does not appear in method signatures at all. A change point that writes to a global silently affects every method that reads that global anywhere in the codebase — including methods in completely unrelated classes. Skipping this step is how developers introduce regressions they cannot explain.
HOW:
ClassName.fieldName), singleton calls (.getInstance()), global variables, or calls that clearly write to shared state (e.g., View.getCurrentDisplay().addText(...)).ARTIFACT: Add to effect-sketch.md:
## Mechanism 3: Global/Static Data
changeMethod() writes: [GlobalClass.sharedField]
→ readers found: [ClassA.methodX(), ClassB.methodY()]
— OR —
No global/static effects detected.
ACTION: If the change point is on an instance method or field, check whether superclasses or subclasses access the same data.
WHY: In OO code, subclasses can override or directly access instance variables from a parent. If a field is protected or package-scoped rather than private, subclasses (and classes in the same package) may read or write it. Forgetting subclasses leads to incomplete effect sketches that miss real propagation paths.
HOW:
ARTIFACT: Add to effect-sketch.md:
## Superclass/Subclass Check
[ClassName.field] visibility: [private/protected/public/package]
Subclasses found: [SubA, SubB]
SubA accesses field in: [SubA.someMethod()] → adds to effect path
ACTION: Consolidate all paths discovered in Steps 2–5 into a single text diagram. Each node is a method or variable. Each arrow represents "can be affected by."
WHY: The effect sketch is not documentation — it is a thinking tool. Seeing all paths in one place reveals convergences (potential pinch points), dead ends (method results that are discarded), and the true scope of the change. Without the sketch, the analysis lives only in your head and is easy to compress incorrectly.
Format: Use indented text or ASCII arrows. There is no required notation — clarity of comprehension is the only standard.
Example sketch for a change to generateIndex():
generateIndex()
└─ writes: elements (collection)
├─ read by: getElementCount() → return value used by callers
└─ read by: getElement(name) → return value used by callers
addElement()
└─ writes: elements (collection)
├─ (same paths as above)
Both getElementCount() and getElement(name) are interception points — places where a test can detect changes.
ARTIFACT: Finalize effect-sketch.md with the consolidated diagram.
ACTION: If the effect sketch covers multiple classes, look for a narrowing — a single method or small set of methods through which all (or most) effect paths pass. That narrowing is a pinch point.
WHY: When three or four classes need coordinated changes and breaking each class's dependencies individually would take hours, a pinch point gives you test coverage over all of them through a single, already-reachable entry point. Pinch point tests are temporary scaffolding, not the goal — but they let you start changing safely today rather than waiting until all dependencies are broken.
HOW:
Pinch Point Trap Warning: Pinch point tests cover a wide area but they are high-level tests. If left in place permanently, they grow into slow mini-integration tests that test cluster behavior rather than individual class behavior. Mark pinch point tests explicitly in the test file (e.g., // PINCH POINT — delete when unit tests cover OrderBuilder, OrderValidator, OrderPricer).
Exit criterion: When every class in the cluster has its own unit tests, delete the pinch point test. It has done its job.
ARTIFACT: Add to effect-sketch.md:
## Pinch Point Analysis
Candidate: [ClassName.methodName]
Effects covered: [changePoint1, changePoint2, changePoint3] ✓
Effects missed: [changePoint4] — needs separate test
Verdict: PINCH POINT (covers 3 of 4 change paths)
ACTION: Using the effect sketch and pinch point analysis, produce an ordered plan: which tests to write first, at which methods, in which order.
WHY: The test placement plan turns the analysis into action. Without it, the effect sketch is only a map — the plan is the route. The plan answers: "Given what I just traced, exactly which methods should I call in my test harness?"
HOW:
ARTIFACT: Write test-placement-plan.md:
# Test Placement Plan
## Phase 1: Pinch Point Coverage (immediate)
- Test: [ClassName.methodName]
Covers change points: [list]
Assertion: [what fails on regression]
Status: TEMPORARY — delete when Phase 2 tests are in place
## Phase 2: Unit Tests (as dependencies are broken)
- Test: [ClassName.methodName]
Covers: [specific change point]
Dependency to break first: [technique]
## Uncovered Effects
- [anything the plan doesn't cover, and why it's acceptable or not]
| Input | Required | Description |
|---|---|---|
| Change point(s) | Yes | Specific method(s) or class(es) being changed |
| Change description | Yes | What the code should do differently |
| Source files | Yes | Readable codebase for tracing callers, globals, subclasses |
| Language | Recommended | Affects visibility rules and parameter passing semantics |
| Output | Description |
|---|---|
effect-sketch.md | Text diagram of all effect paths from change points to detectable endpoints |
test-placement-plan.md | Ordered plan: which methods to test, which tests are pinch points, which are unit tests |
Three mechanisms cover all effect paths — don't skip globals. Return values are the most visible. Parameter mutation is quieter. Globals are invisible from signatures. A thorough analysis executes all three traces, even when the code looks clean. Globals are where the surprises live.
Effect sketches are rough drawings, not UML. The goal is to see which things affect which other things. Formal notation adds no value here — a bulleted indentation tree is sufficient. The point is comprehension for test placement decisions, not documentation for future readers.
Pinch points are temporary scaffolding. A pinch point test is not a goal — it is a bridge. It lets you cover a cluster of classes with one test while you gradually break individual class dependencies. When unit tests cover the cluster, delete the pinch point test. Leaving it in place permanently creates a slow mini-integration test that duplicates work and masks the individual class behavior.
A pinch point that doesn't narrow the sketch isn't a pinch point. A method that sits at the top of the call chain is only a pinch point if testing it actually detects effects from the specific changes you are making. A broad "God method" that touches everything is not a pinch point — it's just a large test surface. Evaluate convergence relative to your change points.
Setup: CppClass.getInterface() needs to add a language-qualifier prefix to all return values. CppClass has three methods: getInterface(), getDeclaration(), getDeclarationCount().
Trace:
getInterface() returns a String. Grep callers. → One caller: Parser.generateOutput(), which formats the interface into a file. generateOutput() returns a String used by FileWriter.write().getInterface() does not mutate any parameter.getInterface().declarations field is private. No subclass access.Effect sketch:
getInterface() → return String
→ Parser.generateOutput() → return String
→ FileWriter.write() → side effect (file output)
Test placement plan:
getInterface() directly (closest to change point, directly callable).Setup: Three classes — OrderBuilder, OrderValidator, OrderPricer — all need changes for a new discount feature. Each takes 2 hours to break dependencies individually. All three are consumed by BillingStatement.makeStatement(), which produces the final billing output.
Trace:
OrderBuilder.build() writes a discount field → OrderValidator.validate() reads it → OrderPricer.price() computes discounted total → both feed into BillingStatement.makeStatement().makeStatement().Pinch point: BillingStatement.makeStatement() — one test covers all three change paths.
Test placement plan:
makeStatement() with a known Invoice. Mark as TEMPORARY.OrderBuilder dependency first, then OrderValidator, then OrderPricer), add narrower unit tests per class.makeStatement() pinch point test.Setup: Element.addText() is being changed to log to a different display system. The current implementation contains View.getCurrentDisplay().addText(newText) — a global reference. The new change will swap getCurrentDisplay() for Logger.getStream().
Trace:
View.getCurrentDisplay. Found in 4 additional places: HeaderRenderer, FooterRenderer, SummaryBuilder, AuditLogger.addText() does not directly break them — but confirms the global is shared across unrelated classes.Effect sketch:
Element.addText() → writes: View.getCurrentDisplay()
→ readers of same global: [HeaderRenderer, FooterRenderer, SummaryBuilder, AuditLogger]
→ these are NOT affected by the addText change (they call getCurrentDisplay() independently)
→ confirmed: no transitive effect through the global
Test placement plan:
Element.addText() directly. Verify logging target changes.View.getCurrentDisplay() itself is being changed, re-run this analysis with getCurrentDisplay() as the new change point.The full 6-step effect-analysis heuristic from Chapter 11 is reproduced verbatim in the process steps above. No supplementary reference file is required — the heuristic is self-contained within this skill.
For the broader algorithm that calls this skill as Step 2, see legacy-code-change-algorithm.
This skill is licensed under CC-BY-SA-4.0. Source: BookForge — Working Effectively with Legacy Code by Michael C. Feathers (2004, Prentice Hall).
legacy-code-change-algorithm — This skill is Step 2 (Find Test Points) of the 5-step algorithm. Always execute the parent algorithm first to classify the change and identify change points.characterization-test-writing — Downstream: once the test placement plan is written, use this skill to write the actual characterization tests at the interception points identified here.big-class-responsibility-extraction — Cross-reference: when effect sketch analysis reveals that a large class has many unrelated effect paths, it is a signal that the class has too many responsibilities. Effect sketches can reveal hidden class boundaries — clusters of methods that only affect each other form natural candidates for extraction.seam-type-selector — When a test placement plan identifies a dependency that blocks test harness construction, use the seam selector to choose the right seam type for that dependency.Install the full book skill set from GitHub: bookforge-skills — working-effectively-with-legacy-code