Claude subprocess auto-fix for remaining ruff errors.
After ruff --fix resolves what it can, this module spawns a Claude
subprocess to attempt fixing the remaining diagnostics — one call per
file (parallel), max one retry cycle.
apply_edits(file_path, edits)
Apply old→new string replacements on file content.
Returns True if at least one edit matched, False otherwise.
Writes back only if at least one replacement was applied.
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def apply_edits(file_path: Path, edits: list[dict[str, str]]) -> bool:
"""Apply old→new string replacements on file content.
Returns True if at least one edit matched, False otherwise.
Writes back only if at least one replacement was applied.
"""
if not edits:
return False
content = file_path.read_text()
# Normalize CRLF to LF for matching
content = content.replace("\r\n", "\n")
matched = False
for edit in edits:
old = edit["old"]
new = edit["new"]
if old in content:
content = content.replace(old, new, 1)
matched = True
if matched:
file_path.write_text(content)
return matched
|
build_prompt(file_path, file_errors)
Build a Claude prompt with error details and targeted snippets.
Files ≤ _MAX_FULL_FILE lines get the full file content.
Larger files get header + error snippets.
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def build_prompt(file_path: Path, file_errors: list[str]) -> str:
"""Build a Claude prompt with error details and targeted snippets.
Files ≤ _MAX_FULL_FILE lines get the full file content.
Larger files get header + error snippets.
"""
total_lines = len(file_path.read_text().splitlines())
if total_lines <= _MAX_FULL_FILE:
return _build_full_file_prompt(file_path, file_errors)
error_block = "\n".join(file_errors)
snippets = build_snippets(file_path, file_errors)
return (
f"File: {file_path.name}\n\n"
f"Ruff errors:\n{error_block}\n\n"
f"Code snippets (with line numbers):\n{snippets}\n\n"
f"Return ONLY a JSON array of edits. Each edit is an object with "
f'"old" (exact text to find) and "new" (replacement text) keys.\n'
f'Example: [{{"old": "except:\\n pass", '
f'"new": "except Exception:\\n logging.exception(\\"Unexpected error\\")"}}]'
)
|
build_snippets(file_path, file_errors)
Build targeted code snippets around each error (±N lines context).
For large files (> _MAX_FULL_FILE), prepends the file header (imports
and module-level code before the first class/def) to ensure Claude
always sees existing imports.
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def build_snippets(file_path: Path, file_errors: list[str]) -> str:
"""Build targeted code snippets around each error (±N lines context).
For large files (> _MAX_FULL_FILE), prepends the file header (imports
and module-level code before the first class/def) to ensure Claude
always sees existing imports.
"""
all_lines = file_path.read_text().splitlines()
total = len(all_lines)
error_lines = _extract_error_lines(file_errors)
# Merge overlapping ranges
ranges: list[tuple[int, int]] = []
for line_no in sorted(set(error_lines)):
start = max(0, line_no - 1 - _SNIPPET_CONTEXT)
end = min(total, line_no - 1 + _SNIPPET_CONTEXT + 1)
if ranges and start <= ranges[-1][1]:
ranges[-1] = (ranges[-1][0], max(ranges[-1][1], end))
else:
ranges.append((start, end))
# Prepend header range for large files
header_end = find_header_end(all_lines)
if header_end > 0:
header_range = (0, header_end)
if ranges and header_range[1] >= ranges[0][0]:
# Header overlaps with first snippet — merge
ranges[0] = (0, max(ranges[0][1], header_end))
else:
ranges.insert(0, header_range)
# Build snippet text with line numbers
parts: list[str] = []
for start, end in ranges:
snippet_lines = [f"{i + 1}: {all_lines[i]}" for i in range(start, end)]
parts.append("\n".join(snippet_lines))
return "\n...\n".join(parts)
|
claude_fix(root, errors, *, warnings=None)
Use Claude subprocess to auto-fix remaining ruff errors.
Groups errors by file and spawns one claude -p call per file
in parallel. Max one attempt per file — if the fix fails or
produces invalid output, the original errors are returned.
Parameters:
| Name |
Type |
Description |
Default |
root
|
Path
|
|
required
|
errors
|
list[str]
|
Ruff diagnostic lines (file:line:col: CODE msg).
|
required
|
warnings
|
list[str] | None
|
Optional list to collect warning messages into.
|
None
|
Returns:
| Type |
Description |
list[str]
|
List of remaining errors after attempted fixes. Empty if all fixed.
|
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def claude_fix(
root: Path,
errors: list[str],
*,
warnings: list[str] | None = None,
) -> list[str]:
"""Use Claude subprocess to auto-fix remaining ruff errors.
Groups errors by file and spawns one ``claude -p`` call per file
in parallel. Max one attempt per file — if the fix fails or
produces invalid output, the original errors are returned.
Args:
root: Project root directory.
errors: Ruff diagnostic lines (``file:line:col: CODE msg``).
warnings: Optional list to collect warning messages into.
Returns:
List of remaining errors after attempted fixes. Empty if all fixed.
"""
if not errors:
return []
if not _has_claude:
if warnings is not None:
warnings.append("claude not found, auto-fix skipped")
return errors
grouped = _group_errors_by_file(errors)
remaining: list[str] = []
with ThreadPoolExecutor() as executor:
futures = {
executor.submit(_fix_single_file, root, filename, file_errors): filename
for filename, file_errors in grouped.items()
}
for future in as_completed(futures):
file_remaining, file_warnings = future.result()
remaining.extend(file_remaining)
if warnings is not None:
warnings.extend(file_warnings)
return remaining
|
fabricates_definition(edit)
Return True if edit introduces a new def or class.
Catches the pathological case where Claude invents a stub function
or class to silence an F821/F822 (undefined name) error instead of
fixing the reference site.
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def fabricates_definition(edit: dict[str, str]) -> bool:
"""Return True if *edit* introduces a new ``def`` or ``class``.
Catches the pathological case where Claude invents a stub function
or class to silence an F821/F822 (undefined name) error instead of
fixing the reference site.
"""
old_defs = len(_DEF_PATTERN.findall(edit["old"]))
new_defs = len(_DEF_PATTERN.findall(edit["new"]))
return new_defs > old_defs
|
filter_ruff_lines(stdout)
Keep real diagnostic lines, dropping ruff summary noise.
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def filter_ruff_lines(stdout: str) -> list[str]:
"""Keep real diagnostic lines, dropping ruff summary noise."""
return [
line
for line in stdout.strip().splitlines()
if line.strip() and not line.startswith(_RUFF_NOISE_PREFIXES)
]
|
Return 0-indexed line number of first class/def at indent 0.
If no class/def found, returns len(all_lines) (whole file is header).
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def find_header_end(all_lines: list[str]) -> int:
"""Return 0-indexed line number of first class/def at indent 0.
If no class/def found, returns len(all_lines) (whole file is header).
"""
for i, line in enumerate(all_lines):
if re.match(r"^(class |def |async def )", line):
return i
return len(all_lines)
|
parse_edits(output)
Parse Claude's JSON output into a list of old/new edit pairs.
Strips markdown code fences before parsing. Returns an empty list
on any parse failure (graceful degradation).
Source code in packages/axm-edit/src/axm_edit/services/lint.py
| Python |
|---|
| def parse_edits(output: str) -> list[dict[str, str]]:
"""Parse Claude's JSON output into a list of old/new edit pairs.
Strips markdown code fences before parsing. Returns an empty list
on any parse failure (graceful degradation).
"""
text = output.strip()
# Strip markdown code fences if present
if text.startswith("```"):
lines = text.splitlines()
# Remove first line (```json or ```) and last line (```)
if lines[-1].strip() == "```":
text = "\n".join(lines[1:-1])
try:
data = json.loads(text)
except (json.JSONDecodeError, ValueError, TypeError):
return []
if not isinstance(data, list):
return []
for entry in data:
if not isinstance(entry, dict) or "old" not in entry or "new" not in entry:
return []
return data
|