mcp-maestro-mobile-ai 1.3.1 → 1.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.
@@ -0,0 +1,701 @@
1
+ /**
2
+ * Prompt Analyzer - Action-Element-Verification Model
3
+ *
4
+ * This module analyzes user prompts to extract:
5
+ * - APP CONTEXT: Which app to test
6
+ * - ACTIONS: tap, input, scroll, swipe, select...
7
+ * - ELEMENTS: button, field, list, chart, table...
8
+ * - VALUES: credentials, data, selections...
9
+ * - VERIFICATIONS: visible, contains, count, state...
10
+ * - SEQUENCE: order of operations
11
+ *
12
+ * It assesses completeness and determines whether to:
13
+ * - Generate YAML directly (complete prompt)
14
+ * - Ask clarifying questions (needs more info)
15
+ * - Generate with warnings (force generate with assumptions)
16
+ */
17
+
18
+ /**
19
+ * Completeness levels for prompt analysis
20
+ */
21
+ export const CompletenessLevel = {
22
+ COMPLETE: 'complete', // All required info present, generate clean YAML
23
+ GENERATABLE: 'generatable', // Missing some info, can generate with assumptions
24
+ NEEDS_CLARIFICATION: 'needs_clarification', // Missing important info, should ask
25
+ INSUFFICIENT: 'insufficient', // Missing critical info (e.g., appId), cannot proceed
26
+ };
27
+
28
+ /**
29
+ * Action types that can be extracted from prompts
30
+ */
31
+ export const ActionType = {
32
+ TAP: 'tap',
33
+ INPUT: 'input',
34
+ SCROLL: 'scroll',
35
+ SWIPE: 'swipe',
36
+ SELECT: 'select',
37
+ VERIFY: 'verify',
38
+ WAIT: 'wait',
39
+ NAVIGATE: 'navigate',
40
+ PRESS: 'press',
41
+ LONG_PRESS: 'long_press',
42
+ CLEAR: 'clear',
43
+ OPEN: 'open',
44
+ CLOSE: 'close',
45
+ SUBMIT: 'submit',
46
+ };
47
+
48
+ /**
49
+ * Element type indicators
50
+ */
51
+ export const ElementType = {
52
+ BUTTON: 'button',
53
+ FIELD: 'field',
54
+ INPUT: 'input',
55
+ DROPDOWN: 'dropdown',
56
+ PICKER: 'picker',
57
+ LIST: 'list',
58
+ TABLE: 'table',
59
+ GRID: 'grid',
60
+ CHART: 'chart',
61
+ IMAGE: 'image',
62
+ CHECKBOX: 'checkbox',
63
+ RADIO: 'radio',
64
+ SWITCH: 'switch',
65
+ TOGGLE: 'toggle',
66
+ TAB: 'tab',
67
+ MENU: 'menu',
68
+ MODAL: 'modal',
69
+ DIALOG: 'dialog',
70
+ LINK: 'link',
71
+ ICON: 'icon',
72
+ TEXT: 'text',
73
+ LABEL: 'label',
74
+ HEADER: 'header',
75
+ CARD: 'card',
76
+ STEP: 'step',
77
+ };
78
+
79
+ /**
80
+ * Keywords that indicate specific actions
81
+ */
82
+ const ACTION_KEYWORDS = {
83
+ tap: ['tap', 'click', 'press', 'touch', 'hit', 'select'],
84
+ input: ['enter', 'input', 'type', 'fill', 'write', 'set'],
85
+ scroll: ['scroll', 'swipe', 'slide'],
86
+ verify: ['verify', 'check', 'confirm', 'assert', 'see', 'visible', 'should see', 'display', 'displayed', 'shows', 'appear'],
87
+ navigate: ['go to', 'navigate', 'open', 'launch', 'switch to', 'move to'],
88
+ select: ['select', 'choose', 'pick', 'dropdown'],
89
+ submit: ['submit', 'send', 'sign in', 'log in', 'login', 'register', 'signup', 'sign up', 'save', 'confirm', 'proceed'],
90
+ wait: ['wait', 'loading', 'until'],
91
+ clear: ['clear', 'erase', 'delete', 'remove'],
92
+ search: ['search', 'find', 'look for', 'query'],
93
+ };
94
+
95
+ /**
96
+ * Keywords that indicate element types
97
+ */
98
+ const ELEMENT_KEYWORDS = {
99
+ button: ['button', 'btn', 'submit', 'sign in', 'log in', 'login', 'register', 'save', 'confirm', 'cancel', 'ok', 'next', 'back', 'continue'],
100
+ field: ['field', 'input', 'textbox', 'text box', 'text field', 'entry', 'username', 'password', 'email', 'phone', 'name', 'address'],
101
+ dropdown: ['dropdown', 'drop down', 'select', 'picker', 'combo box', 'combobox', 'menu', 'region', 'country', 'state', 'category'],
102
+ list: ['list', 'items', 'results', 'options'],
103
+ table: ['table', 'grid', 'rows', 'columns', 'cells'],
104
+ chart: ['chart', 'graph', 'diagram', 'plot', 'visualization'],
105
+ checkbox: ['checkbox', 'check box', 'tick'],
106
+ radio: ['radio', 'radio button', 'option'],
107
+ switch: ['switch', 'toggle', 'on/off'],
108
+ tab: ['tab', 'tabs'],
109
+ modal: ['modal', 'dialog', 'popup', 'pop-up', 'alert', 'overlay'],
110
+ image: ['image', 'photo', 'picture', 'icon', 'avatar'],
111
+ link: ['link', 'hyperlink', 'url'],
112
+ step: ['step', 'stage', 'wizard', 'progress'],
113
+ card: ['card', 'tile', 'panel'],
114
+ };
115
+
116
+ /**
117
+ * Verification keywords
118
+ */
119
+ const VERIFICATION_KEYWORDS = {
120
+ visible: ['visible', 'displayed', 'shown', 'appears', 'can see', 'should see', 'see the'],
121
+ contains: ['contains', 'has', 'includes', 'shows', 'displays'],
122
+ count: ['count', 'number of', 'total', 'how many'],
123
+ state: ['enabled', 'disabled', 'active', 'inactive', 'checked', 'unchecked', 'selected'],
124
+ text: ['text', 'message', 'label', 'title', 'content'],
125
+ success: ['success', 'successful', 'passed', 'complete', 'done', 'logged in', 'registered'],
126
+ error: ['error', 'failed', 'failure', 'invalid', 'wrong', 'incorrect'],
127
+ };
128
+
129
+ /**
130
+ * App ID patterns
131
+ */
132
+ const APP_ID_PATTERNS = [
133
+ /(?:app\s*(?:id)?|package|bundle)\s*[:=]?\s*([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+)/i,
134
+ /\b([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*){2,})\b/,
135
+ ];
136
+
137
+ /**
138
+ * Value extraction patterns
139
+ */
140
+ const VALUE_PATTERNS = {
141
+ quoted: /["']([^"']+)["']/g,
142
+ colon: /:\s*([^\s,]+)/g,
143
+ equals: /=\s*([^\s,]+)/g,
144
+ withLabel: /(?:username|user|email|password|pass|phone|name|value)[\s:=]+["']?([^"'\s,]+)["']?/gi,
145
+ };
146
+
147
+ /**
148
+ * Analyze a user prompt and extract structured information
149
+ *
150
+ * @param {string} userPrompt - The user's test description
151
+ * @param {object} options - Additional context (appId, etc.)
152
+ * @returns {object} Analysis result with extracted info and completeness assessment
153
+ */
154
+ export function analyzePrompt(userPrompt, options = {}) {
155
+ const prompt = userPrompt.trim().toLowerCase();
156
+ const originalPrompt = userPrompt.trim();
157
+
158
+ // Extract all components
159
+ const appContext = extractAppContext(originalPrompt, options.appId);
160
+ const actions = extractActions(originalPrompt);
161
+ const elements = extractElements(originalPrompt);
162
+ const values = extractValues(originalPrompt);
163
+ const verifications = extractVerifications(originalPrompt);
164
+ const sequence = buildSequence(actions, elements, values, verifications);
165
+
166
+ // Assess completeness
167
+ const assessment = assessCompleteness({
168
+ appContext,
169
+ actions,
170
+ elements,
171
+ values,
172
+ verifications,
173
+ sequence,
174
+ });
175
+
176
+ return {
177
+ extracted: {
178
+ appContext,
179
+ actions,
180
+ elements,
181
+ values,
182
+ verifications,
183
+ sequence,
184
+ },
185
+ assessment,
186
+ originalPrompt,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Extract app context from prompt
192
+ */
193
+ function extractAppContext(prompt, providedAppId = null) {
194
+ if (providedAppId) {
195
+ return {
196
+ appId: providedAppId,
197
+ source: 'provided',
198
+ };
199
+ }
200
+
201
+ // Try to find app ID in prompt
202
+ for (const pattern of APP_ID_PATTERNS) {
203
+ const match = prompt.match(pattern);
204
+ if (match && match[1]) {
205
+ return {
206
+ appId: match[1],
207
+ source: 'extracted',
208
+ };
209
+ }
210
+ }
211
+
212
+ // Try to find app name mentions
213
+ const appNamePatterns = [
214
+ /(?:on|in|for|app|application)\s+["']?([A-Z][a-zA-Z0-9\s]+)["']?/i,
215
+ /^test\s+(?:that\s+)?(?:the\s+)?["']?([A-Z][a-zA-Z0-9\s]+)["']?\s+app/i,
216
+ ];
217
+
218
+ for (const pattern of appNamePatterns) {
219
+ const match = prompt.match(pattern);
220
+ if (match && match[1]) {
221
+ return {
222
+ appName: match[1].trim(),
223
+ appId: null,
224
+ source: 'extracted_name',
225
+ };
226
+ }
227
+ }
228
+
229
+ return {
230
+ appId: null,
231
+ source: 'missing',
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Extract actions from prompt
237
+ */
238
+ function extractActions(prompt) {
239
+ const actions = [];
240
+ const promptLower = prompt.toLowerCase();
241
+
242
+ for (const [actionType, keywords] of Object.entries(ACTION_KEYWORDS)) {
243
+ for (const keyword of keywords) {
244
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
245
+ let match;
246
+ while ((match = regex.exec(promptLower)) !== null) {
247
+ actions.push({
248
+ type: actionType.toUpperCase(),
249
+ keyword: keyword,
250
+ position: match.index,
251
+ context: prompt.substring(Math.max(0, match.index - 20), match.index + 50),
252
+ });
253
+ }
254
+ }
255
+ }
256
+
257
+ // Sort by position and deduplicate nearby actions
258
+ actions.sort((a, b) => a.position - b.position);
259
+
260
+ // Remove duplicates that are too close together (same type within 20 chars)
261
+ const dedupedActions = [];
262
+ for (const action of actions) {
263
+ const isDuplicate = dedupedActions.some(
264
+ existing => existing.type === action.type &&
265
+ Math.abs(existing.position - action.position) < 20
266
+ );
267
+ if (!isDuplicate) {
268
+ dedupedActions.push(action);
269
+ }
270
+ }
271
+
272
+ return dedupedActions;
273
+ }
274
+
275
+ /**
276
+ * Extract elements from prompt
277
+ */
278
+ function extractElements(prompt) {
279
+ const elements = [];
280
+ const promptLower = prompt.toLowerCase();
281
+
282
+ for (const [elementType, keywords] of Object.entries(ELEMENT_KEYWORDS)) {
283
+ for (const keyword of keywords) {
284
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
285
+ let match;
286
+ while ((match = regex.exec(promptLower)) !== null) {
287
+ // Try to extract the element name/label
288
+ const context = prompt.substring(Math.max(0, match.index - 30), match.index + keyword.length + 50);
289
+ const labelMatch = context.match(/["']([^"']+)["']/) ||
290
+ context.match(/(?:the|a|an)\s+(\w+(?:\s+\w+)?)\s+\w+/);
291
+
292
+ elements.push({
293
+ type: elementType.toUpperCase(),
294
+ keyword: keyword,
295
+ position: match.index,
296
+ label: labelMatch ? labelMatch[1] : null,
297
+ context: context.trim(),
298
+ });
299
+ }
300
+ }
301
+ }
302
+
303
+ // Deduplicate
304
+ const seen = new Set();
305
+ return elements.filter(el => {
306
+ const key = `${el.type}-${el.position}`;
307
+ if (seen.has(key)) return false;
308
+ seen.add(key);
309
+ return true;
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Extract values from prompt
315
+ */
316
+ function extractValues(prompt) {
317
+ const values = {};
318
+
319
+ // Extract quoted values
320
+ const quotedMatches = [...prompt.matchAll(/["']([^"']+)["']/g)];
321
+ quotedMatches.forEach((match, idx) => {
322
+ values[`quoted_${idx}`] = match[1];
323
+ });
324
+
325
+ // Extract labeled values (username: xyz, password: abc)
326
+ const labelPatterns = [
327
+ { pattern: /username[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'username' },
328
+ { pattern: /user[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'username' },
329
+ { pattern: /email[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'email' },
330
+ { pattern: /password[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'password' },
331
+ { pattern: /pass[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'password' },
332
+ { pattern: /phone[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'phone' },
333
+ { pattern: /name[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'name' },
334
+ { pattern: /region[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'region' },
335
+ { pattern: /country[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'country' },
336
+ { pattern: /search[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'search' },
337
+ { pattern: /query[\s:=]+["']?([^"'\s,]+)["']?/gi, field: 'search' },
338
+ ];
339
+
340
+ for (const { pattern, field } of labelPatterns) {
341
+ const matches = [...prompt.matchAll(pattern)];
342
+ if (matches.length > 0) {
343
+ values[field] = matches[matches.length - 1][1]; // Take last match
344
+ }
345
+ }
346
+
347
+ // Extract with/using patterns (e.g., "with username abc")
348
+ const withPattern = /with\s+(\w+)\s+["']?([^"'\s,]+)["']?/gi;
349
+ const withMatches = [...prompt.matchAll(withPattern)];
350
+ for (const match of withMatches) {
351
+ values[match[1].toLowerCase()] = match[2];
352
+ }
353
+
354
+ return values;
355
+ }
356
+
357
+ /**
358
+ * Extract verifications from prompt
359
+ */
360
+ function extractVerifications(prompt) {
361
+ const verifications = [];
362
+ const promptLower = prompt.toLowerCase();
363
+
364
+ for (const [verificationType, keywords] of Object.entries(VERIFICATION_KEYWORDS)) {
365
+ for (const keyword of keywords) {
366
+ const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
367
+ let match;
368
+ while ((match = regex.exec(promptLower)) !== null) {
369
+ // Extract what should be verified
370
+ const afterKeyword = prompt.substring(match.index + keyword.length, match.index + keyword.length + 100);
371
+ const targetMatch = afterKeyword.match(/\s*["']?([^"'\n,]+?)["']?\s*(?:$|,|and|\.)/);
372
+
373
+ verifications.push({
374
+ type: verificationType.toUpperCase(),
375
+ keyword: keyword,
376
+ target: targetMatch ? targetMatch[1].trim() : null,
377
+ position: match.index,
378
+ });
379
+ }
380
+ }
381
+ }
382
+
383
+ return verifications;
384
+ }
385
+
386
+ /**
387
+ * Build action sequence from extracted components
388
+ */
389
+ function buildSequence(actions, elements, values, verifications) {
390
+ const sequence = [];
391
+ const allItems = [
392
+ ...actions.map(a => ({ ...a, category: 'action' })),
393
+ ...elements.map(e => ({ ...e, category: 'element' })),
394
+ ...verifications.map(v => ({ ...v, category: 'verification' })),
395
+ ];
396
+
397
+ // Sort by position
398
+ allItems.sort((a, b) => a.position - b.position);
399
+
400
+ // Build logical sequence
401
+ for (let i = 0; i < allItems.length; i++) {
402
+ const item = allItems[i];
403
+
404
+ if (item.category === 'action') {
405
+ // Find associated element
406
+ const nextElement = allItems.find(
407
+ (x, j) => j > i && x.category === 'element' && x.position - item.position < 50
408
+ );
409
+
410
+ sequence.push({
411
+ action: item.type,
412
+ element: nextElement ? nextElement.label || nextElement.keyword : null,
413
+ elementType: nextElement ? nextElement.type : null,
414
+ value: findValueForAction(item, values),
415
+ });
416
+ } else if (item.category === 'verification' && !sequence.some(s => s.action === 'VERIFY' && s.target === item.target)) {
417
+ sequence.push({
418
+ action: 'VERIFY',
419
+ type: item.type,
420
+ target: item.target,
421
+ });
422
+ }
423
+ }
424
+
425
+ return sequence;
426
+ }
427
+
428
+ /**
429
+ * Find value associated with an action
430
+ */
431
+ function findValueForAction(action, values) {
432
+ const context = action.context.toLowerCase();
433
+
434
+ // Check for common field-value associations
435
+ if (context.includes('username') || context.includes('user')) {
436
+ return values.username || values.user || null;
437
+ }
438
+ if (context.includes('password') || context.includes('pass')) {
439
+ return values.password || values.pass || null;
440
+ }
441
+ if (context.includes('email')) {
442
+ return values.email || null;
443
+ }
444
+ if (context.includes('search') || context.includes('query')) {
445
+ return values.search || values.query || null;
446
+ }
447
+
448
+ return null;
449
+ }
450
+
451
+ /**
452
+ * Assess completeness of extracted information
453
+ */
454
+ function assessCompleteness(extracted) {
455
+ const { appContext, actions, elements, values, verifications, sequence } = extracted;
456
+
457
+ const issues = [];
458
+ const missing = [];
459
+ const assumptions = [];
460
+ let score = 100;
461
+
462
+ // Critical: App context
463
+ if (!appContext.appId) {
464
+ if (appContext.appName) {
465
+ issues.push('App ID not found (only app name detected)');
466
+ assumptions.push(`Will need app ID for: ${appContext.appName}`);
467
+ score -= 40;
468
+ } else {
469
+ issues.push('No app identifier found');
470
+ missing.push('appId');
471
+ score -= 50;
472
+ }
473
+ }
474
+
475
+ // Important: Actions
476
+ if (actions.length === 0) {
477
+ issues.push('No actions detected in prompt');
478
+ missing.push('actions');
479
+ score -= 20;
480
+ }
481
+
482
+ // Important: Elements
483
+ if (elements.length === 0 && actions.length > 0) {
484
+ issues.push('Actions found but no target elements identified');
485
+ assumptions.push('Will use common element patterns based on action types');
486
+ score -= 15;
487
+ }
488
+
489
+ // Useful: Values for input actions
490
+ const inputActions = actions.filter(a => a.type === 'INPUT');
491
+ if (inputActions.length > 0 && Object.keys(values).length === 0) {
492
+ issues.push('Input actions detected but no values provided');
493
+ missing.push('input_values');
494
+ score -= 15;
495
+ }
496
+
497
+ // Useful: Verifications
498
+ if (verifications.length === 0) {
499
+ assumptions.push('No explicit verification - will add generic success check');
500
+ score -= 5;
501
+ }
502
+
503
+ // Check for element labels
504
+ const unlabeledElements = elements.filter(e => !e.label);
505
+ if (unlabeledElements.length > 0) {
506
+ assumptions.push(`${unlabeledElements.length} element(s) without explicit labels - will use common patterns`);
507
+ score -= unlabeledElements.length * 3;
508
+ }
509
+
510
+ // Determine completeness level
511
+ let level;
512
+ if (score >= 80 && appContext.appId && actions.length > 0) {
513
+ level = CompletenessLevel.COMPLETE;
514
+ } else if (score >= 50 && appContext.appId) {
515
+ level = CompletenessLevel.GENERATABLE;
516
+ } else if (appContext.appId) {
517
+ level = CompletenessLevel.NEEDS_CLARIFICATION;
518
+ } else {
519
+ level = CompletenessLevel.INSUFFICIENT;
520
+ }
521
+
522
+ return {
523
+ level,
524
+ score: Math.max(0, score),
525
+ issues,
526
+ missing,
527
+ assumptions,
528
+ canForceGenerate: level !== CompletenessLevel.INSUFFICIENT,
529
+ hasAppId: !!appContext.appId,
530
+ hasActions: actions.length > 0,
531
+ hasElements: elements.length > 0,
532
+ hasValues: Object.keys(values).length > 0,
533
+ hasVerifications: verifications.length > 0,
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Generate clarification questions based on analysis
539
+ */
540
+ export function generateClarificationQuestions(analysis) {
541
+ const { extracted, assessment } = analysis;
542
+ const questions = [];
543
+
544
+ // Critical questions (blocking)
545
+ if (!extracted.appContext.appId) {
546
+ questions.push({
547
+ priority: 'critical',
548
+ field: 'appId',
549
+ question: 'Which app should be tested? Please provide the package ID (e.g., com.example.app)',
550
+ required: true,
551
+ });
552
+ }
553
+
554
+ // Important questions (improve quality)
555
+ if (assessment.missing.includes('actions')) {
556
+ questions.push({
557
+ priority: 'important',
558
+ field: 'actions',
559
+ question: 'What actions should the test perform? (e.g., tap, enter text, scroll)',
560
+ required: false,
561
+ });
562
+ }
563
+
564
+ // Element labeling questions
565
+ const inputActions = extracted.actions.filter(a => a.type === 'INPUT');
566
+ if (inputActions.length > 0) {
567
+ // Ask about field labels
568
+ if (!extracted.values.username && inputActions.some(a => a.context.includes('user') || a.context.includes('email'))) {
569
+ questions.push({
570
+ priority: 'recommended',
571
+ field: 'usernameLabel',
572
+ question: 'What is the label/placeholder for the username/email field?',
573
+ required: false,
574
+ default: 'Username',
575
+ });
576
+ }
577
+
578
+ if (!extracted.values.password && inputActions.some(a => a.context.includes('password') || a.context.includes('pass'))) {
579
+ questions.push({
580
+ priority: 'recommended',
581
+ field: 'passwordLabel',
582
+ question: 'What is the label/placeholder for the password field?',
583
+ required: false,
584
+ default: 'Password',
585
+ });
586
+ }
587
+ }
588
+
589
+ // Submit button question
590
+ const submitActions = extracted.actions.filter(a => a.type === 'SUBMIT');
591
+ if (submitActions.length > 0) {
592
+ questions.push({
593
+ priority: 'recommended',
594
+ field: 'submitButtonLabel',
595
+ question: 'What is the text on the submit/login button?',
596
+ required: false,
597
+ default: 'Sign In',
598
+ });
599
+ }
600
+
601
+ // Verification question
602
+ if (extracted.verifications.length === 0) {
603
+ questions.push({
604
+ priority: 'recommended',
605
+ field: 'successIndicator',
606
+ question: 'What element confirms successful completion? (e.g., welcome message, dashboard)',
607
+ required: false,
608
+ default: null,
609
+ });
610
+ }
611
+
612
+ // Dropdown/select questions
613
+ const dropdownElements = extracted.elements.filter(e => e.type === 'DROPDOWN');
614
+ for (const dropdown of dropdownElements) {
615
+ questions.push({
616
+ priority: 'recommended',
617
+ field: `dropdown_${dropdown.keyword}`,
618
+ question: `For the ${dropdown.keyword} dropdown: What is the current value shown? What value should be selected?`,
619
+ required: false,
620
+ });
621
+ }
622
+
623
+ return questions;
624
+ }
625
+
626
+ /**
627
+ * Generate warnings for force-generated YAML
628
+ */
629
+ export function generateWarnings(analysis) {
630
+ const { assessment } = analysis;
631
+ const warnings = [];
632
+
633
+ for (const assumption of assessment.assumptions) {
634
+ warnings.push({
635
+ type: 'assumption',
636
+ message: assumption,
637
+ });
638
+ }
639
+
640
+ if (assessment.missing.includes('input_values')) {
641
+ warnings.push({
642
+ type: 'missing_data',
643
+ message: 'Input values not specified - placeholders will be used',
644
+ });
645
+ }
646
+
647
+ if (!assessment.hasVerifications) {
648
+ warnings.push({
649
+ type: 'no_verification',
650
+ message: 'No success verification specified - test may pass without confirming expected outcome',
651
+ });
652
+ }
653
+
654
+ return warnings;
655
+ }
656
+
657
+ /**
658
+ * Get default assumptions for common test patterns
659
+ */
660
+ export function getDefaultAssumptions(analysis) {
661
+ const { extracted } = analysis;
662
+ const defaults = {};
663
+
664
+ // Default element labels based on detected patterns
665
+ const hasLogin = extracted.actions.some(a =>
666
+ ['login', 'sign in', 'log in'].some(k => a.context.toLowerCase().includes(k))
667
+ );
668
+
669
+ if (hasLogin) {
670
+ defaults.usernameField = 'Username';
671
+ defaults.passwordField = 'Password';
672
+ defaults.submitButton = 'Sign In';
673
+ defaults.alternatives = {
674
+ usernameField: ['Email', 'User ID', 'Login'],
675
+ passwordField: ['Password', 'Pass'],
676
+ submitButton: ['Sign In', 'Login', 'Log In', 'Submit'],
677
+ };
678
+ }
679
+
680
+ // Search pattern
681
+ const hasSearch = extracted.actions.some(a => a.type === 'SEARCH');
682
+ if (hasSearch) {
683
+ defaults.searchField = 'Search';
684
+ defaults.alternatives = {
685
+ searchField: ['Search', 'Search...', 'Find'],
686
+ };
687
+ }
688
+
689
+ return defaults;
690
+ }
691
+
692
+ export default {
693
+ CompletenessLevel,
694
+ ActionType,
695
+ ElementType,
696
+ analyzePrompt,
697
+ generateClarificationQuestions,
698
+ generateWarnings,
699
+ getDefaultAssumptions,
700
+ };
701
+