@sun-asterisk/sungen 3.2.33 → 3.2.34-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/dist/cli/commands/audit.d.ts +4 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +54 -10
  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 +5 -1
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/changed.d.ts.map +1 -1
  9. package/dist/cli/commands/changed.js +2 -3
  10. package/dist/cli/commands/changed.js.map +1 -1
  11. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  12. package/dist/cli/commands/dashboard.js +2 -28
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/defect.d.ts +7 -0
  15. package/dist/cli/commands/defect.d.ts.map +1 -0
  16. package/dist/cli/commands/defect.js +68 -0
  17. package/dist/cli/commands/defect.js.map +1 -0
  18. package/dist/cli/commands/delivery.d.ts.map +1 -1
  19. package/dist/cli/commands/delivery.js +75 -42
  20. package/dist/cli/commands/delivery.js.map +1 -1
  21. package/dist/cli/commands/results-merge.d.ts +9 -0
  22. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  23. package/dist/cli/commands/results-merge.js +7 -0
  24. package/dist/cli/commands/results-merge.js.map +1 -1
  25. package/dist/cli/commands/select.d.ts +12 -0
  26. package/dist/cli/commands/select.d.ts.map +1 -0
  27. package/dist/cli/commands/select.js +122 -0
  28. package/dist/cli/commands/select.js.map +1 -0
  29. package/dist/cli/commands/triage.d.ts +7 -0
  30. package/dist/cli/commands/triage.d.ts.map +1 -0
  31. package/dist/cli/commands/triage.js +98 -0
  32. package/dist/cli/commands/triage.js.map +1 -0
  33. package/dist/cli/commands/update.d.ts.map +1 -1
  34. package/dist/cli/commands/update.js +3 -0
  35. package/dist/cli/commands/update.js.map +1 -1
  36. package/dist/cli/commands/viewpoint.d.ts +7 -0
  37. package/dist/cli/commands/viewpoint.d.ts.map +1 -0
  38. package/dist/cli/commands/viewpoint.js +70 -0
  39. package/dist/cli/commands/viewpoint.js.map +1 -0
  40. package/dist/cli/index.js +13 -2
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/dashboard/templates/index.html +1 -1
  43. package/dist/dashboard/types.d.ts +1 -0
  44. package/dist/dashboard/types.d.ts.map +1 -1
  45. package/dist/exporters/csv-exporter.d.ts +4 -0
  46. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  47. package/dist/exporters/csv-exporter.js +12 -11
  48. package/dist/exporters/csv-exporter.js.map +1 -1
  49. package/dist/exporters/feature-parser.d.ts +8 -0
  50. package/dist/exporters/feature-parser.d.ts.map +1 -1
  51. package/dist/exporters/feature-parser.js +12 -0
  52. package/dist/exporters/feature-parser.js.map +1 -1
  53. package/dist/exporters/json-exporter.d.ts.map +1 -1
  54. package/dist/exporters/json-exporter.js +7 -0
  55. package/dist/exporters/json-exporter.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 +55 -12
  62. package/dist/exporters/matrix/build.js.map +1 -1
  63. package/dist/exporters/matrix/carry-manual-results.d.ts +19 -0
  64. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -1
  65. package/dist/exporters/matrix/carry-manual-results.js +76 -6
  66. package/dist/exporters/matrix/carry-manual-results.js.map +1 -1
  67. package/dist/exporters/matrix/export.d.ts +8 -2
  68. package/dist/exporters/matrix/export.d.ts.map +1 -1
  69. package/dist/exporters/matrix/export.js +15 -3
  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-csv.js +1 -1
  74. package/dist/exporters/matrix/render-csv.js.map +1 -1
  75. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  76. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  77. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  78. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  79. package/dist/exporters/matrix/render-xlsx.js +3 -0
  80. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  81. package/dist/exporters/matrix/types.d.ts +13 -1
  82. package/dist/exporters/matrix/types.d.ts.map +1 -1
  83. package/dist/exporters/matrix/types.js.map +1 -1
  84. package/dist/exporters/matrix/wording.d.ts +1 -1
  85. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  86. package/dist/exporters/matrix/wording.js +10 -1
  87. package/dist/exporters/matrix/wording.js.map +1 -1
  88. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  89. package/dist/exporters/playwright-report-parser.js +1 -0
  90. package/dist/exporters/playwright-report-parser.js.map +1 -1
  91. package/dist/exporters/run-environment.d.ts +32 -0
  92. package/dist/exporters/run-environment.d.ts.map +1 -0
  93. package/dist/exporters/run-environment.js +125 -0
  94. package/dist/exporters/run-environment.js.map +1 -0
  95. package/dist/exporters/scenario-merger.d.ts +18 -0
  96. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  97. package/dist/exporters/scenario-merger.js +49 -3
  98. package/dist/exporters/scenario-merger.js.map +1 -1
  99. package/dist/exporters/test-data-resolver.d.ts +0 -4
  100. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  101. package/dist/exporters/test-data-resolver.js +0 -7
  102. package/dist/exporters/test-data-resolver.js.map +1 -1
  103. package/dist/exporters/types.d.ts +5 -0
  104. package/dist/exporters/types.d.ts.map +1 -1
  105. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  106. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  107. package/dist/generators/gherkin-parser/index.js +49 -19
  108. package/dist/generators/gherkin-parser/index.js.map +1 -1
  109. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  110. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  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/test-file.hbs +16 -0
  115. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  116. package/dist/generators/test-generator/code-generator.js +23 -2
  117. package/dist/generators/test-generator/code-generator.js.map +1 -1
  118. package/dist/generators/test-generator/diagnostics.d.ts +3 -1
  119. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  120. package/dist/generators/test-generator/diagnostics.js +3 -1
  121. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  122. package/dist/harness/audit.d.ts +3 -0
  123. package/dist/harness/audit.d.ts.map +1 -1
  124. package/dist/harness/audit.js +48 -15
  125. package/dist/harness/audit.js.map +1 -1
  126. package/dist/harness/counted-assertions.d.ts +2 -0
  127. package/dist/harness/counted-assertions.d.ts.map +1 -0
  128. package/dist/harness/counted-assertions.js +38 -0
  129. package/dist/harness/counted-assertions.js.map +1 -0
  130. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  131. package/dist/harness/data-driven-lint.js +69 -27
  132. package/dist/harness/data-driven-lint.js.map +1 -1
  133. package/dist/harness/defect-draft.d.ts +14 -0
  134. package/dist/harness/defect-draft.d.ts.map +1 -0
  135. package/dist/harness/defect-draft.js +143 -0
  136. package/dist/harness/defect-draft.js.map +1 -0
  137. package/dist/harness/defects.d.ts +24 -0
  138. package/dist/harness/defects.d.ts.map +1 -0
  139. package/dist/harness/defects.js +118 -0
  140. package/dist/harness/defects.js.map +1 -0
  141. package/dist/harness/manifest.d.ts.map +1 -1
  142. package/dist/harness/manifest.js +2 -2
  143. package/dist/harness/manifest.js.map +1 -1
  144. package/dist/harness/next-step.d.ts +2 -4
  145. package/dist/harness/next-step.d.ts.map +1 -1
  146. package/dist/harness/next-step.js +50 -5
  147. package/dist/harness/next-step.js.map +1 -1
  148. package/dist/harness/parse.d.ts +45 -0
  149. package/dist/harness/parse.d.ts.map +1 -1
  150. package/dist/harness/parse.js +142 -24
  151. package/dist/harness/parse.js.map +1 -1
  152. package/dist/harness/quality-gates.d.ts.map +1 -1
  153. package/dist/harness/quality-gates.js +6 -4
  154. package/dist/harness/quality-gates.js.map +1 -1
  155. package/dist/harness/read-text.d.ts +15 -0
  156. package/dist/harness/read-text.d.ts.map +1 -1
  157. package/dist/harness/read-text.js +25 -0
  158. package/dist/harness/read-text.js.map +1 -1
  159. package/dist/harness/script-check.d.ts.map +1 -1
  160. package/dist/harness/script-check.js +7 -1
  161. package/dist/harness/script-check.js.map +1 -1
  162. package/dist/harness/select-by-ids.d.ts +31 -0
  163. package/dist/harness/select-by-ids.d.ts.map +1 -0
  164. package/dist/harness/select-by-ids.js +106 -0
  165. package/dist/harness/select-by-ids.js.map +1 -0
  166. package/dist/harness/sensors.d.ts +8 -1
  167. package/dist/harness/sensors.d.ts.map +1 -1
  168. package/dist/harness/sensors.js +92 -21
  169. package/dist/harness/sensors.js.map +1 -1
  170. package/dist/harness/spec-branches.js +1 -1
  171. package/dist/harness/spec-branches.js.map +1 -1
  172. package/dist/harness/triage-signals.d.ts +20 -0
  173. package/dist/harness/triage-signals.d.ts.map +1 -0
  174. package/dist/harness/triage-signals.js +30 -0
  175. package/dist/harness/triage-signals.js.map +1 -0
  176. package/dist/harness/triage-unit.d.ts +23 -0
  177. package/dist/harness/triage-unit.d.ts.map +1 -0
  178. package/dist/harness/triage-unit.js +142 -0
  179. package/dist/harness/triage-unit.js.map +1 -0
  180. package/dist/harness/triage.d.ts +28 -0
  181. package/dist/harness/triage.d.ts.map +1 -0
  182. package/dist/harness/triage.js +99 -0
  183. package/dist/harness/triage.js.map +1 -0
  184. package/dist/harness/ui-inventory.d.ts +40 -0
  185. package/dist/harness/ui-inventory.d.ts.map +1 -0
  186. package/dist/harness/ui-inventory.js +191 -0
  187. package/dist/harness/ui-inventory.js.map +1 -0
  188. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  189. package/dist/harness/viewpoint-baseline.js +7 -1
  190. package/dist/harness/viewpoint-baseline.js.map +1 -1
  191. package/dist/harness/viewpoint-draft-header.d.ts +7 -0
  192. package/dist/harness/viewpoint-draft-header.d.ts.map +1 -0
  193. package/dist/harness/viewpoint-draft-header.js +100 -0
  194. package/dist/harness/viewpoint-draft-header.js.map +1 -0
  195. package/dist/harness/viewpoint-ledger.d.ts +13 -3
  196. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  197. package/dist/harness/viewpoint-ledger.js +104 -18
  198. package/dist/harness/viewpoint-ledger.js.map +1 -1
  199. package/dist/harness/viewpoint-mode.d.ts +9 -0
  200. package/dist/harness/viewpoint-mode.d.ts.map +1 -0
  201. package/dist/harness/viewpoint-mode.js +52 -0
  202. package/dist/harness/viewpoint-mode.js.map +1 -0
  203. package/dist/harness/viewpoint-priority.d.ts +19 -0
  204. package/dist/harness/viewpoint-priority.d.ts.map +1 -0
  205. package/dist/harness/viewpoint-priority.js +36 -0
  206. package/dist/harness/viewpoint-priority.js.map +1 -0
  207. package/dist/harness/viewpoint-review-open.d.ts +6 -0
  208. package/dist/harness/viewpoint-review-open.d.ts.map +1 -0
  209. package/dist/harness/viewpoint-review-open.js +90 -0
  210. package/dist/harness/viewpoint-review-open.js.map +1 -0
  211. package/dist/harness/viewpoint-scan-refs.d.ts +50 -0
  212. package/dist/harness/viewpoint-scan-refs.d.ts.map +1 -0
  213. package/dist/harness/viewpoint-scan-refs.js +200 -0
  214. package/dist/harness/viewpoint-scan-refs.js.map +1 -0
  215. package/dist/harness/viewpoint-scan.d.ts +41 -0
  216. package/dist/harness/viewpoint-scan.d.ts.map +1 -0
  217. package/dist/harness/viewpoint-scan.js +118 -0
  218. package/dist/harness/viewpoint-scan.js.map +1 -0
  219. package/dist/orchestrator/flow-manager.d.ts +0 -1
  220. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  221. package/dist/orchestrator/flow-manager.js +3 -52
  222. package/dist/orchestrator/flow-manager.js.map +1 -1
  223. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  224. package/dist/orchestrator/screen-manager.js +8 -62
  225. package/dist/orchestrator/screen-manager.js.map +1 -1
  226. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  227. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  228. package/dist/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  229. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  230. package/dist/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  231. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  232. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +22 -0
  233. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -9
  234. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  235. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  236. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  237. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  238. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  239. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  240. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  241. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  242. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  243. package/dist/orchestrator/templates/specs-url-assert.js +24 -2
  244. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  245. package/dist/orchestrator/templates/specs-url-assert.ts +23 -2
  246. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  247. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  248. package/dist/orchestrator/trace-mode-install.js +132 -0
  249. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  250. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +23 -0
  251. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  252. package/dist/orchestrator/viewpoint-pattern-catalog.js +48 -0
  253. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  254. package/dist/orchestrator/viewpoint-scaffold.d.ts +3 -0
  255. package/dist/orchestrator/viewpoint-scaffold.d.ts.map +1 -0
  256. package/dist/orchestrator/viewpoint-scaffold.js +70 -0
  257. package/dist/orchestrator/viewpoint-scaffold.js.map +1 -0
  258. package/package.json +3 -3
  259. package/src/cli/commands/audit.ts +49 -10
  260. package/src/cli/commands/capability.ts +3 -1
  261. package/src/cli/commands/changed.ts +2 -4
  262. package/src/cli/commands/dashboard.ts +1 -26
  263. package/src/cli/commands/defect.ts +49 -0
  264. package/src/cli/commands/delivery.ts +70 -42
  265. package/src/cli/commands/results-merge.ts +7 -1
  266. package/src/cli/commands/select.ts +88 -0
  267. package/src/cli/commands/triage.ts +57 -0
  268. package/src/cli/commands/update.ts +3 -0
  269. package/src/cli/commands/viewpoint.ts +58 -0
  270. package/src/cli/index.ts +13 -2
  271. package/src/dashboard/templates/index.html +1 -1
  272. package/src/dashboard/types.ts +1 -0
  273. package/src/exporters/csv-exporter.ts +12 -11
  274. package/src/exporters/feature-parser.ts +12 -0
  275. package/src/exporters/json-exporter.ts +6 -1
  276. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  277. package/src/exporters/matrix/build.ts +57 -11
  278. package/src/exporters/matrix/carry-manual-results.ts +70 -7
  279. package/src/exporters/matrix/export.ts +16 -2
  280. package/src/exporters/matrix/gates.ts +6 -0
  281. package/src/exporters/matrix/render-csv.ts +1 -1
  282. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  283. package/src/exporters/matrix/render-xlsx.ts +3 -0
  284. package/src/exporters/matrix/types.ts +13 -1
  285. package/src/exporters/matrix/wording.ts +10 -1
  286. package/src/exporters/playwright-report-parser.ts +1 -0
  287. package/src/exporters/run-environment.ts +89 -0
  288. package/src/exporters/scenario-merger.ts +51 -5
  289. package/src/exporters/test-data-resolver.ts +0 -6
  290. package/src/exporters/types.ts +5 -0
  291. package/src/generators/gherkin-parser/index.ts +50 -18
  292. package/src/generators/test-generator/adapters/adapter-interface.ts +4 -0
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  296. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  297. package/src/generators/test-generator/code-generator.ts +23 -2
  298. package/src/generators/test-generator/diagnostics.ts +3 -1
  299. package/src/harness/audit.ts +52 -17
  300. package/src/harness/counted-assertions.ts +33 -0
  301. package/src/harness/data-driven-lint.ts +82 -24
  302. package/src/harness/defect-draft.ts +115 -0
  303. package/src/harness/defects.ts +94 -0
  304. package/src/harness/manifest.ts +2 -2
  305. package/src/harness/next-step.ts +47 -5
  306. package/src/harness/parse.ts +151 -27
  307. package/src/harness/quality-gates.ts +6 -4
  308. package/src/harness/read-text.ts +25 -0
  309. package/src/harness/script-check.ts +7 -1
  310. package/src/harness/select-by-ids.ts +83 -0
  311. package/src/harness/sensors.ts +91 -21
  312. package/src/harness/spec-branches.ts +1 -1
  313. package/src/harness/triage-signals.ts +46 -0
  314. package/src/harness/triage-unit.ts +113 -0
  315. package/src/harness/triage.ts +127 -0
  316. package/src/harness/ui-inventory.ts +164 -0
  317. package/src/harness/viewpoint-baseline.ts +7 -1
  318. package/src/harness/viewpoint-draft-header.ts +55 -0
  319. package/src/harness/viewpoint-ledger.ts +111 -24
  320. package/src/harness/viewpoint-mode.ts +49 -0
  321. package/src/harness/viewpoint-priority.ts +38 -0
  322. package/src/harness/viewpoint-review-open.ts +51 -0
  323. package/src/harness/viewpoint-scan-refs.ts +154 -0
  324. package/src/harness/viewpoint-scan.ts +111 -0
  325. package/src/orchestrator/flow-manager.ts +3 -53
  326. package/src/orchestrator/screen-manager.ts +7 -61
  327. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  328. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  329. package/src/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  330. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  331. package/src/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  332. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  333. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +22 -0
  334. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -9
  335. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  336. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  337. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  338. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  339. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  340. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  341. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  342. package/src/orchestrator/templates/playwright.config.ts +4 -2
  343. package/src/orchestrator/templates/specs-url-assert.ts +23 -2
  344. package/src/orchestrator/trace-mode-install.ts +94 -0
  345. package/src/orchestrator/viewpoint-pattern-catalog.ts +61 -0
  346. package/src/orchestrator/viewpoint-scaffold.ts +36 -0
@@ -17,7 +17,8 @@ import { mergeFeatureAndSpec } from '../scenario-merger';
17
17
  import { getPackageVersion } from '../package-info';
18
18
  import { writeCsv } from '../csv-exporter';
19
19
  import { deliverableXlsxPath, writeXlsx } from '../xlsx-exporter';
20
- import { carryManualResults, ManualCarryReport, readPriorManualResults } from './carry-manual-results';
20
+ import { applyManualCarry, carryManualResults, ManualCarryReport, PriorManualResults, readPriorManualResults } from './carry-manual-results';
21
+ export { readPriorManualResults } from './carry-manual-results';
21
22
  import { loadPerfConfig, perfVerdict } from '../../harness/perf';
22
23
  import { loadDeliveryMap, writeDeliveryMap } from './map-loader';
23
24
  import { mapContentFingerprint } from './fingerprint';
@@ -26,6 +27,7 @@ import { renderMatrixXlsx } from './render-xlsx';
26
27
  import { renderMatrixCsv } from './render-csv';
27
28
  import { DeliveryMap, MatrixFinding, MatrixModel } from './types';
28
29
  import { ApiCatalogEntry } from '../types';
30
+ import { RunEnvironment } from '../run-environment';
29
31
 
30
32
  export interface MatrixTargetPaths {
31
33
  cwd: string;
@@ -46,6 +48,8 @@ export interface MatrixTargetPaths {
46
48
  specMdFile?: string;
47
49
  resultsPath: string | null;
48
50
  mapFile: string;
51
+ /** Executor + environment of the run (#850) — absent for gates-only callers (inspect, approve). */
52
+ runEnv?: RunEnvironment;
49
53
  }
50
54
 
51
55
  function readSpecText(paths: MatrixTargetPaths): string {
@@ -91,6 +95,7 @@ export function loadMatrixModel(paths: MatrixTargetPaths): MatrixLoadResult {
91
95
  specText: readSpecText(paths),
92
96
  apiCatalog: catalogFor(paths),
93
97
  });
98
+ model.runEnv = paths.runEnv;
94
99
  // Perf verdict (#569): budgets are config (qa/perf.yaml), samples are the run's
95
100
  // per-test durations. Advisory — reported here because delivery is where runs
96
101
  // are read; the audit's score stays a pure function of the design artifacts.
@@ -164,12 +169,21 @@ export async function writeMatrixDeliverables(
164
169
  paths: MatrixTargetPaths,
165
170
  model: MatrixModel,
166
171
  format: MatrixFormat = 'xlsx',
172
+ /** The prior workbook's Manual results, when the caller already read them; read here if absent. */
173
+ priorRead?: PriorManualResults | null,
167
174
  ): Promise<{ csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport }> {
168
175
  const out: { csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport } = {};
169
176
  const xlsx = format === 'xlsx' || format === 'both';
170
177
  // Read the workbook about to be overwritten BEFORE writing anything, so the tester's Manual
171
178
  // results survive (#756) and a refusal leaves no half-written target behind.
172
- const prior = xlsx ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit)) : null;
179
+ // A CSV-only export overwrites no workbook, so an unreadable one is no reason to refuse it — but a
180
+ // readable one still holds the Manual results the CSV should show.
181
+ const prior = priorRead !== undefined ? priorRead
182
+ : xlsx ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit))
183
+ : await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit)).catch(() => null);
184
+ // Into the model first: the CSV, every cached total and roll-up, and the caller's summary
185
+ // all read it (#891).
186
+ applyManualCarry(model, prior);
173
187
  if (format === 'csv' || format === 'both') {
174
188
  out.csvPath = writeCsv(paths.cwd, paths.unit, renderMatrixCsv(model));
175
189
  }
@@ -102,6 +102,12 @@ function gateASource(ctx: GateContext, findings: MatrixFinding[]): void {
102
102
  : `scenario ${vpId} has no Then step — a variant without an observable oracle cannot be delivered`,
103
103
  });
104
104
  }
105
+ if (m.unlabeledManualOracle) {
106
+ findings.push({
107
+ gate: 'A', severity: 'warning', ref: vpId,
108
+ message: `manual oracle found only in an unlabeled comment — exported verbatim into Expected; write the \`# Tester verifies:\` block as numbered "N. Observable: …" lines`,
109
+ });
110
+ }
105
111
  }
106
112
  for (const [vpId, n] of seen) {
107
113
  if (n > 1) findings.push({ gate: 'A', severity: 'error', ref: vpId, message: `duplicate VP-id ${vpId} (${n} scenarios)` });
@@ -78,7 +78,7 @@ export function renderMatrixCsv(model: MatrixModel): string {
78
78
  v.mode === 'manual' ? 'Manual' : 'Auto',
79
79
  v.traces.join(' '),
80
80
  variantState(v),
81
- isoDate(v.result?.startTime),
81
+ v.manualDate ?? isoDate(v.result?.startTime),
82
82
  // Redact BEFORE clipping — a clip boundary must never split a secret in
83
83
  // a way that lets its prefix through. Same denylist as the Note column.
84
84
  v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
@@ -17,6 +17,7 @@ import * as ExcelJS from 'exceljs';
17
17
  import { renderReportHeaderBand, ARIAL_FONT } from '../xlsx-report-builder';
18
18
  import { redactSecrets } from '../api-testcase-formatter';
19
19
  import { clipNoteField } from '../playwright-report-parser';
20
+ import { runCells } from '../run-environment';
20
21
  import { variantState } from './build';
21
22
  import { DeliveryItem, MatrixModel } from './types';
22
23
  import { SOFT_BLUE, DRAFT_RED, RESULT_STATES, dataCell, headerCell, draftBanner } from './xlsx-cells';
@@ -42,6 +43,9 @@ export const RESULT_COL = 12;
42
43
 
43
44
  /** Variant sub-row — only what distinguishes it from its parent (review GAP-01). */
44
45
  function variantValues(item: DeliveryItem, v: DeliveryItem['variants'][number], model: MatrixModel): ExcelJS.CellValue[] {
46
+ // Who ran it and against what — the same cells the legacy workbook fills (#850); blank
47
+ // until the variant has actually run.
48
+ const run = runCells(model.runEnv, v.result);
45
49
  return [
46
50
  v.ref,
47
51
  '',
@@ -50,15 +54,17 @@ function variantValues(item: DeliveryItem, v: DeliveryItem['variants'][number],
50
54
  numbered(item.preconditionDeltas[v.ref] ?? []),
51
55
  numbered(v.data),
52
56
  // Only the steps after the parent's shared prefix, numbered so the parent's
53
- // steps + these read as one continuous procedure.
54
- numbered(item.triggerDeltas[v.ref] ?? [], item.trigger.length + 1),
57
+ // steps + these read as one continuous procedure. Counted from the REAL shared
58
+ // steps, not `trigger.length`: with nothing shared the parent shows a generated
59
+ // "the action differs…" note, and the variant's first step read "2." (#848).
60
+ numbered(item.triggerDeltas[v.ref] ?? [], item.sharedTriggerSteps + 1),
55
61
  expectedWithVerification(v.oracle, v.verification),
56
62
  v.traces.join(', '),
57
63
  variantTypeLabel(v),
58
64
  variantState(v),
59
65
  isoDate(v.result?.startTime),
60
- '',
61
- '',
66
+ run.executor,
67
+ run.environment,
62
68
  // Redact BEFORE clipping — same denylist as the testcases CSV Note column.
63
69
  v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
64
70
  ];
@@ -74,6 +74,9 @@ export function projectItems(model: MatrixModel, mode: 'auto' | 'manual'): Deliv
74
74
 
75
75
  export function renderMatrixXlsx(model: MatrixModel, sungenVersion?: string): ExcelJS.Workbook {
76
76
  const wb = new ExcelJS.Workbook();
77
+ // Every total and roll-up is a formula with a cached result. The cache is what a preview
78
+ // (GitHub, Drive, QuickLook) shows; a spreadsheet app recomputes on open as well (#891).
79
+ wb.calcProperties.fullCalcOnLoad = true;
77
80
  const sheets: SheetProjection[] = [
78
81
  { sheetName: 'Auto', items: projectItems(model, 'auto') },
79
82
  { sheetName: 'Manual', items: projectItems(model, 'manual') },
@@ -147,6 +147,11 @@ export interface CoverageVariant {
147
147
  fingerprint: string;
148
148
  /** Execution result, when the run report has one for this variant. */
149
149
  result?: PlaywrightResult;
150
+ /** A Manual variant's result as the tester recorded it in the previous workbook, carried
151
+ * BEFORE rendering so every total, roll-up, the CSV and the CLI count it (#891). */
152
+ manualState?: VariantState;
153
+ /** The Executed Date the tester recorded with it (yyyy-mm-dd, or as typed). */
154
+ manualDate?: string;
150
155
  }
151
156
 
152
157
  export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run' | 'na';
@@ -176,8 +181,12 @@ export interface DeliveryItem {
176
181
  /** Preconditions shared by EVERY variant (the intersection) — written once here. */
177
182
  precondition: string[];
178
183
  /** Leading trigger steps shared by every variant — written once here; each
179
- * variant renders only its remaining steps (`triggerDeltas`). */
184
+ * variant renders only its remaining steps (`triggerDeltas`). When nothing is shared this
185
+ * holds a generated "the action differs…" note instead, which is NOT a step. */
180
186
  trigger: string[];
187
+ /** How many of `trigger` are real steps hoisted from the variants (0 when it is only the
188
+ * note) — the variants' steps are numbered from this + 1 (#848). */
189
+ sharedTriggerSteps: number;
181
190
  variants: CoverageVariant[];
182
191
  /** variant ref → its precondition lines that are not on the parent. */
183
192
  preconditionDeltas: Record<string, string[]>;
@@ -253,6 +262,9 @@ export interface MatrixModel {
253
262
  * the deliverable ships to the client.
254
263
  */
255
264
  secretValues: string[];
265
+ /** Who ran the suite and against what — fills Test Executor / Test Environment on rows that
266
+ * ran (#850). Absent when the caller has no run context (gates-only paths). */
267
+ runEnv?: import('../run-environment').RunEnvironment;
256
268
  }
257
269
 
258
270
  /** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
@@ -13,6 +13,8 @@
13
13
  * Action → action, Observable → expected, Oracle → verification method.
14
14
  */
15
15
 
16
+ import { extractUnlabeledOracle, onlyPresenceAssertions } from '../manual-hybrid-oracle';
17
+
16
18
  /** A step that CAPTURES a runtime value: `User remember [X] text as {{var}}`. */
17
19
  export const RUNTIME_CAPTURE = /\bremember\b/i;
18
20
 
@@ -229,7 +231,7 @@ export interface ManualProcedure {
229
231
  * review feedback §10.2(3). Continuation lines append to the previous item;
230
232
  * pre-amble (rationale/header/dividers) is skipped.
231
233
  */
232
- export function classifyManualComments(comments: string[]): ManualProcedure {
234
+ export function classifyManualComments(comments: string[], thenTexts: string[] = []): ManualProcedure {
233
235
  const out: ManualProcedure = { preconditions: [], actions: [], expected: [], verification: [] };
234
236
  let last: { list: string[]; idx: number } | null = null;
235
237
 
@@ -255,5 +257,12 @@ export function classifyManualComments(comments: string[]): ManualProcedure {
255
257
  last.list[last.idx] += ' ' + line;
256
258
  }
257
259
  }
260
+ // #810 — an oracle written as unlabelled `Tester verifies:` prose is still the oracle: with no
261
+ // labelled Observable line and no Then beyond presence (`thenTexts`), export it rather than fall
262
+ // back to a presence-placeholder Then. A real Then stays the oracle.
263
+ if (!out.expected.length && onlyPresenceAssertions(thenTexts)) {
264
+ const prose = extractUnlabeledOracle(comments);
265
+ if (prose) out.expected.push(prose);
266
+ }
258
267
  return out;
259
268
  }
@@ -161,6 +161,7 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
161
161
  errorMessages: distinct.length > 0 ? distinct : undefined,
162
162
  tracePath: trace,
163
163
  projectName: test?.projectName,
164
+ attempts: results.length || undefined,
164
165
  });
165
166
  }
166
167
  return result;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Who ran the tests, and against what — the Test Executor / Test Environment columns of every
3
+ * deliverable (legacy workbook, matrix workbook) and the dashboard.
4
+ *
5
+ * One module so the surfaces cannot drift: the matrix format shipped with both cells hard-coded
6
+ * blank on every Auto row while the legacy exporter filled them (#850).
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { execSync } from 'child_process';
11
+ import { resolveWebBaseUrl } from './env-qa';
12
+ import { statusToTestResult } from './playwright-report-parser';
13
+ import { EnvironmentInfo, PlaywrightResult } from './types';
14
+
15
+ /**
16
+ * The executor, most explicit source first: `SUNGEN_EXECUTOR` (a CI job names the person or
17
+ * pipeline that ran the suite — whoever runs `sungen delivery` there is not that person), then
18
+ * git user.name, then `CI_USER` / `USER`.
19
+ */
20
+ export function resolveExecutor(cwd: string): string {
21
+ const explicit = process.env.SUNGEN_EXECUTOR?.trim();
22
+ if (explicit) return explicit;
23
+ try {
24
+ const gitUser = execSync('git config user.name', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
25
+ if (gitUser) return gitUser;
26
+ } catch { /* not a git checkout, or no user.name */ }
27
+ return process.env.CI_USER || process.env.USER || '';
28
+ }
29
+
30
+ /** baseURL + browser project from playwright.config.ts, executor from {@link resolveExecutor}. */
31
+ export function getEnvironment(cwd: string): EnvironmentInfo {
32
+ let baseURL = '';
33
+ let projectName = 'chromium';
34
+ const configPath = path.join(cwd, 'playwright.config.ts');
35
+ if (fs.existsSync(configPath)) {
36
+ const c = fs.readFileSync(configPath, 'utf-8');
37
+ // Every config shape sungen has scaffolded: the literal `baseURL: 'x'`, the env-aware
38
+ // `baseURL: resolveBaseUrl('x')`, and the older `baseURL: process.env.SUNGEN_BASE_URL || 'x'`
39
+ // (the quoted value is the static fallback in both wrappers).
40
+ const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*|process\.env\.\w+\s*\|\|\s*)?['"]([^'"]+)['"]/);
41
+ if (baseMatch) baseURL = baseMatch[1];
42
+ const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
43
+ if (projMatch) projectName = projMatch[1];
44
+ }
45
+ // SUNGEN_BASE_URL (shell > .env.qa.<SUNGEN_ENV> > .env.qa) overrides the static
46
+ // value — the Test Environment column must show the host the run actually hit.
47
+ baseURL = resolveWebBaseUrl(cwd, baseURL);
48
+ return { baseURL, projectName, executor: resolveExecutor(cwd) };
49
+ }
50
+
51
+ /** `https://host/path (project)` — or, on mobile (no baseURL), the bare app id `(project)`. */
52
+ export function formatEnv(baseURL: string, featurePath: string | undefined, projectName: string): string {
53
+ const base = (baseURL || '').replace(/\/+$/, '');
54
+ const p = (featurePath || '').replace(/^\/+/, '');
55
+ // Join with '/' only when there is a baseURL (web). For mobile baseURL is empty
56
+ // and `p` is an app id (e.g. com.kngroup.media.converter) — emit it bare, no '/'.
57
+ const url = base && p ? `${base}/${p}` : (p || base);
58
+ return `${url} (${projectName})`;
59
+ }
60
+
61
+ /**
62
+ * The platform a result file belongs to — `<f>-test-result.<os>[.<env>].json` → `<os>`. It names
63
+ * the run actually being read, so it outranks `MOBILE_PLATFORM` / `SUNGEN_ENV`, and SUNGEN_ENV
64
+ * is usually an environment (`staging`, `ja`), not an OS. A web locale file yields its locale,
65
+ * which is harmless: only a dual-id mobile `Path:` (`android | ios`) reads the platform.
66
+ */
67
+ export function platformOfResultFile(resultsPath: string | null | undefined): string | undefined {
68
+ return resultsPath ? path.basename(resultsPath).match(/-test-result\.([^.]+)\./)?.[1] : undefined;
69
+ }
70
+
71
+ /** What a run-result row needs to fill its executor/environment cells. */
72
+ export interface RunEnvironment extends EnvironmentInfo {
73
+ /** The unit's `Path:` (flows: their page selector; mobile: the OS-precise app id). */
74
+ featurePath?: string;
75
+ }
76
+
77
+ /**
78
+ * The Test Executor / Test Environment cells for one result — blank unless the test actually
79
+ * ran (a Pending or N/A row has nobody who executed it and no environment it ran against).
80
+ */
81
+ export function runCells(env: RunEnvironment | undefined, result: PlaywrightResult | undefined): { executor: string; environment: string } {
82
+ if (!env || !result) return { executor: '', environment: '' };
83
+ const outcome = statusToTestResult(result.status);
84
+ if (outcome === 'Pending' || outcome === 'N/A') return { executor: '', environment: '' };
85
+ return {
86
+ executor: env.executor,
87
+ environment: formatEnv(env.baseURL, env.featurePath, result.projectName || env.projectName),
88
+ };
89
+ }
@@ -9,8 +9,9 @@
9
9
  */
10
10
 
11
11
  import { FeatureMetadata, OrderedStep, ScenarioMetadata, SpecFileData, SpecTest } from './types';
12
- import { isStepsBaseScenario, isSampleScaffoldScenario, extractTestcaseType } from './feature-parser';
12
+ import { isStepsBaseScenario, isSampleScaffoldScenario, isHookScenario, extractTestcaseType } from './feature-parser';
13
13
  import { SCENARIO_ID } from './feature-parser';
14
+ import { extractUnlabeledOracle, onlyPresenceAssertions } from './manual-hybrid-oracle';
14
15
 
15
16
  /**
16
17
  * Parse a @manual scenario's `# Tester verifies:` comment block into procedure vs
@@ -21,8 +22,12 @@ import { SCENARIO_ID } from './feature-parser';
21
22
  * (Setup/Action/Precondition/unlabelled) → Steps. A non-numbered line following a
22
23
  * numbered item is a continuation (e.g. a wrapped URL) and is appended to it. Pre-amble
23
24
  * lines (the `MANUAL:` rationale, the `Tester verifies:` header, separators) are ignored.
25
+ *
26
+ * `unlabeledOracle` is the prose a `Tester verifies:` header carries OUTSIDE numbered lines
27
+ * (#810). It is not part of `expected` — the gates judge the numbered shape — but the exporter
28
+ * must never drop it: when no labelled oracle line exists it becomes the Expected cell.
24
29
  */
25
- export function parseManualComments(comments: string[]): { steps: OrderedStep[]; expected: OrderedStep[] } {
30
+ export function parseManualComments(comments: string[]): { steps: OrderedStep[]; expected: OrderedStep[]; unlabeledOracle: string | null } {
26
31
  const steps: OrderedStep[] = [];
27
32
  const expected: OrderedStep[] = [];
28
33
  let last: OrderedStep | null = null;
@@ -45,7 +50,31 @@ export function parseManualComments(comments: string[]): { steps: OrderedStep[];
45
50
  }
46
51
  // else: pre-amble before any numbered item (rationale / header) → skip
47
52
  }
48
- return { steps, expected };
53
+ return { steps, expected, unlabeledOracle: extractUnlabeledOracle(comments) };
54
+ }
55
+
56
+ /**
57
+ * The reason a @manual scenario is manual — the text of its `# MANUAL…: <reason>` comment
58
+ * (`MANUAL:`, `MANUAL (2026-09-16, run-test):`, `MANUAL/ENV:` …) plus its indented continuation
59
+ * lines, up to the `Tester verifies:` header, a numbered item or a divider. '' when absent.
60
+ * Rendered in the deliverable's Note so a tester sees why the case is not automated (#810).
61
+ */
62
+ export function extractManualReason(comments: string[]): string {
63
+ const parts: string[] = [];
64
+ let inReason = false;
65
+ for (const raw of comments) {
66
+ const line = raw.trim();
67
+ if (!inReason) {
68
+ const m = line.match(/^MANUAL\b[^:]*:\s*(.*)$/i);
69
+ if (m) { inReason = true; if (m[1].trim()) parts.push(m[1].trim()); }
70
+ continue;
71
+ }
72
+ // A wrapped reason continues on an INDENTED comment line (`# automate once seeded.`); the
73
+ // comment list carries no line numbers, so an unindented line is a separate note, not more reason.
74
+ if (!line || !/^\s/.test(raw) || /^tester verifies\b/i.test(line) || /^\d+[.)]\s/.test(line) || /^[-=*_]{2,}/.test(line)) break;
75
+ parts.push(line);
76
+ }
77
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
49
78
  }
50
79
 
51
80
  export interface MergedScenario {
@@ -84,6 +113,12 @@ export interface MergedScenario {
84
113
  * so counting them would falsely mark @manual scenarios as interleaved.
85
114
  */
86
115
  resolvedAnchorActionCount: number;
116
+ /**
117
+ * Set when this @manual scenario's oracle exists ONLY as unlabelled `Tester verifies:` prose
118
+ * (#810). The prose is exported verbatim into `resolvedExpected`; delivery warns so the author
119
+ * rewrites it as numbered `Observable:` lines.
120
+ */
121
+ unlabeledManualOracle?: string;
87
122
  }
88
123
 
89
124
  /**
@@ -104,6 +139,7 @@ export function mergeFeatureAndSpec(
104
139
  for (const scenario of feature.scenarios) {
105
140
  if (isStepsBaseScenario(scenario)) continue; // skip @steps base scenarios
106
141
  if (isSampleScaffoldScenario(scenario)) continue; // skip default sample
142
+ if (isHookScenario(scenario)) continue; // skip @beforeAll/@afterEach/@afterAll hooks
107
143
 
108
144
  const specTest = findMatchingSpecTest(scenario, spec.tests, feature.scenarios);
109
145
  const base = scenario.extendsName ? stepsBaseByName.get(scenario.extendsName) : undefined;
@@ -147,7 +183,16 @@ export function mergeFeatureAndSpec(
147
183
  const manualParsed =
148
184
  extractTestcaseType(scenario.tags) === 'Manual' && scenario.comments?.length
149
185
  ? parseManualComments(scenario.comments)
150
- : { steps: [] as OrderedStep[], expected: [] as OrderedStep[] };
186
+ : { steps: [] as OrderedStep[], expected: [] as OrderedStep[], unlabeledOracle: null };
187
+ // Never drop a manual oracle (#810): with no labelled Observable/Oracle line and no Then that
188
+ // checks more than presence, the unlabelled `Tester verifies:` prose IS what the case checks —
189
+ // export it rather than let a presence placeholder stand in for it. A real Then keeps the cell.
190
+ const unlabeledOracle = !manualParsed.expected.length && onlyPresenceAssertions(ownAssertions.map((s) => s.text))
191
+ ? manualParsed.unlabeledOracle
192
+ : null;
193
+ const manualExpected: OrderedStep[] = unlabeledOracle
194
+ ? [{ text: unlabeledOracle, bucket: 'then' }]
195
+ : manualParsed.expected;
151
196
 
152
197
  result.push({
153
198
  feature: scenario,
@@ -158,8 +203,9 @@ export function mergeFeatureAndSpec(
158
203
  ...ownActions,
159
204
  ...manualParsed.steps,
160
205
  ],
161
- resolvedExpected: [...ownAssertions, ...manualParsed.expected],
206
+ resolvedExpected: [...ownAssertions, ...manualExpected],
162
207
  resolvedAnchorActionCount: baseLen + ownActions.length,
208
+ ...(unlabeledOracle ? { unlabeledManualOracle: unlabeledOracle } : {}),
163
209
  });
164
210
  }
165
211
 
@@ -190,9 +190,3 @@ export function renderDynamicTokens(text: string): string {
190
190
  });
191
191
  }
192
192
 
193
- /**
194
- * Resolve the test-data file path for a given screen.
195
- */
196
- export function resolveTestDataPath(cwd: string, screen: string): string {
197
- return path.join(cwd, 'qa', 'screens', screen, 'test-data', `${screen}.yaml`);
198
- }
@@ -95,6 +95,9 @@ export interface ScenarioMetadata {
95
95
  * as Gherkin steps — the merger parses these into Steps/Expected for the report.
96
96
  */
97
97
  comments?: string[];
98
+ /** Set for a `@beforeAll` / `@afterEach` / `@afterAll` scenario — a hook the compiler emits as
99
+ * `test.beforeAll()` etc., never a test case (#880). */
100
+ hookType?: 'beforeAll' | 'afterEach' | 'afterAll';
98
101
  }
99
102
 
100
103
  /**
@@ -134,6 +137,8 @@ export interface PlaywrightResult {
134
137
  errorMessages?: string[];
135
138
  tracePath?: string;
136
139
  projectName?: string;
140
+ /** How many attempts the test took (retries + 1). A pass with attempts > 1 is flaky. */
141
+ attempts?: number;
137
142
  }
138
143
 
139
144
  /**
@@ -154,11 +154,18 @@ export class GherkinParser {
154
154
  }
155
155
 
156
156
  /**
157
- * Range-attach top-level Gherkin comments to the scenario whose body contains them.
158
- * A comment belongs to scenario i when its line is after scenario i's `Scenario:` line
159
- * and before scenario i+1's header (its first tag line, or its `Scenario:` line). The
160
- * leading `# ` is stripped. Used to surface a @manual scenario's tester-procedure comments
157
+ * Attach top-level Gherkin comments to the scenario they describe. The leading `# ` is
158
+ * stripped. Used to surface a @manual scenario's tester procedure and `# MANUAL:` reason
161
159
  * (its only "steps") in the delivery report.
160
+ *
161
+ * A scenario owns:
162
+ * • its LEADING block — the comment lines directly above its header with no blank line in
163
+ * between (and any comment between its tag lines and `Scenario:`), as long as they sit
164
+ * after the previous scenario's last step. `# MANUAL (…): reason` written above the tags
165
+ * belongs to the scenario below it; it used to range onto the scenario ABOVE, where the
166
+ * manual parser appended it to that scenario's last oracle line (#810);
167
+ * • its BODY — every other comment after its `Scenario:` line and before the next
168
+ * scenario's leading block / header.
162
169
  */
163
170
  private attachScenarioComments(
164
171
  doc: Messages.GherkinDocument,
@@ -167,22 +174,32 @@ export class GherkinParser {
167
174
  ): void {
168
175
  const comments = doc.comments || [];
169
176
  if (!comments.length) return;
170
- const scenChildren = feature.children
171
- .filter((c) => c.scenario)
172
- .map((c) => c.scenario!);
177
+ const lineOf = new Map<number, string>(
178
+ comments.map((cm) => [cm.location.line, cm.text.replace(/^\s*#\s?/, '').replace(/\s+$/, '')]),
179
+ );
173
180
  // Same filter/order as parseScenarios → indices line up with `scenarios`.
181
+ const scenChildren = feature.children.filter((c) => c.scenario).map((c) => c.scenario!);
182
+ const background = feature.children.find((c) => c.background)?.background;
183
+ const headerLine = (sc: Messages.Scenario): number =>
184
+ Math.min(sc.location.line, ...(sc.tags || []).map((t) => t.location.line));
185
+
186
+ // Leading block of scenario i: [firstLeading, sc.location.line).
187
+ const leadingStart = scenChildren.map((sc, i) => {
188
+ const prevEnd = i > 0 ? bodyEndLine(scenChildren[i - 1]) : background ? bodyEndLine(background) : feature.location.line;
189
+ let first = headerLine(sc);
190
+ while (lineOf.has(first - 1) && first - 1 > prevEnd) first--;
191
+ return first;
192
+ });
193
+
174
194
  for (let i = 0; i < scenChildren.length && i < scenarios.length; i++) {
175
- const startLine = scenChildren[i].location.line;
176
- const next = scenChildren[i + 1];
177
- let endLine = Number.POSITIVE_INFINITY;
178
- if (next) {
179
- const tagLines = (next.tags || []).map((t) => t.location.line);
180
- endLine = Math.min(next.location.line, ...(tagLines.length ? tagLines : [next.location.line]));
181
- }
182
- const body = comments
183
- .filter((cm) => cm.location.line > startLine && cm.location.line < endLine)
184
- .map((cm) => cm.text.replace(/^\s*#\s?/, '').replace(/\s+$/, ''));
185
- if (body.length) scenarios[i].comments = body;
195
+ const from = leadingStart[i];
196
+ const to = i + 1 < scenChildren.length ? leadingStart[i + 1] : Number.POSITIVE_INFINITY;
197
+ const own = comments
198
+ .map((cm) => cm.location.line)
199
+ .filter((line) => line >= from && line < to)
200
+ .sort((a, b) => a - b)
201
+ .map((line) => lineOf.get(line)!);
202
+ if (own.length) scenarios[i].comments = own;
186
203
  }
187
204
  }
188
205
 
@@ -360,3 +377,18 @@ export class GherkinParser {
360
377
  return files;
361
378
  }
362
379
  }
380
+
381
+ /** Last source line a scenario/background occupies: its steps, their data tables/doc strings, examples. */
382
+ function bodyEndLine(block: Messages.Scenario | Messages.Background): number {
383
+ let end = block.location.line;
384
+ for (const step of block.steps) {
385
+ end = Math.max(end, step.location.line);
386
+ for (const row of step.dataTable?.rows ?? []) end = Math.max(end, row.location.line);
387
+ if (step.docString) end = Math.max(end, step.docString.location.line + step.docString.content.split('\n').length + 1);
388
+ }
389
+ for (const ex of ('examples' in block ? block.examples : [])) {
390
+ end = Math.max(end, ex.location.line);
391
+ for (const row of [...(ex.tableHeader ? [ex.tableHeader] : []), ...ex.tableBody]) end = Math.max(end, row.location.line);
392
+ }
393
+ return end;
394
+ }
@@ -5,7 +5,11 @@
5
5
 
6
6
  export interface AuthGroup {
7
7
  authRole?: string;
8
+ noAuth?: boolean;
8
9
  scenarios: string[];
10
+ /** The feature's `@afterEach`, scoped to this group — set when the feature mixes `@no-auth` with
11
+ * other groups, so the hook never runs on an anonymous test's empty session (#815). */
12
+ afterEach?: string;
9
13
  }
10
14
 
11
15
  /**
@@ -1,2 +1,2 @@
1
1
  await {{> locator}}.click();
2
- await page.getByText('{{selectValue}}', { exact: true }).first().click();
2
+ {{> pick-option}}
@@ -9,7 +9,7 @@ if ((await el.evaluate((n) => n.tagName.toLowerCase())) === 'select') {
9
9
  await el.selectOption('{{selectValue}}');
10
10
  } else {
11
11
  await el.click();
12
- await page.getByText('{{selectValue}}', { exact: true }).first().click();
12
+ {{> pick-option}}
13
13
  }
14
14
  }
15
15
  {{~else~}}
@@ -0,0 +1,13 @@
1
+ {{!-- #847 — pick the value in an OPENED custom listbox (Radix/shadcn, MUI, Headless UI).
2
+ The popup portals to the end of <body>, so a page-wide getByText(…).first() resolved to
3
+ any EARLIER element with the same text (a badge showing the current choice, a result
4
+ card) — and the open popup blocks clicks outside it, so the step timed out every time,
5
+ looking like a race. The ARIA option lives only in the open listbox: wait briefly for it
6
+ (a count() right after opening would race the popup animation), and fall back to the old
7
+ text match only for a widget that exposes no listbox roles. --}}
8
+ { const __opt = page.getByRole('option', { name: '{{selectValue}}', exact: true }).first();
9
+ if (await __opt.waitFor({ state: 'visible', timeout: 3000 }).then(() => true, () => false)) {
10
+ await __opt.click();
11
+ } else {
12
+ await page.getByText('{{selectValue}}', { exact: true }).first().click();
13
+ } }
@@ -46,6 +46,10 @@ test.describe('{{featureName}}', () => {
46
46
  test.describe('{{authRole}}', () => {
47
47
  test.use({ storageState: 'specs/.auth/{{authRole}}.json' });
48
48
 
49
+ {{#if afterEach}}
50
+ {{indent afterEach 2}}
51
+
52
+ {{/if}}
49
53
  {{#each scenarios}}
50
54
  {{indent this 2}}
51
55
 
@@ -63,6 +67,18 @@ test.describe('{{featureName}}', () => {
63
67
  {{#each scenarios}}
64
68
  {{indent this 2}}
65
69
 
70
+ {{/each}}
71
+ });
72
+
73
+ {{else if afterEach}}
74
+ {{!-- The un-tagged group next to a `@no-auth` one (#815): an ANONYMOUS describe scopes the hook to
75
+ these tests without adding a title segment, so test titles (and delivery fingerprints) stay put. --}}
76
+ test.describe(() => {
77
+ {{indent afterEach 2}}
78
+
79
+ {{#each scenarios}}
80
+ {{indent this 2}}
81
+
66
82
  {{/each}}
67
83
  });
68
84