Rh-acm-switchover refactor-simplify
Safely simplify a Python module by extracting long methods, consolidating boilerplate, and applying existing patterns. Usage - /refactor-simplify modules/finalization.py or /refactor-simplify --review modules/finalization.py
git clone https://github.com/tomazb/rh-acm-switchover
T=$(mktemp -d) && git clone --depth=1 https://github.com/tomazb/rh-acm-switchover "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/refactor-simplify" ~/.claude/skills/tomazb-rh-acm-switchover-refactor-simplify && rm -rf "$T"
.claude/skills/refactor-simplify/SKILL.mdSafe Code Simplification
Simplify a Python module without changing its behavior. This skill applies existing project patterns consistently and breaks long methods into smaller, testable pieces.
Philosophy
1. Functionality Is Sacred
Never change behavior. Inputs, outputs, side effects, error handling, log messages, state transitions, and edge cases must remain identical. If you cannot prove preservation, do not make the change.
2. Readability Over Brevity
Fewer lines is not the goal. Clearer lines are. A longer
if block that reads like
prose is better than a dense one-liner that needs mental parsing. Method extraction
should make the orchestrator read like a table of contents.
3. Respect The Codebase
Before touching code, read
CLAUDE.md (or AGENTS.md), setup.cfg, and the
surrounding code in the same module. The local conventions override your preferences
and the patterns in this skill. If the project uses logger.info("Starting %s...", name),
keep that style — do not switch to f-strings.
4. Scope Discipline
Touch only the file the user asked about and code needed to support the simplification. Do not broaden the diff with unrelated cleanup. If you spot an issue in another file, mention it in the summary — do not fix it.
Arguments
The user provides a file path relative to the repository root (e.g.,
modules/finalization.py).
Optional flag:
--review for Review-Only mode (analysis without edits).
If no argument is provided, ask the user which file to simplify. Suggest the highest-impact candidates:
(1,624 lines —modules/finalization.py
is 178 lines)_fix_backup_schedule_collision
(1,259 lines —modules/post_activation.py
is 120 lines)_load_kubeconfig_data
(956 lines —modules/activation.py
is 102 lines)_verify_patch_applied
(1,082 lines —acm_switchover.py
is 231 lines)parse_args
(695 lines — 3modules/preflight/backup_validators.py
methods >100 lines)run()
Operating Modes
Review-Only Mode
Use when the user passes
--review or asks for critique, assessment, or audit
without requesting direct edits.
Produce a structured report of simplification opportunities with:
- priority ranking (high/medium/low)
- estimated line reduction per opportunity
- behavior-preservation risk assessment (safe / needs-care / risky)
- concrete rewrite sketches for the top 3 opportunities
- suggestions intentionally left unapplied and why
Do NOT modify any files in this mode.
Apply-Changes Mode (default)
Use when the user wants the code actually simplified. Make direct edits, verify incrementally, and summarize what changed.
Safety Rules
These rules are non-negotiable. Violating any one of them means the refactoring is rejected.
- No behavior changes — every code path, error, log message, and return value must remain identical
- No new files — do not create new utility modules, helpers, or abstractions
- No new dependencies — do not add imports from outside the existing project
- No signature changes on public methods — only add new private methods (prefixed with
)_ - Preserve all logging — every
call must produce the same outputlogger.info/warning/error - Preserve all state tracking — every
/is_step_completed
/mark_step_completed
call staysstate.step() - Preserve exception types — if a method raises
, the refactored version raises the sameSwitchoverError - Tests must pass — run
before and after; zero regressions allowed./run_tests.sh
When Not To Simplify
Hold back when:
- The code sits on a retry or timeout hot path and the current shape may be intentional for error-recovery semantics (e.g., nested try/except with different fallback strategies per exception type)
- A check looks redundant but appears to protect against a known edge case, Kubernetes API quirk, or ACM version difference — treat it as intentional until you can prove otherwise
- The user asked for a specific file and the cleanup would expand into other files
- The only available change is cosmetic and subjective (e.g., renaming a variable you personally dislike)
- A documented workaround or compatibility shim exists (look for comments containing
,workaround
,compat
,legacy
, or version-gatedTODO
blocks)if - You cannot verify the change with existing tests — if there are no tests covering the code path, flag it as a suggestion instead of applying it
Procedure
Phase 0: Read Project Context
Before analyzing the target file, read these files to understand project conventions:
(orAGENTS.md
) — project-level AI instructions, patterns, constants usageCLAUDE.md
— formatter, linter, and test configurationsetup.cfg
— verify any magic strings used in the target filelib/constants.py
— understandlib/waiter.py
signature for Transform 3wait_for_condition()
This ensures your simplifications align with the codebase, not just generic best practices.
Phase 1: Analyze the Target File
Read the target file completely. Identify and categorize every simplification opportunity into one of these seven transforms (described below). List them as a checklist for the user, ranked by impact.
Present findings like this:
Found N simplification opportunities in <file>: EXTRACT METHOD (high impact): [ ] _fix_backup_schedule_collision (178 lines) → split into 3 methods [safe] [ ] _verify_new_backups (107 lines) → extract polling into helper [safe] CONSOLIDATE ERROR HANDLING (high impact): [ ] Lines 45-55, 120-130, 200-210 → identical try/except (3 instances) [safe] USE EXISTING WAITER (medium impact): [ ] Lines 447-479 → replace manual while-loop with wait_for_condition() [needs-care] CONVERT TO DRY-RUN DECORATOR (medium impact): [ ] Lines 88-92 → replace if self.dry_run with @dry_run_skip [safe] PYTHON IDIOM QUICK-WINS (low impact): [ ] Line 312 → len(lst) == 0 → not lst [safe] Estimated reduction: ~X lines Risk tags: [safe] = provably identical, [needs-care] = verify with tests, [risky] = suggest only
In Review-Only mode: stop here, present the analysis with concrete rewrite sketches for the top 3 opportunities, and list any suggestions you intentionally left unapplied. Do NOT modify files.
In Apply-Changes mode: wait for the user to confirm, then proceed to Phase 2.
Phase 2: Apply Transforms (One at a Time)
Apply each transform individually. After each transform, verify the file is syntactically valid by running:
python3 -c "import ast; ast.parse(open('<file>').read()); print('OK')"
Transform 1: Extract Long Methods
For any method longer than 80 lines, extract logical blocks into private helper methods.
Rules:
- New method names must start with
and clearly describe the extracted block_ - The new method must be on the same class (not a standalone function)
- Pass only the minimum required arguments — do not pass
attributes as arguments if the method is on the same classself - Preserve the exact same logging, error handling, and return values
- Keep the original method as a thin orchestrator that calls the extracted helpers
Pattern:
Before:
def _fix_backup_schedule_collision(self): # Block A: detect collision (40 lines) ... # Block B: resolve naming (60 lines) ... # Block C: apply fix (78 lines) ...
After:
def _fix_backup_schedule_collision(self): collision = self._detect_schedule_collision() if collision: resolved_name = self._resolve_schedule_naming(collision) self._apply_schedule_fix(resolved_name) def _detect_schedule_collision(self): # Block A (40 lines, unchanged) ... def _resolve_schedule_naming(self, collision): # Block B (60 lines, unchanged) ... def _apply_schedule_fix(self, resolved_name): # Block C (78 lines, unchanged) ...
Transform 2: Consolidate Identical Error Handling
When multiple methods share the exact same try/except pattern, extract it.
Pattern — Phase-level error handling:
Before (repeated 4+ times):
def some_phase(self) -> bool: logger.info("Starting X...") try: # ... phase logic ... logger.info("X completed successfully") return True except SwitchoverError as e: logger.error("X failed: %s", e) self.state.add_error(str(e), "phase_x") return False except Exception as e: logger.error("Unexpected error in X: %s", e) self.state.add_error(f"Unexpected: {str(e)}", "phase_x") return False
After:
def some_phase(self) -> bool: return self._run_phase("X", "phase_x", self._do_some_phase) def _do_some_phase(self): # ... phase logic (no try/except needed) ... def _run_phase(self, display_name, error_key, func) -> bool: """Standard phase execution with error handling.""" logger.info("Starting %s...", display_name) try: func() logger.info("%s completed successfully", display_name) return True except SwitchoverError as e: logger.error("%s failed: %s", display_name, e) self.state.add_error(str(e), error_key) return False except Exception as e: logger.error("Unexpected error in %s: %s", display_name, e) self.state.add_error(f"Unexpected: {str(e)}", error_key) return False
Only apply this transform when the try/except structure is truly identical. If there are differences in exception types caught, logging format, or control flow, leave them as-is.
Transform 3: Use Existing wait_for_condition()
wait_for_condition()The project has
lib/waiter.py with a wait_for_condition() utility. Replace manual polling loops that match this shape:
Before:
deadline = time.time() + timeout while time.time() < deadline: result = self._check_something() if result: return result time.sleep(interval) raise SwitchoverError("Timed out waiting for X")
After:
from lib.waiter import wait_for_condition return wait_for_condition( condition_fn=self._check_something, timeout=timeout, interval=interval, description="waiting for X", )
Only apply when the manual loop is a straightforward poll-until-true pattern. If the loop has complex retry logic, partial results, or side effects between iterations, leave it as-is.
Transform 4: Convert Manual Dry-Run Checks to @dry_run_skip
@dry_run_skipThe project has a
@dry_run_skip decorator in lib/utils.py. Replace manual checks:
Before:
def scale_deployment(self, name, namespace, replicas): if self.dry_run: logger.info("[DRY-RUN] Would scale deployment %s/%s to %d replicas", namespace, name, replicas) return {} # ... actual implementation ...
After:
@dry_run_skip(message="Would scale deployment {name} in {namespace} to {replicas} replicas", return_value={}) def scale_deployment(self, name, namespace, replicas): # ... actual implementation ...
Only apply when:
- The dry-run block is at the top of the method (guard clause pattern)
- The return value is a simple literal (
,{}
,None
,True
)[] - The log message can be expressed as a format string using the method's parameter names
Do NOT apply when:
- The dry-run check is in the middle of the method with conditional logic
- The dry-run path computes a mock return value
- The method already has
@dry_run_skip
Transform 5: Remove Dead Code
Remove any code that is provably unreachable:
- Methods that are never called (verify with grep across the entire codebase)
- Variables that are assigned but never read
- Import statements for unused symbols
- Commented-out code blocks (more than 3 consecutive commented lines)
Rules:
- Grep the entire project before removing a method:
grep -r "method_name" --include="*.py" - Do NOT remove methods that are part of a public API or could be called dynamically
- Do NOT remove
or# TODO
comments — those are intentional markers# FIXME
Transform 6: Simplify Conditional Logic
Reduce nesting depth where straightforward:
Early returns:
# Before def check(self): if condition_a: if condition_b: return do_thing() return None # After def check(self): if not condition_a: return None if not condition_b: return None return do_thing()
Guard clauses at method entry:
# Before def process(self, items): if items: for item in items: self._handle(item) # After def process(self, items): if not items: return for item in items: self._handle(item)
Only apply when it clearly improves readability. Do not refactor conditional logic that is intentionally structured for domain clarity.
Transform 7: Python Idiom Quick-Wins
Apply standard Python simplifications when they make the code clearer without changing behavior. These are low-risk, high-readability changes.
Boolean checks:
# Before # After if len(items) == 0: if not items: if len(items) > 0: if items: if x == True: if x: if x == False: if not x: if x is not None and len(x) > 0: if x: # only when falsy=empty is correct
Aggregation with
/ any()
:all()
# Before has_error = False for item in items: if item.status == 'error': has_error = True break # After has_error = any(item.status == 'error' for item in items)
Comprehensions for simple transforms:
# Before names = [] for cluster in clusters: if cluster.get("status") == "ready": names.append(cluster["name"]) # After names = [c["name"] for c in clusters if c.get("status") == "ready"]
for intentional ignoring:contextlib.suppress
# Before try: os.remove(tmp_file) except FileNotFoundError: pass # After from contextlib import suppress with suppress(FileNotFoundError): os.remove(tmp_file)
When NOT to apply idiom changes:
- Comprehensions with side effects, nested loops, or >80 chars — keep the loop
/any()
where the loop has early-exit side effects beyond the booleanall()- Boolean simplification where
vs empty-list distinction mattersNone - When the verbose form matches surrounding code style in the same method
Phase 3: Verify
After all transforms are applied, run verification in order of increasing scope:
Step 1 — Syntax check (already done per-transform):
python3 -c "import ast; ast.parse(open('<file>').read()); print('OK')"
Step 2 — Targeted test (run first, fast feedback):
source .venv/bin/activate # Find and run the matching test file pytest tests/test_<module_name>.py -v --tb=short
Step 3 — Full suite (only after targeted test passes):
./run_tests.sh
If any test fails:
- Revert the most recent transform
- Investigate — is the failure from the transform or a pre-existing flake?
- Re-apply the transform with the fix
- Re-run the targeted test first, then the full suite
If you cannot run verification (missing venv, broken deps), say so explicitly and list what the user should run manually.
Phase 4: Summarize
Present a summary tailored to the mode:
Apply-Changes mode:
Simplification complete for <file>: Before: N lines After: M lines (K lines removed, -X%) Transforms applied: ✓ Extracted 3 methods from _fix_backup_schedule_collision [safe] ✓ Consolidated 2 identical error handlers [safe] ✓ Replaced 1 manual poll loop with wait_for_condition() [needs-care] ✗ Skipped: no manual dry-run checks found ⚠ Deferred: _verify_backup_integrity has complex retry logic [risky — suggest only] Tests: All passing (N tests, targeted + full suite) Verification gaps: None (or: "no tests cover _poll_backup_completion")
Review-Only mode:
Simplification assessment for <file>: Current: N lines, M methods, K methods >80 lines Top opportunities (ranked by impact): 1. [HIGH] Extract _fix_backup_schedule_collision → 3 methods (-80 lines) [safe] 2. [HIGH] Consolidate 3 identical error handlers (-40 lines) [safe] 3. [MEDIUM] Replace 2 poll loops with wait_for_condition() (-30 lines) [needs-care] 4. [LOW] 4 Python idiom quick-wins (-8 lines) [safe] Intentionally deferred: - _verify_backup_integrity: complex retry with partial results, leave as-is - Lines 445-450: redundant-looking check may guard against ACM 2.11 quirk Estimated total reduction: ~158 lines (-10%)
Include any simplification opportunities spotted in other files as a brief note at the end — do not fix them, just mention them for a future pass.
After All Edits
Run
git diff --stat to show the change summary.
Do NOT commit. Tell the user they can review with
git diff and commit when ready.
What Good Simplification Looks Like
After applying this skill, the target file should:
- Read top-to-bottom without forcing the reader to hold >3 things in working memory
- Have methods that fit on one screen (~40-60 lines) with clear orchestrator methods
- Use one consistent pattern per concept (waiter, dry-run, error handling)
- Contain no dead paths, unused variables, or redundant indirection
- Be easier to debug during an incident and easier to extend for the next feature