@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
@@ -1,3 +1,5 @@
1
+ import { scanSegments, type Segment } from './js-literal-scanner';
2
+
1
3
  /**
2
4
  * Wire format of a runtime test-data marker: `__SUNGEN_TD$<key>$__`.
3
5
  *
@@ -14,12 +16,12 @@
14
16
  * swallowed one `_` of the `___` run and decoding produced `get('code_')` plus
15
17
  * a detached `ACCESS_TOKEN`. `$` is also identifier-safe — the whole marker
16
18
  * still parses as ONE bare JS token, so it survives the bare-identifier
17
- * (numeric) positions Pass 3 handles.
19
+ * (numeric) positions the code context handles.
18
20
  *
19
- * Single source of truth: every pass and every whole-literal probe below is
20
- * built from these constants. Invariant: every marker producer goes through
21
- * DataResolver.encodeMarker and every consumer through this module — a
22
- * drifting hand-copied char class is what let the suffix bug hide.
21
+ * Single source of truth: every context below is built from these constants.
22
+ * Invariant: every marker producer goes through DataResolver.encodeMarker and
23
+ * every consumer through this module — a drifting hand-copied char class is what
24
+ * let the suffix bug hide.
23
25
  */
24
26
  const MARKER_KEY = '[A-Za-z0-9_-]+';
25
27
  // Templates that embed a value into a regex literal run it through the escapeRegex
@@ -27,100 +29,148 @@ const MARKER_KEY = '[A-Za-z0-9_-]+';
27
29
  // arrive here spelled `__SUNGEN_TD\$key\$__`. `\\\\?` accepts that optional backslash
28
30
  // before each delimiter; the replacement consumes it, so no escape leaks to output.
29
31
  const DELIM = '\\\\?\\$';
30
- /** Marker with no capture group — for "contains a marker" positions inside bigger regexes. */
32
+ /** Marker with no capture group — for "contains a marker" probes. */
31
33
  const MARKER = `__SUNGEN_TD${DELIM}${MARKER_KEY}${DELIM}__`;
32
34
  /** Marker capturing the encoded key. */
33
35
  const MARKER_CAP = `__SUNGEN_TD${DELIM}(${MARKER_KEY})${DELIM}__`;
34
36
 
35
37
  const MARKER_PATTERN = new RegExp(MARKER);
36
- // Pass 0: full-line // comments carrying a marker (anchored see pass comment).
37
- const RE_COMMENT_LINE = new RegExp(`^(\\s*)\\/\\/(.*)${MARKER_CAP}(.*)$`, 'gm');
38
- // Pass 1: a quoted string literal containing at least one marker. Covers ', " and
39
- // backtick containers; `\\.` lets an ESCAPED quote (`'it\'s {{x}}'`) sit inside the
40
- // body instead of terminating the literal early. The single-char branch excludes `\`
41
- // so every backslash is consumed by exactly ONE branch — an ambiguous `(?!\1).`
42
- // alternative enumerates 2^n segmentations when a marker-less escape-heavy literal
43
- // (escapeQuotes output: multi-line Flutter content-desc) fails to match, hanging
44
- // the generate run. `\n` exclusion just makes the dot's no-newline behavior explicit.
45
- const RE_STRING_LITERAL = new RegExp('([\'"`])((?:\\\\.|(?!\\1)[^\\\\\\n])*?' + MARKER + '(?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1', 'g');
46
- // Pass 2: a single-line regex literal containing at least one marker.
47
- const RE_REGEX_LITERAL = new RegExp(`\\/((?:[^/\\\\\\n]|\\\\.)*?${MARKER}(?:[^/\\\\\\n]|\\\\.)*)\\/([gimsuy]*)`, 'g');
48
- // Whole-literal probe: the body is EXACTLY one marker (bare accessor call, no template).
38
+ /** Whole-literal probe: the body is EXACTLY one marker (bare accessor call, no template). */
49
39
  const RE_WHOLE_MARKER = new RegExp(`^${MARKER_CAP}$`);
50
- // Every marker occurrence, capturing the key (Pass 3 + interpolation).
40
+ /** Every marker occurrence, capturing the key. */
51
41
  const RE_MARKER_G = new RegExp(MARKER_CAP, 'g');
52
42
 
53
43
  /**
54
44
  * Replace __SUNGEN_TD$…$__ markers with testData.get() calls in generated code.
55
- * Four passes: comments, string literals, regex literals, then bare identifiers.
45
+ *
46
+ * The code is scanned ONCE into literal/regex/comment/code segments and each
47
+ * segment is rewritten for the context it is actually in. This replaced four
48
+ * independent global-regex passes whose ordering was the defect: the string pass
49
+ * ran first and, finding no marker inside a genuine literal, re-matched from that
50
+ * literal's CLOSING quote — pairing it with the OPENING quote of the next literal
51
+ * and rewriting the regex literal between them into a broken backtick template
52
+ * (`getByText('Tìm thấy`)` … `state: `visible' })`, a .spec.ts that would not
53
+ * parse at all). See js-literal-scanner.ts for the full field report.
56
54
  */
57
55
  export function transformToRuntimeData(code: string, accessor: string = 'testData'): string {
58
- // Pass 0: Comments — replace markers in // comments with decoded key name.
59
- // Prevents Pass 2 from misinterpreting // comment markers as regex delimiters.
60
- // ANCHORED to line start (^\s*//) so a `//` INSIDE a string literal on a code line
61
- // — e.g. a URL `https://careerforum.aki-inc.jp/` in a selector name — is NOT mistaken
62
- // for a comment. Before this anchor, such a URL preceding a marker on the same line
63
- // made Pass 0 strip the marker to its bare key, so Pass 1 never wrapped it in
64
- // testData.get() (silent: `toHaveAttribute('href', 'official_site_url')`). Generated
65
- // comments are always full-line, so anchoring loses nothing.
66
- code = code.replace(
67
- RE_COMMENT_LINE,
68
- (_, indent, before, enc, after) => `${indent}//${before}${decodeKey(enc)}${after}`
69
- );
70
-
71
- // Pass 1: String literal context — a literal may carry SEVERAL markers
72
- // ('/{{code}}/car-search?{{prefill}}' compiles to two markers in ONE literal).
73
- // Match the whole quoted literal that contains at least one marker, then
74
- // interpolate EVERY marker inside it. The old shape (one marker + lazy
75
- // prefix/suffix) swallowed trailing markers into its suffix verbatim; they
76
- // escaped the quote context (the literal had become a backtick template) and
77
- // Pass 3's numeric catch-all emitted them as literal
78
- // `Number(testData.get(...))` TEXT inside the template — a compiling spec
79
- // navigating to a garbage URL.
80
- // 'marker' → testData.get('key')
81
- // 'a__m1__b__m2__c' → `a${testData.get('k1')}b${testData.get('k2')}c`
82
- code = code.replace(
83
- RE_STRING_LITERAL,
84
- (_, _quote, body) => {
85
- const whole = RE_WHOLE_MARKER.exec(body);
86
- if (whole) return `${accessor}.get('${decodeKey(whole[1])}')`;
87
- return `\`${interpolateMarkers(body, accessor)}\``;
88
- }
89
- );
56
+ if (!MARKER_PATTERN.test(code)) return code;
57
+ return scanSegments(code).map((seg) => transformSegment(seg, accessor)).join('');
58
+ }
90
59
 
91
- // Pass 2: Regex literal context — /…$marker$…/ → new RegExp(`…`). Same
92
- // several-markers-per-literal rule as Pass 1 (/{{min}}-{{max}} items/).
93
- // Only matches within a single line (no newlines in the body).
94
- code = code.replace(
95
- RE_REGEX_LITERAL,
96
- (_, body, flags) => {
97
- const flagStr = flags ? `, '${flags}'` : '';
98
- const whole = RE_WHOLE_MARKER.exec(body);
99
- if (whole) return `new RegExp(${accessor}.get('${decodeKey(whole[1])}')${flagStr})`;
100
- return `new RegExp(\`${interpolateMarkers(body, accessor)}\`${flagStr})`;
101
- }
102
- );
60
+ export function hasRuntimeDataMarkers(code: string): boolean {
61
+ return MARKER_PATTERN.test(code);
62
+ }
103
63
 
104
- // Pass 3: Bare-identifier context any marker left after the string/regex passes
105
- // sits as a bare token in a numeric position (e.g. `toHaveCount(__marker__)` from the
106
- // table/list count templates). testData.get() returns a string, so coerce with Number().
107
- code = code.replace(
108
- RE_MARKER_G,
109
- (_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`
110
- );
64
+ function transformSegment(seg: Segment, accessor: string): string {
65
+ if (!MARKER_PATTERN.test(seg.text)) return seg.text;
66
+ switch (seg.kind) {
67
+ // Decode to the plain key: a marker in a comment documents the ref, it is not code.
68
+ case 'comment':
69
+ return seg.text.replace(RE_MARKER_G, (_, enc) => decodeKey(enc));
70
+ case 'string':
71
+ return transformStringLiteral(seg.text, accessor);
72
+ case 'regex':
73
+ return transformRegexLiteral(seg.text, accessor);
74
+ // Bare-token position (e.g. `toHaveCount(__marker__)` from the table/list count
75
+ // templates). testData.get() returns a string, so coerce with Number().
76
+ case 'code':
77
+ return seg.text.replace(RE_MARKER_G, (_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`);
78
+ }
79
+ }
111
80
 
112
- return code;
81
+ /**
82
+ * 'marker' → testData.get('key')
83
+ * 'a__m1__b__m2__c' → `a${testData.get('k1')}b${testData.get('k2')}c`
84
+ *
85
+ * A literal may carry SEVERAL markers ('/{{code}}/car-search?{{prefill}}' compiles
86
+ * to two markers in ONE literal), so every marker inside it is interpolated.
87
+ */
88
+ function transformStringLiteral(text: string, accessor: string): string {
89
+ const quote = text[0];
90
+ const body = text.slice(1, -1);
91
+ const whole = RE_WHOLE_MARKER.exec(body);
92
+ if (whole) return `${accessor}.get('${decodeKey(whole[1])}')`;
93
+ return `\`${interpolateMarkers(escapeForTemplate(body, quote === '`'), accessor)}\``;
113
94
  }
114
95
 
115
- export function hasRuntimeDataMarkers(code: string): boolean {
116
- return MARKER_PATTERN.test(code);
96
+ /**
97
+ * /…$marker$…/flags → new RegExp(`…`, 'flags'). Same several-markers rule as above.
98
+ *
99
+ * Two regex-only twists on top of the string path:
100
+ * - The runtime VALUE is literal text (test-data holds data, not patterns), so every
101
+ * accessor is wrapped in the runtime escapeRegExp helper — a value containing
102
+ * `( [ $ . * +` must not throw a SyntaxError or silently widen the match. The helper
103
+ * lives in the synced specs/regex-escape.ts; the generator imports it when the
104
+ * transformed body references it. Templates that intend PATTERN semantics (attribute
105
+ * `pattern:`) emit a string container instead of a regex literal, so they skip this.
106
+ * - Marker-less SEGMENTS get their backslashes doubled for the template context: regex
107
+ * source text like a hand-written `\s*` (or escapeRegex output `\)`) means one thing
108
+ * in a /…/ literal but loses its backslash when the template cooks one escape level —
109
+ * the constructed RegExp would read `s*` / a bare `)`. Segments are escaped AFTER the
110
+ * markers are cut out, so an escapeRegex-spelled marker delimiter (`\$`) is never
111
+ * double-escaped into something the marker regex can no longer see.
112
+ */
113
+ function transformRegexLiteral(text: string, accessor: string): string {
114
+ // Flags never contain `/`, so the last slash always closes the pattern.
115
+ const close = text.lastIndexOf('/');
116
+ const body = text.slice(1, close);
117
+ const flags = text.slice(close + 1);
118
+ const flagStr = flags ? `, '${flags}'` : '';
119
+ const whole = RE_WHOLE_MARKER.exec(body);
120
+ if (whole) return `new RegExp(escapeRegExp(${accessor}.get('${decodeKey(whole[1])}'))${flagStr})`;
121
+ return `new RegExp(\`${interpolateMarkersInRegexBody(body, accessor)}\`${flagStr})`;
117
122
  }
118
123
 
119
124
  function decodeKey(encoded: string): string {
120
125
  return encoded.replace(/_D_/g, '.');
121
126
  }
122
127
 
123
- /** Replace every marker in a literal's body with a `${accessor.get('key')}` interpolation. */
128
+ /** Replace every marker in a body with a `${accessor.get('key')}` interpolation. */
124
129
  function interpolateMarkers(body: string, accessor: string): string {
125
130
  return body.replace(RE_MARKER_G, (_, enc) => `\${${accessor}.get('${decodeKey(enc)}')}`);
126
131
  }
132
+
133
+ /**
134
+ * Regex-body variant: markers are cut out FIRST, each accessor is wrapped in the
135
+ * runtime escapeRegExp helper, and only the marker-less segments between them are
136
+ * re-escaped for the template context (backslashes doubled, backtick/`${` neutralized).
137
+ */
138
+ function interpolateMarkersInRegexBody(body: string, accessor: string): string {
139
+ let out = '';
140
+ let last = 0;
141
+ for (const m of body.matchAll(RE_MARKER_G)) {
142
+ out += escapeRegexSegmentForTemplate(body.slice(last, m.index!)) +
143
+ `\${escapeRegExp(${accessor}.get('${decodeKey(m[1])}'))}`;
144
+ last = m.index! + m[0].length;
145
+ }
146
+ return out + escapeRegexSegmentForTemplate(body.slice(last));
147
+ }
148
+
149
+ /**
150
+ * Regex pattern text moving into a template literal: the template layer cooks one
151
+ * level of escapes BEFORE RegExp ever sees the string, so `\)` arrives as a bare `)`
152
+ * (SyntaxError: unmatched paren) and `\.` as a match-anything dot. Double the
153
+ * backslashes and neutralize the template's own metacharacters so the constructed
154
+ * RegExp source equals the original pattern text.
155
+ */
156
+ const escapeRegexSegmentForTemplate = (segment: string): string =>
157
+ segment.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
158
+
159
+ /**
160
+ * Both rewrites move a body into a backtick template, where a raw backtick or `${`
161
+ * would be read as syntax rather than text — a `'…'` selector value or a regex
162
+ * pattern is allowed to contain either (escapeQuotes/escapeRegex escape neither).
163
+ * Escape before interpolating so the `${…}` this module inserts is the only live one.
164
+ * An already-backtick container needs no pass: its body was valid template text.
165
+ *
166
+ * PRECONDITION: the body must already have its backslashes DOUBLED — which every caller
167
+ * satisfies, because every dynamic value reaches a literal through escapeQuotes or
168
+ * escapeRegex (template-engine.ts) and both double `\` before anything else. An unpaired
169
+ * backslash immediately before a backtick would make the pass below emit `\\\``, i.e. an
170
+ * escaped backslash followed by a LIVE backtick, ending the template early. If you
171
+ * hand-write a backslash into a .hbs template, keep it paired.
172
+ */
173
+ function escapeForTemplate(body: string, alreadyTemplate: boolean): string {
174
+ if (alreadyTemplate) return body;
175
+ return body.replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
176
+ }
@@ -229,6 +229,24 @@ export class SelectorResolver {
229
229
  * - Legacy format: "login:email_field" or "login.email_field" (with screen prefix)
230
230
  */
231
231
  resolveSelector(selectorRef: string, featureName?: string, elementType?: string, nth?: number): ResolvedSelector {
232
+ // A [Reference] is a selectors.yaml KEY, looked up verbatim (generateKey only lowercases and
233
+ // normalizes whitespace) — it is never interpolated. Embedding a {{var}} in it is therefore
234
+ // unsupported, and silence was the harmful part: with no matching entry the ref fell through
235
+ // to auto-infer and produced getByRole(..., { name: '{{var}}' }) — an accessible name that can
236
+ // never match — so the run failed far from the cause. Fail here instead, naming the two
237
+ // supported ways to target dynamic text. Browser-storage keys legitimately embed vars but are
238
+ // not selectors and never reach this method (see driver-ui storage-patterns).
239
+ const templateVarInRef = /\{\{\s*([^}]+?)\s*\}\}/.exec(selectorRef);
240
+ if (templateVarInRef) {
241
+ throw new Error(
242
+ `Selector reference "[${selectorRef}]" embeds a template variable ({{${templateVarInRef[1]}}}). ` +
243
+ `A [Reference] is a selectors.yaml key and is matched verbatim, not interpolated. ` +
244
+ `To target text that varies (locale, @cases column), keep the reference static and put the ` +
245
+ `variable in the entry instead — e.g. "confirm answer": { type: role, value: button, name: "{{${templateVarInRef[1]}}}" } ` +
246
+ `used as [Confirm Answer] button. To pick an element by its text inside a container, use ` +
247
+ `\`with {{${templateVarInRef[1]}}}\` (e.g. User click [Answer List] item with {{${templateVarInRef[1]}}}).`
248
+ );
249
+ }
232
250
  const contextName = featureName || this.featureName;
233
251
 
234
252
  // Check if it's legacy format (contains : or has dot with screen prefix)
@@ -590,7 +608,9 @@ export class SelectorResolver {
590
608
  });
591
609
  }
592
610
 
593
- // If type is 'locator', use locator field (or value as fallback) as the CSS locator directly
611
+ // If type is 'locator', use locator field (or value as fallback) as the CSS locator directly.
612
+ // `exact` travels along: a locator LIST filtered by a test-data value (hasText) matches by
613
+ // substring by default, and `exact: true` in the YAML opts into the anchored full-text match.
594
614
  if (type === 'locator') {
595
615
  const locatorValue = locator && locator.trim() ? locator : value;
596
616
  return withExtras({
@@ -598,6 +618,7 @@ export class SelectorResolver {
598
618
  value: locatorValue,
599
619
  locator: locatorValue,
600
620
  nth,
621
+ exact,
601
622
  });
602
623
  }
603
624
 
@@ -0,0 +1,22 @@
1
+ import { parse } from '@babel/parser';
2
+
3
+ /**
4
+ * Compiler self-check for emitted spec files.
5
+ *
6
+ * Every template and post-processor in the generator assembles TEXT, so a delimiter
7
+ * bug in any one of them produces a file Playwright cannot parse AT ALL: one
8
+ * SyntaxError, zero scenarios run, and nothing points at the compiler. That is how the
9
+ * runtime-data quote-pairing defect reached a user (3.2.23) — `sungen generate` printed
10
+ * success and the breakage only surfaced a step later, at `npx playwright test`.
11
+ *
12
+ * Parsing what we are about to write costs one parse per spec file and makes the
13
+ * compiler name its own bad output at the moment it makes it (SG-E022).
14
+ */
15
+ export function findSyntaxError(code: string): string | null {
16
+ try {
17
+ parse(code, { sourceType: 'module', plugins: ['typescript'] });
18
+ return null;
19
+ } catch (error) {
20
+ return error instanceof Error ? error.message : String(error);
21
+ }
22
+ }
@@ -3,13 +3,18 @@
3
3
  *
4
4
  * Parses `name(a={{x}},b="lit",c=3)` overrides into a map of JS expressions, e.g.
5
5
  * `{ a: "testData.raw('x')", b: "\"lit\"", c: "3" }`. Used by the DB and API capability drivers'
6
- * precondition codegen; lives in core so both drivers can share it. Gherkin tags carry no whitespace,
7
- * so values are single tokens flows thread a prior response via a whole-value ref, e.g.
6
+ * precondition codegen; lives in core so both drivers can share it. Gherkin tags carry no
7
+ * whitespace, but a QUOTED value can still embed `{{ref}}` next to static text
8
+ * (`user_email="pre-{{id}}-x"`) — those interpolate through literalExpr; shipping the braces
9
+ * verbatim would bind `{{...}}` text as a SQL parameter or POST it in a live request body.
10
+ * Flows thread a prior response via a whole-value ref, e.g.
8
11
  * `@api:get_profile(token={{login.body.token}})`, with the auth scheme declared in the catalog header.
9
- * A `{{ref}}` override binds into a typed sink (DB query param, JSON request body), so it resolves
10
- * via `testData.raw()` (native type, still interpolates embedded refs) rather than `get()`
12
+ * A bare `{{ref}}` override binds into a typed sink (DB query param, JSON request body), so it
13
+ * resolves via `testData.raw()` (native type, still interpolates embedded refs) rather than `get()`
11
14
  * (Gherkin-text stringify) — a numeric override otherwise reaches the DB/API as a JSON string.
12
15
  */
16
+ import { literalExpr } from '../generators/test-generator/patterns/expect-patterns';
17
+
13
18
  export function parseQueryOverrides(raw?: string): Record<string, string> {
14
19
  const out: Record<string, string> = {};
15
20
  if (!raw) return out;
@@ -22,7 +27,7 @@ export function parseQueryOverrides(raw?: string): Record<string, string> {
22
27
  const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
23
28
  const q = val.match(/^["'](.*)["']$/);
24
29
  if (v) out[key] = `testData.raw(${JSON.stringify(v[1])})`;
25
- else if (q) out[key] = JSON.stringify(q[1]);
30
+ else if (q) out[key] = literalExpr(q[1]);
26
31
  else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
27
32
  else out[key] = JSON.stringify(val);
28
33
  }
@@ -460,7 +460,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
460
460
  }
461
461
  }
462
462
  // #595 — a viewpoint item naming a browser-level gesture, answered with an in-app control.
463
- for (const g of browserGestureSubstitutions(viewpointPath, scenarios).slice(0, 4)) {
463
+ for (const g of browserGestureSubstitutions(viewpointPath, scenarios, platform).slice(0, 4)) {
464
464
  findings.push(`VIEWPOINT-GESTURE-SUBSTITUTED: the viewpoint asks for "${g.gesture}" ("${g.item.slice(0, 80)}…") but the suite exercises an in-app control instead. They are different mechanisms — an app button runs the screen's own re-hydrate logic, browser history does not, and a defect that only shows on history navigation lives in exactly that gap. The step vocabulary has no reload/back step (#593), so defer it with \`@manual\` naming the gesture rather than substituting silently.`);
465
465
  }
466
466
  // #595 — a flow whose requirement list is a hand restatement of the screen specs it
@@ -643,7 +643,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
643
643
  if (intent.capabilitySuggestions) {
644
644
  // The catalog unit id (`flows/<f>`, `api/<a>`) carries the unit KIND, which gates which
645
645
  // drivers can serve it at all.
646
- const plan = buildPlan(screenDir, screenName, catalogScreenName);
646
+ const plan = buildPlan(screenDir, screenName, catalogScreenName, platform);
647
647
  if (plan.recommendations.length) {
648
648
  const recs = plan.recommendations.map((r) => `\`sungen capability add ${r.driver}\` (automates ${r.count})`).join(' · ');
649
649
  findings.push(`CAPABILITY-SUGGESTION: ${plan.capabilityManual} @manual scenario(s) are capability-manual (a driver could automate them) — ${recs}. Recommend-only: nothing is installed automatically; the ${plan.judgmentManual} judgment-manual (M6/M8/M9) correctly stay manual.`);
@@ -651,8 +651,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
651
651
  // Named, not dropped: without this the scenarios read as "nothing could help", and the
652
652
  // operator's only clue was an install that dead-ends (#597).
653
653
  if (plan.unavailable.length) {
654
- const un = plan.unavailable.map((r) => `${r.driver} (would automate ${r.count})`).join(' · ');
655
- findings.push(`CAPABILITY-UNAVAILABLE: ${un} ${plan.unavailable.length === 1 ? 'that driver does' : 'those drivers do'} not support a ${plan.unavailable[0].unitKind} unit, so ${plan.unavailable.length === 1 ? 'it' : 'they'} cannot automate these scenarios here. Do NOT install: the scenarios stay @manual until the driver gains ${plan.unavailable[0].unitKind} support. Automating the same behaviour on the owning SCREEN unit is the available route today.`);
654
+ const un = plan.unavailable.map((r) => `${r.driver} (would automate ${r.count}) — ${r.why}`).join(' · ');
655
+ findings.push(`CAPABILITY-UNAVAILABLE: ${un}. Do NOT install: the scenarios stay @manual until the driver gains that support, and installing it buys an install, a dead end and a manual rollback. ${plan.unavailable.some((r) => /no .* support/.test(r.why)) ? 'On this platform, verify the same behaviour through `@api`/`@query` against the real dependency instead.' : 'Automating the same behaviour on the owning SCREEN unit is the available route today.'}`);
656
656
  }
657
657
  }
658
658
  // TQ-11b — automation-ready (pending capability): @requires:<cap> scenarios whose cap isn't enabled.
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { loadDriverCatalog, unitKindOf, driverServesUnit } from './capability';
11
+ import { loadDriverCatalog, driverMeta, unitKindOf, driverServesUnit, driverServesPlatform } from './capability';
12
12
  import { readTextFile } from './read-text';
13
13
  import { featureFilesFor } from './unit-paths';
14
14
 
@@ -197,11 +197,11 @@ export interface CapabilityPlan {
197
197
  * dropped: the scenarios stay manual for a reason the operator needs to know, and a silent
198
198
  * omission would read as "nothing could help here" (#597).
199
199
  */
200
- unavailable: { driver: string; reason: string; count: number; unitKind: string }[];
200
+ unavailable: { driver: string; reason: string; count: number; unitKind: string; why: string }[];
201
201
  keep: { code: string; count: number }[];
202
202
  }
203
203
 
204
- export function buildPlan(screenDir: string, screenName: string, unitId = screenName): CapabilityPlan {
204
+ export function buildPlan(screenDir: string, screenName: string, unitId = screenName, platform?: string): CapabilityPlan {
205
205
  const scenarios = featureFilesFor(screenDir, screenName).flatMap(parseScenarios);
206
206
  const catalog = loadDriverCatalog();
207
207
  // Recommending a driver that cannot serve this unit KIND costs an install, a dead end and a
@@ -241,14 +241,19 @@ export function buildPlan(screenDir: string, screenName: string, unitId = screen
241
241
  }
242
242
  }
243
243
 
244
- const servable = [...recByDriver.entries()].filter(([d]) => driverServesUnit(d, unitKind));
244
+ // Two axes, same guarantee: a driver must be able to serve this unit KIND and this PLATFORM.
245
+ const serves = (d: string): boolean => driverServesUnit(d, unitKind) && driverServesPlatform(d, platform);
246
+ const servable = [...recByDriver.entries()].filter(([d]) => serves(d));
245
247
  const unavailable = [...recByDriver.entries()]
246
- .filter(([d]) => !driverServesUnit(d, unitKind))
248
+ .filter(([d]) => !serves(d))
247
249
  .map(([driver, v]) => ({
248
250
  driver,
249
251
  reason: v.reason,
250
252
  count: v.scenarios.length,
251
253
  unitKind,
254
+ why: !driverServesUnit(driver, unitKind)
255
+ ? `does not support a ${unitKind} unit`
256
+ : `has no ${platform} support (its step patterns are ${(driverMeta(driver)?.platforms ?? []).join('/')}-only)`,
252
257
  }))
253
258
  .sort((a, b) => b.count - a.count);
254
259
  const recommendations = servable
@@ -81,6 +81,15 @@ export interface DriverMeta {
81
81
  * driver on a flow: install, dead end, manual rollback (#597).
82
82
  */
83
83
  units?: Array<'screen' | 'flow' | 'api'>;
84
+ /**
85
+ * Platforms this driver can serve. Absent = all.
86
+ *
87
+ * The same guarantee as `units`, on the other axis. Step patterns already gate themselves
88
+ * (`platforms: ['web']` → SG-W020 at compile time), but the PLANNER did not know, so a mobile
89
+ * project was still told to install a web-only driver — install, dead end, manual rollback,
90
+ * exactly as in #597 (#626).
91
+ */
92
+ platforms?: string[];
84
93
  }
85
94
 
86
95
  /** The unit kind of a catalog unit id: `flows/<f>` → flow, `api/<a>` → api, else screen. */
@@ -97,6 +106,13 @@ export function driverServesUnit(driverId: string, kind: 'screen' | 'flow' | 'ap
97
106
  return !units || units.includes(kind);
98
107
  }
99
108
 
109
+ /** Can this driver serve that platform? No declared platform on either side → assume yes. */
110
+ export function driverServesPlatform(driverId: string, platform?: string): boolean {
111
+ const platforms = driverMeta(driverId)?.platforms;
112
+ if (!platforms || !platform) return true;
113
+ return platforms.includes(platform.toLowerCase());
114
+ }
115
+
100
116
  export function capabilitiesPath(cwd: string): string {
101
117
  return path.join(cwd, 'qa', 'capabilities.yaml');
102
118
  }
@@ -66,6 +66,11 @@ drivers:
66
66
  # (mockCatalogPath is hardcoded to that shape), so it cannot serve a flow. Recommending
67
67
  # it for one sent an operator through an install, a dead end and a manual rollback (#597).
68
68
  units: [screen]
69
+ # `platforms:` is the same guarantee on the other axis. Absent → every platform.
70
+ # `mock.install` is Playwright route interception (page.route) and its step patterns already
71
+ # declare `platforms: ['web']`, so on mobile they compile to SG-W020. The PLANNER did not know
72
+ # that, so a mobile project was still told to install it — the #597 dead end, one axis over.
73
+ platforms: [web]
69
74
  mail-file:
70
75
  kind: capability
71
76
  package: "@sungen/driver-mail-file"
@@ -230,7 +230,11 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
230
230
  // assert persisted state — without this they'd be scored `shallow`, penalizing
231
231
  // exactly the checks the review encourages. `tab order` is a STRUCTURAL
232
232
  // assertion (a data table of focus stops) with the same no-{{ref}} property.
233
- if (/\{\{|contains|match data|toHaveText|in (local|session) storage|tab order/i.test(step.text)) hasData = true;
233
+ // `matches "<format>"` belongs here for the same reason: it carries no {{ref}} because
234
+ // the contract IS the shape ("Format: yyyy/mm/dd ~ yyyy/mm/dd"), and it is a stricter
235
+ // check than the value assert it replaces — scoring it shallow would penalize the
236
+ // stronger assertion.
237
+ if (/\{\{|contains|match data|matches\s+"|toHaveText|in (local|session) storage|tab order/i.test(step.text)) hasData = true;
234
238
  }
235
239
  }
236
240