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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (503) hide show
  1. package/dist/cli/commands/audit.d.ts +5 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +64 -11
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/capability.d.ts.map +1 -1
  6. package/dist/cli/commands/capability.js +40 -18
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/changed.d.ts.map +1 -1
  9. package/dist/cli/commands/changed.js +2 -3
  10. package/dist/cli/commands/changed.js.map +1 -1
  11. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  12. package/dist/cli/commands/dashboard.js +2 -28
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/defect.d.ts +7 -0
  15. package/dist/cli/commands/defect.d.ts.map +1 -0
  16. package/dist/cli/commands/defect.js +68 -0
  17. package/dist/cli/commands/defect.js.map +1 -0
  18. package/dist/cli/commands/delivery.d.ts.map +1 -1
  19. package/dist/cli/commands/delivery.js +113 -59
  20. package/dist/cli/commands/delivery.js.map +1 -1
  21. package/dist/cli/commands/generate.d.ts.map +1 -1
  22. package/dist/cli/commands/generate.js +14 -7
  23. package/dist/cli/commands/generate.js.map +1 -1
  24. package/dist/cli/commands/results-merge.d.ts +9 -0
  25. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  26. package/dist/cli/commands/results-merge.js +9 -12
  27. package/dist/cli/commands/results-merge.js.map +1 -1
  28. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  29. package/dist/cli/commands/results-sanitize.js +2 -13
  30. package/dist/cli/commands/results-sanitize.js.map +1 -1
  31. package/dist/cli/commands/select.d.ts +12 -0
  32. package/dist/cli/commands/select.d.ts.map +1 -0
  33. package/dist/cli/commands/select.js +122 -0
  34. package/dist/cli/commands/select.js.map +1 -0
  35. package/dist/cli/commands/triage.d.ts +7 -0
  36. package/dist/cli/commands/triage.d.ts.map +1 -0
  37. package/dist/cli/commands/triage.js +98 -0
  38. package/dist/cli/commands/triage.js.map +1 -0
  39. package/dist/cli/commands/update.d.ts.map +1 -1
  40. package/dist/cli/commands/update.js +3 -0
  41. package/dist/cli/commands/update.js.map +1 -1
  42. package/dist/cli/commands/viewpoint.d.ts +7 -0
  43. package/dist/cli/commands/viewpoint.d.ts.map +1 -0
  44. package/dist/cli/commands/viewpoint.js +70 -0
  45. package/dist/cli/commands/viewpoint.js.map +1 -0
  46. package/dist/cli/index.js +13 -2
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  49. package/dist/dashboard/snapshot-builder.js +6 -10
  50. package/dist/dashboard/snapshot-builder.js.map +1 -1
  51. package/dist/dashboard/templates/index.html +1 -1
  52. package/dist/dashboard/types.d.ts +1 -0
  53. package/dist/dashboard/types.d.ts.map +1 -1
  54. package/dist/exporters/csv-exporter.d.ts +4 -0
  55. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  56. package/dist/exporters/csv-exporter.js +12 -11
  57. package/dist/exporters/csv-exporter.js.map +1 -1
  58. package/dist/exporters/feature-parser.d.ts +29 -0
  59. package/dist/exporters/feature-parser.d.ts.map +1 -1
  60. package/dist/exporters/feature-parser.js +44 -1
  61. package/dist/exporters/feature-parser.js.map +1 -1
  62. package/dist/exporters/json-exporter.d.ts.map +1 -1
  63. package/dist/exporters/json-exporter.js +7 -0
  64. package/dist/exporters/json-exporter.js.map +1 -1
  65. package/dist/exporters/locale-variants.d.ts +7 -1
  66. package/dist/exporters/locale-variants.d.ts.map +1 -1
  67. package/dist/exporters/locale-variants.js +23 -16
  68. package/dist/exporters/locale-variants.js.map +1 -1
  69. package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
  70. package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
  71. package/dist/exporters/manual-hybrid-oracle.js +114 -0
  72. package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
  73. package/dist/exporters/matrix/build.d.ts.map +1 -1
  74. package/dist/exporters/matrix/build.js +68 -19
  75. package/dist/exporters/matrix/build.js.map +1 -1
  76. package/dist/exporters/matrix/carry-manual-results.d.ts +89 -0
  77. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  78. package/dist/exporters/matrix/carry-manual-results.js +302 -0
  79. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  80. package/dist/exporters/matrix/export.d.ts +9 -1
  81. package/dist/exporters/matrix/export.d.ts.map +1 -1
  82. package/dist/exporters/matrix/export.js +21 -3
  83. package/dist/exporters/matrix/export.js.map +1 -1
  84. package/dist/exporters/matrix/gates.js +6 -0
  85. package/dist/exporters/matrix/gates.js.map +1 -1
  86. package/dist/exporters/matrix/render-csv.js +1 -1
  87. package/dist/exporters/matrix/render-csv.js.map +1 -1
  88. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  89. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  90. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  91. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  92. package/dist/exporters/matrix/render-xlsx.js +3 -0
  93. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  94. package/dist/exporters/matrix/types.d.ts +13 -1
  95. package/dist/exporters/matrix/types.d.ts.map +1 -1
  96. package/dist/exporters/matrix/types.js.map +1 -1
  97. package/dist/exporters/matrix/wording.d.ts +8 -1
  98. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  99. package/dist/exporters/matrix/wording.js +37 -2
  100. package/dist/exporters/matrix/wording.js.map +1 -1
  101. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  102. package/dist/exporters/playwright-report-parser.js +1 -0
  103. package/dist/exporters/playwright-report-parser.js.map +1 -1
  104. package/dist/exporters/run-environment.d.ts +32 -0
  105. package/dist/exporters/run-environment.d.ts.map +1 -0
  106. package/dist/exporters/run-environment.js +125 -0
  107. package/dist/exporters/run-environment.js.map +1 -0
  108. package/dist/exporters/scenario-merger.d.ts +18 -0
  109. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  110. package/dist/exporters/scenario-merger.js +49 -3
  111. package/dist/exporters/scenario-merger.js.map +1 -1
  112. package/dist/exporters/test-data-resolver.d.ts +0 -4
  113. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  114. package/dist/exporters/test-data-resolver.js +0 -7
  115. package/dist/exporters/test-data-resolver.js.map +1 -1
  116. package/dist/exporters/types.d.ts +5 -0
  117. package/dist/exporters/types.d.ts.map +1 -1
  118. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  119. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  120. package/dist/exporters/xlsx-exporter.js +7 -2
  121. package/dist/exporters/xlsx-exporter.js.map +1 -1
  122. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  123. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  124. package/dist/generators/gherkin-parser/index.js +49 -19
  125. package/dist/generators/gherkin-parser/index.js.map +1 -1
  126. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  127. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  128. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  129. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  130. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  131. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  132. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  133. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  134. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  135. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  136. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  137. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  138. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  139. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  140. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  141. package/dist/generators/test-generator/code-generator.js +90 -5
  142. package/dist/generators/test-generator/code-generator.js.map +1 -1
  143. package/dist/generators/test-generator/diagnostics.d.ts +36 -5
  144. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  145. package/dist/generators/test-generator/diagnostics.js +35 -4
  146. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  147. package/dist/generators/test-generator/patterns/index.d.ts +12 -2
  148. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  149. package/dist/generators/test-generator/patterns/index.js +15 -9
  150. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  151. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  152. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  153. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  154. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  155. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  156. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  157. package/dist/generators/test-generator/step-mapper.d.ts +46 -0
  158. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  159. package/dist/generators/test-generator/step-mapper.js +84 -9
  160. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  161. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  162. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  163. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  164. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  165. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  166. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  167. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  168. package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
  169. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  170. package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
  171. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  172. package/dist/harness/audit.d.ts +3 -0
  173. package/dist/harness/audit.d.ts.map +1 -1
  174. package/dist/harness/audit.js +60 -19
  175. package/dist/harness/audit.js.map +1 -1
  176. package/dist/harness/capability.d.ts +7 -1
  177. package/dist/harness/capability.d.ts.map +1 -1
  178. package/dist/harness/capability.js +15 -12
  179. package/dist/harness/capability.js.map +1 -1
  180. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  181. package/dist/harness/config-file.d.ts +17 -0
  182. package/dist/harness/config-file.d.ts.map +1 -0
  183. package/dist/harness/config-file.js +100 -0
  184. package/dist/harness/config-file.js.map +1 -0
  185. package/dist/harness/counted-assertions.d.ts +2 -0
  186. package/dist/harness/counted-assertions.d.ts.map +1 -0
  187. package/dist/harness/counted-assertions.js +38 -0
  188. package/dist/harness/counted-assertions.js.map +1 -0
  189. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  190. package/dist/harness/data-driven-lint.js +69 -27
  191. package/dist/harness/data-driven-lint.js.map +1 -1
  192. package/dist/harness/defect-draft.d.ts +14 -0
  193. package/dist/harness/defect-draft.d.ts.map +1 -0
  194. package/dist/harness/defect-draft.js +143 -0
  195. package/dist/harness/defect-draft.js.map +1 -0
  196. package/dist/harness/defects.d.ts +24 -0
  197. package/dist/harness/defects.d.ts.map +1 -0
  198. package/dist/harness/defects.js +118 -0
  199. package/dist/harness/defects.js.map +1 -0
  200. package/dist/harness/depth-lint.d.ts.map +1 -1
  201. package/dist/harness/depth-lint.js +2 -1
  202. package/dist/harness/depth-lint.js.map +1 -1
  203. package/dist/harness/flow-check.d.ts.map +1 -1
  204. package/dist/harness/flow-check.js +6 -2
  205. package/dist/harness/flow-check.js.map +1 -1
  206. package/dist/harness/keyword-match.d.ts +59 -0
  207. package/dist/harness/keyword-match.d.ts.map +1 -0
  208. package/dist/harness/keyword-match.js +86 -0
  209. package/dist/harness/keyword-match.js.map +1 -0
  210. package/dist/harness/manifest.d.ts.map +1 -1
  211. package/dist/harness/manifest.js +2 -2
  212. package/dist/harness/manifest.js.map +1 -1
  213. package/dist/harness/next-step.d.ts +2 -4
  214. package/dist/harness/next-step.d.ts.map +1 -1
  215. package/dist/harness/next-step.js +50 -5
  216. package/dist/harness/next-step.js.map +1 -1
  217. package/dist/harness/parse.d.ts +45 -0
  218. package/dist/harness/parse.d.ts.map +1 -1
  219. package/dist/harness/parse.js +142 -24
  220. package/dist/harness/parse.js.map +1 -1
  221. package/dist/harness/perf.d.ts +7 -1
  222. package/dist/harness/perf.d.ts.map +1 -1
  223. package/dist/harness/perf.js +13 -10
  224. package/dist/harness/perf.js.map +1 -1
  225. package/dist/harness/quality-gates.d.ts +9 -2
  226. package/dist/harness/quality-gates.d.ts.map +1 -1
  227. package/dist/harness/quality-gates.js +22 -8
  228. package/dist/harness/quality-gates.js.map +1 -1
  229. package/dist/harness/query-catalog.js +0 -0
  230. package/dist/harness/query-catalog.js.map +1 -1
  231. package/dist/harness/read-text.d.ts +21 -0
  232. package/dist/harness/read-text.d.ts.map +1 -1
  233. package/dist/harness/read-text.js +32 -1
  234. package/dist/harness/read-text.js.map +1 -1
  235. package/dist/harness/script-check.d.ts.map +1 -1
  236. package/dist/harness/script-check.js +7 -1
  237. package/dist/harness/script-check.js.map +1 -1
  238. package/dist/harness/secret-scan.d.ts +27 -1
  239. package/dist/harness/secret-scan.d.ts.map +1 -1
  240. package/dist/harness/secret-scan.js +57 -4
  241. package/dist/harness/secret-scan.js.map +1 -1
  242. package/dist/harness/select-by-ids.d.ts +31 -0
  243. package/dist/harness/select-by-ids.d.ts.map +1 -0
  244. package/dist/harness/select-by-ids.js +106 -0
  245. package/dist/harness/select-by-ids.js.map +1 -0
  246. package/dist/harness/sensors.d.ts +20 -1
  247. package/dist/harness/sensors.d.ts.map +1 -1
  248. package/dist/harness/sensors.js +133 -39
  249. package/dist/harness/sensors.js.map +1 -1
  250. package/dist/harness/spec-branches.d.ts +0 -18
  251. package/dist/harness/spec-branches.d.ts.map +1 -1
  252. package/dist/harness/spec-branches.js +24 -3
  253. package/dist/harness/spec-branches.js.map +1 -1
  254. package/dist/harness/spec-coverage.d.ts.map +1 -1
  255. package/dist/harness/spec-coverage.js +14 -4
  256. package/dist/harness/spec-coverage.js.map +1 -1
  257. package/dist/harness/triage-signals.d.ts +20 -0
  258. package/dist/harness/triage-signals.d.ts.map +1 -0
  259. package/dist/harness/triage-signals.js +30 -0
  260. package/dist/harness/triage-signals.js.map +1 -0
  261. package/dist/harness/triage-unit.d.ts +23 -0
  262. package/dist/harness/triage-unit.d.ts.map +1 -0
  263. package/dist/harness/triage-unit.js +142 -0
  264. package/dist/harness/triage-unit.js.map +1 -0
  265. package/dist/harness/triage.d.ts +28 -0
  266. package/dist/harness/triage.d.ts.map +1 -0
  267. package/dist/harness/triage.js +99 -0
  268. package/dist/harness/triage.js.map +1 -0
  269. package/dist/harness/ui-inventory.d.ts +40 -0
  270. package/dist/harness/ui-inventory.d.ts.map +1 -0
  271. package/dist/harness/ui-inventory.js +191 -0
  272. package/dist/harness/ui-inventory.js.map +1 -0
  273. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  274. package/dist/harness/viewpoint-baseline.js +7 -1
  275. package/dist/harness/viewpoint-baseline.js.map +1 -1
  276. package/dist/harness/viewpoint-draft-header.d.ts +7 -0
  277. package/dist/harness/viewpoint-draft-header.d.ts.map +1 -0
  278. package/dist/harness/viewpoint-draft-header.js +100 -0
  279. package/dist/harness/viewpoint-draft-header.js.map +1 -0
  280. package/dist/harness/viewpoint-ledger.d.ts +13 -3
  281. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  282. package/dist/harness/viewpoint-ledger.js +106 -19
  283. package/dist/harness/viewpoint-ledger.js.map +1 -1
  284. package/dist/harness/viewpoint-mode.d.ts +9 -0
  285. package/dist/harness/viewpoint-mode.d.ts.map +1 -0
  286. package/dist/harness/viewpoint-mode.js +52 -0
  287. package/dist/harness/viewpoint-mode.js.map +1 -0
  288. package/dist/harness/viewpoint-priority.d.ts +19 -0
  289. package/dist/harness/viewpoint-priority.d.ts.map +1 -0
  290. package/dist/harness/viewpoint-priority.js +36 -0
  291. package/dist/harness/viewpoint-priority.js.map +1 -0
  292. package/dist/harness/viewpoint-review-open.d.ts +6 -0
  293. package/dist/harness/viewpoint-review-open.d.ts.map +1 -0
  294. package/dist/harness/viewpoint-review-open.js +90 -0
  295. package/dist/harness/viewpoint-review-open.js.map +1 -0
  296. package/dist/harness/viewpoint-scan-refs.d.ts +50 -0
  297. package/dist/harness/viewpoint-scan-refs.d.ts.map +1 -0
  298. package/dist/harness/viewpoint-scan-refs.js +200 -0
  299. package/dist/harness/viewpoint-scan-refs.js.map +1 -0
  300. package/dist/harness/viewpoint-scan.d.ts +41 -0
  301. package/dist/harness/viewpoint-scan.d.ts.map +1 -0
  302. package/dist/harness/viewpoint-scan.js +118 -0
  303. package/dist/harness/viewpoint-scan.js.map +1 -0
  304. package/dist/harness/write-atomic.d.ts +2 -0
  305. package/dist/harness/write-atomic.d.ts.map +1 -0
  306. package/dist/harness/write-atomic.js +62 -0
  307. package/dist/harness/write-atomic.js.map +1 -0
  308. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  309. package/dist/orchestrator/ai-skills/writer.js +15 -7
  310. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  311. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  312. package/dist/orchestrator/assets-drift.js +11 -6
  313. package/dist/orchestrator/assets-drift.js.map +1 -1
  314. package/dist/orchestrator/flow-manager.d.ts +0 -1
  315. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  316. package/dist/orchestrator/flow-manager.js +3 -52
  317. package/dist/orchestrator/flow-manager.js.map +1 -1
  318. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  319. package/dist/orchestrator/project-deps-sync.js +22 -1
  320. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  321. package/dist/orchestrator/project-initializer.d.ts +8 -0
  322. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  323. package/dist/orchestrator/project-initializer.js +49 -7
  324. package/dist/orchestrator/project-initializer.js.map +1 -1
  325. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  326. package/dist/orchestrator/screen-manager.js +8 -62
  327. package/dist/orchestrator/screen-manager.js.map +1 -1
  328. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  329. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  330. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  331. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  332. package/dist/orchestrator/security-overrides.d.ts +51 -0
  333. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  334. package/dist/orchestrator/security-overrides.js +165 -0
  335. package/dist/orchestrator/security-overrides.js.map +1 -0
  336. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  337. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  338. package/dist/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  339. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  340. package/dist/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  341. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  342. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  343. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +25 -1
  344. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  345. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  346. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  347. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  348. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  349. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  350. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  351. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  352. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  353. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  354. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  355. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  356. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  357. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  358. package/dist/orchestrator/templates/specs-url-assert.js +38 -4
  359. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  360. package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
  361. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  362. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  363. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  364. package/dist/orchestrator/trace-mode-install.js +132 -0
  365. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  366. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +23 -0
  367. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  368. package/dist/orchestrator/viewpoint-pattern-catalog.js +48 -0
  369. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  370. package/dist/orchestrator/viewpoint-scaffold.d.ts +3 -0
  371. package/dist/orchestrator/viewpoint-scaffold.d.ts.map +1 -0
  372. package/dist/orchestrator/viewpoint-scaffold.js +70 -0
  373. package/dist/orchestrator/viewpoint-scaffold.js.map +1 -0
  374. package/package.json +3 -3
  375. package/src/cli/commands/audit.ts +59 -12
  376. package/src/cli/commands/capability.ts +54 -19
  377. package/src/cli/commands/changed.ts +2 -4
  378. package/src/cli/commands/dashboard.ts +1 -26
  379. package/src/cli/commands/defect.ts +49 -0
  380. package/src/cli/commands/delivery.ts +106 -56
  381. package/src/cli/commands/generate.ts +15 -7
  382. package/src/cli/commands/results-merge.ts +9 -9
  383. package/src/cli/commands/results-sanitize.ts +2 -9
  384. package/src/cli/commands/select.ts +88 -0
  385. package/src/cli/commands/triage.ts +57 -0
  386. package/src/cli/commands/update.ts +3 -0
  387. package/src/cli/commands/viewpoint.ts +58 -0
  388. package/src/cli/index.ts +13 -2
  389. package/src/dashboard/snapshot-builder.ts +6 -8
  390. package/src/dashboard/templates/index.html +1 -1
  391. package/src/dashboard/types.ts +1 -0
  392. package/src/exporters/csv-exporter.ts +12 -11
  393. package/src/exporters/feature-parser.ts +44 -2
  394. package/src/exporters/json-exporter.ts +6 -1
  395. package/src/exporters/locale-variants.ts +23 -17
  396. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  397. package/src/exporters/matrix/build.ts +71 -19
  398. package/src/exporters/matrix/carry-manual-results.ts +292 -0
  399. package/src/exporters/matrix/export.ts +24 -4
  400. package/src/exporters/matrix/gates.ts +6 -0
  401. package/src/exporters/matrix/render-csv.ts +1 -1
  402. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  403. package/src/exporters/matrix/render-xlsx.ts +3 -0
  404. package/src/exporters/matrix/types.ts +13 -1
  405. package/src/exporters/matrix/wording.ts +36 -2
  406. package/src/exporters/playwright-report-parser.ts +1 -0
  407. package/src/exporters/run-environment.ts +89 -0
  408. package/src/exporters/scenario-merger.ts +51 -5
  409. package/src/exporters/test-data-resolver.ts +0 -6
  410. package/src/exporters/types.ts +5 -0
  411. package/src/exporters/xlsx-exporter.ts +7 -2
  412. package/src/generators/gherkin-parser/index.ts +50 -18
  413. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
  414. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  415. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  416. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  417. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  418. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  419. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  420. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  421. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  422. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  423. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  424. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  425. package/src/generators/test-generator/code-generator.ts +77 -5
  426. package/src/generators/test-generator/diagnostics.ts +36 -5
  427. package/src/generators/test-generator/patterns/index.ts +15 -8
  428. package/src/generators/test-generator/patterns/types.ts +15 -0
  429. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  430. package/src/generators/test-generator/step-mapper.ts +90 -8
  431. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  432. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  433. package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
  434. package/src/harness/audit.ts +69 -24
  435. package/src/harness/capability.ts +23 -12
  436. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  437. package/src/harness/config-file.ts +66 -0
  438. package/src/harness/counted-assertions.ts +33 -0
  439. package/src/harness/data-driven-lint.ts +82 -24
  440. package/src/harness/defect-draft.ts +115 -0
  441. package/src/harness/defects.ts +94 -0
  442. package/src/harness/depth-lint.ts +2 -1
  443. package/src/harness/flow-check.ts +6 -2
  444. package/src/harness/keyword-match.ts +85 -0
  445. package/src/harness/manifest.ts +2 -2
  446. package/src/harness/next-step.ts +47 -5
  447. package/src/harness/parse.ts +151 -27
  448. package/src/harness/perf.ts +15 -7
  449. package/src/harness/quality-gates.ts +22 -8
  450. package/src/harness/query-catalog.ts +0 -0
  451. package/src/harness/read-text.ts +32 -1
  452. package/src/harness/script-check.ts +7 -1
  453. package/src/harness/secret-scan.ts +60 -4
  454. package/src/harness/select-by-ids.ts +83 -0
  455. package/src/harness/sensors.ts +141 -40
  456. package/src/harness/spec-branches.ts +6 -3
  457. package/src/harness/spec-coverage.ts +14 -4
  458. package/src/harness/triage-signals.ts +46 -0
  459. package/src/harness/triage-unit.ts +113 -0
  460. package/src/harness/triage.ts +127 -0
  461. package/src/harness/ui-inventory.ts +164 -0
  462. package/src/harness/viewpoint-baseline.ts +7 -1
  463. package/src/harness/viewpoint-draft-header.ts +55 -0
  464. package/src/harness/viewpoint-ledger.ts +113 -25
  465. package/src/harness/viewpoint-mode.ts +49 -0
  466. package/src/harness/viewpoint-priority.ts +38 -0
  467. package/src/harness/viewpoint-review-open.ts +51 -0
  468. package/src/harness/viewpoint-scan-refs.ts +154 -0
  469. package/src/harness/viewpoint-scan.ts +111 -0
  470. package/src/harness/write-atomic.ts +22 -0
  471. package/src/orchestrator/ai-skills/writer.ts +18 -3
  472. package/src/orchestrator/assets-drift.ts +10 -5
  473. package/src/orchestrator/flow-manager.ts +3 -53
  474. package/src/orchestrator/project-deps-sync.ts +23 -1
  475. package/src/orchestrator/project-initializer.ts +54 -6
  476. package/src/orchestrator/screen-manager.ts +7 -61
  477. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  478. package/src/orchestrator/security-overrides.ts +149 -0
  479. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  480. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  481. package/src/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  482. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  483. package/src/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  484. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  485. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  486. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +25 -1
  487. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  488. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  489. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  490. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  491. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  492. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  493. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  494. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  495. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  496. package/src/orchestrator/templates/playwright.config.ts +4 -2
  497. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  498. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  499. package/src/orchestrator/templates/specs-url-assert.ts +47 -4
  500. package/src/orchestrator/templates/wdio.conf.ts +24 -1
  501. package/src/orchestrator/trace-mode-install.ts +94 -0
  502. package/src/orchestrator/viewpoint-pattern-catalog.ts +61 -0
  503. package/src/orchestrator/viewpoint-scaffold.ts +36 -0
@@ -11,6 +11,9 @@ import * as path from 'path';
11
11
  import { parse as parseYaml } from 'yaml';
12
12
  import { ScenarioInfo, ViewpointEntry, idPrefix } from './parse';
13
13
  import { readTextFile } from './read-text';
14
+ import { keywordHit, hasKeyword } from './keyword-match';
15
+ import { parsePriority } from './viewpoint-priority';
16
+ import { countedAssertions } from './counted-assertions';
14
17
 
15
18
  // Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
16
19
  // compound category like LIST-DISPLAY / ADD-TO-CART / PRODUCT-DISCOVERY classifies correctly).
@@ -86,18 +89,11 @@ export function loadCatalog(): Catalog {
86
89
  return parseYaml(readTextFile(p)) as Catalog;
87
90
  }
88
91
 
89
- // Word-aware keyword match: `\b<kw>(s|es|ed|ing)?\b`, so a keyword matches whole words (plus
90
- // simple plural/verb suffixes) but NOT as a substring of a larger word — e.g. `form` must not
91
- // match inside `Format` or `@platform`, while `product` still matches `products`. Multi-word
92
- // keywords ("add to cart", "is hidden") match the phrase with a boundary at each end.
93
- const wordRe = (kw: string) => {
94
- const esc = kw.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
95
- return new RegExp(`\\b${esc}(?:s|es|ed|ing)?\\b`);
96
- };
97
- const has = (haystacks: string[], kw: string) => {
98
- const re = wordRe(kw);
99
- return haystacks.some((h) => re.test(h));
100
- };
92
+ // Word-aware keyword match — one rule for every sensor, in ./keyword-match (#776). A keyword
93
+ // matches at a word START but may carry any suffix, so `form` does not match inside `Format` or
94
+ // `@platform` while `submit` still matches `submitted`. The old local copy anchored BOTH ends,
95
+ // which silently dropped every inflected true positive; see that module for the measurements.
96
+ const has = hasKeyword;
101
97
 
102
98
  // ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
103
99
 
@@ -112,6 +108,13 @@ export interface GateResult {
112
108
  coverageRatio: number;
113
109
  gaps: { theme: string; keywords: string[]; status: 'missing' | 'shallow' }[];
114
110
  universalGaps: string[];
111
+ /**
112
+ * WHICH scenario covered each satisfied theme, and on which keyword — the audit's second line of
113
+ * defence against a bogus coverage claim (#776). A bare count ("themes 3/3") gives a reviewer
114
+ * nothing to check, so a nonsense match survives; naming the evidence makes it obvious on sight.
115
+ * Mirrors how the Claim-Proof section already names the scenario behind each claim.
116
+ */
117
+ coveredBy: { theme: string; by: string; keyword: string }[];
115
118
  }
116
119
 
117
120
  /**
@@ -166,20 +169,36 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
166
169
 
167
170
  const evidence = { hits: best, runnerUp };
168
171
  const gaps: GateResult['gaps'] = [];
172
+ const coveredBy: GateResult['coveredBy'] = [];
169
173
  let total = 0, covered = 0;
170
174
  if (pageType) {
171
175
  for (const t of catalog.page_types[pageType].must_cover) {
172
176
  total++;
173
177
  // Scenarios that cover this theme by keyword (include @manual — a manual
174
178
  // scenario with a real assertion still covers the viewpoint for design).
175
- const coverers = scenarios.filter((s) => t.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
179
+ // Word-aware since #776: a substring match let `information` satisfy `format-boundary`.
180
+ const coverers = scenarios.filter((s) => t.keywords.some((k) => keywordHit(s.haystack, k)));
176
181
  if (coverers.length === 0) { gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' }); continue; }
177
182
  // A theme WITHOUT `depth:` is visibility/navigation — landing/seeing/leaving IS the
178
183
  // assertion, so mere presence covers it. Only DATA-correctness themes (depth.requires:
179
184
  // data-assertion) need a data assertion to count as deeply covered.
180
185
  const needsDepth = t.depth?.requires === 'data-assertion';
181
- if (!needsDepth) { covered++; continue; }
182
- if (coverers.some((s) => s.hasDataAssertion)) covered++;
186
+ // Name the evidence for whichever scenario actually carries the theme — the one with the
187
+ // data assertion when depth is required, since that is the scenario the credit rests on.
188
+ const proof = (needsDepth ? coverers.find((s) => s.hasDataAssertion) : undefined) ?? coverers[0];
189
+ const credit = (): void => {
190
+ covered++;
191
+ coveredBy.push({
192
+ theme: t.theme,
193
+ by: proof.haystack,
194
+ // `proof` came out of `coverers`, which was filtered on this same predicate, so the find
195
+ // always lands — the `!` states that rather than inventing a fallback keyword that would
196
+ // misattribute the credit if the invariant ever broke.
197
+ keyword: t.keywords.find((k) => keywordHit(proof.haystack, k))!,
198
+ });
199
+ };
200
+ if (!needsDepth) { credit(); continue; }
201
+ if (coverers.some((s) => s.hasDataAssertion)) credit();
183
202
  else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
184
203
  }
185
204
  }
@@ -203,6 +222,7 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
203
222
  coverageRatio: total ? covered / total : 1,
204
223
  gaps,
205
224
  universalGaps,
225
+ coveredBy,
206
226
  };
207
227
  }
208
228
 
@@ -269,7 +289,7 @@ export function assertionDepth(
269
289
  const isDismissBehavior = (s: ScenarioInfo) =>
270
290
  /\bis hidden\b|\bare hidden\b|\bdismiss|\bdisappear|\bno longer (visible|shown|present|displayed)\b|\bcloses? the (modal|dialog|popup|overlay|panel|menu)\b/.test(s.haystack);
271
291
  const isDepthRequired = (s: ScenarioInfo) =>
272
- s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => s.haystack.includes(k.toLowerCase()));
292
+ s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => keywordHit(s.haystack, k));
273
293
 
274
294
  const required = nonManual.filter(isDepthRequired);
275
295
  const reqShallow = required.filter((s) => s.shallow);
@@ -347,7 +367,7 @@ export function flowCoveredThemes(
347
367
  ): { theme: string; flow: string }[] {
348
368
  const out: { theme: string; flow: string }[] = [];
349
369
  for (const g of gaps) {
350
- const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
370
+ const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => keywordHit(s.haystack, k)));
351
371
  if (hit) out.push({ theme: g.theme, flow: hit.flow });
352
372
  }
353
373
  return out;
@@ -394,7 +414,8 @@ export function flowRegressionDepth(scenarios: ScenarioInfo[]): FlowDepthResult
394
414
  export interface OracleStrengthResult {
395
415
  weak: { name: string; hint: string; code?: string }[]; // facet-by-name-substring, or URL-only (code ORACLE-URL-ONLY)
396
416
  facetClaims: number; // scenarios that touch a category/brand facet (denominator)
397
- ratio: number; // 1 - weak/facetClaims (1 when none) — caps businessDepth
417
+ weakFacet: number; // the facet-class subset of `weak` — the only numerator `facetClaims` denominates
418
+ ratio: number; // 1 - weakFacet/facetClaims, clamped 0..1 (1 when no facet claim) — caps businessDepth
398
419
  }
399
420
 
400
421
  // "see all [<item name/title>] ... contain(s) {{<facet>.term}}" — asserting every item's NAME carries
@@ -429,9 +450,11 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
429
450
  hint: `every \`Then\` is a URL assertion${guardLike ? ' on a permission / guard scenario' : ''} — it proves the router pushed a path, not that the page rendered for this user${guardLike ? ', and this is exactly the scenario a late 403 redirect turns falsely green' : ''}. Give the page selector an \`anchor:\` (a heading/landmark only the rendered page has) — one line per screen, and every \`see [X] page\` on it becomes a content oracle — or add a content assertion after it.`,
430
451
  });
431
452
  }
453
+ let weakFacet = 0;
432
454
  for (const s of scenarios) {
433
455
  if (s.manual) continue; // a @manual facet check is a deliberate deferral, not a weak automated oracle
434
456
  if (WEAK_FACET_ORACLE.test(s.stepsText)) {
457
+ weakFacet++;
435
458
  weak.push({
436
459
  name: s.name.slice(0, 80),
437
460
  hint: 'asserting every item NAME contains a category/brand term does not prove facet membership — assert the results-page TITLE/header carries the facet, a detail-page facet field, or an API/DB oracle; or defer the exhaustive check to @manual:M2.',
@@ -439,7 +462,17 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
439
462
  }
440
463
  }
441
464
  const facetClaims = scenarios.filter((s) => FACET_REF.test(s.stepsText) || FACET_REF.test(s.name)).length;
442
- return { weak, facetClaims, ratio: facetClaims ? 1 - weak.length / Math.max(1, facetClaims) : 1 };
465
+ // Only the FACET class is denominated by `facetClaims`. `weak` also carries ORACLE-URL-ONLY
466
+ // findings (#662), which make no category/brand claim at all — averaging them into this axis
467
+ // drove `ratio` negative (5 URL-only over 2 facet claims → 1 - 5/2 = -1.5), which walked through
468
+ // the businessDepth floor and took the CLI's bar renderer down with it (#762). A URL-only oracle
469
+ // is NAMED as a finding and holds the score out of the senior band (`seniorBandedOverall`);
470
+ // it is not averaged into the facet axis — the invariant `url-only-oracle.run.ts` asserts.
471
+ // The clamp is not belt-and-braces: a weak-facet scenario need not itself satisfy FACET_REF
472
+ // (`see all [name] contains {{price.term}}` is weak but claims no facet), so `weakFacet` can
473
+ // still outrun `facetClaims`. This axis is declared 0..1; it leaves here inside 0..1.
474
+ const ratio = facetClaims ? Math.max(0, Math.min(1, 1 - weakFacet / facetClaims)) : 1;
475
+ return { weak, facetClaims, weakFacet, ratio };
443
476
  }
444
477
 
445
478
  /** Collect data-correctness themes (depth.requires) for a page-type + universal. */
@@ -603,6 +636,9 @@ export interface TraceResult {
603
636
  mappedToOverview: number;
604
637
  withVpCodeRatio: number;
605
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;
606
642
  note: string;
607
643
  }
608
644
 
@@ -614,6 +650,24 @@ export interface TraceResult {
614
650
  */
615
651
  const unprefixed = (id: string): string => id.replace(/^(?:VP|FL)-/i, '').toUpperCase();
616
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
+
617
671
  export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
618
672
  const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
619
673
  const overviewBare = new Set(viewpoints.map((v) => unprefixed(v.id)));
@@ -638,9 +692,10 @@ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEnt
638
692
  mappedToOverview: mapped.length,
639
693
  withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
640
694
  mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
641
- note: withCode.length && mapped.length < withCode.length * 0.5
642
- ? 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).'
643
- : '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),
644
699
  };
645
700
  }
646
701
 
@@ -702,6 +757,31 @@ const CLAIM_RULES: ClaimRule[] = [
702
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.',
703
758
  severity: 'warn',
704
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
+ },
705
785
  {
706
786
  claim: 'all/every/each',
707
787
  title: /\b(all|every|each)\b/,
@@ -869,7 +949,14 @@ export interface TaxonomyResult {
869
949
 
870
950
  const TAXONOMY_KNOWN = new Set(['LIST', 'PRODUCT', 'CART', 'FILTER', 'NAV', 'VAL', 'SEC']);
871
951
 
872
- 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 }[] = [
873
960
  { cat: 'FILTER', re: /\b(categor(y|ies)|sub-?categor\w*|expand\w*|collaps\w*|\bfacet\b|filter\w*|belongs? to)\b/ },
874
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/ },
875
962
  { cat: 'PRODUCT', re: /\b(product detail|product-detail|view product|product page|same product|product name and price|product identity|detail product)\b/ },
@@ -881,9 +968,16 @@ const CATEGORY_SIGNALS: { cat: string; re: RegExp }[] = [
881
968
  // SEC, so the linter told QA to re-tag genuine auth-gate tests as navigation — diluting the
882
969
  // visibility of the security suite (QA field report, 3.2.23). The guard vocabulary below is
883
970
  // what makes the INTENT matchable, and GUARD_INTENT then wins over a mere observable.
884
- { 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 },
885
976
  ];
886
977
 
978
+ const signalMatches = (c: { re: RegExp; requires?: RegExp }, title: string): boolean =>
979
+ c.re.test(title) && (!c.requires || c.requires.test(title));
980
+
887
981
  /**
888
982
  * Categories whose signal describes the test's INTENT/risk rather than the observable action.
889
983
  * When one of these matches, a competing observable-only category (NAV: "redirects", "navigates")
@@ -901,14 +995,19 @@ export function taxonomyLint(scenarios: ScenarioInfo[]): TaxonomyResult {
901
995
  if (!cat || !TAXONOMY_KNOWN.has(cat)) continue;
902
996
  checked++;
903
997
  const title = s.name.toLowerCase();
904
- 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))];
905
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;
906
1004
  // The declared category may be an INTENT category whose signal this title spells
907
1005
  // differently; never "correct" it toward a category that only names the observable.
908
1006
  const suppressed = INTENT_OVER_OBSERVABLE[cat];
909
1007
  const others = matched.filter((c) => c !== cat && !(suppressed && suppressed.has(c)));
910
1008
  if (others.length === 1) { // unambiguous mismatch
911
- 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))!;
912
1011
  const m = title.match(sig.re);
913
1012
  mislabeled.push({ name: s.name, current: cat, suggested: others[0], signal: m ? m[0] : others[0] });
914
1013
  }
@@ -980,9 +1079,8 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
980
1079
  if (!m) continue;
981
1080
  const claimed = NUMBER_WORDS[m[1]] ?? Number(m[1]);
982
1081
  if (!Number.isFinite(claimed) || claimed < 2) continue;
983
- // Only value-bearing assertions count: `see [X] … with/contains {{v}}`. A visibility check
984
- // does not demonstrate one of N values.
985
- 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);
986
1084
  if (asserted > 0 && asserted < claimed) out.push({ name: s.name, claimed, asserted, noun: m[2] });
987
1085
  }
988
1086
  return out;
@@ -994,17 +1092,17 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
994
1092
  * The Priority-Viewpoints table is where release selection is supposed to come from. When the
995
1093
  * table says a flow is `Low` and its scenario is tagged `@high`, a smoke run picks up work the QA
996
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.
997
1099
  */
998
1100
  export function priorityMismatches(
999
- viewpointText: string, scenarios: ScenarioInfo[],
1101
+ viewpoints: ViewpointEntry[], scenarios: ScenarioInfo[],
1000
1102
  ): Array<{ id: string; declared: string; tagged: string; scenario: string }> {
1001
1103
  const declared = new Map<string, string>();
1002
- for (const m of viewpointText.matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)) {
1003
- declared.set(m[1].toUpperCase(), m[2].toLowerCase());
1004
- }
1104
+ for (const v of viewpoints) if (v.priorityWord) declared.set(v.id.toUpperCase(), v.priorityWord);
1005
1105
  if (declared.size === 0) return [];
1006
- // `medium` and `normal` are the same rank under different names in real documents.
1007
- const rank = (p: string): string => (p === 'medium' ? 'normal' : p);
1008
1106
  const out: Array<{ id: string; declared: string; tagged: string; scenario: string }> = [];
1009
1107
  for (const s of scenarios) {
1010
1108
  const id = (s.vpId ?? '').toUpperCase();
@@ -1012,11 +1110,14 @@ export function priorityMismatches(
1012
1110
  // The most specific declared row that prefixes this id.
1013
1111
  const key = [...declared.keys()].filter((k) => id.startsWith(k)).sort((a, b) => b.length - a.length)[0];
1014
1112
  if (!key) continue;
1015
- const want = rank(declared.get(key)!);
1016
- const got = rank(String(s.priority).toLowerCase());
1017
- // `critical` and `deferred` have no tag equivalent — nothing to contradict.
1018
- if (want === 'critical' || want === 'deferred') continue;
1019
- 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 });
1020
1121
  }
1021
1122
  return out;
1022
1123
  }
@@ -16,6 +16,7 @@
16
16
  * explicit — a named heading, a table that declares conditions and outcomes, or an if/else
17
17
  * cascade — because a false "you missed a branch" costs more trust than it buys coverage.
18
18
  */
19
+ import { keywordHit } from './keyword-match';
19
20
  import { ScenarioInfo } from './parse';
20
21
 
21
22
  export interface SpecBranch {
@@ -172,7 +173,9 @@ export function unenumeratedBranches(branches: SpecBranch[], featureText: string
172
173
  const words = [...new Set((b.label.toLowerCase().match(/[a-z][a-z-]{3,}/g) ?? []))]
173
174
  .filter((w) => !GENERIC_WORD.has(w));
174
175
  if (words.length === 0) return !hay.includes(b.label.toLowerCase());
175
- const hits = words.filter((w) => hay.includes(w)).length;
176
+ // Word-aware (#779): `in·active` and `check·list` are not mentions of `active` and `list`,
177
+ // and two of them credited a branch no scenario cites.
178
+ const hits = words.filter((w) => keywordHit(hay, w)).length;
176
179
  return hits < Math.min(2, words.length);
177
180
  });
178
181
  }
@@ -191,7 +194,7 @@ export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: s
191
194
  if (s.manual) continue; // a procedure's oracle is prose
192
195
  const asserts = [...(s.steps ?? [])]
193
196
  .filter((st) => st.bucket === 'then')
194
- .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)
195
198
  .sort();
196
199
  if (asserts.length === 0) continue;
197
200
  const key = asserts.join(' ; ');
@@ -289,7 +292,7 @@ export function visibilityPairGaps(scenarios: ScenarioInfo[]): VisibilityPairGap
289
292
  const subject = distinctive(s);
290
293
  // The counterpart has to be about the SAME thing — two shared distinctive words, the same
291
294
  // bar the ledger's prose matching uses.
292
- const paired = allows.some((a) => subject.filter((w) => a.haystack.includes(w)).length >= Math.min(2, subject.length));
295
+ const paired = allows.some((a) => subject.filter((w) => keywordHit(a.haystack, w)).length >= Math.min(2, subject.length));
293
296
  if (!paired) out.push({ scenario: s.name, subject: subject.slice(0, 4), kind: 'permission' });
294
297
  }
295
298
  return out;
@@ -9,6 +9,7 @@
9
9
  * Generic: no project data.
10
10
  */
11
11
  import * as fs from 'fs';
12
+ import { keywordHit } from './keyword-match';
12
13
  import { ScenarioInfo } from './parse';
13
14
  import { readTextFile } from './read-text';
14
15
 
@@ -139,7 +140,11 @@ function directionCovered(block: string, dir: FrDirection, subject: string[]): b
139
140
  const pool = lines.length ? lines : [block];
140
141
  // Narrow to the lines about the clause's element when the suite names it the same way the spec
141
142
  // does; a spec in one language and selector keys in another must not read as "no evidence".
142
- const about = subject.length ? pool.filter((l) => subject.some((x) => l.includes(x))) : [];
143
+ // Word-aware (#779), or the narrowing lets in a line about something else and then trusts it:
144
+ // `under·score` is not a mention of `Score`, but it selected the only absence assertion in the
145
+ // block and so proved a MUST NOT clause about the Score. (`keywordHit` keeps substring semantics
146
+ // for a non-ASCII element name, which is what the cross-language case above depends on.)
147
+ const about = subject.length ? pool.filter((l) => subject.some((x) => keywordHit(l, x))) : [];
143
148
  const scan = about.length ? about : pool;
144
149
  return scan.some((l) => NEGATIVE_ASSERTION.test(l) === (dir.polarity === 'negate'));
145
150
  }
@@ -269,7 +274,12 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
269
274
  const cited = featLower.includes(idLower) || cites.has(fr.id.toUpperCase());
270
275
  const words = [...new Set((fr.text.toLowerCase().match(/[a-z][a-z-]{4,}/g) || []))]
271
276
  .filter((w) => !/must|should|system|screen|users?|value|input|field/.test(w));
272
- const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= Math.min(2, words.length));
277
+ // Word-aware, not substring (#779). `haystack.includes('format')` is satisfied by
278
+ // in·format·ion and `includes('length')` by wave·length, and two such collisions clear the
279
+ // `>= 2 words` bar as easily as one — so a MUST clause nobody tested read as covered. The
280
+ // threshold is a separate judgement (is one word enough evidence?) and is unchanged; see
281
+ // keyword-match.ts for why the boundary is leading-only.
282
+ const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= Math.min(2, words.length));
273
283
  if (!cited && !kwHit) {
274
284
  if (fr.modality === 'MUST') uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
275
285
  continue;
@@ -279,7 +289,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
279
289
  // Which scenarios speak for this requirement — by its id (tags and comments sit in the block)
280
290
  // or by the same keyword rule used above.
281
291
  const owning = blocks.filter((b) => b.includes(idLower)
282
- || (words.length > 0 && words.filter((w) => b.includes(w)).length >= Math.min(2, words.length)));
292
+ || (words.length > 0 && words.filter((w) => keywordHit(b, w)).length >= Math.min(2, words.length)));
283
293
  // Cited from outside any scenario (a header comment): nothing to judge a direction against, so
284
294
  // report nothing rather than invent a gap.
285
295
  if (!owning.length) { frCovered += units; continue; }
@@ -299,7 +309,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
299
309
  // blocks that belong to this constraint: cite its msg code (primary), or mention a
300
310
  // DISTINCTIVE constraint word (generic words filtered to avoid matching happy paths).
301
311
  const own = blocks.filter((b) =>
302
- (row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => b.includes(w)),
312
+ (row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => keywordHit(b, w)),
303
313
  );
304
314
  if (!own.length) continue; // constraint has no scenarios at all — FR/viewpoint gate covers that
305
315
  const found = [...new Set(own.flatMap((b) => actionTriggersIn(b)))];
@@ -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
+ }