@sun-asterisk/sungen 3.2.32 → 3.2.34

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 (381) 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 +26 -4
  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 +35 -17
  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/delivery.d.ts.map +1 -1
  15. package/dist/cli/commands/delivery.js +79 -53
  16. package/dist/cli/commands/delivery.js.map +1 -1
  17. package/dist/cli/commands/generate.d.ts.map +1 -1
  18. package/dist/cli/commands/generate.js +14 -7
  19. package/dist/cli/commands/generate.js.map +1 -1
  20. package/dist/cli/commands/results-merge.d.ts +9 -0
  21. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  22. package/dist/cli/commands/results-merge.js +9 -12
  23. package/dist/cli/commands/results-merge.js.map +1 -1
  24. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  25. package/dist/cli/commands/results-sanitize.js +2 -13
  26. package/dist/cli/commands/results-sanitize.js.map +1 -1
  27. package/dist/cli/commands/select.d.ts +12 -0
  28. package/dist/cli/commands/select.d.ts.map +1 -0
  29. package/dist/cli/commands/select.js +122 -0
  30. package/dist/cli/commands/select.js.map +1 -0
  31. package/dist/cli/commands/update.d.ts.map +1 -1
  32. package/dist/cli/commands/update.js +3 -0
  33. package/dist/cli/commands/update.js.map +1 -1
  34. package/dist/cli/index.js +7 -2
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  37. package/dist/dashboard/snapshot-builder.js +6 -10
  38. package/dist/dashboard/snapshot-builder.js.map +1 -1
  39. package/dist/dashboard/templates/index.html +1 -1
  40. package/dist/dashboard/types.d.ts +1 -0
  41. package/dist/dashboard/types.d.ts.map +1 -1
  42. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  43. package/dist/exporters/csv-exporter.js +11 -10
  44. package/dist/exporters/csv-exporter.js.map +1 -1
  45. package/dist/exporters/feature-parser.d.ts +21 -0
  46. package/dist/exporters/feature-parser.d.ts.map +1 -1
  47. package/dist/exporters/feature-parser.js +32 -1
  48. package/dist/exporters/feature-parser.js.map +1 -1
  49. package/dist/exporters/json-exporter.d.ts.map +1 -1
  50. package/dist/exporters/json-exporter.js +7 -0
  51. package/dist/exporters/json-exporter.js.map +1 -1
  52. package/dist/exporters/locale-variants.d.ts +7 -1
  53. package/dist/exporters/locale-variants.d.ts.map +1 -1
  54. package/dist/exporters/locale-variants.js +23 -16
  55. package/dist/exporters/locale-variants.js.map +1 -1
  56. package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
  57. package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
  58. package/dist/exporters/manual-hybrid-oracle.js +114 -0
  59. package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
  60. package/dist/exporters/matrix/build.d.ts.map +1 -1
  61. package/dist/exporters/matrix/build.js +19 -9
  62. package/dist/exporters/matrix/build.js.map +1 -1
  63. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  64. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  65. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  66. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  67. package/dist/exporters/matrix/export.d.ts +5 -0
  68. package/dist/exporters/matrix/export.d.ts.map +1 -1
  69. package/dist/exporters/matrix/export.js +8 -1
  70. package/dist/exporters/matrix/export.js.map +1 -1
  71. package/dist/exporters/matrix/gates.js +6 -0
  72. package/dist/exporters/matrix/gates.js.map +1 -1
  73. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  74. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  75. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  76. package/dist/exporters/matrix/types.d.ts +8 -1
  77. package/dist/exporters/matrix/types.d.ts.map +1 -1
  78. package/dist/exporters/matrix/types.js.map +1 -1
  79. package/dist/exporters/matrix/wording.d.ts +8 -1
  80. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  81. package/dist/exporters/matrix/wording.js +37 -2
  82. package/dist/exporters/matrix/wording.js.map +1 -1
  83. package/dist/exporters/run-environment.d.ts +32 -0
  84. package/dist/exporters/run-environment.d.ts.map +1 -0
  85. package/dist/exporters/run-environment.js +125 -0
  86. package/dist/exporters/run-environment.js.map +1 -0
  87. package/dist/exporters/scenario-merger.d.ts +18 -0
  88. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  89. package/dist/exporters/scenario-merger.js +47 -3
  90. package/dist/exporters/scenario-merger.js.map +1 -1
  91. package/dist/exporters/test-data-resolver.d.ts +0 -4
  92. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  93. package/dist/exporters/test-data-resolver.js +0 -7
  94. package/dist/exporters/test-data-resolver.js.map +1 -1
  95. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  96. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  97. package/dist/exporters/xlsx-exporter.js +7 -2
  98. package/dist/exporters/xlsx-exporter.js.map +1 -1
  99. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  100. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  101. package/dist/generators/gherkin-parser/index.js +49 -19
  102. package/dist/generators/gherkin-parser/index.js.map +1 -1
  103. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  104. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  105. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  106. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  107. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  108. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  113. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  114. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  115. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  116. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  117. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  118. package/dist/generators/test-generator/code-generator.js +90 -5
  119. package/dist/generators/test-generator/code-generator.js.map +1 -1
  120. package/dist/generators/test-generator/diagnostics.d.ts +36 -5
  121. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  122. package/dist/generators/test-generator/diagnostics.js +35 -4
  123. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  124. package/dist/generators/test-generator/patterns/index.d.ts +12 -2
  125. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  126. package/dist/generators/test-generator/patterns/index.js +15 -9
  127. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  128. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  129. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  130. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  131. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  132. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  133. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  134. package/dist/generators/test-generator/step-mapper.d.ts +46 -0
  135. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  136. package/dist/generators/test-generator/step-mapper.js +84 -9
  137. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  138. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  139. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  140. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  141. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  142. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  143. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  144. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  145. package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
  146. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  147. package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
  148. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  149. package/dist/harness/audit.d.ts +3 -0
  150. package/dist/harness/audit.d.ts.map +1 -1
  151. package/dist/harness/audit.js +29 -12
  152. package/dist/harness/audit.js.map +1 -1
  153. package/dist/harness/capability.d.ts +7 -1
  154. package/dist/harness/capability.d.ts.map +1 -1
  155. package/dist/harness/capability.js +15 -12
  156. package/dist/harness/capability.js.map +1 -1
  157. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  158. package/dist/harness/config-file.d.ts +17 -0
  159. package/dist/harness/config-file.d.ts.map +1 -0
  160. package/dist/harness/config-file.js +100 -0
  161. package/dist/harness/config-file.js.map +1 -0
  162. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  163. package/dist/harness/data-driven-lint.js +69 -27
  164. package/dist/harness/data-driven-lint.js.map +1 -1
  165. package/dist/harness/depth-lint.d.ts.map +1 -1
  166. package/dist/harness/depth-lint.js +2 -1
  167. package/dist/harness/depth-lint.js.map +1 -1
  168. package/dist/harness/flow-check.d.ts.map +1 -1
  169. package/dist/harness/flow-check.js +6 -2
  170. package/dist/harness/flow-check.js.map +1 -1
  171. package/dist/harness/keyword-match.d.ts +59 -0
  172. package/dist/harness/keyword-match.d.ts.map +1 -0
  173. package/dist/harness/keyword-match.js +86 -0
  174. package/dist/harness/keyword-match.js.map +1 -0
  175. package/dist/harness/next-step.d.ts.map +1 -1
  176. package/dist/harness/next-step.js +1 -0
  177. package/dist/harness/next-step.js.map +1 -1
  178. package/dist/harness/parse.d.ts +24 -0
  179. package/dist/harness/parse.d.ts.map +1 -1
  180. package/dist/harness/parse.js +117 -13
  181. package/dist/harness/parse.js.map +1 -1
  182. package/dist/harness/perf.d.ts +7 -1
  183. package/dist/harness/perf.d.ts.map +1 -1
  184. package/dist/harness/perf.js +13 -10
  185. package/dist/harness/perf.js.map +1 -1
  186. package/dist/harness/quality-gates.d.ts +9 -2
  187. package/dist/harness/quality-gates.d.ts.map +1 -1
  188. package/dist/harness/quality-gates.js +22 -8
  189. package/dist/harness/quality-gates.js.map +1 -1
  190. package/dist/harness/query-catalog.js +0 -0
  191. package/dist/harness/query-catalog.js.map +1 -1
  192. package/dist/harness/read-text.d.ts +21 -0
  193. package/dist/harness/read-text.d.ts.map +1 -1
  194. package/dist/harness/read-text.js +32 -1
  195. package/dist/harness/read-text.js.map +1 -1
  196. package/dist/harness/secret-scan.d.ts +27 -1
  197. package/dist/harness/secret-scan.d.ts.map +1 -1
  198. package/dist/harness/secret-scan.js +57 -4
  199. package/dist/harness/secret-scan.js.map +1 -1
  200. package/dist/harness/select-by-ids.d.ts +31 -0
  201. package/dist/harness/select-by-ids.d.ts.map +1 -0
  202. package/dist/harness/select-by-ids.js +106 -0
  203. package/dist/harness/select-by-ids.js.map +1 -0
  204. package/dist/harness/sensors.d.ts +20 -1
  205. package/dist/harness/sensors.d.ts.map +1 -1
  206. package/dist/harness/sensors.js +102 -27
  207. package/dist/harness/sensors.js.map +1 -1
  208. package/dist/harness/spec-branches.d.ts +0 -18
  209. package/dist/harness/spec-branches.d.ts.map +1 -1
  210. package/dist/harness/spec-branches.js +24 -3
  211. package/dist/harness/spec-branches.js.map +1 -1
  212. package/dist/harness/spec-coverage.d.ts.map +1 -1
  213. package/dist/harness/spec-coverage.js +14 -4
  214. package/dist/harness/spec-coverage.js.map +1 -1
  215. package/dist/harness/ui-inventory.d.ts +40 -0
  216. package/dist/harness/ui-inventory.d.ts.map +1 -0
  217. package/dist/harness/ui-inventory.js +191 -0
  218. package/dist/harness/ui-inventory.js.map +1 -0
  219. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  220. package/dist/harness/viewpoint-ledger.js +66 -10
  221. package/dist/harness/viewpoint-ledger.js.map +1 -1
  222. package/dist/harness/write-atomic.d.ts +2 -0
  223. package/dist/harness/write-atomic.d.ts.map +1 -0
  224. package/dist/harness/write-atomic.js +62 -0
  225. package/dist/harness/write-atomic.js.map +1 -0
  226. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  227. package/dist/orchestrator/ai-skills/writer.js +15 -7
  228. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  229. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  230. package/dist/orchestrator/assets-drift.js +11 -6
  231. package/dist/orchestrator/assets-drift.js.map +1 -1
  232. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  233. package/dist/orchestrator/project-deps-sync.js +22 -1
  234. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  235. package/dist/orchestrator/project-initializer.d.ts +8 -0
  236. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  237. package/dist/orchestrator/project-initializer.js +49 -7
  238. package/dist/orchestrator/project-initializer.js.map +1 -1
  239. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  240. package/dist/orchestrator/screen-manager.js +3 -11
  241. package/dist/orchestrator/screen-manager.js.map +1 -1
  242. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  243. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  244. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  245. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  246. package/dist/orchestrator/security-overrides.d.ts +51 -0
  247. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  248. package/dist/orchestrator/security-overrides.js +165 -0
  249. package/dist/orchestrator/security-overrides.js.map +1 -0
  250. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  251. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  252. package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  253. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  254. package/dist/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  255. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  256. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  257. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  258. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  259. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  260. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  261. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  262. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  263. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  264. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  265. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  266. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  267. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  268. package/dist/orchestrator/templates/specs-url-assert.js +38 -4
  269. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  270. package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
  271. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  272. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  273. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  274. package/dist/orchestrator/trace-mode-install.js +132 -0
  275. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  276. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +20 -0
  277. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  278. package/dist/orchestrator/viewpoint-pattern-catalog.js +45 -0
  279. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  280. package/package.json +3 -3
  281. package/src/cli/commands/audit.ts +24 -5
  282. package/src/cli/commands/capability.ts +51 -18
  283. package/src/cli/commands/changed.ts +2 -4
  284. package/src/cli/commands/dashboard.ts +1 -26
  285. package/src/cli/commands/delivery.ts +75 -48
  286. package/src/cli/commands/generate.ts +15 -7
  287. package/src/cli/commands/results-merge.ts +9 -9
  288. package/src/cli/commands/results-sanitize.ts +2 -9
  289. package/src/cli/commands/select.ts +88 -0
  290. package/src/cli/commands/update.ts +3 -0
  291. package/src/cli/index.ts +7 -2
  292. package/src/dashboard/snapshot-builder.ts +6 -8
  293. package/src/dashboard/templates/index.html +1 -1
  294. package/src/dashboard/types.ts +1 -0
  295. package/src/exporters/csv-exporter.ts +7 -10
  296. package/src/exporters/feature-parser.ts +32 -2
  297. package/src/exporters/json-exporter.ts +6 -1
  298. package/src/exporters/locale-variants.ts +23 -17
  299. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  300. package/src/exporters/matrix/build.ts +20 -10
  301. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  302. package/src/exporters/matrix/export.ts +14 -4
  303. package/src/exporters/matrix/gates.ts +6 -0
  304. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  305. package/src/exporters/matrix/types.ts +8 -1
  306. package/src/exporters/matrix/wording.ts +36 -2
  307. package/src/exporters/run-environment.ts +89 -0
  308. package/src/exporters/scenario-merger.ts +49 -4
  309. package/src/exporters/test-data-resolver.ts +0 -6
  310. package/src/exporters/xlsx-exporter.ts +7 -2
  311. package/src/generators/gherkin-parser/index.ts +50 -18
  312. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
  313. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  314. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  315. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  316. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  317. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  318. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  319. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  320. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  321. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  322. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  323. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  324. package/src/generators/test-generator/code-generator.ts +77 -5
  325. package/src/generators/test-generator/diagnostics.ts +36 -5
  326. package/src/generators/test-generator/patterns/index.ts +15 -8
  327. package/src/generators/test-generator/patterns/types.ts +15 -0
  328. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  329. package/src/generators/test-generator/step-mapper.ts +90 -8
  330. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  331. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  332. package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
  333. package/src/harness/audit.ts +37 -16
  334. package/src/harness/capability.ts +23 -12
  335. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  336. package/src/harness/config-file.ts +66 -0
  337. package/src/harness/data-driven-lint.ts +82 -24
  338. package/src/harness/depth-lint.ts +2 -1
  339. package/src/harness/flow-check.ts +6 -2
  340. package/src/harness/keyword-match.ts +85 -0
  341. package/src/harness/next-step.ts +1 -0
  342. package/src/harness/parse.ts +123 -16
  343. package/src/harness/perf.ts +15 -7
  344. package/src/harness/quality-gates.ts +22 -8
  345. package/src/harness/query-catalog.ts +0 -0
  346. package/src/harness/read-text.ts +32 -1
  347. package/src/harness/secret-scan.ts +60 -4
  348. package/src/harness/select-by-ids.ts +83 -0
  349. package/src/harness/sensors.ts +109 -28
  350. package/src/harness/spec-branches.ts +6 -3
  351. package/src/harness/spec-coverage.ts +14 -4
  352. package/src/harness/ui-inventory.ts +164 -0
  353. package/src/harness/viewpoint-ledger.ts +59 -12
  354. package/src/harness/write-atomic.ts +22 -0
  355. package/src/orchestrator/ai-skills/writer.ts +18 -3
  356. package/src/orchestrator/assets-drift.ts +10 -5
  357. package/src/orchestrator/project-deps-sync.ts +23 -1
  358. package/src/orchestrator/project-initializer.ts +54 -6
  359. package/src/orchestrator/screen-manager.ts +3 -11
  360. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  361. package/src/orchestrator/security-overrides.ts +149 -0
  362. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  363. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  364. package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  365. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  366. package/src/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  367. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  368. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  369. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  370. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  371. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  372. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  373. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  374. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  375. package/src/orchestrator/templates/playwright.config.ts +4 -2
  376. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  377. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  378. package/src/orchestrator/templates/specs-url-assert.ts +47 -4
  379. package/src/orchestrator/templates/wdio.conf.ts +24 -1
  380. package/src/orchestrator/trace-mode-install.ts +94 -0
  381. package/src/orchestrator/viewpoint-pattern-catalog.ts +58 -0
@@ -11,8 +11,7 @@ import { Command } from 'commander';
11
11
  import * as path from 'path';
12
12
  import * as fs from 'fs';
13
13
  import { execSync } from 'child_process';
14
- import { EnvironmentInfo } from '../../exporters/types';
15
- import { resolveWebBaseUrl } from '../../exporters/env-qa';
14
+ import { getEnvironment } from '../../exporters/run-environment';
16
15
  import {
17
16
  buildDashboardSnapshot,
18
17
  DashboardTarget,
@@ -37,30 +36,6 @@ const COLOR = {
37
36
 
38
37
  function log(msg: string): void { console.log(msg); }
39
38
 
40
- function getEnvironment(cwd: string): EnvironmentInfo {
41
- let baseURL = '';
42
- let projectName = 'chromium';
43
- const configPath = path.join(cwd, 'playwright.config.ts');
44
- if (fs.existsSync(configPath)) {
45
- const c = fs.readFileSync(configPath, 'utf-8');
46
- // Both config shapes: legacy literal and env-aware resolveBaseUrl('x') wrapper.
47
- const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
48
- if (baseMatch) baseURL = baseMatch[1];
49
- const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
50
- if (projMatch) projectName = projMatch[1];
51
- }
52
- // Same SUNGEN_BASE_URL override as delivery — the two surfaces must agree.
53
- baseURL = resolveWebBaseUrl(cwd, baseURL);
54
-
55
- let executor = process.env.CI_USER || process.env.USER || '';
56
- try {
57
- const gitUser = execSync('git config user.name', { cwd, encoding: 'utf-8' }).trim();
58
- if (gitUser) executor = gitUser;
59
- } catch { /* ignore */ }
60
-
61
- return { baseURL, projectName, executor };
62
- }
63
-
64
39
  interface DashboardOptions {
65
40
  noHistory?: boolean;
66
41
  open?: boolean;
@@ -6,7 +6,6 @@
6
6
  import { Command } from 'commander';
7
7
  import * as path from 'path';
8
8
  import * as fs from 'fs';
9
- import { execSync } from 'child_process';
10
9
  import { parse as parseYaml } from 'yaml';
11
10
  import { parseFeatureMetadata } from '../../exporters/feature-parser';
12
11
  import { parseSpecFile } from '../../exporters/spec-parser';
@@ -24,9 +23,9 @@ import {
24
23
  import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
25
24
  import { loadApiCatalog } from '../../exporters/api-catalog-loader';
26
25
  import { resolveApiEnvironment } from '../../exporters/api-environment';
27
- import { resolveWebBaseUrl } from '../../exporters/env-qa';
26
+ import { getEnvironment, platformOfResultFile, RunEnvironment } from '../../exporters/run-environment';
28
27
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
29
- import { scanTestDataSecrets } from '../../harness/secret-scan';
28
+ import { scanTestDataSecrets, formatSecretScan, type SecretScanResult } from '../../harness/secret-scan';
30
29
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
31
30
  import { mapFilePath } from '../../exporters/matrix/map-loader';
32
31
  import {
@@ -39,7 +38,9 @@ import {
39
38
  hasReview,
40
39
  } from '../../exporters/matrix/export';
41
40
  import { MatrixFinding } from '../../exporters/matrix/types';
41
+ import { ManualCarryReport } from '../../exporters/matrix/carry-manual-results';
42
42
  import { formatTestOutcome, resultTotals } from '../../exporters/matrix/result-totals';
43
+ import { readCapabilities } from '../../harness/capability';
43
44
 
44
45
  const COLOR = {
45
46
  reset: '\x1b[0m',
@@ -340,34 +341,27 @@ export function hasBlockingMissing(p: PreflightCheck): boolean {
340
341
  // Environment
341
342
  // ----------------------------------------------------------------------------
342
343
 
343
- function getEnvironment(cwd: string): EnvironmentInfo {
344
- let baseURL = '';
345
- let projectName = 'chromium';
346
- const configPath = path.join(cwd, 'playwright.config.ts');
347
- if (fs.existsSync(configPath)) {
348
- const c = fs.readFileSync(configPath, 'utf-8');
349
- // Both config shapes: the legacy literal `baseURL: 'x'` and the env-aware
350
- // `baseURL: resolveBaseUrl('x')` (the quoted value is the static fallback).
351
- const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
352
- if (baseMatch) baseURL = baseMatch[1];
353
- const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
354
- if (projMatch) projectName = projMatch[1];
355
- }
356
- // SUNGEN_BASE_URL (shell > .env.qa.<SUNGEN_ENV> > .env.qa) overrides the static
357
- // value — the Test Environment column must show the host the run actually hit.
358
- baseURL = resolveWebBaseUrl(cwd, baseURL);
359
-
360
- let executor = process.env.CI_USER || process.env.USER || '';
361
- try {
362
- const gitUser = execSync('git config user.name', { cwd, encoding: 'utf-8' }).trim();
363
- if (gitUser) executor = gitUser;
364
- } catch {
365
- // ignore
366
- }
344
+ /** The unit's path for the Environment column — a flow records it in its page selector. */
345
+ function targetFeaturePath(target: DeliveryTarget, declared: string | undefined, selectorsFile: string): string | undefined {
346
+ return declared || (target.isFlow ? readFlowPagePath(selectorsFile) : undefined);
347
+ }
367
348
 
368
- return { baseURL, projectName, executor };
349
+ /**
350
+ * Executor + environment for the matrix's run-result rows (#850) — resolved exactly as the
351
+ * legacy workbook resolves them: the API datasource host for an api unit, and on mobile the
352
+ * app id of the OS the result file came from (`<f>-test-result.<os>[.<env>].json`).
353
+ */
354
+ function matrixRunEnv(cwd: string, target: DeliveryTarget, env: EnvironmentInfo, paths: MatrixTargetPaths): RunEnvironment {
355
+ const base = qaDir(cwd, target);
356
+ let declared: string | undefined;
357
+ try { declared = parseFeatureMetadata(paths.featureFile).featurePath; } catch { /* loadMatrixModel reports it */ }
358
+ const featurePath = targetFeaturePath(target, declared, path.join(base, 'selectors', `${target.featureBaseName}.yaml`));
359
+ const apiEnv = target.kind === 'api' ? resolveApiEnvironment(cwd, loadApiCatalog(cwd, target.kind, target.screen)) : null;
360
+ const os = platformOfResultFile(paths.resultsPath) ?? process.env.MOBILE_PLATFORM ?? process.env.SUNGEN_ENV;
361
+ return { ...env, ...(apiEnv ?? {}), featurePath: resolvePlatformAppId(featurePath, os) };
369
362
  }
370
363
 
364
+
371
365
  /**
372
366
  * True when this is a mobile project (`platform: mobile` in qa/capabilities.yaml).
373
367
  * Mobile results are written PER-OS (`<feature>-test-result.android|ios.json`) —
@@ -375,14 +369,11 @@ function getEnvironment(cwd: string): EnvironmentInfo {
375
369
  * emits one csv+xlsx per OS and skips the (empty/misleading) base aggregate.
376
370
  */
377
371
  function isMobileProject(cwd: string): boolean {
378
- try {
379
- const p = path.join(cwd, 'qa', 'capabilities.yaml');
380
- if (!fs.existsSync(p)) return false;
381
- const parsed = parseYaml(fs.readFileSync(p, 'utf-8')) as { platform?: string } | null;
382
- return parsed?.platform === 'mobile';
383
- } catch {
384
- return false;
385
- }
372
+ // Reads through `readCapabilities` rather than re-parsing the file: the third copy of this parse
373
+ // was the third place a malformed capabilities.yaml quietly read as "web". The shared reader
374
+ // reports the breakage once per process; here it still degrades to web, which is the safe
375
+ // direction (per-OS delivery is skipped, nothing is deleted).
376
+ return readCapabilities(cwd).platform === 'mobile';
386
377
  }
387
378
 
388
379
  /**
@@ -406,6 +397,18 @@ function removeBaseDeliverables(cwd: string, featureBaseName: string): void {
406
397
  // Per-screen export
407
398
  // ----------------------------------------------------------------------------
408
399
 
400
+ /**
401
+ * #810 — a manual oracle that lives only in unlabelled `Tester verifies:` prose is exported
402
+ * verbatim into Expected (never dropped), and the author is told to structure it.
403
+ */
404
+ function warnUnlabeledManualOracles(label: string, merged: ReturnType<typeof mergeFeatureAndSpec>): void {
405
+ const hits = merged.filter((m) => m.unlabeledManualOracle);
406
+ if (!hits.length) return;
407
+ log(`${COLOR.yellow}⚠ ${label}: manual oracle found only in an unlabeled comment (${hits.length})${COLOR.reset} — exported verbatim into Expected; write it as numbered "N. Observable: …" lines:`);
408
+ for (const m of hits.slice(0, 10)) log(` ${m.feature.name.slice(0, 90)}`);
409
+ if (hits.length > 10) log(` … and ${hits.length - 10} more`);
410
+ }
411
+
409
412
  async function exportTarget(
410
413
  cwd: string,
411
414
  target: DeliveryTarget,
@@ -424,14 +427,12 @@ async function exportTarget(
424
427
  const spec = parseSpecFile(specFile);
425
428
 
426
429
  const merged = mergeFeatureAndSpec(feature, spec);
430
+ warnUnlabeledManualOracles(label, merged);
427
431
  // Screens get their URL path from the .feature file directly. Flows record
428
432
  // it in selectors/<feature>.yaml under the page-selector block — fall back
429
433
  // to that so the Environment column shows a full URL for flows too.
430
- let featurePath = feature.featurePath;
431
434
  const selectorsFile = path.join(base, 'selectors', `${target.featureBaseName}.yaml`);
432
- if (!featurePath && target.isFlow) {
433
- featurePath = readFlowPagePath(selectorsFile);
434
- }
435
+ const featurePath = targetFeaturePath(target, feature.featurePath, selectorsFile);
435
436
  const selectorKeyMap = loadSelectorKeyMap(selectorsFile);
436
437
 
437
438
  const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : '';
@@ -674,8 +675,10 @@ async function exportMatrixTarget(
674
675
  cwd: string,
675
676
  target: DeliveryTarget,
676
677
  opts: { check?: boolean; preview?: boolean; format?: MatrixFormat },
678
+ env: EnvironmentInfo,
677
679
  ): Promise<MatrixRunSummary | 'skipped' | null> {
678
680
  const paths = matrixPathsFor(cwd, target);
681
+ paths.runEnv = matrixRunEnv(cwd, target, env, paths);
679
682
 
680
683
  // A feature whose scenarios are ALL @exploration probes has nothing to deliver —
681
684
  // skip it quietly (no map required) instead of demanding a reviewed grouping for
@@ -736,11 +739,20 @@ async function exportMatrixTarget(
736
739
  return null;
737
740
  }
738
741
 
739
- const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
742
+ let deliverables: Awaited<ReturnType<typeof writeMatrixDeliverables>>;
743
+ try {
744
+ deliverables = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
745
+ } catch (e) {
746
+ // The existing workbook could not be read, so writing would destroy its Manual results.
747
+ log(` ${COLOR.red}not written:${COLOR.reset} ${(e as Error).message}`);
748
+ return null;
749
+ }
750
+ const { csvPath, xlsxPath, manualCarry } = deliverables;
740
751
  const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
741
752
  const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
742
753
  const written = [xlsxPath, csvPath].filter((p): p is string => !!p).map((p) => path.relative(cwd, p));
743
754
  for (const rel of written) log(` ${COLOR.green}→ ${rel}${COLOR.reset}`);
755
+ if (manualCarry) printManualCarry(manualCarry);
744
756
  return {
745
757
  label: paths.label,
746
758
  items: model.manifest.itemCount,
@@ -751,6 +763,23 @@ async function exportMatrixTarget(
751
763
  };
752
764
  }
753
765
 
766
+ /** What happened to the tester's hand-entered Manual results on this write (#756). */
767
+ function printManualCarry(r: ManualCarryReport): void {
768
+ const kept = r.carried + r.unchecked;
769
+ if (kept + r.reset.length + r.notPlaced.length + r.duplicates.length === 0) return;
770
+ const list = (refs: string[]) => `${refs.slice(0, 4).join(', ')}${refs.length > 4 ? ', …' : ''}`;
771
+ log(` Manual results: ${kept} kept${r.unchecked > 0 ? ` (${r.unchecked} without a drift check — the file predates the fingerprint stamp)` : ''}`);
772
+ if (r.reset.length > 0) {
773
+ log(` ${COLOR.yellow}reset to Pending${COLOR.reset} — procedure changed since the result was recorded (${r.reset.length}): ${list(r.reset)}; the previous result is kept in Note`);
774
+ }
775
+ if (r.notPlaced.length > 0) {
776
+ log(` ${COLOR.yellow}not carried${COLOR.reset} — no matching manual case in the feature any more (${r.notPlaced.length}): ${list(r.notPlaced.map((n) => `${n.ref} was ${n.result}`))}`);
777
+ }
778
+ if (r.duplicates.length > 0) {
779
+ log(` ${COLOR.yellow}not carried${COLOR.reset} — a second row for the same case; only the first was kept (${r.duplicates.length}): ${list(r.duplicates.map((n) => `${n.ref} was ${n.result}`))}`);
780
+ }
781
+ }
782
+
754
783
  /**
755
784
  * One place that accounts for every target the run did NOT write, so a sweep is
756
785
  * never silently partial: scaffolds skipped by preflight and units whose gates /
@@ -934,12 +963,9 @@ export function registerDeliveryCommand(program: Command): void {
934
963
  // Deliverables are COMMITTED files; delivery is the last stop before a
935
964
  // hardcoded credential lands in one. Same scan `generate` runs.
936
965
  const scanDirs = [...new Set(toExport.map((t) => qaDir(cwd, t)))];
937
- const secretHits = scanDirs.flatMap((d) => scanTestDataSecrets(d));
938
- if (secretHits.length) {
939
- log(`\n⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):`);
940
- for (const h of secretHits.slice(0, 10)) log(` ${h.file}:${h.line} — ${h.reason}`);
941
- log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.\n`);
942
- }
966
+ const secretScan = scanDirs.map((d) => scanTestDataSecrets(d))
967
+ .reduce<SecretScanResult>((a, r) => ({ hits: [...a.hits, ...r.hits], unscanned: [...a.unscanned, ...r.unscanned] }), { hits: [], unscanned: [] });
968
+ for (const line of formatSecretScan(secretScan)) log(line);
943
969
 
944
970
  // 3. Export
945
971
  const legacy = options.legacy || options.full;
@@ -982,10 +1008,11 @@ export function registerDeliveryCommand(program: Command): void {
982
1008
 
983
1009
  const matrixSummaries: MatrixRunSummary[] = [];
984
1010
  const notExported: string[] = [];
1011
+ const env = getEnvironment(cwd);
985
1012
  for (const target of toExport) {
986
1013
  const s = await exportMatrixTarget(cwd, target, {
987
1014
  check: options.check, preview: options.preview, format: format as MatrixFormat,
988
- });
1015
+ }, env);
989
1016
  if (s === 'skipped') continue; // all-@exploration feature — neither exported nor a failure
990
1017
  if (s) matrixSummaries.push(s);
991
1018
  else notExported.push(target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName);
@@ -3,7 +3,7 @@ import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { CodeGenerator } from '../../generators/test-generator/code-generator';
5
5
  import { adapterRegistry } from '../../generators/test-generator/adapters';
6
- import { scanTestDataSecrets } from '../../harness/secret-scan';
6
+ import { scanTestDataSecrets, formatSecretScan, type SecretScanResult } from '../../harness/secret-scan';
7
7
  import { capabilityRegistry } from '../../capabilities/registry';
8
8
  import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
9
9
  import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
@@ -160,6 +160,17 @@ export function registerGenerateCommand(program: Command): void {
160
160
  const cap = readCapabilities(cwd);
161
161
  if (cap.platform) {
162
162
  platform = cap.platform;
163
+ } else if (cap.source === 'unreadable') {
164
+ // The file EXISTS and we failed to parse it. Scaffolding the web default here would
165
+ // overwrite whatever the user declared — a mobile project's platform, drivers and
166
+ // verification scope, gone, while the notice claimed the file was missing. Refuse:
167
+ // the one thing we know is that we do not know what this project is.
168
+ throw new Error(
169
+ `qa/capabilities.yaml exists but could not be parsed — fix the YAML and re-run.\n` +
170
+ ` Refusing to scaffold a default over it: that would discard the platform, drivers ` +
171
+ `and verification scope it declares.\n` +
172
+ ` To compile without reading it, pass --framework <web|mobile|…>.`,
173
+ );
163
174
  } else {
164
175
  platform = 'web';
165
176
  writeCapabilities(cwd, { platform: 'web', enabled: ['web'] });
@@ -225,12 +236,9 @@ export function registerGenerateCommand(program: Command): void {
225
236
  if (fs.existsSync(d)) for (const n of fs.readdirSync(d)) scanDirs.push(path.join(d, n));
226
237
  }
227
238
  }
228
- const secretHits = scanDirs.flatMap((d) => scanTestDataSecrets(d));
229
- if (secretHits.length) {
230
- console.log(`\n⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):`);
231
- for (const h of secretHits.slice(0, 10)) console.log(` ${h.file}:${h.line} — ${h.reason}`);
232
- console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
233
- }
239
+ const secretScan = scanDirs.map((d) => scanTestDataSecrets(d))
240
+ .reduce<SecretScanResult>((a, r) => ({ hits: [...a.hits, ...r.hits], unscanned: [...a.unscanned, ...r.unscanned] }), { hits: [], unscanned: [] });
241
+ for (const line of formatSecretScan(secretScan)) console.log(line);
234
242
 
235
243
  // Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
236
244
  // never block generation. Each capability registers its own; the kernel just runs them.
@@ -13,6 +13,7 @@ import { detectReportRoot, sanitizeReportPaths } from '../../exporters/report-pa
13
13
  import { registerResultsSanitizeCommand } from './results-sanitize';
14
14
  import { resolveResultsPath } from '../../exporters/playwright-report-parser';
15
15
  import { parseSpecFile } from '../../exporters/spec-parser';
16
+ import { writeFileAtomic } from '../../harness/write-atomic';
16
17
 
17
18
  interface MergeCliOptions {
18
19
  out?: string;
@@ -38,7 +39,7 @@ function readReport(file: string, label: string): any {
38
39
  * (flows → screens → api → api flows); the same basename found in more than one
39
40
  * place is an ambiguity error, never a silent pick.
40
41
  */
41
- function resolveFeatureDir(cwd: string, name: string): string {
42
+ export function resolveFeatureDir(cwd: string, name: string): string {
42
43
  const roots = [
43
44
  path.join(cwd, 'specs', 'generated', 'flows'),
44
45
  path.join(cwd, 'specs', 'generated'),
@@ -88,6 +89,12 @@ export function runResultsMerge(baseArg: string | undefined, newArg: string | un
88
89
  if (!baseFile || path.dirname(baseFile) !== dir) {
89
90
  throw new Error(`results merge: no per-target result file for "${opts.screen}" in ${path.relative(cwd, dir)} — nothing to merge into (a first full run writes it directly).`);
90
91
  }
92
+ // Under SUNGEN_ENV the candidate order falls back to the base-locale file when the env file
93
+ // does not exist yet; merging a `vi` subset there would overwrite `ja`/base results in silence.
94
+ const env = process.env.SUNGEN_ENV;
95
+ if (env && !path.basename(baseFile).endsWith(`.${env}.json`)) {
96
+ throw new Error(`results merge: SUNGEN_ENV=${env} but "${opts.screen}" has no ${env} result file yet (found ${path.basename(baseFile)}) — run the full ${env} suite first; it writes the file directly.`);
97
+ }
91
98
  // Ghost/foreign-title checks need the CURRENT spec titles.
92
99
  const specFile = path.join(dir, `${opts.screen}.spec.ts`);
93
100
  try { specTitles = parseSpecFile(specFile).tests.map((t) => t.testTitle); } catch { specTitles = null; }
@@ -125,14 +132,7 @@ export function runResultsMerge(baseArg: string | undefined, newArg: string | un
125
132
  // that file is the single source of execution truth for delivery/dashboard.
126
133
  // (Same-dir rename = same device; concurrent merges are last-rename-wins.)
127
134
  const outFile = opts.out ?? baseFile;
128
- const tmp = `${outFile}.tmp-${process.pid}`;
129
- try {
130
- fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
131
- fs.renameSync(tmp, outFile);
132
- } catch (err) {
133
- try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
134
- throw err;
135
- }
135
+ writeFileAtomic(outFile, JSON.stringify(merged, null, 2));
136
136
 
137
137
  console.log(`✓ merged ${path.basename(newFile)} into ${path.relative(cwd, outFile)} — ${replaced.length} updated, ${added.length} added, ${kept.length} kept`);
138
138
  return outFile;
@@ -16,6 +16,7 @@ import * as fs from 'fs';
16
16
  import * as path from 'path';
17
17
  import fg from 'fast-glob';
18
18
  import { detectReportRoot, sanitizeReportPaths } from '../../exporters/report-path-sanitizer';
19
+ import { writeFileAtomic } from '../../harness/write-atomic';
19
20
 
20
21
  /** Where a project keeps the JSON files that carry paths. */
21
22
  export const DEFAULT_REPORT_GLOBS = [
@@ -50,15 +51,7 @@ export function runResultsSanitize(files: string[] | undefined, cwd: string = pr
50
51
  const root = detectReportRoot(doc) ?? nearestProjectRoot(path.dirname(file)) ?? cwd;
51
52
  const { doc: clean, rewritten } = sanitizeReportPaths(doc, { root });
52
53
  if (rewritten > 0) {
53
- // Atomic write, same as `results merge`: a crash mid-write must never leave a truncated report.
54
- const tmp = `${file}.tmp-${process.pid}`;
55
- try {
56
- fs.writeFileSync(tmp, JSON.stringify(clean, null, 2) + (raw.endsWith('\n') ? '\n' : ''));
57
- fs.renameSync(tmp, file);
58
- } catch (err) {
59
- try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
60
- throw err;
61
- }
54
+ writeFileAtomic(file, JSON.stringify(clean, null, 2) + (raw.endsWith('\n') ? '\n' : ''));
62
55
  }
63
56
  out.push({ file: path.relative(cwd, file), rewritten });
64
57
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `sungen select` — which tests does a list of test-case IDs name? (#835)
3
+ *
4
+ * Backs `run-test --ids`. Like `sungen changed` for `--change-only`, the selection is computed here
5
+ * so it is deterministic and testable; the workflow only runs it and merges the subset result into
6
+ * the existing per-feature result file.
7
+ *
8
+ * Fails loud: an ID that selects nothing runnable exits non-zero, because a 0-test run reports green.
9
+ */
10
+ import { Command } from 'commander';
11
+ import * as path from 'path';
12
+ import { findUnitDir } from '../resolve-unit';
13
+ import { loadUnitScenarios } from '../../harness/parse';
14
+ import { parseIdList, selectByIds } from '../../harness/select-by-ids';
15
+ import { resolveFeatureDir } from './results-merge';
16
+ import { readCapabilities } from '../../harness/capability';
17
+ import { projectRootFromScreenDir } from '../../harness/intent';
18
+
19
+ export function registerSelectCommand(program: Command): void {
20
+ program
21
+ .command('select')
22
+ .description('List the tests a set of test-case IDs selects, grouped per feature (backs run-test --ids)')
23
+ .option('-s, --screen <name>', 'Screen or flow name')
24
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
25
+ .option('--area <name>', 'Alias of --api')
26
+ .requiredOption('--ids <list>', 'Test-case IDs, comma- or space-separated (e.g. VP-UI-001,VP-UI-002)')
27
+ .option('--json', 'Output the selection as JSON (per feature: spec, ids, titles, grep)')
28
+ .option('--grep', 'Output ONLY the regex for -g / --mochaOpts.grep (selection must lie in one feature)')
29
+ .action((options) => {
30
+ try {
31
+ const name = options.screen || options.api || options.area;
32
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
33
+ const dir = findUnitDir(name);
34
+ if (!dir) throw new Error(`Not found: qa/screens|flows|api/${name}`);
35
+ const ids = parseIdList(options.ids);
36
+ if (ids.length === 0) throw new Error('--ids is empty — pass at least one test-case ID');
37
+
38
+ const cwd = process.cwd();
39
+ // Same capability set `generate` compiles against: a @requires:<cap> scenario is a skip without it.
40
+ const enabledCaps = new Set(readCapabilities(projectRootFromScreenDir(path.resolve(dir))).enabled.map((c) => c.toLowerCase()));
41
+ const selection = selectByIds(loadUnitScenarios(dir, name), ids, name, enabledCaps);
42
+ // Resolve each feature's compiled spec: a test cannot run before `sungen generate`.
43
+ const features = selection.features.map((g) => {
44
+ let spec: string | null = null;
45
+ try { spec = path.relative(cwd, path.join(resolveFeatureDir(cwd, g.feature), `${g.feature}.spec.ts`)); } catch { /* reported below */ }
46
+ return { ...g, spec };
47
+ });
48
+ const notCompiled = features.filter((f) => !f.spec).map((f) => f.feature);
49
+ const failed = selection.unknown.length > 0 || selection.notRunnable.length > 0 || notCompiled.length > 0 || features.length === 0;
50
+
51
+ if (options.json) {
52
+ console.log(JSON.stringify({ unit: name, features, unknown: selection.unknown, notRunnable: selection.notRunnable, notCompiled }, null, 2));
53
+ if (failed) process.exitCode = 1;
54
+ return;
55
+ }
56
+ if (options.grep) {
57
+ // Only a clean single-feature selection prints a regex; anything else must not be run
58
+ // as if it were complete, so it prints nothing and exits non-zero.
59
+ if (failed || features.length !== 1) {
60
+ console.error(features.length > 1
61
+ ? `select: the IDs span ${features.length} features (${features.map((f) => f.feature).join(', ')}) — use --json and run each feature on its own.`
62
+ : 'select: nothing runnable for these IDs — run without --grep to see why.');
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ console.log(features[0].grep);
67
+ return;
68
+ }
69
+
70
+ const selected = features.reduce((n, f) => n + f.titles.length, 0);
71
+ // A @cases scenario expands to one test per dataset row, so this counts scenarios.
72
+ console.log(`\n ${selected} scenario(s) selected for ${ids.length} ID(s) in ${name}:\n`);
73
+ for (const f of features) {
74
+ console.log(` ${f.feature} → ${f.spec ?? '(not compiled — run sungen generate first)'}`);
75
+ for (const t of f.titles) console.log(` ▶ ${t}`);
76
+ }
77
+ for (const u of selection.unknown) {
78
+ console.log(` ✗ ${u.id} — no scenario has this ID${u.didYouMean.length ? ` (did you mean ${u.didYouMean.join(', ')}?)` : ''}`);
79
+ }
80
+ for (const n of selection.notRunnable) console.log(` ○ ${n.id} — ${n.reason}`);
81
+ console.log('');
82
+ if (failed) process.exitCode = 1;
83
+ } catch (err) {
84
+ console.error(err instanceof Error ? err.message : String(err));
85
+ process.exitCode = 1;
86
+ }
87
+ });
88
+ }
@@ -103,6 +103,9 @@ export function registerUpdateCommand(program: Command): void {
103
103
  // playwright.config.ts is written once at init and never re-synced — patch the reporter in.
104
104
  const { installReportSanitizeReporter } = require('../../orchestrator/report-sanitize-reporter-install');
105
105
  installReportSanitizeReporter(process.cwd(), options.dryRun ?? false);
106
+ // …and move the old `trace: 'on-first-retry'` default, which never records a trace off CI.
107
+ const { installTraceMode } = require('../../orchestrator/trace-mode-install');
108
+ installTraceMode(process.cwd(), options.dryRun ?? false);
106
109
  } catch (error) {
107
110
  console.error('❌ Update failed:', error);
108
111
  process.exit(1);
package/src/cli/index.ts CHANGED
@@ -24,6 +24,7 @@ import { registerIngestCommand } from './commands/ingest';
24
24
  import { registerEvalCommand } from './commands/eval';
25
25
  import { registerManifestCommand } from './commands/manifest';
26
26
  import { registerChangedCommand } from './commands/changed';
27
+ import { registerSelectCommand } from './commands/select';
27
28
  import { registerLedgerCommand } from './commands/ledger';
28
29
  import { registerFeedbackCommand } from './commands/feedback';
29
30
  import { registerQaFeedbackCommand } from './commands/qa-feedback';
@@ -75,6 +76,7 @@ registerSetupMobileEnvCommand(program);
75
76
  registerGateCommand(program);
76
77
  registerManifestCommand(program);
77
78
  registerChangedCommand(program);
79
+ registerSelectCommand(program);
78
80
  registerLedgerCommand(program);
79
81
  registerFeedbackCommand(program);
80
82
  registerQaFeedbackCommand(program);
@@ -123,12 +125,15 @@ registerSetupMobileEnvCommand(program);
123
125
  const opts = actionCommand.opts() as Record<string, unknown>;
124
126
  const unit = [opts.screen, opts.flow, opts.area].find((v) => typeof v === 'string' && v)
125
127
  ?? (typeof actionCommand.args?.[0] === 'string' ? actionCommand.args[0] : undefined);
126
- handBack = { unit: unit as string | undefined, json: opts.json === true };
128
+ // `--grep` is machine output too: callers capture it with `$(…)`, and a footer on stdout
129
+ // became part of the regex — the last alternative of `changed --grep` silently matched
130
+ // nothing, so `--change-only` skipped its last scenario (#836).
131
+ handBack = { unit: unit as string | undefined, json: opts.json === true || opts.grep === true };
127
132
  });
128
133
  process.on('exit', (code) => {
129
134
  if (!handBack || code === 1) return;
130
135
  const line = `\nNext: sungen next${handBack.unit ? ` ${handBack.unit}` : ''}`;
131
- // stderr under --json so the hand-back never lands in the parsed payload.
136
+ // stderr under --json / --grep so the hand-back never lands in the captured payload.
132
137
  if (handBack.json) process.stderr.write(`${line}\n`);
133
138
  else process.stdout.write(`${line}\n`);
134
139
  });
@@ -29,6 +29,7 @@ import { resolvePlatformAppId } from '../exporters/mobile-app-id';
29
29
  import { discoverLocaleVariants, LocaleVariant } from '../exporters/locale-variants';
30
30
  import { getPackageVersion } from '../exporters/package-info';
31
31
  import { ApiCatalogEntry, EnvironmentInfo } from '../exporters/types';
32
+ import { readCapabilities } from '../harness/capability';
32
33
  import {
33
34
  AggregateSummary,
34
35
  DashboardSnapshot,
@@ -98,14 +99,11 @@ export function buildDashboardSnapshot(options: BuildSnapshotOptions): Dashboard
98
99
  * Mirrors `isMobileProject` in src/cli/commands/delivery.ts.
99
100
  */
100
101
  function isMobileProject(cwd: string): boolean {
101
- try {
102
- const p = path.join(cwd, 'qa', 'capabilities.yaml');
103
- if (!fs.existsSync(p)) return false;
104
- const parsed = parseYaml(fs.readFileSync(p, 'utf-8')) as { platform?: string } | null;
105
- return parsed?.platform === 'mobile';
106
- } catch {
107
- return false;
108
- }
102
+ // Reads through `readCapabilities` rather than re-parsing the file: the third copy of this parse
103
+ // was the third place a malformed capabilities.yaml quietly read as "web". The shared reader
104
+ // reports the breakage once per process; here it still degrades to web, which is the safe
105
+ // direction (per-OS delivery is skipped, nothing is deleted).
106
+ return readCapabilities(cwd).platform === 'mobile';
109
107
  }
110
108
 
111
109
  function buildOneScreen(