synapse-hub 3.0.0 → 3.0.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 (65) hide show
  1. package/.agents/rules/communication-style.md +4 -0
  2. package/.agents/rules/context-engineering.md +1 -0
  3. package/.agents/rules/git-push-protocol.md +17 -0
  4. package/.agents/skills/agentless-qa-repair/examples/three_phase_repair_workflow.md +40 -0
  5. package/.agents/skills/antigravity-test-sentinel/examples/sprint_dod_enforcement.md +47 -0
  6. package/.agents/skills/auth-session-and-cookie-manager/examples/multi_role_auth_matrix.md +70 -0
  7. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_playwright_pom.ts +52 -0
  8. package/.agents/skills/failure-autopsy-and-healing/examples/closed_loop_healing_walkthrough.md +48 -0
  9. package/.agents/skills/lean-impact-scoreboard/examples/sample_scoreboard.md +1 -1
  10. package/.agents/skills/mipro-prompt-compiler/examples/dspy_assertion_optimizer_case.md +46 -0
  11. package/.agents/skills/persistent-planning-files/SKILL.md +1 -1
  12. package/.agents/skills/qa-aci-interface/examples/surgical_code_window_walkthrough.md +40 -0
  13. package/.agents/skills/review-spec/examples/pocock_grilling_decision_rubric.md +40 -0
  14. package/.agents/skills/skillspector-security-guard/examples/audit_threat_vectors_report.json +35 -0
  15. package/.agents/skills/surgical-coding-principles/SKILL.md +1 -1
  16. package/.agents/skills/tot-exploratory-testing/examples/payment_gateway_tot_tree.json +62 -0
  17. package/.agents/skills/verification-before-completion/SKILL.md +2 -2
  18. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +2 -2
  19. package/.agents/skills/web-agent-gym-benchmarking/examples/enterprise_tasks_suite.json +38 -0
  20. package/.agents/skills/web-vitals-and-performance-profiler/examples/playwright_cwv_profiler_fixture.ts +61 -0
  21. package/AGENTS.md +6 -4
  22. package/CLAUDE.md +1 -0
  23. package/README.en.md +8 -8
  24. package/README.md +3 -3
  25. package/action.yml +1 -1
  26. package/bin/agent-qa.js +2 -2
  27. package/docs/ARCHITECTURE.en.md +192 -0
  28. package/docs/ARCHITECTURE.md +127 -126
  29. package/docs/CI_CD_INTEGRATION.en.md +96 -0
  30. package/docs/CI_CD_INTEGRATION.md +24 -22
  31. package/docs/CLI_REFERENCE.en.md +174 -0
  32. package/docs/CLI_REFERENCE.md +120 -115
  33. package/docs/CONTRIBUTING.en.md +49 -0
  34. package/docs/CONTRIBUTING.md +25 -67
  35. package/docs/QUALITY_GATES.en.md +115 -0
  36. package/docs/QUALITY_GATES.md +82 -80
  37. package/docs/research/agent_skill_training_analysis.md +1 -1
  38. package/package.json +1 -1
  39. package/skills_manifest.json +1 -1
  40. package/synapse.config.json +1 -1
  41. package/templates/ai_bridges/CLAUDE.md +1 -0
  42. package/tools/__init__.py +1 -1
  43. package/tools/auto_heal_reflexion.py +8 -0
  44. package/tools/browser_helpers.js +2 -2
  45. package/tools/bump_version.py +15 -14
  46. package/tools/check_version_sync.py +24 -7
  47. package/tools/cli.py +3 -3
  48. package/tools/contract_validator.py +141 -0
  49. package/tools/dashboard_generator.py +11 -11
  50. package/tools/db_sandbox.py +22 -9
  51. package/tools/generate_allure_report.py +1 -1
  52. package/tools/infer_linter.py +135 -21
  53. package/tools/mcp_excel_server.py +1 -1
  54. package/tools/mcp_server.py +7 -4
  55. package/tools/micro_mutator.py +21 -3
  56. package/tools/reversible_sandbox.py +2 -2
  57. package/tools/senior_code_review.py +2 -1
  58. package/tools/skill_vault.py +82 -3
  59. package/tools/skillspector.py +5 -1
  60. package/tools/sync_agent_rules.py +1 -0
  61. package/tools/test_impact.py +2 -1
  62. package/tools/update_sentinel.py +3 -3
  63. package/tools/verify_all.py +2 -2
  64. package/tools/visual_diff_canvas.py +3 -2
  65. package/tools/welcome_page.py +1 -1
@@ -102,7 +102,7 @@ def extract_versions(repo_root: str) -> Dict[str, str]:
102
102
  if m:
103
103
  versions["tools/mcp_excel_server.py"] = m.group(1)
104
104
 
105
- # 10. docs/ARCHITECTURE.md
105
+ # 10. docs/ARCHITECTURE.md & docs/ARCHITECTURE.en.md
106
106
  arch_path = os.path.join(repo_root, "docs", "ARCHITECTURE.md")
107
107
  if os.path.exists(arch_path):
108
108
  with open(arch_path, "r", encoding="utf-8") as f:
@@ -110,24 +110,41 @@ def extract_versions(repo_root: str) -> Dict[str, str]:
110
110
  m = re.search(r'>\s*\*\*Version:\*\*\s*`([^`]+)`', content)
111
111
  if m:
112
112
  versions["docs/ARCHITECTURE.md"] = m.group(1)
113
+ arch_en_path = os.path.join(repo_root, "docs", "ARCHITECTURE.en.md")
114
+ if os.path.exists(arch_en_path):
115
+ with open(arch_en_path, "r", encoding="utf-8") as f:
116
+ content = f.read()
117
+ m = re.search(r'>\s*\*\*Version:\*\*\s*`([^`]+)`', content)
118
+ if m:
119
+ versions["docs/ARCHITECTURE.en.md"] = m.group(1)
113
120
 
114
121
  # 11. README.md
115
122
  readme_path = os.path.join(repo_root, "README.md")
116
123
  if os.path.exists(readme_path):
117
124
  with open(readme_path, "r", encoding="utf-8") as f:
118
125
  content = f.read()
119
- m = re.search(r'rev:\s*v([0-9]+\.[0-9]+\.[0-9]+)', content)
120
- if m:
121
- versions["README.md"] = m.group(1)
126
+ m_badge = re.search(r'Phiên bản(?: Hub)?:\*\*\s*`v([0-9]+\.[0-9]+\.[0-9]+)', content)
127
+ m_rev = re.search(r'rev:\s*v([0-9]+\.[0-9]+\.[0-9]+)', content)
128
+ if m_badge:
129
+ versions["README.md"] = m_badge.group(1)
130
+ elif m_rev:
131
+ versions["README.md"] = m_rev.group(1)
132
+ if m_badge and m_rev:
133
+ versions["README.md (rev)"] = m_rev.group(1)
122
134
 
123
135
  # 12. README.en.md
124
136
  readme_en_path = os.path.join(repo_root, "README.en.md")
125
137
  if os.path.exists(readme_en_path):
126
138
  with open(readme_en_path, "r", encoding="utf-8") as f:
127
139
  content = f.read()
128
- m = re.search(r'rev:\s*v([0-9]+\.[0-9]+\.[0-9]+)', content)
129
- if m:
130
- versions["README.en.md"] = m.group(1)
140
+ m_badge = re.search(r'(?:Hub )?Version:\*\*\s*`v([0-9]+\.[0-9]+\.[0-9]+)', content)
141
+ m_rev = re.search(r'rev:\s*v([0-9]+\.[0-9]+\.[0-9]+)', content)
142
+ if m_badge:
143
+ versions["README.en.md"] = m_badge.group(1)
144
+ elif m_rev:
145
+ versions["README.en.md"] = m_rev.group(1)
146
+ if m_badge and m_rev:
147
+ versions["README.en.md (rev)"] = m_rev.group(1)
131
148
 
132
149
  # 13. SECURITY.md
133
150
  sec_path = os.path.join(repo_root, "SECURITY.md")
package/tools/cli.py CHANGED
@@ -41,7 +41,7 @@ RESET = "\033[0m"
41
41
  def cmd_version(args):
42
42
  """Displays the current hub version, skill catalog count, and license information."""
43
43
  manifest_path = os.path.join(REPO_ROOT, "skills_manifest.json")
44
- version = "3.0.0"
44
+ version = "3.0.1"
45
45
  if os.path.exists(manifest_path):
46
46
  try:
47
47
  import json
@@ -541,14 +541,14 @@ def cmd_release(args):
541
541
 
542
542
  def _hub_version():
543
543
  manifest_path = os.path.join(REPO_ROOT, "skills_manifest.json")
544
- version = "3.0.0"
544
+ version = "3.0.1"
545
545
  if os.path.exists(manifest_path):
546
546
  try:
547
547
  import json
548
548
  with open(manifest_path, "r", encoding="utf-8") as f:
549
549
  version = json.load(f).get("version", version)
550
550
  except (OSError, json.JSONDecodeError):
551
- version = "3.0.0"
551
+ version = "3.0.1"
552
552
  return version
553
553
 
554
554
 
@@ -0,0 +1,141 @@
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
+ Swarm Data Contract Validator (Single Front-Door Integrity Guard)
8
+ -----------------------------------------------------------------
9
+ Validates the JSON Data Contract emitted by `live-explorer` before
10
+ `pom-crafter` consumes it for Clean POM code synthesis.
11
+ Ensures zero locator guessing, validates semantic ARIA roles, and enforces
12
+ latency measurement (< 200ms) for durable selectors.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import json
18
+ import argparse
19
+ from typing import Dict, Any, List, Tuple
20
+
21
+ # Enforce UTF-8 stdout encoding for Windows console compatibility
22
+ if hasattr(sys.stdout, 'reconfigure'):
23
+ try:
24
+ sys.stdout.reconfigure(encoding='utf-8')
25
+ sys.stderr.reconfigure(encoding='utf-8')
26
+ except Exception:
27
+ pass
28
+
29
+ CONTRACT_SCHEMA = {
30
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
31
+ "title": "SelectorContract",
32
+ "type": "object",
33
+ "required": ["url", "screen_name", "elements"],
34
+ "properties": {
35
+ "url": {"type": "string"},
36
+ "screen_name": {"type": "string"},
37
+ "viewport": {
38
+ "type": "object",
39
+ "properties": {
40
+ "width": {"type": "integer"},
41
+ "height": {"type": "integer"}
42
+ }
43
+ },
44
+ "elements": {
45
+ "type": "array",
46
+ "minItems": 1,
47
+ "items": {
48
+ "type": "object",
49
+ "required": ["name", "role"],
50
+ "properties": {
51
+ "name": {"type": "string"},
52
+ "role": {"type": "string"},
53
+ "selector": {"type": "string"},
54
+ "target": {"type": "string"},
55
+ "latency_ms": {"type": "number"},
56
+ "action_type": {"type": "string"}
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+
64
+ def validate_contract_dict(data: Dict[str, Any]) -> Tuple[bool, List[str]]:
65
+ """Validates contract structure without external dependencies."""
66
+ errors = []
67
+ if not isinstance(data, dict):
68
+ return False, ["Root contract must be a JSON object."]
69
+
70
+ # Required top-level fields
71
+ for field in ("url", "screen_name", "elements"):
72
+ if field not in data:
73
+ errors.append(f"Missing required top-level field: '{field}'")
74
+
75
+ if "elements" in data:
76
+ elements = data["elements"]
77
+ if not isinstance(elements, list) or len(elements) == 0:
78
+ errors.append("'elements' must be a non-empty list of interactive components.")
79
+ else:
80
+ for idx, el in enumerate(elements):
81
+ if not isinstance(el, dict):
82
+ errors.append(f"Element at index {idx} must be a dictionary object.")
83
+ continue
84
+ if "name" not in el or not el["name"]:
85
+ errors.append(f"Element at index {idx} missing 'name' identifier.")
86
+ if "role" not in el or not el["role"]:
87
+ errors.append(f"Element at index {idx} missing ARIA 'role' classification.")
88
+ if "selector" not in el and "target" not in el:
89
+ errors.append(f"Element '{el.get('name', f'index_{idx}')}' must provide 'selector' or 'target'.")
90
+
91
+ latency = el.get("latency_ms")
92
+ if latency is not None and isinstance(latency, (int, float)):
93
+ if latency > 200:
94
+ errors.append(
95
+ f"Element '{el.get('name')}' latency ({latency}ms) exceeds the 200ms latency budget."
96
+ )
97
+
98
+ return len(errors) == 0, errors
99
+
100
+
101
+ def validate_contract_file(file_path: str) -> Tuple[bool, List[str]]:
102
+ """Loads and validates a contract file from disk."""
103
+ if not os.path.exists(file_path):
104
+ return False, [f"Contract file does not exist: {file_path}"]
105
+ try:
106
+ with open(file_path, "r", encoding="utf-8") as f:
107
+ data = json.load(f)
108
+ return validate_contract_dict(data)
109
+ except json.JSONDecodeError as err:
110
+ return False, [f"Malformed JSON syntax: {err}"]
111
+ except OSError as err:
112
+ return False, [f"File read error: {err}"]
113
+
114
+
115
+ def main():
116
+ parser = argparse.ArgumentParser(description="Swarm Data Contract Validator")
117
+ parser.add_argument("contract_path", nargs="?", default=None, help="Path to JSON Data Contract file")
118
+ parser.add_argument("--schema", action="store_true", help="Print standard JSON schema and exit")
119
+ args = parser.parse_args()
120
+
121
+ if args.schema:
122
+ print(json.dumps(CONTRACT_SCHEMA, indent=2))
123
+ return 0
124
+
125
+ if not args.contract_path:
126
+ parser.print_help()
127
+ return 1
128
+
129
+ valid, errors = validate_contract_file(args.contract_path)
130
+ if valid:
131
+ print(f"✅ [VALID CONTRACT] '{args.contract_path}' is ready for Clean POM synthesis.")
132
+ return 0
133
+ else:
134
+ print(f"❌ [INVALID CONTRACT] '{args.contract_path}' contains {len(errors)} violation(s):")
135
+ for err in errors:
136
+ print(f" • {err}")
137
+ return 1
138
+
139
+
140
+ if __name__ == "__main__":
141
+ sys.exit(main())
@@ -655,7 +655,7 @@ def build_swarm_timeline_html(default_lang: str = "vi") -> str:
655
655
  ("Tool Exec", "badge-tool", "pom-crafter", "synthesize_page_class (Clean POM)", "210ms", "1,850 tok", "✓ ZERO XPATHS", "badge-pass", "gen_ai.system: google | model: gemini-flash | target: PageClass"),
656
656
  ("Test Run", "badge-ver", "test-runner", "dispatch_headless_cli &amp; distill_log", "3,200ms", "120 tok", "✓ PASS (0 Fails)", "badge-pass", "tool.name: distill_log.py | noise_reduction: 95.2%"),
657
657
  ("Memory Cache", "badge-cat", "failure-analyst", "cache_resolver.py fingerprint &amp; lookup", "8ms", "0 tok", "⚡ 0-TOKEN RESOLVE", "badge-pass", "tool.name: cache_resolver.py | fingerprint: fp_7f2e76cf | hits: 0"),
658
- ("Gatekeeper", "badge-gate", "quality-gatekeeper", "verify_all.py (9 Gates + MicroMutator)", "1,450ms", "0 tok", "🛡️ 9/9 GATES PASS", "badge-pass", "tool.name: verify_all.py | mutation_score: 91.4% (DoD >= 80%)"),
658
+ ("Gatekeeper", "badge-gate", "quality-gatekeeper", "verify_all.py (10 Gates + MicroMutator)", "1,450ms", "0 tok", "🛡️ 10/10 GATES PASS", "badge-pass", "tool.name: verify_all.py | mutation_score: 91.4% (DoD >= 80%)"),
659
659
  ]
660
660
 
661
661
  step_rows = []
@@ -1320,7 +1320,7 @@ def _build_target_translations(
1320
1320
  "th_step_status": "Trạng Thái",
1321
1321
  "th_step_details": "Chi Tiết Kỹ Thuật (OTel & AST)",
1322
1322
 
1323
- "footer_text": "Báo cáo được khởi tạo tự động bởi Synapse-Hub v3.0.0 &bull; Bản quyền &copy; Thế Vị (@TeeVee998) &bull; Chuẩn Clean POM & Kiểm Thử Tự Chủ",
1323
+ "footer_text": "Báo cáo được khởi tạo tự động bởi Synapse-Hub v3.0.1 &bull; Bản quyền &copy; Thế Vị (@TeeVee998) &bull; Chuẩn Clean POM & Kiểm Thử Tự Chủ",
1324
1324
  }
1325
1325
 
1326
1326
  translations_en = {
@@ -1398,7 +1398,7 @@ def _build_target_translations(
1398
1398
  "th_step_status": "Status",
1399
1399
  "th_step_details": "Technical Details (OTel & AST)",
1400
1400
 
1401
- "footer_text": "Generated automatically by Synapse-Hub v3.0.0 &bull; Copyright &copy; Thế Vị (@TeeVee998) &bull; Clean POM & Autonomous Testing",
1401
+ "footer_text": "Generated automatically by Synapse-Hub v3.0.1 &bull; Copyright &copy; Thế Vị (@TeeVee998) &bull; Clean POM & Autonomous Testing",
1402
1402
  }
1403
1403
 
1404
1404
  _inject_gate_translations(translations_vi, translations_en, gates)
@@ -1804,13 +1804,13 @@ def _build_hub_translations(
1804
1804
  """Builds bilingual dictionaries for Hub Dashboard."""
1805
1805
  translations_vi = {
1806
1806
  "header_title": "Bảng Điều Hành Chất Lượng Doanh Nghiệp",
1807
- "header_subtitle": "Synapse-Hub v3.0.0 &bull; Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
1807
+ "header_subtitle": "Synapse-Hub v3.0.1 &bull; Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
1808
1808
  "nav_welcome": "Giới Thiệu",
1809
1809
  "meta_mode": "🏷️ Chế độ: <strong>HỆ SINH THÁI QA HUB</strong>",
1810
1810
  "meta_gen": f"🕒 Khởi tạo: <strong>{now_str}</strong>",
1811
- "meta_status": "🛡️ Gatekeeper: <strong>9/9 ĐẠT CHUẨN</strong>",
1811
+ "meta_status": "🛡️ Gatekeeper: <strong>10/10 ĐẠT CHUẨN</strong>",
1812
1812
 
1813
- "kpi_dod_lbl": "9 Cổng Kiểm Soát DoD",
1813
+ "kpi_dod_lbl": "10 Cổng Kiểm Soát DoD",
1814
1814
  "kpi_dod_sub": "✓ 100% Cổng Đạt Chuẩn",
1815
1815
  "kpi_skills_lbl": "Kỹ Năng AI Chuyên Biệt",
1816
1816
  "kpi_skills_sub": "Phân Bổ Trong 4 Danh Mục Doanh Nghiệp",
@@ -1874,18 +1874,18 @@ def _build_hub_translations(
1874
1874
  "th_sk_ver": "Phiên Bản",
1875
1875
  "th_sk_cap": "Khả Năng Nghiệp Vụ Cốt Lõi",
1876
1876
 
1877
- "footer_text": "Bảng Điều Hành Chất Lượng Doanh Nghiệp &bull; Synapse-Hub v3.0.0 &bull; Bản quyền &copy; Thế Vị (@TeeVee998) &bull; Hệ Thống Kiểm Thử AI Agent Tự Chủ",
1877
+ "footer_text": "Bảng Điều Hành Chất Lượng Doanh Nghiệp &bull; Synapse-Hub v3.0.1 &bull; Bản quyền &copy; Thế Vị (@TeeVee998) &bull; Hệ Thống Kiểm Thử AI Agent Tự Chủ",
1878
1878
  }
1879
1879
 
1880
1880
  translations_en = {
1881
1881
  "header_title": "Enterprise QA Executive Dashboard",
1882
- "header_subtitle": "Synapse-Hub v3.0.0 &bull; Autonomous AI Agent Testing Ecosystem",
1882
+ "header_subtitle": "Synapse-Hub v3.0.1 &bull; Autonomous AI Agent Testing Ecosystem",
1883
1883
  "nav_welcome": "Welcome",
1884
1884
  "meta_mode": "🏷️ Mode: <strong>QA HUB ECOSYSTEM</strong>",
1885
1885
  "meta_gen": f"🕒 Generated: <strong>{now_str}</strong>",
1886
- "meta_status": "🛡️ Gatekeeper: <strong>9/9 COMPLIANT</strong>",
1886
+ "meta_status": "🛡️ Gatekeeper: <strong>10/10 COMPLIANT</strong>",
1887
1887
 
1888
- "kpi_dod_lbl": "9 Quality Gates",
1888
+ "kpi_dod_lbl": "10 Quality Gates",
1889
1889
  "kpi_dod_sub": "✓ 100% Verification Compliant",
1890
1890
  "kpi_skills_lbl": "Specialized AI Skills",
1891
1891
  "kpi_skills_sub": "Spanned across 4 Enterprise Categories",
@@ -1949,7 +1949,7 @@ def _build_hub_translations(
1949
1949
  "th_sk_ver": "Version",
1950
1950
  "th_sk_cap": "Core Capabilities",
1951
1951
 
1952
- "footer_text": "Enterprise QA Executive Dashboard &bull; Synapse-Hub v3.0.0 &bull; Copyright &copy; Thế Vị (@TeeVee998) &bull; Autonomous AI Agent Testing Ecosystem",
1952
+ "footer_text": "Enterprise QA Executive Dashboard &bull; Synapse-Hub v3.0.1 &bull; Copyright &copy; Thế Vị (@TeeVee998) &bull; Autonomous AI Agent Testing Ecosystem",
1953
1953
  }
1954
1954
 
1955
1955
  _inject_gate_translations(translations_vi, translations_en, gates)
@@ -191,23 +191,36 @@ class DBSandbox:
191
191
  cursor = conn.cursor()
192
192
  executed_count = 0
193
193
 
194
+ safe_sp = re.sub(r'[^a-zA-Z0-9_]', '', savepoint_name) or "sp_test_run"
194
195
  try:
195
- cursor.execute(f"SAVEPOINT {savepoint_name};")
196
+ cursor.execute(f"SAVEPOINT {safe_sp};")
196
197
  for sql in sql_commands:
197
198
  cursor.execute(sql)
198
199
  executed_count += 1
199
200
 
200
- # Simulate verification read inside the savepoint
201
- cursor.execute("SELECT COUNT(*) FROM users")
202
- count_inside_tx = cursor.fetchone()[0]
201
+ # Determine sample table for verification read (prefer 'users', fallback to first user table)
202
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
203
+ tables = [row[0] for row in cursor.fetchall()]
204
+ target_table = "users" if "users" in tables else (tables[0] if tables else None)
205
+
206
+ count_inside_tx = 0
207
+ if target_table:
208
+ safe_tbl = target_table.replace('"', '""')
209
+ cursor.execute(f'SELECT COUNT(*) FROM "{safe_tbl}"')
210
+ count_inside_tx = cursor.fetchone()[0]
211
+ else:
212
+ count_inside_tx = executed_count
203
213
 
204
214
  # Enforce automatic zero-pollution rollback
205
- cursor.execute(f"ROLLBACK TO SAVEPOINT {savepoint_name};")
206
- cursor.execute(f"RELEASE SAVEPOINT {savepoint_name};")
215
+ cursor.execute(f"ROLLBACK TO SAVEPOINT {safe_sp};")
216
+ cursor.execute(f"RELEASE SAVEPOINT {safe_sp};")
207
217
 
208
218
  # Verify that outside the savepoint, table is pristine
209
- cursor.execute("SELECT COUNT(*) FROM users")
210
- count_after_rollback = cursor.fetchone()[0]
219
+ count_after_rollback = 0
220
+ if target_table:
221
+ safe_tbl = target_table.replace('"', '""')
222
+ cursor.execute(f'SELECT COUNT(*) FROM "{safe_tbl}"')
223
+ count_after_rollback = cursor.fetchone()[0]
211
224
 
212
225
  return {
213
226
  "status": "SUCCESS",
@@ -219,7 +232,7 @@ class DBSandbox:
219
232
  }
220
233
  except Exception as e:
221
234
  try:
222
- cursor.execute(f"ROLLBACK TO SAVEPOINT {savepoint_name};")
235
+ cursor.execute(f"ROLLBACK TO SAVEPOINT {safe_sp};")
223
236
  except sqlite3.Error as rollback_err:
224
237
  sys.stderr.write(f"Notice: Savepoint rollback notice: {rollback_err}\n")
225
238
  return {
@@ -41,7 +41,7 @@ def generate_allure_report(results_dir=None, report_dir=None, clean=True, open_b
41
41
 
42
42
  print(f"[+] Generating Allure HTML report from '{results_path}' to '{report_path}'...")
43
43
  try:
44
- subprocess.run(cmd, check=True)
44
+ subprocess.run(cmd, check=True, timeout=120.0)
45
45
  print(f"[+] Report generated successfully at: {report_path}")
46
46
  except subprocess.CalledProcessError as e:
47
47
  print(f"[ERROR] Allure report generation failed with exit code {e.returncode}")
@@ -16,6 +16,7 @@ import sys
16
16
  import re
17
17
  import json
18
18
  import glob
19
+ import ast
19
20
  import html as html_lib
20
21
  import argparse
21
22
  from datetime import datetime
@@ -76,15 +77,19 @@ def parse_with_tree_sitter(file_path, content):
76
77
  return None
77
78
 
78
79
  # Generic Detection regexes (including Cypress .should and .and assertions)
80
+ # Note: Standalone expect(...) without chained matcher is caught as DANGLING_ASSERTION_NO_MATCHER.
79
81
  ASSERTION_PATTERN = re.compile(
80
- r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b|\.should\s*\(|\.and\s*\('
82
+ r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b'
83
+ r'|\.should\s*\('
84
+ r'|\.and\s*\(',
85
+ re.MULTILINE
81
86
  )
82
87
 
83
88
  # Rule L1 & L5: Empty catch / swallowed exceptions across JS/TS/Groovy/Java and Python
84
- # Also catches try/catch that only console.log or promise .catch(() => {})
89
+ # Also catches try/catch that only console.log/logger.error or promise .catch(() => {})
85
90
  EMPTY_CATCH_PATTERN = re.compile(
86
- r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}'
87
- r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}\s*\)'
91
+ r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}'
92
+ r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:(?:console|logger|log)\.(?:log|warn|error|info)\([^)]*\);?\s*)?\}\s*\)'
88
93
  r'|\bexcept(?:\s+[^:]*)?:\s*(?:pass|\.\.\.)(?:\s*$|\s*#))',
89
94
  re.MULTILINE
90
95
  )
@@ -96,16 +101,48 @@ BENIGN_TRY_CALL_PATTERN = re.compile(
96
101
  r'^[\w\.]*\.(?:reconfigure|chmod|flush)\s*\('
97
102
  )
98
103
 
99
- # Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, expect(true).toBeTruthy())
104
+ # Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, assert f() == f(), assert len(x) >= 0)
100
105
  TAUTOLOGY_PATTERN = re.compile(
101
- r'(?:expect\s*\(\s*(?:true|1)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy)\s*\(\s*(?:true|1)?\s*\)'
106
+ r'(?:expect(?:\.soft|\.poll)?\s*\(\s*(?:true|1|null|undefined)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy|toBeNull|toBeUndefined)\s*\(\s*(?:true|1|null|undefined)?\s*\)'
107
+ r'|expect(?:\.soft|\.poll)?\s*\(\s*([a-zA-Z0-9_]+)\s*\)\s*\.\s*(?:toBe|toEqual)\s*\(\s*\1\s*\)'
102
108
  r'|\bassert\s+(?:True|true|1\s*==\s*1)\b'
103
- r'|\bassert\s+([a-zA-Z0-9_]+)\s*==\s*\1\b'
104
- r'|assertEquals\s*\(\s*(?:true|1)\s*,\s*(?:true|1)\s*\)'
109
+ r'|\bassert\s+([a-zA-Z0-9_\.]+(?:\([^)]*\))?)\s*==\s*\1\b'
110
+ r'|\bassert\s+(?:len\s*\([^)]*\)|[a-zA-Z0-9_\.]+\.(?:length|size\(\)))\s*(?:>=\s*0|>\s*-(?:1|0))\b'
111
+ r'|assertEquals\s*\(\s*(?:true|1|null)\s*,\s*(?:true|1|null)\s*\)'
105
112
  r'|assertTrue\s*\(\s*(?:true|1)\s*\))',
106
113
  re.IGNORECASE
107
114
  )
108
115
 
116
+
117
+ def find_dangling_expects(clean_code, raw_lines):
118
+ """
119
+ Detects dangling expect(...) calls lacking chained matchers (e.g. expect(page) without .toBeVisible()).
120
+ Correctly handles nested parentheses like expect(page.locator('#id')).
121
+ """
122
+ dangling = []
123
+ pattern = re.compile(r'(?:await\s+)?\bexpect(?:\.soft|\.poll)?\s*\(')
124
+ for m in pattern.finditer(clean_code):
125
+ start_idx = m.end()
126
+ depth = 1
127
+ pos = start_idx
128
+ while pos < len(clean_code) and depth > 0:
129
+ ch = clean_code[pos]
130
+ if ch == '(':
131
+ depth += 1
132
+ elif ch == ')':
133
+ depth -= 1
134
+ elif ch in (';', '\n') and depth > 0:
135
+ # Malformed unclosed expect
136
+ break
137
+ pos += 1
138
+ if depth == 0:
139
+ rest = clean_code[pos:].lstrip()
140
+ if not rest.startswith('.'):
141
+ line_no = clean_code[:m.start()].count("\n") + 1
142
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
143
+ dangling.append((line_no, raw_line))
144
+ return dangling
145
+
109
146
  # Rule L4: Unawaited Playwright assertions in JS/TS
110
147
  # Expanded with all standard Playwright async matchers and optional .not modifier
111
148
  PLAYWRIGHT_ASYNC_MATCHERS = (
@@ -300,7 +337,7 @@ def format_junit(report_data):
300
337
  return "\n".join(lines)
301
338
 
302
339
 
303
- def classify_file_role(file_path: str) -> str:
340
+ def classify_file_role(file_path: str, content: str = "") -> str:
304
341
  """
305
342
  Classifies a source file into:
306
343
  - 'CONFIG': Configuration files (playwright.config, cypress.config, conftest, wdio.conf, jest.config)
@@ -324,6 +361,8 @@ def classify_file_role(file_path: str) -> str:
324
361
  return "PAGE_OBJECT"
325
362
  if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
326
363
  return "PAGE_OBJECT"
364
+ if content and (re.search(r'\bclass\s+[A-Za-z0-9_]*Page\b', content) or re.search(r'\bextends\s+BasePage\b', content)):
365
+ return "PAGE_OBJECT"
327
366
 
328
367
  # 3. Helpers & Fixtures / Tools & Utilities / Benchmarks
329
368
  if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin", "benchmarks", "benchmark") for p in parts):
@@ -348,13 +387,16 @@ def classify_file_role(file_path: str) -> str:
348
387
 
349
388
  def analyze_script(file_path, baseline_set=None, disabled_rules=None):
350
389
  """Performs static rule scanning on a single test script or automation asset."""
351
- role = classify_file_role(file_path)
352
-
353
390
  try:
354
391
  rel_path = os.path.relpath(file_path, REPO_ROOT)
355
392
  except ValueError:
356
393
  rel_path = os.path.abspath(file_path)
357
394
 
395
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
396
+ content = f.read()
397
+
398
+ role = classify_file_role(file_path, content=content)
399
+
358
400
  # Config files have zero test assertions by definition
359
401
  if role == "CONFIG":
360
402
  return {
@@ -364,9 +406,6 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
364
406
  "is_clean": True
365
407
  }
366
408
 
367
- with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
368
- content = f.read()
369
-
370
409
  defects = []
371
410
 
372
411
  # Lexically strip comments and string literals for accurate analysis
@@ -402,12 +441,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
402
441
  elif arg.isdigit():
403
442
  delay_val = int(arg)
404
443
  if delay_val > 0:
405
- d_type = "EXCESSIVE_HARD_DELAY" if delay_val >= 3000 else "HARD_DELAY_ANTI_PATTERN"
444
+ is_excessive = delay_val >= 3000
445
+ d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
446
+ sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
406
447
  defects.append({
407
448
  "type": d_type,
408
- "severity": "WARNING",
449
+ "severity": sev,
409
450
  "line": idx,
410
- "message": f"Static sleep of {delay_val}ms found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
451
+ "message": f"Static sleep of {delay_val}ms found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
411
452
  })
412
453
  continue
413
454
  elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg) and not line.strip().startswith("cy.wait"):
@@ -428,12 +469,14 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
428
469
  if delay_val > 0:
429
470
  delay_ms = int(delay_val * 1000)
430
471
  delay_str = f"{delay_val:g}"
431
- d_type = "EXCESSIVE_HARD_DELAY" if delay_ms >= 3000 else "HARD_DELAY_ANTI_PATTERN"
472
+ is_excessive = delay_ms >= 3000
473
+ d_type = "EXCESSIVE_HARD_DELAY" if is_excessive else "HARD_DELAY_ANTI_PATTERN"
474
+ sev = "CRITICAL" if (is_excessive and role == "TEST_SCRIPT") else "WARNING"
432
475
  defects.append({
433
476
  "type": d_type,
434
- "severity": "WARNING",
477
+ "severity": sev,
435
478
  "line": idx,
436
- "message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
479
+ "message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). {'Prohibited in test scripts by AGENTS.md Rule 1.4. ' if is_excessive else ''}Replace with dynamic Smart Wait (waitForElement/networkidle)."
437
480
  })
438
481
  except ValueError:
439
482
  if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg):
@@ -470,6 +513,68 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
470
513
  "message": f"Tautological assertion detected ('{raw_line}'). Test assertion always evaluates to True without testing system state."
471
514
  })
472
515
 
516
+ # Rule 4.1: Dangling expect(...) calls without chained matcher
517
+ for d_line, d_raw in find_dangling_expects(clean_code, raw_lines):
518
+ defects.append({
519
+ "type": "DANGLING_ASSERTION_NO_MATCHER",
520
+ "severity": "CRITICAL",
521
+ "line": d_line,
522
+ "message": f"Dangling expect() call without matcher detected ('{d_raw}'). expect() returns an expectation object and does not perform any assertion without a matcher (e.g. .toBeVisible(), .toHaveText())."
523
+ })
524
+
525
+ # Rule 4.2: Python AST Compiler-Grade Verification for .py Test Scripts
526
+ if file_path.endswith(".py"):
527
+ try:
528
+ tree = ast.parse(content, filename=file_path)
529
+ for node in ast.walk(tree):
530
+ if isinstance(node, ast.Assert):
531
+ if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1:
532
+ op = node.test.ops[0]
533
+ right = node.test.comparators[0]
534
+ if isinstance(op, ast.Eq):
535
+ if ast.dump(node.test.left) == ast.dump(right):
536
+ line_no = getattr(node, "lineno", 1)
537
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
538
+ defects.append({
539
+ "type": "TAUTOLOGICAL_ASSERTION",
540
+ "severity": "CRITICAL",
541
+ "line": line_no,
542
+ "message": f"Tautological assertion detected via AST ('{raw_line}'). Both sides of comparison evaluate to identical AST expressions."
543
+ })
544
+ elif isinstance(op, (ast.GtE, ast.Gt)):
545
+ if isinstance(node.test.left, ast.Call) and getattr(node.test.left.func, 'id', None) == 'len':
546
+ if isinstance(right, ast.Constant):
547
+ if (isinstance(op, ast.GtE) and right.value == 0) or (isinstance(op, ast.Gt) and right.value < 0):
548
+ line_no = getattr(node, "lineno", 1)
549
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
550
+ defects.append({
551
+ "type": "TAUTOLOGICAL_ASSERTION",
552
+ "severity": "CRITICAL",
553
+ "line": line_no,
554
+ "message": f"Tautological assertion detected via AST ('{raw_line}'). len() is non-negative by definition."
555
+ })
556
+ elif isinstance(node.test, ast.Constant) and node.test.value is True:
557
+ line_no = getattr(node, "lineno", 1)
558
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
559
+ defects.append({
560
+ "type": "TAUTOLOGICAL_ASSERTION",
561
+ "severity": "CRITICAL",
562
+ "line": line_no,
563
+ "message": f"Tautological assertion detected via AST ('{raw_line}'). Constant True assertion."
564
+ })
565
+ elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
566
+ if getattr(node.value.func, 'id', None) == 'expect':
567
+ line_no = getattr(node, "lineno", 1)
568
+ raw_line = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
569
+ defects.append({
570
+ "type": "DANGLING_ASSERTION_NO_MATCHER",
571
+ "severity": "CRITICAL",
572
+ "line": line_no,
573
+ "message": f"Dangling expect() call without matcher detected via AST ('{raw_line}')."
574
+ })
575
+ except SyntaxError:
576
+ _ = False # synapse-ignore
577
+
473
578
  # Rule 5 (L4): Unawaited Playwright Assertions in JS/TS
474
579
  if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
475
580
  for match in UNAWAITED_EXPECT_PATTERN.finditer(clean_code):
@@ -544,6 +649,15 @@ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
544
649
  "message": "Test case file contains executable logic but has no verification or assertion statements."
545
650
  })
546
651
 
652
+ seen_keys = set()
653
+ deduped = []
654
+ for d in defects:
655
+ k = (d.get("type"), d.get("line"))
656
+ if k not in seen_keys:
657
+ seen_keys.add(k)
658
+ deduped.append(d)
659
+ defects = deduped
660
+
547
661
  active_defects = []
548
662
  disabled = {r.upper() for r in (disabled_rules or [])}
549
663
  norm_rel = rel_path.replace("\\", "/")
@@ -777,7 +891,7 @@ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
777
891
  sys.stderr.write(f"Notice: Target report write failed: {err}\n")
778
892
 
779
893
  if strict:
780
- fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST"))
894
+ fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST", "DANGLING_ASSERTION_NO_MATCHER"))
781
895
  return 0 if (critical_count == 0 and fake_pass_count == 0) else 1
782
896
  return 0 if critical_count == 0 else 1
783
897
 
@@ -23,7 +23,7 @@ if hasattr(sys.stdout, 'reconfigure'):
23
23
  except Exception:
24
24
  pass
25
25
 
26
- SERVER_VERSION = "3.0.0"
26
+ SERVER_VERSION = "3.0.1"
27
27
 
28
28
  NS_MAIN = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
29
29
  NS_REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"