From codemap
Atomically renames Python symbols or modules using the structural index, updating all references including imports, __all__ re-exports, and Sphinx cross-refs. Supports deprecated alias or hard-delete.
How this skill is triggered — by the user, by Claude, or both
Slash command
/codemap:rename-refs symbol <old_qname> <new_qname> [--dry-run] [--deprecate[="@deprecated(...)"|"@deprecated_class(...)"]] [--since <ver>] [--removed-in <ver>] [--remove-if-no-callers] | module <old_module_path> <new_module_path> [--dry-run]symbol <old_qname> <new_qname> [--dry-run] [--deprecate[="@deprecated(...)"|"@deprecated_class(...)"]] [--since <ver>] [--removed-in <ver>] [--remove-if-no-callers] | module <old_module_path> <new_module_path> [--dry-run]sonnetThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
<objective>
Rename Python symbol or module atomically. Coverage:
__all__ re-exports in __init__.py files.py and .rst@deprecated alias via pyDeprecate (--deprecate; requires pyDeprecate installed)Subcommands:
symbol <old_qname> <new_qname> — function, class, or method. qname = bare (MyClass), qualified (MyClass.method), or full (mypackage.auth::validate_token)module <old_module_path> <new_module_path> — dotted path (mypackage.old_name). Renames file + all import lines.Flags:
--dry-run — print sites that would change; no edits--deprecate — symbol only: keep old name as pyDeprecate @deprecated wrapper → new name; requires pyDeprecate--since <ver> / --removed-in <ver> — passed to deprecation decorator; default "?"--remove-if-no-callers — symbol only: delete definition when exhaustive=true + zero callers; requires confirmationHard limits (static analysis boundary — not fixable):
getattr(obj, "old_name") — string not statically bound; Step 6 emits grep advisory--deprecate + semver bump for public APIsRouting: IDE/LSP rename wanting coverage preview only → run --dry-run. Handles 1:1 renames only.
NOT for: building index (/codemap:scan-codebase); querying without rename intent (/codemap:query-code); non-Python files; renaming symbols in ABCs/Protocols where subclass overrides exist — overrides not tracked by static import analysis; review fn-rdeps manually, rename overrides explicitly. Note: no --index <path> support — always uses default project index. Monorepo: run /codemap:scan-codebase --root <pkg> first, then rename.
Parse $ARGUMENTS in one Bash block; write all tokens to project-qualified tmpfiles — later steps read tmpfiles (shell vars die at each Bash() boundary):
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
SUBCOMMAND=$(echo "$ARGUMENTS" | awk '{print $1}')
OLD_REF=$(echo "$ARGUMENTS" | awk '{print $2}')
NEW_REF=$(echo "$ARGUMENTS" | awk '{print $3}')
echo "$ARGUMENTS" | grep -q -- '--dry-run' && DRY_RUN="true" || DRY_RUN="false"
echo "$ARGUMENTS" | grep -q -- '--remove-if-no-callers' && REMOVE_IF_ZERO="true" || REMOVE_IF_ZERO="false"
# POSIX sed — avoids PCRE lookbehind, works on macOS BSD sed
SINCE_VER=$(echo "$ARGUMENTS" | sed -n 's/.*--since \([^ ]*\).*/\1/p' || echo "")
REMOVED_IN_VER=$(echo "$ARGUMENTS" | sed -n 's/.*--removed-in \([^ ]*\).*/\1/p' || echo "")
case "$SUBCOMMAND" in
symbol|module) ;;
*) printf "Usage: /codemap:rename-refs symbol <old> <new> [flags] | module <old_path> <new_path> [--dry-run]\n" >&2; exit 1 ;;
esac
# conflicting flags — --deprecate creates alias, --remove-if-no-callers deletes; incompatible
echo "$ARGUMENTS" | grep -q -- '--deprecate' && [ "$REMOVE_IF_ZERO" = "true" ] && { printf "⚠ conflicting flags: --deprecate creates alias, --remove-if-no-callers deletes target; these are incompatible\n" >&2; rm -f "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVE_IF_ZERO"; exit 1; }
printf '%s' "$SUBCOMMAND" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-SUBCOMMAND"
printf '%s' "$OLD_REF" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_REF"
printf '%s' "$NEW_REF" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-NEW_REF"
printf '%s' "$DRY_RUN" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-DRY_RUN"
printf '%s' "$REMOVE_IF_ZERO" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVE_IF_ZERO"
printf '%s' "$SINCE_VER" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-SINCE_VER"
printf '%s' "$REMOVED_IN_VER" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVED_IN_VER"
# bare names for grep
OLD_NAME="${OLD_REF##*::}"; [ "$SUBCOMMAND" = "module" ] && OLD_NAME="${OLD_REF##*.}"
NEW_NAME="${NEW_REF##*::}"; [ "$SUBCOMMAND" = "module" ] && NEW_NAME="${NEW_REF##*.}"
printf '%s' "$OLD_NAME" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME"
printf '%s' "$NEW_NAME" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-NEW_NAME"
Parse --deprecate — bare flag or carrying a decorator value:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
PARSE_OUT=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/parse_deprecate_args.py" --arguments="$ARGUMENTS" 2>"${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-parse-deprecate-err") || { _PARSE_RC=$?; printf "! parse_deprecate_args.py failed (exit %d): %s\n" "$_PARSE_RC" "$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-parse-deprecate-err" 2>/dev/null)" >&2; exit 1; }
FLAG_FILE=$(printf '%s\n' "$PARSE_OUT" | sed -n 1p) # pid-qualified path printed by the script (SEC-M7)
DEC_FILE=$(printf '%s\n' "$PARSE_OUT" | sed -n 2p)
[ -n "$FLAG_FILE" ] || { printf "! parse_deprecate_args.py returned no paths\n" >&2; exit 1; }
DEPRECATE=$(cat "$FLAG_FILE" 2>/dev/null || echo "false")
DEPRECATE_DECORATOR=$(cat "$DEC_FILE" 2>/dev/null || echo "")
printf '%s' "$DEPRECATE" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-DEPRECATE"
printf '%s' "$DEPRECATE_DECORATOR" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-DEPRECATE_DECORATOR"
Unsupported-flag check — scan $ARGUMENTS for -- tokens not in allowlist (--dry-run, --deprecate, --since, --removed-in, --remove-if-no-callers). Found → print ! Unknown flag(s): --<token>. Supported flags: --dry-run, --deprecate[=<decorator>], --since, --removed-in, --remove-if-no-callers. Re-invoke with corrected flags. and stop — do NOT invoke AskUserQuestion (fail-fast keeps worst-case AQQ path at 4: STALE-index, multiple-matches, apply/dry-run, hard-delete confirmation).
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
# stderr to tempfile; eval sees KEY=VAL pairs only
python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/resolve_index_env.py" \
--output-prefix "codemap-${_CM_PROJ}" 2>/dev/null # timeout: 5000
PROJ=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-resolve-proj" 2>/dev/null || echo "")
INDEX=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-resolve-index" 2>/dev/null || echo "")
[ -n "$PROJ" ] || { printf "! resolve_index_env.py failed — check Python availability and CLAUDE_PLUGIN_ROOT\n"; exit 1; }
[ -n "$INDEX" ] || { echo "! index not found — run /codemap:scan-codebase first"; exit 1; }
SMOKE_JSON=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/check_index_smoke.py" --index-path "$INDEX") # timeout: 10000
# jq absent → abort; stale index produces missed call sites
command -v jq >/dev/null 2>&1 || { printf "! jq not found — required for rename-refs index validation; install via brew install jq or apt-get install jq\n"; exit 1; }
STALE=$(echo "$SMOKE_JSON" | jq -r '.stale // "unknown"' 2>/dev/null || echo "unknown")
STALE=true → invoke AskUserQuestion — (a) Proceed anyway (callers may be incomplete) · (b) Abort (re-run /codemap:scan-codebase first). On Abort: print "Run /codemap:scan-codebase then re-invoke" and stop.STALE=unknown (JSON parse failed) → print ⚠ Could not determine index freshness — proceeding but callers may be incomplete, continue with caution; don't silently treat as fresh.Locate scan-query binary (three-tier fallback — same as integration/SKILL.md C1). Re-resolve $SQ at top of every subsequent Bash block (shell var doesn't persist across Bash() calls); use $SQ instead of bare scan-query:
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query") # timeout: 5000
Symbol subcommand:
# timeout: 25000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query")
OLD_REF=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_REF")
FIND_SYMBOL_JSON=$("$SQ" --timeout 20 find-symbol "$OLD_REF" --limit 0)
printf '%s' "$FIND_SYMBOL_JSON" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-find-symbol-json"
find-symbol returns matches array — each: {name, qualified_name, type, module, path, start_line, end_line, source} (same schema as query-code symbol). Use path, start_line, end_line for edits; qualified_name for exact-match filtering. Step 4e reads .matches[0].type.
! Symbol '$OLD_REF' not found. Verify with: scan-query find-symbol <pattern> and stop.AskUserQuestion listing candidates (name, type, module, path) — ask which to rename.Full qname: <module>::<qualified_name> (e.g. mypackage.auth::validate_token).
# timeout: 25000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query")
FIND_SYMBOL_JSON=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-find-symbol-json" 2>/dev/null || echo "{}")
SYM_MODULE=$(echo "$FIND_SYMBOL_JSON" | jq -r '.matches[0].module // ""' 2>/dev/null || echo "")
SYM_QNAME=$(echo "$FIND_SYMBOL_JSON" | jq -r '.matches[0].qualified_name // ""' 2>/dev/null || echo "")
printf '%s' "$SYM_MODULE" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-sym-module"
printf '%s' "$SYM_QNAME" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-sym-qname"
[ -n "$SYM_MODULE" ] && [ -n "$SYM_QNAME" ] || { printf "! could not extract module/qualified_name from find-symbol result\n" >&2; exit 1; }
RDEPS_JSON=$("$SQ" --timeout 20 fn-rdeps "$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-sym-module")::$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-sym-qname")")
RDEP_COUNT=$(echo "$RDEPS_JSON" | jq -r '.count // 0' 2>/dev/null || echo "0")
EXHAUSTIVE=$(echo "$RDEPS_JSON" | jq -r '.index.exhaustive // false' 2>/dev/null || echo "false")
printf '%s' "$RDEP_COUNT" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rdep-count"
printf '%s' "$EXHAUSTIVE" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-EXHAUSTIVE"
fn-rdeps returns {qname, called_by:[{caller, module, path}], count, index:{exhaustive,...}}.
called_by entries have no line numbers — use scan-query symbol <caller> per entry in Step 4c for line range.EXHAUSTIVE from result["index"]["exhaustive"]; false → note in blast-radius report.Module subcommand:
# timeout: 25000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query")
OLD_REF=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_REF")
OLD_MODULE_PATH="$OLD_REF"
printf '%s' "$OLD_MODULE_PATH" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH"
RDEPS_JSON=$("$SQ" --timeout 20 rdeps "$OLD_REF")
RDEP_COUNT=$(echo "$RDEPS_JSON" | jq -r '.imported_by | length' 2>/dev/null || echo "0")
EXHAUSTIVE=$(echo "$RDEPS_JSON" | jq -r '.index.exhaustive // false' 2>/dev/null || echo "false")
printf '%s' "$RDEP_COUNT" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rdep-count"
printf '%s' "$EXHAUSTIVE" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-EXHAUSTIVE"
# Safety: --remove-if-no-callers honored only when explicitly passed AND exhaustive
REMOVE_IF_ZERO_ARG=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVE_IF_ZERO" 2>/dev/null || echo "false")
[ "$REMOVE_IF_ZERO_ARG" = "true" ] && [ "$EXHAUSTIVE" != "true" ] && printf '%s' "false" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVE_IF_ZERO"
Returns {imported_by:[...], index:{exhaustive,...}}. Extract RDEP_COUNT, EXHAUSTIVE.
Print:
Rename: <OLD_REF> → <NEW_REF>
Type: symbol | module
[symbol] Definition: <path>:<start_line>-<end_line>
Static callers: N (across N files)
- src/foo/bar.py (caller: module::fn)
- src/foo/baz.py (caller: module::other_fn)
- tests/test_foo.py
Docstring refs: (will grep :func:/:class:/:meth:/:mod:/:attr: in Step 4d)
[if DEPRECATE]
Deprecation wrapper: <OLD_REF> kept as @deprecated alias → <NEW_REF>
⚠ Not covered (hard limits — grep advisories in Step 8):
- getattr("old_name") dynamic dispatch
- Cross-repo consumers
[if not EXHAUSTIVE]
⚠ Index non-exhaustive — some callers may not appear above
Budget gate: caller count > 50 → derive BRANCH first:
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-' || echo 'main') # timeout: 3000
Write full caller list to .temp/output-rename-refs-blast-${BRANCH}-<YYYY-MM-DD>.md, beginning with YAML header:
---
Title: rename-refs blast-radius — <OLD_REF>
Date: <YYYY-MM-DD>
Scope: <project name>
Focus: caller enumeration (capped at 50 for edit pass)
Agents: codemap:rename-refs
Outcome: <N callers found; exhaustive: true/false>
Confidence: <exhaustive|partial>
Next steps: apply edits for callers 1–50; callers 51–N in this file require manual edit
Path: → .temp/output-rename-refs-blast-<branch>-<YYYY-MM-DD>.md
---
Print path + summary count, then ⚠ >50 callers — capping edit pass at first 50. Callers 51–N listed in blast file as manual advisories. Proceed with first 50 only. Note: Step 7 summary shows residual old-name hits for callers 51–N as "skipped callers" — expected, not missed dynamic references.
--remove-if-no-callers guards — evaluated before any rename edits:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
REMOVE_IF_ZERO=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVE_IF_ZERO" 2>/dev/null || echo "false")
RDEP_COUNT=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rdep-count" 2>/dev/null || echo "0")
EXHAUSTIVE=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-EXHAUSTIVE" 2>/dev/null || echo "false")
REMOVE_IF_ZERO=true AND RDEP_COUNT > 0 → print ! --remove-if-no-callers: N callers found. Remove all callers first or omit flag. and stop entire rename operation.REMOVE_IF_ZERO=true AND EXHAUSTIVE=false → print ! --remove-if-no-callers requires exhaustive=true. Run /codemap:scan-codebase to ensure full coverage. and stop entire rename operation.REMOVE_IF_ZERO=true AND RDEP_COUNT == 0 AND EXHAUSTIVE=true → invoke AskUserQuestion — (a) Delete $OLD_REF — no callers confirmed, remove definition · (b) Abort — keep file. On Abort: stop. On Delete: find-symbol to locate definition line range, then Read to confirm block bounds — verify line at start_line contains expected symbol name (bare OLD_NAME or qualified OLD_REF); not matching → print ! Symbol name mismatch at line <start_line>: expected <OLD_NAME>, index may be stale — run /codemap:scan-codebase first and abort without deleting. Only Edit when verified — remove definition block (from def/class line through final body line, including immediately preceding @decorator lines). Skip Steps 4a–4d. Print ℹ Symbol had no callers — removed $OLD_REF without rename and go to Step 6.REMOVE_IF_ZERO=false): proceed with normal rename flow.--dry-run: derive branch first, then write report:
BRANCH=$(git branch --show-current 2>/dev/null | tr '/' '-' || echo 'main') # timeout: 3000
Write report to .temp/output-rename-refs-dry-${BRANCH}-<YYYY-MM-DD>.md via Write, beginning with YAML header:
---
Title: rename-refs dry-run — <OLD_REF> → <NEW_REF>
Date: <YYYY-MM-DD>
Scope: <project name>
Focus: dry-run: sites that would be changed
Agents: codemap:rename-refs
Outcome: DRY_RUN — no edits applied; <N callers, M import sites, K docstring refs>
Confidence: <exhaustive|partial>
Next steps: re-invoke without --dry-run to apply; or abort
Path: → .temp/output-rename-refs-dry-<branch>-<YYYY-MM-DD>.md
---
Print path, then invoke AskUserQuestion — (a) Apply for real (re-invoke without --dry-run) · (b) Done. Stop.
Otherwise, invoke AskUserQuestion — (a) Apply edits · (b) Abort. On Abort: stop.
Skip to Step 5 if SUBCOMMAND=module.
4a — Rename definition site:
Read path from find-symbol result. Edit definition line at start_line:
def old_name( → def new_name(class OldName( / class OldName: → class NewName( / class NewName:def old_method(self within class body@property descriptors: symbol is property → find-symbol returns only getter. After renaming getter, grep same class body for @old_name.setter / @old_name.deleter, rename to @new_name.setter / @new_name.deleter — else broken descriptor (@old_name.setter after getter renamed raises AttributeError at class-definition time):
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME")
grep -n "@${OLD_NAME}\.setter\|@${OLD_NAME}\.deleter" "<path>" # timeout: 3000
@typing.overload stubs: after renaming implementation, grep same file AND any sibling .pyi stub for @overload-decorated def old_name( — find-symbol returns implementation only, not stubs or stub files. Rename all overload stubs to new_name:
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME")
grep -n "@overload" "<path>" "<path%.py>.pyi" 2>/dev/null | grep -A1 "def $OLD_NAME(" # timeout: 3000
4b — __all__ re-exports:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME")
FIND_SYMBOL_JSON=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-find-symbol-json" 2>/dev/null || echo "{}")
# scope to pkg tree — avoid same-named symbol false positives
OLD_FILE_PATH=$(echo "$FIND_SYMBOL_JSON" | jq -r '.matches[0].path // "."' 2>/dev/null || echo ".")
PKG_DIR=$(dirname "$OLD_FILE_PATH")
printf '%s' "$PKG_DIR" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-pkg-dir"
_GREP_SCOPE="${PKG_DIR:-.}"
grep -rn "\"$OLD_NAME\"\|'$OLD_NAME'" --include="__init__.py" "$_GREP_SCOPE"
For each hit inside __all__ list: Edit "old_name" → "new_name".
4c — Import call sites (per caller from called_by):
Track PROCESSED_IMPORT_FILES set via tmpfile (files already given import-line edits) — file with 3 callers needs import-line edit exactly once. Init before loop:
# timeout: 3000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
printf '' > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-processed-import-files"
Before each import edit for a caller file, check and mark:
# timeout: 3000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
TARGET_FILE="<caller_path>"
grep -qxF "$TARGET_FILE" "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-processed-import-files" && SKIP_IMPORT="true" || SKIP_IMPORT="false"
[ "$SKIP_IMPORT" = "false" ] && echo "$TARGET_FILE" >> "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-processed-import-files"
Apply import-line edits only when SKIP_IMPORT=false.
Per caller entry (called_by[i].caller already module::function — pass directly to scan-query):
# timeout: 5000
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query")
Then "$SQ" symbol "<caller_qname>" — timeout: 10000 — result: {symbols:[{path, start_line, end_line, qualified_name, ...}]}
⚠ symbol not found for caller <caller_qname> — skipping caller and continuesymbols[] by qualified_name (exact match preferred; else first entry)path, start_line, end_line for step 2path:
start_line); (2) qualified calls X.old_name( within start_line–end_line; (3) bare calls old_name( within start_line–end_line onlyold_name( → new_name( only within start_line–end_line — never bare-replace outside confirmed caller scopepath already in processed-import-files (SKIP_IMPORT=true) → skip import-line edit; only edit call sites within caller's rangeModule-level import fix (whole-file scope, once per file):
# timeout: 3000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME")
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH" 2>/dev/null || echo "")
# SYMBOL: derive OLD_MODULE_PATH from OLD_REF; keep dotted — grep matches import syntax, not fs paths
if [ -z "$OLD_MODULE_PATH" ]; then
OLD_REF=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_REF" 2>/dev/null || echo "")
OLD_MODULE_PATH=$(echo "$OLD_REF" | sed 's/::.*//')
printf '%s' "$OLD_MODULE_PATH" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH"
fi
# escape dots — must match literally, not any-char
_OLD_MODULE_GREP=$(printf '%s' "$OLD_MODULE_PATH" | sed 's/\./\\./g')
_OLD_BASENAME_GREP=$(printf '%s' "${OLD_MODULE_PATH##*.}" | sed 's/\./\\./g')
grep -n "from ${_OLD_MODULE_GREP} import .*\b${OLD_NAME}\b\|from .*\\.${_OLD_BASENAME_GREP} import .*\b${OLD_NAME}\b\|^import .*\b${OLD_NAME}\b" "<file>"
Edit each matched import line. Verify from <module> clause references expected module path before editing — skip imports of OLD_NAME from unrelated modules. processed-import-files tmpfile already updated by dedup block above.
4d — Sphinx docstring cross-refs:
# timeout: 10000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME")
# fully-qualified module prefix avoids false positives on same bare name in unrelated packages
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH" 2>/dev/null || echo "")
# pattern matches bare name anywhere; verify module before editing
grep -rn ":func:\`[^']*$OLD_NAME[^']*\`\|:class:\`[^']*$OLD_NAME[^']*\`\|:meth:\`[^']*$OLD_NAME[^']*\`\|:mod:\`[^']*$OLD_NAME[^']*\`\|:attr:\`[^']*$OLD_NAME[^']*\`" --include="*.py" --include="*.rst" .
Edit each match: replace old_name/OldName within backtick-delimited role string. Before editing, verify role string includes expected module path (${OLD_MODULE_PATH} or qualified form) to avoid renaming cross-refs to unrelated packages with same symbol name.
4e — Deprecation wrapper (if DEPRECATE=true, after 4a):
Call gen_deprecation_wrapper.py to produce Python code string, then insert immediately after new definition block in same file.
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
FIND_SYMBOL_JSON=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-find-symbol-json" 2>/dev/null || echo "{}")
SYMBOL_TYPE=$(echo "$FIND_SYMBOL_JSON" | jq -r '.matches[0].type // "function"')
OLD_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_NAME")
NEW_NAME=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-NEW_NAME")
DEPRECATE_DECORATOR=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-DEPRECATE_DECORATOR" 2>/dev/null || echo "")
SINCE_VER=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-SINCE_VER" 2>/dev/null || echo "")
REMOVED_IN_VER=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-REMOVED_IN_VER" 2>/dev/null || echo "")
if [ -n "$DEPRECATE_DECORATOR" ]; then
DEPRECATION_CODE=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/gen_deprecation_wrapper.py" \
--decorator "$DEPRECATE_DECORATOR" \
--old-name "$OLD_NAME" \
${REMOVED_IN_VER:+--removed-in "$REMOVED_IN_VER"}) || { echo "! gen_deprecation_wrapper failed — check symbol type and names"; exit 1; }
else
DEPRECATION_CODE=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/gen_deprecation_wrapper.py" \
--type "$SYMBOL_TYPE" \
--old-name "$OLD_NAME" \
--new-name "$NEW_NAME" \
${SINCE_VER:+--since "$SINCE_VER"} \
${REMOVED_IN_VER:+--removed-in "$REMOVED_IN_VER"}) || { echo "! gen_deprecation_wrapper failed — check symbol type and names"; exit 1; }
fi
Insert $DEPRECATION_CODE as new block immediately after end of new definition (after end_line from Step 2). Requires pyDeprecate installed in target project; if absent, inserted from deprecate import ... raises ImportError at import time — surface in Step 6 summary advisory.
Type→decorator mapping:
"class" → @deprecated_class(target=NewName, ...) — preserves isinstance via transparent proxy"function" / "method" → @deprecated(target=new_fn, ...) — ... body; pydeprecate handles call forwarding4f — Hard-delete definition (if REMOVE_IF_ZERO=true):
Zero-caller + exhaustive path handled entirely in Step 3 — when REMOVE_IF_ZERO=true and RDEP_COUNT == 0, Step 3 deletes OLD definition directly, exits before reaching Steps 4a–4d. This step therefore no-op when reached via normal rename flow: execution reaches Step 4f → REMOVE_IF_ZERO either false or Step 3 guards blocked flow. No action required here.
5a — File rename:
# timeout: 3000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
git rev-parse --is-inside-work-tree 2>/dev/null || { printf "⚠ not a git repo — cannot use git mv; rename file manually\n" >&2; exit 1; }
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH")
# prefer index path — avoids dotted-path conversion errors on src/ layouts
SMOKE_INDEX=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-resolve-index" 2>/dev/null || echo "")
INDEX_PATH=""
if [ -n "$SMOKE_INDEX" ] && command -v jq >/dev/null 2>&1; then
INDEX_PATH=$(jq -r --arg m "$OLD_MODULE_PATH" '.modules[$m].path // ""' "$SMOKE_INDEX" 2>/dev/null || echo "")
fi
if [ -n "$INDEX_PATH" ]; then
old_file_path="$INDEX_PATH"
else
# dotted-path conversion: may be wrong for src/ layouts; verify before git mv
old_file_path=$(echo "$OLD_MODULE_PATH" | tr '.' '/').py
fi
if [ ! -f "$old_file_path" ] && [ -f "$(echo "$OLD_MODULE_PATH" | tr '.' '/')/__init__.py" ]; then # MED-15: package dir
printf "! %s is a package directory — use 'git mv %s %s' directly\n" \
"$OLD_MODULE_PATH" \
"$(echo "$OLD_MODULE_PATH" | tr '.' '/')" \
"$(echo "$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-NEW_REF")" | tr '.' '/')" >&2
exit 1
fi
[ -f "$old_file_path" ] || { printf "! File not found: %s\n" "$old_file_path" >&2; exit 1; }
printf '%s' "$old_file_path" > "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-old-file-path"
git status --porcelain "$old_file_path"
?? prefix: print ! File is untracked — add to git first: git add "<old_file_path>" and stop.! File has uncommitted changes — commit or stash before module rename. and stop.# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
old_file_path=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-old-file-path")
NEW_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-NEW_REF")
# same strategy as old_file_path: index path preferred; avoids src/-layout tr mismatch
SMOKE_INDEX=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-resolve-index" 2>/dev/null || echo "")
new_file_path=""
if [ -n "$SMOKE_INDEX" ] && command -v jq >/dev/null 2>&1; then
new_file_path=$(jq -r --arg m "$NEW_MODULE_PATH" '.modules[$m].path // ""' "$SMOKE_INDEX" 2>/dev/null || echo "")
fi
if [ -z "$new_file_path" ]; then
# fallback: dotted-to-path; src/ layouts: use old_file_path for correct prefix
_OLD_PREFIX=$(dirname "$old_file_path" | sed "s/$(echo "${OLD_MODULE_PATH%.*}" | tr '.' '\/')$//")
new_file_path="${_OLD_PREFIX}$(echo "$NEW_MODULE_PATH" | tr '.' '/').py"
fi
git mv "$old_file_path" "$new_file_path"
5b — Direct imports:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH")
# escape dots — match literally, not any-char
_OLD_MODULE_GREP=$(printf '%s' "$OLD_MODULE_PATH" | sed 's/\./\\./g')
grep -rn "^import ${_OLD_MODULE_GREP}\b\|^import ${_OLD_MODULE_GREP} as " --include="*.py" .
Edit each: import mypackage.old_name → import mypackage.new_name.
5c — From-imports:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH")
# escape dots — match literally, not any-char
_OLD_MODULE_GREP=$(printf '%s' "$OLD_MODULE_PATH" | sed 's/\./\\./g')
grep -rn "^from ${_OLD_MODULE_GREP} import\|^from ${_OLD_MODULE_GREP} as " --include="*.py" .
Edit each: from mypackage.old_name import → from mypackage.new_name import.
5d — __init__.py relative re-exports:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH" 2>/dev/null || echo "")
OLD_BASENAME="${OLD_MODULE_PATH##*.}" # last component of dotted path
# restrict to same package dir; avoids false positives on same-named modules elsewhere
OLD_PKG_DIR=$(echo "${OLD_MODULE_PATH%.*}" | tr '.' '/')
grep -rn "from \.*[^.]*\.${OLD_BASENAME} import\|from \.${OLD_BASENAME} import\|from \.${OLD_BASENAME} as " --include="__init__.py" "${OLD_PKG_DIR:-.}" 2>/dev/null
Edit each: from .old_name import → from .new_name import. Verify match in expected package directory before editing — skip matches in unrelated packages.
5e — pyproject.toml / setup.cfg:
# timeout: 3000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH" 2>/dev/null || echo "")
# full dotted path, not OLD_BASENAME; avoids false positives on common names like 'utils'
grep -rn "${OLD_MODULE_PATH}" pyproject.toml setup.cfg 2>/dev/null
Edit packages / install_requires entries matching old module path if found. Do NOT use bare OLD_BASENAME for this grep — too broad.
5f — Sphinx docstring :mod: refs:
# timeout: 5000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
OLD_MODULE_PATH=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_MODULE_PATH")
OLD_BASENAME="${OLD_MODULE_PATH##*.}"
# full dotted path first to narrow scope; fall back to basename only when no full-path match
grep -rn ":mod:\`[^']*${OLD_MODULE_PATH}[^']*\`" --include="*.py" --include="*.rst" . 2>/dev/null || \
grep -rn ":mod:\`[^']*${OLD_BASENAME}[^']*\`" --include="*.py" --include="*.rst" .
Edit each :mod: reference to new module path. For basename-only matches, verify surrounding module context matches expected package before editing.
# timeout: 400000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query")
# --incremental re-parses changed files only; sufficient post-rename
"${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/scan-index" --incremental --timeout 360
_scan_rc=$?
if [ "$_scan_rc" -ne 0 ]; then
printf "! scan-index --incremental failed — verification may be incomplete; run /codemap:scan-codebase for full rebuild\n"
# don't skip — stale index results are advisory
fi
OLD_REF=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_REF")
"$SQ" --timeout 20 find-symbol "$OLD_REF" --limit 0 # timeout: 25000
For module:
# timeout: 25000
_CM_PROJ=$(git rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null || echo "cm")
SQ=$(python "${CLAUDE_PLUGIN_ROOT:-plugins/codemap}/bin/locate_scan_query.py" 2>/dev/null || echo "scan-query")
OLD_REF=$(cat "${TMPDIR:-/tmp}/codemap-${_CM_PROJ}-rename-OLD_REF")
"$SQ" --timeout 20 rdeps "$OLD_REF"
Expected: old name absent (or present only as deprecated alias for symbol with --deprecate).
Old name still found outside deprecated alias → list residual hit files — surface as advisory in Step 7. These are hard-limit cases (dynamic refs, string refs in templates, config strings outside scanned scope).
Print:
✓ Renamed: <OLD_REF> → <NEW_REF>
Files changed: N
Call sites updated: M
Docstring refs updated: K
[if DEPRECATE] Deprecation alias added at: <path>:<line>
Advisory — check manually (outside static analysis coverage):
- getattr("<old_name>") dynamic dispatch: grep -rn '"<old_name>"' src/
[if cross-repo public API and DEPRECATE not used]
- External consumers: update CHANGELOG; use --deprecate alias until next major release
[if caller count was capped at 50]
- Skipped callers (51–N): edit these manually — listed in .temp/output-rename-refs-blast-* file (see blast-radius report from Step 3)
[if residual hits from Step 6 re-scan]
- Residual index hits (likely dynamic/string refs):
<file>:<line>
npx claudepluginhub borda/ai-rig --plugin codemapRenames, extracts, splits, moves, or restructures code safely using graph-aware impact analysis and automated rename tools. Covers checklists for each refactoring type.
Safe refactoring workflow using trace-mcp — assess risk, find candidates, check impact, and rename symbols across all files without missing import sites or cross-file references.
Guides safe refactoring with impact analysis via CocoSearch semantic search, then executes changes step-by-step with user confirmation.