@sun-asterisk/sungen 3.2.24 → 3.2.25

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 (305) hide show
  1. package/dist/capabilities/context-router.d.ts.map +1 -1
  2. package/dist/capabilities/context-router.js +15 -7
  3. package/dist/capabilities/context-router.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +6 -48
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/changed.d.ts +14 -0
  8. package/dist/cli/commands/changed.d.ts.map +1 -0
  9. package/dist/cli/commands/changed.js +128 -0
  10. package/dist/cli/commands/changed.js.map +1 -0
  11. package/dist/cli/commands/delivery.d.ts.map +1 -1
  12. package/dist/cli/commands/delivery.js +25 -1
  13. package/dist/cli/commands/delivery.js.map +1 -1
  14. package/dist/cli/commands/generate.js +1 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/cli/commands/results-merge.d.ts +19 -0
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -0
  18. package/dist/cli/commands/results-merge.js +191 -0
  19. package/dist/cli/commands/results-merge.js.map +1 -0
  20. package/dist/cli/commands/update.d.ts.map +1 -1
  21. package/dist/cli/commands/update.js +100 -12
  22. package/dist/cli/commands/update.js.map +1 -1
  23. package/dist/cli/index.js +4 -0
  24. package/dist/cli/index.js.map +1 -1
  25. package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
  26. package/dist/exporters/api-catalog-loader.js +10 -3
  27. package/dist/exporters/api-catalog-loader.js.map +1 -1
  28. package/dist/exporters/api-testcase-formatter.d.ts +9 -0
  29. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  30. package/dist/exporters/api-testcase-formatter.js +55 -3
  31. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  32. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  33. package/dist/exporters/csv-exporter.js +10 -1
  34. package/dist/exporters/csv-exporter.js.map +1 -1
  35. package/dist/exporters/json-exporter.d.ts.map +1 -1
  36. package/dist/exporters/json-exporter.js +6 -1
  37. package/dist/exporters/json-exporter.js.map +1 -1
  38. package/dist/exporters/matrix/build.d.ts.map +1 -1
  39. package/dist/exporters/matrix/build.js +10 -0
  40. package/dist/exporters/matrix/build.js.map +1 -1
  41. package/dist/exporters/matrix/export.js +1 -1
  42. package/dist/exporters/matrix/export.js.map +1 -1
  43. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  44. package/dist/exporters/matrix/render-csv.js +5 -1
  45. package/dist/exporters/matrix/render-csv.js.map +1 -1
  46. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  47. package/dist/exporters/matrix/render-xlsx.js +4 -1
  48. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  49. package/dist/exporters/matrix/types.d.ts +7 -0
  50. package/dist/exporters/matrix/types.d.ts.map +1 -1
  51. package/dist/exporters/matrix/types.js.map +1 -1
  52. package/dist/exporters/playwright-report-parser.d.ts +17 -2
  53. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  54. package/dist/exporters/playwright-report-parser.js +68 -8
  55. package/dist/exporters/playwright-report-parser.js.map +1 -1
  56. package/dist/exporters/results-merge.d.ts +46 -0
  57. package/dist/exporters/results-merge.d.ts.map +1 -0
  58. package/dist/exporters/results-merge.js +148 -0
  59. package/dist/exporters/results-merge.js.map +1 -0
  60. package/dist/exporters/selector-key-resolver.d.ts +4 -0
  61. package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
  62. package/dist/exporters/selector-key-resolver.js +101 -1
  63. package/dist/exporters/selector-key-resolver.js.map +1 -1
  64. package/dist/exporters/types.d.ts +8 -1
  65. package/dist/exporters/types.d.ts.map +1 -1
  66. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  67. package/dist/exporters/xlsx-report-builder.js +5 -4
  68. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  69. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  70. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  71. package/dist/generators/gherkin-parser/index.js +52 -1
  72. package/dist/generators/gherkin-parser/index.js.map +1 -1
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
  74. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  78. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  79. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  80. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  81. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  82. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  83. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
  84. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
  85. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  86. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  87. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  88. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  89. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  90. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  91. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  92. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  93. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  94. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  95. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  96. package/dist/generators/test-generator/code-generator.d.ts +6 -0
  97. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  98. package/dist/generators/test-generator/code-generator.js +80 -7
  99. package/dist/generators/test-generator/code-generator.js.map +1 -1
  100. package/dist/generators/test-generator/diagnostics.d.ts +60 -6
  101. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  102. package/dist/generators/test-generator/diagnostics.js +59 -5
  103. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  104. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
  105. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
  106. package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
  107. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
  108. package/dist/generators/test-generator/patterns/index.d.ts +19 -12
  109. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  110. package/dist/generators/test-generator/patterns/index.js +58 -38
  111. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  112. package/dist/generators/test-generator/patterns/types.d.ts +23 -0
  113. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  114. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
  115. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
  116. package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
  117. package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
  118. package/dist/generators/test-generator/recipe-expander.js +9 -1
  119. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  120. package/dist/generators/test-generator/reset-tier.d.ts +68 -0
  121. package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
  122. package/dist/generators/test-generator/reset-tier.js +94 -0
  123. package/dist/generators/test-generator/reset-tier.js.map +1 -0
  124. package/dist/generators/test-generator/step-mapper.d.ts +39 -0
  125. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  126. package/dist/generators/test-generator/step-mapper.js +144 -5
  127. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  128. package/dist/generators/test-generator/template-engine.d.ts +2 -10
  129. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  130. package/dist/generators/test-generator/template-engine.js +16 -2
  131. package/dist/generators/test-generator/template-engine.js.map +1 -1
  132. package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
  133. package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
  134. package/dist/generators/test-generator/utils/format-pattern.js +82 -0
  135. package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
  136. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
  137. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
  138. package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
  139. package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
  140. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
  141. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
  142. package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
  143. package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
  144. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
  145. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  146. package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
  147. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  148. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  149. package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
  150. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  151. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
  152. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
  153. package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
  154. package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
  155. package/dist/harness/annotation-overrides.d.ts +0 -12
  156. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  157. package/dist/harness/annotation-overrides.js +9 -5
  158. package/dist/harness/annotation-overrides.js.map +1 -1
  159. package/dist/harness/audit.js +4 -4
  160. package/dist/harness/audit.js.map +1 -1
  161. package/dist/harness/capability-plan.d.ts +2 -1
  162. package/dist/harness/capability-plan.d.ts.map +1 -1
  163. package/dist/harness/capability-plan.js +8 -3
  164. package/dist/harness/capability-plan.js.map +1 -1
  165. package/dist/harness/capability.d.ts +11 -0
  166. package/dist/harness/capability.d.ts.map +1 -1
  167. package/dist/harness/capability.js +8 -0
  168. package/dist/harness/capability.js.map +1 -1
  169. package/dist/harness/catalog/drivers.yaml +5 -0
  170. package/dist/harness/parse.d.ts.map +1 -1
  171. package/dist/harness/parse.js +5 -1
  172. package/dist/harness/parse.js.map +1 -1
  173. package/dist/harness/run-manifest.d.ts +58 -0
  174. package/dist/harness/run-manifest.d.ts.map +1 -0
  175. package/dist/harness/run-manifest.js +264 -0
  176. package/dist/harness/run-manifest.js.map +1 -0
  177. package/dist/harness/viewpoint-ledger.d.ts +1 -1
  178. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  179. package/dist/harness/viewpoint-ledger.js +25 -4
  180. package/dist/harness/viewpoint-ledger.js.map +1 -1
  181. package/dist/index.d.ts +1 -0
  182. package/dist/index.d.ts.map +1 -1
  183. package/dist/index.js +5 -1
  184. package/dist/index.js.map +1 -1
  185. package/dist/orchestrator/lockstep-version.d.ts +22 -0
  186. package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
  187. package/dist/orchestrator/lockstep-version.js +58 -0
  188. package/dist/orchestrator/lockstep-version.js.map +1 -0
  189. package/dist/orchestrator/project-deps-sync.d.ts +39 -0
  190. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
  191. package/dist/orchestrator/project-deps-sync.js +175 -0
  192. package/dist/orchestrator/project-deps-sync.js.map +1 -0
  193. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  194. package/dist/orchestrator/project-initializer.js +5 -2
  195. package/dist/orchestrator/project-initializer.js.map +1 -1
  196. package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  197. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  198. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  199. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  200. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  201. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
  202. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  203. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
  204. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  205. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  206. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  207. package/dist/orchestrator/templates/env.appium.example +10 -0
  208. package/dist/orchestrator/templates/specs-api.d.ts +4 -2
  209. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  210. package/dist/orchestrator/templates/specs-api.js +233 -10
  211. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  212. package/dist/orchestrator/templates/specs-api.ts +214 -11
  213. package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
  214. package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
  215. package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
  216. package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
  217. package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
  218. package/dist/orchestrator/templates/specs-test-data.ts +53 -6
  219. package/dist/orchestrator/templates/wdio.conf.ts +28 -9
  220. package/package.json +8 -6
  221. package/src/capabilities/context-router.ts +13 -5
  222. package/src/cli/commands/capability.ts +4 -38
  223. package/src/cli/commands/changed.ts +104 -0
  224. package/src/cli/commands/delivery.ts +27 -2
  225. package/src/cli/commands/generate.ts +1 -1
  226. package/src/cli/commands/results-merge.ts +153 -0
  227. package/src/cli/commands/update.ts +72 -13
  228. package/src/cli/index.ts +4 -0
  229. package/src/exporters/api-catalog-loader.ts +10 -2
  230. package/src/exporters/api-testcase-formatter.ts +59 -3
  231. package/src/exporters/csv-exporter.ts +11 -2
  232. package/src/exporters/json-exporter.ts +7 -2
  233. package/src/exporters/matrix/build.ts +9 -0
  234. package/src/exporters/matrix/export.ts +1 -1
  235. package/src/exporters/matrix/render-csv.ts +5 -1
  236. package/src/exporters/matrix/render-xlsx.ts +4 -1
  237. package/src/exporters/matrix/types.ts +7 -0
  238. package/src/exporters/playwright-report-parser.ts +68 -9
  239. package/src/exporters/results-merge.ts +150 -0
  240. package/src/exporters/selector-key-resolver.ts +81 -1
  241. package/src/exporters/types.ts +8 -1
  242. package/src/exporters/xlsx-report-builder.ts +5 -4
  243. package/src/generators/gherkin-parser/index.ts +55 -1
  244. package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
  245. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
  246. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  247. package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  248. package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  249. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  250. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  251. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
  252. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
  253. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  254. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  255. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  256. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  257. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  258. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  259. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  260. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  261. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  262. package/src/generators/test-generator/code-generator.ts +86 -7
  263. package/src/generators/test-generator/diagnostics.ts +60 -6
  264. package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
  265. package/src/generators/test-generator/patterns/index.ts +58 -33
  266. package/src/generators/test-generator/patterns/types.ts +36 -0
  267. package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
  268. package/src/generators/test-generator/recipe-expander.ts +9 -1
  269. package/src/generators/test-generator/reset-tier.ts +93 -0
  270. package/src/generators/test-generator/step-mapper.ts +149 -6
  271. package/src/generators/test-generator/template-engine.ts +18 -3
  272. package/src/generators/test-generator/utils/format-pattern.ts +79 -0
  273. package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
  274. package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
  275. package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
  276. package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
  277. package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
  278. package/src/harness/annotation-overrides.ts +10 -5
  279. package/src/harness/audit.ts +4 -4
  280. package/src/harness/capability-plan.ts +10 -5
  281. package/src/harness/capability.ts +16 -0
  282. package/src/harness/catalog/drivers.yaml +5 -0
  283. package/src/harness/parse.ts +5 -1
  284. package/src/harness/run-manifest.ts +254 -0
  285. package/src/harness/viewpoint-ledger.ts +26 -5
  286. package/src/index.ts +1 -0
  287. package/src/orchestrator/lockstep-version.ts +46 -0
  288. package/src/orchestrator/project-deps-sync.ts +175 -0
  289. package/src/orchestrator/project-initializer.ts +5 -2
  290. package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  291. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  292. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  293. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  294. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  295. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
  296. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  297. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
  298. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  299. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
  300. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  301. package/src/orchestrator/templates/env.appium.example +10 -0
  302. package/src/orchestrator/templates/specs-api.ts +214 -11
  303. package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
  304. package/src/orchestrator/templates/specs-test-data.ts +53 -6
  305. package/src/orchestrator/templates/wdio.conf.ts +28 -9
@@ -5,10 +5,12 @@ import { StepMapper } from './step-mapper';
5
5
  import { TestGeneratorAdapter, adapterRegistry } from './adapters';
6
6
  import { transformToRuntimeData } from './utils/runtime-data-transformer';
7
7
  import { DiagnosticsCollector } from './diagnostics';
8
+ import { parseResetTier, DEFAULT_RESET_TIER } from './reset-tier';
8
9
  import { capabilityRegistry } from '../../capabilities/registry';
9
10
  import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
10
11
  import { readCapabilities } from '../../harness/capability';
11
12
  import { parseManualComments } from '../../exporters/scenario-merger';
13
+ import { findSyntaxError } from './utils/spec-syntax-check';
12
14
 
13
15
  /**
14
16
  * Filter base scenario steps for @extend: only keep Given→When steps.
@@ -80,7 +82,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
80
82
  const FUNCTIONAL_TAG_PREFIXES = [
81
83
  '@parallel', '@serial', '@cleanup:', '@auth:', '@manual', '@no-auth',
82
84
  '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
83
- '@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:',
85
+ '@flow', '@cases:', '@deferred:', '@owned-by:', '@platform:', '@reset:',
84
86
  ];
85
87
 
86
88
  function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string[] {
@@ -328,7 +330,16 @@ export class CodeGenerator {
328
330
  }
329
331
 
330
332
  // Generate test code (async now to support AI mapping)
331
- const testCode = await this.generateTestCode(feature);
333
+ let testCode = await this.generateTestCode(feature);
334
+
335
+ // Runtime data: replace __SUNGEN_TD$…$__ markers with testData.get() calls.
336
+ // Runs BEFORE the imports are rendered so they can reflect what the transformed
337
+ // body actually references (a marker inside a regex literal becomes an
338
+ // escapeRegExp(...) call that needs its helper imported). Imports carry no
339
+ // markers, so transforming only the body loses nothing.
340
+ if (this.options.runtimeData) {
341
+ testCode = transformToRuntimeData(testCode);
342
+ }
332
343
 
333
344
  // `url-assert` is imported from what the body actually emitted, not from an active capability:
334
345
  // page assertions belong to the DEFAULT ui capability, so a capability flag would import the
@@ -338,16 +349,25 @@ export class CodeGenerator {
338
349
  const urlAssertImports = ['urlMatches', 'joinPath'].filter((n) => testCode.includes(`${n}(`)).join(', ');
339
350
  if (urlAssertImports) this.syncGeneratedHelper(outputDir, 'url-assert.ts', 'specs-url-assert.ts');
340
351
 
341
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi, needsMock, urlAssertImports });
352
+ // Same body-first rule for the runtime regex-escape helper: only a spec whose
353
+ // transformed body wraps a value in escapeRegExp() imports (and syncs) it.
354
+ const regexEscapeImport = /\bescapeRegExp\(/.test(testCode);
355
+ if (regexEscapeImport) this.syncGeneratedHelper(outputDir, 'regex-escape.ts', 'specs-regex-escape.ts');
356
+
357
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi, needsMock, urlAssertImports, regexEscapeImport });
342
358
 
343
359
  // Combine and collapse any runs of 3+ newlines down to 2 (one blank line max)
344
360
  const raw = `${imports}\n\n${testCode}`;
345
361
  let code = raw.replace(/\n{3,}/g, '\n\n');
346
362
 
347
- // Runtime data: replace __SUNGEN_TD$…$__ markers with testData.get() calls
348
- if (this.options.runtimeData) {
349
- code = transformToRuntimeData(code);
350
- }
363
+ // SG-E022 compiler self-check. Every template and post-processor above assembles
364
+ // TEXT, so a delimiter bug in any of them yields a file Playwright cannot parse at all:
365
+ // one SyntaxError, zero scenarios run. That is exactly how the runtime-data quote-pairing
366
+ // defect reached a user — `sungen generate` reported success and the breakage only
367
+ // surfaced at `npx playwright test`, a step further from the cause. Parsing what we are
368
+ // about to write makes the compiler name its own bad output, at the moment it makes it.
369
+ // (The runtime-data transform itself ran on the body above, before the imports.)
370
+ this.assertParseable(code, feature.name, filePath);
351
371
 
352
372
  return {
353
373
  featureName: feature.name,
@@ -358,6 +378,24 @@ export class CodeGenerator {
358
378
  };
359
379
  }
360
380
 
381
+ /**
382
+ * Report SG-E022 when the emitted spec is not parseable. Reported, never thrown: the
383
+ * caller still writes the file, so the user keeps the artifact to inspect (and `--strict`
384
+ * turns it into a non-zero exit like every other diagnostic).
385
+ */
386
+ private assertParseable(code: string, featureName: string, filePath: string): void {
387
+ const detail = findSyntaxError(code);
388
+ if (!detail) return;
389
+ this.diagnostics.add(
390
+ 'SG-E022',
391
+ `generated spec "${filePath}" is not parseable TypeScript (${detail}) — Playwright will `
392
+ + 'fail the whole file with one SyntaxError and run NONE of its scenarios. This is a sungen '
393
+ + 'compiler bug, not a problem with the .feature: please report it with the step this file '
394
+ + 'was generated from.',
395
+ { feature: featureName }
396
+ );
397
+ }
398
+
361
399
  /**
362
400
  * Count total steps in feature (excluding background).
363
401
  * For @extend scenarios, counts base steps + own steps (merged total).
@@ -767,6 +805,45 @@ export class CodeGenerator {
767
805
  // package (activity stripped, it isn't needed for terminateApp/activateApp); iosBundleId is the part
768
806
  // after `|` when present. Without these the test-file template's `{{#if appPackage}}` stays false and
769
807
  // the beforeEach (terminate+activate) reset is never emitted → nav scenarios cascade-fail.
808
+ // Mobile: how much state each scenario starts from — `@reset:none|soft|data|full`, default 'soft'
809
+ // (the terminate+activate relaunch this adapter always did, so an untagged feature is unchanged).
810
+ // Appium-only: web features get a clean slate from navigation and carry no tier.
811
+ const resetTier = this.adapter.frameworkName === 'appium'
812
+ ? (parseResetTier(feature.tags || []) ?? DEFAULT_RESET_TIER)
813
+ : undefined;
814
+
815
+ // Per-scenario `@reset:` overrides. Keyed on the scenario's BASE name — NOT the rendered it()
816
+ // title, because a @cases scenario concatenates its title at runtime from the row label, so no
817
+ // compile-time key could match it. The generated spec normalizes a title back to a base name and
818
+ // membership-tests each step, so a miss just inherits the feature tier rather than guessing wrong.
819
+ // `cases` marks a scenario whose it() title is BUILT AT RUNTIME (`name + ' — ' + row.__label`).
820
+ // Only those titles may have a row suffix stripped when looking a tier up; applying that strip to
821
+ // every scenario let a name that merely LOOKED like `<base> — <suffix>` resolve to a different
822
+ // scenario's tier.
823
+ const scenarioResetTiers = this.adapter.frameworkName === 'appium'
824
+ ? (feature.scenarios || []).reduce<Array<{ name: string; tier: string; cases: boolean }>>((acc, sc) => {
825
+ const tier = parseResetTier(sc.tags || []);
826
+ if (tier) acc.push({ name: sc.name, tier, cases: (sc.tags || []).some(t => t.startsWith('@cases:')) });
827
+ return acc;
828
+ }, [])
829
+ : undefined;
830
+ // SG-W024 — the runtime lookup is keyed on the scenario NAME, so two scenarios sharing a name
831
+ // cannot be told apart. Counted over EVERY scenario, not just the tagged ones: an UNTAGGED
832
+ // scenario sharing a name with a tagged one silently inherits that tier (weakening a
833
+ // `@reset:none` chain, or paying a reinstall it never asked for), and scanning only the tagged
834
+ // subset missed exactly that case.
835
+ if (scenarioResetTiers && scenarioResetTiers.length) {
836
+ const tiered = new Set(scenarioResetTiers.map(t => t.name));
837
+ const counts = new Map<string, number>();
838
+ for (const sc of feature.scenarios || []) counts.set(sc.name, (counts.get(sc.name) || 0) + 1);
839
+ for (const [name, n] of counts) {
840
+ if (n < 2 || !tiered.has(name)) continue;
841
+ this.diagnostics.add('SG-W024',
842
+ `${n} scenarios in this feature are named "${name}" and at least one carries a @reset: tag — the generated lookup is keyed on the scenario NAME, so every one of them resolves to the SAME tier, including any that declared none. Rename them so each is unique.`,
843
+ { feature: feature.name, step: name });
844
+ }
845
+ }
846
+
770
847
  let appPackage: string | undefined;
771
848
  let iosBundleId: string | undefined;
772
849
  if (this.adapter.frameworkName === 'appium' && feature.path) {
@@ -802,6 +879,8 @@ export class CodeGenerator {
802
879
  singleAuthRole,
803
880
  appPackage,
804
881
  iosBundleId,
882
+ resetTier,
883
+ scenarioResetTiers: scenarioResetTiers && scenarioResetTiers.length ? scenarioResetTiers : undefined,
805
884
  });
806
885
  }
807
886
 
@@ -3,7 +3,10 @@
3
3
  * and would otherwise silently discard or invent it" (spec: sungen_app_profile_spec.md §4).
4
4
  *
5
5
  * Codes:
6
- * SG-W001 — step matched no pattern (compiles to a no-op `// TODO` comment)
6
+ * SG-W001 — step matched no pattern: sungen has no step form for the sentence. Compiles to a loud
7
+ * `throw` (it emitted a no-op `// TODO` comment until the unsupported-step gate, which
8
+ * let the scenario pass having asserted nothing). `@manual` is the way to park a check
9
+ * sungen cannot express — such scenarios are skipped before mapping and never reach here
7
10
  * SG-W002 — step parsed a {{dataRef}} value but the generated code never uses it
8
11
  * SG-W003 — `name` filter defaulted from the Gherkin reference on a name-from-content
9
12
  * role (status/alert/…) — such a filter can never match; opt out with `name: null`
@@ -21,8 +24,10 @@
21
24
  * entry: the label is display-only, so the step checks existence anywhere in the
22
25
  * table — an ordering rule would pass even when the row moved
23
26
  * SG-W011 — a storage assertion written in `expect … in local/session storage` form:
24
- * `expect` steps only read `{{response}}` refs, so this compiles to a silent
25
- * no-op — the supported grammar is `User see [KEY] in local storage exists/…`
27
+ * `expect` steps only read `{{response}}` refs, so this matches no pattern — the
28
+ * supported grammar is `User see [KEY] in local storage exists/…`. Compiles to a
29
+ * `throw` (it was a silent no-op until the unsupported-step gate); the reason string
30
+ * keeps naming the real grammar rather than falling back to SG-W001's generic text
26
31
  * SG-W012 — a mock-install step AFTER a navigation step in the same block: page.route()
27
32
  * registered after goto() misses every request fired during page load — move
28
33
  * the install before the navigation (or into Background)
@@ -40,6 +45,14 @@
40
45
  * "3 cards are shown" gets an assertion that can only pass if the card's text
41
46
  * IS the digit. Count semantics exist on row/table ("see [T] table with {{n}}",
42
47
  * "see [Ref] row with {{n}}") and lists ("should have count") only
48
+ * SG-E024 — a mobile app-lifecycle step (restart / reinstall) in a feature that declares no
49
+ * `Path:` app id. The templates resolve the app through a helper the test-file only
50
+ * emits when the id is present, so the spec used to compile clean and then die at
51
+ * runtime with a ReferenceError — taking the whole spec, not the step.
52
+ * SG-E023 — the step carries an assertion clause (`matches "…"`, `contains`, `has text`, a state)
53
+ * but the generated code is a BARE VISIBILITY check: the clause was swallowed by the
54
+ * catch-all and the test would pass whatever the element says. Four separate defects
55
+ * have arrived this way, so the step compiles to a runtime throw instead.
43
56
  * SG-E020 — the step matched a pattern whose template the ACTIVE adapter does not ship. Before
44
57
  * the platform SPI this was an unhandled `Template not found` that killed the WHOLE
45
58
  * feature file and named neither the step nor the adapter. The step now compiles to a
@@ -54,17 +67,58 @@
54
67
  * every following step resolves page-wide as if the scope had never been written.
55
68
  * Silently-ignored scope is the exact defect class this channel exists for — the step
56
69
  * LOOKS like it narrows the search and does not
70
+ * SG-E021 — the step carried a trailing state clause (`… is hidden`) that the pattern which matched
71
+ * it does not account for, so the clause would be SILENTLY DROPPED. This is the worst
72
+ * shape in this catalogue: the others weaken an assertion, this one can INVERT it.
73
+ * `see [X] page is hidden` matched `page-assertion` (priority 13) ahead of `is-hidden`
74
+ * (11), the clause vanished, and the step compiled to `toHaveURL(...)` — asserting the
75
+ * page IS displayed, the exact opposite of what it reads as, and passing while doing so.
76
+ * The pattern layer declares what it accounts for via `StepPattern.states`; anything
77
+ * undeclared reports here and compiles to a loud `throw`
57
78
  * SG-W022 — a `page` assertion on the mobile adapter that emitted NO executable code. A native app
58
79
  * has no URL, so `Then User see [X] page` compiles to a bare comment: the step reads as
59
80
  * an assertion, checks nothing, and the scenario PASSES regardless. Worse than the
60
81
  * crash class this plan closed — a crash is visible, a false pass is not. Its `is on`
61
82
  * twin routes to `route-assertion`, which throws; this half was silent
83
+ * SG-W023 — a mobile SETUP step that emitted NO executable code, so a declared PRECONDITION is never
84
+ * established. `Given the user is logged out` matches `user-not-logged-in` → `clear-auth`,
85
+ * whose Appium template is a bare comment: mobile auth lives in the keychain and prefs,
86
+ * not in cookies, so there is nothing for a per-step action to clear. The scenario then
87
+ * runs against whatever the PREVIOUS scenario left signed in — the same false-pass class
88
+ * as SG-W022, arriving through setup rather than assertion.
89
+ * Unlike SG-W020/SG-E021 this does NOT compile to a `throw`: an unestablished precondition
90
+ * weakens the setup, it does not INVERT an assertion, and throwing would break every
91
+ * mobile suite already carrying one of these steps. The remedy is a feature-level reset
92
+ * tier (`@reset:data`), which is what the message names — app state is reset between
93
+ * scenarios in the `beforeEach` hook, not mid-scenario by a step.
94
+ * SG-W024 — two or more scenarios in one feature share a name and at least ONE carries a `@reset:`
95
+ * tag. The generated per-scenario lookup is keyed on the scenario NAME (a @cases title is
96
+ * built at runtime, so no compile-time key can match the rendered title), so every
97
+ * same-named scenario resolves to the SAME tier — including one that declared none, which
98
+ * then silently runs at its namesake's tier: a `@reset:none` chain broken, or a reinstall
99
+ * nobody asked for. Counted over EVERY scenario rather than the tagged subset, because the
100
+ * untagged twin is the one that gets the wrong tier and has no tag to be counted by.
101
+ * Reported rather than resolved by precedence — two tests genuinely sharing one title
102
+ * cannot be told apart by a name-keyed lookup, so the only honest fix is a rename. Silently
103
+ * picking a winner would be the same silent-discard shape as SG-W021
104
+ * SG-E022 — the generated spec is not parseable JS/TS, so NOTHING in the file runs — Playwright
105
+ * reports one SyntaxError and every scenario in the unit is lost. A compiler self-check
106
+ * (@babel/parser) on the emitted code, added after a field report where the runtime-data
107
+ * marker post-processor mis-paired quote delimiters across two literals and the user only
108
+ * found out at `npx playwright test`. Unlike every other code here this one blames the
109
+ * compiler, not the Gherkin: it should be impossible, and it is reported so a broken
110
+ * build names itself at generate time instead of one step further downstream
62
111
  *
63
- * Diagnostics never block generation by themselves; `sungen generate --strict`
64
- * turns any collected diagnostic into a non-zero exit.
112
+ * Diagnostics never change the EXIT CODE by themselves; `sungen generate --strict` turns any
113
+ * collected diagnostic into a non-zero exit. They are not all inert, though: SG-W001, SG-W011,
114
+ * SG-E020, SG-E021, SG-W020, SG-E023 and SG-E024 additionally compile the offending step to a runtime
115
+ * `throw`, because a step that reads as a check and silently checks nothing is worse than one that
116
+ * fails loudly. (SG-E022 is the one exception to the "blame the Gherkin" rule and throws nothing: the
117
+ * file is still written so the user keeps the artifact to inspect.)
118
+ * Generation always COMPLETES — the rest of the scenario and the rest of the file still compile.
65
119
  */
66
120
 
67
- export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-E020' | 'SG-W020' | 'SG-W021' | 'SG-W022';
121
+ export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015' | 'SG-E020' | 'SG-E021' | 'SG-W020' | 'SG-W021' | 'SG-W022' | 'SG-W023' | 'SG-W024' | 'SG-E022' | 'SG-E023' | 'SG-E024';
68
122
 
69
123
  export interface Diagnostic {
70
124
  code: DiagnosticCode;
@@ -25,13 +25,45 @@ const reExpect = new RegExp(`^\\s*(?:User\\s+)?expect\\s+(${VALUE})\\s+is\\s+(no
25
25
  const reMatchesSchemaMasked = /^\s*(?:User\s+)?expect\s+\{\{~\}\}\s+matches\s+schema\s+\[~\]\s*$/i;
26
26
  const reMatchesSchema = new RegExp(`^\\s*(?:User\\s+)?expect\\s+(\\{\\{[^}]+\\}\\})\\s+matches\\s+schema\\s+\\[([A-Za-z0-9_.-]+)\\]\\s*$`, 'i');
27
27
 
28
+ /**
29
+ * `{{var}}` EMBEDDED in a quoted literal — test-data keys and paths only
30
+ * (`user.name`, `q.rows[0].id`, `items[*]`, kebab flow namespaces like
31
+ * `password-reset.email`). `{{$...}}` dynamic tokens are excluded on purpose:
32
+ * they have no test-data key, so converting one to a get() call throws
33
+ * "Test data key not found" at runtime — they stay literal.
34
+ */
35
+ const RE_EMBEDDED_VAR = /\{\{\s*([\w.[\]*-]+?)\s*\}\}/g;
36
+
37
+ /** Escape static literal text for a template-literal context. */
38
+ const escapeForTemplate = (s: string): string =>
39
+ s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
40
+
41
+ /**
42
+ * Render a quoted literal's BODY as a JS expression. Text without vars keeps
43
+ * its exact JSON.stringify shape; an embedded `{{var}}` interpolates the
44
+ * runtime value (the literal used to ship verbatim, so the assertion compared
45
+ * against the raw `{{...}}` text and could never pass with correct data).
46
+ */
47
+ export function literalExpr(text: string): string {
48
+ const vars = [...text.matchAll(RE_EMBEDDED_VAR)];
49
+ if (vars.length === 0) return JSON.stringify(text);
50
+ if (vars.length === 1 && vars[0][0] === text) return `testData.get(${JSON.stringify(vars[0][1])})`;
51
+ let out = '';
52
+ let last = 0;
53
+ for (const m of vars) {
54
+ out += escapeForTemplate(text.slice(last, m.index!)) + `\${testData.get(${JSON.stringify(m[1])})}`;
55
+ last = m.index! + m[0].length;
56
+ }
57
+ return `\`${out + escapeForTemplate(text.slice(last))}\``;
58
+ }
59
+
28
60
  /** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
29
61
  function valueExpr(token: string): string {
30
62
  const t = token.trim();
31
63
  const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
32
64
  if (v) return `testData.get(${JSON.stringify(v[1])})`;
33
65
  const q = t.match(/^["'](.*)["']$/);
34
- if (q) return JSON.stringify(q[1]);
66
+ if (q) return literalExpr(q[1]);
35
67
  if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
36
68
  return JSON.stringify(t);
37
69
  }
@@ -46,7 +78,10 @@ export const expectPatterns: StepPattern[] = [
46
78
  // odd chars) can claim the pattern yet not re-match here — emit a clear TODO instead of crashing.
47
79
  const m = step.text.match(reMatchesSchema);
48
80
  if (!m) return { code: `// TODO: malformed 'matches schema' step (expected: expect {{name.body}} matches schema [Ref]): ${step.text}`, comment: 'Unrecognized matches-schema step' };
49
- const value = valueExpr(m[1]);
81
+ // raw(), not get(): Ajv must receive the NATIVE bound object. get() serializes
82
+ // to JSON text, so every schema validation failed with "(root) must be object"
83
+ // no matter how correct the response body was.
84
+ const value = `testData.raw(${JSON.stringify(m[1].replace(/^\{\{\s*|\s*\}\}$/g, ''))})`;
50
85
  const ref = m[2];
51
86
  const code = `{ const __schema = api.assertSchema(${value}, ${JSON.stringify(ref)}); expect(__schema.ok, \`matches schema [${ref}]: \${__schema.errors}\`).toBe(true); }`;
52
87
  return { code, comment: `Expect ${m[1]} matches schema [${ref}]` };
@@ -3,23 +3,13 @@ import { MappedStep } from '../step-mapper';
3
3
  import { StepPattern, PatternContext, Platform } from './types';
4
4
  import { capabilityRegistry } from '../../../capabilities/registry';
5
5
  import { discoverAndRegisterCapabilities } from '../../../capabilities/discover';
6
+ import { maskRefsForMatching } from '../utils/mask-refs-for-matching';
7
+ import { unsupportedStepCode } from './unsupported-step-code';
6
8
 
7
- /**
8
- * Mask the CONTENT of selector refs `[...]` and data placeholders `{{...}}` before verb matching,
9
- * keeping the delimiters. A step's verb/keywords come from the SENTENCE — never from a selector or
10
- * variable NAME — but matchers scan `step.text` with substring checks (e.g. `includes('click')`),
11
- * so a data ref like `{{double_click_guard_account.email}}` would spuriously satisfy the click
12
- * matcher and pre-empt fill. The content is replaced with an inert placeholder (`~` — carries no
13
- * verb/keyword/digit substring) rather than emptied, so structural matchers that require a NON-EMPTY
14
- * ref survive too — e.g. `expect {{a}} is {{b}}` matches `\{\{[^}]+\}\}`, and `in [X] table` /
15
- * `/\bto\s+\[/` keep their delimiters. The resolver/generator always receives the ORIGINAL step, so
16
- * value extraction and codegen are unaffected.
17
- */
18
- export function maskRefsForMatching(text: string): string {
19
- return text
20
- .replace(/\{\{[^}]*\}\}/g, '{{~}}')
21
- .replace(/\[[^\]]*\]/g, '[~]');
22
- }
9
+ // Both helpers moved out of this file so the Gherkin parser (state-clause extraction) and the
10
+ // step-mapper (SG-W001 no-pattern fallback) can use them without importing the pattern registry.
11
+ // Re-exported here because their historical import path is `patterns/index.ts`.
12
+ export { maskRefsForMatching, unsupportedStepCode };
23
13
 
24
14
  /**
25
15
  * Gesture forms that OWN the word "tap" — each has its own pattern keyed on it (`mobile-double-tap`
@@ -76,23 +66,6 @@ export function normalizeVerbs(text: string): string {
76
66
  */
77
67
  const TEMPLATE_NOT_FOUND = /^Template not found: (.+)\.hbs in any subfolder$/;
78
68
 
79
- /**
80
- * The step body emitted in place of code the active adapter cannot produce. A comment alone would
81
- * let the test go GREEN having asserted nothing — the silent-loss class this SPI exists to close —
82
- * so the step throws at run time as well as reporting at compile time.
83
- *
84
- * `JSON.stringify` does the quoting, NOT a hand-rolled `replace(/'/g, …)`. The reason string embeds
85
- * arbitrary author text (the step sentence), and hand-escaping only the quote character is wrong for
86
- * any input containing a backslash or a raw newline: a step ending `…\` would emit `'…\\'`, whose
87
- * trailing escaped-backslash-then-quote breaks the literal and takes the WHOLE generated spec down
88
- * with a syntax error — reopening, one level deeper, the exact "one bad step kills the file" failure
89
- * this function exists to prevent. Step text here is routinely non-ASCII (Vietnamese, Japanese),
90
- * which JSON.stringify also handles correctly.
91
- */
92
- function unsupportedStepCode(reason: string): string {
93
- return `throw new Error(${JSON.stringify(`[sungen] ${reason}`)});`;
94
- }
95
-
96
69
  /**
97
70
  * Pattern Registry - manages all step patterns
98
71
  */
@@ -170,6 +143,16 @@ export class PatternRegistry {
170
143
  const resolved = pattern.resolver(step, context);
171
144
  if (!resolved) continue; // resolver declined — fall through
172
145
 
146
+ // State-clause gate. Deliberately AFTER the resolver, not before it: a resolver may return
147
+ // null to say "not mine after all", and the search then continues to a lower-priority
148
+ // pattern that may well be the one that asserts the state. Gating earlier judged a pattern
149
+ // that had not yet claimed the step — `row-count` declines a non-numeric `{{value}}` so
150
+ // `see [R] row with {{status_label}} is disabled` should reach see-with-variable-disabled,
151
+ // but the gate short-circuited it into a throw. Only the pattern that actually OWNS the
152
+ // step gets to be judged on what it drops.
153
+ const resolverStateMiss = PatternRegistry.stateMiss(pattern, context, step);
154
+ if (resolverStateMiss) return resolverStateMiss;
155
+
173
156
  // Auto-inject parent scoping if step has parentRef
174
157
  // Skip for table-* patterns — they resolve the table name internally from step text
175
158
  if (step.parentRef && step.parentType && !pattern.name.startsWith('table-')) {
@@ -186,6 +169,9 @@ export class PatternRegistry {
186
169
  }
187
170
 
188
171
  if (pattern.generator) {
172
+ // A legacy generator has no decline path, so this pattern owns the step already.
173
+ const generatorStateMiss = PatternRegistry.stateMiss(pattern, context, step);
174
+ if (generatorStateMiss) return generatorStateMiss;
189
175
  return pattern.generator(step, context);
190
176
  }
191
177
 
@@ -228,6 +214,45 @@ export class PatternRegistry {
228
214
  };
229
215
  }
230
216
 
217
+ /**
218
+ * SG-E021 — the step carries a trailing state clause the matched pattern does not account for.
219
+ *
220
+ * Priority decides which pattern wins, and the winner is not always the one that reads the clause:
221
+ * `see [X] page is hidden` matched `page-assertion` (13) ahead of `is-hidden` (11), so `is hidden`
222
+ * was dropped and the step compiled to `toHaveURL(...)` — asserting the page IS shown, the exact
223
+ * inverse of the sentence, and passing. Nothing detected it, because the clause lived only inside
224
+ * `step.text` and each pattern re-regexed it privately.
225
+ *
226
+ * `states` makes the claim explicit and checkable: a pattern declares what its template accounts
227
+ * for, and anything else lands here. Strict reading — an ABSENT `states` accounts for nothing —
228
+ * because the failure modes are asymmetric. Under-declaring throws on valid input, which is loud,
229
+ * local, and fixed by one annotation. Over-declaring restores the silent inversion this exists to
230
+ * close, and nothing catches it again.
231
+ */
232
+ private static stateMiss(
233
+ pattern: StepPattern, context: PatternContext, step: ParsedStep
234
+ ): MappedStep | null {
235
+ const state = step.state;
236
+ if (!state) return null;
237
+ if (pattern.states?.includes(state)) return null;
238
+
239
+ const reason =
240
+ `step "${step.text}" carries the state clause "is ${state}", but the pattern that matched it ` +
241
+ `("${pattern.name}") does not assert that state — the clause would be SILENTLY DROPPED and the ` +
242
+ `step would check something other than what it reads as. Use a step form that owns the state ` +
243
+ `(e.g. "User see [Ref] <type> is ${state}" on the element itself), or drop the clause if the ` +
244
+ `bare assertion is what you meant. If sungen cannot express the check, tag the scenario @manual.`;
245
+
246
+ context.diagnostics?.add('SG-E021', reason, {
247
+ feature: context.featureName,
248
+ step: step.text,
249
+ });
250
+ return {
251
+ code: unsupportedStepCode(reason),
252
+ comment: `"is ${state}" not asserted by "${pattern.name}" — see SG-E021`,
253
+ };
254
+ }
255
+
231
256
  /**
232
257
  * SG-E020 — the pattern is in-platform but the active adapter ships no template for it. A gap in
233
258
  * the adapter, not in the authored Gherkin: name every coordinate a reader needs to act on it.
@@ -19,6 +19,28 @@ export interface DiagnosticsSink {
19
19
  */
20
20
  export type Platform = 'web' | 'mobile';
21
21
 
22
+ /**
23
+ * The canonical state clauses an assertion step may carry, exactly as documented for authors in
24
+ * `README.md` ("States") and in the `sungen-gherkin-syntax` skill. Deliberately CLOSED: real feature
25
+ * files carry trailing phrases like `is rejected` / `is active` / `is unchanged` that belong to other
26
+ * grammars or are prose, and reading those as state clauses would put valid steps in front of the
27
+ * SG-E021 gate. Widening this union widens that gate — add a member only alongside a pattern that
28
+ * asserts it.
29
+ */
30
+ export type StateClause =
31
+ | 'hidden'
32
+ | 'visible'
33
+ | 'disabled'
34
+ | 'enabled'
35
+ | 'checked'
36
+ | 'unchecked'
37
+ | 'focused'
38
+ | 'empty'
39
+ | 'loading'
40
+ | 'selected'
41
+ | 'sorted ascending'
42
+ | 'sorted descending';
43
+
22
44
  /**
23
45
  * Pattern matcher function or regex
24
46
  */
@@ -104,4 +126,18 @@ export interface StepPattern {
104
126
  * emitted when absent.
105
127
  */
106
128
  platformAlternative?: string;
129
+ /**
130
+ * State clauses this pattern's generated code ACTUALLY asserts. ABSENT = asserts none.
131
+ *
132
+ * The contract runs in one direction and is easy to get backwards: declaring a state is a promise
133
+ * that the emitted code checks it. A pattern that wins a step carrying an UNDECLARED state clause
134
+ * emits SG-E021 and compiles to a loud `throw`, because the alternative is what this field exists
135
+ * to prevent — the clause being silently dropped and the assertion reading as something it is not.
136
+ * (`see [X] page is hidden` matched `page-assertion` at a higher priority than `is-hidden`, so the
137
+ * clause vanished and the step asserted the OPPOSITE: that the page IS displayed.)
138
+ *
139
+ * So: under-declaring throws on valid input, over-declaring re-opens the silent-drop hole. Declare
140
+ * a state only after reading the template the resolver names.
141
+ */
142
+ states?: ReadonlyArray<StateClause>;
107
143
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The step body emitted in place of code sungen cannot honestly produce — because the active adapter
3
+ * ships no template for it (SG-E020), because the pattern is off-platform (SG-W020), because the
4
+ * matched pattern would silently DROP the author's state clause (SG-E021), or because no pattern
5
+ * matched at all (SG-W001/SG-W011). A comment alone would let the test go GREEN having asserted
6
+ * nothing — the silent-loss class these diagnostics exist to close — so the step throws at run time
7
+ * as well as reporting at compile time.
8
+ *
9
+ * `JSON.stringify` does the quoting, NOT a hand-rolled `replace(/'/g, …)`. The reason string embeds
10
+ * arbitrary author text (the step sentence), and hand-escaping only the quote character is wrong for
11
+ * any input containing a backslash or a raw newline: a step ending `…\` would emit `'…\\'`, whose
12
+ * trailing escaped-backslash-then-quote breaks the literal and takes the WHOLE generated spec down
13
+ * with a syntax error — reopening, one level deeper, the exact "one bad step kills the file" failure
14
+ * this function exists to prevent. Step text here is routinely non-ASCII (Vietnamese, Japanese),
15
+ * which JSON.stringify also handles correctly.
16
+ */
17
+ export function unsupportedStepCode(reason: string): string {
18
+ return `throw new Error(${JSON.stringify(`[sungen] ${reason}`)});`;
19
+ }
@@ -103,8 +103,16 @@ function expandPrimitive(prim: unknown, ctx: RecipeExpandContext, itemVar: strin
103
103
  return { partial: 'recipe-click-text', targetExpr: expr };
104
104
  }
105
105
  case 'expectText': {
106
+ // `exact: true` asserts the element's WHOLE text (toHaveText) instead of a substring
107
+ // (toContainText) — same opt-in flag `clickText` carries. Substring stays the default so
108
+ // existing recipes keep their behavior.
106
109
  const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
107
- return { partial: 'recipe-expect-text', targetExpr: targetExpr(o.locator ?? '$trigger'), valueExpr: valueExpr(o.value ?? '$value') };
110
+ return {
111
+ partial: 'recipe-expect-text',
112
+ targetExpr: targetExpr(o.locator ?? '$trigger'),
113
+ valueExpr: valueExpr(o.value ?? '$value'),
114
+ exact: o.exact === true,
115
+ };
108
116
  }
109
117
  case 'expectValue': {
110
118
  const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Mobile reset tiers — how much state a scenario starts from.
3
+ *
4
+ * Web gets a clean slate for free: a URL reload is effectively a fresh state. Mobile does not. To save
5
+ * time, mobile automation keeps the app alive across a whole session, so the end state of one scenario
6
+ * becomes the start state of the next — which breaks test independence three ways: scenarios can no
7
+ * longer run in isolation or in parallel, a "nearly right" leftover state makes the FOLLOWING scenario
8
+ * fail for reasons that have nothing to do with it, and a scenario that dies mid-flow poisons every
9
+ * scenario after it.
10
+ *
11
+ * The reason nobody just reinstalls between scenarios is cost: a cold start is 5–30s, which across a
12
+ * large suite is the whole CI budget. So the state layers (local DB, cache, prefs, keychain, OS
13
+ * permissions) need a LADDER rather than one all-or-nothing switch — the same conclusion every major
14
+ * framework reached (Appium's `mobile: clearApp`, Detox's `device.reloadReactNative()`, the OS-level
15
+ * reset APIs in XCUITest/Espresso).
16
+ *
17
+ * none — nothing is reset. State carries over BY DESIGN (a cart that accumulates across scenarios).
18
+ * soft — terminateApp + activateApp. Clears in-memory state and the navigation stack. THE DEFAULT,
19
+ * and exactly what the compiler emitted before tiers existed, so no existing suite changes.
20
+ * data — soft + wipe the app's data layer (local DB, cache, prefs, keychain).
21
+ * full — remove + reinstall. Also resets permissions and any install-time state.
22
+ *
23
+ * Declared per feature via `@reset:<tier>`; a run can RAISE the floor with SUNGEN_RESET_TIER without
24
+ * regenerating specs (resolved inside the generated spec, the same runtime model as test-data overlays).
25
+ */
26
+
27
+ /** Ordered weakest → strongest. Index order IS the comparison, so never reorder this. */
28
+ export const RESET_TIERS = ['none', 'soft', 'data', 'full'] as const;
29
+
30
+ export type ResetTier = (typeof RESET_TIERS)[number];
31
+
32
+ /**
33
+ * `soft` — the behaviour the appium adapter emitted before tiers existed. Keeping it the default is
34
+ * what makes this feature additive: an untagged feature compiles to the same spec it did before.
35
+ */
36
+ export const DEFAULT_RESET_TIER: ResetTier = 'soft';
37
+
38
+ function isResetTier(value: string): value is ResetTier {
39
+ return (RESET_TIERS as readonly string[]).includes(value);
40
+ }
41
+
42
+ /**
43
+ * Read the `@reset:<tier>` tag off a tag list.
44
+ *
45
+ * Returns `undefined` when no `@reset:` tag is present — deliberately NOT the default. Callers need to
46
+ * tell "absent" from "explicitly soft": a scenario with no tag must inherit its feature's tier, and
47
+ * collapsing absent into `soft` here would silently RAISE a `@reset:none` feature.
48
+ *
49
+ * An unrecognized value warns and yields the default rather than throwing, matching how `@cleanup:`
50
+ * handles an unknown key — a typo in a tag should not stop a build.
51
+ */
52
+ export function parseResetTier(tags: string[]): ResetTier | undefined {
53
+ const tag = tags.find((t) => t.startsWith('@reset:'));
54
+ if (!tag) return undefined;
55
+ const value = tag.slice('@reset:'.length).trim().toLowerCase();
56
+ if (isResetTier(value)) return value;
57
+ console.warn(`⚠ Unknown @reset:${value} — valid tiers: ${RESET_TIERS.join(', ')}. Using '${DEFAULT_RESET_TIER}'.`);
58
+ return DEFAULT_RESET_TIER;
59
+ }
60
+
61
+ /** The stronger of two tiers. Used to combine a declared tier with a run-level floor. */
62
+ export function maxResetTier(a: ResetTier, b: ResetTier): ResetTier {
63
+ return RESET_TIERS.indexOf(a) >= RESET_TIERS.indexOf(b) ? a : b;
64
+ }
65
+
66
+ /**
67
+ * One rung harder — what the NEXT scenario resets to after this one failed.
68
+ *
69
+ * A failed scenario left unknown state behind (a half-written local DB, an open modal) that a plain
70
+ * process relaunch does not undo, so the scenario after it deserves a stronger reset than it asked for.
71
+ *
72
+ * `none` is a FIXED POINT, on purpose. It is an explicit "state carries over" contract — a chain of
73
+ * scenarios building up one cart. Escalating it would silently break that contract, and the chain is
74
+ * already dead once one link fails, so the relaunch would only change how the rest fail, not whether.
75
+ *
76
+ * Applies to the next DIFFERENT scenario only — never to a RETRY of the one that just failed. The
77
+ * runner re-runs a failed scenario, and giving attempt 2 a stronger reset would break the guarantee
78
+ * `mochaOpts.retries` rests on: race victims recover on attempt 2, real failures fail both attempts.
79
+ * A dirty-state failure IS a real failure; if its retry silently got a wiped data layer it would pass,
80
+ * the reporter keeps the last attempt, and a scenario that only works after a wipe would ship as a
81
+ * clean `Passed`. That false pass is worse than the flakiness escalation was added to fix.
82
+ *
83
+ * The generated spec emits this same ladder inline; both exist so the compiler and the runtime cannot
84
+ * drift apart, and a test asserts they agree.
85
+ */
86
+ export function escalateResetTier(tier: ResetTier): ResetTier {
87
+ switch (tier) {
88
+ case 'none': return 'none';
89
+ case 'soft': return 'data';
90
+ case 'data': return 'full';
91
+ case 'full': return 'full';
92
+ }
93
+ }