@sun-asterisk/sungen 3.2.32 → 3.2.34-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 (503) hide show
  1. package/dist/cli/commands/audit.d.ts +5 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +64 -11
  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 +40 -18
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/changed.d.ts.map +1 -1
  9. package/dist/cli/commands/changed.js +2 -3
  10. package/dist/cli/commands/changed.js.map +1 -1
  11. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  12. package/dist/cli/commands/dashboard.js +2 -28
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/defect.d.ts +7 -0
  15. package/dist/cli/commands/defect.d.ts.map +1 -0
  16. package/dist/cli/commands/defect.js +68 -0
  17. package/dist/cli/commands/defect.js.map +1 -0
  18. package/dist/cli/commands/delivery.d.ts.map +1 -1
  19. package/dist/cli/commands/delivery.js +113 -59
  20. package/dist/cli/commands/delivery.js.map +1 -1
  21. package/dist/cli/commands/generate.d.ts.map +1 -1
  22. package/dist/cli/commands/generate.js +14 -7
  23. package/dist/cli/commands/generate.js.map +1 -1
  24. package/dist/cli/commands/results-merge.d.ts +9 -0
  25. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  26. package/dist/cli/commands/results-merge.js +9 -12
  27. package/dist/cli/commands/results-merge.js.map +1 -1
  28. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  29. package/dist/cli/commands/results-sanitize.js +2 -13
  30. package/dist/cli/commands/results-sanitize.js.map +1 -1
  31. package/dist/cli/commands/select.d.ts +12 -0
  32. package/dist/cli/commands/select.d.ts.map +1 -0
  33. package/dist/cli/commands/select.js +122 -0
  34. package/dist/cli/commands/select.js.map +1 -0
  35. package/dist/cli/commands/triage.d.ts +7 -0
  36. package/dist/cli/commands/triage.d.ts.map +1 -0
  37. package/dist/cli/commands/triage.js +98 -0
  38. package/dist/cli/commands/triage.js.map +1 -0
  39. package/dist/cli/commands/update.d.ts.map +1 -1
  40. package/dist/cli/commands/update.js +3 -0
  41. package/dist/cli/commands/update.js.map +1 -1
  42. package/dist/cli/commands/viewpoint.d.ts +7 -0
  43. package/dist/cli/commands/viewpoint.d.ts.map +1 -0
  44. package/dist/cli/commands/viewpoint.js +70 -0
  45. package/dist/cli/commands/viewpoint.js.map +1 -0
  46. package/dist/cli/index.js +13 -2
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  49. package/dist/dashboard/snapshot-builder.js +6 -10
  50. package/dist/dashboard/snapshot-builder.js.map +1 -1
  51. package/dist/dashboard/templates/index.html +1 -1
  52. package/dist/dashboard/types.d.ts +1 -0
  53. package/dist/dashboard/types.d.ts.map +1 -1
  54. package/dist/exporters/csv-exporter.d.ts +4 -0
  55. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  56. package/dist/exporters/csv-exporter.js +12 -11
  57. package/dist/exporters/csv-exporter.js.map +1 -1
  58. package/dist/exporters/feature-parser.d.ts +29 -0
  59. package/dist/exporters/feature-parser.d.ts.map +1 -1
  60. package/dist/exporters/feature-parser.js +44 -1
  61. package/dist/exporters/feature-parser.js.map +1 -1
  62. package/dist/exporters/json-exporter.d.ts.map +1 -1
  63. package/dist/exporters/json-exporter.js +7 -0
  64. package/dist/exporters/json-exporter.js.map +1 -1
  65. package/dist/exporters/locale-variants.d.ts +7 -1
  66. package/dist/exporters/locale-variants.d.ts.map +1 -1
  67. package/dist/exporters/locale-variants.js +23 -16
  68. package/dist/exporters/locale-variants.js.map +1 -1
  69. package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
  70. package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
  71. package/dist/exporters/manual-hybrid-oracle.js +114 -0
  72. package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
  73. package/dist/exporters/matrix/build.d.ts.map +1 -1
  74. package/dist/exporters/matrix/build.js +68 -19
  75. package/dist/exporters/matrix/build.js.map +1 -1
  76. package/dist/exporters/matrix/carry-manual-results.d.ts +89 -0
  77. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  78. package/dist/exporters/matrix/carry-manual-results.js +302 -0
  79. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  80. package/dist/exporters/matrix/export.d.ts +9 -1
  81. package/dist/exporters/matrix/export.d.ts.map +1 -1
  82. package/dist/exporters/matrix/export.js +21 -3
  83. package/dist/exporters/matrix/export.js.map +1 -1
  84. package/dist/exporters/matrix/gates.js +6 -0
  85. package/dist/exporters/matrix/gates.js.map +1 -1
  86. package/dist/exporters/matrix/render-csv.js +1 -1
  87. package/dist/exporters/matrix/render-csv.js.map +1 -1
  88. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  89. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  90. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  91. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  92. package/dist/exporters/matrix/render-xlsx.js +3 -0
  93. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  94. package/dist/exporters/matrix/types.d.ts +13 -1
  95. package/dist/exporters/matrix/types.d.ts.map +1 -1
  96. package/dist/exporters/matrix/types.js.map +1 -1
  97. package/dist/exporters/matrix/wording.d.ts +8 -1
  98. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  99. package/dist/exporters/matrix/wording.js +37 -2
  100. package/dist/exporters/matrix/wording.js.map +1 -1
  101. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  102. package/dist/exporters/playwright-report-parser.js +1 -0
  103. package/dist/exporters/playwright-report-parser.js.map +1 -1
  104. package/dist/exporters/run-environment.d.ts +32 -0
  105. package/dist/exporters/run-environment.d.ts.map +1 -0
  106. package/dist/exporters/run-environment.js +125 -0
  107. package/dist/exporters/run-environment.js.map +1 -0
  108. package/dist/exporters/scenario-merger.d.ts +18 -0
  109. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  110. package/dist/exporters/scenario-merger.js +49 -3
  111. package/dist/exporters/scenario-merger.js.map +1 -1
  112. package/dist/exporters/test-data-resolver.d.ts +0 -4
  113. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  114. package/dist/exporters/test-data-resolver.js +0 -7
  115. package/dist/exporters/test-data-resolver.js.map +1 -1
  116. package/dist/exporters/types.d.ts +5 -0
  117. package/dist/exporters/types.d.ts.map +1 -1
  118. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  119. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  120. package/dist/exporters/xlsx-exporter.js +7 -2
  121. package/dist/exporters/xlsx-exporter.js.map +1 -1
  122. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  123. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  124. package/dist/generators/gherkin-parser/index.js +49 -19
  125. package/dist/generators/gherkin-parser/index.js.map +1 -1
  126. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  127. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  128. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  129. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  130. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  131. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  132. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  133. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  134. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  135. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  136. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  137. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  138. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  139. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  140. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  141. package/dist/generators/test-generator/code-generator.js +90 -5
  142. package/dist/generators/test-generator/code-generator.js.map +1 -1
  143. package/dist/generators/test-generator/diagnostics.d.ts +36 -5
  144. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  145. package/dist/generators/test-generator/diagnostics.js +35 -4
  146. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  147. package/dist/generators/test-generator/patterns/index.d.ts +12 -2
  148. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  149. package/dist/generators/test-generator/patterns/index.js +15 -9
  150. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  151. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  152. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  153. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  154. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  155. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  156. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  157. package/dist/generators/test-generator/step-mapper.d.ts +46 -0
  158. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  159. package/dist/generators/test-generator/step-mapper.js +84 -9
  160. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  161. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  162. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  163. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  164. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  165. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  166. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  167. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  168. package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
  169. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  170. package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
  171. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  172. package/dist/harness/audit.d.ts +3 -0
  173. package/dist/harness/audit.d.ts.map +1 -1
  174. package/dist/harness/audit.js +60 -19
  175. package/dist/harness/audit.js.map +1 -1
  176. package/dist/harness/capability.d.ts +7 -1
  177. package/dist/harness/capability.d.ts.map +1 -1
  178. package/dist/harness/capability.js +15 -12
  179. package/dist/harness/capability.js.map +1 -1
  180. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  181. package/dist/harness/config-file.d.ts +17 -0
  182. package/dist/harness/config-file.d.ts.map +1 -0
  183. package/dist/harness/config-file.js +100 -0
  184. package/dist/harness/config-file.js.map +1 -0
  185. package/dist/harness/counted-assertions.d.ts +2 -0
  186. package/dist/harness/counted-assertions.d.ts.map +1 -0
  187. package/dist/harness/counted-assertions.js +38 -0
  188. package/dist/harness/counted-assertions.js.map +1 -0
  189. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  190. package/dist/harness/data-driven-lint.js +69 -27
  191. package/dist/harness/data-driven-lint.js.map +1 -1
  192. package/dist/harness/defect-draft.d.ts +14 -0
  193. package/dist/harness/defect-draft.d.ts.map +1 -0
  194. package/dist/harness/defect-draft.js +143 -0
  195. package/dist/harness/defect-draft.js.map +1 -0
  196. package/dist/harness/defects.d.ts +24 -0
  197. package/dist/harness/defects.d.ts.map +1 -0
  198. package/dist/harness/defects.js +118 -0
  199. package/dist/harness/defects.js.map +1 -0
  200. package/dist/harness/depth-lint.d.ts.map +1 -1
  201. package/dist/harness/depth-lint.js +2 -1
  202. package/dist/harness/depth-lint.js.map +1 -1
  203. package/dist/harness/flow-check.d.ts.map +1 -1
  204. package/dist/harness/flow-check.js +6 -2
  205. package/dist/harness/flow-check.js.map +1 -1
  206. package/dist/harness/keyword-match.d.ts +59 -0
  207. package/dist/harness/keyword-match.d.ts.map +1 -0
  208. package/dist/harness/keyword-match.js +86 -0
  209. package/dist/harness/keyword-match.js.map +1 -0
  210. package/dist/harness/manifest.d.ts.map +1 -1
  211. package/dist/harness/manifest.js +2 -2
  212. package/dist/harness/manifest.js.map +1 -1
  213. package/dist/harness/next-step.d.ts +2 -4
  214. package/dist/harness/next-step.d.ts.map +1 -1
  215. package/dist/harness/next-step.js +50 -5
  216. package/dist/harness/next-step.js.map +1 -1
  217. package/dist/harness/parse.d.ts +45 -0
  218. package/dist/harness/parse.d.ts.map +1 -1
  219. package/dist/harness/parse.js +142 -24
  220. package/dist/harness/parse.js.map +1 -1
  221. package/dist/harness/perf.d.ts +7 -1
  222. package/dist/harness/perf.d.ts.map +1 -1
  223. package/dist/harness/perf.js +13 -10
  224. package/dist/harness/perf.js.map +1 -1
  225. package/dist/harness/quality-gates.d.ts +9 -2
  226. package/dist/harness/quality-gates.d.ts.map +1 -1
  227. package/dist/harness/quality-gates.js +22 -8
  228. package/dist/harness/quality-gates.js.map +1 -1
  229. package/dist/harness/query-catalog.js +0 -0
  230. package/dist/harness/query-catalog.js.map +1 -1
  231. package/dist/harness/read-text.d.ts +21 -0
  232. package/dist/harness/read-text.d.ts.map +1 -1
  233. package/dist/harness/read-text.js +32 -1
  234. package/dist/harness/read-text.js.map +1 -1
  235. package/dist/harness/script-check.d.ts.map +1 -1
  236. package/dist/harness/script-check.js +7 -1
  237. package/dist/harness/script-check.js.map +1 -1
  238. package/dist/harness/secret-scan.d.ts +27 -1
  239. package/dist/harness/secret-scan.d.ts.map +1 -1
  240. package/dist/harness/secret-scan.js +57 -4
  241. package/dist/harness/secret-scan.js.map +1 -1
  242. package/dist/harness/select-by-ids.d.ts +31 -0
  243. package/dist/harness/select-by-ids.d.ts.map +1 -0
  244. package/dist/harness/select-by-ids.js +106 -0
  245. package/dist/harness/select-by-ids.js.map +1 -0
  246. package/dist/harness/sensors.d.ts +20 -1
  247. package/dist/harness/sensors.d.ts.map +1 -1
  248. package/dist/harness/sensors.js +133 -39
  249. package/dist/harness/sensors.js.map +1 -1
  250. package/dist/harness/spec-branches.d.ts +0 -18
  251. package/dist/harness/spec-branches.d.ts.map +1 -1
  252. package/dist/harness/spec-branches.js +24 -3
  253. package/dist/harness/spec-branches.js.map +1 -1
  254. package/dist/harness/spec-coverage.d.ts.map +1 -1
  255. package/dist/harness/spec-coverage.js +14 -4
  256. package/dist/harness/spec-coverage.js.map +1 -1
  257. package/dist/harness/triage-signals.d.ts +20 -0
  258. package/dist/harness/triage-signals.d.ts.map +1 -0
  259. package/dist/harness/triage-signals.js +30 -0
  260. package/dist/harness/triage-signals.js.map +1 -0
  261. package/dist/harness/triage-unit.d.ts +23 -0
  262. package/dist/harness/triage-unit.d.ts.map +1 -0
  263. package/dist/harness/triage-unit.js +142 -0
  264. package/dist/harness/triage-unit.js.map +1 -0
  265. package/dist/harness/triage.d.ts +28 -0
  266. package/dist/harness/triage.d.ts.map +1 -0
  267. package/dist/harness/triage.js +99 -0
  268. package/dist/harness/triage.js.map +1 -0
  269. package/dist/harness/ui-inventory.d.ts +40 -0
  270. package/dist/harness/ui-inventory.d.ts.map +1 -0
  271. package/dist/harness/ui-inventory.js +191 -0
  272. package/dist/harness/ui-inventory.js.map +1 -0
  273. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  274. package/dist/harness/viewpoint-baseline.js +7 -1
  275. package/dist/harness/viewpoint-baseline.js.map +1 -1
  276. package/dist/harness/viewpoint-draft-header.d.ts +7 -0
  277. package/dist/harness/viewpoint-draft-header.d.ts.map +1 -0
  278. package/dist/harness/viewpoint-draft-header.js +100 -0
  279. package/dist/harness/viewpoint-draft-header.js.map +1 -0
  280. package/dist/harness/viewpoint-ledger.d.ts +13 -3
  281. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  282. package/dist/harness/viewpoint-ledger.js +106 -19
  283. package/dist/harness/viewpoint-ledger.js.map +1 -1
  284. package/dist/harness/viewpoint-mode.d.ts +9 -0
  285. package/dist/harness/viewpoint-mode.d.ts.map +1 -0
  286. package/dist/harness/viewpoint-mode.js +52 -0
  287. package/dist/harness/viewpoint-mode.js.map +1 -0
  288. package/dist/harness/viewpoint-priority.d.ts +19 -0
  289. package/dist/harness/viewpoint-priority.d.ts.map +1 -0
  290. package/dist/harness/viewpoint-priority.js +36 -0
  291. package/dist/harness/viewpoint-priority.js.map +1 -0
  292. package/dist/harness/viewpoint-review-open.d.ts +6 -0
  293. package/dist/harness/viewpoint-review-open.d.ts.map +1 -0
  294. package/dist/harness/viewpoint-review-open.js +90 -0
  295. package/dist/harness/viewpoint-review-open.js.map +1 -0
  296. package/dist/harness/viewpoint-scan-refs.d.ts +50 -0
  297. package/dist/harness/viewpoint-scan-refs.d.ts.map +1 -0
  298. package/dist/harness/viewpoint-scan-refs.js +200 -0
  299. package/dist/harness/viewpoint-scan-refs.js.map +1 -0
  300. package/dist/harness/viewpoint-scan.d.ts +41 -0
  301. package/dist/harness/viewpoint-scan.d.ts.map +1 -0
  302. package/dist/harness/viewpoint-scan.js +118 -0
  303. package/dist/harness/viewpoint-scan.js.map +1 -0
  304. package/dist/harness/write-atomic.d.ts +2 -0
  305. package/dist/harness/write-atomic.d.ts.map +1 -0
  306. package/dist/harness/write-atomic.js +62 -0
  307. package/dist/harness/write-atomic.js.map +1 -0
  308. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  309. package/dist/orchestrator/ai-skills/writer.js +15 -7
  310. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  311. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  312. package/dist/orchestrator/assets-drift.js +11 -6
  313. package/dist/orchestrator/assets-drift.js.map +1 -1
  314. package/dist/orchestrator/flow-manager.d.ts +0 -1
  315. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  316. package/dist/orchestrator/flow-manager.js +3 -52
  317. package/dist/orchestrator/flow-manager.js.map +1 -1
  318. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  319. package/dist/orchestrator/project-deps-sync.js +22 -1
  320. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  321. package/dist/orchestrator/project-initializer.d.ts +8 -0
  322. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  323. package/dist/orchestrator/project-initializer.js +49 -7
  324. package/dist/orchestrator/project-initializer.js.map +1 -1
  325. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  326. package/dist/orchestrator/screen-manager.js +8 -62
  327. package/dist/orchestrator/screen-manager.js.map +1 -1
  328. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  329. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  330. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  331. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  332. package/dist/orchestrator/security-overrides.d.ts +51 -0
  333. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  334. package/dist/orchestrator/security-overrides.js +165 -0
  335. package/dist/orchestrator/security-overrides.js.map +1 -0
  336. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  337. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  338. package/dist/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  339. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  340. package/dist/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  341. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  342. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  343. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +25 -1
  344. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  345. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  346. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  347. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  348. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  349. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  350. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  351. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  352. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  353. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  354. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  355. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  356. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  357. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  358. package/dist/orchestrator/templates/specs-url-assert.js +38 -4
  359. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  360. package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
  361. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  362. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  363. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  364. package/dist/orchestrator/trace-mode-install.js +132 -0
  365. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  366. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +23 -0
  367. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  368. package/dist/orchestrator/viewpoint-pattern-catalog.js +48 -0
  369. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  370. package/dist/orchestrator/viewpoint-scaffold.d.ts +3 -0
  371. package/dist/orchestrator/viewpoint-scaffold.d.ts.map +1 -0
  372. package/dist/orchestrator/viewpoint-scaffold.js +70 -0
  373. package/dist/orchestrator/viewpoint-scaffold.js.map +1 -0
  374. package/package.json +3 -3
  375. package/src/cli/commands/audit.ts +59 -12
  376. package/src/cli/commands/capability.ts +54 -19
  377. package/src/cli/commands/changed.ts +2 -4
  378. package/src/cli/commands/dashboard.ts +1 -26
  379. package/src/cli/commands/defect.ts +49 -0
  380. package/src/cli/commands/delivery.ts +106 -56
  381. package/src/cli/commands/generate.ts +15 -7
  382. package/src/cli/commands/results-merge.ts +9 -9
  383. package/src/cli/commands/results-sanitize.ts +2 -9
  384. package/src/cli/commands/select.ts +88 -0
  385. package/src/cli/commands/triage.ts +57 -0
  386. package/src/cli/commands/update.ts +3 -0
  387. package/src/cli/commands/viewpoint.ts +58 -0
  388. package/src/cli/index.ts +13 -2
  389. package/src/dashboard/snapshot-builder.ts +6 -8
  390. package/src/dashboard/templates/index.html +1 -1
  391. package/src/dashboard/types.ts +1 -0
  392. package/src/exporters/csv-exporter.ts +12 -11
  393. package/src/exporters/feature-parser.ts +44 -2
  394. package/src/exporters/json-exporter.ts +6 -1
  395. package/src/exporters/locale-variants.ts +23 -17
  396. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  397. package/src/exporters/matrix/build.ts +71 -19
  398. package/src/exporters/matrix/carry-manual-results.ts +292 -0
  399. package/src/exporters/matrix/export.ts +24 -4
  400. package/src/exporters/matrix/gates.ts +6 -0
  401. package/src/exporters/matrix/render-csv.ts +1 -1
  402. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  403. package/src/exporters/matrix/render-xlsx.ts +3 -0
  404. package/src/exporters/matrix/types.ts +13 -1
  405. package/src/exporters/matrix/wording.ts +36 -2
  406. package/src/exporters/playwright-report-parser.ts +1 -0
  407. package/src/exporters/run-environment.ts +89 -0
  408. package/src/exporters/scenario-merger.ts +51 -5
  409. package/src/exporters/test-data-resolver.ts +0 -6
  410. package/src/exporters/types.ts +5 -0
  411. package/src/exporters/xlsx-exporter.ts +7 -2
  412. package/src/generators/gherkin-parser/index.ts +50 -18
  413. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
  414. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  415. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  416. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  417. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  418. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  419. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  420. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  421. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  422. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  423. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  424. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  425. package/src/generators/test-generator/code-generator.ts +77 -5
  426. package/src/generators/test-generator/diagnostics.ts +36 -5
  427. package/src/generators/test-generator/patterns/index.ts +15 -8
  428. package/src/generators/test-generator/patterns/types.ts +15 -0
  429. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  430. package/src/generators/test-generator/step-mapper.ts +90 -8
  431. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  432. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  433. package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
  434. package/src/harness/audit.ts +69 -24
  435. package/src/harness/capability.ts +23 -12
  436. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  437. package/src/harness/config-file.ts +66 -0
  438. package/src/harness/counted-assertions.ts +33 -0
  439. package/src/harness/data-driven-lint.ts +82 -24
  440. package/src/harness/defect-draft.ts +115 -0
  441. package/src/harness/defects.ts +94 -0
  442. package/src/harness/depth-lint.ts +2 -1
  443. package/src/harness/flow-check.ts +6 -2
  444. package/src/harness/keyword-match.ts +85 -0
  445. package/src/harness/manifest.ts +2 -2
  446. package/src/harness/next-step.ts +47 -5
  447. package/src/harness/parse.ts +151 -27
  448. package/src/harness/perf.ts +15 -7
  449. package/src/harness/quality-gates.ts +22 -8
  450. package/src/harness/query-catalog.ts +0 -0
  451. package/src/harness/read-text.ts +32 -1
  452. package/src/harness/script-check.ts +7 -1
  453. package/src/harness/secret-scan.ts +60 -4
  454. package/src/harness/select-by-ids.ts +83 -0
  455. package/src/harness/sensors.ts +141 -40
  456. package/src/harness/spec-branches.ts +6 -3
  457. package/src/harness/spec-coverage.ts +14 -4
  458. package/src/harness/triage-signals.ts +46 -0
  459. package/src/harness/triage-unit.ts +113 -0
  460. package/src/harness/triage.ts +127 -0
  461. package/src/harness/ui-inventory.ts +164 -0
  462. package/src/harness/viewpoint-baseline.ts +7 -1
  463. package/src/harness/viewpoint-draft-header.ts +55 -0
  464. package/src/harness/viewpoint-ledger.ts +113 -25
  465. package/src/harness/viewpoint-mode.ts +49 -0
  466. package/src/harness/viewpoint-priority.ts +38 -0
  467. package/src/harness/viewpoint-review-open.ts +51 -0
  468. package/src/harness/viewpoint-scan-refs.ts +154 -0
  469. package/src/harness/viewpoint-scan.ts +111 -0
  470. package/src/harness/write-atomic.ts +22 -0
  471. package/src/orchestrator/ai-skills/writer.ts +18 -3
  472. package/src/orchestrator/assets-drift.ts +10 -5
  473. package/src/orchestrator/flow-manager.ts +3 -53
  474. package/src/orchestrator/project-deps-sync.ts +23 -1
  475. package/src/orchestrator/project-initializer.ts +54 -6
  476. package/src/orchestrator/screen-manager.ts +7 -61
  477. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  478. package/src/orchestrator/security-overrides.ts +149 -0
  479. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  480. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  481. package/src/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  482. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  483. package/src/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  484. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  485. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  486. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +25 -1
  487. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  488. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  489. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  490. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  491. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  492. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  493. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  494. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  495. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  496. package/src/orchestrator/templates/playwright.config.ts +4 -2
  497. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  498. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  499. package/src/orchestrator/templates/specs-url-assert.ts +47 -4
  500. package/src/orchestrator/templates/wdio.conf.ts +24 -1
  501. package/src/orchestrator/trace-mode-install.ts +94 -0
  502. package/src/orchestrator/viewpoint-pattern-catalog.ts +61 -0
  503. package/src/orchestrator/viewpoint-scaffold.ts +36 -0
@@ -7,7 +7,8 @@
7
7
  */
8
8
  import * as fs from 'fs';
9
9
  import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
10
- import { readTextFile } from './read-text';
10
+ import { readTextFile, readViewpointText } from './read-text';
11
+ import { parsePriority, toEntryPriority } from './viewpoint-priority';
11
12
  import { featureFilesFor } from './unit-paths';
12
13
 
13
14
  export type Priority = 'high' | 'normal' | 'low' | 'unknown';
@@ -15,15 +16,57 @@ export type Priority = 'high' | 'normal' | 'low' | 'unknown';
15
16
  export interface ViewpointEntry {
16
17
  id: string; // e.g. VP-DATA-CONSISTENCY
17
18
  priority: 'High' | 'Medium' | 'Low' | 'Unknown';
19
+ /** The declared priority word as written (`critical` · `high` · `medium` · `med` · `normal` · `low` ·
20
+ * `deferred` · `skip`, which also stands for skipped / n/a / none / not applicable), kept because `priority` folds several of them together and the
21
+ * PRIORITY-CONTRADICTED check needs them apart. Absent when no priority was declared. */
22
+ priorityWord?: string;
18
23
  reason: string;
19
24
  group?: 'Required' | 'Recommended' | 'Optional';
20
25
  }
21
26
 
27
+ /**
28
+ * Whole SECTIONS that declare something ABOUT the checklist rather than listing claims or
29
+ * viewpoints — known issues, spec gaps, traceability indexes, references and appendices, changelogs. Shared by the atomic
30
+ * ledger (their rows are not claims to prove, #630) and the viewpoint-overview parser (an id
31
+ * listed there is a reference, not a declaration, #833). `UI Patterns Identified` is the author's
32
+ * pick of WHICH sungen-viewpoint patterns apply — a `Skip` row there is not a claim to cover (#841).
33
+ * A `How to run` / `Procedure` section — that exact heading, optionally naming one id — is a manual
34
+ * check's steps, not claims of its own (#855); `How to test the login flow` stays a normal section.
35
+ */
36
+ export const NON_DECLARATION_SECTION = /^(?:(?:ui patterns?(?: identified)?|(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?)(?:\s+(?:vp|fl)-[^\s:]+)?)\s*:?$|(?:known issues?|open questions?|spec gaps?|appendi(?:x|ces)|phụ lục|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b)/i;
37
+
38
+ /**
39
+ * Every word a viewpoint row may declare as its priority — ONE list for the overview parser, the
40
+ * atomic ledger and the integrity checks, which each carried their own copy and all missed the
41
+ * `Skip` that the `sungen add` / `add-flow` scaffolds themselves suggest (#853). `deferred` and the
42
+ * not-applicable words are declarations as well: such a row is not a claim to cover.
43
+ */
44
+ export const PRIORITY_WORDS = 'critical|high|medium|med|normal|low|deferred|skip(?:ped)?|n\\/a|none|not applicable';
45
+ /** "Declared not applicable" — folded to `skip` so every consumer checks one word. */
46
+ const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/i;
47
+
48
+ // The whole cell must BE the word (emphasis aside), as the old `| VP-… | high |` scan required:
49
+ // "Low risk" or "Not high" declares no priority.
50
+ const PRIORITY_WORD = new RegExp(`^[\\s*_\`]*(${PRIORITY_WORDS})[\\s*_\`.]*$`, 'i');
51
+ /** Header names of a priority column, in the languages viewpoint files are written in. */
52
+ const PRIORITY_HEADER = /^(?:priority|pri|level|severity|優先度?|độ ưu tiên|ưu tiên)(?:\s|\(|$)/i;
53
+
54
+ /** A priority cell → the folded level plus the word as written. */
55
+ function priorityOf(cell: string): { priority: ViewpointEntry['priority']; priorityWord?: string } {
56
+ const raw = cell.match(PRIORITY_WORD)?.[1].toLowerCase();
57
+ const word = raw && NOT_APPLICABLE.test(raw) ? 'skip' : raw;
58
+ const priority = toEntryPriority(parsePriority(cell));
59
+ return word ? { priority, priorityWord: word } : { priority };
60
+ }
61
+
22
62
  export interface ScenarioInfo {
23
63
  name: string;
24
64
  vpCode?: string; // VP-CART-001 style code in the scenario title
25
65
  category?: string; // CART, LIST, VAL, ... (from the code)
26
66
  priority: Priority;
67
+ /** The priority tag as written, lower-cased, without `@` — keeps `critical` / `medium` / `med`, which
68
+ * `priority` has no slot for, so PRIORITY-CONTRADICTED can fold it like the declared word (#861). */
69
+ priorityTag?: string;
27
70
  manual: boolean;
28
71
  thenCount: number;
29
72
  hasDataAssertion: boolean; // a Then asserts {{data}} / contains / match data
@@ -39,8 +82,10 @@ export interface ScenarioInfo {
39
82
  exploration?: boolean; // @exploration — a probe outside the official suite, exempt from source tracing
40
83
  deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
41
84
  ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
85
+ featureManual?: boolean; // its Feature is tagged @manual — codegen skips the whole file (#835)
42
86
  /** Ordered steps with their resolved bucket (And/But inherit) — flow handoff analysis (#569). */
43
- steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string }>;
87
+ /** `arg` = the step's data table, normalised (#817) — absent when the step has none. */
88
+ steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }>;
44
89
  /**
45
90
  * The `.feature` this scenario was read from. A unit may hold several (`add --feature`), and
46
91
  * `scenarioIdReuse` scopes its uniqueness check to ONE file — so the sensor needs to know
@@ -106,7 +151,7 @@ function headingParts(line: string): { level: number; text: string } | null {
106
151
 
107
152
  export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
108
153
  if (!fs.existsSync(filePath)) return [];
109
- const text = readTextFile(filePath);
154
+ const text = readViewpointText(filePath);
110
155
  const lines = text.split('\n');
111
156
 
112
157
  const entries = new Map<string, ViewpointEntry>();
@@ -125,8 +170,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
125
170
  // and an id read with its emphasis markers still attached matches no scenario tag.
126
171
  const id = cells[0].replace(/[*`]/g, '').trim();
127
172
  if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
128
- const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
129
- entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
173
+ entries.set(id.toUpperCase(), { id: id.toUpperCase(), ...priorityOf(cells[1]), reason: cells[2] });
130
174
  }
131
175
  }
132
176
  }
@@ -179,6 +223,52 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
179
223
  }
180
224
  }
181
225
 
226
+ // 3) ID-first tables (#833): per-axis tables such as `| ID | Priority | Input | Expected |`.
227
+ // A row whose FIRST cell is a viewpoint id declares that id — the same rows the
228
+ // PRIORITY-CONTRADICTED check always read, so the two readers now agree. Kept narrow on purpose,
229
+ // because a viewpoint file also holds tables that only MENTION ids:
230
+ // - the id must be a `VP-`/`FL-` id, or belong to a family this file already declared above
231
+ // (`MS-HP` in Priority Viewpoints → `MS-HP-001`); `TC-01`, `FR-001`, `ISO-27001` never are;
232
+ // - reference sections (Known Issues, Traceability…) are skipped WITH their subsections, the
233
+ // way the ledger inherits the exclusion (#657), and fenced blocks are skipped;
234
+ // - the header is the row directly above a `|---|` separator (structure, not guessed words);
235
+ // its Priority column (any position, any alias) gives the priority, else column 2 does, as
236
+ // the old scan read it;
237
+ // - `reason` stays empty: viewpoint reasons feed page-type detection, and these rows were never
238
+ // entries before — the page type of an untouched file must not move.
239
+ const declaredFamilies = [...entries.keys()];
240
+ const familyOk = (id: string): boolean =>
241
+ /^(?:VP|FL)-/i.test(id) || declaredFamilies.some((f) => id.startsWith(`${f}-`) || id === f);
242
+ const isSeparator = (l: string): boolean => /^\|[\s|:-]+\|?$/.test(l) && l.includes('-');
243
+ let excludedAt: number | null = null;
244
+ let inFence = false;
245
+ let priorityCol = 1;
246
+ for (let i = 0; i < lines.length; i++) {
247
+ const line = lines[i].trim();
248
+ if (/^(```|~~~)/.test(line)) { inFence = !inFence; continue; }
249
+ if (inFence) continue;
250
+ const head = headingParts(line);
251
+ if (head) {
252
+ if (excludedAt !== null && head.level > excludedAt) continue; // subsection inherits
253
+ const text = head.text.replace(/[*`]/g, '').trim();
254
+ excludedAt = /^Priority Viewpoints/i.test(text) || NON_DECLARATION_SECTION.test(text) ? head.level : null;
255
+ continue;
256
+ }
257
+ if (excludedAt !== null || !line.startsWith('|') || isSeparator(line)) continue;
258
+ const cells = line.split('|').map((c) => c.trim()).filter((_, k, a) => k > 0 && k < a.length - 1);
259
+ if (isSeparator((lines[i + 1] ?? '').trim())) { // header row
260
+ const col = cells.findIndex((c) => PRIORITY_HEADER.test(c.replace(/[*`]/g, '').trim()));
261
+ priorityCol = col >= 0 ? col : 1;
262
+ continue;
263
+ }
264
+ const id = (cells[0] ?? '').replace(/[*`]/g, '').trim().toUpperCase();
265
+ if (!isViewpointId(id) || !familyOk(id)) continue;
266
+ const declared = priorityOf(cells[priorityCol] ?? '');
267
+ const existing = entries.get(id);
268
+ if (!existing) entries.set(id, { id, ...declared, reason: '' });
269
+ else if (!existing.priorityWord && declared.priorityWord) existing.priorityWord = declared.priorityWord;
270
+ }
271
+
182
272
  return [...entries.values()];
183
273
  }
184
274
 
@@ -199,6 +289,25 @@ function withBackground(sc: ParsedScenario, background?: ParsedScenario): Parsed
199
289
  return { ...sc, steps: [...bg, ...((sc.steps as ParsedStep[]) || [])] } as ParsedScenario;
200
290
  }
201
291
 
292
+ /**
293
+ * The `VP-<CATEGORY>-<n>` / `FL-<PHASE>-<n>` code in a scenario title, and its category — the ONE
294
+ * reading of it; the manifest kept an older copy of this regex and recorded no code for
295
+ * VP-I18N-001, VP-LIST-DISPLAY-01, VP-AF02-01 or any FL- id (#889).
296
+ *
297
+ * Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
298
+ * compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
299
+ * not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
300
+ * Flows use journey-phase ids (FL-HP-001 / FL-ER-002) — the VP- anchor rejected them, so every
301
+ * flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
302
+ * Segments may carry digits: the use-case framework numbers its flows (VP-AF02-01,
303
+ * VP-EF11-01 — one id per Alternate/Exception Flow), and `[A-Z]+` also silently truncated
304
+ * VP-I18N-001 to no category at all (the `18` broke the class mid-segment).
305
+ */
306
+ export function vpCodeOf(title: string): { vpCode?: string; category?: string } {
307
+ const m = title.match(/\b(?:VP|FL)-([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-\d+/i);
308
+ return m ? { vpCode: m[0].toUpperCase(), category: m[1].toUpperCase() } : {};
309
+ }
310
+
202
311
  function classifyScenario(sc: ParsedScenario): ScenarioInfo {
203
312
  const tags = sc.tags || [];
204
313
  const deferredToFlow = tags.includes('@deferred:flow');
@@ -223,18 +332,11 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
223
332
  const requiresCaps = tags.filter((t) => /^@requires:/i.test(t)).map((t) => t.slice('@requires:'.length).trim().toLowerCase()).filter(Boolean);
224
333
  let priority: Priority = 'unknown';
225
334
  for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
335
+ // Deliberately wider than `priority` (every priority word, any case): only PRIORITY-CONTRADICTED reads
336
+ // it, so `@critical` / `@High` are judged there without moving what select-by-ids or the score see.
337
+ const priorityTag = [...tags].reverse().find((t) => /^@(?:critical|high|medium|med|normal|low)$/i.test(t))?.slice(1).toLowerCase();
226
338
 
227
- // Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
228
- // compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
229
- // not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
230
- // Flows use journey-phase ids (FL-HP-001 / FL-ER-002) — the VP- anchor rejected them, so every
231
- // flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
232
- // Segments may carry digits: the use-case framework numbers its flows (VP-AF02-01,
233
- // VP-EF11-01 — one id per Alternate/Exception Flow), and `[A-Z]+` also silently truncated
234
- // VP-I18N-001 to no category at all (the `18` broke the class mid-segment).
235
- const codeMatch = sc.name.match(/\b(?:VP|FL)-([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-\d+/i);
236
- const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
237
- const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
339
+ const { vpCode, category } = vpCodeOf(sc.name);
238
340
  // Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
239
341
  const leadMatch = sc.name.match(/^\s*([A-Za-z][A-Za-z0-9.-]*)/);
240
342
  const vpId = leadMatch && isIdLike(leadMatch[1]) ? leadMatch[1].toUpperCase() : undefined;
@@ -246,22 +348,20 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
246
348
  const skeletonParts: string[] = [];
247
349
  const textParts: string[] = [sc.name];
248
350
  const stepTextParts: string[] = [];
249
- const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string }> = [];
351
+ const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }> = [];
250
352
 
251
353
  for (const step of sc.steps as ParsedStep[]) {
252
354
  const kw = step.keyword.trim();
253
355
  if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
254
356
  textParts.push(step.text);
255
357
  stepTextParts.push(step.text);
256
- orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text });
257
- // normalized skeleton: keep [refs] (distinct targets = distinct tests),
258
- // but neutralize {{vars}} and quoted values so EP/data families collapse.
259
- const skel = step.text
260
- .replace(/\{\{[^}]*\}\}/g, '{}')
261
- .replace(/"[^"]*"/g, '""')
262
- .replace(/\s+/g, ' ')
263
- .trim()
264
- .toLowerCase();
358
+ // The data table is part of what a step says (#817): `User see tab order:` with [FieldA]→[Cancel]
359
+ // and with [Save]→[Close] are different tests, and comparing step text alone called them exact
360
+ // duplicates and one oracle asserted N times. Kept OUT of `text`, which the claim/keyword sensors
361
+ // read as prose.
362
+ const arg = tableArg(step);
363
+ orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text, ...(arg ? { arg } : {}) });
364
+ const skel = skeletonOf(step.text) + (arg ? ` ${skeletonOf(arg)}` : '');
265
365
  skeletonParts.push(`${kw === 'And' || kw === 'But' ? last : kw}:${skel}`);
266
366
 
267
367
  if (last === 'Then') {
@@ -286,6 +386,7 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
286
386
  vpCode,
287
387
  category,
288
388
  priority,
389
+ priorityTag,
289
390
  manual,
290
391
  thenCount,
291
392
  hasDataAssertion: hasData,
@@ -309,9 +410,11 @@ export function loadScenarios(featurePath: string): ScenarioInfo[] {
309
410
  if (!fs.existsSync(featurePath)) return [];
310
411
  const parser = new GherkinParser();
311
412
  const feature = parser.parseFeatureFile(featurePath);
413
+ // Kept apart from `manual` so the audit's accounting is unchanged; only `select` reads it.
414
+ const featureManual = (feature.tags || []).some((t: string) => /^@manual\b/i.test(t)) || undefined;
312
415
  return (feature.scenarios || [])
313
416
  .filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
314
- .map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath }));
417
+ .map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath, featureManual }));
315
418
  }
316
419
 
317
420
  /**
@@ -334,3 +437,24 @@ export function readUnitFeatureText(unitDir: string, unit: string): string {
334
437
  .map((p) => readTextFile(p))
335
438
  .join('\n');
336
439
  }
440
+
441
+ /**
442
+ * Normalised skeleton of step text: keep [refs] (distinct targets = distinct tests), but neutralise
443
+ * {{vars}} and quoted values so EP/data families collapse.
444
+ */
445
+ function skeletonOf(text: string): string {
446
+ return text
447
+ .replace(/\{\{[^}]*\}\}/g, '{}')
448
+ .replace(/"[^"]*"/g, '""')
449
+ .replace(/\s+/g, ' ')
450
+ .trim()
451
+ .toLowerCase();
452
+ }
453
+
454
+ /** A step's data table as one comparable string — `<h1|h2 / c1|c2 / …>` — or undefined. */
455
+ function tableArg(step: ParsedStep): string | undefined {
456
+ const t = step.dataTable;
457
+ if (!t || (!t.headers.length && !t.rows.length)) return undefined;
458
+ const rows = [t.headers, ...t.rows.map((r) => r.cells)].map((cells) => cells.map((c) => c.trim()).join('|'));
459
+ return `<${rows.join(' / ')}>`;
460
+ }
@@ -23,10 +23,8 @@
23
23
  * units:
24
24
  * place-order: { scenario_ms: 20000 }
25
25
  */
26
- import * as fs from 'fs';
27
26
  import * as path from 'path';
28
- import { parse as parseYaml } from 'yaml';
29
- import { readTextFile } from './read-text';
27
+ import { readYamlConfig, warnUnreadableConfig } from './config-file';
30
28
 
31
29
  export interface PerfConfig {
32
30
  /** 0..100 — e.g. 75 for p75. */
@@ -51,12 +49,22 @@ export function perfConfigPath(projectRoot: string): string {
51
49
  return path.join(projectRoot, 'qa', 'perf.yaml');
52
50
  }
53
51
 
54
- /** Absent file → null (perf reporting is opt-in; nothing changes until configured). */
52
+ /**
53
+ * Absent file → null (perf reporting is opt-in; nothing changes until configured).
54
+ *
55
+ * An UNREADABLE file is not that. Returning null for it drops the perf section out of the delivered
56
+ * matrix (`exporters/matrix/export.ts`) exactly as if no budgets had ever been declared — so a typo
57
+ * in qa/perf.yaml reads, in the deliverable, as "this project has no performance budgets". Say so.
58
+ */
55
59
  export function loadPerfConfig(projectRoot: string): PerfConfig | null {
56
60
  const p = perfConfigPath(projectRoot);
57
- if (!fs.existsSync(p)) return null;
58
- let raw: Record<string, unknown>;
59
- try { raw = parseYaml(readTextFile(p)) as Record<string, unknown>; } catch { return null; }
61
+ const read = readYamlConfig<Record<string, unknown> | null>(p);
62
+ if (read.state === 'absent') return null;
63
+ if (read.state === 'unreadable') {
64
+ warnUnreadableConfig(p, read.error, 'perf budgets are not applied and the perf section is omitted from the delivered matrix');
65
+ return null;
66
+ }
67
+ const raw = read.value as Record<string, unknown>;
60
68
  if (!raw || typeof raw !== 'object') return null;
61
69
  const pctRaw = String(raw.percentile ?? 'p75').toLowerCase().replace(/^p/, '');
62
70
  const percentile = Math.min(100, Math.max(1, Number(pctRaw) || 75));
@@ -5,8 +5,10 @@
5
5
  */
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
+ import { keywordHit } from './keyword-match';
8
9
  import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
9
10
  import { parseManualComments } from '../exporters/scenario-merger';
11
+ import { commentLinesOf, isPresencePlaceholderHybrid, keywordStepsOf } from '../exporters/manual-hybrid-oracle';
10
12
  import { readTextFile } from './read-text';
11
13
 
12
14
  // ---------- #2 Downstream-scope ----------
@@ -57,12 +59,19 @@ function downstreamRoutes(specText: string): string[] {
57
59
  }
58
60
 
59
61
  /**
60
- * H5 — a @parallel stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios
61
- * share state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
62
+ * H5 — a stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios share
63
+ * state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
62
64
  * browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
65
+ *
66
+ * #759 — this used to require a literal `@parallel`, which made the gate miss the common case.
67
+ * Parallel is the DEFAULT (code-generator.ts: `isParallelFeature = !isSerialFeature`); `@parallel`
68
+ * only NAMES that default and changes no codegen, so an untagged stateful flow is isolated exactly
69
+ * like a tagged one and carries exactly the same risk. The tag that actually changes the emitted
70
+ * suite is `@serial` — one shared ordered page, where state carry-over is the point — so that is
71
+ * what exempts a flow now. Same reading as serialCascadeRisk() below, which already said so.
63
72
  */
64
73
  export function isolationRisk(featureText: string, stateful: boolean, collection = 'cart'): boolean {
65
- if (!stateful || !/@parallel\b/i.test(featureText)) return false;
74
+ if (!stateful || /@serial\b/i.test(featureText)) return false;
66
75
  // What discharges the risk is isolation of THE MUTATED COLLECTION — so the cleanup tag has
67
76
  // to name it (`@cleanup:cart` for a cart; `@cleanup:registration` for a registration flow).
68
77
  // A cleanup of something else (`@cleanup:overlay`) leaves the shared state shared.
@@ -149,8 +158,12 @@ export function downstreamScope(
149
158
  // names the same screen under its human label (register ↔ [Registration]).
150
159
  const labelsOf = (s: ScenarioInfo): string[] =>
151
160
  [...s.haystack.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1].split(':')[0]);
161
+ // The slug is the route's last segment — in practice an ordinary noun (`order`, `detail`,
162
+ // `login`), so it is matched as a WORD (#779). `dis·order` is not a reference to `/account/order`,
163
+ // and crediting it downgraded a route nothing mentions from `absent` to merely `underCovered`.
164
+ // The full route stays a literal: a path is an identifier, not prose.
152
165
  const refs = scenarios.filter((s) =>
153
- s.haystack.includes(slug) || s.haystack.includes(route.toLowerCase()) ||
166
+ keywordHit(s.haystack, slug) || s.haystack.includes(route.toLowerCase()) ||
154
167
  labelsOf(s).some((l) => sameScreenName(slug, l)));
155
168
  if (!refs.length) {
156
169
  // A scenario's haystack is its name + steps, so a route documented in a @manual scenario's
@@ -208,14 +221,15 @@ export function manualOracle(featureText: string): ManualOracleResult {
208
221
  // Observable:/Oracle:/Expected: line (parseManualComments — the SAME parser the
209
222
  // delivery exporter uses, so this gate can't drift from the exported cells).
210
223
  // Prose paragraphs / rationale-only comments export EMPTY Steps + Expected.
224
+ // • HAS a Then whose every assertion is a bare presence placeholder, with the real oracle
225
+ // in the comments (#810) — the same hybrid wearing a Then (manual-hybrid-oracle.ts, the
226
+ // SAME predicate as compile-time SG-W008).
211
227
  const hasGherkinSteps = /^\s*(Given|When|Then)\s/m.test(b);
228
+ const commentLines = commentLinesOf(b);
212
229
  let bad: boolean;
213
230
  if (hasGherkinSteps) {
214
- bad = !/^\s*Then\s/m.test(b);
231
+ bad = !/^\s*Then\s/m.test(b) || isPresencePlaceholderHybrid(keywordStepsOf(b), commentLines);
215
232
  } else {
216
- const commentLines = b.split('\n')
217
- .filter((l) => /^\s*#/.test(l))
218
- .map((l) => l.replace(/^\s*#\s?/, ''));
219
233
  const parsed = parseManualComments(commentLines);
220
234
  bad = !parsed.steps.length || !parsed.expected.length;
221
235
  }
Binary file
@@ -17,12 +17,43 @@ import * as fs from 'fs';
17
17
  *
18
18
  * A Windows-authored spec is completely normal, so every harness parser that
19
19
  * reads a user file must go through here.
20
+ *
21
+ * A leading UTF-8 BOM is stripped for the same reason. It is what PowerShell's `Out-File` and
22
+ * several Windows editors write by default, and it is invisible in every editor — but it breaks a
23
+ * line-anchored `/^\s*Scenario:/` on line 1, and `JSON.parse` rejects it outright while npm and
24
+ * most other tools quietly ignore it. A file the rest of the ecosystem reads fine must not read as
25
+ * corrupt here.
20
26
  */
21
27
  export function readTextFile(filePath: string): string {
22
- return fs.readFileSync(filePath, 'utf-8').replace(/\r\n?/g, '\n');
28
+ return fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
23
29
  }
24
30
 
25
31
  /** `readTextFile` split into lines — the common case for line-anchored parsers. */
26
32
  export function readTextLines(filePath: string): string[] {
27
33
  return readTextFile(filePath).split('\n');
28
34
  }
35
+
36
+ /**
37
+ * Drop HTML comments (`<!-- … -->`, multi-line too) — they are author notes and scaffold samples,
38
+ * never content. Newlines inside a comment are kept so every later line keeps its position (#841).
39
+ *
40
+ * Only a CLOSED comment is dropped. An unclosed `<!--` (a forgotten `-->`) stays text: dropping it
41
+ * to the end of the file would silently delete every declaration below it. A fenced code block is
42
+ * content, and so is a `<!--` written inside backticks: both are how a document SHOWS the syntax.
43
+ */
44
+ export function stripHtmlComments(text: string): string {
45
+ // A comment body never holds another `<!--`: an opener with no `-->` before the next opener is
46
+ // the unclosed one, and must not reach a LATER comment's closer and eat everything between.
47
+ const blank = (s: string): string => s.replace(/(?<!`)<!--(?:(?!<!--)[\s\S])*?-->/g, (m) => m.replace(/[^\n]/g, ''));
48
+ // split() with a capture group keeps the fences at the odd indexes.
49
+ return text.split(/(^```[^\n]*\n[\s\S]*?^```[^\n]*$)/m).map((part, i) => (i % 2 ? part : blank(part))).join('');
50
+ }
51
+
52
+ /**
53
+ * `test-viewpoint.md` as every harness sensor must read it: normalised, comments removed, and
54
+ * '' when the unit has none. `sungen add` scaffolds its samples inside `<!-- -->`; read raw, an
55
+ * untouched scaffold declared VP-UI/VP-VAL/VP-LOGIC/VP-SEC and carried 12 phantom claims (#841).
56
+ */
57
+ export function readViewpointText(filePath: string): string {
58
+ return fs.existsSync(filePath) ? stripHtmlComments(readTextFile(filePath)) : '';
59
+ }
@@ -107,6 +107,12 @@ const ASSERTION_STEP_COMMENT = /^\s*\/\/\s*Assert\b/;
107
107
  // but as a bare substring it would also exempt a genuinely hollow step whose test-data or step
108
108
  // text happened to contain it.
109
109
  const NOOP_STEP_MARKER = /^\s*\/\/\s*App launched via wdio capabilities/;
110
+ // The same kind of step on the web adapter, where the explanation IS the step comment: the
111
+ // step-mapper compiles `Given User is on [Edit] dialog` (and its exit) to `// Enter dialog scope:
112
+ // Edit` and no code, because it only re-roots the steps after it — the next assertion runs as
113
+ // `page.getByRole('dialog', …).getByRole(…)`. A field unit reported 13 BYPASS, every one of them
114
+ // this comment, and exited 2 on a correct suite (#881). Anchored to the whole marker line.
115
+ const SCOPE_STEP_MARKER = /^\s*\/\/\s*(?:Enter|Exit) dialog scope(?::\s*.*)?$/;
110
116
 
111
117
  /** A spec comment line reduced to its text, for comparison against a compiler step comment. */
112
118
  const commentText = (line: string): string => line.trim().replace(/^\/\/\s?/, '').trim();
@@ -241,7 +247,7 @@ export function analyzeFaithfulness(
241
247
  const hasCode = region.some((l) => !isComment(l) && CODE_LINE.test(l));
242
248
  // A documented no-op emits its explanation INTO the step's region, so read the region
243
249
  // (not just the marker line) for it — see NOOP_STEP_MARKER.
244
- if (hasCode || region.some((l) => NOOP_STEP_MARKER.test(l))) continue;
250
+ if (hasCode || region.some((l) => NOOP_STEP_MARKER.test(l)) || SCOPE_STEP_MARKER.test(body[markers[s]])) continue;
245
251
  if (tailAmbiguous && s === markers.length - 1) continue;
246
252
  hollowSteps.push({ test: title, step: body[markers[s]].trim().slice(0, 70) });
247
253
  }
@@ -11,6 +11,19 @@ import * as path from 'path';
11
11
 
12
12
  export interface SecretHit { file: string; line: number; reason: string }
13
13
 
14
+ /** A yaml sink the scanner could not read. Not a hit — the absence of one it cannot vouch for. */
15
+ export interface UnscannedFile { file: string; reason: string }
16
+
17
+ /**
18
+ * The scan result, deliberately two lists rather than one.
19
+ *
20
+ * This is a security control, and its whole value is that "no hits" means "I looked". A file the
21
+ * scanner could not read used to be dropped by a bare `catch`, so an unread yaml and a yaml with no
22
+ * secrets produced the identical answer: clean. Returning `unscanned` alongside makes the caller
23
+ * hold both facts — what was found, and what the finding does not cover.
24
+ */
25
+ export interface SecretScanResult { hits: SecretHit[]; unscanned: UnscannedFile[] }
26
+
14
27
  // High-confidence vendor token prefixes (very low false-positive rate).
15
28
  const VENDOR_PREFIXES = /\b(figd_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})/;
16
29
  // A secret-named key assigned a long, non-placeholder value.
@@ -51,16 +64,59 @@ function scanText(text: string, file: string): SecretHit[] {
51
64
  /** Scan a unit dir's committed yaml sinks for likely real secrets: test-data/*.yaml,
52
65
  * mock/*.yaml (mock response bodies are a credential magnet — pasted DevTools
53
66
  * responses carry real tokens), and api/*.yaml (endpoint catalogs carry headers). */
54
- export function scanTestDataSecrets(baseDir: string): SecretHit[] {
67
+ export function scanTestDataSecrets(baseDir: string): SecretScanResult {
55
68
  const hits: SecretHit[] = [];
69
+ const unscanned: UnscannedFile[] = [];
70
+ const why = (e: unknown): string => (e instanceof Error ? e.message.split('\n')[0] : String(e));
71
+
56
72
  for (const sub of ['test-data', 'mock', 'api']) {
57
73
  const dir = path.join(baseDir, sub);
58
74
  if (!fs.existsSync(dir)) continue;
59
- for (const f of fs.readdirSync(dir)) {
75
+
76
+ // A directory that cannot be LISTED is a directory that cannot be scanned. This used to throw
77
+ // out of the scanner and abort the whole `generate`/`delivery` run; report it in the same place
78
+ // a hit would appear instead, so the operator learns what the clean verdict does not cover
79
+ // rather than losing the deliverable to an unrelated permission fault.
80
+ let entries: string[];
81
+ try { entries = fs.readdirSync(dir); } catch (e) { unscanned.push({ file: sub, reason: why(e) }); continue; }
82
+
83
+ for (const f of entries) {
60
84
  if (!/\.ya?ml$/i.test(f)) continue;
61
85
  const p = path.join(dir, f);
62
- try { hits.push(...scanText(fs.readFileSync(p, 'utf-8'), path.join(sub, f))); } catch { /* ignore */ }
86
+ const rel = path.join(sub, f);
87
+ let text: string;
88
+ // `readdirSync` listed this file, so it exists as a name. A read that fails here means a
89
+ // restricted mount, a dangling symlink, or a file removed between the two calls — never
90
+ // "there was nothing to read". Silently skipping it let a committed credential ship under an
91
+ // explicit "clean" verdict.
92
+ try { text = fs.readFileSync(p, 'utf-8'); } catch (e) { unscanned.push({ file: rel, reason: why(e) }); continue; }
93
+ hits.push(...scanText(text, rel));
63
94
  }
64
95
  }
65
- return hits;
96
+ return { hits, unscanned };
97
+ }
98
+
99
+ /**
100
+ * The operator-facing lines for a scan result — one renderer, because `generate` and `delivery`
101
+ * had grown two copies of it and only one of them would have learned about `unscanned`.
102
+ *
103
+ * Empty array when there is nothing to say. The unscanned block is NOT folded into the hit block:
104
+ * "possible secret" and "not examined" are different claims, and printing the second under the
105
+ * first's heading invites it to be dismissed as another false positive.
106
+ */
107
+ export function formatSecretScan(result: SecretScanResult, limit = 10): string[] {
108
+ const out: string[] = [];
109
+ if (result.hits.length) {
110
+ out.push('', '⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):');
111
+ for (const h of result.hits.slice(0, limit)) out.push(` ${h.file}:${h.line} — ${h.reason}`);
112
+ if (result.hits.length > limit) out.push(` … and ${result.hits.length - limit} more`);
113
+ out.push(' Move real secrets to an env overlay / CI secret; keep test-data placeholders only.');
114
+ }
115
+ if (result.unscanned.length) {
116
+ out.push('', `⚠️ Secret scan could NOT read ${result.unscanned.length} committed yaml file(s) — they are NOT covered by the result above:`);
117
+ for (const u of result.unscanned.slice(0, limit)) out.push(` ${u.file} — ${u.reason}`);
118
+ if (result.unscanned.length > limit) out.push(` … and ${result.unscanned.length - limit} more`);
119
+ out.push(' Make them readable and re-run before treating this unit as scanned.');
120
+ }
121
+ return out;
66
122
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Select scenarios by test-case ID — backs `sungen select` and `run-test --ids` (#835).
3
+ *
4
+ * An ID means what it means everywhere else in the harness: `vpId`, the leading ID token of the
5
+ * scenario title (`VP-UI-001`, `MS-HP-001`, `VP0-001`). Matching is exact and case-insensitive —
6
+ * `VP-UI-001` never selects `VP-UI-0010` or `VP-UI-001-B` — and every ID that selects nothing
7
+ * runnable is reported, because the alternative is a 0-test run that reports green.
8
+ */
9
+ import * as path from 'path';
10
+ import { ScenarioInfo } from './parse';
11
+
12
+ /**
13
+ * Escape scenario titles into one `-g` / `--mochaOpts.grep` regex. Full titles, not bare IDs:
14
+ * a title starts with its own ID, so it can only appear inside its own test's title — and a
15
+ * `@cases` row test (`<title> — <row label>`) still contains it.
16
+ */
17
+ export function titlesToGrep(titles: string[]): string {
18
+ return titles.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
19
+ }
20
+
21
+ export interface IdSelectionGroup {
22
+ /** Feature basename (`products`, `home-modal`) — names the spec and the result file. */
23
+ feature: string;
24
+ ids: string[];
25
+ titles: string[];
26
+ grep: string;
27
+ }
28
+
29
+ export interface IdSelection {
30
+ features: IdSelectionGroup[];
31
+ /** IDs no scenario carries, with near matches (`VP-VAL-001` → `VP-VAL-001-B`). */
32
+ unknown: Array<{ id: string; didYouMean: string[] }>;
33
+ /** IDs whose every scenario is manual, owned by a flow or waiting on a capability — nothing to run. */
34
+ notRunnable: Array<{ id: string; reason: string }>;
35
+ }
36
+
37
+ /** `"VP-UI-001, vp-ui-002 VP-UI-001"` → `['VP-UI-001', 'VP-UI-002']`. */
38
+ export function parseIdList(raw: string): string[] {
39
+ return [...new Set(raw.split(/[\s,]+/).map((s) => s.trim().toUpperCase()).filter(Boolean))];
40
+ }
41
+
42
+ /** `VP-C-002.` → `VP-C-002`: the title token may carry trailing punctuation the user never types. */
43
+ const idOf = (s: ScenarioInfo): string | undefined => s.vpId?.replace(/[.-]+$/, '') || undefined;
44
+
45
+ function notRunnableReason(s: ScenarioInfo, enabledCaps: Set<string>): string | null {
46
+ if (s.deferredToFlow) return `owned by a flow${s.ownedByFlow ? ` (${s.ownedByFlow})` : ''} — run it there`;
47
+ if (s.featureManual) return 'its Feature is @manual — no spec is generated for it';
48
+ if (s.manual) return 'manual — no automated test exists';
49
+ // Compiled as `test.skip` while the capability is off: the run would report a skip, not a result.
50
+ const missing = (s.requiresCaps ?? []).filter((c) => !enabledCaps.has(c));
51
+ if (missing.length) return `needs capability ${missing.join(', ')} — compiled as a skip until \`sungen capability add ${missing.join(' ')}\``;
52
+ return null;
53
+ }
54
+
55
+ export function selectByIds(scenarios: ScenarioInfo[], ids: string[], unit: string, enabledCaps: Set<string> = new Set()): IdSelection {
56
+ const out: IdSelection = { features: [], unknown: [], notRunnable: [] };
57
+ const groups = new Map<string, IdSelectionGroup>();
58
+ const allIds = [...new Set(scenarios.map(idOf).filter((v): v is string => !!v))];
59
+
60
+ for (const id of ids) {
61
+ const hits = scenarios.filter((s) => idOf(s) === id);
62
+ if (hits.length === 0) {
63
+ const didYouMean = allIds.filter((v) => v.startsWith(`${id}-`) || id.startsWith(`${v}-`)).slice(0, 5);
64
+ out.unknown.push({ id, didYouMean });
65
+ continue;
66
+ }
67
+ const runnable = hits.filter((s) => !notRunnableReason(s, enabledCaps));
68
+ if (runnable.length === 0) {
69
+ out.notRunnable.push({ id, reason: notRunnableReason(hits[0], enabledCaps)! });
70
+ continue;
71
+ }
72
+ for (const s of runnable) {
73
+ const feature = s.sourceFile ? path.basename(s.sourceFile, '.feature') : unit;
74
+ const g = groups.get(feature) ?? { feature, ids: [], titles: [], grep: '' };
75
+ if (!g.ids.includes(id)) g.ids.push(id);
76
+ if (!g.titles.includes(s.name)) g.titles.push(s.name);
77
+ groups.set(feature, g);
78
+ }
79
+ }
80
+ for (const g of groups.values()) g.grep = titlesToGrep(g.titles);
81
+ out.features = [...groups.values()];
82
+ return out;
83
+ }