wogiflow 1.0.11 → 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/lib/unified-wizard.js +569 -30
- 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
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* Executes plans created by Claude using a local LLM.
|
|
7
7
|
* Updates all Wogi Flow state files after each step.
|
|
8
8
|
*
|
|
9
|
+
* LIMITATION: The --resume flag is documented but not yet implemented.
|
|
10
|
+
* Checkpoint data is saved but recovery logic needs completion.
|
|
11
|
+
* TODO: Implement resume from checkpoint functionality.
|
|
12
|
+
*
|
|
9
13
|
* Usage:
|
|
10
14
|
* flow-orchestrate <plan.json> # Execute a plan
|
|
11
|
-
* flow-orchestrate --resume # Resume from checkpoint
|
|
15
|
+
* flow-orchestrate --resume # Resume from checkpoint (NOT YET IMPLEMENTED)
|
|
12
16
|
* flow-orchestrate --rollback # Rollback last execution
|
|
13
17
|
*/
|
|
14
18
|
|
|
@@ -65,6 +69,14 @@ const {
|
|
|
65
69
|
// Import response parser for error recovery
|
|
66
70
|
const { parseOnRetry, cleanCodeBlock } = require('./flow-response-parser');
|
|
67
71
|
|
|
72
|
+
// Import validation module (extracted for modularity)
|
|
73
|
+
const {
|
|
74
|
+
extractCodeFromResponse,
|
|
75
|
+
isValidCode,
|
|
76
|
+
validateOutputMatchesTask,
|
|
77
|
+
validateImports
|
|
78
|
+
} = require('./flow-orchestrate-validation');
|
|
79
|
+
|
|
68
80
|
// Import adaptive learning for smart retries and model improvement
|
|
69
81
|
const {
|
|
70
82
|
analyzeFailure,
|
|
@@ -122,7 +134,7 @@ function saveStructuredFailure(step, errorHistory, attempts, config) {
|
|
|
122
134
|
errors: errorHistory.slice(-5).map(e => ({
|
|
123
135
|
category: e.category,
|
|
124
136
|
signature: e.signature,
|
|
125
|
-
message:
|
|
137
|
+
message: e.message?.slice(0, 500) || ''
|
|
126
138
|
})),
|
|
127
139
|
suggestion: generateFixSuggestion(errorHistory),
|
|
128
140
|
lastErrorCategory: errorHistory[errorHistory.length - 1]?.category || 'unknown'
|
|
@@ -211,511 +223,8 @@ function loadHybridConfig() {
|
|
|
211
223
|
};
|
|
212
224
|
}
|
|
213
225
|
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// ============================================================
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Extracts clean code from LLM response.
|
|
220
|
-
* Handles:
|
|
221
|
-
* - Thinking/reasoning preamble
|
|
222
|
-
* - </think> tags (from models that use thinking tokens)
|
|
223
|
-
* - Markdown code blocks
|
|
224
|
-
* - Trailing explanations
|
|
225
|
-
* - Model-specific artifacts (Llama, Qwen, DeepSeek, etc.)
|
|
226
|
-
* - JSON wrapper responses
|
|
227
|
-
* - Multiple code blocks (selects largest/most relevant)
|
|
228
|
-
*/
|
|
229
|
-
function extractCodeFromResponse(response, modelName = '') {
|
|
230
|
-
if (!response || typeof response !== 'string') {
|
|
231
|
-
return response;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const rawResponse = response;
|
|
235
|
-
let code = response;
|
|
236
|
-
|
|
237
|
-
// 0. Handle JSON wrapper responses (some models wrap code in JSON)
|
|
238
|
-
try {
|
|
239
|
-
const jsonMatch = code.match(/^\s*\{[\s\S]*"code"\s*:\s*"([\s\S]*)"[\s\S]*\}\s*$/);
|
|
240
|
-
if (jsonMatch) {
|
|
241
|
-
code = JSON.parse(`"${jsonMatch[1]}"`); // Unescape JSON string
|
|
242
|
-
}
|
|
243
|
-
} catch { /* not JSON wrapped */ }
|
|
244
|
-
|
|
245
|
-
// 1. Remove model-specific thinking tags and artifacts
|
|
246
|
-
const thinkingPatterns = [
|
|
247
|
-
// Standard thinking tags
|
|
248
|
-
/<think>[\s\S]*?<\/think>/gi,
|
|
249
|
-
/<thinking>[\s\S]*?<\/thinking>/gi,
|
|
250
|
-
/<reasoning>[\s\S]*?<\/reasoning>/gi,
|
|
251
|
-
/<analysis>[\s\S]*?<\/analysis>/gi,
|
|
252
|
-
|
|
253
|
-
// Qwen-specific
|
|
254
|
-
/<\|im_start\|>[\s\S]*?<\|im_end\|>/gi,
|
|
255
|
-
|
|
256
|
-
// DeepSeek-specific artifacts
|
|
257
|
-
/^<\|begin_of_sentence\|>/gm,
|
|
258
|
-
/<\|end_of_sentence\|>$/gm,
|
|
259
|
-
|
|
260
|
-
// Llama-specific
|
|
261
|
-
/\[INST\][\s\S]*?\[\/INST\]/gi,
|
|
262
|
-
/<<SYS>>[\s\S]*?<<\/SYS>>/gi,
|
|
263
|
-
|
|
264
|
-
// Generic assistant markers
|
|
265
|
-
/^Assistant:\s*/gim,
|
|
266
|
-
/^AI:\s*/gim,
|
|
267
|
-
/^Response:\s*/gim,
|
|
268
|
-
/^Output:\s*/gim,
|
|
269
|
-
/^Answer:\s*/gim,
|
|
270
|
-
/^Code:\s*/gim,
|
|
271
|
-
|
|
272
|
-
// Model-specific trailing signatures
|
|
273
|
-
/---\s*End of (response|code|file)[\s\S]*$/gi,
|
|
274
|
-
/\n\nPlease let me know[\s\S]*$/gi,
|
|
275
|
-
/\n\nIs there anything[\s\S]*$/gi,
|
|
276
|
-
/\n\nFeel free to[\s\S]*$/gi,
|
|
277
|
-
/\n\nLet me know if[\s\S]*$/gi,
|
|
278
|
-
];
|
|
279
|
-
|
|
280
|
-
for (const pattern of thinkingPatterns) {
|
|
281
|
-
code = code.replace(pattern, '');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// 2. Handle </think> tag (if partial tag remains)
|
|
285
|
-
const thinkEndMatch = code.match(/<\/think>\s*/i);
|
|
286
|
-
if (thinkEndMatch) {
|
|
287
|
-
code = code.slice(thinkEndMatch.index + thinkEndMatch[0].length);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// 3. Extract from markdown code blocks
|
|
291
|
-
// Find all code blocks and pick the best one
|
|
292
|
-
const codeBlocks = [...code.matchAll(/```(?:typescript|tsx|ts|javascript|jsx|js|plaintext)?\s*\n([\s\S]*?)```/g)];
|
|
293
|
-
|
|
294
|
-
if (codeBlocks.length > 0) {
|
|
295
|
-
// Score each block and pick the best one
|
|
296
|
-
let bestBlock = codeBlocks[0][1];
|
|
297
|
-
let bestScore = scoreCodeBlock(bestBlock);
|
|
298
|
-
|
|
299
|
-
for (let i = 1; i < codeBlocks.length; i++) {
|
|
300
|
-
const blockContent = codeBlocks[i][1];
|
|
301
|
-
const score = scoreCodeBlock(blockContent);
|
|
302
|
-
if (score > bestScore) {
|
|
303
|
-
bestScore = score;
|
|
304
|
-
bestBlock = blockContent;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
code = bestBlock;
|
|
308
|
-
} else {
|
|
309
|
-
// Also try to remove any remaining markdown code block markers
|
|
310
|
-
code = code.replace(/^```(?:typescript|tsx|javascript|jsx|ts|js|plaintext)?\n/gm, '');
|
|
311
|
-
code = code.replace(/\n```$/gm, '');
|
|
312
|
-
code = code.replace(/^```$/gm, '');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// 4. Find first valid TypeScript/JavaScript line
|
|
316
|
-
const validStartPatterns = [
|
|
317
|
-
/^import\s/m,
|
|
318
|
-
/^export\s/m,
|
|
319
|
-
/^const\s/m,
|
|
320
|
-
/^let\s/m,
|
|
321
|
-
/^var\s/m,
|
|
322
|
-
/^function\s/m,
|
|
323
|
-
/^async\s+function\s/m,
|
|
324
|
-
/^class\s/m,
|
|
325
|
-
/^interface\s/m,
|
|
326
|
-
/^type\s/m,
|
|
327
|
-
/^enum\s/m,
|
|
328
|
-
/^declare\s/m,
|
|
329
|
-
/^module\s/m,
|
|
330
|
-
/^namespace\s/m,
|
|
331
|
-
/^\/\*\*/m, // JSDoc comment
|
|
332
|
-
/^\/\*[^*]/m, // Block comment
|
|
333
|
-
/^\/\//m, // Single line comment at start
|
|
334
|
-
/^'use /m, // 'use strict' or 'use client'
|
|
335
|
-
/^"use /m,
|
|
336
|
-
/^@/m, // Decorators
|
|
337
|
-
];
|
|
338
|
-
|
|
339
|
-
let earliestMatch = -1;
|
|
340
|
-
for (const pattern of validStartPatterns) {
|
|
341
|
-
const match = code.search(pattern);
|
|
342
|
-
if (match !== -1 && (earliestMatch === -1 || match < earliestMatch)) {
|
|
343
|
-
earliestMatch = match;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (earliestMatch > 0) {
|
|
348
|
-
code = code.slice(earliestMatch);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// 5. Remove trailing explanations and prose
|
|
352
|
-
const trailingPatterns = [
|
|
353
|
-
// Standard prose after code
|
|
354
|
-
/(\}|\;)\s*\n\s*\n+[A-Z][a-z]/,
|
|
355
|
-
// Numbered explanations
|
|
356
|
-
/(\}|\;)\s*\n\s*\n+\d+\.\s+/,
|
|
357
|
-
// Bullet points
|
|
358
|
-
/(\}|\;)\s*\n\s*\n+[-*•]\s+/,
|
|
359
|
-
// Notes/explanations
|
|
360
|
-
/(\}|\;)\s*\n\s*\n+(?:Note:|Explanation:|Summary:|Key |Important:)/i,
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
for (const pattern of trailingPatterns) {
|
|
364
|
-
const match = code.match(pattern);
|
|
365
|
-
if (match) {
|
|
366
|
-
code = code.slice(0, match.index + 1);
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// 6. Clean up common artifacts
|
|
372
|
-
code = code
|
|
373
|
-
// Remove zero-width characters
|
|
374
|
-
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
375
|
-
// Normalize line endings
|
|
376
|
-
.replace(/\r\n/g, '\n')
|
|
377
|
-
.replace(/\r/g, '\n')
|
|
378
|
-
// Remove trailing whitespace on each line
|
|
379
|
-
.replace(/[ \t]+$/gm, '')
|
|
380
|
-
// Collapse multiple blank lines to max 2
|
|
381
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
382
|
-
.trim();
|
|
383
|
-
|
|
384
|
-
// Debug logging
|
|
385
|
-
if (process.env.DEBUG_HYBRID) {
|
|
386
|
-
console.log('\n--- RAW LLM RESPONSE (first 500 chars) ---');
|
|
387
|
-
console.log(rawResponse.slice(0, 500));
|
|
388
|
-
console.log('\n--- EXTRACTED CODE (first 500 chars) ---');
|
|
389
|
-
console.log(code.slice(0, 500));
|
|
390
|
-
console.log('---\n');
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return code;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Score a code block to determine which is most likely the actual code
|
|
398
|
-
* Higher score = more likely to be the real code
|
|
399
|
-
*/
|
|
400
|
-
function scoreCodeBlock(block) {
|
|
401
|
-
if (!block) return 0;
|
|
402
|
-
|
|
403
|
-
let score = 0;
|
|
404
|
-
|
|
405
|
-
// Length bonus (longer is usually better, but cap it)
|
|
406
|
-
score += Math.min(block.length / 100, 50);
|
|
407
|
-
|
|
408
|
-
// Valid code patterns
|
|
409
|
-
if (/^import\s/m.test(block)) score += 20;
|
|
410
|
-
if (/^export\s/m.test(block)) score += 20;
|
|
411
|
-
if (/^const\s/m.test(block)) score += 10;
|
|
412
|
-
if (/^function\s/m.test(block)) score += 10;
|
|
413
|
-
if (/^class\s/m.test(block)) score += 10;
|
|
414
|
-
if (/^interface\s/m.test(block)) score += 15;
|
|
415
|
-
if (/^type\s/m.test(block)) score += 10;
|
|
416
|
-
|
|
417
|
-
// Code structure indicators
|
|
418
|
-
score += (block.match(/\{/g) || []).length * 2;
|
|
419
|
-
score += (block.match(/\}/g) || []).length * 2;
|
|
420
|
-
score += (block.match(/=>/g) || []).length * 3;
|
|
421
|
-
score += (block.match(/return\s/g) || []).length * 3;
|
|
422
|
-
|
|
423
|
-
// Penalties for prose/non-code
|
|
424
|
-
if (/^[A-Z][a-z]+\s+[a-z]+/m.test(block)) score -= 10; // Starts with prose
|
|
425
|
-
if (/\.$/.test(block.trim())) score -= 5; // Ends with period (prose)
|
|
426
|
-
|
|
427
|
-
return score;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Validates if the extracted code looks like valid TypeScript/JavaScript.
|
|
432
|
-
* Returns { valid: boolean, reason?: string }
|
|
433
|
-
*/
|
|
434
|
-
function isValidCode(code) {
|
|
435
|
-
if (!code) {
|
|
436
|
-
return { valid: false, reason: 'Empty output' };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (code.length < 10) {
|
|
440
|
-
return { valid: false, reason: 'Output too short' };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const trimmed = code.trim();
|
|
444
|
-
|
|
445
|
-
// Check for common LLM prose patterns that indicate thinking/explanation
|
|
446
|
-
const prosePatterns = [
|
|
447
|
-
/^(We need|Let's|The |I |You |This |Maybe|Probably|Actually|But |So |Thus |Given |Here|Now |First|To |In order)/i,
|
|
448
|
-
/^(Looking at|Based on|According to|As you can|Note that|Remember|Consider|Thinking|Output:)/i,
|
|
449
|
-
/^(```|~~~)/, // Markdown code fence at start means extraction failed
|
|
450
|
-
/<think>|<\/think>/i, // Thinking tags leaked through
|
|
451
|
-
];
|
|
452
|
-
|
|
453
|
-
for (const pattern of prosePatterns) {
|
|
454
|
-
if (pattern.test(trimmed)) {
|
|
455
|
-
return { valid: false, reason: `Starts with prose/thinking: "${trimmed.slice(0, 50)}..."` };
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Must start with valid TS/JS syntax
|
|
460
|
-
const validStartPatterns = /^(import|export|const|let|var|function|async|class|interface|type|enum|declare|module|namespace|\/\*\*|\/\*|\/\/|'use |"use |@)/;
|
|
461
|
-
|
|
462
|
-
if (!validStartPatterns.test(trimmed)) {
|
|
463
|
-
return { valid: false, reason: `Invalid start: "${trimmed.slice(0, 50)}..."` };
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Additional sanity checks
|
|
467
|
-
// Should have some code-like structure (braces, semicolons, etc.)
|
|
468
|
-
const hasCodeStructure = /[{};=()]/.test(code);
|
|
469
|
-
if (!hasCodeStructure && code.length > 100) {
|
|
470
|
-
return { valid: false, reason: 'No code structure detected (missing braces/semicolons)' };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return { valid: true };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ============================================================
|
|
477
|
-
// Semantic Output Validation
|
|
478
|
-
// ============================================================
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Validates that the output semantically matches what was requested.
|
|
482
|
-
* This catches cases where the code is syntactically valid but implements
|
|
483
|
-
* the wrong thing (e.g., creating ApprovalChain instead of Button).
|
|
484
|
-
*
|
|
485
|
-
* @param {string} code - The generated code
|
|
486
|
-
* @param {Object} step - The step definition containing type and params
|
|
487
|
-
* @returns {{ valid: boolean, reason?: string, confidence: number }}
|
|
488
|
-
*/
|
|
489
|
-
function validateOutputMatchesTask(code, step) {
|
|
490
|
-
if (!code || !step) {
|
|
491
|
-
return { valid: true, confidence: 0 }; // Can't validate without info
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const stepType = step.type;
|
|
495
|
-
const expectedName = step.params?.name || step.params?.componentName || '';
|
|
496
|
-
const targetPath = step.params?.path || '';
|
|
497
|
-
const codeLower = code.toLowerCase();
|
|
498
|
-
const issues = [];
|
|
499
|
-
let confidence = 100;
|
|
500
|
-
|
|
501
|
-
// Extract the expected filename/component name from path
|
|
502
|
-
const fileBaseName = targetPath
|
|
503
|
-
? path.basename(targetPath, path.extname(targetPath))
|
|
504
|
-
: expectedName;
|
|
505
|
-
|
|
506
|
-
// 1. Check if expected name appears in the code
|
|
507
|
-
if (fileBaseName && fileBaseName.length > 2) {
|
|
508
|
-
const namePattern = new RegExp(`\\b${escapeRegex(fileBaseName)}\\b`, 'i');
|
|
509
|
-
if (!namePattern.test(code)) {
|
|
510
|
-
issues.push(`Expected "${fileBaseName}" not found in output`);
|
|
511
|
-
confidence -= 40;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// 2. Check step-type specific patterns
|
|
516
|
-
switch (stepType) {
|
|
517
|
-
case 'create-component':
|
|
518
|
-
// Should have a function/const that exports a component
|
|
519
|
-
if (!/export\s+(default\s+)?function|export\s+(default\s+)?const/.test(code)) {
|
|
520
|
-
issues.push('No exported function/const found for component');
|
|
521
|
-
confidence -= 30;
|
|
522
|
-
}
|
|
523
|
-
// Should have JSX (tsx file)
|
|
524
|
-
if (targetPath.endsWith('.tsx') && !/<[A-Z]|<[a-z]+\s|<\//.test(code)) {
|
|
525
|
-
issues.push('No JSX found in .tsx component');
|
|
526
|
-
confidence -= 20;
|
|
527
|
-
}
|
|
528
|
-
break;
|
|
529
|
-
|
|
530
|
-
case 'create-hook':
|
|
531
|
-
// Should have a use* function
|
|
532
|
-
if (!/function\s+use[A-Z]|const\s+use[A-Z]/.test(code)) {
|
|
533
|
-
issues.push('No use* hook function found');
|
|
534
|
-
confidence -= 50;
|
|
535
|
-
}
|
|
536
|
-
break;
|
|
537
|
-
|
|
538
|
-
case 'create-service':
|
|
539
|
-
// Should have exports (functions or class)
|
|
540
|
-
if (!/export\s+(const|function|class|async)/.test(code)) {
|
|
541
|
-
issues.push('No exports found in service');
|
|
542
|
-
confidence -= 30;
|
|
543
|
-
}
|
|
544
|
-
break;
|
|
545
|
-
|
|
546
|
-
case 'modify-file':
|
|
547
|
-
// For modifications, the expected changes should be present
|
|
548
|
-
// This is harder to validate without more context
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// 3. Check for common "wrong thing" patterns
|
|
553
|
-
// If the code exports something completely different from expected name
|
|
554
|
-
const exportMatches = code.match(/export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/g) || [];
|
|
555
|
-
if (exportMatches.length > 0 && fileBaseName) {
|
|
556
|
-
const exportNames = exportMatches.map(m => {
|
|
557
|
-
const parts = m.split(/\s+/);
|
|
558
|
-
return parts[parts.length - 1];
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// Check if any export is similar to expected name
|
|
562
|
-
const hasMatchingExport = exportNames.some(name =>
|
|
563
|
-
name.toLowerCase().includes(fileBaseName.toLowerCase()) ||
|
|
564
|
-
fileBaseName.toLowerCase().includes(name.toLowerCase())
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
if (!hasMatchingExport && exportNames.length > 0) {
|
|
568
|
-
issues.push(`Exports [${exportNames.join(', ')}] but expected "${fileBaseName}"`);
|
|
569
|
-
confidence -= 30;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Validation result
|
|
574
|
-
const valid = confidence >= 50;
|
|
575
|
-
return {
|
|
576
|
-
valid,
|
|
577
|
-
reason: issues.length > 0 ? issues.join('; ') : undefined,
|
|
578
|
-
confidence,
|
|
579
|
-
issues
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Escapes special regex characters in a string
|
|
585
|
-
*/
|
|
586
|
-
function escapeRegex(string) {
|
|
587
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// ============================================================
|
|
591
|
-
// Import Validation (Config-Driven)
|
|
592
|
-
// ============================================================
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Validates imports in generated code against the export map.
|
|
596
|
-
* Uses the cached export map for accurate import validation.
|
|
597
|
-
*
|
|
598
|
-
* @param {string} code - The generated code
|
|
599
|
-
* @param {Object} exportMap - The export map (or null to load from cache)
|
|
600
|
-
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
601
|
-
*/
|
|
602
|
-
function validateImports(code, exportMap = null) {
|
|
603
|
-
const errors = [];
|
|
604
|
-
const warnings = [];
|
|
605
|
-
|
|
606
|
-
// Load export map if not provided
|
|
607
|
-
if (!exportMap) {
|
|
608
|
-
exportMap = loadCachedExportMap();
|
|
609
|
-
if (!exportMap) {
|
|
610
|
-
// No export map available, can't validate
|
|
611
|
-
return { valid: true, errors: [], warnings: ['No export map available for validation'] };
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Load doNotImport from config
|
|
616
|
-
let doNotImport = ['React']; // Default
|
|
617
|
-
try {
|
|
618
|
-
const config = getConfig();
|
|
619
|
-
doNotImport = config.hybrid?.projectContext?.doNotImport || ['React'];
|
|
620
|
-
} catch {}
|
|
621
|
-
|
|
622
|
-
// Build a lookup map for all exports by import path
|
|
623
|
-
const exportsByPath = new Map();
|
|
624
|
-
|
|
625
|
-
// Add all exports from the map
|
|
626
|
-
for (const [category, items] of Object.entries(exportMap)) {
|
|
627
|
-
if (category === '_meta') continue;
|
|
628
|
-
|
|
629
|
-
for (const [name, info] of Object.entries(items)) {
|
|
630
|
-
if (!info.importPath) continue;
|
|
631
|
-
|
|
632
|
-
const exports = [];
|
|
633
|
-
if (info.exports?.length > 0) exports.push(...info.exports);
|
|
634
|
-
if (info.types?.length > 0) exports.push(...info.types);
|
|
635
|
-
if (info.defaultExport) exports.push(info.defaultExport);
|
|
636
|
-
|
|
637
|
-
exportsByPath.set(info.importPath, {
|
|
638
|
-
name,
|
|
639
|
-
exports,
|
|
640
|
-
defaultExport: info.defaultExport,
|
|
641
|
-
category
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Extract imports from code
|
|
647
|
-
const importMatches = code.match(/import\s+(?:type\s+)?(?:{[^}]*}|[\w*]+)?\s*(?:,\s*{[^}]*})?\s*from\s+['"]([^'"]+)['"]/g) || [];
|
|
648
|
-
|
|
649
|
-
for (const importLine of importMatches) {
|
|
650
|
-
// Extract the import path
|
|
651
|
-
const pathMatch = importLine.match(/from\s+['"]([^'"]+)['"]/);
|
|
652
|
-
if (!pathMatch) continue;
|
|
653
|
-
|
|
654
|
-
const importPath = pathMatch[1];
|
|
655
|
-
|
|
656
|
-
// Skip external packages
|
|
657
|
-
if (!importPath.startsWith('@/') && !importPath.startsWith('./') && !importPath.startsWith('../')) {
|
|
658
|
-
// Check doNotImport for external packages
|
|
659
|
-
for (const forbidden of doNotImport) {
|
|
660
|
-
if (importLine.includes(`import ${forbidden} `) ||
|
|
661
|
-
importLine.includes(`import ${forbidden},`) ||
|
|
662
|
-
importLine.includes(`import * as ${forbidden}`)) {
|
|
663
|
-
errors.push(`Forbidden import detected: "import ${forbidden}" - use named imports instead`);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
continue;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Check if import path exists in our export map
|
|
670
|
-
const knownExports = exportsByPath.get(importPath);
|
|
671
|
-
|
|
672
|
-
if (!knownExports) {
|
|
673
|
-
// Path not in export map - might be a relative import or unknown path
|
|
674
|
-
if (importPath.startsWith('@/')) {
|
|
675
|
-
warnings.push(`Import path "${importPath}" not found in export map - verify it exists`);
|
|
676
|
-
}
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Extract what's being imported
|
|
681
|
-
const namedImportsMatch = importLine.match(/{([^}]+)}/);
|
|
682
|
-
if (namedImportsMatch) {
|
|
683
|
-
const importedNames = namedImportsMatch[1]
|
|
684
|
-
.split(',')
|
|
685
|
-
.map(n => n.trim().split(/\s+as\s+/)[0].trim()) // Handle "X as Y"
|
|
686
|
-
.filter(n => n && n !== 'type'); // Filter out 'type' keyword
|
|
687
|
-
|
|
688
|
-
const availableExports = knownExports.exports || [];
|
|
689
|
-
|
|
690
|
-
for (const importedName of importedNames) {
|
|
691
|
-
if (importedName && !availableExports.includes(importedName)) {
|
|
692
|
-
const suggestions = availableExports.slice(0, 5).join(', ');
|
|
693
|
-
errors.push(`"${importedName}" is not exported by "${importPath}" - available: ${suggestions}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Check default import
|
|
699
|
-
const defaultImportMatch = importLine.match(/import\s+(\w+)\s*(?:,|from)/);
|
|
700
|
-
if (defaultImportMatch) {
|
|
701
|
-
const defaultImportName = defaultImportMatch[1];
|
|
702
|
-
if (defaultImportName !== 'type' && !knownExports.defaultExport) {
|
|
703
|
-
// Check if they might want a named export
|
|
704
|
-
if (knownExports.exports.includes(defaultImportName)) {
|
|
705
|
-
warnings.push(`"${defaultImportName}" is a named export, not default - use: import { ${defaultImportName} } from '${importPath}'`);
|
|
706
|
-
} else {
|
|
707
|
-
errors.push(`"${importPath}" has no default export - use named imports instead`);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
valid: errors.length === 0,
|
|
715
|
-
errors,
|
|
716
|
-
warnings
|
|
717
|
-
};
|
|
718
|
-
}
|
|
226
|
+
// NOTE: Code extraction and validation functions moved to flow-orchestrate-validation.js
|
|
227
|
+
// Functions: extractCodeFromResponse, scoreCodeBlock, isValidCode, validateOutputMatchesTask, validateImports, escapeRegex
|
|
719
228
|
|
|
720
229
|
// ============================================================
|
|
721
230
|
// Auto-Correction for Common LLM Mistakes
|