wogiflow 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.workflow/specs/architecture.md.template +24 -0
  2. package/.workflow/specs/stack.md.template +33 -0
  3. package/.workflow/specs/testing.md.template +36 -0
  4. package/README.md +90 -1
  5. package/package.json +1 -1
  6. package/scripts/MEMORY-ARCHITECTURE.md +150 -0
  7. package/scripts/flow +20 -19
  8. package/scripts/flow-auto-context.js +97 -3
  9. package/scripts/flow-conflict-resolver.js +735 -0
  10. package/scripts/flow-context-gatherer.js +520 -0
  11. package/scripts/flow-context-monitor.js +148 -19
  12. package/scripts/flow-damage-control.js +5 -1
  13. package/scripts/flow-export-profile +168 -1
  14. package/scripts/flow-import-profile +257 -6
  15. package/scripts/flow-instruction-richness.js +182 -18
  16. package/scripts/flow-knowledge-router.js +2 -0
  17. package/scripts/flow-knowledge-sync.js +2 -0
  18. package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
  19. package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
  20. package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
  21. package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
  22. package/scripts/flow-memory-db.js +386 -1
  23. package/scripts/flow-memory-sync.js +2 -0
  24. package/scripts/flow-model-adapter.js +53 -29
  25. package/scripts/flow-model-router.js +246 -1
  26. package/scripts/flow-morning.js +94 -0
  27. package/scripts/flow-onboard +223 -10
  28. package/scripts/flow-orchestrate-validation.js +539 -0
  29. package/scripts/flow-orchestrate.js +16 -507
  30. package/scripts/flow-pattern-extractor.js +1265 -0
  31. package/scripts/flow-prompt-composer.js +222 -2
  32. package/scripts/flow-quality-guard.js +594 -0
  33. package/scripts/flow-section-index.js +713 -0
  34. package/scripts/flow-section-resolver.js +484 -0
  35. package/scripts/flow-session-end.js +188 -2
  36. package/scripts/flow-skill-create.js +19 -3
  37. package/scripts/flow-skill-matcher.js +122 -7
  38. package/scripts/flow-statusline-setup.js +218 -0
  39. package/scripts/flow-step-review.js +19 -0
  40. package/scripts/flow-tech-debt.js +734 -0
  41. package/scripts/flow-utils.js +2 -0
  42. package/scripts/hooks/core/long-input-gate.js +293 -0
  43. package/scripts/flow-parallel-detector.js +0 -399
  44. package/scripts/flow-parallel-dispatch.js +0 -987
  45. /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Orchestrator Validation Module
5
+ *
6
+ * Extracted from flow-orchestrate.js for modularity.
7
+ * Contains code extraction and validation functions.
8
+ *
9
+ * Functions:
10
+ * - extractCodeFromResponse: Extract code from LLM responses
11
+ * - scoreCodeBlock: Score code blocks for selection
12
+ * - isValidCode: Validate extracted code
13
+ * - validateOutputMatchesTask: Semantic validation
14
+ * - validateImports: Import validation against export map
15
+ */
16
+
17
+ const path = require('path');
18
+ const { getConfig } = require('./flow-utils');
19
+ const { loadCachedExportMap } = require('./flow-export-scanner');
20
+
21
+ // ============================================================
22
+ // Code Extraction
23
+ // ============================================================
24
+
25
+ /**
26
+ * Extracts code from an LLM response, handling various model formats.
27
+ * Handles:
28
+ * - Thinking tags (<think>, <thinking>, etc.)
29
+ * - Model-specific artifacts (Qwen, DeepSeek, Llama)
30
+ * - Markdown code blocks (picks best one)
31
+ * - Trailing prose/explanations
32
+ * - JSON wrapper responses
33
+ * - Multiple code blocks (selects largest/most relevant)
34
+ */
35
+ function extractCodeFromResponse(response, modelName = '') {
36
+ if (!response || typeof response !== 'string') {
37
+ return response;
38
+ }
39
+
40
+ const rawResponse = response;
41
+ let code = response;
42
+
43
+ // 0. Handle JSON wrapper responses (some models wrap code in JSON)
44
+ try {
45
+ const jsonMatch = code.match(/^\s*\{[\s\S]*"code"\s*:\s*"([\s\S]*)"[\s\S]*\}\s*$/);
46
+ if (jsonMatch) {
47
+ code = JSON.parse(`"${jsonMatch[1]}"`); // Unescape JSON string
48
+ }
49
+ } catch { /* not JSON wrapped */ }
50
+
51
+ // 1. Remove model-specific thinking tags and artifacts
52
+ const thinkingPatterns = [
53
+ // Standard thinking tags
54
+ /<think>[\s\S]*?<\/think>/gi,
55
+ /<thinking>[\s\S]*?<\/thinking>/gi,
56
+ /<reasoning>[\s\S]*?<\/reasoning>/gi,
57
+ /<analysis>[\s\S]*?<\/analysis>/gi,
58
+
59
+ // Qwen-specific
60
+ /<\|im_start\|>[\s\S]*?<\|im_end\|>/gi,
61
+
62
+ // DeepSeek-specific artifacts
63
+ /^<\|begin_of_sentence\|>/gm,
64
+ /<\|end_of_sentence\|>$/gm,
65
+
66
+ // Llama-specific
67
+ /\[INST\][\s\S]*?\[\/INST\]/gi,
68
+ /<<SYS>>[\s\S]*?<<\/SYS>>/gi,
69
+
70
+ // Generic assistant markers
71
+ /^Assistant:\s*/gim,
72
+ /^AI:\s*/gim,
73
+ /^Response:\s*/gim,
74
+ /^Output:\s*/gim,
75
+ /^Answer:\s*/gim,
76
+ /^Code:\s*/gim,
77
+
78
+ // Model-specific trailing signatures
79
+ /---\s*End of (response|code|file)[\s\S]*$/gi,
80
+ /\n\nPlease let me know[\s\S]*$/gi,
81
+ /\n\nIs there anything[\s\S]*$/gi,
82
+ /\n\nFeel free to[\s\S]*$/gi,
83
+ /\n\nLet me know if[\s\S]*$/gi,
84
+ ];
85
+
86
+ for (const pattern of thinkingPatterns) {
87
+ code = code.replace(pattern, '');
88
+ }
89
+
90
+ // 2. Handle </think> tag (if partial tag remains)
91
+ const thinkEndMatch = code.match(/<\/think>\s*/i);
92
+ if (thinkEndMatch) {
93
+ code = code.slice(thinkEndMatch.index + thinkEndMatch[0].length);
94
+ }
95
+
96
+ // 3. Extract from markdown code blocks
97
+ // Find all code blocks and pick the best one
98
+ const codeBlocks = [...code.matchAll(/```(?:typescript|tsx|ts|javascript|jsx|js|plaintext)?\s*\n([\s\S]*?)```/g)];
99
+
100
+ if (codeBlocks.length > 0) {
101
+ // Score each block and pick the best one
102
+ let bestBlock = codeBlocks[0][1];
103
+ let bestScore = scoreCodeBlock(bestBlock);
104
+
105
+ for (let i = 1; i < codeBlocks.length; i++) {
106
+ const blockContent = codeBlocks[i][1];
107
+ const score = scoreCodeBlock(blockContent);
108
+ if (score > bestScore) {
109
+ bestScore = score;
110
+ bestBlock = blockContent;
111
+ }
112
+ }
113
+ code = bestBlock;
114
+ } else {
115
+ // Also try to remove any remaining markdown code block markers
116
+ code = code.replace(/^```(?:typescript|tsx|javascript|jsx|ts|js|plaintext)?\n/gm, '');
117
+ code = code.replace(/\n```$/gm, '');
118
+ code = code.replace(/^```$/gm, '');
119
+ }
120
+
121
+ // 4. Find first valid TypeScript/JavaScript line
122
+ const validStartPatterns = [
123
+ /^import\s/m,
124
+ /^export\s/m,
125
+ /^const\s/m,
126
+ /^let\s/m,
127
+ /^var\s/m,
128
+ /^function\s/m,
129
+ /^async\s+function\s/m,
130
+ /^class\s/m,
131
+ /^interface\s/m,
132
+ /^type\s/m,
133
+ /^enum\s/m,
134
+ /^declare\s/m,
135
+ /^module\s/m,
136
+ /^namespace\s/m,
137
+ /^\/\*\*/m, // JSDoc comment
138
+ /^\/\*[^*]/m, // Block comment
139
+ /^\/\//m, // Single line comment at start
140
+ /^'use /m, // 'use strict' or 'use client'
141
+ /^"use /m,
142
+ /^@/m, // Decorators
143
+ ];
144
+
145
+ let earliestMatch = -1;
146
+ for (const pattern of validStartPatterns) {
147
+ const match = code.search(pattern);
148
+ if (match !== -1 && (earliestMatch === -1 || match < earliestMatch)) {
149
+ earliestMatch = match;
150
+ }
151
+ }
152
+
153
+ if (earliestMatch > 0) {
154
+ code = code.slice(earliestMatch);
155
+ }
156
+
157
+ // 5. Remove trailing explanations and prose
158
+ const trailingPatterns = [
159
+ // Standard prose after code
160
+ /(\}|\;)\s*\n\s*\n+[A-Z][a-z]/,
161
+ // Numbered explanations
162
+ /(\}|\;)\s*\n\s*\n+\d+\.\s+/,
163
+ // Bullet points
164
+ /(\}|\;)\s*\n\s*\n+[-*•]\s+/,
165
+ // Notes/explanations
166
+ /(\}|\;)\s*\n\s*\n+(?:Note:|Explanation:|Summary:|Key |Important:)/i,
167
+ ];
168
+
169
+ for (const pattern of trailingPatterns) {
170
+ const match = code.match(pattern);
171
+ if (match) {
172
+ code = code.slice(0, match.index + 1);
173
+ break;
174
+ }
175
+ }
176
+
177
+ // 6. Clean up common artifacts
178
+ code = code
179
+ // Remove zero-width characters
180
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
181
+ // Normalize line endings
182
+ .replace(/\r\n/g, '\n')
183
+ .replace(/\r/g, '\n')
184
+ // Remove trailing whitespace on each line
185
+ .replace(/[ \t]+$/gm, '')
186
+ // Collapse multiple blank lines to max 2
187
+ .replace(/\n{3,}/g, '\n\n')
188
+ .trim();
189
+
190
+ // Debug logging
191
+ if (process.env.DEBUG_HYBRID) {
192
+ console.log('\n--- RAW LLM RESPONSE (first 500 chars) ---');
193
+ console.log(rawResponse.slice(0, 500));
194
+ console.log('\n--- EXTRACTED CODE (first 500 chars) ---');
195
+ console.log(code.slice(0, 500));
196
+ console.log('---\n');
197
+ }
198
+
199
+ return code;
200
+ }
201
+
202
+ /**
203
+ * Score a code block to determine which is most likely the actual code
204
+ * Higher score = more likely to be the real code
205
+ */
206
+ function scoreCodeBlock(block) {
207
+ if (!block) return 0;
208
+
209
+ let score = 0;
210
+
211
+ // Length bonus (longer is usually better, but cap it)
212
+ score += Math.min(block.length / 100, 50);
213
+
214
+ // Valid code patterns
215
+ if (/^import\s/m.test(block)) score += 20;
216
+ if (/^export\s/m.test(block)) score += 20;
217
+ if (/^const\s/m.test(block)) score += 10;
218
+ if (/^function\s/m.test(block)) score += 10;
219
+ if (/^class\s/m.test(block)) score += 10;
220
+ if (/^interface\s/m.test(block)) score += 15;
221
+ if (/^type\s/m.test(block)) score += 10;
222
+
223
+ // Code structure indicators
224
+ score += (block.match(/\{/g) || []).length * 2;
225
+ score += (block.match(/\}/g) || []).length * 2;
226
+ score += (block.match(/=>/g) || []).length * 3;
227
+ score += (block.match(/return\s/g) || []).length * 3;
228
+
229
+ // Penalties for prose/non-code
230
+ if (/^[A-Z][a-z]+\s+[a-z]+/m.test(block)) score -= 10; // Starts with prose
231
+ if (/\.$/.test(block.trim())) score -= 5; // Ends with period (prose)
232
+
233
+ return score;
234
+ }
235
+
236
+ // ============================================================
237
+ // Code Validation
238
+ // ============================================================
239
+
240
+ /**
241
+ * Validates if the extracted code looks like valid TypeScript/JavaScript.
242
+ * Returns { valid: boolean, reason?: string }
243
+ */
244
+ function isValidCode(code) {
245
+ if (!code) {
246
+ return { valid: false, reason: 'Empty output' };
247
+ }
248
+
249
+ if (code.length < 10) {
250
+ return { valid: false, reason: 'Output too short' };
251
+ }
252
+
253
+ const trimmed = code.trim();
254
+
255
+ // Check for common LLM prose patterns that indicate thinking/explanation
256
+ const prosePatterns = [
257
+ /^(We need|Let's|The |I |You |This |Maybe|Probably|Actually|But |So |Thus |Given |Here|Now |First|To |In order)/i,
258
+ /^(Looking at|Based on|According to|As you can|Note that|Remember|Consider|Thinking|Output:)/i,
259
+ /^(```|~~~)/, // Markdown code fence at start means extraction failed
260
+ /<think>|<\/think>/i, // Thinking tags leaked through
261
+ ];
262
+
263
+ for (const pattern of prosePatterns) {
264
+ if (pattern.test(trimmed)) {
265
+ return { valid: false, reason: `Starts with prose/thinking: "${trimmed.slice(0, 50)}..."` };
266
+ }
267
+ }
268
+
269
+ // Must start with valid TS/JS syntax
270
+ const validStartPatterns = /^(import|export|const|let|var|function|async|class|interface|type|enum|declare|module|namespace|\/\*\*|\/\*|\/\/|'use |"use |@)/;
271
+
272
+ if (!validStartPatterns.test(trimmed)) {
273
+ return { valid: false, reason: `Invalid start: "${trimmed.slice(0, 50)}..."` };
274
+ }
275
+
276
+ // Additional sanity checks
277
+ // Should have some code-like structure (braces, semicolons, etc.)
278
+ const hasCodeStructure = /[{};=()]/.test(code);
279
+ if (!hasCodeStructure && code.length > 100) {
280
+ return { valid: false, reason: 'No code structure detected (missing braces/semicolons)' };
281
+ }
282
+
283
+ return { valid: true };
284
+ }
285
+
286
+ // ============================================================
287
+ // Semantic Output Validation
288
+ // ============================================================
289
+
290
+ /**
291
+ * Validates that the output semantically matches what was requested.
292
+ * This catches cases where the code is syntactically valid but implements
293
+ * the wrong thing (e.g., creating ApprovalChain instead of Button).
294
+ *
295
+ * @param {string} code - The generated code
296
+ * @param {Object} step - The step definition containing type and params
297
+ * @returns {{ valid: boolean, reason?: string, confidence: number }}
298
+ */
299
+ function validateOutputMatchesTask(code, step) {
300
+ if (!code || !step) {
301
+ return { valid: true, confidence: 0 }; // Can't validate without info
302
+ }
303
+
304
+ const stepType = step.type;
305
+ const expectedName = step.params?.name || step.params?.componentName || '';
306
+ const targetPath = step.params?.path || '';
307
+ const codeLower = code.toLowerCase();
308
+ const issues = [];
309
+ let confidence = 100;
310
+
311
+ // Extract the expected filename/component name from path
312
+ const fileBaseName = targetPath
313
+ ? path.basename(targetPath, path.extname(targetPath))
314
+ : expectedName;
315
+
316
+ // 1. For component creation, check component name
317
+ if (stepType === 'create-file' || stepType === 'create-component') {
318
+ const expectedLower = fileBaseName.toLowerCase();
319
+
320
+ // Check for component definition
321
+ const componentPatterns = [
322
+ new RegExp(`(function|const|class)\\s+${escapeRegex(fileBaseName)}`, 'i'),
323
+ new RegExp(`export\\s+(default\\s+)?${escapeRegex(fileBaseName)}`, 'i'),
324
+ new RegExp(`export\\s+(default\\s+)?(function|const|class)\\s+${escapeRegex(fileBaseName)}`, 'i'),
325
+ ];
326
+
327
+ let foundComponent = false;
328
+ for (const pattern of componentPatterns) {
329
+ if (pattern.test(code)) {
330
+ foundComponent = true;
331
+ break;
332
+ }
333
+ }
334
+
335
+ if (!foundComponent && expectedLower && expectedLower !== 'index') {
336
+ // Check if a completely different component was created
337
+ const anyComponentMatch = code.match(/(?:function|const|class)\s+([A-Z][a-zA-Z0-9]+)/);
338
+ if (anyComponentMatch && anyComponentMatch[1].toLowerCase() !== expectedLower) {
339
+ issues.push(`Expected component "${fileBaseName}" but found "${anyComponentMatch[1]}"`);
340
+ confidence -= 30;
341
+ } else {
342
+ // Component name not found at all
343
+ confidence -= 10;
344
+ }
345
+ }
346
+ }
347
+
348
+ // 2. For modifications, check target function/component exists
349
+ if (stepType === 'modify-file') {
350
+ const targetFunction = step.params?.function || step.params?.targetFunction;
351
+ if (targetFunction) {
352
+ const funcPattern = new RegExp(`(function|const|async\\s+function)\\s+${escapeRegex(targetFunction)}`, 'i');
353
+ if (!funcPattern.test(code)) {
354
+ confidence -= 15;
355
+ }
356
+ }
357
+ }
358
+
359
+ // 3. Check for hallucinated imports from wrong paths
360
+ if (targetPath.includes('/components/')) {
361
+ // UI component file - should not import from chains/approval etc.
362
+ if (/from\s+['"].*\/(chains|approval|workflow)/.test(code)) {
363
+ issues.push('UI component imports from non-UI paths (chains/approval/workflow)');
364
+ confidence -= 20;
365
+ }
366
+ }
367
+
368
+ // 4. Check export matches file name
369
+ if (fileBaseName && fileBaseName !== 'index') {
370
+ const hasMatchingExport = new RegExp(`export\\s+(default\\s+)?.*${escapeRegex(fileBaseName)}`, 'i').test(code);
371
+ if (!hasMatchingExport) {
372
+ confidence -= 5;
373
+ }
374
+ }
375
+
376
+ const valid = issues.length === 0 && confidence >= 50;
377
+
378
+ return {
379
+ valid,
380
+ reason: issues.length > 0 ? issues.join('; ') : undefined,
381
+ confidence,
382
+ issues
383
+ };
384
+ }
385
+
386
+ /**
387
+ * Escapes special regex characters in a string
388
+ */
389
+ function escapeRegex(string) {
390
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
391
+ }
392
+
393
+ // ============================================================
394
+ // Import Validation (Config-Driven)
395
+ // ============================================================
396
+
397
+ /**
398
+ * Validates imports in generated code against the export map.
399
+ * Uses the cached export map for accurate import validation.
400
+ *
401
+ * @param {string} code - The generated code
402
+ * @param {Object} exportMap - The export map (or null to load from cache)
403
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
404
+ */
405
+ function validateImports(code, exportMap = null) {
406
+ const errors = [];
407
+ const warnings = [];
408
+
409
+ // Load export map if not provided
410
+ if (!exportMap) {
411
+ exportMap = loadCachedExportMap();
412
+ if (!exportMap) {
413
+ // No export map available, can't validate
414
+ return { valid: true, errors: [], warnings: ['No export map available for validation'] };
415
+ }
416
+ }
417
+
418
+ // Load doNotImport from config
419
+ let doNotImport = ['React']; // Default
420
+ try {
421
+ const config = getConfig();
422
+ doNotImport = config.hybrid?.projectContext?.doNotImport || ['React'];
423
+ } catch {}
424
+
425
+ // Build a lookup map for all exports by import path
426
+ const exportsByPath = new Map();
427
+
428
+ // Add all exports from the map
429
+ for (const [category, items] of Object.entries(exportMap)) {
430
+ if (category === '_meta') continue;
431
+
432
+ for (const [name, info] of Object.entries(items)) {
433
+ if (!info.importPath) continue;
434
+
435
+ const exports = [];
436
+ if (info.exports?.length > 0) exports.push(...info.exports);
437
+ if (info.types?.length > 0) exports.push(...info.types);
438
+ if (info.defaultExport) exports.push(info.defaultExport);
439
+
440
+ exportsByPath.set(info.importPath, {
441
+ name,
442
+ exports,
443
+ defaultExport: info.defaultExport,
444
+ category
445
+ });
446
+ }
447
+ }
448
+
449
+ // Extract imports from code
450
+ const importMatches = code.match(/import\s+(?:type\s+)?(?:{[^}]*}|[\w*]+)?\s*(?:,\s*{[^}]*})?\s*from\s+['"]([^'"]+)['"]/g) || [];
451
+
452
+ for (const importLine of importMatches) {
453
+ // Extract the import path
454
+ const pathMatch = importLine.match(/from\s+['"]([^'"]+)['"]/);
455
+ if (!pathMatch) continue;
456
+
457
+ const importPath = pathMatch[1];
458
+
459
+ // Skip external packages
460
+ if (!importPath.startsWith('@/') && !importPath.startsWith('./') && !importPath.startsWith('../')) {
461
+ // Check doNotImport for external packages
462
+ for (const forbidden of doNotImport) {
463
+ if (importLine.includes(`import ${forbidden} `) ||
464
+ importLine.includes(`import ${forbidden},`) ||
465
+ importLine.includes(`import * as ${forbidden}`)) {
466
+ errors.push(`Forbidden import detected: "import ${forbidden}" - use named imports instead`);
467
+ }
468
+ }
469
+ continue;
470
+ }
471
+
472
+ // Check if import path exists in our export map
473
+ const knownExports = exportsByPath.get(importPath);
474
+
475
+ if (!knownExports) {
476
+ // Path not in export map - might be a relative import or unknown path
477
+ if (importPath.startsWith('@/')) {
478
+ warnings.push(`Import path "${importPath}" not found in export map - verify it exists`);
479
+ }
480
+ continue;
481
+ }
482
+
483
+ // Extract what's being imported
484
+ const namedImportsMatch = importLine.match(/{([^}]+)}/);
485
+ if (namedImportsMatch) {
486
+ const importedNames = namedImportsMatch[1]
487
+ .split(',')
488
+ .map(n => n.trim().split(/\s+as\s+/)[0].trim()) // Handle "X as Y"
489
+ .filter(n => n && n !== 'type'); // Filter out 'type' keyword
490
+
491
+ const availableExports = knownExports.exports || [];
492
+
493
+ for (const importedName of importedNames) {
494
+ if (importedName && !availableExports.includes(importedName)) {
495
+ const suggestions = availableExports.slice(0, 5).join(', ');
496
+ errors.push(`"${importedName}" is not exported by "${importPath}" - available: ${suggestions}`);
497
+ }
498
+ }
499
+ }
500
+
501
+ // Check default import
502
+ const defaultImportMatch = importLine.match(/import\s+(\w+)\s*(?:,|from)/);
503
+ if (defaultImportMatch) {
504
+ const defaultImportName = defaultImportMatch[1];
505
+ if (defaultImportName !== 'type' && !knownExports.defaultExport) {
506
+ // Check if they might want a named export
507
+ if (knownExports.exports.includes(defaultImportName)) {
508
+ warnings.push(`"${defaultImportName}" is a named export, not default - use: import { ${defaultImportName} } from '${importPath}'`);
509
+ } else {
510
+ errors.push(`"${importPath}" has no default export - use named imports instead`);
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ return {
517
+ valid: errors.length === 0,
518
+ errors,
519
+ warnings
520
+ };
521
+ }
522
+
523
+ // ============================================================
524
+ // Exports
525
+ // ============================================================
526
+
527
+ module.exports = {
528
+ // Code extraction
529
+ extractCodeFromResponse,
530
+ scoreCodeBlock,
531
+
532
+ // Code validation
533
+ isValidCode,
534
+ validateOutputMatchesTask,
535
+ validateImports,
536
+
537
+ // Utilities
538
+ escapeRegex
539
+ };