tzero-mcp 10.7.0 → 10.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/bin/tzero-mcp.js +9 -16
  2. package/bin/tzero.js +9 -16
  3. package/index.js +7 -2
  4. package/package.json +2 -1
  5. package/src/addmcp.py +1194 -0
  6. package/src/main.py +60 -0
  7. package/src/siber_akademi_icon.py +77 -0
  8. package/src/tzero/jarvis/semantic_compressor.py +373 -0
  9. package/src/tzero.py +58 -0
  10. package/src/tzero_benchmark.py +475 -0
  11. package/src/tzero_bridge.py +362 -0
  12. package/src/tzero_ci.py +153 -0
  13. package/src/tzero_commit.py +192 -0
  14. package/src/tzero_cost.py +163 -0
  15. package/src/tzero_dashboard.py +1448 -0
  16. package/src/tzero_deps.py +287 -0
  17. package/src/tzero_drift.py +208 -0
  18. package/src/tzero_explain.py +240 -0
  19. package/src/tzero_features.py +3022 -0
  20. package/src/tzero_fix.py +228 -0
  21. package/src/tzero_fixer/__init__.py +63 -0
  22. package/src/tzero_fixer/amalgamation.py +334 -0
  23. package/src/tzero_fixer/ast_syntax_healer.py +464 -0
  24. package/src/tzero_fixer/ast_type_modernizer.py +288 -0
  25. package/src/tzero_fixer/async_io_modernizer.py +127 -0
  26. package/src/tzero_fixer/automated_regression_suite.py +600 -0
  27. package/src/tzero_fixer/benchmarks_and_diagnostics.py +174 -0
  28. package/src/tzero_fixer/ci_cd_self_healing_gate.py +246 -0
  29. package/src/tzero_fixer/code_duplicity_and_refactorer.py +151 -0
  30. package/src/tzero_fixer/code_smells_healer.py +260 -0
  31. package/src/tzero_fixer/cwe_remediator.py +171 -0
  32. package/src/tzero_fixer/dead_code_eliminator.py +150 -0
  33. package/src/tzero_fixer/dependency_vulnerability_auditor.py +309 -0
  34. package/src/tzero_fixer/git_advanced_surgeon.py +280 -0
  35. package/src/tzero_fixer/hardware_and_disk_doctor.py +325 -0
  36. package/src/tzero_fixer/import_graph_healer.py +412 -0
  37. package/src/tzero_fixer/interactive_tui.py +91 -0
  38. package/src/tzero_fixer/knowledge_base.py +295 -0
  39. package/src/tzero_fixer/knowledge_base_extended.py +940 -0
  40. package/src/tzero_fixer/linters_and_static_analyzer.py +307 -0
  41. package/src/tzero_fixer/mcp_server.py +447 -0
  42. package/src/tzero_fixer/models.py +385 -0
  43. package/src/tzero_fixer/orchestrator.py +375 -0
  44. package/src/tzero_fixer/os_doctor.py +502 -0
  45. package/src/tzero_fixer/packaging_abi_surgeon.py +323 -0
  46. package/src/tzero_fixer/pc_fixer_doctor.py +599 -0
  47. package/src/tzero_fixer/performance_and_memory_profiler.py +274 -0
  48. package/src/tzero_fixer/polyglot_ast_parsers.py +313 -0
  49. package/src/tzero_fixer/polyglot_build_healer.py +364 -0
  50. package/src/tzero_fixer/polyglot_config_healer.py +318 -0
  51. package/src/tzero_fixer/polyglot_doctor.py +372 -0
  52. package/src/tzero_fixer/polyglot_linters.py +619 -0
  53. package/src/tzero_fixer/polyglot_schema_validator.py +305 -0
  54. package/src/tzero_fixer/security_cwe_database.py +444 -0
  55. package/src/tzero_fixer/security_deep_rules.py +114 -0
  56. package/src/tzero_fixer/security_rules_catalog.py +110 -0
  57. package/src/tzero_fixer/security_zero_leak.py +326 -0
  58. package/src/tzero_fixer/smart_patch_recommender.py +223 -0
  59. package/src/tzero_fixer/synthetic_workspace_generator.py +220 -0
  60. package/src/tzero_fixer/system_diagnostics_extended.py +493 -0
  61. package/src/tzero_fixer/system_repair_recipes.py +447 -0
  62. package/src/tzero_fixer/venv_doctor.py +309 -0
  63. package/src/tzero_gateway.py +885 -0
  64. package/src/tzero_guard.py +243 -0
  65. package/src/tzero_infra.py +274 -0
  66. package/src/tzero_jarvis/__init__.py +124 -0
  67. package/src/tzero_jarvis/causal_graph.py +310 -0
  68. package/src/tzero_jarvis/graph_visualizer.py +395 -0
  69. package/src/tzero_jarvis/hybrid_search.py +214 -0
  70. package/src/tzero_jarvis/latent_press.py +137 -0
  71. package/src/tzero_jarvis/memory_consolidation.py +212 -0
  72. package/src/tzero_jarvis/minhash_lsh.py +184 -0
  73. package/src/tzero_jarvis/orchestrator.py +335 -0
  74. package/src/tzero_jarvis/self_healing.py +308 -0
  75. package/src/tzero_jarvis/semantic_compressor.py +887 -0
  76. package/src/tzero_jarvis/swarm.py +261 -0
  77. package/src/tzero_jarvis/swarm_bft.py +144 -0
  78. package/src/tzero_jarvis/workspace_orchestrator.py +1167 -0
  79. package/src/tzero_jarvis/zero_key_router.py +173 -0
  80. package/src/tzero_mcp.py +4858 -0
  81. package/src/tzero_neural_cortex.py +1425 -0
  82. package/src/tzero_packer.py +370 -0
  83. package/src/tzero_patch.py +624 -0
  84. package/src/tzero_pc_fixer.py +12657 -0
  85. package/src/tzero_pipe.py +95 -0
  86. package/src/tzero_polyglot.py +422 -0
  87. package/src/tzero_reachability.py +272 -0
  88. package/src/tzero_replay.py +141 -0
  89. package/src/tzero_security.py +570 -0
  90. package/src/tzero_snapshot.py +258 -0
  91. package/src/tzero_stubs.py +313 -0
  92. package/src/tzero_superdesign.py +774 -0
  93. package/src/tzero_sync.py +241 -0
  94. package/src/tzero_task_benchmark.py +363 -0
  95. package/src/tzero_tui.py +244 -0
  96. package/src/tzero_v3.py +9435 -0
@@ -0,0 +1,624 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Siber Akademi — T-Zero Smart Micro-Patch & AST-Tolerant Fuzzy Patcher.
4
+ Developer: Toprak Ahmet Aydoğmuş (Siber Akademi)
5
+ License: MIT
6
+
7
+ 1. SmartMicroPatcher: Compresses standard git diffs and unified diffs into ultra-dense
8
+ semantic AST micro-patches, reducing token payload by 80% to 90% for LLM workflows.
9
+ 2. ASTFuzzyPatcher: Applies AI/LLM diffs and patches to codebase files with multi-tier
10
+ tolerance against line-drift, whitespace discrepancies, indentation mismatches,
11
+ AST symbol boundary shifts, and sliding-window fuzzy similarity.
12
+ """
13
+
14
+ import sys
15
+ if sys.platform == "win32":
16
+ try:
17
+ sys.stdout.reconfigure(encoding="utf-8")
18
+ sys.stderr.reconfigure(encoding="utf-8")
19
+ except Exception:
20
+ pass
21
+
22
+ import ast
23
+ import difflib
24
+ import os
25
+ import re
26
+ from dataclasses import dataclass, field
27
+ from typing import Tuple, List, Dict, Any, Optional
28
+
29
+
30
+ # ==============================================================================
31
+ # 1. SMART MICRO-PATCH COMPRESSOR
32
+ # ==============================================================================
33
+
34
+ class SmartMicroPatcher:
35
+ """Condenses Git diffs and file deltas into semantic AST micro-patches."""
36
+
37
+ @staticmethod
38
+ def compress_diff(raw_diff: str, max_context_lines: int = 1) -> Tuple[str, float]:
39
+ """Compresses unified git diff into a compact semantic patch.
40
+
41
+ Args:
42
+ raw_diff: Standard output from 'git diff' or unified diff.
43
+ max_context_lines: Maximum surrounding context lines to retain per hunk (default 1).
44
+
45
+ Returns:
46
+ (compact_micro_patch, token_savings_percentage)
47
+ """
48
+ orig_len = len(raw_diff)
49
+ if orig_len < 40:
50
+ return raw_diff, 0.0
51
+
52
+ lines = raw_diff.splitlines()
53
+ output = []
54
+ current_file = ""
55
+ in_hunk = False
56
+ hunk_changes = []
57
+ trailing_context = []
58
+
59
+ def flush_hunk():
60
+ nonlocal hunk_changes, trailing_context, in_hunk
61
+ if hunk_changes:
62
+ trimmed_trailing = trailing_context[:max_context_lines]
63
+ output.extend(hunk_changes + trimmed_trailing)
64
+ hunk_changes = []
65
+ trailing_context = []
66
+ in_hunk = False
67
+
68
+ for line in lines:
69
+ if line.startswith("diff --git "):
70
+ flush_hunk()
71
+ m = re.search(r"b/(.*)$", line)
72
+ current_file = m.group(1) if m else line[11:]
73
+ output.append(f"\n📁 [{current_file}]")
74
+ continue
75
+
76
+ if line.startswith(("index ", "--- ", "+++ ", "new file mode ", "deleted file mode ")):
77
+ continue
78
+
79
+ if line.startswith("@@"):
80
+ flush_hunk()
81
+ hunk_match = re.match(r"@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@(.*)", line)
82
+ if hunk_match:
83
+ start_line = hunk_match.group(1)
84
+ symbol_scope = hunk_match.group(2).strip()
85
+ scope_tag = f" :: {symbol_scope}" if symbol_scope else ""
86
+ output.append(f" ⚡ L{start_line}{scope_tag}")
87
+ else:
88
+ output.append(f" ⚡ {line.strip()}")
89
+ in_hunk = True
90
+ continue
91
+
92
+ if in_hunk:
93
+ if line.startswith(("+", "-")) and not line.startswith(("+++", "---")):
94
+ flush_trailing = trailing_context[:max_context_lines]
95
+ if flush_trailing:
96
+ hunk_changes.extend(flush_trailing)
97
+ trailing_context = []
98
+ hunk_changes.append(f" {line}")
99
+ elif line.startswith(" "):
100
+ if not hunk_changes:
101
+ if len(trailing_context) < max_context_lines:
102
+ trailing_context.append(f" {line}")
103
+ else:
104
+ trailing_context = [f" {line}"]
105
+ else:
106
+ if len(trailing_context) < max_context_lines:
107
+ trailing_context.append(f" {line}")
108
+ else:
109
+ flush_hunk()
110
+
111
+ flush_hunk()
112
+ compact_res = "\n".join(output).strip()
113
+ savings = max(0.0, ((orig_len - len(compact_res)) / max(orig_len, 1)) * 100.0)
114
+ return compact_res, savings
115
+
116
+ @staticmethod
117
+ def format_micro_patch_report(raw_diff: str, max_context_lines: int = 1) -> str:
118
+ """Formats an AI-ready Markdown report containing the micro-patch."""
119
+ compact, savings = SmartMicroPatcher.compress_diff(raw_diff, max_context_lines=max_context_lines)
120
+ approx_orig_tokens = max(1, int(len(raw_diff) / 3.8))
121
+ approx_compact_tokens = max(1, int(len(compact) / 3.8))
122
+
123
+ return (
124
+ f"# 🧩 T-Zero AST Smart Micro-Patch\n"
125
+ f"- **Raw Diff Tokens:** ~{approx_orig_tokens:,} tokens\n"
126
+ f"- **Compressed Patch Tokens:** ~{approx_compact_tokens:,} tokens\n"
127
+ f"- **Payload Compression:** **-{savings:.1f}% savings**\n\n"
128
+ f"```patch\n{compact}\n```"
129
+ )
130
+
131
+
132
+ # ==============================================================================
133
+ # 2. AST-TOLERANT FUZZY PATCHER
134
+ # ==============================================================================
135
+
136
+ @dataclass
137
+ class PatchHunk:
138
+ """Represents a parsed diff hunk with context, removals, and additions."""
139
+ old_start: int
140
+ old_length: int
141
+ new_start: int
142
+ new_length: int
143
+ symbol_scope: str = ""
144
+ lines: List[str] = field(default_factory=list)
145
+ old_lines: List[str] = field(default_factory=list)
146
+ new_lines: List[str] = field(default_factory=list)
147
+ hunk_index: int = 0
148
+
149
+
150
+ @dataclass
151
+ class FilePatch:
152
+ """Represents hunks targeting a specific file path."""
153
+ source_file: Optional[str] = None
154
+ target_file: Optional[str] = None
155
+ hunks: List[PatchHunk] = field(default_factory=list)
156
+
157
+
158
+ class ASTFuzzyPatcher:
159
+ """Multi-tier AST-Tolerant and Whitespace-Agnostic Fuzzy Patcher.
160
+
161
+ Tiers of tolerance:
162
+ 1. exact: Line-exact match at hunk header coordinates.
163
+ 2. line_drift: Content-exact match across entire file regardless of line offset.
164
+ 3. whitespace_normalized: Strips leading/trailing spaces and adapts indentation.
165
+ 4. ast_symbol_boundary: Bounded search within enclosing function/class/method AST scope.
166
+ 5. fuzzy_similarity: Sliding-window Levenshtein / SequenceMatcher match (ratio >= tolerance).
167
+ """
168
+
169
+ @classmethod
170
+ def parse_patch(cls, patch_text: str) -> List[FilePatch]:
171
+ """Parses unified diff, git diff, or raw hunks into FilePatch structures."""
172
+ lines = patch_text.splitlines()
173
+ file_patches: List[FilePatch] = []
174
+ current_file_patch = FilePatch()
175
+ current_hunk: Optional[PatchHunk] = None
176
+ hunk_idx = 0
177
+
178
+ hunk_header_re = re.compile(r"^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$")
179
+
180
+ for line in lines:
181
+ # Git file header: diff --git a/foo.py b/foo.py
182
+ if line.startswith("diff --git "):
183
+ if current_hunk:
184
+ current_file_patch.hunks.append(current_hunk)
185
+ current_hunk = None
186
+ if current_file_patch.hunks or current_file_patch.target_file:
187
+ file_patches.append(current_file_patch)
188
+ m = re.search(r"b/(.*)$", line)
189
+ target = m.group(1).strip() if m else None
190
+ current_file_patch = FilePatch(target_file=target)
191
+ continue
192
+
193
+ # Standard diff headers: --- a/foo.py, +++ b/foo.py
194
+ if line.startswith("--- ") and not line.startswith("--- /dev/null"):
195
+ m = re.search(r"(?:a/|\b)(.+?)(?:\t.*|$)", line[4:].strip())
196
+ if m:
197
+ current_file_patch.source_file = m.group(1).strip()
198
+ continue
199
+
200
+ if line.startswith("+++ ") and not line.startswith("+++ /dev/null"):
201
+ m = re.search(r"(?:b/|\b)(.+?)(?:\t.*|$)", line[4:].strip())
202
+ if m:
203
+ current_file_patch.target_file = m.group(1).strip()
204
+ continue
205
+
206
+ # Hunk header: @@ -10,5 +10,6 @@ def my_func():
207
+ m_hunk = hunk_header_re.match(line)
208
+ if m_hunk:
209
+ if current_hunk:
210
+ current_file_patch.hunks.append(current_hunk)
211
+ hunk_idx += 1
212
+ old_s = int(m_hunk.group(1))
213
+ old_l = int(m_hunk.group(2)) if m_hunk.group(2) is not None else 1
214
+ new_s = int(m_hunk.group(3))
215
+ new_l = int(m_hunk.group(4)) if m_hunk.group(4) is not None else 1
216
+ scope = m_hunk.group(5).strip()
217
+
218
+ current_hunk = PatchHunk(
219
+ old_start=old_s,
220
+ old_length=old_l,
221
+ new_start=new_s,
222
+ new_length=new_l,
223
+ symbol_scope=scope,
224
+ hunk_index=hunk_idx
225
+ )
226
+ continue
227
+
228
+ # Hunk body lines
229
+ if current_hunk is not None:
230
+ if line.startswith("+"):
231
+ current_hunk.lines.append(line)
232
+ current_hunk.new_lines.append(line[1:])
233
+ elif line.startswith("-"):
234
+ current_hunk.lines.append(line)
235
+ current_hunk.old_lines.append(line[1:])
236
+ elif line.startswith(" "):
237
+ current_hunk.lines.append(line)
238
+ current_hunk.old_lines.append(line[1:])
239
+ current_hunk.new_lines.append(line[1:])
240
+ elif line == "":
241
+ # Empty context line
242
+ current_hunk.lines.append(" ")
243
+ current_hunk.old_lines.append("")
244
+ current_hunk.new_lines.append("")
245
+ elif line.startswith("\\"):
246
+ #
247
+ continue
248
+
249
+ if current_hunk:
250
+ current_file_patch.hunks.append(current_hunk)
251
+ if current_file_patch.hunks or current_file_patch.target_file:
252
+ file_patches.append(current_file_patch)
253
+
254
+ if not file_patches and current_hunk is not None:
255
+ file_patches.append(FilePatch(hunks=[current_hunk]))
256
+
257
+ return file_patches
258
+
259
+ @classmethod
260
+ def extract_symbol_name(cls, scope_hint: str) -> Optional[str]:
261
+ """Extracts identifier name from hunk symbol scope header (e.g., 'def charge(x):')."""
262
+ if not scope_hint:
263
+ return None
264
+ m = re.search(r"(?:def|class|func|fn|function|struct|impl)\s+([A-Za-z0-9_]+)", scope_hint)
265
+ if m:
266
+ return m.group(1)
267
+ # Fallback: look for identifier followed by parens
268
+ m2 = re.search(r"([A-Za-z0-9_]{3,})\s*\(", scope_hint)
269
+ return m2.group(1) if m2 else None
270
+
271
+ @classmethod
272
+ def find_symbol_range(cls, lines: List[str], symbol_name: str) -> Optional[Tuple[int, int]]:
273
+ """Finds line range [start, end] for a symbol in lines using AST or polyglot indentation."""
274
+ if not symbol_name:
275
+ return None
276
+
277
+ # 1. Try Python AST if file is Python
278
+ code_str = "\n".join(lines)
279
+ try:
280
+ tree = ast.parse(code_str)
281
+ for node in ast.walk(tree):
282
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
283
+ if node.name == symbol_name:
284
+ start_line = max(0, node.lineno - 1)
285
+ end_line = getattr(node, "end_lineno", len(lines))
286
+ return (start_line, min(len(lines), end_line))
287
+ except Exception:
288
+ pass
289
+
290
+ # 2. Universal Polyglot Fallback: Regex + Indentation Scope
291
+ sym_pattern = re.compile(r"(?:def|class|func|fn|function|struct|impl|pub fn)\s+" + re.escape(symbol_name) + r"\b")
292
+ for i, line in enumerate(lines):
293
+ if sym_pattern.search(line):
294
+ # Found symbol definition start
295
+ base_indent = len(line) - len(line.lstrip())
296
+ end_i = len(lines)
297
+ for j in range(i + 1, len(lines)):
298
+ cur_line = lines[j]
299
+ if not cur_line.strip():
300
+ continue
301
+ cur_indent = len(cur_line) - len(cur_line.lstrip())
302
+ if cur_indent <= base_indent:
303
+ end_i = j
304
+ break
305
+ return (i, end_i)
306
+
307
+ return None
308
+
309
+ @classmethod
310
+ def adjust_indentation(cls, target_lines: List[str], match_idx: int, old_lines: List[str], new_lines: List[str]) -> List[str]:
311
+ """Adjusts leading indentation of new_lines to match target_lines at match_idx."""
312
+ if not new_lines or not old_lines or match_idx >= len(target_lines):
313
+ return list(new_lines)
314
+
315
+ # Detect target indentation
316
+ target_line = target_lines[match_idx] if match_idx < len(target_lines) else ""
317
+ target_indent_len = len(target_line) - len(target_line.lstrip())
318
+ target_indent_str = target_line[:target_indent_len]
319
+
320
+ # Detect old hunk indentation
321
+ first_old = old_lines[0] if old_lines else ""
322
+ old_indent_len = len(first_old) - len(first_old.lstrip())
323
+ old_indent_str = first_old[:old_indent_len]
324
+
325
+ if target_indent_str == old_indent_str:
326
+ return list(new_lines)
327
+
328
+ adjusted: List[str] = []
329
+ for line in new_lines:
330
+ if not line.strip():
331
+ adjusted.append("")
332
+ continue
333
+ cur_indent_len = len(line) - len(line.lstrip())
334
+ rel_diff = cur_indent_len - old_indent_len
335
+ if rel_diff >= 0:
336
+ adjusted.append(target_indent_str + (" " * rel_diff) + line.lstrip())
337
+ else:
338
+ cut = max(0, len(target_indent_str) + rel_diff)
339
+ adjusted.append(target_indent_str[:cut] + line.lstrip())
340
+
341
+ return adjusted
342
+
343
+ @classmethod
344
+ def match_hunk(
345
+ cls,
346
+ target_lines: List[str],
347
+ hunk: PatchHunk,
348
+ fuzzy: bool = True,
349
+ tolerance: float = 0.75
350
+ ) -> Tuple[bool, int, int, str, float, List[str]]:
351
+ """Finds match slice [start_idx : end_idx] for hunk with multi-tier tolerance.
352
+
353
+ Returns:
354
+ (matched, start_idx, end_idx, strategy, confidence, adjusted_new_lines)
355
+ """
356
+ k = len(hunk.old_lines)
357
+
358
+ # Handle pure addition hunk (no old lines to replace)
359
+ if k == 0:
360
+ exp_pos = max(0, min(len(target_lines), hunk.old_start - 1))
361
+ return True, exp_pos, exp_pos, "exact_insertion", 1.0, list(hunk.new_lines)
362
+
363
+ expected_start = max(0, min(len(target_lines), hunk.old_start - 1))
364
+ norm_old = [line.strip() for line in hunk.old_lines]
365
+
366
+ # Extract symbol scope range if present in hunk header
367
+ symbol_name = cls.extract_symbol_name(hunk.symbol_scope)
368
+ sym_range = cls.find_symbol_range(target_lines, symbol_name) if symbol_name else None
369
+
370
+ # ----------------------------------------------------------------------
371
+ # Tier 1: Exact coordinates & content
372
+ # ----------------------------------------------------------------------
373
+ # Only accept exact match if expected_start is not outside a specified symbol scope
374
+ if expected_start + k <= len(target_lines):
375
+ if target_lines[expected_start : expected_start + k] == hunk.old_lines:
376
+ if not sym_range or (sym_range[0] <= expected_start < sym_range[1]):
377
+ return True, expected_start, expected_start + k, "exact", 1.0, list(hunk.new_lines)
378
+
379
+ # ----------------------------------------------------------------------
380
+ # Tier 2: Line drift search across entire file (exact content)
381
+ # ----------------------------------------------------------------------
382
+ exact_matches = []
383
+ for i in range(len(target_lines) - k + 1):
384
+ if target_lines[i : i + k] == hunk.old_lines:
385
+ exact_matches.append(i)
386
+
387
+ if exact_matches:
388
+ if sym_range:
389
+ sym_matches = [idx for idx in exact_matches if sym_range[0] <= idx < sym_range[1]]
390
+ if sym_matches:
391
+ best_idx = min(sym_matches, key=lambda idx: abs(idx - expected_start))
392
+ strat = "ast_symbol_boundary" if len(exact_matches) > 1 else "line_drift"
393
+ return True, best_idx, best_idx + k, strat, 1.0, list(hunk.new_lines)
394
+ best_idx = min(exact_matches, key=lambda idx: abs(idx - expected_start))
395
+ return True, best_idx, best_idx + k, "line_drift", 1.0, list(hunk.new_lines)
396
+
397
+ if not fuzzy:
398
+ return False, -1, -1, "none", 0.0, []
399
+
400
+ # ----------------------------------------------------------------------
401
+ # Tier 3: Whitespace & Indentation Normalized Search
402
+ # ----------------------------------------------------------------------
403
+ ws_matches = []
404
+ for i in range(len(target_lines) - k + 1):
405
+ candidate = [target_lines[i + j].strip() for j in range(k)]
406
+ if candidate == norm_old:
407
+ ws_matches.append(i)
408
+
409
+ if ws_matches:
410
+ if sym_range:
411
+ sym_ws_matches = [idx for idx in ws_matches if sym_range[0] <= idx < sym_range[1]]
412
+ if sym_ws_matches:
413
+ best_idx = min(sym_ws_matches, key=lambda idx: abs(idx - expected_start))
414
+ strat = "ast_symbol_boundary" if len(ws_matches) > 1 else "whitespace_normalized"
415
+ adjusted = cls.adjust_indentation(target_lines, best_idx, hunk.old_lines, hunk.new_lines)
416
+ return True, best_idx, best_idx + k, strat, 0.95, adjusted
417
+ best_idx = min(ws_matches, key=lambda idx: abs(idx - expected_start))
418
+ adjusted = cls.adjust_indentation(target_lines, best_idx, hunk.old_lines, hunk.new_lines)
419
+ return True, best_idx, best_idx + k, "whitespace_normalized", 0.95, adjusted
420
+
421
+ # ----------------------------------------------------------------------
422
+ # Tier 5: Sliding-Window Fuzzy Similarity (Levenshtein / SequenceMatcher)
423
+ # ----------------------------------------------------------------------
424
+ search_range = range(sym_range[0], max(sym_range[0], sym_range[1] - k + 1)) if sym_range else range(len(target_lines) - k + 1)
425
+ old_block = "\n".join(norm_old)
426
+ best_ratio = 0.0
427
+ best_idx = -1
428
+
429
+ for i in search_range:
430
+ candidate_block = "\n".join(target_lines[i + j].strip() for j in range(k))
431
+ ratio = difflib.SequenceMatcher(None, candidate_block, old_block).ratio()
432
+ distance_penalty = min(0.12, abs(i - expected_start) * 0.0003)
433
+ effective_score = ratio - distance_penalty
434
+ if effective_score > best_ratio:
435
+ best_ratio = effective_score
436
+ best_idx = i
437
+
438
+ if best_idx != -1 and best_ratio >= tolerance:
439
+ strat = "fuzzy_similarity"
440
+ adjusted = cls.adjust_indentation(target_lines, best_idx, hunk.old_lines, hunk.new_lines)
441
+ return True, best_idx, best_idx + k, strat, round(best_ratio, 3), adjusted
442
+
443
+ return False, -1, -1, "none", 0.0, []
444
+
445
+ @classmethod
446
+ def apply_fuzzy_patch(
447
+ cls,
448
+ source_content: str,
449
+ patch_content: str,
450
+ fuzzy: bool = True,
451
+ tolerance: float = 0.75,
452
+ dry_run: bool = False,
453
+ reverse: bool = False
454
+ ) -> Tuple[bool, str, Dict[str, Any]]:
455
+ """Applies patch to source_content with multi-tier tolerance.
456
+
457
+ Args:
458
+ source_content: Original file source code.
459
+ patch_content: Unified diff, git diff, or hunk text.
460
+ fuzzy: Enable whitespace, AST, and similarity tolerance (default: True).
461
+ tolerance: Minimum similarity ratio for Tier 5 fuzzy match (default: 0.75).
462
+ dry_run: Validate applicability without returning mutated code.
463
+ reverse: Un-apply patch (swaps old_lines and new_lines).
464
+
465
+ Returns:
466
+ (success, patched_content, metadata_report)
467
+ """
468
+ file_patches = cls.parse_patch(patch_content)
469
+ all_hunks: List[PatchHunk] = []
470
+ for fp in file_patches:
471
+ all_hunks.extend(fp.hunks)
472
+
473
+ if not all_hunks:
474
+ return False, source_content, {
475
+ "success": False,
476
+ "error": "No valid diff hunks detected in patch content",
477
+ "applied_hunks": 0,
478
+ "total_hunks": 0
479
+ }
480
+
481
+ # Handle reverse patch mode
482
+ if reverse:
483
+ for h in all_hunks:
484
+ h.old_lines, h.new_lines = h.new_lines, h.old_lines
485
+ h.old_start, h.new_start = h.new_start, h.old_start
486
+ h.old_length, h.new_length = h.new_length, h.old_length
487
+
488
+ target_lines = source_content.splitlines()
489
+ hunk_results: List[Dict[str, Any]] = []
490
+
491
+ # Step 1: Match each hunk against target lines
492
+ # To avoid offset collisions, find match positions first or apply bottom-to-top
493
+ matches = []
494
+ for h in all_hunks:
495
+ matched, start_idx, end_idx, strategy, conf, adj_lines = cls.match_hunk(
496
+ target_lines, h, fuzzy=fuzzy, tolerance=tolerance
497
+ )
498
+ matches.append({
499
+ "hunk": h,
500
+ "matched": matched,
501
+ "start_idx": start_idx,
502
+ "end_idx": end_idx,
503
+ "strategy": strategy,
504
+ "confidence": conf,
505
+ "adjusted_lines": adj_lines
506
+ })
507
+
508
+ # Check if all hunks were matched
509
+ all_matched = all(m["matched"] for m in matches)
510
+
511
+ # Sort matches in descending order of start_idx to apply bottom-to-top safely
512
+ sorted_matches = sorted(
513
+ [m for m in matches if m["matched"]],
514
+ key=lambda m: m["start_idx"],
515
+ reverse=True
516
+ )
517
+
518
+ if all_matched and not dry_run:
519
+ for m in sorted_matches:
520
+ s, e = m["start_idx"], m["end_idx"]
521
+ target_lines[s:e] = m["adjusted_lines"]
522
+
523
+ for m in matches:
524
+ h = m["hunk"]
525
+ hunk_results.append({
526
+ "hunk_index": h.hunk_index,
527
+ "status": "applied" if m["matched"] else "rejected",
528
+ "strategy": m["strategy"],
529
+ "confidence": m["confidence"],
530
+ "old_start": h.old_start,
531
+ "matched_line": (m["start_idx"] + 1) if m["matched"] else None,
532
+ "scope_symbol": h.symbol_scope
533
+ })
534
+
535
+ applied_count = sum(1 for m in matches if m["matched"])
536
+ patched_text = "\n".join(target_lines)
537
+ if source_content.endswith("\n") and not patched_text.endswith("\n"):
538
+ patched_text += "\n"
539
+
540
+ meta = {
541
+ "success": all_matched,
542
+ "total_hunks": len(all_hunks),
543
+ "applied_hunks": applied_count,
544
+ "failed_hunks": len(all_hunks) - applied_count,
545
+ "strategies_used": list({m["strategy"] for m in matches if m["matched"]}),
546
+ "hunk_results": hunk_results,
547
+ "dry_run": dry_run,
548
+ "reverse": reverse,
549
+ "original_lines": len(source_content.splitlines()),
550
+ "patched_lines": len(patched_text.splitlines())
551
+ }
552
+
553
+ return all_matched, (patched_text if all_matched and not dry_run else source_content), meta
554
+
555
+ @classmethod
556
+ def patch_file(
557
+ cls,
558
+ target_file_path: str,
559
+ patch_text_or_path: str,
560
+ in_place: bool = True,
561
+ fuzzy: bool = True,
562
+ tolerance: float = 0.75,
563
+ dry_run: bool = False,
564
+ backup: bool = False
565
+ ) -> Dict[str, Any]:
566
+ """Patches a file on disk using AST-tolerant fuzzy matching.
567
+
568
+ Args:
569
+ target_file_path: Absolute or relative path to target file.
570
+ patch_text_or_path: Diff string or path to .diff/.patch file.
571
+ in_place: Write changes directly to target_file_path (default: True).
572
+ fuzzy: Enable fuzzy and AST matching (default: True).
573
+ tolerance: Fuzzy matching threshold (default: 0.75).
574
+ dry_run: Verify patch applicability without writing to disk.
575
+ backup: Create .bak file before modifying (default: False).
576
+
577
+ Returns:
578
+ Dictionary with operation status and hunk metrics.
579
+ """
580
+ if not os.path.exists(target_file_path):
581
+ return {
582
+ "success": False,
583
+ "error": f"Target file not found: {target_file_path}",
584
+ "target_file": target_file_path
585
+ }
586
+
587
+ # Resolve patch content
588
+ if os.path.isfile(patch_text_or_path):
589
+ with open(patch_text_or_path, "r", encoding="utf-8", errors="replace") as pf:
590
+ patch_content = pf.read()
591
+ else:
592
+ patch_content = patch_text_or_path
593
+
594
+ with open(target_file_path, "r", encoding="utf-8", errors="replace") as tf:
595
+ original_code = tf.read()
596
+
597
+ success, patched_code, meta = cls.apply_fuzzy_patch(
598
+ original_code,
599
+ patch_content,
600
+ fuzzy=fuzzy,
601
+ tolerance=tolerance,
602
+ dry_run=dry_run
603
+ )
604
+
605
+ meta["target_file"] = target_file_path
606
+
607
+ if success and in_place and not dry_run:
608
+ if backup:
609
+ bak_path = f"{target_file_path}.bak"
610
+ with open(bak_path, "w", encoding="utf-8") as bf:
611
+ bf.write(original_code)
612
+ meta["backup_created"] = bak_path
613
+
614
+ with open(target_file_path, "w", encoding="utf-8") as out_f:
615
+ out_f.write(patched_code)
616
+ meta["file_written"] = True
617
+
618
+ return meta
619
+
620
+
621
+ # Global convenience aliases
622
+ apply_fuzzy_patch = ASTFuzzyPatcher.apply_fuzzy_patch
623
+ patch_file = ASTFuzzyPatcher.patch_file
624
+