@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
@@ -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
  }
@@ -204,38 +207,93 @@ export function sameOracleClusters(scenarios: ScenarioInfo[]): Array<{ oracle: s
204
207
  }
205
208
 
206
209
  /**
207
- * A "denied → hidden" scenario with no "allowed → shown" counterpart (#630).
210
+ * A "hidden" scenario with no "shown" counterpart (#630, widened in #679).
208
211
  *
209
212
  * Two security cases proved that chat content is hidden when `MESSAGE_READ` is denied. If the code
210
213
  * hid it unconditionally — ignoring the permission entirely — both still pass. The deny side alone
211
214
  * cannot distinguish "the guard works" from "the feature is broken for everyone", so the pair is
212
215
  * the test, exactly as `min-1 / min / max / max+1` is the test for a boundary.
213
216
  *
214
- * The mirror of the negative-claim rule the skill already has, on the other side.
217
+ * The first version gated on PERMISSION vocabulary, and "show A or B, never both" is far broader
218
+ * than permission: a conditional button, an empty-vs-populated state, an active tab. A field report
219
+ * lost exactly that case — "the score button is hidden once a score exists" was written, "it is
220
+ * shown while no score is set" was not, and nothing asked for it. So the trigger is now the
221
+ * ELEMENT the assertion names: an element this suite proves ABSENT and never proves PRESENT is
222
+ * unpaired, whatever the condition is called. That also catches the vacuous form — a selector that
223
+ * matches nothing passes `is hidden` forever.
224
+ *
225
+ * Permission stays its own `kind`, because the repair advice differs and its wording is what the
226
+ * skill teaches.
215
227
  */
216
228
  const DENY_SHAPE = /\b(?:denied|deny|denies|forbidden|not authoriz\w*|unauthoriz\w*|no permission|without (?:the )?(?:permission|right|access|flag)|permission (?:is )?off|flag (?:is )?off|revoked|restricted)\b/i;
217
229
  const HIDDEN_SHAPE = /\b(?:is hidden|are hidden|not (?:visible|shown|displayed)|blocked|masked|redacted|does not (?:show|render|appear))\b/i;
218
230
  const ALLOW_SHAPE = /\b(?:granted|allowed|permitted|authoriz\w*|has (?:the )?(?:permission|right|access)|permission (?:is )?on|flag (?:is )?on|with (?:the )?(?:permission|right|access))\b/i;
219
231
 
220
- export function permissionPairGaps(
221
- scenarios: ScenarioInfo[],
222
- ): Array<{ scenario: string; subject: string[] }> {
223
- const denies = scenarios.filter((s) => DENY_SHAPE.test(s.haystack) && HIDDEN_SHAPE.test(s.haystack));
224
- if (denies.length === 0) return [];
225
- const allows = scenarios.filter((s) => ALLOW_SHAPE.test(s.haystack) && !HIDDEN_SHAPE.test(s.stepsText));
232
+ /** Absence in sungen's own assertion grammar — `is hidden`, `does not exist`, `not exists`. */
233
+ const ABSENT_ASSERTION = /\bis hidden\b|\bare hidden\b|\bnot (?:visible|shown|displayed)\b|\bdoes not (?:exist|show|appear|render)\b|\bnot exists?\b/i;
234
+
235
+ export interface VisibilityPairGap {
236
+ scenario: string;
237
+ subject: string[];
238
+ /** The element reference the pair is about, when the assertion names one. */
239
+ element?: string;
240
+ /** `permission` keeps the deny/allow repair advice; `condition` is any other state. */
241
+ kind: 'permission' | 'condition';
242
+ }
243
+
244
+ /** What a scenario ASSERTS — the Then side. Hand-built fixtures carry no buckets; use their steps. */
245
+ function thenLines(s: ScenarioInfo): string[] {
246
+ const thens = (s.steps ?? []).filter((st) => st.bucket === 'then').map((st) => st.text.toLowerCase());
247
+ if (thens.length) return thens;
248
+ return s.stepsText ? [s.stepsText] : [];
249
+ }
250
+ /** Every `[ref]` of a line. */
251
+ function refsIn(line: string): string[] {
252
+ return [...line.matchAll(/\[([^\]]{1,60})\]/g)].map((m) => m[1].trim().toLowerCase());
253
+ }
254
+
255
+ export function visibilityPairGaps(scenarios: ScenarioInfo[]): VisibilityPairGap[] {
256
+ // Every element the suite proves PRESENT, anywhere. A positive assertion names its element and
257
+ // any element it is scoped INSIDE (`[Row] row in [Table] table`) — both are demonstrably there.
258
+ const shown = new Set<string>();
259
+ for (const s of scenarios) {
260
+ for (const l of thenLines(s)) {
261
+ if (ABSENT_ASSERTION.test(l)) continue;
262
+ for (const ref of refsIn(l)) shown.add(ref);
263
+ }
264
+ }
226
265
  // Words only — the scenario ID is stripped first, because `[a-z][a-z-]{4,}` happily matches
227
266
  // `fl-er-` and an id fragment can never appear in the counterpart's title, so demanding it made
228
267
  // the pair impossible to find.
229
268
  const distinctive = (s: ScenarioInfo): string[] =>
230
269
  [...new Set((s.name.replace(/^\S+\s*/, '').toLowerCase().match(/[a-z]{5,}/g) ?? []))]
231
270
  .filter((w) => !GENERIC_WORD.has(w) && !/denied|denies|hidden|permission|scenario/.test(w));
232
- const out: Array<{ scenario: string; subject: string[] }> = [];
233
- for (const d of denies) {
234
- const subject = distinctive(d);
271
+ const allows = scenarios.filter((s) => ALLOW_SHAPE.test(s.haystack) && !HIDDEN_SHAPE.test(s.stepsText));
272
+ const out: VisibilityPairGap[] = [];
273
+ const reported = new Set<string>();
274
+ for (const s of scenarios) {
275
+ const absent = thenLines(s).filter((l) => ABSENT_ASSERTION.test(l));
276
+ if (!absent.length && !(DENY_SHAPE.test(s.haystack) && HIDDEN_SHAPE.test(s.haystack))) continue;
277
+ const kind = DENY_SHAPE.test(s.haystack) ? 'permission' : 'condition';
278
+ // Only the FIRST ref of an absence line is its subject: in `see [Row] row in [Table] table … is
279
+ // hidden` the table is the scope, the row is what went away.
280
+ const subjects = [...new Set(absent.map((l) => refsIn(l)[0]).filter((r): r is string => !!r))];
281
+ if (subjects.length) {
282
+ for (const el of subjects) {
283
+ if (shown.has(el) || reported.has(el)) continue;
284
+ reported.add(el);
285
+ out.push({ scenario: s.name, subject: distinctive(s).slice(0, 4), element: el, kind });
286
+ }
287
+ continue;
288
+ }
289
+ // The assertion names no element — the permission vocabulary is then the only safe trigger,
290
+ // paired on the scenario's distinctive title words as before.
291
+ if (kind !== 'permission' || !HIDDEN_SHAPE.test(s.haystack)) continue;
292
+ const subject = distinctive(s);
235
293
  // The counterpart has to be about the SAME thing — two shared distinctive words, the same
236
294
  // bar the ledger's prose matching uses.
237
- const paired = allows.some((a) => subject.filter((w) => a.haystack.includes(w)).length >= Math.min(2, subject.length));
238
- if (!paired) out.push({ scenario: d.name, subject: subject.slice(0, 4) });
295
+ const paired = allows.some((a) => subject.filter((w) => keywordHit(a.haystack, w)).length >= Math.min(2, subject.length));
296
+ if (!paired) out.push({ scenario: s.name, subject: subject.slice(0, 4), kind: 'permission' });
239
297
  }
240
298
  return out;
241
299
  }
@@ -9,6 +9,7 @@
9
9
  * Generic: no project data.
10
10
  */
11
11
  import * as fs from 'fs';
12
+ import { keywordHit } from './keyword-match';
12
13
  import { ScenarioInfo } from './parse';
13
14
  import { readTextFile } from './read-text';
14
15
 
@@ -16,6 +17,12 @@ export type Modality = 'MUST' | 'SHOULD' | 'MAY';
16
17
 
17
18
  export interface TriggerGap { constraint: string; code: string; required: string[]; found: string[]; missing: string[] }
18
19
 
20
+ /**
21
+ * One DIRECTION of a requirement that states both — `MUST be shown … MUST NOT be shown …` (#679).
22
+ * The id is cited, so the requirement looks covered; this names the half nothing asserts.
23
+ */
24
+ export interface ClauseGap { id: string; polarity: 'affirm' | 'negate'; text: string }
25
+
19
26
  export interface SpecCoverageResult {
20
27
  hasSpec: boolean;
21
28
  frTotal: number;
@@ -27,10 +34,13 @@ export interface SpecCoverageResult {
27
34
  * requirement table and any later refactor cannot follow it. */
28
35
  inferredOnly: string[];
29
36
  triggerGaps: TriggerGap[]; // per-constraint trigger matrix gaps
37
+ /** Requirements whose two opposing clauses are not BOTH proved (#679). */
38
+ clauseGaps: ClauseGap[];
30
39
  verdict: 'pass' | 'warn' | 'fail';
31
40
  }
32
41
 
33
- interface FrClause { id: string; text: string; modality: Modality }
42
+ export interface FrDirection { polarity: 'affirm' | 'negate'; text: string }
43
+ interface FrClause { id: string; text: string; modality: Modality; directions?: FrDirection[] }
34
44
  interface ValRow { constraint: string; code: string; triggers: string[] }
35
45
 
36
46
  // Parsing the spec's Trigger CELL: loose word match ("blur, submit").
@@ -55,6 +65,90 @@ function modalityOf(text: string): Modality {
55
65
  return 'MAY';
56
66
  }
57
67
 
68
+ /**
69
+ * A requirement that states BOTH directions in one sentence — "`X` MUST be shown when no score is
70
+ * set; MUST NOT be shown once a score exists" (#679).
71
+ *
72
+ * One `FrClause` per LINE carried one modality for the whole sentence, and coverage was a binary
73
+ * check per id: ONE scenario tagged `@spec:FR-013` closed both halves. The half that went missing
74
+ * in the field was the MUST NOT one — the classic "both buttons rendered at once" defect, reported
75
+ * as covered 100% on the first generation.
76
+ *
77
+ * Deliberately narrow, three ways at once, because the finding is GATE-BLOCKING:
78
+ * - only a HARD prohibition paired with a hard obligation ("MUST be shown and enabled" is one
79
+ * direction and stays one unit);
80
+ * - only a requirement that NAMES its element, backticked or bracketed — the direction evidence
81
+ * is an assertion ABOUT something, and without a subject there is nothing to look for;
82
+ * - only a VISIBILITY/enabledness requirement, because that is the vocabulary the evidence test
83
+ * reads. "MUST validate the email format on submit. MUST NOT allow submission when it is
84
+ * invalid." is an opposing pair in prose and NOT one here: nothing in a correct suite for it
85
+ * asserts absence, so judging it would fail a suite with no gap in it.
86
+ */
87
+ const VISIBILITY_CLAUSE = /\b(?:shown|shows?|displayed|displays?|visible|hidden|hides?|rendered|renders?|appears?|enabled|disabled)\b|表示|hiển thị/i;
88
+ const PROHIBITION = /\b(?:must not|must never|shall not|shall never)\b|KHÔNG ĐƯỢC|してはならない/i;
89
+ const OBLIGATION = /\b(?:must|shall)\b|\bPHẢI\b/i;
90
+
91
+ function splitDirections(text: string): FrDirection[] | undefined {
92
+ if (!VISIBILITY_CLAUSE.test(text) || elementRefs(text).length === 0) return undefined;
93
+ const parts = text.split(/\s*(?:;|;|。|\.\s+|,?\s+but\s+)\s*/i).map((p) => p.trim()).filter(Boolean);
94
+ if (parts.length < 2) return undefined;
95
+ const affirm: string[] = [];
96
+ const negate: string[] = [];
97
+ for (const part of parts) {
98
+ if (PROHIBITION.test(part)) negate.push(part);
99
+ else if (OBLIGATION.test(part)) affirm.push(part);
100
+ }
101
+ if (!affirm.length || !negate.length) return undefined;
102
+ return [
103
+ { polarity: 'affirm', text: affirm.join('; ') },
104
+ { polarity: 'negate', text: negate.join('; ') },
105
+ ];
106
+ }
107
+
108
+ /** What the clause is ABOUT: a backticked or bracketed element name is the one distinctive token. */
109
+ function elementRefs(text: string): string[] {
110
+ return [...new Set(
111
+ [...text.matchAll(/`([^`]+)`|\[([^\]]+)\]/g)]
112
+ .map((m) => (m[1] ?? m[2]).trim().toLowerCase())
113
+ .filter((x) => x.length >= 2 && x.length <= 60),
114
+ )];
115
+ }
116
+
117
+ /**
118
+ * Absence, in sungen's own assertion grammar (`is hidden`, `does not exist`) plus the prose a spec
119
+ * restatement uses. Positive visibility has no marker — `User see [T]` never says "is visible" —
120
+ * so the affirm side is "an assertion that is not a denial", which is what the suite actually writes.
121
+ */
122
+ const NEGATIVE_ASSERTION = /\bis hidden\b|\bare hidden\b|\bnot (?:visible|shown|displayed|present)\b|\bdoes not (?:exist|show|appear|render)\b|\bnot exists?\b|\bno longer\b|表示されない|không hiển thị/i;
123
+
124
+ /** Assertion lines of a scenario block — Then plus the And/But that inherit its bucket. */
125
+ function assertionLines(block: string): string[] {
126
+ const out: string[] = [];
127
+ let bucket = '';
128
+ for (const raw of block.split('\n')) {
129
+ const kw = raw.trim().match(/^(given|when|then|and|but)\s+/);
130
+ if (!kw) continue;
131
+ if (kw[1] !== 'and' && kw[1] !== 'but') bucket = kw[1];
132
+ if (bucket === 'then') out.push(raw.trim());
133
+ }
134
+ return out;
135
+ }
136
+
137
+ /** Does this scenario block assert THIS direction of the requirement? */
138
+ function directionCovered(block: string, dir: FrDirection, subject: string[]): boolean {
139
+ const lines = assertionLines(block);
140
+ const pool = lines.length ? lines : [block];
141
+ // Narrow to the lines about the clause's element when the suite names it the same way the spec
142
+ // does; a spec in one language and selector keys in another must not read as "no evidence".
143
+ // Word-aware (#779), or the narrowing lets in a line about something else and then trusts it:
144
+ // `under·score` is not a mention of `Score`, but it selected the only absence assertion in the
145
+ // block and so proved a MUST NOT clause about the Score. (`keywordHit` keeps substring semantics
146
+ // for a non-ASCII element name, which is what the cross-language case above depends on.)
147
+ const about = subject.length ? pool.filter((l) => subject.some((x) => keywordHit(l, x))) : [];
148
+ const scan = about.length ? about : pool;
149
+ return scan.some((l) => NEGATIVE_ASSERTION.test(l) === (dir.polarity === 'negate'));
150
+ }
151
+
58
152
  function specTriggersIn(text: string): string[] {
59
153
  return SPEC_TRIGGER.filter((t) => t.re.test(text)).map((t) => t.trigger);
60
154
  }
@@ -86,7 +180,10 @@ export function parseSpecClauses(specPath: string): { frs: FrClause[]; valRows:
86
180
  const id = m[1].toUpperCase();
87
181
  if (seen.has(id)) continue; // first declaration wins
88
182
  seen.add(id);
89
- frs.push({ id, text: m[2].replace(/\*\*/g, '').trim(), modality: modalityOf(m[2]) });
183
+ const text = m[2].replace(/\*\*/g, '').trim();
184
+ const modality = modalityOf(text);
185
+ // A MUST/MUST NOT pair is TWO requirements wearing one id (#679) — carry both directions.
186
+ frs.push({ id, text, modality, directions: modality === 'MUST' ? splitDirections(text) : undefined });
90
187
  }
91
188
 
92
189
  // Validation Rules table: a row carries a Constraint, a Trigger cell, and (often) a code.
@@ -156,40 +253,63 @@ export function citedIds(featureText: string): Set<string> {
156
253
  export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featureText: string): SpecCoverageResult {
157
254
  const { frs, valRows } = parseSpecClauses(specPath);
158
255
  if (!fs.existsSync(specPath) || (frs.length === 0 && valRows.length === 0)) {
159
- return { hasSpec: fs.existsSync(specPath), frTotal: 0, frCovered: 0, uncoveredMust: [], inferredOnly: [], triggerGaps: [], verdict: 'pass' };
256
+ return { hasSpec: fs.existsSync(specPath), frTotal: 0, frCovered: 0, uncoveredMust: [], inferredOnly: [], triggerGaps: [], clauseGaps: [], verdict: 'pass' };
160
257
  }
161
258
  const featLower = featureText.toLowerCase();
162
259
  const cites = citedIds(featureText);
260
+ const blocks = scenarioBlocks(featureText);
163
261
 
164
262
  // FR coverage: explicit @spec:FR / literal FR-id citation, else keyword fallback.
165
263
  const uncoveredMust: { id: string; text: string }[] = [];
264
+ const clauseGaps: ClauseGap[] = [];
166
265
  const inferredOnly: string[] = [];
266
+ let frTotal = 0;
167
267
  let frCovered = 0;
168
268
  for (const fr of frs) {
269
+ // A requirement stating both directions is TWO units — counting it as one is what let a suite
270
+ // holding only the MUST half read 100% covered (#679).
271
+ const units = fr.directions ? fr.directions.length : 1;
272
+ frTotal += units;
169
273
  const idLower = fr.id.toLowerCase();
170
274
  const cited = featLower.includes(idLower) || cites.has(fr.id.toUpperCase());
171
275
  const words = [...new Set((fr.text.toLowerCase().match(/[a-z][a-z-]{4,}/g) || []))]
172
276
  .filter((w) => !/must|should|system|screen|users?|value|input|field/.test(w));
173
- const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= Math.min(2, words.length));
174
- if (cited || kwHit) {
175
- frCovered++;
176
- if (!cited) inferredOnly.push(fr.id); // covered, but nothing links it explicitly
177
- } else if (fr.modality === 'MUST') {
178
- uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
277
+ // Word-aware, not substring (#779). `haystack.includes('format')` is satisfied by
278
+ // in·format·ion and `includes('length')` by wave·length, and two such collisions clear the
279
+ // `>= 2 words` bar as easily as one — so a MUST clause nobody tested read as covered. The
280
+ // threshold is a separate judgement (is one word enough evidence?) and is unchanged; see
281
+ // keyword-match.ts for why the boundary is leading-only.
282
+ const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= Math.min(2, words.length));
283
+ if (!cited && !kwHit) {
284
+ if (fr.modality === 'MUST') uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
285
+ continue;
286
+ }
287
+ if (!cited) inferredOnly.push(fr.id); // covered, but nothing links it explicitly
288
+ if (!fr.directions) { frCovered++; continue; }
289
+ // Which scenarios speak for this requirement — by its id (tags and comments sit in the block)
290
+ // or by the same keyword rule used above.
291
+ const owning = blocks.filter((b) => b.includes(idLower)
292
+ || (words.length > 0 && words.filter((w) => keywordHit(b, w)).length >= Math.min(2, words.length)));
293
+ // Cited from outside any scenario (a header comment): nothing to judge a direction against, so
294
+ // report nothing rather than invent a gap.
295
+ if (!owning.length) { frCovered += units; continue; }
296
+ const subject = elementRefs(fr.text);
297
+ for (const dir of fr.directions) {
298
+ if (owning.some((b) => directionCovered(b, dir, subject))) frCovered++;
299
+ else if (fr.modality === 'MUST') clauseGaps.push({ id: fr.id, polarity: dir.polarity, text: dir.text.slice(0, 90) });
179
300
  }
180
301
  }
181
302
 
182
303
  // Per-constraint trigger coverage — the matrix-collapse catch.
183
304
  // generic words that don't identify a constraint (would over-match happy-path scenarios)
184
305
  const GENERIC = new Set(['valid', 'local', 'part', 'domain', 'email', 'address', 'must', 'exist', 'store', 'field', 'input', 'value', 'formed', 'well', 'character', 'characters']);
185
- const blocks = scenarioBlocks(featureText);
186
306
  const triggerGaps: TriggerGap[] = [];
187
307
  for (const row of valRows) {
188
308
  const kw = (row.constraint.toLowerCase().match(/[a-z]{4,}/g) || []).filter((w) => !GENERIC.has(w));
189
309
  // blocks that belong to this constraint: cite its msg code (primary), or mention a
190
310
  // DISTINCTIVE constraint word (generic words filtered to avoid matching happy paths).
191
311
  const own = blocks.filter((b) =>
192
- (row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => b.includes(w)),
312
+ (row.code && b.includes(row.code.toLowerCase())) || kw.some((w) => keywordHit(b, w)),
193
313
  );
194
314
  if (!own.length) continue; // constraint has no scenarios at all — FR/viewpoint gate covers that
195
315
  const found = [...new Set(own.flatMap((b) => actionTriggersIn(b)))];
@@ -198,9 +318,9 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
198
318
  }
199
319
 
200
320
  const verdict: SpecCoverageResult['verdict'] =
201
- uncoveredMust.length > 0 || triggerGaps.length > 0 ? 'fail' : 'pass';
321
+ uncoveredMust.length > 0 || triggerGaps.length > 0 || clauseGaps.length > 0 ? 'fail' : 'pass';
202
322
 
203
- return { hasSpec: true, frTotal: frs.length, frCovered, uncoveredMust, inferredOnly, triggerGaps, verdict };
323
+ return { hasSpec: true, frTotal, frCovered, uncoveredMust, inferredOnly, triggerGaps, clauseGaps, verdict };
204
324
  }
205
325
 
206
326
  /**
@@ -11,6 +11,7 @@
11
11
  import * as fs from 'fs';
12
12
  import { ScenarioInfo } from './parse';
13
13
  import { readTextFile } from './read-text';
14
+ import { keywordHit } from './keyword-match';
14
15
 
15
16
  export interface LedgerItem { id?: string; text: string; covered: boolean }
16
17
 
@@ -198,7 +199,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
198
199
  } else {
199
200
  const words = [...new Set((item.text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w)))];
200
201
  const need = Math.min(2, words.length);
201
- isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= need);
202
+ isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= need);
202
203
  }
203
204
  if (isCovered) covered++;
204
205
  else missing.push({ id: item.id, text: item.text });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Write a file so that a crash mid-write can never leave a truncated one behind: write a sibling
3
+ * tmp, then rename. Same directory = same device, so the rename is atomic; a concurrent writer is
4
+ * last-rename-wins. On failure the tmp is removed and the error rethrown — the caller still learns.
5
+ *
6
+ * `results merge` and `results sanitize` each grew this block inline, for the same reason and with
7
+ * the same comment. The third caller (`ai-skills/writer.ts`) did not, and its truncated manifest is
8
+ * precisely the corrupt input that #782's readers then absorbed in silence — so the two halves of
9
+ * that defect are one file apart.
10
+ */
11
+ import * as fs from 'fs';
12
+
13
+ export function writeFileAtomic(file: string, data: string): void {
14
+ const tmp = `${file}.tmp-${process.pid}`;
15
+ try {
16
+ fs.writeFileSync(tmp, data);
17
+ fs.renameSync(tmp, file);
18
+ } catch (err) {
19
+ try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup — the real error is below */ }
20
+ throw err;
21
+ }
22
+ }
@@ -16,6 +16,8 @@ import * as path from 'path';
16
16
  import * as crypto from 'crypto';
17
17
  import { BuiltFile } from './build';
18
18
  import { AdapterKey } from './adapters';
19
+ import { readJsonConfig, warnUnreadableConfig } from '../../harness/config-file';
20
+ import { writeFileAtomic } from '../../harness/write-atomic';
19
21
 
20
22
  export interface ApplyOptions { force?: boolean; dryRun?: boolean; version: string }
21
23
  export interface ApplyReport {
@@ -42,10 +44,21 @@ function adapterOf(relPath: string): AdapterKey | null {
42
44
  return null;
43
45
  }
44
46
 
47
+ /**
48
+ * The manifest is what makes orphan cleanup possible: it is the only record of which files a
49
+ * PREVIOUS version managed. An unreadable one used to read as an empty one — so cleanup found
50
+ * nothing to remove, and the stale files it failed to remove are absent from the NEW manifest too.
51
+ * No later run can ever find them again. Say it, then carry on with what can still be done.
52
+ */
45
53
  function readManifest(cwd: string): Manifest {
46
54
  const p = path.join(cwd, MANIFEST_REL);
47
- if (!fs.existsSync(p)) return { version: '', managed: {} };
48
- try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return { version: '', managed: {} }; }
55
+ const read = readJsonConfig<Manifest>(p);
56
+ if (read.state === 'unreadable') {
57
+ warnUnreadableConfig(p, read.error,
58
+ 'orphan cleanup is skipped this run, so files managed by an older version stay in place');
59
+ return { version: '', managed: {} };
60
+ }
61
+ return read.state === 'ok' ? read.value : { version: '', managed: {} };
49
62
  }
50
63
 
51
64
  export function applyOutputs(cwd: string, outputs: BuiltFile[], opts: ApplyOptions): ApplyReport {
@@ -121,7 +134,9 @@ export function applyOutputs(cwd: string, outputs: BuiltFile[], opts: ApplyOptio
121
134
  Object.assign(mergedManaged, newManaged);
122
135
  const mPath = path.join(cwd, MANIFEST_REL);
123
136
  fs.mkdirSync(path.dirname(mPath), { recursive: true });
124
- fs.writeFileSync(mPath, JSON.stringify({ version: opts.version, managed: mergedManaged }, null, 2));
137
+ // Atomic: an interrupted `sungen init`/`update` must not leave a half-written manifest, because
138
+ // the reader above can only report that damage — it cannot undo it.
139
+ writeFileAtomic(mPath, JSON.stringify({ version: opts.version, managed: mergedManaged }, null, 2));
125
140
  }
126
141
  return r;
127
142
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Appium environment bundle — emit `appium-environment/` into a mobile project and run its entry point.
3
+ *
4
+ * The bundle (bash for macOS, PowerShell for Windows) provisions the OS layer BELOW Appium: JDK, Android
5
+ * SDK + env vars, Xcode CLT, applesimutils, then the project-local Appium platform drivers, and ends with
6
+ * `appium driver doctor`. Emitted by `sungen init --mobile` / `sungen capability add mobile` and run once
7
+ * right after the driver install (spec: docs/spec/sungen_mobile_environment_setup_spec.md, issue #718).
8
+ *
9
+ * Core ships the scripts as templates only — no runtime is embedded, nothing is installed globally.
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { spawnSync } from 'child_process';
14
+
15
+ export const APPIUM_ENV_DIR = 'appium-environment';
16
+
17
+ export interface EmitResult {
18
+ created: string[];
19
+ skipped: string[];
20
+ }
21
+
22
+ export type EnvSetupOutcome = 'ran' | 'failed' | 'skipped' | 'unsupported';
23
+
24
+ /** Bundled template root — resolves from src/orchestrator (tsx) and dist/orchestrator (copy-templates). */
25
+ const TEMPLATE_ROOT = path.join(__dirname, 'templates', APPIUM_ENV_DIR);
26
+
27
+ /** Files under `dir`, relative and posix-separated, sorted for a deterministic emit order. */
28
+ function listTemplateFiles(dir: string, prefix = ''): string[] {
29
+ const out: string[] = [];
30
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
31
+ for (const entry of entries) {
32
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
33
+ if (entry.isDirectory()) out.push(...listTemplateFiles(path.join(dir, entry.name), rel));
34
+ else out.push(rel);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * Copy the bundle into `<cwd>/appium-environment/`, skipping every file that already exists (a QA may
41
+ * have edited the pinned cmdline-tools build or a hint — same idempotency as the other emitted files).
42
+ * `*.sh` get the executable bit explicitly: npm and git do not reliably preserve it.
43
+ */
44
+ export function emitAppiumEnvironment(cwd: string): EmitResult {
45
+ const created: string[] = [];
46
+ const skipped: string[] = [];
47
+ // Non-fatal like the other provisioning steps: a broken install (templates missing) must not abort
48
+ // `init` / `capability add` — the harness scaffold is still valid without the bundle.
49
+ try {
50
+ for (const rel of listTemplateFiles(TEMPLATE_ROOT)) {
51
+ const dest = path.join(cwd, APPIUM_ENV_DIR, rel);
52
+ const label = `${APPIUM_ENV_DIR}/${rel}`;
53
+ if (fs.existsSync(dest)) { skipped.push(label); continue; }
54
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
55
+ fs.copyFileSync(path.join(TEMPLATE_ROOT, rel), dest);
56
+ if (rel.endsWith('.sh')) fs.chmodSync(dest, 0o755);
57
+ created.push(label);
58
+ }
59
+ } catch (e) {
60
+ console.warn(`⚠️ Could not emit ${APPIUM_ENV_DIR}/ (${e instanceof Error ? e.message : e}) — reinstall @sun-asterisk/sungen and re-run: sungen capability add mobile`);
61
+ }
62
+ return { created, skipped };
63
+ }
64
+
65
+ /** The command a QA re-runs by hand, for hints and the init summary. */
66
+ export function appiumEnvironmentRerunCommand(): string {
67
+ return process.platform === 'win32'
68
+ ? `powershell -NoProfile -ExecutionPolicy Bypass -File .\\${APPIUM_ENV_DIR}\\setup.ps1`
69
+ : `./${APPIUM_ENV_DIR}/setup.sh`;
70
+ }
71
+
72
+ /**
73
+ * Translate the bundle's POSIX flags to their PowerShell switches. `sungen setup-mobile-env` speaks
74
+ * one flag vocabulary on both OSes, so the user does not have to know which script is underneath.
75
+ */
76
+ const PS_SWITCH: Record<string, string> = {
77
+ '--check': '-Check',
78
+ '--skip-android': '-SkipAndroid',
79
+ '--no-profile': '-NoProfile',
80
+ // `--skip-ios` has no Windows counterpart: setup.ps1 has no iOS steps to skip.
81
+ };
82
+
83
+ function entryCommand(cwd: string, flags: string[] = []): { file: string; args: string[] } | null {
84
+ const bundle = path.join(cwd, APPIUM_ENV_DIR);
85
+ if (process.platform === 'win32') {
86
+ const switches = flags.map(f => PS_SWITCH[f]).filter((s): s is string => !!s);
87
+ return {
88
+ file: 'powershell',
89
+ // -NoProfile here is powershell.exe's own "skip the user profile" flag, unrelated to the
90
+ // bundle's -NoProfile switch; the latter arrives through `switches` after -File.
91
+ args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', path.join(bundle, 'setup.ps1'), ...switches],
92
+ };
93
+ }
94
+ // setup.sh handles Darwin; on Linux it prints the manual-path notice and exits 0.
95
+ if (process.platform === 'darwin' || process.platform === 'linux') {
96
+ return { file: 'bash', args: [path.join(bundle, 'setup.sh'), ...flags] };
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Run the emitted entry point for this OS with inherited stdio — its per-step output IS the UX, and any
103
+ * Homebrew / sudo prompt must reach the user. Non-fatal by contract: problems are reported with the re-run
104
+ * command and init continues, because the scaffold itself is already complete.
105
+ */
106
+ export function runAppiumEnvironmentSetup(
107
+ cwd: string,
108
+ opts: { skip?: boolean; flags?: string[] } = {},
109
+ ): EnvSetupOutcome {
110
+ if (opts.skip) {
111
+ console.log(`– skipped environment setup (--skip-env-setup) — run it later: sungen setup-mobile-env`);
112
+ return 'skipped';
113
+ }
114
+ const cmd = entryCommand(cwd, opts.flags);
115
+ if (!cmd) {
116
+ console.log(`ℹ️ environment setup ships for macOS (setup.sh) and Windows (setup.ps1) — on ${process.platform} follow ${APPIUM_ENV_DIR}/README.md → Manual fallback`);
117
+ return 'unsupported';
118
+ }
119
+ console.log(`\n🔧 Provisioning the Appium environment (${APPIUM_ENV_DIR}/) — idempotent, re-run any time...`);
120
+ const result = spawnSync(cmd.file, cmd.args, { cwd, stdio: 'inherit' });
121
+ if (result.status === 0) return 'ran';
122
+ // status null = the shell itself could not start (ENOENT) or was killed — name the real cause.
123
+ const exit = result.error ? `could not start ${cmd.file}: ${result.error.message}`
124
+ : result.status === null ? `signal ${result.signal ?? '?'}` : `exit ${result.status}`;
125
+ console.warn(`⚠️ environment setup reported problems (${exit}) — fix the ✗ items above, then re-run: sungen setup-mobile-env (or ${appiumEnvironmentRerunCommand()} directly)`);
126
+ return 'failed';
127
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { getPackageVersion } from '../exporters/package-info';
4
+ import { readJsonConfig, warnUnreadableConfig } from '../harness/config-file';
4
5
 
5
6
  /**
6
7
  * Are this project's AI assets (commands + skills under `.claude/` etc.) the ones
@@ -27,12 +28,16 @@ export function checkAssetsDrift(cwd: string): AssetsDrift | null {
27
28
  const manifestPath = path.join(cwd, '.sungen', 'manifest.json');
28
29
  if (!fs.existsSync(manifestPath)) return null; // not a sungen project (yet)
29
30
 
30
- let manifest: { version?: string; managed?: Record<string, string> };
31
- try {
32
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
33
- } catch {
34
- return null; // unreadable → say nothing
31
+ const read = readJsonConfig<{ version?: string; managed?: Record<string, string> }>(manifestPath);
32
+ if (read.state === 'unreadable') {
33
+ // This function IS the drift alarm. Staying silent on a broken manifest disables the only
34
+ // warning that a project's .claude/ assets have gone stale — and a truncated manifest is the
35
+ // likeliest way to get one (it is written non-atomically by `ai-skills/writer.ts`).
36
+ warnUnreadableConfig(manifestPath, read.error, 'stale-assets drift cannot be checked — re-run `sungen init` to rewrite it');
37
+ return null;
35
38
  }
39
+ if (read.state === 'absent') return null;
40
+ const manifest = read.value;
36
41
  const recordedVersion = manifest.version ?? '';
37
42
  const runningVersion = getPackageVersion();
38
43
  const managed = manifest.managed ?? {};
@@ -22,6 +22,8 @@ export interface MobileInitOptions {
22
22
  iosBundleId?: string;
23
23
  iosDevice?: string;
24
24
  iosVersion?: string;
25
+ /** Emit `appium-environment/` but do not run it now (`--skip-env-setup`); QA re-runs it by hand. */
26
+ skipEnvSetup?: boolean;
25
27
  }
26
28
 
27
29
  export interface ScaffoldResult {
@@ -2,6 +2,7 @@ import { spawnSync } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { resolveDriverVersion } from './lockstep-version';
5
+ import { ensureSecurityOverrides, reportSecurityOverrides } from './security-overrides';
5
6
 
6
7
  /**
7
8
  * Project-local sync of the sungen package family after a global `sungen update`.
@@ -143,8 +144,29 @@ export function syncProjectSungenDeps(cwd: string, core: string, dryRun: boolean
143
144
  }
144
145
  for (const s of plan.skipped) console.log(` – skipped ${s.pkg} — ${s.why}`);
145
146
 
147
+ // A project scaffolded before #764 carries no security overrides, and npm will never pick them up
148
+ // from sungen's own manifest — `sungen update` is the one pass that can put them there. Write them
149
+ // before any install below, since npm reads `overrides` when it resolves.
150
+ const overridePlan = ensureSecurityOverrides(path.join(cwd, 'package.json'), dryRun);
151
+ reportSecurityOverrides(overridePlan, dryRun);
152
+
146
153
  if (!plan.specs.length) {
147
- console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}\n`);
154
+ console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}`);
155
+ // Family already in lockstep, so nothing would re-resolve — but freshly written overrides only
156
+ // reach the tree through a resolution pass. Run one.
157
+ if (overridePlan?.add.length) {
158
+ if (dryRun) {
159
+ console.log(' [dry-run] would run: npm install (re-resolve under the new security overrides)\n');
160
+ return true;
161
+ }
162
+ console.log(' 📦 npm install (re-resolving under the new security overrides)');
163
+ const r = spawnSync('npm', ['install'], { cwd, stdio: 'inherit', shell: true });
164
+ if (r.status !== 0) {
165
+ console.warn('⚠️ Overrides were written to package.json but `npm install` failed — run it by hand.\n');
166
+ return false;
167
+ }
168
+ }
169
+ console.log('');
148
170
  return true;
149
171
  }
150
172