@sun-asterisk/sungen 2.5.2 → 2.6.0

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 (170) hide show
  1. package/dist/cli/commands/add-flow.d.ts +3 -0
  2. package/dist/cli/commands/add-flow.d.ts.map +1 -0
  3. package/dist/cli/commands/add-flow.js +27 -0
  4. package/dist/cli/commands/add-flow.js.map +1 -0
  5. package/dist/cli/commands/delivery.d.ts.map +1 -1
  6. package/dist/cli/commands/delivery.js +95 -60
  7. package/dist/cli/commands/delivery.js.map +1 -1
  8. package/dist/cli/commands/generate.d.ts.map +1 -1
  9. package/dist/cli/commands/generate.js +38 -6
  10. package/dist/cli/commands/generate.js.map +1 -1
  11. package/dist/cli/index.js +3 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  14. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  15. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  16. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  17. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -2
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +2 -2
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +1 -0
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +2 -1
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -2
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  27. package/dist/generators/test-generator/code-generator.d.ts +1 -0
  28. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  29. package/dist/generators/test-generator/code-generator.js +30 -12
  30. package/dist/generators/test-generator/code-generator.js.map +1 -1
  31. package/dist/generators/test-generator/step-mapper.d.ts +4 -0
  32. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  33. package/dist/generators/test-generator/step-mapper.js +7 -0
  34. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  35. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  36. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  37. package/dist/generators/test-generator/template-engine.js +1 -1
  38. package/dist/generators/test-generator/template-engine.js.map +1 -1
  39. package/dist/generators/test-generator/utils/data-resolver.d.ts +3 -20
  40. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  41. package/dist/generators/test-generator/utils/data-resolver.js +23 -66
  42. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  43. package/dist/generators/test-generator/utils/selector-resolver.d.ts +2 -6
  44. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  45. package/dist/generators/test-generator/utils/selector-resolver.js +18 -80
  46. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  47. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  48. package/dist/orchestrator/ai-rules-updater.js +4 -0
  49. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  50. package/dist/orchestrator/flow-manager.d.ts +22 -0
  51. package/dist/orchestrator/flow-manager.d.ts.map +1 -0
  52. package/dist/orchestrator/flow-manager.js +251 -0
  53. package/dist/orchestrator/flow-manager.js.map +1 -0
  54. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  55. package/dist/orchestrator/project-initializer.js +1 -0
  56. package/dist/orchestrator/project-initializer.js.map +1 -1
  57. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +88 -0
  58. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -8
  59. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -6
  60. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +15 -11
  61. package/dist/orchestrator/templates/ai-instructions/claude-config.md +41 -10
  62. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +12 -0
  63. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +19 -18
  64. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +12 -0
  65. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +52 -0
  66. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +31 -3
  67. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +45 -0
  68. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +69 -0
  69. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +30 -0
  70. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +86 -0
  71. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +13 -10
  72. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +16 -15
  73. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
  74. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +21 -17
  75. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +40 -9
  76. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +12 -0
  77. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +19 -18
  78. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +12 -0
  79. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +52 -0
  80. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +31 -3
  81. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +45 -0
  82. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +70 -0
  83. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +30 -0
  84. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  85. package/dist/orchestrator/templates/playwright.config.js +3 -1
  86. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  87. package/dist/orchestrator/templates/playwright.config.ts +4 -1
  88. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  89. package/dist/orchestrator/templates/specs-base.js +11 -56
  90. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  91. package/dist/orchestrator/templates/specs-base.ts +11 -61
  92. package/dist/orchestrator/templates/specs-test-data.d.ts +3 -1
  93. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -1
  94. package/dist/orchestrator/templates/specs-test-data.js +10 -2
  95. package/dist/orchestrator/templates/specs-test-data.js.map +1 -1
  96. package/dist/orchestrator/templates/specs-test-data.ts +9 -2
  97. package/package.json +1 -1
  98. package/src/cli/commands/add-flow.ts +25 -0
  99. package/src/cli/commands/delivery.ts +109 -58
  100. package/src/cli/commands/generate.ts +43 -6
  101. package/src/cli/index.ts +3 -1
  102. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  103. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  104. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -2
  105. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  106. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  107. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  108. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +2 -2
  109. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +1 -0
  110. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +2 -1
  111. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -2
  112. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  113. package/src/generators/test-generator/code-generator.ts +32 -14
  114. package/src/generators/test-generator/step-mapper.ts +8 -0
  115. package/src/generators/test-generator/template-engine.ts +2 -2
  116. package/src/generators/test-generator/utils/data-resolver.ts +25 -77
  117. package/src/generators/test-generator/utils/selector-resolver.ts +23 -109
  118. package/src/orchestrator/ai-rules-updater.ts +5 -0
  119. package/src/orchestrator/flow-manager.ts +243 -0
  120. package/src/orchestrator/project-initializer.ts +1 -0
  121. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +88 -0
  122. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -8
  123. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -6
  124. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +15 -11
  125. package/src/orchestrator/templates/ai-instructions/claude-config.md +41 -10
  126. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +12 -0
  127. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +19 -18
  128. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +12 -0
  129. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +52 -0
  130. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +31 -3
  131. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +45 -0
  132. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +69 -0
  133. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +30 -0
  134. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +86 -0
  135. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +13 -10
  136. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +16 -15
  137. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
  138. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +21 -17
  139. package/src/orchestrator/templates/ai-instructions/copilot-config.md +40 -9
  140. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +12 -0
  141. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +19 -18
  142. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +12 -0
  143. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +52 -0
  144. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +31 -3
  145. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +45 -0
  146. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +70 -0
  147. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +30 -0
  148. package/src/orchestrator/templates/playwright.config.ts +4 -1
  149. package/src/orchestrator/templates/specs-base.ts +11 -61
  150. package/src/orchestrator/templates/specs-test-data.ts +9 -2
  151. package/dist/utils/feature-finder.d.ts +0 -9
  152. package/dist/utils/feature-finder.d.ts.map +0 -1
  153. package/dist/utils/feature-finder.js +0 -67
  154. package/dist/utils/feature-finder.js.map +0 -1
  155. package/dist/utils/screen-paths.d.ts +0 -10
  156. package/dist/utils/screen-paths.d.ts.map +0 -1
  157. package/dist/utils/screen-paths.js +0 -73
  158. package/dist/utils/screen-paths.js.map +0 -1
  159. package/dist/utils/selector-loader.d.ts +0 -6
  160. package/dist/utils/selector-loader.d.ts.map +0 -1
  161. package/dist/utils/selector-loader.js +0 -20
  162. package/dist/utils/selector-loader.js.map +0 -1
  163. package/dist/utils/test-data-loader.d.ts +0 -6
  164. package/dist/utils/test-data-loader.d.ts.map +0 -1
  165. package/dist/utils/test-data-loader.js +0 -20
  166. package/dist/utils/test-data-loader.js.map +0 -1
  167. package/src/utils/feature-finder.ts +0 -33
  168. package/src/utils/screen-paths.ts +0 -37
  169. package/src/utils/selector-loader.ts +0 -23
  170. package/src/utils/test-data-loader.ts +0 -23
@@ -1,6 +1,6 @@
1
- import { test, expect } from '../base';
1
+ import { test, expect } from '{{basePath}}/base';
2
2
  {{#if runtimeData}}
3
- import { TestDataLoader } from '../test-data';
3
+ import { TestDataLoader } from '{{basePath}}/test-data';
4
4
  {{/if}}
5
5
 
6
6
  // This file is auto-generated from Gherkin feature files
@@ -1 +1 @@
1
- page.once('dialog', dialog => dialog.accept());
1
+ page.on('dialog', dialog => dialog.accept());
@@ -1 +1 @@
1
- page.once('dialog', dialog => dialog.dismiss());
1
+ page.on('dialog', dialog => dialog.dismiss());
@@ -1 +1 @@
1
- page.once('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
1
+ page.on('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
@@ -1,8 +1,8 @@
1
1
  const dialogPromise{{stepCounter}} = new Promise<string>(resolve => {
2
- page.once('dialog', dialog => {
2
+ page.on('dialog', dialog => {
3
3
  resolve(dialog.message());
4
4
  dialog.accept();
5
5
  });
6
6
  });
7
7
  // Trigger the dialog action, then:
8
- // expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
8
+ // expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
@@ -1,2 +1,3 @@
1
+ await expect({{> locator}}.getByRole('columnheader', { name: '{{columnName}}' })).toBeVisible();
1
2
  const {{columnIndexVar}} = (await page.getByRole('columnheader').allTextContents()).findIndex(h => h.includes('{{columnName}}'));
2
3
  await expect(page.getByRole('row').nth({{rowNth}}).getByRole('cell').nth({{columnIndexVar}})).toHaveText('{{dataValue}}');
@@ -1 +1,2 @@
1
- await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
1
+ await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
2
+ await page.waitForLoadState('domcontentloaded');
@@ -1,2 +1 @@
1
- await page.waitForURL('**{{path}}', { timeout: 5000 });
2
- expect(page.url()).toContain('{{path}}');
1
+ await expect(page).toHaveURL(/{{escapeRegex path}}/);
@@ -1 +1 @@
1
- await page.waitForTimeout({{duration}});
1
+ await page.waitForLoadState('networkidle');
@@ -134,7 +134,7 @@ export class CodeGenerator {
134
134
  // Steps registry built per feature during generateTestCode(); used by countSteps()
135
135
  private stepsRegistry = new Map<string, ParsedScenario>();
136
136
 
137
- constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean } = {}) {
137
+ constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean; flowMode?: boolean } = {}) {
138
138
  this.options = options;
139
139
  this.screenName = options.screenName;
140
140
  this.stepMapper = new StepMapper(options);
@@ -165,25 +165,33 @@ export class CodeGenerator {
165
165
  fileName = this.featureNameToFileName(feature.name);
166
166
  }
167
167
 
168
- // Extract screen name from source file path if available
168
+ // Extract screen/flow name from source file path for output subdirectory
169
169
  // qa/screens/{screenName}/features/{featureName}.feature -> screenName
170
- let screenSubdir = '';
170
+ // qa/flows/{flowName}/features/{featureName}.feature -> flows/flowName
171
+ let outputSubdir = '';
171
172
  if (feature.sourceFile) {
172
173
  const sourceDir = path.dirname(feature.sourceFile);
173
174
  const parts = sourceDir.split(path.sep);
175
+ const flowsIndex = parts.indexOf('flows');
174
176
  const screensIndex = parts.indexOf('screens');
175
- if (screensIndex >= 0 && screensIndex < parts.length - 2) {
176
- screenSubdir = parts[screensIndex + 1]; // screen name after 'screens'
177
+ if (flowsIndex >= 0 && flowsIndex < parts.length - 2) {
178
+ outputSubdir = path.join('flows', parts[flowsIndex + 1]);
179
+ } else if (screensIndex >= 0 && screensIndex < parts.length - 2) {
180
+ outputSubdir = parts[screensIndex + 1];
177
181
  }
178
182
  }
179
-
180
- // Build output path with screen subdirectory
181
- const filePath = screenSubdir
182
- ? path.join(outputDir, screenSubdir, fileName)
183
+
184
+ // Build output path with subdirectory
185
+ const filePath = outputSubdir
186
+ ? path.join(outputDir, outputSubdir, fileName)
183
187
  : path.join(outputDir, fileName);
184
188
 
189
+ // Compute relative path from output file back to specs/generated/
190
+ const depth = outputSubdir ? outputSubdir.split(path.sep).length : 0;
191
+ const basePath = depth > 0 ? Array(depth).fill('..').join('/') : '..';
192
+
185
193
  // Generate imports using adapter
186
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData });
194
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath });
187
195
 
188
196
  // Generate test code (async now to support AI mapping)
189
197
  const testCode = await this.generateTestCode(feature);
@@ -278,19 +286,29 @@ export class CodeGenerator {
278
286
  featureName = this.featureNameToFileName(feature.name).replace('.spec.ts', '');
279
287
  }
280
288
 
281
- // Derive screen name from source file path when not explicitly set
289
+ // Derive screen/flow name from source file path when not explicitly set
282
290
  // qa/screens/{screenName}/features/{featureName}.feature -> screenName
291
+ // qa/flows/{flowName}/features/{featureName}.feature -> flowName
283
292
  let effectiveScreenName = this.screenName;
293
+ let isFlowFeature = !!this.options.flowMode;
284
294
  if (!this.screenName && feature.sourceFile) {
285
295
  const sourceDir = path.dirname(feature.sourceFile);
286
296
  const parts = sourceDir.split(path.sep);
297
+ const flowsIndex = parts.indexOf('flows');
287
298
  const screensIndex = parts.indexOf('screens');
288
- if (screensIndex >= 0 && screensIndex < parts.length - 2) {
299
+ if (flowsIndex >= 0 && flowsIndex < parts.length - 2) {
300
+ effectiveScreenName = parts[flowsIndex + 1];
301
+ isFlowFeature = true;
302
+ } else if (screensIndex >= 0 && screensIndex < parts.length - 2) {
289
303
  effectiveScreenName = parts[screensIndex + 1];
290
- this.stepMapper.setScreenContext(effectiveScreenName);
304
+ isFlowFeature = false;
291
305
  }
306
+ this.stepMapper.setScreenContext(effectiveScreenName);
292
307
  }
293
308
 
309
+ // Reset flow mode per feature to prevent state leak in --all mode
310
+ this.stepMapper.setFlowMode(isFlowFeature);
311
+
294
312
  // Set feature context for data resolution and navigation
295
313
  this.stepMapper.setFeatureContext(featureName, feature.path);
296
314
 
@@ -419,7 +437,7 @@ export class CodeGenerator {
419
437
  cleanupConfig,
420
438
  screenshotOnFailure,
421
439
  runtimeData: this.options.runtimeData,
422
- screenName: effectiveScreenName,
440
+ screenName: isFlowFeature ? `flows/${effectiveScreenName}` : effectiveScreenName,
423
441
  featureFileName: featureName,
424
442
  scenarios: needsGrouping ? [] : scenarios,
425
443
  authGroups: needsGrouping ? authGroups : undefined,
@@ -71,6 +71,14 @@ export class StepMapper {
71
71
  this.dataResolver.setScreenContext(screenName);
72
72
  }
73
73
 
74
+ /**
75
+ * Enable flow mode — colon in selector refs is a namespace separator, not cross-screen
76
+ */
77
+ setFlowMode(enabled: boolean): void {
78
+ this.selectorResolver.setFlowMode(enabled);
79
+ this.dataResolver.setFlowMode(enabled);
80
+ }
81
+
74
82
  /**
75
83
  * Set scenario context for path variable resolution
76
84
  */
@@ -229,8 +229,8 @@ export class TemplateEngine {
229
229
  this.baseContext = {};
230
230
  }
231
231
 
232
- renderImports(options?: { runtimeData?: boolean }): string {
233
- return this.render('imports', { runtimeData: options?.runtimeData });
232
+ renderImports(options?: { runtimeData?: boolean; basePath?: string }): string {
233
+ return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..' });
234
234
  }
235
235
 
236
236
  renderTestFile(data: {
@@ -1,6 +1,4 @@
1
- import * as fs from 'fs';
2
1
  import * as path from 'path';
3
- import yaml from 'yaml';
4
2
  import { readYamlIfExists } from '../../../utils/yaml-io';
5
3
 
6
4
  /**
@@ -9,17 +7,13 @@ import { readYamlIfExists } from '../../../utils/yaml-io';
9
7
  * Two modes:
10
8
  * - Compile-time (default): resolves YAML values and bakes them into generated code
11
9
  * - Runtime (runtimeMode=true): returns markers that post-processor converts to testData.get() calls
12
- *
13
- * Supports override files with priority:
14
- * 1. .override.yaml (highest - user customizations)
15
- * 2. -override.yaml (legacy - backward compat)
16
- * 3. .yaml (lowest - auto-generated base)
17
10
  */
18
11
  export class DataResolver {
19
12
  private dataCache = new Map<string, any>();
20
13
  private testDataDir: string;
21
14
  private screenName?: string;
22
15
  private runtimeMode: boolean;
16
+ private flowMode: boolean = false;
23
17
 
24
18
  constructor(testDataDir?: string, screenName?: string, runtimeMode: boolean = false) {
25
19
  this.testDataDir = testDataDir || path.join(process.cwd(), 'qa', 'test-data');
@@ -27,13 +21,14 @@ export class DataResolver {
27
21
  this.runtimeMode = runtimeMode;
28
22
  }
29
23
 
30
- /**
31
- * Set screen context for data resolution
32
- */
33
24
  setScreenContext(screenName: string): void {
34
25
  this.screenName = screenName;
35
26
  }
36
27
 
28
+ setFlowMode(enabled: boolean): void {
29
+ this.flowMode = enabled;
30
+ }
31
+
37
32
  /**
38
33
  * Resolve data reference to actual value
39
34
  * @param dataRef - Format: email.valid (path.to.value without filename)
@@ -107,89 +102,42 @@ export class DataResolver {
107
102
 
108
103
  /**
109
104
  * Load data file from disk (with caching)
110
- * Searches new screen-based directory structure only
105
+ * Merges base + env-specific: {name}.yaml + {name}.{SUNGEN_ENV}.yaml
111
106
  */
112
107
  private loadDataFile(fileName: string): any {
113
108
  if (this.dataCache.has(fileName)) {
114
109
  return this.dataCache.get(fileName)!;
115
110
  }
116
111
 
117
- // Merge base + override: base provides defaults, override wins on conflict
118
- const possiblePaths = this.getPossibleDataPaths(fileName);
119
- let merged: any = {};
120
- let found = false;
121
-
122
- // Load in reverse priority order (base first, then overrides on top)
123
- for (const p of [...possiblePaths].reverse()) {
124
- const data = readYamlIfExists(p);
125
- if (data && typeof data === 'object' && Object.keys(data).length > 0) {
126
- merged = { ...merged, ...data };
127
- found = true;
128
- }
112
+ if (!this.screenName) {
113
+ throw new Error(`Data resolution requires screen/flow context for "${fileName}"`);
129
114
  }
130
115
 
131
- if (!found) {
116
+ const subDir = this.flowMode ? 'flows' : 'screens';
117
+ const baseDir = path.join(process.cwd(), 'qa', subDir, this.screenName, 'test-data');
118
+ const basePath = path.join(baseDir, `${fileName}.yaml`);
119
+ const data = readYamlIfExists(basePath);
120
+
121
+ if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
132
122
  throw new Error(
133
- `Data file not found. Tried:\n${possiblePaths.join('\n')}`
123
+ `Data file not found or empty: qa/${subDir}/${this.screenName}/test-data/${fileName}.yaml`
134
124
  );
135
125
  }
136
126
 
137
- this.dataCache.set(fileName, merged);
138
- return merged;
139
- }
127
+ let merged = { ...data };
140
128
 
141
- /**
142
- * Find data file path (checks new screen-based structure)
143
- * Priority: .override.yaml > -override.yaml > base .yaml
144
- * Skips files that exist but contain no data (only comments)
145
- */
146
- private findDataFilePath(fileName: string): string | null {
147
- const possiblePaths = this.getPossibleDataPaths(fileName);
148
-
149
- for (const p of possiblePaths) {
150
- const data = readYamlIfExists(p);
151
- if (data !== null) {
152
- // Skip if file is empty or only contains null/undefined
153
- if (data === undefined || (typeof data === 'object' && Object.keys(data).length === 0)) {
154
- continue;
155
- }
156
-
157
- // Emit deprecation warning for old -override.yaml pattern
158
- if (p.includes('-override.yaml')) {
159
- console.warn(`⚠️ Deprecated: ${path.basename(p)}`);
160
- console.warn(` Rename to: ${fileName}.override.yaml for better compatibility`);
161
- }
162
- return p;
129
+ // Merge env-specific data: {name}.{SUNGEN_ENV}.yaml
130
+ const env = process.env.SUNGEN_ENV;
131
+ if (env) {
132
+ const envPath = path.join(baseDir, `${fileName}.${env}.yaml`);
133
+ const envData = readYamlIfExists(envPath);
134
+ if (envData && typeof envData === 'object') {
135
+ merged = { ...merged, ...envData };
163
136
  }
164
137
  }
165
-
166
- return null;
167
- }
168
138
 
169
- /**
170
- * Get list of possible data file paths (priority order)
171
- * Priority: .override.yaml > -override.yaml > base .yaml
172
- */
173
- private getPossibleDataPaths(fileName: string): string[] {
174
- const paths: string[] = [];
175
-
176
- // New structure: qa/screens/{screenName}/test-data/
177
- if (this.screenName) {
178
- const cwd = process.cwd();
179
- const basePath = path.join(cwd, 'qa', 'screens', this.screenName, 'test-data');
180
-
181
- // Priority 1: New .override.yaml pattern (highest)
182
- paths.push(path.join(basePath, `${fileName}.override.yaml`));
183
-
184
- // Priority 2: Old -override.yaml pattern (backward compat)
185
- paths.push(path.join(basePath, `${fileName}-override.yaml`));
186
-
187
- // Priority 3: Base file (lowest)
188
- paths.push(path.join(basePath, `${fileName}.yaml`));
189
- paths.push(path.join(basePath, `${fileName}.yml`));
190
- }
191
-
192
- return paths;
139
+ this.dataCache.set(fileName, merged);
140
+ return merged;
193
141
  }
194
142
 
195
143
  /**
@@ -82,12 +82,17 @@ export class SelectorResolver {
82
82
  private selectorsDir: string;
83
83
  private featureName?: string;
84
84
  private screenName?: string;
85
+ private flowMode: boolean = false;
85
86
 
86
87
  constructor(selectorsDir?: string, screenName?: string) {
87
88
  this.selectorsDir = selectorsDir || path.join(process.cwd(), 'qa', 'selectors', 'screens');
88
89
  this.screenName = screenName;
89
90
  }
90
91
 
92
+ setFlowMode(enabled: boolean): void {
93
+ this.flowMode = enabled;
94
+ }
95
+
91
96
  /**
92
97
  * Set screen context for base selector file lookup
93
98
  */
@@ -160,9 +165,15 @@ export class SelectorResolver {
160
165
  // Check if it's legacy format (contains : or has dot with screen prefix)
161
166
  // Legacy: "login:email_field" or "login.email_field"
162
167
  // Natural: "Email Address", "Password", "Submit Button"
168
+ // Flow namespaced: "Login:Submit" → key "login:submit" in flow's own YAML
163
169
  const hasColonSeparator = selectorRef.includes(':');
164
170
  const hasDotSeparator = selectorRef.includes('.');
165
-
171
+
172
+ // Flow mode: colon is a namespace separator, resolve as natural language key
173
+ if (hasColonSeparator && this.flowMode) {
174
+ return this.resolveNaturalLanguage(selectorRef, contextName!, elementType, nth);
175
+ }
176
+
166
177
  // If it has colon, it's definitely legacy
167
178
  if (hasColonSeparator) {
168
179
  return this.resolveLegacyFormat(selectorRef);
@@ -601,7 +612,7 @@ export class SelectorResolver {
601
612
 
602
613
  const selectors: SelectorFile = {};
603
614
 
604
- // 1. Try load base file (screen-level shared selectors) - OPTIONAL
615
+ // 1. Try load base file (screen/flow-level shared selectors) - OPTIONAL
605
616
  if (this.screenName) {
606
617
  const basePath = this.findBaseSelectorPath(this.screenName);
607
618
  if (basePath) {
@@ -610,21 +621,11 @@ export class SelectorResolver {
610
621
  Object.assign(selectors, SelectorResolver.normalizeKeys(baseSelectors));
611
622
  }
612
623
  }
613
-
614
- // Load base override file
615
- const baseOverridePath = this.findOverrideSelectorPath(this.screenName);
616
- if (baseOverridePath) {
617
- const overrideSelectors = readYamlIfExists<SelectorFile>(baseOverridePath);
618
- if (overrideSelectors) {
619
- Object.assign(selectors, SelectorResolver.normalizeKeys(overrideSelectors));
620
- }
621
- }
622
624
  }
623
625
 
624
626
  // 2. Load feature-specific selectors - REQUIRED
625
627
  const featurePath = this.findBaseSelectorPath(featureName);
626
628
  if (!featurePath) {
627
- // If no base file, that's OK if we have base from screen
628
629
  if (Object.keys(selectors).length === 0) {
629
630
  throw new Error(
630
631
  `Selector file not found for feature "${featureName}". ` +
@@ -638,15 +639,6 @@ export class SelectorResolver {
638
639
  }
639
640
  }
640
641
 
641
- // 3. Load feature override file
642
- const featureOverridePath = this.findOverrideSelectorPath(featureName);
643
- if (featureOverridePath) {
644
- const overrideSelectors = readYamlIfExists<SelectorFile>(featureOverridePath);
645
- if (overrideSelectors) {
646
- Object.assign(selectors, SelectorResolver.normalizeKeys(overrideSelectors));
647
- }
648
- }
649
-
650
642
  this.selectorCache.set(cacheKey, selectors);
651
643
  return selectors;
652
644
  }
@@ -657,72 +649,25 @@ export class SelectorResolver {
657
649
  private findBaseSelectorPath(name: string): string | null {
658
650
  const possiblePaths: string[] = [];
659
651
 
660
- // New structure: qa/screens/{screenName}/selectors/
652
+ // New structure: qa/screens/{screenName}/selectors/ or qa/flows/{flowName}/selectors/
661
653
  const qaDir = path.dirname(path.dirname(this.selectorsDir));
662
-
663
- if (this.screenName) {
664
- // Priority: Base .yaml only
665
- possiblePaths.push(
666
- path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}.yaml`)
667
- );
668
- } else {
669
- possiblePaths.push(
670
- path.join(qaDir, 'screens', name, 'selectors', `${name}.yaml`)
671
- );
672
- }
673
-
674
- // Old structure
675
- possiblePaths.push(path.join(this.selectorsDir, `${name}.yaml`));
676
-
677
- for (const p of possiblePaths) {
678
- if (fs.existsSync(p)) {
679
- return p;
680
- }
681
- }
682
-
683
- return null;
684
- }
685
-
686
- /**
687
- * Find OVERRIDE selector file path
688
- * Priority: .override.yaml > -override.yaml
689
- */
690
- private findOverrideSelectorPath(name: string): string | null {
691
- const possiblePaths: string[] = [];
654
+ const subDir = this.flowMode ? 'flows' : 'screens';
692
655
 
693
- // New structure: qa/screens/{screenName}/selectors/
694
- const qaDir = path.dirname(path.dirname(this.selectorsDir));
695
-
696
656
  if (this.screenName) {
697
- // Priority 1: New .override.yaml
698
657
  possiblePaths.push(
699
- path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}.override.yaml`)
700
- );
701
-
702
- // Priority 2: Old -override.yaml
703
- possiblePaths.push(
704
- path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}-override.yaml`)
658
+ path.join(qaDir, subDir, this.screenName, 'selectors', `${name}.yaml`)
705
659
  );
706
660
  } else {
707
661
  possiblePaths.push(
708
- path.join(qaDir, 'screens', name, 'selectors', `${name}.override.yaml`)
709
- );
710
- possiblePaths.push(
711
- path.join(qaDir, 'screens', name, 'selectors', `${name}-override.yaml`)
662
+ path.join(qaDir, subDir, name, 'selectors', `${name}.yaml`)
712
663
  );
713
664
  }
714
665
 
715
666
  // Old structure
716
- possiblePaths.push(path.join(this.selectorsDir, `${name}.override.yaml`));
717
- possiblePaths.push(path.join(this.selectorsDir, `${name}-override.yaml`));
667
+ possiblePaths.push(path.join(this.selectorsDir, `${name}.yaml`));
718
668
 
719
669
  for (const p of possiblePaths) {
720
670
  if (fs.existsSync(p)) {
721
- // Emit deprecation warning for old -override.yaml pattern
722
- if (p.includes('-override.yaml')) {
723
- console.warn(`⚠️ Deprecated: ${path.basename(p)}`);
724
- console.warn(` Rename to: ${name}.override.yaml for better compatibility`);
725
- }
726
671
  return p;
727
672
  }
728
673
  }
@@ -732,59 +677,28 @@ export class SelectorResolver {
732
677
 
733
678
  /**
734
679
  * Find selector file path (checks both old and new structures)
735
- * Priority: .override.yaml > -override.yaml > base .yaml
736
680
  */
737
681
  private findSelectorPath(name: string): string | null {
738
- // Build list of possible paths (priority order)
739
682
  const possiblePaths: string[] = [];
740
683
 
741
- // New structure: qa/screens/<screenName>/selectors/
742
- const qaDir = path.dirname(path.dirname(this.selectorsDir)); // go up from qa/selectors/screens to qa/
743
-
684
+ const qaDir = path.dirname(path.dirname(this.selectorsDir));
685
+ const subDir = this.flowMode ? 'flows' : 'screens';
686
+
744
687
  if (this.screenName) {
745
- // Priority 1: New .override.yaml pattern (highest)
746
688
  possiblePaths.push(
747
- path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}.override.yaml`)
748
- );
749
-
750
- // Priority 2: Old -override.yaml pattern (backward compat)
751
- possiblePaths.push(
752
- path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}-override.yaml`)
753
- );
754
-
755
- // Priority 3: Base file (lowest)
756
- possiblePaths.push(
757
- path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}.yaml`)
689
+ path.join(qaDir, subDir, this.screenName, 'selectors', `${name}.yaml`)
758
690
  );
759
691
  } else {
760
- // Fallback: assume name is both screen and feature
761
- possiblePaths.push(
762
- path.join(qaDir, 'screens', name, 'selectors', `${name}.override.yaml`)
763
- );
764
692
  possiblePaths.push(
765
- path.join(qaDir, 'screens', name, 'selectors', `${name}-override.yaml`)
766
- );
767
- possiblePaths.push(
768
- path.join(qaDir, 'screens', name, 'selectors', `${name}.yaml`)
693
+ path.join(qaDir, subDir, name, 'selectors', `${name}.yaml`)
769
694
  );
770
695
  }
771
696
 
772
697
  // Old structure: qa/selectors/screens/
773
- // Priority 1: New .override.yaml
774
- possiblePaths.push(path.join(this.selectorsDir, `${name}.override.yaml`));
775
- // Priority 2: Old -override.yaml
776
- possiblePaths.push(path.join(this.selectorsDir, `${name}-override.yaml`));
777
- // Priority 3: Base
778
698
  possiblePaths.push(path.join(this.selectorsDir, `${name}.yaml`));
779
699
 
780
- // Find first existing file and emit deprecation warning for old pattern
781
700
  for (const p of possiblePaths) {
782
701
  if (fs.existsSync(p)) {
783
- // Emit deprecation warning for old -override.yaml pattern
784
- if (p.includes('-override.yaml')) {
785
- console.warn(`⚠️ Deprecated: ${path.basename(p)}`);
786
- console.warn(` Rename to: ${name}.override.yaml for better compatibility`);
787
- }
788
702
  return p;
789
703
  }
790
704
  }
@@ -16,6 +16,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
16
16
 
17
17
  // Commands — Claude Code
18
18
  ['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
19
+ ['claude-cmd-add-flow.md', '.claude/commands/sungen/add-flow.md'],
19
20
  ['claude-cmd-create-test.md', '.claude/commands/sungen/create-test.md'],
20
21
  ['claude-cmd-run-test.md', '.claude/commands/sungen/run-test.md'],
21
22
  ['claude-cmd-review.md', '.claude/commands/sungen/review.md'],
@@ -23,6 +24,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
23
24
 
24
25
  // Commands — GitHub Copilot
25
26
  ['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
27
+ ['copilot-cmd-add-flow.md', '.github/prompts/sungen-add-flow.prompt.md'],
26
28
  ['copilot-cmd-create-test.md', '.github/prompts/sungen-create-test.prompt.md'],
27
29
  ['copilot-cmd-run-test.md', '.github/prompts/sungen-run-test.prompt.md'],
28
30
  ['copilot-cmd-review.md', '.github/prompts/sungen-review.prompt.md'],
@@ -43,6 +45,9 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
43
45
  ['claude-skill-capture-live.md', '.claude/skills/sungen-capture-live/SKILL.md'],
44
46
  ['claude-skill-figma-source.md', '.claude/skills/sungen-figma-source/SKILL.md'],
45
47
 
48
+ // Skills — Copilot (prompt-based)
49
+ ['copilot-skill-figma-source.md', '.github/prompts/sungen-figma-source.prompt.md'],
50
+
46
51
  // Skills — GitHub Copilot
47
52
  ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
48
53
  ['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],