@sun-asterisk/sungen 3.1.2-beta.93 → 3.1.2-beta.97

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 (150) 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/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  32. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  33. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  34. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  35. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  36. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  37. package/dist/generators/test-generator/code-generator.d.ts +11 -9
  38. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  39. package/dist/generators/test-generator/code-generator.js +53 -76
  40. package/dist/generators/test-generator/code-generator.js.map +1 -1
  41. package/dist/generators/test-generator/patterns/index.d.ts +0 -10
  42. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  43. package/dist/generators/test-generator/patterns/index.js +10 -47
  44. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  45. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  46. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  47. package/dist/generators/test-generator/template-engine.js +1 -1
  48. package/dist/generators/test-generator/template-engine.js.map +1 -1
  49. package/dist/harness/annotation-overrides.d.ts +9 -0
  50. package/dist/harness/annotation-overrides.d.ts.map +1 -0
  51. package/dist/harness/annotation-overrides.js +36 -0
  52. package/dist/harness/annotation-overrides.js.map +1 -0
  53. package/dist/harness/audit.d.ts.map +1 -1
  54. package/dist/harness/audit.js +35 -7
  55. package/dist/harness/audit.js.map +1 -1
  56. package/dist/harness/catalog/drivers.yaml +35 -12
  57. package/dist/harness/parse.d.ts +1 -0
  58. package/dist/harness/parse.d.ts.map +1 -1
  59. package/dist/harness/parse.js +13 -4
  60. package/dist/harness/parse.js.map +1 -1
  61. package/dist/index.d.ts +20 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +32 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  66. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  67. package/dist/orchestrator/templates/specs-api.d.ts +19 -0
  68. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
  69. package/dist/orchestrator/templates/specs-api.js +128 -0
  70. package/dist/orchestrator/templates/specs-api.js.map +1 -0
  71. package/dist/orchestrator/templates/specs-api.ts +101 -0
  72. package/package.json +7 -30
  73. package/src/capabilities/builtins.ts +85 -0
  74. package/src/capabilities/context-router.ts +66 -0
  75. package/src/capabilities/context.ts +46 -0
  76. package/src/capabilities/discover.ts +42 -0
  77. package/src/capabilities/registry.ts +111 -0
  78. package/src/capabilities/sensor.ts +47 -0
  79. package/src/cli/commands/generate.ts +7 -3
  80. package/src/cli/index.ts +10 -1
  81. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  82. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  83. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  84. package/src/generators/test-generator/code-generator.ts +51 -74
  85. package/src/generators/test-generator/patterns/index.ts +9 -35
  86. package/src/generators/test-generator/template-engine.ts +2 -2
  87. package/src/harness/annotation-overrides.ts +25 -0
  88. package/src/harness/audit.ts +37 -8
  89. package/src/harness/catalog/drivers.yaml +35 -12
  90. package/src/harness/parse.ts +7 -2
  91. package/src/index.ts +30 -0
  92. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  93. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  94. package/src/orchestrator/templates/specs-api.ts +101 -0
  95. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
  96. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
  97. package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
  98. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
  99. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
  100. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
  101. package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
  102. package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
  103. package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -6
  104. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
  105. package/dist/generators/test-generator/patterns/database-patterns.js +0 -95
  106. package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
  107. package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
  108. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
  109. package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
  110. package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
  111. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
  112. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
  113. package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
  114. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
  115. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
  116. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
  117. package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
  118. package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
  119. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
  120. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
  121. package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
  122. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
  123. package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
  124. package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
  125. package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
  126. package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
  127. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
  128. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
  129. package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
  130. package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
  131. package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
  132. package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
  133. package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
  134. package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
  135. package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
  136. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
  137. package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
  138. package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
  139. package/docs/orchestration-spec.md +0 -267
  140. package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
  141. package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
  142. package/src/generators/test-generator/patterns/database-patterns.ts +0 -96
  143. package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
  144. package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
  145. package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
  146. package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
  147. package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
  148. package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
  149. package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
  150. package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
@@ -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,62 +306,40 @@ 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
  }
@@ -378,11 +362,6 @@ export class CodeGenerator {
378
362
  console.log(`✓ ${exists ? 'Updated' : 'Created'}: specs/${fileName}`);
379
363
  }
380
364
 
381
- /** Ensure specs/db.ts is present and current (Data Driver runtime helper). */
382
- ensureDbFile(outputDir: string): void {
383
- this.syncGeneratedHelper(outputDir, 'db.ts', 'specs-db.ts');
384
- }
385
-
386
365
  ensureBaseFile(outputDir: string): void {
387
366
  this.syncGeneratedHelper(outputDir, 'base.ts', 'specs-base.ts');
388
367
  // base.ts depends on locale-fixture.ts — keep them paired.
@@ -683,18 +662,16 @@ export class CodeGenerator {
683
662
  for (const r of refs) this.stepMapper.registerCaptured(r);
684
663
  }
685
664
 
686
- // @query-bound vars: `{{name.col}}` / `{{name[2].col}}` / `{{name.count}}` exist only at
687
- // runtime (the bind fetches them) register as captured so they resolve to a runtime get()
688
- // instead of a compile-time YAML lookup that would fail.
689
- const queryNames = (scenario.tags || [])
690
- .map((t) => t.match(/^@query:([A-Za-z_][A-Za-z0-9_]*)/))
691
- .filter((m): m is RegExpMatchArray => !!m)
692
- .map((m) => m[1]);
693
- 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) {
694
671
  for (const st of stepsToMap) {
695
672
  for (const mt of (st.text || '').matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
696
673
  const head = mt[1].split(/[.[]/)[0];
697
- if (queryNames.includes(head)) this.stepMapper.registerCaptured(mt[1]);
674
+ if (boundVars.includes(head)) this.stepMapper.registerCaptured(mt[1]);
698
675
  }
699
676
  }
700
677
  }
@@ -728,11 +705,11 @@ export class CodeGenerator {
728
705
  }
729
706
  }
730
707
 
731
- // @query:<name>[(p={{v}},…)] tags run the catalog query as a PRECONDITION and bind its
732
- // result to a `{{name}}` variable (scenario asserts on it with `expect …` + path access).
733
- // Prepended so the binds run before the scenario's own steps.
734
- const queryBinds = this.buildQueryBinds(scenario);
735
- 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
+ }
736
713
 
737
714
  // Extract pass-through tags (feature + scenario, excluding functional tags)
738
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
+ }
@@ -11,7 +11,7 @@ import * as fs from 'fs';
11
11
  import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
12
12
  import {
13
13
  loadCatalog, viewpointGate, assertionDepth, dataThemesFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
14
- GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult,
14
+ GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog,
15
15
  } from './sensors';
16
16
  import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
17
17
  import { getProvenance, Provenance } from './provenance';
@@ -19,6 +19,9 @@ import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-cover
19
19
  import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
20
20
  negativeSideEffect, sourceBacked, crossArtifactOwnership } from './quality-gates';
21
21
  import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-ledger';
22
+ import { capabilityRegistry } from '../capabilities/registry';
23
+ import { discoverAndRegisterCapabilities } from '../capabilities/discover';
24
+ import { contextRouter } from '../capabilities/context-router';
22
25
 
23
26
  export interface AuditReport {
24
27
  screen: string;
@@ -63,13 +66,23 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
63
66
 
64
67
  const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
65
68
  const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
66
- const catalog = loadCatalog();
69
+ // The viewpoint catalog is owned by the default (UI) capability via the SPI; falls back to the
70
+ // in-core loader if no capability provides one. Same catalog content → identical scores (R2).
71
+ discoverAndRegisterCapabilities();
72
+ const defaultCap = capabilityRegistry.defaultCapabilityId();
73
+ const catalog = ((defaultCap && capabilityRegistry.get(defaultCap)?.viewpoints?.()) as Catalog | undefined) || loadCatalog();
67
74
  const spec = specCoverage(specPath, scenarios, featureText);
68
75
 
69
- const gate = viewpointGate(scenarios, viewpoints, catalog);
70
76
  // P3 — intent profile from qa/context.md drives the depth threshold (focus).
71
77
  const intent = readIntent(projectRootFromScreenDir(screenDir));
72
- const depth = assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
78
+ // The viewpoint coverage gate + assertion depth are owned by the default (UI) capability and
79
+ // obtained via its `gateProvider` (R2.2b). Same functions underneath → byte-identical gate/depth
80
+ // → identical score. Falls back to the in-core functions if no capability provides them.
81
+ const uiGate = (defaultCap && capabilityRegistry.get(defaultCap)?.gateProvider) as
82
+ ((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus }) => { gate: GateResult; depth: DepthResult }) | undefined;
83
+ const provided = uiGate?.({ scenarios, viewpoints, catalog, focus: intent.focus });
84
+ const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog);
85
+ const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
73
86
  const claim = claimProof(scenarios, intent.focus);
74
87
  const taxonomy = taxonomyLint(scenarios);
75
88
  const balance = coverageBalance(scenarios);
@@ -125,9 +138,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
125
138
  if (trace.mappedRatio < 0.5) {
126
139
  findings.push(`TRACE: ${trace.note}`);
127
140
  }
128
- if (gate.universalGaps.length) {
129
- findings.push(`UNIVERSAL: missing theme(s): ${gate.universalGaps.join(', ')} (low priority reminder).`);
130
- }
141
+ // (UNIVERSAL viewpoint-gap finding now emitted by the `ui` gate sensor — see the gate block below.)
131
142
  for (const g of spec.triggerGaps) {
132
143
  findings.push(`TRIGGER-UNCOVERED: spec validates "${g.constraint}"${g.code ? ` (${g.code})` : ''} on [${g.required.join(', ')}] but scenarios only exercise it on [${g.found.join(', ') || 'none'}] → add a ${g.missing.join(', ')}-trigger scenario for this constraint (don't collapse the trigger × input matrix).`);
133
144
  }
@@ -157,6 +168,24 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
157
168
  findings.push(`UNSOURCEABLE-SCENARIO: "${u}" doesn't trace to any FR / viewpoint item — link it to a source, or tag it @exploration (not part of the official suite).`);
158
169
  }
159
170
 
171
+ // Capability gate sensors (Capability SPI): the ContextRouter scopes WHICH gate sensors run to
172
+ // the capabilities this feature actually uses — generic ('core') + the default UI + any whose
173
+ // annotation tags appear (e.g. @query). Today core+ui gate sensors are always in scope, so this
174
+ // is behaviour-identical; it bounds the set as capability-specific gate sensors (@api, …) are
175
+ // added. Each runs over the audit context; an 'error' finding fails the gate.
176
+ const featureTags = [
177
+ ...(scenarios.some((s) => s.queryRefs && s.queryRefs.length) ? ['@query'] : []),
178
+ ...(scenarios.some((s) => s.apiRefs && s.apiRefs.length) ? ['@api'] : []),
179
+ ];
180
+ const routedGateIds = contextRouter.route({ target: { kind: 'screen', id: screenName }, artifact: 'feature', tags: featureTags }).gateSensorIds;
181
+ const gateSensorFindings = capabilityRegistry.sensors('gate')
182
+ .filter((s) => routedGateIds.includes(s.id))
183
+ .flatMap((s) => s.run({ screenName, cwd: projectRootFromScreenDir(screenDir), featureText, scenarios, universalGaps: gate.universalGaps }));
184
+ // Each gate sensor's message carries its own code prefix (VERIFICATION-FAIL / UNIVERSAL / …)
185
+ // → push verbatim.
186
+ for (const f of gateSensorFindings) findings.push(f.message);
187
+ const gateSensorError = gateSensorFindings.some((f) => f.severity === 'error');
188
+
160
189
  // #8 — multi-axis calibration: a high overall must not hide a weak axis.
161
190
  const manualCompleteness = manualOracleResult.manualTotal
162
191
  ? 1 - manualOracleResult.insufficient.length / manualOracleResult.manualTotal : 1;
@@ -180,7 +209,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
180
209
  // Gate spans coverage (viewpoint themes), depth, claim-proof, spec-clause coverage,
181
210
  // AND taxonomy-match (scenarios must use the project's viewpoint IDs when defined).
182
211
  const gateStatus: 'PASS' | 'FAIL' =
183
- gate.gaps.length === 0 && depth.verdict !== 'fail' && claim.verdict !== 'fail' && spec.verdict !== 'fail' && !taxonomyMismatch ? 'PASS' : 'FAIL';
212
+ gate.gaps.length === 0 && depth.verdict !== 'fail' && claim.verdict !== 'fail' && spec.verdict !== 'fail' && !taxonomyMismatch && !gateSensorError ? 'PASS' : 'FAIL';
184
213
 
185
214
  return {
186
215
  screen: screenName,
@@ -1,21 +1,37 @@
1
1
  # Driver Catalog (metadata only — NO driver code is bundled here).
2
2
  # Lets Sungen RECOMMEND/RESOLVE a driver that may not be installed yet, and tells
3
- # `sungen capability add` which package to install. See docs/spec/sungen_phase2a_spec.md.
3
+ # `sungen capability add` which package to install. See docs/spec/sungen_phase2a_spec.md
4
+ # and docs/spec/sungen_packaging_spec.md (R5 — the capability SPI + npm packages).
4
5
  #
5
- # kind: platform → the runtime/codegen adapter for a target (pick ONE per project)
6
- # kind: capability an extra ability added on top of a platform (Phase 3)
7
- # unblocks: manual-reason codes (M1–M9) this driver can resolve (Phase 2b taxonomy)
6
+ # Two axes:
7
+ # kind: platform HOW tests run (runtime/codegen adapter). Pick ONE per project.
8
+ # kind: capability WHAT extra thing is verified, added on top of a platform.
9
+ # Fields:
10
+ # status: shipped → published as an npm package (R5); planned → not built yet.
11
+ # bundled: true → installed automatically (a dependency of @sun-asterisk/sungen),
12
+ # so `capability add` is unnecessary.
13
+ # unblocks: manual-reason codes (M1–M9) this driver resolves (Phase 2b taxonomy).
14
+ #
15
+ # R5 status: the three real capabilities ship as packages — @sungen/driver-ui (web UI,
16
+ # bundled as the default), @sungen/driver-db, @sungen/driver-api. The web *platform*
17
+ # entry below points at @sungen/driver-ui (the UI step vocabulary + viewpoint gate); the
18
+ # Playwright codegen *adapter* itself is still in-core (Phase 2a). Mobile + the remaining
19
+ # capabilities are planned. See the "Platform axis & mobile evolution" section of the
20
+ # packaging spec for the `sungen init --platform <web|mobile>` roadmap.
8
21
 
9
22
  drivers:
10
23
  web:
11
24
  kind: platform
12
- package: "@sungen/driver-web" # Phase 2a: bundled Playwright adapter serves this (back-compat)
13
- runtime: playwright
14
- adapter: web # registry adapter name
25
+ package: "@sungen/driver-ui" # R5: web UI capability (step patterns + viewpoint gate)
26
+ status: shipped
27
+ bundled: true # @sun-asterisk/sungen depends on it → UI works out of the box
28
+ runtime: playwright # codegen adapter still in-core (Phase 2a)
29
+ adapter: web # registry adapter name
15
30
  capabilities: ["@ui"]
16
31
  mobile:
17
32
  kind: platform
18
33
  package: "@sungen/driver-mobile"
34
+ status: planned # PoC on the feat/mobile branch (Appium / Flutter)
19
35
  runtime: appium
20
36
  adapter: mobile
21
37
  capabilities: ["@ui"]
@@ -23,35 +39,42 @@ drivers:
23
39
  api:
24
40
  kind: capability
25
41
  package: "@sungen/driver-api"
42
+ status: shipped
26
43
  capabilities: ["@api", "@apiAssert", "@hybrid"]
27
44
  unblocks: [M2]
28
- data-factory:
29
- kind: capability
30
- package: "@sungen/driver-data-factory"
31
- capabilities: ["@dataFactory"]
32
- unblocks: [M1]
33
45
  db:
34
46
  kind: capability
35
47
  package: "@sungen/driver-db"
48
+ status: shipped
36
49
  capabilities: ["@dbAssert"]
37
50
  unblocks: [M2]
51
+ data-factory:
52
+ kind: capability
53
+ package: "@sungen/driver-data-factory"
54
+ status: planned
55
+ capabilities: ["@dataFactory"]
56
+ unblocks: [M1]
38
57
  mock:
39
58
  kind: capability
40
59
  package: "@sungen/driver-mock"
60
+ status: planned
41
61
  capabilities: ["@mock", "@network"]
42
62
  unblocks: [M3]
43
63
  mail-file:
44
64
  kind: capability
45
65
  package: "@sungen/driver-mail-file"
66
+ status: planned
46
67
  capabilities: ["@mail", "@file"]
47
68
  unblocks: [M5]
48
69
  contract:
49
70
  kind: capability
50
71
  package: "@sungen/driver-contract"
72
+ status: planned
51
73
  capabilities: ["@contract"]
52
74
  unblocks: [M5]
53
75
  specialized:
54
76
  kind: capability
55
77
  package: "@sungen/driver-specialized"
78
+ status: planned
56
79
  capabilities: ["@specialized"]
57
80
  unblocks: [M6]
@@ -32,6 +32,7 @@ export interface ScenarioInfo {
32
32
  vpId?: string; // raw leading ID token of the title (project's scheme: VP0-001, MS-HP-001, VP-LIST-001)
33
33
  casesDataset?: string; // @cases:<dataset> — data-driven; one scenario expands to N row-tests
34
34
  queryRefs?: string[]; // named queries referenced by this scenario (inline `query [name]` + @query: tags)
35
+ apiRefs?: string[]; // named API endpoints referenced by this scenario (@api: tags)
35
36
  }
36
37
 
37
38
  /** Format-tolerant: is this token an ID (project's scheme), not a prose word?
@@ -102,12 +103,15 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
102
103
  const manual = tags.includes('@manual');
103
104
  const casesTag = tags.find((t) => t.startsWith('@cases:'));
104
105
  const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
105
- // Named-query references: @query:<name> tags + inline `query [name]` step refs.
106
+ // Named-query references: @query:<name>[(overrides)] tags + inline `query [name]` step refs.
106
107
  const queryRefs = new Set<string>();
107
- for (const t of tags) if (t.startsWith('@query:')) { const n = t.slice('@query:'.length).trim(); if (n) queryRefs.add(n); }
108
+ for (const t of tags) if (t.startsWith('@query:')) { const m = t.slice('@query:'.length).match(/^([A-Za-z_][A-Za-z0-9_]*)/); if (m) queryRefs.add(m[1]); }
108
109
  for (const step of (sc.steps as ParsedStep[]) || []) {
109
110
  for (const m of (step.text || '').matchAll(/\bquery\s+\[([A-Za-z_][A-Za-z0-9_]*)\]/gi)) queryRefs.add(m[1]);
110
111
  }
112
+ // Named-API references: @api:<name>[(overrides)] tags.
113
+ const apiRefs = new Set<string>();
114
+ for (const t of tags) if (t.startsWith('@api:')) { const m = t.slice('@api:'.length).match(/^([A-Za-z_][A-Za-z0-9_]*)/); if (m) apiRefs.add(m[1]); }
111
115
  let priority: Priority = 'unknown';
112
116
  for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
113
117
 
@@ -164,6 +168,7 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
164
168
  vpId,
165
169
  casesDataset,
166
170
  queryRefs: queryRefs.size ? [...queryRefs] : undefined,
171
+ apiRefs: apiRefs.size ? [...apiRefs] : undefined,
167
172
  };
168
173
  }
169
174
 
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Public API of `@sun-asterisk/sungen` — the capability SPI plus the shared compiler/harness surface
3
+ * that capability drivers (`@sungen/driver-*`) build against. Drivers import from here; core never
4
+ * imports from a driver (discovery loads them at runtime). Keep this surface small and intentional.
5
+ */
6
+
7
+ // --- Capability SPI ---
8
+ export { capabilityRegistry, CapabilityRegistry } from './capabilities/registry';
9
+ export type { CapabilityDescriptor } from './capabilities/registry';
10
+ export type { Sensor, SensorFinding, AdvisoryScanInput, GateInput } from './capabilities/sensor';
11
+ export type { Context, DiscoveryProvider, ContextMapper, GenerationUnit } from './capabilities/context';
12
+
13
+ // --- Step-pattern authoring (a driver contributes step patterns via its descriptor) ---
14
+ export type { PatternContext, StepPattern, StepTemplateData } from './generators/test-generator/patterns/types';
15
+ export type { MappedStep } from './generators/test-generator/step-mapper';
16
+ export type { ParsedStep } from './generators/gherkin-parser';
17
+ export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
18
+
19
+ // --- Precondition-annotation override grammar (shared by the @query / @api driver codegen) ---
20
+ export { parseQueryOverrides } from './harness/annotation-overrides';
21
+
22
+ // --- Named-query catalog (shared: the DB driver's codegen + core's data-driven advisory lint) ---
23
+ export { resolveQuery, compileQuery, lintCatalog } from './harness/query-catalog';
24
+ export type { QueryEntry } from './harness/query-catalog';
25
+
26
+ // --- Shared harness: viewpoint catalog + coverage gate / assertion depth ---
27
+ // (the UI capability's gateProvider composes these; they also back core's ingest + audit fallback)
28
+ export { loadCatalog, viewpointGate, assertionDepth, dataThemesFor } from './harness/sensors';
29
+ export type { Catalog, GateResult, DepthResult } from './harness/sensors';
30
+ export type { ScenarioInfo, ViewpointEntry } from './harness/parse';
@@ -213,6 +213,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
213
213
  | `@flow` | Mark feature as E2E flow (cross-screen testing) |
214
214
  | `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
215
215
  | `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
216
+ | `@api:name` | API: run the named request from `api/apis.yaml` (precondition) and bind the response to `{{name}}`; assert with `expect {{name.status}} …` + path access (`{{name.body.<path>}}`). Override params `@api:name(p={{v}})`. Repeatable. (Optional API Driver) |
216
217
 
217
218
  ### Data-driven scenarios (`@cases`)
218
219
 
@@ -213,6 +213,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
213
213
  | `@flow` | Mark feature as E2E flow (cross-screen testing) |
214
214
  | `@cases:dataset` | Data-driven: run the scenario once per row of the `dataset` LIST in test-data → one `test()` per row |
215
215
  | `@query:name` | Database: run the named query from `database/queries.yaml` (precondition) and bind its rows to `{{name}}`; assert with `expect {{name.count}} …` + path access. Override params `@query:name(p={{v}})`. Repeatable. (Optional Data Driver — see Database verification above) |
216
+ | `@api:name` | API: run the named request from `api/apis.yaml` (precondition) and bind the response to `{{name}}`; assert with `expect {{name.status}} …` + path access (`{{name.body.<path>}}`). Override params `@api:name(p={{v}})`. Repeatable. (Optional API Driver) |
216
217
 
217
218
  ### Data-driven scenarios (`@cases`)
218
219