@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
@@ -0,0 +1,52 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { discoverUnitContext } from '../../orchestrator/context-discovery';
5
+
6
+ /**
7
+ * `sungen context` — run the Discover → Contextualize phase (AO-3) for a unit and emit the
8
+ * normalized Context + generation-unit work-list. The `/sungen:design` loop calls this first so it
9
+ * knows WHAT to generate (the endpoints/screens + their modes) before authoring scenarios. Pure +
10
+ * deterministic; writes `.sungen/reports/<unit>-context.json`.
11
+ */
12
+ export function registerContextCommand(program: Command): void {
13
+ program
14
+ .command('context')
15
+ .description('Discover + contextualize a unit (screen/flow/api area) → Context + generation units')
16
+ .option('-s, --screen <name>', 'Screen name')
17
+ .option('--flow <name>', 'Flow name (resolved as flows/<name>)')
18
+ .option('--api <name>', 'API-first area or api flow (resolved as api/<name>)')
19
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
20
+ .option('--json', 'Print the raw JSON')
21
+ .action(async (o: { screen?: string; flow?: string; api?: string; area?: string; json?: boolean }) => {
22
+ try {
23
+ const unitId = (o.api || o.area) ? `api/${o.api || o.area}` : o.flow ? `flows/${o.flow}` : o.screen;
24
+ if (!unitId) throw new Error('Provide --screen <name>, --flow <name>, or --api <area>.');
25
+ const cwd = process.cwd();
26
+ const result = await discoverUnitContext(unitId, cwd);
27
+
28
+ const outDir = path.join(cwd, '.sungen', 'reports');
29
+ fs.mkdirSync(outDir, { recursive: true });
30
+ const slug = unitId.replace(/\//g, '-');
31
+ const outPath = path.join(outDir, `${slug}-context.json`);
32
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf-8');
33
+ if (o.json) { console.log(JSON.stringify(result, null, 2)); return; }
34
+
35
+ const L = console.log;
36
+ const facts = result.context.facts as { endpoints?: Array<{ name: string; method: string; path: string }> };
37
+ L(`\n━━━ Context: ${unitId} (capability: ${result.capability}) ━━━`);
38
+ const srcKeys = Object.keys(result.context.sources);
39
+ L(` Sources: ${srcKeys.length ? srcKeys.join(', ') : '(none)'}`);
40
+ if (facts.endpoints?.length) {
41
+ L(` Endpoints (${facts.endpoints.length}):`);
42
+ for (const e of facts.endpoints) L(` • ${e.method} ${e.path} (@api:${e.name})`);
43
+ }
44
+ const byMode = result.units.reduce<Record<string, number>>((m, u) => { m[u.mode] = (m[u.mode] || 0) + 1; return m; }, {});
45
+ L(` Generation units: ${result.units.length} — ${Object.entries(byMode).map(([m, n]) => `${m}×${n}`).join(' · ') || '(none)'}`);
46
+ L(`\n Report: ${path.relative(cwd, outPath)}\n`);
47
+ } catch (e) {
48
+ console.error('Error:', e instanceof Error ? e.message : e);
49
+ process.exit(1);
50
+ }
51
+ });
52
+ }
@@ -52,45 +52,54 @@ function log(msg: string): void {
52
52
  * `name` is kept as an alias of `featureBaseName` so existing callers/labels
53
53
  * (preflight table, summary) read naturally — every visible row is per-feature.
54
54
  */
55
+ type UnitKind = 'screen' | 'flow' | 'api';
56
+ /** qa/ subfolder for a unit kind. */
57
+ const qaParent = (kind: UnitKind): string => (kind === 'flow' ? 'flows' : kind === 'api' ? 'api' : 'screens');
58
+
55
59
  interface DeliveryTarget {
56
60
  screen: string;
57
61
  featureBaseName: string;
58
62
  /** Alias of `featureBaseName` — preserves the old `target.name` call sites. */
59
63
  name: string;
64
+ kind: UnitKind;
65
+ /** Back-compat: flows kept distinct labels/paths before api was added. */
60
66
  isFlow: boolean;
61
67
  }
62
68
 
63
- function makeTarget(screen: string, featureBaseName: string, isFlow: boolean): DeliveryTarget {
64
- return { screen, featureBaseName, name: featureBaseName, isFlow };
69
+ function makeTarget(screen: string, featureBaseName: string, kind: UnitKind): DeliveryTarget {
70
+ return { screen, featureBaseName, name: featureBaseName, kind, isFlow: kind === 'flow' };
65
71
  }
66
72
 
67
73
  /**
68
- * List all `.feature` files inside a screen/flow as separate targets.
74
+ * List all `.feature` files inside a screen/flow/api unit as separate targets.
69
75
  * Returns empty array when the directory has no features yet.
70
76
  */
71
- function listFeatureTargets(cwd: string, screen: string, isFlow: boolean): DeliveryTarget[] {
72
- const featuresDir = path.join(cwd, 'qa', isFlow ? 'flows' : 'screens', screen, 'features');
77
+ function listFeatureTargets(cwd: string, screen: string, kind: UnitKind): DeliveryTarget[] {
78
+ const featuresDir = path.join(cwd, 'qa', qaParent(kind), screen, 'features');
73
79
  if (!fs.existsSync(featuresDir)) return [];
74
80
  return fs.readdirSync(featuresDir)
75
81
  .filter((f) => f.endsWith('.feature'))
76
- .map((f) => makeTarget(screen, f.slice(0, -'.feature'.length), isFlow))
82
+ .map((f) => makeTarget(screen, f.slice(0, -'.feature'.length), kind))
77
83
  .sort((a, b) => a.featureBaseName.localeCompare(b.featureBaseName));
78
84
  }
79
85
 
80
86
  function listAllTargets(cwd: string): DeliveryTarget[] {
81
87
  const targets: DeliveryTarget[] = [];
82
-
83
- const screensDir = path.join(cwd, 'qa', 'screens');
84
- if (fs.existsSync(screensDir)) {
85
- for (const d of fs.readdirSync(screensDir, { withFileTypes: true })) {
86
- if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, d.name, false));
88
+ const scan = (kind: UnitKind, skip: (n: string) => boolean = () => false) => {
89
+ const root = path.join(cwd, 'qa', qaParent(kind));
90
+ if (!fs.existsSync(root)) return;
91
+ for (const d of fs.readdirSync(root, { withFileTypes: true })) {
92
+ if (d.isDirectory() && !skip(d.name)) targets.push(...listFeatureTargets(cwd, d.name, kind));
87
93
  }
88
- }
89
-
90
- const flowsDir = path.join(cwd, 'qa', 'flows');
91
- if (fs.existsSync(flowsDir)) {
92
- for (const d of fs.readdirSync(flowsDir, { withFileTypes: true })) {
93
- if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, d.name, true));
94
+ };
95
+ scan('screen');
96
+ scan('flow');
97
+ scan('api', (n) => n === 'flows'); // api areas: qa/api/<area>
98
+ // api flows: qa/api/flows/<flow> → screen `flows/<flow>`, kind 'api' (paths compose to qa/api/flows/… + specs/generated/api/flows/…)
99
+ const apiFlowsRoot = path.join(cwd, 'qa', 'api', 'flows');
100
+ if (fs.existsSync(apiFlowsRoot)) {
101
+ for (const d of fs.readdirSync(apiFlowsRoot, { withFileTypes: true })) {
102
+ if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, path.posix.join('flows', d.name), 'api'));
94
103
  }
95
104
  }
96
105
 
@@ -108,28 +117,27 @@ function listAllTargets(cwd: string): DeliveryTarget[] {
108
117
  * feature file with the basename across all screens & flows.
109
118
  */
110
119
  function resolveTargetsFromArg(cwd: string, name: string): DeliveryTarget[] {
111
- if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) {
112
- return listFeatureTargets(cwd, name, true);
113
- }
114
- if (fs.existsSync(path.join(cwd, 'qa', 'screens', name))) {
115
- return listFeatureTargets(cwd, name, false);
116
- }
117
- // Treat as feature basename — find the parent screen/flow that hosts it.
120
+ if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) return listFeatureTargets(cwd, name, 'flow');
121
+ if (fs.existsSync(path.join(cwd, 'qa', 'screens', name))) return listFeatureTargets(cwd, name, 'screen');
122
+ if (fs.existsSync(path.join(cwd, 'qa', 'api', name))) return listFeatureTargets(cwd, name, 'api');
123
+ // `flows/<flow>` or a bare api-flow name → qa/api/flows/<flow>
124
+ const apiFlow = name.startsWith('flows/') ? name : path.posix.join('flows', name);
125
+ if (fs.existsSync(path.join(cwd, 'qa', 'api', apiFlow))) return listFeatureTargets(cwd, apiFlow, 'api');
126
+ // Treat as feature basename — find the parent unit that hosts it.
118
127
  const candidates = listAllTargets(cwd).filter((t) => t.featureBaseName === name);
119
128
  if (candidates.length > 0) return candidates;
120
129
  // Fallback: treat as screen name even if directory missing (lets preflight
121
130
  // surface the "feature file missing" error with the right path).
122
- return [makeTarget(name, name, false)];
131
+ return [makeTarget(name, name, 'screen')];
123
132
  }
124
133
 
125
134
  function qaDir(cwd: string, target: DeliveryTarget): string {
126
- return path.join(cwd, 'qa', target.isFlow ? 'flows' : 'screens', target.screen);
135
+ return path.join(cwd, 'qa', qaParent(target.kind), target.screen);
127
136
  }
128
137
 
129
138
  function generatedDir(cwd: string, target: DeliveryTarget): string {
130
- return target.isFlow
131
- ? path.join(cwd, 'specs', 'generated', 'flows', target.screen)
132
- : path.join(cwd, 'specs', 'generated', target.screen);
139
+ const sub = target.kind === 'flow' ? path.join('flows', target.screen) : target.kind === 'api' ? path.join('api', target.screen) : target.screen;
140
+ return path.join(cwd, 'specs', 'generated', sub);
133
141
  }
134
142
 
135
143
  // ----------------------------------------------------------------------------
@@ -262,7 +270,8 @@ function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
262
270
 
263
271
  const featureOk = checkFeatureReal(featureFile);
264
272
  const testDataOk = checkTestDataHasVars(testDataFile);
265
- const selectorsOk = checkSelectorsHasEntries(selectorsFile, target.featureBaseName);
273
+ // API units have no DOM → no selectors; the catalog (apis.yaml) is the contract. N/A, not missing.
274
+ const selectorsOk = target.kind === 'api' ? true : checkSelectorsHasEntries(selectorsFile, target.featureBaseName);
266
275
  const specOk = fs.existsSync(specFile);
267
276
  const resultsOk = resultsFile !== null;
268
277
 
@@ -284,7 +293,7 @@ function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
284
293
  }
285
294
  if (!specOk) {
286
295
  missing.push(`compiled .spec.ts missing: ${path.relative(cwd, specFile)}`);
287
- suggestions.push(target.isFlow ? `sungen generate --flow ${target.screen}` : `sungen generate --screen ${target.screen}`);
296
+ suggestions.push(`sungen generate --${target.kind === 'flow' ? 'flow' : target.kind === 'api' ? 'api' : 'screen'} ${target.screen}`);
288
297
  }
289
298
  if (!resultsOk) {
290
299
  const env = process.env.SUNGEN_ENV;
@@ -4,7 +4,8 @@ import * as fs from 'fs';
4
4
  import { CodeGenerator } from '../../generators/test-generator/code-generator';
5
5
  import { adapterRegistry } from '../../generators/test-generator/adapters';
6
6
  import { scanTestDataSecrets } from '../../harness/secret-scan';
7
- import { lintDataDriven } from '../../harness/data-driven-lint';
7
+ import { capabilityRegistry } from '../../capabilities/registry';
8
+ import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
8
9
  import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
9
10
 
10
11
  /**
@@ -43,12 +44,21 @@ function findFeatureFilesForFlow(flowName: string): string[] {
43
44
  }
44
45
 
45
46
  /**
46
- * Find all feature files across all screens and flows
47
+ * Find feature files for a specific API-first unit — an area (`orders`) or an api flow
48
+ * (`flows/<flow>`). Catalog/test-data live alongside under qa/api/<name>/.
49
+ */
50
+ function findFeatureFilesForApi(name: string): string[] {
51
+ return findFeatureFiles(path.join(process.cwd(), 'qa', 'api', name, 'features'));
52
+ }
53
+
54
+ /**
55
+ * Find all feature files across all screens, flows, and API-first areas/flows.
47
56
  */
48
57
  function findAllFeatureFiles(): string[] {
49
58
  const allFiles: string[] = [];
50
59
  const screensDir = path.join(process.cwd(), 'qa', 'screens');
51
60
  const flowsDir = path.join(process.cwd(), 'qa', 'flows');
61
+ const apiDir = path.join(process.cwd(), 'qa', 'api');
52
62
 
53
63
  if (fs.existsSync(screensDir)) {
54
64
  const screenDirs = fs.readdirSync(screensDir, { withFileTypes: true })
@@ -72,6 +82,9 @@ function findAllFeatureFiles(): string[] {
72
82
  }
73
83
  }
74
84
 
85
+ // API-first: every .feature under qa/api/** (areas qa/api/<area>/features + flows qa/api/flows/<flow>/features).
86
+ if (fs.existsSync(apiDir)) allFiles.push(...findFeatureFiles(apiDir));
87
+
75
88
  return allFiles;
76
89
  }
77
90
 
@@ -81,19 +94,32 @@ export function registerGenerateCommand(program: Command): void {
81
94
  .description('Generate Playwright test code from Gherkin features')
82
95
  .option('-s, --screen <name>', 'Generate tests for a specific screen')
83
96
  .option('--flow <name>', 'Generate tests for a specific flow')
84
- .option('--all', 'Generate tests for all screens and flows')
97
+ .option('--api <name>', 'Generate tests for an API-first area or api flow (e.g. orders, flows/signup)')
98
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
99
+ .option('--all', 'Generate tests for all screens, flows, and API areas')
85
100
  .option('--framework <name>', 'Override the platform driver (else read from qa/capabilities.yaml)')
86
101
  .option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
87
102
  .action(async (options) => {
88
103
  try {
89
104
  const screenName = options.screen;
90
105
  const flowName = options.flow;
106
+ const apiName = options.api || options.area;
91
107
 
92
108
  // Find feature files
93
109
  let featureFiles: string[];
94
110
  let qaSourceDir: string;
95
111
 
96
- if (screenName) {
112
+ if (apiName) {
113
+ featureFiles = findFeatureFilesForApi(apiName);
114
+ if (featureFiles.length === 0) {
115
+ throw new Error(
116
+ `No feature files found for API unit: ${apiName}\n` +
117
+ `Looked in: qa/api/${apiName}/features/ (scaffold with \`sungen api add --area ${apiName}\` or \`--flow\`)`
118
+ );
119
+ }
120
+ qaSourceDir = path.join(process.cwd(), 'qa', 'api');
121
+ console.log(`\nGenerating tests: api/${apiName}\n`);
122
+ } else if (screenName) {
97
123
  featureFiles = findFeatureFilesForScreen(screenName);
98
124
  if (featureFiles.length === 0) {
99
125
  throw new Error(
@@ -116,10 +142,10 @@ export function registerGenerateCommand(program: Command): void {
116
142
  } else {
117
143
  featureFiles = findAllFeatureFiles();
118
144
  if (featureFiles.length === 0) {
119
- throw new Error('No feature files found in qa/screens/ or qa/flows/');
145
+ throw new Error('No feature files found in qa/screens/, qa/flows/, or qa/api/');
120
146
  }
121
147
  qaSourceDir = path.join(process.cwd(), 'qa');
122
- console.log(`\nGenerating tests for all screens and flows\n`);
148
+ console.log(`\nGenerating tests for all screens, flows, and API areas\n`);
123
149
  }
124
150
 
125
151
  // Output directory
@@ -183,8 +209,11 @@ export function registerGenerateCommand(program: Command): void {
183
209
  console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
184
210
  }
185
211
 
186
- // Data-driven lint (@cases / @query) — advisory, never blocks generation.
187
- const ddWarnings = scanDirs.flatMap((d) => lintDataDriven(d, cwd));
212
+ // Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
213
+ // never block generation. Each capability registers its own; the kernel just runs them.
214
+ discoverAndRegisterCapabilities();
215
+ const advisorySensors = capabilityRegistry.sensors('advisory');
216
+ const ddWarnings = scanDirs.flatMap((d) => advisorySensors.flatMap((s) => s.run({ dir: d, cwd })));
188
217
  if (ddWarnings.length) {
189
218
  console.log(`\n⚠️ Data-driven lint (@cases / @query) — review:`);
190
219
  for (const w of ddWarnings.slice(0, 20)) console.log(` ${w.scenario ? w.scenario + ': ' : ''}${w.message}`);
@@ -9,7 +9,9 @@ export function registerLedgerCommand(program: Command): void {
9
9
  ledger
10
10
  .command('record')
11
11
  .description('Append a step event to the ledger')
12
- .requiredOption('-s, --screen <name>', 'Screen or flow name')
12
+ .option('-s, --screen <name>', 'Screen or flow name')
13
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
14
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
13
15
  .requiredOption('--step <name>', 'Step name (discovery | viewpoint | gherkin | audit | repair:1 ...)')
14
16
  .option('--run <id>', 'Run id — groups all phases of one create-test invocation (else auto-segmented by time gap)')
15
17
  .option('--model <id>', 'Model id')
@@ -19,10 +21,12 @@ export function registerLedgerCommand(program: Command): void {
19
21
  .option('--note <text>', 'Free note')
20
22
  .action((o) => {
21
23
  try {
22
- recordEvent(o.screen, {
24
+ const name = o.screen || o.api || o.area;
25
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
26
+ recordEvent(name, {
23
27
  step: o.step, runId: o.run, model: o.model, tokensIn: o.tokensIn, tokensOut: o.tokensOut, ms: o.ms, note: o.note,
24
28
  });
25
- console.log(`✓ ledger: ${o.screen} · ${o.step}`);
29
+ console.log(`✓ ledger: ${name} · ${o.step}`);
26
30
  } catch (e) {
27
31
  console.error('Error:', e instanceof Error ? e.message : e);
28
32
  process.exit(1);
@@ -32,12 +36,16 @@ export function registerLedgerCommand(program: Command): void {
32
36
  ledger
33
37
  .command('report')
34
38
  .description('Summarise ledger + efficiency verdicts (pulls audit score if present)')
35
- .requiredOption('-s, --screen <name>', 'Screen or flow name')
39
+ .option('-s, --screen <name>', 'Screen or flow name')
40
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
41
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
36
42
  .option('--all-runs', 'Aggregate ALL runs (default: latest run only)')
37
43
  .option('--json', 'Output raw JSON')
38
44
  .action((o) => {
39
45
  try {
40
- const r = buildReport(o.screen, { allRuns: o.allRuns });
46
+ const name = o.screen || o.api || o.area;
47
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
48
+ const r = buildReport(name, { allRuns: o.allRuns });
41
49
  if (o.json) { console.log(JSON.stringify(r, null, 2)); return; }
42
50
  const scope = r.runScope === 'all' ? `all ${r.runs} runs` : `latest run${r.runs > 1 ? ` of ${r.runs}` : ''}`;
43
51
  console.log(`\n━━━ Usage Ledger: ${r.screen} (${r.events} events · ${scope}) ━━━`);
@@ -4,10 +4,10 @@ import * as fs from 'fs';
4
4
  import { buildManifest, diffManifest, loadManifest, saveManifest } from '../../harness/manifest';
5
5
 
6
6
  function findScreenDir(name: string): string | null {
7
- const screen = path.join(process.cwd(), 'qa', 'screens', name);
8
- if (fs.existsSync(screen)) return screen;
9
- const flow = path.join(process.cwd(), 'qa', 'flows', name);
10
- if (fs.existsSync(flow)) return flow;
7
+ for (const p of ['screens', 'flows', 'api']) {
8
+ const d = path.join(process.cwd(), 'qa', p, name);
9
+ if (fs.existsSync(d)) return d;
10
+ }
11
11
  return null;
12
12
  }
13
13
 
@@ -16,14 +16,16 @@ export function registerManifestCommand(program: Command): void {
16
16
  .command('manifest')
17
17
  .description('Spec-fingerprint manifest: build a scenario↔spec-section map, or diff to plan keep/regenerate/retire')
18
18
  .option('-s, --screen <name>', 'Screen or flow name')
19
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
20
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
19
21
  .option('--diff', 'Compare current spec vs stored manifest → change plan')
20
22
  .option('--json', 'Output raw JSON')
21
23
  .action((options) => {
22
24
  try {
23
- const name = options.screen;
24
- if (!name) throw new Error('Provide --screen <name>');
25
+ const name = options.screen || options.api || options.area;
26
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
25
27
  const dir = findScreenDir(name);
26
- if (!dir) throw new Error(`Screen/flow not found: ${name}`);
28
+ if (!dir) throw new Error(`Not found: qa/screens|flows|api/${name}`);
27
29
 
28
30
  if (options.diff) {
29
31
  const manifest = loadManifest(name);
@@ -0,0 +1,57 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { planRepair } from '../../harness/repair';
5
+
6
+ /**
7
+ * `sungen repair` (#343) — turn a unit's audit findings + Playwright failures into a concrete fix
8
+ * plan, using the unit-capability's fix catalog (the `repair` SPI). Deterministic; the `/sungen:design`
9
+ * + `/sungen:run-test` repair loops apply the same proposals. Run `sungen audit`/the tests first.
10
+ */
11
+ export function registerRepairCommand(program: Command): void {
12
+ program
13
+ .command('repair')
14
+ .description('Propose concrete fixes for a unit from its audit findings + test failures (capability fix catalog)')
15
+ .option('-s, --screen <name>', 'Screen or flow name')
16
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
17
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
18
+ .option('--json', 'Output raw JSON')
19
+ .action((o: { screen?: string; api?: string; area?: string; json?: boolean }) => {
20
+ try {
21
+ const name = o.screen || o.api || o.area;
22
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
23
+ const cwd = process.cwd();
24
+ // Capability-resolution id + the generated dir, per kind.
25
+ const kind = fs.existsSync(path.join(cwd, 'qa', 'api', name)) || o.api ? 'api'
26
+ : fs.existsSync(path.join(cwd, 'qa', 'flows', name)) ? 'flow' : 'screen';
27
+ const unitId = kind === 'api' ? `api/${name}` : kind === 'flow' ? `flows/${name}` : name;
28
+ const generatedDir = path.join(cwd, 'specs', 'generated', kind === 'api' ? path.join('api', name) : kind === 'flow' ? path.join('flows', name) : name);
29
+
30
+ const plan = planRepair(unitId, name, cwd, generatedDir);
31
+ if (o.json) { console.log(JSON.stringify(plan, null, 2)); return; }
32
+
33
+ const L = console.log;
34
+ L(`\n━━━ Repair plan: ${name} (capability: ${plan.capability ?? 'none'}) ━━━`);
35
+ if (!plan.rulesAvailable) L(` (this capability ships no repair rules)`);
36
+ if (!plan.proposals.length && !plan.unmatched.length) {
37
+ L(` ✓ Nothing to repair — run \`sungen audit --${kind === 'api' ? 'api' : 'screen'} ${name}\` + the tests first, or the unit is clean.\n`);
38
+ return;
39
+ }
40
+ if (plan.proposals.length) {
41
+ L(`\n Proposed fixes (${plan.proposals.length}):`);
42
+ for (const p of plan.proposals) {
43
+ L(` • [${p.ruleId}] ${p.source === 'runtime' ? '(test) ' : ''}${p.signal}`);
44
+ L(` → ${p.fix}`);
45
+ }
46
+ }
47
+ if (plan.unmatched.length) {
48
+ L(`\n No known rule (review manually):`);
49
+ for (const u of plan.unmatched.slice(0, 10)) L(` - ${u}`);
50
+ }
51
+ L('');
52
+ } catch (e) {
53
+ console.error('Error:', e instanceof Error ? e.message : e);
54
+ process.exit(1);
55
+ }
56
+ });
57
+ }
@@ -2,28 +2,32 @@ import { Command } from 'commander';
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
4
  import { runScriptCheck } from '../../harness/script-check';
5
+ import { reportSlug } from '../../harness/unit-paths';
5
6
 
6
7
  export function registerScriptCheckCommand(program: Command): void {
7
8
  program
8
9
  .command('script-check')
9
10
  .description('Verify the generated Playwright spec is a faithful 1:1 of the Gherkin feature (no hand-edit / stale drift)')
10
11
  .option('-s, --screen <name>', 'Screen or flow name')
12
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
13
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
11
14
  .option('--json', 'Output raw JSON')
12
15
  .action(async (options) => {
13
16
  try {
14
- const name = options.screen;
15
- if (!name) throw new Error('Provide --screen <name>');
17
+ const name = options.screen || options.api || options.area;
18
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
16
19
  const screen = path.join(process.cwd(), 'qa', 'screens', name);
17
20
  const flow = path.join(process.cwd(), 'qa', 'flows', name);
18
- const flowMode = !fs.existsSync(screen) && fs.existsSync(flow);
19
- const dir = fs.existsSync(screen) ? screen : (fs.existsSync(flow) ? flow : null);
20
- if (!dir) throw new Error(`Screen/flow not found: ${name}`);
21
+ const api = path.join(process.cwd(), 'qa', 'api', name);
22
+ const kind = fs.existsSync(screen) ? 'screen' : fs.existsSync(flow) ? 'flow' : fs.existsSync(api) ? 'api' : null;
23
+ const dir = kind === 'screen' ? screen : kind === 'flow' ? flow : kind === 'api' ? api : null;
24
+ if (!dir || !kind) throw new Error(`Not found: qa/screens|flows|api/${name}`);
21
25
 
22
- const r = await runScriptCheck(dir, name, flowMode);
26
+ const r = await runScriptCheck(dir, name, kind);
23
27
 
24
28
  const outDir = path.join(process.cwd(), '.sungen', 'reports');
25
29
  fs.mkdirSync(outDir, { recursive: true });
26
- fs.writeFileSync(path.join(outDir, `${name}-script-check.json`), JSON.stringify(r, null, 2), 'utf-8');
30
+ fs.writeFileSync(path.join(outDir, `${reportSlug(name)}-script-check.json`), JSON.stringify(r, null, 2), 'utf-8');
27
31
 
28
32
  if (options.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.status === 'OK' ? 0 : 2); }
29
33
 
@@ -39,7 +43,7 @@ export function registerScriptCheckCommand(program: Command): void {
39
43
  if (r.findings.length) { L(' findings:'); for (const f of r.findings) L(` • ${f}`); }
40
44
  else L(' ✓ The test code faithfully reflects the Gherkin (1:1).');
41
45
  L('');
42
- if (r.drift === 'drift') L(' → Fix: re-run `sungen generate --screen ' + name + '` (or /sungen:run-test) so the spec matches the feature. Never hand-edit generated specs.');
46
+ if (r.drift === 'drift') L(` → Fix: re-run \`sungen generate --${kind === 'api' ? 'api' : kind === 'flow' ? 'flow' : 'screen'} ${name}\` (or /sungen:run-test) so the spec matches the feature. Never hand-edit generated specs.`);
43
47
  L('');
44
48
  process.exit(r.status === 'OK' ? 0 : 2);
45
49
  } catch (error) {
@@ -8,16 +8,19 @@ export function registerTraceCommand(program: Command): void {
8
8
  .command('trace')
9
9
  .description('Visualise the executed test-design process (workflow/skill steps, repair loops), find bottlenecks, and show where to focus human review')
10
10
  .option('-s, --screen <name>', 'Screen or flow name')
11
+ .option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
12
+ .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
11
13
  .option('--json', 'Output raw JSON')
12
14
  .option('--mermaid', 'Print only the Mermaid flowchart')
13
15
  .action((options) => {
14
16
  try {
15
- const name = options.screen;
16
- if (!name) throw new Error('Provide --screen <name>');
17
+ const name = options.screen || options.api || options.area;
18
+ if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
17
19
  const screen = path.join(process.cwd(), 'qa', 'screens', name);
18
20
  const flow = path.join(process.cwd(), 'qa', 'flows', name);
19
- const dir = fs.existsSync(screen) ? screen : (fs.existsSync(flow) ? flow : null);
20
- if (!dir) throw new Error(`Screen/flow not found: ${name}`);
21
+ const api = path.join(process.cwd(), 'qa', 'api', name);
22
+ const dir = fs.existsSync(screen) ? screen : fs.existsSync(flow) ? flow : fs.existsSync(api) ? api : null;
23
+ if (!dir) throw new Error(`Not found: qa/screens|flows|api/${name}`);
21
24
 
22
25
  const r = buildTrace(dir, name);
23
26
  if (options.json) { console.log(JSON.stringify(r, null, 2)); return; }
package/src/cli/index.ts CHANGED
@@ -26,6 +26,10 @@ import { registerChallengeCommand } from './commands/challenge';
26
26
  import { registerBlindspotCommand } from './commands/blindspot';
27
27
  import { registerCapabilityCommand } from './commands/capability';
28
28
  import { registerFlowCheckCommand } from './commands/flow-check';
29
+ import { registerContextCommand } from './commands/context';
30
+ import { registerRepairCommand } from './commands/repair';
31
+ import { capabilityRegistry } from '../capabilities/registry';
32
+ import { discoverAndRegisterCapabilities } from '../capabilities/discover';
29
33
 
30
34
  // Read version from package.json so `--version` never drifts from the released version.
31
35
  const { version } = require('../../package.json') as { version: string };
@@ -42,7 +46,7 @@ async function main() {
42
46
  program
43
47
  .option('-v, --verbose', 'Enable verbose logging');
44
48
 
45
- // Register commands (9)
49
+ // Register commands
46
50
  registerInitCommand(program);
47
51
  registerAddCommand(program);
48
52
  registerGenerateCommand(program);
@@ -62,9 +66,18 @@ async function main() {
62
66
  registerBlindspotCommand(program);
63
67
  registerCapabilityCommand(program);
64
68
  registerFlowCheckCommand(program);
69
+ registerContextCommand(program);
70
+ registerRepairCommand(program);
65
71
  registerIngestCommand(program);
66
72
  registerEvalCommand(program);
67
73
 
74
+ // Capability-contributed CLI commands (Capability SPI): drivers own their authoring commands
75
+ // (e.g. @sungen/driver-api → `sungen api import`). Discover, then register each.
76
+ discoverAndRegisterCapabilities();
77
+ for (const cap of capabilityRegistry.all()) {
78
+ for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
79
+ }
80
+
68
81
  await program.parseAsync(process.argv);
69
82
  }
70
83
 
@@ -64,7 +64,7 @@ export interface TestGeneratorAdapter {
64
64
  // Template rendering methods
65
65
  renderTestFile(data: TestFileData): string;
66
66
  renderScenario(data: ScenarioData): string;
67
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string;
67
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string;
68
68
  renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
69
69
  renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
70
70
  renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
26
26
  return this.templateEngine.renderScenario(data);
27
27
  }
28
28
 
29
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
29
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
30
30
  return this.templateEngine.renderImports(options);
31
31
  }
32
32
 
@@ -6,6 +6,9 @@ import { TestDataLoader } from '{{basePath}}/test-data';
6
6
  {{#if needsDb}}
7
7
  import { db } from '{{basePath}}/db';
8
8
  {{/if}}
9
+ {{#if needsApi}}
10
+ import { api } from '{{basePath}}/api';
11
+ {{/if}}
9
12
 
10
13
  // This file is auto-generated from Gherkin feature files
11
14
  // DO NOT EDIT MANUALLY - changes will be overwritten