synapse-hub 2.2.2 → 2.2.3
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/rules/author-integrity-guard.md +9 -10
- package/.cursorrules +7 -8
- package/.windsurfrules +7 -8
- package/AGENTS.md +3 -3
- package/CLAUDE.md +4 -5
- package/README.md +28 -26
- package/package.json +1 -1
- package/skills_manifest.json +2 -2
- package/tools/author_integrity.py +10 -4
- package/tools/browser_helpers.js +2 -2
- package/tools/cache_resolver.py +16 -5
- package/tools/cli.py +1 -1
- package/tools/distill_log.py +19 -10
- package/tools/infer_linter.py +45 -15
- package/tools/mcp_excel_server.ps1 +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/micro_mutator.py +78 -7
- package/tools/repo_map.py +90 -56
- package/tools/setup_project.py +43 -13
- package/tools/verify_all.py +55 -47
package/tools/infer_linter.py
CHANGED
|
@@ -154,21 +154,40 @@ def analyze_script(file_path):
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
def run_infer_linter(target_dir=None):
|
|
157
|
-
"""Scans
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
"""Scans test scripts across Groovy, TypeScript, JavaScript, Python, and Java."""
|
|
158
|
+
EXTENSIONS = ("*.groovy", "*.ts", "*.js", "*.py", "*.java")
|
|
159
|
+
|
|
160
|
+
if target_dir:
|
|
161
|
+
if os.path.isfile(target_dir):
|
|
162
|
+
script_files = [target_dir]
|
|
163
|
+
scan_dir = os.path.dirname(target_dir)
|
|
164
|
+
elif os.path.isdir(target_dir):
|
|
165
|
+
scan_dir = target_dir
|
|
166
|
+
script_files = []
|
|
167
|
+
for ext in EXTENSIONS:
|
|
168
|
+
script_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
|
|
169
|
+
else:
|
|
170
|
+
scan_dir = target_dir
|
|
171
|
+
script_files = []
|
|
163
172
|
else:
|
|
164
|
-
|
|
173
|
+
# Search candidate directories across test frameworks (Katalon, Playwright, Pytest, Cypress)
|
|
174
|
+
script_files = []
|
|
175
|
+
scan_dir = SCRIPTS_DIR
|
|
176
|
+
for cand in ["Scripts", "tests", "test", "e2e", "specs", "cypress"]:
|
|
177
|
+
cand_path = os.path.join(REPO_ROOT, cand)
|
|
178
|
+
if os.path.isdir(cand_path):
|
|
179
|
+
for ext in EXTENSIONS:
|
|
180
|
+
script_files.extend(glob.glob(os.path.join(cand_path, "**", ext), recursive=True))
|
|
181
|
+
if script_files:
|
|
182
|
+
scan_dir = cand_path
|
|
183
|
+
break
|
|
165
184
|
|
|
166
185
|
results = []
|
|
167
186
|
critical_count = 0
|
|
168
187
|
warning_count = 0
|
|
169
188
|
clean_count = 0
|
|
170
189
|
|
|
171
|
-
for s in sorted(script_files):
|
|
190
|
+
for s in sorted(set(script_files)):
|
|
172
191
|
res = analyze_script(s)
|
|
173
192
|
results.append(res)
|
|
174
193
|
crit = sum(1 for d in res["defects"] if d["severity"] == "CRITICAL")
|
|
@@ -179,17 +198,28 @@ def run_infer_linter(target_dir=None):
|
|
|
179
198
|
clean_count += 1
|
|
180
199
|
|
|
181
200
|
total_scripts = len(script_files)
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
if total_scripts == 0:
|
|
202
|
+
health_score = 0.0
|
|
203
|
+
clean_pct = 0.0
|
|
204
|
+
else:
|
|
205
|
+
health_score = round(((total_scripts - critical_count) / total_scripts) * 100, 1)
|
|
206
|
+
clean_pct = round((clean_count / total_scripts) * 100, 1)
|
|
184
207
|
|
|
185
208
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
186
209
|
print(f"{BOLD}{CYAN} ENTERPRISE STATIC DEFECT & FAKE-PASS SENTINEL (Meta Infer-Style) {RESET}")
|
|
187
210
|
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
if total_scripts == 0:
|
|
212
|
+
print(f" Target Scope : 0 Test Scripts discovered in {os.path.basename(scan_dir)}/ (No tests found)")
|
|
213
|
+
print(f" Critical Defects : {GREEN}0{RESET}")
|
|
214
|
+
print(f" Warnings / Suggestions : {GREEN}0{RESET}")
|
|
215
|
+
print(f" Clean Scripts : 0/0 (N/A)")
|
|
216
|
+
print(f" Codebase Health Score : {YELLOW}N/A (0 test scripts discovered){RESET}")
|
|
217
|
+
else:
|
|
218
|
+
print(f" Target Scope : {total_scripts} Test Scripts in {os.path.basename(scan_dir)}/ (Polyglot)")
|
|
219
|
+
print(f" Critical Defects : {RED if critical_count > 0 else GREEN}{critical_count}{RESET} (Swallowed exceptions, empty scripts)")
|
|
220
|
+
print(f" Warnings / Suggestions : {YELLOW if warning_count > 0 else GREEN}{warning_count}{RESET} (Missing explicit asserts, excessive delays)")
|
|
221
|
+
print(f" Clean Scripts : {GREEN}{clean_count}/{total_scripts} ({clean_pct:.1f}%){RESET}")
|
|
222
|
+
print(f" Codebase Health Score : {GREEN if health_score == 100 else YELLOW}{health_score:.1f}%{RESET}")
|
|
193
223
|
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
194
224
|
|
|
195
225
|
# Print defect breakdown if any
|
|
@@ -160,7 +160,7 @@ while (($line = $inputReader.ReadLine()) -ne $null) {
|
|
|
160
160
|
result = @{
|
|
161
161
|
protocolVersion = "2024-11-05"
|
|
162
162
|
capabilities = @{ tools = @{} }
|
|
163
|
-
serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.2.
|
|
163
|
+
serverInfo = @{ name = "excel-qa-mcp-server"; version = "2.2.3" }
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
[System.Console]::Out.WriteLine(($resp | ConvertTo-Json -Compress -Depth 10))
|
package/tools/mcp_server.py
CHANGED
package/tools/micro_mutator.py
CHANGED
|
@@ -68,7 +68,7 @@ ASSERTION_PATTERN = re.compile(
|
|
|
68
68
|
|
|
69
69
|
# Tautological / fake assertions that do not test application logic
|
|
70
70
|
TAUTOLOGY_PATTERN = re.compile(
|
|
71
|
-
r'(?:assert\s+(?:true|1\s*==\s*1)\b|assertEquals\s*\(\s*
|
|
71
|
+
r'(?:assert\s+(?:true|1\s*==\s*1)\b|assert\s+([a-zA-Z_]\w*)\s*==\s*\1\b|assertEquals\s*\(\s*([a-zA-Z_]\w*)\s*,\s*\2\s*\)|expect\s*\(\s*([a-zA-Z_]\w*)\s*\)\.toBe\s*\(\s*\3\s*\)|assertTrue\s*\(\s*true\s*\))',
|
|
72
72
|
re.IGNORECASE
|
|
73
73
|
)
|
|
74
74
|
|
|
@@ -195,7 +195,12 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
195
195
|
- If mutant is inside an unasserted block or swallowed exception: mutant SURVIVES.
|
|
196
196
|
"""
|
|
197
197
|
if not mutants:
|
|
198
|
-
|
|
198
|
+
has_asserts = any(
|
|
199
|
+
ASSERTION_PATTERN.search(l) and not TAUTOLOGY_PATTERN.search(l)
|
|
200
|
+
for l in code.splitlines()
|
|
201
|
+
if not l.strip().startswith(("//", "#", "/*"))
|
|
202
|
+
)
|
|
203
|
+
return mutants, (100.0 if has_asserts else 0.0)
|
|
199
204
|
|
|
200
205
|
lines = code.splitlines()
|
|
201
206
|
has_empty_catch = bool(EMPTY_CATCH_PATTERN.search(code))
|
|
@@ -245,6 +250,55 @@ def evaluate_mutants_statically(code: str, mutants: List[Dict[str, Any]]) -> Tup
|
|
|
245
250
|
return mutants, score
|
|
246
251
|
|
|
247
252
|
|
|
253
|
+
def evaluate_mutants_dynamically(file_path: str, code: str, mutants: List[Dict[str, Any]], test_cmd: str) -> Tuple[List[Dict[str, Any]], float]:
|
|
254
|
+
"""
|
|
255
|
+
Executes live mutation testing by injecting mutants into disk file and executing test_cmd.
|
|
256
|
+
If the test command fails (exit code != 0), the mutant is KILLED by the test suite.
|
|
257
|
+
If the test command passes (exit code == 0), the mutant SURVIVED (unasserted logic branch).
|
|
258
|
+
Safely restores original file content in all cases.
|
|
259
|
+
"""
|
|
260
|
+
if not mutants:
|
|
261
|
+
return mutants, 0.0
|
|
262
|
+
|
|
263
|
+
killed_count = 0
|
|
264
|
+
abs_path = os.path.abspath(file_path)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
for m in mutants:
|
|
268
|
+
mutated_code = m["mutated_code"]
|
|
269
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
270
|
+
f.write(mutated_code)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
proc = subprocess.run(
|
|
274
|
+
test_cmd,
|
|
275
|
+
shell=True,
|
|
276
|
+
capture_output=True,
|
|
277
|
+
timeout=30,
|
|
278
|
+
text=True
|
|
279
|
+
)
|
|
280
|
+
if proc.returncode != 0:
|
|
281
|
+
m["status"] = "KILLED"
|
|
282
|
+
m["reason"] = f"Dynamic test caught mutant: command failed (exit code {proc.returncode})"
|
|
283
|
+
killed_count += 1
|
|
284
|
+
else:
|
|
285
|
+
m["status"] = "SURVIVED"
|
|
286
|
+
m["reason"] = "Dynamic test suite passed unconditionally: missing assertion for this mutant."
|
|
287
|
+
except subprocess.TimeoutExpired:
|
|
288
|
+
m["status"] = "KILLED"
|
|
289
|
+
m["reason"] = "Mutant triggered execution timeout (> 30s): caught by runtime guard"
|
|
290
|
+
killed_count += 1
|
|
291
|
+
except Exception as e:
|
|
292
|
+
m["status"] = "SURVIVED"
|
|
293
|
+
m["reason"] = f"Test execution error: {e}"
|
|
294
|
+
finally:
|
|
295
|
+
with open(abs_path, "w", encoding="utf-8") as f:
|
|
296
|
+
f.write(code)
|
|
297
|
+
|
|
298
|
+
score = evaluate_mutation_score(killed_count, len(mutants))
|
|
299
|
+
return mutants, score
|
|
300
|
+
|
|
301
|
+
|
|
248
302
|
def scan_directory_for_mutants(target_dir: str) -> List[Dict[str, Any]]:
|
|
249
303
|
"""Scans target test/page scripts and generates a collection of mutants."""
|
|
250
304
|
all_mutants = []
|
|
@@ -269,21 +323,23 @@ def scan_directory_for_mutants(target_dir: str) -> List[Dict[str, Any]]:
|
|
|
269
323
|
def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "") -> Dict[str, Any]:
|
|
270
324
|
"""
|
|
271
325
|
Evaluates target file or directory for mutation robustness.
|
|
272
|
-
|
|
326
|
+
Dispatches to live test execution if test_cmd is provided, or static heuristic otherwise.
|
|
273
327
|
"""
|
|
274
328
|
target = os.path.abspath(target_path)
|
|
275
329
|
all_mutants = []
|
|
330
|
+
exec_mode = "DYNAMIC" if test_cmd else "STATIC"
|
|
276
331
|
|
|
277
332
|
if os.path.isfile(target):
|
|
278
333
|
with open(target, "r", encoding="utf-8", errors="ignore") as f:
|
|
279
334
|
code = f.read()
|
|
280
335
|
mutants = generate_mutants_for_code(code, file_path=os.path.basename(target))
|
|
281
|
-
|
|
336
|
+
if test_cmd:
|
|
337
|
+
evaluated_mutants, score = evaluate_mutants_dynamically(target, code, mutants, test_cmd)
|
|
338
|
+
else:
|
|
339
|
+
evaluated_mutants, score = evaluate_mutants_statically(code, mutants)
|
|
282
340
|
all_mutants.extend(evaluated_mutants)
|
|
283
341
|
else:
|
|
284
|
-
# Directory evaluation
|
|
285
342
|
valid_exts = {".groovy", ".py", ".ts", ".js"}
|
|
286
|
-
total_killed = 0
|
|
287
343
|
for root, _, files in os.walk(target):
|
|
288
344
|
for f in files:
|
|
289
345
|
if any(f.endswith(ext) for ext in valid_exts):
|
|
@@ -293,7 +349,10 @@ def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "
|
|
|
293
349
|
code = fp.read()
|
|
294
350
|
rel_path = os.path.relpath(file_full, target)
|
|
295
351
|
f_mutants = generate_mutants_for_code(code, file_path=rel_path)
|
|
296
|
-
|
|
352
|
+
if test_cmd:
|
|
353
|
+
eval_m, _ = evaluate_mutants_dynamically(file_full, code, f_mutants, test_cmd)
|
|
354
|
+
else:
|
|
355
|
+
eval_m, _ = evaluate_mutants_statically(code, f_mutants)
|
|
297
356
|
all_mutants.extend(eval_m)
|
|
298
357
|
except Exception:
|
|
299
358
|
pass
|
|
@@ -305,6 +364,18 @@ def evaluate_target(target_path: str, min_score: float = 80.0, test_cmd: str = "
|
|
|
305
364
|
survived = sum(1 for m in all_mutants if m["status"] == "SURVIVED")
|
|
306
365
|
passed = score >= min_score
|
|
307
366
|
|
|
367
|
+
return {
|
|
368
|
+
"target": target,
|
|
369
|
+
"mode": exec_mode,
|
|
370
|
+
"total_mutants": len(all_mutants),
|
|
371
|
+
"killed": killed,
|
|
372
|
+
"survived": survived,
|
|
373
|
+
"mutation_score": score,
|
|
374
|
+
"min_score": min_score,
|
|
375
|
+
"passed": passed,
|
|
376
|
+
"mutants": all_mutants
|
|
377
|
+
}
|
|
378
|
+
|
|
308
379
|
return {
|
|
309
380
|
"target": target,
|
|
310
381
|
"total_mutants": len(all_mutants),
|
package/tools/repo_map.py
CHANGED
|
@@ -21,10 +21,11 @@ HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
|
21
21
|
OUTPUT_TXT = os.path.join(SCRIPT_DIR, "repo_map.txt")
|
|
22
22
|
OUTPUT_JSON = os.path.join(SCRIPT_DIR, "repo_map.json")
|
|
23
23
|
|
|
24
|
-
# Regex patterns for Groovy classes and methods
|
|
25
|
-
CLASS_PATTERN = re.compile(r'^\s*(?:public\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?', re.MULTILINE)
|
|
24
|
+
# Regex patterns for Groovy/Java classes and methods
|
|
25
|
+
CLASS_PATTERN = re.compile(r'^\s*(?:public\s+|export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?', re.MULTILINE)
|
|
26
|
+
PY_CLASS_PATTERN = re.compile(r'^\s*class\s+(\w+)(?:\(([^)]*)\))?\s*:', re.MULTILINE)
|
|
26
27
|
METHOD_PATTERN = re.compile(
|
|
27
|
-
r'^\s*(?:@\w+\s+)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:def\s+|void\s+|[\w<>\[\],]+\s+)?(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?\s*\{',
|
|
28
|
+
r'^\s*(?:@\w+\s+)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:async\s+)?(?:def\s+|void\s+|[\w<>\[\],]+\s+)?(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?\s*\{',
|
|
28
29
|
re.MULTILINE
|
|
29
30
|
)
|
|
30
31
|
DOC_PATTERN = re.compile(r'/\*\*(.*?)\*/', re.DOTALL)
|
|
@@ -35,7 +36,7 @@ def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
|
35
36
|
Resolves the target repository path for repo mapping:
|
|
36
37
|
1. Explicit CLI flag: --repo / --path or positional target.
|
|
37
38
|
2. Environment variable: TARGET_PROJECT_DIR.
|
|
38
|
-
3. Current Working Directory (if different from Hub and has Keywords
|
|
39
|
+
3. Current Working Directory (if different from Hub and has Keywords/, pages/, or tests/).
|
|
39
40
|
4. Default fallback: QA Hub root (parent of tools/).
|
|
40
41
|
"""
|
|
41
42
|
chosen = cli_repo or cli_target
|
|
@@ -51,14 +52,15 @@ def resolve_target_repo(cli_target=None, cli_repo=None):
|
|
|
51
52
|
|
|
52
53
|
cwd = os.path.abspath(os.getcwd())
|
|
53
54
|
if cwd.lower() != HUB_ROOT.lower():
|
|
54
|
-
|
|
55
|
+
markers = ("Keywords", "Scripts", "pages", "tests", "src/pages", "e2e")
|
|
56
|
+
if any(os.path.exists(os.path.join(cwd, m)) for m in markers):
|
|
55
57
|
return cwd
|
|
56
58
|
|
|
57
59
|
return HUB_ROOT
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def extract_class_info(file_path, repo_root=None):
|
|
61
|
-
"""Parses a Groovy file and extracts class name
|
|
63
|
+
"""Parses a Groovy, TypeScript, JavaScript, or Python file and extracts class name and methods."""
|
|
62
64
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
63
65
|
content = f.read()
|
|
64
66
|
|
|
@@ -68,48 +70,72 @@ def extract_class_info(file_path, repo_root=None):
|
|
|
68
70
|
except ValueError:
|
|
69
71
|
rel_path = os.path.abspath(file_path)
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
if not class_match:
|
|
73
|
-
return None
|
|
74
|
-
|
|
75
|
-
class_name = class_match.group(1)
|
|
76
|
-
extends_name = class_match.group(2) or ""
|
|
77
|
-
|
|
73
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
78
74
|
methods = []
|
|
79
|
-
control_keywords = {"if", "for", "while", "switch", "catch", "synchronized"}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if
|
|
75
|
+
control_keywords = {"if", "for", "while", "switch", "catch", "synchronized", "constructor"}
|
|
76
|
+
|
|
77
|
+
if ext in (".groovy", ".java", ".ts", ".js"):
|
|
78
|
+
class_match = CLASS_PATTERN.search(content)
|
|
79
|
+
if not class_match:
|
|
80
|
+
return None
|
|
81
|
+
class_name = class_match.group(1)
|
|
82
|
+
extends_name = class_match.group(2) or ""
|
|
83
|
+
|
|
84
|
+
lines = content.splitlines()
|
|
85
|
+
for idx, line in enumerate(lines):
|
|
86
|
+
line_clean = line.strip()
|
|
87
|
+
if not line_clean or line_clean.startswith(("//", "/*", "*")):
|
|
92
88
|
continue
|
|
93
89
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
if "=" in line_clean:
|
|
91
|
+
eq_pos = line_clean.find("=")
|
|
92
|
+
paren_pos = line_clean.find("(")
|
|
93
|
+
if paren_pos != -1 and eq_pos < paren_pos:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
if "(" in line_clean and ")" in line_clean:
|
|
97
|
+
has_brace = "{" in line_clean or (idx + 1 < len(lines) and lines[idx + 1].strip().startswith("{"))
|
|
98
|
+
if has_brace:
|
|
99
|
+
m = re.search(r'(?:public\s+|protected\s+|private\s+|static\s+|async\s+|def\s+|void\s+|[\w<>\[\],]+\s+)*([a-zA-Z_]\w*)\s*\(([^)]*)\)', line_clean)
|
|
100
|
+
if m:
|
|
101
|
+
name = m.group(1)
|
|
102
|
+
params = m.group(2).strip()
|
|
103
|
+
if name not in control_keywords and name != class_name:
|
|
104
|
+
comment = ""
|
|
105
|
+
if idx > 0 and lines[idx - 1].strip().startswith("//"):
|
|
106
|
+
comment = lines[idx - 1].strip().lstrip("/ ").strip()
|
|
107
|
+
methods.append({
|
|
108
|
+
"name": name,
|
|
109
|
+
"params": re.sub(r'\s+', ' ', params),
|
|
110
|
+
"comment": comment
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
elif ext == ".py":
|
|
114
|
+
class_match = PY_CLASS_PATTERN.search(content)
|
|
115
|
+
if not class_match:
|
|
116
|
+
return None
|
|
117
|
+
class_name = class_match.group(1)
|
|
118
|
+
extends_name = class_match.group(2) or ""
|
|
119
|
+
|
|
120
|
+
lines = content.splitlines()
|
|
121
|
+
for idx, line in enumerate(lines):
|
|
122
|
+
line_clean = line.strip()
|
|
123
|
+
if line_clean.startswith("def "):
|
|
124
|
+
m = re.search(r'def\s+([a-zA-Z_]\w*)\s*\(([^)]*)\):', line_clean)
|
|
100
125
|
if m:
|
|
101
126
|
name = m.group(1)
|
|
102
|
-
params = m.group(2).strip()
|
|
103
|
-
if
|
|
104
|
-
# Extract preceding single-line comment if any
|
|
127
|
+
params = m.group(2).replace("self,", "").replace("self", "").strip()
|
|
128
|
+
if not name.startswith("__") and name not in control_keywords:
|
|
105
129
|
comment = ""
|
|
106
|
-
if idx
|
|
107
|
-
comment = lines[idx
|
|
130
|
+
if idx + 1 < len(lines) and '"""' in lines[idx + 1]:
|
|
131
|
+
comment = lines[idx + 1].replace('"""', '').strip()
|
|
108
132
|
methods.append({
|
|
109
133
|
"name": name,
|
|
110
134
|
"params": re.sub(r'\s+', ' ', params),
|
|
111
135
|
"comment": comment
|
|
112
136
|
})
|
|
137
|
+
else:
|
|
138
|
+
return None
|
|
113
139
|
|
|
114
140
|
# Deduplicate methods preserving order
|
|
115
141
|
seen = set()
|
|
@@ -120,6 +146,7 @@ def extract_class_info(file_path, repo_root=None):
|
|
|
120
146
|
seen.add(sig)
|
|
121
147
|
unique_methods.append(meth)
|
|
122
148
|
|
|
149
|
+
lines = content.splitlines()
|
|
123
150
|
return {
|
|
124
151
|
"rel_path": rel_path,
|
|
125
152
|
"class_name": class_name,
|
|
@@ -131,13 +158,7 @@ def extract_class_info(file_path, repo_root=None):
|
|
|
131
158
|
|
|
132
159
|
|
|
133
160
|
def build_repo_map(target_repo=None, output_txt=None, output_json=None):
|
|
134
|
-
"""Scans
|
|
135
|
-
try:
|
|
136
|
-
from author_integrity import assert_integrity_or_raise
|
|
137
|
-
assert_integrity_or_raise()
|
|
138
|
-
except Exception:
|
|
139
|
-
pass
|
|
140
|
-
|
|
161
|
+
"""Scans Page Classes across polyglot targets (Groovy, TypeScript, Python) and builds map."""
|
|
141
162
|
repo = target_repo or resolve_target_repo()
|
|
142
163
|
txt_dest = output_txt or OUTPUT_TXT
|
|
143
164
|
json_dest = output_json or OUTPUT_JSON
|
|
@@ -146,18 +167,31 @@ def build_repo_map(target_repo=None, output_txt=None, output_json=None):
|
|
|
146
167
|
total_raw_bytes = 0
|
|
147
168
|
total_raw_lines = 0
|
|
148
169
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
candidate_dirs = [
|
|
171
|
+
os.path.join(repo, "Keywords"),
|
|
172
|
+
os.path.join(repo, "pages"),
|
|
173
|
+
os.path.join(repo, "page-objects"),
|
|
174
|
+
os.path.join(repo, "page_objects"),
|
|
175
|
+
os.path.join(repo, "src", "pages"),
|
|
176
|
+
os.path.join(repo, "tests", "pages"),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
scanned_files = set()
|
|
180
|
+
for d in candidate_dirs:
|
|
181
|
+
if os.path.isdir(d):
|
|
182
|
+
for root, _, files in os.walk(d):
|
|
183
|
+
if any(ignored in root for ignored in (".git", "node_modules", "__pycache__", ".venv")):
|
|
184
|
+
continue
|
|
185
|
+
for fname in sorted(files):
|
|
186
|
+
if fname.endswith((".groovy", ".ts", ".js", ".py", ".java")):
|
|
187
|
+
full_path = os.path.join(root, fname)
|
|
188
|
+
if full_path not in scanned_files:
|
|
189
|
+
scanned_files.add(full_path)
|
|
190
|
+
info = extract_class_info(full_path, repo_root=repo)
|
|
191
|
+
if info and info.get("methods"):
|
|
192
|
+
classes.append(info)
|
|
193
|
+
total_raw_bytes += info["raw_size_bytes"]
|
|
194
|
+
total_raw_lines += info["line_count"]
|
|
161
195
|
|
|
162
196
|
# Sort classes: BasePage first, then alphabetical
|
|
163
197
|
classes.sort(key=lambda c: (0 if c["class_name"] == "BasePage" else 1, c["class_name"]))
|
package/tools/setup_project.py
CHANGED
|
@@ -175,7 +175,10 @@ def ensure_project_ai_guide(target_dir, stack_info):
|
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
def install_git_pre_push_hook(target_dir):
|
|
178
|
-
"""
|
|
178
|
+
"""
|
|
179
|
+
Installs or appends an automated pre-push hook into target project's .git repository.
|
|
180
|
+
Safely backs up any existing pre-push hook (e.g. Husky, Lefthook) to prevent side-effects.
|
|
181
|
+
"""
|
|
179
182
|
git_dir = os.path.join(target_dir, ".git")
|
|
180
183
|
if not os.path.isdir(git_dir):
|
|
181
184
|
print(" [SKIP] Not a Git repository root (.git folder not found). Skipped Git Hook installation.")
|
|
@@ -185,37 +188,64 @@ def install_git_pre_push_hook(target_dir):
|
|
|
185
188
|
os.makedirs(hooks_dir, exist_ok=True)
|
|
186
189
|
hook_file = os.path.join(hooks_dir, "pre-push")
|
|
187
190
|
|
|
191
|
+
# Resolve pinned package version
|
|
192
|
+
manifest_file = os.path.join(HUB_ROOT, "skills_manifest.json")
|
|
193
|
+
pkg_ver = "2.2.2"
|
|
194
|
+
if os.path.exists(manifest_file):
|
|
195
|
+
try:
|
|
196
|
+
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
197
|
+
pkg_ver = json.load(f).get("version", pkg_ver)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
188
201
|
verify_all_script = os.path.join(SCRIPT_DIR, "verify_all.py").replace("\\", "/")
|
|
189
202
|
analyze_script = os.path.join(SCRIPT_DIR, "analyze_project.py").replace("\\", "/")
|
|
190
203
|
|
|
191
|
-
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
echo "\\n[AI-GUARD] Running Pre-Push Verification (8 Quality Gates DoD)..."
|
|
204
|
+
hook_snippet = f"""
|
|
205
|
+
# --- Synapse-Hub Pre-Push Quality Gate (v{pkg_ver}) ---
|
|
206
|
+
echo "\\n[AI-GUARD] Running Pre-Push Verification (Quality Gates DoD)..."
|
|
196
207
|
if command -v npx >/dev/null 2>&1; then
|
|
197
|
-
npx synapse-hub verify
|
|
208
|
+
npx synapse-hub@{pkg_ver} verify || npx synapse-hub verify
|
|
198
209
|
elif [ -f "{verify_all_script}" ]; then
|
|
199
210
|
python3 "{verify_all_script}" || python "{verify_all_script}"
|
|
200
211
|
fi
|
|
201
212
|
if [ $? -ne 0 ]; then
|
|
202
213
|
echo "\\n[AI-GUARD - REJECTED] Git Push aborted: Quality Gates verification failed!"
|
|
203
|
-
echo "Please fix defects or run 'npx synapse-hub verify' locally before pushing.\\n"
|
|
214
|
+
echo "Please fix defects or run 'npx synapse-hub@{pkg_ver} verify' locally before pushing.\\n"
|
|
204
215
|
exit 1
|
|
205
216
|
fi
|
|
206
217
|
|
|
207
|
-
echo "[AI-GUARD - PASSED]
|
|
218
|
+
echo "[AI-GUARD - PASSED] Quality Gates satisfied. Syncing project health report..."
|
|
208
219
|
if command -v npx >/dev/null 2>&1; then
|
|
209
|
-
npx synapse-hub analyze
|
|
220
|
+
npx synapse-hub@{pkg_ver} analyze || npx synapse-hub analyze
|
|
210
221
|
elif [ -f "{analyze_script}" ]; then
|
|
211
222
|
python3 "{analyze_script}" || python "{analyze_script}"
|
|
212
223
|
fi
|
|
213
224
|
echo "[AI-GUARD - READY] Pushing to remote...\\n"
|
|
214
|
-
|
|
225
|
+
# --- End Synapse-Hub Quality Gate ---
|
|
215
226
|
"""
|
|
227
|
+
|
|
216
228
|
try:
|
|
229
|
+
if os.path.exists(hook_file):
|
|
230
|
+
with open(hook_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
231
|
+
existing_content = f.read()
|
|
232
|
+
|
|
233
|
+
if "Synapse-Hub" in existing_content or "synapse-hub" in existing_content:
|
|
234
|
+
print(f" {CYAN}[SKIP] Pre-Push Git Hook already present:{RESET} {hook_file}")
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
# Safely backup existing hook (e.g. Husky, Lefthook)
|
|
238
|
+
backup_file = f"{hook_file}.backup"
|
|
239
|
+
shutil.copy2(hook_file, backup_file)
|
|
240
|
+
print(f" {YELLOW}[BACKUP] Existing pre-push hook backed up to:{RESET} {backup_file}")
|
|
241
|
+
|
|
242
|
+
# Append non-destructively
|
|
243
|
+
final_content = existing_content.rstrip() + "\n" + hook_snippet
|
|
244
|
+
else:
|
|
245
|
+
final_content = "#!/bin/sh\n" + hook_snippet
|
|
246
|
+
|
|
217
247
|
with open(hook_file, "w", encoding="utf-8", newline="\n") as f:
|
|
218
|
-
f.write(
|
|
248
|
+
f.write(final_content)
|
|
219
249
|
|
|
220
250
|
# Attempt to make executable on Unix-like environments
|
|
221
251
|
try:
|
|
@@ -223,7 +253,7 @@ exit 0
|
|
|
223
253
|
except Exception:
|
|
224
254
|
pass
|
|
225
255
|
|
|
226
|
-
print(f" {GREEN}[OK]
|
|
256
|
+
print(f" {GREEN}[OK] Configured automated Pre-Push Git Hook:{RESET} {hook_file} (pinned to v{pkg_ver})")
|
|
227
257
|
return True
|
|
228
258
|
except Exception as e:
|
|
229
259
|
print(f" ⚠️ Could not install Git hook: {e}")
|