@sun-asterisk/sungen 3.1.1 → 3.1.2-beta.100

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 (162) 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 +51 -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 +48 -0
  17. package/dist/capabilities/discover.js.map +1 -0
  18. package/dist/capabilities/registry.d.ts +90 -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 +49 -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/generate.d.ts.map +1 -1
  27. package/dist/cli/commands/generate.js +7 -3
  28. package/dist/cli/commands/generate.js.map +1 -1
  29. package/dist/cli/index.js +10 -1
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/exporters/spec-parser.d.ts.map +1 -1
  32. package/dist/exporters/spec-parser.js +4 -1
  33. package/dist/exporters/spec-parser.js.map +1 -1
  34. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  35. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  36. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  37. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  38. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  39. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  40. package/dist/generators/test-generator/code-generator.d.ts +18 -9
  41. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  42. package/dist/generators/test-generator/code-generator.js +76 -119
  43. package/dist/generators/test-generator/code-generator.js.map +1 -1
  44. package/dist/generators/test-generator/patterns/index.d.ts +0 -10
  45. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  46. package/dist/generators/test-generator/patterns/index.js +10 -47
  47. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  48. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  49. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  50. package/dist/generators/test-generator/template-engine.js +1 -1
  51. package/dist/generators/test-generator/template-engine.js.map +1 -1
  52. package/dist/harness/annotation-overrides.d.ts +9 -0
  53. package/dist/harness/annotation-overrides.d.ts.map +1 -0
  54. package/dist/harness/annotation-overrides.js +36 -0
  55. package/dist/harness/annotation-overrides.js.map +1 -0
  56. package/dist/harness/audit.d.ts.map +1 -1
  57. package/dist/harness/audit.js +35 -7
  58. package/dist/harness/audit.js.map +1 -1
  59. package/dist/harness/catalog/drivers.yaml +35 -12
  60. package/dist/harness/parse.d.ts +1 -0
  61. package/dist/harness/parse.d.ts.map +1 -1
  62. package/dist/harness/parse.js +13 -4
  63. package/dist/harness/parse.js.map +1 -1
  64. package/dist/index.d.ts +20 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +32 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  69. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  70. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +3 -0
  71. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  72. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  73. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -0
  74. package/dist/orchestrator/templates/specs-api.d.ts +19 -0
  75. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
  76. package/dist/orchestrator/templates/specs-api.js +128 -0
  77. package/dist/orchestrator/templates/specs-api.js.map +1 -0
  78. package/dist/orchestrator/templates/specs-api.ts +101 -0
  79. package/package.json +7 -30
  80. package/src/capabilities/builtins.ts +85 -0
  81. package/src/capabilities/context-router.ts +66 -0
  82. package/src/capabilities/context.ts +46 -0
  83. package/src/capabilities/discover.ts +42 -0
  84. package/src/capabilities/registry.ts +111 -0
  85. package/src/capabilities/sensor.ts +47 -0
  86. package/src/cli/commands/generate.ts +7 -3
  87. package/src/cli/index.ts +10 -1
  88. package/src/exporters/spec-parser.ts +4 -1
  89. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  90. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  91. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  92. package/src/generators/test-generator/code-generator.ts +71 -118
  93. package/src/generators/test-generator/patterns/index.ts +9 -35
  94. package/src/generators/test-generator/template-engine.ts +2 -2
  95. package/src/harness/annotation-overrides.ts +25 -0
  96. package/src/harness/audit.ts +37 -8
  97. package/src/harness/catalog/drivers.yaml +35 -12
  98. package/src/harness/parse.ts +7 -2
  99. package/src/index.ts +30 -0
  100. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  101. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  102. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +3 -0
  103. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  104. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  105. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -0
  106. package/src/orchestrator/templates/specs-api.ts +101 -0
  107. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
  108. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
  109. package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
  110. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
  111. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
  112. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
  113. package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
  114. package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
  115. package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -6
  116. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
  117. package/dist/generators/test-generator/patterns/database-patterns.js +0 -95
  118. package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
  119. package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
  120. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
  121. package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
  122. package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
  123. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
  124. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
  125. package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
  126. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
  127. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
  128. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
  129. package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
  130. package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
  131. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
  132. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
  133. package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
  134. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
  135. package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
  136. package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
  137. package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
  138. package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
  139. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
  140. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
  141. package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
  142. package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
  143. package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
  144. package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
  145. package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
  146. package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
  147. package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
  148. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
  149. package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
  150. package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
  151. package/docs/orchestration-spec.md +0 -267
  152. package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
  153. package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
  154. package/src/generators/test-generator/patterns/database-patterns.ts +0 -96
  155. package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
  156. package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
  157. package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
  158. package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
  159. package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
  160. package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
  161. package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
  162. package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Capability registry — the extension seam for the microkernel (Capability SPI, R1).
3
+ *
4
+ * Replaces hardcoded `this.patterns.push(...)` wiring with **registration**: each capability
5
+ * (ui · db · api · …) declares what it contributes; the kernel composes it. This is the
6
+ * "shared framework, pull in what you use" mechanism (`docs/spec/sungen_capability_spi_spec.md`).
7
+ *
8
+ * R1 scope (behaviour-preserving): only the **patterns** slice is wired through here — the set of
9
+ * registered patterns is identical to the old hardcoded list, so compiled output is unchanged
10
+ * (golden + audit snapshots are the contract). The remaining SPI slices (discovery, contextMapper,
11
+ * sensors, adapter, viewpoints, runtimeHelpers) are declared on the descriptor for the upcoming
12
+ * steps but are not consumed yet.
13
+ */
14
+ import type { StepPattern } from '../generators/test-generator/patterns/types';
15
+ import type { Sensor } from './sensor';
16
+ import type { DiscoveryProvider, ContextMapper } from './context';
17
+
18
+ export interface CapabilityDescriptor {
19
+ /** Stable id: 'ui' | 'db' | 'api' | 'core' | … */
20
+ id: string;
21
+ /** The implicit/default capability (a scenario with no capability tag). UI sets this. */
22
+ default?: boolean;
23
+ /** Annotation tags this capability owns (e.g. ['@query'], ['@api','@hybrid']). */
24
+ annotations?: string[];
25
+ /** Step patterns this capability contributes to the compiler. */
26
+ patterns?: StepPattern[];
27
+ /** Harness sensors this capability contributes (advisory and/or gate). */
28
+ sensors?: Sensor[];
29
+ /** Orchestration phase hooks — declared in R1, consumed by the pipeline from R2 onward. */
30
+ discovery?: DiscoveryProvider; // sources → Context slice
31
+ contextMapper?: ContextMapper; // Context → generation units + modes
32
+ /** Provider for this capability's viewpoint catalog (UI: the 17-pattern universal catalog). */
33
+ viewpoints?: () => unknown;
34
+ /**
35
+ * Score-bearing gate computation owned by this capability (UI: viewpoint coverage + assertion
36
+ * depth). The audit engine calls it and assembles the score from the result. Typed generically
37
+ * so the registry stays decoupled from harness internals; the caller knows the concrete shape.
38
+ */
39
+ gateProvider?: (input: unknown) => unknown;
40
+ /** Runtime helper file(s) this capability emits into specs/ when it is active for a feature
41
+ * (db → specs/db.ts). Emitted via the same sync-when-changed mechanism. */
42
+ runtimeHelpers?: Array<{ file: string; template: string }>;
43
+ /** Optional step detector: the capability is "active" for a feature if any step matches —
44
+ * for steps that carry no annotation tag (db's declarative `User see [t] row where …`). */
45
+ detectsStep?: (stepText: string) => boolean;
46
+ /**
47
+ * Precondition codegen for this capability's annotations on a scenario (db's `@query:<name>`
48
+ * → `testData.bind(name, await db.fetchQuery(...))`). Returns raw statements (the compiler
49
+ * indents) + `boundVars` (the `{{name}}` variables it binds, so the compiler registers them as
50
+ * runtime-resolved). Keeps `@query` codegen owned by `db`, not hardcoded in the compiler.
51
+ */
52
+ preconditionCodegen?: (input: { tags: string[]; screenName: string; cwd: string }) =>
53
+ Array<{ comment?: string; code: string; boundVars?: string[] }>;
54
+
55
+ /**
56
+ * CLI commands this capability contributes. The CLI calls each with the commander `program`
57
+ * (typed generically so the registry stays decoupled from commander; the driver knows the shape).
58
+ * Lets a driver own its authoring commands (api → `sungen api import`) instead of hardcoding them
59
+ * in core's CLI. Invoked once, after discovery, in `src/cli/index.ts`.
60
+ */
61
+ cliCommands?: Array<(program: unknown) => void>;
62
+
63
+ // --- Declared for later R-steps; not consumed yet (kept here so the SPI shape is visible). ---
64
+ // adapter?: string; // codegen adapter id (existing adapterRegistry)
65
+ }
66
+
67
+ export class CapabilityRegistry {
68
+ private caps = new Map<string, CapabilityDescriptor>();
69
+
70
+ /** Register (or replace, by id) a capability descriptor. Idempotent by id. */
71
+ register(descriptor: CapabilityDescriptor): void {
72
+ this.caps.set(descriptor.id, descriptor);
73
+ }
74
+
75
+ get(id: string): CapabilityDescriptor | undefined {
76
+ return this.caps.get(id);
77
+ }
78
+
79
+ /** True once any capability is registered — discovery guards on this (replaces the builtins boolean). */
80
+ isPopulated(): boolean {
81
+ return this.caps.size > 0;
82
+ }
83
+
84
+ all(): CapabilityDescriptor[] {
85
+ return [...this.caps.values()];
86
+ }
87
+
88
+ /** The id of the implicit/default capability (UI), if registered. */
89
+ defaultCapabilityId(): string | undefined {
90
+ return this.all().find((c) => c.default)?.id;
91
+ }
92
+
93
+ /** All step patterns contributed by registered capabilities (composition order = registration order). */
94
+ patterns(): StepPattern[] {
95
+ return this.all().flatMap((c) => c.patterns ?? []);
96
+ }
97
+
98
+ /** All registered sensors, optionally filtered by kind ('advisory' | 'gate'). */
99
+ sensors(kind?: Sensor['kind']): Sensor[] {
100
+ const all = this.all().flatMap((c) => c.sensors ?? []);
101
+ return kind ? all.filter((s) => s.kind === kind) : all;
102
+ }
103
+
104
+ /** Test seam: drop all registrations. */
105
+ _reset(): void {
106
+ this.caps.clear();
107
+ }
108
+ }
109
+
110
+ /** Process-wide singleton (mirrors `adapterRegistry`). */
111
+ export const capabilityRegistry = new CapabilityRegistry();
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Harness Sensor SPI (Capability SPI, R1 — Sensor step).
3
+ *
4
+ * A sensor is a deterministic harness check a capability contributes. The kernel collects all
5
+ * registered sensors and runs them; findings merge into the harness output. Two kinds:
6
+ * - `advisory` — surfaced as warnings (e.g. at `sungen generate`), never blocks.
7
+ * - `gate` — feeds the audit scorecard / pass-fail (migrated from the hardcoded `audit.ts`
8
+ * calls in the next sub-step; gate sensors carry an intent-aware threshold).
9
+ *
10
+ * Generic over the input so the same registry holds both generate-time advisory sensors
11
+ * (which scan a screen/flow dir) and audit-time gate sensors (which read parsed scenarios) —
12
+ * each sensor declares the input it consumes; callers filter by `kind` and pass the right input.
13
+ */
14
+ export interface SensorFinding {
15
+ sensorId: string;
16
+ capability?: string;
17
+ scenario?: string;
18
+ message: string;
19
+ severity?: 'info' | 'warn' | 'error';
20
+ }
21
+
22
+ export interface Sensor<I = unknown> {
23
+ id: string;
24
+ capability?: string;
25
+ kind: 'gate' | 'advisory';
26
+ run(input: I): SensorFinding[];
27
+ }
28
+
29
+ /** Input for generate-time advisory sensors that scan a screen/flow directory. */
30
+ export interface AdvisoryScanInput {
31
+ dir: string;
32
+ cwd: string;
33
+ }
34
+
35
+ /**
36
+ * Input for audit-time gate sensors. Intentionally minimal/structural (not the full ScenarioInfo
37
+ * type) so `src/capabilities` doesn't depend on harness internals — the audit caller passes the
38
+ * parsed scenarios it already has.
39
+ */
40
+ export interface GateInput {
41
+ screenName: string;
42
+ cwd: string;
43
+ featureText: string;
44
+ scenarios: Array<{ name: string; queryRefs?: string[]; apiRefs?: string[] }>;
45
+ /** UI: universal-viewpoint theme gaps the coverage gate found (generic string list). */
46
+ universalGaps?: string[];
47
+ }
@@ -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
  /**
@@ -183,8 +184,11 @@ export function registerGenerateCommand(program: Command): void {
183
184
  console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
184
185
  }
185
186
 
186
- // Data-driven lint (@cases / @query) — advisory, never blocks generation.
187
- const ddWarnings = scanDirs.flatMap((d) => lintDataDriven(d, cwd));
187
+ // Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
188
+ // never block generation. Each capability registers its own; the kernel just runs them.
189
+ discoverAndRegisterCapabilities();
190
+ const advisorySensors = capabilityRegistry.sensors('advisory');
191
+ const ddWarnings = scanDirs.flatMap((d) => advisorySensors.flatMap((s) => s.run({ dir: d, cwd })));
188
192
  if (ddWarnings.length) {
189
193
  console.log(`\n⚠️ Data-driven lint (@cases / @query) — review:`);
190
194
  for (const w of ddWarnings.slice(0, 20)) console.log(` ${w.scenario ? w.scenario + ': ' : ''}${w.message}`);
package/src/cli/index.ts CHANGED
@@ -26,6 +26,8 @@ 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 { capabilityRegistry } from '../capabilities/registry';
30
+ import { discoverAndRegisterCapabilities } from '../capabilities/discover';
29
31
 
30
32
  // Read version from package.json so `--version` never drifts from the released version.
31
33
  const { version } = require('../../package.json') as { version: string };
@@ -42,7 +44,7 @@ async function main() {
42
44
  program
43
45
  .option('-v, --verbose', 'Enable verbose logging');
44
46
 
45
- // Register commands (9)
47
+ // Register commands
46
48
  registerInitCommand(program);
47
49
  registerAddCommand(program);
48
50
  registerGenerateCommand(program);
@@ -65,6 +67,13 @@ async function main() {
65
67
  registerIngestCommand(program);
66
68
  registerEvalCommand(program);
67
69
 
70
+ // Capability-contributed CLI commands (Capability SPI): drivers own their authoring commands
71
+ // (e.g. @sungen/driver-api → `sungen api import`). Discover, then register each.
72
+ discoverAndRegisterCapabilities();
73
+ for (const cap of capabilityRegistry.all()) {
74
+ for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
75
+ }
76
+
68
77
  await program.parseAsync(process.argv);
69
78
  }
70
79
 
@@ -22,7 +22,10 @@ function extractTestBlock(content: string, startIdx: number): {
22
22
  // (e.g. test('Footer "X" link', ...) — common when scenarios cite UI labels).
23
23
  // Quote char may be ' " or ` — the backtick form is a data-driven (@cases) title
24
24
  // like `VP-… — ${__row.__label}`.
25
- const testRegex = /test\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{[^}]*\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
25
+ // The options object uses `\{.*?\}` (non-greedy, not `[^}]*`) so a tag value containing a
26
+ // `}` — e.g. `@query:q(p={{var}})` — doesn't truncate the match (issue #271). The header is
27
+ // single-line, so `.*?` anchored on `}, async` is safe.
28
+ const testRegex = /test\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
26
29
  testRegex.lastIndex = startIdx;
27
30
  const match = testRegex.exec(content);
28
31
  if (!match) return null;
@@ -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
@@ -4,8 +4,8 @@ import { ParsedFeature, ParsedScenario, ParsedStep } from '../gherkin-parser';
4
4
  import { StepMapper } from './step-mapper';
5
5
  import { TestGeneratorAdapter, adapterRegistry } from './adapters';
6
6
  import { transformToRuntimeData } from './utils/runtime-data-transformer';
7
- import { isDbStep } from './patterns/database-patterns';
8
- import { resolveQuery, compileQuery } from '../../harness/query-catalog';
7
+ import { capabilityRegistry } from '../../capabilities/registry';
8
+ import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
9
9
 
10
10
  /**
11
11
  * Filter base scenario steps for @extend: only keep Given→When steps.
@@ -240,11 +240,17 @@ export class CodeGenerator {
240
240
  const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
241
241
  const needsCleanupImport = !isParallelFeature && hasCleanupTags;
242
242
 
243
- // Data Driver: if any step verifies DB state, import the `db` helper + emit specs/db.ts
244
- const needsDb = this.featureUsesDb(feature);
245
- if (needsDb) this.ensureDbFile(outputDir);
243
+ // Active capabilities for this feature (registry-driven): the default UI + any whose annotation
244
+ // tags appear (@query) or whose detectsStep matches (declarative DB steps). Each active
245
+ // capability emits its declared runtime helpers (db → specs/db.ts).
246
+ const activeCapabilityIds = this.activeCapabilityIds(feature);
247
+ const needsDb = activeCapabilityIds.includes('db');
248
+ const needsApi = activeCapabilityIds.includes('api');
249
+ for (const id of activeCapabilityIds) {
250
+ for (const h of capabilityRegistry.get(id)?.runtimeHelpers ?? []) this.syncGeneratedHelper(outputDir, h.file, h.template);
251
+ }
246
252
 
247
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb });
253
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi });
248
254
 
249
255
  // Generate test code (async now to support AI mapping)
250
256
  const testCode = await this.generateTestCode(feature);
@@ -300,119 +306,68 @@ export class CodeGenerator {
300
306
  /**
301
307
  * Ensure specs/base.ts exists in the output directory
302
308
  */
303
- /** True when any step (background or scenario) in the feature is a DB-verification step. */
304
- private featureUsesDb(feature: ParsedFeature): boolean {
309
+ /**
310
+ * Capabilities active for a feature (registry-driven): the default (UI) capability, plus any
311
+ * whose annotation tags appear on a scenario (e.g. `@query` → db) or whose `detectsStep`
312
+ * matches a step (db's declarative `User see [table] row where …`). Drives runtime-helper
313
+ * emission + the `db` import — replaces the hardcoded `featureUsesDb` check (R4).
314
+ */
315
+ private activeCapabilityIds(feature: ParsedFeature): string[] {
316
+ discoverAndRegisterCapabilities();
305
317
  const steps: ParsedStep[] = [];
306
318
  if (feature.background?.steps) steps.push(...feature.background.steps);
307
319
  for (const sc of feature.scenarios || []) if (sc.steps) steps.push(...sc.steps);
308
- if (steps.some((s) => s && typeof s.text === 'string' && isDbStep(s.text))) return true;
309
- // A scenario may carry only a @query:<name> post-condition tag (no DB step in its body).
310
- return (feature.scenarios || []).some((sc) => (sc.tags || []).some((t) => t.startsWith('@query:')));
320
+ const scenarioTags = (feature.scenarios || []).flatMap((sc) => sc.tags || []);
321
+ const ids = new Set<string>();
322
+ const def = capabilityRegistry.defaultCapabilityId();
323
+ if (def) ids.add(def);
324
+ for (const cap of capabilityRegistry.all()) {
325
+ const annoMatch = (cap.annotations ?? []).some((a) => scenarioTags.some((t) => t === a || t.startsWith(a + ':')));
326
+ const stepMatch = cap.detectsStep ? steps.some((s) => s && typeof s.text === 'string' && cap.detectsStep!(s.text)) : false;
327
+ if (annoMatch || stepMatch) ids.add(cap.id);
328
+ }
329
+ return [...ids];
311
330
  }
312
331
 
313
332
  /**
314
- * Build the precondition bind steps for a scenario's `@query:<name>[(p={{v}},…)]` tags.
315
- * Each runs the catalog query and binds the result array to a `{{name}}` variable.
333
+ * Precondition steps a scenario's capabilities inject (e.g. db `@query:<name>` → bind {{name}}).
334
+ * Each capability owns its annotation codegen via the SPI (`preconditionCodegen`); the compiler
335
+ * just composes + indents the returned statements (R4).
316
336
  */
317
- private buildQueryBinds(scenario: ParsedScenario): Array<{ comment?: string; code: string }> {
318
- const out: Array<{ comment?: string; code: string }> = [];
319
- const TAG = /^@query:([A-Za-z_][A-Za-z0-9_]*)(?:\((.*)\))?$/;
320
- for (const tag of scenario.tags || []) {
321
- const m = tag.match(TAG);
322
- if (!m) continue;
323
- const name = m[1];
324
- const overrides = this.parseQueryOverrides(m[2]);
325
- const entry = resolveQuery(name, this.queryScreenName); // throws (fail-fast) if missing/ambiguous
326
- const { sql, paramNames } = compileQuery(entry);
327
- const paramExprs = paramNames.map((p) =>
328
- p in overrides ? overrides[p] : `testData.get(${JSON.stringify(p)})`,
329
- );
330
- const label = JSON.stringify(entry.description ? `query "${name}" — ${entry.description}` : `query "${name}"`);
331
- const ds = entry.datasource ? JSON.stringify(entry.datasource) : 'undefined';
332
- out.push({
333
- comment: `@query:${name} → bind {{${name}}} from ${entry.datasource || 'default datasource'}`,
334
- code: this.indentCode(
335
- `testData.bind(${JSON.stringify(name)}, await db.fetchQuery(${label}, ${JSON.stringify(sql)}, [${paramExprs.join(', ')}], ${ds}));`,
336
- 4,
337
- ),
338
- });
339
- }
340
- return out;
341
- }
342
-
343
- /** Parse `@query:name(a={{x}},b="lit",c=3)` overrides → { a: "testData.get('x')", … } JS exprs. */
344
- private parseQueryOverrides(raw?: string): Record<string, string> {
345
- const out: Record<string, string> = {};
346
- if (!raw) return out;
347
- for (const part of raw.split(',')) {
348
- const eq = part.indexOf('=');
349
- if (eq < 0) continue;
350
- const key = part.slice(0, eq).trim();
351
- const val = part.slice(eq + 1).trim();
352
- if (!key) continue;
353
- const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
354
- const q = val.match(/^["'](.*)["']$/);
355
- if (v) out[key] = `testData.get(${JSON.stringify(v[1])})`;
356
- else if (q) out[key] = JSON.stringify(q[1]);
357
- else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
358
- else out[key] = JSON.stringify(val);
337
+ private capabilityPreconditions(scenario: ParsedScenario): Array<{ comment?: string; code: string; boundVars?: string[] }> {
338
+ discoverAndRegisterCapabilities();
339
+ const out: Array<{ comment?: string; code: string; boundVars?: string[] }> = [];
340
+ for (const cap of capabilityRegistry.all()) {
341
+ if (!cap.preconditionCodegen) continue;
342
+ out.push(...cap.preconditionCodegen({ tags: scenario.tags || [], screenName: this.queryScreenName, cwd: process.cwd() }));
359
343
  }
360
344
  return out;
361
345
  }
362
346
 
363
- /** Copy the Data Driver runtime helper into specs/db.ts (idempotent). */
364
- ensureDbFile(outputDir: string): void {
365
- const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
366
- const dbPath = path.join(outputDir, 'db.ts');
367
- if (!fs.existsSync(dbPath)) {
368
- const templatePath = path.join(templatesRoot, 'specs-db.ts');
369
- if (fs.existsSync(templatePath)) {
370
- if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
371
- fs.copyFileSync(templatePath, dbPath);
372
- console.log('✓ Created: specs/db.ts');
373
- }
374
- }
347
+ /**
348
+ * Copy an auto-generated runtime helper into specs/, **refreshing it when the template changed**.
349
+ * These files carry a `DO NOT EDIT — regenerated` header, so a stale copy left by an older sungen
350
+ * (e.g. a `specs/db.ts` from before a feature was added) is replaced instead of kept (issue #270).
351
+ * No-ops when the on-disk content already matches the template, so it stays quiet + idempotent.
352
+ */
353
+ private syncGeneratedHelper(outputDir: string, fileName: string, templateName: string): void {
354
+ const templatePath = path.join(__dirname, '..', '..', 'orchestrator', 'templates', templateName);
355
+ if (!fs.existsSync(templatePath)) return;
356
+ const targetPath = path.join(outputDir, fileName);
357
+ const next = fs.readFileSync(templatePath, 'utf8');
358
+ const exists = fs.existsSync(targetPath);
359
+ if (exists && fs.readFileSync(targetPath, 'utf8') === next) return; // already current
360
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
361
+ fs.writeFileSync(targetPath, next);
362
+ console.log(`✓ ${exists ? 'Updated' : 'Created'}: specs/${fileName}`);
375
363
  }
376
364
 
377
365
  ensureBaseFile(outputDir: string): void {
378
- const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
379
-
380
- const basePath = path.join(outputDir, 'base.ts');
381
- if (!fs.existsSync(basePath)) {
382
- const templatePath = path.join(templatesRoot, 'specs-base.ts');
383
- if (fs.existsSync(templatePath)) {
384
- const baseDir = path.dirname(basePath);
385
- if (!fs.existsSync(baseDir)) {
386
- fs.mkdirSync(baseDir, { recursive: true });
387
- }
388
- fs.copyFileSync(templatePath, basePath);
389
- console.log('✓ Created: specs/base.ts');
390
- }
391
- }
392
-
393
- // base.ts now depends on locale-fixture.ts — keep them paired.
394
- const localeFixturePath = path.join(outputDir, 'locale-fixture.ts');
395
- if (!fs.existsSync(localeFixturePath)) {
396
- const templatePath = path.join(templatesRoot, 'specs-locale-fixture.ts');
397
- if (fs.existsSync(templatePath)) {
398
- const baseDir = path.dirname(localeFixturePath);
399
- if (!fs.existsSync(baseDir)) {
400
- fs.mkdirSync(baseDir, { recursive: true });
401
- }
402
- fs.copyFileSync(templatePath, localeFixturePath);
403
- console.log('✓ Created: specs/locale-fixture.ts');
404
- }
405
- }
406
-
366
+ this.syncGeneratedHelper(outputDir, 'base.ts', 'specs-base.ts');
367
+ // base.ts depends on locale-fixture.ts — keep them paired.
368
+ this.syncGeneratedHelper(outputDir, 'locale-fixture.ts', 'specs-locale-fixture.ts');
407
369
  if (this.options.runtimeData) {
408
- const testDataPath = path.join(outputDir, 'test-data.ts');
409
- if (!fs.existsSync(testDataPath)) {
410
- const templatePath = path.join(templatesRoot, 'specs-test-data.ts');
411
- if (fs.existsSync(templatePath)) {
412
- fs.copyFileSync(templatePath, testDataPath);
413
- console.log('✓ Created: specs/test-data.ts');
414
- }
415
- }
370
+ this.syncGeneratedHelper(outputDir, 'test-data.ts', 'specs-test-data.ts');
416
371
  }
417
372
  }
418
373
 
@@ -707,18 +662,16 @@ export class CodeGenerator {
707
662
  for (const r of refs) this.stepMapper.registerCaptured(r);
708
663
  }
709
664
 
710
- // @query-bound vars: `{{name.col}}` / `{{name[2].col}}` / `{{name.count}}` exist only at
711
- // runtime (the bind fetches them) register as captured so they resolve to a runtime get()
712
- // instead of a compile-time YAML lookup that would fail.
713
- const queryNames = (scenario.tags || [])
714
- .map((t) => t.match(/^@query:([A-Za-z_][A-Za-z0-9_]*)/))
715
- .filter((m): m is RegExpMatchArray => !!m)
716
- .map((m) => m[1]);
717
- if (queryNames.length) {
665
+ // Capability preconditions (db `@query:<name>` bind {{name}}) are owned by the capability via
666
+ // the SPI. Their bound `{{name.*}}` vars exist only at runtime register them as captured so
667
+ // they resolve to a runtime get() instead of a compile-time YAML lookup that would fail.
668
+ const preconditions = this.capabilityPreconditions(scenario);
669
+ const boundVars = preconditions.flatMap((p) => p.boundVars || []);
670
+ if (boundVars.length) {
718
671
  for (const st of stepsToMap) {
719
672
  for (const mt of (st.text || '').matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
720
673
  const head = mt[1].split(/[.[]/)[0];
721
- if (queryNames.includes(head)) this.stepMapper.registerCaptured(mt[1]);
674
+ if (boundVars.includes(head)) this.stepMapper.registerCaptured(mt[1]);
722
675
  }
723
676
  }
724
677
  }
@@ -752,11 +705,11 @@ export class CodeGenerator {
752
705
  }
753
706
  }
754
707
 
755
- // @query:<name>[(p={{v}},…)] tags run the catalog query as a PRECONDITION and bind its
756
- // result to a `{{name}}` variable (scenario asserts on it with `expect …` + path access).
757
- // Prepended so the binds run before the scenario's own steps.
758
- const queryBinds = this.buildQueryBinds(scenario);
759
- if (queryBinds.length) steps.unshift(...queryBinds);
708
+ // Capability preconditions (db `@query:<name>` → bind {{name}}; computed above) run BEFORE the
709
+ // scenario's own steps prepend them, indenting the capability-supplied statements.
710
+ if (preconditions.length) {
711
+ steps.unshift(...preconditions.map((p) => ({ comment: p.comment, code: this.indentCode(p.code, 4) })));
712
+ }
760
713
 
761
714
  // Extract pass-through tags (feature + scenario, excluding functional tags)
762
715
  const tags = extractPassThroughTags(scenario.tags, featureTags);
@@ -1,18 +1,8 @@
1
1
  import { ParsedStep } from '../../gherkin-parser';
2
2
  import { MappedStep } from '../step-mapper';
3
3
  import { StepPattern, PatternContext } from './types';
4
- import { navigationPatterns } from './navigation-patterns';
5
- import { formPatterns } from './form-patterns';
6
- import { interactionPatterns } from './interaction-patterns';
7
- import { assertionPatterns } from './assertion-patterns';
8
- import { setupPatterns } from './setup-patterns';
9
- import { keyboardPatterns } from './keyboard-patterns';
10
- import { scrollPatterns } from './scroll-patterns';
11
- import { scopePatterns } from './scope-patterns';
12
- import { tablePatterns } from './table-patterns';
13
- import { capturePatterns } from './capture-patterns';
14
- import { databasePatterns } from './database-patterns';
15
- import { expectPatterns } from './expect-patterns';
4
+ import { capabilityRegistry } from '../../../capabilities/registry';
5
+ import { discoverAndRegisterCapabilities } from '../../../capabilities/discover';
16
6
 
17
7
  /**
18
8
  * Pattern Registry - manages all step patterns
@@ -28,18 +18,11 @@ export class PatternRegistry {
28
18
  * Register default patterns from all pattern modules
29
19
  */
30
20
  private registerDefaultPatterns(): void {
31
- this.patterns.push(...setupPatterns);
32
- this.patterns.push(...navigationPatterns);
33
- this.patterns.push(...formPatterns);
34
- this.patterns.push(...interactionPatterns);
35
- this.patterns.push(...assertionPatterns);
36
- this.patterns.push(...keyboardPatterns);
37
- this.patterns.push(...scrollPatterns);
38
- this.patterns.push(...scopePatterns);
39
- this.patterns.push(...tablePatterns);
40
- this.patterns.push(...capturePatterns);
41
- this.patterns.push(...databasePatterns);
42
- this.patterns.push(...expectPatterns);
21
+ // Patterns are composed from the capability registry (Capability SPI, R1) instead of a
22
+ // hardcoded push list. Built-ins (ui · db · core) register the same set as before, so the
23
+ // composed list + priority sort is behaviour-identical (golden snapshots are the contract).
24
+ discoverAndRegisterCapabilities();
25
+ this.patterns.push(...capabilityRegistry.patterns());
43
26
 
44
27
  // Sort by priority (higher first)
45
28
  this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
@@ -159,15 +142,6 @@ export class PatternRegistry {
159
142
  }
160
143
  }
161
144
 
162
- // Export pattern modules for advanced usage
163
- export { setupPatterns } from './setup-patterns';
164
- export { navigationPatterns } from './navigation-patterns';
165
- export { formPatterns } from './form-patterns';
166
- export { interactionPatterns } from './interaction-patterns';
167
- export { assertionPatterns } from './assertion-patterns';
168
- export { keyboardPatterns } from './keyboard-patterns';
169
- export { scrollPatterns } from './scroll-patterns';
170
- export { scopePatterns } from './scope-patterns';
171
- export { tablePatterns } from './table-patterns';
172
- export { databasePatterns, isDbStep } from './database-patterns';
145
+ // The UI step patterns now live in @sungen/driver-ui (R5.4) and the DB patterns in @sungen/driver-db
146
+ // (R5.5); both are contributed via the capability registry, not re-exported here.
173
147
  export * from './types';
@@ -229,8 +229,8 @@ export class TemplateEngine {
229
229
  this.baseContext = {};
230
230
  }
231
231
 
232
- renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
233
- return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb });
232
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
233
+ return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi });
234
234
  }
235
235
 
236
236
  renderTestFile(data: {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared annotation-override grammar for precondition annotations (`@query`/`@api`).
3
+ *
4
+ * Parses `name(a={{x}},b="lit",c=3)` overrides into a map of JS expressions, e.g.
5
+ * `{ a: "testData.get('x')", b: "\"lit\"", c: "3" }`. Used by the DB and API capability drivers'
6
+ * precondition codegen; lives in core so both drivers (and core's `api` until R5.6) can share it.
7
+ */
8
+ export function parseQueryOverrides(raw?: string): Record<string, string> {
9
+ const out: Record<string, string> = {};
10
+ if (!raw) return out;
11
+ for (const part of raw.split(',')) {
12
+ const eq = part.indexOf('=');
13
+ if (eq < 0) continue;
14
+ const key = part.slice(0, eq).trim();
15
+ const val = part.slice(eq + 1).trim();
16
+ if (!key) continue;
17
+ const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
18
+ const q = val.match(/^["'](.*)["']$/);
19
+ if (v) out[key] = `testData.get(${JSON.stringify(v[1])})`;
20
+ else if (q) out[key] = JSON.stringify(q[1]);
21
+ else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
22
+ else out[key] = JSON.stringify(val);
23
+ }
24
+ return out;
25
+ }