@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
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Select scenarios by test-case ID — backs `sungen select` and `run-test --ids` (#835).
3
+ *
4
+ * An ID means what it means everywhere else in the harness: `vpId`, the leading ID token of the
5
+ * scenario title (`VP-UI-001`, `MS-HP-001`, `VP0-001`). Matching is exact and case-insensitive —
6
+ * `VP-UI-001` never selects `VP-UI-0010` or `VP-UI-001-B` — and every ID that selects nothing
7
+ * runnable is reported, because the alternative is a 0-test run that reports green.
8
+ */
9
+ import * as path from 'path';
10
+ import { ScenarioInfo } from './parse';
11
+
12
+ /**
13
+ * Escape scenario titles into one `-g` / `--mochaOpts.grep` regex. Full titles, not bare IDs:
14
+ * a title starts with its own ID, so it can only appear inside its own test's title — and a
15
+ * `@cases` row test (`<title> — <row label>`) still contains it.
16
+ */
17
+ export function titlesToGrep(titles: string[]): string {
18
+ return titles.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
19
+ }
20
+
21
+ export interface IdSelectionGroup {
22
+ /** Feature basename (`products`, `home-modal`) — names the spec and the result file. */
23
+ feature: string;
24
+ ids: string[];
25
+ titles: string[];
26
+ grep: string;
27
+ }
28
+
29
+ export interface IdSelection {
30
+ features: IdSelectionGroup[];
31
+ /** IDs no scenario carries, with near matches (`VP-VAL-001` → `VP-VAL-001-B`). */
32
+ unknown: Array<{ id: string; didYouMean: string[] }>;
33
+ /** IDs whose every scenario is manual, owned by a flow or waiting on a capability — nothing to run. */
34
+ notRunnable: Array<{ id: string; reason: string }>;
35
+ }
36
+
37
+ /** `"VP-UI-001, vp-ui-002 VP-UI-001"` → `['VP-UI-001', 'VP-UI-002']`. */
38
+ export function parseIdList(raw: string): string[] {
39
+ return [...new Set(raw.split(/[\s,]+/).map((s) => s.trim().toUpperCase()).filter(Boolean))];
40
+ }
41
+
42
+ /** `VP-C-002.` → `VP-C-002`: the title token may carry trailing punctuation the user never types. */
43
+ const idOf = (s: ScenarioInfo): string | undefined => s.vpId?.replace(/[.-]+$/, '') || undefined;
44
+
45
+ function notRunnableReason(s: ScenarioInfo, enabledCaps: Set<string>): string | null {
46
+ if (s.deferredToFlow) return `owned by a flow${s.ownedByFlow ? ` (${s.ownedByFlow})` : ''} — run it there`;
47
+ if (s.featureManual) return 'its Feature is @manual — no spec is generated for it';
48
+ if (s.manual) return 'manual — no automated test exists';
49
+ // Compiled as `test.skip` while the capability is off: the run would report a skip, not a result.
50
+ const missing = (s.requiresCaps ?? []).filter((c) => !enabledCaps.has(c));
51
+ if (missing.length) return `needs capability ${missing.join(', ')} — compiled as a skip until \`sungen capability add ${missing.join(' ')}\``;
52
+ return null;
53
+ }
54
+
55
+ export function selectByIds(scenarios: ScenarioInfo[], ids: string[], unit: string, enabledCaps: Set<string> = new Set()): IdSelection {
56
+ const out: IdSelection = { features: [], unknown: [], notRunnable: [] };
57
+ const groups = new Map<string, IdSelectionGroup>();
58
+ const allIds = [...new Set(scenarios.map(idOf).filter((v): v is string => !!v))];
59
+
60
+ for (const id of ids) {
61
+ const hits = scenarios.filter((s) => idOf(s) === id);
62
+ if (hits.length === 0) {
63
+ const didYouMean = allIds.filter((v) => v.startsWith(`${id}-`) || id.startsWith(`${v}-`)).slice(0, 5);
64
+ out.unknown.push({ id, didYouMean });
65
+ continue;
66
+ }
67
+ const runnable = hits.filter((s) => !notRunnableReason(s, enabledCaps));
68
+ if (runnable.length === 0) {
69
+ out.notRunnable.push({ id, reason: notRunnableReason(hits[0], enabledCaps)! });
70
+ continue;
71
+ }
72
+ for (const s of runnable) {
73
+ const feature = s.sourceFile ? path.basename(s.sourceFile, '.feature') : unit;
74
+ const g = groups.get(feature) ?? { feature, ids: [], titles: [], grep: '' };
75
+ if (!g.ids.includes(id)) g.ids.push(id);
76
+ if (!g.titles.includes(s.name)) g.titles.push(s.name);
77
+ groups.set(feature, g);
78
+ }
79
+ }
80
+ for (const g of groups.values()) g.grep = titlesToGrep(g.titles);
81
+ out.features = [...groups.values()];
82
+ return out;
83
+ }
@@ -12,6 +12,8 @@ import { parse as parseYaml } from 'yaml';
12
12
  import { ScenarioInfo, ViewpointEntry, idPrefix } from './parse';
13
13
  import { readTextFile } from './read-text';
14
14
  import { keywordHit, hasKeyword } from './keyword-match';
15
+ import { parsePriority } from './viewpoint-priority';
16
+ import { countedAssertions } from './counted-assertions';
15
17
 
16
18
  // Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
17
19
  // compound category like LIST-DISPLAY / ADD-TO-CART / PRODUCT-DISCOVERY classifies correctly).
@@ -634,6 +636,9 @@ export interface TraceResult {
634
636
  mappedToOverview: number;
635
637
  withVpCodeRatio: number;
636
638
  mappedRatio: number;
639
+ /** Viewpoint IDs read from test-viewpoint.md. 0 = nothing to map to, so `mappedRatio` is not a
640
+ * measurement — the scenario IDs were never checked (#832). */
641
+ overviewIds: number;
637
642
  note: string;
638
643
  }
639
644
 
@@ -645,6 +650,24 @@ export interface TraceResult {
645
650
  */
646
651
  const unprefixed = (id: string): string => id.replace(/^(?:VP|FL)-/i, '').toUpperCase();
647
652
 
653
+ /**
654
+ * The traceability note, worded from the SAME ratio the audit's TRACE finding fires on —
655
+ * mapped / ALL scenarios (#852). It used to compare mapped against scenarios WITH an id, so 0 of
656
+ * 0 (no scenario yet, or none tagged) and 2 of 2 tagged out of 10 all read "Traceable." while
657
+ * the audit reported `TRACE: Traceable.` for a 0–20% mapping.
658
+ */
659
+ function traceNote(total: number, withCode: number, mapped: number, declared: number, exampleId?: string): string {
660
+ if (declared === 0) {
661
+ return 'No viewpoint ID found in test-viewpoint.md — the file is missing, or none of its sections declares one — so the scenario IDs were not checked. Declarations are read from a `## Priority Viewpoints` table, `### Required / Recommended / Optional` lists, and tables whose first column is a `VP-`/`FL-` ID or an ID of a family declared there (Known Issues / Traceability / References sections are skipped).';
662
+ }
663
+ if (total === 0) return 'No scenarios yet — traceability is measured once the feature has scenarios.';
664
+ if (mapped >= total * 0.5) return 'Traceable.';
665
+ const example = exampleId ? ` (e.g. \`${exampleId}-001 …\`)` : '';
666
+ if (withCode === 0) return `No scenario carries a viewpoint ID — start each title with one of the IDs declared in test-viewpoint.md${example}.`;
667
+ if (mapped < withCode * 0.5) return 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).';
668
+ return `Only ${mapped} of ${total} scenarios carry a declared viewpoint ID — ${total - withCode} have no ID at all; tag them with the project's IDs${example}.`;
669
+ }
670
+
648
671
  export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
649
672
  const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
650
673
  const overviewBare = new Set(viewpoints.map((v) => unprefixed(v.id)));
@@ -669,9 +692,10 @@ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEnt
669
692
  mappedToOverview: mapped.length,
670
693
  withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
671
694
  mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
672
- note: withCode.length && mapped.length < withCode.length * 0.5
673
- ? 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).'
674
- : 'Traceable.',
695
+ overviewIds: overviewIds.size,
696
+ // With no ID read there is nothing to match against: telling the author to re-tag sends them
697
+ // to change scenarios that may be right (#832). Name the file and the sections read instead.
698
+ note: traceNote(scenarios.length, withCode.length, mapped.length, overviewIds.size, viewpoints[0]?.id),
675
699
  };
676
700
  }
677
701
 
@@ -733,6 +757,31 @@ const CLAIM_RULES: ClaimRule[] = [
733
757
  hint: 'swap `contains {{v}}` for `with {{v}}` on the continuity rows. A substring match cannot distinguish "unchanged" from "changed by adding a prefix" — and if the screen genuinely renders extra text around the value, say so in the title instead of claiming exactness.',
734
758
  severity: 'warn',
735
759
  },
760
+ {
761
+ // An ORDER claim ("sorted by date descending", "ties broken by last-updated") is invisible to
762
+ // a presence check: `see [List] table` passes whatever order the rows are in. It sits ahead of
763
+ // the generic rules so "all results sorted …" is judged as an ordering claim, not an all-claim.
764
+ // A tiebreaker is a Tier-1 guard in the tc-generation skill, yet no rule recognised it (#810).
765
+ claim: 'ordering/sort',
766
+ title: /\b(ascending|descending|sorted|sorts|sort order|ordered by|order by|in (?:chronological|alphabetical|reverse|numeric) order|(?:newest|oldest|latest|earliest|highest|lowest) first|tie-?break(?:s|er|ers)?|ties?)\b|降順|昇順|並び順|giảm dần|tăng dần|sắp xếp/,
767
+ proof: /match data|is sorted|sorted (?:ascending|descending)|\brow \d+\b|\.first\.|\.last\.|\[\d+\]\.|order by/,
768
+ need: 'an order-sensitive assertion (`table match data:` rows in the expected order, `is sorted ascending|descending`, or positional `row N` checks)',
769
+ hint: 'a presence check passes in any order — assert the rows in their expected sequence (`User see [T] table match data:`), or the column sort state; a tiebreak needs two rows sharing the primary key.',
770
+ severity: 'fail',
771
+ },
772
+ {
773
+ // A LIMIT claim ("never exceeds 10", "at most 5 files") is a count claim in disguise.
774
+ // Ahead of 'no-side-effect', whose negation+effect pattern would otherwise swallow
775
+ // "never exceeds 10 records" and ask for a duplicate-count proof.
776
+ claim: 'limit/cap',
777
+ // The quantity must be ≥2 ("at most ONE order" is a no-duplicate claim — left to the rules
778
+ // below) and not a duration ("up to 30 seconds" is a timing claim, not a cap).
779
+ title: /\b(?:never exceeds?|(?:does not|doesn['’]t|not) exceed|at most|no more than|up to|capped at|caps? at|maximum of|max(?:imum)?)\s+(?:\d{2,}|[2-9]|two|three|four|five|six|seven|eight|nine|ten)\b(?!\s*(?:ms|milliseconds?|s|secs?|seconds?|mins?|minutes?|hours?|days?)\b)|\b(?:never exceeds?|(?:does not|doesn['’]t) exceed) (?:the )?(?:limit|cap|quota|maximum)\b|以内|上限|tối đa/,
780
+ proof: /\bcount\b|tohavecount|table with|row with \{\{|match data|is at most|\bexactly\b|field with \{\{|\berror\b|invalid|is disabled/,
781
+ need: 'a count/boundary assertion (count is N, `is at most`, the N+1th input rejected or truncated)',
782
+ hint: 'assert the boundary itself — the count at the cap, or the value/error when one more is attempted — not just that the list or field is present.',
783
+ severity: 'fail',
784
+ },
736
785
  {
737
786
  claim: 'all/every/each',
738
787
  title: /\b(all|every|each)\b/,
@@ -900,7 +949,14 @@ export interface TaxonomyResult {
900
949
 
901
950
  const TAXONOMY_KNOWN = new Set(['LIST', 'PRODUCT', 'CART', 'FILTER', 'NAV', 'VAL', 'SEC']);
902
951
 
903
- const CATEGORY_SIGNALS: { cat: string; re: RegExp }[] = [
952
+ /**
953
+ * Words of ACCESS — a title about who may reach something. They make the robustness/routing words
954
+ * below security evidence ("deep link without authentication"); alone those words describe routing.
955
+ */
956
+ const ACCESS_CONTEXT = /\b(xss|sql|injection|without authentication|unauthenticated|unauthori[sz]ed|without an? (?:authenticated|valid|active) \w+|without (?:permission|login|logging in|signing in|an? account|access)|not (?:logged|signed) in|logged[- ]out|direct(?:ly)? access\w*|auth[- ]?guard|forbidden|\b403\b|(?:no|insufficient|missing) permissions?|permission required|session expired|expired session|tamper\w*)\b/;
957
+
958
+ /** `requires`: the signal counts only when the title ALSO matches it (#888). */
959
+ const CATEGORY_SIGNALS: { cat: string; re: RegExp; requires?: RegExp }[] = [
904
960
  { cat: 'FILTER', re: /\b(categor(y|ies)|sub-?categor\w*|expand\w*|collaps\w*|\bfacet\b|filter\w*|belongs? to)\b/ },
905
961
  { cat: 'CART', re: /\b(add(ed|ing)? to cart|added (dialog|modal|message|confirmation)|cart (page|table|line|item)|view cart|continue shopping|quantit)\b/ },
906
962
  { cat: 'PRODUCT', re: /\b(product detail|product-detail|view product|product page|same product|product name and price|product identity|detail product)\b/ },
@@ -912,9 +968,16 @@ const CATEGORY_SIGNALS: { cat: string; re: RegExp }[] = [
912
968
  // SEC, so the linter told QA to re-tag genuine auth-gate tests as navigation — diluting the
913
969
  // visibility of the security suite (QA field report, 3.2.23). The guard vocabulary below is
914
970
  // what makes the INTENT matchable, and GUARD_INTENT then wins over a mere observable.
915
- { cat: 'SEC', re: /\b(xss|sql|injection|payload|without authentication|unauthenticated|without an? (?:authenticated|valid|active) \w+|direct(?:ly)? access\w*|deep[- ]link\w*|auth[- ]?guard|not skippable|without having (?:completed|submitted|logged)|remains? unauthenticated|\binert\b|tamper\w*|malformed|url parameter|query param\w*|falls? back|gracefull?y|without a crash|not[- ]found|\b404\b|special character|abuse)\b/ },
971
+ { cat: 'SEC', re: /\b(xss|sql|injection|payload|without authentication|unauthenticated|without an? (?:authenticated|valid|active) \w+|direct(?:ly)? access\w*|auth[- ]?guard|not skippable|without having (?:completed|submitted|logged)|remains? unauthenticated|\binert\b|tamper\w*|malformed|special character|abuse)\b/ },
972
+ // Robustness and routing words are SEC evidence only beside an ACCESS word. Alone they describe
973
+ // routing — "Unknown route shows the 404 page", "Deep link opens the item", "the tab from the url
974
+ // parameter" — and the linter told QA to re-tag those navigation tests as VP-SEC (#888).
975
+ { cat: 'SEC', re: /\b(deep[- ]link\w*|url parameter|query param\w*|falls? back|gracefull?y|without a crash|not[- ]found|\b404\b)\b/, requires: ACCESS_CONTEXT },
916
976
  ];
917
977
 
978
+ const signalMatches = (c: { re: RegExp; requires?: RegExp }, title: string): boolean =>
979
+ c.re.test(title) && (!c.requires || c.requires.test(title));
980
+
918
981
  /**
919
982
  * Categories whose signal describes the test's INTENT/risk rather than the observable action.
920
983
  * When one of these matches, a competing observable-only category (NAV: "redirects", "navigates")
@@ -932,14 +995,19 @@ export function taxonomyLint(scenarios: ScenarioInfo[]): TaxonomyResult {
932
995
  if (!cat || !TAXONOMY_KNOWN.has(cat)) continue;
933
996
  checked++;
934
997
  const title = s.name.toLowerCase();
935
- const matched = [...new Set(CATEGORY_SIGNALS.filter((c) => c.re.test(title)).map((c) => c.cat))];
998
+ const matched = [...new Set(CATEGORY_SIGNALS.filter((c) => signalMatches(c, title)).map((c) => c.cat))];
936
999
  if (matched.includes(cat)) continue; // title supports its own category → fine
1000
+ // A gate (`requires`) only stops a word from SUGGESTING its category. An author who already
1001
+ // chose that category is supported by the word alone: a VP-SEC "Deep link to the filtered
1002
+ // list …" stays SEC instead of being pushed toward FILTER (#888 review).
1003
+ if (CATEGORY_SIGNALS.some((c) => c.cat === cat && c.requires && c.re.test(title))) continue;
937
1004
  // The declared category may be an INTENT category whose signal this title spells
938
1005
  // differently; never "correct" it toward a category that only names the observable.
939
1006
  const suppressed = INTENT_OVER_OBSERVABLE[cat];
940
1007
  const others = matched.filter((c) => c !== cat && !(suppressed && suppressed.has(c)));
941
1008
  if (others.length === 1) { // unambiguous mismatch
942
- const sig = CATEGORY_SIGNALS.find((c) => c.cat === others[0])!;
1009
+ // The entry that MATCHED — a category can have several (SEC: guard words, gated robustness words).
1010
+ const sig = CATEGORY_SIGNALS.find((c) => c.cat === others[0] && signalMatches(c, title))!;
943
1011
  const m = title.match(sig.re);
944
1012
  mislabeled.push({ name: s.name, current: cat, suggested: others[0], signal: m ? m[0] : others[0] });
945
1013
  }
@@ -1011,9 +1079,8 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
1011
1079
  if (!m) continue;
1012
1080
  const claimed = NUMBER_WORDS[m[1]] ?? Number(m[1]);
1013
1081
  if (!Number.isFinite(claimed) || claimed < 2) continue;
1014
- // Only value-bearing assertions count: `see [X] … with/contains {{v}}`. A visibility check
1015
- // does not demonstrate one of N values.
1016
- const asserted = (s.stepsText.match(/\bsee \[[^\]]+\][^|]*?\b(?:with|contains) \{\{/g) ?? []).length;
1082
+ // Values, positive states and indicator presence count; plain text visibility does not (#866).
1083
+ const asserted = countedAssertions(s.stepsText);
1017
1084
  if (asserted > 0 && asserted < claimed) out.push({ name: s.name, claimed, asserted, noun: m[2] });
1018
1085
  }
1019
1086
  return out;
@@ -1025,17 +1092,17 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
1025
1092
  * The Priority-Viewpoints table is where release selection is supposed to come from. When the
1026
1093
  * table says a flow is `Low` and its scenario is tagged `@high`, a smoke run picks up work the QA
1027
1094
  * deliberately deprioritised — and nobody can tell which of the two was the decision (#622).
1095
+ *
1096
+ * Reads the SAME parse traceability does (`parseViewpointOverview`). It used to scan the raw text
1097
+ * for any `| VP-… | high |` row, so one audit run "saw" an id for this check and not for
1098
+ * traceability (#833), and a row in a Known Issues table could raise a contradiction.
1028
1099
  */
1029
1100
  export function priorityMismatches(
1030
- viewpointText: string, scenarios: ScenarioInfo[],
1101
+ viewpoints: ViewpointEntry[], scenarios: ScenarioInfo[],
1031
1102
  ): Array<{ id: string; declared: string; tagged: string; scenario: string }> {
1032
1103
  const declared = new Map<string, string>();
1033
- for (const m of viewpointText.matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)) {
1034
- declared.set(m[1].toUpperCase(), m[2].toLowerCase());
1035
- }
1104
+ for (const v of viewpoints) if (v.priorityWord) declared.set(v.id.toUpperCase(), v.priorityWord);
1036
1105
  if (declared.size === 0) return [];
1037
- // `medium` and `normal` are the same rank under different names in real documents.
1038
- const rank = (p: string): string => (p === 'medium' ? 'normal' : p);
1039
1106
  const out: Array<{ id: string; declared: string; tagged: string; scenario: string }> = [];
1040
1107
  for (const s of scenarios) {
1041
1108
  const id = (s.vpId ?? '').toUpperCase();
@@ -1043,11 +1110,14 @@ export function priorityMismatches(
1043
1110
  // The most specific declared row that prefixes this id.
1044
1111
  const key = [...declared.keys()].filter((k) => id.startsWith(k)).sort((a, b) => b.length - a.length)[0];
1045
1112
  if (!key) continue;
1046
- const want = rank(declared.get(key)!);
1047
- const got = rank(String(s.priority).toLowerCase());
1048
- // `critical` and `deferred` have no tag equivalent — nothing to contradict.
1049
- if (want === 'critical' || want === 'deferred') continue;
1050
- if (want !== got) out.push({ id: key, declared: declared.get(key)!, tagged: got, scenario: s.name });
1113
+ // Both sides fold through the one priority reading (`critical` ≡ high, `medium`/`med` ≡ normal).
1114
+ const want = parsePriority(declared.get(key)!);
1115
+ // `deferred` and `skip` (declared not applicable, #853) have no tag equivalent — nothing to contradict.
1116
+ if (want === 'deferred' || want === 'skip') continue;
1117
+ // An untagged scenario states no priority, so it cannot contradict one (#861).
1118
+ const tagged = s.priorityTag ?? (s.priority === 'unknown' ? undefined : String(s.priority));
1119
+ if (!tagged) continue;
1120
+ if (want !== parsePriority(tagged)) out.push({ id: key, declared: declared.get(key)!, tagged, scenario: s.name });
1051
1121
  }
1052
1122
  return out;
1053
1123
  }
@@ -194,7 +194,7 @@ export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: s
194
194
  if (s.manual) continue; // a procedure's oracle is prose
195
195
  const asserts = [...(s.steps ?? [])]
196
196
  .filter((st) => st.bucket === 'then')
197
- .map((st) => st.text.toLowerCase().replace(/\s+/g, ' ').trim())
197
+ .map((st) => `${st.text} ${st.arg ?? ''}`.toLowerCase().replace(/\s+/g, ' ').trim()) // the table is the oracle too (#817)
198
198
  .sort();
199
199
  if (asserts.length === 0) continue;
200
200
  const key = asserts.join(' ; ');
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Triage signal library — the ordered message patterns `sungen triage` matches a red scenario's
3
+ * error against. Seeded from the repair rules (`harness/repair.ts`, driver-mobile `mobile-repair.ts`);
4
+ * the patterns are COPIED, not imported, because core never loads a driver at runtime. The parity
5
+ * check in tests/harness/triage-classification.run.ts keeps the environment rules in step.
6
+ *
7
+ * Order matters and is the classification order: environment first (nothing about the test or the
8
+ * app can be judged when the session never started), then test defects (the test never reached the
9
+ * app). Assertions are decided in triage.ts from the parsed Expected/Received, not here.
10
+ */
11
+ export type SignalClass = 'environment' | 'test-defect';
12
+
13
+ export interface TriageSignal {
14
+ id: string;
15
+ class: SignalClass;
16
+ match: RegExp;
17
+ }
18
+
19
+ export const TRIAGE_SIGNALS: TriageSignal[] = [
20
+ // --- environment: the run could not reach a usable app/session -----------------------------------
21
+ { id: 'env-network', class: 'environment', match: /net::ERR_[A-Z_]+|\bECONNREFUSED\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bETIMEDOUT\b/ },
22
+ // mirrors mobile-session-not-created
23
+ { id: 'env-session-not-created', class: 'environment', match: /A new session could not be created|session not created/i },
24
+ // mirrors mobile-driver-proxy-lost
25
+ { id: 'env-driver-proxy-lost', class: 'environment', match: /Could not proxy command to (?:the )?remote server|socket hang up|ECONNRESET/ },
26
+ // mirrors mobile-instrumentation-failed
27
+ { id: 'env-instrumentation-failed', class: 'environment', match: /instrumentation process (?:cannot|could not) be (?:initialized|started)|Cannot start the instrumentation process/ },
28
+ // mirrors mobile-chromedriver-mismatch
29
+ { id: 'env-chromedriver-mismatch', class: 'environment', match: /No Chromedriver found|chromedriver.*(?:version|mismatch)|Chrome version must be/i },
30
+ // mirrors mobile-webview-context-absent
31
+ { id: 'env-webview-context-absent', class: 'environment', match: /no WEBVIEW\/CHROMIUM context appeared within/ },
32
+ { id: 'env-auth-state-missing', class: 'environment', match: /storageState|\.auth\/[\w.-]+\.json|ENOENT[^\n]*\.auth/ },
33
+ { id: 'env-base-url-unresolved', class: 'environment', match: /Cannot navigate to invalid URL|baseURL (?:is )?(?:not set|undefined)|Invalid URL/ },
34
+ { id: 'env-browser-closed', class: 'environment', match: /Target page, context or browser has been closed|browserType\.launch/ },
35
+
36
+ // --- test defect: the test could not reach the element or its data -------------------------------
37
+ { id: 'test-strict-mode', class: 'test-defect', match: /strict mode violation/ },
38
+ { id: 'test-locator-unresolved', class: 'test-defect', match: /locator\.\w+: [^\n]*(?:Timeout|timeout)[^\n]*exceeded[\s\S]*waiting for |resolved to 0 elements/ },
39
+ { id: 'test-element-absent', class: 'test-defect', match: /no such element|Element not found:|An element could not be located/ },
40
+ { id: 'test-data-missing', class: 'test-defect', match: /Test data key not found:|@cases dataset "[^"]+" not found/ },
41
+ { id: 'test-step-unmatched', class: 'test-defect', match: /\[sungen\] step "[^"]*" matched no pattern|\bSG\d{3}\b/ },
42
+ { id: 'test-tap-intercepted', class: 'test-defect', match: /element click intercepted|is not clickable at point|element is obscured|intercepts pointer events/i },
43
+ ];
44
+
45
+ /** A failure that is only Playwright's test-level timeout — no locator, no assertion detail (→ unclassified). */
46
+ export const BARE_TIMEOUT = /^(?:Error:\s*)?Test timeout of \d+ms exceeded\.?$/;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * `sungen triage` IO: find a unit's feature(s) and result file(s), join them on the same seam
3
+ * delivery and the dashboard use (`mergeFeatureAndSpec` + `resolveResultVariants`), classify each
4
+ * scenario, and persist `.sungen/triage/<unit>.json`. Read-only apart from that one report file.
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { parseFeatureMetadata } from '../exporters/feature-parser';
9
+ import { loadPlaywrightReport, resolveResultsPath } from '../exporters/playwright-report-parser';
10
+ import { collectQaEnvSecretDenylist } from '../exporters/qa-env-secret-denylist';
11
+ import { isSensitiveKey } from '../exporters/sensitive-key';
12
+ import { resolveResultVariants } from '../exporters/result-variants';
13
+ import { mergeFeatureAndSpec } from '../exporters/scenario-merger';
14
+ import { parseSpecFile } from '../exporters/spec-parser';
15
+ import { loadTestData } from '../exporters/test-data-resolver';
16
+ import type { ResolvedUnit } from '../cli/resolve-unit';
17
+ import { classifyScenario, TriageClass, TriageRow } from './triage';
18
+
19
+ export interface TriageReport {
20
+ unit: string;
21
+ generatedAt: string;
22
+ /** Result files read, relative to the project root. Empty → nothing has run yet. */
23
+ resultFiles: string[];
24
+ counts: Partial<Record<TriageClass, number>>;
25
+ rows: TriageRow[];
26
+ }
27
+
28
+ /**
29
+ * Values that must never reach a triage evidence line or a defect record: the resolved `${QA_*}`
30
+ * secrets AND the literal values of credential-named test-data keys — an assertion on a password
31
+ * field prints that value in its Expected/Received.
32
+ */
33
+ export function unitSecrets(testData: Record<string, string>, cwd: string): string[] {
34
+ const literal = Object.entries(testData)
35
+ .filter(([k, v]) => isSensitiveKey(k) && typeof v === 'string' && v.length >= 3 && !v.includes('${'))
36
+ .map(([, v]) => String(v));
37
+ return [...new Set([...collectQaEnvSecretDenylist(testData, cwd).values, ...literal])].sort((a, b) => b.length - a.length);
38
+ }
39
+
40
+ function generatedDir(cwd: string, u: ResolvedUnit): string {
41
+ return path.join(cwd, 'specs', 'generated', u.unitId);
42
+ }
43
+
44
+ /**
45
+ * Every result file of one feature, labelled by variant: `<base>-test-result.json` (no label),
46
+ * `<base>-test-result.<os|env>[.<env>].json` (label = the middle part). A mobile unit has only
47
+ * per-OS files, so all of them are read rather than the first one found. Falls back to the shared
48
+ * lookup (incl. `test-results/results.json`) when the generated dir holds none.
49
+ */
50
+ function resultFilesFor(cwd: string, genDir: string, base: string): Array<{ file: string; variant?: string }> {
51
+ const prefix = `${base}-test-result`;
52
+ const own = fs.existsSync(genDir)
53
+ ? fs.readdirSync(genDir).filter((f) => f.startsWith(prefix) && f.endsWith('.json')).sort()
54
+ : [];
55
+ if (own.length > 0) {
56
+ return own.map((f) => {
57
+ const variant = f.slice(prefix.length, -'.json'.length).replace(/^\./, '');
58
+ return { file: path.join(genDir, f), ...(variant ? { variant } : {}) };
59
+ });
60
+ }
61
+ const shared = resolveResultsPath(cwd, genDir, base);
62
+ return shared ? [{ file: shared }] : [];
63
+ }
64
+
65
+ export function triageUnit(cwd: string, u: ResolvedUnit): TriageReport {
66
+ const genDir = generatedDir(cwd, u);
67
+ const featuresDir = path.join(u.dir, 'features');
68
+ const features = fs.existsSync(featuresDir) ? fs.readdirSync(featuresDir).filter((f) => f.endsWith('.feature')).sort() : [];
69
+ const rows: TriageRow[] = [];
70
+ const resultFiles: string[] = [];
71
+
72
+ for (const f of features) {
73
+ const base = f.replace(/\.feature$/, '');
74
+ const files = resultFilesFor(cwd, genDir, base);
75
+ if (files.length === 0) continue;
76
+ const merged = mergeFeatureAndSpec(parseFeatureMetadata(path.join(featuresDir, f)), parseSpecFile(path.join(genDir, `${base}.spec.ts`)));
77
+ const dataFile = path.join(u.dir, 'test-data', `${base}.yaml`);
78
+ const testData = fs.existsSync(dataFile) ? loadTestData(dataFile) : {};
79
+ const secrets = unitSecrets(testData, cwd);
80
+
81
+ for (const { file, variant } of files) {
82
+ resultFiles.push(path.relative(cwd, file));
83
+ const results = loadPlaywrightReport(file);
84
+ for (const m of merged) {
85
+ for (const v of resolveResultVariants(m, results, testData)) {
86
+ const r = classifyScenario({ unit: u.unitId, title: `${m.feature.name}${v.nameSuffix}`, tags: m.feature.tags, result: v.result, variant }, secrets);
87
+ if (r) rows.push(r);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ const counts: Partial<Record<TriageClass, number>> = {};
94
+ for (const r of rows) counts[r.class] = (counts[r.class] ?? 0) + 1;
95
+ return { unit: u.unitId, generatedAt: new Date().toISOString(), resultFiles, counts, rows };
96
+ }
97
+
98
+ export function triageReportPath(cwd: string, unitId: string): string {
99
+ return path.join(cwd, '.sungen', 'triage', `${unitId.replace(/\//g, '-')}.json`);
100
+ }
101
+
102
+ /** The last triage of a unit, or null when `sungen triage` has not run for it. */
103
+ export function readTriageReport(cwd: string, unitId: string): TriageReport | null {
104
+ try { return JSON.parse(fs.readFileSync(triageReportPath(cwd, unitId), 'utf-8')) as TriageReport; } catch { return null; }
105
+ }
106
+
107
+ /** Persist under `.sungen/triage/<unit>.json` (a flow `flows/x` → `flows-x.json`); returns the path. */
108
+ export function writeTriageReport(cwd: string, report: TriageReport): string {
109
+ const out = triageReportPath(cwd, report.unit);
110
+ fs.mkdirSync(path.dirname(out), { recursive: true });
111
+ fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`, 'utf-8');
112
+ return out;
113
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `sungen triage` — classify each red or flaky scenario of a run into exactly one class, with the
3
+ * rule that fired and one evidence line. Deterministic and read-only; it reports, it never gates.
4
+ *
5
+ * Order (first match wins):
6
+ * 1. known-defect `@known-defect` / `@known-defect:<id>` — an expected red.
7
+ * 2. blocked-capability `@manual:*` / `@requires:*`, or skipped by design.
8
+ * 3. environment the session or network never gave the test a usable app.
9
+ * 4. test-defect the test could not reach its element or data.
10
+ * 5. product-candidate an assertion whose Expected and Received both exist and differ.
11
+ * 6. unclassified anything else — left for the agent or QA, with the raw first message.
12
+ * A `passed` scenario that needed a retry is `flaky` (green, listed separately).
13
+ *
14
+ * 5 is tested before 3 and 4 in code: a parsed Expected/Received pair that differs means the app
15
+ * answered, which outranks any wrapper text (a test timeout around the assertion included).
16
+ *
17
+ * A bare test timeout (no locator, no assertion) is `unclassified` too: a slow environment, a page
18
+ * that hangs on a product bug and a test waiting on the wrong thing all print the same line.
19
+ *
20
+ * An assertion on an element that never appeared (`Received: <element(s) not found>`) is NOT a test
21
+ * defect: a wrong selector and a missing message look identical, so it is left for judgment rather
22
+ * than hiding a possible product bug behind "fix the selector".
23
+ */
24
+ import { redactSecrets } from '../exporters/api-testcase-formatter';
25
+ import { parseExpectedReceived } from '../exporters/playwright-report-parser';
26
+ import type { PlaywrightResult } from '../exporters/types';
27
+ import { BARE_TIMEOUT, TRIAGE_SIGNALS } from './triage-signals';
28
+
29
+ export type TriageClass =
30
+ | 'known-defect' | 'blocked-capability' | 'environment' | 'test-defect' | 'product-candidate' | 'unclassified' | 'flaky';
31
+
32
+ export interface TriageInput {
33
+ unit: string;
34
+ title: string;
35
+ tags: string[];
36
+ result?: PlaywrightResult;
37
+ /** Result-file variant (`android`, `ios`, an env name) when the unit has several. */
38
+ variant?: string;
39
+ }
40
+
41
+ export interface TriageRow {
42
+ unit: string;
43
+ scenario: string;
44
+ variant?: string;
45
+ class: TriageClass;
46
+ ruleId: string;
47
+ evidence: string;
48
+ ids: string[];
49
+ legacy: string[];
50
+ defectId?: string;
51
+ tracePath?: string;
52
+ /** product-candidate only: the two halves of the assertion, for a defect draft. */
53
+ expected?: string;
54
+ received?: string;
55
+ }
56
+
57
+ const EVIDENCE_MAX = 200;
58
+ const VIEWPOINT_ID = /\b(?:VP|FL)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
59
+ const ELEMENT_ABSENT = /Received:\s*<element\(s\) not found>/;
60
+
61
+ /** The part of a message the run itself wrote: before `Call log:`, without `Locator:` echo lines. */
62
+ const runHead = (m: string): string => m.split(/\n\s*Call log:/)[0].replace(/^\s*Locator:.*$/gm, '');
63
+
64
+ /** Every failure message, stack frames and a leading `Error:` dropped. */
65
+ function messagesOf(result: PlaywrightResult): string[] {
66
+ const raw = result.errorMessages?.length ? result.errorMessages : [result.error ?? ''];
67
+ return raw.map((m) => m.split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim()).filter(Boolean);
68
+ }
69
+
70
+ function evidenceLine(text: string, secrets: string[]): string {
71
+ const flat = redactSecrets(text, secrets).replace(/\s+/g, ' ').trim();
72
+ return flat.length > EVIDENCE_MAX ? `${flat.slice(0, EVIDENCE_MAX - 1)}…` : flat;
73
+ }
74
+
75
+ /** Classify one scenario. Null when it needs no triage (clean pass, or never ran). */
76
+ export function classifyScenario(input: TriageInput, secrets: string[] = []): TriageRow | null {
77
+ const { result, tags } = input;
78
+ if (!result) return null;
79
+ const base = {
80
+ unit: input.unit,
81
+ scenario: input.title,
82
+ ...(input.variant ? { variant: input.variant } : {}),
83
+ ids: [...new Set([input.title, ...tags].join(' ').match(VIEWPOINT_ID) ?? [])],
84
+ legacy: tags.filter((t) => t.startsWith('@legacy:')).map((t) => t.slice('@legacy:'.length)),
85
+ ...(result.tracePath ? { tracePath: result.tracePath } : {}),
86
+ };
87
+ const row = (cls: TriageClass, ruleId: string, evidence: string): TriageRow =>
88
+ ({ ...base, class: cls, ruleId, evidence: evidenceLine(evidence, secrets) });
89
+
90
+ if (result.status === 'passed') {
91
+ return (result.attempts ?? 1) > 1 ? row('flaky', 'passed-after-retry', `passed on attempt ${result.attempts}`) : null;
92
+ }
93
+ const messages = messagesOf(result);
94
+ const first = messages[0] ?? result.status;
95
+
96
+ const known = tags.find((t) => t === '@known-defect' || t.startsWith('@known-defect:'));
97
+ if (known) {
98
+ const r = row('known-defect', 'tag-known-defect', first);
99
+ const id = known.split(':')[1];
100
+ return id ? { ...r, defectId: id } : r;
101
+ }
102
+ if (tags.some((t) => t.startsWith('@manual'))) return row('blocked-capability', 'tag-manual', `${tags.find((t) => t.startsWith('@manual'))} — ${first}`);
103
+ if (tags.some((t) => t.startsWith('@requires:'))) return row('blocked-capability', 'tag-requires', `${tags.find((t) => t.startsWith('@requires:'))} — ${first}`);
104
+ if (result.status === 'skipped') return row('blocked-capability', 'status-skipped', 'skipped');
105
+
106
+ // An assertion with both halves decides the case even when a timeout wraps it.
107
+ for (const m of messages) {
108
+ const { expected, received } = parseExpectedReceived(m);
109
+ if (expected && received && !ELEMENT_ABSENT.test(m) && expected !== received) {
110
+ return { ...row('product-candidate', 'assertion-mismatch', `Expected ${expected} · Received ${received}`),
111
+ expected: evidenceLine(expected, secrets), received: evidenceLine(received, secrets) };
112
+ }
113
+ }
114
+ for (const s of TRIAGE_SIGNALS) {
115
+ // Environment signals read only what the RUN said, never the test's own selector text that a
116
+ // message echoes in `Locator:` / `Call log:` — `getByText('Invalid URL')` is a missing message,
117
+ // not a bad base URL. Test-defect signals need the call log (`waiting for …`), so they read it all.
118
+ const hit = messages.find((m) => s.match.test(s.class === 'environment' ? runHead(m) : m));
119
+ if (hit) return row(s.class, s.id, hit);
120
+ }
121
+ const absent = messages.find((m) => ELEMENT_ABSENT.test(m));
122
+ if (absent) return row('unclassified', 'assertion-element-absent', absent);
123
+ if (messages.length > 0 && messages.every((m) => BARE_TIMEOUT.test(m))) {
124
+ return row('unclassified', 'bare-timeout', `${first} No locator or assertion detail: a slow environment, a hanging page, or a wait in the test. Re-run once, then read the trace's last action.`);
125
+ }
126
+ return row('unclassified', 'no-signal', first);
127
+ }