wogiflow 2.7.0 → 2.8.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,782 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Runtime Verification Gate
5
+ *
6
+ * Ensures UI tasks are verified through actual browser interaction,
7
+ * not just static analysis (TypeScript, build, bundle grep).
8
+ *
9
+ * Verification Hierarchy (highest to lowest):
10
+ * 1. WebMCP browser verification (automated, default when configured)
11
+ * 2. Playwright/Puppeteer test generation (automated, runnable)
12
+ * 3. User verification checklist (manual, always available)
13
+ *
14
+ * The gate activates for tasks that touch UI files (*.tsx, *.jsx, *.vue,
15
+ * *.svelte, *.css, *.styled.*) and blocks completion until behavioral
16
+ * evidence is provided.
17
+ *
18
+ * Commands:
19
+ * flow-runtime-verification.js detect <files...> — Detect if task needs UI verification
20
+ * flow-runtime-verification.js classify <files...> — Classify risk level (high/standard)
21
+ * flow-runtime-verification.js checklist <spec> — Generate user verification checklist
22
+ * flow-runtime-verification.js playwright <spec> — Generate Playwright test script
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+ const { PATHS, getConfig, safeJsonParse } = require('./flow-utils');
30
+
31
+ // ============================================================
32
+ // Constants
33
+ // ============================================================
34
+
35
+ /** File patterns that trigger UI runtime verification */
36
+ const UI_FILE_PATTERNS = [
37
+ /\.tsx$/,
38
+ /\.jsx$/,
39
+ /\.vue$/,
40
+ /\.svelte$/,
41
+ /\.css$/,
42
+ /\.scss$/,
43
+ /\.styled\./,
44
+ /\.module\.css$/,
45
+ /\.module\.scss$/
46
+ ];
47
+
48
+ /** Patterns in code that indicate state mutation (high-risk) */
49
+ const HIGH_RISK_PATTERNS = [
50
+ 'useMutation',
51
+ 'invalidateQueries',
52
+ 'queryClient',
53
+ 'setQueryData',
54
+ 'onMutate',
55
+ 'optimistic',
56
+ 'useState.*fetch',
57
+ 'onSubmit',
58
+ 'handleSubmit',
59
+ 'useReducer'
60
+ ];
61
+
62
+ /** Banned verification methods — these NEVER count as evidence */
63
+ const BANNED_EVIDENCE = [
64
+ 'bundle_grep', // grep deployed bundle for function names
65
+ 'build_success_only', // "vite build succeeds"
66
+ 'code_reading_only', // "I read the code and it's logically correct"
67
+ 'type_check_only', // "tsc --noEmit passes"
68
+ 'deploy_success_only' // "aws s3 sync completes"
69
+ ];
70
+
71
+ /** Evidence tier definitions */
72
+ const EVIDENCE_TIERS = {
73
+ STATIC: { level: 0, name: 'Static', sufficient: false, description: 'Compilation, build, lint passed' },
74
+ STRUCTURAL: { level: 1, name: 'Structural', sufficient: false, description: 'File exists, component imported, route registered' },
75
+ OBSERVATIONAL: { level: 2, name: 'Observational', sufficient: true, description: 'Page loads, feature renders, no console errors' },
76
+ INTERACTIVE: { level: 3, name: 'Interactive', sufficient: true, description: 'Clicked/typed/submitted and observed expected result persist' },
77
+ AUTOMATED: { level: 4, name: 'Automated', sufficient: true, description: 'Test script exercised scenario and asserted result' }
78
+ };
79
+
80
+ // ============================================================
81
+ // Detection: Does this task need UI verification?
82
+ // ============================================================
83
+
84
+ /**
85
+ * Detect whether a set of changed files requires UI runtime verification.
86
+ *
87
+ * @param {string[]} changedFiles — list of changed file paths
88
+ * @returns {{ needsVerification: boolean, uiFiles: string[], reason: string }}
89
+ */
90
+ function detectUIVerification(changedFiles) {
91
+ const uiFiles = changedFiles.filter(f =>
92
+ UI_FILE_PATTERNS.some(p => p.test(f))
93
+ );
94
+
95
+ if (uiFiles.length === 0) {
96
+ return { needsVerification: false, uiFiles: [], reason: 'No UI files changed' };
97
+ }
98
+
99
+ return {
100
+ needsVerification: true,
101
+ uiFiles,
102
+ reason: `${uiFiles.length} UI file(s) changed: ${uiFiles.slice(0, 5).join(', ')}${uiFiles.length > 5 ? '...' : ''}`
103
+ };
104
+ }
105
+
106
+ // ============================================================
107
+ // Classification: Standard vs High-Risk
108
+ // ============================================================
109
+
110
+ /**
111
+ * Classify the risk level of a UI task based on code patterns.
112
+ * High-risk tasks involve state mutation and require extra verification.
113
+ *
114
+ * @param {string[]} changedFiles — files to analyze
115
+ * @returns {{ risk: 'high'|'standard', reasons: string[], highRiskFiles: string[] }}
116
+ */
117
+ function classifyRisk(changedFiles) {
118
+ const highRiskFiles = [];
119
+ const reasons = [];
120
+
121
+ for (const filePath of changedFiles) {
122
+ const absPath = path.resolve(PATHS.root, filePath);
123
+ try {
124
+ if (!fs.existsSync(absPath)) continue;
125
+ const content = fs.readFileSync(absPath, 'utf-8');
126
+
127
+ for (const pattern of HIGH_RISK_PATTERNS) {
128
+ const regex = new RegExp(pattern, 'i');
129
+ if (regex.test(content)) {
130
+ if (!highRiskFiles.includes(filePath)) {
131
+ highRiskFiles.push(filePath);
132
+ reasons.push(`${filePath}: contains ${pattern}`);
133
+ }
134
+ }
135
+ }
136
+ } catch (_err) {
137
+ // Skip unreadable files
138
+ }
139
+ }
140
+
141
+ return {
142
+ risk: highRiskFiles.length > 0 ? 'high' : 'standard',
143
+ reasons,
144
+ highRiskFiles
145
+ };
146
+ }
147
+
148
+ // ============================================================
149
+ // Checklist Generation (Fallback — always available)
150
+ // ============================================================
151
+
152
+ /**
153
+ * Generate a user verification checklist from acceptance criteria.
154
+ *
155
+ * @param {Object} params
156
+ * @param {string[]} params.criteria — acceptance criteria from the spec
157
+ * @param {string} params.pagePath — URL path to verify (e.g., '/customers/123/integration')
158
+ * @param {string} params.risk — 'high' or 'standard'
159
+ * @returns {string} formatted checklist for the user
160
+ */
161
+ function generateChecklist(params) {
162
+ const { criteria = [], pagePath = '/', risk = 'standard' } = params;
163
+ const lines = [];
164
+
165
+ lines.push('━━━ USER VERIFICATION CHECKLIST ━━━');
166
+ lines.push(`Page: ${pagePath}`);
167
+ lines.push(`Risk: ${risk.toUpperCase()}`);
168
+ lines.push('');
169
+ lines.push('I cannot verify UI behavior from the CLI. Please check:');
170
+ lines.push('');
171
+
172
+ for (let i = 0; i < criteria.length; i++) {
173
+ lines.push(`□ ${i + 1}. ${criteria[i]}`);
174
+ }
175
+
176
+ lines.push('');
177
+
178
+ if (risk === 'high') {
179
+ lines.push('HIGH-RISK EXTRA CHECKS (state mutation detected):');
180
+ lines.push('□ After each action, wait 3 seconds (refetch/re-render)');
181
+ lines.push('□ Refresh the page (F5) and verify changes persisted');
182
+ lines.push('□ Navigate away and back — is the state still correct?');
183
+ lines.push('□ Check browser DevTools console for errors');
184
+ lines.push('');
185
+ }
186
+
187
+ lines.push('Reply "verified" when all checks pass, or describe what\'s broken.');
188
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
189
+
190
+ return lines.join('\n');
191
+ }
192
+
193
+ // ============================================================
194
+ // Playwright Test Generation
195
+ // ============================================================
196
+
197
+ /**
198
+ * Generate a Playwright test script from acceptance criteria.
199
+ *
200
+ * @param {Object} params
201
+ * @param {string[]} params.criteria — acceptance criteria
202
+ * @param {string} params.pagePath — URL path
203
+ * @param {string} params.taskId — for the test file name
204
+ * @param {string} params.risk — 'high' or 'standard'
205
+ * @returns {{ script: string, filePath: string }}
206
+ */
207
+ function generatePlaywrightTest(params) {
208
+ const { criteria = [], pagePath = '/', taskId = 'unknown', risk = 'standard' } = params;
209
+
210
+ const lines = [
211
+ '// Auto-generated runtime verification test',
212
+ `// Task: ${taskId}`,
213
+ `// Generated: ${new Date().toISOString()}`,
214
+ `// Risk level: ${risk}`,
215
+ '//',
216
+ '// Run: npx playwright test <this-file>',
217
+ "// Or: npx playwright test --headed <this-file> (to watch)",
218
+ '',
219
+ "import { test, expect } from '@playwright/test';",
220
+ '',
221
+ `test.describe('Runtime Verification: ${taskId}', () => {`,
222
+ ''
223
+ ];
224
+
225
+ for (let i = 0; i < criteria.length; i++) {
226
+ const criterion = criteria[i];
227
+ const safeCriterion = criterion.replace(/'/g, "\\'").replace(/`/g, '\\`').replace(/\$/g, '\\$');
228
+ const safePath = pagePath.replace(/'/g, "\\'").replace(/`/g, '\\`');
229
+ lines.push(` test('Criterion ${i + 1}: ${safeCriterion}', async ({ page }) => {`);
230
+ lines.push(` await page.goto('${safePath}');`);
231
+ lines.push('');
232
+ lines.push(` // TODO: Implement verification for: ${criterion}`);
233
+ lines.push(' // 1. Perform the user action');
234
+ lines.push(' // 2. Assert the expected result');
235
+ lines.push(' // 3. Wait for any async updates');
236
+ lines.push('');
237
+
238
+ if (risk === 'high') {
239
+ lines.push(' // HIGH-RISK: State mutation detected — verify persistence');
240
+ lines.push(' await page.waitForTimeout(3000); // Wait for refetch');
241
+ lines.push(' // Assert the state is still correct after refetch');
242
+ lines.push('');
243
+ lines.push(' // Persistence check: reload and verify');
244
+ lines.push(' await page.reload();');
245
+ lines.push(' await page.waitForLoadState("networkidle");');
246
+ lines.push(' // Assert the state survived page reload');
247
+ }
248
+
249
+ lines.push(' });');
250
+ lines.push('');
251
+ }
252
+
253
+ lines.push('});');
254
+
255
+ const script = lines.join('\n');
256
+ const fileName = `verify-${taskId}.spec.ts`;
257
+ const filePath = path.join(PATHS.root, 'tests', 'verification', fileName);
258
+
259
+ return { script, filePath, fileName };
260
+ }
261
+
262
+ // ============================================================
263
+ // WebMCP Verification Instructions
264
+ // ============================================================
265
+
266
+ /**
267
+ * Generate WebMCP browser verification instructions.
268
+ * These are injected into the AI's context when WebMCP is available.
269
+ *
270
+ * @param {Object} params
271
+ * @param {string[]} params.criteria — acceptance criteria
272
+ * @param {string} params.pagePath — URL path
273
+ * @param {string} params.risk — 'high' or 'standard'
274
+ * @param {string} params.devServerUrl — dev server URL (e.g., 'http://localhost:5173')
275
+ * @returns {string} WebMCP verification instructions
276
+ */
277
+ function generateWebMCPInstructions(params) {
278
+ const { criteria = [], pagePath = '/', risk = 'standard', devServerUrl = 'http://localhost:5173' } = params;
279
+ const fullUrl = `${devServerUrl}${pagePath}`;
280
+
281
+ const lines = [];
282
+ lines.push('━━━ WEBMCP RUNTIME VERIFICATION ━━━');
283
+ lines.push('');
284
+ lines.push('Use the browser MCP tools to verify each criterion:');
285
+ lines.push('');
286
+ lines.push(`1. Navigate: mcp_browser_navigate("${fullUrl}")`);
287
+ lines.push('2. Wait for page load: mcp_browser_wait_for_load_state("networkidle")');
288
+ lines.push('3. Screenshot BEFORE any changes: mcp_browser_screenshot()');
289
+ lines.push('');
290
+
291
+ for (let i = 0; i < criteria.length; i++) {
292
+ lines.push(`CRITERION ${i + 1}: ${criteria[i]}`);
293
+ lines.push(' ACTION: [perform the user action via mcp_browser_click/type/select]');
294
+ lines.push(' WAIT: mcp_browser_wait_for_timeout(2000)');
295
+ lines.push(' VERIFY: mcp_browser_screenshot() — check the result visually');
296
+ lines.push(' ASSERT: mcp_browser_evaluate("document.querySelector(...)") — check DOM state');
297
+ lines.push('');
298
+ }
299
+
300
+ if (risk === 'high') {
301
+ lines.push('HIGH-RISK EXTRA STEPS:');
302
+ lines.push(' After all criteria verified:');
303
+ lines.push(' 1. Wait 3 seconds: mcp_browser_wait_for_timeout(3000)');
304
+ lines.push(' 2. Screenshot again: mcp_browser_screenshot() — check state persisted');
305
+ lines.push(` 3. Reload: mcp_browser_navigate("${fullUrl}")`);
306
+ lines.push(' 4. Wait: mcp_browser_wait_for_load_state("networkidle")');
307
+ lines.push(' 5. Screenshot: mcp_browser_screenshot() — check state survived reload');
308
+ lines.push('');
309
+ }
310
+
311
+ lines.push('Record results in BEHAVIORAL EVIDENCE LOG:');
312
+ lines.push(' For each criterion: ACTION → EXPECTED → OBSERVED → VERDICT');
313
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
314
+
315
+ return lines.join('\n');
316
+ }
317
+
318
+ // ============================================================
319
+ // Behavioral Evidence Log Template
320
+ // ============================================================
321
+
322
+ /**
323
+ * Generate a Behavioral Evidence Log template.
324
+ *
325
+ * @param {string} taskId
326
+ * @param {string[]} criteria
327
+ * @returns {string} BEL template
328
+ */
329
+ function generateBELTemplate(taskId, criteria) {
330
+ const lines = [
331
+ '━━━ BEHAVIORAL EVIDENCE LOG ━━━',
332
+ `Task: ${taskId}`,
333
+ `Verified on: [localhost:PORT or deployed URL]`,
334
+ `Time: ${new Date().toISOString()}`,
335
+ `Method: [WEBMCP / PLAYWRIGHT / USER_CHECKLIST]`,
336
+ ''
337
+ ];
338
+
339
+ for (const criterion of criteria) {
340
+ lines.push(`CRITERION: "${criterion}"`);
341
+ lines.push(' ACTION: [exact user action performed]');
342
+ lines.push(' EXPECTED: [what should happen]');
343
+ lines.push(' OBSERVED: [what ACTUALLY appeared — describe the UI, not the code]');
344
+ lines.push(' WAIT: [waited N seconds for refetch/re-render]');
345
+ lines.push(' VERDICT: PASS / FAIL');
346
+ lines.push(' EVIDENCE TIER: [OBSERVATIONAL / INTERACTIVE / AUTOMATED]');
347
+ lines.push('');
348
+ }
349
+
350
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
351
+ return lines.join('\n');
352
+ }
353
+
354
+ // ============================================================
355
+ // Repeat Failure Detection
356
+ // ============================================================
357
+
358
+ /**
359
+ * Check if this task has had repeated failures (Groundhog Day detector).
360
+ *
361
+ * @param {string} taskId
362
+ * @returns {{ strikeCount: number, previousAttempts: string[], requiresDifferentApproach: boolean }}
363
+ */
364
+ function checkRepeatFailures(taskId) {
365
+ // Validate taskId format
366
+ if (!/^wf-[0-9a-f]{8}$/i.test(taskId)) {
367
+ return { strikeCount: 0, requiresDifferentApproach: false };
368
+ }
369
+
370
+ const feedbackPath = path.join(PATHS.state, 'feedback-patterns.md');
371
+ let strikeCount = 0;
372
+
373
+ try {
374
+ if (fs.existsSync(feedbackPath)) {
375
+ const content = fs.readFileSync(feedbackPath, 'utf-8');
376
+ // Look for REPEAT-FAILURE entries mentioning this task
377
+ const pattern = new RegExp(`REPEAT-FAILURE.*${taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'gi');
378
+ const matches = content.match(pattern);
379
+ if (matches) {
380
+ strikeCount = matches.length;
381
+ }
382
+
383
+ // Also count task-related entries
384
+ const taskPattern = new RegExp(`Strike count: (\\d+).*${taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i');
385
+ const strikeMatch = content.match(taskPattern);
386
+ if (strikeMatch) {
387
+ strikeCount = Math.max(strikeCount, parseInt(strikeMatch[1], 10));
388
+ }
389
+ }
390
+ } catch (_err) {
391
+ // Non-critical
392
+ }
393
+
394
+ return {
395
+ strikeCount,
396
+ requiresDifferentApproach: strikeCount >= 2
397
+ };
398
+ }
399
+
400
+ // ============================================================
401
+ // Backend Detection and API Test Generation
402
+ // ============================================================
403
+
404
+ /** File patterns that indicate backend/API work */
405
+ const BACKEND_FILE_PATTERNS = [
406
+ /\.controller\./,
407
+ /\.service\./,
408
+ /\.resolver\./,
409
+ /\.handler\./,
410
+ /\/routes?\//,
411
+ /\/api\//,
412
+ /\/controllers?\//,
413
+ /\/endpoints?\//,
414
+ /\.dto\./,
415
+ /\.guard\./,
416
+ /\.middleware\./,
417
+ /\.interceptor\./
418
+ ];
419
+
420
+ /**
421
+ * Detect whether a set of changed files requires backend API verification.
422
+ *
423
+ * @param {string[]} changedFiles
424
+ * @returns {{ needsVerification: boolean, apiFiles: string[], reason: string }}
425
+ */
426
+ function detectAPIVerification(changedFiles) {
427
+ const apiFiles = changedFiles.filter(f =>
428
+ BACKEND_FILE_PATTERNS.some(p => p.test(f))
429
+ );
430
+
431
+ if (apiFiles.length === 0) {
432
+ return { needsVerification: false, apiFiles: [], reason: 'No API/backend files changed' };
433
+ }
434
+
435
+ return {
436
+ needsVerification: true,
437
+ apiFiles,
438
+ reason: `${apiFiles.length} API file(s) changed: ${apiFiles.slice(0, 5).join(', ')}${apiFiles.length > 5 ? '...' : ''}`
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Generate an API integration test script (like Postman but baked in).
444
+ * Tests request/response shapes, status codes, and data persistence.
445
+ *
446
+ * @param {Object} params
447
+ * @param {Array<Object>} params.endpoints — endpoints to test: { method, path, description, requestBody?, expectedStatus?, expectedFields? }
448
+ * @param {string[]} params.criteria — acceptance criteria from spec
449
+ * @param {string} params.taskId
450
+ * @param {string} params.baseUrl — API base URL (e.g., 'http://localhost:3000')
451
+ * @returns {{ script: string, filePath: string, fileName: string }}
452
+ */
453
+ function generateAPITest(params) {
454
+ const { endpoints = [], criteria = [], taskId = 'unknown', baseUrl = 'http://localhost:3000' } = params;
455
+
456
+ const lines = [
457
+ '// Auto-generated API integration test',
458
+ `// Task: ${taskId}`,
459
+ `// Generated: ${new Date().toISOString()}`,
460
+ '//',
461
+ '// Run: node --test <this-file>',
462
+ '// Or: npx vitest run <this-file>',
463
+ '',
464
+ "const { describe, it } = require('node:test');",
465
+ "const assert = require('node:assert/strict');",
466
+ '',
467
+ `const BASE_URL = process.env.API_URL || '${baseUrl}';`,
468
+ '',
469
+ '/**',
470
+ ' * Helper: make HTTP request and parse JSON response',
471
+ ' */',
472
+ 'async function apiRequest(method, path, body = null, headers = {}) {',
473
+ ' const url = `${BASE_URL}${path}`;',
474
+ ' const options = {',
475
+ ' method,',
476
+ " headers: { 'Content-Type': 'application/json', ...headers },",
477
+ ' };',
478
+ ' if (body) options.body = JSON.stringify(body);',
479
+ '',
480
+ ' const response = await fetch(url, options);',
481
+ ' const contentType = response.headers.get("content-type") || "";',
482
+ ' const data = contentType.includes("json") ? await response.json() : await response.text();',
483
+ '',
484
+ ' return { status: response.status, data, headers: Object.fromEntries(response.headers) };',
485
+ '}',
486
+ '',
487
+ `describe('API Integration: ${taskId}', () => {`,
488
+ ''
489
+ ];
490
+
491
+ // Generate tests from endpoints
492
+ for (let i = 0; i < endpoints.length; i++) {
493
+ const ep = endpoints[i];
494
+ const method = (ep.method || 'GET').toUpperCase();
495
+ const epPath = ep.path || '/';
496
+ const desc = ep.description || `${method} ${epPath}`;
497
+ const expectedStatus = ep.expectedStatus || 200;
498
+
499
+ lines.push(` it('${desc.replace(/'/g, "\\'")}', async () => {`);
500
+
501
+ if (ep.requestBody) {
502
+ lines.push(` const body = ${JSON.stringify(ep.requestBody, null, 4).split('\n').join('\n ')};`);
503
+ lines.push(` const res = await apiRequest('${method}', '${epPath}', body);`);
504
+ } else {
505
+ lines.push(` const res = await apiRequest('${method}', '${epPath}');`);
506
+ }
507
+
508
+ lines.push('');
509
+ lines.push(` // Status code check`);
510
+ lines.push(` assert.equal(res.status, ${expectedStatus}, \`Expected ${expectedStatus}, got \${res.status}: \${JSON.stringify(res.data)}\`);`);
511
+
512
+ if (ep.expectedFields && ep.expectedFields.length > 0) {
513
+ lines.push('');
514
+ lines.push(' // Response shape check');
515
+ for (const field of ep.expectedFields) {
516
+ lines.push(` assert.ok(res.data.${field} !== undefined, 'Response missing field: ${field}');`);
517
+ }
518
+ }
519
+
520
+ if (ep.persistenceCheck) {
521
+ lines.push('');
522
+ lines.push(' // Persistence check: re-fetch and verify data was stored');
523
+ lines.push(` const verify = await apiRequest('GET', '${ep.persistenceCheck}');`);
524
+ lines.push(` assert.equal(verify.status, 200, 'Persistence check: GET failed');`);
525
+ if (ep.expectedFields && ep.expectedFields.length > 0) {
526
+ for (const field of ep.expectedFields) {
527
+ lines.push(` assert.ok(verify.data.${field} !== undefined, 'Persistence: field ${field} not stored');`);
528
+ }
529
+ }
530
+ }
531
+
532
+ lines.push(' });');
533
+ lines.push('');
534
+ }
535
+
536
+ // Generate tests from criteria (for endpoints not explicitly listed)
537
+ if (criteria.length > 0 && endpoints.length === 0) {
538
+ for (let i = 0; i < criteria.length; i++) {
539
+ lines.push(` it('Criterion ${i + 1}: ${criteria[i].replace(/'/g, "\\'")}', async () => {`);
540
+ lines.push(` // TODO: Implement API test for: ${criteria[i]}`);
541
+ lines.push(' // 1. Make the API request');
542
+ lines.push(' // 2. Assert status code');
543
+ lines.push(' // 3. Assert response shape');
544
+ lines.push(' // 4. If mutation: re-fetch and verify persistence');
545
+ lines.push(' });');
546
+ lines.push('');
547
+ }
548
+ }
549
+
550
+ lines.push('});');
551
+
552
+ const script = lines.join('\n');
553
+ const fileName = `api-verify-${taskId}.test.js`;
554
+ const filePath = path.join(PATHS.root, 'tests', 'verification', fileName);
555
+
556
+ return { script, filePath, fileName };
557
+ }
558
+
559
+ /**
560
+ * Generate curl-based API verification commands (quick manual check).
561
+ *
562
+ * @param {Array<Object>} endpoints — { method, path, requestBody?, expectedStatus? }
563
+ * @param {string} baseUrl
564
+ * @returns {string} formatted curl commands
565
+ */
566
+ function generateCurlVerification(endpoints, baseUrl = 'http://localhost:3000') {
567
+ const lines = ['━━━ API CURL VERIFICATION ━━━', ''];
568
+
569
+ for (const ep of endpoints) {
570
+ const method = (ep.method || 'GET').toUpperCase();
571
+ const url = `${baseUrl}${ep.path}`;
572
+
573
+ let cmd = `curl -s -w "\\n%{http_code}" -X ${method}`;
574
+ cmd += ` -H "Content-Type: application/json"`;
575
+ if (ep.requestBody) {
576
+ // Escape single quotes for shell safety: replace ' with '\''
577
+ const jsonBody = JSON.stringify(ep.requestBody).replace(/'/g, "'\\''");
578
+ cmd += ` -d '${jsonBody}'`;
579
+ }
580
+ cmd += ` "${url}"`;
581
+
582
+ lines.push(`# ${ep.description || `${method} ${ep.path}`}`);
583
+ lines.push(`# Expected: ${ep.expectedStatus || 200}`);
584
+ lines.push(cmd);
585
+ lines.push('');
586
+ }
587
+
588
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
589
+ return lines.join('\n');
590
+ }
591
+
592
+ // ============================================================
593
+ // Combined Task Type Detection
594
+ // ============================================================
595
+
596
+ /**
597
+ * Detect the task type (frontend, backend, or full-stack) from changed files.
598
+ *
599
+ * @param {string[]} changedFiles
600
+ * @returns {{ type: 'frontend'|'backend'|'fullstack'|'other', frontend: Object, backend: Object }}
601
+ */
602
+ function detectTaskType(changedFiles) {
603
+ const frontend = detectUIVerification(changedFiles);
604
+ const backend = detectAPIVerification(changedFiles);
605
+
606
+ let type = 'other';
607
+ if (frontend.needsVerification && backend.needsVerification) type = 'fullstack';
608
+ else if (frontend.needsVerification) type = 'frontend';
609
+ else if (backend.needsVerification) type = 'backend';
610
+
611
+ return { type, frontend, backend };
612
+ }
613
+
614
+ // ============================================================
615
+ // Verification Method Selection
616
+ // ============================================================
617
+
618
+ /**
619
+ * Determine the best available verification method.
620
+ *
621
+ * @returns {{ method: 'webmcp'|'playwright'|'checklist', available: boolean, reason: string }}
622
+ */
623
+ function selectVerificationMethod() {
624
+ const config = getConfig();
625
+
626
+ // 1. Check for WebMCP (default, highest priority)
627
+ if (config.webmcp?.enabled) {
628
+ return { method: 'webmcp', available: true, reason: 'WebMCP is configured — using browser verification' };
629
+ }
630
+
631
+ // Check for .mcp.json with browser-related servers
632
+ const mcpPath = path.join(PATHS.root, '.mcp.json');
633
+ const mcpConfig = safeJsonParse(mcpPath, null);
634
+ if (mcpConfig) {
635
+ const servers = mcpConfig.mcpServers || {};
636
+ const hasBrowser = Object.keys(servers).some(k =>
637
+ k.includes('browser') || k.includes('puppeteer') || k.includes('playwright')
638
+ );
639
+ if (hasBrowser) {
640
+ return { method: 'webmcp', available: true, reason: 'Browser MCP server detected in .mcp.json' };
641
+ }
642
+ }
643
+
644
+ // 2. Check for Playwright/Puppeteer
645
+ const pkgPath = path.join(PATHS.root, 'package.json');
646
+ const pkg = safeJsonParse(pkgPath, {});
647
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
648
+
649
+ if (allDeps['@playwright/test'] || allDeps['playwright']) {
650
+ return { method: 'playwright', available: true, reason: 'Playwright detected in dependencies' };
651
+ }
652
+
653
+ if (allDeps['puppeteer'] || allDeps['puppeteer-core']) {
654
+ return { method: 'playwright', available: true, reason: 'Puppeteer detected — generating Playwright-compatible test' };
655
+ }
656
+
657
+ // 3. Fallback: user checklist
658
+ return { method: 'checklist', available: true, reason: 'No browser automation available — using manual verification checklist' };
659
+ }
660
+
661
+ // ============================================================
662
+ // CLI
663
+ // ============================================================
664
+
665
+ function main() {
666
+ const command = process.argv[2] || 'help';
667
+ const args = process.argv.slice(3);
668
+
669
+ switch (command) {
670
+ case 'detect': {
671
+ const result = detectUIVerification(args);
672
+ console.log(JSON.stringify(result, null, 2));
673
+ break;
674
+ }
675
+
676
+ case 'classify': {
677
+ const result = classifyRisk(args);
678
+ console.log(JSON.stringify(result, null, 2));
679
+ break;
680
+ }
681
+
682
+ case 'method': {
683
+ const result = selectVerificationMethod();
684
+ console.log(JSON.stringify(result, null, 2));
685
+ break;
686
+ }
687
+
688
+ case 'checklist': {
689
+ const criteria = args;
690
+ console.log(generateChecklist({ criteria, pagePath: '/', risk: 'standard' }));
691
+ break;
692
+ }
693
+
694
+ case 'playwright': {
695
+ const criteria = args;
696
+ const { script } = generatePlaywrightTest({ criteria, pagePath: '/', taskId: 'manual', risk: 'standard' });
697
+ console.log(script);
698
+ break;
699
+ }
700
+
701
+ case 'task-type': {
702
+ const result = detectTaskType(args);
703
+ console.log(JSON.stringify(result, null, 2));
704
+ break;
705
+ }
706
+
707
+ case 'api-test': {
708
+ const criteria = args;
709
+ const { script } = generateAPITest({ criteria, taskId: 'manual', baseUrl: 'http://localhost:3000' });
710
+ console.log(script);
711
+ break;
712
+ }
713
+
714
+ case 'api-detect': {
715
+ const result = detectAPIVerification(args);
716
+ console.log(JSON.stringify(result, null, 2));
717
+ break;
718
+ }
719
+
720
+ case 'repeat': {
721
+ const taskId = args[0] || 'unknown';
722
+ const result = checkRepeatFailures(taskId);
723
+ console.log(JSON.stringify(result, null, 2));
724
+ break;
725
+ }
726
+
727
+ default:
728
+ console.log(`
729
+ Wogi Flow - Runtime Verification Gate
730
+
731
+ Usage: flow-runtime-verification.js <command> [args...]
732
+
733
+ Commands:
734
+ detect <files...> Detect if task needs UI verification
735
+ api-detect <files...> Detect if task needs API verification
736
+ task-type <files...> Detect task type (frontend/backend/fullstack)
737
+ classify <files...> Classify risk level (high/standard)
738
+ method Determine best verification method
739
+ checklist <criteria> Generate user verification checklist
740
+ playwright <criteria> Generate Playwright test script
741
+ api-test <criteria> Generate API integration test script
742
+ repeat <taskId> Check for repeat failures
743
+ `);
744
+ }
745
+ }
746
+
747
+ // ============================================================
748
+ // Exports
749
+ // ============================================================
750
+
751
+ module.exports = {
752
+ // Detection
753
+ detectUIVerification,
754
+ detectAPIVerification,
755
+ detectTaskType,
756
+ classifyRisk,
757
+ UI_FILE_PATTERNS,
758
+ BACKEND_FILE_PATTERNS,
759
+ HIGH_RISK_PATTERNS,
760
+
761
+ // Evidence
762
+ EVIDENCE_TIERS,
763
+ BANNED_EVIDENCE,
764
+
765
+ // Frontend verification
766
+ selectVerificationMethod,
767
+ generateChecklist,
768
+ generatePlaywrightTest,
769
+ generateWebMCPInstructions,
770
+ generateBELTemplate,
771
+
772
+ // Backend verification
773
+ generateAPITest,
774
+ generateCurlVerification,
775
+
776
+ // Repeat failure
777
+ checkRepeatFailures
778
+ };
779
+
780
+ if (require.main === module) {
781
+ main();
782
+ }