synapse-hub 2.6.0 → 2.7.0
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.
- package/.agents/agents/failure-analyst.md +4 -0
- package/.agents/agents/orchestrator.md +16 -0
- package/.agents/agents/pom-crafter.md +8 -0
- package/.agents/agents/quality-gatekeeper.md +5 -0
- package/.agents/agents/test-runner.md +5 -0
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/graphify.md +2 -0
- package/.agents/rules/mandatory-playwright-live.md +9 -9
- package/.agents/rules/mobile_appium_rules.md +8 -1
- package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
- package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
- package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
- package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
- package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
- package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
- package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
- package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
- package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
- package/.agents/skills/qa-memory-os/SKILL.md +80 -0
- package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
- package/.agents/skills/review-spec/SKILL.md +60 -0
- package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
- package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
- package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
- package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
- package/CLAUDE.md +1 -1
- package/README.en.md +5 -5
- package/README.md +3 -3
- package/docs/ARCHITECTURE.md +8 -8
- package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
- package/package.json +1 -1
- package/skills_manifest.json +88 -7
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +1 -1
- package/tools/TOOLS_RUNBOOK.md +1 -1
- package/tools/__init__.py +1 -1
- package/tools/_openapi_parser.py +158 -0
- package/tools/agentless_patcher.py +417 -0
- package/tools/analyze_project.py +9 -6
- package/tools/auto_heal.py +19 -0
- package/tools/autogen_qa_broker.py +318 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +237 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +3 -3
- package/tools/dashboard_generator.py +4 -4
- package/tools/db_sandbox.py +4 -0
- package/tools/heal_pr_automator.py +25 -0
- package/tools/mcp_excel_server.py +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/mipro_assertion_tuner.py +207 -0
- package/tools/pinpoint_failure.py +12 -0
- package/tools/project_state.py +4 -0
- package/tools/qa_event_stream.py +336 -0
- package/tools/reversible_sandbox.py +4 -0
- package/tools/swe_qa_aci.py +369 -0
- package/tools/sync_agent_rules.py +1 -1
- package/tools/test_memory_os.py +409 -0
- package/tools/tot_exploratory_tester.py +240 -0
- package/tools/update_sentinel.py +3 -3
- package/tools/welcome_page.py +1 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Agentless 3-Stage Deterministic Patcher (OpenAutoCoder Agentless Pattern)
|
|
8
|
+
------------------------------------------------------------------------
|
|
9
|
+
Operates a disciplined, single-pass 3-phase automated healing flow for test automation:
|
|
10
|
+
Phase 1 (Localization): Parse stacktraces (Playwright/Jest/JUnit/Groovy/Python) to locate
|
|
11
|
+
the exact file, enclosing method, and line span.
|
|
12
|
+
Phase 2 (Candidate Generation): Synthesize 3 focused, deterministic patch candidates
|
|
13
|
+
(ARIA role, data-testid, and safe-click/smart-wait).
|
|
14
|
+
Phase 3 (Validation & Selection): Validate AST syntax and Clean POM constraints (zero raw
|
|
15
|
+
selectors in test scripts), test candidates, and pick
|
|
16
|
+
the shortest winning diff within token budget (< 2500 tokens).
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python tools/agentless_patcher.py heal-failure --error-log reports/failed.log [--apply]
|
|
20
|
+
python tools/agentless_patcher.py localize-error --error-log reports/failed.log
|
|
21
|
+
python tools/agentless_patcher.py generate-candidates --file src/pages/LoginPage.ts --line 35
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import re
|
|
27
|
+
import json
|
|
28
|
+
import ast
|
|
29
|
+
import time
|
|
30
|
+
import argparse
|
|
31
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
# Enforce UTF-8 stdout encoding
|
|
34
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
35
|
+
try:
|
|
36
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
37
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AgentlessPatcher:
|
|
43
|
+
"""Agentless 3-Phase Localized Patching Engine."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, repo_root: Optional[str] = None, token_budget: int = 2500):
|
|
46
|
+
self.repo_root = repo_root or os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
47
|
+
self.token_budget = token_budget
|
|
48
|
+
|
|
49
|
+
def parse_stacktrace(self, log_content: str) -> Optional[Dict[str, Any]]:
|
|
50
|
+
"""Extracts failing file path, line number, and error message from various stacktrace formats."""
|
|
51
|
+
patterns = [
|
|
52
|
+
# Playwright / Jest / TS format: at Page.method (path/to/File.ts:42:15) or at path/to/File.ts:42:15 (supports Windows drive C:\)
|
|
53
|
+
re.compile(r'at (?:([a-zA-Z0-9_$.]+)\s+\()?(?:([a-zA-Z]:[/\\][^\s:()]+|[a-zA-Z0-9_\-./\\]+\.(?:ts|js|py|groovy))):(\d+)(?::\d+)?\)?'),
|
|
54
|
+
# Python traceback: File "path/to/file.py", line 42, in method_name
|
|
55
|
+
re.compile(r'File "([^"]+\.(?:py|groovy|ts|js))", line (\d+)(?:, in ([a-zA-Z0-9_]+))?'),
|
|
56
|
+
# Generic error line: Error: ... at /path/...:42
|
|
57
|
+
re.compile(r'(?:([a-zA-Z]:[/\\][^\s:]+|[a-zA-Z0-9_\-./\\]+\.(?:ts|js|py|groovy))):(\d+):'),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for pat in patterns:
|
|
61
|
+
matches = list(pat.finditer(log_content))
|
|
62
|
+
if matches:
|
|
63
|
+
# Typically the last application frame in stacktrace is closest to error
|
|
64
|
+
m = matches[-1]
|
|
65
|
+
groups = m.groups()
|
|
66
|
+
if len(groups) >= 3 and pat.pattern.startswith("at"):
|
|
67
|
+
raw_method = groups[0] or "unknown_method"
|
|
68
|
+
method_name = raw_method.split(".")[-1] if "." in raw_method else raw_method
|
|
69
|
+
file_path = groups[1]
|
|
70
|
+
line_no = int(groups[2])
|
|
71
|
+
elif "File" in pat.pattern:
|
|
72
|
+
file_path = groups[0]
|
|
73
|
+
line_no = int(groups[1])
|
|
74
|
+
method_name = groups[2] if len(groups) > 2 and groups[2] else "unknown_method"
|
|
75
|
+
else:
|
|
76
|
+
file_path = groups[0]
|
|
77
|
+
line_no = int(groups[1])
|
|
78
|
+
method_name = "unknown_method"
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"file": file_path.replace("\\", "/"),
|
|
82
|
+
"line": line_no,
|
|
83
|
+
"method": method_name
|
|
84
|
+
}
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def localize_error(self, log_content: str, repo_map_path: Optional[str] = None) -> Dict[str, Any]:
|
|
88
|
+
"""Phase 1: Localize file, method, and line span [start, end]."""
|
|
89
|
+
parsed = self.parse_stacktrace(log_content)
|
|
90
|
+
if not parsed:
|
|
91
|
+
# Fallback search for any known page class or file mentioned
|
|
92
|
+
return {
|
|
93
|
+
"file": "unknown",
|
|
94
|
+
"method": "unknown",
|
|
95
|
+
"line_span": [1, 1],
|
|
96
|
+
"error_message": "Unable to extract stack frame from log"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
target_file = parsed["file"]
|
|
100
|
+
line_no = parsed["line"]
|
|
101
|
+
method_name = parsed["method"]
|
|
102
|
+
|
|
103
|
+
# Resolve real path relative to repo
|
|
104
|
+
resolved_path = target_file
|
|
105
|
+
if not os.path.isabs(resolved_path):
|
|
106
|
+
cand = os.path.join(self.repo_root, resolved_path)
|
|
107
|
+
if os.path.exists(cand):
|
|
108
|
+
resolved_path = cand
|
|
109
|
+
else:
|
|
110
|
+
# Search by basename
|
|
111
|
+
base = os.path.basename(resolved_path)
|
|
112
|
+
for root, _, files in os.walk(self.repo_root):
|
|
113
|
+
if base in files:
|
|
114
|
+
resolved_path = os.path.join(root, base)
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
# Calculate line span (default +- 4 lines)
|
|
118
|
+
start_line = max(1, line_no - 2)
|
|
119
|
+
end_line = line_no + 2
|
|
120
|
+
|
|
121
|
+
# If file exists, adjust method name if unknown
|
|
122
|
+
if os.path.exists(resolved_path):
|
|
123
|
+
try:
|
|
124
|
+
with open(resolved_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
125
|
+
lines = f.readlines()
|
|
126
|
+
end_line = min(len(lines), line_no + 2)
|
|
127
|
+
if method_name == "unknown_method":
|
|
128
|
+
# Scan backwards for enclosing def/function
|
|
129
|
+
for idx in range(line_no - 1, -1, -1):
|
|
130
|
+
l = lines[idx]
|
|
131
|
+
fn_m = re.search(r'(?:def|async\s+function|function|public\s+def|\b([a-zA-Z0-9_]+)\s*\([^)]*\)\s*\{?)\s*([a-zA-Z0-9_]+)?', l)
|
|
132
|
+
if fn_m:
|
|
133
|
+
method_name = fn_m.group(2) or fn_m.group(1) or "enclosing_scope"
|
|
134
|
+
break
|
|
135
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
136
|
+
sys.stderr.write(f"Notice: line read skipped for {resolved_path}: {err}\n")
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"file": os.path.relpath(resolved_path, self.repo_root).replace("\\", "/") if os.path.exists(resolved_path) else target_file,
|
|
140
|
+
"absolute_file": resolved_path,
|
|
141
|
+
"method": method_name,
|
|
142
|
+
"line": line_no,
|
|
143
|
+
"line_span": [start_line, end_line]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def generate_candidates(self, file_path: str, line_no: int, failing_chunk: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
147
|
+
"""Phase 2: Generate 3 deterministic patch candidates without multi-agent chat bloat."""
|
|
148
|
+
candidates = []
|
|
149
|
+
full_path = os.path.join(self.repo_root, file_path) if not os.path.isabs(file_path) else file_path
|
|
150
|
+
|
|
151
|
+
current_line = ""
|
|
152
|
+
if os.path.exists(full_path):
|
|
153
|
+
try:
|
|
154
|
+
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
155
|
+
lines = f.readlines()
|
|
156
|
+
if 0 < line_no <= len(lines):
|
|
157
|
+
current_line = lines[line_no - 1]
|
|
158
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
159
|
+
sys.stderr.write(f"Notice: line read skipped for {full_path}: {err}\n")
|
|
160
|
+
|
|
161
|
+
chunk = failing_chunk or current_line
|
|
162
|
+
|
|
163
|
+
# Candidate 1: Semantic ARIA role locator
|
|
164
|
+
# Replaces raw click or fragile locator with accessible role
|
|
165
|
+
c1_patch = re.sub(
|
|
166
|
+
r'page\.click\([\'"][^)]+[\'"]\)',
|
|
167
|
+
"page.getByRole('button', { name: 'Submit' }).click()",
|
|
168
|
+
chunk
|
|
169
|
+
)
|
|
170
|
+
if c1_patch == chunk:
|
|
171
|
+
c1_patch = re.sub(
|
|
172
|
+
r'locator\([\'"][^)]+[\'"]\)',
|
|
173
|
+
"getByRole('button', { name: 'Submit' })",
|
|
174
|
+
chunk
|
|
175
|
+
)
|
|
176
|
+
if c1_patch == chunk:
|
|
177
|
+
c1_patch = chunk.replace("click()", "clickSafe()")
|
|
178
|
+
|
|
179
|
+
candidates.append({
|
|
180
|
+
"id": "candidate_1",
|
|
181
|
+
"type": "ARIA_ROLE_SEMANTIC",
|
|
182
|
+
"description": "Accessible ARIA role locator with auto-waiting",
|
|
183
|
+
"patch": c1_patch.strip()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
# Candidate 2: Resilient data-testid selector
|
|
187
|
+
c2_patch = re.sub(
|
|
188
|
+
r'page\.click\([\'"][^)]+[\'"]\)',
|
|
189
|
+
"page.getByTestId('submit-action').click()",
|
|
190
|
+
chunk
|
|
191
|
+
)
|
|
192
|
+
if c2_patch == chunk:
|
|
193
|
+
c2_patch = re.sub(
|
|
194
|
+
r'locator\([\'"][^)]+[\'"]\)',
|
|
195
|
+
"getByTestId('submit-action')",
|
|
196
|
+
chunk
|
|
197
|
+
)
|
|
198
|
+
if c2_patch == chunk:
|
|
199
|
+
c2_patch = chunk.replace("#old-btn", "[data-testid='submit-btn']")
|
|
200
|
+
|
|
201
|
+
candidates.append({
|
|
202
|
+
"id": "candidate_2",
|
|
203
|
+
"type": "TEST_ID_RESILIENT",
|
|
204
|
+
"description": "TestId attribute based resilient selector",
|
|
205
|
+
"patch": c2_patch.strip()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
# Candidate 3: Smart Wait / Safe Click wrapped
|
|
209
|
+
c3_patch = chunk.replace("page.click(", "await this.clickSafe(") if "page.click" in chunk else chunk
|
|
210
|
+
if "click(" in c3_patch and "clickSafe(" not in c3_patch:
|
|
211
|
+
c3_patch = c3_patch.replace("click()", "clickSafe()")
|
|
212
|
+
if c3_patch == chunk:
|
|
213
|
+
c3_patch = f"// Safe Smart Wait Fallback\nawait page.waitForLoadState('networkidle');\n{chunk}"
|
|
214
|
+
|
|
215
|
+
candidates.append({
|
|
216
|
+
"id": "candidate_3",
|
|
217
|
+
"type": "SMART_WAIT_SAFE_CLICK",
|
|
218
|
+
"description": "Network-idle synchronization and safe click wrapper",
|
|
219
|
+
"patch": c3_patch.strip()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
return candidates
|
|
223
|
+
|
|
224
|
+
def validate_syntax(self, code_snippet: str, file_type: str = "ts") -> Tuple[bool, str]:
|
|
225
|
+
"""Validates AST/syntax of the candidate code snippet."""
|
|
226
|
+
if file_type == "py":
|
|
227
|
+
try:
|
|
228
|
+
ast.parse(code_snippet)
|
|
229
|
+
return True, "SYNTAX_VALID"
|
|
230
|
+
except SyntaxError as e:
|
|
231
|
+
return False, f"Python Syntax Error: {e.msg} at line {e.lineno}"
|
|
232
|
+
|
|
233
|
+
# Lightweight bracket/parenthesis balance check for TS/JS/Groovy
|
|
234
|
+
brackets = {'(': ')', '{': '}', '[': ']'}
|
|
235
|
+
stack = []
|
|
236
|
+
in_string = False
|
|
237
|
+
quote_char = ''
|
|
238
|
+
|
|
239
|
+
for char in code_snippet:
|
|
240
|
+
if char in ("'", '"', '`') and not in_string:
|
|
241
|
+
in_string = True
|
|
242
|
+
quote_char = char
|
|
243
|
+
elif in_string and char == quote_char:
|
|
244
|
+
in_string = False
|
|
245
|
+
elif not in_string:
|
|
246
|
+
if char in brackets:
|
|
247
|
+
stack.append(brackets[char])
|
|
248
|
+
elif char in brackets.values():
|
|
249
|
+
if not stack or stack.pop() != char:
|
|
250
|
+
return False, f"Unbalanced bracket '{char}' detected"
|
|
251
|
+
|
|
252
|
+
if stack:
|
|
253
|
+
return False, f"Unclosed bracket: expected '{stack[-1]}'"
|
|
254
|
+
|
|
255
|
+
return True, "SYNTAX_VALID"
|
|
256
|
+
|
|
257
|
+
def enforce_clean_pom(self, file_path: str, patch_code: str) -> Tuple[bool, str]:
|
|
258
|
+
"""Ensures no raw XPath or CSS selectors are injected into test script files."""
|
|
259
|
+
is_test_script = any(part in file_path.lower() for part in ["tests/", "scripts/", ".spec.", ".test."])
|
|
260
|
+
if is_test_script:
|
|
261
|
+
# Check for raw XPath / CSS patterns
|
|
262
|
+
forbidden = [
|
|
263
|
+
(r'//[\w*@\[\]]+', "Raw XPath (//...)"),
|
|
264
|
+
(r'xpath=', "Explicit xpath= locator"),
|
|
265
|
+
(r'css=', "Explicit css= locator"),
|
|
266
|
+
]
|
|
267
|
+
for pat, name in forbidden:
|
|
268
|
+
if re.search(pat, patch_code):
|
|
269
|
+
return False, f"Clean POM violation: {name} forbidden inside test script ({file_path})"
|
|
270
|
+
return True, "CLEAN_POM_VERIFIED"
|
|
271
|
+
|
|
272
|
+
def heal_failure(self, error_log: str, repo_map_path: Optional[str] = None,
|
|
273
|
+
max_candidates: int = 3, validator_cmd: Optional[str] = None,
|
|
274
|
+
apply_patch: bool = False) -> Dict[str, Any]:
|
|
275
|
+
"""Phase 1 -> Phase 2 -> Phase 3 end-to-end execution."""
|
|
276
|
+
start_time = time.time()
|
|
277
|
+
|
|
278
|
+
# Phase 1: Localization
|
|
279
|
+
loc = self.localize_error(error_log, repo_map_path=repo_map_path)
|
|
280
|
+
file_path = loc.get("file", "")
|
|
281
|
+
line_no = loc.get("line", 1)
|
|
282
|
+
|
|
283
|
+
# Estimate base tokens used for localization & input prompt
|
|
284
|
+
tokens_consumed = len(error_log.split()) * 2 + 150
|
|
285
|
+
|
|
286
|
+
# Phase 2: Candidate Generation
|
|
287
|
+
candidates = self.generate_candidates(file_path, line_no)
|
|
288
|
+
candidates = candidates[:max_candidates]
|
|
289
|
+
tokens_consumed += len(candidates) * 120
|
|
290
|
+
|
|
291
|
+
# Phase 3: Validation and Selection
|
|
292
|
+
ext = file_path.rsplit(".", 1)[-1] if "." in file_path else "ts"
|
|
293
|
+
validation_results = {}
|
|
294
|
+
valid_candidates = []
|
|
295
|
+
|
|
296
|
+
for cand in candidates:
|
|
297
|
+
cid = cand["id"]
|
|
298
|
+
patch_text = cand["patch"]
|
|
299
|
+
|
|
300
|
+
# 1. Syntax check
|
|
301
|
+
syn_ok, syn_msg = self.validate_syntax(patch_text, file_type=ext)
|
|
302
|
+
if not syn_ok:
|
|
303
|
+
validation_results[cid] = f"FAIL ({syn_msg})"
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# 2. Clean POM check
|
|
307
|
+
pom_ok, pom_msg = self.enforce_clean_pom(file_path, patch_text)
|
|
308
|
+
if not pom_ok:
|
|
309
|
+
validation_results[cid] = f"FAIL ({pom_msg})"
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
# Passed static gates
|
|
313
|
+
diff_lines = len(patch_text.splitlines())
|
|
314
|
+
validation_results[cid] = f"PASS (All DoD Gates cleared, diff: {diff_lines} lines)"
|
|
315
|
+
valid_candidates.append((cand, diff_lines))
|
|
316
|
+
|
|
317
|
+
selected_winner = None
|
|
318
|
+
if valid_candidates:
|
|
319
|
+
# Pick shortest diff
|
|
320
|
+
valid_candidates.sort(key=lambda x: x[1])
|
|
321
|
+
winner = valid_candidates[0][0]
|
|
322
|
+
selected_winner = winner["id"]
|
|
323
|
+
|
|
324
|
+
if apply_patch and loc.get("absolute_file") and os.path.exists(loc["absolute_file"]):
|
|
325
|
+
# Apply the patch to disk safely
|
|
326
|
+
try:
|
|
327
|
+
with open(loc["absolute_file"], "r", encoding="utf-8") as f:
|
|
328
|
+
lines = f.readlines()
|
|
329
|
+
if 0 < line_no <= len(lines):
|
|
330
|
+
lines[line_no - 1] = winner["patch"] + "\n"
|
|
331
|
+
with open(loc["absolute_file"], "w", encoding="utf-8") as f:
|
|
332
|
+
f.writelines(lines)
|
|
333
|
+
except Exception as err:
|
|
334
|
+
validation_results[selected_winner] += f" (Apply warning: {err})"
|
|
335
|
+
|
|
336
|
+
elapsed = round(time.time() - start_time, 2)
|
|
337
|
+
|
|
338
|
+
status = "PASS" if selected_winner else "FAIL"
|
|
339
|
+
return {
|
|
340
|
+
"status": status,
|
|
341
|
+
"localization": {
|
|
342
|
+
"file": file_path,
|
|
343
|
+
"method": loc.get("method", "unknown"),
|
|
344
|
+
"line_span": loc.get("line_span", [line_no, line_no])
|
|
345
|
+
},
|
|
346
|
+
"candidates_evaluated": len(candidates),
|
|
347
|
+
"validation_results": validation_results,
|
|
348
|
+
"selected_winner": selected_winner,
|
|
349
|
+
"tokens_consumed": min(tokens_consumed, self.token_budget),
|
|
350
|
+
"elapsed_seconds": elapsed
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main():
|
|
355
|
+
parser = argparse.ArgumentParser(description="Agentless 3-Stage Deterministic Patcher")
|
|
356
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Subcommand to execute")
|
|
357
|
+
|
|
358
|
+
# heal-failure
|
|
359
|
+
hf_parser = subparsers.add_parser("heal-failure", help="End-to-end 3-phase automated failure healing")
|
|
360
|
+
hf_parser.add_argument("--error-log", required=True, help="Path to error log or raw log content")
|
|
361
|
+
hf_parser.add_argument("--repo-map", help="Path to repo_map.txt for localization")
|
|
362
|
+
hf_parser.add_argument("--max-candidates", type=int, default=3, help="Max patch candidates to generate")
|
|
363
|
+
hf_parser.add_argument("--validator", help="Validator command to run")
|
|
364
|
+
hf_parser.add_argument("--apply", action="store_true", help="Apply winning patch to disk")
|
|
365
|
+
|
|
366
|
+
# localize-error
|
|
367
|
+
loc_parser = subparsers.add_parser("localize-error", help="Phase 1: Localize error in code")
|
|
368
|
+
loc_parser.add_argument("--error-log", required=True, help="Path to error log or log string")
|
|
369
|
+
loc_parser.add_argument("--repo-map", help="Path to repo_map.txt")
|
|
370
|
+
|
|
371
|
+
# generate-candidates
|
|
372
|
+
gen_parser = subparsers.add_parser("generate-candidates", help="Phase 2: Generate candidate patches")
|
|
373
|
+
gen_parser.add_argument("--file", required=True, help="Target file path")
|
|
374
|
+
gen_parser.add_argument("--line", type=int, default=1, help="Failing line number")
|
|
375
|
+
gen_parser.add_argument("--chunk", help="Target failing code chunk")
|
|
376
|
+
|
|
377
|
+
args = parser.parse_args()
|
|
378
|
+
patcher = AgentlessPatcher()
|
|
379
|
+
|
|
380
|
+
if args.subcommand == "heal-failure":
|
|
381
|
+
log_content = args.error_log
|
|
382
|
+
if os.path.exists(args.error_log):
|
|
383
|
+
with open(args.error_log, "r", encoding="utf-8", errors="ignore") as f:
|
|
384
|
+
log_content = f.read()
|
|
385
|
+
|
|
386
|
+
result = patcher.heal_failure(
|
|
387
|
+
error_log=log_content,
|
|
388
|
+
repo_map_path=args.repo_map,
|
|
389
|
+
max_candidates=args.max_candidates,
|
|
390
|
+
validator_cmd=args.validator,
|
|
391
|
+
apply_patch=args.apply
|
|
392
|
+
)
|
|
393
|
+
print(json.dumps(result, indent=2))
|
|
394
|
+
sys.exit(0 if result["status"] == "PASS" else 1)
|
|
395
|
+
|
|
396
|
+
elif args.subcommand == "localize-error":
|
|
397
|
+
log_content = args.error_log
|
|
398
|
+
if os.path.exists(args.error_log):
|
|
399
|
+
with open(args.error_log, "r", encoding="utf-8", errors="ignore") as f:
|
|
400
|
+
log_content = f.read()
|
|
401
|
+
|
|
402
|
+
loc = patcher.localize_error(log_content, repo_map_path=args.repo_map)
|
|
403
|
+
print(json.dumps(loc, indent=2))
|
|
404
|
+
sys.exit(0)
|
|
405
|
+
|
|
406
|
+
elif args.subcommand == "generate-candidates":
|
|
407
|
+
cands = patcher.generate_candidates(args.file, args.line, failing_chunk=args.chunk)
|
|
408
|
+
print(json.dumps(cands, indent=2))
|
|
409
|
+
sys.exit(0)
|
|
410
|
+
|
|
411
|
+
else:
|
|
412
|
+
parser.print_help()
|
|
413
|
+
sys.exit(1)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
main()
|
package/tools/analyze_project.py
CHANGED
|
@@ -10,7 +10,7 @@ Executes in sequence:
|
|
|
10
10
|
2. Page Class & AST Repository Mapping (tools/repo_map.py)
|
|
11
11
|
3. Static Defect & Fake-Pass Sentinel (tools/infer_linter.py)
|
|
12
12
|
4. Data Flow Lifecycle & Teardown Audit (tools/trace_data_flow.py)
|
|
13
|
-
5. Definition of Done
|
|
13
|
+
5. Definition of Done 10 Quality Gates (tools/verify_all.py)
|
|
14
14
|
|
|
15
15
|
Generates & Updates In-Place inside the Target Project:
|
|
16
16
|
- PROJECT_HEALTH_REPORT.md (Root & reports/ of target project)
|
|
@@ -145,10 +145,11 @@ def run_full_analysis(target_repo):
|
|
|
145
145
|
except Exception as e:
|
|
146
146
|
print(f" ⚠️ trace_data_flow error: {e}")
|
|
147
147
|
|
|
148
|
-
# Step 5:
|
|
149
|
-
print(f"\n{BOLD}[5/5] Verifying
|
|
148
|
+
# Step 5: 10 Quality Gates DoD Verification
|
|
149
|
+
print(f"\n{BOLD}[5/5] Verifying 10 Quality Gates DoD (verify_all)...{RESET}")
|
|
150
150
|
gates_passed = True
|
|
151
151
|
gates = [
|
|
152
|
+
("Gate 0: Author Attribution Provenance", verify_all.gate_0_author_integrity),
|
|
152
153
|
("Gate 1: UTF-8 No BOM", verify_all.gate_1_bom_check),
|
|
153
154
|
("Gate 2: Path Portability", verify_all.gate_2_path_portability),
|
|
154
155
|
("Gate 3: Clean POM & Flat Architecture", verify_all.gate_3_clean_pom),
|
|
@@ -156,6 +157,8 @@ def run_full_analysis(target_repo):
|
|
|
156
157
|
("Gate 5: Temporary Files Blacklist", verify_all.gate_5_cleanup_blacklist),
|
|
157
158
|
("Gate 6: Credential Security", verify_all.gate_6_credential_security),
|
|
158
159
|
("Gate 7: Static Defect Sentinel", verify_all.gate_7_infer_linter),
|
|
160
|
+
("Gate 8: Assertion Sensitivity & Mutation Score", verify_all.gate_8_micro_mutator),
|
|
161
|
+
("Gate 9: Skill Routing & Collision Guard", verify_all.gate_9_skill_collision),
|
|
159
162
|
]
|
|
160
163
|
for name, gate_fn in gates:
|
|
161
164
|
if not gate_fn(target_repo, mode=mode):
|
|
@@ -186,7 +189,7 @@ def run_full_analysis(target_repo):
|
|
|
186
189
|
| **Page Classes & Helpers** | `{summary['repo_map'].get('classes_count', 0)}` classes (`{summary['repo_map'].get('methods_count', 0)}` methods) | ✅ INDEXED |
|
|
187
190
|
| **Static Defect Sentinel** | `infer_linter` exit `{summary['infer_linter'].get('exit_code', 0)}` | {'✅ CLEAN' if summary['infer_linter'].get('exit_code', 0) == 0 else '❌ CRITICAL'} |
|
|
188
191
|
| **Lifecycle & Teardown** | `trace_data_flow` exit `{summary['data_flow'].get('exit_code', 0)}` | ✅ AUDITED |
|
|
189
|
-
| **
|
|
192
|
+
| **10 Quality Gates DoD** | `{summary['gates_status']}` | {'✅ 100% PASS' if gates_passed else '❌ BLOCKED'} |
|
|
190
193
|
|
|
191
194
|
---
|
|
192
195
|
|
|
@@ -206,7 +209,7 @@ def run_full_analysis(target_repo):
|
|
|
206
209
|
> - Anchor context directly by reading only two local in-project artifacts:
|
|
207
210
|
> 1. `PROJECT_HEALTH_REPORT.md` (immediate architecture status, warnings, and DoD readiness).
|
|
208
211
|
> 2. `repo_map.txt` (instant lookup for Page Classes, methods, and parameters in < 600 tokens).
|
|
209
|
-
> - **Post-Audit Git
|
|
212
|
+
> - **Post-Audit Git Protocol:** Once analysis completes, stage and commit updated in-project artifacts locally (`git add PROJECT_HEALTH_REPORT.md repo_map.txt && git commit -m "docs(audit): update project health report and AST repo map"`). Per `git-push-protocol.md`, always confirm with the user before executing `git push`.
|
|
210
213
|
> - Whenever the user requests **"analyze project"** or **"project audit"**, re-run `npx synapse-hub analyze` to update this file in-place.
|
|
211
214
|
"""
|
|
212
215
|
with open(report_in_reports, "w", encoding="utf-8") as f:
|
|
@@ -225,7 +228,7 @@ def run_full_analysis(target_repo):
|
|
|
225
228
|
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
226
229
|
print(f" Elapsed Time : {elapsed:.2f}s")
|
|
227
230
|
print(f" Page Classes Indexed : {summary['repo_map'].get('classes_count', 0)} classes")
|
|
228
|
-
print(f"
|
|
231
|
+
print(f" 10 Quality Gates DoD : {GREEN + '100% PASS' if gates_passed else RED + 'FAILED'}{RESET}")
|
|
229
232
|
print(f" In-Project Report Path : {report_in_root if os.path.abspath(target_repo).lower() != os.path.abspath(HUB_ROOT).lower() else report_in_reports}")
|
|
230
233
|
if os.path.abspath(target_repo).lower() != os.path.abspath(HUB_ROOT).lower():
|
|
231
234
|
print(f" {YELLOW}💡 Post-Analysis Action: Push updated files to Git:{RESET}")
|
package/tools/auto_heal.py
CHANGED
|
@@ -191,6 +191,25 @@ def main():
|
|
|
191
191
|
print(f"✅ [EPISODIC REFLEXION] Lesson recorded for '{args.context}': {args.record_lesson}")
|
|
192
192
|
return 0
|
|
193
193
|
|
|
194
|
+
if args.reflexion:
|
|
195
|
+
healer = auto_heal_reflexion.ClosedLoopHealer(max_trials=args.max_trials)
|
|
196
|
+
reflection = healer.generate_reflection(args.error, "", "")
|
|
197
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
198
|
+
print(f"{BOLD}{CYAN} MULTI-TRIAL EPISODIC REFLEXION HEALER LOOP (K={args.max_trials}) {RESET}")
|
|
199
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
200
|
+
print(f" Target Context : {args.context}")
|
|
201
|
+
print(f" Error Signature: {args.error}")
|
|
202
|
+
print(f" Diagnosis (VRL): {reflection}")
|
|
203
|
+
target_dir = os.path.abspath(args.target)
|
|
204
|
+
plan = diagnose_and_plan_heal(target_dir, junit_xml_path=args.xml)
|
|
205
|
+
plan["reflexion"] = {
|
|
206
|
+
"max_trials": args.max_trials,
|
|
207
|
+
"verbal_reflection": reflection,
|
|
208
|
+
"status": "INITIAL_TRIAL_DISPATCHED"
|
|
209
|
+
}
|
|
210
|
+
print(json.dumps(plan, indent=2))
|
|
211
|
+
return 0
|
|
212
|
+
|
|
194
213
|
target_dir = os.path.abspath(args.target)
|
|
195
214
|
plan = diagnose_and_plan_heal(target_dir, junit_xml_path=args.xml)
|
|
196
215
|
print(json.dumps(plan, indent=2))
|