@sun-asterisk/sungen 3.2.32 → 3.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/dist/cli/commands/audit.d.ts +5 -0
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +26 -4
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/capability.d.ts.map +1 -1
  6. package/dist/cli/commands/capability.js +35 -17
  7. package/dist/cli/commands/capability.js.map +1 -1
  8. package/dist/cli/commands/changed.d.ts.map +1 -1
  9. package/dist/cli/commands/changed.js +2 -3
  10. package/dist/cli/commands/changed.js.map +1 -1
  11. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  12. package/dist/cli/commands/dashboard.js +2 -28
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/delivery.d.ts.map +1 -1
  15. package/dist/cli/commands/delivery.js +79 -53
  16. package/dist/cli/commands/delivery.js.map +1 -1
  17. package/dist/cli/commands/generate.d.ts.map +1 -1
  18. package/dist/cli/commands/generate.js +14 -7
  19. package/dist/cli/commands/generate.js.map +1 -1
  20. package/dist/cli/commands/results-merge.d.ts +9 -0
  21. package/dist/cli/commands/results-merge.d.ts.map +1 -1
  22. package/dist/cli/commands/results-merge.js +9 -12
  23. package/dist/cli/commands/results-merge.js.map +1 -1
  24. package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
  25. package/dist/cli/commands/results-sanitize.js +2 -13
  26. package/dist/cli/commands/results-sanitize.js.map +1 -1
  27. package/dist/cli/commands/select.d.ts +12 -0
  28. package/dist/cli/commands/select.d.ts.map +1 -0
  29. package/dist/cli/commands/select.js +122 -0
  30. package/dist/cli/commands/select.js.map +1 -0
  31. package/dist/cli/commands/update.d.ts.map +1 -1
  32. package/dist/cli/commands/update.js +3 -0
  33. package/dist/cli/commands/update.js.map +1 -1
  34. package/dist/cli/index.js +7 -2
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  37. package/dist/dashboard/snapshot-builder.js +6 -10
  38. package/dist/dashboard/snapshot-builder.js.map +1 -1
  39. package/dist/dashboard/templates/index.html +1 -1
  40. package/dist/dashboard/types.d.ts +1 -0
  41. package/dist/dashboard/types.d.ts.map +1 -1
  42. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  43. package/dist/exporters/csv-exporter.js +11 -10
  44. package/dist/exporters/csv-exporter.js.map +1 -1
  45. package/dist/exporters/feature-parser.d.ts +21 -0
  46. package/dist/exporters/feature-parser.d.ts.map +1 -1
  47. package/dist/exporters/feature-parser.js +32 -1
  48. package/dist/exporters/feature-parser.js.map +1 -1
  49. package/dist/exporters/json-exporter.d.ts.map +1 -1
  50. package/dist/exporters/json-exporter.js +7 -0
  51. package/dist/exporters/json-exporter.js.map +1 -1
  52. package/dist/exporters/locale-variants.d.ts +7 -1
  53. package/dist/exporters/locale-variants.d.ts.map +1 -1
  54. package/dist/exporters/locale-variants.js +23 -16
  55. package/dist/exporters/locale-variants.js.map +1 -1
  56. package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
  57. package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
  58. package/dist/exporters/manual-hybrid-oracle.js +114 -0
  59. package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
  60. package/dist/exporters/matrix/build.d.ts.map +1 -1
  61. package/dist/exporters/matrix/build.js +19 -9
  62. package/dist/exporters/matrix/build.js.map +1 -1
  63. package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
  64. package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
  65. package/dist/exporters/matrix/carry-manual-results.js +232 -0
  66. package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
  67. package/dist/exporters/matrix/export.d.ts +5 -0
  68. package/dist/exporters/matrix/export.d.ts.map +1 -1
  69. package/dist/exporters/matrix/export.js +8 -1
  70. package/dist/exporters/matrix/export.js.map +1 -1
  71. package/dist/exporters/matrix/gates.js +6 -0
  72. package/dist/exporters/matrix/gates.js.map +1 -1
  73. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
  74. package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
  75. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
  76. package/dist/exporters/matrix/types.d.ts +8 -1
  77. package/dist/exporters/matrix/types.d.ts.map +1 -1
  78. package/dist/exporters/matrix/types.js.map +1 -1
  79. package/dist/exporters/matrix/wording.d.ts +8 -1
  80. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  81. package/dist/exporters/matrix/wording.js +37 -2
  82. package/dist/exporters/matrix/wording.js.map +1 -1
  83. package/dist/exporters/run-environment.d.ts +32 -0
  84. package/dist/exporters/run-environment.d.ts.map +1 -0
  85. package/dist/exporters/run-environment.js +125 -0
  86. package/dist/exporters/run-environment.js.map +1 -0
  87. package/dist/exporters/scenario-merger.d.ts +18 -0
  88. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  89. package/dist/exporters/scenario-merger.js +47 -3
  90. package/dist/exporters/scenario-merger.js.map +1 -1
  91. package/dist/exporters/test-data-resolver.d.ts +0 -4
  92. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  93. package/dist/exporters/test-data-resolver.js +0 -7
  94. package/dist/exporters/test-data-resolver.js.map +1 -1
  95. package/dist/exporters/xlsx-exporter.d.ts +2 -0
  96. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  97. package/dist/exporters/xlsx-exporter.js +7 -2
  98. package/dist/exporters/xlsx-exporter.js.map +1 -1
  99. package/dist/generators/gherkin-parser/index.d.ts +11 -4
  100. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  101. package/dist/generators/gherkin-parser/index.js +49 -19
  102. package/dist/generators/gherkin-parser/index.js.map +1 -1
  103. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
  104. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  105. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  106. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  107. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  108. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  109. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  110. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  111. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  112. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  113. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  114. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  115. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  116. package/dist/generators/test-generator/code-generator.d.ts +2 -1
  117. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  118. package/dist/generators/test-generator/code-generator.js +90 -5
  119. package/dist/generators/test-generator/code-generator.js.map +1 -1
  120. package/dist/generators/test-generator/diagnostics.d.ts +36 -5
  121. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  122. package/dist/generators/test-generator/diagnostics.js +35 -4
  123. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  124. package/dist/generators/test-generator/patterns/index.d.ts +12 -2
  125. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  126. package/dist/generators/test-generator/patterns/index.js +15 -9
  127. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  128. package/dist/generators/test-generator/patterns/types.d.ts +15 -0
  129. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  130. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
  131. package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
  132. package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
  133. package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
  134. package/dist/generators/test-generator/step-mapper.d.ts +46 -0
  135. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  136. package/dist/generators/test-generator/step-mapper.js +84 -9
  137. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  138. package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
  139. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  140. package/dist/generators/test-generator/utils/data-resolver.js +39 -8
  141. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  142. package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
  143. package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
  144. package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
  145. package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
  146. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  147. package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
  148. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  149. package/dist/harness/audit.d.ts +3 -0
  150. package/dist/harness/audit.d.ts.map +1 -1
  151. package/dist/harness/audit.js +29 -12
  152. package/dist/harness/audit.js.map +1 -1
  153. package/dist/harness/capability.d.ts +7 -1
  154. package/dist/harness/capability.d.ts.map +1 -1
  155. package/dist/harness/capability.js +15 -12
  156. package/dist/harness/capability.js.map +1 -1
  157. package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
  158. package/dist/harness/config-file.d.ts +17 -0
  159. package/dist/harness/config-file.d.ts.map +1 -0
  160. package/dist/harness/config-file.js +100 -0
  161. package/dist/harness/config-file.js.map +1 -0
  162. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  163. package/dist/harness/data-driven-lint.js +69 -27
  164. package/dist/harness/data-driven-lint.js.map +1 -1
  165. package/dist/harness/depth-lint.d.ts.map +1 -1
  166. package/dist/harness/depth-lint.js +2 -1
  167. package/dist/harness/depth-lint.js.map +1 -1
  168. package/dist/harness/flow-check.d.ts.map +1 -1
  169. package/dist/harness/flow-check.js +6 -2
  170. package/dist/harness/flow-check.js.map +1 -1
  171. package/dist/harness/keyword-match.d.ts +59 -0
  172. package/dist/harness/keyword-match.d.ts.map +1 -0
  173. package/dist/harness/keyword-match.js +86 -0
  174. package/dist/harness/keyword-match.js.map +1 -0
  175. package/dist/harness/next-step.d.ts.map +1 -1
  176. package/dist/harness/next-step.js +1 -0
  177. package/dist/harness/next-step.js.map +1 -1
  178. package/dist/harness/parse.d.ts +24 -0
  179. package/dist/harness/parse.d.ts.map +1 -1
  180. package/dist/harness/parse.js +117 -13
  181. package/dist/harness/parse.js.map +1 -1
  182. package/dist/harness/perf.d.ts +7 -1
  183. package/dist/harness/perf.d.ts.map +1 -1
  184. package/dist/harness/perf.js +13 -10
  185. package/dist/harness/perf.js.map +1 -1
  186. package/dist/harness/quality-gates.d.ts +9 -2
  187. package/dist/harness/quality-gates.d.ts.map +1 -1
  188. package/dist/harness/quality-gates.js +22 -8
  189. package/dist/harness/quality-gates.js.map +1 -1
  190. package/dist/harness/query-catalog.js +0 -0
  191. package/dist/harness/query-catalog.js.map +1 -1
  192. package/dist/harness/read-text.d.ts +21 -0
  193. package/dist/harness/read-text.d.ts.map +1 -1
  194. package/dist/harness/read-text.js +32 -1
  195. package/dist/harness/read-text.js.map +1 -1
  196. package/dist/harness/secret-scan.d.ts +27 -1
  197. package/dist/harness/secret-scan.d.ts.map +1 -1
  198. package/dist/harness/secret-scan.js +57 -4
  199. package/dist/harness/secret-scan.js.map +1 -1
  200. package/dist/harness/select-by-ids.d.ts +31 -0
  201. package/dist/harness/select-by-ids.d.ts.map +1 -0
  202. package/dist/harness/select-by-ids.js +106 -0
  203. package/dist/harness/select-by-ids.js.map +1 -0
  204. package/dist/harness/sensors.d.ts +20 -1
  205. package/dist/harness/sensors.d.ts.map +1 -1
  206. package/dist/harness/sensors.js +102 -27
  207. package/dist/harness/sensors.js.map +1 -1
  208. package/dist/harness/spec-branches.d.ts +0 -18
  209. package/dist/harness/spec-branches.d.ts.map +1 -1
  210. package/dist/harness/spec-branches.js +24 -3
  211. package/dist/harness/spec-branches.js.map +1 -1
  212. package/dist/harness/spec-coverage.d.ts.map +1 -1
  213. package/dist/harness/spec-coverage.js +14 -4
  214. package/dist/harness/spec-coverage.js.map +1 -1
  215. package/dist/harness/ui-inventory.d.ts +40 -0
  216. package/dist/harness/ui-inventory.d.ts.map +1 -0
  217. package/dist/harness/ui-inventory.js +191 -0
  218. package/dist/harness/ui-inventory.js.map +1 -0
  219. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  220. package/dist/harness/viewpoint-ledger.js +66 -10
  221. package/dist/harness/viewpoint-ledger.js.map +1 -1
  222. package/dist/harness/write-atomic.d.ts +2 -0
  223. package/dist/harness/write-atomic.d.ts.map +1 -0
  224. package/dist/harness/write-atomic.js +62 -0
  225. package/dist/harness/write-atomic.js.map +1 -0
  226. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  227. package/dist/orchestrator/ai-skills/writer.js +15 -7
  228. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  229. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  230. package/dist/orchestrator/assets-drift.js +11 -6
  231. package/dist/orchestrator/assets-drift.js.map +1 -1
  232. package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
  233. package/dist/orchestrator/project-deps-sync.js +22 -1
  234. package/dist/orchestrator/project-deps-sync.js.map +1 -1
  235. package/dist/orchestrator/project-initializer.d.ts +8 -0
  236. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  237. package/dist/orchestrator/project-initializer.js +49 -7
  238. package/dist/orchestrator/project-initializer.js.map +1 -1
  239. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  240. package/dist/orchestrator/screen-manager.js +3 -11
  241. package/dist/orchestrator/screen-manager.js.map +1 -1
  242. package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
  243. package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
  244. package/dist/orchestrator/security-overrides-catalog.js +63 -0
  245. package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
  246. package/dist/orchestrator/security-overrides.d.ts +51 -0
  247. package/dist/orchestrator/security-overrides.d.ts.map +1 -0
  248. package/dist/orchestrator/security-overrides.js +165 -0
  249. package/dist/orchestrator/security-overrides.js.map +1 -0
  250. package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  251. package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  252. package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  253. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
  254. package/dist/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  255. package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  256. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  257. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  258. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  259. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  260. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  261. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  262. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  263. package/dist/orchestrator/templates/playwright.config.ts +4 -2
  264. package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
  265. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  266. package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
  267. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
  268. package/dist/orchestrator/templates/specs-url-assert.js +38 -4
  269. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
  270. package/dist/orchestrator/templates/specs-url-assert.ts +47 -4
  271. package/dist/orchestrator/templates/wdio.conf.ts +24 -1
  272. package/dist/orchestrator/trace-mode-install.d.ts +14 -0
  273. package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
  274. package/dist/orchestrator/trace-mode-install.js +132 -0
  275. package/dist/orchestrator/trace-mode-install.js.map +1 -0
  276. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +20 -0
  277. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
  278. package/dist/orchestrator/viewpoint-pattern-catalog.js +45 -0
  279. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
  280. package/package.json +3 -3
  281. package/src/cli/commands/audit.ts +24 -5
  282. package/src/cli/commands/capability.ts +51 -18
  283. package/src/cli/commands/changed.ts +2 -4
  284. package/src/cli/commands/dashboard.ts +1 -26
  285. package/src/cli/commands/delivery.ts +75 -48
  286. package/src/cli/commands/generate.ts +15 -7
  287. package/src/cli/commands/results-merge.ts +9 -9
  288. package/src/cli/commands/results-sanitize.ts +2 -9
  289. package/src/cli/commands/select.ts +88 -0
  290. package/src/cli/commands/update.ts +3 -0
  291. package/src/cli/index.ts +7 -2
  292. package/src/dashboard/snapshot-builder.ts +6 -8
  293. package/src/dashboard/templates/index.html +1 -1
  294. package/src/dashboard/types.ts +1 -0
  295. package/src/exporters/csv-exporter.ts +7 -10
  296. package/src/exporters/feature-parser.ts +32 -2
  297. package/src/exporters/json-exporter.ts +6 -1
  298. package/src/exporters/locale-variants.ts +23 -17
  299. package/src/exporters/manual-hybrid-oracle.ts +112 -0
  300. package/src/exporters/matrix/build.ts +20 -10
  301. package/src/exporters/matrix/carry-manual-results.ts +229 -0
  302. package/src/exporters/matrix/export.ts +14 -4
  303. package/src/exporters/matrix/gates.ts +6 -0
  304. package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
  305. package/src/exporters/matrix/types.ts +8 -1
  306. package/src/exporters/matrix/wording.ts +36 -2
  307. package/src/exporters/run-environment.ts +89 -0
  308. package/src/exporters/scenario-merger.ts +49 -4
  309. package/src/exporters/test-data-resolver.ts +0 -6
  310. package/src/exporters/xlsx-exporter.ts +7 -2
  311. package/src/generators/gherkin-parser/index.ts +50 -18
  312. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -2
  313. package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
  314. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
  315. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
  316. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
  317. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
  318. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
  319. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
  320. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
  321. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
  322. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
  323. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
  324. package/src/generators/test-generator/code-generator.ts +77 -5
  325. package/src/generators/test-generator/diagnostics.ts +36 -5
  326. package/src/generators/test-generator/patterns/index.ts +15 -8
  327. package/src/generators/test-generator/patterns/types.ts +15 -0
  328. package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
  329. package/src/generators/test-generator/step-mapper.ts +90 -8
  330. package/src/generators/test-generator/utils/data-resolver.ts +41 -12
  331. package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
  332. package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
  333. package/src/harness/audit.ts +37 -16
  334. package/src/harness/capability.ts +23 -12
  335. package/src/harness/catalog/universal-viewpoints.yaml +5 -5
  336. package/src/harness/config-file.ts +66 -0
  337. package/src/harness/data-driven-lint.ts +82 -24
  338. package/src/harness/depth-lint.ts +2 -1
  339. package/src/harness/flow-check.ts +6 -2
  340. package/src/harness/keyword-match.ts +85 -0
  341. package/src/harness/next-step.ts +1 -0
  342. package/src/harness/parse.ts +123 -16
  343. package/src/harness/perf.ts +15 -7
  344. package/src/harness/quality-gates.ts +22 -8
  345. package/src/harness/query-catalog.ts +0 -0
  346. package/src/harness/read-text.ts +32 -1
  347. package/src/harness/secret-scan.ts +60 -4
  348. package/src/harness/select-by-ids.ts +83 -0
  349. package/src/harness/sensors.ts +109 -28
  350. package/src/harness/spec-branches.ts +6 -3
  351. package/src/harness/spec-coverage.ts +14 -4
  352. package/src/harness/ui-inventory.ts +164 -0
  353. package/src/harness/viewpoint-ledger.ts +59 -12
  354. package/src/harness/write-atomic.ts +22 -0
  355. package/src/orchestrator/ai-skills/writer.ts +18 -3
  356. package/src/orchestrator/assets-drift.ts +10 -5
  357. package/src/orchestrator/project-deps-sync.ts +23 -1
  358. package/src/orchestrator/project-initializer.ts +54 -6
  359. package/src/orchestrator/screen-manager.ts +3 -11
  360. package/src/orchestrator/security-overrides-catalog.ts +69 -0
  361. package/src/orchestrator/security-overrides.ts +149 -0
  362. package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
  363. package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
  364. package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  365. package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
  366. package/src/orchestrator/templates/ai-src/commands/run-test.md +70 -13
  367. package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
  368. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
  369. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +23 -1
  370. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +26 -8
  371. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
  372. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
  373. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +41 -1
  374. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
  375. package/src/orchestrator/templates/playwright.config.ts +4 -2
  376. package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
  377. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  378. package/src/orchestrator/templates/specs-url-assert.ts +47 -4
  379. package/src/orchestrator/templates/wdio.conf.ts +24 -1
  380. package/src/orchestrator/trace-mode-install.ts +94 -0
  381. package/src/orchestrator/viewpoint-pattern-catalog.ts +58 -0
@@ -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
 
@@ -139,7 +140,11 @@ function directionCovered(block: string, dir: FrDirection, subject: string[]): b
139
140
  const pool = lines.length ? lines : [block];
140
141
  // Narrow to the lines about the clause's element when the suite names it the same way the spec
141
142
  // does; a spec in one language and selector keys in another must not read as "no evidence".
142
- const about = subject.length ? pool.filter((l) => subject.some((x) => l.includes(x))) : [];
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))) : [];
143
148
  const scan = about.length ? about : pool;
144
149
  return scan.some((l) => NEGATIVE_ASSERTION.test(l) === (dir.polarity === 'negate'));
145
150
  }
@@ -269,7 +274,12 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
269
274
  const cited = featLower.includes(idLower) || cites.has(fr.id.toUpperCase());
270
275
  const words = [...new Set((fr.text.toLowerCase().match(/[a-z][a-z-]{4,}/g) || []))]
271
276
  .filter((w) => !/must|should|system|screen|users?|value|input|field/.test(w));
272
- const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= Math.min(2, words.length));
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));
273
283
  if (!cited && !kwHit) {
274
284
  if (fr.modality === 'MUST') uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
275
285
  continue;
@@ -279,7 +289,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
279
289
  // Which scenarios speak for this requirement — by its id (tags and comments sit in the block)
280
290
  // or by the same keyword rule used above.
281
291
  const owning = blocks.filter((b) => b.includes(idLower)
282
- || (words.length > 0 && words.filter((w) => b.includes(w)).length >= Math.min(2, words.length)));
292
+ || (words.length > 0 && words.filter((w) => keywordHit(b, w)).length >= Math.min(2, words.length)));
283
293
  // Cited from outside any scenario (a header comment): nothing to judge a direction against, so
284
294
  // report nothing rather than invent a gap.
285
295
  if (!owning.length) { frCovered += units; continue; }
@@ -299,7 +309,7 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
299
309
  // blocks that belong to this constraint: cite its msg code (primary), or mention a
300
310
  // DISTINCTIVE constraint word (generic words filtered to avoid matching happy paths).
301
311
  const own = blocks.filter((b) =>
302
- (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)),
303
313
  );
304
314
  if (!own.length) continue; // constraint has no scenarios at all — FR/viewpoint gate covers that
305
315
  const found = [...new Set(own.flatMap((b) => actionTriggersIn(b)))];
@@ -0,0 +1,164 @@
1
+ /**
2
+ * #811 — the design inventory: is every item the DESIGN shows accounted for by the suite?
3
+ *
4
+ * Design sources in `requirements/ui/` (images, `figma-meta.md`) and `requirements/spec_figma.md`
5
+ * were read "for visual context" and never turned into a checkable list, so a column that exists
6
+ * only on the design — or contradicts the spec — was missed by the author AND the reviewer, and a
7
+ * deliberate deferral looked exactly like an oversight.
8
+ *
9
+ * create-test now writes `requirements/ui-inventory.md`: one `UI-NNN` row per component / column /
10
+ * button / state / annotation, with its source (`spec+design` | `design-only` | `conflict`). An
11
+ * item is accounted for by a scenario tag (`@ui:UI-014`) or a reasoned deferral comment
12
+ * (`# UI-DEFERRED: UI-002..013 — <reason>`) — no other mention counts; ranges and `/`
13
+ * enumerations read the same way as spec ids (spec-coverage `citedIds`). `spec+design` items are already measured
14
+ * by spec-coverage, so only `design-only` / `conflict` items are reported uncovered.
15
+ *
16
+ * Advisory: findings never move the score or the gate.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { citedIds } from './spec-coverage';
21
+ import { readTextFile } from './read-text';
22
+
23
+ export type UiItemSource = 'spec+design' | 'design-only' | 'conflict';
24
+
25
+ export interface UiItem {
26
+ id: string;
27
+ item: string;
28
+ kind: string;
29
+ source: UiItemSource;
30
+ }
31
+
32
+ export interface UiInventoryResult {
33
+ /** Design sources the unit holds: image files, figma-meta.md, spec_figma.md. */
34
+ designSources: string[];
35
+ /** `requirements/ui-inventory.md` exists. */
36
+ hasInventory: boolean;
37
+ items: UiItem[];
38
+ /** `images read: a/b` from the inventory header — null when the line is absent. */
39
+ imagesRead: { read: number; total: number } | null;
40
+ /** Images on disk, for comparing against `imagesRead.total`. */
41
+ imagesOnDisk: number;
42
+ covered: string[];
43
+ deferred: string[];
44
+ /** design-only / conflict items neither covered nor deferred. */
45
+ uncovered: UiItem[];
46
+ }
47
+
48
+ const IMAGE = /\.(png|jpe?g|webp|gif)$/i;
49
+
50
+ /** Every design source in a unit's `requirements/` — what makes an inventory necessary. */
51
+ export function designSourcesOf(reqDir: string): string[] {
52
+ const out: string[] = [];
53
+ const uiDir = path.join(reqDir, 'ui');
54
+ try {
55
+ for (const f of fs.readdirSync(uiDir).sort()) {
56
+ if (IMAGE.test(f) || f === 'figma-meta.md') out.push(`ui/${f}`);
57
+ }
58
+ } catch { /* no ui/ dir */ }
59
+ if (fs.existsSync(path.join(reqDir, 'spec_figma.md'))) out.push('spec_figma.md');
60
+ return out;
61
+ }
62
+
63
+ /**
64
+ * The Source cell, read leniently ("design only", "Spec + Design", "conflict — …"). Anything else
65
+ * is NOT assumed to be spec-backed: an unreadable source is reported like a design-only item, so a
66
+ * typo can never make an item disappear from the check.
67
+ */
68
+ function sourceOf(cell: string): UiItemSource | null {
69
+ const c = cell.trim().toLowerCase();
70
+ if (/^design[\s_-]*only\b/.test(c)) return 'design-only';
71
+ if (/^conflict\b/.test(c)) return 'conflict';
72
+ if (/^spec\s*(?:\+|&|and)\s*design\b/.test(c)) return 'spec+design';
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Parse the inventory table: any markdown row whose first cell is a `UI-<n>` id. Leading/trailing
78
+ * pipes are optional (GFM), and the Source cell is FOUND rather than counted — a `|` written inside
79
+ * the Item cell shifts the columns, and a positional read then misfiled a design-only row.
80
+ */
81
+ export function parseUiInventory(text: string): { items: UiItem[]; imagesRead: UiInventoryResult['imagesRead'] } {
82
+ const items: UiItem[] = [];
83
+ for (const line of text.split('\n')) {
84
+ if (!line.includes('|')) continue;
85
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
86
+ if (!/^UI-\d+$/i.test(cells[0] ?? '')) continue;
87
+ // Source = the last cell that reads as one (Seen-in follows it; prose in Item never starts so).
88
+ let srcIdx = -1;
89
+ for (let i = cells.length - 1; i >= 2; i--) if (sourceOf(cells[i])) { srcIdx = i; break; }
90
+ const source: UiItemSource = srcIdx >= 0 ? sourceOf(cells[srcIdx])! : 'design-only';
91
+ const kindIdx = srcIdx >= 0 ? srcIdx - 1 : Math.min(2, cells.length - 1);
92
+ items.push({
93
+ id: cells[0].toUpperCase(),
94
+ item: cells.slice(1, Math.max(2, kindIdx)).join(' | '),
95
+ kind: cells[kindIdx] ?? '',
96
+ source,
97
+ });
98
+ }
99
+ const m = text.match(/images read:\s*(\d+)\s*\/\s*(\d+)/i);
100
+ return { items, imagesRead: m ? { read: Number(m[1]), total: Number(m[2]) } : null };
101
+ }
102
+
103
+ /**
104
+ * Ids cited on a `UI-DEFERRED` comment line — a deliberate, REASONED omission. A line that lists
105
+ * ids and nothing else is not a decision anyone can review, so it defers nothing.
106
+ */
107
+ function deferredIds(featureText: string): Set<string> {
108
+ const lines = featureText.split('\n').filter((l) => {
109
+ if (!/^\s*#\s*UI-DEFERRED\b/i.test(l)) return false;
110
+ const reason = l.replace(/^\s*#\s*UI-DEFERRED\b\s*:?/i, '').replace(/\bUI-\d+(?:\s*(?:\.\.|–|—|~|\/)\s*(?:UI-)?\d+)*/gi, '');
111
+ return /[\p{L}]{3,}/u.test(reason);
112
+ });
113
+ return citedIds(lines.join('\n'));
114
+ }
115
+
116
+ export function uiInventoryCoverage(reqDir: string, featureText: string): UiInventoryResult {
117
+ const designSources = designSourcesOf(reqDir);
118
+ const invPath = path.join(reqDir, 'ui-inventory.md');
119
+ const invText = fs.existsSync(invPath) ? readTextFile(invPath) : null;
120
+ const { items, imagesRead } = invText ? parseUiInventory(invText) : { items: [], imagesRead: null };
121
+ // Coverage is the `@ui:` TAG, nothing else: a `UI-001`-shaped string elsewhere (an `@spec:UI-001`
122
+ // requirement in a project that numbers its FRs that way, prose, an unreasoned deferral line)
123
+ // must not mark a design item as proven.
124
+ const cites = citedIds((featureText.match(/@ui:\S+/gi) ?? []).join(' '));
125
+ const deferSet = deferredIds(featureText);
126
+ const covered: string[] = [];
127
+ const deferred: string[] = [];
128
+ const uncovered: UiItem[] = [];
129
+ for (const it of items) {
130
+ if (deferSet.has(it.id)) deferred.push(it.id);
131
+ else if (cites.has(it.id)) covered.push(it.id);
132
+ else if (it.source !== 'spec+design') uncovered.push(it);
133
+ }
134
+ return {
135
+ designSources,
136
+ hasInventory: invText !== null,
137
+ items,
138
+ imagesRead,
139
+ imagesOnDisk: designSources.filter((s) => IMAGE.test(s)).length,
140
+ covered,
141
+ deferred,
142
+ uncovered,
143
+ };
144
+ }
145
+
146
+ /** Advisory findings for the audit report. */
147
+ export function uiInventoryFindings(r: UiInventoryResult): string[] {
148
+ const out: string[] = [];
149
+ if (r.designSources.length === 0) return out;
150
+ if (!r.hasInventory) {
151
+ out.push(`UI-INVENTORY-MISSING: the unit has design sources (${r.designSources.slice(0, 4).join(', ')}${r.designSources.length > 4 ? ', …' : ''}) but no \`requirements/ui-inventory.md\` — nothing shows the design was read item by item, so an item that exists only on the design (or contradicts the spec) is invisible to the gate and to review. Re-run /sungen:create-test so it writes the inventory, or write it by hand: one \`| UI-NNN | item | kind | spec+design / design-only / conflict |\` row per component, column, button, state and annotation.`);
152
+ return out;
153
+ }
154
+ if (r.imagesRead && r.imagesRead.read < Math.max(r.imagesRead.total, r.imagesOnDisk)) {
155
+ out.push(`UI-INVENTORY-INCOMPLETE: the inventory records "images read: ${r.imagesRead.read}/${r.imagesRead.total}" and the unit holds ${r.imagesOnDisk} image(s) — the unread design is not inventoried. Read the remaining images and add their items.`);
156
+ } else if (!r.imagesRead && r.imagesOnDisk > 0) {
157
+ out.push(`UI-INVENTORY-INCOMPLETE: the unit holds ${r.imagesOnDisk} image(s) but the inventory does not record "images read: N/N" — nothing shows every image was opened. Add the line after reading them all.`);
158
+ }
159
+ for (const it of r.uncovered.slice(0, 10)) {
160
+ out.push(`UI-ITEM-UNCOVERED: ${it.id} "${it.item}" (${it.kind}, ${it.source}) has no scenario and no deferral — ${it.source === 'conflict' ? 'the design and spec.md disagree on it; resolve which is right, then cover it' : 'it exists only on the design, outside spec.md'}. Cover it (\`@ui:${it.id}\` on the scenario that proves it), or record the decision: \`# UI-DEFERRED: ${it.id} — <reason>\`.`);
161
+ }
162
+ if (r.uncovered.length > 10) out.push(`UI-ITEM-UNCOVERED: … and ${r.uncovered.length - 10} more design-only/conflict item(s).`);
163
+ return out;
164
+ }
@@ -9,8 +9,9 @@
9
9
  * gaps that inflate a "looks-covered" score; it does not fail the gate.
10
10
  */
11
11
  import * as fs from 'fs';
12
- import { ScenarioInfo } from './parse';
13
- import { readTextFile } from './read-text';
12
+ import { NON_DECLARATION_SECTION, PRIORITY_WORDS, ScenarioInfo } from './parse';
13
+ import { readViewpointText } from './read-text';
14
+ import { keywordHit } from './keyword-match';
14
15
 
15
16
  export interface LedgerItem { id?: string; text: string; covered: boolean }
16
17
 
@@ -68,7 +69,10 @@ const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.T
68
69
  const PLACEHOLDER_ITEM = /^(none\b|n\/a\b|tbd\b|no known\b|nothing\b|do not invent\b|-+$)/i;
69
70
  /** A priority-DECLARATION row: `| VP-LOGIC | High | <reason prose> |`. The category id and its
70
71
  * priority are consumed by the traceability + balance axes; the reason is rationale. */
71
- const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|normal|low|deferred)\b/i;
72
+ // The word must be the WHOLE second cell: `skip` / `none` / `n/a` also open real claims
73
+ // ("Skip link moves focus to main", "None of the optional fields block submit") — and so did
74
+ // `high`/`low` ("High contrast mode …"), which the old word-boundary match swallowed too.
75
+ const PRIORITY_ROW = new RegExp(`^(?:VP|FL)[A-Z0-9._-]*\\s+—\\s+[*_\`]*(?:${PRIORITY_WORDS})[*_\`.]*(?=\\s+—|\\s*$)`, 'i');
72
76
  /**
73
77
  * Whole SECTIONS that declare something ABOUT the checklist rather than listing claims to prove.
74
78
  *
@@ -81,13 +85,26 @@ const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|nor
81
85
  * `Design Decisions` deliberately stays IN: "values are buffered between step 3 and 4, a reload
82
86
  * loses them" is a real claim, and it is what the continuity check reads.
83
87
  */
84
- const DECLARATION_SECTION = /^(?:known issues?|open questions?|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b/i;
88
+ const DECLARATION_SECTION = NON_DECLARATION_SECTION; // shared with the overview parser (#833)
89
+ /**
90
+ * A lead-in that introduces a manual check's PROCEDURE — `How to run VP-SEC-1:`, `Procedures:`,
91
+ * `Steps to reproduce:`. The list under it is how to EXECUTE a claim already declared above;
92
+ * counting each step as its own claim made gaps no scenario can close (8 of 13 surplus items on a
93
+ * field unit, #855).
94
+ *
95
+ * Deliberately narrow, because a false match HIDES claims: the lead may name one id and nothing
96
+ * else (`How to verify the following claims hold:` introduces claims, not steps), and a bare
97
+ * `Steps:` (a flow lists real step claims under it) and `Tester verifies:` (expected outcomes —
98
+ * exactly what the ledger counts) never qualify.
99
+ */
100
+ const PROCEDURE_LEAD = /^(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?|manual steps|steps to reproduce)(?:\s+(?:VP|FL)-[\w.-]+)?\s*:$/i;
101
+ const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+/;
85
102
  const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
86
103
 
87
104
  /** Extract atomic checklist items from a viewpoint file (format-tolerant). */
88
105
  export function parseViewpointItems(viewpointPath: string): { id?: string; text: string }[] {
89
106
  if (!fs.existsSync(viewpointPath)) return [];
90
- const lines = readTextFile(viewpointPath).split('\n');
107
+ const lines = readViewpointText(viewpointPath).split('\n');
91
108
  const items: { id?: string; text: string }[] = [];
92
109
  let inFence = false;
93
110
  let inDeclarationSection = false;
@@ -95,10 +112,36 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
95
112
  // nested under `## 8. Traceability Index` used to re-open the section and hand its rows
96
113
  // back as claims, because exclusion was recomputed from scratch at every heading.
97
114
  let excludedAt: number | null = null;
98
- for (const raw of lines) {
99
- const line = raw.trim();
115
+ // Inside the list a PROCEDURE_LEAD introduced: the indent of the lead (-1 for a paragraph lead),
116
+ // whose list items — and their wrapped continuation lines — are steps. A heading, a table, a
117
+ // new lead, or (after a paragraph lead) a blank line once a step was read ends it: when in
118
+ // doubt a step stays a claim, because a swallowed claim is a gap nobody ever sees — so a LOOSE
119
+ // procedure (steps separated by blank lines) leaks its later steps as claims, by design.
120
+ // Only an INDENTED line continues a step (Markdown's continuation); unindented prose ends it.
121
+ let procIndent: number | null = null;
122
+ let procSteps = 0;
123
+ let prevBlank = false;
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const line = lines[i].trim();
100
126
  if (line.startsWith('```')) { inFence = !inFence; continue; }
101
- if (inFence || !line) continue;
127
+ if (inFence) continue;
128
+ if (!line) { prevBlank = true; continue; }
129
+ const wasBlank = prevBlank;
130
+ prevBlank = false;
131
+ const isItem = LIST_ITEM.test(line);
132
+ const indent = lines[i].length - lines[i].trimStart().length;
133
+ if (PROCEDURE_LEAD.test(line.replace(LIST_ITEM, '').replace(/[*_`]/g, '').trim())) {
134
+ procIndent = isItem ? indent : -1;
135
+ procSteps = 0;
136
+ continue;
137
+ }
138
+ if (procIndent !== null) {
139
+ const listEnded = wasBlank && procIndent < 0 && procSteps > 0;
140
+ if (!listEnded && isItem && indent > procIndent) { procSteps++; continue; } // a step
141
+ if (!listEnded && !isItem && !wasBlank && procSteps > 0 && indent > Math.max(procIndent, 0)
142
+ && !/^[#|]/.test(line) && !line.endsWith(':')) continue; // a step that wraps
143
+ procIndent = null;
144
+ }
102
145
  const heading = line.match(/^(#{1,6})\s+(.*)$/);
103
146
  if (heading) {
104
147
  const level = heading[1].length;
@@ -120,6 +163,11 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
120
163
  if (bullet) { text = bullet[1]; lead = bullet[1]; }
121
164
  else if (line.startsWith('|')) { // table data row
122
165
  if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
166
+ // The row right above a `|---|` separator IS the header, whatever its words (`Skill #`,
167
+ // `Case #`) — structure, not a guessed word list, so a data row such as `C#` or `Issue #`
168
+ // is never mistaken for one (#832). The word list below still covers a separator-less table.
169
+ const next = (lines[i + 1] ?? '').trim();
170
+ if (/^\|[\s|:-]+\|?$/.test(next) && next.includes('-')) continue; // header
123
171
  const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
124
172
  if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status|step|flow|ref|level|question|screen|actor|branches from|own steps|component|thĆ nh phįŗ§n|trĘ°į»ng|bước)$/i.test(cells[0] || '')) continue; // header
125
173
  text = cells.join(' — ');
@@ -198,7 +246,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
198
246
  } else {
199
247
  const words = [...new Set((item.text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w)))];
200
248
  const need = Math.min(2, words.length);
201
- isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= need);
249
+ isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => keywordHit(s.haystack, w)).length >= need);
202
250
  }
203
251
  if (isCovered) covered++;
204
252
  else missing.push({ id: item.id, text: item.text });
@@ -348,10 +396,9 @@ export function viewpointIntegrity(
348
396
  }
349
397
 
350
398
  // The Priority-Viewpoints table: `| <id> | <priority> | <reason> |`.
351
- // `readTextFile` throws on a missing file, and a unit may legitimately have no viewpoint yet.
352
- const vpText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
399
+ const vpText = readViewpointText(viewpointPath);
353
400
  const rows = [...vpText
354
- .matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)]
401
+ .matchAll(new RegExp(`^\\|\\s*((?:VP|FL)[A-Z0-9._-]*)\\s*\\|\\s*(${PRIORITY_WORDS})\\s*\\|`, 'gim'))]
355
402
  .map((m) => m[2].toLowerCase());
356
403
  const levels = [...new Set(rows)];
357
404
  const uniformPriority = rows.length >= 3 && levels.length === 1 ? { levels, rows: rows.length } : null;
@@ -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
  }
@@ -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 ?? {};
@@ -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
 
@@ -17,6 +17,8 @@ import {
17
17
  installMobileDriver,
18
18
  } from './mobile-runtime-scaffolder';
19
19
  import { emitAppiumEnvironment, runAppiumEnvironmentSetup, appiumEnvironmentRerunCommand } from './appium-environment-setup';
20
+ import { ensureSecurityOverrides, reportSecurityOverrides } from './security-overrides';
21
+ import { readTextFile } from '../harness/read-text';
20
22
 
21
23
  export { MobileInitOptions };
22
24
 
@@ -94,6 +96,14 @@ export class ProjectInitializer {
94
96
  private cwd: string;
95
97
  private createdItems: string[] = [];
96
98
  private skippedItems: string[] = [];
99
+ /**
100
+ * Actions the user asked for that could NOT be performed. Kept apart from `skippedItems`:
101
+ * that list renders under "Already exists", which is a statement of fact about the project —
102
+ * printing "package.json scripts (invalid JSON)" under it tells the user the opposite of what
103
+ * happened. An action that was not done needs its own heading, or the init summary reads as a
104
+ * clean success while `npm test` is missing.
105
+ */
106
+ private notDoneItems: string[] = [];
97
107
  /** set by createCodexMCPConfig when the project isn't Codex-trusted (config would never load) */
98
108
  private codexTrustWarning = false;
99
109
  private agents: AdapterKey[];
@@ -135,6 +145,7 @@ export class ProjectInitializer {
135
145
  : this.baseCwd;
136
146
  this.createdItems = [];
137
147
  this.skippedItems = [];
148
+ this.notDoneItems = [];
138
149
 
139
150
  console.log(`šŸš€ Initializing Sungen ${this.mobileOnly ? 'mobile-only ' : ''}project...\n`);
140
151
 
@@ -439,9 +450,17 @@ export class ProjectInitializer {
439
450
 
440
451
  if (existed) {
441
452
  try {
442
- config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
443
- } catch {
444
- this.skippedItems.push(`${displayName} (invalid JSON, skipped)`);
453
+ // via readTextFile: a UTF-8 BOM is the one thing `JSON.parse` rejects that every other tool
454
+ // reading this file ignores, and it is invisible in an editor. A config the rest of the
455
+ // toolchain loads fine must not cost the user their MCP servers here.
456
+ config = JSON.parse(readTextFile(filePath));
457
+ } catch (e) {
458
+ // Anything still unparseable is genuinely broken. Say WHERE — "is not valid JSON" alone
459
+ // leaves the user to find a missing comma in a file they did not know was the problem.
460
+ this.notDoneItems.push(
461
+ `${displayName} (${(e as Error).message.split('\n')[0]}) — MCP servers not registered; `
462
+ + `fix the JSON and re-run \`sungen init\``,
463
+ );
445
464
  return;
446
465
  }
447
466
  }
@@ -487,6 +506,13 @@ export class ProjectInitializer {
487
506
  });
488
507
  }
489
508
 
509
+ if (this.notDoneItems.length > 0) {
510
+ console.log('\n⚠ Not done — needs your attention:');
511
+ this.notDoneItems.forEach(item => {
512
+ console.log(` āœ— ${item}`);
513
+ });
514
+ }
515
+
490
516
  if (this.codexTrustWarning) {
491
517
  console.log('\n⚠ Codex: this project is not trusted yet, so .codex/config.toml (the MCP browser');
492
518
  console.log(' config for run-test) will be SILENTLY IGNORED by Codex. To fix, either:');
@@ -685,6 +711,13 @@ export class ProjectInitializer {
685
711
  // Ensure standard scripts exist in package.json
686
712
  this.ensurePackageScripts(packageJsonPath);
687
713
 
714
+ // Pin the transitive packages that would otherwise make `npm audit` red here. This MUST happen
715
+ // before the install below: npm reads `overrides` at resolution time, so a lockfile written
716
+ // first would already hold the vulnerable versions. See security-overrides.ts for why the pins
717
+ // cannot simply live in sungen's own manifest.
718
+ const overridePlan = ensureSecurityOverrides(packageJsonPath);
719
+ reportSecurityOverrides(overridePlan);
720
+
688
721
  // Mobile-only drops @playwright/test (no web runner needed). `ajv`/`ajv-formats` back the API
689
722
  // Driver's `expect {{name.body}} matches schema [Ref]` (loaded lazily; harmless if unused).
690
723
  // fast-xml-parser: the API driver's XML request/response support lazy-requires it
@@ -696,7 +729,7 @@ export class ProjectInitializer {
696
729
 
697
730
  let missingDeps: string[] = requiredDeps;
698
731
  try {
699
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
732
+ const packageJson = JSON.parse(readTextFile(packageJsonPath));
700
733
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
701
734
  missingDeps = requiredDeps.filter(d => !deps[d]);
702
735
  } catch {
@@ -705,6 +738,12 @@ export class ProjectInitializer {
705
738
 
706
739
  if (missingDeps.length === 0) {
707
740
  console.log('āœ“ All dependencies already installed\n');
741
+ // Nothing to install means nothing re-resolves, so overrides just written would sit in
742
+ // package.json while the lockfile keeps the vulnerable tree. Re-resolve once so they land.
743
+ if (overridePlan?.add.length) {
744
+ console.log('šŸ“¦ Re-resolving the tree so the new security overrides take effect...\n');
745
+ execSync('npm install', execOpts);
746
+ }
708
747
  } else {
709
748
  console.log(`šŸ“¦ Installing ${missingDeps.join(', ')}...\n`);
710
749
  execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
@@ -725,8 +764,17 @@ export class ProjectInitializer {
725
764
  private ensurePackageScripts(packageJsonPath: string): void {
726
765
  let packageJson: Record<string, any>;
727
766
  try {
728
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
729
- } catch {
767
+ // via readTextFile: a package.json carrying a UTF-8 BOM is one npm reads without complaint,
768
+ // so it must not be treated as unparseable here. That was the trigger that actually reached
769
+ // this catch — genuinely broken JSON aborts `npm install` a few lines below instead.
770
+ packageJson = JSON.parse(readTextFile(packageJsonPath));
771
+ } catch (e) {
772
+ // Returning here used to add nothing and say nothing, so init reported success and the user
773
+ // met `npm error Missing script: "test"` later with nothing pointing back to this run.
774
+ this.notDoneItems.push(
775
+ `package.json scripts (${(e as Error).message.split('\n')[0]}) — `
776
+ + `fix the JSON and re-run \`sungen init\`, or add the scripts by hand`,
777
+ );
730
778
  return;
731
779
  }
732
780
 
@@ -9,6 +9,7 @@ import { chromium } from '@playwright/test';
9
9
  import { readCapabilities } from '../harness/capability';
10
10
  import { testDataHeader, ensureTestDataGuideFiles } from './test-data-guide';
11
11
  import { resolveWebBaseUrl } from '../exporters/env-qa';
12
+ import { uiPatternRows } from './viewpoint-pattern-catalog';
12
13
 
13
14
  export interface ScreenOptions {
14
15
  name: string;
@@ -191,20 +192,11 @@ export class ScreenManager {
191
192
  '',
192
193
  '## UI Patterns Identified',
193
194
  '',
194
- '<!-- Check applicable patterns for this screen (from sungen-viewpoint skill):',
195
+ '<!-- Check applicable patterns for this screen (numbered as in the sungen-viewpoint skill):',
195
196
  '',
196
197
  '| # | Pattern | Applicable? | Notes |',
197
198
  '|---|---------|-------------|-------|',
198
- '| 1 | Form & Inputs | ☐ | |',
199
- '| 2 | Data Table | ☐ | |',
200
- '| 3 | Create / Add | ☐ | |',
201
- '| 4 | Update / Edit | ☐ | |',
202
- '| 5 | Delete | ☐ | |',
203
- '| 6 | Search | ☐ | |',
204
- '| 7 | Filter | ☐ | |',
205
- '| 8 | Pagination | ☐ | |',
206
- '| 9 | Modal / Dialog | ☐ | |',
207
- '| 10 | List / Card | ☐ | |',
199
+ ...uiPatternRows(isMobile),
208
200
  '-->',
209
201
  '',
210
202
  '## Priority Viewpoints',