@sun-asterisk/sungen 2.2.2 → 2.3.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 (105) hide show
  1. package/README.md +66 -37
  2. package/dist/cli/commands/update.d.ts +3 -0
  3. package/dist/cli/commands/update.d.ts.map +1 -0
  4. package/dist/cli/commands/update.js +21 -0
  5. package/dist/cli/commands/update.js.map +1 -0
  6. package/dist/cli/index.js +3 -1
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  9. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  10. package/dist/generators/gherkin-parser/index.js +17 -3
  11. package/dist/generators/gherkin-parser/index.js.map +1 -1
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +3 -0
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +12 -1
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +12 -1
  20. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  21. package/dist/generators/test-generator/patterns/assertion-patterns.js +95 -57
  22. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  23. package/dist/generators/test-generator/patterns/index.d.ts +9 -0
  24. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  25. package/dist/generators/test-generator/patterns/index.js +32 -0
  26. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  27. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +1 -1
  28. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  29. package/dist/generators/test-generator/patterns/interaction-patterns.js +56 -1
  30. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  31. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +1 -1
  32. package/dist/generators/test-generator/patterns/table-patterns.js +8 -5
  33. package/dist/generators/test-generator/patterns/table-patterns.js.map +1 -1
  34. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  35. package/dist/generators/test-generator/utils/selector-resolver.js +16 -0
  36. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  37. package/dist/orchestrator/ai-rules-updater.d.ts +13 -0
  38. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -0
  39. package/dist/orchestrator/ai-rules-updater.js +157 -0
  40. package/dist/orchestrator/ai-rules-updater.js.map +1 -0
  41. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  42. package/dist/orchestrator/project-initializer.js +2 -27
  43. package/dist/orchestrator/project-initializer.js.map +1 -1
  44. package/dist/orchestrator/screen-manager.d.ts +1 -0
  45. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  46. package/dist/orchestrator/screen-manager.js +70 -3
  47. package/dist/orchestrator/screen-manager.js.map +1 -1
  48. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
  49. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -4
  50. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -6
  51. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
  52. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +170 -24
  53. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +118 -12
  54. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
  55. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +124 -71
  56. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -5
  57. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +12 -4
  58. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +11 -6
  59. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
  60. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +170 -24
  61. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +93 -12
  62. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
  63. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +124 -72
  64. package/dist/orchestrator/templates/readme.md +13 -8
  65. package/package.json +1 -1
  66. package/src/cli/commands/update.ts +18 -0
  67. package/src/cli/index.ts +3 -1
  68. package/src/generators/gherkin-parser/index.ts +20 -3
  69. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
  70. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
  71. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
  72. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
  73. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +3 -0
  74. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
  75. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +12 -1
  76. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +12 -1
  77. package/src/generators/test-generator/patterns/assertion-patterns.ts +106 -65
  78. package/src/generators/test-generator/patterns/index.ts +41 -0
  79. package/src/generators/test-generator/patterns/interaction-patterns.ts +58 -1
  80. package/src/generators/test-generator/patterns/table-patterns.ts +8 -5
  81. package/src/generators/test-generator/utils/selector-resolver.ts +16 -0
  82. package/src/orchestrator/ai-rules-updater.ts +139 -0
  83. package/src/orchestrator/project-initializer.ts +2 -32
  84. package/src/orchestrator/screen-manager.ts +72 -3
  85. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
  86. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -4
  87. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -6
  88. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
  89. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +170 -24
  90. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +118 -12
  91. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
  92. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +124 -71
  93. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -5
  94. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +12 -4
  95. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +11 -6
  96. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
  97. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +170 -24
  98. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +93 -12
  99. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
  100. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +124 -72
  101. package/src/orchestrator/templates/readme.md +13 -8
  102. package/docs/gherkin standards/gherkin-core-standard.md +0 -428
  103. package/docs/gherkin standards/gherkin-core-standard.vi.md +0 -513
  104. package/docs/gherkin-dictionary.md +0 -1071
  105. package/docs/makeauth.md +0 -225
@@ -6,6 +6,30 @@ import { StepPattern, StepTemplateData } from './types';
6
6
  * Uses template engine for framework-agnostic code generation
7
7
  */
8
8
  export const assertionPatterns: StepPattern[] = [
9
+ // Browser alert text assertion: "see [Are you sure?] alert"
10
+ {
11
+ name: 'alert-text-assertion',
12
+ matcher: (step: ParsedStep) =>
13
+ (step.text.includes('see') || step.text.includes('sees')) &&
14
+ step.elementType === 'alert' &&
15
+ !!step.selectorRef,
16
+ resolver: (step, context): StepTemplateData => {
17
+ let dataValue = step.selectorRef || '';
18
+ if (step.dataRef) {
19
+ try {
20
+ dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
21
+ } catch {
22
+ dataValue = `\${${step.dataRef}}`;
23
+ }
24
+ }
25
+ return {
26
+ templateName: 'alert-text-assertion',
27
+ data: { dataValue, stepCounter: context.stepCounter },
28
+ comment: `Assert browser alert contains "${step.selectorRef}"`,
29
+ };
30
+ },
31
+ priority: 21,
32
+ },
9
33
  // Column cell assertion: "see [Department] column 1 with {{value}}" -> check table cell text
10
34
  {
11
35
  name: 'column-cell-assertion',
@@ -218,6 +242,7 @@ export const assertionPatterns: StepPattern[] = [
218
242
  // Pattern: Then User see [error] message with {{fail_message}}
219
243
  // If selector YAML has empty value, uses variable-only matching
220
244
  // If selector has value, combines both (static text + variable)
245
+ // Input types → toHaveValue, everything else → toHaveText
221
246
  {
222
247
  name: 'see-with-variable',
223
248
  matcher: (step: ParsedStep) =>
@@ -227,6 +252,12 @@ export const assertionPatterns: StepPattern[] = [
227
252
  !!step.dataRef &&
228
253
  step.text.includes('with'),
229
254
  generator: (step, context) => {
255
+ // Input element types use toHaveValue instead of toHaveText
256
+ const INPUT_TYPES = new Set([
257
+ 'field', 'textarea', 'search', 'dropdown', 'slider', 'date-picker',
258
+ 'input', 'textbox', 'editor', 'select', 'combobox',
259
+ ]);
260
+
230
261
  // Resolve data variable value
231
262
  let dataValue: string;
232
263
  try {
@@ -235,19 +266,55 @@ export const assertionPatterns: StepPattern[] = [
235
266
  dataValue = `\${${step.dataRef}}`;
236
267
  }
237
268
 
238
- // Label-value pattern: "User see [X] label with {{Y}}"
239
- // Uses regex getByText to match container with both label and value text
269
+ // Resolve selector from YAML with feature context
270
+ let resolved: any = {};
271
+ try {
272
+ resolved = context.selectorResolver.resolveSelector(
273
+ step.selectorRef!,
274
+ context.featureName,
275
+ step.elementType,
276
+ step.nth
277
+ );
278
+ } catch (error) {
279
+ // Selector not in YAML or context issue - will use variable-only
280
+ }
281
+
282
+ // --- Attribute assertion: toHaveAttribute ---
283
+ // When selector YAML has `attribute` field (e.g., attribute: 'src', 'href')
284
+ if (resolved.attribute) {
285
+ const code = context.templateEngine.renderStep('attribute-assertion', {
286
+ ...resolved,
287
+ dataValue,
288
+ });
289
+ return {
290
+ code,
291
+ comment: `Assert ${step.selectorRef} ${resolved.attribute} matches ${step.dataRef}`,
292
+ };
293
+ }
294
+
295
+ // --- Input types: toHaveValue ---
296
+ if (step.elementType && INPUT_TYPES.has(step.elementType)) {
297
+ const code = context.templateEngine.renderStep('have-value-assertion', {
298
+ ...resolved,
299
+ dataValue,
300
+ });
301
+ return {
302
+ code,
303
+ comment: `Assert ${step.selectorRef} has value ${step.dataRef}`,
304
+ };
305
+ }
306
+
307
+ // --- Label-value pattern: "User see [X] label with {{Y}}" ---
240
308
  if (step.elementType === 'label' && step.dataRef) {
241
- // Check if selector override has empty value — if so, omit label from regex
242
309
  let label: string | undefined = step.selectorRef;
243
310
  try {
244
- const resolved = context.selectorResolver.resolveSelector(
311
+ const labelResolved = context.selectorResolver.resolveSelector(
245
312
  step.selectorRef!,
246
313
  context.featureName,
247
314
  step.elementType,
248
315
  step.nth
249
316
  );
250
- if (resolved.value !== undefined && resolved.value.trim() === '') {
317
+ if (labelResolved.value !== undefined && labelResolved.value.trim() === '') {
251
318
  label = undefined;
252
319
  }
253
320
  } catch {
@@ -264,93 +331,67 @@ export const assertionPatterns: StepPattern[] = [
264
331
  };
265
332
  }
266
333
 
267
- // Resolve selector from YAML with feature context
268
- let resolved: any = {};
269
- try {
270
- resolved = context.selectorResolver.resolveSelector(
271
- step.selectorRef!,
272
- context.featureName,
273
- step.elementType,
274
- step.nth
275
- );
276
- } catch (error) {
277
- // Selector not in YAML or context issue - will use variable-only
278
- }
279
-
280
- const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
281
-
282
- // Check if it's a locator strategy - use locator with filter
283
- if (resolved.strategy === 'locator') {
284
- const code = context.templateEngine.renderStep('visible-with-locator-variable-assertion', {
285
- ...resolved,
334
+ // --- Dialog role: check heading inside dialog ---
335
+ if (resolved.strategy === 'role' && resolved.role === 'dialog') {
336
+ const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
286
337
  dataValue,
287
- nth: resolved.nth,
288
338
  });
289
339
  return {
290
340
  code,
291
- comment: `Assert ${step.selectorRef} with value ${step.dataRef}`,
341
+ comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
292
342
  };
293
343
  }
294
344
 
295
- // Check if it's a role-based selector with data
296
- if (resolved.strategy === 'role' && resolved.role) {
297
- // Dialog role: check heading inside dialog instead of filter on full text content
298
- if (resolved.role === 'dialog') {
299
- const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
300
- dataValue,
301
- });
302
- return {
303
- code,
304
- comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
305
- };
306
- }
307
-
345
+ // --- Image role: images have no text, use name ---
346
+ if (resolved.strategy === 'role' && resolved.role === 'img') {
308
347
  const hasName = resolved.name && resolved.name.trim();
309
- // For img role: images have no text content, so put dataValue in name instead of filter
310
- const isImgRole = resolved.role === 'img';
311
348
  const code = context.templateEngine.renderStep('visible-with-role-variable-assertion', {
312
349
  role: resolved.role,
313
- name: isImgRole ? (hasName ? resolved.name : dataValue) : (hasName ? resolved.name : undefined),
314
- dataValue: isImgRole ? undefined : dataValue,
350
+ name: hasName ? resolved.name : dataValue,
315
351
  exact: resolved.exact || false,
316
352
  nth: resolved.nth,
317
353
  });
318
354
  return {
319
355
  code,
320
- comment: `Assert ${step.selectorRef} with value ${step.dataRef}`,
356
+ comment: `Assert ${step.selectorRef} image with name ${step.dataRef}`,
321
357
  };
322
358
  }
323
359
 
324
- const selectorValue = resolved.value || '';
325
-
326
- // If selector has a value in YAML, combine with variable
327
- // Otherwise use variable-only
328
- if (selectorValue && selectorValue.trim()) {
329
- // Has selector value - combine both
330
- const code = context.templateEngine.renderStep('visible-with-value-assertion', {
331
- value: selectorValue,
332
- dataValue,
333
- dataRef: step.dataRef,
334
- nth: resolved.nth,
335
- exact: resolved.exact || false,
360
+ // --- Everything else: toHaveText (exact full match) ---
361
+ // Locator strategy
362
+ if (resolved.strategy === 'locator') {
363
+ const code = context.templateEngine.renderStep('have-text-assertion', {
364
+ ...resolved,
365
+ expectedText: dataValue,
336
366
  });
337
367
  return {
338
368
  code,
339
- comment: `Assert ${step.selectorRef} with value ${step.dataRef}`,
369
+ comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
340
370
  };
341
- } else {
342
- // Empty selector - use variable only
343
- const code = context.templateEngine.renderStep('visible-with-variable-assertion', {
344
- selectorRef: step.selectorRef,
345
- selectorValue: resolved.value || '',
346
- dataValue,
347
- nth: resolved.nth,
371
+ }
372
+
373
+ // Role-based selector
374
+ if (resolved.strategy === 'role' && resolved.role) {
375
+ const hasName = resolved.name && resolved.name.trim();
376
+ const code = context.templateEngine.renderStep('have-text-assertion', {
377
+ ...resolved,
378
+ expectedText: dataValue,
348
379
  });
349
380
  return {
350
381
  code,
351
- comment: `Assert ${step.selectorRef} visible with ${step.dataRef}`,
382
+ comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
352
383
  };
353
384
  }
385
+
386
+ // Text/placeholder/testid/other strategies
387
+ const code = context.templateEngine.renderStep('have-text-assertion', {
388
+ ...resolved,
389
+ expectedText: dataValue,
390
+ });
391
+ return {
392
+ code,
393
+ comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
394
+ };
354
395
  },
355
396
  priority: 13,
356
397
  },
@@ -72,6 +72,14 @@ export class PatternRegistry {
72
72
  // Prefer resolver (framework-agnostic) over generator (legacy)
73
73
  if (pattern.resolver) {
74
74
  const resolved = pattern.resolver(step, context);
75
+
76
+ // Auto-inject parent scoping if step has parentRef
77
+ if (step.parentRef && step.parentType) {
78
+ resolved.data.parentLocator = PatternRegistry.resolveParentLocator(
79
+ step.parentRef, step.parentType, context
80
+ );
81
+ }
82
+
75
83
  const code = context.templateEngine.renderStep(resolved.templateName, resolved.data);
76
84
  return {
77
85
  code,
@@ -86,6 +94,39 @@ export class PatternRegistry {
86
94
  return null;
87
95
  }
88
96
 
97
+ /**
98
+ * Resolve parent scoping to a Playwright locator string.
99
+ * Tries YAML lookup first, falls back to auto-infer from parentType.
100
+ *
101
+ * Parent type → Playwright role:
102
+ * table → 'table', list → 'list', section → 'region',
103
+ * dialog → 'dialog', form → 'form'
104
+ */
105
+ private static resolveParentLocator(
106
+ parentRef: string, parentType: string, context: PatternContext
107
+ ): string {
108
+ // Try resolving from selectors YAML
109
+ try {
110
+ const resolved = context.selectorResolver.resolveSelector(
111
+ parentRef, context.featureName, parentType, 0
112
+ );
113
+ return context.renderLocator(resolved);
114
+ } catch {
115
+ // Fallback: auto-infer from parentType + parentRef as accessible name
116
+ }
117
+
118
+ const roleMap: Record<string, string> = {
119
+ table: 'table',
120
+ list: 'list',
121
+ section: 'region',
122
+ dialog: 'dialog',
123
+ form: 'form',
124
+ };
125
+ const role = roleMap[parentType] || parentType;
126
+ const escapedName = parentRef.replace(/'/g, "\\'");
127
+ return `page.getByRole('${role}', { name: '${escapedName}' })`;
128
+ }
129
+
89
130
  /**
90
131
  * Check if step matches a pattern matcher
91
132
  */
@@ -27,9 +27,66 @@ function elementKeywordToRole(text: string): string | null {
27
27
  }
28
28
 
29
29
  /**
30
- * Interaction patterns: click, hover, press, wait
30
+ * Interaction patterns: click, hover, press, wait, alert
31
31
  */
32
32
  export const interactionPatterns: StepPattern[] = [
33
+ // === Browser Alert (system dialog) patterns ===
34
+ // These must appear BEFORE the click that triggers the alert in Gherkin:
35
+ // When User click [OK] alert ← registers page.once('dialog', ...)
36
+ // And User click [Delete] button ← triggers the alert
37
+ {
38
+ name: 'alert-accept',
39
+ matcher: (step: ParsedStep) =>
40
+ step.elementType === 'alert' &&
41
+ (step.text.includes('click') || step.text.includes('clicks')) &&
42
+ !step.dataRef &&
43
+ !!(step.selectorRef && /^(ok|accept|yes|confirm)$/i.test(step.selectorRef)),
44
+ resolver: (step, context) => {
45
+ return {
46
+ templateName: 'alert-accept-action',
47
+ data: {},
48
+ comment: `Accept browser alert`,
49
+ };
50
+ },
51
+ priority: 20,
52
+ },
53
+ {
54
+ name: 'alert-dismiss',
55
+ matcher: (step: ParsedStep) =>
56
+ step.elementType === 'alert' &&
57
+ (step.text.includes('click') || step.text.includes('clicks')) &&
58
+ !step.dataRef &&
59
+ !!(step.selectorRef && /^(cancel|dismiss|no)$/i.test(step.selectorRef)),
60
+ resolver: (step, context) => {
61
+ return {
62
+ templateName: 'alert-dismiss-action',
63
+ data: {},
64
+ comment: `Dismiss browser alert`,
65
+ };
66
+ },
67
+ priority: 20,
68
+ },
69
+ {
70
+ name: 'alert-fill',
71
+ matcher: (step: ParsedStep) =>
72
+ step.elementType === 'alert' &&
73
+ step.text.includes('fill') &&
74
+ !!step.dataRef,
75
+ resolver: (step, context) => {
76
+ let fillValue: string;
77
+ try {
78
+ fillValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
79
+ } catch {
80
+ fillValue = `\${${step.dataRef}}`;
81
+ }
82
+ return {
83
+ templateName: 'alert-fill-action',
84
+ data: { fillValue },
85
+ comment: `Fill browser prompt with ${step.dataRef}`,
86
+ };
87
+ },
88
+ priority: 20,
89
+ },
33
90
  {
34
91
  name: 'unknown-element-action',
35
92
  matcher: (step: ParsedStep) => {
@@ -6,11 +6,12 @@
6
6
  import { StepPattern } from './types';
7
7
 
8
8
  export const tablePatterns: StepPattern[] = [
9
- // "User see [Users] table has {{count}} rows" — must have "rows" AFTER {{data}}
9
+ // "User see [Users] table with {{count}} rows" (preferred)
10
+ // Also accepts: "User see [Users] table has {{count}} rows" (backward compat)
10
11
  {
11
12
  name: 'table-row-count',
12
13
  matcher: (step) => {
13
- return /\btable\s+has\b/i.test(step.text) &&
14
+ return /\btable\s+(?:has|with)\b/i.test(step.text) &&
14
15
  /\}\}\s*rows?\b/i.test(step.text) &&
15
16
  !!step.dataRef;
16
17
  },
@@ -29,7 +30,8 @@ export const tablePatterns: StepPattern[] = [
29
30
  priority: 16,
30
31
  },
31
32
 
32
- // "User see [Users] table has [Email] column"
33
+ // "User see [Users] table has [Email] column" (backward compat)
34
+ // Preferred: "User see [Email] column in [Users] table" (parent scoping)
33
35
  {
34
36
  name: 'table-column-exists',
35
37
  matcher: (step) => {
@@ -152,11 +154,12 @@ export const tablePatterns: StepPattern[] = [
152
154
  priority: 17,
153
155
  },
154
156
 
155
- // "User see [Users] table has row with {{name}}"
157
+ // "User see [Users] table row with {{name}}" (preferred)
158
+ // Also accepts: "User see [Users] table has row with {{name}}" (backward compat)
156
159
  {
157
160
  name: 'table-row-exists',
158
161
  matcher: (step) => {
159
- return /\btable\s+has\s+row\s+with\b/i.test(step.text) && !!step.dataRef;
162
+ return /\btable\s+(?:has\s+)?row\s+with\b/i.test(step.text) && !!step.dataRef;
160
163
  },
161
164
  resolver: (step, context) => {
162
165
  const resolved = context.selectorResolver.resolveSelector(
@@ -211,6 +211,13 @@ export class SelectorResolver {
211
211
  'heading': () => ({ strategy: 'role', role: 'heading', name: label, value: 'heading' }),
212
212
  'list': () => ({ strategy: 'role', role: 'list', name: label, value: 'list' }),
213
213
  'listitem': () => ({ strategy: 'role', role: 'listitem', value: 'listitem' }),
214
+ 'searchbox': () => ({ strategy: 'role', role: 'searchbox', name: label, value: 'searchbox' }),
215
+ 'option': () => ({ strategy: 'role', role: 'option', name: label, value: 'option' }),
216
+ 'slider': () => ({ strategy: 'role', role: 'slider', name: label, value: 'slider' }),
217
+ 'switch': () => ({ strategy: 'role', role: 'switch', name: label, value: 'switch' }),
218
+ 'tab': () => ({ strategy: 'role', role: 'tab', name: label, value: 'tab' }),
219
+ 'dialog': () => ({ strategy: 'role', role: 'dialog', name: label, value: 'dialog' }),
220
+ 'alertdialog': () => ({ strategy: 'role', role: 'alertdialog', name: label, value: 'alertdialog' }),
214
221
  };
215
222
 
216
223
  const factory = strategyMap[normalized];
@@ -247,6 +254,15 @@ export class SelectorResolver {
247
254
  'textbox': 'field',
248
255
  'textarea': 'field',
249
256
  'editor': 'field',
257
+ // Search alias → searchbox role
258
+ 'search': 'searchbox',
259
+ // Toggle alias → switch role
260
+ 'toggle': 'switch',
261
+ // Alert alias → alertdialog role
262
+ 'alert': 'alertdialog',
263
+ // Modal/drawer alias → dialog role
264
+ 'modal': 'dialog',
265
+ 'drawer': 'dialog',
250
266
  };
251
267
  return aliasMap[elementType] || elementType;
252
268
  }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * AI Rules Updater
3
+ * Updates AI rules, commands, and skills from bundled templates.
4
+ * Used by `sungen update` command.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+
10
+ // File mapping: [templateFile, outputPath]
11
+ // Shared with project-initializer.ts
12
+ export const AI_RULES_FILE_MAPPING: [string, string][] = [
13
+ // Config
14
+ ['claude-config.md', 'CLAUDE.md'],
15
+ ['copilot-config.md', '.github/copilot-instructions.md'],
16
+
17
+ // Commands — Claude Code
18
+ ['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
19
+ ['claude-cmd-make-tc.md', '.claude/commands/sungen/make-tc.md'],
20
+ ['claude-cmd-make-test.md', '.claude/commands/sungen/make-test.md'],
21
+
22
+ // Commands — GitHub Copilot
23
+ ['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
24
+ ['copilot-cmd-make-tc.md', '.github/prompts/sungen-make-tc.prompt.md'],
25
+ ['copilot-cmd-make-test.md', '.github/prompts/sungen-make-test.prompt.md'],
26
+
27
+ // Skills — Claude Code
28
+ ['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
29
+ ['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
30
+ ['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
31
+ ['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
32
+ ['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
33
+
34
+ // Skills — GitHub Copilot
35
+ ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
36
+ ['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
37
+ ['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
38
+ ['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
39
+ ['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
40
+ ];
41
+
42
+ export class AIRulesUpdater {
43
+ private cwd: string;
44
+ private aiTemplateDir: string;
45
+
46
+ constructor(cwd: string) {
47
+ this.cwd = cwd;
48
+ this.aiTemplateDir = path.join(__dirname, 'templates', 'ai-instructions');
49
+ }
50
+
51
+ async update(dryRun: boolean): Promise<void> {
52
+ console.log('🔄 Updating AI rules, commands, and skills...\n');
53
+
54
+ const updated: string[] = [];
55
+ const created: string[] = [];
56
+ const unchanged: string[] = [];
57
+ const missing: string[] = [];
58
+
59
+ for (const [templateFile, outputRelPath] of AI_RULES_FILE_MAPPING) {
60
+ const templatePath = path.join(this.aiTemplateDir, templateFile);
61
+ const outputPath = path.join(this.cwd, outputRelPath);
62
+
63
+ if (!fs.existsSync(templatePath)) {
64
+ missing.push(templateFile);
65
+ continue;
66
+ }
67
+
68
+ const newContent = fs.readFileSync(templatePath, 'utf-8');
69
+
70
+ if (fs.existsSync(outputPath)) {
71
+ const currentContent = fs.readFileSync(outputPath, 'utf-8');
72
+ if (currentContent === newContent) {
73
+ unchanged.push(outputRelPath);
74
+ continue;
75
+ }
76
+
77
+ if (!dryRun) {
78
+ fs.writeFileSync(outputPath, newContent, 'utf-8');
79
+ }
80
+ updated.push(outputRelPath);
81
+ } else {
82
+ if (!dryRun) {
83
+ const outputDir = path.dirname(outputPath);
84
+ if (!fs.existsSync(outputDir)) {
85
+ fs.mkdirSync(outputDir, { recursive: true });
86
+ }
87
+ fs.writeFileSync(outputPath, newContent, 'utf-8');
88
+ }
89
+ created.push(outputRelPath);
90
+ }
91
+ }
92
+
93
+ // Print results
94
+ if (dryRun) {
95
+ console.log('📋 Dry run — no files changed\n');
96
+ }
97
+
98
+ if (updated.length > 0) {
99
+ console.log(`✏️ Updated (${updated.length}):`);
100
+ for (const f of updated) {
101
+ console.log(` ${f}`);
102
+ }
103
+ console.log();
104
+ }
105
+
106
+ if (created.length > 0) {
107
+ console.log(`✨ Created (${created.length}):`);
108
+ for (const f of created) {
109
+ console.log(` ${f}`);
110
+ }
111
+ console.log();
112
+ }
113
+
114
+ if (unchanged.length > 0) {
115
+ console.log(`✅ Already up to date (${unchanged.length}):`);
116
+ for (const f of unchanged) {
117
+ console.log(` ${f}`);
118
+ }
119
+ console.log();
120
+ }
121
+
122
+ if (missing.length > 0) {
123
+ console.log(`⚠️ Template not found (${missing.length}):`);
124
+ for (const f of missing) {
125
+ console.log(` ${f}`);
126
+ }
127
+ console.log();
128
+ }
129
+
130
+ const totalChanges = updated.length + created.length;
131
+ if (totalChanges === 0) {
132
+ console.log('All AI rules are up to date. No changes needed.');
133
+ } else if (dryRun) {
134
+ console.log(`${totalChanges} file(s) would be changed. Run without --dry-run to apply.`);
135
+ } else {
136
+ console.log(`${totalChanges} file(s) updated successfully.`);
137
+ }
138
+ }
139
+ }
@@ -6,6 +6,7 @@
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { execSync } from 'child_process';
9
+ import { AI_RULES_FILE_MAPPING } from './ai-rules-updater';
9
10
 
10
11
  export class ProjectInitializer {
11
12
  private baseCwd: string;
@@ -250,38 +251,7 @@ export class ProjectInitializer {
250
251
  private createAIRules(): void {
251
252
  const aiTemplateDir = path.join(__dirname, 'templates', 'ai-instructions');
252
253
 
253
- // File mapping: [templateFile, outputPath]
254
- const fileMapping: [string, string][] = [
255
- // Config
256
- ['claude-config.md', 'CLAUDE.md'],
257
- ['copilot-config.md', '.github/copilot-instructions.md'],
258
-
259
- // Commands — Claude Code
260
- ['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
261
- ['claude-cmd-make-tc.md', '.claude/commands/sungen/make-tc.md'],
262
- ['claude-cmd-make-test.md', '.claude/commands/sungen/make-test.md'],
263
-
264
- // Commands — GitHub Copilot
265
- ['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
266
- ['copilot-cmd-make-tc.md', '.github/prompts/sungen-make-tc.prompt.md'],
267
- ['copilot-cmd-make-test.md', '.github/prompts/sungen-make-test.prompt.md'],
268
-
269
- // Skills — Claude Code
270
- ['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
271
- ['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
272
- ['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
273
- ['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
274
- ['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
275
-
276
- // Skills — GitHub Copilot (separate copies with Copilot-friendly descriptions)
277
- ['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
278
- ['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
279
- ['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
280
- ['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
281
- ['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
282
- ];
283
-
284
- for (const [templateFile, outputRelPath] of fileMapping) {
254
+ for (const [templateFile, outputRelPath] of AI_RULES_FILE_MAPPING) {
285
255
  const outputPath = path.join(this.cwd, outputRelPath);
286
256
 
287
257
  if (fs.existsSync(outputPath)) {