@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
@@ -10,6 +10,7 @@ import { capabilityRegistry } from '../../capabilities/registry';
10
10
  import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
11
11
  import { readCapabilities } from '../../harness/capability';
12
12
  import { parseManualComments } from '../../exporters/scenario-merger';
13
+ import { isPresencePlaceholderHybrid } from '../../exporters/manual-hybrid-oracle';
13
14
  import { findSyntaxError } from './utils/spec-syntax-check';
14
15
 
15
16
  /** One capability-injected precondition statement (`@api:`/`@query:` → `testData.bind(name, …)`). */
@@ -790,11 +791,18 @@ export class CodeGenerator {
790
791
  // labeled `Observable:`/`Oracle:`/`Expected:` line (parseManualComments — the
791
792
  // SAME parser delivery uses). Field finding (student-site, 87 scenarios): prose
792
793
  // paragraphs and rationale-only comments exported EMPTY Steps + Expected cells.
794
+ // • HAS a Then, but every assertion is a bare presence placeholder while the real oracle
795
+ // sits in the comments (#810) — the same banned hybrid wearing a Then: the deliverable
796
+ // reads "See [X] table" and a tester ticking it Passed records a check nobody made.
793
797
  const isStructured = scenario.steps.length > 0;
794
798
  if (isStructured && !hasThenStep(scenario)) {
795
799
  this.diagnostics.add('SG-W008',
796
800
  `@manual scenario "${scenario.name}" has Gherkin steps but no Then — a When without Then is invalid Gherkin. Add the Then step, or remove the steps and write the full procedure (action steps, then "Observable:"/"Oracle:" lines) in its \`# Tester verifies:\` block`,
797
801
  { feature: feature.name, step: scenario.name });
802
+ } else if (isStructured && isPresencePlaceholderHybrid(scenario.steps, scenario.comments ?? [])) {
803
+ this.diagnostics.add('SG-W008',
804
+ `@manual scenario "${scenario.name}" asserts only presence (e.g. "User see [X] table") while its real oracle sits in the \`# Tester verifies:\` comment — the deliverable's Expected would not say what the case checks. Write the oracle as the Then step (e.g. \`table match data:\`), or remove the steps and write the full numbered procedure ("1. <action>" … "N. Observable: <what>")`,
805
+ { feature: feature.name, step: scenario.name });
798
806
  } else if (!isStructured) {
799
807
  const parsed = parseManualComments(scenario.comments ?? []);
800
808
  if (!parsed.steps.length || !parsed.expected.length) {
@@ -880,7 +888,7 @@ export class CodeGenerator {
880
888
  authGroupMap.get(key)!.push(code);
881
889
  }
882
890
 
883
- const authGroups = groupOrder.map(key => ({
891
+ const authGroups = groupOrder.map((key): { authRole?: string; noAuth: boolean; scenarios: string[]; afterEach?: string } => ({
884
892
  authRole: key === NO_AUTH_GROUP_KEY ? undefined : (key || undefined),
885
893
  noAuth: key === NO_AUTH_GROUP_KEY,
886
894
  scenarios: authGroupMap.get(key)!,
@@ -923,6 +931,19 @@ export class CodeGenerator {
923
931
  }
924
932
  const scenarios = renderedScenarios.map(s => s.code);
925
933
 
934
+ // #815 — a feature-level `@afterEach` used to sit in the outermost describe, so it also ran after
935
+ // every `@no-auth` test, on an EMPTY session: a hook that needs the signed-in page (`Given User is
936
+ // on [probe] page` → redirected to /login) failed an anonymous test whose own steps passed. When
937
+ // the feature mixes `@no-auth` with any other group, the hook moves INTO each other group and the
938
+ // no-auth group runs without it. A feature that is anonymous throughout keeps the hook — it was
939
+ // written for those tests.
940
+ // (A no-auth group beside another one means ≥2 groups, so `renderAuthBlocks` is already true.)
941
+ const afterEachInGroups = !!afterEach
942
+ && authGroups.some(g => g.noAuth) && authGroups.some(g => !g.noAuth);
943
+ if (afterEachInGroups) {
944
+ for (const g of authGroups) if (!g.noAuth) g.afterEach = afterEach;
945
+ }
946
+
926
947
  // For single group, extract the auth role to put test.use at describe level. Keyed on
927
948
  // `renderAuthBlocks`, not `needsGrouping`: a lone `@no-auth` group now renders its own block, so
928
949
  // hoisting anything to the feature describe would double up. Its authRole is undefined anyway.
@@ -1019,7 +1040,7 @@ export class CodeGenerator {
1019
1040
  featureDescription: feature.description,
1020
1041
  background,
1021
1042
  beforeAll,
1022
- afterEach,
1043
+ afterEach: afterEachInGroups ? undefined : afterEach,
1023
1044
  afterAll,
1024
1045
  cleanupConfig,
1025
1046
  screenshotOnFailure,
@@ -24,7 +24,9 @@
24
24
  * SG-W008 — @manual scenario is incomplete in either direction: Gherkin steps without a
25
25
  * Then (invalid Gherkin; comments cannot substitute), or a documentation-style
26
26
  * manual (no steps) whose comments lack the extractable procedure — ≥1 numbered
27
- * action line + ≥1 labeled Observable:/Oracle:/Expected: line
27
+ * action line + ≥1 labeled Observable:/Oracle:/Expected: line — or a Then that
28
+ * asserts only presence while the real oracle sits in a `# Tester verifies:`
29
+ * comment (#810)
28
30
  * SG-W009 — test-data references a `${QA_*}` secret that is declared in neither .env.qa
29
31
  * nor .env.qa.example — the test will throw "not set" the moment the key is used
30
32
  * SG-W010 — a row step's [Ref] looks POSITIONAL (First/Last/Top/…) but has no selectors.yaml
@@ -32,7 +32,11 @@ const MULTI_SCREEN_RISK_COUNT = MULTI_SCREEN_RISKS.length;
32
32
  import { capabilityRegistry } from '../capabilities/registry';
33
33
  import { discoverAndRegisterCapabilities } from '../capabilities/discover';
34
34
  import { contextRouter } from '../capabilities/context-router';
35
- import { readTextFile } from './read-text';
35
+ import { readTextFile, readViewpointText } from './read-text';
36
+ import { isAiDraft } from './viewpoint-draft-header';
37
+ import { viewpointReviewOpenFindings } from './viewpoint-review-open';
38
+ import { uiInventoryCoverage, uiInventoryFindings, UiInventoryResult } from './ui-inventory';
39
+ import { defectLinkFindings } from './defects';
36
40
 
37
41
  export interface AuditReport {
38
42
  screen: string;
@@ -57,6 +61,8 @@ export interface AuditReport {
57
61
  flowQuality?: FlowQualityResult; // #569 — flow contract verification (flows only)
58
62
  /** #595 — coverage per DECLARED FLOW; absent when the contract declares no `flows:`. */
59
63
  flowInventory?: InventoryResult;
64
+ /** #811 — design items vs the suite; present only when the unit holds design sources. */
65
+ uiInventory?: UiInventoryResult;
60
66
  calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
61
67
  axes: Record<string, number>;
62
68
  weakest: { axis: string; value: number };
@@ -185,6 +191,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
185
191
  const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
186
192
  const catalog = (scoringCap?.viewpoints?.() as Catalog | undefined) || loadCatalog();
187
193
  const spec = specCoverage(specPath, scenarios, featureText);
194
+ const uiInv = uiInventoryCoverage(path.join(screenDir, 'requirements'), featureText);
188
195
 
189
196
  // P3 — intent profile from qa/context.md drives the depth threshold (focus).
190
197
  const intent = readIntent(projectRootFromScreenDir(screenDir));
@@ -196,7 +203,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
196
203
  // Is the declaration this suite is measured against still the one a human accepted?
197
204
  const viewpointBaseline = checkViewpointBaseline(
198
205
  projectRootFromScreenDir(screenDir), catalogScreenName, viewpointPath);
199
- const viewpointText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
206
+ const viewpointText = readViewpointText(viewpointPath);
207
+ // `drafted-by: ai` — unreviewed until `--accept-viewpoint` removes it. Only a draft that declares ids
208
+ // has anything to accept; an id-less one is VIEWPOINT-MISSING's to report, not a dead-end finding.
209
+ const aiDraft = isAiDraft(viewpointText) && viewpointBaseline.status !== 'absent';
200
210
  // #569 — a flow WITH a contract is measured against ITS declaration, not a screen's.
201
211
  // Keyword page-type detection force-fit flows into screen checklists (a registration
202
212
  // flow judged as a `form` screen scored coverage 0%), so for contract-holding flows the
@@ -211,9 +221,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
211
221
  ? { ...catalog, page_types: {} } as Catalog
212
222
  : catalog;
213
223
  const capGate = scoringCap?.gateProvider as
214
- ((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
215
- const provided = capGate?.({ scenarios, viewpoints, catalog: gateCatalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
216
- const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, gateCatalog, platform === 'mobile', viewpointText);
224
+ ((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[]; isMobile: boolean; viewpointText: string }) => { gate: GateResult; depth: DepthResult }) | undefined;
225
+ // The capability gate gets the SAME isMobile + viewpoint text as the in-core fallback, or a declared
226
+ // `page-type:` is ignored wherever driver-ui scores — every web project, and a mobile project whose
227
+ // CLI cannot resolve driver-mobile (#864).
228
+ const isMobile = platform === 'mobile';
229
+ const provided = capGate?.({ scenarios, viewpoints, catalog: gateCatalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods, isMobile, viewpointText });
230
+ const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, gateCatalog, isMobile, viewpointText);
217
231
  const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(gateCatalog, gate.pageType), intent.focus);
218
232
 
219
233
  // TQ-4 — deferral-aware coverage credit: an inherently cross-screen theme (cart / detail /
@@ -342,7 +356,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
342
356
  // so it can only measure what their author already thought of. One field run wrote the file
343
357
  // after generating the suite and said so in its own header — and both axes still read 100%,
344
358
  // because a first audit records the baseline silently (#622). Untrusted until a human accepts it.
345
- const viewpointMoved = viewpointBaseline.status === 'changed' || adoptedPostHoc;
359
+ // An AI draft rides the same hold: its line is only removed when a human accepts it.
360
+ const viewpointMoved = viewpointBaseline.status === 'changed' || adoptedPostHoc || aiDraft;
346
361
  // #618 — is there a YARDSTICK at all? `atomicLedger` and `traceability` are both measured
347
362
  // against test-viewpoint.md, and four more sensors read it (the ledger, the declaration-integrity
348
363
  // checks, the browser-gesture check, the continuity check). When the file is absent every one of
@@ -373,7 +388,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
373
388
  { key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true, withheld: viewpointMoved && ledger.hasViewpoint && ledger.total > 0 },
374
389
  { key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
375
390
  { key: 'claimProof', value: claim.ratio, weight: 0.15, applicable: claim.withClaims > 0, critical: true },
376
- { key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false, withheld: viewpointMoved && viewpoints.length > 0 },
391
+ { key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && scenarios.length > 0 && !viewpointMoved, critical: false, withheld: viewpointMoved && viewpoints.length > 0 && scenarios.length > 0 },
377
392
  // A use-case decomposition is EF-heavy by construction (1 basic flow, N alternates, N
378
393
  // exceptions) — that is the shape of a well-decomposed use case, not neglected business
379
394
  // core. `flowCoverage` already measures whether the journey's phases are covered, so
@@ -585,7 +600,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
585
600
  findings.push(`COVERED-VIA-FLOW: critical theme "${c.theme}" is not covered on this screen but is deeply covered by flow "${c.flow}" — cross-screen depth correctly owned by the flow, so it is credited (not a screen gap). Verify with \`sungen flow-check\`.`);
586
601
  }
587
602
  // Verification-scope (advisory): API/DB verification the project's recorded test type never asked for.
588
- for (const v of verificationScopeFindings(featureText, fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '', capProfile)) {
603
+ for (const v of verificationScopeFindings(featureText, viewpointText, capProfile)) {
589
604
  findings.push(`VERIFICATION-OUT-OF-SCOPE (${v.layer}): this unit ${v.message}`);
590
605
  }
591
606
  for (const g of gate.gaps) {
@@ -614,9 +629,15 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
614
629
  // #414 — serial-cascade masking: an @serial suite of independent (re-navigating) scenarios hides
615
630
  // failures, because Playwright's describe.serial SKIPS everything after the first red. Isolation is
616
631
  // the default, so this only fires when @serial was added to a suite that didn't need it.
617
- const cascadeN = serialCascadeRisk(featureText, scenarios.length);
618
- if (cascadeN) {
619
- findings.push(`SERIAL-CASCADE-RISK: this \`@serial\` suite has ${cascadeN} INDEPENDENT scenarios (each re-navigates via Background) that don't need a shared-page sequence. \`describe.serial\` SKIPS every test after the first failure → one early red MASKS the rest as never-run. **Drop \`@serial\`** — the default per-scenario isolation runs every scenario and isolates failures. Keep \`@serial\` only for a genuinely stateful ordered sequence (where scenario N depends on N-1's state).`);
632
+ // `@serial` is a property of ONE feature file — only that file compiles to `describe.serial` — so
633
+ // each file is judged on its own text and its own scenarios. The unit-wide text paired one
634
+ // feature's @serial with another's navigating Background and counted every scenario of the unit,
635
+ // telling QA to drop @serial from a genuinely ordered 2-scenario feature (#887).
636
+ for (const file of featureFiles.filter((f) => fs.existsSync(f))) {
637
+ const cascadeN = serialCascadeRisk(readTextFile(file), scenarios.filter((s) => s.sourceFile === file).length);
638
+ if (cascadeN) {
639
+ findings.push(`SERIAL-CASCADE-RISK: the \`@serial\` suite ${path.basename(file)} has ${cascadeN} INDEPENDENT scenarios (each re-navigates via Background) that don't need a shared-page sequence. \`describe.serial\` SKIPS every test after the first failure → one early red MASKS the rest as never-run. **Drop \`@serial\`** — the default per-scenario isolation runs every scenario and isolates failures. Keep \`@serial\` only for a genuinely stateful ordered sequence (where scenario N depends on N-1's state).`);
640
+ }
620
641
  }
621
642
  if (flowDepth.stateful && flowDepth.missing.length) {
622
643
  // The advice names the collection the CONTRACT declared. It used to say "the cart" whatever
@@ -643,12 +664,19 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
643
664
  for (const m of taxonomy.mislabeled) {
644
665
  findings.push(`VP-MISLABEL: "${m.name}" is coded VP-${m.current} but reads as ${m.suggested} (signal: "${m.signal}") → re-tag VP-${m.suggested}-NNN so the coverage matrix isn't skewed.`);
645
666
  }
667
+ // Scenario ↔ defect record links (qa/defects/) — advisory, never scored.
668
+ findings.push(...defectLinkFindings(projectRootFromScreenDir(screenDir), catalogScreenName, featureText, scenarios.map((sc) => sc.name)));
646
669
  if (balance.unclassifiedRatio > 0.4) {
647
670
  findings.push(`TAXONOMY-UNCLASSIFIED: ${balance.note} → align the VP-<CATEGORY> codes with the catalog (or extend the bucket keywords) so coverage-balance is meaningful.`);
648
671
  } else if (balance.imbalanced && !flowScored) {
649
672
  findings.push(`BALANCE: ${balance.note} Stop expanding secondary viewpoints until business-core gaps are filled.`);
650
673
  }
651
- if (adoptedPostHoc) {
674
+ if (aiDraft) {
675
+ findings.push(`VIEWPOINT-DRAFT-UNREVIEWED: \`requirements/test-viewpoint.md\` is marked \`drafted-by: ai\` and no QA has accepted it, so it is not recorded as the baseline and \`atomicLedger\` + \`traceability\` are held as unverified — a checklist the AI wrote would otherwise score the suite it generated at 100% by construction. QA: review the claims (add what you care about, delete what you do not), then \`sungen audit --screen ${screenName} --accept-viewpoint\` — it records the baseline and removes the \`drafted-by: ai\` line. A QA decision: an agent presents it and does not run it.`);
676
+ }
677
+ // create-test's checklist review of the viewpoint, still waiting for QA — advisory, never scored.
678
+ findings.push(...viewpointReviewOpenFindings(path.dirname(viewpointPath)));
679
+ if (adoptedPostHoc && !aiDraft) { // a draft's own finding already says this and names the accept command
652
680
  findings.push(`VIEWPOINT-ADOPTED-POST-HOC: this is the first audit of \`requirements/test-viewpoint.md\` and the suite ALREADY has ${scenarios.length} scenarios, so the yardstick is being introduced against tests that already exist. A declaration written after the suite can only measure what its author already thought of — and if it was derived FROM the suite, \`atomicLedger\` and \`traceability\` read 100% by construction while anything dropped along the way stops being missing from anything. Both axes are held as unverified until you review the claims (add what you care about, delete what you do not) and confirm: \`sungen audit --screen ${screenName} --accept-viewpoint\` — a QA decision: an agent presents it and does not run it.`);
653
681
  }
654
682
  if (!hasYardstick) {
@@ -657,7 +685,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
657
685
  ...(unsourced.length ? [`${unsourced.length} scenario(s) could not be traced to a source`] : []),
658
686
  ];
659
687
  findings.push(`VIEWPOINT-MISSING: \`requirements/test-viewpoint.md\` ${ledger.hasViewpoint ? 'declares no viewpoints' : 'does not exist'}, so this unit has no yardstick. Two scored axes are excluded (atomicLedger, traceability) and four sensors cannot run: the atomic ledger, the declaration-integrity checks (reused ids, dangling refs, uniform priority), the browser-gesture check and the two-way continuity check. Nothing here is a pass — it is unmeasured [${lost.join(', ')}]. A flow contract does NOT substitute: the contract declares the use case, the viewpoint declares the CLAIMS the suite is scored against, and only the second is independent of what was generated. Author it (\`sungen add-flow\` scaffolds a template), then \`sungen audit --screen ${screenName} --accept-viewpoint\`.`);
660
- } else if (trace.mappedRatio < 0.5) {
688
+ } else if (scenarios.length > 0 && trace.mappedRatio < 0.5) {
689
+ // No scenario yet is not a traceability gap — the axis is n/a, like coverage and claimProof (#852).
661
690
  findings.push(`TRACE: ${trace.note}`);
662
691
  }
663
692
  // (UNIVERSAL viewpoint-gap finding now emitted by the `ui` gate sensor — see the gate block below.)
@@ -698,8 +727,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
698
727
  for (const d of downstream.underCovered) {
699
728
  findings.push(`DOWNSTREAM-SCOPE-MISSING: "${d.route}" is a navigation target but is covered only by a page-nav assertion — cover its content/guards, or scaffold it (\`sungen add --screen ${d.slug}\`).`);
700
729
  }
730
+ // #811 — design inventory (advisory): never moves the score or the gate.
731
+ findings.push(...uiInventoryFindings(uiInv));
701
732
  for (const m of manualOracleResult.insufficient.slice(0, 8)) {
702
- findings.push(`MANUAL-STEPS-INSUFFICIENT: "${m}" — @manual is incomplete: either Gherkin steps without a Then (invalid Gherkin; comments can't substitute), or a documentation manual whose comments lack numbered action steps + a labeled Observable:/Oracle: line. Write \`# Tester verifies:\` as "1. <action>" … "N. Observable: <what>" / "Oracle: <tool>" — that is what the delivery parser extracts into Steps/Expected.`);
733
+ findings.push(`MANUAL-STEPS-INSUFFICIENT: "${m}" — @manual is incomplete: either Gherkin steps without a Then (invalid Gherkin; comments can't substitute), a Then that only asserts presence (\`see [X] table\`) while the real oracle sits in a \`# Tester verifies:\` comment (the deliverable's Expected would not say what is checked), or a documentation manual whose comments lack numbered action steps + a labeled Observable:/Oracle: line. Write \`# Tester verifies:\` as "1. <action>" … "N. Observable: <what>" / "Oracle: <tool>" — that is what the delivery parser extracts into Steps/Expected.`);
703
734
  }
704
735
  // TQ-2 — automatable @manual: deferred (usually cross-screen) but fully DSL-expressible.
705
736
  for (const m of autoManual.scenarios.slice(0, 8)) {
@@ -797,7 +828,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
797
828
  // #622 — the priority table is where release selection comes from; a tag that contradicts it
798
829
  // means nobody can tell which of the two was the decision.
799
830
  {
800
- const pm = priorityMismatches(hasYardstick ? (readText(viewpointPath) ?? '') : '', scenarios);
831
+ const pm = priorityMismatches(hasYardstick ? viewpoints : [], scenarios);
801
832
  const byId = new Map<string, { declared: string; tagged: string; n: number }>();
802
833
  for (const m of pm) {
803
834
  const cur = byId.get(m.id) ?? { declared: m.declared, tagged: m.tagged, n: 0 };
@@ -870,8 +901,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
870
901
  axes.manualOracle = Math.round(manualCompleteness * 100) / 100;
871
902
  // Only a real mismatch scores 0. Without a yardstick `trace.mappedRatio` is 0 because there is
872
903
  // nothing to map to, and reporting that as the weakest axis sent authors to "raise taxonomy" —
873
- // an axis that was never measured.
874
- if (hasYardstick) axes.taxonomy = taxonomyMismatch ? 0 : Math.round(trace.mappedRatio * 100) / 100;
904
+ // an axis that was never measured. The same holds for a yardstick the ledger can read but that
905
+ // declares no viewpoint ID traceability can read (per-axis tables only, #832): the ledger has
906
+ // items, so `hasYardstick` is true, yet there is still nothing to map to. Same condition as the
907
+ // `traceability` score axis (`applicable: viewpoints.length > 0`).
908
+ if (hasYardstick && viewpoints.length > 0 && scenarios.length > 0) axes.taxonomy = taxonomyMismatch ? 0 : Math.round(trace.mappedRatio * 100) / 100;
875
909
  const notApplicable = axisDefs.filter((a) => !a.applicable).map((a) => a.key);
876
910
  const weakestEntry = Object.entries(axes).sort((a, b) => a[1] - b[1])[0];
877
911
  const weakest = { axis: weakestEntry[0], value: weakestEntry[1] };
@@ -899,6 +933,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
899
933
  gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
900
934
  taxonomyMismatch, reusedScenarioIds: reusedIds, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
901
935
  ...(isUiFlow ? { flowQuality: flowQ, flowInventory: inventory ?? undefined } : {}),
936
+ ...(uiInv.designSources.length ? { uiInventory: uiInv } : {}),
902
937
  score: {
903
938
  overall: Math.round(overall * 10) / 10,
904
939
  coverage: Math.round(coverage * 100) / 100,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * How many items a scenario's assertions demonstrate — the denominator of CLAIM-PARTIAL (#622).
3
+ *
4
+ * One reading for both places that check a counted claim (the viewpoint ledger and the title-level
5
+ * `numericClaimGaps`), which used to carry their own copy of the regex.
6
+ *
7
+ * Counted, once per distinct assertion:
8
+ * - a value: `see [X] … with|contains {{v}}`;
9
+ * - a positive state: `see [X] … is checked|selected|enabled|disabled`;
10
+ * - an indicator's presence: `see [X] icon|checkbox|toggle|radio|badge|image` (#866) — on mobile a
11
+ * ticked item is often observable ONLY as its tick icon, so one per item is the proof.
12
+ * Not counted: plain visibility of text/headers (it shows a label, not one of N values) and absence
13
+ * (`is hidden`, `not …`), which proves nothing is there.
14
+ */
15
+ // The tail stops before the NEXT `see [`, or consecutive assertions would swallow each other.
16
+ const SEE_STEP = /\bsee \[([^\]]+)\]((?:(?!\bsee \[)[^[])*)/g;
17
+ const VALUE = /^[^|]*?\b(?:with|contains) (\{\{[^}]*\}\})/;
18
+ const ABSENT = /^\s*\S*\s*(?:is hidden|is not\b|not\b|does not\b)/;
19
+ const STATE = /^\s*\S*\s*is (checked|selected|enabled|disabled)\b/;
20
+ const INDICATOR = /^\s*(icon|checkbox|toggle|radio|badge|image)\b/;
21
+
22
+ export function countedAssertions(stepsText: string): number {
23
+ const seen = new Set<string>();
24
+ for (const m of stepsText.matchAll(SEE_STEP)) {
25
+ const [, target, rest] = m;
26
+ if (ABSENT.test(rest)) continue;
27
+ // Keyed on the target and WHAT it proves — never on the tail, which runs into the next step, so
28
+ // the same element asserted before and after an action is still one assertion.
29
+ const proof = VALUE.exec(rest)?.[1] ?? STATE.exec(rest)?.[1] ?? INDICATOR.exec(rest)?.[1];
30
+ if (proof) seen.add(`${target}|${proof}`);
31
+ }
32
+ return seen.size;
33
+ }
@@ -155,29 +155,100 @@ function overrideKeys(raw?: string): Set<string> {
155
155
  return out;
156
156
  }
157
157
 
158
+ interface FeatureUnderLint {
159
+ /** Feature file basename — names the test-data file the runtime loads for it. */
160
+ name: string;
161
+ scenarios: ParsedScenario[];
162
+ tags: string[];
163
+ }
164
+
165
+ /** What every feature of a unit shares: unit-level refs and the @api catalog. */
166
+ interface UnitLintContext {
167
+ screenDir: string;
168
+ screenName: string;
169
+ cwd: string;
170
+ fileRefs: Set<string>;
171
+ apiParams: Map<string, string[]>;
172
+ }
173
+
158
174
  /** Lint the @cases/@query usage of one screen/flow directory. Returns advisory warnings. */
159
175
  export function lintDataDriven(screenDir: string, cwd: string = process.cwd()): DataDrivenWarning[] {
160
176
  const base = path.basename(screenDir);
161
177
  const screenName = unitIdOf(screenDir);
162
178
  // Every `.feature` of the unit — `add --feature` sub-features lint too. A file that
163
179
  // fails to parse contributes nothing rather than voiding the whole unit's lint.
164
- const scenarios: ParsedScenario[] = [];
165
- const featureTags: string[] = [];
166
- for (const p of featureFilesFor(screenDir, base).filter((f) => fs.existsSync(f))) {
180
+ const features: FeatureUnderLint[] = [];
181
+ const featureFiles = featureFilesFor(screenDir, base).filter((f) => fs.existsSync(f));
182
+ for (const p of featureFiles) {
167
183
  try {
168
184
  const feature = new GherkinParser().parseFeatureFile(p);
169
- scenarios.push(...(feature.scenarios || []));
170
- featureTags.push(...(feature.tags || []));
185
+ features.push({ name: path.basename(p, '.feature'), scenarios: feature.scenarios || [], tags: feature.tags || [] });
171
186
  } catch {
172
187
  /* a file that fails to parse contributes nothing */
173
188
  }
174
189
  }
175
- if (scenarios.length === 0) return [];
190
+ if (!features.some((f) => f.scenarios.length > 0)) return [];
191
+
192
+ const ctx: UnitLintContext = {
193
+ screenDir, screenName, cwd,
194
+ // `{{key}}` refs carried by the unit's selectors (i18n) and mock catalog are live too.
195
+ fileRefs: refsInUnitFiles(screenDir),
196
+ apiParams: apiParamsByName(screenName, cwd),
197
+ };
198
+
199
+ // #839 — each feature against ITS OWN test-data file. The runtime loads
200
+ // `test-data/<feature file basename>.yaml` (TestDataLoader.load(unit, featureFileName)), with no
201
+ // fallback and no merging; reading `<unit dir>.yaml` instead found nothing whenever the names
202
+ // differ — the default for `add --screen x --path /a/b` (→ b.feature) and for `--feature` — so
203
+ // every key read as missing and no orphan was ever reported. The tables are deliberately NOT
204
+ // merged: a key in another feature's file compiles to the literal `${key}` here.
205
+ const warns: DataDrivenWarning[] = [];
206
+ for (const f of features) if (f.scenarios.length > 0) warns.push(...lintFeature(f, ctx));
207
+ warns.push(...unloadedTestDataFiles(screenDir, featureFiles.map((f) => path.basename(f, '.feature'))));
208
+
209
+ // Catalog-level lint (SELECT-only, params declared/used, datasource present).
210
+ try {
211
+ for (const e of lintCatalog(screenName, null, cwd).errors) warns.push({ message: e });
212
+ } catch {
213
+ /* no catalog → nothing to lint */
214
+ }
215
+ return warns;
216
+ }
217
+
218
+ /**
219
+ * A test-data file no feature loads is dead in full: the runtime reads only
220
+ * `<feature>.yaml` and its `<feature>.<env>.yaml` overlays. Typically a `<unit>.yaml` written by
221
+ * hand next to a feature `add --path` named after the URL — its keys never reach a test.
222
+ * Every existing feature file counts (even one that failed to parse), so a parse error never
223
+ * turns its data into a false "loaded by no feature".
224
+ */
225
+ function unloadedTestDataFiles(screenDir: string, featureNames: string[]): DataDrivenWarning[] {
226
+ let files: string[];
227
+ try {
228
+ files = fs.readdirSync(path.join(screenDir, 'test-data')).filter((f) => /\.ya?ml$/.test(f) && !f.startsWith('_'));
229
+ } catch {
230
+ return [];
231
+ }
232
+ const loaded = (stem: string): boolean => featureNames.some((n) => stem === n || stem.startsWith(`${n}.`));
233
+ return files
234
+ .filter((f) => !loaded(f.replace(/\.ya?ml$/, '')) || !f.endsWith('.yaml'))
235
+ .map((f) => ({
236
+ message: `test-data/${f} is loaded by no feature — the runtime reads test-data/<feature>.yaml (+ <feature>.<env>.yaml) for ${featureNames.map((n) => `${n}.feature`).join(', ')}: move its keys there or remove it.`,
237
+ }));
238
+ }
176
239
 
177
- const tdPath = path.join(screenDir, 'test-data', `${base}.yaml`);
178
- const td: Record<string, any> = tdPath && fs.existsSync(tdPath) ? parseYaml(fs.readFileSync(tdPath, 'utf8')) || {} : {};
240
+ function lintFeature(feature: FeatureUnderLint, ctx: UnitLintContext): DataDrivenWarning[] {
241
+ const { screenName, cwd, fileRefs, apiParams } = ctx;
242
+ const { scenarios, tags: featureTags } = feature;
243
+ const tdFile = `test-data/${feature.name}.yaml`;
244
+ const tdPath = path.join(ctx.screenDir, 'test-data', `${feature.name}.yaml`);
245
+ const td: Record<string, any> = fs.existsSync(tdPath) ? parseYaml(fs.readFileSync(tdPath, 'utf8')) || {} : {};
179
246
  const topKeys = new Set(Object.keys(td));
180
247
  const warns: DataDrivenWarning[] = [];
248
+ // H3: an `@extend:<base>` scenario runs the base's steps first — the base's captures bind for it.
249
+ // Per feature, like the compiler's steps registry: an `@extend` never reaches another file's base.
250
+ const stepsBase = new Map<string, ParsedScenario>();
251
+ for (const sc of scenarios) if (sc.stepsName) stepsBase.set(sc.stepsName, sc);
181
252
  // Bindings a `@beforeAll` hook's `@api:`/`@query:` preconditions make are readable from EVERY
182
253
  // scenario of the feature (the compiler registers them feature-wide). A `remember … as` inside the
183
254
  // hook is NOT — it stays hook-local — so only the tag-bound names are exempted here.
@@ -186,18 +257,12 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
186
257
  if (sc.hookType !== 'beforeAll') continue;
187
258
  for (const t of sc.tags || []) { const m = t.match(/^@(?:api|query):([A-Za-z_]\w*)/); if (m) featureBound.add(m[1]); }
188
259
  }
189
- // H3: an `@extend:<base>` scenario runs the base's steps first — the base's captures bind for it.
190
- const stepsBase = new Map<string, ParsedScenario>();
191
- for (const sc of scenarios) if (sc.stepsName) stepsBase.set(sc.stepsName, sc);
192
- // `{{key}}` refs carried by the unit's selectors (i18n) and mock catalog are live too.
193
- const fileRefs = refsInUnitFiles(screenDir);
194
260
  // A feature-level `@manual` makes every scenario manual (the whole file is skipped by the compiler).
195
261
  const featureManual = featureTags.some((t) => t === '@manual' || t.startsWith('@manual:'));
196
262
 
197
263
  // What the scenarios bind, collected up front — the @query pass below adds the params it binds by name.
198
264
  const usedHeads = new Set<string>();
199
265
  const usedDatasets = new Set<string>();
200
- const apiParams = apiParamsByName(screenName, cwd);
201
266
  for (const h of fileRefs) usedHeads.add(h);
202
267
  for (const sc of scenarios) {
203
268
  for (const r of collectRefs(sc, featureManual || isManualScenario(sc))) usedHeads.add(r.split(/[.[]/)[0]);
@@ -228,7 +293,7 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
228
293
  const ds = casesTag.slice('@cases:'.length).trim();
229
294
  const rows = td[ds];
230
295
  if (!Array.isArray(rows)) {
231
- warns.push({ scenario: sc.name, message: `@cases:${ds} → dataset "${ds}" is missing or not a list in test-data.` });
296
+ warns.push({ scenario: sc.name, message: `@cases:${ds} → dataset "${ds}" is missing or not a list in ${tdFile}.` });
232
297
  } else {
233
298
  const colSets = rows.map((r) => new Set(Object.keys(r || {}).filter((k) => !ROW_META.has(k))));
234
299
  const allCols = new Set<string>();
@@ -256,7 +321,7 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
256
321
  for (const r of refs) {
257
322
  const head = r.split(/[.[]/)[0];
258
323
  if (head.startsWith('$') || bound.has(head) || featureBound.has(head) || topKeys.has(head)) continue;
259
- warns.push({ scenario: sc.name, message: `{{${r}}} is not a test-data key and nothing binds it at run time — it compiles to the literal text '\${${r}}' (add the key to test-data, or fix the ref).` });
324
+ warns.push({ scenario: sc.name, message: `{{${r}}} is not a test-data key and nothing binds it at run time — it compiles to the literal text '\${${r}}' (add the key to ${tdFile}, or fix the ref).` });
260
325
  }
261
326
  }
262
327
 
@@ -289,15 +354,8 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
289
354
  for (const k of topKeys) {
290
355
  if (k.startsWith('_')) continue; // Data Factory metadata, not scenario data
291
356
  if (!usedHeads.has(k) && !usedDatasets.has(k)) {
292
- warns.push({ message: `test-data key "${k}" is defined but never referenced ({{${k}}}, a @cases dataset, or an override) — dead data: bind it into a scenario or remove it.` });
357
+ warns.push({ message: `test-data key "${k}" is defined but never referenced ({{${k}}}, a @cases dataset, or an override) by ${feature.name}.feature — dead data: bind it into a scenario or remove it from ${tdFile}.` });
293
358
  }
294
359
  }
295
-
296
- // Catalog-level lint (SELECT-only, params declared/used, datasource present).
297
- try {
298
- for (const e of lintCatalog(screenName, null, cwd).errors) warns.push({ message: e });
299
- } catch {
300
- /* no catalog → nothing to lint */
301
- }
302
360
  return warns;
303
361
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * `sungen defect draft` — turn each `product-candidate` row of a unit's last triage into a record
3
+ * `qa/defects/DEF-NNN-<slug>.md` (see defects.ts for the link checks). Deterministic: Steps come
4
+ * from the scenario's Gherkin with test-data resolved, Expected/Actual from the assertion, and the
5
+ * severity is PROPOSED by rule until QA confirms it. Never overwrites a file, and never drafts a
6
+ * scenario an existing record already names.
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { stringify as toYaml } from 'yaml';
11
+ import type { ResolvedUnit } from '../cli/resolve-unit';
12
+ import { redactSecrets } from '../exporters/api-testcase-formatter';
13
+ import { getEnvironment } from '../exporters/run-environment';
14
+ import { parseFeatureMetadata } from '../exporters/feature-parser';
15
+ import { mergeFeatureAndSpec, MergedScenario } from '../exporters/scenario-merger';
16
+ import { maskCredentialValue } from '../exporters/sensitive-key';
17
+ import { parseSpecFile } from '../exporters/spec-parser';
18
+ import { loadTestData, substituteTestDataVars } from '../exporters/test-data-resolver';
19
+ import { readCapabilities } from './capability';
20
+ import { defectsDir, nextDefectId, readDefects } from './defects';
21
+ import type { TriageRow } from './triage';
22
+ import { readTriageReport, unitSecrets } from './triage-unit';
23
+
24
+ export interface DraftResult {
25
+ /** `sungen triage` has not run for this unit. */
26
+ missingTriage?: boolean;
27
+ written: string[];
28
+ skipped: Array<{ scenario: string; id: string }>;
29
+ }
30
+
31
+ /** Blocking actions: a @high scenario that fails on one of these is `major`, otherwise `minor`. */
32
+ const BLOCKING = /\b(?:submit|save|apply|log ?in|sign ?in|sign ?up|register|checkout|pay|confirm|send|navigate|redirect)\b/i;
33
+
34
+ const slug = (s: string): string =>
35
+ s.replace(/^\s*(?:VP|FL)-[A-Z0-9-]+\s+/i, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50).replace(/-$/, '');
36
+
37
+ interface UnitScenario { merged: MergedScenario; testData: Record<string, string>; secrets: string[] }
38
+
39
+ /** Scenario title → its merged Gherkin, test-data (credentials masked) and secret values. */
40
+ function unitScenarios(cwd: string, unit: ResolvedUnit): Map<string, UnitScenario> {
41
+ const out = new Map<string, UnitScenario>();
42
+ const featuresDir = path.join(unit.dir, 'features');
43
+ if (!fs.existsSync(featuresDir)) return out;
44
+ for (const f of fs.readdirSync(featuresDir).filter((x) => x.endsWith('.feature'))) {
45
+ const base = f.replace(/\.feature$/, '');
46
+ const spec = parseSpecFile(path.join(cwd, 'specs', 'generated', unit.unitId, `${base}.spec.ts`));
47
+ const dataFile = path.join(unit.dir, 'test-data', `${base}.yaml`);
48
+ const raw = fs.existsSync(dataFile) ? loadTestData(dataFile) : {};
49
+ const testData = Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, maskCredentialValue(k, String(v))]));
50
+ const secrets = unitSecrets(raw, cwd);
51
+ for (const m of mergeFeatureAndSpec(parseFeatureMetadata(path.join(featuresDir, f)), spec)) {
52
+ out.set(m.feature.name, { merged: m, testData, secrets });
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function render(cwd: string, unit: ResolvedUnit, id: string, row: TriageRow, sc: UnitScenario | undefined): string {
59
+ const tags = sc?.merged.feature.tags ?? [];
60
+ const resolve = (t: string): string => (sc ? substituteTestDataVars(t, sc.testData) : t);
61
+ const steps = (sc?.merged.resolvedSteps ?? []).map((s) => resolve(s.text));
62
+ const thens = (sc?.merged.resolvedExpected ?? []).map((s) => resolve(s.text));
63
+ const high = tags.some((t) => t === '@high' || t === '@critical');
64
+ const severity = high && BLOCKING.test([row.scenario, ...steps].join(' ')) ? 'major' : 'minor';
65
+ const specFile = path.join(unit.dir, 'requirements', 'spec.md');
66
+ const env = getEnvironment(cwd);
67
+ const front = {
68
+ id, title: row.scenario, severity, severity_source: 'proposed', status: 'open',
69
+ units: [unit.unitId], scenarios: [row.scenario], claims: row.ids, legacy: row.legacy,
70
+ spec: fs.existsSync(specFile) ? path.relative(cwd, specFile) : '',
71
+ found: {
72
+ date: new Date().toISOString().slice(0, 10),
73
+ env: process.env.SUNGEN_ENV || 'default',
74
+ base_url: env.baseURL,
75
+ platform: row.variant || readCapabilities(cwd).platform || 'web',
76
+ },
77
+ tracker: '',
78
+ };
79
+ const body = [
80
+ '---', toYaml(front).trimEnd(), '---', '',
81
+ `# ${id}: ${row.scenario}`, '',
82
+ '## Summary', '', '(to fill: what is wrong, where, and who it affects)', '',
83
+ '## Steps to reproduce', '', ...(steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ['(scenario steps not found — see the feature)']), '',
84
+ '## Expected', '', ...thens.map((t) => `- ${t}`), ...(row.expected ? [`- Asserted value: ${row.expected}`] : []), '',
85
+ '## Actual', '', `- ${row.received ? `Received: ${row.received}` : row.evidence}`, '',
86
+ '## Evidence', '', `- Error: ${row.evidence}`, ...(row.tracePath ? [`- Trace: ${row.tracePath}`] : []), '',
87
+ '## Notes', '', `- Triage: ${row.class} (${row.ruleId})${row.variant ? ` on ${row.variant}` : ''}`, '',
88
+ ].join('\n');
89
+ return redactSecrets(body, sc?.secrets ?? []);
90
+ }
91
+
92
+ export function draftDefects(cwd: string, unit: ResolvedUnit, opts: { scenario?: string } = {}): DraftResult {
93
+ const triage = readTriageReport(cwd, unit.unitId);
94
+ if (!triage) return { missingTriage: true, written: [], skipped: [] };
95
+ const recorded = new Map<string, string>();
96
+ for (const r of readDefects(cwd)) if (r.units.includes(unit.unitId)) for (const s of r.scenarios) recorded.set(s, r.id);
97
+
98
+ const wanted = triage.rows.filter((r) => r.class === 'product-candidate'
99
+ && (!opts.scenario || r.scenario.includes(opts.scenario) || r.ids.includes(opts.scenario)));
100
+ const scenarios = unitScenarios(cwd, unit);
101
+ const result: DraftResult = { written: [], skipped: [] };
102
+ const seen = new Set<string>();
103
+ for (const row of wanted) {
104
+ if (seen.has(row.scenario)) continue; // one record per scenario, even across OS variants
105
+ seen.add(row.scenario);
106
+ const existing = recorded.get(row.scenario);
107
+ if (existing) { result.skipped.push({ scenario: row.scenario, id: existing }); continue; }
108
+ const id = nextDefectId(cwd);
109
+ const file = path.join(defectsDir(cwd), `${id}-${slug(row.scenario) || 'defect'}.md`);
110
+ fs.mkdirSync(path.dirname(file), { recursive: true });
111
+ fs.writeFileSync(file, render(cwd, unit, id, row, scenarios.get(row.scenario)), { encoding: 'utf-8', flag: 'wx' });
112
+ result.written.push(path.relative(cwd, file));
113
+ }
114
+ return result;
115
+ }