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.
- package/.claude/commands/wogi-audit.md +156 -8
- package/.claude/commands/wogi-start.md +276 -0
- package/.claude/settings.json +2 -11
- package/lib/workspace-channel-server.js +19 -4
- package/lib/workspace-routing.js +17 -6
- package/lib/workspace.js +18 -0
- package/package.json +1 -1
- package/scripts/flow-audit-gates.js +766 -0
- package/scripts/flow-runtime-verification.js +782 -0
- package/scripts/hooks/entry/claude-code/permission-denied.js +111 -0
- package/scripts/postinstall.js +64 -2
|
@@ -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
|
+
}
|