@sun-asterisk/sungen 3.2.32 → 3.2.34

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 (381) hide show
  1. package/dist/cli/commands/audit.d.ts +5 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +26 -4
  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 +35 -17
  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/delivery.d.ts.map +1 -1
  15. package/dist/cli/commands/delivery.js +79 -53
  16. package/dist/cli/commands/delivery.js.map +1 -1
  17. package/dist/cli/commands/generate.d.ts.map +1 -1
  18. package/dist/cli/commands/generate.js +14 -7
  19. package/dist/cli/commands/generate.js.map +1 -1
  20. package/dist/cli/commands/results-merge.d.ts +9 -0
  21. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  22. package/dist/cli/commands/results-merge.js +9 -12
  23. package/dist/cli/commands/results-merge.js.map +1 -1
  24. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  25. package/dist/cli/commands/results-sanitize.js +2 -13
  26. package/dist/cli/commands/results-sanitize.js.map +1 -1
  27. package/dist/cli/commands/select.d.ts +12 -0
  28. package/dist/cli/commands/select.d.ts.map +1 -0
  29. package/dist/cli/commands/select.js +122 -0
  30. package/dist/cli/commands/select.js.map +1 -0
  31. package/dist/cli/commands/update.d.ts.map +1 -1
  32. package/dist/cli/commands/update.js +3 -0
  33. package/dist/cli/commands/update.js.map +1 -1
  34. package/dist/cli/index.js +7 -2
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  37. package/dist/dashboard/snapshot-builder.js +6 -10
  38. package/dist/dashboard/snapshot-builder.js.map +1 -1
  39. package/dist/dashboard/templates/index.html +1 -1
  40. package/dist/dashboard/types.d.ts +1 -0
  41. package/dist/dashboard/types.d.ts.map +1 -1
  42. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  43. package/dist/exporters/csv-exporter.js +11 -10
  44. package/dist/exporters/csv-exporter.js.map +1 -1
  45. package/dist/exporters/feature-parser.d.ts +21 -0
  46. package/dist/exporters/feature-parser.d.ts.map +1 -1
  47. package/dist/exporters/feature-parser.js +32 -1
  48. package/dist/exporters/feature-parser.js.map +1 -1
  49. package/dist/exporters/json-exporter.d.ts.map +1 -1
  50. package/dist/exporters/json-exporter.js +7 -0
  51. package/dist/exporters/json-exporter.js.map +1 -1
  52. package/dist/exporters/locale-variants.d.ts +7 -1
  53. package/dist/exporters/locale-variants.d.ts.map +1 -1
  54. package/dist/exporters/locale-variants.js +23 -16
  55. package/dist/exporters/locale-variants.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 +19 -9
  62. package/dist/exporters/matrix/build.js.map +1 -1
  63. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  64. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  65. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  66. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  67. package/dist/exporters/matrix/export.d.ts +5 -0
  68. package/dist/exporters/matrix/export.d.ts.map +1 -1
  69. package/dist/exporters/matrix/export.js +8 -1
  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-testcase-sheet.d.ts.map +1 -1
  74. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  75. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  76. package/dist/exporters/matrix/types.d.ts +8 -1
  77. package/dist/exporters/matrix/types.d.ts.map +1 -1
  78. package/dist/exporters/matrix/types.js.map +1 -1
  79. package/dist/exporters/matrix/wording.d.ts +8 -1
  80. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  81. package/dist/exporters/matrix/wording.js +37 -2
  82. package/dist/exporters/matrix/wording.js.map +1 -1
  83. package/dist/exporters/run-environment.d.ts +32 -0
  84. package/dist/exporters/run-environment.d.ts.map +1 -0
  85. package/dist/exporters/run-environment.js +125 -0
  86. package/dist/exporters/run-environment.js.map +1 -0
  87. package/dist/exporters/scenario-merger.d.ts +18 -0
  88. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  89. package/dist/exporters/scenario-merger.js +47 -3
  90. package/dist/exporters/scenario-merger.js.map +1 -1
  91. package/dist/exporters/test-data-resolver.d.ts +0 -4
  92. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  93. package/dist/exporters/test-data-resolver.js +0 -7
  94. package/dist/exporters/test-data-resolver.js.map +1 -1
  95. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  96. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  97. package/dist/exporters/xlsx-exporter.js +7 -2
  98. package/dist/exporters/xlsx-exporter.js.map +1 -1
  99. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  100. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  101. package/dist/generators/gherkin-parser/index.js +49 -19
  102. package/dist/generators/gherkin-parser/index.js.map +1 -1
  103. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  104. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  105. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  106. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  107. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  108. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  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/steps/partials/url-assertion.hbs +5 -1
  115. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  116. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  117. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  118. package/dist/generators/test-generator/code-generator.js +90 -5
  119. package/dist/generators/test-generator/code-generator.js.map +1 -1
  120. package/dist/generators/test-generator/diagnostics.d.ts +36 -5
  121. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  122. package/dist/generators/test-generator/diagnostics.js +35 -4
  123. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  124. package/dist/generators/test-generator/patterns/index.d.ts +12 -2
  125. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  126. package/dist/generators/test-generator/patterns/index.js +15 -9
  127. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  128. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  129. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  130. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  131. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  132. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  133. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  134. package/dist/generators/test-generator/step-mapper.d.ts +46 -0
  135. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  136. package/dist/generators/test-generator/step-mapper.js +84 -9
  137. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  138. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  139. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  140. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  141. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  142. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  143. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  144. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  145. package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
  146. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  147. package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
  148. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  149. package/dist/harness/audit.d.ts +3 -0
  150. package/dist/harness/audit.d.ts.map +1 -1
  151. package/dist/harness/audit.js +29 -12
  152. package/dist/harness/audit.js.map +1 -1
  153. package/dist/harness/capability.d.ts +7 -1
  154. package/dist/harness/capability.d.ts.map +1 -1
  155. package/dist/harness/capability.js +15 -12
  156. package/dist/harness/capability.js.map +1 -1
  157. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  158. package/dist/harness/config-file.d.ts +17 -0
  159. package/dist/harness/config-file.d.ts.map +1 -0
  160. package/dist/harness/config-file.js +100 -0
  161. package/dist/harness/config-file.js.map +1 -0
  162. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  163. package/dist/harness/data-driven-lint.js +69 -27
  164. package/dist/harness/data-driven-lint.js.map +1 -1
  165. package/dist/harness/depth-lint.d.ts.map +1 -1
  166. package/dist/harness/depth-lint.js +2 -1
  167. package/dist/harness/depth-lint.js.map +1 -1
  168. package/dist/harness/flow-check.d.ts.map +1 -1
  169. package/dist/harness/flow-check.js +6 -2
  170. package/dist/harness/flow-check.js.map +1 -1
  171. package/dist/harness/keyword-match.d.ts +59 -0
  172. package/dist/harness/keyword-match.d.ts.map +1 -0
  173. package/dist/harness/keyword-match.js +86 -0
  174. package/dist/harness/keyword-match.js.map +1 -0
  175. package/dist/harness/next-step.d.ts.map +1 -1
  176. package/dist/harness/next-step.js +1 -0
  177. package/dist/harness/next-step.js.map +1 -1
  178. package/dist/harness/parse.d.ts +24 -0
  179. package/dist/harness/parse.d.ts.map +1 -1
  180. package/dist/harness/parse.js +117 -13
  181. package/dist/harness/parse.js.map +1 -1
  182. package/dist/harness/perf.d.ts +7 -1
  183. package/dist/harness/perf.d.ts.map +1 -1
  184. package/dist/harness/perf.js +13 -10
  185. package/dist/harness/perf.js.map +1 -1
  186. package/dist/harness/quality-gates.d.ts +9 -2
  187. package/dist/harness/quality-gates.d.ts.map +1 -1
  188. package/dist/harness/quality-gates.js +22 -8
  189. package/dist/harness/quality-gates.js.map +1 -1
  190. package/dist/harness/query-catalog.js +0 -0
  191. package/dist/harness/query-catalog.js.map +1 -1
  192. package/dist/harness/read-text.d.ts +21 -0
  193. package/dist/harness/read-text.d.ts.map +1 -1
  194. package/dist/harness/read-text.js +32 -1
  195. package/dist/harness/read-text.js.map +1 -1
  196. package/dist/harness/secret-scan.d.ts +27 -1
  197. package/dist/harness/secret-scan.d.ts.map +1 -1
  198. package/dist/harness/secret-scan.js +57 -4
  199. package/dist/harness/secret-scan.js.map +1 -1
  200. package/dist/harness/select-by-ids.d.ts +31 -0
  201. package/dist/harness/select-by-ids.d.ts.map +1 -0
  202. package/dist/harness/select-by-ids.js +106 -0
  203. package/dist/harness/select-by-ids.js.map +1 -0
  204. package/dist/harness/sensors.d.ts +20 -1
  205. package/dist/harness/sensors.d.ts.map +1 -1
  206. package/dist/harness/sensors.js +102 -27
  207. package/dist/harness/sensors.js.map +1 -1
  208. package/dist/harness/spec-branches.d.ts +0 -18
  209. package/dist/harness/spec-branches.d.ts.map +1 -1
  210. package/dist/harness/spec-branches.js +24 -3
  211. package/dist/harness/spec-branches.js.map +1 -1
  212. package/dist/harness/spec-coverage.d.ts.map +1 -1
  213. package/dist/harness/spec-coverage.js +14 -4
  214. package/dist/harness/spec-coverage.js.map +1 -1
  215. package/dist/harness/ui-inventory.d.ts +40 -0
  216. package/dist/harness/ui-inventory.d.ts.map +1 -0
  217. package/dist/harness/ui-inventory.js +191 -0
  218. package/dist/harness/ui-inventory.js.map +1 -0
  219. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  220. package/dist/harness/viewpoint-ledger.js +66 -10
  221. package/dist/harness/viewpoint-ledger.js.map +1 -1
  222. package/dist/harness/write-atomic.d.ts +2 -0
  223. package/dist/harness/write-atomic.d.ts.map +1 -0
  224. package/dist/harness/write-atomic.js +62 -0
  225. package/dist/harness/write-atomic.js.map +1 -0
  226. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  227. package/dist/orchestrator/ai-skills/writer.js +15 -7
  228. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  229. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  230. package/dist/orchestrator/assets-drift.js +11 -6
  231. package/dist/orchestrator/assets-drift.js.map +1 -1
  232. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  233. package/dist/orchestrator/project-deps-sync.js +22 -1
  234. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  235. package/dist/orchestrator/project-initializer.d.ts +8 -0
  236. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  237. package/dist/orchestrator/project-initializer.js +49 -7
  238. package/dist/orchestrator/project-initializer.js.map +1 -1
  239. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  240. package/dist/orchestrator/screen-manager.js +3 -11
  241. package/dist/orchestrator/screen-manager.js.map +1 -1
  242. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  243. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  244. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  245. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  246. package/dist/orchestrator/security-overrides.d.ts +51 -0
  247. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  248. package/dist/orchestrator/security-overrides.js +165 -0
  249. package/dist/orchestrator/security-overrides.js.map +1 -0
  250. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  251. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  252. package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  253. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  254. package/dist/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  255. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  256. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  257. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  258. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  259. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  260. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  261. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  262. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  263. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  264. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  265. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  266. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  267. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  268. package/dist/orchestrator/templates/specs-url-assert.js +38 -4
  269. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  270. package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
  271. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  272. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  273. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  274. package/dist/orchestrator/trace-mode-install.js +132 -0
  275. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  276. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +20 -0
  277. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  278. package/dist/orchestrator/viewpoint-pattern-catalog.js +45 -0
  279. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  280. package/package.json +3 -3
  281. package/src/cli/commands/audit.ts +24 -5
  282. package/src/cli/commands/capability.ts +51 -18
  283. package/src/cli/commands/changed.ts +2 -4
  284. package/src/cli/commands/dashboard.ts +1 -26
  285. package/src/cli/commands/delivery.ts +75 -48
  286. package/src/cli/commands/generate.ts +15 -7
  287. package/src/cli/commands/results-merge.ts +9 -9
  288. package/src/cli/commands/results-sanitize.ts +2 -9
  289. package/src/cli/commands/select.ts +88 -0
  290. package/src/cli/commands/update.ts +3 -0
  291. package/src/cli/index.ts +7 -2
  292. package/src/dashboard/snapshot-builder.ts +6 -8
  293. package/src/dashboard/templates/index.html +1 -1
  294. package/src/dashboard/types.ts +1 -0
  295. package/src/exporters/csv-exporter.ts +7 -10
  296. package/src/exporters/feature-parser.ts +32 -2
  297. package/src/exporters/json-exporter.ts +6 -1
  298. package/src/exporters/locale-variants.ts +23 -17
  299. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  300. package/src/exporters/matrix/build.ts +20 -10
  301. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  302. package/src/exporters/matrix/export.ts +14 -4
  303. package/src/exporters/matrix/gates.ts +6 -0
  304. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  305. package/src/exporters/matrix/types.ts +8 -1
  306. package/src/exporters/matrix/wording.ts +36 -2
  307. package/src/exporters/run-environment.ts +89 -0
  308. package/src/exporters/scenario-merger.ts +49 -4
  309. package/src/exporters/test-data-resolver.ts +0 -6
  310. package/src/exporters/xlsx-exporter.ts +7 -2
  311. package/src/generators/gherkin-parser/index.ts +50 -18
  312. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
  313. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  314. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  315. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  316. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  317. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  318. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  319. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  320. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  321. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  322. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  323. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  324. package/src/generators/test-generator/code-generator.ts +77 -5
  325. package/src/generators/test-generator/diagnostics.ts +36 -5
  326. package/src/generators/test-generator/patterns/index.ts +15 -8
  327. package/src/generators/test-generator/patterns/types.ts +15 -0
  328. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  329. package/src/generators/test-generator/step-mapper.ts +90 -8
  330. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  331. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  332. package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
  333. package/src/harness/audit.ts +37 -16
  334. package/src/harness/capability.ts +23 -12
  335. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  336. package/src/harness/config-file.ts +66 -0
  337. package/src/harness/data-driven-lint.ts +82 -24
  338. package/src/harness/depth-lint.ts +2 -1
  339. package/src/harness/flow-check.ts +6 -2
  340. package/src/harness/keyword-match.ts +85 -0
  341. package/src/harness/next-step.ts +1 -0
  342. package/src/harness/parse.ts +123 -16
  343. package/src/harness/perf.ts +15 -7
  344. package/src/harness/quality-gates.ts +22 -8
  345. package/src/harness/query-catalog.ts +0 -0
  346. package/src/harness/read-text.ts +32 -1
  347. package/src/harness/secret-scan.ts +60 -4
  348. package/src/harness/select-by-ids.ts +83 -0
  349. package/src/harness/sensors.ts +109 -28
  350. package/src/harness/spec-branches.ts +6 -3
  351. package/src/harness/spec-coverage.ts +14 -4
  352. package/src/harness/ui-inventory.ts +164 -0
  353. package/src/harness/viewpoint-ledger.ts +59 -12
  354. package/src/harness/write-atomic.ts +22 -0
  355. package/src/orchestrator/ai-skills/writer.ts +18 -3
  356. package/src/orchestrator/assets-drift.ts +10 -5
  357. package/src/orchestrator/project-deps-sync.ts +23 -1
  358. package/src/orchestrator/project-initializer.ts +54 -6
  359. package/src/orchestrator/screen-manager.ts +3 -11
  360. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  361. package/src/orchestrator/security-overrides.ts +149 -0
  362. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  363. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  364. package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  365. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  366. package/src/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  367. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  368. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  369. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  370. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  371. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  372. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  373. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  374. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  375. package/src/orchestrator/templates/playwright.config.ts +4 -2
  376. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  377. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  378. package/src/orchestrator/templates/specs-url-assert.ts +47 -4
  379. package/src/orchestrator/templates/wdio.conf.ts +24 -1
  380. package/src/orchestrator/trace-mode-install.ts +94 -0
  381. package/src/orchestrator/viewpoint-pattern-catalog.ts +58 -0
@@ -23,10 +23,8 @@
23
23
  * units:
24
24
  * place-order: { scenario_ms: 20000 }
25
25
  */
26
- import * as fs from 'fs';
27
26
  import * as path from 'path';
28
- import { parse as parseYaml } from 'yaml';
29
- import { readTextFile } from './read-text';
27
+ import { readYamlConfig, warnUnreadableConfig } from './config-file';
30
28
 
31
29
  export interface PerfConfig {
32
30
  /** 0..100 — e.g. 75 for p75. */
@@ -51,12 +49,22 @@ export function perfConfigPath(projectRoot: string): string {
51
49
  return path.join(projectRoot, 'qa', 'perf.yaml');
52
50
  }
53
51
 
54
- /** Absent file → null (perf reporting is opt-in; nothing changes until configured). */
52
+ /**
53
+ * Absent file → null (perf reporting is opt-in; nothing changes until configured).
54
+ *
55
+ * An UNREADABLE file is not that. Returning null for it drops the perf section out of the delivered
56
+ * matrix (`exporters/matrix/export.ts`) exactly as if no budgets had ever been declared — so a typo
57
+ * in qa/perf.yaml reads, in the deliverable, as "this project has no performance budgets". Say so.
58
+ */
55
59
  export function loadPerfConfig(projectRoot: string): PerfConfig | null {
56
60
  const p = perfConfigPath(projectRoot);
57
- if (!fs.existsSync(p)) return null;
58
- let raw: Record<string, unknown>;
59
- try { raw = parseYaml(readTextFile(p)) as Record<string, unknown>; } catch { return null; }
61
+ const read = readYamlConfig<Record<string, unknown> | null>(p);
62
+ if (read.state === 'absent') return null;
63
+ if (read.state === 'unreadable') {
64
+ warnUnreadableConfig(p, read.error, 'perf budgets are not applied and the perf section is omitted from the delivered matrix');
65
+ return null;
66
+ }
67
+ const raw = read.value as Record<string, unknown>;
60
68
  if (!raw || typeof raw !== 'object') return null;
61
69
  const pctRaw = String(raw.percentile ?? 'p75').toLowerCase().replace(/^p/, '');
62
70
  const percentile = Math.min(100, Math.max(1, Number(pctRaw) || 75));
@@ -5,8 +5,10 @@
5
5
  */
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
+ import { keywordHit } from './keyword-match';
8
9
  import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
9
10
  import { parseManualComments } from '../exporters/scenario-merger';
11
+ import { commentLinesOf, isPresencePlaceholderHybrid, keywordStepsOf } from '../exporters/manual-hybrid-oracle';
10
12
  import { readTextFile } from './read-text';
11
13
 
12
14
  // ---------- #2 Downstream-scope ----------
@@ -57,12 +59,19 @@ function downstreamRoutes(specText: string): string[] {
57
59
  }
58
60
 
59
61
  /**
60
- * H5 — a @parallel stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios
61
- * share state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
62
+ * H5 — a stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios share
63
+ * state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
62
64
  * browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
65
+ *
66
+ * #759 — this used to require a literal `@parallel`, which made the gate miss the common case.
67
+ * Parallel is the DEFAULT (code-generator.ts: `isParallelFeature = !isSerialFeature`); `@parallel`
68
+ * only NAMES that default and changes no codegen, so an untagged stateful flow is isolated exactly
69
+ * like a tagged one and carries exactly the same risk. The tag that actually changes the emitted
70
+ * suite is `@serial` — one shared ordered page, where state carry-over is the point — so that is
71
+ * what exempts a flow now. Same reading as serialCascadeRisk() below, which already said so.
63
72
  */
64
73
  export function isolationRisk(featureText: string, stateful: boolean, collection = 'cart'): boolean {
65
- if (!stateful || !/@parallel\b/i.test(featureText)) return false;
74
+ if (!stateful || /@serial\b/i.test(featureText)) return false;
66
75
  // What discharges the risk is isolation of THE MUTATED COLLECTION — so the cleanup tag has
67
76
  // to name it (`@cleanup:cart` for a cart; `@cleanup:registration` for a registration flow).
68
77
  // A cleanup of something else (`@cleanup:overlay`) leaves the shared state shared.
@@ -149,8 +158,12 @@ export function downstreamScope(
149
158
  // names the same screen under its human label (register ↔ [Registration]).
150
159
  const labelsOf = (s: ScenarioInfo): string[] =>
151
160
  [...s.haystack.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1].split(':')[0]);
161
+ // The slug is the route's last segment — in practice an ordinary noun (`order`, `detail`,
162
+ // `login`), so it is matched as a WORD (#779). `dis·order` is not a reference to `/account/order`,
163
+ // and crediting it downgraded a route nothing mentions from `absent` to merely `underCovered`.
164
+ // The full route stays a literal: a path is an identifier, not prose.
152
165
  const refs = scenarios.filter((s) =>
153
- s.haystack.includes(slug) || s.haystack.includes(route.toLowerCase()) ||
166
+ keywordHit(s.haystack, slug) || s.haystack.includes(route.toLowerCase()) ||
154
167
  labelsOf(s).some((l) => sameScreenName(slug, l)));
155
168
  if (!refs.length) {
156
169
  // A scenario's haystack is its name + steps, so a route documented in a @manual scenario's
@@ -208,14 +221,15 @@ export function manualOracle(featureText: string): ManualOracleResult {
208
221
  // Observable:/Oracle:/Expected: line (parseManualComments — the SAME parser the
209
222
  // delivery exporter uses, so this gate can't drift from the exported cells).
210
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).
211
227
  const hasGherkinSteps = /^\s*(Given|When|Then)\s/m.test(b);
228
+ const commentLines = commentLinesOf(b);
212
229
  let bad: boolean;
213
230
  if (hasGherkinSteps) {
214
- bad = !/^\s*Then\s/m.test(b);
231
+ bad = !/^\s*Then\s/m.test(b) || isPresencePlaceholderHybrid(keywordStepsOf(b), commentLines);
215
232
  } else {
216
- const commentLines = b.split('\n')
217
- .filter((l) => /^\s*#/.test(l))
218
- .map((l) => l.replace(/^\s*#\s?/, ''));
219
233
  const parsed = parseManualComments(commentLines);
220
234
  bad = !parsed.steps.length || !parsed.expected.length;
221
235
  }
Binary file
@@ -17,12 +17,43 @@ import * as fs from 'fs';
17
17
  *
18
18
  * A Windows-authored spec is completely normal, so every harness parser that
19
19
  * reads a user file must go through here.
20
+ *
21
+ * A leading UTF-8 BOM is stripped for the same reason. It is what PowerShell's `Out-File` and
22
+ * several Windows editors write by default, and it is invisible in every editor — but it breaks a
23
+ * line-anchored `/^\s*Scenario:/` on line 1, and `JSON.parse` rejects it outright while npm and
24
+ * most other tools quietly ignore it. A file the rest of the ecosystem reads fine must not read as
25
+ * corrupt here.
20
26
  */
21
27
  export function readTextFile(filePath: string): string {
22
- return fs.readFileSync(filePath, 'utf-8').replace(/\r\n?/g, '\n');
28
+ return fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
23
29
  }
24
30
 
25
31
  /** `readTextFile` split into lines — the common case for line-anchored parsers. */
26
32
  export function readTextLines(filePath: string): string[] {
27
33
  return readTextFile(filePath).split('\n');
28
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
+ }
@@ -11,6 +11,19 @@ import * as path from 'path';
11
11
 
12
12
  export interface SecretHit { file: string; line: number; reason: string }
13
13
 
14
+ /** A yaml sink the scanner could not read. Not a hit — the absence of one it cannot vouch for. */
15
+ export interface UnscannedFile { file: string; reason: string }
16
+
17
+ /**
18
+ * The scan result, deliberately two lists rather than one.
19
+ *
20
+ * This is a security control, and its whole value is that "no hits" means "I looked". A file the
21
+ * scanner could not read used to be dropped by a bare `catch`, so an unread yaml and a yaml with no
22
+ * secrets produced the identical answer: clean. Returning `unscanned` alongside makes the caller
23
+ * hold both facts — what was found, and what the finding does not cover.
24
+ */
25
+ export interface SecretScanResult { hits: SecretHit[]; unscanned: UnscannedFile[] }
26
+
14
27
  // High-confidence vendor token prefixes (very low false-positive rate).
15
28
  const VENDOR_PREFIXES = /\b(figd_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})/;
16
29
  // A secret-named key assigned a long, non-placeholder value.
@@ -51,16 +64,59 @@ function scanText(text: string, file: string): SecretHit[] {
51
64
  /** Scan a unit dir's committed yaml sinks for likely real secrets: test-data/*.yaml,
52
65
  * mock/*.yaml (mock response bodies are a credential magnet — pasted DevTools
53
66
  * responses carry real tokens), and api/*.yaml (endpoint catalogs carry headers). */
54
- export function scanTestDataSecrets(baseDir: string): SecretHit[] {
67
+ export function scanTestDataSecrets(baseDir: string): SecretScanResult {
55
68
  const hits: SecretHit[] = [];
69
+ const unscanned: UnscannedFile[] = [];
70
+ const why = (e: unknown): string => (e instanceof Error ? e.message.split('\n')[0] : String(e));
71
+
56
72
  for (const sub of ['test-data', 'mock', 'api']) {
57
73
  const dir = path.join(baseDir, sub);
58
74
  if (!fs.existsSync(dir)) continue;
59
- for (const f of fs.readdirSync(dir)) {
75
+
76
+ // A directory that cannot be LISTED is a directory that cannot be scanned. This used to throw
77
+ // out of the scanner and abort the whole `generate`/`delivery` run; report it in the same place
78
+ // a hit would appear instead, so the operator learns what the clean verdict does not cover
79
+ // rather than losing the deliverable to an unrelated permission fault.
80
+ let entries: string[];
81
+ try { entries = fs.readdirSync(dir); } catch (e) { unscanned.push({ file: sub, reason: why(e) }); continue; }
82
+
83
+ for (const f of entries) {
60
84
  if (!/\.ya?ml$/i.test(f)) continue;
61
85
  const p = path.join(dir, f);
62
- try { hits.push(...scanText(fs.readFileSync(p, 'utf-8'), path.join(sub, f))); } catch { /* ignore */ }
86
+ const rel = path.join(sub, f);
87
+ let text: string;
88
+ // `readdirSync` listed this file, so it exists as a name. A read that fails here means a
89
+ // restricted mount, a dangling symlink, or a file removed between the two calls — never
90
+ // "there was nothing to read". Silently skipping it let a committed credential ship under an
91
+ // explicit "clean" verdict.
92
+ try { text = fs.readFileSync(p, 'utf-8'); } catch (e) { unscanned.push({ file: rel, reason: why(e) }); continue; }
93
+ hits.push(...scanText(text, rel));
63
94
  }
64
95
  }
65
- return hits;
96
+ return { hits, unscanned };
97
+ }
98
+
99
+ /**
100
+ * The operator-facing lines for a scan result — one renderer, because `generate` and `delivery`
101
+ * had grown two copies of it and only one of them would have learned about `unscanned`.
102
+ *
103
+ * Empty array when there is nothing to say. The unscanned block is NOT folded into the hit block:
104
+ * "possible secret" and "not examined" are different claims, and printing the second under the
105
+ * first's heading invites it to be dismissed as another false positive.
106
+ */
107
+ export function formatSecretScan(result: SecretScanResult, limit = 10): string[] {
108
+ const out: string[] = [];
109
+ if (result.hits.length) {
110
+ out.push('', '⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):');
111
+ for (const h of result.hits.slice(0, limit)) out.push(` ${h.file}:${h.line} — ${h.reason}`);
112
+ if (result.hits.length > limit) out.push(` … and ${result.hits.length - limit} more`);
113
+ out.push(' Move real secrets to an env overlay / CI secret; keep test-data placeholders only.');
114
+ }
115
+ if (result.unscanned.length) {
116
+ out.push('', `⚠️ Secret scan could NOT read ${result.unscanned.length} committed yaml file(s) — they are NOT covered by the result above:`);
117
+ for (const u of result.unscanned.slice(0, limit)) out.push(` ${u.file} — ${u.reason}`);
118
+ if (result.unscanned.length > limit) out.push(` … and ${result.unscanned.length - limit} more`);
119
+ out.push(' Make them readable and re-run before treating this unit as scanned.');
120
+ }
121
+ return out;
66
122
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Select scenarios by test-case ID — backs `sungen select` and `run-test --ids` (#835).
3
+ *
4
+ * An ID means what it means everywhere else in the harness: `vpId`, the leading ID token of the
5
+ * scenario title (`VP-UI-001`, `MS-HP-001`, `VP0-001`). Matching is exact and case-insensitive —
6
+ * `VP-UI-001` never selects `VP-UI-0010` or `VP-UI-001-B` — and every ID that selects nothing
7
+ * runnable is reported, because the alternative is a 0-test run that reports green.
8
+ */
9
+ import * as path from 'path';
10
+ import { ScenarioInfo } from './parse';
11
+
12
+ /**
13
+ * Escape scenario titles into one `-g` / `--mochaOpts.grep` regex. Full titles, not bare IDs:
14
+ * a title starts with its own ID, so it can only appear inside its own test's title — and a
15
+ * `@cases` row test (`<title> — <row label>`) still contains it.
16
+ */
17
+ export function titlesToGrep(titles: string[]): string {
18
+ return titles.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
19
+ }
20
+
21
+ export interface IdSelectionGroup {
22
+ /** Feature basename (`products`, `home-modal`) — names the spec and the result file. */
23
+ feature: string;
24
+ ids: string[];
25
+ titles: string[];
26
+ grep: string;
27
+ }
28
+
29
+ export interface IdSelection {
30
+ features: IdSelectionGroup[];
31
+ /** IDs no scenario carries, with near matches (`VP-VAL-001` → `VP-VAL-001-B`). */
32
+ unknown: Array<{ id: string; didYouMean: string[] }>;
33
+ /** IDs whose every scenario is manual, owned by a flow or waiting on a capability — nothing to run. */
34
+ notRunnable: Array<{ id: string; reason: string }>;
35
+ }
36
+
37
+ /** `"VP-UI-001, vp-ui-002 VP-UI-001"` → `['VP-UI-001', 'VP-UI-002']`. */
38
+ export function parseIdList(raw: string): string[] {
39
+ return [...new Set(raw.split(/[\s,]+/).map((s) => s.trim().toUpperCase()).filter(Boolean))];
40
+ }
41
+
42
+ /** `VP-C-002.` → `VP-C-002`: the title token may carry trailing punctuation the user never types. */
43
+ const idOf = (s: ScenarioInfo): string | undefined => s.vpId?.replace(/[.-]+$/, '') || undefined;
44
+
45
+ function notRunnableReason(s: ScenarioInfo, enabledCaps: Set<string>): string | null {
46
+ if (s.deferredToFlow) return `owned by a flow${s.ownedByFlow ? ` (${s.ownedByFlow})` : ''} — run it there`;
47
+ if (s.featureManual) return 'its Feature is @manual — no spec is generated for it';
48
+ if (s.manual) return 'manual — no automated test exists';
49
+ // Compiled as `test.skip` while the capability is off: the run would report a skip, not a result.
50
+ const missing = (s.requiresCaps ?? []).filter((c) => !enabledCaps.has(c));
51
+ if (missing.length) return `needs capability ${missing.join(', ')} — compiled as a skip until \`sungen capability add ${missing.join(' ')}\``;
52
+ return null;
53
+ }
54
+
55
+ export function selectByIds(scenarios: ScenarioInfo[], ids: string[], unit: string, enabledCaps: Set<string> = new Set()): IdSelection {
56
+ const out: IdSelection = { features: [], unknown: [], notRunnable: [] };
57
+ const groups = new Map<string, IdSelectionGroup>();
58
+ const allIds = [...new Set(scenarios.map(idOf).filter((v): v is string => !!v))];
59
+
60
+ for (const id of ids) {
61
+ const hits = scenarios.filter((s) => idOf(s) === id);
62
+ if (hits.length === 0) {
63
+ const didYouMean = allIds.filter((v) => v.startsWith(`${id}-`) || id.startsWith(`${v}-`)).slice(0, 5);
64
+ out.unknown.push({ id, didYouMean });
65
+ continue;
66
+ }
67
+ const runnable = hits.filter((s) => !notRunnableReason(s, enabledCaps));
68
+ if (runnable.length === 0) {
69
+ out.notRunnable.push({ id, reason: notRunnableReason(hits[0], enabledCaps)! });
70
+ continue;
71
+ }
72
+ for (const s of runnable) {
73
+ const feature = s.sourceFile ? path.basename(s.sourceFile, '.feature') : unit;
74
+ const g = groups.get(feature) ?? { feature, ids: [], titles: [], grep: '' };
75
+ if (!g.ids.includes(id)) g.ids.push(id);
76
+ if (!g.titles.includes(s.name)) g.titles.push(s.name);
77
+ groups.set(feature, g);
78
+ }
79
+ }
80
+ for (const g of groups.values()) g.grep = titlesToGrep(g.titles);
81
+ out.features = [...groups.values()];
82
+ return out;
83
+ }
@@ -11,6 +11,7 @@ import * as path from 'path';
11
11
  import { parse as parseYaml } from 'yaml';
12
12
  import { ScenarioInfo, ViewpointEntry, idPrefix } from './parse';
13
13
  import { readTextFile } from './read-text';
14
+ import { keywordHit, hasKeyword } from './keyword-match';
14
15
 
15
16
  // Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
16
17
  // compound category like LIST-DISPLAY / ADD-TO-CART / PRODUCT-DISCOVERY classifies correctly).
@@ -86,18 +87,11 @@ export function loadCatalog(): Catalog {
86
87
  return parseYaml(readTextFile(p)) as Catalog;
87
88
  }
88
89
 
89
- // Word-aware keyword match: `\b<kw>(s|es|ed|ing)?\b`, so a keyword matches whole words (plus
90
- // simple plural/verb suffixes) but NOT as a substring of a larger word — e.g. `form` must not
91
- // match inside `Format` or `@platform`, while `product` still matches `products`. Multi-word
92
- // keywords ("add to cart", "is hidden") match the phrase with a boundary at each end.
93
- const wordRe = (kw: string) => {
94
- const esc = kw.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
95
- return new RegExp(`\\b${esc}(?:s|es|ed|ing)?\\b`);
96
- };
97
- const has = (haystacks: string[], kw: string) => {
98
- const re = wordRe(kw);
99
- return haystacks.some((h) => re.test(h));
100
- };
90
+ // Word-aware keyword match — one rule for every sensor, in ./keyword-match (#776). A keyword
91
+ // matches at a word START but may carry any suffix, so `form` does not match inside `Format` or
92
+ // `@platform` while `submit` still matches `submitted`. The old local copy anchored BOTH ends,
93
+ // which silently dropped every inflected true positive; see that module for the measurements.
94
+ const has = hasKeyword;
101
95
 
102
96
  // ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
103
97
 
@@ -112,6 +106,13 @@ export interface GateResult {
112
106
  coverageRatio: number;
113
107
  gaps: { theme: string; keywords: string[]; status: 'missing' | 'shallow' }[];
114
108
  universalGaps: string[];
109
+ /**
110
+ * WHICH scenario covered each satisfied theme, and on which keyword — the audit's second line of
111
+ * defence against a bogus coverage claim (#776). A bare count ("themes 3/3") gives a reviewer
112
+ * nothing to check, so a nonsense match survives; naming the evidence makes it obvious on sight.
113
+ * Mirrors how the Claim-Proof section already names the scenario behind each claim.
114
+ */
115
+ coveredBy: { theme: string; by: string; keyword: string }[];
115
116
  }
116
117
 
117
118
  /**
@@ -166,20 +167,36 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
166
167
 
167
168
  const evidence = { hits: best, runnerUp };
168
169
  const gaps: GateResult['gaps'] = [];
170
+ const coveredBy: GateResult['coveredBy'] = [];
169
171
  let total = 0, covered = 0;
170
172
  if (pageType) {
171
173
  for (const t of catalog.page_types[pageType].must_cover) {
172
174
  total++;
173
175
  // Scenarios that cover this theme by keyword (include @manual — a manual
174
176
  // scenario with a real assertion still covers the viewpoint for design).
175
- const coverers = scenarios.filter((s) => t.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
177
+ // Word-aware since #776: a substring match let `information` satisfy `format-boundary`.
178
+ const coverers = scenarios.filter((s) => t.keywords.some((k) => keywordHit(s.haystack, k)));
176
179
  if (coverers.length === 0) { gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' }); continue; }
177
180
  // A theme WITHOUT `depth:` is visibility/navigation — landing/seeing/leaving IS the
178
181
  // assertion, so mere presence covers it. Only DATA-correctness themes (depth.requires:
179
182
  // data-assertion) need a data assertion to count as deeply covered.
180
183
  const needsDepth = t.depth?.requires === 'data-assertion';
181
- if (!needsDepth) { covered++; continue; }
182
- if (coverers.some((s) => s.hasDataAssertion)) covered++;
184
+ // Name the evidence for whichever scenario actually carries the theme — the one with the
185
+ // data assertion when depth is required, since that is the scenario the credit rests on.
186
+ const proof = (needsDepth ? coverers.find((s) => s.hasDataAssertion) : undefined) ?? coverers[0];
187
+ const credit = (): void => {
188
+ covered++;
189
+ coveredBy.push({
190
+ theme: t.theme,
191
+ by: proof.haystack,
192
+ // `proof` came out of `coverers`, which was filtered on this same predicate, so the find
193
+ // always lands — the `!` states that rather than inventing a fallback keyword that would
194
+ // misattribute the credit if the invariant ever broke.
195
+ keyword: t.keywords.find((k) => keywordHit(proof.haystack, k))!,
196
+ });
197
+ };
198
+ if (!needsDepth) { credit(); continue; }
199
+ if (coverers.some((s) => s.hasDataAssertion)) credit();
183
200
  else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
184
201
  }
185
202
  }
@@ -203,6 +220,7 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
203
220
  coverageRatio: total ? covered / total : 1,
204
221
  gaps,
205
222
  universalGaps,
223
+ coveredBy,
206
224
  };
207
225
  }
208
226
 
@@ -269,7 +287,7 @@ export function assertionDepth(
269
287
  const isDismissBehavior = (s: ScenarioInfo) =>
270
288
  /\bis hidden\b|\bare hidden\b|\bdismiss|\bdisappear|\bno longer (visible|shown|present|displayed)\b|\bcloses? the (modal|dialog|popup|overlay|panel|menu)\b/.test(s.haystack);
271
289
  const isDepthRequired = (s: ScenarioInfo) =>
272
- s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => s.haystack.includes(k.toLowerCase()));
290
+ s.category !== 'NAV' && !isDismissBehavior(s) && depthKeywords.some((k) => keywordHit(s.haystack, k));
273
291
 
274
292
  const required = nonManual.filter(isDepthRequired);
275
293
  const reqShallow = required.filter((s) => s.shallow);
@@ -347,7 +365,7 @@ export function flowCoveredThemes(
347
365
  ): { theme: string; flow: string }[] {
348
366
  const out: { theme: string; flow: string }[] = [];
349
367
  for (const g of gaps) {
350
- const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
368
+ const hit = flowScenarios.find((s) => s.deep && g.keywords.some((k) => keywordHit(s.haystack, k)));
351
369
  if (hit) out.push({ theme: g.theme, flow: hit.flow });
352
370
  }
353
371
  return out;
@@ -394,7 +412,8 @@ export function flowRegressionDepth(scenarios: ScenarioInfo[]): FlowDepthResult
394
412
  export interface OracleStrengthResult {
395
413
  weak: { name: string; hint: string; code?: string }[]; // facet-by-name-substring, or URL-only (code ORACLE-URL-ONLY)
396
414
  facetClaims: number; // scenarios that touch a category/brand facet (denominator)
397
- ratio: number; // 1 - weak/facetClaims (1 when none) — caps businessDepth
415
+ weakFacet: number; // the facet-class subset of `weak` — the only numerator `facetClaims` denominates
416
+ ratio: number; // 1 - weakFacet/facetClaims, clamped 0..1 (1 when no facet claim) — caps businessDepth
398
417
  }
399
418
 
400
419
  // "see all [<item name/title>] ... contain(s) {{<facet>.term}}" — asserting every item's NAME carries
@@ -429,9 +448,11 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
429
448
  hint: `every \`Then\` is a URL assertion${guardLike ? ' on a permission / guard scenario' : ''} — it proves the router pushed a path, not that the page rendered for this user${guardLike ? ', and this is exactly the scenario a late 403 redirect turns falsely green' : ''}. Give the page selector an \`anchor:\` (a heading/landmark only the rendered page has) — one line per screen, and every \`see [X] page\` on it becomes a content oracle — or add a content assertion after it.`,
430
449
  });
431
450
  }
451
+ let weakFacet = 0;
432
452
  for (const s of scenarios) {
433
453
  if (s.manual) continue; // a @manual facet check is a deliberate deferral, not a weak automated oracle
434
454
  if (WEAK_FACET_ORACLE.test(s.stepsText)) {
455
+ weakFacet++;
435
456
  weak.push({
436
457
  name: s.name.slice(0, 80),
437
458
  hint: 'asserting every item NAME contains a category/brand term does not prove facet membership — assert the results-page TITLE/header carries the facet, a detail-page facet field, or an API/DB oracle; or defer the exhaustive check to @manual:M2.',
@@ -439,7 +460,17 @@ export function oracleStrength(scenarios: ScenarioInfo[]): OracleStrengthResult
439
460
  }
440
461
  }
441
462
  const facetClaims = scenarios.filter((s) => FACET_REF.test(s.stepsText) || FACET_REF.test(s.name)).length;
442
- return { weak, facetClaims, ratio: facetClaims ? 1 - weak.length / Math.max(1, facetClaims) : 1 };
463
+ // Only the FACET class is denominated by `facetClaims`. `weak` also carries ORACLE-URL-ONLY
464
+ // findings (#662), which make no category/brand claim at all — averaging them into this axis
465
+ // drove `ratio` negative (5 URL-only over 2 facet claims → 1 - 5/2 = -1.5), which walked through
466
+ // the businessDepth floor and took the CLI's bar renderer down with it (#762). A URL-only oracle
467
+ // is NAMED as a finding and holds the score out of the senior band (`seniorBandedOverall`);
468
+ // it is not averaged into the facet axis — the invariant `url-only-oracle.run.ts` asserts.
469
+ // The clamp is not belt-and-braces: a weak-facet scenario need not itself satisfy FACET_REF
470
+ // (`see all [name] contains {{price.term}}` is weak but claims no facet), so `weakFacet` can
471
+ // still outrun `facetClaims`. This axis is declared 0..1; it leaves here inside 0..1.
472
+ const ratio = facetClaims ? Math.max(0, Math.min(1, 1 - weakFacet / facetClaims)) : 1;
473
+ return { weak, facetClaims, weakFacet, ratio };
443
474
  }
444
475
 
445
476
  /** Collect data-correctness themes (depth.requires) for a page-type + universal. */
@@ -603,6 +634,9 @@ export interface TraceResult {
603
634
  mappedToOverview: number;
604
635
  withVpCodeRatio: number;
605
636
  mappedRatio: number;
637
+ /** Viewpoint IDs read from test-viewpoint.md. 0 = nothing to map to, so `mappedRatio` is not a
638
+ * measurement — the scenario IDs were never checked (#832). */
639
+ overviewIds: number;
606
640
  note: string;
607
641
  }
608
642
 
@@ -614,6 +648,24 @@ export interface TraceResult {
614
648
  */
615
649
  const unprefixed = (id: string): string => id.replace(/^(?:VP|FL)-/i, '').toUpperCase();
616
650
 
651
+ /**
652
+ * The traceability note, worded from the SAME ratio the audit's TRACE finding fires on —
653
+ * mapped / ALL scenarios (#852). It used to compare mapped against scenarios WITH an id, so 0 of
654
+ * 0 (no scenario yet, or none tagged) and 2 of 2 tagged out of 10 all read "Traceable." while
655
+ * the audit reported `TRACE: Traceable.` for a 0–20% mapping.
656
+ */
657
+ function traceNote(total: number, withCode: number, mapped: number, declared: number, exampleId?: string): string {
658
+ if (declared === 0) {
659
+ return 'No viewpoint ID found in test-viewpoint.md — the file is missing, or none of its sections declares one — so the scenario IDs were not checked. Declarations are read from a `## Priority Viewpoints` table, `### Required / Recommended / Optional` lists, and tables whose first column is a `VP-`/`FL-` ID or an ID of a family declared there (Known Issues / Traceability / References sections are skipped).';
660
+ }
661
+ if (total === 0) return 'No scenarios yet — traceability is measured once the feature has scenarios.';
662
+ if (mapped >= total * 0.5) return 'Traceable.';
663
+ const example = exampleId ? ` (e.g. \`${exampleId}-001 …\`)` : '';
664
+ if (withCode === 0) return `No scenario carries a viewpoint ID — start each title with one of the IDs declared in test-viewpoint.md${example}.`;
665
+ if (mapped < withCode * 0.5) return 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).';
666
+ return `Only ${mapped} of ${total} scenarios carry a declared viewpoint ID — ${total - withCode} have no ID at all; tag them with the project's IDs${example}.`;
667
+ }
668
+
617
669
  export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[]): TraceResult {
618
670
  const overviewIds = new Set(viewpoints.map((v) => v.id.toUpperCase()));
619
671
  const overviewBare = new Set(viewpoints.map((v) => unprefixed(v.id)));
@@ -638,9 +690,10 @@ export function traceability(scenarios: ScenarioInfo[], viewpoints: ViewpointEnt
638
690
  mappedToOverview: mapped.length,
639
691
  withVpCodeRatio: scenarios.length ? withCode.length / scenarios.length : 0,
640
692
  mappedRatio: scenarios.length ? mapped.length / scenarios.length : 0,
641
- note: withCode.length && mapped.length < withCode.length * 0.5
642
- ? 'Scenario IDs do not match the viewpoint-overview ids (weak traceability — re-tag to the project viewpoint IDs).'
643
- : 'Traceable.',
693
+ overviewIds: overviewIds.size,
694
+ // With no ID read there is nothing to match against: telling the author to re-tag sends them
695
+ // to change scenarios that may be right (#832). Name the file and the sections read instead.
696
+ note: traceNote(scenarios.length, withCode.length, mapped.length, overviewIds.size, viewpoints[0]?.id),
644
697
  };
645
698
  }
646
699
 
@@ -702,6 +755,31 @@ const CLAIM_RULES: ClaimRule[] = [
702
755
  hint: 'swap `contains {{v}}` for `with {{v}}` on the continuity rows. A substring match cannot distinguish "unchanged" from "changed by adding a prefix" — and if the screen genuinely renders extra text around the value, say so in the title instead of claiming exactness.',
703
756
  severity: 'warn',
704
757
  },
758
+ {
759
+ // An ORDER claim ("sorted by date descending", "ties broken by last-updated") is invisible to
760
+ // a presence check: `see [List] table` passes whatever order the rows are in. It sits ahead of
761
+ // the generic rules so "all results sorted …" is judged as an ordering claim, not an all-claim.
762
+ // A tiebreaker is a Tier-1 guard in the tc-generation skill, yet no rule recognised it (#810).
763
+ claim: 'ordering/sort',
764
+ title: /\b(ascending|descending|sorted|sorts|sort order|ordered by|order by|in (?:chronological|alphabetical|reverse|numeric) order|(?:newest|oldest|latest|earliest|highest|lowest) first|tie-?break(?:s|er|ers)?|ties?)\b|降順|昇順|並び順|giảm dần|tăng dần|sắp xếp/,
765
+ proof: /match data|is sorted|sorted (?:ascending|descending)|\brow \d+\b|\.first\.|\.last\.|\[\d+\]\.|order by/,
766
+ need: 'an order-sensitive assertion (`table match data:` rows in the expected order, `is sorted ascending|descending`, or positional `row N` checks)',
767
+ hint: 'a presence check passes in any order — assert the rows in their expected sequence (`User see [T] table match data:`), or the column sort state; a tiebreak needs two rows sharing the primary key.',
768
+ severity: 'fail',
769
+ },
770
+ {
771
+ // A LIMIT claim ("never exceeds 10", "at most 5 files") is a count claim in disguise.
772
+ // Ahead of 'no-side-effect', whose negation+effect pattern would otherwise swallow
773
+ // "never exceeds 10 records" and ask for a duplicate-count proof.
774
+ claim: 'limit/cap',
775
+ // The quantity must be ≥2 ("at most ONE order" is a no-duplicate claim — left to the rules
776
+ // below) and not a duration ("up to 30 seconds" is a timing claim, not a cap).
777
+ title: /\b(?:never exceeds?|(?:does not|doesn['’]t|not) exceed|at most|no more than|up to|capped at|caps? at|maximum of|max(?:imum)?)\s+(?:\d{2,}|[2-9]|two|three|four|five|six|seven|eight|nine|ten)\b(?!\s*(?:ms|milliseconds?|s|secs?|seconds?|mins?|minutes?|hours?|days?)\b)|\b(?:never exceeds?|(?:does not|doesn['’]t) exceed) (?:the )?(?:limit|cap|quota|maximum)\b|以内|上限|tối đa/,
778
+ proof: /\bcount\b|tohavecount|table with|row with \{\{|match data|is at most|\bexactly\b|field with \{\{|\berror\b|invalid|is disabled/,
779
+ need: 'a count/boundary assertion (count is N, `is at most`, the N+1th input rejected or truncated)',
780
+ hint: 'assert the boundary itself — the count at the cap, or the value/error when one more is attempted — not just that the list or field is present.',
781
+ severity: 'fail',
782
+ },
705
783
  {
706
784
  claim: 'all/every/each',
707
785
  title: /\b(all|every|each)\b/,
@@ -994,14 +1072,16 @@ export function numericClaimGaps(scenarios: ScenarioInfo[]): Array<{ name: strin
994
1072
  * The Priority-Viewpoints table is where release selection is supposed to come from. When the
995
1073
  * table says a flow is `Low` and its scenario is tagged `@high`, a smoke run picks up work the QA
996
1074
  * deliberately deprioritised — and nobody can tell which of the two was the decision (#622).
1075
+ *
1076
+ * Reads the SAME parse traceability does (`parseViewpointOverview`). It used to scan the raw text
1077
+ * for any `| VP-… | high |` row, so one audit run "saw" an id for this check and not for
1078
+ * traceability (#833), and a row in a Known Issues table could raise a contradiction.
997
1079
  */
998
1080
  export function priorityMismatches(
999
- viewpointText: string, scenarios: ScenarioInfo[],
1081
+ viewpoints: ViewpointEntry[], scenarios: ScenarioInfo[],
1000
1082
  ): Array<{ id: string; declared: string; tagged: string; scenario: string }> {
1001
1083
  const declared = new Map<string, string>();
1002
- for (const m of viewpointText.matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)) {
1003
- declared.set(m[1].toUpperCase(), m[2].toLowerCase());
1004
- }
1084
+ for (const v of viewpoints) if (v.priorityWord) declared.set(v.id.toUpperCase(), v.priorityWord);
1005
1085
  if (declared.size === 0) return [];
1006
1086
  // `medium` and `normal` are the same rank under different names in real documents.
1007
1087
  const rank = (p: string): string => (p === 'medium' ? 'normal' : p);
@@ -1014,8 +1094,9 @@ export function priorityMismatches(
1014
1094
  if (!key) continue;
1015
1095
  const want = rank(declared.get(key)!);
1016
1096
  const got = rank(String(s.priority).toLowerCase());
1017
- // `critical` and `deferred` have no tag equivalent — nothing to contradict.
1018
- if (want === 'critical' || want === 'deferred') continue;
1097
+ // `critical`, `deferred` and `skip` (declared not applicable, #853) have no tag equivalent —
1098
+ // nothing to contradict.
1099
+ if (want === 'critical' || want === 'deferred' || want === 'skip') continue;
1019
1100
  if (want !== got) out.push({ id: key, declared: declared.get(key)!, tagged: got, scenario: s.name });
1020
1101
  }
1021
1102
  return out;
@@ -16,6 +16,7 @@
16
16
  * explicit — a named heading, a table that declares conditions and outcomes, or an if/else
17
17
  * cascade — because a false "you missed a branch" costs more trust than it buys coverage.
18
18
  */
19
+ import { keywordHit } from './keyword-match';
19
20
  import { ScenarioInfo } from './parse';
20
21
 
21
22
  export interface SpecBranch {
@@ -172,7 +173,9 @@ export function unenumeratedBranches(branches: SpecBranch[], featureText: string
172
173
  const words = [...new Set((b.label.toLowerCase().match(/[a-z][a-z-]{3,}/g) ?? []))]
173
174
  .filter((w) => !GENERIC_WORD.has(w));
174
175
  if (words.length === 0) return !hay.includes(b.label.toLowerCase());
175
- const hits = words.filter((w) => hay.includes(w)).length;
176
+ // Word-aware (#779): `in·active` and `check·list` are not mentions of `active` and `list`,
177
+ // and two of them credited a branch no scenario cites.
178
+ const hits = words.filter((w) => keywordHit(hay, w)).length;
176
179
  return hits < Math.min(2, words.length);
177
180
  });
178
181
  }
@@ -191,7 +194,7 @@ export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: s
191
194
  if (s.manual) continue; // a procedure's oracle is prose
192
195
  const asserts = [...(s.steps ?? [])]
193
196
  .filter((st) => st.bucket === 'then')
194
- .map((st) => st.text.toLowerCase().replace(/\s+/g, ' ').trim())
197
+ .map((st) => `${st.text} ${st.arg ?? ''}`.toLowerCase().replace(/\s+/g, ' ').trim()) // the table is the oracle too (#817)
195
198
  .sort();
196
199
  if (asserts.length === 0) continue;
197
200
  const key = asserts.join(' ; ');
@@ -289,7 +292,7 @@ export function visibilityPairGaps(scenarios: ScenarioInfo[]): VisibilityPairGap
289
292
  const subject = distinctive(s);
290
293
  // The counterpart has to be about the SAME thing — two shared distinctive words, the same
291
294
  // bar the ledger's prose matching uses.
292
- const paired = allows.some((a) => subject.filter((w) => a.haystack.includes(w)).length >= Math.min(2, subject.length));
295
+ const paired = allows.some((a) => subject.filter((w) => keywordHit(a.haystack, w)).length >= Math.min(2, subject.length));
293
296
  if (!paired) out.push({ scenario: s.name, subject: subject.slice(0, 4), kind: 'permission' });
294
297
  }
295
298
  return out;