From 5d675a2ca78d16a324e57403498cc4190193ede5 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:50:10 -0700 Subject: [PATCH] feat(patch): whitespace-visualized diagnosis on residual no-match errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/tools/test_patch_ws_diagnosis.py | 64 ++++++++++++++++++++++++++ tools/fuzzy_match.py | 32 ++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/tools/test_patch_ws_diagnosis.py diff --git a/tests/tools/test_patch_ws_diagnosis.py b/tests/tools/test_patch_ws_diagnosis.py new file mode 100644 index 0000000000000..fd932f4e02804 --- /dev/null +++ b/tests/tools/test_patch_ws_diagnosis.py @@ -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 diff --git a/tools/fuzzy_match.py b/tools/fuzzy_match.py index d0353dd4b6d7a..ad36e2f650e54 100644 --- a/tools/fuzzy_match.py +++ b/tools/fuzzy_match.py @@ -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,