feat(patch): whitespace-visualized diagnosis on residual no-match errors
When old_string survives all 9 fuzzy strategies without a match but
the closest candidate line matches after stripping whitespace, the
failure is whitespace-shaped (tabs vs spaces, indent depth). The
did-you-mean hint now appends a two-line diagnosis with leading
whitespace made visible:
Whitespace difference detected (→ = tab, · = space):
file has: →def start(self):
you sent: ····def start(self):
Use the exact whitespace shown in 'file has'.
Pattern ported from crush's diagnoseMismatch (agent-codebase survey) —
it converts the residual dead-end error into a one-turn fix. Only the
leading run is visualized (interior spacing stays readable); content-
shaped misses and raw-exact candidates are unchanged.
This commit is contained in:
parent
75a8d4a597
commit
5d675a2ca7
|
|
@ -0,0 +1,64 @@
|
|||
"""Tests for whitespace-visualized mismatch diagnosis in patch no-match hints."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.fuzzy_match import (
|
||||
_visualize_whitespace,
|
||||
find_closest_lines,
|
||||
fuzzy_find_and_replace,
|
||||
)
|
||||
|
||||
|
||||
class TestVisualizeWhitespace:
|
||||
def test_spaces_and_tabs_visualized(self):
|
||||
assert _visualize_whitespace("\t x = 1") == "→····x = 1"
|
||||
|
||||
def test_interior_whitespace_untouched(self):
|
||||
assert _visualize_whitespace(" a b") == "··a b"
|
||||
|
||||
def test_no_leading_ws(self):
|
||||
assert _visualize_whitespace("plain") == "plain"
|
||||
|
||||
|
||||
class TestWhitespaceDiagnosis:
|
||||
def test_whitespace_shaped_miss_shows_both_lines(self):
|
||||
# File uses tabs; the model sends 4 spaces AND a wrong second line so
|
||||
# every fuzzy strategy misses (content mismatch), but line 1 is a
|
||||
# whitespace-only difference worth diagnosing.
|
||||
content = "def f():\n\tvalue = compute_thing()\n\treturn value\n"
|
||||
old = " value = compute_thing()\n return wrong_name"
|
||||
hint = find_closest_lines(old, content)
|
||||
assert "Whitespace difference detected" in hint
|
||||
assert "→value = compute_thing()" in hint
|
||||
assert "····value = compute_thing()" in hint
|
||||
|
||||
def test_content_miss_has_no_whitespace_block(self):
|
||||
content = "alpha = 1\nbeta = 2\n"
|
||||
old = "alpha = 999"
|
||||
hint = find_closest_lines(old, content)
|
||||
assert "Whitespace difference detected" not in hint
|
||||
|
||||
def test_exact_line_present_no_ws_block(self):
|
||||
# anchor matches raw -> no whitespace diagnosis needed
|
||||
content = " x = 1\n y = 2\n"
|
||||
old = " x = 1\n z = 3"
|
||||
hint = find_closest_lines(old, content)
|
||||
assert "Whitespace difference detected" not in hint
|
||||
|
||||
|
||||
class TestEndToEndHint:
|
||||
def test_no_match_error_carries_ws_diagnosis(self):
|
||||
# Second line's CONTENT differs (wrong call name) so all 9 fuzzy
|
||||
# strategies genuinely miss; first line differs only in whitespace,
|
||||
# so the hint should include the whitespace diagnosis.
|
||||
content = "class A:\n\tdef run(self):\n\t\tstart_engine()\n\t\treturn 1\n"
|
||||
old = " def run(self):\n boot_engine_wrong()\n return 2"
|
||||
new = " def run(self):\n start_engine()\n return 3"
|
||||
_c, count, _s, err = fuzzy_find_and_replace(content, old, new)
|
||||
assert count == 0
|
||||
from tools.fuzzy_match import format_no_match_hint
|
||||
hint = format_no_match_hint(err, count, old, content)
|
||||
assert "Did you mean" in hint
|
||||
assert "Whitespace difference detected" in hint
|
||||
|
|
@ -968,6 +968,20 @@ def _map_normalized_positions(original: str, normalized: str,
|
|||
return original_matches
|
||||
|
||||
|
||||
def _visualize_whitespace(line: str) -> str:
|
||||
"""Render leading whitespace visibly (→ = tab, · = space).
|
||||
|
||||
Only the leading run is visualized — interior spacing is rarely the
|
||||
culprit and full visualization makes lines unreadable.
|
||||
"""
|
||||
i = 0
|
||||
prefix = []
|
||||
while i < len(line) and line[i] in (" ", "\t"):
|
||||
prefix.append("→" if line[i] == "\t" else "·")
|
||||
i += 1
|
||||
return "".join(prefix) + line[i:]
|
||||
|
||||
|
||||
def find_closest_lines(old_string: str, content: str, context_lines: int = 2, max_results: int = 3) -> str:
|
||||
"""Find lines in content most similar to old_string for "did you mean?" feedback.
|
||||
|
||||
|
|
@ -1027,7 +1041,23 @@ def find_closest_lines(old_string: str, content: str, context_lines: int = 2, ma
|
|||
if not parts:
|
||||
return ""
|
||||
|
||||
return "\n---\n".join(parts)
|
||||
result = "\n---\n".join(parts)
|
||||
|
||||
# Whitespace diagnosis (pattern from crush's diagnoseMismatch): when the
|
||||
# best candidate line matches the anchor after stripping but differs in
|
||||
# raw text, the failure is whitespace-shaped. Show BOTH lines with
|
||||
# leading whitespace made visible so the model can copy the file's
|
||||
# exact indentation instead of guessing again.
|
||||
best_line = content_lines[top[0][1]]
|
||||
if best_line.strip() == anchor and best_line != old_lines[0]:
|
||||
result += (
|
||||
"\n\nWhitespace difference detected (→ = tab, · = space):\n"
|
||||
f" file has: {_visualize_whitespace(best_line)}\n"
|
||||
f" you sent: {_visualize_whitespace(old_lines[0])}\n"
|
||||
"Use the exact whitespace shown in 'file has'."
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_no_match_hint(error: Optional[str], match_count: int,
|
||||
|
|
|
|||
Loading…
Reference in New Issue