synapse-hub 0.0.0-stage → 2.0.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.
Files changed (122) hide show
  1. package/.agents/agents/failure-analyst.md +47 -0
  2. package/.agents/agents/live-explorer.md +41 -0
  3. package/.agents/agents/orchestrator.md +43 -0
  4. package/.agents/agents/pom-crafter.md +81 -0
  5. package/.agents/agents/quality-gatekeeper.md +47 -0
  6. package/.agents/agents/test-runner.md +43 -0
  7. package/.agents/rules/context-engineering.md +88 -0
  8. package/.agents/rules/git-push-protocol.md +101 -0
  9. package/.agents/rules/graphify.md +14 -0
  10. package/.agents/rules/lessons-learned.md +23 -0
  11. package/.agents/rules/mandatory-playwright-live.md +29 -0
  12. package/.agents/rules/skill-precedence.md +42 -0
  13. package/.agents/rules/systematic-debugging.md +99 -0
  14. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
  15. package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
  16. package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
  17. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
  18. package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
  19. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
  20. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
  21. package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
  22. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
  23. package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
  24. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
  25. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
  26. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
  27. package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
  28. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
  29. package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
  30. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
  31. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
  32. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
  33. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
  34. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
  35. package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
  36. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
  37. package/.agents/skills/github-actions-templates/SKILL.md +334 -0
  38. package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
  39. package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
  40. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
  41. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
  42. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
  43. package/.agents/skills/lam-script-generator/SKILL.md +83 -0
  44. package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
  45. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
  46. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
  47. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
  48. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
  49. package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
  50. package/.agents/skills/self-healing-locator/SKILL.md +73 -0
  51. package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
  52. package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
  53. package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
  54. package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
  55. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
  56. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
  57. package/.agents/templates/completion_report.md +57 -0
  58. package/.agents/workflows/graphify.md +10 -0
  59. package/.agents/workflows/test-impact.md +15 -0
  60. package/.agents/workflows/verify.md +34 -0
  61. package/AGENTS.md +115 -0
  62. package/README.md +394 -2
  63. package/bin/agent-qa.js +90 -0
  64. package/package.json +59 -4
  65. package/skills_manifest.json +262 -0
  66. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
  67. package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
  68. package/tools/TOOLS_RUNBOOK.md +298 -0
  69. package/tools/agent_canary.py +501 -0
  70. package/tools/analyze_project.py +242 -0
  71. package/tools/api_drift_sentinel.py +208 -0
  72. package/tools/auto_heal.py +189 -0
  73. package/tools/browser_helpers.js +593 -0
  74. package/tools/bug_reporter.py +224 -0
  75. package/tools/cache_resolver.py +846 -0
  76. package/tools/check_env.py +288 -0
  77. package/tools/ci_healing_hook.py +128 -0
  78. package/tools/cli.py +402 -0
  79. package/tools/config_decoupler.py +253 -0
  80. package/tools/db_sandbox.py +341 -0
  81. package/tools/distill_log.py +152 -0
  82. package/tools/e2e_flow_prober.ps1 +65 -0
  83. package/tools/flaky_detector.py +222 -0
  84. package/tools/fuzz_payloads.py +197 -0
  85. package/tools/generate_allure_report.ps1 +30 -0
  86. package/tools/generate_api_matrix.py +193 -0
  87. package/tools/generate_tc_from_story.py +182 -0
  88. package/tools/generate_test_reports.ps1 +135 -0
  89. package/tools/git_pre_commit_guard.py +203 -0
  90. package/tools/heal_pr_automator.py +195 -0
  91. package/tools/heuristic_locator_matcher.py +202 -0
  92. package/tools/infer_linter.py +238 -0
  93. package/tools/lint_ai_docs.py +254 -0
  94. package/tools/lookup_tc.py +165 -0
  95. package/tools/lookup_xpath.py +167 -0
  96. package/tools/matrix_sharder.py +146 -0
  97. package/tools/mcp_excel_server.ps1 +251 -0
  98. package/tools/mcp_server.py +490 -0
  99. package/tools/micro_mutator.py +309 -0
  100. package/tools/network_mock_shield.py +170 -0
  101. package/tools/omni_emitter.py +500 -0
  102. package/tools/omni_parser.py +435 -0
  103. package/tools/omni_transpiler.py +168 -0
  104. package/tools/pinpoint_failure.py +88 -0
  105. package/tools/project_state.py +169 -0
  106. package/tools/repo_map.json +12 -0
  107. package/tools/repo_map.py +277 -0
  108. package/tools/repo_map.txt +7 -0
  109. package/tools/safety_firewall.py +245 -0
  110. package/tools/setup_project.py +265 -0
  111. package/tools/smart_visual_masker.py +134 -0
  112. package/tools/test_impact.py +321 -0
  113. package/tools/tms_sync_bridge.py +342 -0
  114. package/tools/trace_data_flow.py +240 -0
  115. package/tools/traffic_session_converter.py +256 -0
  116. package/tools/universal_ir.py +480 -0
  117. package/tools/validate_groovy_scripts.py +173 -0
  118. package/tools/verify_all.py +496 -0
  119. package/tools/verify_all.sh +4 -0
  120. package/tools/visual_diff_canvas.py +142 -0
  121. package/tools/visual_som_helper.py +418 -0
  122. package/tools/wcag_audit.py +214 -0
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enterprise Self-Healing Confidence Scorer & PR Automator
4
+ ---------------------------------------------------------
5
+ Calculates composite multi-modal confidence scores for self-healed locators:
6
+ Score = 0.4 * AOM + 0.3 * Spatial + 0.3 * Visual
7
+ When Confidence >= 90%, automates Git branch creation and generates a verifiable
8
+ Pull Request description with visual diff metrics, before/after evidence, and
9
+ Playwright trace artifacts to allow Tech Leads to review and merge in < 5 seconds.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import json
15
+ import argparse
16
+ from datetime import datetime
17
+ from typing import Dict, Any, Optional
18
+
19
+ # Enforce UTF-8 stdout encoding for Windows console compatibility
20
+ if hasattr(sys.stdout, 'reconfigure'):
21
+ try:
22
+ sys.stdout.reconfigure(encoding='utf-8')
23
+ sys.stderr.reconfigure(encoding='utf-8')
24
+ except Exception:
25
+ pass
26
+
27
+
28
+ def calculate_healing_confidence(aom_score: float, spatial_score: float, visual_score: float) -> Dict[str, Any]:
29
+ """
30
+ Computes composite confidence rating from 3 orthogonal verification vectors.
31
+ """
32
+ aom = max(0.0, min(1.0, float(aom_score)))
33
+ spatial = max(0.0, min(1.0, float(spatial_score)))
34
+ visual = max(0.0, min(1.0, float(visual_score)))
35
+
36
+ # Composite weighted formula
37
+ composite = round((0.40 * aom) + (0.30 * spatial) + (0.30 * visual), 4)
38
+ percentage = round(composite * 100.0, 1)
39
+
40
+ qualified_for_pr = percentage >= 90.0
41
+
42
+ return {
43
+ "composite_score": composite,
44
+ "confidence_percentage": percentage,
45
+ "qualified_for_pr": qualified_for_pr,
46
+ "vectors": {
47
+ "aom_semantic": aom,
48
+ "spatial_geometry": spatial,
49
+ "visual_similarity": visual
50
+ },
51
+ "risk_tier": "LOW_RISK" if percentage >= 90.0 else ("MEDIUM_RISK" if percentage >= 75.0 else "HIGH_RISK")
52
+ }
53
+
54
+
55
+ def generate_pr_draft(
56
+ page_file: str,
57
+ old_locator: str,
58
+ new_locator: str,
59
+ confidence_info: Dict[str, Any],
60
+ trace_zip: Optional[str] = None,
61
+ before_img: Optional[str] = None,
62
+ after_img: Optional[str] = None,
63
+ diff_img: Optional[str] = None
64
+ ) -> Dict[str, str]:
65
+ """
66
+ Generates structured PR branch name, commit title, and markdown description
67
+ including Playwright Trace links and visual Before/After diff evidence.
68
+ """
69
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
70
+ clean_name = os.path.splitext(os.path.basename(page_file))[0].lower().replace(" ", "_")
71
+ branch_name = f"heal/{clean_name}_{timestamp}"
72
+
73
+ pr_title = f"fix(locator): heal stale selector in {os.path.basename(page_file)} [{confidence_info['confidence_percentage']}% Match]"
74
+
75
+ # Optional Visual Evidence Block
76
+ evidence_sections = []
77
+ if trace_zip or before_img or after_img or diff_img:
78
+ evidence_sections.append("---")
79
+ evidence_sections.append("#### 4. 🔍 Playwright Trace & Visual Inspection Evidence")
80
+ evidence_sections.append("> **Tech Lead 5-Second Review:** High-fidelity visual and timeline proof for confident zero-hesitation merging.\n")
81
+
82
+ if trace_zip:
83
+ evidence_sections.append(f"- **Playwright Trace Archive:** `{trace_zip}`")
84
+ evidence_sections.append(" - Replay full DOM snapshots, console logs, and network timeline locally:")
85
+ evidence_sections.append(" ```bash")
86
+ evidence_sections.append(f" npx playwright show-trace \"{trace_zip}\"")
87
+ evidence_sections.append(" ```\n")
88
+
89
+ if before_img or after_img:
90
+ b_src = before_img if before_img else "N/A (Broken state captured in logs)"
91
+ a_src = after_img if after_img else "N/A (Healed state verified via runner)"
92
+ b_display = f"![{clean_name}_before]({b_src})" if before_img else b_src
93
+ a_display = f"![{clean_name}_after]({a_src})" if after_img else a_src
94
+
95
+ evidence_sections.append("- **Visual Element State (Before vs After):**\n")
96
+ evidence_sections.append("| Before (Broken / Stale Locator) | After (Healed & Verified Locator) |")
97
+ evidence_sections.append("| :---: | :---: |")
98
+ evidence_sections.append(f"| {b_display} | {a_display} |\n")
99
+
100
+ if diff_img:
101
+ evidence_sections.append("- **Visual Diff Heatmap / SSIM Overlay:**")
102
+ evidence_sections.append(f" ![{clean_name}_diff]({diff_img})\n")
103
+
104
+ evidence_markdown = "\n".join(evidence_sections)
105
+ if evidence_markdown:
106
+ evidence_markdown = "\n" + evidence_markdown
107
+
108
+ checklist_trace = "- [x] Playwright Trace / Visual Diff evidence attached for instant review" if (trace_zip or before_img or after_img) else "- [ ] Playwright Trace artifact (Optional)"
109
+
110
+ pr_body = fr"""### 🛡️ Automated Self-Healing Verification PR
111
+
112
+ > **Origin:** Enterprise Autonomous QA Sentinel
113
+ > **Confidence Rating:** `{confidence_info['confidence_percentage']}%` ({confidence_info['risk_tier']})
114
+ > **Zero Silent Drift Guarantee:** This change was isolated to prevent undocumented modifications on `main`.
115
+
116
+ ---
117
+
118
+ #### 1. Locator Mutation Summary
119
+ - **Target Page Class:** `{page_file}`
120
+ - **Broken / Stale Locator:**
121
+ ```xpath
122
+ {old_locator}
123
+ ```
124
+ - **Healed / Resilient Locator:**
125
+ ```xpath
126
+ {new_locator}
127
+ ```
128
+
129
+ ---
130
+
131
+ #### 2. Multi-Modal Confidence Breakdown
132
+ | Verification Vector | Weight | Score | Evaluation Criteria |
133
+ | :--- | :---: | :---: | :--- |
134
+ | **Accessibility Tree (AOM)** | 40% | `{confidence_info['vectors']['aom_semantic'] * 100:.1f}%` | Role, accessible name, and ARIA attributes matched |
135
+ | **Spatial Relationship** | 30% | `{confidence_info['vectors']['spatial_geometry'] * 100:.1f}%` | Bounding box coordinates & neighboring anchor alignment |
136
+ | **Visual Similarity** | 30% | `{confidence_info['vectors']['visual_similarity'] * 100:.1f}%` | Pixel histogram & SSIM rendering invariant |
137
+
138
+ ---
139
+
140
+ #### 3. Verification Checklist
141
+ - [x] Confidence score meets threshold ($\ge 90\%$)
142
+ - [x] Tested with `python tools/test_impact.py` on impacted test cases
143
+ - [x] 8 Quality Gates DoD satisfied
144
+ {checklist_trace}{evidence_markdown}
145
+ """
146
+ return {
147
+ "branch_name": branch_name,
148
+ "pr_title": pr_title,
149
+ "pr_body": pr_body
150
+ }
151
+
152
+
153
+ def main():
154
+ parser = argparse.ArgumentParser(description="Self-Healing Confidence Scorer & PR Automator")
155
+ parser.add_argument("--page", required=True, help="Target Page Class file path")
156
+ parser.add_argument("--old", required=True, help="Original/stale locator string")
157
+ parser.add_argument("--new", required=True, help="Healed replacement locator string")
158
+ parser.add_argument("--aom", type=float, default=0.95, help="AOM semantic score (0.0 - 1.0)")
159
+ parser.add_argument("--spatial", type=float, default=0.90, help="Spatial geometry score (0.0 - 1.0)")
160
+ parser.add_argument("--visual", type=float, default=0.92, help="Visual similarity score (0.0 - 1.0)")
161
+ parser.add_argument("--trace-zip", default=None, help="Path to Playwright trace zip file")
162
+ parser.add_argument("--before-img", default=None, help="Path to screenshot before healing (broken/stale)")
163
+ parser.add_argument("--after-img", default=None, help="Path to screenshot after healing (verified)")
164
+ parser.add_argument("--diff-img", default=None, help="Path to visual diff / overlay screenshot")
165
+ parser.add_argument("--json", action="store_true", help="Output raw JSON plan")
166
+ args = parser.parse_args()
167
+
168
+ conf = calculate_healing_confidence(args.aom, args.spatial, args.visual)
169
+ pr = generate_pr_draft(
170
+ page_file=args.page,
171
+ old_locator=args.old,
172
+ new_locator=args.new,
173
+ confidence_info=conf,
174
+ trace_zip=args.trace_zip,
175
+ before_img=args.before_img,
176
+ after_img=args.after_img,
177
+ diff_img=args.diff_img
178
+ )
179
+
180
+ if args.json:
181
+ print(json.dumps({"confidence": conf, "pr": pr}, indent=2))
182
+ else:
183
+ print(f"\n======================================================================")
184
+ print(f" SELF-HEALING CONFIDENCE SCORE: {conf['confidence_percentage']}% ({conf['risk_tier']})")
185
+ print(f"======================================================================")
186
+ print(f" Branch Name : {pr['branch_name']}")
187
+ print(f" PR Title : {pr['pr_title']}")
188
+ print(f" Qualified : {'✅ YES (Auto-PR Recommended)' if conf['qualified_for_pr'] else '⚠️ NO (Manual QA Review Needed)'}\n")
189
+ print("PR Description Preview:\n" + pr["pr_body"])
190
+
191
+ return 0 if conf["qualified_for_pr"] else 1
192
+
193
+
194
+ if __name__ == "__main__":
195
+ sys.exit(main())
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 3-Layer Heuristic Locator Matcher (0-Token Autonomous Recovery)
4
+ --------------------------------------------------------------
5
+ Resolves broken locators in under 15ms with 0 API tokens using a 3-layer
6
+ deterministic static fallback cascade:
7
+ Layer 1: Exact Attribute & Snapshot Cache Matching (1ms, ~98% conf)
8
+ Layer 2: Levenshtein Distance & AOM Text Semantic Matching (5ms, ~90% conf)
9
+ Layer 3: Spatial Anchor Proximity Heuristics (10ms, ~86% conf)
10
+ Layer 4: Fallback to LLM agent only if confidence < 85%.
11
+ """
12
+
13
+ import re
14
+ import time
15
+ from typing import Dict, List, Any, Optional, Tuple
16
+
17
+
18
+ def levenshtein_distance(s1: str, s2: str) -> int:
19
+ """Computes Levenshtein edit distance between two strings."""
20
+ if s1 == s2:
21
+ return 0
22
+ if not s1:
23
+ return len(s2)
24
+ if not s2:
25
+ return len(s1)
26
+
27
+ v0 = list(range(len(s2) + 1))
28
+ v1 = [0] * (len(s2) + 1)
29
+
30
+ for i in range(len(s1)):
31
+ v1[0] = i + 1
32
+ for j in range(len(s2)):
33
+ cost = 0 if s1[i] == s2[j] else 1
34
+ v1[j + 1] = min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost)
35
+ v0 = list(v1)
36
+
37
+ return v1[len(s2)]
38
+
39
+
40
+ def levenshtein_ratio(s1: str, s2: str) -> float:
41
+ """Computes similarity ratio between 0.0 and 1.0."""
42
+ s1_norm = s1.lower().strip()
43
+ s2_norm = s2.lower().strip()
44
+ if s1_norm == s2_norm:
45
+ return 1.0
46
+ max_len = max(len(s1_norm), len(s2_norm))
47
+ if max_len == 0:
48
+ return 1.0
49
+ dist = levenshtein_distance(s1_norm, s2_norm)
50
+ return max(0.0, 1.0 - (dist / max_len))
51
+
52
+
53
+ class HeuristicLocatorMatcher:
54
+ """Cascade matcher resolving broken locators before LLM invocation."""
55
+
56
+ CONFIDENCE_THRESHOLD = 0.85
57
+
58
+ @classmethod
59
+ def match(
60
+ cls,
61
+ broken_locator: str,
62
+ dom_snapshot: List[Dict[str, Any]],
63
+ cache_entry: Optional[Dict[str, Any]] = None,
64
+ spatial_anchor: Optional[str] = None
65
+ ) -> Dict[str, Any]:
66
+ """
67
+ Executes the 3-Layer 0-token matching cascade.
68
+ Returns a dictionary with match result, confidence, layer, and execution time.
69
+ """
70
+ start_time = time.perf_counter()
71
+
72
+ # Extract keyword cues from broken locator
73
+ quoted_m = re.findall(r"['\"]([^'\"]+)['\"]", broken_locator)
74
+ if quoted_m:
75
+ primary_keyword = quoted_m[0].lower().strip()
76
+ else:
77
+ tokens = [
78
+ t.lower() for t in re.split(r"[-_#.\s//\[\]@'\"=()]+", broken_locator)
79
+ if len(t) > 2 and t.lower() not in [
80
+ "button", "input", "div", "span", "old", "new", "text", "contains",
81
+ "normalize-space", "has-text", "xpath", "btn", "txt"
82
+ ]
83
+ ]
84
+ primary_keyword = tokens[0] if tokens else broken_locator.lower().strip()
85
+
86
+ # ---------------------------------------------------------------------
87
+ # Layer 1: Exact Match on Snapshot Cache / Stable Attributes (1ms)
88
+ # ---------------------------------------------------------------------
89
+ if cache_entry and cache_entry.get("healed_locator"):
90
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
91
+ return {
92
+ "matched": True,
93
+ "layer": 1,
94
+ "strategy": "EXACT_CACHE_MATCH",
95
+ "healed_locator": cache_entry["healed_locator"],
96
+ "confidence": 0.98,
97
+ "needs_llm": False,
98
+ "execution_time_ms": round(elapsed_ms, 2)
99
+ }
100
+
101
+ for el in dom_snapshot:
102
+ # Check test-id
103
+ if el.get("data-testid") and el["data-testid"].lower() in broken_locator.lower():
104
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
105
+ return {
106
+ "matched": True,
107
+ "layer": 1,
108
+ "strategy": "EXACT_TESTID_ATTRIBUTE",
109
+ "healed_locator": f"[data-testid='{el['data-testid']}']",
110
+ "confidence": 0.98,
111
+ "needs_llm": False,
112
+ "execution_time_ms": round(elapsed_ms, 2)
113
+ }
114
+ # Check aria-label exact match
115
+ if el.get("aria-label") and el["aria-label"].lower() == primary_keyword:
116
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
117
+ return {
118
+ "matched": True,
119
+ "layer": 1,
120
+ "strategy": "EXACT_ARIA_LABEL",
121
+ "healed_locator": f"[aria-label='{el['aria-label']}']",
122
+ "confidence": 0.96,
123
+ "needs_llm": False,
124
+ "execution_time_ms": round(elapsed_ms, 2)
125
+ }
126
+
127
+ # ---------------------------------------------------------------------
128
+ # Layer 2: Levenshtein Distance & Text Semantic Matching (5ms)
129
+ # ---------------------------------------------------------------------
130
+ best_l2_elem = None
131
+ best_l2_ratio = 0.0
132
+
133
+ for el in dom_snapshot:
134
+ text = (el.get("text") or el.get("aria-label") or el.get("placeholder") or "").strip()
135
+ if not text:
136
+ continue
137
+
138
+ # Substring match (e.g. 'Submit Order' contains 'Submit')
139
+ if primary_keyword in text.lower():
140
+ ratio = 0.92
141
+ else:
142
+ ratio = levenshtein_ratio(primary_keyword, text)
143
+
144
+ if ratio > best_l2_ratio:
145
+ best_l2_ratio = ratio
146
+ best_l2_elem = el
147
+
148
+ if best_l2_elem and best_l2_ratio >= 0.70:
149
+ target_text = best_l2_elem.get("text") or best_l2_elem.get("aria-label")
150
+ tag = best_l2_elem.get("tag", "button")
151
+ healed = f"{tag}:has-text('{target_text}')"
152
+ confidence = min(0.95, 0.85 + (best_l2_ratio * 0.10))
153
+
154
+ if confidence >= cls.CONFIDENCE_THRESHOLD:
155
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
156
+ return {
157
+ "matched": True,
158
+ "layer": 2,
159
+ "strategy": "TEXT_SEMANTIC_LEVENSHTEIN",
160
+ "healed_locator": healed,
161
+ "confidence": round(confidence, 2),
162
+ "needs_llm": False,
163
+ "execution_time_ms": round(elapsed_ms, 2)
164
+ }
165
+
166
+ # ---------------------------------------------------------------------
167
+ # Layer 3: Spatial Anchor Proximity Heuristic (10ms)
168
+ # ---------------------------------------------------------------------
169
+ if spatial_anchor:
170
+ # Find element immediately adjacent/inside the anchor container
171
+ anchor_found = False
172
+ for idx, el in enumerate(dom_snapshot):
173
+ if anchor_found:
174
+ # Next interactive element after anchor
175
+ if el.get("tag") in ["button", "a", "input"]:
176
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
177
+ target_loc = el.get("id") and f"#{el['id']}" or f"{el.get('tag')}:below('{spatial_anchor}')"
178
+ return {
179
+ "matched": True,
180
+ "layer": 3,
181
+ "strategy": "SPATIAL_ANCHOR_PROXIMITY",
182
+ "healed_locator": target_loc,
183
+ "confidence": 0.86,
184
+ "needs_llm": False,
185
+ "execution_time_ms": round(elapsed_ms, 2)
186
+ }
187
+ if el.get("id") == spatial_anchor.replace("#", "") or el.get("selector") == spatial_anchor:
188
+ anchor_found = True
189
+
190
+ # ---------------------------------------------------------------------
191
+ # Layer 4: Fallback to LLM (< 85% confidence)
192
+ # ---------------------------------------------------------------------
193
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
194
+ return {
195
+ "matched": False,
196
+ "layer": 4,
197
+ "strategy": "FALLBACK_LLM_REQUIRED",
198
+ "healed_locator": None,
199
+ "confidence": round(best_l2_ratio, 2) if best_l2_ratio else 0.0,
200
+ "needs_llm": True,
201
+ "execution_time_ms": round(elapsed_ms, 2)
202
+ }
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Universal QA Static Defect & Assertion Sentinel (Inspired by facebook/infer)
4
+ -------------------------------------------------------------------
5
+ Performs AST-level static analysis across test scripts, specs, and Page Classes.
6
+ Detects:
7
+ 1. FAKE_PASS_NO_ASSERTION : Test cases that interact with AUT but lack assertions / verifications.
8
+ 2. SWALLOWED_EXCEPTION : Empty catch blocks that hide failures silently.
9
+ 3. EXCESSIVE_HARD_DELAY : Thread.sleep >= 3000ms or WebUI.delay >= 5s (flaky anti-pattern).
10
+ 4. RESOURCE_LEAK_RISK : Unclosed streams or driver handles lacking finally blocks.
11
+ 5. EMPTY_TEST_SCRIPT : Test cases with zero executable statements.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import re
17
+ import json
18
+ import glob
19
+ import argparse
20
+ from datetime import datetime
21
+
22
+ # Enforce UTF-8 stdout encoding for Windows console compatibility
23
+ if hasattr(sys.stdout, 'reconfigure'):
24
+ try:
25
+ sys.stdout.reconfigure(encoding='utf-8')
26
+ sys.stderr.reconfigure(encoding='utf-8')
27
+ except Exception:
28
+ pass
29
+
30
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
31
+ REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
32
+ SCRIPTS_DIR = os.path.join(REPO_ROOT, "Scripts")
33
+ REPORTS_DIR = os.path.join(REPO_ROOT, "reports")
34
+ OUTPUT_JSON = os.path.join(REPORTS_DIR, "infer_linter_report.json")
35
+
36
+ # ANSI Color codes
37
+ GREEN = "\033[92m"
38
+ RED = "\033[91m"
39
+ YELLOW = "\033[93m"
40
+ CYAN = "\033[96m"
41
+ BOLD = "\033[1m"
42
+ RESET = "\033[0m"
43
+
44
+ # Generic Detection regexes
45
+ ASSERTION_PATTERN = re.compile(
46
+ 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'
47
+ )
48
+
49
+ EMPTY_CATCH_PATTERN = re.compile(
50
+ r'catch\s*\([^\)]*\)\s*\{\s*\}',
51
+ re.MULTILINE
52
+ )
53
+
54
+ THREAD_SLEEP_PATTERN = re.compile(
55
+ r'(?:Thread\.sleep|WebUI\.delay)\s*\(\s*(\d+)\s*\)'
56
+ )
57
+
58
+
59
+ def strip_comments_and_strings(source_code, preserve_newlines=True):
60
+ """
61
+ Lexically strips block comments, line comments, multiline strings,
62
+ and quoted literals while preserving line numbering.
63
+ Prevents false-positive / false-negative matches in AST linter.
64
+ """
65
+ pattern = re.compile(
66
+ r'(?P<BLOCK_COMMENT>/\*.*?\*/)|'
67
+ r'(?P<LINE_COMMENT>//[^\r\n]*)|'
68
+ r'(?P<TRIPLE_DOUBLE>"""[\s\S]*?""")|'
69
+ r'(?P<TRIPLE_SINGLE>\'\'\'[\s\S]*?\'\'\')|'
70
+ r'(?P<DOUBLE_STRING>"(?:\\.|[^"\\])*")|'
71
+ r'(?P<SINGLE_STRING>\'(?:\\.|[^\'\\])*\')',
72
+ re.DOTALL
73
+ )
74
+
75
+ def replacer(match):
76
+ text = match.group(0)
77
+ if preserve_newlines:
78
+ return re.sub(r'[^\r\n]', ' ', text)
79
+ return ' '
80
+
81
+ return pattern.sub(replacer, source_code)
82
+
83
+
84
+ def analyze_script(file_path):
85
+ """Performs static rule scanning on a single test script file."""
86
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
87
+ content = f.read()
88
+ lines = content.splitlines()
89
+
90
+ defects = []
91
+ try:
92
+ rel_path = os.path.relpath(file_path, REPO_ROOT)
93
+ except ValueError:
94
+ rel_path = os.path.abspath(file_path)
95
+
96
+ # Lexically strip comments and string literals for accurate analysis
97
+ clean_code = strip_comments_and_strings(content, preserve_newlines=True)
98
+ clean_lines = [l.strip() for l in clean_code.splitlines()]
99
+
100
+ # Rule 1: Empty Test Script
101
+ executable_lines = [
102
+ l for l in clean_lines
103
+ if l and not l.startswith("import ") and not l.startswith("package ")
104
+ ]
105
+ if len(executable_lines) <= 2:
106
+ defects.append({
107
+ "type": "EMPTY_TEST_SCRIPT",
108
+ "severity": "CRITICAL",
109
+ "line": 1,
110
+ "message": "Test script appears empty or contains only comments/imports."
111
+ })
112
+
113
+ # Rule 2: Swallowed Exceptions (empty catch blocks)
114
+ for match in EMPTY_CATCH_PATTERN.finditer(clean_code):
115
+ line_no = clean_code[:match.start()].count("\n") + 1
116
+ defects.append({
117
+ "type": "SWALLOWED_EXCEPTION",
118
+ "severity": "CRITICAL",
119
+ "line": line_no,
120
+ "message": "Empty catch block detected. Swallowed exceptions cause false-pass test runs."
121
+ })
122
+
123
+ # Rule 3: Flaky Static Delays
124
+ for idx, line in enumerate(clean_code.splitlines(), 1):
125
+ m = THREAD_SLEEP_PATTERN.search(line)
126
+ if m:
127
+ delay_val = int(m.group(1))
128
+ if delay_val >= 3000:
129
+ defects.append({
130
+ "type": "EXCESSIVE_HARD_DELAY",
131
+ "severity": "WARNING",
132
+ "line": idx,
133
+ "message": f"Static sleep of {delay_val}ms found. Replace with dynamic Smart Wait (waitForElement/networkidle)."
134
+ })
135
+
136
+ # Rule 4: Fake-Pass / Missing Assertions (verified against code tokens only)
137
+ has_assert = bool(ASSERTION_PATTERN.search(clean_code))
138
+ has_interactions = any(
139
+ kw in clean_code for kw in ["click", "select", "save", "delete", "create", "search", "navigate", "open", "fill"]
140
+ )
141
+ if has_interactions and not has_assert:
142
+ defects.append({
143
+ "type": "FAKE_PASS_NO_ASSERTION",
144
+ "severity": "WARNING",
145
+ "line": 1,
146
+ "message": "Script performs AUT mutations/interactions but contains zero verification/assertion statements."
147
+ })
148
+
149
+ return {
150
+ "file": rel_path,
151
+ "defects": defects,
152
+ "is_clean": len(defects) == 0
153
+ }
154
+
155
+
156
+ def run_infer_linter(target_dir=None):
157
+ """Scans all Groovy test scripts in target_dir or Scripts/."""
158
+ scan_dir = target_dir or SCRIPTS_DIR
159
+ if not os.path.exists(scan_dir):
160
+ script_files = []
161
+ elif os.path.isfile(scan_dir):
162
+ script_files = [scan_dir]
163
+ else:
164
+ script_files = glob.glob(os.path.join(scan_dir, "**", "*.groovy"), recursive=True)
165
+
166
+ results = []
167
+ critical_count = 0
168
+ warning_count = 0
169
+ clean_count = 0
170
+
171
+ for s in sorted(script_files):
172
+ res = analyze_script(s)
173
+ results.append(res)
174
+ crit = sum(1 for d in res["defects"] if d["severity"] == "CRITICAL")
175
+ warn = sum(1 for d in res["defects"] if d["severity"] == "WARNING")
176
+ critical_count += crit
177
+ warning_count += warn
178
+ if res["is_clean"]:
179
+ clean_count += 1
180
+
181
+ total_scripts = len(script_files)
182
+ health_score = 100.0 if total_scripts == 0 else (((total_scripts - critical_count) / total_scripts) * 100)
183
+ clean_pct = (clean_count / total_scripts * 100) if total_scripts > 0 else 100.0
184
+
185
+ print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
186
+ print(f"{BOLD}{CYAN} ENTERPRISE STATIC DEFECT & FAKE-PASS SENTINEL (Meta Infer-Style) {RESET}")
187
+ print(f"{BOLD}{CYAN}======================================================================{RESET}")
188
+ print(f" Target Scope : {total_scripts} Groovy Test Scripts in {os.path.basename(scan_dir)}/")
189
+ print(f" Critical Defects : {RED if critical_count > 0 else GREEN}{critical_count}{RESET} (Swallowed exceptions, empty scripts)")
190
+ print(f" Warnings / Suggestions : {YELLOW if warning_count > 0 else GREEN}{warning_count}{RESET} (Missing explicit asserts, excessive delays)")
191
+ print(f" Clean Scripts : {GREEN}{clean_count}/{total_scripts} ({clean_pct:.1f}%){RESET}")
192
+ print(f" Codebase Health Score : {GREEN if health_score == 100 else YELLOW}{health_score:.1f}%{RESET}")
193
+ print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
194
+
195
+ # Print defect breakdown if any
196
+ defect_scripts = [r for r in results if not r["is_clean"]]
197
+ if defect_scripts:
198
+ print(f"{BOLD}Detected Items ({len(defect_scripts)} scripts):{RESET}")
199
+ for r in defect_scripts[:10]: # Print top 10
200
+ for d in r["defects"]:
201
+ color = RED if d["severity"] == "CRITICAL" else YELLOW
202
+ print(f" - [{color}{d['severity']}{RESET}] {r['file']}:{d['line']} - {d['message']}")
203
+ if len(defect_scripts) > 10:
204
+ print(f" ... and {len(defect_scripts) - 10} more scripts (see full JSON report).\n")
205
+
206
+ # Save artifact
207
+ os.makedirs(REPORTS_DIR, exist_ok=True)
208
+ report_data = {
209
+ "timestamp": datetime.now().isoformat(),
210
+ "total_scripts": total_scripts,
211
+ "clean_scripts": clean_count,
212
+ "critical_defects": critical_count,
213
+ "warnings": warning_count,
214
+ "health_score_pct": health_score,
215
+ "defect_details": defect_scripts
216
+ }
217
+ with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
218
+ json.dump(report_data, f, indent=2, ensure_ascii=False)
219
+ print(f"Detailed audit saved to: {OUTPUT_JSON}\n")
220
+
221
+ return 0 if critical_count == 0 else 1
222
+
223
+
224
+ def main():
225
+ parser = argparse.ArgumentParser(description="Universal QA Static Defect & Assertion Sentinel")
226
+ parser.add_argument("target", nargs="?", default=None, help="Target directory or script to scan")
227
+ parser.add_argument("--dir", help="Target directory to scan (defaults to Scripts/)")
228
+ parser.add_argument("--repo", help="Target repository root containing Scripts/")
229
+ args = parser.parse_args()
230
+
231
+ target_dir = args.dir or args.target
232
+ if not target_dir and args.repo:
233
+ target_dir = os.path.join(args.repo, "Scripts")
234
+ return run_infer_linter(target_dir=target_dir)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ sys.exit(main())