synapse-hub 2.2.4 → 2.4.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 (311) hide show
  1. package/.agents/agents/failure-analyst.md +11 -0
  2. package/.agents/agents/live-explorer.md +28 -0
  3. package/.agents/agents/orchestrator.md +20 -4
  4. package/.agents/agents/pom-crafter.md +12 -0
  5. package/.agents/agents/quality-gatekeeper.md +19 -4
  6. package/.agents/agents/test-runner.md +11 -0
  7. package/.agents/rules/communication-style.md +40 -0
  8. package/.agents/rules/context-engineering.md +23 -0
  9. package/.agents/rules/git-push-protocol.md +10 -16
  10. package/.agents/rules/graphify.md +14 -14
  11. package/.agents/rules/mandatory-playwright-live.md +29 -29
  12. package/.agents/rules/mobile_appium_rules.md +132 -0
  13. package/.agents/rules/performance_testing_rules.md +74 -0
  14. package/.agents/rules/systematic-debugging.md +6 -6
  15. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
  16. package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
  17. package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
  18. package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
  19. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  20. package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
  21. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
  22. package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
  23. package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
  24. package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
  25. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
  26. package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
  27. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
  28. package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
  29. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  30. package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
  31. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  32. package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
  33. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  34. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  35. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
  36. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
  37. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
  38. package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
  39. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  40. package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
  41. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  42. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
  43. package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
  44. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  45. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  46. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  47. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  48. package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
  49. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
  50. package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
  51. package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
  52. package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
  53. package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
  54. package/.agents/skills/github-actions-templates/SKILL.md +47 -304
  55. package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
  56. package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
  57. package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
  58. package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
  59. package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
  60. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  61. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  62. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  63. package/.agents/skills/lam-script-generator/SKILL.md +42 -1
  64. package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
  65. package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
  66. package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
  67. package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
  68. package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
  69. package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
  70. package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
  71. package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
  72. package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
  73. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  74. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  75. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  76. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  77. package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
  78. package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
  79. package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
  80. package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
  81. package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
  82. package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
  83. package/.agents/skills/self-healing-locator/SKILL.md +24 -10
  84. package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
  85. package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
  86. package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
  87. package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
  88. package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
  89. package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
  90. package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
  91. package/.agents/skills/systematic-debugging/SKILL.md +100 -0
  92. package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
  93. package/.agents/skills/test-driven-development/SKILL.md +114 -0
  94. package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
  95. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  96. package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
  97. package/.agents/skills/verification-before-completion/SKILL.md +100 -0
  98. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
  99. package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
  100. package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
  101. package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
  102. package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
  103. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  104. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  105. package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
  106. package/.agents/templates/completion_report.md +57 -57
  107. package/.agents/workflows/analyze_requirement_document.md +18 -0
  108. package/.agents/workflows/export_testcases_excel.md +32 -0
  109. package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
  110. package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
  111. package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
  112. package/.agents/workflows/generate_requirements_from_website.md +25 -0
  113. package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
  114. package/.agents/workflows/graphify.md +10 -10
  115. package/.agents/workflows/verify.md +2 -2
  116. package/.cursor/rules/language.mdc +7 -7
  117. package/.cursor/rules/synapse-qa.mdc +23 -0
  118. package/.cursorrules +7 -0
  119. package/.pre-commit-hooks.yaml +14 -0
  120. package/.windsurfrules +7 -0
  121. package/AGENTS.md +54 -15
  122. package/CLAUDE.md +35 -12
  123. package/LICENSE +30 -0
  124. package/NOTICE +180 -0
  125. package/README.en.md +325 -0
  126. package/README.md +247 -516
  127. package/action.yml +51 -0
  128. package/benchmarks/qa_eval/README.md +71 -0
  129. package/benchmarks/qa_eval/build_dataset.py +677 -0
  130. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
  131. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
  132. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
  133. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
  134. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
  135. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
  136. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
  137. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
  138. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
  139. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
  140. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
  141. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
  142. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
  143. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
  144. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
  145. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
  146. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
  147. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
  148. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
  149. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
  150. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
  151. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
  152. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
  153. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
  154. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
  155. package/benchmarks/qa_eval/eval_runner.py +418 -0
  156. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
  157. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
  158. package/benchmarks/qa_eval/reports/eval_results.json +105 -0
  159. package/bin/agent-qa.js +10 -3
  160. package/docs/ARCHITECTURE.md +107 -0
  161. package/docs/CI_CD_INTEGRATION.md +94 -0
  162. package/docs/CLI_REFERENCE.md +157 -0
  163. package/docs/CONTRIBUTING.md +91 -0
  164. package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
  165. package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
  166. package/docs/adopted-ecosystem-tools.md +87 -0
  167. package/docs/schemas/synapse.config.schema.json +64 -0
  168. package/examples/cypress-ecommerce-suite/README.md +45 -0
  169. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
  170. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
  171. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
  172. package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
  173. package/examples/cypress-ecommerce-suite/package.json +13 -0
  174. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
  175. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
  176. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
  177. package/examples/katalon-suite/Profiles/default.glbl +12 -0
  178. package/examples/katalon-suite/README.md +18 -0
  179. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
  180. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
  181. package/examples/katalon-suite/katalon_suite.prj +21 -0
  182. package/examples/playwright-crm-suite/README.md +40 -0
  183. package/examples/playwright-crm-suite/package.json +14 -0
  184. package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
  185. package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
  186. package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
  187. package/examples/playwright-crm-suite/playwright.config.ts +27 -0
  188. package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
  189. package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
  190. package/examples/plugins/sample-custom-plugin/README.md +14 -0
  191. package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
  192. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
  193. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
  194. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
  195. package/examples/practices/README.md +22 -0
  196. package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
  197. package/examples/practices/requirements/crm/requirements_customers.md +462 -0
  198. package/examples/practices/requirements/crm/requirements_login.md +202 -0
  199. package/examples/practices/requirements/crm/requirements_projects.md +396 -0
  200. package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
  201. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
  202. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  203. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
  204. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  205. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  206. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
  207. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
  208. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  209. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  210. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
  211. package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
  212. package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
  213. package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
  214. package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
  215. package/examples/pytest-api-suite/README.md +45 -0
  216. package/examples/pytest-api-suite/conftest.py +31 -0
  217. package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
  218. package/examples/pytest-api-suite/requirements.txt +2 -0
  219. package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
  220. package/examples/selenium-suite/README.md +18 -0
  221. package/examples/selenium-suite/pages/base_page.py +27 -0
  222. package/examples/selenium-suite/pages/login_page.py +29 -0
  223. package/examples/selenium-suite/pages/order_page.py +25 -0
  224. package/examples/selenium-suite/pytest.ini +6 -0
  225. package/examples/selenium-suite/requirements.txt +2 -0
  226. package/examples/selenium-suite/tests/test_login.py +10 -0
  227. package/examples/selenium-suite/tests/test_order.py +10 -0
  228. package/package.json +22 -7
  229. package/scripts/generate_skill_examples.py +244 -0
  230. package/scripts/install.ps1 +112 -0
  231. package/scripts/install_global_plugin.ps1 +94 -0
  232. package/scripts/install_global_plugin.sh +49 -0
  233. package/scripts/lint_skills.ps1 +57 -0
  234. package/scripts/package_dist.py +323 -0
  235. package/scripts/setup.sh +59 -0
  236. package/scripts/update_hub.ps1 +19 -0
  237. package/scripts/update_hub.sh +15 -0
  238. package/skills_manifest.json +209 -6
  239. package/synapse.config.json +30 -0
  240. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
  241. package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
  242. package/templates/ai_bridges/CLAUDE.md +33 -16
  243. package/templates/ai_bridges/copilot-instructions.md +14 -21
  244. package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
  245. package/templates/ai_bridges/windsurfrules +21 -21
  246. package/templates/ci/github-actions-synapse.yml +71 -4
  247. package/templates/welcome.html.tmpl +1674 -0
  248. package/tools/.infer_baseline.json +1102 -0
  249. package/tools/TOOLS_RUNBOOK.md +92 -130
  250. package/tools/__init__.py +26 -0
  251. package/tools/agent_canary.py +501 -501
  252. package/tools/analyze_project.py +31 -17
  253. package/tools/api_fuzzer.py +278 -6
  254. package/tools/author_integrity.py +6 -6
  255. package/tools/auto_heal.py +1 -1
  256. package/tools/browser_helpers.js +105 -7
  257. package/tools/bug_reporter.py +31 -1
  258. package/tools/bump_version.py +23 -3
  259. package/tools/cache_resolver.py +248 -14
  260. package/tools/check_env.py +55 -14
  261. package/tools/check_version_sync.py +153 -0
  262. package/tools/ci_healing_hook.py +1 -1
  263. package/tools/cli.py +263 -13
  264. package/tools/count_tokens.py +1257 -0
  265. package/tools/dashboard_generator.py +2638 -0
  266. package/tools/db_sandbox.py +198 -20
  267. package/tools/e2e_flow_prober.ps1 +12 -64
  268. package/tools/e2e_flow_prober.py +99 -0
  269. package/tools/e2e_flow_prober.sh +4 -0
  270. package/tools/flaky_detector.py +4 -3
  271. package/tools/generate_allure_report.ps1 +12 -29
  272. package/tools/generate_allure_report.py +77 -0
  273. package/tools/generate_allure_report.sh +4 -0
  274. package/tools/generate_jmx_plan.py +257 -58
  275. package/tools/generate_test_reports.ps1 +12 -134
  276. package/tools/generate_test_reports.py +133 -0
  277. package/tools/generate_test_reports.sh +4 -0
  278. package/tools/gherkin_suite.py +758 -0
  279. package/tools/git_pre_commit_guard.py +12 -6
  280. package/tools/goldfish_review.py +366 -0
  281. package/tools/heal_pr_automator.py +1 -1
  282. package/tools/heuristic_locator_matcher.py +170 -21
  283. package/tools/infer_linter.py +612 -70
  284. package/tools/lint_ai_docs.py +64 -0
  285. package/tools/matrix_sharder.py +1 -1
  286. package/tools/mcp_excel_server.ps1 +12 -250
  287. package/tools/mcp_excel_server.py +249 -0
  288. package/tools/mcp_excel_server.sh +4 -0
  289. package/tools/mcp_server.py +368 -144
  290. package/tools/md_to_xlsx.py +307 -0
  291. package/tools/micro_mutator.py +300 -69
  292. package/tools/omni_transpiler.py +2 -2
  293. package/tools/output_formatter.py +128 -0
  294. package/tools/pinpoint_failure.py +88 -88
  295. package/tools/project_state.py +2 -2
  296. package/tools/repo_map.py +1 -1
  297. package/tools/safety_firewall.py +196 -45
  298. package/tools/setup_project.py +294 -30
  299. package/tools/spec_drift_sentinel.py +439 -0
  300. package/tools/sync_agent_rules.py +248 -0
  301. package/tools/telemetry_correlator.py +45 -0
  302. package/tools/test_impact.py +51 -12
  303. package/tools/tms_sync_bridge.py +21 -5
  304. package/tools/universal_ir.py +3 -0
  305. package/tools/update_sentinel.py +87 -15
  306. package/tools/verify_all.ps1 +13 -0
  307. package/tools/verify_all.py +463 -79
  308. package/tools/visual_diff_canvas.py +1 -0
  309. package/tools/visual_som_helper.py +1 -1
  310. package/tools/watch_mode.py +305 -0
  311. package/tools/welcome_page.py +291 -0
@@ -16,6 +16,7 @@ import sys
16
16
  import re
17
17
  import json
18
18
  import glob
19
+ import html as html_lib
19
20
  import argparse
20
21
  from datetime import datetime
21
22
 
@@ -41,20 +42,96 @@ CYAN = "\033[96m"
41
42
  BOLD = "\033[1m"
42
43
  RESET = "\033[0m"
43
44
 
44
- # Generic Detection regexes
45
+ # Optional Tree-Sitter Integration for compiler-grade AST parsing across TS/Java/C#
46
+ try:
47
+ import tree_sitter # type: ignore
48
+ HAS_TREE_SITTER = True
49
+ except ImportError:
50
+ HAS_TREE_SITTER = False
51
+
52
+
53
+ def parse_with_tree_sitter(file_path, content):
54
+ """
55
+ Optional compiler-grade AST parsing via tree-sitter when installed.
56
+ Extracts syntactic AST nodes for TypeScript, JavaScript, Java, and C#.
57
+ Returns None if tree-sitter is unavailable or grammar is not installed.
58
+ """
59
+ if not HAS_TREE_SITTER:
60
+ return None
61
+ try:
62
+ from tree_sitter_languages import get_parser # type: ignore
63
+ ext = os.path.splitext(file_path)[1].lower()
64
+ lang_map = {
65
+ ".ts": "typescript",
66
+ ".js": "javascript",
67
+ ".java": "java",
68
+ ".cs": "c_sharp",
69
+ }
70
+ lang_name = lang_map.get(ext)
71
+ if not lang_name:
72
+ return None
73
+ parser = get_parser(lang_name)
74
+ return parser.parse(bytes(content, "utf-8"))
75
+ except Exception:
76
+ return None
77
+
78
+ # Generic Detection regexes (including Cypress .should and .and assertions)
45
79
  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'
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*\('
47
81
  )
48
82
 
83
+ # 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(() => {})
49
85
  EMPTY_CATCH_PATTERN = re.compile(
50
- r'catch\s*\([^\)]*\)\s*\{\s*\}',
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*\)'
88
+ r'|\bexcept(?:\s+[^:]*)?:\s*(?:pass|\.\.\.)(?:\s*$|\s*#))',
89
+ re.MULTILINE
90
+ )
91
+
92
+ # Whitelisted best-effort compatibility calls (UTF-8 console reconfigure, file
93
+ # permission chmod, stream flush). An empty catch guarding ONLY these is an
94
+ # intentional no-op-on-failure idiom, not a swallowed test failure.
95
+ BENIGN_TRY_CALL_PATTERN = re.compile(
96
+ r'^[\w\.]*\.(?:reconfigure|chmod|flush)\s*\('
97
+ )
98
+
99
+ # Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, expect(true).toBeTruthy())
100
+ TAUTOLOGY_PATTERN = re.compile(
101
+ r'(?:expect\s*\(\s*(?:true|1)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy)\s*\(\s*(?:true|1)?\s*\)'
102
+ 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*\)'
105
+ r'|assertTrue\s*\(\s*(?:true|1)\s*\))',
106
+ re.IGNORECASE
107
+ )
108
+
109
+ # Rule L4: Unawaited Playwright assertions in JS/TS
110
+ # Expanded with all standard Playwright async matchers and optional .not modifier
111
+ PLAYWRIGHT_ASYNC_MATCHERS = (
112
+ r'(?:toBeVisible|toBeHidden|toHaveText|toHaveValue|toHaveCount|toBeEnabled|toBeDisabled|'
113
+ r'toBeChecked|toHaveURL|toContainText|toHaveTitle|toHaveAttribute|toHaveClass|toHaveId|'
114
+ r'toBeFocused|toBeEmpty|toBeAttached|toBeInViewport|toBeOK)'
115
+ )
116
+ UNAWAITED_EXPECT_PATTERN = re.compile(
117
+ r'\bexpect\s*\([^;\n]+\)\s*(?:\.\s*not)?\s*\.\s*' + PLAYWRIGHT_ASYNC_MATCHERS + r'\s*\(',
51
118
  re.MULTILINE
52
119
  )
53
120
 
54
- THREAD_SLEEP_PATTERN = re.compile(
55
- r'(?:Thread\.sleep|WebUI\.delay)\s*\(\s*(\d+)\s*\)'
121
+ # Rule 3: Flaky Static Delays across Groovy, Java, Playwright (JS/TS/Python), Pytest/Python, Cypress
122
+ # Millisecond-based delays: Thread.sleep(ms), waitForTimeout(ms), wait_for_timeout(ms), cy.wait(ms)
123
+ MS_DELAY_PATTERN = re.compile(
124
+ r'(?:Thread\.sleep|(?:page\.)?waitForTimeout|(?:page\.)?wait_for_timeout|cy\.wait)\s*\(\s*([^)]+)\s*\)'
56
125
  )
57
126
 
127
+ # Second-based delays: WebUI.delay(sec), time.sleep(sec)
128
+ SEC_DELAY_PATTERN = re.compile(
129
+ r'(?:WebUI\.delay|time\.sleep)\s*\(\s*([^)]+)\s*\)'
130
+ )
131
+
132
+ # Backward-compatibility alias
133
+ THREAD_SLEEP_PATTERN = MS_DELAY_PATTERN
134
+
58
135
 
59
136
  def strip_comments_and_strings(source_code, preserve_newlines=True):
60
137
  """
@@ -63,12 +140,13 @@ def strip_comments_and_strings(source_code, preserve_newlines=True):
63
140
  Prevents false-positive / false-negative matches in AST linter.
64
141
  """
65
142
  pattern = re.compile(
66
- r'(?P<BLOCK_COMMENT>/\*.*?\*/)|'
67
- r'(?P<LINE_COMMENT>//[^\r\n]*)|'
68
143
  r'(?P<TRIPLE_DOUBLE>"""[\s\S]*?""")|'
69
144
  r'(?P<TRIPLE_SINGLE>\'\'\'[\s\S]*?\'\'\')|'
70
145
  r'(?P<DOUBLE_STRING>"(?:\\.|[^"\\])*")|'
71
- r'(?P<SINGLE_STRING>\'(?:\\.|[^\'\\])*\')',
146
+ r'(?P<SINGLE_STRING>\'(?:\\.|[^\'\\])*\')|'
147
+ r'(?P<BLOCK_COMMENT>/\*.*?\*/)|'
148
+ r'(?P<LINE_COMMENT>//[^\r\n]*)|'
149
+ r'(?P<HASH_COMMENT>#[^\r\n]*)',
72
150
  re.DOTALL
73
151
  )
74
152
 
@@ -81,37 +159,227 @@ def strip_comments_and_strings(source_code, preserve_newlines=True):
81
159
  return pattern.sub(replacer, source_code)
82
160
 
83
161
 
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()
162
+ def _is_benign_empty_catch(clean_code, match_start):
163
+ """
164
+ True when an empty catch block exclusively guards whitelisted best-effort
165
+ calls (e.g. sys.stdout.reconfigure(), os.chmod()) - an intentional
166
+ no-op-on-failure compatibility idiom rather than a swallowed test failure.
167
+ Conservative by design: only Python-style 'try:' blocks are whitelisted;
168
+ C-style brace bodies still get flagged.
169
+ """
170
+ body_lines = []
171
+ for line in reversed(clean_code[:match_start].splitlines()):
172
+ stripped = line.strip()
173
+ if not stripped:
174
+ continue
175
+ if re.match(r'try\s*(?:[:{]|$)', stripped):
176
+ return bool(body_lines) and all(BENIGN_TRY_CALL_PATTERN.match(l) for l in body_lines)
177
+ body_lines.append(stripped)
178
+ return False
179
+
180
+
181
+ def load_config(repo_root=None, config_path=None):
182
+ """Loads configuration from synapse.config.json if present."""
183
+ if config_path and os.path.exists(config_path):
184
+ try:
185
+ with open(config_path, "r", encoding="utf-8") as f:
186
+ return json.load(f)
187
+ except (OSError, json.JSONDecodeError):
188
+ pass
189
+
190
+ root = repo_root or REPO_ROOT
191
+ candidates = [
192
+ os.path.join(root, "synapse.config.json"),
193
+ os.path.join(os.getcwd(), "synapse.config.json")
194
+ ]
195
+ for c in candidates:
196
+ if os.path.exists(c):
197
+ try:
198
+ with open(c, "r", encoding="utf-8") as f:
199
+ return json.load(f)
200
+ except (OSError, json.JSONDecodeError):
201
+ pass
202
+ return {}
203
+
204
+
205
+ def load_baseline(baseline_path):
206
+ """Loads existing baseline findings to suppress known technical debt."""
207
+ if not baseline_path or not os.path.exists(baseline_path):
208
+ return set()
209
+ try:
210
+ with open(baseline_path, "r", encoding="utf-8") as f:
211
+ data = json.load(f)
212
+ items = data.get("defects", data) if isinstance(data, dict) else data
213
+ signatures = set()
214
+ for it in items:
215
+ f_path = it.get("file", "").replace("\\", "/")
216
+ d_type = it.get("type", "").upper()
217
+ d_line = it.get("line")
218
+ signatures.add((f_path, d_type))
219
+ if d_line is not None:
220
+ signatures.add((f_path, d_type, d_line))
221
+ return signatures
222
+ except (OSError, json.JSONDecodeError):
223
+ return set()
224
+
225
+
226
+ def save_baseline(baseline_path, all_defects):
227
+ """Saves current findings into a baseline JSON file."""
228
+ os.makedirs(os.path.dirname(os.path.abspath(baseline_path)), exist_ok=True)
229
+ with open(baseline_path, "w", encoding="utf-8") as f:
230
+ json.dump({
231
+ "generated_at": datetime.now().isoformat(),
232
+ "total_defects": len(all_defects),
233
+ "defects": all_defects
234
+ }, f, indent=2, ensure_ascii=False)
235
+
236
+
237
+ def format_sarif(report_data):
238
+ """Formats findings in standard SARIF 2.1.0 JSON format for GitHub Code Scanning."""
239
+ results = []
240
+ rules_dict = {}
241
+ for d in report_data.get("all_defects", []):
242
+ rule_id = d.get("type", "STATIC_DEFECT")
243
+ if rule_id not in rules_dict:
244
+ rules_dict[rule_id] = {
245
+ "id": rule_id,
246
+ "shortDescription": {"text": d.get("message", rule_id)},
247
+ "defaultConfiguration": {
248
+ "level": "error" if d.get("severity") == "CRITICAL" else "warning"
249
+ }
250
+ }
251
+ results.append({
252
+ "ruleId": rule_id,
253
+ "level": "error" if d.get("severity") == "CRITICAL" else "warning",
254
+ "message": {"text": d.get("message", "")},
255
+ "locations": [{
256
+ "physicalLocation": {
257
+ "artifactLocation": {"uri": d.get("file", "")},
258
+ "region": {"startLine": max(1, d.get("line", 1))}
259
+ }
260
+ }]
261
+ })
262
+ sarif_doc = {
263
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
264
+ "version": "2.1.0",
265
+ "runs": [{
266
+ "tool": {
267
+ "driver": {
268
+ "name": "synapse-infer-linter",
269
+ "version": "2.3.0",
270
+ "rules": list(rules_dict.values())
271
+ }
272
+ },
273
+ "results": results
274
+ }]
275
+ }
276
+ return json.dumps(sarif_doc, indent=2, ensure_ascii=False)
277
+
278
+
279
+ def format_junit(report_data):
280
+ """Formats findings in standard JUnit XML format."""
281
+ total = max(1, report_data.get("total_scripts", 0))
282
+ defects = report_data.get("all_defects", [])
283
+ failures = len([d for d in defects if d.get("severity") == "CRITICAL"])
284
+ warnings = len([d for d in defects if d.get("severity") != "CRITICAL"])
285
+ lines = [
286
+ '<?xml version="1.0" encoding="utf-8"?>',
287
+ f'<testsuite name="synapse-infer-linter" tests="{max(total, len(defects))}" failures="{failures}" errors="{warnings}">'
288
+ ]
289
+ for d in defects:
290
+ cname = d.get("file", "").replace("/", ".").replace("\\", ".")
291
+ tname = f"{d.get('type')}_L{d.get('line', 1)}"
292
+ msg = html_lib.escape(d.get("message", ""))
293
+ tag = "failure" if d.get("severity") == "CRITICAL" else "error"
294
+ lines.append(f' <testcase classname="{cname}" name="{tname}">')
295
+ lines.append(f' <{tag} message="{msg}" type="{d.get("type")}">Line {d.get("line", 1)}: {msg}</{tag}>')
296
+ lines.append(' </testcase>')
297
+ if not defects:
298
+ lines.append(' <testcase classname="synapse.linter" name="clean_run"/>')
299
+ lines.append('</testsuite>')
300
+ return "\n".join(lines)
301
+
302
+
303
+ def classify_file_role(file_path: str) -> str:
304
+ """
305
+ Classifies a source file into:
306
+ - 'CONFIG': Configuration files (playwright.config, cypress.config, conftest, wdio.conf, jest.config)
307
+ - 'PAGE_OBJECT': Page Classes / POM (files in pages/, ending in Page.ts/js/py/groovy/java, or BasePage)
308
+ - 'HELPER': Utilities / Fixtures / Custom Keywords (files in helpers/, fixtures/, utils/, support/)
309
+ - 'TEST_SCRIPT': Test specifications / test cases (test_*, *test, *.spec, *.cy, *.tc, files in tests/, Scripts/)
310
+ """
311
+ norm = file_path.replace("\\", "/").lower()
312
+ base = os.path.basename(norm)
313
+ parts = norm.split("/")[:-1]
314
+
315
+ # 1. Config files
316
+ if any(cfg in base for cfg in ["playwright.config", "cypress.config", "wdio.conf", "jest.config", "karma.conf", "conftest.py", "tsconfig"]):
317
+ return "CONFIG"
318
+ if any(p in ("config", ".config") for p in parts):
319
+ return "CONFIG"
320
+
321
+ # 2. Page Objects (Clean POM)
322
+ if any(p in ("pages", "page_objects", "page-objects", "po") for p in parts) or \
323
+ ("keywords" in parts and "pages" in norm):
324
+ return "PAGE_OBJECT"
325
+ if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
326
+ return "PAGE_OBJECT"
327
+
328
+ # 3. Helpers & Fixtures / Tools & Utilities
329
+ if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin") for p in parts):
330
+ return "HELPER"
331
+ if any(base.endswith(sfx) for sfx in ("helper.ts", "helper.js", "helper.py", "helper.groovy", "utils.ts", "utils.js", "utils.py", "fixture.ts", "fixture.js")):
332
+ return "HELPER"
333
+
334
+ # 4. Test Scripts
335
+ test_suffixes = ("_test.py", ".spec.ts", ".spec.js", ".test.ts", ".test.js", ".cy.js", ".cy.ts", ".spec.jsx", ".test.jsx", ".tc", "test.java", "tests.java", "spec.groovy")
336
+ if base.startswith("test_") or base.endswith(test_suffixes):
337
+ return "TEST_SCRIPT"
338
+ if any(p in ("tests", "test", "e2e", "specs", "cypress") for p in parts):
339
+ return "TEST_SCRIPT"
340
+ if "scripts" in parts:
341
+ if file_path.endswith((".groovy", ".tc")) or base.startswith("test_") or base.endswith(test_suffixes):
342
+ return "TEST_SCRIPT"
343
+ return "HELPER"
344
+
345
+ # Default fallback: standalone script without directory context
346
+ return "TEST_SCRIPT"
347
+
348
+
349
+ def analyze_script(file_path, baseline_set=None, disabled_rules=None):
350
+ """Performs static rule scanning on a single test script or automation asset."""
351
+ role = classify_file_role(file_path)
89
352
 
90
- defects = []
91
353
  try:
92
354
  rel_path = os.path.relpath(file_path, REPO_ROOT)
93
355
  except ValueError:
94
356
  rel_path = os.path.abspath(file_path)
95
357
 
358
+ # Config files have zero test assertions by definition
359
+ if role == "CONFIG":
360
+ return {
361
+ "file": rel_path,
362
+ "role": role,
363
+ "defects": [],
364
+ "is_clean": True
365
+ }
366
+
367
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
368
+ content = f.read()
369
+
370
+ defects = []
371
+
96
372
  # Lexically strip comments and string literals for accurate analysis
97
373
  clean_code = strip_comments_and_strings(content, preserve_newlines=True)
98
374
  clean_lines = [l.strip() for l in clean_code.splitlines()]
99
375
 
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
- })
376
+ raw_lines = content.splitlines()
112
377
 
378
+ # Code quality checks across all automation assets (Test Scripts, Page Objects, Helpers)
113
379
  # Rule 2: Swallowed Exceptions (empty catch blocks)
114
380
  for match in EMPTY_CATCH_PATTERN.finditer(clean_code):
381
+ if _is_benign_empty_catch(clean_code, match.start()):
382
+ continue
115
383
  line_no = clean_code[:match.start()].count("\n") + 1
116
384
  defects.append({
117
385
  "type": "SWALLOWED_EXCEPTION",
@@ -120,42 +388,220 @@ def analyze_script(file_path):
120
388
  "message": "Empty catch block detected. Swallowed exceptions cause false-pass test runs."
121
389
  })
122
390
 
123
- # Rule 3: Flaky Static Delays
391
+ # Rule 3: Flaky Static Delays (Playwright, Cypress, Pytest, Katalon, Java/Groovy)
124
392
  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:
393
+ raw_line = raw_lines[idx - 1].strip() if idx <= len(raw_lines) else line.strip()
394
+
395
+ # Millisecond delays
396
+ m_ms = MS_DELAY_PATTERN.search(line)
397
+ if m_ms:
398
+ arg = m_ms.group(1).strip()
399
+ # F08: Ignore Cypress route alias waits like cy.wait('@getUsers')
400
+ if arg.startswith(("@", "'@", '"@')):
401
+ pass
402
+ elif arg.isdigit():
403
+ delay_val = int(arg)
404
+ if delay_val > 0:
405
+ d_type = "EXCESSIVE_HARD_DELAY" if delay_val >= 3000 else "HARD_DELAY_ANTI_PATTERN"
406
+ defects.append({
407
+ "type": d_type,
408
+ "severity": "WARNING",
409
+ "line": idx,
410
+ "message": f"Static sleep of {delay_val}ms found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
411
+ })
412
+ continue
413
+ elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg) and not line.strip().startswith("cy.wait"):
129
414
  defects.append({
130
- "type": "EXCESSIVE_HARD_DELAY",
415
+ "type": "HARD_DELAY_ANTI_PATTERN",
131
416
  "severity": "WARNING",
132
417
  "line": idx,
133
- "message": f"Static sleep of {delay_val}ms found. Replace with dynamic Smart Wait (waitForElement/networkidle)."
418
+ "message": f"Static sleep with variable/expression '{arg}' found ('{raw_line}'). Replace with dynamic Smart Wait."
419
+ })
420
+ continue
421
+
422
+ # Second delays
423
+ m_sec = SEC_DELAY_PATTERN.search(line)
424
+ if m_sec:
425
+ arg = m_sec.group(1).strip()
426
+ try:
427
+ delay_val = float(arg)
428
+ if delay_val > 0:
429
+ delay_ms = int(delay_val * 1000)
430
+ delay_str = f"{delay_val:g}"
431
+ d_type = "EXCESSIVE_HARD_DELAY" if delay_ms >= 3000 else "HARD_DELAY_ANTI_PATTERN"
432
+ defects.append({
433
+ "type": d_type,
434
+ "severity": "WARNING",
435
+ "line": idx,
436
+ "message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
437
+ })
438
+ except ValueError:
439
+ if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg):
440
+ defects.append({
441
+ "type": "HARD_DELAY_ANTI_PATTERN",
442
+ "severity": "WARNING",
443
+ "line": idx,
444
+ "message": f"Static sleep with variable/expression '{arg}' found ('{raw_line}'). Replace with dynamic Smart Wait."
445
+ })
446
+
447
+ # Test Script Specific Invariants (Do NOT apply to Page Objects or Helpers)
448
+ if role == "TEST_SCRIPT":
449
+ executable_lines = [
450
+ l for l in clean_lines
451
+ if l and not l.startswith("import ") and not l.startswith("package ") and not l.startswith("from ")
452
+ ]
453
+ # Rule 1: Empty Test Script
454
+ if len(executable_lines) == 0:
455
+ defects.append({
456
+ "type": "EMPTY_TEST_SCRIPT",
457
+ "severity": "CRITICAL",
458
+ "line": 1,
459
+ "message": "Test script is empty or contains only comments/imports."
460
+ })
461
+
462
+ # Rule 4 (L3): Tautological Assertions
463
+ for idx, line in enumerate(clean_code.splitlines(), 1):
464
+ if TAUTOLOGY_PATTERN.search(line):
465
+ raw_line = raw_lines[idx - 1].strip() if idx <= len(raw_lines) else line.strip()
466
+ defects.append({
467
+ "type": "TAUTOLOGICAL_ASSERTION",
468
+ "severity": "CRITICAL",
469
+ "line": idx,
470
+ "message": f"Tautological assertion detected ('{raw_line}'). Test assertion always evaluates to True without testing system state."
134
471
  })
135
472
 
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
- })
473
+ # Rule 5 (L4): Unawaited Playwright Assertions in JS/TS
474
+ if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
475
+ for match in UNAWAITED_EXPECT_PATTERN.finditer(clean_code):
476
+ prefix = clean_code[max(0, match.start() - 40):match.start()]
477
+ if re.search(r'\b(?:await|return)\s+$', prefix):
478
+ continue
479
+ surrounding = clean_code[max(0, match.start() - 150):match.start()]
480
+ if any(w in surrounding for w in ("Promise.all", "Promise.allSettled", "return ")):
481
+ continue
482
+ line_no = clean_code[:match.start()].count("\n") + 1
483
+ line_text = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
484
+ defects.append({
485
+ "type": "UNAWAITED_ASYNC_ASSERTION",
486
+ "severity": "CRITICAL",
487
+ "line": line_no,
488
+ "message": f"Unawaited async Playwright assertion detected: '{line_text}'. Without 'await', assertion returns a Pending Promise that never validates."
489
+ })
490
+
491
+ # Rule 6 (L2 & L6): Missing Assertions / Fake-Pass
492
+ assert_matches = list(ASSERTION_PATTERN.finditer(clean_code))
493
+ tautology_matches = list(TAUTOLOGY_PATTERN.finditer(clean_code))
494
+ has_real_assert = any(
495
+ not any(tm.start() <= am.start() < tm.end() for tm in tautology_matches)
496
+ for am in assert_matches
497
+ )
498
+
499
+ # F06: Check test cases per function block if multiple tests exist
500
+ TEST_BLOCK_PATTERN = re.compile(
501
+ r'(?m)^[ \t]*(?:(?:test|it)(?:\.(?:only|skip))?\s*\(|def\s+test_[a-zA-Z0-9_]+\s*\(|(?:@Test\s+)?(?:public\s+)?(?:void|def)\s+test[a-zA-Z0-9_]*\s*\()'
502
+ )
503
+ test_matches = list(TEST_BLOCK_PATTERN.finditer(clean_code))
504
+ if len(test_matches) >= 2:
505
+ for i, tm in enumerate(test_matches):
506
+ b_start = tm.start()
507
+ b_end = test_matches[i + 1].start() if i + 1 < len(test_matches) else len(clean_code)
508
+ block_code = clean_code[b_start:b_end]
509
+ b_asserts = list(ASSERTION_PATTERN.finditer(block_code))
510
+ b_tauts = list(TAUTOLOGY_PATTERN.finditer(block_code))
511
+ block_has_real_assert = any(
512
+ not any(tt.start() <= ta.start() < tt.end() for tt in b_tauts)
513
+ for ta in b_asserts
514
+ )
515
+ b_has_interactions = any(
516
+ kw in block_code for kw in ["click", "select", "save", "delete", "create", "search", "navigate", "open", "fill", "goto", "post", "get"]
517
+ )
518
+ b_line = clean_code[:b_start].count("\n") + 1
519
+ if b_has_interactions and not block_has_real_assert:
520
+ defects.append({
521
+ "type": "FAKE_PASS_NO_ASSERTION",
522
+ "severity": "WARNING",
523
+ "line": b_line,
524
+ "message": f"Test block starting at line {b_line} performs interactions but contains zero verification/assertion statements."
525
+ })
526
+ else:
527
+ has_interactions = any(
528
+ kw in clean_code for kw in ["click", "select", "save", "delete", "create", "search", "navigate", "open", "fill", "goto", "post", "get"]
529
+ )
530
+ is_explicit_test = any(t in os.path.basename(file_path).lower() for t in ["test", "spec", "tc-", "tc_"])
531
+
532
+ if has_interactions and not has_real_assert:
533
+ defects.append({
534
+ "type": "FAKE_PASS_NO_ASSERTION",
535
+ "severity": "WARNING",
536
+ "line": 1,
537
+ "message": "Test script performs AUT mutations/interactions but contains zero verification/assertion statements."
538
+ })
539
+ elif is_explicit_test and not has_real_assert and not has_interactions and len(executable_lines) > 0:
540
+ defects.append({
541
+ "type": "NO_ASSERTION_IN_TEST",
542
+ "severity": "WARNING",
543
+ "line": 1,
544
+ "message": "Test case file contains executable logic but has no verification or assertion statements."
545
+ })
546
+
547
+ active_defects = []
548
+ disabled = {r.upper() for r in (disabled_rules or [])}
549
+ norm_rel = rel_path.replace("\\", "/")
550
+
551
+ for d in defects:
552
+ d_type = d.get("type", "").upper()
553
+ if d_type in disabled:
554
+ continue
555
+ line_idx = d.get("line", 1)
556
+
557
+ # Check suppression comment (# synapse-ignore or // synapse-ignore)
558
+ suppressed = False
559
+ candidates = []
560
+ if 1 <= line_idx <= len(raw_lines):
561
+ candidates.append(raw_lines[line_idx - 1])
562
+ if line_idx > 1 and line_idx - 1 <= len(raw_lines):
563
+ candidates.append(raw_lines[line_idx - 2])
564
+ for c in candidates:
565
+ if "synapse-ignore" in c.lower():
566
+ m = re.search(r'synapse-ignore:\s*([A-Za-z0-9_,\s\*\-]+)', c, re.IGNORECASE)
567
+ if m:
568
+ rules = [r.strip().upper() for r in m.group(1).split(",") if r.strip()]
569
+ if "*" in rules or "ALL" in rules or d_type in rules:
570
+ suppressed = True
571
+ break
572
+ if suppressed:
573
+ continue
574
+
575
+ # Check baseline suppression
576
+ if baseline_set:
577
+ if (norm_rel, d_type) in baseline_set or (norm_rel, d_type, line_idx) in baseline_set:
578
+ continue
579
+
580
+ active_defects.append(d)
581
+
582
+ defects = active_defects
148
583
 
149
584
  return {
150
585
  "file": rel_path,
586
+ "role": role,
151
587
  "defects": defects,
152
588
  "is_clean": len(defects) == 0
153
589
  }
154
590
 
155
591
 
156
- def run_infer_linter(target_dir=None):
157
- """Scans test scripts across Groovy, TypeScript, JavaScript, Python, and Java."""
158
- EXTENSIONS = ("*.groovy", "*.ts", "*.js", "*.py", "*.java")
592
+ def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
593
+ output_format="text", baseline_path=None, generate_baseline_path=None, config_path=None):
594
+ """Scans test scripts across Groovy, TypeScript, JavaScript, Python, and Java with baseline and SARIF support."""
595
+ EXTENSIONS = ("*.groovy", "*.ts", "*.tsx", "*.js", "*.jsx", "*.py", "*.java")
596
+
597
+ cfg = load_config(config_path=config_path)
598
+ linter_cfg = cfg.get("infer_linter", {})
599
+ extra_ignored = set(linter_cfg.get("exclude_paths", []))
600
+ disabled_rules = set(linter_cfg.get("disabled_rules", []))
601
+ baseline_set = load_baseline(baseline_path)
602
+
603
+ if linter_cfg.get("strict") is not None and not strict:
604
+ strict = bool(linter_cfg.get("strict"))
159
605
 
160
606
  if target_dir:
161
607
  if os.path.isfile(target_dir):
@@ -163,9 +609,16 @@ def run_infer_linter(target_dir=None):
163
609
  scan_dir = os.path.dirname(target_dir)
164
610
  elif os.path.isdir(target_dir):
165
611
  scan_dir = target_dir
166
- script_files = []
612
+ raw_files = []
167
613
  for ext in EXTENSIONS:
168
- script_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
614
+ raw_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
615
+ # Filter out non-test files like tools, __init__.py, node_modules when scanning generic directory
616
+ ignored_parts = {"tools", "node_modules", ".git", "__pycache__", "venv", ".venv"} | extra_ignored
617
+ script_files = [
618
+ f for f in raw_files
619
+ if not any(part.lower() in ignored_parts for part in os.path.relpath(f, scan_dir).replace("\\", "/").split("/")[:-1])
620
+ and os.path.basename(f) not in {"__init__.py", "setup.py"}
621
+ ]
169
622
  else:
170
623
  scan_dir = target_dir
171
624
  script_files = []
@@ -186,9 +639,10 @@ def run_infer_linter(target_dir=None):
186
639
  critical_count = 0
187
640
  warning_count = 0
188
641
  clean_count = 0
642
+ all_defects = []
189
643
 
190
644
  for s in sorted(set(script_files)):
191
- res = analyze_script(s)
645
+ res = analyze_script(s, baseline_set=baseline_set, disabled_rules=disabled_rules)
192
646
  results.append(res)
193
647
  crit = sum(1 for d in res["defects"] if d["severity"] == "CRITICAL")
194
648
  warn = sum(1 for d in res["defects"] if d["severity"] == "WARNING")
@@ -196,15 +650,84 @@ def run_infer_linter(target_dir=None):
196
650
  warning_count += warn
197
651
  if res["is_clean"]:
198
652
  clean_count += 1
653
+ for d in res["defects"]:
654
+ all_defects.append({
655
+ "file": res["file"],
656
+ "role": res["role"],
657
+ "type": d["type"],
658
+ "severity": d["severity"],
659
+ "line": d["line"],
660
+ "message": d["message"]
661
+ })
662
+
663
+ # If requested to generate a baseline file, save and exit
664
+ if generate_baseline_path:
665
+ save_baseline(generate_baseline_path, all_defects)
666
+ print(f"✅ Generated baseline file with {len(all_defects)} defects at: {generate_baseline_path}")
667
+ return 0
199
668
 
200
669
  total_scripts = len(script_files)
201
670
  if total_scripts == 0:
202
671
  health_score = 0.0
203
672
  clean_pct = 0.0
204
673
  else:
205
- health_score = round(((total_scripts - critical_count) / total_scripts) * 100, 1)
206
674
  clean_pct = round((clean_count / total_scripts) * 100, 1)
675
+ critical_penalty = (critical_count * 20.0) / total_scripts
676
+ raw_score = (clean_count / total_scripts) * 100.0 - critical_penalty
677
+ health_score = max(0.0, min(100.0, round(raw_score, 1)))
678
+
679
+ if clean_count < total_scripts and health_score >= 100.0:
680
+ health_score = 99.0
681
+ if clean_count == 0 and total_scripts > 0:
682
+ health_score = 0.0
683
+
684
+ defect_scripts = [r for r in results if not r["is_clean"]]
685
+ report_data = {
686
+ "timestamp": datetime.now().isoformat(),
687
+ "total_scripts": total_scripts,
688
+ "clean_scripts": clean_count,
689
+ "critical_defects": critical_count,
690
+ "warnings": warning_count,
691
+ "health_score_pct": health_score,
692
+ "defect_details": defect_scripts,
693
+ "all_defects": all_defects
694
+ }
695
+
696
+ # Handle specialized output formats
697
+ if output_format == "sarif":
698
+ sarif_out = format_sarif(report_data)
699
+ if output_path:
700
+ os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
701
+ with open(output_path, "w", encoding="utf-8") as f:
702
+ f.write(sarif_out)
703
+ print(f"SARIF 2.1.0 report saved to: {output_path}")
704
+ else:
705
+ print(sarif_out)
706
+ return 1 if critical_count > 0 else 0
707
+
708
+ if output_format == "junit":
709
+ junit_out = format_junit(report_data)
710
+ if output_path:
711
+ os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
712
+ with open(output_path, "w", encoding="utf-8") as f:
713
+ f.write(junit_out)
714
+ print(f"JUnit XML report saved to: {output_path}")
715
+ else:
716
+ print(junit_out)
717
+ return 1 if critical_count > 0 else 0
718
+
719
+ if output_format == "json":
720
+ json_out = json.dumps(report_data, indent=2, ensure_ascii=False)
721
+ if output_path:
722
+ os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
723
+ with open(output_path, "w", encoding="utf-8") as f:
724
+ f.write(json_out)
725
+ print(f"JSON report saved to: {output_path}")
726
+ else:
727
+ print(json_out)
728
+ return 1 if critical_count > 0 else 0
207
729
 
730
+ # Default: Text output with ANSI banner
208
731
  print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
209
732
  print(f"{BOLD}{CYAN} ENTERPRISE STATIC DEFECT & FAKE-PASS SENTINEL (Meta Infer-Style) {RESET}")
210
733
  print(f"{BOLD}{CYAN}======================================================================{RESET}")
@@ -216,38 +739,41 @@ def run_infer_linter(target_dir=None):
216
739
  print(f" Codebase Health Score : {YELLOW}N/A (0 test scripts discovered){RESET}")
217
740
  else:
218
741
  print(f" Target Scope : {total_scripts} Test Scripts in {os.path.basename(scan_dir)}/ (Polyglot)")
742
+ if baseline_set:
743
+ print(f" Baseline Filter : {CYAN}ACTIVE ({len(baseline_set)} known debt items suppressed){RESET}")
219
744
  print(f" Critical Defects : {RED if critical_count > 0 else GREEN}{critical_count}{RESET} (Swallowed exceptions, empty scripts)")
220
745
  print(f" Warnings / Suggestions : {YELLOW if warning_count > 0 else GREEN}{warning_count}{RESET} (Missing explicit asserts, excessive delays)")
221
746
  print(f" Clean Scripts : {GREEN}{clean_count}/{total_scripts} ({clean_pct:.1f}%){RESET}")
222
747
  print(f" Codebase Health Score : {GREEN if health_score == 100 else YELLOW}{health_score:.1f}%{RESET}")
223
748
  print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
224
749
 
225
- # Print defect breakdown if any
226
- defect_scripts = [r for r in results if not r["is_clean"]]
227
750
  if defect_scripts:
228
751
  print(f"{BOLD}Detected Items ({len(defect_scripts)} scripts):{RESET}")
229
- for r in defect_scripts[:10]: # Print top 10
752
+ for r in defect_scripts[:10]:
230
753
  for d in r["defects"]:
231
754
  color = RED if d["severity"] == "CRITICAL" else YELLOW
232
755
  print(f" - [{color}{d['severity']}{RESET}] {r['file']}:{d['line']} - {d['message']}")
233
756
  if len(defect_scripts) > 10:
234
757
  print(f" ... and {len(defect_scripts) - 10} more scripts (see full JSON report).\n")
235
758
 
236
- # Save artifact
237
- os.makedirs(REPORTS_DIR, exist_ok=True)
238
- report_data = {
239
- "timestamp": datetime.now().isoformat(),
240
- "total_scripts": total_scripts,
241
- "clean_scripts": clean_count,
242
- "critical_defects": critical_count,
243
- "warnings": warning_count,
244
- "health_score_pct": health_score,
245
- "defect_details": defect_scripts
246
- }
247
- with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
759
+ dest_json = os.path.abspath(output_path) if output_path else OUTPUT_JSON
760
+ os.makedirs(os.path.dirname(dest_json), exist_ok=True)
761
+ with open(dest_json, "w", encoding="utf-8") as f:
248
762
  json.dump(report_data, f, indent=2, ensure_ascii=False)
249
- print(f"Detailed audit saved to: {OUTPUT_JSON}\n")
250
-
763
+ print(f"Detailed audit saved to: {dest_json}\n")
764
+
765
+ if not output_path and target_dir and os.path.isdir(target_dir) and not os.path.abspath(target_dir).startswith(REPO_ROOT):
766
+ try:
767
+ target_out = os.path.join(os.path.abspath(target_dir), "reports", "infer_linter_report.json")
768
+ os.makedirs(os.path.dirname(target_out), exist_ok=True)
769
+ with open(target_out, "w", encoding="utf-8") as f:
770
+ json.dump(report_data, f, indent=2, ensure_ascii=False)
771
+ except OSError:
772
+ pass
773
+
774
+ if strict:
775
+ 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"))
776
+ return 0 if (critical_count == 0 and fake_pass_count == 0) else 1
251
777
  return 0 if critical_count == 0 else 1
252
778
 
253
779
 
@@ -256,12 +782,28 @@ def main():
256
782
  parser.add_argument("target", nargs="?", default=None, help="Target directory or script to scan")
257
783
  parser.add_argument("--dir", help="Target directory to scan (defaults to Scripts/)")
258
784
  parser.add_argument("--repo", help="Target repository root containing Scripts/")
785
+ parser.add_argument("--out", "--output", dest="output_file", help="Path to save report")
786
+ parser.add_argument("--format", choices=["text", "json", "sarif", "junit"], default="text", help="Output format (default: text)")
787
+ parser.add_argument("--baseline", help="Path to baseline JSON file to ignore known technical debt")
788
+ parser.add_argument("--generate-baseline", dest="generate_baseline", help="Generate a baseline JSON file from current findings")
789
+ parser.add_argument("--config", help="Path to synapse.config.json")
790
+ parser.add_argument("--strict", action="store_true", default=True, help="Treat missing assertions / fake-passes as fatal errors (default: True)")
791
+ parser.add_argument("--no-strict", "--lenient", action="store_false", dest="strict", help="Allow missing assertions with warning only")
259
792
  args = parser.parse_args()
260
793
 
261
794
  target_dir = args.dir or args.target
262
795
  if not target_dir and args.repo:
263
796
  target_dir = os.path.join(args.repo, "Scripts")
264
- return run_infer_linter(target_dir=target_dir)
797
+
798
+ return run_infer_linter(
799
+ target_dir=target_dir,
800
+ output_path=args.output_file,
801
+ strict=args.strict,
802
+ output_format=args.format,
803
+ baseline_path=args.baseline,
804
+ generate_baseline_path=args.generate_baseline,
805
+ config_path=args.config
806
+ )
265
807
 
266
808
 
267
809
  if __name__ == "__main__":