@sun-asterisk/sungen 3.2.24 → 3.2.26

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 (345) hide show
  1. package/dist/capabilities/context-router.d.ts.map +1 -1
  2. package/dist/capabilities/context-router.js +15 -7
  3. package/dist/capabilities/context-router.js.map +1 -1
  4. package/dist/cli/commands/audit.d.ts.map +1 -1
  5. package/dist/cli/commands/audit.js +23 -4
  6. package/dist/cli/commands/audit.js.map +1 -1
  7. package/dist/cli/commands/capability.d.ts.map +1 -1
  8. package/dist/cli/commands/capability.js +6 -48
  9. package/dist/cli/commands/capability.js.map +1 -1
  10. package/dist/cli/commands/changed.d.ts +14 -0
  11. package/dist/cli/commands/changed.d.ts.map +1 -0
  12. package/dist/cli/commands/changed.js +128 -0
  13. package/dist/cli/commands/changed.js.map +1 -0
  14. package/dist/cli/commands/delivery.d.ts.map +1 -1
  15. package/dist/cli/commands/delivery.js +25 -1
  16. package/dist/cli/commands/delivery.js.map +1 -1
  17. package/dist/cli/commands/generate.js +1 -1
  18. package/dist/cli/commands/generate.js.map +1 -1
  19. package/dist/cli/commands/results-merge.d.ts +19 -0
  20. package/dist/cli/commands/results-merge.d.ts.map +1 -0
  21. package/dist/cli/commands/results-merge.js +191 -0
  22. package/dist/cli/commands/results-merge.js.map +1 -0
  23. package/dist/cli/commands/update.d.ts.map +1 -1
  24. package/dist/cli/commands/update.js +100 -12
  25. package/dist/cli/commands/update.js.map +1 -1
  26. package/dist/cli/index.js +4 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
  29. package/dist/exporters/api-catalog-loader.js +10 -3
  30. package/dist/exporters/api-catalog-loader.js.map +1 -1
  31. package/dist/exporters/api-testcase-formatter.d.ts +9 -0
  32. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  33. package/dist/exporters/api-testcase-formatter.js +55 -3
  34. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  35. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  36. package/dist/exporters/csv-exporter.js +10 -1
  37. package/dist/exporters/csv-exporter.js.map +1 -1
  38. package/dist/exporters/json-exporter.d.ts.map +1 -1
  39. package/dist/exporters/json-exporter.js +6 -1
  40. package/dist/exporters/json-exporter.js.map +1 -1
  41. package/dist/exporters/matrix/build.d.ts.map +1 -1
  42. package/dist/exporters/matrix/build.js +14 -1
  43. package/dist/exporters/matrix/build.js.map +1 -1
  44. package/dist/exporters/matrix/export.js +1 -1
  45. package/dist/exporters/matrix/export.js.map +1 -1
  46. package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
  47. package/dist/exporters/matrix/map-loader.js +5 -0
  48. package/dist/exporters/matrix/map-loader.js.map +1 -1
  49. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  50. package/dist/exporters/matrix/render-csv.js +5 -1
  51. package/dist/exporters/matrix/render-csv.js.map +1 -1
  52. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  53. package/dist/exporters/matrix/render-xlsx.js +4 -1
  54. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  55. package/dist/exporters/matrix/types.d.ts +18 -0
  56. package/dist/exporters/matrix/types.d.ts.map +1 -1
  57. package/dist/exporters/matrix/types.js.map +1 -1
  58. package/dist/exporters/playwright-report-parser.d.ts +17 -2
  59. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  60. package/dist/exporters/playwright-report-parser.js +68 -8
  61. package/dist/exporters/playwright-report-parser.js.map +1 -1
  62. package/dist/exporters/results-merge.d.ts +46 -0
  63. package/dist/exporters/results-merge.d.ts.map +1 -0
  64. package/dist/exporters/results-merge.js +148 -0
  65. package/dist/exporters/results-merge.js.map +1 -0
  66. package/dist/exporters/selector-key-resolver.d.ts +4 -0
  67. package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
  68. package/dist/exporters/selector-key-resolver.js +101 -1
  69. package/dist/exporters/selector-key-resolver.js.map +1 -1
  70. package/dist/exporters/types.d.ts +8 -1
  71. package/dist/exporters/types.d.ts.map +1 -1
  72. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  73. package/dist/exporters/xlsx-report-builder.js +5 -4
  74. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  75. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  76. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  77. package/dist/generators/gherkin-parser/index.js +52 -1
  78. package/dist/generators/gherkin-parser/index.js.map +1 -1
  79. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
  80. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  81. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
  82. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  83. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  84. package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  85. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  86. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  87. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  88. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  89. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
  90. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
  91. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  92. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  93. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  94. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  95. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  96. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  97. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  98. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  99. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  100. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  101. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  102. package/dist/generators/test-generator/code-generator.d.ts +6 -0
  103. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  104. package/dist/generators/test-generator/code-generator.js +80 -7
  105. package/dist/generators/test-generator/code-generator.js.map +1 -1
  106. package/dist/generators/test-generator/diagnostics.d.ts +60 -6
  107. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  108. package/dist/generators/test-generator/diagnostics.js +59 -5
  109. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  110. package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
  111. package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
  112. package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
  113. package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
  114. package/dist/generators/test-generator/patterns/index.d.ts +19 -12
  115. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  116. package/dist/generators/test-generator/patterns/index.js +58 -38
  117. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  118. package/dist/generators/test-generator/patterns/types.d.ts +23 -0
  119. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  120. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
  121. package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
  122. package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
  123. package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
  124. package/dist/generators/test-generator/recipe-expander.js +9 -1
  125. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  126. package/dist/generators/test-generator/reset-tier.d.ts +68 -0
  127. package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
  128. package/dist/generators/test-generator/reset-tier.js +94 -0
  129. package/dist/generators/test-generator/reset-tier.js.map +1 -0
  130. package/dist/generators/test-generator/step-mapper.d.ts +39 -0
  131. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  132. package/dist/generators/test-generator/step-mapper.js +144 -5
  133. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  134. package/dist/generators/test-generator/template-engine.d.ts +2 -10
  135. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  136. package/dist/generators/test-generator/template-engine.js +16 -2
  137. package/dist/generators/test-generator/template-engine.js.map +1 -1
  138. package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
  139. package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
  140. package/dist/generators/test-generator/utils/format-pattern.js +82 -0
  141. package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
  142. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
  143. package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
  144. package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
  145. package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
  146. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
  147. package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
  148. package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
  149. package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
  150. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
  151. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  152. package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
  153. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  154. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  155. package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
  156. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  157. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
  158. package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
  159. package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
  160. package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
  161. package/dist/harness/annotation-overrides.d.ts +0 -12
  162. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  163. package/dist/harness/annotation-overrides.js +9 -5
  164. package/dist/harness/annotation-overrides.js.map +1 -1
  165. package/dist/harness/audit.d.ts +7 -0
  166. package/dist/harness/audit.d.ts.map +1 -1
  167. package/dist/harness/audit.js +78 -10
  168. package/dist/harness/audit.js.map +1 -1
  169. package/dist/harness/capability-plan.d.ts +2 -1
  170. package/dist/harness/capability-plan.d.ts.map +1 -1
  171. package/dist/harness/capability-plan.js +8 -3
  172. package/dist/harness/capability-plan.js.map +1 -1
  173. package/dist/harness/capability.d.ts +11 -0
  174. package/dist/harness/capability.d.ts.map +1 -1
  175. package/dist/harness/capability.js +8 -0
  176. package/dist/harness/capability.js.map +1 -1
  177. package/dist/harness/catalog/drivers.yaml +5 -0
  178. package/dist/harness/flow-contract.d.ts +18 -1
  179. package/dist/harness/flow-contract.d.ts.map +1 -1
  180. package/dist/harness/flow-contract.js +72 -9
  181. package/dist/harness/flow-contract.js.map +1 -1
  182. package/dist/harness/parse.d.ts.map +1 -1
  183. package/dist/harness/parse.js +5 -1
  184. package/dist/harness/parse.js.map +1 -1
  185. package/dist/harness/quality-gates.d.ts +12 -1
  186. package/dist/harness/quality-gates.d.ts.map +1 -1
  187. package/dist/harness/quality-gates.js +62 -7
  188. package/dist/harness/quality-gates.js.map +1 -1
  189. package/dist/harness/run-manifest.d.ts +58 -0
  190. package/dist/harness/run-manifest.d.ts.map +1 -0
  191. package/dist/harness/run-manifest.js +264 -0
  192. package/dist/harness/run-manifest.js.map +1 -0
  193. package/dist/harness/spec-branches.d.ts +88 -0
  194. package/dist/harness/spec-branches.d.ts.map +1 -0
  195. package/dist/harness/spec-branches.js +280 -0
  196. package/dist/harness/spec-branches.js.map +1 -0
  197. package/dist/harness/spec-coverage.d.ts +1 -1
  198. package/dist/harness/spec-coverage.js +4 -4
  199. package/dist/harness/spec-coverage.js.map +1 -1
  200. package/dist/harness/viewpoint-baseline.d.ts +9 -0
  201. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  202. package/dist/harness/viewpoint-baseline.js +33 -3
  203. package/dist/harness/viewpoint-baseline.js.map +1 -1
  204. package/dist/harness/viewpoint-ledger.d.ts +1 -1
  205. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  206. package/dist/harness/viewpoint-ledger.js +88 -9
  207. package/dist/harness/viewpoint-ledger.js.map +1 -1
  208. package/dist/index.d.ts +1 -0
  209. package/dist/index.d.ts.map +1 -1
  210. package/dist/index.js +5 -1
  211. package/dist/index.js.map +1 -1
  212. package/dist/orchestrator/lockstep-version.d.ts +22 -0
  213. package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
  214. package/dist/orchestrator/lockstep-version.js +58 -0
  215. package/dist/orchestrator/lockstep-version.js.map +1 -0
  216. package/dist/orchestrator/project-deps-sync.d.ts +39 -0
  217. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
  218. package/dist/orchestrator/project-deps-sync.js +175 -0
  219. package/dist/orchestrator/project-deps-sync.js.map +1 -0
  220. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  221. package/dist/orchestrator/project-initializer.js +5 -2
  222. package/dist/orchestrator/project-initializer.js.map +1 -1
  223. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
  224. package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
  225. package/dist/orchestrator/templates/ai-src/commands/delivery.md +9 -2
  226. package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  227. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  228. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  229. package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  230. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  231. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
  232. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  233. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
  234. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  235. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
  236. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  237. package/dist/orchestrator/templates/env.appium.example +10 -0
  238. package/dist/orchestrator/templates/specs-api.d.ts +4 -2
  239. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
  240. package/dist/orchestrator/templates/specs-api.js +233 -10
  241. package/dist/orchestrator/templates/specs-api.js.map +1 -1
  242. package/dist/orchestrator/templates/specs-api.ts +214 -11
  243. package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
  244. package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
  245. package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
  246. package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
  247. package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
  248. package/dist/orchestrator/templates/specs-test-data.ts +53 -6
  249. package/dist/orchestrator/templates/wdio.conf.ts +28 -9
  250. package/package.json +8 -6
  251. package/src/capabilities/context-router.ts +13 -5
  252. package/src/cli/commands/audit.ts +22 -3
  253. package/src/cli/commands/capability.ts +4 -38
  254. package/src/cli/commands/changed.ts +104 -0
  255. package/src/cli/commands/delivery.ts +27 -2
  256. package/src/cli/commands/generate.ts +1 -1
  257. package/src/cli/commands/results-merge.ts +153 -0
  258. package/src/cli/commands/update.ts +72 -13
  259. package/src/cli/index.ts +4 -0
  260. package/src/exporters/api-catalog-loader.ts +10 -2
  261. package/src/exporters/api-testcase-formatter.ts +59 -3
  262. package/src/exporters/csv-exporter.ts +11 -2
  263. package/src/exporters/json-exporter.ts +7 -2
  264. package/src/exporters/matrix/build.ts +13 -1
  265. package/src/exporters/matrix/export.ts +1 -1
  266. package/src/exporters/matrix/map-loader.ts +5 -0
  267. package/src/exporters/matrix/render-csv.ts +5 -1
  268. package/src/exporters/matrix/render-xlsx.ts +4 -1
  269. package/src/exporters/matrix/types.ts +18 -0
  270. package/src/exporters/playwright-report-parser.ts +68 -9
  271. package/src/exporters/results-merge.ts +150 -0
  272. package/src/exporters/selector-key-resolver.ts +81 -1
  273. package/src/exporters/types.ts +8 -1
  274. package/src/exporters/xlsx-report-builder.ts +5 -4
  275. package/src/generators/gherkin-parser/index.ts +55 -1
  276. package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
  277. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
  278. package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
  279. package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
  280. package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
  281. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
  282. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
  283. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
  284. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
  285. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  286. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
  287. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
  288. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
  289. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
  290. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
  291. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
  292. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
  294. package/src/generators/test-generator/code-generator.ts +86 -7
  295. package/src/generators/test-generator/diagnostics.ts +60 -6
  296. package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
  297. package/src/generators/test-generator/patterns/index.ts +58 -33
  298. package/src/generators/test-generator/patterns/types.ts +36 -0
  299. package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
  300. package/src/generators/test-generator/recipe-expander.ts +9 -1
  301. package/src/generators/test-generator/reset-tier.ts +93 -0
  302. package/src/generators/test-generator/step-mapper.ts +149 -6
  303. package/src/generators/test-generator/template-engine.ts +18 -3
  304. package/src/generators/test-generator/utils/format-pattern.ts +79 -0
  305. package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
  306. package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
  307. package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
  308. package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
  309. package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
  310. package/src/harness/annotation-overrides.ts +10 -5
  311. package/src/harness/audit.ts +82 -12
  312. package/src/harness/capability-plan.ts +10 -5
  313. package/src/harness/capability.ts +16 -0
  314. package/src/harness/catalog/drivers.yaml +5 -0
  315. package/src/harness/flow-contract.ts +87 -9
  316. package/src/harness/parse.ts +5 -1
  317. package/src/harness/quality-gates.ts +64 -6
  318. package/src/harness/run-manifest.ts +254 -0
  319. package/src/harness/spec-branches.ts +346 -0
  320. package/src/harness/spec-coverage.ts +4 -4
  321. package/src/harness/viewpoint-baseline.ts +41 -6
  322. package/src/harness/viewpoint-ledger.ts +82 -9
  323. package/src/index.ts +1 -0
  324. package/src/orchestrator/lockstep-version.ts +46 -0
  325. package/src/orchestrator/project-deps-sync.ts +175 -0
  326. package/src/orchestrator/project-initializer.ts +5 -2
  327. package/src/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
  328. package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
  329. package/src/orchestrator/templates/ai-src/commands/delivery.md +9 -2
  330. package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
  331. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  332. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  333. package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
  334. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
  335. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
  336. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
  337. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
  338. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
  339. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
  340. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
  341. package/src/orchestrator/templates/env.appium.example +10 -0
  342. package/src/orchestrator/templates/specs-api.ts +214 -11
  343. package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
  344. package/src/orchestrator/templates/specs-test-data.ts +53 -6
  345. package/src/orchestrator/templates/wdio.conf.ts +28 -9
@@ -98,7 +98,7 @@ function callBlockLines(call: ApiCall, entry: ApiCatalogEntry, localize: (text:
98
98
  const filesStr = formatFiles(entry.files, call.args, localize);
99
99
  const bodyFileStr = formatBodyFile(entry.bodyFile, call.args, localize);
100
100
  // Body is scalar fields; a pure file upload may have none → still show `Body: —` for a stable shape.
101
- lines.push(`Body: ${formatBody(entry.body, call.args, localize)}`);
101
+ lines.push(`Body: ${formatBody(entry.body, call.args, localize, entry)}`);
102
102
  if (filesStr) lines.push(`Files: ${filesStr}`);
103
103
  if (bodyFileStr) lines.push(`Body file: ${bodyFileStr}`);
104
104
  return lines;
@@ -111,6 +111,24 @@ function callBlockLines(call: ApiCall, entry: ApiCatalogEntry, localize: (text:
111
111
  * has more than one call — same style as formatApiRequest — which also keeps a
112
112
  * deliberately repeated call distinct through the matrix's precondition Set-dedup.
113
113
  */
114
+ /**
115
+ * Terse one-line-per-call summary for a screen/flow row's PRE-CONDITION cell:
116
+ * `Call API <name> — <METHOD> <path>`. Built on the strict parseApiCalls so the
117
+ * deliverable never documents a call the runtime silently dropped (a malformed
118
+ * tag never runs). Only name/method/path from the catalog render — tag args
119
+ * carry runtime values. A missing endpoint is loud; a present entry missing a
120
+ * field shows `—` for that field instead of blaming the wrong file.
121
+ */
122
+ export function formatApiCallSummaryLines(tags: string[], catalog: Record<string, ApiCatalogEntry>): string[] {
123
+ return parseApiCalls(tags).map(({ name }) => {
124
+ const entry = catalog[name];
125
+ if (!entry) return `Call API ${name} — (endpoint not in apis.yaml)`;
126
+ const method = typeof entry.method === 'string' && entry.method ? entry.method.toUpperCase() : '—';
127
+ const apiPath = typeof entry.path === 'string' && entry.path ? entry.path : '—';
128
+ return `Call API ${name} — ${method} ${apiPath}`;
129
+ });
130
+ }
131
+
114
132
  export function formatApiCallLines(
115
133
  calls: ApiCall[],
116
134
  catalog: Record<string, ApiCatalogEntry>,
@@ -185,6 +203,38 @@ function bindParams(raw: string, args: Record<string, string>): string {
185
203
  });
186
204
  }
187
205
 
206
+ /**
207
+ * Whitelist variant for XML string bodies: bind ONLY the declared param names
208
+ * (longest first, word-boundary guarded) — the generic `:word` scan above would
209
+ * rewrite namespace prefixes (soapenv:Envelope) into garbage cells. Mirrors the
210
+ * runtime's whitelist binding.
211
+ */
212
+ function bindXmlParamsForCell(raw: string, args: Record<string, string>, paramNames: string[]): string {
213
+ let out = raw;
214
+ for (const p of [...paramNames].sort((a, b) => b.length - a.length)) {
215
+ const expr = p in args ? args[p] : `{{${p}}}`;
216
+ out = out.replace(new RegExp(`:${p}(?![A-Za-z0-9_])`, 'g'), () => unquoteExpr(expr));
217
+ }
218
+ return out;
219
+ }
220
+
221
+ /**
222
+ * Mask the value of credential-named ELEMENTS inside an XML string body
223
+ * (`<signature>…</signature>` → `<signature>***</signature>`). The object-body
224
+ * masking below never sees a string template, so without this the raw signature
225
+ * from the catalog example would print verbatim in the client's API detail sheet.
226
+ */
227
+ function maskXmlSensitiveElements(xml: string): string {
228
+ // The open tag may carry attributes (<signature type="hmac">) and the value may
229
+ // be a CDATA section — both are common spellings for exactly the credential
230
+ // elements this masks, so the pattern must accept them or the secret prints raw.
231
+ return xml.replace(
232
+ /(<([A-Za-z0-9_.:-]+)(?:\s[^>]*)?>)((?:<!\[CDATA\[[\s\S]*?\]\]>|[^<])*)(<\/\2>)/g,
233
+ (whole, open: string, tag: string, _value: string, close: string) =>
234
+ SENSITIVE_NAME.test(tag) ? `${open}***${close}` : whole,
235
+ );
236
+ }
237
+
188
238
  /** Strip a matching pair of surrounding quotes from a tag-arg literal (`"x"` → `x`). */
189
239
  function unquoteExpr(e: string): string {
190
240
  const m = e.match(/^"(.*)"$/) || e.match(/^'(.*)'$/);
@@ -230,6 +280,7 @@ function formatHeaders(
230
280
  function contentTypeFor(encoding?: string): string {
231
281
  if (encoding === 'form') return 'application/x-www-form-urlencoded';
232
282
  if (encoding === 'multipart') return 'multipart/form-data';
283
+ if (encoding === 'xml') return 'application/xml';
233
284
  return 'application/json';
234
285
  }
235
286
 
@@ -247,8 +298,13 @@ function uploadContentType(entry: ApiCatalogEntry): string {
247
298
  * that field can be replaced with `***`; otherwise the body resolves wholesale
248
299
  * (preserving numbers/nesting exactly as before).
249
300
  */
250
- function formatBody(body: unknown, args: Record<string, string>, localize: (text: string) => string): string {
301
+ function formatBody(body: unknown, args: Record<string, string>, localize: (text: string) => string, entry?: ApiCatalogEntry): string {
251
302
  if (body === undefined || body === null) return '—';
303
+ // XML string template: whitelist-bind declared params, then mask credential elements.
304
+ if (entry?.encoding === 'xml' && typeof body === 'string') {
305
+ const names = Array.isArray(entry.params) ? (entry.params as string[]) : [];
306
+ return localize(maskXmlSensitiveElements(bindXmlParamsForCell(body, args, names)));
307
+ }
252
308
  if (isPlainObject(body)) {
253
309
  const obj = body as Record<string, unknown>;
254
310
  if (Object.keys(obj).some(isSensitiveField)) {
@@ -321,7 +377,7 @@ function formatBodyFile(bodyFile: unknown, args: Record<string, string>, localiz
321
377
  * test-data keys — catches variants like confirmPassword / new_password /
322
378
  * password_confirmation / access_token / client_secret / otp / passcode.
323
379
  */
324
- export const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization/i;
380
+ export const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization|signature/i;
325
381
  function isSensitiveField(key: string): boolean {
326
382
  return SENSITIVE_NAME.test(key);
327
383
  }
@@ -23,7 +23,7 @@ import {
23
23
  statusToTestResult,
24
24
  } from './playwright-report-parser';
25
25
  import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult, ScreenSummary, TestCaseRow } from './types';
26
- import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
26
+ import { collectSecretValues, formatApiCallSummaryLines, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
27
27
  import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
28
28
  import { formatVariantRow, resolveResultVariants } from './result-variants';
29
29
  import { collectQaEnvSecretDenylist, warnIfRedactionIncomplete } from './qa-env-secret-denylist';
@@ -103,9 +103,18 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
103
103
  };
104
104
  const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
105
105
  localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
106
+ // `@api:` preconditions on a screen/flow scenario run before its UI steps —
107
+ // without these lines the deliverable never names the real endpoint (QA
108
+ // post-processed every export to add them back). They belong in the
109
+ // PRE-CONDITION cell, the same home Background moved to: inlining setup in
110
+ // Steps shifts the numbering and every `— after step N` anchor with it.
111
+ // kind:api keeps its own richer request-block rendering below.
112
+ const apiCallLines = (input.kind === 'screen' || input.kind === 'flow') && input.apiCatalog && hasApiCalls(m.feature.tags)
113
+ ? formatApiCallSummaryLines(m.feature.tags, input.apiCatalog)
114
+ : [];
106
115
  const precondition = formatPrecondition(
107
116
  authRole,
108
- (m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
117
+ [...(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean), ...apiCallLines]
109
118
  );
110
119
  const stepLines = m.resolvedSteps.map(formatStep).filter(Boolean);
111
120
  // Scenarios whose assertions verify different moments render Expected as
@@ -16,7 +16,7 @@ import {
16
16
  } from './feature-parser';
17
17
  import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
18
18
  import { formatTestData, substituteTestDataVars } from './test-data-resolver';
19
- import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
19
+ import { collectSecretValues, formatApiCallSummaryLines, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
20
20
  import {
21
21
  formatExecutedDate,
22
22
  statusToTestResult,
@@ -99,9 +99,14 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
99
99
  };
100
100
  const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
101
101
  localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
102
+ // Same `@api:` precondition lines as csv-exporter's buildTestCaseRows — the
103
+ // dashboard and the delivery CSV must not disagree about what a row does.
104
+ const apiCallLines = (input.kind === 'screen' || input.kind === 'flow') && input.apiCatalog && hasApiCalls(m.feature.tags)
105
+ ? formatApiCallSummaryLines(m.feature.tags, input.apiCatalog)
106
+ : [];
102
107
  const precondition = formatPrecondition(
103
108
  authRole,
104
- (m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
109
+ [...(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean), ...apiCallLines]
105
110
  );
106
111
  let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
107
112
  // Same anchor rule as csv-exporter (shared groupExpectedByAnchor) so the
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { ApiCatalogEntry, FeatureMetadata, PlaywrightResult } from '../types';
9
9
  import { formatApiCallLines, parseApiCalls } from '../api-testcase-formatter';
10
+ import { collectQaEnvSecretDenylist } from '../qa-env-secret-denylist';
10
11
  import { bucketTexts } from './step-buckets';
11
12
  import { MergedScenario } from '../scenario-merger';
12
13
  import {
@@ -140,6 +141,10 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
140
141
  const { vpId, category1 } = splitVpAndName(m.feature.name);
141
142
  if (!vpId) continue; // Gate A reports it — no variant can be addressed without a VP-id
142
143
  const tags = m.feature.tags;
144
+ // @exploration probes live outside the official suite (the same contract the audit's
145
+ // source-tracing exemption follows) — they never become deliverable coverage variants,
146
+ // so the gates and the map never ask for them.
147
+ if (tags.some((t) => /^@exploration\b/i.test(t))) continue;
143
148
  const mode = extractTestcaseType(tags) === 'Manual' ? 'manual' : 'auto';
144
149
  const authRole = extractAuthRole(tags);
145
150
  // Category segment of the id, whatever the project's scheme:
@@ -513,6 +518,9 @@ export function expandMapRef(ref: string, variantsByVp: Map<string, CoverageVari
513
518
  export function buildMatrix(inputs: BuildInputs): MatrixModel {
514
519
  const { unit, map, transformerVersion } = inputs;
515
520
  const variants = deriveVariants(inputs);
521
+ // Same denylist the testcases CSV applies to its Note column — the matrix error
522
+ // cells render the same failure messages and go into the same client hand-off.
523
+ const secretValues = inputs.testData ? collectQaEnvSecretDenylist(inputs.testData).values : [];
516
524
 
517
525
  const variantsByVp = new Map<string, CoverageVariant[]>();
518
526
  for (const v of variants) {
@@ -551,7 +559,10 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
551
559
  priority,
552
560
  mode: modes.size > 1 ? 'mixed' : (first?.mode ?? 'auto'),
553
561
  layers: Array.from(new Set(groupVariants.flatMap((v) => v.layers))),
554
- traces: Array.from(new Set(groupVariants.flatMap((v) => v.traces))),
562
+ // The map's own `trace:` codes join the scenarios' `@spec:` ids on the SAME column: a
563
+ // reviewer wants one place to see what an item traces to, whether the id came from the spec
564
+ // or from the analysis. Map codes lead, because they are the ones a person chose for it.
565
+ traces: Array.from(new Set([...(g.trace ?? []), ...groupVariants.flatMap((v) => v.traces)])),
555
566
  dimensions: g.dimensions,
556
567
  // Only the preconditions COMMON to every variant belong on the parent —
557
568
  // copying the first variant's setup mis-states the start state of the
@@ -612,6 +623,7 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
612
623
  dispositions,
613
624
  requirements,
614
625
  findings,
626
+ secretValues,
615
627
  manifest: {
616
628
  unit,
617
629
  transformerVersion,
@@ -53,7 +53,7 @@ function readSpecText(paths: MatrixTargetPaths): string {
53
53
  : '';
54
54
  }
55
55
 
56
- /** apis.yaml endpoint catalog for an api unit ({} for screen/flow loader no-ops). */
56
+ /** apis.yaml endpoint catalog — resolved for EVERY kind (screen/flow read their unit-scoped catalog + the shared one), so hybrid @api preconditions render their real request contract. */
57
57
  function catalogFor(paths: MatrixTargetPaths): Record<string, ApiCatalogEntry> {
58
58
  return loadApiCatalog(paths.cwd, paths.kind ?? 'screen', paths.area ?? paths.unit);
59
59
  }
@@ -79,6 +79,10 @@ export function loadDeliveryMap(file: string): MapLoadResult {
79
79
  intent: String(grp.intent ?? ''),
80
80
  oracle: String(grp.oracle ?? ''),
81
81
  ...(grp.dimensions !== undefined ? { dimensions: String(grp.dimensions) } : {}),
82
+ // Free-form by design: these are the project's own analysis codes, not a fixed scheme.
83
+ ...(Array.isArray(grp.trace) && grp.trace.length
84
+ ? { trace: (grp.trace as unknown[]).map((t) => String(t).trim()).filter(Boolean) }
85
+ : {}),
82
86
  category: grp.category as MapCategory,
83
87
  review,
84
88
  variants: Array.isArray(variants) ? variants.map(String) : [],
@@ -150,6 +154,7 @@ export function writeDeliveryMap(file: string, map: DeliveryMap): void {
150
154
  intent: g.intent,
151
155
  oracle: g.oracle,
152
156
  ...(g.dimensions !== undefined ? { dimensions: g.dimensions } : {}),
157
+ ...(g.trace && g.trace.length ? { trace: g.trace } : {}),
153
158
  category: g.category,
154
159
  review: g.review,
155
160
  variants: g.variants,
@@ -5,6 +5,8 @@
5
5
  * and result), so downstream pipelines can re-derive both views from one file.
6
6
  */
7
7
 
8
+ import { redactSecrets } from '../api-testcase-formatter';
9
+ import { clipNoteField } from '../playwright-report-parser';
8
10
  import { variantState } from './build';
9
11
  import { coverageSummary, itemModeLabel, itemResultLabel } from './render-xlsx';
10
12
  import { MatrixModel } from './types';
@@ -75,7 +77,9 @@ export function renderMatrixCsv(model: MatrixModel): string {
75
77
  v.traces.join(' '),
76
78
  variantState(v),
77
79
  isoDate(v.result?.startTime),
78
- v.result?.error ? String(v.result.error).slice(0, 200) : '',
80
+ // Redact BEFORE clipping a clip boundary must never split a secret in
81
+ // a way that lets its prefix through. Same denylist as the Note column.
82
+ v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
79
83
  '',
80
84
  ].map(esc).join(','));
81
85
  }
@@ -27,6 +27,8 @@ import {
27
27
  BLACK,
28
28
  LAVENDER,
29
29
  } from '../xlsx-report-builder';
30
+ import { redactSecrets } from '../api-testcase-formatter';
31
+ import { clipNoteField } from '../playwright-report-parser';
30
32
  import { variantState } from './build';
31
33
  import { DeliveryItem, MatrixModel, MapCategory } from './types';
32
34
 
@@ -229,7 +231,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
229
231
  variantState(v),
230
232
  isoDate(v.result?.startTime),
231
233
  '',
232
- v.result?.error ? String(v.result.error).slice(0, 200) : '',
234
+ // Redact BEFORE clipping same denylist as the testcases CSV Note column.
235
+ v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
233
236
  '',
234
237
  ];
235
238
  vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
@@ -33,6 +33,17 @@ export interface MapGroup {
33
33
  * (e.g. "required ×3 · format ×9 · full-width ×2"). Rendered on the parent row
34
34
  * instead of concatenating every variant's title — the compact review view. */
35
35
  dimensions?: string;
36
+ /**
37
+ * Domain-logic reference ids this item traces to — a decision table, an algorithm, an internal
38
+ * business rule (`DEC-003`, `ALG-002`, `DISC-002`, `BR-001`).
39
+ *
40
+ * `requirements:` already traces to the FR/TR/NFR ids in `requirements/spec.md`. The codes a QA
41
+ * assigns while ANALYSING — which mechanism a case actually exercises — had no field, so they
42
+ * lived as comments in the `.feature` and never reached the Coverage sheet handed to the
43
+ * reviewer: the Trace column came out blank for exactly the items whose traceability mattered
44
+ * most (#651). Optional, and free-form on purpose: these ids are the project's own vocabulary.
45
+ */
46
+ trace?: string[];
36
47
  /** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
37
48
  variants: string[];
38
49
  }
@@ -235,6 +246,13 @@ export interface MatrixModel {
235
246
  manifest: MatrixManifest;
236
247
  /** Perf verdict (#569) — present when qa/perf.yaml sets a budget AND the run produced durations. */
237
248
  perf?: import('../../harness/perf').PerfVerdict | null;
249
+ /**
250
+ * Resolved ${QA_*} secret values referenced by this unit's test data. Failure
251
+ * messages quote runtime values (a Received: line can echo a typed password),
252
+ * so every error cell the renderers emit must be redacted against this list —
253
+ * the deliverable ships to the client.
254
+ */
255
+ secretValues: string[];
238
256
  }
239
257
 
240
258
  /** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
@@ -16,14 +16,22 @@ import { PlaywrightResult } from './types';
16
16
  * Candidate order (first existing wins):
17
17
  * 1. <genDir>/<featureBaseName>-test-result.<SUNGEN_ENV>.json (env runs)
18
18
  * 2. <genDir>/<featureBaseName>-test-result.json (per-feature reporter)
19
- * 3. <cwd>/test-results/results.<SUNGEN_ENV>.json
20
- * 4. <cwd>/test-results/results.json (playwright default)
19
+ * 3. <genDir>/<featureBaseName>-test-result.<os>[.<SUNGEN_ENV>].json — mobile per-OS
20
+ * reporter files (wdio writes one per capability; a mobile project has NO base file).
21
+ * MOBILE_PLATFORM picks the OS explicitly; otherwise android, then ios.
22
+ * 4. <cwd>/test-results/results.<SUNGEN_ENV>.json
23
+ * 5. <cwd>/test-results/results.json (playwright default)
21
24
  */
22
25
  export function resolveResultsPath(cwd: string, genDir: string, featureBaseName: string): string | null {
23
26
  const env = process.env.SUNGEN_ENV;
24
27
  const candidates: string[] = [];
25
28
  if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${env}.json`));
26
29
  candidates.push(path.join(genDir, `${featureBaseName}-test-result.json`));
30
+ const mobileOs = (process.env.MOBILE_PLATFORM || '').toLowerCase();
31
+ for (const os of mobileOs === 'android' || mobileOs === 'ios' ? [mobileOs] : ['android', 'ios']) {
32
+ if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.${env}.json`));
33
+ candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.json`));
34
+ }
27
35
  if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
28
36
  candidates.push(path.join(cwd, 'test-results', 'results.json'));
29
37
  for (const c of candidates) {
@@ -57,6 +65,9 @@ interface PlaywrightSpec {
57
65
  duration?: number; // ms — Playwright emits this on every result (#569 perf budgets)
58
66
  startTime?: string;
59
67
  error?: { message?: string; stack?: string };
68
+ // ALL errors of the attempt. `error` above is only the first one — when a
69
+ // timeout interrupts an assertion, the assertion detail is errors[1+].
70
+ errors?: Array<{ message?: string; stack?: string }>;
60
71
  attachments?: Array<{ name?: string; path?: string; contentType?: string }>;
61
72
  }>;
62
73
  }>;
@@ -110,7 +121,30 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
110
121
  const results = test?.results || [];
111
122
  const res = results[results.length - 1];
112
123
  const status = normalizeStatus(res?.status);
113
- const errorMsg = stripAnsi(res?.error?.message || '');
124
+ // Collect EVERY failure message of the attempt, not just the first: a test
125
+ // timeout wraps the real assertion failure, so `error` alone holds only
126
+ // "Test timeout of 15000ms exceeded." while the Locator/Expected/Received
127
+ // detail QA needs sits in errors[1+]. Stack frames are stripped PER message
128
+ // before joining — otherwise the first message's stack swallows the rest
129
+ // when downstream consumers cut at the first "at " frame.
130
+ const rawMessages = (res?.errors?.length ? res.errors.map((e) => e.message) : [res?.error?.message])
131
+ .map((m) => stripAnsi(m || ''))
132
+ .filter(Boolean);
133
+ const distinct: string[] = [];
134
+ const seenKeys = new Set<string>();
135
+ for (const m of rawMessages) {
136
+ const stripped = m.split(/\n\s*at\s/)[0].trim();
137
+ // Key ignores a leading `Error:` — "Error: page closed" and "page closed"
138
+ // are the same failure and must not render as two Note bullets.
139
+ const key = stripped.replace(/^Error:\s*/i, '');
140
+ if (!key || seenKeys.has(key)) continue;
141
+ seenKeys.add(key);
142
+ distinct.push(stripped);
143
+ }
144
+ // Single message: keep the legacy shape (stack intact) so every existing
145
+ // consumer renders byte-identical output. Multiple: join the stack-stripped
146
+ // distinct messages so none can hide behind another's stack.
147
+ const errorMsg = distinct.length > 1 ? distinct.join('\n') : stripAnsi(res?.error?.message || rawMessages[0] || '');
114
148
  const trace = res?.attachments?.find((a) => a.name === 'trace' || a.contentType === 'application/zip')?.path;
115
149
 
116
150
  // Strip the leading file-path suite title if present.
@@ -124,6 +158,7 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
124
158
  duration: typeof res?.duration === 'number' ? res.duration : undefined,
125
159
  startTime: res?.startTime,
126
160
  error: errorMsg,
161
+ errorMessages: distinct.length > 0 ? distinct : undefined,
127
162
  tracePath: trace,
128
163
  projectName: test?.projectName,
129
164
  });
@@ -178,7 +213,7 @@ function normalizeStatus(s: string | undefined): PlaywrightResult['status'] {
178
213
  * "specs/generated/kudos/kudos.spec.ts > Feature Name > ...",
179
214
  * strip that leading segment so matching works against "Feature Name > ...".
180
215
  */
181
- function stripLeadingFilePathSuite(fullTitle: string): string {
216
+ export function stripLeadingFilePathSuite(fullTitle: string): string {
182
217
  const parts = fullTitle.split(' > ');
183
218
  if (parts.length > 1 && /\.spec\.ts$/i.test(parts[0])) {
184
219
  return parts.slice(1).join(' > ');
@@ -256,11 +291,23 @@ export function parseExpectedReceived(raw: string): { expected?: string; receive
256
291
  export function formatNote(result: PlaywrightResult | undefined, expectedFallback = ''): string {
257
292
  if (!result) return '';
258
293
  if (result.status !== 'failed' && result.status !== 'timedOut') return '';
259
- // Full assertion message with stack frames dropped (frames start with "at ") + a leading `Error:`
260
- // stripped. Keep newlines here so parseExpectedReceived can delimit the Expected/Received lines.
261
- const raw = (result.error || '').split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim();
262
- const { expected, received } = parseExpectedReceived(raw);
263
- const actual = received || clip(raw); // received value, else the raw error
294
+ // Per-message: stack frames dropped (frames start with "at ") + a leading `Error:` stripped.
295
+ // Keep newlines inside each message so parseExpectedReceived can delimit Expected/Received.
296
+ const messages = (result.errorMessages?.length ? result.errorMessages : [result.error || ''])
297
+ .map((m) => m.split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim())
298
+ .filter(Boolean);
299
+ // The message that carries the assertion detail drives Expected/Actual; the other
300
+ // messages (e.g. the "Test timeout of 15000ms exceeded." wrapper) must still be
301
+ // shown — a real assertion failure rendered as a bare timeout reads as flaky.
302
+ // Prefer a message carrying BOTH halves: a half-match (Expected in one message,
303
+ // Received in another) would otherwise put the wrong text in Actual. With
304
+ // expect.soft the first assertion drives the split and the later assertion
305
+ // messages render as `• Error:` bullets — content preserved, columns describe #1.
306
+ const halves = (m: string): number => { const er = parseExpectedReceived(m); return (er.expected ? 1 : 0) + (er.received ? 1 : 0); };
307
+ const assertion = messages.find((m) => halves(m) === 2) ?? messages.find((m) => halves(m) >= 1) ?? messages[0] ?? '';
308
+ const wrappers = messages.filter((m) => m !== assertion);
309
+ const { expected, received } = parseExpectedReceived(assertion);
310
+ const actual = received || clip(assertion); // received value, else the raw error
264
311
 
265
312
  // Newline-separated bullets so each field lands on its own row when the cell wraps — matches the
266
313
  // Test Data column's `• key: value` formatting. Excel/CSV honour `\n` inside quoted cells; the
@@ -276,6 +323,18 @@ export function formatNote(result: PlaywrightResult | undefined, expectedFallbac
276
323
  else if (lines.length > 1) parts.push(`• Expected result:\n${lines.map((l) => ` ${l}`).join('\n')}`);
277
324
  }
278
325
  if (actual) parts.push(`• Actual result: ${actual}`);
326
+ // Non-assertion companions of an assertion failure (timeout wrapper etc.), one
327
+ // clipped bullet each so no cell field can blow past the XLSX-safe cap.
328
+ for (const w of wrappers) parts.push(`• Error: ${clip(w)}`);
279
329
  if (result.tracePath) parts.push(`• Trace: ${result.tracePath}`);
280
330
  return parts.join('\n');
281
331
  }
332
+
333
+ /**
334
+ * One-cell error rendering for the matrix deliverable: newlines collapsed and
335
+ * clipped to the same cap as Note fields. Replaces ad-hoc `slice(0, 200)` calls
336
+ * that could cut a joined multi-error message mid-assertion.
337
+ */
338
+ export function clipNoteField(s: string): string {
339
+ return clip(s);
340
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Merge two Playwright JSON reports by FULL TEST TITLE — the fix for subset
3
+ * re-runs: Playwright's JSON reporter overwrites the per-target result file,
4
+ * so re-running the 3 failed cases of a 100-case screen used to throw away
5
+ * the 97 passing results (QA then re-ran everything, or asked an AI to merge
6
+ * by hand — non-deterministic).
7
+ *
8
+ * Semantics (deterministic):
9
+ * - same title in both → the NEW spec wins wholesale (its results, its
10
+ * attempts, its timing);
11
+ * - title only in base → kept as-is;
12
+ * - title only in new → appended (its suite structure preserved).
13
+ * Titles compare AFTER stripping the leading file-path suite segment, so a
14
+ * base and a new report generated from different cwd/suite paths still match.
15
+ * `@cases` rows carry a ` — <label>` title suffix and merge as independent
16
+ * entries — re-running one dataset row keeps the other rows.
17
+ *
18
+ * The merge never deletes: a base title missing from the current spec (a
19
+ * scenario renamed by a spec edit) is a GHOST the caller may WARN about, but
20
+ * pruning is not this function's job — the delivery builds rows from the
21
+ * feature, so ghosts never render; only the dead `stats` block would drift,
22
+ * and it is recomputed below.
23
+ *
24
+ * Everything outside suites/specs/tests is passed through untouched from the
25
+ * BASE report (config, metadata, unknown future fields) — tolerance over
26
+ * strictness, the shape belongs to Playwright.
27
+ */
28
+
29
+ import { stripLeadingFilePathSuite } from './playwright-report-parser';
30
+
31
+ interface SpecRef {
32
+ title: string; // stripped full title
33
+ parent: any[]; // the specs array holding the node
34
+ index: number;
35
+ node: any;
36
+ }
37
+
38
+ export interface MergeOutcome {
39
+ merged: any;
40
+ replaced: string[]; // titles taken from the new report over the base
41
+ added: string[]; // titles only in the new report (appended)
42
+ kept: string[]; // titles only in the base (untouched)
43
+ }
44
+
45
+ /** Collect every spec node with its stripped full title and parent array. */
46
+ function collectSpecs(report: any): SpecRef[] {
47
+ const out: SpecRef[] = [];
48
+ const walk = (suites: any[] | undefined, stack: string[]): void => {
49
+ if (!Array.isArray(suites)) return;
50
+ for (const suite of suites) {
51
+ if (!suite || typeof suite !== 'object') continue;
52
+ const nextStack = typeof suite.title === 'string' && suite.title ? [...stack, suite.title] : stack;
53
+ if (Array.isArray(suite.specs)) {
54
+ suite.specs.forEach((spec: any, index: number) => {
55
+ if (!spec || typeof spec.title !== 'string') return;
56
+ const fullTitle = stripLeadingFilePathSuite([...nextStack, spec.title].join(' > '));
57
+ out.push({ title: fullTitle, parent: suite.specs, index, node: spec });
58
+ });
59
+ }
60
+ walk(suite.suites, nextStack);
61
+ }
62
+ };
63
+ walk(report?.suites, []);
64
+ return out;
65
+ }
66
+
67
+ /** Drop consumed specs from a suite tree; prune suites left with nothing. */
68
+ function pruneConsumed(suites: any[], consumed: Set<any>): any[] {
69
+ const kept: any[] = [];
70
+ for (const suite of suites) {
71
+ if (!suite || typeof suite !== 'object') continue;
72
+ if (Array.isArray(suite.specs)) suite.specs = suite.specs.filter((s: any) => !consumed.has(s));
73
+ if (Array.isArray(suite.suites)) suite.suites = pruneConsumed(suite.suites, consumed);
74
+ const hasSpecs = Array.isArray(suite.specs) && suite.specs.length > 0;
75
+ const hasSuites = Array.isArray(suite.suites) && suite.suites.length > 0;
76
+ if (hasSpecs || hasSuites) kept.push(suite);
77
+ }
78
+ return kept;
79
+ }
80
+
81
+ /** Recompute the report-level stats counters from the merged tree's final attempts. */
82
+ function recomputeStats(merged: any): void {
83
+ if (!merged.stats || typeof merged.stats !== 'object') return;
84
+ let expected = 0, unexpected = 0, skipped = 0, flaky = 0;
85
+ for (const ref of collectSpecs(merged)) {
86
+ for (const test of ref.node.tests ?? []) {
87
+ const results = test?.results ?? [];
88
+ const last = results[results.length - 1];
89
+ const status = last?.status;
90
+ if (status === 'passed') { if (results.some((r: any) => r?.status === 'failed' || r?.status === 'timedOut')) flaky++; else expected++; }
91
+ else if (status === 'skipped') skipped++;
92
+ else if (status) unexpected++;
93
+ }
94
+ }
95
+ merged.stats = { ...merged.stats, expected, unexpected, skipped, flaky };
96
+ }
97
+
98
+ /**
99
+ * Merge `next` into a DEEP COPY of `base` (neither input is mutated).
100
+ * Throws on inputs that are not report-shaped objects.
101
+ */
102
+ export function mergeResultsReports(base: any, next: any): MergeOutcome {
103
+ for (const [label, r] of [['base', base], ['new', next]] as const) {
104
+ if (!r || typeof r !== 'object' || Array.isArray(r)) {
105
+ throw new Error(`results merge: the ${label} report is not a JSON object.`);
106
+ }
107
+ }
108
+ const merged = JSON.parse(JSON.stringify(base));
109
+ const nextCopy = JSON.parse(JSON.stringify(next));
110
+
111
+ // Last occurrence wins within one report — the same tie-break walkSuites uses.
112
+ const newByTitle = new Map<string, SpecRef>();
113
+ for (const ref of collectSpecs(nextCopy)) newByTitle.set(ref.title, ref);
114
+
115
+ const replaced: string[] = [];
116
+ const kept: string[] = [];
117
+ const consumed = new Set<any>();
118
+ for (const ref of collectSpecs(merged)) {
119
+ const incoming = newByTitle.get(ref.title);
120
+ if (incoming) {
121
+ ref.parent[ref.index] = incoming.node; // the new spec wins wholesale
122
+ consumed.add(incoming.node);
123
+ replaced.push(ref.title);
124
+ } else {
125
+ kept.push(ref.title);
126
+ }
127
+ }
128
+
129
+ const added: string[] = [];
130
+ for (const [title, ref] of newByTitle) {
131
+ if (!consumed.has(ref.node)) added.push(title);
132
+ }
133
+ if (added.length > 0 && Array.isArray(nextCopy.suites)) {
134
+ merged.suites = [...(Array.isArray(merged.suites) ? merged.suites : []), ...pruneConsumed(nextCopy.suites, consumed)];
135
+ }
136
+
137
+ recomputeStats(merged);
138
+ return { merged, replaced, added, kept };
139
+ }
140
+
141
+ /**
142
+ * Ghost detection for the WARN (never a prune): base-kept titles that no
143
+ * longer correspond to a CURRENT spec title. A `@cases` report title carries
144
+ * a ` — <label>` suffix while the spec title is the base form, so a ghost is
145
+ * a title that neither equals a spec title nor extends one with that suffix.
146
+ */
147
+ export function findGhostTitles(keptTitles: string[], specTitles: string[]): string[] {
148
+ return keptTitles.filter((title) =>
149
+ !specTitles.some((spec) => title === spec || title.startsWith(`${spec} — `)));
150
+ }