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,435 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Omni-Parser: Universal Polyglot AST & Structure Parser
4
+ ------------------------------------------------------
5
+ Zero-Token deterministic parser supporting Java, TypeScript/JavaScript,
6
+ Python, Groovy (Katalon), and C#.
7
+ Extracts Page Classes, Locators, Action Methods, and Test Cases into
8
+ the Universal Intermediate Representation (universal_ir.ProjectIR).
9
+ """
10
+
11
+ import os
12
+ import re
13
+ import sys
14
+ from typing import Dict, List, Optional, Tuple
15
+
16
+ try:
17
+ from tools.universal_ir import (
18
+ ProjectIR, PageClassIR, TestCaseIR, MethodIR, StepIR, LocatorIR,
19
+ ActionType, LocatorStrategy, AssertionType, WaitStrategy
20
+ )
21
+ except ImportError:
22
+ from universal_ir import (
23
+ ProjectIR, PageClassIR, TestCaseIR, MethodIR, StepIR, LocatorIR,
24
+ ActionType, LocatorStrategy, AssertionType, WaitStrategy
25
+ )
26
+
27
+
28
+ def extract_quoted_string(match_groups: Tuple[Optional[str], Optional[str]]) -> str:
29
+ """Helper to return the unescaped string from either double-quote or single-quote match."""
30
+ g1, g2 = match_groups
31
+ val = g1 if g1 else (g2 if g2 else "")
32
+ return val.replace(r"\'", "'").replace(r'\"', '"')
33
+
34
+
35
+ class OmniParser:
36
+ """Universal Zero-Token Polyglot Parser for Automation Codebases."""
37
+
38
+ @staticmethod
39
+ def detect_language_and_framework(file_path: str, content: str) -> Tuple[str, str]:
40
+ ext = os.path.splitext(file_path)[1].lower()
41
+
42
+ # Groovy / Katalon
43
+ if ext == ".groovy":
44
+ return "groovy", "katalon"
45
+
46
+ # Java
47
+ if ext == ".java":
48
+ if "playwright" in content.lower():
49
+ return "java", "playwright"
50
+ if "restassured" in content.lower():
51
+ return "java", "restassured"
52
+ return "java", "selenium"
53
+
54
+ # TypeScript / JavaScript
55
+ if ext in [".ts", ".tsx", ".js", ".jsx"]:
56
+ lang = "typescript" if "ts" in ext else "javascript"
57
+ if "@playwright/test" in content or "page.locator" in content or "getByRole" in content:
58
+ return lang, "playwright"
59
+ if "cy." in content or "cypress" in content.lower():
60
+ return lang, "cypress"
61
+ if "webdriverio" in content.lower() or "browser.$" in content:
62
+ return lang, "webdriverio"
63
+ return lang, "playwright"
64
+
65
+ # Python
66
+ if ext == ".py":
67
+ if "playwright" in content:
68
+ return "python", "playwright"
69
+ if "selenium" in content:
70
+ return "python", "selenium"
71
+ if "robot" in content.lower():
72
+ return "python", "robot"
73
+ return "python", "pytest"
74
+
75
+ # C#
76
+ if ext == ".cs":
77
+ if "playwright" in content.lower():
78
+ return "csharp", "playwright"
79
+ return "csharp", "selenium"
80
+
81
+ return "unknown", "generic"
82
+
83
+ @classmethod
84
+ def parse_file(cls, file_path: str) -> ProjectIR:
85
+ if not os.path.exists(file_path):
86
+ raise FileNotFoundError(f"Source file not found: {file_path}")
87
+
88
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
89
+ content = f.read()
90
+
91
+ lang, framework = cls.detect_language_and_framework(file_path, content)
92
+ basename = os.path.basename(file_path)
93
+ project = ProjectIR(
94
+ name=os.path.splitext(basename)[0],
95
+ source_language=lang,
96
+ source_framework=framework
97
+ )
98
+
99
+ if lang in ["typescript", "javascript"]:
100
+ cls._parse_ts_js(content, project, framework)
101
+ elif lang == "java":
102
+ cls._parse_java(content, project, framework)
103
+ elif lang == "groovy":
104
+ cls._parse_groovy(content, project, framework)
105
+ elif lang == "python":
106
+ cls._parse_python(content, project, framework)
107
+ elif lang == "csharp":
108
+ cls._parse_csharp(content, project, framework)
109
+ else:
110
+ cls._parse_generic(content, project)
111
+
112
+ return project
113
+
114
+ # -------------------------------------------------------------------------
115
+ # TypeScript / JavaScript Parser
116
+ # -------------------------------------------------------------------------
117
+ @classmethod
118
+ def _parse_ts_js(cls, content: str, project: ProjectIR, framework: str):
119
+ class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_]+))?", content)
120
+ page_name = class_match.group(1) if class_match else f"{project.name}Page"
121
+ base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
122
+
123
+ page = PageClassIR(name=page_name, base_class=base_class)
124
+ locators: Dict[str, LocatorIR] = {}
125
+
126
+ # 1. Playwright getByRole
127
+ role_matches = re.finditer(
128
+ r"(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.getByRole\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*,\s*\{\s*name:\s*(?:\"([^\"]*)\"|'([^']*)')\s*\}\s*\)",
129
+ content
130
+ )
131
+ for m in role_matches:
132
+ loc_name = m.group(1)
133
+ role_type = extract_quoted_string((m.group(2), m.group(3)))
134
+ acc_name = extract_quoted_string((m.group(4), m.group(5)))
135
+ locators[loc_name] = LocatorIR(name=loc_name, strategy=LocatorStrategy.ROLE, value=f"{role_type}[name='{acc_name}']")
136
+
137
+ # 2. Playwright getByTestId
138
+ tid_matches = re.finditer(
139
+ r"(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.getByTestId\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)",
140
+ content
141
+ )
142
+ for m in tid_matches:
143
+ loc_name = m.group(1)
144
+ val = extract_quoted_string((m.group(2), m.group(3)))
145
+ locators[loc_name] = LocatorIR(name=loc_name, strategy=LocatorStrategy.TEST_ID, value=val)
146
+
147
+ # 3. Playwright getByLabel
148
+ label_matches = re.finditer(
149
+ r"(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.getByLabel\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)",
150
+ content
151
+ )
152
+ for m in label_matches:
153
+ loc_name = m.group(1)
154
+ val = extract_quoted_string((m.group(2), m.group(3)))
155
+ locators[loc_name] = LocatorIR(name=loc_name, strategy=LocatorStrategy.LABEL, value=val)
156
+
157
+ # 4. Playwright page.locator('...')
158
+ loc_matches = re.finditer(
159
+ r"""(?:readonly\s+)?(?:this\.)?([A-Za-z0-9_$]+)\s*=\s*(?:this\.)?page\.locator\(\s*(?:\"((?:[^"\\]|\\.)*)\"|'((?:[^'\\]|\\.)*)')\s*\)""",
160
+ content
161
+ )
162
+ for m in loc_matches:
163
+ loc_name = m.group(1)
164
+ selector = extract_quoted_string((m.group(2), m.group(3)))
165
+ strat = LocatorStrategy.XPATH if selector.startswith(("//", "(//")) else LocatorStrategy.CSS
166
+ locators[loc_name] = LocatorIR(name=loc_name, strategy=strat, value=selector)
167
+
168
+ page.locators = list(locators.values())
169
+
170
+ # Methods
171
+ method_blocks = re.findall(r"(async\s+)?([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*(?::\s*[^\{]+)?\s*\{([^}]+)\}", content, re.MULTILINE)
172
+ for _, m_name, params_str, body in method_blocks:
173
+ if m_name in ["constructor"]:
174
+ continue
175
+ params = [p.strip().split(":")[0].strip() for p in params_str.split(",") if p.strip()]
176
+ steps = cls._extract_steps_from_body(body)
177
+ page.methods.append(MethodIR(name=m_name, parameters=params, steps=steps))
178
+
179
+ project.pages.append(page)
180
+
181
+ # Test cases (Playwright `test(...)` or Cypress / Mocha `it(...)`)
182
+ test_blocks = re.findall(
183
+ r"(?:test|it)(?:\.only|\.skip)?\s*\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*,\s*(?:async\s*)?(?:\([^)]*\)|function\s*\([^)]*\))\s*=>?\s*\{([^}]+)\}",
184
+ content,
185
+ re.MULTILINE
186
+ )
187
+ for idx, (t_m1, t_m2, t_body) in enumerate(test_blocks, 1):
188
+ t_title = t_m1 if t_m1 else t_m2
189
+ steps = cls._extract_steps_from_body(t_body)
190
+ project.test_cases.append(TestCaseIR(id=f"TC_{idx:03d}", title=t_title, steps=steps))
191
+
192
+ # -------------------------------------------------------------------------
193
+ # Java Parser (Selenium & Playwright)
194
+ # -------------------------------------------------------------------------
195
+ @classmethod
196
+ def _parse_java(cls, content: str, project: ProjectIR, framework: str):
197
+ class_match = re.search(r"public\s+class\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_]+))?", content)
198
+ page_name = class_match.group(1) if class_match else f"{project.name}Page"
199
+ base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
200
+
201
+ page = PageClassIR(name=page_name, base_class=base_class)
202
+ locators: Dict[str, LocatorIR] = {}
203
+
204
+ # 1. Selenium @FindBy annotations
205
+ findby_matches = re.finditer(
206
+ r"@FindBy\s*\(\s*([a-zA-Z0-9_]+)\s*=\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)\s*(?:(?:private|public|protected|static|final)\s+)*(?:WebElement|List<WebElement>)\s+([A-Za-z0-9_]+);",
207
+ content
208
+ )
209
+ for m in findby_matches:
210
+ by_type = m.group(1).lower()
211
+ selector = extract_quoted_string((m.group(2), m.group(3)))
212
+ var_name = m.group(4)
213
+ strat = LocatorStrategy.XPATH if by_type == "xpath" else (LocatorStrategy.ID if by_type == "id" else LocatorStrategy.CSS)
214
+ locators[var_name] = LocatorIR(name=var_name, strategy=strat, value=selector)
215
+
216
+ # 2. By locators
217
+ by_matches = re.finditer(
218
+ r"(?:(?:private|public|protected|static|final)\s+)*By\s+([A-Za-z0-9_]+)\s*=\s*By\.([a-zA-Z0-9_]+)\s*\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\);",
219
+ content
220
+ )
221
+ for m in by_matches:
222
+ var_name = m.group(1)
223
+ by_type = m.group(2).lower()
224
+ selector = extract_quoted_string((m.group(3), m.group(4)))
225
+ strat = LocatorStrategy.XPATH if "xpath" in by_type else (LocatorStrategy.ID if "id" in by_type else LocatorStrategy.CSS)
226
+ locators[var_name] = LocatorIR(name=var_name, strategy=strat, value=selector)
227
+
228
+ page.locators = list(locators.values())
229
+
230
+ # 3. Methods
231
+ method_blocks = re.findall(r"(?:public|protected|private)\s+([A-Za-z0-9_<>\s]+)\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*(?:throws\s+[A-Za-z0-9_,\s]+)?\s*\{([^}]+)\}", content, re.MULTILINE)
232
+ for ret_type, m_name, params_str, body in method_blocks:
233
+ if m_name == page_name:
234
+ continue
235
+ params = [p.strip().split()[-1] for p in params_str.split(",") if p.strip()]
236
+ steps = cls._extract_steps_from_body(body)
237
+ page.methods.append(MethodIR(name=m_name, parameters=params, return_type=ret_type.strip(), steps=steps))
238
+
239
+ project.pages.append(page)
240
+
241
+ # 4. TestNG / JUnit
242
+ test_matches = re.findall(r"@Test[^\n]*\s*(?:public|void|\s)+([A-Za-z0-9_]+)\s*\([^)]*\)[^{]*\{([^}]+)\}", content, re.MULTILINE)
243
+ for idx, (t_name, t_body) in enumerate(test_matches, 1):
244
+ steps = cls._extract_steps_from_body(t_body)
245
+ project.test_cases.append(TestCaseIR(id=f"TC_{idx:03d}", title=t_name, steps=steps))
246
+
247
+ # -------------------------------------------------------------------------
248
+ # Groovy / Katalon Parser (Clean POM Flat Architecture)
249
+ # -------------------------------------------------------------------------
250
+ @classmethod
251
+ def _parse_groovy(cls, content: str, project: ProjectIR, framework: str):
252
+ class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_]+))?", content)
253
+ page_name = class_match.group(1) if class_match else f"{project.name}Page"
254
+ base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
255
+
256
+ page = PageClassIR(name=page_name, base_class=base_class)
257
+ locators: Dict[str, LocatorIR] = {}
258
+
259
+ # Extract static final String XPaths
260
+ xpath_matches = re.finditer(
261
+ r"(?:(?:public|private|protected|static|final)\s+)*(?:String|def)\s+([A-Za-z0-9_]+)\s*=\s*(?:\"([^\"]*)\"|'([^']*)')",
262
+ content
263
+ )
264
+ for m in xpath_matches:
265
+ var_name = m.group(1)
266
+ selector = extract_quoted_string((m.group(2), m.group(3)))
267
+ strat = LocatorStrategy.XPATH if selector.startswith(("//", "(//")) else LocatorStrategy.CSS
268
+ locators[var_name] = LocatorIR(name=var_name, strategy=strat, value=selector)
269
+
270
+ page.locators = list(locators.values())
271
+
272
+ # Methods
273
+ method_blocks = re.findall(r"(?:(?:public|private|protected|def|static|void|[A-Za-z0-9_<>]+)\s+)+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*\{([^}]+)\}", content, re.MULTILINE)
274
+ for m_name, params_str, body in method_blocks:
275
+ if m_name in [page_name, "main"]:
276
+ continue
277
+ params = [p.strip().split()[-1] for p in params_str.split(",") if p.strip()]
278
+ steps = cls._extract_steps_from_body(body)
279
+ page.methods.append(MethodIR(name=m_name, parameters=params, steps=steps))
280
+
281
+ project.pages.append(page)
282
+
283
+ if not class_match:
284
+ steps = cls._extract_steps_from_body(content)
285
+ project.test_cases.append(TestCaseIR(id="TC_001", title=project.name, steps=steps))
286
+
287
+ # -------------------------------------------------------------------------
288
+ # Python Parser (Pytest / Playwright / Selenium)
289
+ # -------------------------------------------------------------------------
290
+ @classmethod
291
+ def _parse_python(cls, content: str, project: ProjectIR, framework: str):
292
+ class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\(([^)]+)\))?:", content)
293
+ page_name = class_match.group(1) if class_match else f"{project.name}Page"
294
+ base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
295
+
296
+ page = PageClassIR(name=page_name, base_class=base_class)
297
+ locators: Dict[str, LocatorIR] = {}
298
+
299
+ loc_matches = re.finditer(
300
+ r"self\.([a-zA-Z0-9_]+)\s*=\s*(?:self\.)?page\.locator\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)",
301
+ content
302
+ )
303
+ for m in loc_matches:
304
+ name = m.group(1)
305
+ sel = extract_quoted_string((m.group(2), m.group(3)))
306
+ strat = LocatorStrategy.XPATH if sel.startswith(("//", "(//")) else LocatorStrategy.CSS
307
+ locators[name] = LocatorIR(name=name, strategy=strat, value=sel)
308
+
309
+ role_matches = re.finditer(
310
+ r"self\.([a-zA-Z0-9_]+)\s*=\s*(?:self\.)?page\.get_by_role\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*,\s*name=(?:\"([^\"]*)\"|'([^']*)')\s*\)",
311
+ content
312
+ )
313
+ for m in role_matches:
314
+ name = m.group(1)
315
+ r_type = extract_quoted_string((m.group(2), m.group(3)))
316
+ r_name = extract_quoted_string((m.group(4), m.group(5)))
317
+ locators[name] = LocatorIR(name=name, strategy=LocatorStrategy.ROLE, value=f"{r_type}[name='{r_name}']")
318
+
319
+ page.locators = list(locators.values())
320
+
321
+ func_blocks = re.findall(r"def\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*(?:->\s*[^:]+)?:([^d]+(?=def\s+|$))", content, re.MULTILINE)
322
+ for f_name, f_params, f_body in func_blocks:
323
+ if f_name.startswith("test_"):
324
+ steps = cls._extract_steps_from_body(f_body)
325
+ project.test_cases.append(TestCaseIR(id=f"TC_{len(project.test_cases)+1:03d}", title=f_name, steps=steps))
326
+ elif f_name != "__init__":
327
+ params = [p.strip().split(":")[0].strip() for p in f_params.split(",") if p.strip() and p.strip() != "self"]
328
+ steps = cls._extract_steps_from_body(f_body)
329
+ page.methods.append(MethodIR(name=f_name, parameters=params, steps=steps))
330
+
331
+ project.pages.append(page)
332
+
333
+ # -------------------------------------------------------------------------
334
+ # C# Parser (.NET Selenium / Playwright)
335
+ # -------------------------------------------------------------------------
336
+ @classmethod
337
+ def _parse_csharp(cls, content: str, project: ProjectIR, framework: str):
338
+ class_match = re.search(r"class\s+([A-Za-z0-9_]+)(?:\s*:\s*([A-Za-z0-9_]+))?", content)
339
+ page_name = class_match.group(1) if class_match else f"{project.name}Page"
340
+ base_class = class_match.group(2) if (class_match and class_match.group(2)) else "BasePage"
341
+
342
+ page = PageClassIR(name=page_name, base_class=base_class)
343
+ project.pages.append(page)
344
+
345
+ # -------------------------------------------------------------------------
346
+ # Generic Fallback Parser
347
+ # -------------------------------------------------------------------------
348
+ @classmethod
349
+ def _parse_generic(cls, content: str, project: ProjectIR):
350
+ page = PageClassIR(name=f"{project.name}Page")
351
+ steps = cls._extract_steps_from_body(content)
352
+ project.test_cases.append(TestCaseIR(id="TC_001", title=project.name, steps=steps))
353
+ project.pages.append(page)
354
+
355
+ # -------------------------------------------------------------------------
356
+ # Universal Step Extractor (With Async/Sync Bridge & Antipattern Sanitizer)
357
+ # -------------------------------------------------------------------------
358
+ @classmethod
359
+ def _extract_steps_from_body(cls, body: str) -> List[StepIR]:
360
+ steps: List[StepIR] = []
361
+ # Unroll Cypress .then() closures into sequential lines
362
+ normalized_body = re.sub(r"\.then\s*\(\s*\([^)]*\)\s*=>\s*\{?", "\n", body)
363
+
364
+ for line in normalized_body.splitlines():
365
+ line_str = line.strip()
366
+ if not line_str or line_str.startswith(("//", "#", "/*", "*")) or line_str in ["}", "});"]:
367
+ continue
368
+
369
+ # 0. Detect Hardcoded Sleep Antipatterns (Thread.sleep, WebUI.delay, cy.wait, time.sleep)
370
+ sleep_m = re.search(r"(?:Thread\.sleep|WebUI\.delay|time\.sleep|cy\.wait)\s*\(\s*(\d+)\s*\)", line_str, re.IGNORECASE)
371
+ if sleep_m:
372
+ val = int(sleep_m.group(1))
373
+ ms = val * 1000 if val <= 60 else val # WebUI.delay(3) vs Thread.sleep(3000)
374
+ steps.append(StepIR(
375
+ action=ActionType.WAIT,
376
+ wait_strategy=WaitStrategy.STATIC_SLEEP,
377
+ is_sanitized_wait=True,
378
+ timeout_ms=ms,
379
+ description=f"Auto-healed from static sleep {line_str}"
380
+ ))
381
+ continue
382
+
383
+ # 1. Cypress Command Chaining: cy.get('selector').action(...)
384
+ cy_chain = re.search(r"cy\.get\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)\s*\.\s*(click|type|clear|should)\s*\(([^)]*)\)", line_str, re.IGNORECASE)
385
+ if cy_chain:
386
+ sel = extract_quoted_string((cy_chain.group(1), cy_chain.group(2)))
387
+ cy_act = cy_chain.group(3).lower()
388
+ cy_args = cy_chain.group(4)
389
+ if cy_act == "click":
390
+ steps.append(StepIR(action=ActionType.CLICK, target=sel, description=f"Cypress click on {sel}"))
391
+ elif cy_act in ["type", "clear"]:
392
+ type_val = extract_quoted_string(re.search(r"(?:\"([^\"]*)\"|'([^']*)')", cy_args).groups()) if re.search(r"(?:\"([^\"]*)\"|'([^']*)')", cy_args) else ""
393
+ steps.append(StepIR(action=ActionType.FILL, target=sel, value=type_val, description=f"Cypress type on {sel}"))
394
+ elif cy_act == "should":
395
+ steps.append(StepIR(action=ActionType.ASSERT, target=sel, assertion_type=AssertionType.VISIBLE, description=line_str))
396
+ continue
397
+
398
+ # 2. Navigation
399
+ nav_m = re.search(r"(?:goto|navigate|openBrowser|navigateToUrl|visit|get)\s*\(\s*(?:\"([^\"]*)\"|'([^']*)')\s*\)", line_str, re.IGNORECASE)
400
+ if nav_m:
401
+ val = extract_quoted_string((nav_m.group(1), nav_m.group(2)))
402
+ steps.append(StepIR(action=ActionType.GOTO, value=val, description=f"Navigate to {val}"))
403
+ continue
404
+
405
+ # 3. Click
406
+ click_m = re.search(r"(?:click|clickJS)\s*\(\s*(?:(?:\"([^\"]*)\"|'([^']*)')|([A-Za-z0-9_$.]+))?\s*\)", line_str, re.IGNORECASE)
407
+ if click_m:
408
+ target = extract_quoted_string((click_m.group(1), click_m.group(2))) or click_m.group(3) or "element"
409
+ target_clean = target.replace("this.", "").replace("self.", "")
410
+ steps.append(StepIR(action=ActionType.CLICK, target=target_clean, description=f"Click on {target_clean}"))
411
+ continue
412
+
413
+ # 4. Fill
414
+ fill_m = re.search(r"(?:fill|sendKeys|setText|type)\s*\(\s*(?:([A-Za-z0-9_$.]+)\s*,\s*)?(?:\"([^\"]*)\"|'([^']*)')\s*\)", line_str, re.IGNORECASE)
415
+ if fill_m:
416
+ target = fill_m.group(1) or "input"
417
+ val = extract_quoted_string((fill_m.group(2), fill_m.group(3)))
418
+ target_clean = target.replace("this.", "").replace("self.", "")
419
+ steps.append(StepIR(action=ActionType.FILL, target=target_clean, value=val, description=f"Fill {val} into {target_clean}"))
420
+ continue
421
+
422
+ # 5. Assertions
423
+ if any(k in line_str for k in ["expect", "Assert.", "verify", "should("]):
424
+ if "visible" in line_str.lower() or "present" in line_str.lower():
425
+ steps.append(StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.VISIBLE, description=line_str))
426
+ elif "equal" in line_str.lower() or "match" in line_str.lower():
427
+ steps.append(StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.EQUALS, description=line_str))
428
+ else:
429
+ steps.append(StepIR(action=ActionType.ASSERT, description=line_str))
430
+ continue
431
+
432
+ if "(" in line_str and ")" in line_str:
433
+ steps.append(StepIR(action=ActionType.CUSTOM_CALL, description=line_str))
434
+
435
+ return steps
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Omni-Transpiler & Repository Scanner
4
+ -----------------------------------
5
+ Connects OmniParser and OmniEmitter to provide:
6
+ 1. Instant 0-token transpilation between test automation frameworks.
7
+ 2. Recursive repo scanner that inventories Page Classes, Locators, and Test Cases
8
+ across polyglot codebases (Java, Groovy, TS, Python, C#).
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import json
14
+ import argparse
15
+ from typing import Dict, List, Any, Optional
16
+
17
+ # Dynamic imports
18
+ try:
19
+ from tools.universal_ir import ProjectIR, resolve_hermetic_fixtures, analyze_data_dependencies
20
+ from tools.omni_parser import OmniParser
21
+ from tools.omni_emitter import OmniEmitter
22
+ except ImportError:
23
+ from universal_ir import ProjectIR, resolve_hermetic_fixtures, analyze_data_dependencies
24
+ from omni_parser import OmniParser
25
+ from omni_emitter import OmniEmitter
26
+
27
+
28
+ def transpile_file(
29
+ source_path: str,
30
+ target_framework: str,
31
+ output_dir: Optional[str] = None,
32
+ hermetic: bool = True
33
+ ) -> Dict[str, str]:
34
+ """Transpiles a single automation source file to the target framework."""
35
+ if not os.path.exists(source_path):
36
+ raise FileNotFoundError(f"Source file not found: {source_path}")
37
+
38
+ # 1. Parse source with 0-token deterministic parser
39
+ project_ir = OmniParser.parse_file(source_path)
40
+
41
+ # 2. Resolve data dependencies & inject hermetic pre-flight fixtures
42
+ if hermetic:
43
+ project_ir = resolve_hermetic_fixtures(project_ir)
44
+
45
+ # 3. Emit target code
46
+ emitted_files = OmniEmitter.emit(project_ir, target_framework)
47
+
48
+ # 4. Write to disk if output_dir specified
49
+ if output_dir:
50
+ os.makedirs(output_dir, exist_ok=True)
51
+ for rel_path, content in emitted_files.items():
52
+ full_out_path = os.path.join(output_dir, rel_path)
53
+ os.makedirs(os.path.dirname(full_out_path), exist_ok=True)
54
+ with open(full_out_path, "w", encoding="utf-8") as f:
55
+ f.write(content)
56
+
57
+ return emitted_files
58
+
59
+
60
+ def scan_repository(repo_path: str) -> Dict[str, Any]:
61
+ """
62
+ Recursively scans a repository and builds a unified polyglot inventory
63
+ of all Page Objects, Locators, and Test Cases with 0 tokens.
64
+ """
65
+ if not os.path.exists(repo_path):
66
+ raise FileNotFoundError(f"Path does not exist: {repo_path}")
67
+
68
+ inventory = {
69
+ "repository": os.path.abspath(repo_path),
70
+ "total_files_scanned": 0,
71
+ "languages": {},
72
+ "frameworks": {},
73
+ "pages": [],
74
+ "test_cases": [],
75
+ "total_locators": 0,
76
+ "total_methods": 0
77
+ }
78
+
79
+ test_extensions = {".java", ".ts", ".js", ".groovy", ".py", ".cs"}
80
+ exclude_dirs = {"node_modules", ".git", ".gradle", "build", "target", "dist", ".gemini", "__pycache__"}
81
+
82
+ for root, dirs, files in os.walk(repo_path):
83
+ dirs[:] = [d for d in dirs if d not in exclude_dirs]
84
+ for f in files:
85
+ ext = os.path.splitext(f)[1].lower()
86
+ if ext in test_extensions:
87
+ file_full = os.path.join(root, f)
88
+ try:
89
+ p_ir = OmniParser.parse_file(file_full)
90
+ inventory["total_files_scanned"] += 1
91
+
92
+ # Language counts
93
+ inventory["languages"][p_ir.source_language] = inventory["languages"].get(p_ir.source_language, 0) + 1
94
+ inventory["frameworks"][p_ir.source_framework] = inventory["frameworks"].get(p_ir.source_framework, 0) + 1
95
+
96
+ for p in p_ir.pages:
97
+ if p.locators or p.methods:
98
+ inventory["pages"].append({
99
+ "name": p.name,
100
+ "file": os.path.relpath(file_full, repo_path).replace("\\", "/"),
101
+ "locators_count": len(p.locators),
102
+ "methods_count": len(p.methods)
103
+ })
104
+ inventory["total_locators"] += len(p.locators)
105
+ inventory["total_methods"] += len(p.methods)
106
+
107
+ for tc in p_ir.test_cases:
108
+ inventory["test_cases"].append({
109
+ "id": tc.id,
110
+ "title": tc.title,
111
+ "file": os.path.relpath(file_full, repo_path).replace("\\", "/"),
112
+ "steps_count": len(tc.steps)
113
+ })
114
+ except Exception:
115
+ pass
116
+
117
+ return inventory
118
+
119
+
120
+ def main():
121
+ parser = argparse.ArgumentParser(description="Omni-Transpiler & Polyglot Test Inventory Scanner")
122
+ sub = parser.add_subparsers(dest="cmd")
123
+
124
+ # transpile command
125
+ p_trans = sub.add_parser("transpile", help="Transpile test files across languages and frameworks")
126
+ p_trans.add_argument("source", help="Source file path")
127
+ p_trans.add_argument("--to", required=True, choices=OmniEmitter.SUPPORTED_TARGETS, help="Target framework")
128
+ p_trans.add_argument("--out", default=None, help="Output directory")
129
+ p_trans.add_argument("--no-hermetic", action="store_true", help="Disable automatic hermetic data dependency resolution")
130
+
131
+ # scan command
132
+ p_scan = sub.add_parser("scan", help="Scan repository and build polyglot test inventory")
133
+ p_scan.add_argument("path", default=".", nargs="?", help="Directory to scan")
134
+ p_scan.add_argument("--json", action="store_true", help="Output raw JSON")
135
+
136
+ args = parser.parse_args()
137
+
138
+ if args.cmd == "transpile":
139
+ res = transpile_file(args.source, args.to, args.out, hermetic=not args.no_hermetic)
140
+ print(f"✅ Successfully transpiled '{args.source}' -> {args.to} ({len(res)} files generated):")
141
+ for p, code in res.items():
142
+ print(f" • {p} ({len(code.splitlines())} lines)")
143
+ if not args.out:
144
+ print("---")
145
+ print(code)
146
+ print("---")
147
+
148
+ elif args.cmd == "scan":
149
+ inv = scan_repository(args.path)
150
+ if args.json:
151
+ print(json.dumps(inv, indent=2))
152
+ else:
153
+ print("======================================================================")
154
+ print(" OMNI-AUTOMATION POLYGLOT REPOSITORY INVENTORY (0-TOKEN AST SCAN)")
155
+ print("======================================================================")
156
+ print(f"📁 Target Directory : {inv['repository']}")
157
+ print(f"📄 Scanned Files : {inv['total_files_scanned']}")
158
+ print(f"🌐 Languages Detected : {inv['languages']}")
159
+ print(f"⚡ Frameworks : {inv['frameworks']}")
160
+ print(f"🏛️ Page Classes Found : {len(inv['pages'])}")
161
+ print(f"🎯 Total Locators : {inv['total_locators']}")
162
+ print(f"⚙️ Action Methods : {inv['total_methods']}")
163
+ print(f"🧪 Test Cases Found : {len(inv['test_cases'])}")
164
+ print("======================================================================")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()