@sun-asterisk/sungen 3.2.31 → 3.2.33

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 (363) hide show
  1. package/dist/cli/commands/audit.d.ts +1 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +13 -2
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/capability.d.ts.map +1 -1
  6. package/dist/cli/commands/capability.js +46 -17
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/delivery.d.ts.map +1 -1
  9. package/dist/cli/commands/delivery.js +39 -18
  10. package/dist/cli/commands/delivery.js.map +1 -1
  11. package/dist/cli/commands/generate.d.ts.map +1 -1
  12. package/dist/cli/commands/generate.js +14 -7
  13. package/dist/cli/commands/generate.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2 -0
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  18. package/dist/cli/commands/results-merge.js +2 -12
  19. package/dist/cli/commands/results-merge.js.map +1 -1
  20. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  21. package/dist/cli/commands/results-sanitize.js +2 -13
  22. package/dist/cli/commands/results-sanitize.js.map +1 -1
  23. package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
  24. package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
  25. package/dist/cli/commands/setup-mobile-env.js +65 -0
  26. package/dist/cli/commands/setup-mobile-env.js.map +1 -0
  27. package/dist/cli/index.js +2 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  30. package/dist/dashboard/snapshot-builder.js +6 -10
  31. package/dist/dashboard/snapshot-builder.js.map +1 -1
  32. package/dist/exporters/feature-parser.d.ts +21 -0
  33. package/dist/exporters/feature-parser.d.ts.map +1 -1
  34. package/dist/exporters/feature-parser.js +32 -1
  35. package/dist/exporters/feature-parser.js.map +1 -1
  36. package/dist/exporters/locale-variants.d.ts +7 -1
  37. package/dist/exporters/locale-variants.d.ts.map +1 -1
  38. package/dist/exporters/locale-variants.js +23 -16
  39. package/dist/exporters/locale-variants.js.map +1 -1
  40. package/dist/exporters/matrix/build.d.ts.map +1 -1
  41. package/dist/exporters/matrix/build.js +13 -7
  42. package/dist/exporters/matrix/build.js.map +1 -1
  43. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  44. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  45. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  46. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  47. package/dist/exporters/matrix/export.d.ts +2 -0
  48. package/dist/exporters/matrix/export.d.ts.map +1 -1
  49. package/dist/exporters/matrix/export.js +7 -1
  50. package/dist/exporters/matrix/export.js.map +1 -1
  51. package/dist/exporters/matrix/wording.d.ts +7 -0
  52. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  53. package/dist/exporters/matrix/wording.js +27 -1
  54. package/dist/exporters/matrix/wording.js.map +1 -1
  55. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  56. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  57. package/dist/exporters/xlsx-exporter.js +7 -2
  58. package/dist/exporters/xlsx-exporter.js.map +1 -1
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  60. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  61. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  62. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  63. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  72. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  73. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  74. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  75. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  76. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  77. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  78. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  79. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  83. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  84. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  85. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  86. package/dist/generators/test-generator/code-generator.js +122 -15
  87. package/dist/generators/test-generator/code-generator.js.map +1 -1
  88. package/dist/generators/test-generator/diagnostics.d.ts +70 -4
  89. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  90. package/dist/generators/test-generator/diagnostics.js +69 -3
  91. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  92. package/dist/generators/test-generator/patterns/index.d.ts +13 -3
  93. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  94. package/dist/generators/test-generator/patterns/index.js +16 -10
  95. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  96. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  97. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  98. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  99. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  100. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  101. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  102. package/dist/generators/test-generator/step-mapper.d.ts +81 -0
  103. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  104. package/dist/generators/test-generator/step-mapper.js +187 -13
  105. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +18 -0
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  114. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  115. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  116. package/dist/generators/test-generator/utils/selector-resolver.d.ts +84 -4
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  118. package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
  119. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  120. package/dist/harness/audit.d.ts.map +1 -1
  121. package/dist/harness/audit.js +35 -8
  122. package/dist/harness/audit.js.map +1 -1
  123. package/dist/harness/capability.d.ts +7 -1
  124. package/dist/harness/capability.d.ts.map +1 -1
  125. package/dist/harness/capability.js +15 -12
  126. package/dist/harness/capability.js.map +1 -1
  127. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  128. package/dist/harness/config-file.d.ts +17 -0
  129. package/dist/harness/config-file.d.ts.map +1 -0
  130. package/dist/harness/config-file.js +100 -0
  131. package/dist/harness/config-file.js.map +1 -0
  132. package/dist/harness/depth-lint.d.ts.map +1 -1
  133. package/dist/harness/depth-lint.js +2 -1
  134. package/dist/harness/depth-lint.js.map +1 -1
  135. package/dist/harness/flow-check.d.ts.map +1 -1
  136. package/dist/harness/flow-check.js +6 -2
  137. package/dist/harness/flow-check.js.map +1 -1
  138. package/dist/harness/keyword-match.d.ts +59 -0
  139. package/dist/harness/keyword-match.d.ts.map +1 -0
  140. package/dist/harness/keyword-match.js +86 -0
  141. package/dist/harness/keyword-match.js.map +1 -0
  142. package/dist/harness/next-step.d.ts.map +1 -1
  143. package/dist/harness/next-step.js +2 -1
  144. package/dist/harness/next-step.js.map +1 -1
  145. package/dist/harness/parse.d.ts.map +1 -1
  146. package/dist/harness/parse.js +42 -7
  147. package/dist/harness/parse.js.map +1 -1
  148. package/dist/harness/perf.d.ts +7 -1
  149. package/dist/harness/perf.d.ts.map +1 -1
  150. package/dist/harness/perf.js +13 -10
  151. package/dist/harness/perf.js.map +1 -1
  152. package/dist/harness/quality-gates.d.ts +9 -2
  153. package/dist/harness/quality-gates.d.ts.map +1 -1
  154. package/dist/harness/quality-gates.js +16 -4
  155. package/dist/harness/quality-gates.js.map +1 -1
  156. package/dist/harness/query-catalog.js +0 -0
  157. package/dist/harness/query-catalog.js.map +1 -1
  158. package/dist/harness/read-text.d.ts +6 -0
  159. package/dist/harness/read-text.d.ts.map +1 -1
  160. package/dist/harness/read-text.js +7 -1
  161. package/dist/harness/read-text.js.map +1 -1
  162. package/dist/harness/secret-scan.d.ts +27 -1
  163. package/dist/harness/secret-scan.d.ts.map +1 -1
  164. package/dist/harness/secret-scan.js +57 -4
  165. package/dist/harness/secret-scan.js.map +1 -1
  166. package/dist/harness/sensors.d.ts +12 -0
  167. package/dist/harness/sensors.d.ts.map +1 -1
  168. package/dist/harness/sensors.js +41 -18
  169. package/dist/harness/sensors.js.map +1 -1
  170. package/dist/harness/spec-branches.d.ts +7 -20
  171. package/dist/harness/spec-branches.d.ts.map +1 -1
  172. package/dist/harness/spec-branches.js +85 -13
  173. package/dist/harness/spec-branches.js.map +1 -1
  174. package/dist/harness/spec-coverage.d.ts +16 -0
  175. package/dist/harness/spec-coverage.d.ts.map +1 -1
  176. package/dist/harness/spec-coverage.js +131 -12
  177. package/dist/harness/spec-coverage.js.map +1 -1
  178. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  179. package/dist/harness/viewpoint-ledger.js +2 -1
  180. package/dist/harness/viewpoint-ledger.js.map +1 -1
  181. package/dist/harness/write-atomic.d.ts +2 -0
  182. package/dist/harness/write-atomic.d.ts.map +1 -0
  183. package/dist/harness/write-atomic.js +62 -0
  184. package/dist/harness/write-atomic.js.map +1 -0
  185. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  186. package/dist/orchestrator/ai-skills/writer.js +15 -7
  187. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  188. package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
  189. package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
  190. package/dist/orchestrator/appium-environment-setup.js +157 -0
  191. package/dist/orchestrator/appium-environment-setup.js.map +1 -0
  192. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  193. package/dist/orchestrator/assets-drift.js +11 -6
  194. package/dist/orchestrator/assets-drift.js.map +1 -1
  195. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
  196. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  197. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  198. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  199. package/dist/orchestrator/project-deps-sync.js +22 -1
  200. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  201. package/dist/orchestrator/project-initializer.d.ts +9 -0
  202. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  203. package/dist/orchestrator/project-initializer.js +63 -7
  204. package/dist/orchestrator/project-initializer.js.map +1 -1
  205. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  206. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  207. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  208. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  209. package/dist/orchestrator/security-overrides.d.ts +51 -0
  210. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  211. package/dist/orchestrator/security-overrides.js +165 -0
  212. package/dist/orchestrator/security-overrides.js.map +1 -0
  213. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  214. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  215. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  216. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  217. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  218. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  219. package/dist/orchestrator/templates/appium-environment/README.md +83 -0
  220. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  221. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  222. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  223. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  224. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  225. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  226. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  227. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  228. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  229. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  230. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  231. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  232. package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  233. package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  234. package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  235. package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
  236. package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
  237. package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
  238. package/dist/orchestrator/templates/specs-checked-state.js +26 -3
  239. package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
  240. package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
  241. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  242. package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
  243. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
  244. package/dist/orchestrator/templates/specs-mock.js +10 -3
  245. package/dist/orchestrator/templates/specs-mock.js.map +1 -1
  246. package/dist/orchestrator/templates/specs-mock.ts +12 -4
  247. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  248. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  249. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  250. package/dist/orchestrator/templates/specs-url-assert.js +14 -2
  251. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  252. package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
  253. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  254. package/package.json +3 -3
  255. package/src/cli/commands/audit.ts +12 -3
  256. package/src/cli/commands/capability.ts +63 -19
  257. package/src/cli/commands/delivery.ts +38 -16
  258. package/src/cli/commands/generate.ts +15 -7
  259. package/src/cli/commands/init.ts +3 -0
  260. package/src/cli/commands/results-merge.ts +2 -8
  261. package/src/cli/commands/results-sanitize.ts +2 -9
  262. package/src/cli/commands/setup-mobile-env.ts +64 -0
  263. package/src/cli/index.ts +2 -0
  264. package/src/dashboard/snapshot-builder.ts +6 -8
  265. package/src/exporters/feature-parser.ts +32 -2
  266. package/src/exporters/locale-variants.ts +23 -17
  267. package/src/exporters/matrix/build.ts +14 -8
  268. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  269. package/src/exporters/matrix/export.ts +10 -4
  270. package/src/exporters/matrix/wording.ts +26 -1
  271. package/src/exporters/xlsx-exporter.ts +7 -2
  272. package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
  273. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  274. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  275. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  276. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  277. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  278. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  279. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  280. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  281. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  282. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  283. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  284. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  285. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  286. package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  287. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  288. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  289. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  290. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  291. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  292. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  296. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  297. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  298. package/src/generators/test-generator/code-generator.ts +119 -19
  299. package/src/generators/test-generator/diagnostics.ts +70 -4
  300. package/src/generators/test-generator/patterns/index.ts +16 -9
  301. package/src/generators/test-generator/patterns/types.ts +15 -0
  302. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  303. package/src/generators/test-generator/step-mapper.ts +196 -13
  304. package/src/generators/test-generator/template-engine.ts +18 -0
  305. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  306. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  307. package/src/generators/test-generator/utils/selector-resolver.ts +256 -85
  308. package/src/harness/audit.ts +41 -12
  309. package/src/harness/capability.ts +23 -12
  310. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  311. package/src/harness/config-file.ts +66 -0
  312. package/src/harness/depth-lint.ts +2 -1
  313. package/src/harness/flow-check.ts +6 -2
  314. package/src/harness/keyword-match.ts +85 -0
  315. package/src/harness/next-step.ts +2 -1
  316. package/src/harness/parse.ts +42 -7
  317. package/src/harness/perf.ts +15 -7
  318. package/src/harness/quality-gates.ts +16 -4
  319. package/src/harness/query-catalog.ts +0 -0
  320. package/src/harness/read-text.ts +7 -1
  321. package/src/harness/secret-scan.ts +60 -4
  322. package/src/harness/sensors.ts +50 -19
  323. package/src/harness/spec-branches.ts +72 -14
  324. package/src/harness/spec-coverage.ts +133 -13
  325. package/src/harness/viewpoint-ledger.ts +2 -1
  326. package/src/harness/write-atomic.ts +22 -0
  327. package/src/orchestrator/ai-skills/writer.ts +18 -3
  328. package/src/orchestrator/appium-environment-setup.ts +127 -0
  329. package/src/orchestrator/assets-drift.ts +10 -5
  330. package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
  331. package/src/orchestrator/project-deps-sync.ts +23 -1
  332. package/src/orchestrator/project-initializer.ts +69 -7
  333. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  334. package/src/orchestrator/security-overrides.ts +149 -0
  335. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  336. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  337. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  338. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  339. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  340. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  341. package/src/orchestrator/templates/appium-environment/README.md +83 -0
  342. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  343. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  344. package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  345. package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  346. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  347. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  348. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  349. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  350. package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  351. package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  352. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  353. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  354. package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  355. package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  356. package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  357. package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
  358. package/src/orchestrator/templates/specs-checked-state.ts +31 -6
  359. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  360. package/src/orchestrator/templates/specs-mock.ts +12 -4
  361. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  362. package/src/orchestrator/templates/specs-url-assert.ts +24 -2
  363. package/src/orchestrator/templates/wdio.conf.ts +24 -1
@@ -16,6 +16,9 @@ import {
16
16
  writeMobileCapabilities,
17
17
  installMobileDriver,
18
18
  } from './mobile-runtime-scaffolder';
19
+ import { emitAppiumEnvironment, runAppiumEnvironmentSetup, appiumEnvironmentRerunCommand } from './appium-environment-setup';
20
+ import { ensureSecurityOverrides, reportSecurityOverrides } from './security-overrides';
21
+ import { readTextFile } from '../harness/read-text';
19
22
 
20
23
  export { MobileInitOptions };
21
24
 
@@ -93,13 +96,21 @@ export class ProjectInitializer {
93
96
  private cwd: string;
94
97
  private createdItems: string[] = [];
95
98
  private skippedItems: string[] = [];
99
+ /**
100
+ * Actions the user asked for that could NOT be performed. Kept apart from `skippedItems`:
101
+ * that list renders under "Already exists", which is a statement of fact about the project —
102
+ * printing "package.json scripts (invalid JSON)" under it tells the user the opposite of what
103
+ * happened. An action that was not done needs its own heading, or the init summary reads as a
104
+ * clean success while `npm test` is missing.
105
+ */
106
+ private notDoneItems: string[] = [];
96
107
  /** set by createCodexMCPConfig when the project isn't Codex-trusted (config would never load) */
97
108
  private codexTrustWarning = false;
98
109
  private agents: AdapterKey[];
99
110
  private force: boolean;
100
111
  private mobile: MobileInitOptions;
101
112
 
102
- constructor(private baseUrl: string, opts: { agents?: AdapterKey[]; force?: boolean; enabled?: boolean; mobileOnly?: boolean; appPackage?: string; appActivity?: string; appApk?: string; androidUdid?: string; iosBundleId?: string; iosDevice?: string; iosVersion?: string } = {}) {
113
+ constructor(private baseUrl: string, opts: { agents?: AdapterKey[]; force?: boolean; enabled?: boolean; mobileOnly?: boolean; appPackage?: string; appActivity?: string; appApk?: string; androidUdid?: string; iosBundleId?: string; iosDevice?: string; iosVersion?: string; skipEnvSetup?: boolean } = {}) {
103
114
  this.baseCwd = process.cwd();
104
115
  this.cwd = this.baseCwd;
105
116
  this.agents = opts.agents && opts.agents.length ? opts.agents : (['claude'] as AdapterKey[]);
@@ -114,6 +125,8 @@ export class ProjectInitializer {
114
125
  iosBundleId: opts.iosBundleId,
115
126
  iosDevice: opts.iosDevice,
116
127
  iosVersion: opts.iosVersion,
128
+ // `--skip-env-setup`: emit appium-environment/ but do not run it (CI, or the manual path).
129
+ skipEnvSetup: opts.skipEnvSetup ?? false,
117
130
  };
118
131
  }
119
132
 
@@ -132,6 +145,7 @@ export class ProjectInitializer {
132
145
  : this.baseCwd;
133
146
  this.createdItems = [];
134
147
  this.skippedItems = [];
148
+ this.notDoneItems = [];
135
149
 
136
150
  console.log(`🚀 Initializing Sungen ${this.mobileOnly ? 'mobile-only ' : ''}project...\n`);
137
151
 
@@ -436,9 +450,17 @@ export class ProjectInitializer {
436
450
 
437
451
  if (existed) {
438
452
  try {
439
- config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
440
- } catch {
441
- this.skippedItems.push(`${displayName} (invalid JSON, skipped)`);
453
+ // via readTextFile: a UTF-8 BOM is the one thing `JSON.parse` rejects that every other tool
454
+ // reading this file ignores, and it is invisible in an editor. A config the rest of the
455
+ // toolchain loads fine must not cost the user their MCP servers here.
456
+ config = JSON.parse(readTextFile(filePath));
457
+ } catch (e) {
458
+ // Anything still unparseable is genuinely broken. Say WHERE — "is not valid JSON" alone
459
+ // leaves the user to find a missing comma in a file they did not know was the problem.
460
+ this.notDoneItems.push(
461
+ `${displayName} (${(e as Error).message.split('\n')[0]}) — MCP servers not registered; `
462
+ + `fix the JSON and re-run \`sungen init\``,
463
+ );
442
464
  return;
443
465
  }
444
466
  }
@@ -484,6 +506,13 @@ export class ProjectInitializer {
484
506
  });
485
507
  }
486
508
 
509
+ if (this.notDoneItems.length > 0) {
510
+ console.log('\n⚠ Not done — needs your attention:');
511
+ this.notDoneItems.forEach(item => {
512
+ console.log(` ✗ ${item}`);
513
+ });
514
+ }
515
+
487
516
  if (this.codexTrustWarning) {
488
517
  console.log('\n⚠ Codex: this project is not trusted yet, so .codex/config.toml (the MCP browser');
489
518
  console.log(' config for run-test) will be SILENTLY IGNORED by Codex. To fix, either:');
@@ -500,6 +529,8 @@ export class ProjectInitializer {
500
529
  console.log(` ${stepIndex++}. cd ${projectName}`);
501
530
  }
502
531
  if (this.mobileOnly) {
532
+ console.log(` ${stepIndex++}. Open a NEW terminal if the environment setup wrote your shell profile (PATH / JAVA_HOME / ANDROID_HOME)`);
533
+ console.log(` ${stepIndex++}. Environment not ready, or a new machine? Re-run: ${appiumEnvironmentRerunCommand()} (idempotent)`);
503
534
  console.log(` ${stepIndex++}. Fill in .env.appium with your device/app details (see .env.appium.example)`);
504
535
  console.log(` ${stepIndex++}. Create your first screen: sungen add --screen <name>`);
505
536
  console.log(` ${stepIndex++}. Use AI (Copilot/Claude) to generate Gherkin + selectors from the live app`);
@@ -642,8 +673,17 @@ export class ProjectInitializer {
642
673
  this.createdItems.push(...result.created);
643
674
  this.skippedItems.push(...result.skipped);
644
675
 
676
+ // appium-environment/ provisioning bundle — emitted BEFORE the driver install so it lands even when
677
+ // npm is offline, run AFTER it so its Appium step finds the project-local CLI. One summary line, not 17.
678
+ const env = emitAppiumEnvironment(this.cwd);
679
+ if (env.created.length > 0) this.createdItems.push(`appium-environment/ (${env.created.length} files)`);
680
+ else this.skippedItems.push('appium-environment/');
681
+
645
682
  // Best-effort driver install — non-fatal so offline init still scaffolds.
646
683
  installMobileDriver(this.cwd);
684
+
685
+ // Provision JDK / Android SDK / Xcode CLT / env vars / platform drivers — non-fatal by contract.
686
+ runAppiumEnvironmentSetup(this.cwd, { skip: this.mobile.skipEnvSetup });
647
687
  }
648
688
 
649
689
  /**
@@ -671,6 +711,13 @@ export class ProjectInitializer {
671
711
  // Ensure standard scripts exist in package.json
672
712
  this.ensurePackageScripts(packageJsonPath);
673
713
 
714
+ // Pin the transitive packages that would otherwise make `npm audit` red here. This MUST happen
715
+ // before the install below: npm reads `overrides` at resolution time, so a lockfile written
716
+ // first would already hold the vulnerable versions. See security-overrides.ts for why the pins
717
+ // cannot simply live in sungen's own manifest.
718
+ const overridePlan = ensureSecurityOverrides(packageJsonPath);
719
+ reportSecurityOverrides(overridePlan);
720
+
674
721
  // Mobile-only drops @playwright/test (no web runner needed). `ajv`/`ajv-formats` back the API
675
722
  // Driver's `expect {{name.body}} matches schema [Ref]` (loaded lazily; harmless if unused).
676
723
  // fast-xml-parser: the API driver's XML request/response support lazy-requires it
@@ -682,7 +729,7 @@ export class ProjectInitializer {
682
729
 
683
730
  let missingDeps: string[] = requiredDeps;
684
731
  try {
685
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
732
+ const packageJson = JSON.parse(readTextFile(packageJsonPath));
686
733
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
687
734
  missingDeps = requiredDeps.filter(d => !deps[d]);
688
735
  } catch {
@@ -691,6 +738,12 @@ export class ProjectInitializer {
691
738
 
692
739
  if (missingDeps.length === 0) {
693
740
  console.log('✓ All dependencies already installed\n');
741
+ // Nothing to install means nothing re-resolves, so overrides just written would sit in
742
+ // package.json while the lockfile keeps the vulnerable tree. Re-resolve once so they land.
743
+ if (overridePlan?.add.length) {
744
+ console.log('📦 Re-resolving the tree so the new security overrides take effect...\n');
745
+ execSync('npm install', execOpts);
746
+ }
694
747
  } else {
695
748
  console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
696
749
  execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
@@ -711,8 +764,17 @@ export class ProjectInitializer {
711
764
  private ensurePackageScripts(packageJsonPath: string): void {
712
765
  let packageJson: Record<string, any>;
713
766
  try {
714
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
715
- } catch {
767
+ // via readTextFile: a package.json carrying a UTF-8 BOM is one npm reads without complaint,
768
+ // so it must not be treated as unparseable here. That was the trigger that actually reached
769
+ // this catch — genuinely broken JSON aborts `npm install` a few lines below instead.
770
+ packageJson = JSON.parse(readTextFile(packageJsonPath));
771
+ } catch (e) {
772
+ // Returning here used to add nothing and say nothing, so init reported success and the user
773
+ // met `npm error Missing script: "test"` later with nothing pointing back to this run.
774
+ this.notDoneItems.push(
775
+ `package.json scripts (${(e as Error).message.split('\n')[0]}) — `
776
+ + `fix the JSON and re-run \`sungen init\`, or add the scripts by hand`,
777
+ );
716
778
  return;
717
779
  }
718
780
 
@@ -0,0 +1,69 @@
1
+ /**
2
+ * npm `overrides` a sungen project needs in order to pass `npm audit`.
3
+ *
4
+ * Why this lives in the CLI instead of in `packages/core/package.json`: npm honours `overrides`
5
+ * ONLY in the root `package.json` of the project doing the installing. A block declared by a
6
+ * DEPENDENCY is inert — it is not read, not merged, and not carried in the published tarball. So
7
+ * the monorepo root can (and does) pin these for itself while every consumer of the published
8
+ * packages still resolves the vulnerable versions. Repo green, install red, and nobody working in
9
+ * the repo ever sees what a consumer sees. Issue #764.
10
+ *
11
+ * Sungen already writes to the consuming project's `package.json` (standard scripts on `init`,
12
+ * family version pinning on `update`), so the overrides go in on the same two passes. That makes
13
+ * the fix reachable the ordinary way: a new project is clean from its first install, an existing
14
+ * one becomes clean on the next `sungen update`.
15
+ *
16
+ * Every entry below is here because NOTHING ELSE CLEARS IT — the offending package is either at its
17
+ * latest published version already, or is hard-pinned by an unmaintained parent. Bumping sungen's
18
+ * own declared ranges does not move any of them: `@sungen/driver-mobile` is already on the newest
19
+ * appium (3.x) and webdriverio (9.x), and npm's own `fix available` for that chain is a DOWNGRADE
20
+ * to appium 1.22.3 / webdriverio 8.14.6, not a fix.
21
+ *
22
+ * Measured on a clean-room install of the published 3.2.32 family (2026-09-23): 25 findings
23
+ * (12 moderate, 13 high) without this block, 0 with it.
24
+ */
25
+
26
+ export interface SecurityOverride {
27
+ /** Package to pin, as it appears in `overrides`. */
28
+ pkg: string;
29
+ /** Range to pin it to — the lowest published version clear of the advisory. */
30
+ range: string;
31
+ /** Advisory + the path that drags the vulnerable copy in, so a reader can re-check the entry. */
32
+ why: string;
33
+ }
34
+
35
+ /**
36
+ * The pinned set. Keep it MINIMAL: an entry earns its place only while a clean install is red
37
+ * without it. When an upstream finally ships a fix inside its own declared range, delete the entry
38
+ * rather than raising it — a stale override silently holds a whole subtree back.
39
+ */
40
+ export const SECURITY_OVERRIDES: SecurityOverride[] = [
41
+ {
42
+ pkg: 'uuid',
43
+ range: '^11.1.1',
44
+ // exceljs 4.4.0 IS the latest (published 2024-12-20) and hard-declares "uuid": "^8.3.0".
45
+ // There is no newer exceljs to bump to, which is why npm reports fixAvailable: false.
46
+ why: 'GHSA-w5hq-g745-h8pq (moderate) — uuid@8.3.2 via exceljs@4.4.0, which is unmaintained and pins ^8.3.0',
47
+ },
48
+ {
49
+ pkg: 'morgan',
50
+ range: '^1.12.1',
51
+ // @appium/base-driver pins the exact version "1.11.0", so no resolution reaches 1.12.x on its own.
52
+ why: 'GHSA-jxfw-x594-9x9m (moderate) — morgan@1.11.0 exact-pinned by @appium/base-driver',
53
+ },
54
+ {
55
+ pkg: 'serialize-javascript',
56
+ range: '^7.1.2',
57
+ why: 'GHSA-5c6j-r48x-rmvq (high) + GHSA-qj8w-gfj5-8c6v (moderate) — serialize-javascript@6.0.2 via mocha (^6.0.2)',
58
+ },
59
+ {
60
+ pkg: '@puppeteer/browsers',
61
+ range: '^3.2.3',
62
+ // The vulnerable package is extract-zip, whose latest (2.0.1) IS the flagged version — there is
63
+ // nothing to pin it to. @puppeteer/browsers 3.x dropped extract-zip for modern-tar, so the pin
64
+ // goes on the parent instead. @wdio/utils asks for ^2.2.0 but imports only install, canDownload,
65
+ // resolveBuildId, detectBrowserPlatform, Browser, ChromeReleaseChannel and computeExecutablePath
66
+ // — all seven still exported by 3.2.3, verified against @wdio/utils@9.32.0.
67
+ why: 'GHSA-jmr9-qjv8-65gv + GHSA-7pqw-9j4j-h8q3 (high) — extract-zip@2.0.1 via @puppeteer/browsers@2.x ← @wdio/utils; extract-zip has no fixed release, 3.x replaced it with modern-tar',
68
+ },
69
+ ];
@@ -0,0 +1,149 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { SECURITY_OVERRIDES, SecurityOverride } from './security-overrides-catalog';
4
+
5
+ /**
6
+ * Merge the security-override catalog into a consuming project's `package.json`.
7
+ *
8
+ * The catalog itself — which packages, which ranges, and why each one cannot be cleared any other
9
+ * way — lives in `security-overrides-catalog.ts`. That is the file to edit when an advisory moves;
10
+ * this one holds only the merge, the write and the reporting, which do not change when it does.
11
+ */
12
+
13
+ export { SECURITY_OVERRIDES } from './security-overrides-catalog';
14
+ export type { SecurityOverride } from './security-overrides-catalog';
15
+
16
+ export interface OverridePlan {
17
+ /** Entries missing from the project, to be written. */
18
+ add: SecurityOverride[];
19
+ /** Entries the project already pins itself — left untouched, reported so the user can judge. */
20
+ existing: Array<{ pkg: string; declared: string; wanted: string }>;
21
+ /** Set when the pins were needed but the write failed — `add` is then empty, nothing was applied. */
22
+ writeFailed?: boolean;
23
+ /** Lockfile of a package manager that ignores `overrides`, when the project has one. */
24
+ foreignLockfile?: 'yarn' | 'pnpm';
25
+ }
26
+
27
+ /**
28
+ * Decide what a project's `overrides` block is missing. Pure — no I/O, so the tests and `--dry-run`
29
+ * can plan without touching anything.
30
+ *
31
+ * A pin the project already declares is NEVER rewritten, even when it differs from ours. Someone who
32
+ * typed a range into their own `package.json` had a reason (a private mirror, a pinned fork, a
33
+ * downgrade forced by another tool), and silently overwriting it would be the more surprising bug.
34
+ * It is reported instead, so a still-vulnerable pin is visible rather than swallowed.
35
+ */
36
+ export function planSecurityOverrides(pkgJson: Record<string, any>): OverridePlan {
37
+ const current: Record<string, unknown> =
38
+ pkgJson.overrides && typeof pkgJson.overrides === 'object' ? pkgJson.overrides : {};
39
+
40
+ const add: SecurityOverride[] = [];
41
+ const existing: OverridePlan['existing'] = [];
42
+
43
+ for (const entry of SECURITY_OVERRIDES) {
44
+ const declared = current[entry.pkg];
45
+ if (typeof declared === 'string') {
46
+ if (declared !== entry.range) existing.push({ pkg: entry.pkg, declared, wanted: entry.range });
47
+ continue;
48
+ }
49
+ // A nested override object ({ "uuid": { ".": "^11" } }) is a deliberate, more specific shape —
50
+ // treat it as the project's own decision and leave it alone.
51
+ if (declared !== undefined) {
52
+ existing.push({ pkg: entry.pkg, declared: JSON.stringify(declared), wanted: entry.range });
53
+ continue;
54
+ }
55
+ add.push(entry);
56
+ }
57
+
58
+ return { add, existing };
59
+ }
60
+
61
+ /**
62
+ * Write the missing overrides into a project's `package.json`.
63
+ *
64
+ * Best-effort by design, exactly like the rest of the project-file touching in `init` / `update`: an
65
+ * unreadable or malformed `package.json` means we skip silently rather than fail the command the
66
+ * user actually ran. The audit gate is not worth losing a scaffold over.
67
+ *
68
+ * Must run BEFORE the project's `npm install`, or the first resolution pass writes a lockfile that
69
+ * already pins the vulnerable versions.
70
+ *
71
+ * @param dryRun plan only — report what would change and write nothing
72
+ * @returns the plan that was applied, or null when there was no `package.json` to apply it to
73
+ */
74
+ export function ensureSecurityOverrides(packageJsonPath: string, dryRun = false): OverridePlan | null {
75
+ let pkgJson: Record<string, any>;
76
+ try {
77
+ pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
78
+ } catch {
79
+ return null;
80
+ }
81
+
82
+ const plan = planSecurityOverrides(pkgJson);
83
+ plan.foreignLockfile = detectForeignLockfile(path.dirname(packageJsonPath));
84
+
85
+ if (plan.add.length && !dryRun) {
86
+ const overrides: Record<string, unknown> =
87
+ pkgJson.overrides && typeof pkgJson.overrides === 'object' ? pkgJson.overrides : {};
88
+ for (const entry of plan.add) overrides[entry.pkg] = entry.range;
89
+ pkgJson.overrides = overrides;
90
+ try {
91
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
92
+ } catch {
93
+ // Read-only checkout, root-owned file, read-only container mount. `sungen update` in
94
+ // particular has ALREADY reinstalled globally by the time it gets here, so throwing would
95
+ // abandon the AI-rules refresh and the family sync over a hardening step. Report and move on.
96
+ return { add: [], existing: plan.existing, foreignLockfile: plan.foreignLockfile, writeFailed: true };
97
+ }
98
+ }
99
+
100
+ return plan;
101
+ }
102
+
103
+ /**
104
+ * npm is the only package manager that reads a top-level `overrides` block. Yarn wants
105
+ * `resolutions`, pnpm wants `pnpm.overrides` — so on those projects the block we write is inert and
106
+ * the install stays vulnerable. We do NOT guess at the equivalent field (getting a resolutions
107
+ * pattern subtly wrong is worse than not writing one), but silence would be the same class of bug
108
+ * as #764 itself: a fix that looks applied and is not. So: say so.
109
+ */
110
+ function detectForeignLockfile(dir: string): 'yarn' | 'pnpm' | undefined {
111
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
112
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
113
+ return undefined;
114
+ }
115
+
116
+ /** Print what `ensureSecurityOverrides` did, in the voice the surrounding commands already use. */
117
+ export function reportSecurityOverrides(plan: OverridePlan | null, dryRun = false): void {
118
+ if (!plan) return;
119
+
120
+ if (plan.writeFailed) {
121
+ console.warn(
122
+ '⚠️ Could not write the security overrides into package.json (read-only file?). Nothing else\n' +
123
+ ' failed. Add this block by hand to keep `npm audit` clean:\n' +
124
+ ` "overrides": ${JSON.stringify(Object.fromEntries(SECURITY_OVERRIDES.map((o) => [o.pkg, o.range])), null, 2).replace(/\n/g, '\n ')}\n`,
125
+ );
126
+ return;
127
+ }
128
+
129
+ if (plan.add.length) {
130
+ const verb = dryRun ? '[dry-run] would add' : 'Added';
131
+ console.log(`🔒 ${verb} ${plan.add.length} security override(s) to package.json — npm honours these only here, not from sungen's own manifest:`);
132
+ for (const entry of plan.add) console.log(` • ${entry.pkg}@${entry.range} — ${entry.why}`);
133
+ console.log('');
134
+ }
135
+
136
+ if (plan.existing.length) {
137
+ console.log('🔒 Left your own overrides alone (sungen never rewrites a pin you declared):');
138
+ for (const e of plan.existing) console.log(` – ${e.pkg}: ${e.declared} (sungen would use ${e.wanted})`);
139
+ console.log(' If `npm audit` still flags one of these, raise it by hand.\n');
140
+ }
141
+
142
+ if (plan.foreignLockfile && (plan.add.length || plan.existing.length)) {
143
+ const field = plan.foreignLockfile === 'pnpm' ? '"pnpm": { "overrides": … }' : '"resolutions"';
144
+ console.warn(
145
+ `⚠️ This project has a ${plan.foreignLockfile} lockfile. ${plan.foreignLockfile} ignores npm's "overrides" —\n` +
146
+ ` mirror the block into ${field} as well, or the pins do nothing here.\n`,
147
+ );
148
+ }
149
+ }
@@ -93,6 +93,20 @@ every variant is N/A) and never a bare state word. **`N/A` leaves the denominato
93
93
  and actual result share one column `P`; the header row is frozen; ISO dates; landscape print with
94
94
  repeated header rows.
95
95
 
96
+ **Re-running delivery keeps the tester's Manual results** (#756, since 3.2.33). Before overwriting,
97
+ the CLI reads the existing workbook's **Manual** sheet and carries each variant's `L`–`P` cells into
98
+ the new one by `VP-…` ref, verbatim. A hidden `_sungen_manual` sheet stamps each case's fingerprint,
99
+ so a result recorded against an older procedure is reset to `Pending` with the previous result
100
+ moved into Note. Relay the CLI's `Manual results:` lines to the user: `reset to Pending` means those
101
+ cases must be run again by hand; `not carried` names results whose case left the feature. The
102
+ **Auto** sheet is not carried (the run report owns its Result), and rows a tester added by hand
103
+ have no ref and are not carried. Never tell the user to copy or rename the file to protect manual
104
+ results. A second `not carried` line names a duplicated row (only the first copy is kept) — tell
105
+ the user to merge it by hand. If the CLI prints `not written: cannot read …` (locked or damaged) or
106
+ `not written: … has no "Manual" sheet` (renamed), the unit was deliberately NOT written to protect
107
+ the results: ask the user to close the file or rename the sheet back to `Manual`, or move the file
108
+ aside. Never delete, rename or move it yourself.
109
+
96
110
  `Coverage` sheet — requirement coverage table (every FR/TR/NFR id with an explicit status),
97
111
  target × category grid with explicit `—` gaps, dispositions, manifest. CSV mirrors the same model
98
112
  flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
@@ -124,8 +124,14 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
124
124
  | `SG-E027` | A selectors.yaml entry declares a **`type:` that is not a selector type** — `type: 'testId'` (capital I), `'xpth'`, `'rol'`, `'test-id'`. YAML is untyped at run time, so this used to compile clean and resolve through the strategy switch's `default:` as `getByPlaceholder('<the value>')`: a locator that cannot match, surfacing much later as a timeout naming placeholder text nobody wrote. Reported once per entry, naming the entry key and the nearest valid spelling; every step using that entry now compiles to a `throw` | Fix the spelling in selectors.yaml. Valid types: `placeholder`, `role`, `testid`, `label`, `text`, `locator`, `id`, `upload`, `page`, `column`, `frame`, `table`, and the mobile strategies `accessibility-id`, `xpath`, `android-uiautomator`, `ios-predicate`, `css`. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
125
125
  | `SG-W030` | A selectors.yaml **`expanded.class`/`expanded.attribute`** value carries a character a class or attribute name cannot — whitespace, a quote, a backslash, `$`, `{`, `}`. The value is pasted into the generated spec (string literal, regex, `classList` argument) and selectors.yaml is often generated from the page under test, so it is refused and the field dropped: the step falls back to driving and asserting `aria-expanded`. Reported once per entry and field. The checked/selected signal is refused outright instead of warned about, because dropping it leaves the step with no state to read at all | Use the plain class or attribute name (Tailwind arbitrary values such as `bg-[#eff6ff]` or `!mt-0` are accepted); never paste a selector fragment or quoted text into the field |
126
126
  | `SG-E031` | A selectors.yaml **`checked:`/`selected:` declaration the compiler refuses**: the two spellings disagree (`checked.class` and `selected.class` with different values), a class token or attribute name carries whitespace / a quote / a backslash, or one entry declares both a `class` and an `attribute`. Every one of them is silent at run time — the generated matcher tests a single token, so the step would simply time out blaming your component — and an entry declaring two signals lets an action drive one while the assertion after it reads the other. Refused, not dropped: with no signal the step is back on the *"has no on/off state to set"* failure the declaration exists to remove | Declare ONE spelling and ONE of `class` / `attribute`, as a single plain token: `checked: { class: 'is-checked' }`, `selected: { class: 'border-link' }`, or `checked: { attribute: 'data-checked' }`. This is a selectors.yaml-only fix, allowed inside the run-test auto-fix loop |
127
-
128
- A diagnostic never changes generation's own exit code — `sungen generate --strict` turns any collected diagnostic into a non-zero exit for CI gates. They are not all inert, though: **`SG-W001`, `SG-W011`, `SG-E020`, `SG-E021`, `SG-W020`, `SG-E023`, `SG-E024`, `SG-E025`, `SG-E026` and `SG-E027` additionally compile the offending step to a runtime `throw`, and `SG-E031` refuses the whole file** — a step that reads as a check and silently checks nothing is worse than one that fails loudly. Generation always COMPLETES: the rest of the scenario and the rest of the file still compile. `SG-E022` is the odd one out — it blames the compiler, not your `.feature`, and throws nothing (the file is written so you can inspect it).
127
+ | `SG-W032` | A step carries the row filter **`in [T] table with {{v}}`** but the pattern that compiled it dropped the clause, so the generated code runs against the WHOLE table. `Then User see [Status] text in [Job Table] table with {{job_title}}` asserts on every `.status` in the table and passes on a row the sentence never named. The `table-*` family (positioned rows included) resolves its own container and is not flagged, nor is the two-`with` form, which scopes the row correctly, nor a step that WRITES — a write needs a value, so its `with {{v}}` IS that value and `fill [Search] field in [T] table with {{v}}` is an ordinary container-scoped write | Rewrite with the two-`with` form: `User see [Status] text with <expected> in [Job Row] row with {{job_title}}`. Point the second `[…]` at a ROW entry in selectors.yaml, not at the table entry — a filter applied to the table narrows nothing |
128
+ | `SG-W034` | A `see [X] column with {{v}}` step runs inside a table whose rows are declared with a **`rows:` shape**, so the row is not an ARIA row and holds no ARIA cells — but the assertion's only other way to reach a cell is `getByRole('cell')`, which matches nothing there. `columns.<col>.index` cannot rescue it: it indexes that same empty set. Before this the step failed as a timeout blaming the app. Not reported on a real `<table>`, where the role fallback is correct | Name the cell on the column: `columns: { title: { index: 0, header: 'Title', elements: { cell: { type: locator, value: '.job-title' } } } }`. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
129
+ | `SG-E035` | **`table match data:` on a table declared with a `rows:` shape** and no matching `columns:` entries. The assertion reads its header row from `thead tr` and addresses cells as `td, th`; a <div> list has neither — forever, not as a render race — so the step could only wait out every timeout and fail on an empty header row. Refused at compile time, naming the columns it needs | Declare each DataTable column on the table entry with the selector that matches its cell inside a row: `columns: { title: { index: 0, header: 'Title', elements: { cell: { type: locator, value: '.job-title' } } } }`. Declaring columns also removes the runtime header read on a real `<table>`. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
130
+ | `SG-W036` | A **`remember [X] … as {{v}}` capture through a `locator`/`css` selector with no `nth:`**. A capture reads ONE element and the step takes the FIRST match — the right default, and what the documented `nth: 0` workaround asked for after Playwright's strict mode turned a list-shaped selector into a hard failure naming the count. But the first of many is a decision: if the selector describes every item in a list, the captured value is the first row's. Not reported for testid/label/placeholder/role+name/text entries, which name a particular thing | Declare the index (`nth: 0`) to say the first one is meant, or narrow the selector to the single element. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
131
+ | `SG-W037` | A **`type: page` entry saying something about the QUERY that the compiler cannot use**. Two causes: the entry's `value` carries a `?`/`#`, which is DROPPED (a page value is the path only — the pattern is matched against `u.pathname`, which never holds a query), or `query:` declares something other than `any`, which is ignored rather than read as permissive so a typo cannot quietly switch off the bare-page assertion. Both used to be silent, and the first is the trap: an author who hit "a bare `see [X] page` requires NO query" reaches for the query in the value, gets byte-identical output, and no hint why | To assert the query, declare it on the STEP (`see [X] page with {{v}}`). If the query is generated at run time — `/admins/detail?id=<the row just clicked>`, which no literal can express — add `query: any` to the entry: the query is then not part of that page's identity. selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
132
+ | `SG-E038` | A **`{{var}}` the compiler cannot resolve** — the feature has no test-data file of its own, the key is absent, or it holds a list/map where a value was needed. This used to emit the PLACEHOLDER TEXT as the value: `fill [Partner Customer ID] field with {{valid_partner_id}}` compiled to `fill('${valid_partner_id}')`, typing those 20 literal characters into the app with `generate` exiting 0. A field with no format validation accepts the string, the server sees an unknown user, and the scenario fails at the assertion it was written to fail at — a suite that looks correct while feeding the system garbage. Every run also shares the SAME literal, so a scenario needing a fresh user silently reuses one. The step now compiles to a `throw` naming the ref and the file | Add the key to the file the message names. Note a feature resolves `{{var}}` against a data file named after **itself** — `qa/screens/<screen>/test-data/<feature>.yaml` — so the second `.feature` in a screen directory needs its own data file, not the screen-named one. `sungen data crosscheck` reports the same gap before you compile |
133
+
134
+ A diagnostic never changes generation's own exit code — `sungen generate --strict` turns any collected diagnostic into a non-zero exit for CI gates. They are not all inert, though: **`SG-W001`, `SG-W011`, `SG-E020`, `SG-E021`, `SG-W020`, `SG-E023`, `SG-E024`, `SG-E025`, `SG-E026`, `SG-E027`, `SG-E035` and `SG-E038` additionally compile the offending step to a runtime `throw`, and `SG-E031` refuses the whole file** — a step that reads as a check and silently checks nothing is worse than one that fails loudly. Generation always COMPLETES: the rest of the scenario and the rest of the file still compile. `SG-E022` is the odd one out — it blames the compiler, not your `.feature`, and throws nothing (the file is written so you can inspect it).
129
135
 
130
136
  ### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
131
137
 
@@ -154,6 +154,7 @@ two content-filtered queries can hit different rows if the table re-renders in b
154
154
  # dropdown | select | combobox → tag probed at runtime (toHaveValue native, toHaveText custom trigger)
155
155
  # 5. State: User see [T] button is disabled | checkbox is checked | dialog with {{v}} is hidden
156
156
  # 6. Attribute (toHaveAttribute): User see [T] image | link with {{v}}
157
+ # WHICH attribute comes from the entry's `attribute:` — any name, not just href/src
157
158
  # 7. Count: User see [T] row with {{count}}
158
159
  # 8. Page: User see [T] page | page with {{url}} ← `with` also asserts the query string
159
160
  # NEVER a state: `page is hidden` / `page is empty` … are NOT step forms (see Pattern 8 below)
@@ -291,7 +292,22 @@ job table:
291
292
  value: 'div.job-list'
292
293
  rows: 'div.job-row' # what EVERY row matches — NOT a row label's entry
293
294
  ```
294
- Two different keys, two meanings: `rows:` on the table = every row (the shape); a `type: locator` entry on a **row label** = ONE positioned row (`tbody tr:first-child`). A shape put on a row label fails at run time with a `toHaveCount(1)` message that points back here. A CSS-located table with no `rows:` gets `SG-W016`. On a shaped table `table row {{n}}` counts from the first data row (no header offset).
295
+ Two different keys, two meanings: `rows:` on the table = every row (the shape); a `type: locator` entry on a **row label** = ONE positioned row (`tbody tr:first-child`). A shape put on a row label fails at run time with a `toHaveCount(1)` message that points back here. A table located by CSS — or by a role that holds no rows, such as `role: list` or `role: region` — with no `rows:` gets `SG-W016`. On a shaped table `table row {{n}}` counts from the first data row (no header offset).
296
+
297
+ **A shaped row has no CELLS either, so the two cell-reading steps need one more declaration.** `see [Col] column with {{v}}` reaches the cell through `getByRole('cell')`, and `table match data:` reads its header row from `thead tr` and addresses cells as `td, th` — a `<div>` row has none of those. Name the cell on the column:
298
+ ```yaml
299
+ job table:
300
+ type: locator
301
+ value: 'div.job-list'
302
+ rows: 'div.job-row'
303
+ columns:
304
+ title:
305
+ index: 0
306
+ header: 'Title'
307
+ elements:
308
+ cell: { type: locator, value: '.job-title' } # relative to ONE row
309
+ ```
310
+ With it, `see [Title] column with {{v}}` compiles to `tableRow.locator('.job-title')` and `table match data:` stops reading `thead` altogether — the header-to-column mapping is then known at compile time, so there is nothing left to race with. Without it on a shaped table, `see [Col] column` warns (`SG-W034`) and `table match data:` is refused outright (`SG-E035`) rather than waiting out every timeout against a header row that will never exist.
295
311
 
296
312
  ### Browser storage `[web]`
297
313
 
@@ -556,6 +572,7 @@ award:
556
572
  | `page` | relative URL | — | navigation |
557
573
  | `upload` | — | — | file inputs |
558
574
  | `frame` | iframe selector | — | iframes |
575
+ | `id` | `#the-id` (a bare `the-id` also works) | — | an element whose only stable handle is its DOM id |
559
576
 
560
577
  Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `columns` `checked` `selected`
561
578
 
@@ -569,6 +586,39 @@ when the state differs, then assert it; `is checked` / `is unchecked` / `is sele
569
586
  the same entry (`is focused`, …) keep their native matchers. The value must be ONE plain class token or
570
587
  attribute name — whitespace, a quote or a backslash is refused at compile time, naming the entry.
571
588
 
589
+ `attribute:` names the attribute an `image`/`link` assertion reads, and it takes ANY attribute name
590
+ straight from the YAML — nothing is hardcoded to `href`/`src`. To assert several attributes of ONE
591
+ element, declare several keys with the SAME locator and different `attribute:`; each key is then a
592
+ separate reference in the Gherkin:
593
+ ```yaml
594
+ job link: # the link's TEXT
595
+ type: locator
596
+ value: 'a.job-link'
597
+ job link href: # the same element, its href
598
+ type: locator
599
+ value: 'a.job-link'
600
+ attribute: href
601
+ job link target:
602
+ type: locator
603
+ value: 'a.job-link'
604
+ attribute: target
605
+ ```
606
+
607
+ **A `page` value that is an ABSOLUTE URL now anchors the HOST as well as the path.** `value:
608
+ 'https://admin.example.com/jobs'` means "that path on that host": `see [X] page`, `is on [X] page`
609
+ and `should remain on` all fail on `https://admin-stg.example.com/jobs`. Before, only `wait for [X]
610
+ page` checked the host and the assertions matched the path on any host at all — so an assertion
611
+ naming one environment silently passed in another. If the page is the same in every environment,
612
+ declare it **relative** (`value: '/jobs'`) and let `baseURL` supply the host; keep the absolute form
613
+ for a destination that really is on another host.
614
+
615
+ `anchor:` is a `page` entry's rendered-content proof: `Then User see [X] page` asserts the URL AND
616
+ that the anchor element is present, so a route guard that leaves the URL alone cannot pass it. Three
617
+ things it does NOT do, each verified rather than assumed: it is read only by the `Then`-side page
618
+ assertion, so `Given User is on [X] page` ignores it; the `should see route` / `should remain on`
619
+ forms ignore it too — they compile through the route assertion, which is never handed an anchor; and
620
+ nothing warns when a page entry declares one and no step asserts that page.
621
+
572
622
  `scope` (e.g. `scope: dialog`) is `[web]`-effective only — no Appium template reads `inDialog`, so
573
623
  on mobile a dialog-scoped ref still resolves against the whole screen (`SG-W021`).
574
624
 
@@ -580,6 +630,7 @@ on mobile a dialog-scoped ref still resolves against the whole screen (`SG-W021`
580
630
  |---|---|
581
631
  | `@spec:<id>` | **Traceability**: this scenario proves requirement `<id>` from `requirements/spec.md` (`@spec:FR-003`). Repeatable — a scenario may prove several. Read by `sungen audit` (requirement coverage) and by delivery (the Coverage sheet's requirement table). Without it the link is only inferred from wording and cannot survive an edit. |
582
632
  | `@manual` | Skip in generation |
633
+ | `@known-defect` | The scenario is EXPECTED to be red — it asserts a live defect. It still runs and still reports as failed (never skipped, never "fixed"), and it stays a pass-through tag so `--grep @known-defect` selects it. **`[mobile]`** Its failure does NOT escalate the next scenario's `@reset:` tier: a red that was designed in is not evidence of leftover state |
583
634
  | `@auth:role` | Use auth storage state for role |
584
635
  | `@no-auth` | Disable inherited auth |
585
636
  | `@steps:name` | Define reusable step block (base scenario) |
@@ -645,6 +696,14 @@ Rules that matter when generating:
645
696
  relaunch does not undo a half-written local DB. One rung, one scenario, and never from `none`.
646
697
  A **retry** of the failed scenario is NOT escalated — it re-runs at its declared tier, so a
647
698
  state-dependent failure fails both attempts and is reported honestly instead of passing on attempt 2.
699
+ A scenario tagged **`@known-defect`** escalates nothing either: its red was designed in, so making
700
+ the next scenario pay a harder reset for it buys nothing, on every run, forever.
701
+ - **Escalation never picks a tier this session cannot perform.** Reinstalling needs the app's build
702
+ file, so when the app is attached by package the ladder caps at `data` (Android) / `soft` (iOS) and
703
+ says it capped, instead of throwing. Nothing asked for the escalated tier, and the throw would land
704
+ in `beforeEach` — a hook failure, which skips every remaining scenario in the FILE. The cap never
705
+ goes BELOW the declared tier: a tier you asked for that the session cannot do still throws by name,
706
+ because that one is an unmet contract rather than a recovery the runner chose.
648
707
  - **`SUNGEN_RESET_TIER=data` raises the floor for a whole run** without regenerating specs — CI can run
649
708
  stricter than local dev. It never lowers a declared tier.
650
709
  - **`data`/`full` on iOS reinstall the app**, because `mobile: clearApp` is Android-only (UiAutomator2).
@@ -724,7 +783,9 @@ npx playwright test --grep "@smoke|@high" # smoke OR high
724
783
  | Serial (default) | `test.describe.serial()` | Shared | `beforeAll` (1 goto) | Skip remaining |
725
784
  | `@parallel` | `test.describe()` | Fresh per test | `beforeEach` (N goto) | Continue |
726
785
 
727
- **`@parallel` is required** when a feature has multiple auth groups (e.g., `@auth:user` + `@no-auth` scenarios). Serial mode uses one shared browser context and cannot mix auth roles. The compiler will error if `@parallel` is missing in this case.
786
+ **Multiple auth groups need the default isolation.** A feature mixing `@auth:user` with `@no-auth` compiles as-is — features are parallel by default, and `@parallel` only names that default. What breaks it is `@serial`: one shared browser context cannot carry two different sessions, so the compiler errors and asks you to drop `@serial` or split the feature.
787
+
788
+ `@no-auth` is a demand for an **empty** session, not merely the absence of `@auth:`. Its scenarios compile into their own `test.describe('no-auth')` with an inline empty `storageState`, so they stay signed out even when `playwright.config.ts` sets a global one. An untagged scenario is different again: it inherits whatever that config says.
728
789
 
729
790
  ### `@flow` tag (E2E cross-screen testing)
730
791
 
@@ -806,6 +867,7 @@ dashboard:
806
867
  | Assertion clause the pattern drops | `Then User see [Code] text matches "<format>"` compiling to a bare `toBeVisible()` | Use the exact supported phrasing for the clause (see the `matches` / `contains` / `has text` forms above). A clause that gets swallowed leaves an assertion that passes whatever the element says, so the step is rejected with `SG-E023` instead. If the step still compiles bare, you are on an older global sungen — run `npx sungen` |
807
868
  | Mobile lifecycle step with no app id | `When User restart the app` in a feature whose `Path:` carries only a route | Declare the app id on the Feature: `Path: com.example.app/.MainActivity \| com.example.app`. The lifecycle steps resolve the app from `Path`; without it the whole spec would die at runtime, so the step is rejected with `SG-E024`. For a mobile unit `--path` carries the APP ID, `--reach` carries the navigation recipe |
808
869
  | Missing target type | `fill [email] with {{v}}` | `fill [email] field with {{v}}` |
870
+ | Asserting a cell with the row filter | `Then User see [Status] text in [Job Table] table with {{v}}` | The single `with` is read as the EXPECTED VALUE, not as a row filter, so the assertion runs against the WHOLE table and can pass on a row the sentence never named. Use the two-`with` form — `Then User see [Status] text with <expected> in [Job Row] row with {{v}}` — pointing the second `[…]` at a ROW entry, not at the table entry (a filter applied to the table narrows nothing). Warned with `SG-W032`. A `fill` is NOT flagged: a write needs a value, so its `with {{v}}` is that value and the step is an ordinary container-scoped write |
809
871
  | Background with scope | `Background: ... And User is on [X] dialog` | Use `@steps` + `@extend` for scope-dependent flows |
810
872
  | `is on` after When | `When ... And User is on [X] dialog` | `And User see [X] dialog` or separate Given |
811
873
  | Literal URL navigate | `User navigate to "/dashboard"` | `User is on [Dashboard] page` (add page selector in `selectors.yaml`) |
@@ -128,7 +128,10 @@ back to the feature's own path (or `/<ref>/`) and warns `SG-W013`: a test that r
128
128
 
129
129
  If the label is used by only ONE type, the base key (no suffix) is fine — suffix only on collision.
130
130
 
131
- Type aliases (normalized automatically):
131
+ Type aliases (normalized automatically). The suffix and the step may use **different spellings of
132
+ the same type** — `confirm--modal` resolves for `[Confirm] dialog` and `confirm--dialog` resolves
133
+ for `[Confirm] modal`. Prefer the normalized suffix for consistency; either is found.
134
+
132
135
  | Gherkin type | Normalized suffix |
133
136
  |---|---|
134
137
  | title, label, caption, message | `text` |
@@ -143,6 +146,15 @@ Type aliases (normalized automatically):
143
146
  | column | `columnheader` |
144
147
  | list-item | `listitem` |
145
148
 
149
+ When the step names a type from this table, only a key whose suffix is a spelling of that type is
150
+ used — `[Confirm] dialog` will not fall back to an unrelated `confirm--popup` entry. It reports the
151
+ keys it looked for, so the fix is to add one of them.
152
+
153
+ A bare `[Confirm]` keeps the older, looser fallback and matches any `confirm--*` entry, because with
154
+ no type there is nothing to match a suffix against. So does a `[Ref]` followed by a word that is not
155
+ an element type at all — `Then User see [Quick Panel] is hidden` reads `is` as that word. This is
156
+ what keeps state-clause steps working on a label whose keys are all suffixed.
157
+
146
158
  ## Nth-Suffixed Keys
147
159
 
148
160
  When targeting a specific occurrence by index, add `--N` suffix: