@sun-asterisk/sungen 3.2.9 → 3.2.10-beta.1

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 (147) hide show
  1. package/dist/capabilities/discover.js +1 -1
  2. package/dist/capabilities/discover.js.map +1 -1
  3. package/dist/capabilities/registry.d.ts +9 -0
  4. package/dist/capabilities/registry.d.ts.map +1 -1
  5. package/dist/capabilities/registry.js +4 -0
  6. package/dist/capabilities/registry.js.map +1 -1
  7. package/dist/cli/commands/delivery.d.ts.map +1 -1
  8. package/dist/cli/commands/delivery.js +15 -5
  9. package/dist/cli/commands/delivery.js.map +1 -1
  10. package/dist/cli/commands/generate.d.ts.map +1 -1
  11. package/dist/cli/commands/generate.js +16 -0
  12. package/dist/cli/commands/generate.js.map +1 -1
  13. package/dist/exporters/spec-parser.d.ts.map +1 -1
  14. package/dist/exporters/spec-parser.js +16 -5
  15. package/dist/exporters/spec-parser.js.map +1 -1
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-row-column.hbs +2 -0
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
  32. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
  33. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
  34. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
  35. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
  36. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
  37. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
  38. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
  41. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
  42. package/dist/generators/test-generator/app-profile.d.ts +89 -0
  43. package/dist/generators/test-generator/app-profile.d.ts.map +1 -0
  44. package/dist/generators/test-generator/app-profile.js +157 -0
  45. package/dist/generators/test-generator/app-profile.js.map +1 -0
  46. package/dist/generators/test-generator/code-generator.d.ts +4 -0
  47. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  48. package/dist/generators/test-generator/code-generator.js +7 -0
  49. package/dist/generators/test-generator/code-generator.js.map +1 -1
  50. package/dist/generators/test-generator/diagnostics.d.ts +36 -0
  51. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -0
  52. package/dist/generators/test-generator/diagnostics.js +44 -0
  53. package/dist/generators/test-generator/diagnostics.js.map +1 -0
  54. package/dist/generators/test-generator/patterns/types.d.ts +12 -0
  55. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  56. package/dist/generators/test-generator/recipe-expander.d.ts +32 -0
  57. package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -0
  58. package/dist/generators/test-generator/recipe-expander.js +118 -0
  59. package/dist/generators/test-generator/recipe-expander.js.map +1 -0
  60. package/dist/generators/test-generator/step-mapper.d.ts +17 -0
  61. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  62. package/dist/generators/test-generator/step-mapper.js +70 -0
  63. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  64. package/dist/generators/test-generator/template-engine.d.ts +5 -1
  65. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  66. package/dist/generators/test-generator/template-engine.js +27 -3
  67. package/dist/generators/test-generator/template-engine.js.map +1 -1
  68. package/dist/generators/test-generator/utils/selector-resolver.d.ts +10 -1
  69. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  70. package/dist/generators/test-generator/utils/selector-resolver.js +32 -0
  71. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  72. package/dist/harness/catalog/drivers.yaml +1 -1
  73. package/dist/harness/query-catalog.d.ts +32 -2
  74. package/dist/harness/query-catalog.d.ts.map +1 -1
  75. package/dist/harness/query-catalog.js +0 -0
  76. package/dist/harness/query-catalog.js.map +1 -1
  77. package/dist/index.d.ts +5 -2
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +5 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/orchestrator/templates/ai-src/commands/create-data-test.md +83 -0
  82. package/dist/orchestrator/templates/ai-src/config/claude.md +3 -1
  83. package/dist/orchestrator/templates/ai-src/config/copilot.md +3 -1
  84. package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +93 -0
  85. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
  86. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +43 -8
  87. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
  88. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
  89. package/dist/orchestrator/templates/specs-db.d.ts +201 -11
  90. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
  91. package/dist/orchestrator/templates/specs-db.js +440 -91
  92. package/dist/orchestrator/templates/specs-db.js.map +1 -1
  93. package/dist/orchestrator/templates/specs-db.ts +463 -79
  94. package/dist/orchestrator/templates/specs-test-data.ts +39 -3
  95. package/package.json +7 -4
  96. package/src/capabilities/discover.ts +1 -1
  97. package/src/capabilities/registry.ts +13 -0
  98. package/src/cli/commands/delivery.ts +13 -5
  99. package/src/cli/commands/generate.ts +15 -0
  100. package/src/exporters/spec-parser.ts +17 -5
  101. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  102. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  103. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  104. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-row-column.hbs +2 -0
  105. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  106. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
  107. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
  108. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
  109. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
  110. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
  111. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
  112. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
  113. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
  114. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
  115. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
  116. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
  117. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
  118. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
  119. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
  120. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
  121. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
  122. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
  123. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
  124. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
  125. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
  126. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
  127. package/src/generators/test-generator/app-profile.ts +185 -0
  128. package/src/generators/test-generator/code-generator.ts +9 -0
  129. package/src/generators/test-generator/diagnostics.ts +51 -0
  130. package/src/generators/test-generator/patterns/types.ts +12 -0
  131. package/src/generators/test-generator/recipe-expander.ts +129 -0
  132. package/src/generators/test-generator/step-mapper.ts +73 -0
  133. package/src/generators/test-generator/template-engine.ts +29 -3
  134. package/src/generators/test-generator/utils/selector-resolver.ts +49 -2
  135. package/src/harness/catalog/drivers.yaml +1 -1
  136. package/src/harness/query-catalog.ts +0 -0
  137. package/src/index.ts +5 -2
  138. package/src/orchestrator/templates/ai-src/commands/create-data-test.md +83 -0
  139. package/src/orchestrator/templates/ai-src/config/claude.md +3 -1
  140. package/src/orchestrator/templates/ai-src/config/copilot.md +3 -1
  141. package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +93 -0
  142. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
  143. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +43 -8
  144. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
  145. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
  146. package/src/orchestrator/templates/specs-db.ts +463 -79
  147. package/src/orchestrator/templates/specs-test-data.ts +39 -3
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Compile-time diagnostics — the warning channel for "the compiler knows something
3
+ * and would otherwise silently discard or invent it" (spec: sungen_app_profile_spec.md §4).
4
+ *
5
+ * Codes:
6
+ * SG-W001 — step matched no pattern (compiles to a no-op `// TODO` comment)
7
+ * SG-W002 — step parsed a {{dataRef}} value but the generated code never uses it
8
+ * SG-W003 — `name` filter defaulted from the Gherkin reference on a name-from-content
9
+ * role (status/alert/…) — such a filter can never match; opt out with `name: null`
10
+ * SG-W005 — qa/app.yaml problem (unknown key, unknown preset, unparseable)
11
+ * SG-W006 — qa/app.<env>.yaml found (per-environment profiles are unsupported by design)
12
+ * SG-W007 — browser-alert handler step written AFTER an action step: the listener
13
+ * registers too late, an earlier dialog auto-dismisses with no error
14
+ *
15
+ * Diagnostics never block generation by themselves; `sungen generate --strict`
16
+ * turns any collected diagnostic into a non-zero exit.
17
+ */
18
+
19
+ export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007';
20
+
21
+ export interface Diagnostic {
22
+ code: DiagnosticCode;
23
+ message: string;
24
+ feature?: string;
25
+ step?: string;
26
+ }
27
+
28
+ export class DiagnosticsCollector {
29
+ private items: Diagnostic[] = [];
30
+
31
+ add(code: DiagnosticCode, message: string, context: { feature?: string; step?: string } = {}): void {
32
+ this.items.push({ code, message, ...context });
33
+ }
34
+
35
+ all(): Diagnostic[] {
36
+ return this.items;
37
+ }
38
+
39
+ get count(): number {
40
+ return this.items.length;
41
+ }
42
+
43
+ /** One line per diagnostic, ready for CLI output. */
44
+ formatLines(): string[] {
45
+ return this.items.map((d) => {
46
+ const where = d.feature ? `${d.feature}: ` : '';
47
+ const step = d.step ? ` — "${d.step}"` : '';
48
+ return `[${d.code}] ${where}${d.message}${step}`;
49
+ });
50
+ }
51
+ }
@@ -44,6 +44,18 @@ export interface PatternContext {
44
44
  scenarioSteps?: ParsedStep[];
45
45
  /** Effective Gherkin keyword (Given/When/Then), resolved from And/But */
46
46
  effectiveKeyword?: string;
47
+ /** App profile (qa/app.yaml merged over presets/defaults) — widget variants, recipes,
48
+ * loading indicator, table row conventions. Typed loosely to keep the SPI decoupled;
49
+ * the concrete shape is ResolvedAppProfile. */
50
+ appProfile?: {
51
+ widgetVariants: Record<string, string>;
52
+ recipes: Record<string, Record<string, unknown[]>>;
53
+ loadingIndicator: string;
54
+ tableRowBase: number;
55
+ tableHeaderIsRow: boolean;
56
+ portalRoles: string[];
57
+ contentNamedRoles: string[];
58
+ };
47
59
  }
48
60
 
49
61
  /**
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Widget-recipe expander (spec: sungen_app_profile_spec.md §3.1 [recipes]).
3
+ *
4
+ * A recipe is a LINEAR sequence of closed primitives that describes one widget
5
+ * intent ("select", "fill", …). Recipes come from `qa/app.yaml` `recipes:` or from
6
+ * driver-shipped built-ins (the reserved `builtin` preset), selected by the widget's
7
+ * variant name (per-element `variant:` > profile `widgets.<kind>`).
8
+ *
9
+ * This expander holds ZERO framework syntax. It resolves each primitive to a
10
+ * `{ partial, ...data }` NODE; the actual Playwright code lives in the per-primitive
11
+ * `recipe-*.hbs` partials, rendered by the template engine via `recipe-sequence.hbs`
12
+ * (dynamic partial lookup). The only strings it builds are locator/value EXPRESSIONS
13
+ * (data — the same split every resolver uses when it hands the locator template a value).
14
+ *
15
+ * Placeholders: `$trigger` = the resolved widget element; `$value` = the step's data
16
+ * value (a runtime marker in runtime mode, transformed to testData.get later); `$item`
17
+ * = the current forEach item. Throws on an unknown primitive/shape — callers fall back
18
+ * to the generic template.
19
+ */
20
+
21
+ export interface RecipeExpandContext {
22
+ /** Pre-rendered locator code for the widget element (dialog/frame scoping included). */
23
+ triggerCode: string;
24
+ /** The step's value — a literal or a __SUNGEN_TD__ runtime marker. */
25
+ value: string;
26
+ }
27
+
28
+ /** A render node: the partial name plus the data it interpolates. */
29
+ export interface RecipeNode {
30
+ partial: string;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ const escape = (s: string): string => String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
35
+
36
+ export function expandRecipe(primitives: unknown[], ctx: RecipeExpandContext): RecipeNode[] {
37
+ if (!Array.isArray(primitives) || primitives.length === 0) {
38
+ throw new Error('recipe intent is not a primitive list');
39
+ }
40
+ return primitives.map((p) => expandPrimitive(p, ctx, null));
41
+ }
42
+
43
+ /** `itemVar` is non-null inside a forEach body — `$item` renders as that identifier. */
44
+ function expandPrimitive(prim: unknown, ctx: RecipeExpandContext, itemVar: string | null): RecipeNode {
45
+ if (typeof prim !== 'object' || prim === null || Array.isArray(prim)) {
46
+ throw new Error(`invalid primitive: ${JSON.stringify(prim)}`);
47
+ }
48
+ const entries = Object.entries(prim as Record<string, unknown>);
49
+ if (entries.length !== 1) throw new Error(`a primitive must have exactly one key: ${JSON.stringify(prim)}`);
50
+ const [kind, arg] = entries[0];
51
+
52
+ // Resolve a `$trigger`/css target into a locator EXPRESSION (data for the partial).
53
+ const targetExpr = (v: unknown): string =>
54
+ v === '$trigger' ? ctx.triggerCode : `page.locator('${escape(String(v))}')`;
55
+ // Resolve a `$value`/`$item`/literal into a JS value EXPRESSION (quoted unless $item;
56
+ // a quoted __SUNGEN_TD__ marker is rewritten to testData.get(...) by the runtime transform).
57
+ const valueExpr = (v: unknown): string => {
58
+ if (v === '$item') {
59
+ if (!itemVar) throw new Error('$item is only valid inside forEach');
60
+ return itemVar;
61
+ }
62
+ const s = v === '$value' ? ctx.value : String(v);
63
+ return `'${escape(s)}'`;
64
+ };
65
+
66
+ switch (kind) {
67
+ case 'click':
68
+ return { partial: 'recipe-click', targetExpr: targetExpr(arg) };
69
+ case 'hover':
70
+ return { partial: 'recipe-hover', targetExpr: targetExpr(arg) };
71
+ case 'type': {
72
+ const v = typeof arg === 'object' && arg !== null ? (arg as any).value : arg;
73
+ return { partial: 'recipe-type', valueExpr: valueExpr(v ?? '$value') };
74
+ }
75
+ case 'press':
76
+ return { partial: 'recipe-press', key: escape(String(arg)) };
77
+ case 'waitFor': {
78
+ // `waitFor: '<css>'` → wait visible. `waitFor: { target, state }` allows an explicit
79
+ // state ('attached' | 'visible' | 'hidden' | 'detached'); `attached` covers the AntD
80
+ // case where a selected chip collapses the input to ~0px (node exists before actionable).
81
+ const o = (typeof arg === 'object' && arg !== null ? arg : { target: arg }) as Record<string, unknown>;
82
+ const state = ['attached', 'visible', 'hidden', 'detached'].includes(String(o.state)) ? String(o.state) : 'visible';
83
+ return { partial: 'recipe-wait-for', targetExpr: targetExpr(o.target), state };
84
+ }
85
+ case 'waitForHidden':
86
+ return { partial: 'recipe-wait-for', targetExpr: targetExpr(arg), state: 'hidden' };
87
+ case 'scrollIntoView':
88
+ // Playwright's own actionability scroll (not arbitrary evaluate) — nudges a lazy list
89
+ // to render+reveal the target before a click. Deterministic, no loop.
90
+ return { partial: 'recipe-scroll-into-view', targetExpr: targetExpr(arg) };
91
+ case 'blur':
92
+ // One bounded el.blur() to dismiss a widget's own overlay (AntD DatePicker panel keeps
93
+ // pointer-blocking after fill; Escape/force-click are unreliable). No retry loop.
94
+ return { partial: 'recipe-blur', targetExpr: targetExpr(arg) };
95
+ case 'clickText': {
96
+ const o = (typeof arg === 'object' && arg !== null ? arg : { text: arg }) as Record<string, unknown>;
97
+ const text = valueExpr(o.text ?? '$value');
98
+ const exact = o.exact === true;
99
+ // Build the option-locator expression; the partial scrolls it into view then clicks.
100
+ const expr = o.role
101
+ ? `page.getByRole('${escape(String(o.role))}', { name: ${text}${exact ? ', exact: true' : ''} })`
102
+ : `${o.within ? `page.locator('${escape(String(o.within))}')` : 'page'}.getByText(${text}${exact ? ', { exact: true }' : ''}).first()`;
103
+ return { partial: 'recipe-click-text', targetExpr: expr };
104
+ }
105
+ case 'expectText': {
106
+ const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
107
+ return { partial: 'recipe-expect-text', targetExpr: targetExpr(o.locator ?? '$trigger'), valueExpr: valueExpr(o.value ?? '$value') };
108
+ }
109
+ case 'expectValue': {
110
+ const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
111
+ return { partial: 'recipe-expect-value', targetExpr: targetExpr(o.locator ?? '$trigger'), valueExpr: valueExpr(o.value ?? '$value') };
112
+ }
113
+ case 'forEach': {
114
+ const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
115
+ if (itemVar) throw new Error('nested forEach is not supported');
116
+ const inner = o.do;
117
+ if (!Array.isArray(inner) || inner.length === 0) throw new Error('forEach.do must be a primitive list');
118
+ const source = o.in === '$values' || o.in === undefined ? '$value' : o.in;
119
+ return {
120
+ partial: 'recipe-for-each',
121
+ sourceExpr: valueExpr(source),
122
+ split: escape(String(o.split ?? '>')),
123
+ body: inner.map((p) => expandPrimitive(p, ctx, 'item')),
124
+ };
125
+ }
126
+ default:
127
+ throw new Error(`unknown recipe primitive "${kind}"`);
128
+ }
129
+ }
@@ -4,6 +4,9 @@ import { adapterRegistry } from './adapters';
4
4
  import { PatternRegistry, PatternContext } from './patterns';
5
5
  import { SelectorResolver } from './utils/selector-resolver';
6
6
  import { DataResolver } from './utils/data-resolver';
7
+ import { DiagnosticsCollector } from './diagnostics';
8
+ import { loadAppProfile, ResolvedAppProfile } from './app-profile';
9
+ import { capabilityRegistry } from '../../capabilities/registry';
7
10
 
8
11
  export interface MappedStep {
9
12
  code: string; // Generated Playwright code
@@ -34,6 +37,12 @@ export class StepMapper {
34
37
  /** Row scope: when active, column assertions are scoped to this row */
35
38
  private inRowScope: boolean = false;
36
39
  private rowScopeTable: string = '';
40
+ /** SG-W007: an action step already ran in this scenario (alert handlers registered after it are too late) */
41
+ private sawActionStep: boolean = false;
42
+ /** Compile-time warning channel (SG-W001/W002); optional — absent means silent legacy behavior */
43
+ private diagnostics?: DiagnosticsCollector;
44
+ /** App profile (qa/app.yaml merged over presets/defaults) — the per-project BEHAVIOR layer */
45
+ private appProfile: ResolvedAppProfile;
37
46
 
38
47
  constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean; framework?: string } = {}) {
39
48
  this.verbose = options.verbose ?? false;
@@ -51,11 +60,26 @@ export class StepMapper {
51
60
  this.selectorResolver = new SelectorResolver(undefined, options.screenName);
52
61
  this.dataResolver = new DataResolver(undefined, options.screenName, options.runtimeData);
53
62
 
63
+ // App profile (qa/app.yaml) — loaded after PatternRegistry so capability discovery has
64
+ // populated the preset registry (driver-ui ships native/radix/antd). Missing file →
65
+ // built-in defaults → pre-profile behavior.
66
+ this.appProfile = loadAppProfile(process.cwd(), capabilityRegistry.appProfilePresets());
67
+ this.templateEngine.setPersistentContext({ portalRoles: this.appProfile.portalRoles });
68
+ this.selectorResolver.setContentNamedRoles(this.appProfile.contentNamedRoles);
69
+
54
70
  if (this.verbose) {
55
71
  console.log(` [StepMapper] ${this.patternRegistry.getPatternCount()} patterns loaded`);
56
72
  }
57
73
  }
58
74
 
75
+ /** Attach the compile-diagnostics collector (owned by CodeGenerator) */
76
+ setDiagnostics(collector: DiagnosticsCollector): void {
77
+ this.diagnostics = collector;
78
+ this.selectorResolver.setDiagnostics(collector);
79
+ // Flush profile-load warnings (SG-W005/W006) gathered before the collector existed.
80
+ for (const w of this.appProfile.warnings) collector.add(w.code, w.message);
81
+ }
82
+
59
83
  /**
60
84
  * Set feature context for data and selector resolution
61
85
  */
@@ -92,6 +116,7 @@ export class StepMapper {
92
116
  this.inFrameScope = false;
93
117
  this.inRowScope = false;
94
118
  this.rowScopeTable = '';
119
+ this.sawActionStep = false;
95
120
  this.lastPrimaryKeyword = 'Given';
96
121
  this.dataResolver.clearCaptured(); // captured vars are scenario-scoped
97
122
  this.templateEngine.resetBaseContext();
@@ -119,6 +144,23 @@ export class StepMapper {
119
144
  this.lastPrimaryKeyword = keyword;
120
145
  }
121
146
 
147
+ // SG-W007 — a two-step-form browser-alert handler (`click [OK] alert`) written AFTER
148
+ // an action step registers its page.once('dialog') listener too late: a dialog
149
+ // triggered by the earlier action auto-dismisses with no error (issue #472).
150
+ // Fix: move the alert step BEFORE the trigger, or use the compound form
151
+ // `click [X] button and accept [OK] alert`.
152
+ const isAlertHandlerStep = step.elementType === 'alert' &&
153
+ /\b(click|clicks|fill|fills)\b/.test(step.text) &&
154
+ !/\band\s+(accept|accepts|dismiss|dismisses)\b/i.test(step.text);
155
+ if (isAlertHandlerStep && this.sawActionStep) {
156
+ this.diagnostics?.add('SG-W007',
157
+ 'browser-alert handler registered AFTER an action step — a dialog triggered earlier auto-dismisses silently. Move the alert step before the trigger, or use `click [X] button and accept [OK] alert`',
158
+ { feature: this.featureName, step: step.text });
159
+ } else if (!isAlertHandlerStep &&
160
+ /\b(click|clicks|fill|fills|select|selects|press|presses|check|checks|uncheck|unchecks|drag|drags)\b/.test(step.text)) {
161
+ this.sawActionStep = true;
162
+ }
163
+
122
164
  // Frame scope directives — intercept before pattern matching
123
165
  if (/\bswitch(?:es)?\s+to\b/i.test(step.text) &&
124
166
  (step.elementType === 'frame' || step.elementType === 'iframe')) {
@@ -215,16 +257,46 @@ export class StepMapper {
215
257
  if (this.verbose) {
216
258
  console.log(` ✓ Pattern matched: ${step.text.substring(0, 50)}...`);
217
259
  }
260
+ this.checkDataRefConsumed(step, mappedStep);
218
261
  return mappedStep;
219
262
  }
220
263
 
221
264
  // No pattern matched - return TODO comment
265
+ this.diagnostics?.add('SG-W001', 'step matched no pattern — compiled to a no-op // TODO comment', {
266
+ feature: this.featureName,
267
+ step: step.text,
268
+ });
222
269
  return {
223
270
  code: `// TODO: Implement step: ${step.text}`,
224
271
  comment: `Unrecognized step pattern: ${step.text}`,
225
272
  };
226
273
  }
227
274
 
275
+ /**
276
+ * SG-W002 — a step carried a parsed {{dataRef}} but the matched pattern's generated
277
+ * code never uses it (neither the runtime marker, the ${ref} fallback, nor the
278
+ * inline-resolved value). This is the class of bug where `with {{v}}` silently
279
+ * disappears from the output (e.g. issue #464). Warning-only: loose containment
280
+ * checks err on the side of NOT flagging.
281
+ */
282
+ private checkDataRefConsumed(step: ParsedStep, mapped: MappedStep): void {
283
+ if (!step.dataRef || !this.diagnostics) return;
284
+ const code = mapped.code || '';
285
+ if (code.includes(DataResolver.encodeMarker(step.dataRef))) return; // runtime marker
286
+ if (code.includes(`\${${step.dataRef}}`)) return; // unresolved fallback
287
+ if (code.includes(step.dataRef)) return; // testData.set('ref'), comments
288
+ try {
289
+ const value = this.dataResolver.resolveData(step.dataRef, this.featureName);
290
+ if (value && (code.includes(value) || code.includes(value.replace(/'/g, "\\'")))) return; // inline mode
291
+ } catch {
292
+ return; // unresolvable ref → other errors surface elsewhere; don't double-report
293
+ }
294
+ this.diagnostics.add('SG-W002', `parsed {{${step.dataRef}}} but the generated code does not use it`, {
295
+ feature: this.featureName,
296
+ step: step.text,
297
+ });
298
+ }
299
+
228
300
  /**
229
301
  * Generate column assertion scoped to the current row.
230
302
  * Uses columns config for exact cell targeting (nth), or filter fallback.
@@ -296,6 +368,7 @@ export class StepMapper {
296
368
  selectorResolver: this.selectorResolver,
297
369
  dataResolver: this.dataResolver,
298
370
  renderLocator: (resolved) => this.templateEngine.renderStep('locator', resolved),
371
+ appProfile: this.appProfile,
299
372
  baseURL: this.baseURL,
300
373
  featureName: this.featureName,
301
374
  screenName: this.screenName,
@@ -7,6 +7,8 @@ export class TemplateEngine {
7
7
  private templatesDir: string;
8
8
  private stepsTemplatesDir: string;
9
9
  private baseContext: Record<string, any> = {};
10
+ /** Per-project context that survives scenario resets (e.g. app-profile portalRoles) */
11
+ private persistentContext: Record<string, any> = {};
10
12
 
11
13
  constructor(templatesDir: string) {
12
14
  this.templatesDir = templatesDir;
@@ -127,7 +129,17 @@ export class TemplateEngine {
127
129
  Handlebars.registerHelper('pageRoot', function(this: any) {
128
130
  // Frame scope takes precedence (frame > dialog > page)
129
131
  if (this.inFrame) return 'frame';
130
- if (!this.inDialog) return 'page';
132
+ // `scope: dialog` on the selector entry is the explicit opt-IN sentinel — the
133
+ // element is dialog-rooted even if its role is portal-exempt (issue #465).
134
+ const explicitDialog = this.scope === 'dialog';
135
+ if (!this.inDialog && !explicitDialog) return 'page';
136
+ // Portal-rendered roles (Radix/MUI/Headless UI/antd render option/menu/tooltip
137
+ // layers onto document.body) live OUTSIDE the dialog's DOM subtree — wrapping
138
+ // them in the dialog root guarantees a timeout. List comes from the app profile
139
+ // (qa/app.yaml layers.portal_roles), default DEFAULT_PORTAL_ROLES.
140
+ if (!explicitDialog && this.role && Array.isArray(this.portalRoles) && this.portalRoles.includes(this.role)) {
141
+ return 'page';
142
+ }
131
143
  const partial = Handlebars.partials['dialog-root'];
132
144
  if (partial) {
133
145
  const fn = typeof partial === 'string' ? Handlebars.compile(partial) : partial;
@@ -152,6 +164,14 @@ export class TemplateEngine {
152
164
  Handlebars.registerPartial('locator-base', locatorBaseContent);
153
165
  }
154
166
 
167
+ // Register has-text-value partial — the ONE place that renders a .filter({ hasText })
168
+ // value, so action templates cannot drift from the exact/anchored-regex standard the
169
+ // assertion templates follow (issue #466).
170
+ const hasTextValuePath = path.join(this.stepsTemplatesDir, 'partials', 'has-text-value.hbs');
171
+ if (fs.existsSync(hasTextValuePath)) {
172
+ Handlebars.registerPartial('has-text-value', fs.readFileSync(hasTextValuePath, 'utf-8'));
173
+ }
174
+
155
175
  // Register locator strategy partials
156
176
  const strategiesDir = path.join(this.stepsTemplatesDir, 'partials', 'locator-strategies');
157
177
  if (fs.existsSync(strategiesDir)) {
@@ -246,9 +266,15 @@ export class TemplateEngine {
246
266
  this.baseContext = { ...this.baseContext, ...ctx };
247
267
  }
248
268
 
249
- /** Reset base context (e.g., on new scenario) */
269
+ /** Context that survives resetBaseContext (per-project facts, e.g. app-profile portalRoles) */
270
+ setPersistentContext(ctx: Record<string, any>): void {
271
+ this.persistentContext = { ...this.persistentContext, ...ctx };
272
+ this.baseContext = { ...this.baseContext, ...ctx };
273
+ }
274
+
275
+ /** Reset base context (e.g., on new scenario) — persistent (per-project) context stays */
250
276
  resetBaseContext(): void {
251
- this.baseContext = {};
277
+ this.baseContext = { ...this.persistentContext };
252
278
  }
253
279
 
254
280
  renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
@@ -5,6 +5,8 @@ import { readYaml, readYamlIfExists } from '../../../utils/yaml-io';
5
5
 
6
6
  import { SelectorType } from '../../../utils/selector-types';
7
7
  import { DataResolver } from './data-resolver';
8
+ import { DiagnosticsCollector } from '../diagnostics';
9
+ import { DEFAULT_CONTENT_NAMED_ROLES } from '../app-profile';
8
10
 
9
11
  // Structured selector format v2
10
12
  interface SelectorEntry {
@@ -27,7 +29,9 @@ interface SelectorEntry {
27
29
  ios?: SelectorEntry;
28
30
 
29
31
  // === v2: Complex Elements ===
30
- variant?: 'native' | 'custom' | 'dragdrop'; // Dropdown/upload subtype
32
+ // Dropdown/upload subtype: 'native' | 'custom' | 'dragdrop', or a widget-recipe/variant
33
+ // name ('radix-select', 'office-datepicker', …) — wins over qa/app.yaml widgets.<kind>.
34
+ variant?: string;
31
35
  trigger?: string; // Visible button that triggers hidden file input
32
36
  frame?: string; // iframe selector for cross-frame elements
33
37
  contenteditable?: boolean; // Rich text editor (contenteditable div)
@@ -96,7 +100,7 @@ export interface ResolvedSelector {
96
100
  // === v2 fields ===
97
101
  scope?: string; // Parent landmark to scope within
98
102
  match?: 'exact' | 'partial'; // For getByText exact matching
99
- variant?: 'native' | 'custom' | 'dragdrop'; // Dropdown/upload type
103
+ variant?: string; // Dropdown/upload subtype or widget-recipe name
100
104
  trigger?: string; // Button that triggers hidden file input
101
105
  frame?: string; // iframe selector
102
106
  contenteditable?: boolean; // Rich text editor
@@ -117,12 +121,26 @@ export class SelectorResolver {
117
121
  private featureName?: string;
118
122
  private screenName?: string;
119
123
  private flowMode: boolean = false;
124
+ /** SG-W003 channel + config: roles whose accessible name derives from content. */
125
+ private diagnostics?: DiagnosticsCollector;
126
+ private contentNamedRoles: string[] = DEFAULT_CONTENT_NAMED_ROLES;
127
+ private warnedContentNamed = new Set<string>();
120
128
 
121
129
  constructor(selectorsDir?: string, screenName?: string) {
122
130
  this.selectorsDir = selectorsDir || path.join(process.cwd(), 'qa', 'selectors', 'screens');
123
131
  this.screenName = screenName;
124
132
  }
125
133
 
134
+ /** Attach the compile-diagnostics collector (SG-W003) */
135
+ setDiagnostics(collector: DiagnosticsCollector): void {
136
+ this.diagnostics = collector;
137
+ }
138
+
139
+ /** Override the name-from-content role list from the app profile (qa/app.yaml feedback.toast) */
140
+ setContentNamedRoles(roles: string[]): void {
141
+ this.contentNamedRoles = roles;
142
+ }
143
+
126
144
  setFlowMode(enabled: boolean): void {
127
145
  this.flowMode = enabled;
128
146
  }
@@ -390,6 +408,20 @@ export class SelectorResolver {
390
408
  if (elementType) {
391
409
  const inferred = SelectorResolver.inferFromElementType(elementType, displayLabel);
392
410
  if (inferred) {
411
+ // Same SG-W003 class as the explicit-entry path: an auto-inferred name on a
412
+ // name-from-content role ([Loading] spinner → getByRole('status', { name: 'Loading' }))
413
+ // can never match. Add a YAML entry with name: null to opt out.
414
+ if (inferred.strategy === 'role' && inferred.name && inferred.role && this.contentNamedRoles.includes(inferred.role)) {
415
+ const warnKey = `${inferred.role}:${displayLabel}`;
416
+ if (!this.warnedContentNamed.has(warnKey)) {
417
+ this.warnedContentNamed.add(warnKey);
418
+ this.diagnostics?.add(
419
+ 'SG-W003',
420
+ `role "${inferred.role}" derives its accessible name from content, but "[${displayLabel}]" auto-inferred { name: '${inferred.name}' } which will likely never match. Add a selectors.yaml entry with an explicit name: or name: null`,
421
+ { feature: this.featureName }
422
+ );
423
+ }
424
+ }
393
425
  return inferred;
394
426
  }
395
427
  }
@@ -566,6 +598,21 @@ export class SelectorResolver {
566
598
  });
567
599
 
568
600
  case 'role':
601
+ // SG-W003 — the entry has no `name:` so the compiler defaulted it to the Gherkin
602
+ // reference text. For name-from-content roles (status/alert/…) that filter can
603
+ // never match the live element (its accessible name is computed from its content,
604
+ // e.g. a toast's message text) — issue #467. Opt out with `name: null`.
605
+ if (entry.name === undefined && name && this.contentNamedRoles.includes(value)) {
606
+ const warnKey = `${value}:${originalLabel}`;
607
+ if (!this.warnedContentNamed.has(warnKey)) {
608
+ this.warnedContentNamed.add(warnKey);
609
+ this.diagnostics?.add(
610
+ 'SG-W003',
611
+ `role "${value}" derives its accessible name from content, but the entry for "${originalLabel}" has no name: — the defaulted { name: '${name}' } will likely never match. Set an explicit name: or opt out with name: null`,
612
+ { feature: this.featureName }
613
+ );
614
+ }
615
+ }
569
616
  return withExtras({
570
617
  strategy: 'role',
571
618
  role: value,
@@ -51,7 +51,7 @@ drivers:
51
51
  data-factory:
52
52
  kind: capability
53
53
  package: "@sungen/driver-data-factory"
54
- status: planned
54
+ status: shipped
55
55
  capabilities: ["@dataFactory"]
56
56
  unblocks: [M1]
57
57
  mock:
Binary file
package/src/index.ts CHANGED
@@ -14,6 +14,9 @@ export type { DiscoveredContext } from './orchestrator/context-discovery';
14
14
 
15
15
  // --- Step-pattern authoring (a driver contributes step patterns via its descriptor) ---
16
16
  export type { PatternContext, StepPattern, StepTemplateData } from './generators/test-generator/patterns/types';
17
+ export { expandRecipe } from './generators/test-generator/recipe-expander';
18
+ export type { RecipeExpandContext, RecipeNode } from './generators/test-generator/recipe-expander';
19
+ export type { ResolvedAppProfile } from './generators/test-generator/app-profile';
17
20
  export type { MappedStep } from './generators/test-generator/step-mapper';
18
21
  export type { ParsedStep } from './generators/gherkin-parser';
19
22
  export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
@@ -22,8 +25,8 @@ export { getPathCode, inferPath, resolvePathVariables } from './generators/test-
22
25
  export { parseQueryOverrides } from './harness/annotation-overrides';
23
26
 
24
27
  // --- Named-query catalog (shared: the DB driver's codegen + core's data-driven advisory lint) ---
25
- export { resolveQuery, compileQuery, lintCatalog } from './harness/query-catalog';
26
- export type { QueryEntry } from './harness/query-catalog';
28
+ export { resolveQuery, compileQuery, compileShape, isShapeEntry, lintCatalog } from './harness/query-catalog';
29
+ export type { QueryEntry, MongoFind, MongoPipeline } from './harness/query-catalog';
27
30
 
28
31
  // --- Shared harness: viewpoint catalog + coverage gate / assertion depth ---
29
32
  // (the UI capability's gateProvider composes these; they also back core's ingest + audit fallback)
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: create-data-test
3
+ description: "Generate standardized test-data for a screen/flow from the Data Factory catalog — valid/boundary/invalid values with CHK-* traceability (no free-form guessing)"
4
+ argument-hint: [screen-or-flow-name]
5
+ order: 25
6
+ claude-tools: "Read, Grep, Bash, Glob, Write, Edit, AskUserQuestion, Skill"
7
+ copilot-tools: "[vscode, execute, read, agent, edit, search, todo]"
8
+ codex-trigger: "Run when the user asks to CREATE, generate, or standardize TEST DATA / test values for a screen or flow (valid/boundary/invalid, boundary values, negative data). Uses the Data Factory driver. Not for generating the .feature scenarios themselves (that is create-test)."
9
+ ---
10
+
11
+ **Input**: A screen or flow name (e.g., `/sungen:create-data-test login`), OR **no name** to do every unit (with a confirmation — see ALL mode). Load the `sungen-data-factory` skill for the full method.
12
+
13
+ ## Role
14
+
15
+ You are a **Senior QA Engineer** producing **test data to a standard**, not by guessing. You do NOT invent values field-by-field. You follow the 4-source method (spec rules → field-type catalog → design technique → DB mapping), driven by the **Data Factory** capability driver. Values come from the catalog (`common/` + the project's `.overwrite`), each tagged with a `CHK-*` id that traces back to the spec. Where the catalog cannot cover a project-specific field, you **ask the QA** — you never fabricate domain data.
16
+
17
+ ## Preconditions
18
+
19
+ 1. **Capability present** — check `qa/capabilities.yaml`. If `data-factory` is not enabled, tell the user and run `sungen capability add data-factory` (it is runtime-free — safe to enable). Then continue.
20
+ 2. Determine mode from `$ARGUMENTS`:
21
+ - A **name** given → single-unit mode (that screen/flow only).
22
+ - **No name** → **ALL mode** (see below — always confirm first).
23
+
24
+ ## ALL mode (no name) — confirm before doing everything
25
+
26
+ If no name was given, the user wants **all** screens/flows — but this is wide, and a missing name is often a typo (they meant `create-data-test home`). So:
27
+
28
+ 1. List every unit under `qa/screens/*` and `qa/flows/*`, and classify each as **new** (no `test-data/<name>.yaml` yet) vs **would-overwrite**.
29
+ 2. Show the list and use `AskUserQuestion` to confirm: *"Generate test-data for ALL N units? (X new · Y would overwrite)"* — default to **No**.
30
+ 3. Only proceed when the user confirms. Then run the per-unit flow below for each (the CLI `sungen data gen --all` also enforces this confirm; `--yes` after the user agrees).
31
+
32
+ ## Per-unit flow (single unit, or each unit in ALL mode)
33
+
34
+ Auto-detect: `qa/flows/<name>/` → flow; else `qa/screens/<name>/` → screen. (API units carry their contract in `apis.yaml` — data there is `@cases` in create-test, not this command.)
35
+
36
+ ### 1. Read the sources (Nguồn 1 + 4)
37
+
38
+ Read `qa/<screens|flows>/<name>/requirements/spec.md` and `test-viewpoint.md`. For **each input field / parameter**, extract:
39
+ - data **type** (map to a catalog field-type: email · password · phone · string · number-integer · number-float · currency · postcode · katakana …),
40
+ - `required`, format, **min/max** length or value,
41
+ - the real **error/message code** the spec shows when a rule is violated,
42
+ - any **DB mapping** (unique, cross-entity dependency) noted in the spec.
43
+
44
+ Viewpoint tells you which fields are high-priority → give them the fullest data.
45
+
46
+ ### 2. Write the field-map — `qa/data-factory/<name>.fields.yaml`
47
+
48
+ This is the input the deterministic generator expands. One entry per field:
49
+
50
+ ```yaml
51
+ screen: <name> # or: flow: <name>
52
+ fields:
53
+ - name: email
54
+ type: email
55
+ required: true
56
+ constraints: { maxLocal: 64, maxTotal: 254 } # project's real limits
57
+ errorMap: { M13: E1042, M02: E1001 } # catalog placeholder → project's real code
58
+ - name: password
59
+ type: password
60
+ constraints: { minLength: 8, maxLength: 32 }
61
+ ```
62
+
63
+ Map every placeholder error code you can (`errorMap`) to the spec's real code. Leave a code unmapped only if the spec truly has none — the lint will flag it as an open point to confirm with BA/Dev.
64
+
65
+ ### 3. Blind spots — ask, do not invent
66
+
67
+ If a field's type is **not** in the catalog (a business enum, a cross-entity constraint, a project data model), **stop and ask the QA** with `AskUserQuestion` for: (1) the valid set, (2) invalid / non-existent values, (3) the error code when wrong, (4) dependencies on other fields. Record the answer in `qa/data-factory/project/<entity>.yaml` for reuse. Never fabricate domain values.
68
+
69
+ ### 4. Lint the coverage
70
+
71
+ Run (local-first): `[ -x ./bin/sungen.js ] && ./bin/sungen.js data lint --screen <name> || npx sungen data lint --screen <name>`. Fix what it reports: add missing boundary values, resolve `UNRESOLVED` items (supply the constraint), map `OPEN_ERRORREF` codes. The bar is the phuong-phap §6 checklist — every required field has valid + boundary + invalid, one invalid per rule.
72
+
73
+ ### 5. Generate the test-data
74
+
75
+ Run (local-first): `[ -x ./bin/sungen.js ] && ./bin/sungen.js data gen --screen <name> || npx sungen data gen --screen <name>`. If `test-data/<name>.yaml` already exists it **asks before overwriting** — respect the user's choice (in ALL mode after they confirmed, add `--yes`; use `--force` only when the user explicitly wants an unconditional overwrite). The output is standardized `valid/boundary/invalid` values with `CHK-*` traceability.
76
+
77
+ ### 6. Weave into scenarios (if create-test already ran)
78
+
79
+ If the `.feature` exists, turn the generated invalid/boundary sets into data-driven `Scenario Outline` + `Examples` (`@cases`) referencing the values — following `sungen-gherkin-syntax`. Otherwise leave the standardized `test-data/<name>.yaml` for `/sungen:run-test` to consume.
80
+
81
+ ## After running
82
+
83
+ Use `AskUserQuestion` to offer the next step: review the generated data, run `/sungen:create-test <name>` (if scenarios don't exist yet), or `/sungen:run-test <name>`.
@@ -18,14 +18,16 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
18
18
  | `sungen-delivery` | Export Gherkin + Playwright results → CSV test case deliverable |
19
19
  | `sungen-capture` | Acquire visual/design context — one skill, 4 modes: figma-mcp (Dev Mode MCP), figma-pat (URL → spec_figma.md), live (Playwright MCP scan), local (images in `requirements/ui/`) |
20
20
  | `sungen-locale` | Bootstrap i18n — audit selectors, detect locale switch mechanism, generate test-data overlay |
21
+ | `sungen-data-factory` | Standardized test-data — field-type/security catalog, 4-source method, `.overwrite`, blind-spot prompts |
21
22
 
22
- ## Workflow (7 AI commands)
23
+ ## Workflow (8 AI commands)
23
24
 
24
25
  | Command | What it does |
25
26
  |---|---|
26
27
  | `/sungen:add-screen <name> <path>` | Scaffold `qa/screens/<name>/` directories |
27
28
  | `/sungen:add-flow <name> [--path <url>]` | Scaffold `qa/flows/<name>/` directories for E2E cross-screen testing |
28
29
  | `/sungen:create-test <name>` | Generate `.feature` + `test-data.yaml` (auto-detects screen or flow) |
30
+ | `/sungen:create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
29
31
  | `/sungen:review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
30
32
  | `/sungen:run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
31
33
  | `/sungen:delivery [name...]` | Export test cases → CSV for QA delivery (all screens if no arg) |
@@ -18,14 +18,16 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
18
18
  | `sungen-delivery` | Export Gherkin + Playwright results → CSV test case deliverable |
19
19
  | `sungen-capture` | Acquire visual/design context — one skill, 4 modes: figma-mcp (Dev Mode MCP), figma-pat (URL → spec_figma.md), live (Playwright MCP scan), local (images in `requirements/ui/`) |
20
20
  | `sungen-locale` | Bootstrap i18n — audit selectors, detect locale switch mechanism, generate test-data overlay |
21
+ | `sungen-data-factory` | Standardized test-data — field-type/security catalog, 4-source method, `.overwrite`, blind-spot prompts |
21
22
 
22
- ## Workflow (7 AI commands)
23
+ ## Workflow (8 AI commands)
23
24
 
24
25
  | Command | What it does |
25
26
  |---|---|
26
27
  | `/sungen-add-screen <name> <path>` | Scaffold `qa/screens/<name>/` directories |
27
28
  | `/sungen-add-flow <name> [--path <url>]` | Scaffold `qa/flows/<name>/` directories for E2E cross-screen testing |
28
29
  | `/sungen-create-test <name>` | Generate `.feature` + `test-data.yaml` (auto-detects screen or flow) |
30
+ | `/sungen-create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
29
31
  | `/sungen-review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
30
32
  | `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
31
33
  | `/sungen-delivery [name...]` | Export test cases → CSV for QA delivery (all screens if no arg) |