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.
- package/.workflow/specs/architecture.md.template +24 -0
- package/.workflow/specs/stack.md.template +33 -0
- package/.workflow/specs/testing.md.template +36 -0
- package/README.md +90 -1
- package/package.json +1 -1
- package/scripts/MEMORY-ARCHITECTURE.md +150 -0
- package/scripts/flow +20 -19
- package/scripts/flow-auto-context.js +97 -3
- package/scripts/flow-conflict-resolver.js +735 -0
- package/scripts/flow-context-gatherer.js +520 -0
- package/scripts/flow-context-monitor.js +148 -19
- package/scripts/flow-damage-control.js +5 -1
- package/scripts/flow-export-profile +168 -1
- package/scripts/flow-import-profile +257 -6
- package/scripts/flow-instruction-richness.js +182 -18
- package/scripts/flow-knowledge-router.js +2 -0
- package/scripts/flow-knowledge-sync.js +2 -0
- package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
- package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
- package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
- package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
- package/scripts/flow-memory-db.js +386 -1
- package/scripts/flow-memory-sync.js +2 -0
- package/scripts/flow-model-adapter.js +53 -29
- package/scripts/flow-model-router.js +246 -1
- package/scripts/flow-morning.js +94 -0
- package/scripts/flow-onboard +223 -10
- package/scripts/flow-orchestrate-validation.js +539 -0
- package/scripts/flow-orchestrate.js +16 -507
- package/scripts/flow-pattern-extractor.js +1265 -0
- package/scripts/flow-prompt-composer.js +222 -2
- package/scripts/flow-quality-guard.js +594 -0
- package/scripts/flow-section-index.js +713 -0
- package/scripts/flow-section-resolver.js +484 -0
- package/scripts/flow-session-end.js +188 -2
- package/scripts/flow-skill-create.js +19 -3
- package/scripts/flow-skill-matcher.js +122 -7
- package/scripts/flow-statusline-setup.js +218 -0
- package/scripts/flow-step-review.js +19 -0
- package/scripts/flow-tech-debt.js +734 -0
- package/scripts/flow-utils.js +2 -0
- package/scripts/hooks/core/long-input-gate.js +293 -0
- package/scripts/flow-parallel-detector.js +0 -399
- package/scripts/flow-parallel-dispatch.js +0 -987
- /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
|
+
};
|