AI Workflows: Refactoring

changing structure without changing behavior

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:
  1. List first. Ask Claude to enumerate every change it will make before making any. Review the list against the invariants in refactor.md.
  2. 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.
  3. Update references immediately. If a change moves or renames something, update all references to it before touching anything else.
  4. 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

PitfallPrevention
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