@sun-asterisk/sungen 3.2.0 → 3.2.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/dist/capabilities/context-router.d.ts +11 -2
  2. package/dist/capabilities/context-router.d.ts.map +1 -1
  3. package/dist/capabilities/context-router.js +10 -3
  4. package/dist/capabilities/context-router.js.map +1 -1
  5. package/dist/capabilities/discover.js +1 -1
  6. package/dist/capabilities/discover.js.map +1 -1
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.js +5 -3
  9. package/dist/cli/commands/audit.js.map +1 -1
  10. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +54 -0
  11. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -0
  12. package/dist/generators/test-generator/adapters/appium/appium-adapter.js +52 -0
  13. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -0
  14. package/dist/generators/test-generator/adapters/appium/templates/after-all.hbs +8 -0
  15. package/dist/generators/test-generator/adapters/appium/templates/after-each.hbs +8 -0
  16. package/dist/generators/test-generator/adapters/appium/templates/before-all.hbs +8 -0
  17. package/dist/generators/test-generator/adapters/appium/templates/before-each.hbs +8 -0
  18. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +8 -0
  19. package/dist/generators/test-generator/adapters/appium/templates/scenario.hbs +8 -0
  20. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-accept-action.hbs +4 -0
  21. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-dismiss-action.hbs +2 -0
  22. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +2 -0
  23. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +10 -0
  24. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/clear-action.hbs +1 -0
  25. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-action.hbs +1 -0
  26. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +2 -0
  27. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/click-select-action.hbs +4 -0
  28. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/dismiss-action.hbs +3 -0
  29. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +6 -0
  30. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +2 -0
  31. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/expand-action.hbs +2 -0
  32. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/fill-action.hbs +14 -0
  33. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/fill-editor-action.hbs +12 -0
  34. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +6 -0
  35. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/frame-exit-action.hbs +2 -0
  36. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hide-keyboard-action.hbs +4 -0
  37. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +2 -0
  38. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +2 -0
  39. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/press-action.hbs +4 -0
  40. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/radio-select-action.hbs +4 -0
  41. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +19 -0
  42. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/select-action.hbs +5 -0
  43. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row.hbs +2 -0
  44. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  45. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +8 -0
  46. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +2 -0
  47. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/upload-action.hbs +3 -0
  48. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-page.hbs +3 -0
  49. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role-with-data.hbs +2 -0
  50. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role.hbs +3 -0
  51. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/alert-text-assertion.hbs +2 -0
  52. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/attribute-assertion.hbs +1 -0
  53. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +1 -0
  54. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/clipboard-text-assertion.hbs +2 -0
  55. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/column-cell-assertion.hbs +2 -0
  56. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +14 -0
  57. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +1 -0
  58. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-assertion.hbs +1 -0
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/empty-assertion.hbs +1 -0
  60. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/enabled-assertion.hbs +1 -0
  61. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/focused-assertion.hbs +1 -0
  62. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/have-text-assertion.hbs +15 -0
  63. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +1 -0
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +12 -0
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +2 -0
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/loading-assertion.hbs +2 -0
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +1 -0
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/page-assertion.hbs +1 -0
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/route-assertion.hbs +2 -0
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/selected-assertion.hbs +1 -0
  72. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/sorted-assertion.hbs +2 -0
  73. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-column-exists.hbs +2 -0
  74. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-empty.hbs +2 -0
  75. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-match-data.hbs +2 -0
  76. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-count.hbs +2 -0
  77. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-exists.hbs +2 -0
  78. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-not-exists.hbs +2 -0
  79. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -0
  80. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/background-action.hbs +1 -0
  81. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/grant-permission-action.hbs +11 -0
  82. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +5 -0
  83. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/open-notifications-action.hbs +3 -0
  84. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +5 -0
  85. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +5 -0
  86. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/rotate-action.hbs +1 -0
  87. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/set-clipboard-action.hbs +2 -0
  88. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +2 -0
  89. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +5 -0
  90. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +24 -0
  91. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/navigation.hbs +1 -0
  92. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
  93. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element.hbs +1 -0
  94. package/dist/generators/test-generator/adapters/appium/templates/steps/navigation/wait-timeout.hbs +1 -0
  95. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector-expr.hbs +8 -0
  96. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +15 -0
  97. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +8 -0
  98. package/dist/generators/test-generator/adapters/appium/templates/steps/setup/application-running.hbs +1 -0
  99. package/dist/generators/test-generator/adapters/appium/templates/steps/setup/clear-auth.hbs +2 -0
  100. package/dist/generators/test-generator/adapters/appium/templates/steps/setup/clear-browser-state.hbs +1 -0
  101. package/dist/generators/test-generator/adapters/appium/templates/steps/setup/clear-database.hbs +1 -0
  102. package/dist/generators/test-generator/adapters/appium/templates/steps/setup/user-login-todo.hbs +2 -0
  103. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +226 -0
  104. package/dist/generators/test-generator/adapters/index.d.ts +1 -0
  105. package/dist/generators/test-generator/adapters/index.d.ts.map +1 -1
  106. package/dist/generators/test-generator/adapters/index.js +9 -1
  107. package/dist/generators/test-generator/adapters/index.js.map +1 -1
  108. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  109. package/dist/generators/test-generator/code-generator.js +3 -2
  110. package/dist/generators/test-generator/code-generator.js.map +1 -1
  111. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  112. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  113. package/dist/generators/test-generator/step-mapper.js +7 -37
  114. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  115. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  116. package/dist/generators/test-generator/template-engine.js +13 -1
  117. package/dist/generators/test-generator/template-engine.js.map +1 -1
  118. package/dist/harness/audit.d.ts +16 -2
  119. package/dist/harness/audit.d.ts.map +1 -1
  120. package/dist/harness/audit.js +74 -11
  121. package/dist/harness/audit.js.map +1 -1
  122. package/dist/harness/capability-plan.d.ts +2 -0
  123. package/dist/harness/capability-plan.d.ts.map +1 -1
  124. package/dist/harness/capability-plan.js +4 -1
  125. package/dist/harness/capability-plan.js.map +1 -1
  126. package/dist/harness/catalog/drivers.yaml +1 -1
  127. package/dist/harness/flow-check.d.ts.map +1 -1
  128. package/dist/harness/flow-check.js +13 -4
  129. package/dist/harness/flow-check.js.map +1 -1
  130. package/dist/harness/parse.d.ts +2 -0
  131. package/dist/harness/parse.d.ts.map +1 -1
  132. package/dist/harness/parse.js +10 -2
  133. package/dist/harness/parse.js.map +1 -1
  134. package/dist/harness/quality-gates.d.ts +6 -0
  135. package/dist/harness/quality-gates.d.ts.map +1 -1
  136. package/dist/harness/quality-gates.js +15 -1
  137. package/dist/harness/quality-gates.js.map +1 -1
  138. package/dist/harness/sensors.d.ts +27 -0
  139. package/dist/harness/sensors.d.ts.map +1 -1
  140. package/dist/harness/sensors.js +91 -21
  141. package/dist/harness/sensors.js.map +1 -1
  142. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  143. package/dist/orchestrator/ai-rules-updater.js +8 -0
  144. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  145. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md +184 -0
  146. package/dist/orchestrator/templates/ai-instructions/claude-skill-mobile-gestures.md +109 -0
  147. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix-mobile.md +316 -0
  148. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +184 -0
  149. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md +109 -0
  150. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +316 -0
  151. package/dist/orchestrator/templates/env.appium.example +25 -0
  152. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +92 -0
  153. package/dist/orchestrator/templates/wdio.conf.ts +295 -0
  154. package/dist/utils/selector-types.d.ts +1 -1
  155. package/dist/utils/selector-types.d.ts.map +1 -1
  156. package/dist/utils/selector-types.js +5 -0
  157. package/dist/utils/selector-types.js.map +1 -1
  158. package/package.json +3 -3
  159. package/src/capabilities/context-router.ts +15 -3
  160. package/src/capabilities/discover.ts +1 -1
  161. package/src/cli/commands/audit.ts +5 -3
  162. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +57 -0
  163. package/src/generators/test-generator/adapters/appium/templates/after-all.hbs +8 -0
  164. package/src/generators/test-generator/adapters/appium/templates/after-each.hbs +8 -0
  165. package/src/generators/test-generator/adapters/appium/templates/before-all.hbs +8 -0
  166. package/src/generators/test-generator/adapters/appium/templates/before-each.hbs +8 -0
  167. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +8 -0
  168. package/src/generators/test-generator/adapters/appium/templates/scenario.hbs +8 -0
  169. package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-accept-action.hbs +4 -0
  170. package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-dismiss-action.hbs +2 -0
  171. package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +2 -0
  172. package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +10 -0
  173. package/src/generators/test-generator/adapters/appium/templates/steps/actions/clear-action.hbs +1 -0
  174. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-action.hbs +1 -0
  175. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-element-with-text.hbs +2 -0
  176. package/src/generators/test-generator/adapters/appium/templates/steps/actions/click-select-action.hbs +4 -0
  177. package/src/generators/test-generator/adapters/appium/templates/steps/actions/dismiss-action.hbs +3 -0
  178. package/src/generators/test-generator/adapters/appium/templates/steps/actions/double-click-action.hbs +6 -0
  179. package/src/generators/test-generator/adapters/appium/templates/steps/actions/drag-action.hbs +2 -0
  180. package/src/generators/test-generator/adapters/appium/templates/steps/actions/expand-action.hbs +2 -0
  181. package/src/generators/test-generator/adapters/appium/templates/steps/actions/fill-action.hbs +14 -0
  182. package/src/generators/test-generator/adapters/appium/templates/steps/actions/fill-editor-action.hbs +12 -0
  183. package/src/generators/test-generator/adapters/appium/templates/steps/actions/frame-enter-action.hbs +6 -0
  184. package/src/generators/test-generator/adapters/appium/templates/steps/actions/frame-exit-action.hbs +2 -0
  185. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hide-keyboard-action.hbs +4 -0
  186. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +2 -0
  187. package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +2 -0
  188. package/src/generators/test-generator/adapters/appium/templates/steps/actions/press-action.hbs +4 -0
  189. package/src/generators/test-generator/adapters/appium/templates/steps/actions/radio-select-action.hbs +4 -0
  190. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +19 -0
  191. package/src/generators/test-generator/adapters/appium/templates/steps/actions/select-action.hbs +5 -0
  192. package/src/generators/test-generator/adapters/appium/templates/steps/actions/table-action-in-row.hbs +2 -0
  193. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  194. package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +8 -0
  195. package/src/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +2 -0
  196. package/src/generators/test-generator/adapters/appium/templates/steps/actions/upload-action.hbs +3 -0
  197. package/src/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-page.hbs +3 -0
  198. package/src/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role-with-data.hbs +2 -0
  199. package/src/generators/test-generator/adapters/appium/templates/steps/actions/wait-for-role.hbs +3 -0
  200. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/alert-text-assertion.hbs +2 -0
  201. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/attribute-assertion.hbs +1 -0
  202. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +1 -0
  203. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/clipboard-text-assertion.hbs +2 -0
  204. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/column-cell-assertion.hbs +2 -0
  205. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +14 -0
  206. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +1 -0
  207. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-assertion.hbs +1 -0
  208. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/empty-assertion.hbs +1 -0
  209. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/enabled-assertion.hbs +1 -0
  210. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/focused-assertion.hbs +1 -0
  211. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/have-text-assertion.hbs +15 -0
  212. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +1 -0
  213. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
  214. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +12 -0
  215. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +2 -0
  216. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/loading-assertion.hbs +2 -0
  217. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +1 -0
  218. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/page-assertion.hbs +1 -0
  219. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/route-assertion.hbs +2 -0
  220. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/selected-assertion.hbs +1 -0
  221. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/sorted-assertion.hbs +2 -0
  222. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-column-exists.hbs +2 -0
  223. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-empty.hbs +2 -0
  224. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-match-data.hbs +2 -0
  225. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-count.hbs +2 -0
  226. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-exists.hbs +2 -0
  227. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/table-row-not-exists.hbs +2 -0
  228. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +1 -0
  229. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/background-action.hbs +1 -0
  230. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/grant-permission-action.hbs +11 -0
  231. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/long-press-action.hbs +5 -0
  232. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/open-notifications-action.hbs +3 -0
  233. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pinch-zoom-action.hbs +5 -0
  234. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +5 -0
  235. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/rotate-action.hbs +1 -0
  236. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/set-clipboard-action.hbs +2 -0
  237. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +2 -0
  238. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +5 -0
  239. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +24 -0
  240. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/navigation.hbs +1 -0
  241. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
  242. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-for-element.hbs +1 -0
  243. package/src/generators/test-generator/adapters/appium/templates/steps/navigation/wait-timeout.hbs +1 -0
  244. package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector-expr.hbs +8 -0
  245. package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +15 -0
  246. package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +8 -0
  247. package/src/generators/test-generator/adapters/appium/templates/steps/setup/application-running.hbs +1 -0
  248. package/src/generators/test-generator/adapters/appium/templates/steps/setup/clear-auth.hbs +2 -0
  249. package/src/generators/test-generator/adapters/appium/templates/steps/setup/clear-browser-state.hbs +1 -0
  250. package/src/generators/test-generator/adapters/appium/templates/steps/setup/clear-database.hbs +1 -0
  251. package/src/generators/test-generator/adapters/appium/templates/steps/setup/user-login-todo.hbs +2 -0
  252. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +226 -0
  253. package/src/generators/test-generator/adapters/index.ts +7 -0
  254. package/src/generators/test-generator/code-generator.ts +3 -2
  255. package/src/generators/test-generator/step-mapper.ts +8 -5
  256. package/src/generators/test-generator/template-engine.ts +13 -1
  257. package/src/harness/audit.ts +84 -14
  258. package/src/harness/capability-plan.ts +5 -2
  259. package/src/harness/catalog/drivers.yaml +1 -1
  260. package/src/harness/flow-check.ts +13 -4
  261. package/src/harness/parse.ts +12 -2
  262. package/src/harness/quality-gates.ts +14 -1
  263. package/src/harness/sensors.ts +110 -22
  264. package/src/orchestrator/ai-rules-updater.ts +8 -0
  265. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mobile.md +184 -0
  266. package/src/orchestrator/templates/ai-instructions/claude-skill-mobile-gestures.md +109 -0
  267. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix-mobile.md +316 -0
  268. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mobile.md +184 -0
  269. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-mobile-gestures.md +109 -0
  270. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix-mobile.md +316 -0
  271. package/src/orchestrator/templates/env.appium.example +25 -0
  272. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +92 -0
  273. package/src/orchestrator/templates/wdio.conf.ts +295 -0
  274. package/src/utils/selector-types.ts +5 -0
@@ -0,0 +1,226 @@
1
+ {{#if platform}}
2
+ // sungen:platform={{platform}}
3
+ {{/if}}
4
+ {{#if runtimeData}}
5
+
6
+ const testData = TestDataLoader.load('{{screenName}}', '{{featureFileName}}');
7
+ {{/if}}
8
+ {{#if singleAuthRole}}
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ // ── @auth (mobile) ──────────────────────────────────────────────────────────
13
+ // App-agnostic auth: the framework supplies the MECHANISM; each app supplies the DATA via a
14
+ // gitignored contract at specs/.auth-mobile/<role>.json:
15
+ // { "authedMarker": {type,value}, // present ⟺ already logged in
16
+ // "credentials": { ... }, // referenced as {{key}} in step values
17
+ // "loginSteps": [ {action:"tap"|"fill"|"wait", selector:{type,value}, value?} ] }
18
+ // ensureAuth is idempotent (skips when already logged in) and fail-loud (throws → the @auth spec is
19
+ // blocked, never silently run logged-out). See MOBILE_INTEGRATION_PLAN.md §Phase 5.1.
20
+ function __authSelector(s: { type: string; value: string }): string {
21
+ switch (s.type) {
22
+ case 'accessibility-id': case 'testid': case 'label': case 'placeholder': case 'text': return '~' + s.value;
23
+ case 'android-uiautomator': return 'android=' + s.value;
24
+ case 'ios-predicate': return '-ios predicate string:' + s.value;
25
+ case 'id': return 'id=' + s.value;
26
+ case 'xpath': case 'locator': default: return s.value;
27
+ }
28
+ }
29
+ function __authInterpolate(v: string, creds: Record<string, unknown> = {}): string {
30
+ return String(v).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k) => (creds[k] != null ? String(creds[k]) : ''));
31
+ }
32
+ // Robust mobile text entry: Flutter often ignores a single setValue, and W3C key-streams can drop a
33
+ // char — so we type, read back, and retry once. Masked (password) fields can't be verified → trusted.
34
+ async function __robustFill(el: WebdriverIO.Element, value: string): Promise<void> {
35
+ await el.click();
36
+ try { await el.clearValue(); } catch { /* not clearable */ }
37
+ await el.setValue(value);
38
+ let got = '';
39
+ try { got = (await el.getText()) || (await el.getAttribute('text')) || ''; } catch { /* ignore */ }
40
+ const masked = /[•●*]/.test(got);
41
+ if (!masked && got.replace(/\s/g, '') !== value.replace(/\s/g, '')) {
42
+ try { await el.clearValue(); } catch { /* ignore */ }
43
+ await el.addValue(value);
44
+ }
45
+ }
46
+ async function __ensureAuth(role: string): Promise<void> {
47
+ const cfgPath = path.join(process.cwd(), 'specs', '.auth-mobile', role + '.json');
48
+ const rel = path.relative(process.cwd(), cfgPath);
49
+ if (!fs.existsSync(cfgPath)) {
50
+ throw new Error(`@auth:${role} requires ${rel} (loginSteps + credentials + a login-state marker). See MOBILE_INTEGRATION_PLAN.md §Phase 5.1.`);
51
+ }
52
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
53
+ const present = async (marker: { type: string; value: string } | undefined, timeout = 6000): Promise<boolean> => {
54
+ if (!marker) return false;
55
+ try { await $(__authSelector(marker)).waitForDisplayed({ timeout }); return true; }
56
+ catch { return false; }
57
+ };
58
+ const absent = async (marker: { type: string; value: string } | undefined, timeout = 8000): Promise<boolean> => {
59
+ if (!marker) return true;
60
+ try { await $(__authSelector(marker)).waitForDisplayed({ reverse: true, timeout }); return true; }
61
+ catch { return false; }
62
+ };
63
+ const runSteps = async (steps: any[]): Promise<void> => {
64
+ for (const step of (steps || [])) {
65
+ // Selector-less actions first (no element to find).
66
+ if (step.action === 'hideKeyboard') { try { await driver.hideKeyboard(); } catch { /* no keyboard shown */ } continue; }
67
+ if (step.action === 'back') { try { await driver.back(); } catch { /* nothing to dismiss */ } continue; }
68
+ const el = await $(__authSelector(step.selector));
69
+ await el.waitForDisplayed();
70
+ if (step.action === 'fill') await __robustFill(el, __authInterpolate(step.value, cfg.credentials));
71
+ else if (step.action === 'tap') await el.click();
72
+ // 'wait' = just the waitForDisplayed above
73
+ }
74
+ };
75
+
76
+ // ── Mode A: navigation-based (preferred) ──────────────────────────────────
77
+ // For apps whose login state is NOT visible on the post-relaunch landing screen (e.g. a Flutter
78
+ // app that always opens on Home). We navigate to a DECISION screen (loginEntrySteps, e.g. My Page
79
+ // → Account Settings) and read `loggedOutMarker` there (present ⟺ logged out, e.g. the Login
80
+ // button). loginSteps then start FROM that decision screen.
81
+ if (cfg.loginEntrySteps && cfg.loggedOutMarker) {
82
+ await runSteps(cfg.loginEntrySteps);
83
+ if (!(await present(cfg.loggedOutMarker, 8000))) return; // logged-out marker absent ⟹ already logged in (noReset)
84
+ await runSteps(cfg.loginSteps);
85
+ // Verify: a credentials error means rejection; the login form staying put (logged-out marker
86
+ // never disappears) means it didn't submit. Success = the form navigated away with no error.
87
+ if (await present(cfg.errorMarker, 3000)) {
88
+ throw new Error(`@auth:${role} login rejected (error shown) — check credentials.email/password in ${rel}.`);
89
+ }
90
+ if (!(await absent(cfg.loggedOutMarker, 8000))) {
91
+ throw new Error(`@auth:${role} login did not complete (still on the login screen) — check loginSteps in ${rel}.`);
92
+ }
93
+ return;
94
+ }
95
+
96
+ // ── Mode B: legacy authedMarker (present on the landing screen ⟺ logged in) ──
97
+ const markerPresent = async (): Promise<boolean> => present(cfg.authedMarker, 6000);
98
+ if (await markerPresent()) return; // idempotent — already authenticated (session kept by noReset)
99
+ await runSteps(cfg.loginSteps);
100
+ if (!(await markerPresent())) {
101
+ throw new Error(`@auth:${role} login failed — verify credentials / loginSteps / authedMarker in ${rel}.`);
102
+ }
103
+ }
104
+ {{/if}}
105
+
106
+ {{#if featureDescription}}
107
+ /**
108
+ * Feature: {{featureName}}
109
+ * {{featureDescription}}
110
+ */
111
+ {{/if}}
112
+ describe('{{featureName}}', () => {
113
+ {{#if singleAuthRole}}
114
+ // @auth:{{singleAuthRole}} — authenticate ONCE before any scenario; the session persists across
115
+ // the per-scenario relaunch below (noReset:true). Throws (blocks the spec) if login can't be done.
116
+ before(async () => {
117
+ {{#if appPackage}}
118
+ // Relaunch to a known launch screen first — the session may attach to the app in any state.
119
+ // Platform-aware id (NOT the Android literal): on iOS a @platform:mobile feature's app is the
120
+ // dual-id bundle id — a literal appPackage here would throw and block the whole spec.
121
+ // (__resolveAppId is a hoisted function declaration in this describe scope.)
122
+ const __AUTH_APP__ = __resolveAppId();
123
+ try { await driver.terminateApp(__AUTH_APP__); } catch { /* not running */ }
124
+ await driver.activateApp(__AUTH_APP__);
125
+ {{/if}}
126
+ await __ensureAuth('{{singleAuthRole}}');
127
+ });
128
+
129
+ {{/if}}
130
+ {{#if appPackage}}
131
+ // Multi-app: this feature's app (from the `Path:` line). Reset to its launch screen before each
132
+ // scenario so one `wdio run` can host screens from different apps — each spec launches its own.
133
+ // The `Path:` holds the ANDROID package (or, for an @platform:ios feature, the iOS bundle id).
134
+ const __APP_PKG_DEFAULT__ = '{{appPackage}}';
135
+ {{#if iosBundleId}}
136
+ // Per-feature iOS bundle id from the dual-id `Path: <androidPkg>/<activity> | <iosBundleId>` —
137
+ // app ids differ per OS (e.g. an iOS `.dev` flavor), and keeping it in the feature file makes the
138
+ // spec self-describing + lets one iOS run host multiple apps.
139
+ const __IOS_BUNDLE__ = '{{iosBundleId}}';
140
+ {{/if}}
141
+ // Platform-aware app id: an @platform:mobile spec runs on BOTH OSes. On iOS prefer the feature's own
142
+ // bundle id (dual-id Path), then the session cap (IOS_BUNDLE_ID in wdio.conf), then the Path value;
143
+ // on Android (or any non-iOS session) use the Path package unchanged.
144
+ function __resolveAppId(): string {
145
+ if (typeof driver !== 'undefined' && driver.isIOS) {
146
+ const caps = (driver.requestedCapabilities || {}) as Record<string, string>;
147
+ return {{#if iosBundleId}}__IOS_BUNDLE__ || {{/if}}caps['appium:bundleId'] || caps['bundleId'] || __APP_PKG_DEFAULT__;
148
+ }
149
+ return __APP_PKG_DEFAULT__;
150
+ }
151
+ beforeEach(async () => {
152
+ const __APP_PKG__ = __resolveAppId();
153
+ try { await driver.terminateApp(__APP_PKG__); } catch { /* app may not be running yet */ }
154
+ await driver.activateApp(__APP_PKG__);
155
+ // Settle after a cold (re)launch: a Flutter app first renders a BLANK accessibility skeleton
156
+ // (nested FrameLayout/View, no content-desc) for several seconds before the real tree appears.
157
+ // That skeleton is static, so a "two equal reads" check alone false-positives on it and we'd
158
+ // proceed before any element exists (→ every find fails). So require the tree to (a) contain
159
+ // rendered content — at least one non-empty content-desc/text — AND (b) be stable across two
160
+ // reads. A generous timeout covers slow cold renders on a stressed emulator. Best-effort: a
161
+ // timeout never fails the scenario, it just falls through to the step's own auto-wait.
162
+ let __prevSrc = '';
163
+ await driver
164
+ .waitUntil(
165
+ async () => {
166
+ const src = await driver.getPageSource();
167
+ const hasContent = /content-desc="[^"]+"|\btext="[^"]+"/.test(src);
168
+ const stable = hasContent && src === __prevSrc;
169
+ __prevSrc = src;
170
+ return stable;
171
+ },
172
+ { timeout: 30000, interval: 400, timeoutMsg: 'UI did not settle (no rendered content)' },
173
+ )
174
+ .catch(() => { /* best-effort — fall through to per-step auto-wait */ });
175
+ {{#if backgroundSteps}}
176
+ // Background runs here (inside beforeEach) — the app resets to its launch screen every scenario,
177
+ // so Background setup (nav, dismiss launch promos…) must re-run after each relaunch, not once.
178
+ {{#each backgroundSteps}}
179
+ {{#if comment}}
180
+ // {{comment}}
181
+ {{/if}}
182
+ {{code}}
183
+ {{/each}}
184
+ {{/if}}
185
+ });
186
+
187
+ {{/if}}
188
+ {{#unless appPackage}}
189
+ {{#if backgroundSteps}}
190
+ before(async () => {
191
+ {{#each backgroundSteps}}
192
+ {{#if comment}}
193
+ // {{comment}}
194
+ {{/if}}
195
+ {{code}}
196
+ {{/each}}
197
+ });
198
+
199
+ {{/if}}
200
+ {{/unless}}
201
+ {{#if beforeAll}}
202
+ {{beforeAll}}
203
+
204
+ {{/if}}
205
+ {{#if afterEach}}
206
+ {{afterEach}}
207
+
208
+ {{/if}}
209
+ {{#if afterAll}}
210
+ {{afterAll}}
211
+
212
+ {{/if}}
213
+ {{#if authGroups}}
214
+ {{#each authGroups}}
215
+ {{#each scenarios}}
216
+ {{this}}
217
+
218
+ {{/each}}
219
+ {{/each}}
220
+ {{else}}
221
+ {{#each scenarios}}
222
+ {{this}}
223
+
224
+ {{/each}}
225
+ {{/if}}
226
+ });
@@ -1,12 +1,19 @@
1
1
  export { TestGeneratorAdapter, TestFileData, ScenarioData, StepTemplateData, LocatorExpression } from './adapter-interface';
2
2
  export { AdapterRegistry, adapterRegistry, DriverNotInstalledError } from './adapter-registry';
3
3
  export { PlaywrightAdapter } from './playwright/playwright-adapter';
4
+ export { AppiumAdapter } from './appium/appium-adapter';
4
5
 
5
6
  // Auto-register built-in adapters
6
7
  import { adapterRegistry } from './adapter-registry';
7
8
  import { PlaywrightAdapter } from './playwright/playwright-adapter';
9
+ import { AppiumAdapter } from './appium/appium-adapter';
8
10
 
9
11
  adapterRegistry.register('playwright', () => new PlaywrightAdapter());
10
12
  // Phase 2a: platform alias. `web` is the bundled Playwright adapter (back-compat
11
13
  // baseline) until it is externalized to @sungen/driver-web in a later cut.
12
14
  adapterRegistry.register('web', () => new PlaywrightAdapter());
15
+ // MOB-2: the Appium codegen adapter — template strings only, NO appium/wdio dep in core.
16
+ // `platform: mobile` (qa/capabilities.yaml) loads @sungen/driver-mobile (the SPI capability)
17
+ // and selects this adapter (the runtime codegen). `appium` is the explicit alias.
18
+ adapterRegistry.register('mobile', () => new AppiumAdapter());
19
+ adapterRegistry.register('appium', () => new AppiumAdapter());
@@ -76,7 +76,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
76
76
  const FUNCTIONAL_TAG_PREFIXES = [
77
77
  '@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
78
78
  '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
79
- '@flow', '@cases:',
79
+ '@flow', '@cases:', '@deferred:', '@owned-by:',
80
80
  ];
81
81
 
82
82
  function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
@@ -173,7 +173,8 @@ function getEffectiveAuthRole(
173
173
  * @manual at scenario level → skip that scenario
174
174
  */
175
175
  function isManual(tags: string[]): boolean {
176
- return tags.some(tag => tag === '@manual');
176
+ // @deferred:flow is owned by a flow → not automated on this screen, so it skips like @manual (H6).
177
+ return tags.some(tag => tag === '@manual' || tag === '@deferred:flow');
177
178
  }
178
179
 
179
180
  /**
@@ -1,9 +1,9 @@
1
1
  import { ParsedStep } from '../gherkin-parser';
2
2
  import { TemplateEngine } from './template-engine';
3
+ import { adapterRegistry } from './adapters';
3
4
  import { PatternRegistry, PatternContext } from './patterns';
4
5
  import { SelectorResolver } from './utils/selector-resolver';
5
6
  import { DataResolver } from './utils/data-resolver';
6
- import * as path from 'path';
7
7
 
8
8
  export interface MappedStep {
9
9
  code: string; // Generated Playwright code
@@ -35,15 +35,18 @@ export class StepMapper {
35
35
  private inRowScope: boolean = false;
36
36
  private rowScopeTable: string = '';
37
37
 
38
- constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean } = {}) {
38
+ constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean; framework?: string } = {}) {
39
39
  this.verbose = options.verbose ?? false;
40
40
  this.baseURL = options.baseURL || null; // null means path-only navigation
41
41
  this.featureName = options.featureName;
42
42
  this.screenName = options.screenName;
43
43
  this.featurePath = options.featurePath;
44
- // Use Playwright templates as default for pattern-based generation
45
- const playwrightTemplatesDir = path.join(__dirname, 'adapters', 'playwright', 'templates');
46
- this.templateEngine = new TemplateEngine(playwrightTemplatesDir);
44
+ // Render step templates from the ACTIVE adapter's template set (web→Playwright,
45
+ // mobile→Appium). The adapter owns its templatesDir; resolving it here keeps step
46
+ // rendering and the file skeleton (code-generator) on the same framework.
47
+ const framework = options.framework || 'web';
48
+ const templatesDir = adapterRegistry.getAdapter(framework).templatesDir;
49
+ this.templateEngine = new TemplateEngine(templatesDir);
47
50
  this.patternRegistry = new PatternRegistry();
48
51
  this.selectorResolver = new SelectorResolver(undefined, options.screenName);
49
52
  this.dataResolver = new DataResolver(undefined, options.screenName, options.runtimeData);
@@ -168,6 +168,18 @@ export class TemplateEngine {
168
168
  const dialogRootContent = fs.readFileSync(dialogRootPath, 'utf-8');
169
169
  Handlebars.registerPartial('dialog-root', dialogRootContent);
170
170
  }
171
+
172
+ // Adapter-agnostic: register any OTHER top-level partial in partials/ by its basename
173
+ // (the named registrations above are playwright's; the appium adapter ships
174
+ // appium-selector / appium-selector-expr). Idempotent — re-registering by name is harmless.
175
+ const partialsDir = path.join(this.stepsTemplatesDir, 'partials');
176
+ if (fs.existsSync(partialsDir)) {
177
+ for (const f of fs.readdirSync(partialsDir)) {
178
+ if (!f.endsWith('.hbs')) continue;
179
+ const name = f.replace(/\.hbs$/, '');
180
+ Handlebars.registerPartial(name, fs.readFileSync(path.join(partialsDir, f), 'utf-8'));
181
+ }
182
+ }
171
183
  }
172
184
 
173
185
  private loadTemplate(templateName: string, isStepTemplate: boolean = false): HandlebarsTemplateDelegate {
@@ -179,7 +191,7 @@ export class TemplateEngine {
179
191
 
180
192
  // Try to find template in organized folders
181
193
  if (isStepTemplate) {
182
- const folders = ['actions', 'assertions', 'navigation', 'setup', 'partials'];
194
+ const folders = ['actions', 'assertions', 'navigation', 'setup', 'gestures', 'partials'];
183
195
 
184
196
  for (const folder of folders) {
185
197
  const templatePath = path.join(baseDir, folder, `${templateName}.hbs`);
@@ -12,8 +12,8 @@ import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } f
12
12
  import { featureBasename } from './unit-paths';
13
13
  import {
14
14
  loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
15
- automatableManual, flowCoveredThemes,
16
- GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog, AutomatableManualResult,
15
+ automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength,
16
+ GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog, AutomatableManualResult, FlowDepthResult, OracleStrengthResult,
17
17
  } from './sensors';
18
18
  import { loadFlowScenarios } from './flow-check';
19
19
  import { manualReasonMismatches, MANUAL_REASONS, buildPlan } from './capability-plan';
@@ -22,7 +22,7 @@ import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
22
22
  import { getProvenance, Provenance } from './provenance';
23
23
  import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
24
24
  import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
25
- negativeSideEffect, sourceBacked, crossArtifactOwnership } from './quality-gates';
25
+ negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk } from './quality-gates';
26
26
  import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-ledger';
27
27
  import { capabilityRegistry } from '../capabilities/registry';
28
28
  import { discoverAndRegisterCapabilities } from '../capabilities/discover';
@@ -42,6 +42,8 @@ export interface AuditReport {
42
42
  downstream: DownstreamResult; // downstream screens referenced but under-covered
43
43
  manualOracle: ManualOracleResult; // @manual scenarios lacking setup/action/oracle
44
44
  automatableManual: AutomatableManualResult; // @manual that is actually automatable (deferred, not judgment) — TQ-2
45
+ flowDepth: FlowDepthResult; // H3 — stateful-flow regression depth (count / teardown / multi-source)
46
+ oracle: OracleStrengthResult; // H4 — facet-oracle strength (weak name-substring vs title/detail/API/DB)
45
47
  ledger: LedgerResult; // atomic viewpoint-item coverage (per-bullet status)
46
48
  calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
47
49
  axes: Record<string, number>;
@@ -81,9 +83,28 @@ function catalogIdFromScreenDir(screenDir: string): string {
81
83
  * future `mobile/<x>` or `perf/<x>` unit routes to that capability with no core change. `flows/<flow>`
82
84
  * has no `flows` capability → default (UI), which is correct (flows are a UI concept).
83
85
  */
84
- export function scoringCapabilityFor(catalogScreenName: string, defaultCap: string | undefined): string | undefined {
86
+ export function scoringCapabilityFor(catalogScreenName: string, defaultCap: string | undefined, platform?: string): string | undefined {
85
87
  const seg = catalogScreenName.split('/')[0];
86
- return seg && capabilityRegistry.get(seg) ? seg : defaultCap;
88
+ // Path segment wins (api/<area> → api). Then the active platform capability (mobile → mobile),
89
+ // when a driver registered one with that id. Else the default (web/bare screen → ui — unchanged,
90
+ // since `web` has no capability of its own). See docs/spec/sungen-platform-capability-routing-spec.md.
91
+ if (seg && capabilityRegistry.get(seg)) return seg;
92
+ if (platform && capabilityRegistry.get(platform)) return platform;
93
+ return defaultCap;
94
+ }
95
+
96
+ /**
97
+ * H7 — senior-grade band. The top decile (≥9) is reserved for suites that ALSO clear the senior
98
+ * axes: a stateful flow with FULL regression depth (count + teardown + multi-source), no weak facet
99
+ * oracle, and no parallel-cart isolation risk. Otherwise the score is held just below 9 (8.9), so
100
+ * "≥9" means senior-grade — not merely "themes covered". Neutral for screens/api (no signals → 10).
101
+ */
102
+ export function seniorBandedOverall(
103
+ rawOverall: number,
104
+ s: { flowStateful: boolean; flowRatio: number; oracleWeak: number; isolationRisk: boolean },
105
+ ): number {
106
+ const seniorGrade = (!s.flowStateful || s.flowRatio >= 1) && s.oracleWeak === 0 && !s.isolationRisk;
107
+ return Math.min(rawOverall, seniorGrade ? 10 : 8.9);
87
108
  }
88
109
 
89
110
  export function runAudit(screenDir: string, screenName: string): AuditReport {
@@ -106,8 +127,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
106
127
  // UI capability). A capability that provides no catalog/gate falls back to the in-core UI
107
128
  // functions, so UI units — and api units until AO-2 adds the api providers — are byte-identical.
108
129
  discoverAndRegisterCapabilities();
130
+ // The active platform (web | mobile | …) activates its own capability for scoring + sensor routing.
131
+ // `web` has no capability of its own → scoringCap stays the default `ui` (byte-identical).
132
+ const platform = readCapabilities(projectRootFromScreenDir(screenDir)).platform;
109
133
  const defaultCap = capabilityRegistry.defaultCapabilityId();
110
- const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap);
134
+ const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap, platform);
111
135
  const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
112
136
  const catalog = (scoringCap?.viewpoints?.() as Catalog | undefined) || loadCatalog();
113
137
  const spec = specCoverage(specPath, scenarios, featureText);
@@ -157,16 +181,43 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
157
181
  const ownership = crossArtifactOwnership(screenDir, scenarios);
158
182
  const unsourced = sourceBacked(scenarios, parseSpecClauses(specPath).frs.map((f) => f.id), parseViewpointItems(viewpointPath).map((i) => i.text), viewpoints.map((v) => v.id), featureText);
159
183
 
184
+ // H3 — stateful-flow regression depth. For a UI flow whose scenarios mutate a cart/checkout
185
+ // collection, the regression dimensions (count/quantity proof · teardown · multi-source) cap the
186
+ // businessDepth headroom: it can reach 1.0 only when all three are exercised, so a present-but-
187
+ // shallow flow can't claim a perfect score (floor 0.5 — assertion depth still dominates).
188
+ const isUiFlow = /^flows\//.test(catalogScreenName);
189
+ const flowDepth = isUiFlow ? flowRegressionDepth(scenarios) : { stateful: false, countProof: false, teardown: false, multiSource: false, ratio: 1, missing: [] } as FlowDepthResult;
190
+ const FLOW_DEPTH_FLOOR = 0.5;
191
+ // H4 — oracle strength: a weak facet oracle (name-substring "proves" category/brand membership)
192
+ // caps businessDepth the same way (floor 0.5). A suite with no facet claim, or a strong oracle, is
193
+ // neutral (ratio 1 → no cap), so existing snapshots are unaffected.
194
+ const oracle = oracleStrength(scenarios);
195
+ const ORACLE_FLOOR = 0.5;
196
+
160
197
  // Sub-scores
161
198
  const coverage = gate.coverageRatio;
162
- const businessDepth = depth.bcDepthRatio;
163
- const balanceScore = balance.coreCount + balance.secondaryCount > 0
164
- ? Math.min(1, balance.coreCount / Math.max(1, balance.secondaryCount))
165
- : 1;
199
+ const businessDepth = Math.min(
200
+ depth.bcDepthRatio,
201
+ flowDepth.stateful ? FLOW_DEPTH_FLOOR + (1 - FLOW_DEPTH_FLOOR) * flowDepth.ratio : 1,
202
+ oracle.weak.length ? ORACLE_FLOOR + (1 - ORACLE_FLOOR) * oracle.ratio : 1,
203
+ );
204
+ // When the taxonomy drifted (most scenarios unclassified), the balance axis is unreliable — cap it
205
+ // at 0.5 instead of awarding a vacuous 1.0 so a stale taxonomy fails loudly, not silently (H1).
206
+ const balanceScore = balance.unclassifiedRatio > 0.4
207
+ ? 0.5
208
+ : balance.coreCount + balance.secondaryCount > 0
209
+ ? Math.min(1, balance.coreCount / Math.max(1, balance.secondaryCount))
210
+ : 1;
166
211
  const traceScore = 0.5 * trace.withVpCodeRatio + 0.5 * trace.mappedRatio;
167
212
 
168
213
  // Business-weighted overall (coverage + depth dominate)
169
- const overall = (0.4 * coverage + 0.3 * businessDepth + 0.15 * balanceScore + 0.15 * traceScore) * 10;
214
+ const rawOverall = (0.4 * coverage + 0.3 * businessDepth + 0.15 * balanceScore + 0.15 * traceScore) * 10;
215
+ // H7 — senior-grade band: the top decile (≥9) is reserved for suites that also clear the senior
216
+ // axes — a stateful flow with FULL regression depth (count + teardown + multi-source), no weak
217
+ // facet oracle, and no parallel-cart isolation risk. Otherwise the score is held just below 9, so
218
+ // "≥9" means senior-grade, not merely "themes covered". Neutral for screens/api (no signals).
219
+ const isoRisk = isolationRisk(featureText, flowDepth.stateful);
220
+ const overall = seniorBandedOverall(rawOverall, { flowStateful: flowDepth.stateful, flowRatio: flowDepth.ratio, oracleWeak: oracle.weak.length, isolationRisk: isoRisk });
170
221
 
171
222
  const findings: string[] = [];
172
223
  for (const c of flowCredits) {
@@ -187,6 +238,23 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
187
238
  `add data assertions (\`... with {{value}}\`, \`see all ... contain {{v}}\`) or, if cross-screen, defer to a flow with @manual + reason.`,
188
239
  );
189
240
  }
241
+ // H5 — state isolation: a @parallel stateful flow that mutates the cart needs per-scenario
242
+ // isolation, else count/quantity asserts go flaky. Warn (advisory) when no mitigation is present
243
+ // (@cleanup:cart / @isolate / a "Given … empty cart" background / fresh context).
244
+ if (isoRisk) {
245
+ findings.push('ISOLATION-RISK: this @parallel flow mutates the cart but has no per-scenario isolation → cart/count/quantity asserts can go flaky when scenarios share state. Add `@cleanup:cart` (or `@isolate`, or a `Given User has an empty cart` background) so each scenario starts clean.');
246
+ }
247
+ if (flowDepth.stateful && flowDepth.missing.length) {
248
+ const how: Record<string, string> = {
249
+ 'count-proof': 'assert the cart ROW COUNT / item QUANTITY (e.g. `... table with {{two_rows}}`, `Quantity column with {{qty}}`), not just the row presence',
250
+ 'teardown': 'add a REMOVE/clear scenario that returns the cart to its empty state (the inverse operation)',
251
+ 'multi-source': 'add to the cart from EVERY source on the page (the main list AND the recommended/related rail), not just one',
252
+ };
253
+ findings.push(`FLOW-DEPTH: this stateful flow exercises ${3 - flowDepth.missing.length}/3 regression dimensions — missing [${flowDepth.missing.join(', ')}] → ${flowDepth.missing.map((m) => how[m]).join('; ')}. (businessDepth is capped until covered.)`);
254
+ }
255
+ for (const w of oracle.weak) {
256
+ findings.push(`ORACLE-WEAK: "${w.name}" — ${w.hint}`);
257
+ }
190
258
  for (const u of claim.unproven) {
191
259
  const tag = u.severity === 'fail' ? 'CLAIM-UNPROVEN' : 'CLAIM-WEAK';
192
260
  findings.push(`${tag}: "${u.name}" — title claims [${u.claim}] but steps lack ${u.need}. ${u.hint}`);
@@ -194,7 +262,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
194
262
  for (const m of taxonomy.mislabeled) {
195
263
  findings.push(`VP-MISLABEL: "${m.name}" is coded VP-${m.current} but reads as ${m.suggested} (signal: "${m.signal}") → re-tag VP-${m.suggested}-NNN so the coverage matrix isn't skewed.`);
196
264
  }
197
- if (balance.imbalanced) {
265
+ if (balance.unclassifiedRatio > 0.4) {
266
+ findings.push(`TAXONOMY-UNCLASSIFIED: ${balance.note} → align the VP-<CATEGORY> codes with the catalog (or extend the bucket keywords) so coverage-balance is meaningful.`);
267
+ } else if (balance.imbalanced) {
198
268
  findings.push(`BALANCE: ${balance.note} Stop expanding secondary viewpoints until business-core gaps are filled.`);
199
269
  }
200
270
  if (trace.mappedRatio < 0.5) {
@@ -273,7 +343,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
273
343
  ...(scenarios.some((s) => s.queryRefs && s.queryRefs.length) ? ['@query'] : []),
274
344
  ...(scenarios.some((s) => s.apiRefs && s.apiRefs.length) ? ['@api'] : []),
275
345
  ];
276
- const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags }).gateSensorIds;
346
+ const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags, platform }).gateSensorIds;
277
347
  const gateSensorFindings = capabilityRegistry.sensors('gate')
278
348
  .filter((s) => routedGateIds.includes(s.id))
279
349
  .flatMap((s) => s.run({ screenName: catalogScreenName, cwd: projectRootFromScreenDir(screenDir), featureText, scenarios, universalGaps: gate.universalGaps }));
@@ -315,7 +385,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
315
385
  screen: screenName,
316
386
  scenarioCount: scenarios.length,
317
387
  gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
318
- taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, ledger, calibration,
388
+ taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, calibration,
319
389
  score: {
320
390
  overall: Math.round(overall * 10) / 10,
321
391
  coverage: Math.round(coverage * 100) / 100,
@@ -56,7 +56,7 @@ export function classifyReason(text: string): string {
56
56
  return '';
57
57
  }
58
58
 
59
- interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason: string }
59
+ interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason: string; deferredToFlow: boolean; ownedBy?: string }
60
60
 
61
61
  /** Parse scenarios with their tags + the reason comment line above (for @manual). */
62
62
  export function parseScenarios(featurePath: string): ParsedScenario[] {
@@ -84,7 +84,10 @@ export function parseScenarios(featurePath: string): ParsedScenario[] {
84
84
  else if (l === '') continue;
85
85
  else break; // a real step → stop
86
86
  }
87
- out.push({ name: m[1].trim(), tags, manual: tags.some((t) => /^@manual\b/i.test(t)), reason });
87
+ const deferredToFlow = tags.some((t) => /^@deferred:flow$/i.test(t));
88
+ const ownedBy = (tags.find((t) => /^@owned-by:/i.test(t)) || '').slice('@owned-by:'.length) || undefined;
89
+ // @deferred:flow accounts like @manual on the screen (owned by a flow, not automated here) (H6).
90
+ out.push({ name: m[1].trim(), tags, manual: tags.some((t) => /^@manual\b/i.test(t)) || deferredToFlow, reason, deferredToFlow, ownedBy });
88
91
  }
89
92
  return out;
90
93
  }
@@ -31,7 +31,7 @@ drivers:
31
31
  mobile:
32
32
  kind: platform
33
33
  package: "@sungen/driver-mobile"
34
- status: planned # PoC on the feat/mobile branch (Appium / Flutter)
34
+ status: shipped # @sungen/driver-mobile Appium/WebdriverIO, gesture steps + web-parity gate
35
35
  runtime: appium
36
36
  adapter: mobile
37
37
  capabilities: ["@ui"]
@@ -74,14 +74,23 @@ export function buildFlowCheck(cwd: string, onlyFlow?: string): FlowCheckReport
74
74
  const deferrals: Deferral[] = [];
75
75
  for (const sc of screens) {
76
76
  for (const s of parseScenarios(featurePath(cwd, 'screens', sc))) {
77
- if (!s.manual || !/deferred to a flow/i.test(s.reason)) continue;
78
- const targets = targetsFromHint(s.reason);
79
- const matches = flowScenarios.filter((fs2) => targets.some((t) => fs2.haystack.includes(t)));
77
+ // A deferral is the first-class `@deferred:flow` tag (H6) OR the legacy `@manual` + a
78
+ // "deferred to a flow" comment (back-compat). Either marks a cross-screen case owned by a flow.
79
+ const isDeferral = s.deferredToFlow || (s.manual && /deferred to a flow/i.test(s.reason));
80
+ if (!isDeferral) continue;
81
+ // Targets come from the comment hint; a tag-only @deferred:flow (no comment) falls back to the
82
+ // scenario TITLE so the covering flow scenario can still be located.
83
+ const targets = targetsFromHint([s.reason, s.name].join(' '));
84
+ // `@owned-by:<flow>` names the owner explicitly → only that flow's scenarios can cover it
85
+ // (a false @owned-by is then surfaced as missing). Else any flow may cover it (legacy).
86
+ const pool = s.ownedBy ? flowScenarios.filter((fs2) => fs2.flow === s.ownedBy) : flowScenarios;
87
+ const matches = pool.filter((fs2) => targets.some((t) => fs2.haystack.includes(t)));
80
88
  let verdict: Deferral['verdict'] = 'missing';
81
89
  let via: string | undefined;
82
90
  if (matches.some((m) => m.deep)) { verdict = 'covered'; via = matches.find((m) => m.deep)!.flow; }
83
91
  else if (matches.length) { verdict = 'shallow'; via = matches[0].flow; }
84
- deferrals.push({ screen: sc, scenario: s.name, hint: s.reason, targets, verdict, via });
92
+ const hint = s.ownedBy ? `${s.reason || 'deferred to a flow'} (owned-by: ${s.ownedBy})` : s.reason;
93
+ deferrals.push({ screen: sc, scenario: s.name, hint, targets, verdict, via });
85
94
  }
86
95
  }
87
96
 
@@ -34,6 +34,8 @@ export interface ScenarioInfo {
34
34
  queryRefs?: string[]; // named queries referenced by this scenario (inline `query [name]` + @query: tags)
35
35
  apiRefs?: string[]; // named API endpoints referenced by this scenario (@api: tags)
36
36
  requiresCaps?: string[]; // @requires:<cap> — automation-ready but needs an opt-in driver (TQ-11)
37
+ deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
38
+ ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
37
39
  }
38
40
 
39
41
  /** Format-tolerant: is this token an ID (project's scheme), not a prose word?
@@ -101,7 +103,10 @@ const PRIORITY_TAGS: Record<string, Priority> = { '@high': 'high', '@normal': 'n
101
103
 
102
104
  function classifyScenario(sc: ParsedScenario): ScenarioInfo {
103
105
  const tags = sc.tags || [];
104
- const manual = tags.includes('@manual');
106
+ const deferredToFlow = tags.includes('@deferred:flow');
107
+ const ownedByFlow = (tags.find((t: string) => /^@owned-by:/i.test(t)) || '').slice('@owned-by:'.length) || undefined;
108
+ // @deferred:flow is owned by a flow → not automated on this screen, so it accounts like @manual (H6).
109
+ const manual = tags.includes('@manual') || deferredToFlow;
105
110
  const casesTag = tags.find((t) => t.startsWith('@cases:'));
106
111
  const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
107
112
  // Named-query references: @query:<name>[(overrides)] tags + inline `query [name]` step refs.
@@ -118,7 +123,10 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
118
123
  let priority: Priority = 'unknown';
119
124
  for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
120
125
 
121
- const codeMatch = sc.name.match(/\bVP-([A-Z]+)-\d+/i);
126
+ // Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
127
+ // compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
128
+ // not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
129
+ const codeMatch = sc.name.match(/\bVP-([A-Z]+(?:-[A-Z]+)*)-\d+/i);
122
130
  const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
123
131
  const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
124
132
  // Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
@@ -173,6 +181,8 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
173
181
  queryRefs: queryRefs.size ? [...queryRefs] : undefined,
174
182
  apiRefs: apiRefs.size ? [...apiRefs] : undefined,
175
183
  requiresCaps: requiresCaps.length ? requiresCaps : undefined,
184
+ deferredToFlow: deferredToFlow || undefined,
185
+ ownedByFlow,
176
186
  };
177
187
  }
178
188
 
@@ -20,7 +20,10 @@ function downstreamRoutes(specText: string): string[] {
20
20
  const routes = new Set<string>();
21
21
  for (const line of specText.split('\n')) {
22
22
  if (!/success|navigat|to \(|→/i.test(line)) continue;
23
- for (const m of line.matchAll(/`?(\/[a-z][a-z0-9/_-]+)`?/gi)) {
23
+ // A real route's leading `/` sits at a path boundary (start, whitespace, backtick, quote, paren),
24
+ // NOT after a letter/digit. The lookbehind rejects prose slashes like "text/icon" or
25
+ // "category/brand" that aren't routes at all (H2 — they produced /icon, /button, /brand).
26
+ for (const m of line.matchAll(/(?<![A-Za-z0-9])(\/[a-z][a-z0-9/_-]+)`?/gi)) {
24
27
  const r = m[1];
25
28
  if (r !== ownRoute && r.split('/').length > ownRoute.split('/').length - 0) routes.add(r);
26
29
  }
@@ -29,6 +32,16 @@ function downstreamRoutes(specText: string): string[] {
29
32
  return [...routes].filter((r) => r !== ownRoute && (!ownRoute || r.startsWith(ownRoute + '/') || r.split('/').length >= 3));
30
33
  }
31
34
 
35
+ /**
36
+ * H5 — a @parallel stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios
37
+ * share state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
38
+ * browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
39
+ */
40
+ export function isolationRisk(featureText: string, stateful: boolean): boolean {
41
+ if (!stateful || !/@parallel\b/i.test(featureText)) return false;
42
+ return !/@cleanup:cart\b|@isolate\b|empty cart|fresh (?:browser )?context|new context/i.test(featureText);
43
+ }
44
+
32
45
  export function downstreamScope(specText: string, scenarios: ScenarioInfo[]): DownstreamResult {
33
46
  const routes = downstreamRoutes(specText);
34
47
  const underCovered: { route: string; slug: string }[] = [];