@sun-asterisk/sungen 3.1.2 → 3.2.0-beta.142

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 (290) hide show
  1. package/README.md +4 -428
  2. package/dist/capabilities/builtins.d.ts +31 -0
  3. package/dist/capabilities/builtins.d.ts.map +1 -0
  4. package/dist/capabilities/builtins.js +84 -0
  5. package/dist/capabilities/builtins.js.map +1 -0
  6. package/dist/capabilities/context-router.d.ts +34 -0
  7. package/dist/capabilities/context-router.d.ts.map +1 -0
  8. package/dist/capabilities/context-router.js +49 -0
  9. package/dist/capabilities/context-router.js.map +1 -0
  10. package/dist/capabilities/context.d.ts +68 -0
  11. package/dist/capabilities/context.d.ts.map +1 -0
  12. package/dist/capabilities/context.js +17 -0
  13. package/dist/capabilities/context.js.map +1 -0
  14. package/dist/capabilities/discover.d.ts +2 -0
  15. package/dist/capabilities/discover.d.ts.map +1 -0
  16. package/dist/capabilities/discover.js +109 -0
  17. package/dist/capabilities/discover.js.map +1 -0
  18. package/dist/capabilities/registry.d.ts +92 -0
  19. package/dist/capabilities/registry.d.ts.map +1 -0
  20. package/dist/capabilities/registry.js +43 -0
  21. package/dist/capabilities/registry.js.map +1 -0
  22. package/dist/capabilities/sensor.d.ts +52 -0
  23. package/dist/capabilities/sensor.d.ts.map +1 -0
  24. package/dist/capabilities/sensor.js +3 -0
  25. package/dist/capabilities/sensor.js.map +1 -0
  26. package/dist/cli/commands/audit.d.ts.map +1 -1
  27. package/dist/cli/commands/audit.js +17 -11
  28. package/dist/cli/commands/audit.js.map +1 -1
  29. package/dist/cli/commands/capability.d.ts.map +1 -1
  30. package/dist/cli/commands/capability.js +57 -5
  31. package/dist/cli/commands/capability.js.map +1 -1
  32. package/dist/cli/commands/context.d.ts +9 -0
  33. package/dist/cli/commands/context.d.ts.map +1 -0
  34. package/dist/cli/commands/context.js +91 -0
  35. package/dist/cli/commands/context.js.map +1 -0
  36. package/dist/cli/commands/delivery.d.ts.map +1 -1
  37. package/dist/cli/commands/delivery.js +42 -30
  38. package/dist/cli/commands/delivery.js.map +1 -1
  39. package/dist/cli/commands/generate.d.ts.map +1 -1
  40. package/dist/cli/commands/generate.js +35 -8
  41. package/dist/cli/commands/generate.js.map +1 -1
  42. package/dist/cli/commands/ledger.d.ts.map +1 -1
  43. package/dist/cli/commands/ledger.js +15 -5
  44. package/dist/cli/commands/ledger.js.map +1 -1
  45. package/dist/cli/commands/manifest.d.ts.map +1 -1
  46. package/dist/cli/commands/manifest.js +10 -9
  47. package/dist/cli/commands/manifest.js.map +1 -1
  48. package/dist/cli/commands/repair.d.ts +8 -0
  49. package/dist/cli/commands/repair.d.ts.map +1 -0
  50. package/dist/cli/commands/repair.js +97 -0
  51. package/dist/cli/commands/repair.js.map +1 -0
  52. package/dist/cli/commands/script-check.d.ts.map +1 -1
  53. package/dist/cli/commands/script-check.js +13 -9
  54. package/dist/cli/commands/script-check.js.map +1 -1
  55. package/dist/cli/commands/trace.d.ts.map +1 -1
  56. package/dist/cli/commands/trace.js +7 -4
  57. package/dist/cli/commands/trace.js.map +1 -1
  58. package/dist/cli/index.js +14 -1
  59. package/dist/cli/index.js.map +1 -1
  60. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  61. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  62. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  63. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  64. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  65. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  66. package/dist/generators/test-generator/code-generator.d.ts +18 -9
  67. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  68. package/dist/generators/test-generator/code-generator.js +162 -115
  69. package/dist/generators/test-generator/code-generator.js.map +1 -1
  70. package/dist/generators/test-generator/patterns/index.d.ts +0 -10
  71. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  72. package/dist/generators/test-generator/patterns/index.js +10 -47
  73. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  74. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  75. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  76. package/dist/generators/test-generator/template-engine.js +1 -1
  77. package/dist/generators/test-generator/template-engine.js.map +1 -1
  78. package/dist/harness/annotation-overrides.d.ts +11 -0
  79. package/dist/harness/annotation-overrides.d.ts.map +1 -0
  80. package/dist/harness/annotation-overrides.js +38 -0
  81. package/dist/harness/annotation-overrides.js.map +1 -0
  82. package/dist/harness/audit.d.ts +9 -1
  83. package/dist/harness/audit.d.ts.map +1 -1
  84. package/dist/harness/audit.js +140 -10
  85. package/dist/harness/audit.js.map +1 -1
  86. package/dist/harness/capability-plan.d.ts +14 -0
  87. package/dist/harness/capability-plan.d.ts.map +1 -1
  88. package/dist/harness/capability-plan.js +63 -1
  89. package/dist/harness/capability-plan.js.map +1 -1
  90. package/dist/harness/catalog/drivers.yaml +35 -12
  91. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  92. package/dist/harness/data-driven-lint.js +23 -0
  93. package/dist/harness/data-driven-lint.js.map +1 -1
  94. package/dist/harness/flow-check.d.ts +9 -0
  95. package/dist/harness/flow-check.d.ts.map +1 -1
  96. package/dist/harness/flow-check.js +13 -6
  97. package/dist/harness/flow-check.js.map +1 -1
  98. package/dist/harness/intent.d.ts +6 -0
  99. package/dist/harness/intent.d.ts.map +1 -1
  100. package/dist/harness/intent.js +20 -4
  101. package/dist/harness/intent.js.map +1 -1
  102. package/dist/harness/ledger.d.ts.map +1 -1
  103. package/dist/harness/ledger.js +3 -2
  104. package/dist/harness/ledger.js.map +1 -1
  105. package/dist/harness/manifest.d.ts.map +1 -1
  106. package/dist/harness/manifest.js +3 -2
  107. package/dist/harness/manifest.js.map +1 -1
  108. package/dist/harness/parse.d.ts +2 -0
  109. package/dist/harness/parse.d.ts.map +1 -1
  110. package/dist/harness/parse.js +16 -4
  111. package/dist/harness/parse.js.map +1 -1
  112. package/dist/harness/quality-gates.js +1 -1
  113. package/dist/harness/quality-gates.js.map +1 -1
  114. package/dist/harness/query-catalog.d.ts.map +1 -1
  115. package/dist/harness/query-catalog.js +0 -0
  116. package/dist/harness/query-catalog.js.map +1 -1
  117. package/dist/harness/repair.d.ts +20 -0
  118. package/dist/harness/repair.d.ts.map +1 -0
  119. package/dist/harness/repair.js +111 -0
  120. package/dist/harness/repair.js.map +1 -0
  121. package/dist/harness/script-check.d.ts +3 -1
  122. package/dist/harness/script-check.d.ts.map +1 -1
  123. package/dist/harness/script-check.js +22 -8
  124. package/dist/harness/script-check.js.map +1 -1
  125. package/dist/harness/sensors.d.ts +40 -0
  126. package/dist/harness/sensors.d.ts.map +1 -1
  127. package/dist/harness/sensors.js +54 -2
  128. package/dist/harness/sensors.js.map +1 -1
  129. package/dist/harness/trace.d.ts.map +1 -1
  130. package/dist/harness/trace.js +4 -3
  131. package/dist/harness/trace.js.map +1 -1
  132. package/dist/harness/unit-paths.d.ts +3 -0
  133. package/dist/harness/unit-paths.d.ts.map +1 -0
  134. package/dist/harness/unit-paths.js +52 -0
  135. package/dist/harness/unit-paths.js.map +1 -0
  136. package/dist/index.d.ts +22 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +36 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  141. package/dist/orchestrator/ai-rules-updater.js +2 -0
  142. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  143. package/dist/orchestrator/context-discovery.d.ts +12 -0
  144. package/dist/orchestrator/context-discovery.d.ts.map +1 -0
  145. package/dist/orchestrator/context-discovery.js +46 -0
  146. package/dist/orchestrator/context-discovery.js.map +1 -0
  147. package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +7 -1
  148. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +10 -5
  149. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +18 -1
  150. package/dist/orchestrator/templates/ai-instructions/claude-skill-api-design.md +62 -0
  151. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  152. package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +2 -1
  153. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -2
  154. package/dist/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
  155. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +10 -5
  156. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +11 -1
  157. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +62 -0
  158. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  159. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +2 -1
  160. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -2
  161. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
  162. package/dist/orchestrator/templates/specs-api.d.ts +55 -0
  163. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
  164. package/dist/orchestrator/templates/specs-api.js +171 -0
  165. package/dist/orchestrator/templates/specs-api.js.map +1 -0
  166. package/dist/orchestrator/templates/specs-api.ts +154 -0
  167. package/dist/orchestrator/templates/specs-db.d.ts +3 -0
  168. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  169. package/dist/orchestrator/templates/specs-db.js +78 -1
  170. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  171. package/dist/orchestrator/templates/specs-db.ts +78 -1
  172. package/dist/orchestrator/templates/specs-test-data.ts +2 -1
  173. package/package.json +7 -30
  174. package/src/capabilities/builtins.ts +85 -0
  175. package/src/capabilities/context-router.ts +66 -0
  176. package/src/capabilities/context.ts +65 -0
  177. package/src/capabilities/discover.ts +62 -0
  178. package/src/capabilities/registry.ts +113 -0
  179. package/src/capabilities/sensor.ts +47 -0
  180. package/src/cli/commands/audit.ts +15 -9
  181. package/src/cli/commands/capability.ts +53 -5
  182. package/src/cli/commands/context.ts +52 -0
  183. package/src/cli/commands/delivery.ts +40 -31
  184. package/src/cli/commands/generate.ts +37 -8
  185. package/src/cli/commands/ledger.ts +13 -5
  186. package/src/cli/commands/manifest.ts +9 -7
  187. package/src/cli/commands/repair.ts +57 -0
  188. package/src/cli/commands/script-check.ts +12 -8
  189. package/src/cli/commands/trace.ts +7 -4
  190. package/src/cli/index.ts +14 -1
  191. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  192. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  193. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  194. package/src/generators/test-generator/code-generator.ts +163 -111
  195. package/src/generators/test-generator/patterns/index.ts +9 -35
  196. package/src/generators/test-generator/template-engine.ts +2 -2
  197. package/src/harness/annotation-overrides.ts +27 -0
  198. package/src/harness/audit.ts +141 -12
  199. package/src/harness/capability-plan.ts +51 -1
  200. package/src/harness/catalog/drivers.yaml +35 -12
  201. package/src/harness/data-driven-lint.ts +20 -0
  202. package/src/harness/flow-check.ts +15 -6
  203. package/src/harness/intent.ts +25 -4
  204. package/src/harness/ledger.ts +3 -2
  205. package/src/harness/manifest.ts +3 -2
  206. package/src/harness/parse.ts +11 -2
  207. package/src/harness/quality-gates.ts +1 -1
  208. package/src/harness/query-catalog.ts +0 -0
  209. package/src/harness/repair.ts +75 -0
  210. package/src/harness/script-check.ts +25 -8
  211. package/src/harness/sensors.ts +71 -2
  212. package/src/harness/trace.ts +4 -3
  213. package/src/harness/unit-paths.ts +14 -0
  214. package/src/index.ts +32 -0
  215. package/src/orchestrator/ai-rules-updater.ts +2 -0
  216. package/src/orchestrator/context-discovery.ts +50 -0
  217. package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +7 -1
  218. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +10 -5
  219. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +18 -1
  220. package/src/orchestrator/templates/ai-instructions/claude-skill-api-design.md +62 -0
  221. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  222. package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +2 -1
  223. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +19 -2
  224. package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +14 -0
  225. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +10 -5
  226. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +11 -1
  227. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-api-design.md +62 -0
  228. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  229. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +2 -1
  230. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +19 -2
  231. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +14 -0
  232. package/src/orchestrator/templates/specs-api.ts +154 -0
  233. package/src/orchestrator/templates/specs-db.ts +78 -1
  234. package/src/orchestrator/templates/specs-test-data.ts +2 -1
  235. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
  236. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
  237. package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
  238. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
  239. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
  240. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
  241. package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
  242. package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
  243. package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -6
  244. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
  245. package/dist/generators/test-generator/patterns/database-patterns.js +0 -95
  246. package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
  247. package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
  248. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
  249. package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
  250. package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
  251. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
  252. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
  253. package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
  254. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
  255. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
  256. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
  257. package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
  258. package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
  259. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
  260. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
  261. package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
  262. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
  263. package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
  264. package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
  265. package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
  266. package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
  267. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
  268. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
  269. package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
  270. package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
  271. package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
  272. package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
  273. package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
  274. package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
  275. package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
  276. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
  277. package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
  278. package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
  279. package/docs/orchestration-spec.md +0 -267
  280. package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
  281. package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
  282. package/src/generators/test-generator/patterns/database-patterns.ts +0 -96
  283. package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
  284. package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
  285. package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
  286. package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
  287. package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
  288. package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
  289. package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
  290. package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
@@ -6,6 +6,9 @@ user-invocable: false
6
6
 
7
7
  ## ⚠️ Gotchas — read before generating
8
8
 
9
+ - **Write incrementally — never emit the whole suite in one response.** Build the `.feature` in batches via successive `Write`/`Edit` (≈10–15 scenarios per call). For **Full coverage**, write tier-by-tier: `Write` Tier 1 → `Edit` append Tier 2 → `Edit` append Tier 3.
10
+ → One huge `Write` can exceed the model's output-token cap → `API Error: Claude's response exceeded the N output token maximum`. Single-pass full coverage only fits when `CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`; otherwise batch. Batching also lets the audit/reviewer run per batch — higher quality.
11
+
9
12
  - `spec_figma.md` exists → read file only, **NEVER** call `mcp__figma__*`
10
13
  → PAT auth flow already done by `sungen-capture` (mode figma-pat); re-calling fails or duplicates work.
11
14
 
@@ -265,12 +268,26 @@ Security: [S1 – admin only]
265
268
  Then User see [Detail Product Name] header with {{selected_product_name}}
266
269
  And User see [Detail Product Price] text contains {{selected_product_price}}
267
270
  ```
268
- Cross-screen target → tag `@manual` + `# Deferred to a flow (home -> detail)`.
271
+ Cross-screen target → **automate it in the flow** (`/sungen:add-flow`), NOT as a `@manual` screen copy. A single home→target journey runs as one Playwright test, so it is automatable — "needs another screen" is not a reason for `@manual`. The screen keeps its screen-contract scenarios; the flow owns the cross-screen depth.
269
272
  - Filter result (category AND brand, separately): `Then User see all [Result Product Name] contain {{selected_category}}` — proves EVERY item belongs, not one.
270
273
 
271
274
  **Depth is a GATE dimension (harness-roadmap P1) — self-raise, never silently go shallow:**
272
275
  - For every data-correctness theme the catalog marks `depth.requires: data-assertion`, emit its `depth.template` shape by **default** — don't wait for the repair loop. `sungen audit` measures `businessDepth` (ratio of these scenarios that assert data) against an intent threshold (functional ≥ 0.70); below it the **gate FAILs**.
273
- - `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape but tag `@manual` + `# Deferred to a flow (...)`. These are excluded from the ratio (they're correctly deferred), so they don't hurt depth.
276
+ - `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape as an **automated flow scenario** (in the flow — do NOT leave a full-step `@manual` duplicate on the screen). `@manual` is **only** for genuine judgment (M6 visual/UX · M8 not-worth · M9 human) or a missing capability (M1–M5/M7), and it **must** carry a reason code (`@manual:Mx`, or a reason comment the planner can infer). A `@manual` scenario that still has full automatable steps (a data assertion, no visual/mock/a11y judgment) is now flagged by `sungen audit` as `MANUAL-AUTOMATABLE`, and business-critical scenarios you defer to `@manual` are reported as `DEPTH-DEFERRED` (they do NOT silently inflate `businessDepth`). Deferring automatable work to `@manual` lowers quality — automate it in the flow instead.
277
+ - **Pick the right `@manual:Mx` code — it decides which driver can later automate the case** (`sungen audit` flags a code↔reason mismatch). Tag the code that matches the **oracle the reason describes**:
278
+
279
+ | The reason needs… | Code | Unblocked by |
280
+ |---|---|---|
281
+ | a data state you can't make from the UI (empty list, seeded record, missing-image product) | `M1` | data-factory / db |
282
+ | an **API/DB/persistence** assertion (stored value, parameterized-query / SQLi-safe, server-side effect) | `M2` | **api / db** |
283
+ | network / fault injection (offline, slow, request failure) | `M3` | mock |
284
+ | a stable selector / test-id that doesn't exist | `M4` | — (locator contract) |
285
+ | an external dependency (email, payment gateway, download) | `M5` | mail-file / contract |
286
+ | visual / UX / responsive / a11y judgment | `M6` | — (keep manual) |
287
+ | not worth automating · true human judgment | `M8` / `M9` | — (keep manual) |
288
+
289
+ e.g. "submit a payload then check the subscribers **table**" is an API+DB oracle → `@manual:M2` (NOT `M1`); "seed a DB with zero products" is a data state → `M1`; "throttle the network" → `M3`.
290
+ - **Prefer automation-ready `@requires:<cap>` over prose `@manual`.** When you *can* write the steps for a capability-manual case (an API/DB oracle, a seeded state), write it **automation-ready** — the real `@api`/`@query`/… steps tagged `@requires:<cap>` (e.g. `@requires:db @query:subscriber_row`) — instead of a prose `@manual:M2`. It compiles to a skipped-with-reason stub until `sungen capability add <cap>`, then runs as a real test with **no rewrite**. Reserve prose `@manual:Mx` for cases whose steps genuinely can't be expressed (M6/M8/M9 judgment, or a capability with no driver). `sungen audit` reports these as `AUTOMATION-READY-PENDING` (not a gap, not manual).
274
291
  - **If the spec lacks the concrete value** a deep assertion needs (exact message, price, count): still write the deep shape with a `{{var}}` placeholder and leave a `# SPEC-GAP: <field> value not in spec` comment — do **not** downgrade to `see [X] section`. A visible gap is better than a silent shallow pass.
275
292
  - **Blind-Spot Memory:** before finishing, run `sungen blindspot list --prompt` (Bash) and make sure the suite satisfies each recorded pattern (e.g. "for any Add/Create action: check success + resulting data state + duplicate/double-submit"). These are gaps QA hit before — don't repeat them.
276
293
 
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
69
69
  - VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
70
70
  - VP-SEC = checks access control and malicious input
71
71
 
72
+ ### Domain category codes — required for the coverage-balance gate
73
+
74
+ The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
75
+
76
+ | Bucket | Codes | Use for |
77
+ |---|---|---|
78
+ | **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
79
+ | presentation | `UI` | layout / visual state |
80
+ | validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
81
+ | behavior | `LOGIC` | action-driven state changes |
82
+ | navigation | `NAV` | landing on / moving between pages |
83
+
84
+ **On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
85
+
72
86
  ---
73
87
 
74
88
  ## Shared Checks
@@ -18,7 +18,11 @@ You are a **Senior QA Engineer**. You structure test cases by viewpoint categori
18
18
 
19
19
  - **name** — ${input:name:screen or flow name (e.g., login, award-submission)}
20
20
 
21
- **Auto-detect context**: check if `qa/flows/<name>/` exists → flow mode (base path: `qa/flows/<name>/`). Else check `qa/screens/<name>/` → screen mode (base path: `qa/screens/<name>/`).
21
+ **Auto-detect context**: check if `qa/api/<name>/` or `qa/api/flows/<name>/` exists → **API unit mode** (below). Else if `qa/flows/<name>/` → flow mode (base path: `qa/flows/<name>/`). Else `qa/screens/<name>/` → screen mode (base path: `qa/screens/<name>/`).
22
+
23
+ ## API unit mode (driver-api)
24
+
25
+ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the design loop differs — **no visual capture, no selectors**; the contract is the named-endpoint catalog. **Follow the `sungen-api-design` skill end-to-end** instead of the screen/flow steps: `sungen context --area <name>` (discover) → API viewpoint overview → generate `@api`/`@cases`/flow/`@concurrent`/`@query` scenarios → **`sungen audit --area <name>` gate + reviewer + repair loop to businessDepth ≥ 0.7** → record + trace. Then recommend `/sungen-run-test <name>`. The capture / viewpoint-group / selector steps do **not** apply.
22
26
 
23
27
  ## Steps
24
28
 
@@ -26,9 +30,10 @@ You are a **Senior QA Engineer**. You structure test cases by viewpoint categori
26
30
  **Screen**: Verify `qa/screens/${input:name}/` exists. If not → `/sungen-add-screen` first.
27
31
  2. Check if `.feature` already has scenarios.
28
32
  - If yes → summarize existing coverage and ask update mode (options depend on which tiers already exist — see `sungen-tc-generation` skill for details).
29
- - If no → fresh creation. Ask generation scope:
30
- - **1) Tier 1 — Critical & High priority** — ~10-15 scenarios/section covering happy paths, core validation, security basics **(Recommended)**
31
- - **2) Full coverage — All tiers at once** — generates Tier 1 + 2 + 3 in one run. Large output (~40-60 scenarios/section), best for experienced users who want complete coverage immediately
33
+ - If no → fresh creation. **Write the feature file incrementally** (successive writes/edits, ≈10-15 scenarios per call) — never emit the whole suite in one response, or it can exceed the model's output-token cap (`response exceeded the N output token maximum`). Ask generation scope:
34
+ - **1) Tier 1 — Critical & High priority** — ~10-15 scenarios/section: happy paths, core validation, security basics **(Recommended)**
35
+ - **2) Full coverage (incremental)** — Tier 1 + 2 + 3, written tier-by-tier in batches. Safe on any output-token budget.
36
+ - **3) Full coverage (single pass)** — everything in one go (~40-60 scenarios/section). Faster, but **only if you raised your output cap** (`CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`) — otherwise it errors mid-generation. For power users on a high-token model/config.
32
37
  3. **Read project context + screen requirements**
33
38
 
34
39
  **Project context** — check `qa/context.md` (project root, not screen-specific):
@@ -60,7 +65,7 @@ You are a **Senior QA Engineer**. You structure test cases by viewpoint categori
60
65
 
61
66
  4. Follow the `sungen-tc-generation` skill for section identification, viewpoint generation, and output format. **For flows**, use the "Flow Test Generation" section in the skill. When requirements exist, use the "Requirements-Driven Generation" strategy. **For Tier 1**, apply the **Lightweight Guard** — verify required fields, validation rules, business rules, security checks, and key state transitions all have TCs after generation. **For Tier 2+**, **MUST** apply the full **Mapping Contract** — walk every `spec.md` section top-to-bottom and produce the indicated TCs per Table 1; handle `test-viewpoint.md` per Table 2. Do not silently skip sections. Present sections as a numbered list and let user pick.
62
67
  5. Generate or update `.feature` + `test-data.yaml` following `sungen-gherkin-syntax` and `sungen-tc-generation` skills. **For flows**: use `[Screen:Element]` namespace format, namespace test-data by phase, add `@flow` tag.
63
- 5.5. **Quality gate & repair (harness — always run).** Per `sungen-harness-audit`: run `sungen audit --screen ${input:name}` (structural), THEN do an **independent semantic review inline** using the `sungen-reviewer` criteria (does each scenario's steps PROVE its title/viewpoint? observable Thens? business-critical assertion depth?). Merge both sets of issues; if gate FAILs / findings exist, repair (budget 3) and re-audit — GATE missing theme → generate it (cross-screen → write data assertions, tag `@manual`, comment `# Deferred to a flow`); DEPTH → add data assertions; BALANCE → add business-core first; TRACE → align VP ids. Never fake a pass.
68
+ 5.5. **Quality gate & repair (harness — always run).** Per `sungen-harness-audit`: run `sungen audit --screen ${input:name}` (structural), THEN do an **independent semantic review inline** using the `sungen-reviewer` criteria (does each scenario's steps PROVE its title/viewpoint? observable Thens? business-critical assertion depth?). Merge both sets of issues; if gate FAILs / findings exist, repair (budget 3) and re-audit — GATE missing theme → generate it (cross-screen → **automate it in the flow** via `/sungen:add-flow`, NOT a full `@manual` screen duplicate `sungen audit` flags an automatable `@manual` as `MANUAL-AUTOMATABLE`; reserve `@manual:Mx` for true judgment/missing-capability); DEPTH → add data assertions; BALANCE → add business-core first; TRACE → align VP ids. Never fake a pass.
64
69
  5.6. **Record.** `sungen manifest --screen ${input:name}`. Ledger **each phase** (not just repair) — pick one `runId` at the start and pass it so `trace`/`ledger report` show THIS run, not a mix: `sungen ledger record --screen ${input:name} --run <runId> --step <discovery|viewpoint|gherkin|audit|repair:N> --ms <elapsed>`. On re-run, start with `sungen manifest --screen ${input:name} --diff` and only regenerate changed sections.
65
70
  6. **Converge — show the trace.** Run `sungen trace --screen ${input:name}` and present: process map (phases + repair rounds), bottlenecks, **HUMAN-LOOP FOCUS** (@manual to verify), audit score + gate + residual gaps. Then offer next steps based on which tier was just generated:
66
71
 
@@ -30,7 +30,16 @@ Count 0 → offer the user:
30
30
 
31
31
  Skip when `--env` matches the base locale.
32
32
 
33
- **Auto-detect context**: check if `qa/flows/<name>/` exists → flow mode (base path: `qa/flows/<name>/`). Else check `qa/screens/<name>/` → screen mode (base path: `qa/screens/<name>/`).
33
+ **Auto-detect context**: check if `qa/api/<name>/` or `qa/api/flows/<name>/` exists → **API unit mode** (below). Else if `qa/flows/<name>/` → flow mode (base path: `qa/flows/<name>/`). Else `qa/screens/<name>/` → screen mode (base path: `qa/screens/<name>/`).
34
+
35
+ ## API unit mode (driver-api) — no selectors
36
+
37
+ If the unit is **api-first**, skip every selector/capture phase (an API test has no DOM):
38
+ 1. **Resolve the datasource** — `base_url` + auth wired in `qa/datasources.yaml` + `.env.qa` (`${X_URL}` from `sungen api init`); a `production` datasource is refused unless `SUNGEN_ALLOW_PROD=1`.
39
+ 2. **Compile**: `npx sungen generate --area <name>` → `specs/generated/api/<name>/`.
40
+ 3. **Run**: `npx playwright test specs/generated/api/<name>/<name>.spec.ts`.
41
+ 4. **Auto-fix** (use `sungen-error-mapping`): 401/403 → `@hybrid`+`@auth` or `Bearer :token` header (`sungen makeauth`); base_url unresolved → set `${X_URL}`; missing param → trace `{{var}}` to test-data/a prior `@api` response; `expect.status` mismatch → reconcile against `apis.yaml` (re-`generate --area`, never hand-edit the spec); **400 "parameter missing" / body ignored → set `encoding: form` (or `multipart`) on the catalog entry, don't mark @manual**; flaky → self-clean + `@concurrent` caps.
42
+ 5. **Integrity + trace** — `sungen script-check --area <name>` (1:1; on DRIFT re-`generate --area`, never hand-edit the spec) + `sungen trace --area <name>` (process map + HUMAN-LOOP FOCUS). Report + offer next steps.
34
43
 
35
44
  ## Pre-run (phased — per `sungen-selector-fix` skill)
36
45
 
@@ -84,6 +93,7 @@ Skip when `--env` matches the base locale.
84
93
  7. **Phase 3 — Full Run**: Run all tests. Fix only **new** failures (elements unique to `@normal`/`@low`). Max 1 attempt. Don't loop on low-priority failures.
85
94
  8. **Phase 4 — Regression**: One final full run. Report results. No more fix loops.
86
95
  9. **Integrity & trace (always run after the final run).** `sungen script-check --screen <name>` — verify the spec is a **1:1** of the Gherkin; if **DRIFT**, re-run `sungen generate --screen <name>` (never hand-edit the `.spec.ts` — auto-fix edits `selectors.yaml`). Then `sungen ledger record --screen <name> --step run --ms <elapsed>` and `sungen trace --screen <name>` to show the process map + bottlenecks + **HUMAN-LOOP FOCUS**.
96
+ 10. **Capability-pending offer (consent-gated).** If `sungen audit` reports `AUTOMATION-READY-PENDING` (or `@requires:<cap>` tests are skipped "requires …"), offer: *"N scenario(s) are automation-ready — enable `<cap>` to run them? (`sungen capability add <cap>`)"*. Only on the user's yes, run `sungen capability add <cap>` + re-run; on no, leave skipped (not failures, not manual). **Never auto-install.**
87
97
 
88
98
  ## Playwright command guidelines
89
99
 
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: sungen-api-design
3
+ description: The API-first design loop for an api unit (qa/api/<area> or qa/api/flows/<flow>) — discover the catalog, lay out the API viewpoints, generate @api/@cases/flow/@concurrent scenarios, then drive the sungen audit --area gate + reviewer + repair to a high businessDepth (≥0.7). Use when create-test/run-test detects an api unit (no selectors, no visual capture).
4
+ ---
5
+
6
+ # API design loop (driver-api · Orchestration + Harness)
7
+
8
+ Use this when the unit is **api-first** — `qa/api/<area>/` or `qa/api/flows/<flow>/`. There are **no selectors and no visual capture**: the contract is the **named-endpoint catalog** (`api/apis.yaml`), referenced by `@api:<name>`. QA writes **no HTTP code**. Full annotation reference: the **API Steps** guide (`@api` / `@cases` / flows / `@concurrent` / `@hybrid`).
9
+
10
+ ## The loop (mirror of /sungen:design, API-native)
11
+
12
+ ### 1. Discover (no capture)
13
+ Run `sungen context --area <name>` — it reads the catalog and prints the **endpoints** + the **generation units** (one `matrix` unit per endpoint, an `async` unit per mutating endpoint, a `flow` unit for an api flow). Read `qa/api/<name>/requirements/spec.md` if present. No `apis.yaml` yet? → `sungen api import <openapi|csv>` or `sungen api add --area <name>` first.
14
+
15
+ ### 2. API viewpoint overview (by method-profile)
16
+ For each endpoint, cover its viewpoints — severity-weighted by method:
17
+
18
+ | Profile | Endpoints | Must cover | Then |
19
+ |---|---|---|---|
20
+ | read | GET, HEAD | `contract` (status + body shape) | `pagination`/`filter` (list), `not-found` (by-id) |
21
+ | mutating | POST/PUT/PATCH/DELETE | `contract`, `error` (validation/4xx/auth) | `idempotency` (`@concurrent`), `side-effect` (`@query`) |
22
+
23
+ Bands: **~70%** success+failure matrix · **~20%** flows (auth/CRUD chains) · **~10%** async/idempotency.
24
+
25
+ ### 3. Generate (incremental — never the whole suite in one Write)
26
+ - **Contract**: `@api:<name>` + `expect {{name.status}} is …` **and a body assertion** (`{{name.body.<path>}}`).
27
+ - **Error matrix**: `@api:<name>(p={{p}}) @cases:<dataset>` — one scenario, a dataset of `input → expected status`.
28
+ - **Flow**: ordered `@api` tags threading a prior response (`token={{login.body.token}}` → the catalog `Bearer :token` header; `id={{create.body.id}}` → a path param). Self-clean (delete what you create).
29
+ - **Idempotency**: `@api:<name> @concurrent:N` + `expect {{name.ok_count}} is 1`, cross-checked with `@query` (the DB is the oracle).
30
+
31
+ ### 4. Gate + repair (always — businessDepth ≥ 0.7 is the bar)
32
+ Run `sungen audit --area <name>`; read `gateStatus` + `findings`. Then the **semantic reviewer** (sungen-reviewer sub-agent, API criteria). Repair **both** (budget 3 rounds), re-audit until PASS:
33
+
34
+ | Finding | Repair |
35
+ |---|---|
36
+ | `VIEWPOINT-API-CONTRACT` | the endpoint is invoked but its response is never asserted → add `expect {{name.status}}` + a `{{name.body.…}}` check |
37
+ | `VIEWPOINT-API-ERROR` | a mutating endpoint has no failure scenario → add a `@cases` error matrix (or an explicit 4xx) |
38
+ | `VIEWPOINT-API-IDEMPOTENCY` | a mutating endpoint has no race check → add `@concurrent:N` + a `@query` DB cross-check |
39
+ | `VIEWPOINT-API-MANUAL-AUTOMATABLE` | a `@manual` scenario whose endpoint resolves is automatable → drop `@manual`, use `@api` (+ `@cases`); reserve `@manual` for genuine judgment cases |
40
+ | **`DEPTH-FAIL`** (businessDepth < 0.7) | a **mutating success** scenario asserts only `status` → make it **prove the effect**: assert a response **body** field, a **`@query`** side-effect, or a **`@concurrent` `ok_count`** invariant. (An error/`@cases` scenario proving the status is correct — it is *not* depth-required.) |
41
+
42
+ Stop when the gate PASSes + businessDepth ≥ 0.7, or the budget is exhausted → report residual gaps honestly (mark genuinely-unautomatable cases `@manual` with an oracle). Never fake a pass.
43
+
44
+ ### 5. Record + converge
45
+ `sungen manifest --area <name>` (reuse) and ledger each phase; show the trace + the HUMAN-LOOP FOCUS. (Integrity `script-check`/`trace` for api: see run-test.)
46
+
47
+ ## Taxonomy (label scenarios correctly)
48
+
49
+ | Class | What | Examples |
50
+ |---|---|---|
51
+ | **Functional** | single-endpoint behaviour | happy contract · error/validation (`@cases`) · boundary/edge |
52
+ | **Functional — flow/integration** | multi-endpoint journeys | auth/CRUD lifecycle (`create → login → get → delete`), cross-endpoint invariants |
53
+ | **Non-Functional** | performance · reliability · **security** · concurrency/idempotency | `@concurrent` race/idempotency |
54
+
55
+ A flow (`create → login → delete`) is a **Functional integration** test, **not** non-functional — don't file it under "Non-Functional". Reserve non-functional for perf/security/concurrency.
56
+
57
+ ## Rules
58
+ - **No HTTP, no selectors** — only `.feature` + the reviewed `apis.yaml` + `test-data`.
59
+ - **Non-prod default** — a `production` datasource is refused unless `SUNGEN_ALLOW_PROD=1`.
60
+ - **The DB is the oracle** for idempotency/side-effects — HTTP status alone can lie; pair `@api` with `@query`.
61
+ - **`@parallel` + mutating endpoints** — give each scenario **isolated data** (a `{{$uuid}}` email, a `@cases` row, or its own created resource) and **self-clean** (delete what it created); shared inputs race under parallel execution.
62
+ - **No dead data** — every `test-data` key must be bound into a scenario (`{{key}}`, a `@cases` dataset, or an override). `sungen audit`/the generate lint flag unreferenced keys.
@@ -213,6 +213,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
213
213
  | `@flow` | Mark feature as E2E flow (cross-screen testing) |
214
214
  | `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
215
215
  | `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
216
+ | `@api:name` | API: run the named request from `api/apis.yaml` (precondition) and bind the response to `{{name}}`; assert with `expect {{name.status}} …` + path access (`{{name.body.<path>}}`). Override params `@api:name(p={{v}})`. Repeatable. (Optional API Driver) |
216
217
 
217
218
  ### Data-driven scenarios (`@cases`)
218
219
 
@@ -58,7 +58,7 @@ Use these when repairing GATE/DEPTH findings for the hard viewpoints (cart/detai
58
58
  ```
59
59
  `see all [X] contain {{v}}` asserts EVERY matching element contains the value → "all displayed products belong to the selected category/brand", not just one.
60
60
 
61
- > Cross-screen flows (home → detail/cart): if the target screen is a separate screen, prefer a **flow** (`/sungen:add-flow`) so the journey is one test. On a single screen, keep the cross-screen assertion but tag `@manual` with a `# Deferred to a flow` comment.
61
+ > Cross-screen flows (home → detail/cart): **automate the journey as a flow** (`/sungen:add-flow`) it runs as one test, so it is automatable. Do **not** keep a full `@manual` duplicate of it on the screen (a non-running dead copy that `sungen audit` flags as `MANUAL-AUTOMATABLE` and that inflates nothing — deferred business-critical is reported as `DEPTH-DEFERRED`). The screen keeps its screen-contract; the flow owns the cross-screen depth. `@manual` is for genuine judgment / missing-capability only, tagged `@manual:Mx`.
62
62
 
63
63
  ## Repair loop rules
64
64
 
@@ -66,6 +66,7 @@ Use these when repairing GATE/DEPTH findings for the hard viewpoints (cart/detai
66
66
  2. **Stop when** `gateStatus == PASS` AND `findings` empty — or budget exhausted.
67
67
  3. **Never fake a pass.** A shallow `see [Cart] page` does not satisfy `cart-correctness`. If a gap is genuinely cross-screen or needs capabilities the DSL lacks (e.g. capture an element value to compare elsewhere), **report it as a residual gap / flow item** instead of forcing a green gate.
68
68
  4. **EP/data families are OK.** A `duplicates` cluster with `sameDataLikely=false` is an intentional equivalence-partition family (e.g. many invalid-email cases) — keep it; only collapse `sameDataLikely=true` exact duplicates.
69
+ 5. **Advisory findings — surface, don't gate.** `MANUAL-REASON-MISMATCH` → fix the scenario's `@manual:Mx` code (so the planner recommends the right driver) during repair. `CAPABILITY-SUGGESTION` → **present it to the user as a next-step option** (e.g. "N @manual could be automated — `sungen capability add api db`?"), **recommend-only — never auto-install**. Neither fails the gate.
69
70
 
70
71
  ## Discovery / fallback tree (when input is limited)
71
72
 
@@ -6,6 +6,9 @@ user-invocable: false
6
6
 
7
7
  ## ⚠️ Gotchas — read before generating
8
8
 
9
+ - **Write incrementally — never emit the whole suite in one response.** Build the `.feature` in batches via successive `Write`/`Edit` (≈10–15 scenarios per call). For **Full coverage**, write tier-by-tier: `Write` Tier 1 → `Edit` append Tier 2 → `Edit` append Tier 3.
10
+ → One huge `Write` can exceed the model's output-token cap → `API Error: Claude's response exceeded the N output token maximum`. Single-pass full coverage only fits when `CLAUDE_CODE_MAX_OUTPUT_TOKENS ≥ 64000`; otherwise batch. Batching also lets the audit/reviewer run per batch — higher quality.
11
+
9
12
  - `spec_figma.md` exists → read file only, **NEVER** call `mcp__figma__*`
10
13
  → PAT auth flow already done by `sungen-capture` (mode figma-pat); re-calling fails or duplicates work.
11
14
 
@@ -265,12 +268,26 @@ Security: [S1 – admin only]
265
268
  Then User see [Detail Product Name] header with {{selected_product_name}}
266
269
  And User see [Detail Product Price] text contains {{selected_product_price}}
267
270
  ```
268
- Cross-screen target → tag `@manual` + `# Deferred to a flow (home -> detail)`.
271
+ Cross-screen target → **automate it in the flow** (`/sungen:add-flow`), NOT as a `@manual` screen copy. A single home→target journey runs as one Playwright test, so it is automatable — "needs another screen" is not a reason for `@manual`. The screen keeps its screen-contract scenarios; the flow owns the cross-screen depth.
269
272
  - Filter result (category AND brand, separately): `Then User see all [Result Product Name] contain {{selected_category}}` — proves EVERY item belongs, not one.
270
273
 
271
274
  **Depth is a GATE dimension (harness-roadmap P1) — self-raise, never silently go shallow:**
272
275
  - For every data-correctness theme the catalog marks `depth.requires: data-assertion`, emit its `depth.template` shape by **default** — don't wait for the repair loop. `sungen audit` measures `businessDepth` (ratio of these scenarios that assert data) against an intent threshold (functional ≥ 0.70); below it the **gate FAILs**.
273
- - `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape but tag `@manual` + `# Deferred to a flow (...)`. These are excluded from the ratio (they're correctly deferred), so they don't hurt depth.
276
+ - `depth.cross_screen: true` (cart / detail / filter / brand correctness) → write the deep capture/compare shape as an **automated flow scenario** (in the flow — do NOT leave a full-step `@manual` duplicate on the screen). `@manual` is **only** for genuine judgment (M6 visual/UX · M8 not-worth · M9 human) or a missing capability (M1–M5/M7), and it **must** carry a reason code (`@manual:Mx`, or a reason comment the planner can infer). A `@manual` scenario that still has full automatable steps (a data assertion, no visual/mock/a11y judgment) is now flagged by `sungen audit` as `MANUAL-AUTOMATABLE`, and business-critical scenarios you defer to `@manual` are reported as `DEPTH-DEFERRED` (they do NOT silently inflate `businessDepth`). Deferring automatable work to `@manual` lowers quality — automate it in the flow instead.
277
+ - **Pick the right `@manual:Mx` code — it decides which driver can later automate the case** (`sungen audit` flags a code↔reason mismatch). Tag the code that matches the **oracle the reason describes**:
278
+
279
+ | The reason needs… | Code | Unblocked by |
280
+ |---|---|---|
281
+ | a data state you can't make from the UI (empty list, seeded record, missing-image product) | `M1` | data-factory / db |
282
+ | an **API/DB/persistence** assertion (stored value, parameterized-query / SQLi-safe, server-side effect) | `M2` | **api / db** |
283
+ | network / fault injection (offline, slow, request failure) | `M3` | mock |
284
+ | a stable selector / test-id that doesn't exist | `M4` | — (locator contract) |
285
+ | an external dependency (email, payment gateway, download) | `M5` | mail-file / contract |
286
+ | visual / UX / responsive / a11y judgment | `M6` | — (keep manual) |
287
+ | not worth automating · true human judgment | `M8` / `M9` | — (keep manual) |
288
+
289
+ e.g. "submit a payload then check the subscribers **table**" is an API+DB oracle → `@manual:M2` (NOT `M1`); "seed a DB with zero products" is a data state → `M1`; "throttle the network" → `M3`.
290
+ - **Prefer automation-ready `@requires:<cap>` over prose `@manual`.** When you *can* write the steps for a capability-manual case (an API/DB oracle, a seeded state), write it **automation-ready** — the real `@api`/`@query`/… steps tagged `@requires:<cap>` (e.g. `@requires:db @query:subscriber_row`) — instead of a prose `@manual:M2`. It compiles to a skipped-with-reason stub until `sungen capability add <cap>`, then runs as a real test with **no rewrite**. Reserve prose `@manual:Mx` for cases whose steps genuinely can't be expressed (M6/M8/M9 judgment, or a capability with no driver). `sungen audit` reports these as `AUTOMATION-READY-PENDING` (not a gap, not manual).
274
291
  - **If the spec lacks the concrete value** a deep assertion needs (exact message, price, count): still write the deep shape with a `{{var}}` placeholder and leave a `# SPEC-GAP: <field> value not in spec` comment — do **not** downgrade to `see [X] section`. A visible gap is better than a silent shallow pass.
275
292
  - **Blind-Spot Memory:** before finishing, run `sungen blindspot list --prompt` (Bash) and make sure the suite satisfies each recorded pattern (e.g. "for any Add/Create action: check success + resulting data state + duplicate/double-submit"). These are gaps QA hit before — don't repeat them.
276
293
 
@@ -69,6 +69,20 @@ A screen often matches several patterns at once — a login screen is *both* a f
69
69
  - VP-LOGIC = outcome depends on the user's *action* (click, submit, navigate)
70
70
  - VP-SEC = checks access control and malicious input
71
71
 
72
+ ### Domain category codes — required for the coverage-balance gate
73
+
74
+ The 4 viewpoints above are the *generic* axes. On a domain screen, the `VP-<CAT>` code must use the **canonical short code** for what the scenario exercises, so the audit's coverage-balance gate buckets it correctly. Use these exact codes — **never long-form or freeform** (`VP-NAV` not `VP-NAVIGATION`, `VP-SUB` not `VP-SUBSCRIPTION`, `VP-FILTER` not `VP-FILTERING`):
75
+
76
+ | Bucket | Codes | Use for |
77
+ |---|---|---|
78
+ | **business-core** | `LIST` · `CART` · `PRODUCT` · `FILTER` · `CHECKOUT` · `ORDER` | the screen's core domain data/actions (product list, cart, checkout, order, filtered results) |
79
+ | presentation | `UI` | layout / visual state |
80
+ | validation-security | `VAL` · `SEC` · `SUB` | input validation · access/injection · subscribe/newsletter |
81
+ | behavior | `LOGIC` | action-driven state changes |
82
+ | navigation | `NAV` | landing on / moving between pages |
83
+
84
+ **On a business-core page** (product list, cart, checkout, search results), the core data scenarios MUST carry a **business-core** code (`VP-LIST-*`, `VP-CART-*`, `VP-PRODUCT-*`, …) — not a generic `VP-UI`/`VP-LOGIC` or a freeform `VP-<word>`. A freeform/long-form prefix parses as `NONE`, scores **0 on the balance axis**, and drops the audit score (~9.3 → ~7.7 in practice). Keep `VP-UI/VAL/LOGIC/SEC` for the cross-cutting checks; give the domain scenarios their domain code.
85
+
72
86
  ---
73
87
 
74
88
  ## Shared Checks
@@ -0,0 +1,154 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Sungen API Driver — runtime helper (auto-generated into specs/api.ts).
4
+ *
5
+ * Runs a catalog-defined HTTP request and returns { status, ok, body, headers } — bound to a
6
+ * `{{name}}` variable by the `@api:<name>` annotation, asserted with `expect {{name.status}} …` /
7
+ * `{{name.body.<path>}}`. Base URL + auth come from a `kind: api` datasource in datasources.yaml,
8
+ * with `${VAR}` resolved from .env.qa / process.env — never inline.
9
+ *
10
+ * Safety: a datasource flagged `env: production` is refused unless SUNGEN_ALLOW_PROD=1.
11
+ * DO NOT EDIT — regenerated by `sungen generate`.
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import { request, type APIRequestContext } from '@playwright/test';
16
+
17
+ interface ApiDataSource {
18
+ kind?: string;
19
+ base_url?: string;
20
+ baseUrl?: string;
21
+ env?: string;
22
+ headers?: Record<string, string>;
23
+ timeout_ms?: number;
24
+ }
25
+
26
+ function loadEnvQa(): void {
27
+ for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
28
+ const p = path.join(process.cwd(), name);
29
+ if (!name.endsWith('.') && fs.existsSync(p)) {
30
+ for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
31
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
32
+ if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ function loadConfig(): Record<string, ApiDataSource> {
39
+ loadEnvQa();
40
+ const file = [path.join(process.cwd(), 'datasources.yaml'), path.join(process.cwd(), 'qa', 'datasources.yaml')].find((f) => fs.existsSync(f));
41
+ if (!file) throw new Error('API Driver: no datasources.yaml found (project root or qa/).');
42
+ const raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
43
+ const { parse } = require('yaml');
44
+ const doc = parse(raw) || {};
45
+ return doc.datasources || {};
46
+ }
47
+
48
+ function substitute(text: string, params: Record<string, any>): string {
49
+ return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => encodeURIComponent(String(params[p] ?? '')));
50
+ }
51
+
52
+ class ApiClient {
53
+ private configs: Record<string, ApiDataSource> | null = null;
54
+
55
+ private cfg(name?: string): { key: string; conf: ApiDataSource } {
56
+ if (!this.configs) this.configs = loadConfig();
57
+ const key = name || Object.keys(this.configs).find((k) => (this.configs![k].kind || 'api') === 'api') || Object.keys(this.configs)[0];
58
+ const conf = this.configs[key];
59
+ if (!conf) throw new Error(`API Driver: datasource "${key}" not found in datasources.yaml`);
60
+ if (conf.env === 'production' && process.env.SUNGEN_ALLOW_PROD !== '1') {
61
+ throw new Error(`API Driver: datasource "${key}" is env: production — refused (set SUNGEN_ALLOW_PROD=1 to override).`);
62
+ }
63
+ return { key, conf };
64
+ }
65
+
66
+ /**
67
+ * Run a catalog request and return the response. `req` is embedded at compile time; `params` (path
68
+ * `:id`, JSON body `:fields`, and header `:tokens`) bind at runtime. Catalog `headers` layer over the
69
+ * datasource headers and may carry `:param` placeholders — e.g. `authorization: "Bearer :token"` with
70
+ * the dynamic token threaded from a prior response (flow chaining).
71
+ */
72
+ async call(
73
+ label: string,
74
+ req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; headers?: Record<string, string>; datasource?: string },
75
+ params: Record<string, any> = {},
76
+ opts: { storageState?: string } = {},
77
+ ): Promise<{ status: number; ok: boolean; body: any; headers: Record<string, string> }> {
78
+ const { conf } = this.cfg(req.datasource);
79
+ const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
80
+ if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
81
+ const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
82
+
83
+ const headers: Record<string, string> = { ...(conf.headers || {}) };
84
+ // catalog headers; :param tokens bind at runtime — raw (no URL-encoding, unlike the path)
85
+ for (const [k, v] of Object.entries(req.headers || {}))
86
+ headers[k] = String(v).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
87
+ // Body: substitute `:param` into the body template (object values), then encode per `encoding`.
88
+ let body: any;
89
+ if (req.body !== undefined && req.body !== null) {
90
+ body = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
91
+ }
92
+ // Map the wire format to the right Playwright option (#345): json → data (application/json,
93
+ // default), form → form (application/x-www-form-urlencoded), multipart → multipart (form-data).
94
+ const bodyOpt: Record<string, unknown> = {};
95
+ if (body !== undefined) {
96
+ const enc = req.encoding ?? 'json';
97
+ if (enc === 'form') bodyOpt.form = body;
98
+ else if (enc === 'multipart') bodyOpt.multipart = body;
99
+ else bodyOpt.data = body;
100
+ }
101
+
102
+ // Playwright APIRequestContext: same runner/report/retries as UI tests. @hybrid passes
103
+ // `storageState` (the @auth role's saved session) so the request shares the browser's
104
+ // authenticated cookies. Disposed per call so no request context lingers and hangs the process.
105
+ const ctx: APIRequestContext = await request.newContext({
106
+ baseURL: base,
107
+ extraHTTPHeaders: headers,
108
+ timeout: conf.timeout_ms ?? 15000,
109
+ ...(opts.storageState ? { storageState: opts.storageState } : {}),
110
+ });
111
+ try {
112
+ const res = await ctx.fetch(urlPath, { method: req.method, ...bodyOpt });
113
+ const text = await res.text();
114
+ let parsed: any = text;
115
+ try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
116
+ return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
117
+ } finally {
118
+ await ctx.dispose();
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Fire the same request N times in parallel (the `@concurrent:N` primitive) and bind aggregates —
124
+ * the idempotency/race oracle. Returns the full `responses` array plus `ok_count`, `status_counts`,
125
+ * and `statuses`, asserted with `expect {{name.ok_count}} is 1` (and cross-checked against the DB via
126
+ * `@query` to prove "exactly one charge"). Path access works on the bound value: `{{name.ok_count}}`,
127
+ * `{{name.status_counts.409}}`, `{{name.responses.count}}`, `{{name.responses[0].body.id}}`.
128
+ */
129
+ async callN(
130
+ label: string,
131
+ req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; headers?: Record<string, string>; datasource?: string },
132
+ params: Record<string, any> = {},
133
+ n = 1,
134
+ opts: { storageState?: string } = {},
135
+ ): Promise<{
136
+ responses: Array<{ status: number; ok: boolean; body: any; headers: Record<string, string> }>;
137
+ ok_count: number;
138
+ status_counts: Record<string, number>;
139
+ statuses: number[];
140
+ }> {
141
+ const count = Math.max(1, Math.floor(n));
142
+ const responses = await Promise.all(Array.from({ length: count }, () => this.call(label, req, params, opts)));
143
+ const status_counts: Record<string, number> = {};
144
+ for (const r of responses) status_counts[String(r.status)] = (status_counts[String(r.status)] || 0) + 1;
145
+ return {
146
+ responses,
147
+ ok_count: responses.filter((r) => r.ok).length,
148
+ status_counts,
149
+ statuses: responses.map((r) => r.status),
150
+ };
151
+ }
152
+ }
153
+
154
+ export const api = new ApiClient();
@@ -21,12 +21,73 @@ const ident = (s: string): string => {
21
21
  return s;
22
22
  };
23
23
 
24
+ interface SshConfig {
25
+ host: string; // jump host reachable from the runner
26
+ port?: number; // default 22
27
+ user: string;
28
+ private_key?: string; // PEM contents (from ${VAR} in .env.qa) — preferred for CI
29
+ private_key_path?: string; // or a filesystem path (local dev)
30
+ passphrase?: string; // for an encrypted key
31
+ known_host?: string; // base64 of the server's host key to pin (optional; else warn-and-proceed)
32
+ }
33
+
24
34
  interface DataSourceConfig {
25
35
  engine: 'postgres' | 'mysql' | 'sqlite';
26
36
  url: string;
27
37
  readonly?: boolean;
28
38
  statement_timeout_ms?: number;
29
39
  max_rows?: number;
40
+ // Cách B (fallback): tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
41
+ // still run on the runner; only PG traffic crosses. See docs/spec/sungen_data_driver_ssh_tunnel_spec.md.
42
+ ssh?: SshConfig;
43
+ }
44
+
45
+ /**
46
+ * Open a local TCP forward (127.0.0.1:<ephemeral> → ssh bastion → dstHost:dstPort) for a DB socket.
47
+ * Sockets are unref()'d so a dangling tunnel never keeps the test process alive after the run.
48
+ */
49
+ async function openSshTunnel(ssh: SshConfig, dstHost: string, dstPort: number): Promise<{ host: string; port: number; close: () => void }> {
50
+ const { Client } = require('ssh2');
51
+ const net = require('net');
52
+ const privateKey = ssh.private_key
53
+ ? ssh.private_key
54
+ : ssh.private_key_path
55
+ ? fs.readFileSync(ssh.private_key_path.replace(/^~(?=\/)/, process.env.HOME || ''), 'utf8')
56
+ : undefined;
57
+ if (!privateKey) throw new Error('Data Driver: datasource `ssh` requires `private_key` or `private_key_path`.');
58
+
59
+ const conn = new Client();
60
+ await new Promise<void>((resolve, reject) => {
61
+ conn.on('ready', resolve).on('error', reject).connect({
62
+ host: ssh.host,
63
+ port: ssh.port ?? 22,
64
+ username: ssh.user,
65
+ privateKey,
66
+ passphrase: ssh.passphrase,
67
+ hostVerifier: (key: Buffer) => {
68
+ const got = Buffer.isBuffer(key) ? key.toString('base64') : String(key);
69
+ if (ssh.known_host) {
70
+ if (got === ssh.known_host.trim()) return true;
71
+ throw new Error(`Data Driver: SSH host-key mismatch for ${ssh.host} — refused (known_host pin).`);
72
+ }
73
+ console.warn(`Data Driver: SSH host key for ${ssh.host} is not pinned (set datasource ssh.known_host to verify). Proceeding (TOFU).`);
74
+ return true;
75
+ },
76
+ });
77
+ });
78
+
79
+ const server = net.createServer((sock: any) => {
80
+ conn.forwardOut(sock.remoteAddress || '127.0.0.1', sock.remotePort || 0, dstHost, dstPort, (err: any, stream: any) => {
81
+ if (err) { sock.destroy(); return; }
82
+ sock.pipe(stream).pipe(sock);
83
+ });
84
+ });
85
+ await new Promise<void>((resolve, reject) => server.on('error', reject).listen(0, '127.0.0.1', () => resolve()));
86
+ const addr = server.address();
87
+ const port = addr && typeof addr === 'object' ? addr.port : 0;
88
+ server.unref(); // don't keep the event loop alive after tests
89
+ try { (conn as any)._sock?.unref?.(); } catch { /* best-effort */ }
90
+ return { host: '127.0.0.1', port, close: () => { try { server.close(); } catch {} try { conn.end(); } catch {} } };
30
91
  }
31
92
 
32
93
  function loadEnvQa(): void {
@@ -64,6 +125,7 @@ type Engine = { query(sql: string, params: any[]): Promise<any[]>; };
64
125
  class DataSource {
65
126
  private configs: Record<string, DataSourceConfig> | null = null;
66
127
  private engines = new Map<string, Engine>();
128
+ private tunnels: Array<{ close: () => void }> = [];
67
129
 
68
130
  private cfg(name?: string): { key: string; conf: DataSourceConfig } {
69
131
  if (!this.configs) this.configs = loadConfig();
@@ -79,10 +141,19 @@ class DataSource {
79
141
  if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
80
142
  let engine: Engine;
81
143
  if (conf.engine === 'postgres') {
144
+ let connectionString = conf.url;
145
+ if (conf.ssh) { // Cách B: tunnel the DB socket through a bastion
146
+ const u = new URL(conf.url);
147
+ const t = await openSshTunnel(conf.ssh, u.hostname, Number(u.port || 5432));
148
+ this.tunnels.push(t);
149
+ u.hostname = t.host; u.port = String(t.port); // rewrite host:port → 127.0.0.1:<tunnel> (keep user/pass/db/query)
150
+ connectionString = u.toString();
151
+ }
82
152
  const { Pool } = require('pg');
83
- const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
153
+ const pool = new Pool({ connectionString, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
84
154
  engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
85
155
  } else if (conf.engine === 'sqlite') {
156
+ if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
86
157
  const Database = require('better-sqlite3');
87
158
  const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
88
159
  engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
@@ -93,6 +164,12 @@ class DataSource {
93
164
  return { engine, conf };
94
165
  }
95
166
 
167
+ /** Close any open SSH tunnels (optional explicit teardown; tunnels are unref'd so the process exits regardless). */
168
+ close(): void {
169
+ for (const t of this.tunnels) t.close();
170
+ this.tunnels = [];
171
+ }
172
+
96
173
  private build(table: string, filter: Record<string, any>): { sql: string; params: any[] } {
97
174
  const cols = Object.keys(filter);
98
175
  const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
@@ -23,7 +23,8 @@ export class TestDataLoader {
23
23
  */
24
24
  static load(screenName: string, featureName: string): TestDataLoader {
25
25
  let baseDir: string;
26
- if (screenName.startsWith('flows/')) {
26
+ if (screenName.startsWith('flows/') || screenName.startsWith('api/')) {
27
+ // flows/<flow> · api/<area> · api/flows/<flow> → qa/<screenName>/test-data
27
28
  baseDir = path.join(process.cwd(), 'qa', screenName, 'test-data');
28
29
  } else {
29
30
  baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
@@ -1,7 +0,0 @@
1
- import { StepPattern } from './types';
2
- /**
3
- * Assertion patterns: visibility, text content, state, attributes
4
- * Uses template engine for framework-agnostic code generation
5
- */
6
- export declare const assertionPatterns: StepPattern[];
7
- //# sourceMappingURL=assertion-patterns.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"assertion-patterns.d.ts","sourceRoot":"","sources":["../../../../src/generators/test-generator/patterns/assertion-patterns.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAoB,MAAM,SAAS,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,WAAW,EA2qB1C,CAAC"}