@sun-asterisk/sungen 3.2.31 → 3.2.33

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 (363) hide show
  1. package/dist/cli/commands/audit.d.ts +1 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +13 -2
  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 +46 -17
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/delivery.d.ts.map +1 -1
  9. package/dist/cli/commands/delivery.js +39 -18
  10. package/dist/cli/commands/delivery.js.map +1 -1
  11. package/dist/cli/commands/generate.d.ts.map +1 -1
  12. package/dist/cli/commands/generate.js +14 -7
  13. package/dist/cli/commands/generate.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2 -0
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  18. package/dist/cli/commands/results-merge.js +2 -12
  19. package/dist/cli/commands/results-merge.js.map +1 -1
  20. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  21. package/dist/cli/commands/results-sanitize.js +2 -13
  22. package/dist/cli/commands/results-sanitize.js.map +1 -1
  23. package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
  24. package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
  25. package/dist/cli/commands/setup-mobile-env.js +65 -0
  26. package/dist/cli/commands/setup-mobile-env.js.map +1 -0
  27. package/dist/cli/index.js +2 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  30. package/dist/dashboard/snapshot-builder.js +6 -10
  31. package/dist/dashboard/snapshot-builder.js.map +1 -1
  32. package/dist/exporters/feature-parser.d.ts +21 -0
  33. package/dist/exporters/feature-parser.d.ts.map +1 -1
  34. package/dist/exporters/feature-parser.js +32 -1
  35. package/dist/exporters/feature-parser.js.map +1 -1
  36. package/dist/exporters/locale-variants.d.ts +7 -1
  37. package/dist/exporters/locale-variants.d.ts.map +1 -1
  38. package/dist/exporters/locale-variants.js +23 -16
  39. package/dist/exporters/locale-variants.js.map +1 -1
  40. package/dist/exporters/matrix/build.d.ts.map +1 -1
  41. package/dist/exporters/matrix/build.js +13 -7
  42. package/dist/exporters/matrix/build.js.map +1 -1
  43. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  44. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  45. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  46. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  47. package/dist/exporters/matrix/export.d.ts +2 -0
  48. package/dist/exporters/matrix/export.d.ts.map +1 -1
  49. package/dist/exporters/matrix/export.js +7 -1
  50. package/dist/exporters/matrix/export.js.map +1 -1
  51. package/dist/exporters/matrix/wording.d.ts +7 -0
  52. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  53. package/dist/exporters/matrix/wording.js +27 -1
  54. package/dist/exporters/matrix/wording.js.map +1 -1
  55. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  56. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  57. package/dist/exporters/xlsx-exporter.js +7 -2
  58. package/dist/exporters/xlsx-exporter.js.map +1 -1
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  60. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  61. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  62. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  63. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  72. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  73. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  74. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  75. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  76. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  77. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  78. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  79. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  83. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  84. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  85. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  86. package/dist/generators/test-generator/code-generator.js +122 -15
  87. package/dist/generators/test-generator/code-generator.js.map +1 -1
  88. package/dist/generators/test-generator/diagnostics.d.ts +70 -4
  89. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  90. package/dist/generators/test-generator/diagnostics.js +69 -3
  91. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  92. package/dist/generators/test-generator/patterns/index.d.ts +13 -3
  93. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  94. package/dist/generators/test-generator/patterns/index.js +16 -10
  95. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  96. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  97. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  98. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  99. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  100. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  101. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  102. package/dist/generators/test-generator/step-mapper.d.ts +81 -0
  103. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  104. package/dist/generators/test-generator/step-mapper.js +187 -13
  105. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +18 -0
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  114. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  115. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  116. package/dist/generators/test-generator/utils/selector-resolver.d.ts +84 -4
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  118. package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
  119. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  120. package/dist/harness/audit.d.ts.map +1 -1
  121. package/dist/harness/audit.js +35 -8
  122. package/dist/harness/audit.js.map +1 -1
  123. package/dist/harness/capability.d.ts +7 -1
  124. package/dist/harness/capability.d.ts.map +1 -1
  125. package/dist/harness/capability.js +15 -12
  126. package/dist/harness/capability.js.map +1 -1
  127. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  128. package/dist/harness/config-file.d.ts +17 -0
  129. package/dist/harness/config-file.d.ts.map +1 -0
  130. package/dist/harness/config-file.js +100 -0
  131. package/dist/harness/config-file.js.map +1 -0
  132. package/dist/harness/depth-lint.d.ts.map +1 -1
  133. package/dist/harness/depth-lint.js +2 -1
  134. package/dist/harness/depth-lint.js.map +1 -1
  135. package/dist/harness/flow-check.d.ts.map +1 -1
  136. package/dist/harness/flow-check.js +6 -2
  137. package/dist/harness/flow-check.js.map +1 -1
  138. package/dist/harness/keyword-match.d.ts +59 -0
  139. package/dist/harness/keyword-match.d.ts.map +1 -0
  140. package/dist/harness/keyword-match.js +86 -0
  141. package/dist/harness/keyword-match.js.map +1 -0
  142. package/dist/harness/next-step.d.ts.map +1 -1
  143. package/dist/harness/next-step.js +2 -1
  144. package/dist/harness/next-step.js.map +1 -1
  145. package/dist/harness/parse.d.ts.map +1 -1
  146. package/dist/harness/parse.js +42 -7
  147. package/dist/harness/parse.js.map +1 -1
  148. package/dist/harness/perf.d.ts +7 -1
  149. package/dist/harness/perf.d.ts.map +1 -1
  150. package/dist/harness/perf.js +13 -10
  151. package/dist/harness/perf.js.map +1 -1
  152. package/dist/harness/quality-gates.d.ts +9 -2
  153. package/dist/harness/quality-gates.d.ts.map +1 -1
  154. package/dist/harness/quality-gates.js +16 -4
  155. package/dist/harness/quality-gates.js.map +1 -1
  156. package/dist/harness/query-catalog.js +0 -0
  157. package/dist/harness/query-catalog.js.map +1 -1
  158. package/dist/harness/read-text.d.ts +6 -0
  159. package/dist/harness/read-text.d.ts.map +1 -1
  160. package/dist/harness/read-text.js +7 -1
  161. package/dist/harness/read-text.js.map +1 -1
  162. package/dist/harness/secret-scan.d.ts +27 -1
  163. package/dist/harness/secret-scan.d.ts.map +1 -1
  164. package/dist/harness/secret-scan.js +57 -4
  165. package/dist/harness/secret-scan.js.map +1 -1
  166. package/dist/harness/sensors.d.ts +12 -0
  167. package/dist/harness/sensors.d.ts.map +1 -1
  168. package/dist/harness/sensors.js +41 -18
  169. package/dist/harness/sensors.js.map +1 -1
  170. package/dist/harness/spec-branches.d.ts +7 -20
  171. package/dist/harness/spec-branches.d.ts.map +1 -1
  172. package/dist/harness/spec-branches.js +85 -13
  173. package/dist/harness/spec-branches.js.map +1 -1
  174. package/dist/harness/spec-coverage.d.ts +16 -0
  175. package/dist/harness/spec-coverage.d.ts.map +1 -1
  176. package/dist/harness/spec-coverage.js +131 -12
  177. package/dist/harness/spec-coverage.js.map +1 -1
  178. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  179. package/dist/harness/viewpoint-ledger.js +2 -1
  180. package/dist/harness/viewpoint-ledger.js.map +1 -1
  181. package/dist/harness/write-atomic.d.ts +2 -0
  182. package/dist/harness/write-atomic.d.ts.map +1 -0
  183. package/dist/harness/write-atomic.js +62 -0
  184. package/dist/harness/write-atomic.js.map +1 -0
  185. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  186. package/dist/orchestrator/ai-skills/writer.js +15 -7
  187. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  188. package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
  189. package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
  190. package/dist/orchestrator/appium-environment-setup.js +157 -0
  191. package/dist/orchestrator/appium-environment-setup.js.map +1 -0
  192. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  193. package/dist/orchestrator/assets-drift.js +11 -6
  194. package/dist/orchestrator/assets-drift.js.map +1 -1
  195. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
  196. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  197. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  198. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  199. package/dist/orchestrator/project-deps-sync.js +22 -1
  200. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  201. package/dist/orchestrator/project-initializer.d.ts +9 -0
  202. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  203. package/dist/orchestrator/project-initializer.js +63 -7
  204. package/dist/orchestrator/project-initializer.js.map +1 -1
  205. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  206. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  207. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  208. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  209. package/dist/orchestrator/security-overrides.d.ts +51 -0
  210. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  211. package/dist/orchestrator/security-overrides.js +165 -0
  212. package/dist/orchestrator/security-overrides.js.map +1 -0
  213. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  214. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  215. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  216. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  217. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  218. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  219. package/dist/orchestrator/templates/appium-environment/README.md +83 -0
  220. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  221. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  222. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  223. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  224. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  225. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  226. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  227. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  228. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  229. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  230. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  231. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  232. package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  233. package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  234. package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  235. package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
  236. package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
  237. package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
  238. package/dist/orchestrator/templates/specs-checked-state.js +26 -3
  239. package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
  240. package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
  241. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  242. package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
  243. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
  244. package/dist/orchestrator/templates/specs-mock.js +10 -3
  245. package/dist/orchestrator/templates/specs-mock.js.map +1 -1
  246. package/dist/orchestrator/templates/specs-mock.ts +12 -4
  247. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  248. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  249. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  250. package/dist/orchestrator/templates/specs-url-assert.js +14 -2
  251. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  252. package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
  253. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  254. package/package.json +3 -3
  255. package/src/cli/commands/audit.ts +12 -3
  256. package/src/cli/commands/capability.ts +63 -19
  257. package/src/cli/commands/delivery.ts +38 -16
  258. package/src/cli/commands/generate.ts +15 -7
  259. package/src/cli/commands/init.ts +3 -0
  260. package/src/cli/commands/results-merge.ts +2 -8
  261. package/src/cli/commands/results-sanitize.ts +2 -9
  262. package/src/cli/commands/setup-mobile-env.ts +64 -0
  263. package/src/cli/index.ts +2 -0
  264. package/src/dashboard/snapshot-builder.ts +6 -8
  265. package/src/exporters/feature-parser.ts +32 -2
  266. package/src/exporters/locale-variants.ts +23 -17
  267. package/src/exporters/matrix/build.ts +14 -8
  268. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  269. package/src/exporters/matrix/export.ts +10 -4
  270. package/src/exporters/matrix/wording.ts +26 -1
  271. package/src/exporters/xlsx-exporter.ts +7 -2
  272. package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
  273. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  274. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  275. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  276. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  277. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  278. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  279. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  280. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  281. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  282. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  283. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  284. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  285. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  286. package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  287. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
  288. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  289. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  290. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  291. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  292. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  293. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  294. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  295. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  296. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
  297. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  298. package/src/generators/test-generator/code-generator.ts +119 -19
  299. package/src/generators/test-generator/diagnostics.ts +70 -4
  300. package/src/generators/test-generator/patterns/index.ts +16 -9
  301. package/src/generators/test-generator/patterns/types.ts +15 -0
  302. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  303. package/src/generators/test-generator/step-mapper.ts +196 -13
  304. package/src/generators/test-generator/template-engine.ts +18 -0
  305. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  306. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  307. package/src/generators/test-generator/utils/selector-resolver.ts +256 -85
  308. package/src/harness/audit.ts +41 -12
  309. package/src/harness/capability.ts +23 -12
  310. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  311. package/src/harness/config-file.ts +66 -0
  312. package/src/harness/depth-lint.ts +2 -1
  313. package/src/harness/flow-check.ts +6 -2
  314. package/src/harness/keyword-match.ts +85 -0
  315. package/src/harness/next-step.ts +2 -1
  316. package/src/harness/parse.ts +42 -7
  317. package/src/harness/perf.ts +15 -7
  318. package/src/harness/quality-gates.ts +16 -4
  319. package/src/harness/query-catalog.ts +0 -0
  320. package/src/harness/read-text.ts +7 -1
  321. package/src/harness/secret-scan.ts +60 -4
  322. package/src/harness/sensors.ts +50 -19
  323. package/src/harness/spec-branches.ts +72 -14
  324. package/src/harness/spec-coverage.ts +133 -13
  325. package/src/harness/viewpoint-ledger.ts +2 -1
  326. package/src/harness/write-atomic.ts +22 -0
  327. package/src/orchestrator/ai-skills/writer.ts +18 -3
  328. package/src/orchestrator/appium-environment-setup.ts +127 -0
  329. package/src/orchestrator/assets-drift.ts +10 -5
  330. package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
  331. package/src/orchestrator/project-deps-sync.ts +23 -1
  332. package/src/orchestrator/project-initializer.ts +69 -7
  333. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  334. package/src/orchestrator/security-overrides.ts +149 -0
  335. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  336. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
  337. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
  338. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  339. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  340. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  341. package/src/orchestrator/templates/appium-environment/README.md +83 -0
  342. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  343. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  344. package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  345. package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  346. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  347. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  348. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  349. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  350. package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  351. package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  352. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  353. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  354. package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  355. package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  356. package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  357. package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
  358. package/src/orchestrator/templates/specs-checked-state.ts +31 -6
  359. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  360. package/src/orchestrator/templates/specs-mock.ts +12 -4
  361. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  362. package/src/orchestrator/templates/specs-url-assert.ts +24 -2
  363. package/src/orchestrator/templates/wdio.conf.ts +24 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Reading a config file has THREE outcomes, not two: absent, parsed, and present-but-unreadable.
3
+ *
4
+ * Collapsing the third into the first is the defect class #782 catalogues. The shape recurs because
5
+ * it looks harmless: an `fs.existsSync` guard already answers absence, so the `catch` below it only
6
+ * ever fires for CORRUPTION — and returning the absent value there is the one case that must not be
7
+ * silent. A file that is absent was never written; a file that is broken WAS written, by someone who
8
+ * believes it is in effect.
9
+ *
10
+ * `run-manifest.ts:hashTopLevel` already draws the distinction for its own reader (`__unparseable__`
11
+ * rather than `{}`); this is that idea as a seam every config reader can share, so the next one does
12
+ * not have to rediscover it.
13
+ *
14
+ * Nothing here throws: the caller owns the consequence, because only the caller knows it. What the
15
+ * type removes is the option of not deciding.
16
+ */
17
+ import * as fs from 'fs';
18
+ import { parse as parseYaml } from 'yaml';
19
+
20
+ export type ConfigRead<T> =
21
+ | { state: 'absent' }
22
+ | { state: 'ok'; value: T }
23
+ | { state: 'unreadable'; error: string };
24
+
25
+ const message = (e: unknown): string => (e instanceof Error ? e.message.split('\n')[0] : String(e));
26
+
27
+ /** Read + parse a YAML config. An empty or comment-only file parses to null — that is `ok`, not broken. */
28
+ export function readYamlConfig<T>(file: string): ConfigRead<T> {
29
+ if (!fs.existsSync(file)) return { state: 'absent' };
30
+ try {
31
+ return { state: 'ok', value: (parseYaml(fs.readFileSync(file, 'utf-8')) ?? null) as T };
32
+ } catch (e) {
33
+ return { state: 'unreadable', error: message(e) };
34
+ }
35
+ }
36
+
37
+ /** Read + parse a JSON config. Unlike YAML, an empty file is NOT valid JSON and reads as unreadable. */
38
+ export function readJsonConfig<T>(file: string): ConfigRead<T> {
39
+ if (!fs.existsSync(file)) return { state: 'absent' };
40
+ try {
41
+ return { state: 'ok', value: JSON.parse(fs.readFileSync(file, 'utf-8')) as T };
42
+ } catch (e) {
43
+ return { state: 'unreadable', error: message(e) };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * One line to stderr naming the file, the parse error, and WHAT THE RUN WILL DO INSTEAD — the last
49
+ * part is the whole point: "capabilities.yaml is broken" leaves the reader guessing; "…so this run
50
+ * treats the project as web" tells them why their mobile suite just compiled wrong.
51
+ *
52
+ * At most once per file per process. `readCapabilities` alone has fifteen call sites, and a warning
53
+ * repeated fifteen times reads as noise — which is its own way of being ignored.
54
+ */
55
+ const warned = new Set<string>();
56
+
57
+ export function warnUnreadableConfig(file: string, error: string, consequence: string): void {
58
+ if (warned.has(file)) return;
59
+ warned.add(file);
60
+ console.error(`⚠️ ${file}: unreadable (${error}) — ${consequence}`);
61
+ }
62
+
63
+ /** Test seam: the dedup set is process-global, so a suite exercising several cases must clear it. */
64
+ export function resetUnreadableConfigWarnings(): void {
65
+ warned.clear();
66
+ }
@@ -15,6 +15,7 @@ import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import { loadUnitScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
17
17
  import { loadCatalog, viewpointGate, assertionDepth, dataThemesFor, CatalogTheme } from './sensors';
18
+ import { keywordHit } from './keyword-match';
18
19
 
19
20
  export type DepthAction = 'deepen' | 'defer';
20
21
 
@@ -45,7 +46,7 @@ export interface DepthLintReport {
45
46
  function matchTheme(s: ScenarioInfo, dataThemes: CatalogTheme[]): CatalogTheme | undefined {
46
47
  return dataThemes.find((t) => {
47
48
  const kws = t.depth?.keywords?.length ? t.depth.keywords : t.keywords;
48
- return kws.some((k) => s.haystack.includes(k.toLowerCase()));
49
+ return kws.some((k) => keywordHit(s.haystack, k));
49
50
  });
50
51
  }
51
52
 
@@ -12,6 +12,7 @@
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import { featureFilesFor } from './unit-paths';
15
+ import { keywordHit } from './keyword-match';
15
16
  import { loadScenarios } from './parse';
16
17
  import { parseScenarios } from './capability-plan';
17
18
  import { readTextFile } from './read-text';
@@ -45,7 +46,10 @@ function featureTags(fp: string): string[] {
45
46
  }
46
47
  function targetsFromHint(hint: string): string[] {
47
48
  const h = hint.toLowerCase();
48
- const hits = KNOWN_TARGETS.filter((k) => h.includes(k));
49
+ // Word-aware (#779): every KNOWN_TARGET is an ordinary noun, so a substring test read
50
+ // `re·order` / `dis·order` as the target `order` and then matched the two to each other —
51
+ // a deferral about reorder history reported COVERED by a flow scenario about a disorder.
52
+ const hits = KNOWN_TARGETS.filter((k) => keywordHit(h, k));
49
53
  if (hits.length) return hits;
50
54
  // fallback: significant words after "->"
51
55
  const after = h.split('->').pop() || h;
@@ -87,7 +91,7 @@ export function buildFlowCheck(cwd: string, onlyFlow?: string): FlowCheckReport
87
91
  // `@owned-by:<flow>` names the owner explicitly → only that flow's scenarios can cover it
88
92
  // (a false @owned-by is then surfaced as missing). Else any flow may cover it (legacy).
89
93
  const pool = s.ownedBy ? flowScenarios.filter((fs2) => fs2.flow === s.ownedBy) : flowScenarios;
90
- const matches = pool.filter((fs2) => targets.some((t) => fs2.haystack.includes(t)));
94
+ const matches = pool.filter((fs2) => targets.some((t) => keywordHit(fs2.haystack, t)));
91
95
  let verdict: Deferral['verdict'] = 'missing';
92
96
  let via: string | undefined;
93
97
  if (matches.some((m) => m.deep)) { verdict = 'covered'; via = matches.find((m) => m.deep)!.flow; }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Keyword matching for the audit's coverage sensors — the single rule every "does a scenario
3
+ * mention this?" question goes through.
4
+ *
5
+ * WHY THIS EXISTS (#776). The sensors used to ask `haystack.includes(keyword)`. A substring match
6
+ * is satisfied by a keyword buried inside an unrelated word, and it fails in the direction that
7
+ * HIDES problems: a theme nobody tested reads as covered. Measured case — the scenario title
8
+ * "…five steps from email through to information entry" made `format-boundary` covered on a screen
9
+ * with no text input at all, because in·format·ion contains `format`. That one word moved the
10
+ * screen from 7.9 FAIL to 8.9 PASS.
11
+ *
12
+ * THE RULE: anchor a word boundary at the START of the keyword, leave the END free.
13
+ *
14
+ * Not `\b…\b` at both ends, which is the obvious fix and is wrong. Every collision this defect
15
+ * produces is a PREFIX collision — the innocent word carries extra letters BEFORE the keyword:
16
+ *
17
+ * format ← in·format·ion, ·format·ting range ← ar·range·ment, o·range
18
+ * saved ← un·saved created ← re·created
19
+ * success ← un·success·ful length ← wave·length
20
+ *
21
+ * The `unsaved` / `unsuccessful` pair is the sharp end: the prefix INVERTS the meaning, so a
22
+ * scenario asserting a submit FAILED is what marks the submit-SUCCESS theme covered.
23
+ *
24
+ * Meanwhile English inflection lives entirely in the SUFFIX, and a trailing `\b` destroys it:
25
+ * `submit` stops matching "submitted", `success` stops matching "successfully", `valid` stops
26
+ * matching "validation", `create` stops matching "created". Those are true positives, and a
27
+ * both-ends boundary turns 4 of 5 of them into false gaps — trading a silent failure for a noisy
28
+ * one. A leading boundary alone kills all six collisions above and keeps all of them.
29
+ *
30
+ * WHAT THIS RULE DOES NOT FIX, stated rather than implied. A free suffix still admits a collision
31
+ * when the keyword is a true PREFIX of a common unrelated word:
32
+ *
33
+ * feed → "feedback" list → "listener" length → "lengthy"
34
+ *
35
+ * No boundary rule can separate `feed` from `feedback`, so the cure is a less ambiguous KEYWORD,
36
+ * which is a catalog decision rather than a matcher one. #778 took it: the shipped catalog no
37
+ * longer carries any of the three bare forms — `"product list"`, `"feed item"` and
38
+ * `"max length"` / `"min length"` replaced them, and `cart-correctness` lost bare `price` and
39
+ * `quantity` (ordinary listing vocabulary that credited a cart theme on a screen with no cart).
40
+ *
41
+ * That closes today's instances, not the class. `tests/harness/keyword-word-boundary.run.ts`
42
+ * section (e) sweeps every single-word ASCII keyword in the catalog — theme `keywords`,
43
+ * `detect_keywords` and `depth.keywords` alike — so a NEW loose keyword fails the suite instead of
44
+ * entering silently. Adding one is possible, but only as a conscious act via that file's
45
+ * `KNOWN_LOOSE` map, which #778 left empty.
46
+ *
47
+ * NON-ASCII KEYWORDS fall back to substring, on purpose. JavaScript's `\b` is defined against
48
+ * `[A-Za-z0-9_]`, so `\bログイン` and `\bđăng nhập` never match anything at all — the anchor would
49
+ * silently disable the keyword rather than tighten it. Japanese and Chinese have no word
50
+ * boundaries to anchor to, which is precisely where substring matching is the CORRECT behaviour
51
+ * and the false-positive risk is low. Today every keyword reaching here is ASCII by construction
52
+ * (the catalog is English; the ledger and spec sensors extract words with `[a-z]` character
53
+ * classes), so this branch is a guard against a future non-ASCII catalog entry failing closed.
54
+ */
55
+
56
+ /** True when `kw`'s first character can carry a `\b` anchor — i.e. it is an ASCII word character. */
57
+ const anchorable = (kw: string): boolean => /^[a-z0-9_]/i.test(kw);
58
+
59
+ const cache = new Map<string, RegExp | null>();
60
+
61
+ /**
62
+ * Compile `kw` to its matcher, or `null` when the keyword must be compared as a plain substring.
63
+ * Cached: the sensors re-ask the same handful of catalog keywords once per scenario.
64
+ */
65
+ function matcherFor(kw: string): RegExp | null {
66
+ const key = kw.toLowerCase();
67
+ if (cache.has(key)) return cache.get(key)!;
68
+ const re = anchorable(key)
69
+ ? new RegExp(`\\b${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
70
+ : null;
71
+ cache.set(key, re);
72
+ return re;
73
+ }
74
+
75
+ /** Does `haystack` mention `kw` as a word (not as the tail of a longer one)? */
76
+ export function keywordHit(haystack: string, kw: string): boolean {
77
+ const h = haystack.toLowerCase();
78
+ const re = matcherFor(kw);
79
+ return re ? re.test(h) : h.includes(kw.toLowerCase());
80
+ }
81
+
82
+ /** Does ANY of `haystacks` mention `kw`? */
83
+ export function hasKeyword(haystacks: string[], kw: string): boolean {
84
+ return haystacks.some((h) => keywordHit(h, kw));
85
+ }
@@ -167,7 +167,8 @@ const REPAIRABLE = [
167
167
  'FLOW-CONTRACT-MISSING', 'FLOW-INVENTORY-MISSING', 'FLOW-UNCOVERED', 'FLOW-UNDECLARED',
168
168
  'FLOW-PHASE-MISFILED', 'FLOW-GUARANTEE-MISSING', 'FLOW-OUTCOME-UNPROVEN', 'CONTINUITY-ONE-SIDED',
169
169
  'ORACLE-URL-ONLY',
170
- 'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'TRIGGER-UNCOVERED',
170
+ 'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'SPEC-CLAUSE-UNCOVERED',
171
+ 'TRIGGER-UNCOVERED', 'PERMISSION-PAIR-MISSING', 'VISIBILITY-PAIR-MISSING',
171
172
  'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
172
173
  ];
173
174
 
@@ -78,6 +78,32 @@ function isViewpointId(s: string): boolean {
78
78
  return isIdLike(s) || /^(?:VP|FL)-[A-Z][A-Z0-9]*$/i.test(s);
79
79
  }
80
80
 
81
+ /**
82
+ * A markdown heading's TEXT, normalised the way the ledger already normalises it.
83
+ *
84
+ * Headings in a real viewpoint file are numbered ("## 2. Priority Viewpoints") and often bolded
85
+ * ("## **Priority Viewpoints**"), and both forms are what the authoring template produces. Every
86
+ * pass below anchored on the bare literal, so a numbered file parsed to ZERO viewpoints: the
87
+ * priority table was dropped whole, clean rows included, and the audit lost its traceability
88
+ * table. `viewpoint-ledger.ts` learned the same lesson at its own heading test (#657); this is
89
+ * that normalisation, applied where the overview reads headings.
90
+ *
91
+ * Returns null for a line that is not a heading, so a caller can tell "not a heading" from
92
+ * "a heading whose text is empty".
93
+ */
94
+ function headingParts(line: string): { level: number; text: string } | null {
95
+ const m = line.match(/^(#{1,6})\s+(.*)$/);
96
+ if (!m) return null;
97
+ const text = m[2]
98
+ .replace(/[*`]/g, '')
99
+ // Multi-level (`2.1`, `3.2.4`) and roman numbering are at least as common in a numbered spec as
100
+ // a single digit, and a strip that only handled one level left `## 2.1 Priority Viewpoints`
101
+ // reading as `1 Priority Viewpoints` — the same zero-viewpoint failure, one dot away.
102
+ .replace(/^\s*(?:\d+(?:\.\d+)*|[IVXLCDM]+|[A-Z])\s*[.):]?\s+/, '')
103
+ .trim();
104
+ return { level: m[1].length, text };
105
+ }
106
+
81
107
  export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
82
108
  if (!fs.existsSync(filePath)) return [];
83
109
  const text = readTextFile(filePath);
@@ -89,12 +115,15 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
89
115
  let inPriorityTable = false;
90
116
  for (const raw of lines) {
91
117
  const line = raw.trim();
92
- if (/^##\s+Priority Viewpoints/i.test(line)) { inPriorityTable = true; continue; }
93
- if (inPriorityTable && /^##\s/.test(line)) { inPriorityTable = false; }
118
+ const heading = headingParts(line);
119
+ if (heading && heading.level === 2 && /^Priority Viewpoints/i.test(heading.text)) { inPriorityTable = true; continue; }
120
+ if (inPriorityTable && heading && heading.level === 2) { inPriorityTable = false; }
94
121
  if (inPriorityTable && line.startsWith('|')) {
95
122
  const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
96
123
  if (cells.length >= 3) {
97
- const id = cells[0];
124
+ // The same `**` the heading carries: ids are routinely written `| **VP-LOGIC** | High | … |`,
125
+ // and an id read with its emphasis markers still attached matches no scenario tag.
126
+ const id = cells[0].replace(/[*`]/g, '').trim();
98
127
  if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
99
128
  const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
100
129
  entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
@@ -110,7 +139,10 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
110
139
  // FL-HP-001 were reported as unmapped. Both forms are additive here.
111
140
  for (const raw of lines) {
112
141
  const line = raw.trim();
113
- const section = line.match(/^##\s+([A-Z]{2,}(?:-[A-Z0-9]{2,})*)\s+[—–-]\s*(.*)$/);
142
+ const head = headingParts(line);
143
+ const section = head && head.level === 2
144
+ ? head.text.match(/^([A-Z]{2,}(?:-[A-Z0-9]{2,})*)\s+[—–-]\s*(.*)$/)
145
+ : null;
114
146
  if (section && isViewpointId(section[1] + '-0')) {
115
147
  const id = section[1].toUpperCase();
116
148
  if (!entries.has(id)) entries.set(id, { id, priority: 'Unknown', reason: section[2] ?? '' });
@@ -130,11 +162,14 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
130
162
  let group: ViewpointEntry['group'] | undefined;
131
163
  for (const raw of lines) {
132
164
  const line = raw.trim();
133
- const g = line.match(/^###\s+(Required|Recommended|Optional)/i);
165
+ const head = headingParts(line);
166
+ const g = head && head.level === 3 ? head.text.match(/^(Required|Recommended|Optional)/i) : null;
134
167
  if (g) { group = (g[1][0].toUpperCase() + g[1].slice(1).toLowerCase()) as any; continue; }
135
- if (/^##\s/.test(line)) { group = undefined; }
168
+ if (head && head.level === 2) { group = undefined; }
136
169
  if (group) {
137
- const m = line.match(/^[-*+]\s+([A-Za-z][A-Za-z0-9.-]*)/);
170
+ // `**` and backticks only — a single `*` is a bullet marker here, and stripping it would
171
+ // stop `* VP-A-001` being a bullet at all.
172
+ const m = line.replace(/\*\*|`/g, '').match(/^[-*+]\s+([A-Za-z][A-Za-z0-9.-]*)/);
138
173
  if (m && isViewpointId(m[1])) {
139
174
  const id = m[1].toUpperCase();
140
175
  const existing = entries.get(id);
@@ -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,6 +5,7 @@
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';
10
11
  import { readTextFile } from './read-text';
@@ -57,12 +58,19 @@ function downstreamRoutes(specText: string): string[] {
57
58
  }
58
59
 
59
60
  /**
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
61
+ * H5 — a stateful (cart-mutating) flow with NO per-scenario isolation is flaky: scenarios share
62
+ * state, so cart count/quantity asserts race. Mitigations: @cleanup:cart, @isolate, a fresh
62
63
  * browser context, or a "Given … empty cart" background. Returns true when the risk is unmitigated.
64
+ *
65
+ * #759 — this used to require a literal `@parallel`, which made the gate miss the common case.
66
+ * Parallel is the DEFAULT (code-generator.ts: `isParallelFeature = !isSerialFeature`); `@parallel`
67
+ * only NAMES that default and changes no codegen, so an untagged stateful flow is isolated exactly
68
+ * like a tagged one and carries exactly the same risk. The tag that actually changes the emitted
69
+ * suite is `@serial` — one shared ordered page, where state carry-over is the point — so that is
70
+ * what exempts a flow now. Same reading as serialCascadeRisk() below, which already said so.
63
71
  */
64
72
  export function isolationRisk(featureText: string, stateful: boolean, collection = 'cart'): boolean {
65
- if (!stateful || !/@parallel\b/i.test(featureText)) return false;
73
+ if (!stateful || /@serial\b/i.test(featureText)) return false;
66
74
  // What discharges the risk is isolation of THE MUTATED COLLECTION — so the cleanup tag has
67
75
  // to name it (`@cleanup:cart` for a cart; `@cleanup:registration` for a registration flow).
68
76
  // A cleanup of something else (`@cleanup:overlay`) leaves the shared state shared.
@@ -149,8 +157,12 @@ export function downstreamScope(
149
157
  // names the same screen under its human label (register ↔ [Registration]).
150
158
  const labelsOf = (s: ScenarioInfo): string[] =>
151
159
  [...s.haystack.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1].split(':')[0]);
160
+ // The slug is the route's last segment — in practice an ordinary noun (`order`, `detail`,
161
+ // `login`), so it is matched as a WORD (#779). `dis·order` is not a reference to `/account/order`,
162
+ // and crediting it downgraded a route nothing mentions from `absent` to merely `underCovered`.
163
+ // The full route stays a literal: a path is an identifier, not prose.
152
164
  const refs = scenarios.filter((s) =>
153
- s.haystack.includes(slug) || s.haystack.includes(route.toLowerCase()) ||
165
+ keywordHit(s.haystack, slug) || s.haystack.includes(route.toLowerCase()) ||
154
166
  labelsOf(s).some((l) => sameScreenName(slug, l)));
155
167
  if (!refs.length) {
156
168
  // A scenario's haystack is its name + steps, so a route documented in a @manual scenario's
Binary file
@@ -17,9 +17,15 @@ 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. */
@@ -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
  }
@@ -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. */