@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
@@ -6,7 +6,7 @@ import { readYaml, readYamlIfExists } from '../../../utils/yaml-io';
6
6
 
7
7
  import { SelectorType } from '../../../utils/selector-types';
8
8
  import { DataResolver } from './data-resolver';
9
- import { DiagnosticsCollector } from '../diagnostics';
9
+ import { DiagnosticsCollector, type DiagnosticCode } from '../diagnostics';
10
10
  import { DEFAULT_CONTENT_NAMED_ROLES } from '../app-profile';
11
11
  import { reportUnknownEntryFields } from './selector-entry-fields';
12
12
  import { reportUnknownSelectorTypes } from './selector-type-validation';
@@ -33,6 +33,25 @@ interface SelectorEntry {
33
33
  // that bounces the user after the URL has already matched turns the step red instead of green.
34
34
  anchor?: SelectorEntry;
35
35
 
36
+ // === Page query identity (#748) ===
37
+ // For a `type: page` entry: `any` means the query string is NOT part of this page's identity.
38
+ //
39
+ // A bare `see [X] page` asserts that the URL carries NO query at all (#464 — a filter/search
40
+ // regression read as green while the query was wrong or gone). That is right for a page whose
41
+ // identity IS its path, and unsatisfiable for one keyed by a query — `/admins/detail?id=5`,
42
+ // where the id comes from the row the previous step clicked and is unknowable when the scenario
43
+ // is written. `with {{v}}` cannot rescue it: that asserts a LITERAL value.
44
+ //
45
+ // Declared on the ENTRY rather than added to the step grammar because it is a fact about the
46
+ // PAGE — every step targeting it needs the same reading — and because `wait for [X] page`
47
+ // already ignores the query. A step-form escape hatch would serve only the Then side and widen
48
+ // the very drift the shared url-assertion partial was written to end.
49
+ //
50
+ // It does NOT weaken a step that declares its own query: `with {{v}}` still asserts every
51
+ // declared param. This changes only what SILENCE means — from "there must be no query" to
52
+ // "the query is not mine to judge".
53
+ query?: 'any';
54
+
36
55
  // === Mobile per-platform variant (issue #392) ===
37
56
  // `android:`/`ios:` sub-selectors for one logical element whose locator differs per OS (composite
38
57
  // content-desc, partial/dynamic text, native predicate). Each is a full SelectorEntry.
@@ -166,12 +185,114 @@ export interface ResolvedSelector {
166
185
  checkedClass?: string;
167
186
  checkedAttribute?: string;
168
187
  anchor?: ResolvedSelector; // page entries only — the rendered-content proof behind `see [X] page` (#662)
188
+ query?: 'any'; // page entries only — the query is not part of this page's identity (#748)
169
189
  }
170
190
 
171
191
  /**
172
192
  * SelectorResolver - Resolves selector references to direct Playwright locator code
173
193
  * Supports natural language references with auto-generated keys
174
194
  */
195
+ /**
196
+ * Read an OWN property of a lookup table, or undefined.
197
+ *
198
+ * Every type table here — the alias map, the strategy map, the reverse index, and the parsed
199
+ * selectors.yaml itself — is a bare object literal, so it inherits Object.prototype and answers for
200
+ * keys it does not have. `elementType` is whatever word the author wrote after `]`, and the three
201
+ * readers failed three different ways on an inherited one: normalizeElementType is declared
202
+ * `: string` and returned a FUNCTION to be interpolated into a selector key; inferFromElementType
203
+ * CALLED that function with no receiver and crashed the compiler with "Cannot convert undefined or
204
+ * null to object"; and `ALIAS_SPELLINGS[t] ?? []` sailed past its own `??` guard, because a
205
+ * function is neither null nor undefined, and died on `.map`.
206
+ *
207
+ * No real feature file says `[X] valueOf`. The rule is the point: a lookup table must not answer
208
+ * for a key it does not have, and one helper is cheaper to keep right than four call sites.
209
+ */
210
+ function own<T>(table: Record<string, T>, key: string): T | undefined {
211
+ return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : undefined;
212
+ }
213
+
214
+ /**
215
+ * Gherkin element-type spelling → the canonical suffix used in selectors.yaml keys.
216
+ *
217
+ * Module-scope so the table has exactly ONE reader-facing definition and the reverse index below
218
+ * can be DERIVED from it. A second, hand-written reverse table is the failure mode issue #728
219
+ * recorded: a hand-maintained mirror of something the source already states drifts, quietly, for
220
+ * months. Declaration order is meaningful — it decides which alias wins when a selectors.yaml
221
+ * declares two spellings of one canonical type (see ALIAS_SPELLINGS).
222
+ */
223
+ const ELEMENT_TYPE_ALIASES: Record<string, string> = {
224
+ // Text-like aliases → "text"
225
+ 'title': 'text',
226
+ 'label': 'text',
227
+ 'caption': 'text',
228
+ // Heading aliases → "heading" (role-based)
229
+ 'heading': 'heading',
230
+ 'header': 'heading',
231
+ 'message': 'text',
232
+ // Table-related aliases
233
+ 'column': 'columnheader',
234
+ // List-related aliases
235
+ 'list-item': 'listitem',
236
+ // Image aliases
237
+ 'logo': 'img',
238
+ 'image': 'img',
239
+ 'icon': 'img',
240
+ // Button aliases
241
+ 'btn': 'button',
242
+ // Field aliases
243
+ 'input': 'field',
244
+ 'textbox': 'field',
245
+ 'textarea': 'field',
246
+ 'editor': 'field',
247
+ // Search alias → searchbox role
248
+ 'search': 'searchbox',
249
+ // Toggle alias → switch role
250
+ 'toggle': 'switch',
251
+ // Alert alias → alertdialog role.
252
+ // UNREACHABLE in practice, kept for the same reason it is not changed: in sungen's grammar
253
+ // `alert` means the BROWSER system dialog, so `see [X] alert` matches a dedicated step pattern
254
+ // and compiles to `page.on('dialog', …)` without ever entering the selector resolver
255
+ // (verified by compiling the step; target-dictionary.mdx documents it as `page.on('dialog')`).
256
+ // This entry only answers a step form that reaches the resolver with the type `alert`, and
257
+ // none does. Removing it would be a behaviour change with no caller to benefit, so it stays.
258
+ 'alert': 'alertdialog',
259
+ // Modal/drawer alias → dialog role
260
+ 'modal': 'dialog',
261
+ 'drawer': 'dialog',
262
+ // Dropdown/select alias -> combobox role
263
+ 'dropdown': 'combobox',
264
+ 'select': 'combobox',
265
+ // Data aliases
266
+ 'item': 'listitem',
267
+ 'card': 'article',
268
+ 'cell': 'cell',
269
+ 'section': 'region',
270
+ // Feedback aliases
271
+ 'badge': 'text',
272
+ 'tooltip': 'text',
273
+ 'tag': 'text',
274
+ // Trigger aliases
275
+ 'menuitem': 'menuitem',
276
+ // System aliases
277
+ 'progressbar': 'progressbar',
278
+ 'spinner': 'status',
279
+ 'breadcrumb': 'navigation',
280
+ };
281
+
282
+ /**
283
+ * Canonical type → every alias spelling that normalizes to it, in ELEMENT_TYPE_ALIASES declaration
284
+ * order. Derived, never authored. Identity entries ('heading': 'heading') are excluded: they are
285
+ * already covered by the canonical candidate and would only duplicate a lookup.
286
+ */
287
+ const ALIAS_SPELLINGS: Record<string, string[]> = (() => {
288
+ const out: Record<string, string[]> = {};
289
+ for (const [alias, canonical] of Object.entries(ELEMENT_TYPE_ALIASES)) {
290
+ if (alias === canonical) continue;
291
+ (out[canonical] ??= []).push(alias);
292
+ }
293
+ return out;
294
+ })();
295
+
175
296
  export class SelectorResolver {
176
297
  private selectorCache = new Map<string, SelectorFile | LegacySelectorFile>();
177
298
  private selectorsDir: string;
@@ -393,69 +514,54 @@ export class SelectorResolver {
393
514
  'navigation': () => ({ strategy: 'role', role: 'navigation', name: label, value: 'navigation' }),
394
515
  };
395
516
 
396
- const factory = strategyMap[normalized];
517
+ const factory = own(strategyMap, normalized);
397
518
  return factory ? factory() : null;
398
519
  }
399
520
 
521
+ /**
522
+ * Does the compiler have an OPINION about this element type — is it an alias it normalizes, or a
523
+ * type auto-infer can serve?
524
+ *
525
+ * The distinction decides whether the last-resort sweep may run, and it exists because
526
+ * `step.elementType` is not always an element type. The parser takes the first word after `]`
527
+ * (gherkin-parser/index.ts), so `see [Quick Panel] is hidden` arrives here with the "type" `is`
528
+ * and `drag [Item] to [Drop Zone]` with `to`. Nine such function words appear across the shipped
529
+ * features. A word the compiler cannot reason about carries no type information at all, so it must
530
+ * not be treated as a narrowing signal — a step like that has to keep the same open fallback a
531
+ * bare `[Ref]` gets, or a project whose label needs suffixed keys loses every state-clause step.
532
+ *
533
+ * Derived from the two maps rather than listed, so a new alias or strategy is known here the day
534
+ * it is added. tests/docs/element-type-vocabulary-parity.run.ts pins the maps against the parser's
535
+ * own list and the published table.
536
+ */
537
+ private static isKnownElementType(elementType: string): boolean {
538
+ if (own(ELEMENT_TYPE_ALIASES, elementType) !== undefined) return true;
539
+ return SelectorResolver.inferFromElementType(elementType, '') !== null;
540
+ }
541
+
542
+ /**
543
+ * Every selectors.yaml key that could legitimately hold the entry for `[label] <elementType>`.
544
+ *
545
+ * Shared by the lookup and by the not-found message, so the error can never advertise a key the
546
+ * resolver did not actually try — the two drifting apart is how #752 stayed invisible: the throw
547
+ * that would have named the missing key was unreachable, and when it did fire it listed two
548
+ * candidates while the resolver had its own idea of which to check.
549
+ */
550
+ private static typedKeyCandidates(key: string, elementType: string, normalizedType: string): string[] {
551
+ return [...new Set([
552
+ `${key}--${normalizedType}`,
553
+ `${key}--${elementType}`,
554
+ ...(own(ALIAS_SPELLINGS, normalizedType) ?? []).map((alias) => `${key}--${alias}`),
555
+ ])];
556
+ }
557
+
400
558
  /**
401
559
  * Normalize elementType aliases to canonical YAML suffix
402
560
  * Gherkin writers may use: title, label, heading, text → all map to "text"
403
561
  * Similarly: btn → button, link → link, input/field → field
404
562
  */
405
563
  private static normalizeElementType(elementType: string): string {
406
- const aliasMap: Record<string, string> = {
407
- // Text-like aliases → "text"
408
- 'title': 'text',
409
- 'label': 'text',
410
- 'caption': 'text',
411
- // Heading aliases → "heading" (role-based)
412
- 'heading': 'heading',
413
- 'header': 'heading',
414
- 'message': 'text',
415
- // Table-related aliases
416
- 'column': 'columnheader',
417
- // List-related aliases
418
- 'list-item': 'listitem',
419
- // Image aliases
420
- 'logo': 'img',
421
- 'image': 'img',
422
- 'icon': 'img',
423
- // Button aliases
424
- 'btn': 'button',
425
- // Field aliases
426
- 'input': 'field',
427
- 'textbox': 'field',
428
- 'textarea': 'field',
429
- 'editor': 'field',
430
- // Search alias → searchbox role
431
- 'search': 'searchbox',
432
- // Toggle alias → switch role
433
- 'toggle': 'switch',
434
- // Alert alias → alertdialog role
435
- 'alert': 'alertdialog',
436
- // Modal/drawer alias → dialog role
437
- 'modal': 'dialog',
438
- 'drawer': 'dialog',
439
- // Dropdown/select alias -> combobox role
440
- 'dropdown': 'combobox',
441
- 'select': 'combobox',
442
- // Data aliases
443
- 'item': 'listitem',
444
- 'card': 'article',
445
- 'cell': 'cell',
446
- 'section': 'region',
447
- // Feedback aliases
448
- 'badge': 'text',
449
- 'tooltip': 'text',
450
- 'tag': 'text',
451
- // Trigger aliases
452
- 'menuitem': 'menuitem',
453
- // System aliases
454
- 'progressbar': 'progressbar',
455
- 'spinner': 'status',
456
- 'breadcrumb': 'navigation',
457
- };
458
- return aliasMap[elementType] || elementType;
564
+ return own(ELEMENT_TYPE_ALIASES, elementType) ?? elementType;
459
565
  }
460
566
 
461
567
  /**
@@ -475,33 +581,37 @@ export class SelectorResolver {
475
581
  // Try nth-suffixed key first: hay.gui.loi.cam.on--3
476
582
  if (nth && nth > 0) {
477
583
  const nthKey = `${key}--${nth}`;
478
- const nthEntry = selectorFile[nthKey];
584
+ const nthEntry = own(selectorFile, nthKey);
479
585
  if (nthEntry) {
480
586
  return this.resolveFromEntry(nthEntry, displayLabel);
481
587
  }
482
588
  }
483
589
 
484
- // Try type-suffixed key first: add.campaign--button, add.campaign--text
590
+ // Try type-suffixed keys: add.campaign--button, add.campaign--text
591
+ //
592
+ // EVERY spelling of the step's type is tried, not just the two the step itself could have been
593
+ // written with (#752). The alias table maps a Gherkin spelling onto a canonical one, but it was
594
+ // only ever applied to the STEP; the file's keys were compared raw. So `confirm--modal` was
595
+ // invisible to `[Confirm] dialog` while `confirm--dialog` resolved for `[Confirm] modal` —
596
+ // the mapping is one-directional and the lookup inherited that. The hole was not specific to
597
+ // modal/drawer: all 30 non-identity aliases had it.
598
+ //
599
+ // Order is deterministic and NOT file order: canonical first (the spelling the compiler thinks
600
+ // in), then whatever the author literally typed, then the remaining aliases in alias-table
601
+ // declaration order. A file declaring both `--modal` and `--drawer` therefore resolves the same
602
+ // way on every machine.
485
603
  if (elementType) {
486
604
  const normalizedType = SelectorResolver.normalizeElementType(elementType);
487
- const typedKey = `${key}--${normalizedType}`;
488
- const typedEntry = selectorFile[typedKey];
489
- if (typedEntry) {
490
- return this.resolveFromEntry(typedEntry, displayLabel);
491
- }
492
-
493
- // If normalized differs from original, also try original as-is
494
- if (normalizedType !== elementType) {
495
- const originalKey = `${key}--${elementType}`;
496
- const originalEntry = selectorFile[originalKey];
497
- if (originalEntry) {
498
- return this.resolveFromEntry(originalEntry, displayLabel);
605
+ for (const candidate of SelectorResolver.typedKeyCandidates(key, elementType, normalizedType)) {
606
+ const typedEntry = own(selectorFile, candidate);
607
+ if (typedEntry) {
608
+ return this.resolveFromEntry(typedEntry, displayLabel);
499
609
  }
500
610
  }
501
611
  }
502
612
 
503
613
  // Fallback to base key
504
- const entry = selectorFile[key];
614
+ const entry = own(selectorFile, key);
505
615
  if (entry) {
506
616
  return this.resolveFromEntry(entry, displayLabel);
507
617
  }
@@ -545,20 +655,30 @@ export class SelectorResolver {
545
655
  }
546
656
  }
547
657
 
548
- // Last resort: find any suffixed key matching key--*
549
- const suffixedKeys = Object.keys(selectorFile).filter(k => k.startsWith(`${key}--`));
550
- if (suffixedKeys.length > 0) {
551
- return this.resolveFromEntry(selectorFile[suffixedKeys[0]], displayLabel);
552
- }
553
-
554
- const tried: string[] = [];
555
- if (elementType) {
556
- const normalizedType = SelectorResolver.normalizeElementType(elementType);
557
- tried.push(`${key}--${normalizedType}`);
558
- if (normalizedType !== elementType) {
559
- tried.push(`${key}--${elementType}`);
658
+ // Last resort: any suffixed key matching key--*, for a [Ref] the compiler could not type.
659
+ //
660
+ // Two cases reach here and they are the same case: a bare [Ref] with no word after it, and a
661
+ // [Ref] followed by a word that is not an element type. The parser cannot tell the two apart —
662
+ // it takes the FIRST word after `]`, so `see [Quick Panel] is hidden` arrives typed `is`. In
663
+ // both, nothing is known about what kind of element is wanted, so any suffixed key is as good a
664
+ // guess as the next, and a project whose label needs suffixed keys keeps its state-clause steps.
665
+ //
666
+ // A step the compiler CAN type gets no such guess. The candidate scan above already tried every
667
+ // spelling that normalizes to that type, so whatever is still sitting under `key--` is a
668
+ // DIFFERENT element, and handing it back is the silent-wrong-answer shape of #752 itself.
669
+ // Falling through to the throw is better: it names the keys that were tried, and "add an entry"
670
+ // is an instruction the author can act on.
671
+ if (!elementType || !SelectorResolver.isKnownElementType(elementType)) {
672
+ const suffixedKeys = Object.keys(selectorFile).filter(k => k.startsWith(`${key}--`));
673
+ if (suffixedKeys.length > 0) {
674
+ return this.resolveFromEntry(selectorFile[suffixedKeys[0]], displayLabel);
560
675
  }
561
676
  }
677
+
678
+ const tried: string[] = elementType
679
+ ? SelectorResolver.typedKeyCandidates(
680
+ key, elementType, SelectorResolver.normalizeElementType(elementType))
681
+ : [];
562
682
  const suffix = tried.length > 0 ? ` (also tried ${tried.join(', ')})` : '';
563
683
  throw new Error(
564
684
  `Selector "${label}" (key: ${key}) not found in ${featureName}.yaml${suffix}. ` +
@@ -751,6 +871,29 @@ export class SelectorResolver {
751
871
  return out.class || out.attribute ? out : undefined;
752
872
  }
753
873
 
874
+ /**
875
+ * Report a selectors.yaml problem ONCE per entry+field, not once per step that resolves it.
876
+ *
877
+ * An entry is resolved again for every step that names it, so an unguarded `diagnostics.add`
878
+ * here repeats the same sentence a dozen times for one typo and buries everything else in the
879
+ * channel. `warnedContentNamed` is the existing de-dupe set; `key` namespaces the callers so two
880
+ * diagnostics about one entry cannot silence each other.
881
+ */
882
+ private warnOnce(key: string, code: DiagnosticCode, message: string): void {
883
+ if (this.warnedContentNamed.has(key)) return;
884
+ this.warnedContentNamed.add(key);
885
+ this.diagnostics?.add(code, message, { feature: this.featureName });
886
+ }
887
+
888
+ /**
889
+ * Resolve a selectors.yaml entry that is NOT reachable by key — a `columns.<col>.elements.cell`
890
+ * declaration, which lives nested under a table entry. Public for that one caller; everything
891
+ * addressable by name goes through `resolveSelector`.
892
+ */
893
+ resolveNestedEntry(entry: SelectorEntry, originalLabel: string): ResolvedSelector {
894
+ return this.resolveFromEntry(entry, originalLabel);
895
+ }
896
+
754
897
  private resolveFromEntry(entry: SelectorEntry, originalLabel: string): ResolvedSelector {
755
898
  // Default values
756
899
  const locator = entry.locator || '';
@@ -815,6 +958,28 @@ export class SelectorResolver {
815
958
  // The anchor is a selector in its own right; it must not inherit the PAGE's label as its
816
959
  // accessible name, so it resolves against its own name (or nothing).
817
960
  if (entry.anchor) v2Fields.anchor = this.resolveFromEntry(entry.anchor, entry.anchor.name ?? '');
961
+ // Gated on the entry KIND, like the value check below: `query:` means nothing on a non-page
962
+ // entry, and both the field and its diagnostic are worded purely in page terms. Today's two
963
+ // consumers re-check `selectorType === 'page'` at the read site (the `anchor:` precedent), so
964
+ // this is belt-and-braces — but a third consumer that forgets the guard would otherwise inherit
965
+ // a field that was never meant for it.
966
+ if (type === 'page' && entry.query !== undefined) {
967
+ const declared = String(entry.query).trim().toLowerCase();
968
+ // Exactly one value is legal today. A typo must NOT read as the permissive form: it would
969
+ // silently turn off the bare-page query assertion (#464) on a page the author meant to keep
970
+ // strict, which is the class of quiet weakening this whole diagnostic channel exists for.
971
+ if (declared === 'any') v2Fields.query = 'any';
972
+ else this.warnOnce(`w037:${originalLabel}:query`, 'SG-W037',
973
+ `selector "${originalLabel}": query: ${JSON.stringify(String(entry.query))} is not a query rule — the only value is \`any\` ("the query is not part of this page's identity"). The declaration is ignored, so a bare \`see [${originalLabel}] page\` still requires the URL to carry NO query at all.`);
974
+ }
975
+ // A page `value` is the PATH only — pathToRegexSource strips a `?`/`#` before building the
976
+ // pattern. Silently, until now: an author who hit the bare-page query rule (#464) and reached
977
+ // for the obvious workaround got byte-identical output and no hint why, which is exactly how
978
+ // #748 was misdiagnosed in the field as a regex-escaping bug.
979
+ if (type === 'page' && typeof entry.value === 'string' && /[?#]/.test(entry.value)) {
980
+ this.warnOnce(`w037:${originalLabel}:value`, 'SG-W037',
981
+ `selector "${originalLabel}": the page value ${JSON.stringify(entry.value)} carries a query/fragment, which is DROPPED — a page value is the path only, and the pattern is matched against \`u.pathname\`, which never holds one. Writing it here changes nothing. To assert the query, declare it on the step (\`see [${originalLabel}] page with {{v}}\`); to say the query is not part of this page's identity, add \`query: any\` to the entry.`);
982
+ }
818
983
 
819
984
  // Helper to attach v2 fields and inputMethod
820
985
  const withExtras = (resolved: ResolvedSelector): ResolvedSelector => {
@@ -971,9 +1136,15 @@ export class SelectorResolver {
971
1136
  exact,
972
1137
  });
973
1138
 
974
- // Mobile (Appium) native strategies — pass through unchanged; the appium adapter's
975
- // `appium-selector` switch renders the right WDIO selector (`~` / raw xpath / `android=` /
976
- // `-ios predicate string:` / `id=`). On web these types never appear. See issue #392.
1139
+ // Native strategies — pass through unchanged; the appium adapter's `appium-selector` switch
1140
+ // renders the right WDIO selector (`~` / raw xpath / `android=` / `-ios predicate string:` /
1141
+ // `id=`). See issue #392.
1142
+ //
1143
+ // `id` is NOT mobile-only, whatever this comment used to claim: the legacy branch at the
1144
+ // bottom of resolveByPriority extracts `#id` out of a web `locator:` and returns
1145
+ // `strategy: 'id'`, and the playwright adapter ships an `id` partial to serve it. The two
1146
+ // paths disagree on whether `value` carries the hash, which is why the partial reads the
1147
+ // selector through `cssIdSelector` rather than off `value` directly.
977
1148
  case 'accessibility-id':
978
1149
  case 'xpath':
979
1150
  case 'android-uiautomator':
@@ -27,7 +27,7 @@ import { specCoverage, SpecCoverageResult, parseSpecClauses, restatedRequirement
27
27
  import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
28
28
  negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk, serialCascadeRisk } from './quality-gates';
29
29
  import { viewpointLedger, parseViewpointItems, browserGestureSubstitutions, substitutionRemedy, viewpointIntegrity, LedgerResult } from './viewpoint-ledger';
30
- import { specBranches, unenumeratedBranches, sameOracleClusters, permissionPairGaps, awaitingSeedData, unconsideredRisks, MULTI_SCREEN_RISKS } from './spec-branches';
30
+ import { specBranches, unenumeratedBranches, sameOracleClusters, visibilityPairGaps, awaitingSeedData, unconsideredRisks, MULTI_SCREEN_RISKS } from './spec-branches';
31
31
  const MULTI_SCREEN_RISK_COUNT = MULTI_SCREEN_RISKS.length;
32
32
  import { capabilityRegistry } from '../capabilities/registry';
33
33
  import { discoverAndRegisterCapabilities } from '../capabilities/discover';
@@ -227,7 +227,12 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
227
227
  if (flowScenarios.length) {
228
228
  for (const c of flowCoveredThemes(gate.gaps, flowScenarios)) {
229
229
  const i = gate.gaps.findIndex((g) => g.theme === c.theme);
230
- if (i >= 0) { gate.gaps.splice(i, 1); gate.themesCovered++; flowCredits.push(c); }
230
+ if (i >= 0) {
231
+ gate.gaps.splice(i, 1); gate.themesCovered++; flowCredits.push(c);
232
+ // Keep the claim-proof list in step with the count (#776) — a theme credited to a flow
233
+ // must SAY it came from a flow, or the evidence lines silently under-report the total.
234
+ gate.coveredBy.push({ theme: c.theme, by: `(flow) ${c.flow}`, keyword: '—' });
235
+ }
231
236
  }
232
237
  gate.coverageRatio = gate.themesTotal ? gate.themesCovered / gate.themesTotal : 1;
233
238
  }
@@ -243,7 +248,18 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
243
248
  const downstream = downstreamScope(readText(specPath), scenarios, featureText);
244
249
  const manualOracleResult = manualOracle(featureText);
245
250
  const autoManual = automatableManual(scenarios); // TQ-2 — @manual that is really automatable
246
- const ledger = viewpointLedger(viewpointPath, scenarios, featureText);
251
+ // The ledger decides "covered" by looking the claim's id up in the feature TEXT, and a comment
252
+ // is text: a file whose header comment merely mentions VP-74 scored it covered while no scenario
253
+ // implemented it. That is a false GREEN on a critical axis — the one kind of wrong answer an
254
+ // audit must not give. Comments live on their own lines in Gherkin, so dropping those lines is
255
+ // the whole fix; no parser is needed.
256
+ //
257
+ // Stripped HERE and nowhere else. The shared `featureText` above feeds twelve other readers, one
258
+ // of which (`manualOracle`) reads `#` lines ON PURPOSE — a `@manual` scenario with no steps
259
+ // carries its entire procedure in comments — so stripping at the source would delete that
260
+ // oracle's only input.
261
+ const ledgerFeatureText = featureText.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
262
+ const ledger = viewpointLedger(viewpointPath, scenarios, ledgerFeatureText);
247
263
  const negSideEffect = negativeSideEffect(scenarios);
248
264
  const ownership = crossArtifactOwnership(screenDir, scenarios);
249
265
  // @exploration scenarios are probes outside the official suite — the finding's own fix
@@ -281,12 +297,16 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
281
297
  // #569 — handoff integrity: a flow's depth is also whether each cross-screen transition
282
298
  // is followed by an assertion on the carried state (same floor discipline as the other caps).
283
299
  const HANDOFF_FLOOR = 0.5;
284
- const businessDepth = Math.min(
300
+ // Each FLOOR is a floor on the CAP, not on the result — an out-of-range input ratio walks
301
+ // straight through it (a -1.5 oracle ratio yielded businessDepth -0.25 and crashed the CLI's
302
+ // bar renderer, #762). The axis is declared `0..1` above and weighted `critical`, so a negative
303
+ // value would also drag `score.overall` below zero. Clamp once, here, where the axis is formed.
304
+ const businessDepth = Math.max(0, Math.min(1, Math.min(
285
305
  depth.bcDepthRatio,
286
306
  flowDepth.stateful ? FLOW_DEPTH_FLOOR + (1 - FLOW_DEPTH_FLOOR) * flowDepth.ratio : 1,
287
- oracle.weak.length ? ORACLE_FLOOR + (1 - ORACLE_FLOOR) * oracle.ratio : 1,
307
+ oracle.weakFacet ? ORACLE_FLOOR + (1 - ORACLE_FLOOR) * oracle.ratio : 1,
288
308
  flowQ.hasContract && flowQ.handoffs.total > 0 ? HANDOFF_FLOOR + (1 - HANDOFF_FLOOR) * flowQ.handoffs.ratio : 1,
289
- );
309
+ )));
290
310
  // When the taxonomy drifted (most scenarios unclassified), the balance axis is unreliable — cap it
291
311
  // at 0.5 instead of awarding a vacuous 1.0 so a stale taxonomy fails loudly, not silently (H1).
292
312
  const balanceScore = balance.unclassifiedRatio > 0.4
@@ -583,12 +603,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
583
603
  `add data assertions (\`... with {{value}}\`, \`see all ... contain {{v}}\`) or, if cross-screen, defer to a flow with @manual + reason.`,
584
604
  );
585
605
  }
586
- // H5 — state isolation: a @parallel stateful flow that mutates the cart needs per-scenario
587
- // isolation, else count/quantity asserts go flaky. Warn (advisory) when no mitigation is present
606
+ // H5 — state isolation: a stateful flow that mutates the cart needs per-scenario isolation, else
607
+ // count/quantity asserts go flaky. Warn (advisory) when no mitigation is present
588
608
  // (@cleanup:cart / @isolate / a "Given … empty cart" background / fresh context).
609
+ // #759 — scoped by the ABSENCE of @serial, not the presence of @parallel: parallel is the default.
589
610
  if (isoRisk) {
590
611
  const what = flowQ.contract?.stateful ?? 'cart';
591
- findings.push(`ISOLATION-RISK: this @parallel flow mutates ${what} but has no per-scenario isolation → count/quantity/state asserts can go flaky when scenarios share it. Add \`@cleanup:${what}\` (or \`@isolate\`, a fresh context, or \`{{$timestamp}}\` data) so each scenario starts clean.`);
612
+ findings.push(`ISOLATION-RISK: this flow mutates ${what} but has no per-scenario isolation → count/quantity/state asserts can go flaky when scenarios share it. Add \`@cleanup:${what}\` (or \`@isolate\`, a fresh context, or \`{{$timestamp}}\` data) so each scenario starts clean.`);
592
613
  }
593
614
  // #414 — serial-cascade masking: an @serial suite of independent (re-navigating) scenarios hides
594
615
  // failures, because Playwright's describe.serial SKIPS everything after the first red. Isolation is
@@ -646,6 +667,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
646
667
  for (const u of spec.uncoveredMust) {
647
668
  findings.push(`SPEC-UNCOVERED: ${u.id} (MUST) has no covering scenario — "${u.text}" → add a scenario or tag one @spec:${u.id}.`);
648
669
  }
670
+ // #679 — a requirement that states both directions is two requirements wearing one id. Citing
671
+ // the id closes the id, not the clause the suite never asserts.
672
+ for (const c of spec.clauseGaps) {
673
+ findings.push(`SPEC-CLAUSE-UNCOVERED: ${c.id} states both directions and only one is proved — nothing asserts its ${c.polarity === 'negate' ? 'MUST NOT' : 'MUST'} clause ("${c.text}"). One scenario tagged @spec:${c.id} cannot demonstrate a rule and its opposite: the states are mutually exclusive, so each needs its own case. Write the ${c.polarity === 'negate' ? 'negative' : 'positive'} case, or narrow the requirement to the direction the suite actually covers.`);
674
+ }
649
675
  if (spec.inferredOnly.length > 0) {
650
676
  // Covered, but only by keyword inference — no scenario cites the id. Delivery's
651
677
  // requirement table (and any later refactor) cannot follow an inferred link, so
@@ -742,9 +768,12 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
742
768
  }
743
769
  }
744
770
  // #630 — a deny-side security case with no allow-side counterpart cannot tell a working guard
745
- // from a feature that is broken for everyone.
746
- for (const g of permissionPairGaps(scenarios).slice(0, 4)) {
747
- findings.push(`PERMISSION-PAIR-MISSING: "${g.scenario}" proves the content is hidden when permission is denied, but no scenario proves it is SHOWN when permission is granted. If the code hid it unconditionally this would still pass — the pair is the test, the way min-1/min/max/max+1 is the test for a boundary. Add the allow-side case${g.subject.length ? ` for [${g.subject.join(', ')}]` : ''}.`);
771
+ // from a feature that is broken for everyone. #679 — the same reasoning holds for any condition,
772
+ // so the trigger is the ELEMENT proved absent and never proved present, not permission wording.
773
+ for (const g of visibilityPairGaps(scenarios).slice(0, 4)) {
774
+ findings.push(g.kind === 'permission'
775
+ ? `PERMISSION-PAIR-MISSING: "${g.scenario}" proves ${g.element ? `[${g.element}]` : 'the content'} is hidden when permission is denied, but no scenario proves it is SHOWN when permission is granted. If the code hid it unconditionally this would still pass — the pair is the test, the way min-1/min/max/max+1 is the test for a boundary. Add the allow-side case${g.subject.length ? ` for [${g.subject.join(', ')}]` : ''}.`
776
+ : `VISIBILITY-PAIR-MISSING: "${g.scenario}" proves [${g.element}] is ABSENT, and no scenario in this unit proves it is ever SHOWN. "Show A or B, never both" is the defect this misses — and a selector that matches nothing passes \`is hidden\` forever, so a one-sided assertion can be vacuous. Add the counterpart case under the opposite condition (the state where [${g.element}] must appear).`);
748
777
  }
749
778
  // #630 — a scenario blocked on unseeded data is a DATA task, not unfinished test design.
750
779
  {
@@ -11,6 +11,7 @@
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
14
+ import { readYamlConfig, warnUnreadableConfig } from './config-file';
14
15
 
15
16
  export interface CapabilityProfile {
16
17
  platform?: string; // web | mobile | … (the runtime adapter)
@@ -23,7 +24,13 @@ export interface CapabilityProfile {
23
24
  * never asked for.
24
25
  */
25
26
  verification?: string[];
26
- source: 'capabilities.yaml' | 'absent';
27
+ /**
28
+ * Where the profile came from. `'unreadable'` is NOT `'absent'`: the file exists and says
29
+ * something, we just cannot read it. Callers that WRITE capabilities.yaml must branch on this —
30
+ * scaffolding a default over a file whose contents we failed to parse destroys the user's
31
+ * declaration (see `cli/commands/generate.ts`).
32
+ */
33
+ source: 'capabilities.yaml' | 'absent' | 'unreadable';
27
34
  }
28
35
 
29
36
  export type VerifyLayer = 'ui' | 'api' | 'db';
@@ -119,18 +126,22 @@ export function capabilitiesPath(cwd: string): string {
119
126
 
120
127
  export function readCapabilities(cwd: string): CapabilityProfile {
121
128
  const p = capabilitiesPath(cwd);
122
- if (!fs.existsSync(p)) return { platform: undefined, enabled: [], source: 'absent' };
123
- try {
124
- const y = parseYaml(fs.readFileSync(p, 'utf-8')) || {};
125
- return {
126
- platform: typeof y.platform === 'string' ? y.platform : undefined,
127
- enabled: Array.isArray(y.enabled) ? y.enabled.map(String) : [],
128
- verification: Array.isArray(y.verification) ? y.verification.map(String) : undefined,
129
- source: 'capabilities.yaml',
130
- };
131
- } catch {
132
- return { platform: undefined, enabled: [], source: 'absent' };
129
+ const read = readYamlConfig<Record<string, unknown> | null>(p);
130
+ if (read.state === 'absent') return { platform: undefined, enabled: [], source: 'absent' };
131
+ if (read.state === 'unreadable') {
132
+ // The profile decides the runtime adapter, the driver set and the verification scope. Reporting
133
+ // it once here covers all fifteen call sites; what each does about it is still its own choice.
134
+ warnUnreadableConfig(p, read.error,
135
+ 'this run cannot tell which platform or drivers the project declares, and falls back to none');
136
+ return { platform: undefined, enabled: [], source: 'unreadable' };
133
137
  }
138
+ const y = (read.value || {}) as Record<string, unknown>;
139
+ return {
140
+ platform: typeof y.platform === 'string' ? y.platform : undefined,
141
+ enabled: Array.isArray(y.enabled) ? y.enabled.map(String) : [],
142
+ verification: Array.isArray(y.verification) ? y.verification.map(String) : undefined,
143
+ source: 'capabilities.yaml',
144
+ };
134
145
  }
135
146
 
136
147
  export function writeCapabilities(cwd: string, profile: { platform?: string; enabled: string[]; verification?: string[] }): string {
@@ -24,7 +24,7 @@ page_types:
24
24
  detect_keywords: [cart, product, checkout, catalog, brand, category, "add to cart"]
25
25
  must_cover:
26
26
  - theme: list-data
27
- keywords: [list, displayed, card, "product card", grid]
27
+ keywords: ["product list", displayed, card, "product card", grid]
28
28
  depth:
29
29
  requires: data-assertion
30
30
  cross_screen: false
@@ -38,7 +38,7 @@ page_types:
38
38
  keywords: ["product detail", "detail page", "same product name", "matching name", "consistency", "same price"]
39
39
  template: "User remember [Product Name] text as {{v}} … User see [Detail Product Name] header with {{v}}"
40
40
  - theme: cart-correctness
41
- keywords: ["cart contains", quantity, "price", subtotal, "in cart", "cart content", "cart product"]
41
+ keywords: ["cart contains", "cart quantity", "cart price", subtotal, "in cart", "cart content", "cart product"]
42
42
  depth:
43
43
  requires: data-assertion
44
44
  cross_screen: true
@@ -77,7 +77,7 @@ page_types:
77
77
  keywords: ["required", "must be", "error message", "validation message"]
78
78
  template: "User see [Field Error] message with {{error_text}}"
79
79
  - theme: format-boundary
80
- keywords: [format, invalid, boundary, length, range]
80
+ keywords: [format, invalid, boundary, "max length", "min length", range]
81
81
  depth:
82
82
  requires: data-assertion
83
83
  cross_screen: false
@@ -125,10 +125,10 @@ page_types:
125
125
  # suite, which is a scoring decision for an owner, not a drive-by. `viewpoint-ledger` already
126
126
  # covers the dangerous half by refusing `tap [Back]` as an answer to an OS-back viewpoint item.
127
127
  mobile-list:
128
- detect_keywords: [feed, "pull to refresh", "infinite scroll", "load more", "list item", "list row"]
128
+ detect_keywords: ["feed item", "pull to refresh", "infinite scroll", "load more", "list item", "list row"]
129
129
  must_cover:
130
130
  - theme: list-data
131
- keywords: [list, feed, row, "list item", displayed]
131
+ keywords: ["feed item", row, "list item", displayed]
132
132
  depth:
133
133
  requires: data-assertion
134
134
  cross_screen: false