@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,164 @@
1
+ /**
2
+ * #811 — the design inventory: is every item the DESIGN shows accounted for by the suite?
3
+ *
4
+ * Design sources in `requirements/ui/` (images, `figma-meta.md`) and `requirements/spec_figma.md`
5
+ * were read "for visual context" and never turned into a checkable list, so a column that exists
6
+ * only on the design — or contradicts the spec — was missed by the author AND the reviewer, and a
7
+ * deliberate deferral looked exactly like an oversight.
8
+ *
9
+ * create-test now writes `requirements/ui-inventory.md`: one `UI-NNN` row per component / column /
10
+ * button / state / annotation, with its source (`spec+design` | `design-only` | `conflict`). An
11
+ * item is accounted for by a scenario tag (`@ui:UI-014`) or a reasoned deferral comment
12
+ * (`# UI-DEFERRED: UI-002..013 — <reason>`) — no other mention counts; ranges and `/`
13
+ * enumerations read the same way as spec ids (spec-coverage `citedIds`). `spec+design` items are already measured
14
+ * by spec-coverage, so only `design-only` / `conflict` items are reported uncovered.
15
+ *
16
+ * Advisory: findings never move the score or the gate.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { citedIds } from './spec-coverage';
21
+ import { readTextFile } from './read-text';
22
+
23
+ export type UiItemSource = 'spec+design' | 'design-only' | 'conflict';
24
+
25
+ export interface UiItem {
26
+ id: string;
27
+ item: string;
28
+ kind: string;
29
+ source: UiItemSource;
30
+ }
31
+
32
+ export interface UiInventoryResult {
33
+ /** Design sources the unit holds: image files, figma-meta.md, spec_figma.md. */
34
+ designSources: string[];
35
+ /** `requirements/ui-inventory.md` exists. */
36
+ hasInventory: boolean;
37
+ items: UiItem[];
38
+ /** `images read: a/b` from the inventory header — null when the line is absent. */
39
+ imagesRead: { read: number; total: number } | null;
40
+ /** Images on disk, for comparing against `imagesRead.total`. */
41
+ imagesOnDisk: number;
42
+ covered: string[];
43
+ deferred: string[];
44
+ /** design-only / conflict items neither covered nor deferred. */
45
+ uncovered: UiItem[];
46
+ }
47
+
48
+ const IMAGE = /\.(png|jpe?g|webp|gif)$/i;
49
+
50
+ /** Every design source in a unit's `requirements/` — what makes an inventory necessary. */
51
+ export function designSourcesOf(reqDir: string): string[] {
52
+ const out: string[] = [];
53
+ const uiDir = path.join(reqDir, 'ui');
54
+ try {
55
+ for (const f of fs.readdirSync(uiDir).sort()) {
56
+ if (IMAGE.test(f) || f === 'figma-meta.md') out.push(`ui/${f}`);
57
+ }
58
+ } catch { /* no ui/ dir */ }
59
+ if (fs.existsSync(path.join(reqDir, 'spec_figma.md'))) out.push('spec_figma.md');
60
+ return out;
61
+ }
62
+
63
+ /**
64
+ * The Source cell, read leniently ("design only", "Spec + Design", "conflict — …"). Anything else
65
+ * is NOT assumed to be spec-backed: an unreadable source is reported like a design-only item, so a
66
+ * typo can never make an item disappear from the check.
67
+ */
68
+ function sourceOf(cell: string): UiItemSource | null {
69
+ const c = cell.trim().toLowerCase();
70
+ if (/^design[\s_-]*only\b/.test(c)) return 'design-only';
71
+ if (/^conflict\b/.test(c)) return 'conflict';
72
+ if (/^spec\s*(?:\+|&|and)\s*design\b/.test(c)) return 'spec+design';
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Parse the inventory table: any markdown row whose first cell is a `UI-<n>` id. Leading/trailing
78
+ * pipes are optional (GFM), and the Source cell is FOUND rather than counted — a `|` written inside
79
+ * the Item cell shifts the columns, and a positional read then misfiled a design-only row.
80
+ */
81
+ export function parseUiInventory(text: string): { items: UiItem[]; imagesRead: UiInventoryResult['imagesRead'] } {
82
+ const items: UiItem[] = [];
83
+ for (const line of text.split('\n')) {
84
+ if (!line.includes('|')) continue;
85
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
86
+ if (!/^UI-\d+$/i.test(cells[0] ?? '')) continue;
87
+ // Source = the last cell that reads as one (Seen-in follows it; prose in Item never starts so).
88
+ let srcIdx = -1;
89
+ for (let i = cells.length - 1; i >= 2; i--) if (sourceOf(cells[i])) { srcIdx = i; break; }
90
+ const source: UiItemSource = srcIdx >= 0 ? sourceOf(cells[srcIdx])! : 'design-only';
91
+ const kindIdx = srcIdx >= 0 ? srcIdx - 1 : Math.min(2, cells.length - 1);
92
+ items.push({
93
+ id: cells[0].toUpperCase(),
94
+ item: cells.slice(1, Math.max(2, kindIdx)).join(' | '),
95
+ kind: cells[kindIdx] ?? '',
96
+ source,
97
+ });
98
+ }
99
+ const m = text.match(/images read:\s*(\d+)\s*\/\s*(\d+)/i);
100
+ return { items, imagesRead: m ? { read: Number(m[1]), total: Number(m[2]) } : null };
101
+ }
102
+
103
+ /**
104
+ * Ids cited on a `UI-DEFERRED` comment line — a deliberate, REASONED omission. A line that lists
105
+ * ids and nothing else is not a decision anyone can review, so it defers nothing.
106
+ */
107
+ function deferredIds(featureText: string): Set<string> {
108
+ const lines = featureText.split('\n').filter((l) => {
109
+ if (!/^\s*#\s*UI-DEFERRED\b/i.test(l)) return false;
110
+ const reason = l.replace(/^\s*#\s*UI-DEFERRED\b\s*:?/i, '').replace(/\bUI-\d+(?:\s*(?:\.\.|–|—|~|\/)\s*(?:UI-)?\d+)*/gi, '');
111
+ return /[\p{L}]{3,}/u.test(reason);
112
+ });
113
+ return citedIds(lines.join('\n'));
114
+ }
115
+
116
+ export function uiInventoryCoverage(reqDir: string, featureText: string): UiInventoryResult {
117
+ const designSources = designSourcesOf(reqDir);
118
+ const invPath = path.join(reqDir, 'ui-inventory.md');
119
+ const invText = fs.existsSync(invPath) ? readTextFile(invPath) : null;
120
+ const { items, imagesRead } = invText ? parseUiInventory(invText) : { items: [], imagesRead: null };
121
+ // Coverage is the `@ui:` TAG, nothing else: a `UI-001`-shaped string elsewhere (an `@spec:UI-001`
122
+ // requirement in a project that numbers its FRs that way, prose, an unreasoned deferral line)
123
+ // must not mark a design item as proven.
124
+ const cites = citedIds((featureText.match(/@ui:\S+/gi) ?? []).join(' '));
125
+ const deferSet = deferredIds(featureText);
126
+ const covered: string[] = [];
127
+ const deferred: string[] = [];
128
+ const uncovered: UiItem[] = [];
129
+ for (const it of items) {
130
+ if (deferSet.has(it.id)) deferred.push(it.id);
131
+ else if (cites.has(it.id)) covered.push(it.id);
132
+ else if (it.source !== 'spec+design') uncovered.push(it);
133
+ }
134
+ return {
135
+ designSources,
136
+ hasInventory: invText !== null,
137
+ items,
138
+ imagesRead,
139
+ imagesOnDisk: designSources.filter((s) => IMAGE.test(s)).length,
140
+ covered,
141
+ deferred,
142
+ uncovered,
143
+ };
144
+ }
145
+
146
+ /** Advisory findings for the audit report. */
147
+ export function uiInventoryFindings(r: UiInventoryResult): string[] {
148
+ const out: string[] = [];
149
+ if (r.designSources.length === 0) return out;
150
+ if (!r.hasInventory) {
151
+ out.push(`UI-INVENTORY-MISSING: the unit has design sources (${r.designSources.slice(0, 4).join(', ')}${r.designSources.length > 4 ? ', …' : ''}) but no \`requirements/ui-inventory.md\` — nothing shows the design was read item by item, so an item that exists only on the design (or contradicts the spec) is invisible to the gate and to review. Re-run /sungen:create-test so it writes the inventory, or write it by hand: one \`| UI-NNN | item | kind | spec+design / design-only / conflict |\` row per component, column, button, state and annotation.`);
152
+ return out;
153
+ }
154
+ if (r.imagesRead && r.imagesRead.read < Math.max(r.imagesRead.total, r.imagesOnDisk)) {
155
+ out.push(`UI-INVENTORY-INCOMPLETE: the inventory records "images read: ${r.imagesRead.read}/${r.imagesRead.total}" and the unit holds ${r.imagesOnDisk} image(s) — the unread design is not inventoried. Read the remaining images and add their items.`);
156
+ } else if (!r.imagesRead && r.imagesOnDisk > 0) {
157
+ out.push(`UI-INVENTORY-INCOMPLETE: the unit holds ${r.imagesOnDisk} image(s) but the inventory does not record "images read: N/N" — nothing shows every image was opened. Add the line after reading them all.`);
158
+ }
159
+ for (const it of r.uncovered.slice(0, 10)) {
160
+ out.push(`UI-ITEM-UNCOVERED: ${it.id} "${it.item}" (${it.kind}, ${it.source}) has no scenario and no deferral — ${it.source === 'conflict' ? 'the design and spec.md disagree on it; resolve which is right, then cover it' : 'it exists only on the design, outside spec.md'}. Cover it (\`@ui:${it.id}\` on the scenario that proves it), or record the decision: \`# UI-DEFERRED: ${it.id} — <reason>\`.`);
161
+ }
162
+ if (r.uncovered.length > 10) out.push(`UI-ITEM-UNCOVERED: … and ${r.uncovered.length - 10} more design-only/conflict item(s).`);
163
+ return out;
164
+ }
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { parseViewpointOverview } from './parse';
5
+ import { isAiDraft, removeDraftLine } from './viewpoint-draft-header';
5
6
 
6
7
  /**
7
8
  * Keep the yardstick from moving with the thing it measures.
@@ -89,7 +90,12 @@ function fingerprint(ids: string[]): string {
89
90
  */
90
91
  function fileFingerprint(viewpointPath: string): string {
91
92
  try {
92
- return crypto.createHash('sha1').update(fs.readFileSync(viewpointPath)).digest('hex').slice(0, 12);
93
+ const bytes = fs.readFileSync(viewpointPath);
94
+ // `drafted-by: ai` is provenance that accept REMOVES, so it is not part of what was reviewed:
95
+ // counted, accepting a draft would read as the declaration changing. Every other byte stays raw.
96
+ const text = bytes.toString('utf-8');
97
+ const hashed = isAiDraft(text) ? Buffer.from(removeDraftLine(text), 'utf-8') : bytes;
98
+ return crypto.createHash('sha1').update(hashed).digest('hex').slice(0, 12);
93
99
  } catch { return ''; }
94
100
  }
95
101
 
@@ -0,0 +1,55 @@
1
+ import * as fs from 'fs';
2
+ import { stripHtmlComments } from './read-text';
3
+ import { writeFileAtomic } from './write-atomic';
4
+
5
+ /**
6
+ * `drafted-by: ai` — provenance, not acceptance. The AI writes it as the first header line of a
7
+ * viewpoint it drafted; while it is there the file is an unreviewed draft: the audit records no
8
+ * baseline for it and holds the two axes measured against it. `--accept-viewpoint` removes it.
9
+ *
10
+ * Read by a line scan, not by the viewpoint parser: the key must be found in a file the parser
11
+ * judges nothing about, and in a QA-written file it simply is not there.
12
+ */
13
+ const DRAFT_KEY = /^\s*drafted-by:\s*ai\b/i;
14
+ /** The key is a header line: looking further down would read prose that merely mentions it. */
15
+ const HEADER_LINES = 15;
16
+
17
+ /** A line with its own terminator, so a removal keeps CRLF and a lone `\r` exactly as written. */
18
+ const withTerminator = (raw: string): string[] => raw.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) ?? [];
19
+
20
+ /** Index of the draft line in `raw`, or -1. Comments are blanked line-for-line, so indexes agree with the raw file. */
21
+ function draftLineIndex(raw: string): number {
22
+ const visible = stripHtmlComments(raw.replace(/\r\n?/g, '\n')).split('\n');
23
+ let seen = 0;
24
+ for (let i = 0; i < visible.length && seen < HEADER_LINES; i++) {
25
+ if (visible[i].trim() === '') continue;
26
+ if (DRAFT_KEY.test(visible[i])) return i;
27
+ seen++;
28
+ }
29
+ return -1;
30
+ }
31
+
32
+ export function isAiDraft(text: string): boolean {
33
+ return draftLineIndex(text) >= 0;
34
+ }
35
+
36
+ /** `raw` without the one draft line; every other byte is untouched. A file with no draft line comes back as is. */
37
+ export function removeDraftLine(raw: string): string {
38
+ const at = draftLineIndex(raw);
39
+ if (at < 0) return raw;
40
+ return withTerminator(raw).filter((_, i) => i !== at).join('');
41
+ }
42
+
43
+ export function viewpointFileIsAiDraft(viewpointPath: string): boolean {
44
+ try { return isAiDraft(fs.readFileSync(viewpointPath, 'utf-8')); } catch { return false; }
45
+ }
46
+
47
+ /** Remove the draft line from the file, atomically. True when a line was removed; throws a clear error when the write fails. */
48
+ export function removeDraftLineFromFile(viewpointPath: string): boolean {
49
+ const raw = fs.readFileSync(viewpointPath, 'utf-8');
50
+ if (!isAiDraft(raw)) return false;
51
+ try { writeFileAtomic(viewpointPath, removeDraftLine(raw)); } catch (err) {
52
+ throw new Error(`could not remove the \`drafted-by: ai\` line from ${viewpointPath} (${err instanceof Error ? err.message : err}) — the file is unchanged; remove the line by hand`);
53
+ }
54
+ return true;
55
+ }
@@ -9,9 +9,10 @@
9
9
  * gaps that inflate a "looks-covered" score; it does not fail the gate.
10
10
  */
11
11
  import * as fs from 'fs';
12
- import { ScenarioInfo } from './parse';
13
- import { readTextFile } from './read-text';
12
+ import { NON_DECLARATION_SECTION, PRIORITY_WORDS, ScenarioInfo } from './parse';
13
+ import { readViewpointText } from './read-text';
14
14
  import { keywordHit } from './keyword-match';
15
+ import { countedAssertions } from './counted-assertions';
15
16
 
16
17
  export interface LedgerItem { id?: string; text: string; covered: boolean }
17
18
 
@@ -57,6 +58,13 @@ function claimedCount(text: string): number | null {
57
58
  }
58
59
 
59
60
  const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.Title, VP7-002, MS-HP-001, TV-01
61
+ /**
62
+ * A bullet declares only the id it STARTS with (after a `[ ]` checkbox); one further along is a
63
+ * citation (`… (SG-01, …)`). `- **High** VP-…` or `- Email shown — VP-…` therefore carry no id.
64
+ */
65
+ const BULLET_ID = new RegExp(`^[\\s(#]*(?:\\[[ xX]\\]\\s*)?\\[?${ID_RE.source}`);
66
+ /** A table cell that is an id and nothing else. */
67
+ const WHOLE_ID = new RegExp(`^${ID_RE.source}$`);
60
68
 
61
69
  /**
62
70
  * Lines that DECLARE something about the checklist rather than being a checklist item —
@@ -69,7 +77,10 @@ const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.T
69
77
  const PLACEHOLDER_ITEM = /^(none\b|n\/a\b|tbd\b|no known\b|nothing\b|do not invent\b|-+$)/i;
70
78
  /** A priority-DECLARATION row: `| VP-LOGIC | High | <reason prose> |`. The category id and its
71
79
  * priority are consumed by the traceability + balance axes; the reason is rationale. */
72
- const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|normal|low|deferred)\b/i;
80
+ // The word must be the WHOLE second cell: `skip` / `none` / `n/a` also open real claims
81
+ // ("Skip link moves focus to main", "None of the optional fields block submit") — and so did
82
+ // `high`/`low` ("High contrast mode …"), which the old word-boundary match swallowed too.
83
+ const PRIORITY_ROW = new RegExp(`^(?:VP|FL)[A-Z0-9._-]*\\s+—\\s+[*_\`]*(?:${PRIORITY_WORDS})[*_\`.]*(?=\\s+—|\\s*$)`, 'i');
73
84
  /**
74
85
  * Whole SECTIONS that declare something ABOUT the checklist rather than listing claims to prove.
75
86
  *
@@ -82,49 +93,121 @@ const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|nor
82
93
  * `Design Decisions` deliberately stays IN: "values are buffered between step 3 and 4, a reload
83
94
  * loses them" is a real claim, and it is what the continuity check reads.
84
95
  */
85
- const DECLARATION_SECTION = /^(?:known issues?|open questions?|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b/i;
96
+ const DECLARATION_SECTION = NON_DECLARATION_SECTION; // shared with the overview parser (#833)
97
+ /**
98
+ * A lead-in that introduces a manual check's PROCEDURE — `How to run VP-SEC-1:`, `Procedures:`,
99
+ * `Steps to reproduce:`. The list under it is how to EXECUTE a claim already declared above;
100
+ * counting each step as its own claim made gaps no scenario can close (8 of 13 surplus items on a
101
+ * field unit, #855).
102
+ *
103
+ * Deliberately narrow, because a false match HIDES claims: the lead may name one id and nothing
104
+ * else (`How to verify the following claims hold:` introduces claims, not steps), and a bare
105
+ * `Steps:` (a flow lists real step claims under it) and `Tester verifies:` (expected outcomes —
106
+ * exactly what the ledger counts) never qualify.
107
+ */
108
+ const PROCEDURE_LEAD = /^(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?|manual steps|steps to reproduce)(?:\s+(?:VP|FL)-[\w.-]+)?\s*:$/i;
109
+ const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+/;
86
110
  const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
87
111
 
112
+ /**
113
+ * `decision`: a `- D4: …` line under Design Decisions. It names the spec's intent so rows can cite
114
+ * it; its testable part is carried by a VP row (review check 8). No scenario ever carries `D4`, so
115
+ * as a ledger item it stayed MISSING for good — "not tested" decisions included (phase-7 D5). The
116
+ * ledger skips it; the continuity, source and integrity checks still read it. Only that section
117
+ * declares: a QA claim labelled `D8` elsewhere stays a claim, and an id-less decision bullet stays
118
+ * the prose claim it always was.
119
+ */
120
+ export interface ViewpointItem { id?: string; text: string; decision?: true }
121
+ const DECISION_ID = /^D\d+$/;
122
+ const DECISIONS_SECTION = /^(?:design decisions?|quyết định(?: thiết kế)?)\b/i;
123
+
88
124
  /** Extract atomic checklist items from a viewpoint file (format-tolerant). */
89
- export function parseViewpointItems(viewpointPath: string): { id?: string; text: string }[] {
125
+ export function parseViewpointItems(viewpointPath: string): ViewpointItem[] {
90
126
  if (!fs.existsSync(viewpointPath)) return [];
91
- const lines = readTextFile(viewpointPath).split('\n');
92
- const items: { id?: string; text: string }[] = [];
127
+ const lines = readViewpointText(viewpointPath).split('\n');
128
+ const items: ViewpointItem[] = [];
93
129
  let inFence = false;
94
130
  let inDeclarationSection = false;
95
131
  // The level an excluded section was opened at. A SUBSECTION inherits it: `### Optional`
96
132
  // nested under `## 8. Traceability Index` used to re-open the section and hand its rows
97
133
  // back as claims, because exclusion was recomputed from scratch at every heading.
98
134
  let excludedAt: number | null = null;
99
- for (const raw of lines) {
100
- const line = raw.trim();
135
+ // The level the Design Decisions section was opened at, while inside it (see `decision`).
136
+ let decisionsAt: number | null = null;
137
+ // Inside the list a PROCEDURE_LEAD introduced: the indent of the lead (-1 for a paragraph lead),
138
+ // whose list items — and their wrapped continuation lines — are steps. A heading, a table, a
139
+ // new lead, or (after a paragraph lead) a blank line once a step was read ends it: when in
140
+ // doubt a step stays a claim, because a swallowed claim is a gap nobody ever sees — so a LOOSE
141
+ // procedure (steps separated by blank lines) leaks its later steps as claims, by design.
142
+ // Only an INDENTED line continues a step (Markdown's continuation); unindented prose ends it.
143
+ let procIndent: number | null = null;
144
+ let procSteps = 0;
145
+ let prevBlank = false;
146
+ for (let i = 0; i < lines.length; i++) {
147
+ const line = lines[i].trim();
101
148
  if (line.startsWith('```')) { inFence = !inFence; continue; }
102
- if (inFence || !line) continue;
149
+ if (inFence) continue;
150
+ if (!line) { prevBlank = true; continue; }
151
+ const wasBlank = prevBlank;
152
+ prevBlank = false;
153
+ const isItem = LIST_ITEM.test(line);
154
+ const indent = lines[i].length - lines[i].trimStart().length;
155
+ if (PROCEDURE_LEAD.test(line.replace(LIST_ITEM, '').replace(/[*_`]/g, '').trim())) {
156
+ procIndent = isItem ? indent : -1;
157
+ procSteps = 0;
158
+ continue;
159
+ }
160
+ if (procIndent !== null) {
161
+ const listEnded = wasBlank && procIndent < 0 && procSteps > 0;
162
+ if (!listEnded && isItem && indent > procIndent) { procSteps++; continue; } // a step
163
+ if (!listEnded && !isItem && !wasBlank && procSteps > 0 && indent > Math.max(procIndent, 0)
164
+ && !/^[#|]/.test(line) && !line.endsWith(':')) continue; // a step that wraps
165
+ procIndent = null;
166
+ }
103
167
  const heading = line.match(/^(#{1,6})\s+(.*)$/);
104
168
  if (heading) {
105
169
  const level = heading[1].length;
106
170
  if (excludedAt !== null && level > excludedAt) continue; // still inside the excluded section
171
+ if (decisionsAt !== null && level > decisionsAt) continue; // a subsection of Design Decisions
107
172
  // A heading both ends the previous section and decides whether this one declares or claims.
108
173
  // Strip an ordinal prefix ("## 5. Known Issues", "## 8. Viewpoint Grouping") before
109
174
  // testing. The regex anchors at the start, so a NUMBERED heading matched nothing and
110
175
  // every excluded section leaked its rows back in as claims — a real viewpoint numbers
111
176
  // its sections, so in practice the exclusion list was doing nothing at all (#657).
112
- const headText = heading[2].replace(/[*`]/g, '').replace(/^\s*(?:\d+|[A-Z])\s*[.):]\s*/, '').trim();
177
+ // `Phần 4:` / `Part 4.` / `Mục 4.` number a section the same way.
178
+ const headText = heading[2].replace(/[*`]/g, '').replace(/^\s*(?:(?:[Pp]hần|[Pp]art|[Mm]ục|[Ss]ection)\s+)?(?:\d+|[A-Z])\s*[.):]\s*/, '').trim();
113
179
  inDeclarationSection = DECLARATION_SECTION.test(headText);
114
180
  excludedAt = inDeclarationSection ? level : null;
181
+ decisionsAt = DECISIONS_SECTION.test(headText) ? level : null;
115
182
  continue;
116
183
  }
117
184
  if (inDeclarationSection) continue;
118
185
  let text = '';
119
186
  let lead = '';
187
+ let declared: RegExpMatchArray | null = null;
120
188
  const bullet = line.match(/^(?:[-*+]|\d+[.)])\s+(.*)$/);
121
- if (bullet) { text = bullet[1]; lead = bullet[1]; }
189
+ if (bullet) {
190
+ text = bullet[1]; lead = bullet[1];
191
+ declared = lead.replace(/[*`]/g, '').match(BULLET_ID);
192
+ }
122
193
  else if (line.startsWith('|')) { // table data row
123
194
  if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
195
+ // The row right above a `|---|` separator IS the header, whatever its words (`Skill #`,
196
+ // `Case #`) — structure, not a guessed word list, so a data row such as `C#` or `Issue #`
197
+ // is never mistaken for one (#832). The word list below still covers a separator-less table.
198
+ const next = (lines[i + 1] ?? '').trim();
199
+ if (/^\|[\s|:-]+\|?$/.test(next) && next.includes('-')) continue; // header
124
200
  const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
125
201
  if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status|step|flow|ref|level|question|screen|actor|branches from|own steps|component|thành phần|trường|bước)$/i.test(cells[0] || '')) continue; // header
126
202
  text = cells.join(' — ');
127
- lead = cells[0] ?? '';
203
+ // The lead is the row's FIRST cell, empty or not: a continuation row (`| | | case | … see SG-02 |`)
204
+ // declares nothing. A later cell holding an id and nothing else still declares (`| x | VP-UI-001 |`),
205
+ // and so does a row number followed by a cell that starts with one (`| 1 | VP-UI-001 — … |`).
206
+ const raw = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.replace(/[*`]/g, '').trim());
207
+ lead = raw[0] ?? '';
208
+ declared = lead.match(ID_RE)
209
+ ?? (/^\d+\.?$/.test(lead) ? (raw[1] ?? '').match(BULLET_ID) : null)
210
+ ?? raw.slice(1).map((c) => c.match(WHOLE_ID)).find(Boolean) ?? null;
128
211
  } else continue;
129
212
  text = text.replace(/[*`]/g, '').trim();
130
213
  if (!text) continue;
@@ -139,15 +222,16 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
139
222
  // under a grouping section. It declares nothing; it points at things declared elsewhere.
140
223
  const leadIds = new Set((leadClean.match(new RegExp(ID_RE.source, 'g')) || []).filter((x) => /\d/.test(x)));
141
224
  if (leadIds.size > 1) continue;
142
- const idM = leadClean.match(ID_RE) ?? text.match(ID_RE);
143
- const id = idM && /\d/.test(idM[1]) ? idM[1] : undefined; // require a digit so prose words aren't IDs
225
+ const id = declared && /\d/.test(declared[1]) ? declared[1] : undefined; // require a digit so prose words aren't IDs
144
226
  // A row keyed by a bare GROUP prefix ("VP-LOGIC — transitions — High — why the flow exists")
145
227
  // declares a family and its priority, not a checklist item: nothing can "cover" it, and
146
228
  // demanding coverage sends the author looking for a test that was never meant to exist.
147
229
  if (/^VP-[A-Z]+\s*(?:—|-|\||$)/.test(leadClean) && !/\d/.test(leadClean)) continue;
148
230
  const words = (text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w));
149
231
  if (!id && words.length < 2) continue; // not substantive enough to track
150
- items.push({ id, text: text.slice(0, 100) });
232
+ items.push(decisionsAt !== null && id && DECISION_ID.test(id)
233
+ ? { id, text: text.slice(0, 100), decision: true }
234
+ : { id, text: text.slice(0, 100) });
151
235
  }
152
236
  return items;
153
237
  }
@@ -158,6 +242,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
158
242
  // (#622) — silently collapsing those would remove its input.
159
243
  const seen = new Set<string>();
160
244
  const items = parseViewpointItems(viewpointPath)
245
+ .filter((i) => !i.decision)
161
246
  .filter((i) => { const k = `${i.id ?? ''}|${i.text}`; if (seen.has(k)) return false; seen.add(k); return true; });
162
247
  if (!fs.existsSync(viewpointPath) || items.length === 0) {
163
248
  return { hasViewpoint: fs.existsSync(viewpointPath), total: 0, covered: 0, ratio: 1, missing: [], manualOnly: [], partial: [] };
@@ -191,8 +276,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
191
276
  const n = claimedCount(item.text);
192
277
  const auto = carriers.filter((s) => !s.manual);
193
278
  if (n !== null && auto.length > 0 && !auto.some((s) => /\bsee all\b/.test(s.stepsText))) {
194
- const asserted = Math.max(...auto.map((s) =>
195
- (s.stepsText.match(/\bsee \[[^\]]+\][^|]*?\b(?:with|contains) \{\{/g) ?? []).length));
279
+ const asserted = Math.max(...auto.map((s) => countedAssertions(s.stepsText)));
196
280
  if (asserted > 0 && asserted < n) partial.push({ id: item.id, text: item.text, claimed: n, asserted });
197
281
  }
198
282
  }
@@ -349,13 +433,16 @@ export function viewpointIntegrity(
349
433
  }
350
434
 
351
435
  // The Priority-Viewpoints table: `| <id> | <priority> | <reason> |`.
352
- // `readTextFile` throws on a missing file, and a unit may legitimately have no viewpoint yet.
353
- const vpText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
354
- const rows = [...vpText
355
- .matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)]
436
+ const vpText = readViewpointText(viewpointPath);
437
+ const words = [...vpText
438
+ .matchAll(new RegExp(`^\\|\\s*((?:VP|FL)[A-Z0-9._-]*)\\s*\\|\\s*(${PRIORITY_WORDS})\\s*\\|`, 'gim'))]
356
439
  .map((m) => m[2].toLowerCase());
357
- const levels = [...new Set(rows)];
358
- const uniformPriority = rows.length >= 3 && levels.length === 1 ? { levels, rows: rows.length } : null;
440
+ // Fold only synonyms (Medium / Med / Normal are one level). `critical` stays its own word: a
441
+ // Critical / High / High table is a ranking, and folding it into `high` made it read as uniform.
442
+ const level = (w: string): string => (/^(?:medium|med|normal)$/.test(w) ? 'normal' : w);
443
+ const uniformPriority = words.length >= 3 && new Set(words.map(level)).size === 1
444
+ ? { levels: [words[0]], rows: words.length } // the word QA wrote, not the folded key
445
+ : null;
359
446
 
360
447
  return { duplicateIds, danglingRefs, uniformPriority };
361
448
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Which create-test mode a unit's test-viewpoint.md calls for — the table in create-test.md,
3
+ * computed so the agent reads it from `sungen viewpoint scan` instead of inferring it.
4
+ *
5
+ * mode 2 (AI drafts / re-reviews its draft): file missing · untouched scaffold · first line
6
+ * `drafted-by: ai` · VP rows exist but none has a Kỳ vọng
7
+ * mode 1 (review only, never edit): anything else — a QA-written file, with or without VP ids
8
+ *
9
+ * "Untouched scaffold" is checked on RAW lines against the template the scaffold is written from:
10
+ * apart from the first `# ` line (the title), every non-blank line must be a template line —
11
+ * comments included, so a QA note inside `<!-- -->` or under its own `# ` heading makes the file
12
+ * the QA's (owner decision 2026-10-14: a QA file is never overwritten). Every doubt falls to
13
+ * mode 1: a scaffold from an older sungen (other template lines) or an unreadable template.
14
+ */
15
+ import { isAiDraft } from './viewpoint-draft-header';
16
+ import { viewpointScaffold } from '../orchestrator/viewpoint-scaffold';
17
+
18
+ export type ViewpointMode = 1 | 2;
19
+
20
+ /** Trimmed non-blank lines without the title (the first `# ` line), BOM and CRLF ignored. */
21
+ function bodyLines(text: string): string[] {
22
+ const lines = text.replace(/^/, '').replace(/\r\n?/g, '\n').split('\n').map((l) => l.trim()).filter(Boolean);
23
+ const title = lines.findIndex((l) => l.startsWith('# '));
24
+ if (title >= 0) lines.splice(title, 1);
25
+ return lines;
26
+ }
27
+
28
+ let templateLines: Set<string> | null = null;
29
+ function isUntouchedScaffold(raw: string): boolean {
30
+ if (!templateLines) {
31
+ try {
32
+ templateLines = new Set([false, true].flatMap((mobile) => bodyLines(viewpointScaffold('', mobile))));
33
+ } catch {
34
+ return false; // no template to compare with → treat the file as the QA's
35
+ }
36
+ }
37
+ const known = templateLines;
38
+ return bodyLines(raw).every((l) => known.has(l));
39
+ }
40
+
41
+ /** `raw` is the file as written (null when missing); `rows` are its VP rows from the scan. */
42
+ export function viewpointMode(raw: string | null, rows: { hasExpected: boolean }[]): { mode: ViewpointMode; reason: string } {
43
+ if (raw === null) return { mode: 2, reason: 'file missing' };
44
+ if (isAiDraft(raw)) return { mode: 2, reason: 'drafted-by: ai (re-review)' };
45
+ if (rows.length > 0) {
46
+ return rows.some((r) => r.hasExpected) ? { mode: 1, reason: 'QA-written' } : { mode: 2, reason: 'VP rows without Kỳ vọng' };
47
+ }
48
+ return isUntouchedScaffold(raw) ? { mode: 2, reason: 'untouched scaffold' } : { mode: 1, reason: 'QA-written, no VP ids' };
49
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * One reading of a priority — a viewpoint row's cell or a scenario's tag.
3
+ *
4
+ * The overview parser, the ledger's uniform-priority check and PRIORITY-CONTRADICTED each read a
5
+ * priority their own way (`/high/i` substrings, raw words, a one-off medium→normal), so the same
6
+ * row could be `Medium` to one and `Unknown` to another — `Med`, which QA writes, was `Unknown`.
7
+ * The vocabulary is `high | normal | low`; `critical`, `medium` and `med` are read as aliases.
8
+ */
9
+ export type PriorityLevel = 'high' | 'normal' | 'low' | 'deferred' | 'skip' | 'unknown';
10
+
11
+ const WORD: Record<string, PriorityLevel> = {
12
+ critical: 'high', high: 'high',
13
+ medium: 'normal', med: 'normal', normal: 'normal',
14
+ low: 'low', deferred: 'deferred',
15
+ };
16
+ /** "Declared not applicable" — one level, so every consumer checks one word. */
17
+ const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/;
18
+
19
+ /**
20
+ * A priority cell or tag → its level. The whole cell being a word is the clean case; otherwise the
21
+ * overview has always read prose by SUBSTRING in precedence order (`High — complex form`,
22
+ * `Medium-High`), and keeps doing so. The substring words stay unbounded for `high|medium|low`
23
+ * (existing behaviour); the newer `critical|normal|med` need word boundaries (`Medical` is not `Med`).
24
+ */
25
+ export function parsePriority(cell: string): PriorityLevel {
26
+ const w = cell.replace(/[*_`]/g, '').replace(/\.+$/, '').trim().toLowerCase();
27
+ if (WORD[w]) return WORD[w];
28
+ if (NOT_APPLICABLE.test(w)) return 'skip';
29
+ if (/high|\bcritical\b/.test(w)) return 'high';
30
+ if (/medium|\bnormal\b|\bmed\b/.test(w)) return 'normal';
31
+ if (/low/.test(w)) return 'low';
32
+ return 'unknown';
33
+ }
34
+
35
+ /** The public `ViewpointEntry.priority` spelling of a level — the exported type is unchanged. */
36
+ export function toEntryPriority(level: PriorityLevel): 'High' | 'Medium' | 'Low' | 'Unknown' {
37
+ return level === 'high' ? 'High' : level === 'normal' ? 'Medium' : level === 'low' ? 'Low' : 'Unknown';
38
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * VIEWPOINT-REVIEW-OPEN (info) — the audit's one line about `requirements/viewpoint-review.md`.
3
+ *
4
+ * create-test reviews test-viewpoint.md with the sungen-viewpoint checklist and writes each point a
5
+ * QA should look at as a table row whose last cell is `[ ]`; the QA ticks it `[x]` or deletes the
6
+ * row. While rows stay unticked the audit says how many, once. Advisory only: no score, no gate, no
7
+ * baseline effect. A missing or unreadable file says nothing — the review is optional input.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { stripHtmlComments } from './read-text';
12
+
13
+ export const VIEWPOINT_REVIEW_FILE = 'viewpoint-review.md';
14
+
15
+ const SEPARATOR = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/;
16
+ /** A table separator, not a `---` rule under a row. */
17
+ const isSeparator = (line: string): boolean => SEPARATOR.test(line) && line.includes('|');
18
+ const UNTICKED = /^\[\s?\]$/;
19
+
20
+ /** The last cell of a markdown table row, or null for a line that is not one. */
21
+ function lastCell(line: string): string | null {
22
+ const t = line.trim();
23
+ if (!t.startsWith('|') || !t.endsWith('|') || t.length < 2) return null;
24
+ const cells = t.slice(1, -1).split(/(?<!\\)\|/);
25
+ return cells[cells.length - 1].trim();
26
+ }
27
+
28
+ /** Body rows whose last cell is `[ ]`. The header row (`… | [ ] |` above `|---|`) is not a point. */
29
+ export function openReviewPoints(text: string): number {
30
+ const lines = stripHtmlComments(text.replace(/\r\n?/g, '\n')).split('\n');
31
+ let open = 0;
32
+ for (let i = 0; i < lines.length; i++) {
33
+ if (isSeparator(lines[i]) || isSeparator(lines[i + 1] ?? '')) continue;
34
+ const last = lastCell(lines[i]);
35
+ if (last !== null && UNTICKED.test(last)) open++;
36
+ }
37
+ return open;
38
+ }
39
+
40
+ /** The info finding for one unit's `requirements/` directory; empty when nothing is open. */
41
+ export function viewpointReviewOpenFindings(requirementsDir: string): string[] {
42
+ let text: string;
43
+ try {
44
+ text = fs.readFileSync(path.join(requirementsDir, VIEWPOINT_REVIEW_FILE), 'utf-8');
45
+ } catch {
46
+ return [];
47
+ }
48
+ const open = openReviewPoints(text);
49
+ if (open === 0) return [];
50
+ return [`VIEWPOINT-REVIEW-OPEN (info): ${open} open point(s) in requirements/${VIEWPOINT_REVIEW_FILE} — the checklist review of test-viewpoint.md is waiting for QA. QA: fix the viewpoint where you agree, then tick the row \`[x]\` or delete it. Not scored; an agent does not tick rows for the QA.`];
51
+ }