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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/dist/cli/commands/audit.d.ts +4 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +54 -10
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/capability.d.ts.map +1 -1
  6. package/dist/cli/commands/capability.js +5 -1
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/changed.d.ts.map +1 -1
  9. package/dist/cli/commands/changed.js +2 -3
  10. package/dist/cli/commands/changed.js.map +1 -1
  11. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  12. package/dist/cli/commands/dashboard.js +2 -28
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/defect.d.ts +7 -0
  15. package/dist/cli/commands/defect.d.ts.map +1 -0
  16. package/dist/cli/commands/defect.js +68 -0
  17. package/dist/cli/commands/defect.js.map +1 -0
  18. package/dist/cli/commands/delivery.d.ts.map +1 -1
  19. package/dist/cli/commands/delivery.js +75 -42
  20. package/dist/cli/commands/delivery.js.map +1 -1
  21. package/dist/cli/commands/results-merge.d.ts +9 -0
  22. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  23. package/dist/cli/commands/results-merge.js +7 -0
  24. package/dist/cli/commands/results-merge.js.map +1 -1
  25. package/dist/cli/commands/select.d.ts +12 -0
  26. package/dist/cli/commands/select.d.ts.map +1 -0
  27. package/dist/cli/commands/select.js +122 -0
  28. package/dist/cli/commands/select.js.map +1 -0
  29. package/dist/cli/commands/triage.d.ts +7 -0
  30. package/dist/cli/commands/triage.d.ts.map +1 -0
  31. package/dist/cli/commands/triage.js +98 -0
  32. package/dist/cli/commands/triage.js.map +1 -0
  33. package/dist/cli/commands/update.d.ts.map +1 -1
  34. package/dist/cli/commands/update.js +3 -0
  35. package/dist/cli/commands/update.js.map +1 -1
  36. package/dist/cli/commands/viewpoint.d.ts +7 -0
  37. package/dist/cli/commands/viewpoint.d.ts.map +1 -0
  38. package/dist/cli/commands/viewpoint.js +70 -0
  39. package/dist/cli/commands/viewpoint.js.map +1 -0
  40. package/dist/cli/index.js +13 -2
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/dashboard/templates/index.html +1 -1
  43. package/dist/dashboard/types.d.ts +1 -0
  44. package/dist/dashboard/types.d.ts.map +1 -1
  45. package/dist/exporters/csv-exporter.d.ts +4 -0
  46. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  47. package/dist/exporters/csv-exporter.js +12 -11
  48. package/dist/exporters/csv-exporter.js.map +1 -1
  49. package/dist/exporters/feature-parser.d.ts +8 -0
  50. package/dist/exporters/feature-parser.d.ts.map +1 -1
  51. package/dist/exporters/feature-parser.js +12 -0
  52. package/dist/exporters/feature-parser.js.map +1 -1
  53. package/dist/exporters/json-exporter.d.ts.map +1 -1
  54. package/dist/exporters/json-exporter.js +7 -0
  55. package/dist/exporters/json-exporter.js.map +1 -1
  56. package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
  57. package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
  58. package/dist/exporters/manual-hybrid-oracle.js +114 -0
  59. package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
  60. package/dist/exporters/matrix/build.d.ts.map +1 -1
  61. package/dist/exporters/matrix/build.js +55 -12
  62. package/dist/exporters/matrix/build.js.map +1 -1
  63. package/dist/exporters/matrix/carry-manual-results.d.ts +19 -0
  64. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -1
  65. package/dist/exporters/matrix/carry-manual-results.js +76 -6
  66. package/dist/exporters/matrix/carry-manual-results.js.map +1 -1
  67. package/dist/exporters/matrix/export.d.ts +8 -2
  68. package/dist/exporters/matrix/export.d.ts.map +1 -1
  69. package/dist/exporters/matrix/export.js +15 -3
  70. package/dist/exporters/matrix/export.js.map +1 -1
  71. package/dist/exporters/matrix/gates.js +6 -0
  72. package/dist/exporters/matrix/gates.js.map +1 -1
  73. package/dist/exporters/matrix/render-csv.js +1 -1
  74. package/dist/exporters/matrix/render-csv.js.map +1 -1
  75. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  76. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  77. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  78. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  79. package/dist/exporters/matrix/render-xlsx.js +3 -0
  80. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  81. package/dist/exporters/matrix/types.d.ts +13 -1
  82. package/dist/exporters/matrix/types.d.ts.map +1 -1
  83. package/dist/exporters/matrix/types.js.map +1 -1
  84. package/dist/exporters/matrix/wording.d.ts +1 -1
  85. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  86. package/dist/exporters/matrix/wording.js +10 -1
  87. package/dist/exporters/matrix/wording.js.map +1 -1
  88. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  89. package/dist/exporters/playwright-report-parser.js +1 -0
  90. package/dist/exporters/playwright-report-parser.js.map +1 -1
  91. package/dist/exporters/run-environment.d.ts +32 -0
  92. package/dist/exporters/run-environment.d.ts.map +1 -0
  93. package/dist/exporters/run-environment.js +125 -0
  94. package/dist/exporters/run-environment.js.map +1 -0
  95. package/dist/exporters/scenario-merger.d.ts +18 -0
  96. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  97. package/dist/exporters/scenario-merger.js +49 -3
  98. package/dist/exporters/scenario-merger.js.map +1 -1
  99. package/dist/exporters/test-data-resolver.d.ts +0 -4
  100. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  101. package/dist/exporters/test-data-resolver.js +0 -7
  102. package/dist/exporters/test-data-resolver.js.map +1 -1
  103. package/dist/exporters/types.d.ts +5 -0
  104. package/dist/exporters/types.d.ts.map +1 -1
  105. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  106. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  107. package/dist/generators/gherkin-parser/index.js +49 -19
  108. package/dist/generators/gherkin-parser/index.js.map +1 -1
  109. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  110. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  113. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  114. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  115. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  116. package/dist/generators/test-generator/code-generator.js +23 -2
  117. package/dist/generators/test-generator/code-generator.js.map +1 -1
  118. package/dist/generators/test-generator/diagnostics.d.ts +3 -1
  119. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  120. package/dist/generators/test-generator/diagnostics.js +3 -1
  121. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  122. package/dist/harness/audit.d.ts +3 -0
  123. package/dist/harness/audit.d.ts.map +1 -1
  124. package/dist/harness/audit.js +48 -15
  125. package/dist/harness/audit.js.map +1 -1
  126. package/dist/harness/counted-assertions.d.ts +2 -0
  127. package/dist/harness/counted-assertions.d.ts.map +1 -0
  128. package/dist/harness/counted-assertions.js +38 -0
  129. package/dist/harness/counted-assertions.js.map +1 -0
  130. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  131. package/dist/harness/data-driven-lint.js +69 -27
  132. package/dist/harness/data-driven-lint.js.map +1 -1
  133. package/dist/harness/defect-draft.d.ts +14 -0
  134. package/dist/harness/defect-draft.d.ts.map +1 -0
  135. package/dist/harness/defect-draft.js +143 -0
  136. package/dist/harness/defect-draft.js.map +1 -0
  137. package/dist/harness/defects.d.ts +24 -0
  138. package/dist/harness/defects.d.ts.map +1 -0
  139. package/dist/harness/defects.js +118 -0
  140. package/dist/harness/defects.js.map +1 -0
  141. package/dist/harness/manifest.d.ts.map +1 -1
  142. package/dist/harness/manifest.js +2 -2
  143. package/dist/harness/manifest.js.map +1 -1
  144. package/dist/harness/next-step.d.ts +2 -4
  145. package/dist/harness/next-step.d.ts.map +1 -1
  146. package/dist/harness/next-step.js +50 -5
  147. package/dist/harness/next-step.js.map +1 -1
  148. package/dist/harness/parse.d.ts +45 -0
  149. package/dist/harness/parse.d.ts.map +1 -1
  150. package/dist/harness/parse.js +142 -24
  151. package/dist/harness/parse.js.map +1 -1
  152. package/dist/harness/quality-gates.d.ts.map +1 -1
  153. package/dist/harness/quality-gates.js +6 -4
  154. package/dist/harness/quality-gates.js.map +1 -1
  155. package/dist/harness/read-text.d.ts +15 -0
  156. package/dist/harness/read-text.d.ts.map +1 -1
  157. package/dist/harness/read-text.js +25 -0
  158. package/dist/harness/read-text.js.map +1 -1
  159. package/dist/harness/script-check.d.ts.map +1 -1
  160. package/dist/harness/script-check.js +7 -1
  161. package/dist/harness/script-check.js.map +1 -1
  162. package/dist/harness/select-by-ids.d.ts +31 -0
  163. package/dist/harness/select-by-ids.d.ts.map +1 -0
  164. package/dist/harness/select-by-ids.js +106 -0
  165. package/dist/harness/select-by-ids.js.map +1 -0
  166. package/dist/harness/sensors.d.ts +8 -1
  167. package/dist/harness/sensors.d.ts.map +1 -1
  168. package/dist/harness/sensors.js +92 -21
  169. package/dist/harness/sensors.js.map +1 -1
  170. package/dist/harness/spec-branches.js +1 -1
  171. package/dist/harness/spec-branches.js.map +1 -1
  172. package/dist/harness/triage-signals.d.ts +20 -0
  173. package/dist/harness/triage-signals.d.ts.map +1 -0
  174. package/dist/harness/triage-signals.js +30 -0
  175. package/dist/harness/triage-signals.js.map +1 -0
  176. package/dist/harness/triage-unit.d.ts +23 -0
  177. package/dist/harness/triage-unit.d.ts.map +1 -0
  178. package/dist/harness/triage-unit.js +142 -0
  179. package/dist/harness/triage-unit.js.map +1 -0
  180. package/dist/harness/triage.d.ts +28 -0
  181. package/dist/harness/triage.d.ts.map +1 -0
  182. package/dist/harness/triage.js +99 -0
  183. package/dist/harness/triage.js.map +1 -0
  184. package/dist/harness/ui-inventory.d.ts +40 -0
  185. package/dist/harness/ui-inventory.d.ts.map +1 -0
  186. package/dist/harness/ui-inventory.js +191 -0
  187. package/dist/harness/ui-inventory.js.map +1 -0
  188. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  189. package/dist/harness/viewpoint-baseline.js +7 -1
  190. package/dist/harness/viewpoint-baseline.js.map +1 -1
  191. package/dist/harness/viewpoint-draft-header.d.ts +7 -0
  192. package/dist/harness/viewpoint-draft-header.d.ts.map +1 -0
  193. package/dist/harness/viewpoint-draft-header.js +100 -0
  194. package/dist/harness/viewpoint-draft-header.js.map +1 -0
  195. package/dist/harness/viewpoint-ledger.d.ts +13 -3
  196. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  197. package/dist/harness/viewpoint-ledger.js +104 -18
  198. package/dist/harness/viewpoint-ledger.js.map +1 -1
  199. package/dist/harness/viewpoint-mode.d.ts +9 -0
  200. package/dist/harness/viewpoint-mode.d.ts.map +1 -0
  201. package/dist/harness/viewpoint-mode.js +52 -0
  202. package/dist/harness/viewpoint-mode.js.map +1 -0
  203. package/dist/harness/viewpoint-priority.d.ts +19 -0
  204. package/dist/harness/viewpoint-priority.d.ts.map +1 -0
  205. package/dist/harness/viewpoint-priority.js +36 -0
  206. package/dist/harness/viewpoint-priority.js.map +1 -0
  207. package/dist/harness/viewpoint-review-open.d.ts +6 -0
  208. package/dist/harness/viewpoint-review-open.d.ts.map +1 -0
  209. package/dist/harness/viewpoint-review-open.js +90 -0
  210. package/dist/harness/viewpoint-review-open.js.map +1 -0
  211. package/dist/harness/viewpoint-scan-refs.d.ts +50 -0
  212. package/dist/harness/viewpoint-scan-refs.d.ts.map +1 -0
  213. package/dist/harness/viewpoint-scan-refs.js +200 -0
  214. package/dist/harness/viewpoint-scan-refs.js.map +1 -0
  215. package/dist/harness/viewpoint-scan.d.ts +41 -0
  216. package/dist/harness/viewpoint-scan.d.ts.map +1 -0
  217. package/dist/harness/viewpoint-scan.js +118 -0
  218. package/dist/harness/viewpoint-scan.js.map +1 -0
  219. package/dist/orchestrator/flow-manager.d.ts +0 -1
  220. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  221. package/dist/orchestrator/flow-manager.js +3 -52
  222. package/dist/orchestrator/flow-manager.js.map +1 -1
  223. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  224. package/dist/orchestrator/screen-manager.js +8 -62
  225. package/dist/orchestrator/screen-manager.js.map +1 -1
  226. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  227. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  228. package/dist/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  229. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  230. package/dist/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  231. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  232. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +22 -0
  233. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -9
  234. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  235. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  236. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  237. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  238. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  239. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  240. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  241. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  242. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  243. package/dist/orchestrator/templates/specs-url-assert.js +24 -2
  244. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  245. package/dist/orchestrator/templates/specs-url-assert.ts +23 -2
  246. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  247. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  248. package/dist/orchestrator/trace-mode-install.js +132 -0
  249. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  250. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +23 -0
  251. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  252. package/dist/orchestrator/viewpoint-pattern-catalog.js +48 -0
  253. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  254. package/dist/orchestrator/viewpoint-scaffold.d.ts +3 -0
  255. package/dist/orchestrator/viewpoint-scaffold.d.ts.map +1 -0
  256. package/dist/orchestrator/viewpoint-scaffold.js +70 -0
  257. package/dist/orchestrator/viewpoint-scaffold.js.map +1 -0
  258. package/package.json +3 -3
  259. package/src/cli/commands/audit.ts +49 -10
  260. package/src/cli/commands/capability.ts +3 -1
  261. package/src/cli/commands/changed.ts +2 -4
  262. package/src/cli/commands/dashboard.ts +1 -26
  263. package/src/cli/commands/defect.ts +49 -0
  264. package/src/cli/commands/delivery.ts +70 -42
  265. package/src/cli/commands/results-merge.ts +7 -1
  266. package/src/cli/commands/select.ts +88 -0
  267. package/src/cli/commands/triage.ts +57 -0
  268. package/src/cli/commands/update.ts +3 -0
  269. package/src/cli/commands/viewpoint.ts +58 -0
  270. package/src/cli/index.ts +13 -2
  271. package/src/dashboard/templates/index.html +1 -1
  272. package/src/dashboard/types.ts +1 -0
  273. package/src/exporters/csv-exporter.ts +12 -11
  274. package/src/exporters/feature-parser.ts +12 -0
  275. package/src/exporters/json-exporter.ts +6 -1
  276. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  277. package/src/exporters/matrix/build.ts +57 -11
  278. package/src/exporters/matrix/carry-manual-results.ts +70 -7
  279. package/src/exporters/matrix/export.ts +16 -2
  280. package/src/exporters/matrix/gates.ts +6 -0
  281. package/src/exporters/matrix/render-csv.ts +1 -1
  282. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  283. package/src/exporters/matrix/render-xlsx.ts +3 -0
  284. package/src/exporters/matrix/types.ts +13 -1
  285. package/src/exporters/matrix/wording.ts +10 -1
  286. package/src/exporters/playwright-report-parser.ts +1 -0
  287. package/src/exporters/run-environment.ts +89 -0
  288. package/src/exporters/scenario-merger.ts +51 -5
  289. package/src/exporters/test-data-resolver.ts +0 -6
  290. package/src/exporters/types.ts +5 -0
  291. package/src/generators/gherkin-parser/index.ts +50 -18
  292. package/src/generators/test-generator/adapters/adapter-interface.ts +4 -0
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  296. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  297. package/src/generators/test-generator/code-generator.ts +23 -2
  298. package/src/generators/test-generator/diagnostics.ts +3 -1
  299. package/src/harness/audit.ts +52 -17
  300. package/src/harness/counted-assertions.ts +33 -0
  301. package/src/harness/data-driven-lint.ts +82 -24
  302. package/src/harness/defect-draft.ts +115 -0
  303. package/src/harness/defects.ts +94 -0
  304. package/src/harness/manifest.ts +2 -2
  305. package/src/harness/next-step.ts +47 -5
  306. package/src/harness/parse.ts +151 -27
  307. package/src/harness/quality-gates.ts +6 -4
  308. package/src/harness/read-text.ts +25 -0
  309. package/src/harness/script-check.ts +7 -1
  310. package/src/harness/select-by-ids.ts +83 -0
  311. package/src/harness/sensors.ts +91 -21
  312. package/src/harness/spec-branches.ts +1 -1
  313. package/src/harness/triage-signals.ts +46 -0
  314. package/src/harness/triage-unit.ts +113 -0
  315. package/src/harness/triage.ts +127 -0
  316. package/src/harness/ui-inventory.ts +164 -0
  317. package/src/harness/viewpoint-baseline.ts +7 -1
  318. package/src/harness/viewpoint-draft-header.ts +55 -0
  319. package/src/harness/viewpoint-ledger.ts +111 -24
  320. package/src/harness/viewpoint-mode.ts +49 -0
  321. package/src/harness/viewpoint-priority.ts +38 -0
  322. package/src/harness/viewpoint-review-open.ts +51 -0
  323. package/src/harness/viewpoint-scan-refs.ts +154 -0
  324. package/src/harness/viewpoint-scan.ts +111 -0
  325. package/src/orchestrator/flow-manager.ts +3 -53
  326. package/src/orchestrator/screen-manager.ts +7 -61
  327. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  328. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  329. package/src/orchestrator/templates/ai-src/commands/create-test.md +32 -24
  330. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  331. package/src/orchestrator/templates/ai-src/commands/run-test.md +78 -15
  332. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  333. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +22 -0
  334. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -9
  335. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
  336. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  337. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
  338. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  339. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
  340. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
  341. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
  342. package/src/orchestrator/templates/playwright.config.ts +4 -2
  343. package/src/orchestrator/templates/specs-url-assert.ts +23 -2
  344. package/src/orchestrator/trace-mode-install.ts +94 -0
  345. package/src/orchestrator/viewpoint-pattern-catalog.ts +61 -0
  346. package/src/orchestrator/viewpoint-scaffold.ts +36 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Defect records — `qa/defects/DEF-NNN-<slug>.md`, one project-wide folder. A scenario points at a
3
+ * record with `@known-defect:DEF-NNN`; the record points back at units, scenarios, claims and legacy
4
+ * TCs in its frontmatter. This module reads the records and checks both directions of that link.
5
+ * Drafting a record from a triage row lives in defect-draft.ts.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { parse as parseYaml } from 'yaml';
10
+
11
+ export const DEFECT_STATUSES = ['open', 'fixed', 'verified', 'closed', 'wont-fix'] as const;
12
+ export type DefectStatus = typeof DEFECT_STATUSES[number];
13
+
14
+ export interface DefectRecord {
15
+ id: string;
16
+ title: string;
17
+ severity: string;
18
+ status: string;
19
+ units: string[];
20
+ scenarios: string[];
21
+ /** Project-relative path of the record file. */
22
+ file: string;
23
+ }
24
+
25
+ const DEF_FILE = /^(DEF-(\d+))-.*\.md$/;
26
+ const KNOWN_DEFECT_TAG = /@known-defect:(DEF-\d+)\b/g;
27
+
28
+ export function defectsDir(cwd: string): string {
29
+ return path.join(cwd, 'qa', 'defects');
30
+ }
31
+
32
+ function defectFiles(cwd: string): string[] {
33
+ const dir = defectsDir(cwd);
34
+ return fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => DEF_FILE.test(f)).sort() : [];
35
+ }
36
+
37
+ /** The next free id: one above the highest number on disk, zero-padded to 3. */
38
+ export function nextDefectId(cwd: string): string {
39
+ const max = defectFiles(cwd).reduce((m, f) => Math.max(m, Number(f.match(DEF_FILE)![2])), 0);
40
+ return `DEF-${String(max + 1).padStart(3, '0')}`;
41
+ }
42
+
43
+ const list = (v: unknown): string[] => (Array.isArray(v) ? v.map(String) : []);
44
+
45
+ /** Every record whose frontmatter parses; a malformed file is skipped, never thrown on. */
46
+ export function readDefects(cwd: string): DefectRecord[] {
47
+ const out: DefectRecord[] = [];
48
+ for (const f of defectFiles(cwd)) {
49
+ const file = path.join(defectsDir(cwd), f);
50
+ try {
51
+ const fm = parseYaml(fs.readFileSync(file, 'utf-8').split(/^---\s*$/m)[1] ?? '') as Record<string, unknown> | null;
52
+ if (!fm || typeof fm !== 'object') continue;
53
+ out.push({
54
+ id: String(fm.id ?? f.match(DEF_FILE)![1]),
55
+ title: String(fm.title ?? ''),
56
+ severity: String(fm.severity ?? ''),
57
+ status: String(fm.status ?? ''),
58
+ units: list(fm.units),
59
+ scenarios: list(fm.scenarios),
60
+ file: path.relative(cwd, file),
61
+ });
62
+ } catch {
63
+ // unreadable frontmatter: not a record we can reason about
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+
69
+ /**
70
+ * Advisory audit findings for one unit (never scored):
71
+ * DEFECT-REF-MISSING a `@known-defect:DEF-NNN` tag with no record file.
72
+ * DEFECT-ORPHAN (info) an OPEN record of this unit names a scenario the unit no longer has.
73
+ */
74
+ export function defectLinkFindings(cwd: string, unitId: string, featureText: string, scenarioNames: string[]): string[] {
75
+ const records = readDefects(cwd);
76
+ const findings: string[] = [];
77
+ const known = new Set(records.map((r) => r.id));
78
+ const tagged = new Set([...featureText.matchAll(KNOWN_DEFECT_TAG)].map((m) => m[1]));
79
+ for (const id of tagged) {
80
+ if (!known.has(id)) {
81
+ findings.push(`DEFECT-REF-MISSING: \`@known-defect:${id}\` has no record in qa/defects/ — draft it with \`sungen defect draft --screen ${unitId}\`, or correct the id.`);
82
+ }
83
+ }
84
+ const names = new Set(scenarioNames);
85
+ for (const r of records) {
86
+ if (r.status !== 'open' || !r.units.includes(unitId)) continue;
87
+ for (const s of r.scenarios) {
88
+ if (!names.has(s)) {
89
+ findings.push(`DEFECT-ORPHAN (info): ${r.id} is open and names scenario "${s}", which ${unitId} no longer has — update the record's scenarios, or close it.`);
90
+ }
91
+ }
92
+ }
93
+ return findings;
94
+ }
@@ -13,6 +13,7 @@ import * as path from 'path';
13
13
  import { createHash } from 'crypto';
14
14
  import { featureFilesFor, reportSlug } from './unit-paths';
15
15
  import { readTextFile } from './read-text';
16
+ import { vpCodeOf } from './parse';
16
17
 
17
18
  export interface SpecSection { name: string; hash: string }
18
19
  export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
@@ -90,8 +91,7 @@ function parseFeatureSections(featurePath: string): { scenario: string; vpCode?:
90
91
  const s = line.match(/^Scenario:\s*(.*)$/);
91
92
  if (s) {
92
93
  const name = s[1].trim();
93
- const code = name.match(/\bVP-[A-Z]+-\d+/i)?.[0];
94
- out.push({ scenario: name, vpCode: code, section });
94
+ out.push({ scenario: name, vpCode: vpCodeOf(name).vpCode, section });
95
95
  }
96
96
  }
97
97
  return out;
@@ -16,9 +16,12 @@
16
16
  */
17
17
  import * as fs from 'fs';
18
18
  import * as path from 'path';
19
+ import { parse as parseYaml } from 'yaml';
19
20
  import { reportSlug, featureFilesFor } from './unit-paths';
20
21
  import { readTextFile } from './read-text';
21
22
  import { checkAssetsDrift } from '../orchestrator/assets-drift';
23
+ import { viewpointFileIsAiDraft } from './viewpoint-draft-header';
24
+ import { readViewpointDeclaration } from './viewpoint-baseline';
22
25
 
23
26
  export interface NextStep {
24
27
  /** The command to run, as the operator would type it. */
@@ -40,6 +43,8 @@ export interface UnitState {
40
43
  kind: 'screen' | 'flow' | 'api';
41
44
  hasSpec: boolean;
42
45
  hasViewpoint: boolean;
46
+ /** test-viewpoint.md carries `drafted-by: ai` — written by the AI, not yet accepted by a QA. */
47
+ viewpointIsAiDraft?: boolean;
43
48
  hasContract: boolean;
44
49
  hasFlowInventory: boolean;
45
50
  scenarioCount: number;
@@ -87,6 +92,29 @@ function countScenarios(unitDir: string, unit: string): number {
87
92
  * Everything the resolver needs, read from disk. `unitId` is the catalog id
88
93
  * (`<screen>` · `flows/<f>` · `api/<a>`); `unitDir` is its directory.
89
94
  */
95
+ const RESULT_FILE = /-test-result(?:\.[^.]+)*\.json$/;
96
+
97
+ /**
98
+ * Does the selector text resolve at least one element? A scaffold resolves none: the web one holds
99
+ * only the `page` route, the mobile one only header comments. Matching the WORD "placeholder" was
100
+ * wrong both ways (#868): it flagged a real file whose key names an input's placeholder text, and
101
+ * missed both scaffolds, which never contain it.
102
+ */
103
+ function hasResolvedSelector(selectorText: string): boolean {
104
+ let doc: unknown;
105
+ try { doc = parseYaml(selectorText); } catch { return selectorText.trim().length >= 40; }
106
+ if (!doc || typeof doc !== 'object') return false;
107
+ // A real cell, not a marker left to be filled in. Numbers count (an id or index can be numeric).
108
+ const real = (v: unknown): boolean => (typeof v === 'string' || typeof v === 'number')
109
+ && String(v).trim() !== '' && !/^(?:todo|tbd|placeholder|replace me)$/i.test(String(v).trim());
110
+ return Object.values(doc as Record<string, unknown>).some((e) => {
111
+ if (!e || typeof e !== 'object') return false;
112
+ const { type, value, name, locator } = e as { type?: unknown; value?: unknown; name?: unknown; locator?: unknown };
113
+ // Every SelectorEntry shape: a CSS `locator`, or a non-page `type` with a value or a role name.
114
+ return real(locator) || (typeof type === 'string' && type !== 'page' && (real(value) || real(name)));
115
+ });
116
+ }
117
+
90
118
  export function readUnitState(
91
119
  projectRoot: string, unitDir: string, unit: string, kind: UnitState['kind'],
92
120
  ): UnitState {
@@ -101,7 +129,7 @@ export function readUnitState(
101
129
  // `readTextFile` throws on a missing file, and every path here is legitimately absent at some
102
130
  // point in a unit's life — the resolver exists to report exactly that, so it must not crash on it.
103
131
  const read = (p: string): string => (fs.existsSync(p) ? readTextFile(p) : '');
104
- const selectorText = selectorFiles.map((f) => read(path.join(selectorsDir, f))).join('\n');
132
+ const selectorTexts = selectorFiles.map((f) => read(path.join(selectorsDir, f)));
105
133
  const contractPath = path.join(req, 'flow-contract.yaml');
106
134
  const contractText = read(contractPath);
107
135
 
@@ -116,7 +144,9 @@ export function readUnitState(
116
144
  });
117
145
  const hasResults = specDirs.some((d) => {
118
146
  try {
119
- return fs.readdirSync(d).some((f) => f.endsWith('-test-result.json'));
147
+ // `<f>-test-result[.<os>][.<env>].json` — a mobile run writes the OS into the name, an env
148
+ // run (SUNGEN_ENV=staging) the env; both are runs (#868).
149
+ return fs.readdirSync(d).some((f) => RESULT_FILE.test(f));
120
150
  } catch { return false; }
121
151
  });
122
152
  let hasDeliverable = false;
@@ -131,6 +161,9 @@ export function readUnitState(
131
161
  unit, kind,
132
162
  hasSpec: exists(path.join(req, 'spec.md')),
133
163
  hasViewpoint: exists(path.join(req, 'test-viewpoint.md')),
164
+ // An id-less draft has nothing to accept: only the missing-yardstick step speaks for it.
165
+ viewpointIsAiDraft: viewpointFileIsAiDraft(path.join(req, 'test-viewpoint.md'))
166
+ && readViewpointDeclaration(path.join(req, 'test-viewpoint.md')) !== null,
134
167
  hasContract: exists(contractPath),
135
168
  // A contract WITHOUT `flows:` can only be scored per phase, which is a materially weaker
136
169
  // claim — so it is part of the state, not a detail.
@@ -139,8 +172,7 @@ export function readUnitState(
139
172
  hasSelectors: selectorFiles.length > 0,
140
173
  // A scaffolded selector file exists but resolves nothing; treating it as "done" is what
141
174
  // makes a run look further along than it is.
142
- selectorsArePlaceholder: selectorFiles.length > 0
143
- && (/TODO|PLACEHOLDER|REPLACE ME/i.test(selectorText) || selectorText.trim().length < 40),
175
+ selectorsArePlaceholder: selectorFiles.length > 0 && !selectorTexts.some(hasResolvedSelector),
144
176
  compiled, hasResults, hasDeliverable,
145
177
  assetsDrift: (() => {
146
178
  const d = checkAssetsDrift(projectRoot);
@@ -170,6 +202,7 @@ const REPAIRABLE = [
170
202
  'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'SPEC-CLAUSE-UNCOVERED',
171
203
  'TRIGGER-UNCOVERED', 'PERMISSION-PAIR-MISSING', 'VISIBILITY-PAIR-MISSING',
172
204
  'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
205
+ 'UI-INVENTORY-MISSING', 'UI-INVENTORY-INCOMPLETE', 'UI-ITEM-UNCOVERED',
173
206
  ];
174
207
 
175
208
  /**
@@ -219,6 +252,15 @@ export function deriveSteps(s: UnitState): NextStep[] {
219
252
  });
220
253
  return out;
221
254
  }
255
+ // Before any generation: the AI drafts the viewpoint and audits it BEFORE it writes scenarios, so
256
+ // this is exactly where a draft would otherwise be accepted by the process that wrote it.
257
+ if (s.viewpointIsAiDraft) {
258
+ out.push({
259
+ kind: 'decide',
260
+ command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
261
+ because: 'test-viewpoint.md is marked `drafted-by: ai` — nobody has reviewed it, so the ledger and traceability axes are held as unverified. A QA reviews the claims (adds what matters, deletes what does not), then accepts; the command records the baseline and removes the marker. A decision, not a step: an agent presents it and does not run it',
262
+ });
263
+ }
222
264
  if (s.scenarioCount === 0) {
223
265
  out.push({ kind: 'do', command: design, because: 'no scenarios exist yet' });
224
266
  return out;
@@ -234,7 +276,7 @@ export function deriveSteps(s: UnitState): NextStep[] {
234
276
  out.push({ kind: 'do', command: `sungen audit --screen ${s.unit}`, because: 'the suite has never been measured' });
235
277
  return out;
236
278
  }
237
- if (s.audit.viewpointBaselineStatus === 'changed') {
279
+ if (s.audit.viewpointBaselineStatus === 'changed' && !s.viewpointIsAiDraft) { // the draft's own decide step is already above
238
280
  out.push({
239
281
  kind: 'decide',
240
282
  command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
@@ -7,7 +7,8 @@
7
7
  */
8
8
  import * as fs from 'fs';
9
9
  import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
10
- import { readTextFile } from './read-text';
10
+ import { readTextFile, readViewpointText } from './read-text';
11
+ import { parsePriority, toEntryPriority } from './viewpoint-priority';
11
12
  import { featureFilesFor } from './unit-paths';
12
13
 
13
14
  export type Priority = 'high' | 'normal' | 'low' | 'unknown';
@@ -15,15 +16,57 @@ export type Priority = 'high' | 'normal' | 'low' | 'unknown';
15
16
  export interface ViewpointEntry {
16
17
  id: string; // e.g. VP-DATA-CONSISTENCY
17
18
  priority: 'High' | 'Medium' | 'Low' | 'Unknown';
19
+ /** The declared priority word as written (`critical` · `high` · `medium` · `med` · `normal` · `low` ·
20
+ * `deferred` · `skip`, which also stands for skipped / n/a / none / not applicable), kept because `priority` folds several of them together and the
21
+ * PRIORITY-CONTRADICTED check needs them apart. Absent when no priority was declared. */
22
+ priorityWord?: string;
18
23
  reason: string;
19
24
  group?: 'Required' | 'Recommended' | 'Optional';
20
25
  }
21
26
 
27
+ /**
28
+ * Whole SECTIONS that declare something ABOUT the checklist rather than listing claims or
29
+ * viewpoints — known issues, spec gaps, traceability indexes, references and appendices, changelogs. Shared by the atomic
30
+ * ledger (their rows are not claims to prove, #630) and the viewpoint-overview parser (an id
31
+ * listed there is a reference, not a declaration, #833). `UI Patterns Identified` is the author's
32
+ * pick of WHICH sungen-viewpoint patterns apply — a `Skip` row there is not a claim to cover (#841).
33
+ * A `How to run` / `Procedure` section — that exact heading, optionally naming one id — is a manual
34
+ * check's steps, not claims of its own (#855); `How to test the login flow` stays a normal section.
35
+ */
36
+ export const NON_DECLARATION_SECTION = /^(?:(?:ui patterns?(?: identified)?|(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?)(?:\s+(?:vp|fl)-[^\s:]+)?)\s*:?$|(?:known issues?|open questions?|spec gaps?|appendi(?:x|ces)|phụ lục|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b)/i;
37
+
38
+ /**
39
+ * Every word a viewpoint row may declare as its priority — ONE list for the overview parser, the
40
+ * atomic ledger and the integrity checks, which each carried their own copy and all missed the
41
+ * `Skip` that the `sungen add` / `add-flow` scaffolds themselves suggest (#853). `deferred` and the
42
+ * not-applicable words are declarations as well: such a row is not a claim to cover.
43
+ */
44
+ export const PRIORITY_WORDS = 'critical|high|medium|med|normal|low|deferred|skip(?:ped)?|n\\/a|none|not applicable';
45
+ /** "Declared not applicable" — folded to `skip` so every consumer checks one word. */
46
+ const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/i;
47
+
48
+ // The whole cell must BE the word (emphasis aside), as the old `| VP-… | high |` scan required:
49
+ // "Low risk" or "Not high" declares no priority.
50
+ const PRIORITY_WORD = new RegExp(`^[\\s*_\`]*(${PRIORITY_WORDS})[\\s*_\`.]*$`, 'i');
51
+ /** Header names of a priority column, in the languages viewpoint files are written in. */
52
+ const PRIORITY_HEADER = /^(?:priority|pri|level|severity|優先度?|độ ưu tiên|ưu tiên)(?:\s|\(|$)/i;
53
+
54
+ /** A priority cell → the folded level plus the word as written. */
55
+ function priorityOf(cell: string): { priority: ViewpointEntry['priority']; priorityWord?: string } {
56
+ const raw = cell.match(PRIORITY_WORD)?.[1].toLowerCase();
57
+ const word = raw && NOT_APPLICABLE.test(raw) ? 'skip' : raw;
58
+ const priority = toEntryPriority(parsePriority(cell));
59
+ return word ? { priority, priorityWord: word } : { priority };
60
+ }
61
+
22
62
  export interface ScenarioInfo {
23
63
  name: string;
24
64
  vpCode?: string; // VP-CART-001 style code in the scenario title
25
65
  category?: string; // CART, LIST, VAL, ... (from the code)
26
66
  priority: Priority;
67
+ /** The priority tag as written, lower-cased, without `@` — keeps `critical` / `medium` / `med`, which
68
+ * `priority` has no slot for, so PRIORITY-CONTRADICTED can fold it like the declared word (#861). */
69
+ priorityTag?: string;
27
70
  manual: boolean;
28
71
  thenCount: number;
29
72
  hasDataAssertion: boolean; // a Then asserts {{data}} / contains / match data
@@ -39,8 +82,10 @@ export interface ScenarioInfo {
39
82
  exploration?: boolean; // @exploration — a probe outside the official suite, exempt from source tracing
40
83
  deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
41
84
  ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
85
+ featureManual?: boolean; // its Feature is tagged @manual — codegen skips the whole file (#835)
42
86
  /** Ordered steps with their resolved bucket (And/But inherit) — flow handoff analysis (#569). */
43
- steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string }>;
87
+ /** `arg` = the step's data table, normalised (#817) — absent when the step has none. */
88
+ steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }>;
44
89
  /**
45
90
  * The `.feature` this scenario was read from. A unit may hold several (`add --feature`), and
46
91
  * `scenarioIdReuse` scopes its uniqueness check to ONE file — so the sensor needs to know
@@ -106,7 +151,7 @@ function headingParts(line: string): { level: number; text: string } | null {
106
151
 
107
152
  export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
108
153
  if (!fs.existsSync(filePath)) return [];
109
- const text = readTextFile(filePath);
154
+ const text = readViewpointText(filePath);
110
155
  const lines = text.split('\n');
111
156
 
112
157
  const entries = new Map<string, ViewpointEntry>();
@@ -125,8 +170,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
125
170
  // and an id read with its emphasis markers still attached matches no scenario tag.
126
171
  const id = cells[0].replace(/[*`]/g, '').trim();
127
172
  if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
128
- const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
129
- entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
173
+ entries.set(id.toUpperCase(), { id: id.toUpperCase(), ...priorityOf(cells[1]), reason: cells[2] });
130
174
  }
131
175
  }
132
176
  }
@@ -179,6 +223,52 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
179
223
  }
180
224
  }
181
225
 
226
+ // 3) ID-first tables (#833): per-axis tables such as `| ID | Priority | Input | Expected |`.
227
+ // A row whose FIRST cell is a viewpoint id declares that id — the same rows the
228
+ // PRIORITY-CONTRADICTED check always read, so the two readers now agree. Kept narrow on purpose,
229
+ // because a viewpoint file also holds tables that only MENTION ids:
230
+ // - the id must be a `VP-`/`FL-` id, or belong to a family this file already declared above
231
+ // (`MS-HP` in Priority Viewpoints → `MS-HP-001`); `TC-01`, `FR-001`, `ISO-27001` never are;
232
+ // - reference sections (Known Issues, Traceability…) are skipped WITH their subsections, the
233
+ // way the ledger inherits the exclusion (#657), and fenced blocks are skipped;
234
+ // - the header is the row directly above a `|---|` separator (structure, not guessed words);
235
+ // its Priority column (any position, any alias) gives the priority, else column 2 does, as
236
+ // the old scan read it;
237
+ // - `reason` stays empty: viewpoint reasons feed page-type detection, and these rows were never
238
+ // entries before — the page type of an untouched file must not move.
239
+ const declaredFamilies = [...entries.keys()];
240
+ const familyOk = (id: string): boolean =>
241
+ /^(?:VP|FL)-/i.test(id) || declaredFamilies.some((f) => id.startsWith(`${f}-`) || id === f);
242
+ const isSeparator = (l: string): boolean => /^\|[\s|:-]+\|?$/.test(l) && l.includes('-');
243
+ let excludedAt: number | null = null;
244
+ let inFence = false;
245
+ let priorityCol = 1;
246
+ for (let i = 0; i < lines.length; i++) {
247
+ const line = lines[i].trim();
248
+ if (/^(```|~~~)/.test(line)) { inFence = !inFence; continue; }
249
+ if (inFence) continue;
250
+ const head = headingParts(line);
251
+ if (head) {
252
+ if (excludedAt !== null && head.level > excludedAt) continue; // subsection inherits
253
+ const text = head.text.replace(/[*`]/g, '').trim();
254
+ excludedAt = /^Priority Viewpoints/i.test(text) || NON_DECLARATION_SECTION.test(text) ? head.level : null;
255
+ continue;
256
+ }
257
+ if (excludedAt !== null || !line.startsWith('|') || isSeparator(line)) continue;
258
+ const cells = line.split('|').map((c) => c.trim()).filter((_, k, a) => k > 0 && k < a.length - 1);
259
+ if (isSeparator((lines[i + 1] ?? '').trim())) { // header row
260
+ const col = cells.findIndex((c) => PRIORITY_HEADER.test(c.replace(/[*`]/g, '').trim()));
261
+ priorityCol = col >= 0 ? col : 1;
262
+ continue;
263
+ }
264
+ const id = (cells[0] ?? '').replace(/[*`]/g, '').trim().toUpperCase();
265
+ if (!isViewpointId(id) || !familyOk(id)) continue;
266
+ const declared = priorityOf(cells[priorityCol] ?? '');
267
+ const existing = entries.get(id);
268
+ if (!existing) entries.set(id, { id, ...declared, reason: '' });
269
+ else if (!existing.priorityWord && declared.priorityWord) existing.priorityWord = declared.priorityWord;
270
+ }
271
+
182
272
  return [...entries.values()];
183
273
  }
184
274
 
@@ -199,6 +289,25 @@ function withBackground(sc: ParsedScenario, background?: ParsedScenario): Parsed
199
289
  return { ...sc, steps: [...bg, ...((sc.steps as ParsedStep[]) || [])] } as ParsedScenario;
200
290
  }
201
291
 
292
+ /**
293
+ * The `VP-<CATEGORY>-<n>` / `FL-<PHASE>-<n>` code in a scenario title, and its category — the ONE
294
+ * reading of it; the manifest kept an older copy of this regex and recorded no code for
295
+ * VP-I18N-001, VP-LIST-DISPLAY-01, VP-AF02-01 or any FL- id (#889).
296
+ *
297
+ * Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
298
+ * compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
299
+ * not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
300
+ * Flows use journey-phase ids (FL-HP-001 / FL-ER-002) — the VP- anchor rejected them, so every
301
+ * flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
302
+ * Segments may carry digits: the use-case framework numbers its flows (VP-AF02-01,
303
+ * VP-EF11-01 — one id per Alternate/Exception Flow), and `[A-Z]+` also silently truncated
304
+ * VP-I18N-001 to no category at all (the `18` broke the class mid-segment).
305
+ */
306
+ export function vpCodeOf(title: string): { vpCode?: string; category?: string } {
307
+ const m = title.match(/\b(?:VP|FL)-([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-\d+/i);
308
+ return m ? { vpCode: m[0].toUpperCase(), category: m[1].toUpperCase() } : {};
309
+ }
310
+
202
311
  function classifyScenario(sc: ParsedScenario): ScenarioInfo {
203
312
  const tags = sc.tags || [];
204
313
  const deferredToFlow = tags.includes('@deferred:flow');
@@ -223,18 +332,11 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
223
332
  const requiresCaps = tags.filter((t) => /^@requires:/i.test(t)).map((t) => t.slice('@requires:'.length).trim().toLowerCase()).filter(Boolean);
224
333
  let priority: Priority = 'unknown';
225
334
  for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
335
+ // Deliberately wider than `priority` (every priority word, any case): only PRIORITY-CONTRADICTED reads
336
+ // it, so `@critical` / `@High` are judged there without moving what select-by-ids or the score see.
337
+ const priorityTag = [...tags].reverse().find((t) => /^@(?:critical|high|medium|med|normal|low)$/i.test(t))?.slice(1).toLowerCase();
226
338
 
227
- // Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
228
- // compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
229
- // not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
230
- // Flows use journey-phase ids (FL-HP-001 / FL-ER-002) — the VP- anchor rejected them, so every
231
- // flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
232
- // Segments may carry digits: the use-case framework numbers its flows (VP-AF02-01,
233
- // VP-EF11-01 — one id per Alternate/Exception Flow), and `[A-Z]+` also silently truncated
234
- // VP-I18N-001 to no category at all (the `18` broke the class mid-segment).
235
- const codeMatch = sc.name.match(/\b(?:VP|FL)-([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-\d+/i);
236
- const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
237
- const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
339
+ const { vpCode, category } = vpCodeOf(sc.name);
238
340
  // Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
239
341
  const leadMatch = sc.name.match(/^\s*([A-Za-z][A-Za-z0-9.-]*)/);
240
342
  const vpId = leadMatch && isIdLike(leadMatch[1]) ? leadMatch[1].toUpperCase() : undefined;
@@ -246,22 +348,20 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
246
348
  const skeletonParts: string[] = [];
247
349
  const textParts: string[] = [sc.name];
248
350
  const stepTextParts: string[] = [];
249
- const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string }> = [];
351
+ const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }> = [];
250
352
 
251
353
  for (const step of sc.steps as ParsedStep[]) {
252
354
  const kw = step.keyword.trim();
253
355
  if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
254
356
  textParts.push(step.text);
255
357
  stepTextParts.push(step.text);
256
- orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text });
257
- // normalized skeleton: keep [refs] (distinct targets = distinct tests),
258
- // but neutralize {{vars}} and quoted values so EP/data families collapse.
259
- const skel = step.text
260
- .replace(/\{\{[^}]*\}\}/g, '{}')
261
- .replace(/"[^"]*"/g, '""')
262
- .replace(/\s+/g, ' ')
263
- .trim()
264
- .toLowerCase();
358
+ // The data table is part of what a step says (#817): `User see tab order:` with [FieldA]→[Cancel]
359
+ // and with [Save]→[Close] are different tests, and comparing step text alone called them exact
360
+ // duplicates and one oracle asserted N times. Kept OUT of `text`, which the claim/keyword sensors
361
+ // read as prose.
362
+ const arg = tableArg(step);
363
+ orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text, ...(arg ? { arg } : {}) });
364
+ const skel = skeletonOf(step.text) + (arg ? ` ${skeletonOf(arg)}` : '');
265
365
  skeletonParts.push(`${kw === 'And' || kw === 'But' ? last : kw}:${skel}`);
266
366
 
267
367
  if (last === 'Then') {
@@ -286,6 +386,7 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
286
386
  vpCode,
287
387
  category,
288
388
  priority,
389
+ priorityTag,
289
390
  manual,
290
391
  thenCount,
291
392
  hasDataAssertion: hasData,
@@ -309,9 +410,11 @@ export function loadScenarios(featurePath: string): ScenarioInfo[] {
309
410
  if (!fs.existsSync(featurePath)) return [];
310
411
  const parser = new GherkinParser();
311
412
  const feature = parser.parseFeatureFile(featurePath);
413
+ // Kept apart from `manual` so the audit's accounting is unchanged; only `select` reads it.
414
+ const featureManual = (feature.tags || []).some((t: string) => /^@manual\b/i.test(t)) || undefined;
312
415
  return (feature.scenarios || [])
313
416
  .filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
314
- .map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath }));
417
+ .map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath, featureManual }));
315
418
  }
316
419
 
317
420
  /**
@@ -334,3 +437,24 @@ export function readUnitFeatureText(unitDir: string, unit: string): string {
334
437
  .map((p) => readTextFile(p))
335
438
  .join('\n');
336
439
  }
440
+
441
+ /**
442
+ * Normalised skeleton of step text: keep [refs] (distinct targets = distinct tests), but neutralise
443
+ * {{vars}} and quoted values so EP/data families collapse.
444
+ */
445
+ function skeletonOf(text: string): string {
446
+ return text
447
+ .replace(/\{\{[^}]*\}\}/g, '{}')
448
+ .replace(/"[^"]*"/g, '""')
449
+ .replace(/\s+/g, ' ')
450
+ .trim()
451
+ .toLowerCase();
452
+ }
453
+
454
+ /** A step's data table as one comparable string — `<h1|h2 / c1|c2 / …>` — or undefined. */
455
+ function tableArg(step: ParsedStep): string | undefined {
456
+ const t = step.dataTable;
457
+ if (!t || (!t.headers.length && !t.rows.length)) return undefined;
458
+ const rows = [t.headers, ...t.rows.map((r) => r.cells)].map((cells) => cells.map((c) => c.trim()).join('|'));
459
+ return `<${rows.join(' / ')}>`;
460
+ }
@@ -8,6 +8,7 @@ import * as path from 'path';
8
8
  import { keywordHit } from './keyword-match';
9
9
  import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
10
10
  import { parseManualComments } from '../exporters/scenario-merger';
11
+ import { commentLinesOf, isPresencePlaceholderHybrid, keywordStepsOf } from '../exporters/manual-hybrid-oracle';
11
12
  import { readTextFile } from './read-text';
12
13
 
13
14
  // ---------- #2 Downstream-scope ----------
@@ -220,14 +221,15 @@ export function manualOracle(featureText: string): ManualOracleResult {
220
221
  // Observable:/Oracle:/Expected: line (parseManualComments — the SAME parser the
221
222
  // delivery exporter uses, so this gate can't drift from the exported cells).
222
223
  // Prose paragraphs / rationale-only comments export EMPTY Steps + Expected.
224
+ // • HAS a Then whose every assertion is a bare presence placeholder, with the real oracle
225
+ // in the comments (#810) — the same hybrid wearing a Then (manual-hybrid-oracle.ts, the
226
+ // SAME predicate as compile-time SG-W008).
223
227
  const hasGherkinSteps = /^\s*(Given|When|Then)\s/m.test(b);
228
+ const commentLines = commentLinesOf(b);
224
229
  let bad: boolean;
225
230
  if (hasGherkinSteps) {
226
- bad = !/^\s*Then\s/m.test(b);
231
+ bad = !/^\s*Then\s/m.test(b) || isPresencePlaceholderHybrid(keywordStepsOf(b), commentLines);
227
232
  } else {
228
- const commentLines = b.split('\n')
229
- .filter((l) => /^\s*#/.test(l))
230
- .map((l) => l.replace(/^\s*#\s?/, ''));
231
233
  const parsed = parseManualComments(commentLines);
232
234
  bad = !parsed.steps.length || !parsed.expected.length;
233
235
  }
@@ -32,3 +32,28 @@ export function readTextFile(filePath: string): string {
32
32
  export function readTextLines(filePath: string): string[] {
33
33
  return readTextFile(filePath).split('\n');
34
34
  }
35
+
36
+ /**
37
+ * Drop HTML comments (`<!-- … -->`, multi-line too) — they are author notes and scaffold samples,
38
+ * never content. Newlines inside a comment are kept so every later line keeps its position (#841).
39
+ *
40
+ * Only a CLOSED comment is dropped. An unclosed `<!--` (a forgotten `-->`) stays text: dropping it
41
+ * to the end of the file would silently delete every declaration below it. A fenced code block is
42
+ * content, and so is a `<!--` written inside backticks: both are how a document SHOWS the syntax.
43
+ */
44
+ export function stripHtmlComments(text: string): string {
45
+ // A comment body never holds another `<!--`: an opener with no `-->` before the next opener is
46
+ // the unclosed one, and must not reach a LATER comment's closer and eat everything between.
47
+ const blank = (s: string): string => s.replace(/(?<!`)<!--(?:(?!<!--)[\s\S])*?-->/g, (m) => m.replace(/[^\n]/g, ''));
48
+ // split() with a capture group keeps the fences at the odd indexes.
49
+ return text.split(/(^```[^\n]*\n[\s\S]*?^```[^\n]*$)/m).map((part, i) => (i % 2 ? part : blank(part))).join('');
50
+ }
51
+
52
+ /**
53
+ * `test-viewpoint.md` as every harness sensor must read it: normalised, comments removed, and
54
+ * '' when the unit has none. `sungen add` scaffolds its samples inside `<!-- -->`; read raw, an
55
+ * untouched scaffold declared VP-UI/VP-VAL/VP-LOGIC/VP-SEC and carried 12 phantom claims (#841).
56
+ */
57
+ export function readViewpointText(filePath: string): string {
58
+ return fs.existsSync(filePath) ? stripHtmlComments(readTextFile(filePath)) : '';
59
+ }
@@ -107,6 +107,12 @@ const ASSERTION_STEP_COMMENT = /^\s*\/\/\s*Assert\b/;
107
107
  // but as a bare substring it would also exempt a genuinely hollow step whose test-data or step
108
108
  // text happened to contain it.
109
109
  const NOOP_STEP_MARKER = /^\s*\/\/\s*App launched via wdio capabilities/;
110
+ // The same kind of step on the web adapter, where the explanation IS the step comment: the
111
+ // step-mapper compiles `Given User is on [Edit] dialog` (and its exit) to `// Enter dialog scope:
112
+ // Edit` and no code, because it only re-roots the steps after it — the next assertion runs as
113
+ // `page.getByRole('dialog', …).getByRole(…)`. A field unit reported 13 BYPASS, every one of them
114
+ // this comment, and exited 2 on a correct suite (#881). Anchored to the whole marker line.
115
+ const SCOPE_STEP_MARKER = /^\s*\/\/\s*(?:Enter|Exit) dialog scope(?::\s*.*)?$/;
110
116
 
111
117
  /** A spec comment line reduced to its text, for comparison against a compiler step comment. */
112
118
  const commentText = (line: string): string => line.trim().replace(/^\/\/\s?/, '').trim();
@@ -241,7 +247,7 @@ export function analyzeFaithfulness(
241
247
  const hasCode = region.some((l) => !isComment(l) && CODE_LINE.test(l));
242
248
  // A documented no-op emits its explanation INTO the step's region, so read the region
243
249
  // (not just the marker line) for it — see NOOP_STEP_MARKER.
244
- if (hasCode || region.some((l) => NOOP_STEP_MARKER.test(l))) continue;
250
+ if (hasCode || region.some((l) => NOOP_STEP_MARKER.test(l)) || SCOPE_STEP_MARKER.test(body[markers[s]])) continue;
245
251
  if (tailAmbiguous && s === markers.length - 1) continue;
246
252
  hollowSteps.push({ test: title, step: body[markers[s]].trim().slice(0, 70) });
247
253
  }