synapse-hub 2.2.4 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) 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 +23 -4
  4. package/.agents/agents/pom-crafter.md +15 -0
  5. package/.agents/agents/quality-gatekeeper.md +20 -4
  6. package/.agents/agents/test-runner.md +12 -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 +331 -0
  126. package/README.md +252 -515
  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 +16 -4
  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 +27 -12
  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 +98 -131
  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 +30 -3
  259. package/tools/cache_resolver.py +248 -14
  260. package/tools/check_env.py +55 -14
  261. package/tools/check_version_sync.py +171 -0
  262. package/tools/ci_healing_hook.py +1 -1
  263. package/tools/cli.py +454 -42
  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 +302 -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 +295 -31
  299. package/tools/skillspector.py +31 -0
  300. package/tools/spec_drift_sentinel.py +439 -0
  301. package/tools/sync_agent_rules.py +248 -0
  302. package/tools/telemetry_correlator.py +45 -0
  303. package/tools/test_impact.py +51 -12
  304. package/tools/tms_sync_bridge.py +21 -5
  305. package/tools/universal_ir.py +3 -0
  306. package/tools/update_sentinel.py +87 -15
  307. package/tools/verify_all.ps1 +13 -0
  308. package/tools/verify_all.py +479 -85
  309. package/tools/visual_diff_canvas.py +1 -0
  310. package/tools/visual_som_helper.py +1 -1
  311. package/tools/watch_mode.py +305 -0
  312. package/tools/welcome_page.py +291 -0
@@ -0,0 +1,758 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
3
+ # SPDX-License-Identifier: MIT
4
+ # Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
5
+
6
+ """
7
+ Gherkin BDD Suite Compiler - Natural-Language Test Ingestion (0-Token)
8
+ ----------------------------------------------------------------------
9
+ Parses Gherkin `.feature` files (Feature / Rule / Background / Scenario /
10
+ Scenario Outline / Examples / Tags / Doc Strings / Data Tables) and compiles
11
+ them into:
12
+
13
+ 1. An expanded, deterministic test-case matrix (one case per Example row).
14
+ 2. Hub-native Universal IR (TestCaseIR + StepIR) for the omni transpile
15
+ pipeline (--emit-ir).
16
+ 3. A Markdown BDD test plan with "proven vs owed" coverage-gap analysis
17
+ (missing assertions, missing negative paths, thin example tables,
18
+ unresolved parameters).
19
+
20
+ Provenance:
21
+ - Concept inspired by the Gherkin-driven E2E ingestion workflow popularized
22
+ by TestZeus Hercules (AGPL-3.0). This is an independent, from-scratch,
23
+ stdlib-only re-expression of the concept - no upstream code or text is
24
+ used (AGPL content must not be copied into this MIT repository).
25
+ - The "proven vs owed" coverage traceability idea is inspired by Kane CLI
26
+ (Apache-2.0), re-expressed in original wording.
27
+ - English Gherkin keywords only (Given/When/Then/And/But); a `# language:`
28
+ header is ignored by design.
29
+
30
+ Exit codes: 0 = compiled (warnings allowed), 1 = parse/authoring errors
31
+ (report and plan are still emitted so the suite can be iterated on).
32
+ """
33
+
34
+ import os
35
+ import re
36
+ import sys
37
+ import json
38
+ import copy
39
+ import glob
40
+ import argparse
41
+ from dataclasses import dataclass, field
42
+ from datetime import datetime
43
+ from typing import Dict, List, Optional, Any
44
+
45
+ # Enforce UTF-8 stdout encoding for Windows console compatibility
46
+ if hasattr(sys.stdout, 'reconfigure'):
47
+ try:
48
+ sys.stdout.reconfigure(encoding='utf-8')
49
+ sys.stderr.reconfigure(encoding='utf-8')
50
+ except Exception:
51
+ pass
52
+
53
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
54
+ REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
55
+ if SCRIPT_DIR not in sys.path:
56
+ sys.path.insert(0, SCRIPT_DIR)
57
+
58
+ from universal_ir import ( # noqa: E402
59
+ ActionType,
60
+ AssertionType,
61
+ StepIR,
62
+ TestCaseIR,
63
+ WaitStrategy,
64
+ )
65
+
66
+ REPORTS_DIR = os.path.join(REPO_ROOT, "reports")
67
+ OUTPUT_JSON = os.path.join(REPORTS_DIR, "gherkin_suite_report.json")
68
+
69
+ # ANSI Color codes
70
+ GREEN = "\033[92m"
71
+ RED = "\033[91m"
72
+ YELLOW = "\033[93m"
73
+ CYAN = "\033[96m"
74
+ BOLD = "\033[1m"
75
+ RESET = "\033[0m"
76
+
77
+ CONCRETE_STEP_KEYWORDS = ("Given", "When", "Then")
78
+ STEP_ROLES = {"Given": "arrange", "When": "act", "Then": "assert"}
79
+ NEGATIVE_SIGNAL_PATTERN = re.compile(
80
+ r"invalid|error|fail|negativ|empty|denied|unauthor|wrong|incorrect", re.IGNORECASE
81
+ )
82
+ QUOTE_PATTERN = re.compile(r'"([^"]+)"|\'([^\']+)\'')
83
+ PARAM_PATTERN = re.compile(r"<([^<>\n]+)>")
84
+ URL_PATTERN = re.compile(r"https?://[^\s\"']+")
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Gherkin document model
89
+ # ---------------------------------------------------------------------------
90
+
91
+ @dataclass
92
+ class GherkinStep:
93
+ keyword: str # Concrete keyword (And/But already resolved)
94
+ raw_keyword: str # Keyword as written in the file (may be And/But)
95
+ text: str
96
+ role: str # arrange | act | assert
97
+ docstring: Optional[str] = None
98
+ docstring_type: str = ""
99
+ table_rows: List[List[str]] = field(default_factory=list)
100
+
101
+ def display_text(self) -> str:
102
+ parts = [self.text]
103
+ if self.table_rows:
104
+ parts.append("\n".join("| " + " | ".join(r) + " |" for r in self.table_rows))
105
+ if self.docstring is not None:
106
+ parts.append('"""\n' + self.docstring + '\n"""')
107
+ return "\n".join(parts)
108
+
109
+
110
+ @dataclass
111
+ class GherkinExamples:
112
+ name: str = ""
113
+ tags: List[str] = field(default_factory=list)
114
+ header: Optional[List[str]] = None
115
+ rows: List[Dict[str, str]] = field(default_factory=list)
116
+
117
+
118
+ @dataclass
119
+ class GherkinScenario:
120
+ name: str
121
+ tags: List[str] = field(default_factory=list)
122
+ description: str = ""
123
+ steps: List[GherkinStep] = field(default_factory=list)
124
+ is_outline: bool = False
125
+ examples: List[GherkinExamples] = field(default_factory=list)
126
+ rule_name: str = ""
127
+
128
+
129
+ @dataclass
130
+ class GherkinFeature:
131
+ file: str
132
+ name: str = ""
133
+ tags: List[str] = field(default_factory=list)
134
+ description: str = ""
135
+ background: List[GherkinStep] = field(default_factory=list)
136
+ scenarios: List[GherkinScenario] = field(default_factory=list)
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Parser (line-based state machine, stdlib only)
141
+ # ---------------------------------------------------------------------------
142
+
143
+ def _split_table_row(line: str) -> List[str]:
144
+ """Splits a Gherkin data-table row into cells, honoring \\| escapes."""
145
+ escaped = line.strip().replace("\\|", "\x00")
146
+ if escaped.startswith("|"):
147
+ escaped = escaped[1:]
148
+ if escaped.endswith("|"):
149
+ escaped = escaped[:-1]
150
+ return [cell.replace("\x00", "|").strip() for cell in escaped.split("|")]
151
+
152
+
153
+ def _dedent_doc_line(line: str, indent: int) -> str:
154
+ """Dedents a doc-string content line by the opening delimiter's indent
155
+ (per Gherkin spec); blank lines collapse to empty strings."""
156
+ if not line.strip():
157
+ return ""
158
+ if len(line) >= indent and line[:indent].strip() == "":
159
+ return line[indent:]
160
+ return line.lstrip()
161
+
162
+
163
+ class GherkinParser:
164
+ """Parses English Gherkin text into a GherkinFeature model."""
165
+
166
+ def __init__(self):
167
+ self.errors: List[str] = []
168
+
169
+ def _err(self, message: str) -> None:
170
+ self.errors.append(message)
171
+
172
+ def parse(self, text: str, filename: str = "<memory>") -> GherkinFeature:
173
+ feature = GherkinFeature(file=filename)
174
+ pending_tags: List[str] = []
175
+ rule_tags: List[str] = []
176
+ rule_name = ""
177
+ container: Optional[str] = None # None | "background" | "scenario"
178
+ current_scenario: Optional[GherkinScenario] = None
179
+ current_examples: Optional[GherkinExamples] = None
180
+ examples_mode = False
181
+ last_step: Optional[GherkinStep] = None
182
+ last_concrete: Optional[str] = None
183
+ in_docstring = False
184
+ doc_delim = '"""'
185
+ doc_type = ""
186
+ doc_indent = 0
187
+ doc_lines: List[str] = []
188
+
189
+ lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
190
+ for line_no, raw in enumerate(lines, 1):
191
+ if in_docstring:
192
+ if raw.strip() == doc_delim:
193
+ if last_step is not None:
194
+ last_step.docstring = "\n".join(doc_lines)
195
+ last_step.docstring_type = doc_type
196
+ else:
197
+ self._err(f"line {line_no}: doc string closed but no step is open")
198
+ in_docstring = False
199
+ doc_lines = []
200
+ doc_type = ""
201
+ else:
202
+ doc_lines.append(_dedent_doc_line(raw, doc_indent))
203
+ continue
204
+
205
+ stripped = raw.strip()
206
+ if not stripped or stripped.startswith("#"):
207
+ continue # blank line or comment (incl. `# language:` header)
208
+
209
+ if stripped.startswith("@"):
210
+ pending_tags.extend(stripped.split())
211
+ continue
212
+
213
+ # Doc string openers attach to the most recent step.
214
+ if stripped.startswith('"""') or stripped.startswith("```"):
215
+ if last_step is None or examples_mode:
216
+ self._err(f"line {line_no}: doc string outside a step")
217
+ else:
218
+ in_docstring = True
219
+ doc_delim = stripped[:3]
220
+ doc_type = stripped[3:].strip()
221
+ doc_indent = len(raw) - len(raw.lstrip())
222
+ continue
223
+
224
+ m = re.match(r"Feature\s*:\s*(.*)", stripped)
225
+ if m:
226
+ feature.name = m.group(1).strip()
227
+ feature.tags = pending_tags
228
+ pending_tags = []
229
+ container = None
230
+ examples_mode = False
231
+ continue
232
+
233
+ m = re.match(r"Rule\s*:\s*(.*)", stripped)
234
+ if m:
235
+ rule_name = m.group(1).strip()
236
+ rule_tags = pending_tags
237
+ pending_tags = []
238
+ container = None
239
+ examples_mode = False
240
+ continue
241
+
242
+ m = re.match(r"Background\s*:\s*(.*)", stripped)
243
+ if m:
244
+ container = "background"
245
+ examples_mode = False
246
+ last_step = None
247
+ last_concrete = None
248
+ pending_tags = [] # Background tags are not modeled
249
+ continue
250
+
251
+ m = re.match(r"Scenario\s+(?:Outline|Template)\s*:\s*(.*)", stripped)
252
+ if m:
253
+ container = "scenario"
254
+ examples_mode = False
255
+ last_step = None
256
+ last_concrete = None
257
+ current_scenario = GherkinScenario(
258
+ name=m.group(1).strip(),
259
+ tags=feature.tags + rule_tags + pending_tags,
260
+ is_outline=True,
261
+ rule_name=rule_name,
262
+ )
263
+ feature.scenarios.append(current_scenario)
264
+ pending_tags = []
265
+ continue
266
+
267
+ m = re.match(r"Scenario\s*:\s*(.*)", stripped)
268
+ if m:
269
+ container = "scenario"
270
+ examples_mode = False
271
+ last_step = None
272
+ last_concrete = None
273
+ current_scenario = GherkinScenario(
274
+ name=m.group(1).strip(),
275
+ tags=feature.tags + rule_tags + pending_tags,
276
+ rule_name=rule_name,
277
+ )
278
+ feature.scenarios.append(current_scenario)
279
+ pending_tags = []
280
+ continue
281
+
282
+ m = re.match(r"Examples\s*:\s*(.*)", stripped)
283
+ if m:
284
+ if current_scenario is None or not current_scenario.is_outline:
285
+ self._err(f"line {line_no}: Examples block without a Scenario Outline")
286
+ else:
287
+ current_examples = GherkinExamples(name=m.group(1).strip(), tags=pending_tags)
288
+ current_scenario.examples.append(current_examples)
289
+ examples_mode = True
290
+ pending_tags = []
291
+ continue
292
+
293
+ m = re.match(r"(Given|When|Then|And|But)\s+(.*)", stripped)
294
+ if m:
295
+ raw_kw, step_text = m.group(1), m.group(2).strip()
296
+ if raw_kw in CONCRETE_STEP_KEYWORDS:
297
+ concrete = raw_kw
298
+ role = STEP_ROLES[concrete]
299
+ else:
300
+ if last_concrete is None:
301
+ self._err(f"line {line_no}: '{raw_kw}' before any Given/When/Then")
302
+ concrete = "Given"
303
+ role = "arrange"
304
+ else:
305
+ concrete = last_concrete
306
+ role = STEP_ROLES[concrete]
307
+ step = GherkinStep(keyword=concrete, raw_keyword=raw_kw, text=step_text, role=role)
308
+ if container == "background":
309
+ feature.background.append(step)
310
+ elif container == "scenario" and current_scenario is not None:
311
+ current_scenario.steps.append(step)
312
+ else:
313
+ self._err(f"line {line_no}: step outside Scenario/Background - '{stripped}'")
314
+ last_step = step
315
+ last_concrete = concrete
316
+ continue
317
+
318
+ if stripped.startswith("|"):
319
+ cells = _split_table_row(stripped)
320
+ if examples_mode and current_examples is not None:
321
+ if current_examples.header is None:
322
+ current_examples.header = cells
323
+ else:
324
+ if len(cells) != len(current_examples.header):
325
+ self._err(
326
+ f"line {line_no}: example row has {len(cells)} cells, "
327
+ f"header has {len(current_examples.header)}"
328
+ )
329
+ else:
330
+ current_examples.rows.append(dict(zip(current_examples.header, cells)))
331
+ elif last_step is not None:
332
+ last_step.table_rows.append(cells)
333
+ else:
334
+ self._err(f"line {line_no}: data table outside a step or Examples block")
335
+ continue
336
+
337
+ # Prose: attach to the current container description.
338
+ if container == "scenario" and current_scenario is not None:
339
+ current_scenario.description = (current_scenario.description + " " + stripped).strip()
340
+ elif container is None:
341
+ feature.description = (feature.description + " " + stripped).strip()
342
+
343
+ if in_docstring:
344
+ self._err("unterminated doc string at end of file")
345
+
346
+ return feature
347
+
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # Scenario Outline expansion
351
+ # ---------------------------------------------------------------------------
352
+
353
+ def _substitute_param(text: str, row: Dict[str, str]) -> str:
354
+ for key, value in row.items():
355
+ text = text.replace("<" + key + ">", value)
356
+ return text
357
+
358
+
359
+ def _slug(text: str) -> str:
360
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower()).strip("-")
361
+ return slug or "unnamed"
362
+
363
+
364
+ def expand_feature(feature: GherkinFeature, errors: List[str]) -> List[Dict[str, Any]]:
365
+ """Expands Scenario Outlines into concrete cases (one per Examples row)."""
366
+ seen_ids: Dict[str, int] = {}
367
+ cases: List[Dict[str, Any]] = []
368
+
369
+ def register(case_id: str) -> str:
370
+ if case_id in seen_ids:
371
+ seen_ids[case_id] += 1
372
+ return f"{case_id}-{seen_ids[case_id]}"
373
+ seen_ids[case_id] = 1
374
+ return case_id
375
+
376
+ for scenario in feature.scenarios:
377
+ base_steps = feature.background + scenario.steps
378
+ if not scenario.is_outline:
379
+ cases.append({
380
+ "id": register(f"{_slug(feature.name)}--{_slug(scenario.name)}"),
381
+ "name": scenario.name,
382
+ "tags": scenario.tags,
383
+ "steps": copy.deepcopy(base_steps),
384
+ "scenario": scenario.name,
385
+ "row": None,
386
+ "errors": [],
387
+ })
388
+ continue
389
+
390
+ if not scenario.examples:
391
+ errors.append(f"Scenario Outline '{scenario.name}' has no Examples block")
392
+ continue
393
+
394
+ for example_block in scenario.examples:
395
+ if example_block.header is None:
396
+ errors.append(f"Scenario Outline '{scenario.name}' has an Examples block without a table")
397
+ continue
398
+ for row_no, row in enumerate(example_block.rows, 1):
399
+ case_errors: List[str] = []
400
+ expanded: List[GherkinStep] = []
401
+ for step in base_steps:
402
+ text = _substitute_param(step.text, row)
403
+ doc = _substitute_param(step.docstring, row) if step.docstring is not None else None
404
+ table = [
405
+ [_substitute_param(cell, row) for cell in row_cells]
406
+ for row_cells in step.table_rows
407
+ ]
408
+ leftover = PARAM_PATTERN.search(text)
409
+ if leftover:
410
+ case_errors.append(
411
+ f"unresolved parameter <{leftover.group(1)}> in step '{step.text}' "
412
+ f"(not found in Examples header)"
413
+ )
414
+ expanded.append(GherkinStep(
415
+ keyword=step.keyword,
416
+ raw_keyword=step.raw_keyword,
417
+ text=text,
418
+ role=step.role,
419
+ docstring=doc,
420
+ table_rows=table,
421
+ ))
422
+ row_tag = f"@row-{row_no}"
423
+ cases.append({
424
+ "id": register(f"{_slug(feature.name)}--{_slug(scenario.name)}--row-{row_no}"),
425
+ "name": f"{scenario.name} [{row_tag}]",
426
+ "tags": scenario.tags + [row_tag],
427
+ "steps": expanded,
428
+ "scenario": scenario.name,
429
+ "row": row_no,
430
+ "errors": case_errors,
431
+ })
432
+ return cases
433
+
434
+
435
+ # ---------------------------------------------------------------------------
436
+ # Deterministic NL -> Universal IR mapping (keyword heuristics, 0 LLM tokens)
437
+ # ---------------------------------------------------------------------------
438
+
439
+ def _first_quoted(text: str) -> Optional[str]:
440
+ m = QUOTE_PATTERN.search(text)
441
+ if m:
442
+ return m.group(1) if m.group(1) is not None else m.group(2)
443
+ return None
444
+
445
+
446
+ def _after_word(text: str, words: tuple) -> Optional[str]:
447
+ for w in words:
448
+ m = re.search(r"\b" + w + r"\s+(.+)$", text, re.IGNORECASE)
449
+ if m:
450
+ return m.group(1).strip()
451
+ return None
452
+
453
+
454
+ def map_step_to_ir(step: GherkinStep) -> StepIR:
455
+ """Maps a Gherkin step onto a StepIR action using deterministic rules.
456
+
457
+ Best-effort by design: unrecognizable phrasing falls back to CUSTOM_CALL
458
+ with the original text preserved in `description`.
459
+ """
460
+ text = step.text
461
+ lower = text.lower()
462
+ quoted = _first_quoted(text)
463
+ role = step.role
464
+
465
+ if role == "assert":
466
+ if re.search(r"\burl\b", lower) and re.search(r"\b(should|must|is|contains)\b", lower):
467
+ expected = quoted or (URL_PATTERN.search(text).group(0) if URL_PATTERN.search(text) else text)
468
+ return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.URL_CONTAINS,
469
+ expected=expected, description=text)
470
+ # A quoted expectation ("should see "Order Confirmed"") is a text
471
+ # assertion - stronger than a bare visibility check, so it wins.
472
+ if quoted and re.search(r"\b(see|sees|contains|shows|displays|message|text)\b", lower):
473
+ return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.CONTAINS_TEXT,
474
+ target=None, expected=quoted, description=text)
475
+ if re.search(r"\b(see|sees|visible|displayed|shown|appears|exists)\b", lower):
476
+ return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.VISIBLE,
477
+ target=quoted, expected=quoted, description=text)
478
+ if quoted:
479
+ return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.CONTAINS_TEXT,
480
+ expected=quoted, description=text)
481
+ return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.VISIBLE,
482
+ description=text)
483
+
484
+ if role == "act":
485
+ # Verbs are matched anywhere in the sentence: Gherkin steps commonly
486
+ # start with a subject ("the user clicks ..."), including 3rd person.
487
+ if re.search(r"\b(navigates?|go(?:es)?\s+to|opens?|visits?|launch(?:es)?|browse(?:s)?)\b", lower):
488
+ url_match = URL_PATTERN.search(text)
489
+ return StepIR(action=ActionType.GOTO, value=url_match.group(0) if url_match else quoted,
490
+ description=text)
491
+ if re.search(r"\b(fill(?:s)?\s+in|enter(?:s)?|type(?:s)?|input(?:s)?|provide(?:s)?|set(?:s)?)\b", lower):
492
+ quotes = [
493
+ m.group(1) if m.group(1) is not None else m.group(2)
494
+ for m in QUOTE_PATTERN.finditer(text)
495
+ ]
496
+ value = quotes[0] if quotes else None
497
+ if len(quotes) >= 2:
498
+ target = quotes[1]
499
+ else:
500
+ target = _after_word(text, ("into", "in the", "in", "on"))
501
+ return StepIR(action=ActionType.FILL, target=target, value=value, description=text)
502
+ if re.search(r"\b(select|choose|pick)", lower):
503
+ target = _after_word(text, ("from", "in"))
504
+ return StepIR(action=ActionType.SELECT, target=target, value=quoted, description=text)
505
+ if re.search(r"\b(uncheck|untick|deselect|clear)", lower):
506
+ return StepIR(action=ActionType.UNCHECK, target=quoted, description=text)
507
+ if re.search(r"\b(check|tick)", lower):
508
+ return StepIR(action=ActionType.CHECK, target=quoted, description=text)
509
+ if re.search(r"\b(hover|mouse\s?over)", lower):
510
+ return StepIR(action=ActionType.HOVER, target=quoted, description=text)
511
+ if re.search(r"\bwait", lower):
512
+ return StepIR(action=ActionType.WAIT, wait_strategy=WaitStrategy.AUTO, description=text)
513
+ if re.search(r"\b(double.?click|dblclick)", lower):
514
+ return StepIR(action=ActionType.DBLCLICK, target=quoted, description=text)
515
+ if re.search(r"\b(click|tap|press|submit)", lower):
516
+ target = quoted or _after_word(text, ("on the", "on", "the"))
517
+ return StepIR(action=ActionType.CLICK, target=target, description=text)
518
+
519
+ # arrange (Given) and any unmapped phrasing -> explicit custom call
520
+ return StepIR(action=ActionType.CUSTOM_CALL, description=f"{step.keyword} {text}")
521
+
522
+
523
+ def build_test_case_ir(case: Dict[str, Any]) -> TestCaseIR:
524
+ steps = [map_step_to_ir(s) for s in case["steps"]]
525
+ return TestCaseIR(
526
+ id=case["id"],
527
+ title=case["name"],
528
+ steps=steps,
529
+ tags=case["tags"],
530
+ description=case.get("description", ""),
531
+ )
532
+
533
+
534
+ # ---------------------------------------------------------------------------
535
+ # "Proven vs owed" coverage-gap analysis (idea re-expressed from Kane CLI)
536
+ # ---------------------------------------------------------------------------
537
+
538
+ def analyze_gaps(feature: GherkinFeature, cases: List[Dict[str, Any]]) -> List[Dict[str, str]]:
539
+ gaps: List[Dict[str, str]] = []
540
+
541
+ for case in cases:
542
+ if not any(s.role == "assert" for s in case["steps"]):
543
+ gaps.append({
544
+ "severity": "WARNING",
545
+ "rule": "G1-MISSING_ASSERTION",
546
+ "target": case["id"],
547
+ "message": "Case has no Then/assert step - add an observable outcome assertion.",
548
+ })
549
+
550
+ has_negative = any(
551
+ NEGATIVE_SIGNAL_PATTERN.search(" ".join([sc.name] + sc.tags)) for sc in feature.scenarios
552
+ )
553
+ if not has_negative:
554
+ gaps.append({
555
+ "severity": "WARNING",
556
+ "rule": "G2-MISSING_NEGATIVE_PATH",
557
+ "target": feature.name or feature.file,
558
+ "message": "No negative/invalid scenario detected - add at least one "
559
+ "(e.g. invalid input, unauthorized access, empty state).",
560
+ })
561
+
562
+ for scenario in feature.scenarios:
563
+ if not scenario.is_outline:
564
+ continue
565
+ for block in scenario.examples:
566
+ if block.header is not None and len(block.rows) < 2:
567
+ gaps.append({
568
+ "severity": "WARNING",
569
+ "rule": "G3-THIN_EXAMPLES",
570
+ "target": scenario.name,
571
+ "message": f"Examples table has {len(block.rows)} row(s) - add boundary and "
572
+ "negative rows (empty, max length, special chars).",
573
+ })
574
+
575
+ return gaps
576
+
577
+
578
+ # ---------------------------------------------------------------------------
579
+ # Emitters
580
+ # ---------------------------------------------------------------------------
581
+
582
+ def emit_markdown_plan(feature: GherkinFeature, cases: List[Dict[str, Any]],
583
+ gaps: List[Dict[str, str]]) -> str:
584
+ lines: List[str] = []
585
+ lines.append(f"# BDD Test Plan - {feature.name or os.path.basename(feature.file)}")
586
+ lines.append("")
587
+ if feature.tags:
588
+ lines.append("**Tags:** " + " ".join(feature.tags))
589
+ if feature.description:
590
+ lines.append(f"> {feature.description}")
591
+ lines.append(f"- Source: `{feature.file}`")
592
+ lines.append(f"- Scenarios: {len(feature.scenarios)} | Expanded cases: {len(cases)}")
593
+ lines.append("")
594
+
595
+ if feature.background:
596
+ lines.append("## Background")
597
+ for step in feature.background:
598
+ lines.append(f"- **{step.keyword}** {step.display_text()}")
599
+ lines.append("")
600
+
601
+ for scenario in feature.scenarios:
602
+ kind = "Scenario Outline" if scenario.is_outline else "Scenario"
603
+ heading = f"## {kind}: {scenario.name}"
604
+ if scenario.rule_name:
605
+ heading += f" (Rule: {scenario.rule_name})"
606
+ lines.append(heading)
607
+ if scenario.tags:
608
+ lines.append(" ".join(scenario.tags))
609
+ lines.append("")
610
+ lines.append("| # | Step | Role | Mapped Action |")
611
+ lines.append("| :--- | :--- | :--- | :--- |")
612
+ case_for_scenario = next((c for c in cases if c["scenario"] == scenario.name), None)
613
+ step_source = case_for_scenario["steps"] if case_for_scenario else scenario.steps
614
+ for idx, step in enumerate(step_source, 1):
615
+ ir = map_step_to_ir(step)
616
+ lines.append(f"| {idx} | **{step.keyword}** {step.text.splitlines()[0]} | {step.role} | `{ir.action.value}` |")
617
+ if scenario.is_outline:
618
+ for block in scenario.examples:
619
+ lines.append("")
620
+ label = f"Examples: {block.name}" if block.name else "Examples"
621
+ tags = " " + " ".join(block.tags) if block.tags else ""
622
+ lines.append(f"**{label}**{tags}")
623
+ if block.header:
624
+ lines.append("")
625
+ lines.append("| " + " | ".join(block.header) + " |")
626
+ lines.append("|" + "---|" * len(block.header))
627
+ for row in block.rows:
628
+ lines.append("| " + " | ".join(row[h] for h in block.header) + " |")
629
+ lines.append("")
630
+
631
+ lines.append("## Coverage Gaps (proven vs owed)")
632
+ if gaps:
633
+ for gap in gaps:
634
+ lines.append(f"- **[{gap['severity']}] {gap['rule']}** `{gap['target']}` - {gap['message']}")
635
+ else:
636
+ lines.append("- No gaps detected.")
637
+ lines.append("")
638
+ return "\n".join(lines)
639
+
640
+
641
+ def compile_feature(path: str) -> Dict[str, Any]:
642
+ """Parses, expands, maps and audits one .feature file into a result dict."""
643
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
644
+ text = f.read()
645
+
646
+ parser = GherkinParser()
647
+ feature = parser.parse(text, filename=os.path.relpath(path, REPO_ROOT)
648
+ if path.startswith(REPO_ROOT) else path)
649
+ errors = list(parser.errors)
650
+
651
+ cases = expand_feature(feature, errors)
652
+ error_count = sum(len(c["errors"]) for c in cases) + len(errors)
653
+ gaps = analyze_gaps(feature, cases)
654
+
655
+ mapped = {c["id"]: [map_step_to_ir(s).to_dict() for s in c["steps"]] for c in cases}
656
+ total = len(cases)
657
+ invalid = sum(1 for c in cases if c["errors"])
658
+ health_score = 100.0 if total == 0 else max(0.0, round(((total - invalid) / total) * 100, 1))
659
+
660
+ return {
661
+ "timestamp": datetime.now().isoformat(),
662
+ "file": feature.file,
663
+ "feature": feature.name,
664
+ "tags": feature.tags,
665
+ "totals": {
666
+ "scenarios": len(feature.scenarios),
667
+ "expanded_cases": total,
668
+ "steps": sum(len(c["steps"]) for c in cases),
669
+ "invalid_cases": invalid,
670
+ },
671
+ "errors": errors + [f"{c['id']}: {e}" for c in cases for e in c["errors"]],
672
+ "gaps": gaps,
673
+ "cases": [
674
+ {
675
+ "id": c["id"],
676
+ "name": c["name"],
677
+ "tags": c["tags"],
678
+ "steps": mapped[c["id"]],
679
+ "valid": not c["errors"],
680
+ }
681
+ for c in cases
682
+ ],
683
+ "health_score_pct": health_score,
684
+ "markdown_plan": emit_markdown_plan(feature, cases, gaps),
685
+ "_feature": feature,
686
+ "_ir_cases": [
687
+ {"id": c["id"], "ir": build_test_case_ir(c).to_dict()}
688
+ for c in cases if not c["errors"]
689
+ ],
690
+ }
691
+
692
+
693
+ def run(target: str, as_json: bool = False, emit_ir: bool = False) -> int:
694
+ if os.path.isfile(target):
695
+ feature_files = [target]
696
+ elif os.path.isdir(target):
697
+ feature_files = sorted(glob.glob(os.path.join(target, "**", "*.feature"), recursive=True))
698
+ else:
699
+ print(f"{RED}[ERROR]{RESET} Target not found: {target}")
700
+ return 1
701
+ if not feature_files:
702
+ print(f"{YELLOW}[WARN]{RESET} No .feature files discovered under: {target}")
703
+ return 1
704
+
705
+ results = [compile_feature(p) for p in feature_files]
706
+ all_errors = [e for r in results for e in r["errors"]]
707
+ total_cases = sum(r["totals"]["expanded_cases"] for r in results)
708
+ total_gaps = sum(len(r["gaps"]) for r in results)
709
+
710
+ print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
711
+ print(f"{BOLD}{CYAN} GHERKIN BDD SUITE COMPILER (Natural-Language Test Ingestion) {RESET}")
712
+ print(f"{BOLD}{CYAN}======================================================================{RESET}")
713
+ print(f" Feature Files : {len(feature_files)}")
714
+ print(f" Expanded Test Cases : {GREEN if total_cases else RED}{total_cases}{RESET}")
715
+ print(f" Coverage Gap Warnings : {YELLOW if total_gaps else GREEN}{total_gaps}{RESET}")
716
+ print(f" Errors : {RED if all_errors else GREEN}{len(all_errors)}{RESET}")
717
+ scores = [r["health_score_pct"] for r in results if r["totals"]["expanded_cases"]]
718
+ avg = round(sum(scores) / len(scores), 1) if scores else 0.0
719
+ print(f" Case Validity Score : {GREEN if avg == 100 else YELLOW}{avg}%{RESET}")
720
+ print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
721
+
722
+ for r in results:
723
+ for e in r["errors"]:
724
+ print(f" - [{RED}ERROR{RESET}] {e}")
725
+ for g in r["gaps"]:
726
+ print(f" - [{YELLOW}{g['severity']}{RESET}] {g['rule']} {g['target']} - {g['message']}")
727
+
728
+ os.makedirs(REPORTS_DIR, exist_ok=True)
729
+ persist = [{k: v for k, v in r.items() if not k.startswith("_")} for r in results]
730
+ with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
731
+ json.dump({"timestamp": datetime.now().isoformat(), "features": persist}, f,
732
+ indent=2, ensure_ascii=False)
733
+ print(f"Detailed audit saved to: {OUTPUT_JSON}\n")
734
+
735
+ if emit_ir:
736
+ ir_payload = [case["ir"] for r in results for case in r["_ir_cases"]]
737
+ print(json.dumps(ir_payload, indent=2, ensure_ascii=False))
738
+
739
+ if as_json:
740
+ print(json.dumps(persist, indent=2, ensure_ascii=False))
741
+
742
+ return 1 if all_errors else 0
743
+
744
+
745
+ def main() -> int:
746
+ parser = argparse.ArgumentParser(
747
+ description="Gherkin BDD Suite Compiler - compiles .feature files into a test matrix, "
748
+ "Universal IR and coverage-gap report (0 LLM tokens).")
749
+ parser.add_argument("target", help="A .feature file or a directory containing .feature files")
750
+ parser.add_argument("--json", action="store_true", help="Print the full JSON report to stdout")
751
+ parser.add_argument("--emit-ir", action="store_true",
752
+ help="Print hub Universal IR (TestCaseIR) dicts for the omni pipeline")
753
+ args = parser.parse_args()
754
+ return run(args.target, as_json=args.json, emit_ir=args.emit_ir)
755
+
756
+
757
+ if __name__ == "__main__":
758
+ sys.exit(main())