Files
hermes-agent/tools/fuzzy_match.py
Teknium 2776813df3 compat(plugins): temporary import-path shims for external plugins — ONE commit, revert on schedule
The Sep 2026 decomposition (PR #102117) makes internal import paths a non-API: names now live in
the focused modules that define them. This commit is the ONLY thing keeping the old paths alive,
so external plugins have time to update. It is deliberately a single, unsquashed commit:

    git revert <this sha>

removes every shim, stub and manifest at once on the announced date. Nothing in-tree may depend on
these pointers: scripts/check_compat_pointers.py (wired into lint.yml) fails CI if it does.

What it adds (see COMPAT_MANIFEST.md, compat_manifest.json):
- 332 facade modules get one delimited `PLUGIN-COMPAT` block appended at the end of the file
- 1,172 moved names resolved lazily via a module `__getattr__` (PEP 562) — never a top-level import,
  so no import cycles; facades that already had `__getattr__` get a chained one
- 592 third-party/stdlib names the old modules used to expose, with their original import statements
- 266 public definitions that had been deleted as unused, restored byte-for-byte from the pre-decomposition
  tree (+40 private helpers and 16 imports pulled in only because a restored definition needs them)
- 3 deleted modules recreated as re-export stubs (gateway/startup_watchdog, hermes_cli/observability/
  relay_runtime, tools/environments/modal_utils)
- private names (`_x`) get no pointer: they were never API (3,792 skipped)

Verified: all 335 touched modules import under a fresh HERMES_HOME and every manifest name resolves;
the lint reports zero in-tree uses; ruff clean; targeted suites unchanged.
2026-09-03 17:13:22 -07:00

597 lines
27 KiB
Python

#!/usr/bin/env python3
"""Fuzzy find-and-replace for LLM-generated edits.
An ordered chain of increasingly permissive strategies (``STRATEGIES``) lets
whitespace, indentation, escaping and Unicode drift in tool-call arguments
still land on the intended region::
new_content, match_count, strategy, error = fuzzy_find_and_replace(
content, old_string, new_string, replace_all=False)
"""
import re
from difflib import SequenceMatcher
from typing import Callable, Optional
Span = tuple[int, int]
IDENTICAL_STRINGS_ERROR = (
"No edit was applied because old_string and new_string are identical. "
"Provide the existing text to replace in old_string and the changed "
"replacement text in new_string.")
UNICODE_MAP = {
"\u201c": '"', "\u201d": '"', # smart double quotes
"\u2018": "'", "\u2019": "'", # smart single quotes
"\u2014": "--", "\u2013": "-", # em/en dashes
"\u2026": "...", "\u00a0": " ", # ellipsis and non-breaking space
"\u2212": "-", # typographic minus (math/scientific docs)
# Space-separator family (Zs): otherwise such files fall to the similarity fallback.
"\u2000": " ", "\u2001": " ", "\u2002": " ", "\u2003": " ",
"\u2004": " ", "\u2005": " ", "\u2006": " ", "\u2007": " ",
"\u2008": " ", "\u2009": " ", "\u200a": " ", "\u202f": " ",
"\u205f": " ", "\u3000": " "}
def _unicode_normalize(text: str) -> str:
"""Map typographic Unicode variants to ASCII equivalents."""
for char, repl in UNICODE_MAP.items():
text = text.replace(char, repl)
return text
# ── Position helpers ─────────────────────────────────────────────────────
def _calculate_line_positions(content_lines: list[str], start_line: int,
end_line: int, content_length: int) -> Span:
"""Character span covering ``content_lines[start_line:end_line]`` (end exclusive)."""
start_pos = sum(len(line) + 1 for line in content_lines[:start_line])
end_pos = sum(len(line) + 1 for line in content_lines[:end_line]) - 1
return start_pos, min(content_length, end_pos)
def _window_spans(content: str, content_lines: list[str], n: int,
accept: Callable[[int], bool]) -> list[Span]:
"""Spans of every ``n``-line window starting at ``i`` for which ``accept(i)``."""
return [
_calculate_line_positions(content_lines, i, i + n, len(content))
for i in range(len(content_lines) - n + 1) if accept(i)]
def _match_transformed_lines(content: str, pattern: str,
transform: Callable[[list[str]], list[str]]) -> list[Span]:
"""Match ``pattern`` against ``content`` after applying ``transform`` to each line block."""
content_lines = content.split('\n')
pattern_norm = transform(pattern.split('\n'))
n = len(pattern_norm)
return _window_spans(content, content_lines, n,
lambda i: transform(content_lines[i:i + n]) == pattern_norm)
def _strip_boundary(lines: list[str]) -> list[str]:
"""Strip whitespace on the first and last lines only."""
lines = list(lines)
lines[0] = lines[0].strip()
if len(lines) > 1:
lines[-1] = lines[-1].strip()
return lines
def _build_orig_to_norm_map(original: str) -> list[int]:
"""Map each original index to its index in ``_unicode_normalize(original)``
(replacements can expand one char into several). Length ``len(original)+1``;
the last entry is a sentinel one past the final character."""
result: list[int] = []
norm_pos = 0
for char in original:
result.append(norm_pos)
repl = UNICODE_MAP.get(char)
norm_pos += len(repl) if repl is not None else 1
result.append(norm_pos)
return result
def _invert_norm_map(orig_to_norm: list[int]) -> dict[int, int]:
"""norm_pos -> first original position mapping to it."""
inverted: dict[int, int] = {}
for orig_pos, norm_pos in enumerate(orig_to_norm[:-1]):
inverted.setdefault(norm_pos, orig_pos)
return inverted
def _norm_end_to_orig(orig_to_norm: list[int], orig_start: int, norm_end: int) -> int:
"""Walk from ``orig_start`` until the mapped position reaches ``norm_end``."""
orig_len = len(orig_to_norm) - 1
orig_end = orig_start
while orig_end < orig_len and orig_to_norm[orig_end] < norm_end:
orig_end += 1
return orig_end
def _map_positions_norm_to_orig(orig_to_norm: list[int], norm_matches: list[Span]) -> list[Span]:
"""Convert spans in the normalised string to original-string spans."""
norm_to_orig_start = _invert_norm_map(orig_to_norm)
results: list[Span] = []
for norm_start, norm_end in norm_matches:
if norm_start in norm_to_orig_start:
orig_start = norm_to_orig_start[norm_start]
results.append((orig_start, _norm_end_to_orig(orig_to_norm, orig_start, norm_end)))
return results
def _map_normalized_positions(original: str, normalized: str,
normalized_matches: list[Span]) -> list[Span]:
"""Best-effort span mapping for ``[ \\t]+`` -> ``' '`` whitespace collapsing."""
orig_to_norm = [] # orig_to_norm[i] = position in normalized
orig_idx = norm_idx = 0
while orig_idx < len(original) and norm_idx < len(normalized):
if original[orig_idx] == normalized[norm_idx]:
orig_to_norm.append(norm_idx)
orig_idx += 1
norm_idx += 1
elif original[orig_idx] in ' \t' and normalized[norm_idx] == ' ':
# Collapsed run: advance norm_idx only once the run is consumed.
orig_to_norm.append(norm_idx)
orig_idx += 1
if orig_idx < len(original) and original[orig_idx] not in ' \t':
norm_idx += 1
else:
# Extra whitespace in original, or a mismatch normalization should
# never produce — either way, pin to the current norm_idx.
orig_to_norm.append(norm_idx)
orig_idx += 1
orig_to_norm.extend([len(normalized)] * (len(original) - orig_idx))
norm_to_orig_start = {}
norm_to_orig_end = {}
for orig_pos, norm_pos in enumerate(orig_to_norm):
norm_to_orig_start.setdefault(norm_pos, orig_pos)
norm_to_orig_end[norm_pos] = orig_pos
original_matches = []
for norm_start, norm_end in normalized_matches:
if norm_start in norm_to_orig_start:
orig_start = norm_to_orig_start[norm_start]
else:
orig_start = min(i for i, n in enumerate(orig_to_norm) if n >= norm_start)
if norm_end - 1 in norm_to_orig_end:
orig_end = norm_to_orig_end[norm_end - 1] + 1
else:
orig_end = orig_start + (norm_end - norm_start)
# Absorb trailing collapsed whitespace only when the normalized match
# itself ended in a space; otherwise the first whitespace after the
# match is a word boundary that must survive.
if norm_end < len(normalized) and normalized[norm_end - 1] == ' ':
while orig_end < len(original) and original[orig_end] in ' \t':
orig_end += 1
original_matches.append((orig_start, min(orig_end, len(original))))
return original_matches
# ── Strategies ───────────────────────────────────────────────────────────
# Each takes ``(content, pattern)`` and returns ``(start, end)`` spans in the
# ORIGINAL content.
def _strategy_exact(content: str, pattern: str) -> list[Span]:
"""Strategy 1: exact, non-overlapping occurrences (str.replace semantics —
overlapping spans would corrupt the file under replace_all)."""
return [m.span() for m in re.finditer(re.escape(pattern), content)]
def _strategy_line_trimmed(content: str, pattern: str) -> list[Span]:
"""Strategy 2: strip each line before comparing."""
return _match_transformed_lines(content, pattern, lambda ls: [l.strip() for l in ls])
def _strategy_whitespace_normalized(content: str, pattern: str) -> list[Span]:
"""Strategy 3: collapse runs of spaces/tabs to a single space."""
def normalize(s):
return re.sub(r'[ \t]+', ' ', s)
content_normalized = normalize(content)
matches_in_normalized = _strategy_exact(content_normalized, normalize(pattern))
if not matches_in_normalized:
return []
return _map_normalized_positions(content, content_normalized, matches_in_normalized)
def _strategy_indentation_flexible(content: str, pattern: str) -> list[Span]:
"""Strategy 4: ignore leading indentation entirely."""
return _match_transformed_lines(content, pattern, lambda ls: [l.lstrip() for l in ls])
def _strategy_escape_normalized(content: str, pattern: str) -> list[Span]:
"""Strategy 5: treat literal ``\\n``/``\\t``/``\\r`` in the pattern as control chars."""
pattern_unescaped = pattern.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
if pattern_unescaped == pattern:
return []
return _strategy_exact(content, pattern_unescaped)
def _strategy_trimmed_boundary(content: str, pattern: str) -> list[Span]:
"""Strategy 6: strip whitespace on the first and last lines only."""
return _match_transformed_lines(content, pattern, _strip_boundary)
def _strategy_unicode_normalized(content: str, pattern: str) -> list[Span]:
"""Strategy 7: exact/line-trimmed match after Unicode->ASCII normalisation of both sides."""
norm_pattern = _unicode_normalize(pattern)
norm_content = _unicode_normalize(content)
if norm_content == content and norm_pattern == pattern:
return []
norm_matches = (_strategy_exact(norm_content, norm_pattern)
or _strategy_line_trimmed(norm_content, norm_pattern))
if not norm_matches:
return []
return _map_positions_norm_to_orig(_build_orig_to_norm_map(content), norm_matches)
def _strategy_block_anchor(content: str, pattern: str) -> list[Span]:
"""Strategy 8: anchor on first+last lines, similarity-score the middle."""
pattern_lines = _unicode_normalize(pattern).split('\n')
if len(pattern_lines) < 2:
return []
first_line = pattern_lines[0].strip()
last_line = pattern_lines[-1].strip()
n = len(pattern_lines)
# Match on normalized lines; compute offsets from the ORIGINAL lines so
# multi-char expansions (em-dash -> '--') don't shift positions.
norm_content_lines = _unicode_normalize(content).split('\n')
potential_matches = {
i for i in range(len(norm_content_lines) - n + 1)
if norm_content_lines[i].strip() == first_line
and norm_content_lines[i + n - 1].strip() == last_line}
# Looser thresholds (0.10/0.30) matched unrelated blocks; these are the safe floor.
threshold = 0.50 if len(potential_matches) == 1 else 0.70
pattern_middle = '\n'.join(pattern_lines[1:-1])
def similar(i: int) -> bool:
if i not in potential_matches:
return False
if n <= 2:
return True
content_middle = '\n'.join(norm_content_lines[i + 1:i + n - 1])
return SequenceMatcher(None, content_middle, pattern_middle).ratio() >= threshold
return _window_spans(content, content.split('\n'), n, similar)
def _strategy_context_aware(content: str, pattern: str) -> list[Span]:
"""Strategy 9 (last resort): anchored per-line similarity, every non-blank line >= 0.80.
The anchor pre-filter bounds the scan; the all-lines rule stops coincidental matches."""
pattern_lines = pattern.split('\n')
content_lines = content.split('\n')
n = len(pattern_lines)
if n > len(content_lines):
return []
first_pat = pattern_lines[0].strip()
last_pat = pattern_lines[-1].strip()
def _sim(a: str, b: str) -> float:
return 1.0 if a == b else SequenceMatcher(None, a, b).ratio()
def accept(i: int) -> bool:
block_lines = content_lines[i:i + n]
if _sim(first_pat, block_lines[0].strip()) < 0.80:
return False
if _sim(last_pat, block_lines[-1].strip()) < 0.80:
return False
return all(
not p_line.strip() or _sim(p_line.strip(), c_line.strip()) >= 0.80
for p_line, c_line in zip(pattern_lines, block_lines))
return _window_spans(content, content_lines, n, accept)
# Ordered chain: precise strategies first, similarity-based last.
STRATEGIES: list[tuple[str, Callable[[str, str], list[Span]]]] = [
("exact", _strategy_exact),
("line_trimmed", _strategy_line_trimmed),
("whitespace_normalized", _strategy_whitespace_normalized),
("indentation_flexible", _strategy_indentation_flexible),
("escape_normalized", _strategy_escape_normalized),
("trimmed_boundary", _strategy_trimmed_boundary),
("unicode_normalized", _strategy_unicode_normalized),
("block_anchor", _strategy_block_anchor),
("context_aware", _strategy_context_aware)]
# Matches from these only *approximately* resemble old_string — fine for one
# unique replacement, never safe under replace_all.
SIMILARITY_STRATEGIES = frozenset({"block_anchor", "context_aware"})
# ── Orchestrator ─────────────────────────────────────────────────────────
def is_already_applied(content: str, old_string: str, new_string: str) -> bool:
"""True when the edit is already present (re-sent edit -> success-shaped no-op).
Conservative: new_string non-trivial (>= 8 chars) and present EXACTLY; old_string gone."""
if not new_string or len(new_string.strip()) < 8 or new_string not in content:
return False
return old_string == new_string or old_string not in content
def _matched_regions(content: str, matches: list[Span]) -> str:
return "".join(content[start:end] for start, end in matches)
def _format_match_locations(content: str, matches: list[Span], cap: int = 5) -> str:
"""Render up to ``cap`` match positions as 'L<line>: <snippet>' rows."""
rows = []
for start, _end in matches[:cap]:
line_no = content.count("\n", 0, start) + 1
line_start = content.rfind("\n", 0, start) + 1
line_end = content.find("\n", line_start)
if line_end == -1:
line_end = len(content)
snippet = content[line_start:line_end].strip()
if len(snippet) > 80:
snippet = snippet[:77] + "..."
rows.append(f" L{line_no}: {snippet}")
extra = len(matches) - cap
if extra > 0:
rows.append(f" ... and {extra} more")
return "\n".join(rows)
def fuzzy_find_and_replace(content: str, old_string: str, new_string: str,
replace_all: bool = False) -> tuple[str, int, Optional[str], Optional[str]]:
"""Find and replace via the strategy chain.
Returns ``(new_content, match_count, strategy_name, error)``; on failure
``(content, 0, None, error)``.
"""
if not old_string:
return content, 0, None, "old_string cannot be empty"
if not old_string.strip():
# Whitespace-only anchors match trivially and mass-replace or
# ambiguity-error; never meaningful.
return content, 0, None, "old_string is only whitespace — provide non-blank text to match"
if old_string == new_string:
return content, 0, None, IDENTICAL_STRINGS_ERROR
for strategy_name, strategy_fn in STRATEGIES:
matches = strategy_fn(content, old_string)
if not matches:
continue
if len(matches) > 1 and not replace_all:
locations = _format_match_locations(content, matches)
return content, 0, None, (
f"Found {len(matches)} matches for old_string. "
f"Provide more context to make it unique, or use replace_all=True. "
f"Matches:\n{locations}")
if replace_all and len(matches) > 1 and strategy_name in SIMILARITY_STRATEGIES:
return content, 0, None, (
f"Found {len(matches)} approximate matches via the "
f"'{strategy_name}' strategy; replace_all only applies to exact "
f"matches. Provide the precise text (whitespace included) so an "
f"exact/line-trimmed match can be made.")
# Non-exact matches came through some normalization, so new_string may
# carry serialization drift the file doesn't have.
if strategy_name != "exact":
drift_err = _detect_escape_drift(content, matches, old_string, new_string)
if drift_err:
return content, 0, None, drift_err
effective_new = _maybe_unescape_new_string(new_string, content, matches)
if strategy_name == "unicode_normalized":
effective_new = _preserve_unicode_in_replacement(content, matches, old_string, effective_new)
new_content = _apply_replacements(
content, matches, effective_new,
old_string=old_string if strategy_name != "exact" else None)
return new_content, len(matches), strategy_name, None
return content, 0, None, "Could not find a match for old_string in the file"
# ── Escape-drift guards ──────────────────────────────────────────────────
def _detect_escape_drift(content: str, matches: list[Span],
old_string: str, new_string: str) -> Optional[str]:
"""Error string when new_string carries tool-call escape artifacts, else None:
``\\'``/``\\"`` in both strings but not the matched region, or doubled backslash runs."""
has_quote_suspects = "\\'" in new_string or '\\"' in new_string
if not has_quote_suspects and "\\" not in old_string:
return None
matched_regions = _matched_regions(content, matches)
if has_quote_suspects:
for suspect in ("\\'", '\\"'):
if suspect in new_string and suspect in old_string and suspect not in matched_regions:
plain = suspect[1]
return (
f"Escape-drift detected: old_string and new_string contain "
f"the literal sequence {suspect!r} but the matched region of "
f"the file does not. This is almost always a tool-call "
f"serialization artifact where an apostrophe or quote got "
f"prefixed with a spurious backslash. Re-read the file with "
f"read_file and pass old_string/new_string without "
f"backslash-escaping {plain!r} characters.")
return _detect_backslash_doubling(matched_regions, old_string, new_string)
def _backslash_runs(s: str) -> list[int]:
"""Lengths of maximal backslash runs in ``s``, in order."""
return [len(run) for run in re.findall(r"\\+", s)]
def _detect_backslash_doubling(matched_regions: str, old_string: str,
new_string: str) -> Optional[str]:
"""Detect old_string whose every backslash run is exactly 2x the file's (arguments
JSON-escaped one extra time). Requires the same run count, a non-trivial signal
(a run >= 2 or 2+ runs), and new_string not already matching the file's counts."""
old_runs = _backslash_runs(old_string)
file_runs = _backslash_runs(matched_regions)
if (not old_runs or not file_runs or len(old_runs) != len(file_runs)
or old_runs == file_runs
or any(o != f * 2 for o, f in zip(old_runs, file_runs))
or not (any(f >= 2 for f in file_runs) or len(file_runs) >= 2)
or _backslash_runs(new_string) == file_runs):
return None
return (
"Escape-drift detected: every backslash run in old_string is exactly "
"twice as long as in the matched region of the file (e.g. the file "
"has `\\\\` where old_string has `\\\\\\\\`). The tool-call arguments "
"were JSON-escaped one extra time; applying new_string verbatim would "
"double every backslash in the file. Re-read the file with read_file "
"and resend old_string/new_string with the backslash counts exactly "
"as they appear in the file.")
def _maybe_unescape_new_string(new_string: str, content: str, matches: list[Span]) -> str:
"""Convert literal ``\\t``/``\\r`` in new_string to control chars, per sequence, only
when the matched region already contains the real control char (so ``sep = "\\t"`` files
are left alone). ``\\n`` is excluded: rewriting it would mangle source escape literals."""
if "\\t" not in new_string and "\\r" not in new_string:
return new_string
matched_regions = _matched_regions(content, matches)
for literal, control in (("\\t", "\t"), ("\\r", "\r")):
if literal in new_string and control in matched_regions:
new_string = new_string.replace(literal, control)
return new_string
# ── Replacement shaping ──────────────────────────────────────────────────
def _leading_whitespace(line: str) -> str:
return line[:len(line) - len(line.lstrip(" \t"))]
def _first_meaningful_line(text: str) -> Optional[str]:
return next((line for line in text.split("\n") if line.strip()), None)
def _reindent_replacement(file_region: str, old_string: str, new_string: str) -> str:
"""Re-anchor ``new_string``'s indentation onto the file's actual base indent after a
non-exact match: swap the LLM base prefix (first non-blank old_string line) for the
file's, preserving relative nesting; shallower lines anchor to the file base."""
if not new_string:
return new_string
old_first = _first_meaningful_line(old_string)
file_first = _first_meaningful_line(file_region)
if old_first is None or file_first is None:
return new_string
old_indent = _leading_whitespace(old_first)
file_indent = _leading_whitespace(file_first)
if old_indent == file_indent:
return new_string
out_lines: list[str] = []
for line in new_string.split("\n"):
if not line.strip():
out_lines.append(line)
elif _leading_whitespace(line).startswith(old_indent):
out_lines.append(file_indent + line[len(old_indent):])
else:
out_lines.append(file_indent + line.lstrip(" \t"))
return "\n".join(out_lines)
def _preserve_unicode_in_replacement(content: str, matches: list[Span],
old_string: str, new_string: str) -> str:
"""Apply only the old->new edits onto the file's original (Unicode) text, so a
unicode_normalized match doesn't flatten the file's em-dashes/smart quotes."""
file_region = _matched_regions(content, matches)
norm_old = _unicode_normalize(old_string)
if norm_old != _unicode_normalize(file_region):
return new_string # strategy shouldn't have fired; fall back
file_orig_to_norm = _build_orig_to_norm_map(file_region)
file_norm_to_orig = _invert_norm_map(file_orig_to_norm)
result_parts: list[str] = []
for tag, i1, i2, j1, j2 in SequenceMatcher(None, norm_old, new_string).get_opcodes():
if tag == "equal":
orig_start = file_norm_to_orig.get(i1, 0)
orig_end = _norm_end_to_orig(file_orig_to_norm, orig_start, i2)
result_parts.append(file_region[orig_start:orig_end])
elif tag != "delete":
result_parts.append(new_string[j1:j2])
return "".join(result_parts)
def _apply_replacements(content: str, matches: list[Span],
new_string: str, old_string: Optional[str] = None) -> str:
"""Splice ``new_string`` over each span (end-to-start so offsets stay valid);
``old_string`` non-None (non-exact match) re-indents it per region."""
result = content
for start, end in sorted(matches, key=lambda x: x[0], reverse=True):
adjusted = new_string
if old_string is not None:
adjusted = _reindent_replacement(content[start:end], old_string, new_string)
result = result[:start] + adjusted + result[end:]
return result
# ── "Did you mean?" diagnostics ──────────────────────────────────────────
def _visualize_whitespace(line: str) -> str:
"""Render the leading whitespace run visibly (→ = tab, · = space)."""
stripped = line.lstrip(" \t")
prefix = line[:len(line) - len(stripped)]
return prefix.replace("\t", "→").replace(" ", "·") + stripped
def find_closest_lines(old_string: str, content: str, context_lines: int = 2, max_results: int = 3) -> str:
"""Numbered snippets of the lines most similar to old_string's anchor line, or ''."""
if not old_string or not content:
return ""
old_lines = old_string.splitlines()
content_lines = content.splitlines()
if not old_lines or not content_lines:
return ""
anchor = old_lines[0].strip() or next((l.strip() for l in old_lines if l.strip()), "")
if not anchor:
return ""
scored = sorted(((SequenceMatcher(None, anchor, line.strip()).ratio(), i)
for i, line in enumerate(content_lines) if line.strip()), key=lambda x: -x[0])
top = [s for s in scored if s[0] > 0.3][:max_results]
if not top:
return ""
parts = []
seen_ranges = set()
for _, line_idx in top:
start = max(0, line_idx - context_lines)
end = min(len(content_lines), line_idx + len(old_lines) + context_lines)
if (start, end) in seen_ranges:
continue
seen_ranges.add((start, end))
parts.append("\n".join(
f"{start + j + 1:4d}| {content_lines[start + j]}" for j in range(end - start)))
result = "\n---\n".join(parts)
# Whitespace-shaped miss: best line equals the anchor once stripped. Show
# both with visible leading whitespace so the model copies the file's.
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,
old_string: str, content: str) -> str:
"""'\\n\\nDid you mean...' snippet for plain no-match errors only, else '' (ambiguous /
escape-drift / identical errors also have ``match_count == 0`` but a hint would mislead)."""
if match_count != 0 or not error or not error.startswith("Could not find"):
return ""
hint = find_closest_lines(old_string, content)
return "\n\nDid you mean one of these sections?\n" + hint if hint else ""
# ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ----
# Names external plugins imported from this module before the Sep 2026 decomposition.
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
from typing import List # noqa: F401,E402
from typing import Tuple # noqa: F401,E402
# ---- END PLUGIN-COMPAT ----