1. Summary
Refactoring changes how code is organized without changing what it does. The risk is regression -
introducing behavior changes while moving code. Left unconstrained, Claude tends to combine
refactoring with "improvements": renaming things, adding error handling, pulling in abstractions.
These are not refactors. A refactor.md file holds the line: it states the transformation to apply,
the scope, and the invariants that must hold throughout.
2. refactor.md Structure
# Refactor Context
## Goal
One sentence: what structural change to make.
## Scope
Files in scope: [list]
Files not in scope: [list - be explicit]
## Invariants
These behaviors must not change:
- [public API signatures stay identical]
- [error handling behavior unchanged]
- [specific behavioral invariants for this codebase]
## Constraints
- Do not rename anything not directly involved in the refactor
- Do not add new dependencies
- Do not add new features or error handling
- Do not reformat code unrelated to the change
## Done When
[Specific checkable condition - e.g., "all tests pass", "file X imports from Y instead of Z"]
3. Safe Refactoring Sequence
Refactoring fails when changes batch together, making it impossible to identify which change
introduced a regression. The sequence below keeps each change small and verifiable:
- List first. Ask Claude to enumerate every change it will make before making any.
Review the list against the invariants in refactor.md.
- One logical unit at a time. Apply one item from the list, stop, verify.
Do not approve the next item until the current one is confirmed correct.
- Update references immediately. If a change moves or renames something,
update all references to it before touching anything else.
- Check the done condition. Verify it explicitly against the condition in
refactor.md, not against a general sense that things look right.
4. Example Prompts
Opening the session:
Read refactor.md.
Before making any changes, list every file and every specific change you will
make. State which invariant from refactor.md each change preserves. Stop after
the list so I can review it.
After approving the list, apply changes one item at a time:
Apply change 1 only - stop after that and show me what changed.
Keeping Claude from expanding scope:
Read refactor.md.
You will see some functions in [file] that could be simplified. Do not simplify
them. This session is only for the structural change stated in refactor.md.
Apply change 2 now.
Extracting a function:
Read refactor.md.
Read [source_file].
Extract the block starting at line [N] through line [M] into a function named
[name]. The function signature must match refactor.md. Do not change any other
code in the file. Show the diff only.
Moving a module:
Read refactor.md.
Move [module] from [old_path] to [new_path]. Then update every import and
reference to it across the codebase. Show the list of files you will update
before making any changes.
5. Case Study: CppNoSqlDB Refactoring Survey
This refactoring survey was run on CppNoSqlDB before any structural changes were made.
The goal was to identify specific structural problems - mixed responsibilities, duplicated
patterns, unsafe preconditions - and get concrete proposals for each, without touching
the code. The Mode: read-only instruction in refactor.md is the constraint
that keeps Claude from writing changes before the suggestions have been reviewed.
refactor.md
# Refactor Context
## Target
CppNoSqlDB -- C++ NoSql key-value database:
DbCore/DbCore.h core database and element types
Query/Query.h query conditions and execution
Persist/Persist.h XML persistence and sharding
## Mode
Read-only analysis. Do not change any files.
For each suggestion: describe the current structure, propose the change,
and state what structural property it improves.
## Questions
1. DbCore.h contains ~90 lines of display functions alongside database logic.
What would a clean separation look like?
2. containsChildKey() in DbCore.h and containsKey() in Persist.h both use
the same iterator/std::find pattern. How should this be unified?
3. Persist<P> mixes shard-selection logic with XML serialization.
What would those two responsibilities look like as separate classes?
4. Conditions<P> uses a raw pointer for element access with scattered match
methods. What structural change would improve this?
## Constraints
Analysis only. Do not change any files.
Cite file:line for each current-code observation.
Format each suggestion as: current structure | proposed change | benefit.
Prompt
Read refactor.md.
Read DbCore/DbCore.h.
Read Query/Query.h.
Read Persist/Persist.h.
Answer each question in refactor.md. Cite file:line for every observation.
Do not change any files.
Suggestions
Q1 -- Display functions in DbCore.h
Current: showKeys(), showHeader(), showElem(), showRecord(), and showDb()
are defined inside DbCore.h (lines 308-399), directly after the DbCore
class body. DbCore.h includes <iomanip> solely to support them (DbCore.h:44).
Proposed: move the five functions to a new Display.h header that includes
DbCore.h. The functions remain template functions in the NoSqlDb namespace;
no signatures need to change. Callers that need display include Display.h;
callers that only need the database include DbCore.h.
Benefit: DbCore.h becomes the database contract; Display.h becomes the
presentation layer. Tests can verify database logic without pulling in
display dependencies. Adding an alternate output format means a new header,
not changes to the database header.
Q2 -- Duplicated iterator/find pattern
Current: containsChildKey() (DbCore.h:99-104) and containsKey() in Persist.h
(Persist.h:94-100) both do the same thing:
Keys::iterator start = vec.begin();
Keys::iterator end = vec.end();
return std::find(start, end, key) != end;
removeChildKey() (DbCore.h:122-133) extends the same pattern to erase.
Proposed: replace the three-line iterator construction with the one-liner
already available from <algorithm>:
return std::find(vec.cbegin(), vec.cend(), key) != vec.cend();
If the pattern recurs in more than two places, add a
containsKey(const Keys&, const Key&) free function to Definitions.h
and call it from both sites.
Benefit: eliminates repeated boilerplate, makes the intent immediately
visible, and centralizes any future change (e.g., switching from
vector to set for O(log n) lookup).
Q3 -- Persist<P> mixed responsibilities
Current: Persist<P> (Persist.h:51-68) owns two unrelated concerns:
shard state: shardKeys_, shard(), addShardKey(), removeShard(),
containsKey() (Persist.h:58-100)
XML serialization: toXmlRecord(), toXml(), fromXml() (Persist.h:61-62,
116-239)
The fromXml() augment parameter (Persist.h:62) changes behavior
fundamentally -- false clears the database before loading.
Proposed: split into two classes:
ShardFilter<P> owns shardKeys_ and a shouldPersist(Key) predicate
XmlStore<P> owns toXml(ShardFilter<P>&) and fromXml(bool augment)
Persist<P> can remain as a thin facade composing both if callers should
not see the split.
Benefit: shard policy becomes independently testable and reusable with any
future serialization format. The augment semantics are easier to follow when
database-clearing lives next to load logic, not alongside shard methods.
Adding JSON persistence means a new class, not modifying Persist.
Q4 -- Conditions<P> raw pointer and scattered match methods
Current: Conditions<P> (Query.h:50-94) stores pDbElem_ as a raw pointer
(Query.h:84) set through a value() setter (Query.h:57). The caller must
call value() before any match*() call; calling match() without doing so
is undefined behavior. Five independent match methods all live on the
same class with no grouping by concern.
Proposed: pass the element by const reference directly into match():
bool match(const DbElement<P>& elem) const;
This eliminates the pointer, the setter, and the ordering requirement.
The five match* helpers remain private; match() becomes safe to call
at any time with no precondition.
Benefit: the interface is const-correct and has no precondition.
The raw pointer and the lifetime concern it introduces are removed.
The class becomes usable with const DbElement references.
6. Common Pitfalls
| Pitfall | Prevention |
| Claude adds features during refactor |
State explicitly in refactor.md and in the prompt: "do not add features" |
| Claude renames things "for clarity" |
State: "do not rename anything not directly involved in the refactor" |
| Claude changes error handling |
Name error-handling invariants explicitly in refactor.md |
| References updated partially |
Ask for the full list of references before any move, then check them after |
| Multiple changes batched together |
Enforce "one change, stop, confirm" in every prompt |