mcp-maestro-mobile-ai 1.4.0 → 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,426 @@
1
+ /**
2
+ * YAML Generator with Warnings Support
3
+ *
4
+ * Generates Maestro YAML from analyzed prompts.
5
+ * Supports:
6
+ * - Clean YAML for complete prompts
7
+ * - YAML with warnings for force-generated (incomplete) prompts
8
+ * - Automatic application of interaction patterns
9
+ */
10
+
11
+ import { CompletenessLevel, getDefaultAssumptions } from './promptAnalyzer.js';
12
+ import { getInteractionPatternHints, getYamlGenerationRules } from './knownIssues.js';
13
+
14
+ /**
15
+ * Generate YAML from analysis result
16
+ *
17
+ * @param {object} analysis - Result from analyzePrompt()
18
+ * @param {object} options - Generation options
19
+ * @returns {object} { yaml, warnings, assumptions, success }
20
+ */
21
+ export function generateYamlFromAnalysis(analysis, options = {}) {
22
+ const { extracted, assessment } = analysis;
23
+ const { forceGenerate = false, includeComments = true } = options;
24
+
25
+ // Check if generation is possible
26
+ if (assessment.level === CompletenessLevel.INSUFFICIENT && !forceGenerate) {
27
+ return {
28
+ success: false,
29
+ yaml: null,
30
+ error: 'Cannot generate YAML: missing required information (app ID)',
31
+ missing: assessment.missing,
32
+ };
33
+ }
34
+
35
+ if (assessment.level === CompletenessLevel.NEEDS_CLARIFICATION && !forceGenerate) {
36
+ return {
37
+ success: false,
38
+ yaml: null,
39
+ needsClarification: true,
40
+ level: assessment.level,
41
+ };
42
+ }
43
+
44
+ // Get defaults for assumptions
45
+ const defaults = getDefaultAssumptions(analysis);
46
+ const warnings = [];
47
+ const assumptions = [];
48
+
49
+ // Build YAML
50
+ const yamlParts = [];
51
+
52
+ // App ID header
53
+ const appId = extracted.appContext.appId || options.appId;
54
+ if (!appId) {
55
+ return {
56
+ success: false,
57
+ yaml: null,
58
+ error: 'App ID is required',
59
+ };
60
+ }
61
+
62
+ yamlParts.push(`appId: ${appId}`);
63
+ yamlParts.push('---');
64
+
65
+ // Add warning header if force generating
66
+ if (forceGenerate && assessment.level !== CompletenessLevel.COMPLETE) {
67
+ const warningHeader = generateWarningHeader(assessment, defaults);
68
+ yamlParts.push(warningHeader);
69
+ warnings.push(...assessment.assumptions);
70
+ }
71
+
72
+ // Standard start
73
+ yamlParts.push('- clearState');
74
+ yamlParts.push('- launchApp');
75
+ yamlParts.push('');
76
+
77
+ // Wait for initial screen
78
+ if (includeComments) {
79
+ yamlParts.push('# Wait for app to fully load');
80
+ }
81
+ yamlParts.push('- waitForAnimationToEnd');
82
+ yamlParts.push('');
83
+
84
+ // Generate steps based on detected pattern
85
+ const steps = generateStepsFromSequence(analysis, defaults, options);
86
+ yamlParts.push(...steps.yaml);
87
+ warnings.push(...steps.warnings);
88
+ assumptions.push(...steps.assumptions);
89
+
90
+ // Add verification if detected or assumed
91
+ const verification = generateVerification(analysis, defaults, options);
92
+ if (verification.yaml.length > 0) {
93
+ yamlParts.push('');
94
+ yamlParts.push(...verification.yaml);
95
+ if (verification.assumed) {
96
+ assumptions.push('Verification element assumed based on common patterns');
97
+ }
98
+ }
99
+
100
+ return {
101
+ success: true,
102
+ yaml: yamlParts.join('\n'),
103
+ warnings,
104
+ assumptions,
105
+ forceGenerated: forceGenerate && assessment.level !== CompletenessLevel.COMPLETE,
106
+ completenessScore: assessment.score,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Generate warning header for force-generated YAML
112
+ */
113
+ function generateWarningHeader(assessment, defaults) {
114
+ const lines = [
115
+ '# ⚠️ GENERATED WITH ASSUMPTIONS',
116
+ '# The following values were assumed - verify they match your app:',
117
+ ];
118
+
119
+ for (const assumption of assessment.assumptions) {
120
+ lines.push(`# - ${assumption}`);
121
+ }
122
+
123
+ if (defaults.alternatives) {
124
+ lines.push('#');
125
+ lines.push('# Alternative selectors to try if defaults fail:');
126
+ for (const [field, alts] of Object.entries(defaults.alternatives)) {
127
+ lines.push(`# ${field}: ${alts.join(' / ')}`);
128
+ }
129
+ }
130
+
131
+ lines.push('#');
132
+ lines.push('');
133
+
134
+ return lines.join('\n');
135
+ }
136
+
137
+ /**
138
+ * Generate steps from analyzed sequence
139
+ */
140
+ function generateStepsFromSequence(analysis, defaults, options) {
141
+ const { extracted } = analysis;
142
+ const yaml = [];
143
+ const warnings = [];
144
+ const assumptions = [];
145
+
146
+ const includeComments = options.includeComments !== false;
147
+
148
+ // Track if we've had text input (need to hide keyboard before buttons)
149
+ let hadTextInput = false;
150
+
151
+ for (const step of extracted.sequence) {
152
+ switch (step.action) {
153
+ case 'INPUT': {
154
+ const fieldLabel = step.element || defaults.usernameField || '{field_name}';
155
+ const value = step.value || extracted.values[step.elementType?.toLowerCase()] || '{value}';
156
+
157
+ if (includeComments) {
158
+ yaml.push(`# Enter ${step.elementType || 'text'}`);
159
+ }
160
+ yaml.push(`- tapOn: "${fieldLabel}"`);
161
+ yaml.push(`- inputText: "${value}"`);
162
+ yaml.push('');
163
+
164
+ hadTextInput = true;
165
+
166
+ if (!step.element) {
167
+ assumptions.push(`Field label assumed: "${fieldLabel}"`);
168
+ }
169
+ if (!step.value) {
170
+ warnings.push(`Value placeholder used for: ${fieldLabel}`);
171
+ }
172
+ break;
173
+ }
174
+
175
+ case 'TAP':
176
+ case 'SUBMIT': {
177
+ // Hide keyboard before tapping if we had text input
178
+ if (hadTextInput) {
179
+ if (includeComments) {
180
+ yaml.push('# Hide keyboard before tapping button');
181
+ }
182
+ yaml.push('- hideKeyboard');
183
+ yaml.push('- waitForAnimationToEnd');
184
+ yaml.push('');
185
+ }
186
+
187
+ const buttonLabel = step.element || defaults.submitButton || '{button_text}';
188
+
189
+ if (includeComments) {
190
+ yaml.push(`# Tap ${step.action === 'SUBMIT' ? 'submit button' : 'element'}`);
191
+ }
192
+ yaml.push(`- tapOn: "${buttonLabel}"`);
193
+ yaml.push('');
194
+
195
+ if (!step.element) {
196
+ assumptions.push(`Button text assumed: "${buttonLabel}"`);
197
+ }
198
+ break;
199
+ }
200
+
201
+ case 'SELECT': {
202
+ // Hide keyboard before dropdown
203
+ if (hadTextInput) {
204
+ yaml.push('- hideKeyboard');
205
+ yaml.push('- waitForAnimationToEnd');
206
+ yaml.push('');
207
+ }
208
+
209
+ const dropdownTrigger = step.element || '{dropdown_trigger}';
210
+ const optionValue = step.value || '{option_to_select}';
211
+
212
+ if (includeComments) {
213
+ yaml.push('# Dropdown interaction');
214
+ }
215
+ yaml.push(`- tapOn: "${dropdownTrigger}"`);
216
+ yaml.push('- waitForAnimationToEnd');
217
+ yaml.push(`- tapOn: "${optionValue}"`);
218
+ yaml.push('- waitForAnimationToEnd');
219
+ yaml.push('');
220
+
221
+ if (!step.element) {
222
+ assumptions.push(`Dropdown trigger assumed: "${dropdownTrigger}"`);
223
+ }
224
+ if (!step.value) {
225
+ warnings.push(`Dropdown option placeholder used`);
226
+ }
227
+ break;
228
+ }
229
+
230
+ case 'SCROLL': {
231
+ if (includeComments) {
232
+ yaml.push('# Scroll to find element');
233
+ }
234
+ yaml.push('- scroll');
235
+ yaml.push('- waitForAnimationToEnd');
236
+ yaml.push('');
237
+ break;
238
+ }
239
+
240
+ case 'SEARCH': {
241
+ const searchField = step.element || defaults.searchField || 'Search';
242
+ const searchValue = step.value || extracted.values.search || '{search_term}';
243
+
244
+ if (includeComments) {
245
+ yaml.push('# Search');
246
+ }
247
+ yaml.push(`- tapOn: "${searchField}"`);
248
+ yaml.push(`- inputText: "${searchValue}"`);
249
+ yaml.push('- pressKey: enter');
250
+ yaml.push('- waitForAnimationToEnd');
251
+ yaml.push('');
252
+
253
+ hadTextInput = true;
254
+ break;
255
+ }
256
+
257
+ case 'NAVIGATE': {
258
+ if (step.element) {
259
+ if (includeComments) {
260
+ yaml.push(`# Navigate to ${step.element}`);
261
+ }
262
+ yaml.push(`- tapOn: "${step.element}"`);
263
+ yaml.push('- waitForAnimationToEnd');
264
+ yaml.push('');
265
+ }
266
+ break;
267
+ }
268
+
269
+ case 'VERIFY': {
270
+ // Verifications are handled separately
271
+ break;
272
+ }
273
+
274
+ default: {
275
+ if (step.element) {
276
+ yaml.push(`- tapOn: "${step.element}"`);
277
+ yaml.push('');
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ // If no explicit steps but we have values, generate login-like pattern
284
+ if (yaml.length === 0 && Object.keys(extracted.values).length > 0) {
285
+ const { values } = extracted;
286
+
287
+ if (values.username || values.email || values.user) {
288
+ yaml.push(`- tapOn: "${defaults.usernameField || 'Username'}"`);
289
+ yaml.push(`- inputText: "${values.username || values.email || values.user}"`);
290
+ yaml.push('');
291
+ assumptions.push('Assumed login pattern from username/password values');
292
+ }
293
+
294
+ if (values.password || values.pass) {
295
+ yaml.push(`- tapOn: "${defaults.passwordField || 'Password'}"`);
296
+ yaml.push(`- inputText: "${values.password || values.pass}"`);
297
+ yaml.push('');
298
+ }
299
+
300
+ // Add submit
301
+ yaml.push('- hideKeyboard');
302
+ yaml.push('- waitForAnimationToEnd');
303
+ yaml.push('');
304
+ yaml.push(`- tapOn: "${defaults.submitButton || 'Sign In'}"`);
305
+ yaml.push('');
306
+ }
307
+
308
+ return { yaml, warnings, assumptions };
309
+ }
310
+
311
+ /**
312
+ * Generate verification steps
313
+ */
314
+ function generateVerification(analysis, defaults, options) {
315
+ const { extracted } = analysis;
316
+ const yaml = [];
317
+ let assumed = false;
318
+
319
+ const includeComments = options.includeComments !== false;
320
+
321
+ if (extracted.verifications.length > 0) {
322
+ if (includeComments) {
323
+ yaml.push('# Verify expected result');
324
+ }
325
+
326
+ // Wait for navigation/loading
327
+ yaml.push('- extendedWaitUntil:');
328
+
329
+ for (const verification of extracted.verifications) {
330
+ if (verification.target) {
331
+ yaml.push(` visible: "${verification.target}"`);
332
+ yaml.push(' timeout: 30000');
333
+ yaml.push('');
334
+ yaml.push(`- assertVisible: "${verification.target}"`);
335
+ break; // Just use first verification for extendedWaitUntil
336
+ }
337
+ }
338
+
339
+ // Add remaining verifications as assertions
340
+ for (let i = 1; i < extracted.verifications.length; i++) {
341
+ const v = extracted.verifications[i];
342
+ if (v.target) {
343
+ yaml.push(`- assertVisible: "${v.target}"`);
344
+ }
345
+ }
346
+ } else {
347
+ // No explicit verification - add warning comment
348
+ if (includeComments) {
349
+ yaml.push('# ⚠️ WARNING: No success verification specified');
350
+ yaml.push('# Add: - assertVisible: "{your_success_element}"');
351
+ }
352
+ yaml.push('- waitForAnimationToEnd');
353
+ assumed = true;
354
+ }
355
+
356
+ return { yaml, assumed };
357
+ }
358
+
359
+ /**
360
+ * Generate YAML structure with applied interaction patterns
361
+ * This ensures best practices are followed
362
+ */
363
+ export function applyInteractionPatterns(yaml) {
364
+ const rules = getYamlGenerationRules();
365
+ let modifiedYaml = yaml;
366
+ const appliedPatterns = [];
367
+
368
+ // Pattern: tapOn before inputText
369
+ if (rules.mandatory.tapBeforeInput) {
370
+ // Check for inputText without preceding tapOn
371
+ const lines = modifiedYaml.split('\n');
372
+ const fixedLines = [];
373
+
374
+ for (let i = 0; i < lines.length; i++) {
375
+ const line = lines[i];
376
+ const prevLine = i > 0 ? lines[i - 1] : '';
377
+
378
+ if (line.trim().startsWith('- inputText:') && !prevLine.trim().startsWith('- tapOn:')) {
379
+ // This is a pattern violation - add warning comment
380
+ fixedLines.push('# ⚠️ PATTERN: tapOn should precede inputText');
381
+ appliedPatterns.push('Added tapOn reminder before inputText');
382
+ }
383
+ fixedLines.push(line);
384
+ }
385
+
386
+ modifiedYaml = fixedLines.join('\n');
387
+ }
388
+
389
+ return {
390
+ yaml: modifiedYaml,
391
+ appliedPatterns,
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Format a complete YAML generation response
397
+ */
398
+ export function formatGenerationResponse(result) {
399
+ if (!result.success) {
400
+ return {
401
+ success: false,
402
+ error: result.error,
403
+ needsClarification: result.needsClarification || false,
404
+ missing: result.missing || [],
405
+ };
406
+ }
407
+
408
+ return {
409
+ success: true,
410
+ yaml: result.yaml,
411
+ forceGenerated: result.forceGenerated || false,
412
+ completenessScore: result.completenessScore,
413
+ warnings: result.warnings || [],
414
+ assumptions: result.assumptions || [],
415
+ message: result.forceGenerated
416
+ ? 'YAML generated with assumptions. Review warnings and update placeholders.'
417
+ : 'YAML generated successfully.',
418
+ };
419
+ }
420
+
421
+ export default {
422
+ generateYamlFromAnalysis,
423
+ applyInteractionPatterns,
424
+ formatGenerationResponse,
425
+ };
426
+