vigthoria-cli 1.6.24 → 1.6.25
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/dist/commands/chat.d.ts +7 -0
- package/dist/commands/chat.js +24 -2
- package/dist/commands/generate.d.ts +5 -0
- package/dist/commands/generate.js +39 -0
- package/dist/index.js +2 -2
- package/dist/utils/api.d.ts +24 -0
- package/dist/utils/api.js +222 -28
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare class ChatCommand {
|
|
|
75
75
|
private slugifyWorkspaceName;
|
|
76
76
|
private allocateManagedWorkspacePath;
|
|
77
77
|
private initializeSession;
|
|
78
|
+
/**
|
|
79
|
+
* Determine whether a direct --prompt call is simple enough to skip the
|
|
80
|
+
* full V3 Agent planner and answer directly via the simple chat pipeline.
|
|
81
|
+
* This prevents trivial prompts like "Reply with OK" from triggering a
|
|
82
|
+
* multi-step planning workflow with quality_profile: premium-visual-build.
|
|
83
|
+
*/
|
|
84
|
+
private isSimpleDirectPrompt;
|
|
78
85
|
private handleDirectPrompt;
|
|
79
86
|
private formatWorkflowTargetResult;
|
|
80
87
|
private runWorkflowTargetPrompt;
|
package/dist/commands/chat.js
CHANGED
|
@@ -576,6 +576,27 @@ class ChatCommand {
|
|
|
576
576
|
this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
|
|
577
577
|
this.messages = [...this.currentSession.messages];
|
|
578
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Determine whether a direct --prompt call is simple enough to skip the
|
|
581
|
+
* full V3 Agent planner and answer directly via the simple chat pipeline.
|
|
582
|
+
* This prevents trivial prompts like "Reply with OK" from triggering a
|
|
583
|
+
* multi-step planning workflow with quality_profile: premium-visual-build.
|
|
584
|
+
*/
|
|
585
|
+
isSimpleDirectPrompt(prompt) {
|
|
586
|
+
const trimmed = prompt.trim();
|
|
587
|
+
// Short prompts (≤ 12 words) that don't reference files, builds, or code tasks
|
|
588
|
+
const wordCount = trimmed.split(/\s+/).length;
|
|
589
|
+
if (wordCount <= 12 && !/(create|build|generate|implement|write.*function|add.*feature|refactor|deploy|fix|edit|modify|update|delete|remove)\b/i.test(trimmed)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
// Conversational / Q&A prompts
|
|
593
|
+
if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should|tell|explain|describe|list|show|help|reply|say|respond|answer|translate|summarize|define)\b/i.test(trimmed)) {
|
|
594
|
+
if (!/(file|code|project|workspace|repo|module|component|function|class|api|endpoint|route|database|schema|migration|docker|deploy|build|test)\b/i.test(trimmed)) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
579
600
|
async handleDirectPrompt(prompt) {
|
|
580
601
|
if (!this.jsonOutput) {
|
|
581
602
|
console.log(chalk_1.default.cyan('Running single prompt in direct mode.'));
|
|
@@ -590,7 +611,8 @@ class ChatCommand {
|
|
|
590
611
|
await this.runWorkflowTargetPrompt(prompt);
|
|
591
612
|
return;
|
|
592
613
|
}
|
|
593
|
-
|
|
614
|
+
// Smart routing: skip agent planner for simple/conversational prompts
|
|
615
|
+
if (this.agentMode && !this.isSimpleDirectPrompt(prompt)) {
|
|
594
616
|
await this.runAgentTurn(prompt);
|
|
595
617
|
return;
|
|
596
618
|
}
|
|
@@ -1611,7 +1633,7 @@ class ChatCommand {
|
|
|
1611
1633
|
if (this.isServerBindableWorkspace(this.currentProjectPath)) {
|
|
1612
1634
|
return false;
|
|
1613
1635
|
}
|
|
1614
|
-
return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate)/i.test(prompt);
|
|
1636
|
+
return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate|list|show|find|search|check|scan|count|what|describe)/i.test(prompt);
|
|
1615
1637
|
}
|
|
1616
1638
|
shouldRequireV3AgentWorkflow(prompt) {
|
|
1617
1639
|
if (!this.directPromptMode) {
|
|
@@ -28,5 +28,10 @@ export declare class GenerateCommand {
|
|
|
28
28
|
private saveToFile;
|
|
29
29
|
private promptForAction;
|
|
30
30
|
private suggestFilename;
|
|
31
|
+
/**
|
|
32
|
+
* Auto-detect the target language from the user's description.
|
|
33
|
+
* Falls back to 'javascript' if no language is recognized.
|
|
34
|
+
*/
|
|
35
|
+
private detectLanguageFromDescription;
|
|
31
36
|
}
|
|
32
37
|
export {};
|
|
@@ -33,6 +33,10 @@ class GenerateCommand {
|
|
|
33
33
|
}
|
|
34
34
|
// Determine mode
|
|
35
35
|
const proMode = options.pro === true;
|
|
36
|
+
// Auto-detect language from description if not explicitly specified
|
|
37
|
+
if (!options.language) {
|
|
38
|
+
options.language = this.detectLanguageFromDescription(description);
|
|
39
|
+
}
|
|
36
40
|
this.logger.section(proMode ? '🚀 Senior Developer Mode' : 'Code Generation');
|
|
37
41
|
console.log(chalk_1.default.gray(`Language: ${options.language}`));
|
|
38
42
|
console.log(chalk_1.default.gray(`Description: ${description}`));
|
|
@@ -214,5 +218,40 @@ class GenerateCommand {
|
|
|
214
218
|
};
|
|
215
219
|
return extensions[language] || 'generated.txt';
|
|
216
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Auto-detect the target language from the user's description.
|
|
223
|
+
* Falls back to 'javascript' if no language is recognized.
|
|
224
|
+
*/
|
|
225
|
+
detectLanguageFromDescription(description) {
|
|
226
|
+
const lower = description.toLowerCase();
|
|
227
|
+
const patterns = [
|
|
228
|
+
[/\b(typescript|\.ts\b)/i, 'typescript'],
|
|
229
|
+
[/\b(javascript|\.js\b|node\.?js|es6|ecmascript)\b/i, 'javascript'],
|
|
230
|
+
[/\b(python|\.py\b|django|flask|fastapi)\b/i, 'python'],
|
|
231
|
+
[/\b(rust|\.rs\b|cargo)\b/i, 'rust'],
|
|
232
|
+
[/\b(go|golang|\.go\b)\b/i, 'go'],
|
|
233
|
+
[/\b(java\b|\.java\b|spring\b)/i, 'java'],
|
|
234
|
+
[/\b(c#|csharp|\.cs\b|dotnet|\.net)\b/i, 'csharp'],
|
|
235
|
+
[/\b(c\+\+|cpp|\.cpp\b|\.hpp\b)\b/i, 'cpp'],
|
|
236
|
+
[/\b(ruby|\.rb\b|rails)\b/i, 'ruby'],
|
|
237
|
+
[/\b(php|\.php\b|laravel)\b/i, 'php'],
|
|
238
|
+
[/\b(swift|\.swift\b|swiftui)\b/i, 'swift'],
|
|
239
|
+
[/\b(kotlin|\.kt\b)\b/i, 'kotlin'],
|
|
240
|
+
[/\b(html|\.html\b|webpage|web page|website|landing page)\b/i, 'html'],
|
|
241
|
+
[/\b(css|\.css\b|stylesheet)\b/i, 'css'],
|
|
242
|
+
[/\b(sql|\.sql\b|database query|select\s+\*)\b/i, 'sql'],
|
|
243
|
+
[/\b(bash|shell|\.sh\b|script)\b/i, 'bash'],
|
|
244
|
+
];
|
|
245
|
+
for (const [regex, lang] of patterns) {
|
|
246
|
+
if (regex.test(lower)) {
|
|
247
|
+
return lang;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// If the description mentions "function" without any language hint, default to javascript
|
|
251
|
+
if (/\bfunction\b/i.test(lower)) {
|
|
252
|
+
return 'javascript';
|
|
253
|
+
}
|
|
254
|
+
return 'javascript';
|
|
255
|
+
}
|
|
217
256
|
}
|
|
218
257
|
exports.GenerateCommand = GenerateCommand;
|
package/dist/index.js
CHANGED
|
@@ -98,7 +98,7 @@ function getVersion() {
|
|
|
98
98
|
catch (e) {
|
|
99
99
|
// Fallback to hardcoded version
|
|
100
100
|
}
|
|
101
|
-
return '1.6.
|
|
101
|
+
return '1.6.25';
|
|
102
102
|
}
|
|
103
103
|
const VERSION = getVersion();
|
|
104
104
|
/**
|
|
@@ -334,7 +334,7 @@ async function main() {
|
|
|
334
334
|
.command('generate <description>')
|
|
335
335
|
.alias('g')
|
|
336
336
|
.description('Generate code from description')
|
|
337
|
-
.option('-l, --language <lang>', 'Target language (
|
|
337
|
+
.option('-l, --language <lang>', 'Target language (auto-detected from description, or specify: javascript, typescript, python, html, etc.)')
|
|
338
338
|
.option('-o, --output <file>', 'Output file path')
|
|
339
339
|
.option('-m, --model <model>', 'Select AI model', 'code')
|
|
340
340
|
.option('-p, --pro', 'Senior Developer Mode: plan, generate, quality check (recommended)', false)
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -320,6 +320,30 @@ export declare class APIClient {
|
|
|
320
320
|
chatStream(messages: ChatMessage[], model: string): AsyncGenerator<StreamChunk>;
|
|
321
321
|
chatWithCallback(messages: ChatMessage[], model: string, onChunk: (chunk: string) => void, onDone: () => void, onError: (error: Error) => void): Promise<void>;
|
|
322
322
|
generateCode(prompt: string, language: string, model: string): Promise<string>;
|
|
323
|
+
/**
|
|
324
|
+
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
325
|
+
*/
|
|
326
|
+
private ensureBalancedBraces;
|
|
327
|
+
/**
|
|
328
|
+
* Extract the first complete function/class from code.
|
|
329
|
+
* Used as last-resort when the model keeps over-producing.
|
|
330
|
+
*/
|
|
331
|
+
private extractFirstFunction;
|
|
332
|
+
/**
|
|
333
|
+
* Detect if code is excessively over-engineered for a short prompt.
|
|
334
|
+
* E.g. a "multiply function" request producing 20+ lines.
|
|
335
|
+
*/
|
|
336
|
+
private codeIsOverEngineered;
|
|
337
|
+
/**
|
|
338
|
+
* Detect if generated code contains DOM/HTML pollution inappropriate
|
|
339
|
+
* for a pure programming language like JavaScript, Python, etc.
|
|
340
|
+
*/
|
|
341
|
+
private codeContainsDomPollution;
|
|
342
|
+
/**
|
|
343
|
+
* Strip DOM pollution from generated code, keeping only the pure logic.
|
|
344
|
+
* Used as last-resort fallback when the model repeatedly ignores constraints.
|
|
345
|
+
*/
|
|
346
|
+
private stripDomPollution;
|
|
323
347
|
generateProject(prompt: string, projectType: string, model: string): Promise<{
|
|
324
348
|
code: string;
|
|
325
349
|
plan?: any;
|
package/dist/utils/api.js
CHANGED
|
@@ -3242,31 +3242,171 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3242
3242
|
}
|
|
3243
3243
|
// Code operations - Using Vigthoria Centralized API
|
|
3244
3244
|
async generateCode(prompt, language, model) {
|
|
3245
|
+
const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
|
|
3246
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
3245
3247
|
// Prepend a forceful scope-enforcement instruction so the model
|
|
3246
3248
|
// doesn't expand a small task into an oversized glossy page.
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3249
|
+
const buildScopedPrompt = (retry) => {
|
|
3250
|
+
if (retry && isNonHtmlLang) {
|
|
3251
|
+
// Ultra-minimal retry prompt — no long instruction block
|
|
3252
|
+
return `OUTPUT ONLY RAW ${language.toUpperCase()} CODE. MAXIMUM 10 LINES. NO DOM. NO HTML. NO CSS. NO ANIMATION. NO VALIDATION. NO COMMENTS.\n\n${prompt}`;
|
|
3253
|
+
}
|
|
3254
|
+
const lines = [
|
|
3255
|
+
'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
|
|
3256
|
+
`1. You MUST output ONLY ${language.toUpperCase()} code. Do NOT output HTML, CSS, or DOM manipulation unless the language is html/css.`,
|
|
3257
|
+
`2. The target language is: ${language}. Do not switch to a different language.`,
|
|
3258
|
+
'3. Output ONLY what the user explicitly asked for. Nothing more.',
|
|
3259
|
+
'4. If the prompt is ≤ 15 words, produce ≤ 80 lines of code maximum.',
|
|
3260
|
+
'5. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
|
|
3261
|
+
'6. NEVER add any of these unless the user EXPLICITLY requests them:',
|
|
3262
|
+
' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
|
|
3263
|
+
' - CSS animations, gradients, neon effects, glass-morphism, particles',
|
|
3264
|
+
' - Google Fonts, Font Awesome, external CDN links, icon libraries',
|
|
3265
|
+
' - DOM manipulation, document.createElement, innerHTML',
|
|
3266
|
+
' - Responsive breakpoints, media queries (unless asked)',
|
|
3267
|
+
' - Multiple pages or components when one was requested',
|
|
3268
|
+
' - Input validation, error handling beyond what was requested',
|
|
3269
|
+
'7. Prefer inline styles or a small <style> block ONLY if the language is html. No CSS frameworks.',
|
|
3270
|
+
'8. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
|
|
3271
|
+
'9. Match the complexity of the request: a "hello world" is 1-5 lines, a "multiply function" is 3-5 lines, a "button" is 5-15 lines.',
|
|
3272
|
+
];
|
|
3273
|
+
if (isNonHtmlLang) {
|
|
3274
|
+
lines.push(`10. This is a PURE ${language.toUpperCase()} task. ABSOLUTELY NO document.*, window.*, DOM, createElement, innerHTML, querySelector, addEventListener. Output a plain ${language} function/module ONLY.`);
|
|
3275
|
+
}
|
|
3276
|
+
lines.push('', prompt);
|
|
3277
|
+
return lines.join('\n');
|
|
3278
|
+
};
|
|
3279
|
+
// First attempt
|
|
3280
|
+
let response = await this.client.post('/api/ai/generate', {
|
|
3281
|
+
prompt: buildScopedPrompt(false),
|
|
3266
3282
|
language,
|
|
3267
3283
|
model: this.resolvePermittedModelId(model),
|
|
3268
3284
|
});
|
|
3269
|
-
|
|
3285
|
+
let code = response.data.code || '';
|
|
3286
|
+
// Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
|
|
3287
|
+
const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
|
|
3288
|
+
this.codeIsOverEngineered(code, prompt));
|
|
3289
|
+
if (needsRetry) {
|
|
3290
|
+
// Retry once with stronger constraint
|
|
3291
|
+
response = await this.client.post('/api/ai/generate', {
|
|
3292
|
+
prompt: buildScopedPrompt(true),
|
|
3293
|
+
language,
|
|
3294
|
+
model: this.resolvePermittedModelId(model),
|
|
3295
|
+
});
|
|
3296
|
+
code = response.data.code || '';
|
|
3297
|
+
// If still polluted, strip DOM code client-side
|
|
3298
|
+
if (this.codeContainsDomPollution(code)) {
|
|
3299
|
+
code = this.stripDomPollution(code, language);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
// Final cleanup: for non-HTML, if still over-engineered after retry,
|
|
3303
|
+
// extract only the first complete function/class definition.
|
|
3304
|
+
if (isNonHtmlLang && this.codeIsOverEngineered(code, prompt)) {
|
|
3305
|
+
code = this.extractFirstFunction(code);
|
|
3306
|
+
}
|
|
3307
|
+
// Ensure balanced braces — model sometimes truncates closing braces
|
|
3308
|
+
code = this.ensureBalancedBraces(code);
|
|
3309
|
+
return code;
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
3313
|
+
*/
|
|
3314
|
+
ensureBalancedBraces(code) {
|
|
3315
|
+
let depth = 0;
|
|
3316
|
+
for (const ch of code) {
|
|
3317
|
+
if (ch === '{')
|
|
3318
|
+
depth++;
|
|
3319
|
+
else if (ch === '}')
|
|
3320
|
+
depth--;
|
|
3321
|
+
}
|
|
3322
|
+
if (depth > 0) {
|
|
3323
|
+
const trimmed = code.trimEnd();
|
|
3324
|
+
for (let i = 0; i < depth; i++) {
|
|
3325
|
+
code = trimmed + '\n};';
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
return code;
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Extract the first complete function/class from code.
|
|
3332
|
+
* Used as last-resort when the model keeps over-producing.
|
|
3333
|
+
*/
|
|
3334
|
+
extractFirstFunction(code) {
|
|
3335
|
+
const lines = code.split('\n');
|
|
3336
|
+
const funcStart = lines.findIndex(l => /^(export\s+)?(const\s+\w+\s*=\s*(\(|function)|function\s+\w+|class\s+\w+|def\s+\w+|fn\s+\w+|func\s+\w+)/.test(l.trim()));
|
|
3337
|
+
if (funcStart === -1)
|
|
3338
|
+
return code;
|
|
3339
|
+
let braceDepth = 0;
|
|
3340
|
+
let foundOpen = false;
|
|
3341
|
+
const result = [];
|
|
3342
|
+
for (let i = funcStart; i < lines.length; i++) {
|
|
3343
|
+
const line = lines[i];
|
|
3344
|
+
result.push(line);
|
|
3345
|
+
braceDepth += (line.match(/{/g) || []).length;
|
|
3346
|
+
braceDepth -= (line.match(/}/g) || []).length;
|
|
3347
|
+
if (braceDepth > 0)
|
|
3348
|
+
foundOpen = true;
|
|
3349
|
+
if (foundOpen && braceDepth <= 0) {
|
|
3350
|
+
break;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
// If we reached the end without balancing braces, add closing
|
|
3354
|
+
if (braceDepth > 0) {
|
|
3355
|
+
result.push('};');
|
|
3356
|
+
}
|
|
3357
|
+
return result.join('\n');
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Detect if code is excessively over-engineered for a short prompt.
|
|
3361
|
+
* E.g. a "multiply function" request producing 20+ lines.
|
|
3362
|
+
*/
|
|
3363
|
+
codeIsOverEngineered(code, prompt) {
|
|
3364
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
3365
|
+
const lineCount = code.trim().split('\n').length;
|
|
3366
|
+
// Short prompt (≤15 words) should not produce more than 15 lines
|
|
3367
|
+
if (wordCount <= 15 && lineCount > 15)
|
|
3368
|
+
return true;
|
|
3369
|
+
return false;
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Detect if generated code contains DOM/HTML pollution inappropriate
|
|
3373
|
+
* for a pure programming language like JavaScript, Python, etc.
|
|
3374
|
+
*/
|
|
3375
|
+
codeContainsDomPollution(code) {
|
|
3376
|
+
const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)/;
|
|
3377
|
+
return domPatterns.test(code);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Strip DOM pollution from generated code, keeping only the pure logic.
|
|
3381
|
+
* Used as last-resort fallback when the model repeatedly ignores constraints.
|
|
3382
|
+
*/
|
|
3383
|
+
stripDomPollution(code, language) {
|
|
3384
|
+
const lines = code.split('\n');
|
|
3385
|
+
const cleanLines = [];
|
|
3386
|
+
let insideDomBlock = false;
|
|
3387
|
+
let braceDepth = 0;
|
|
3388
|
+
for (const line of lines) {
|
|
3389
|
+
// Detect start of DOM blocks
|
|
3390
|
+
if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener/.test(line) && !insideDomBlock) {
|
|
3391
|
+
insideDomBlock = true;
|
|
3392
|
+
braceDepth = 0;
|
|
3393
|
+
}
|
|
3394
|
+
if (insideDomBlock) {
|
|
3395
|
+
braceDepth += (line.match(/{/g) || []).length;
|
|
3396
|
+
braceDepth -= (line.match(/}/g) || []).length;
|
|
3397
|
+
if (braceDepth <= 0) {
|
|
3398
|
+
insideDomBlock = false;
|
|
3399
|
+
}
|
|
3400
|
+
continue; // Skip DOM lines
|
|
3401
|
+
}
|
|
3402
|
+
cleanLines.push(line);
|
|
3403
|
+
}
|
|
3404
|
+
const cleaned = cleanLines.join('\n').trim();
|
|
3405
|
+
// If we stripped too aggressively (less than 2 lines left), return a minimal stub
|
|
3406
|
+
if (cleaned.split('\n').length < 2) {
|
|
3407
|
+
return `// Auto-generated: model returned DOM code for a ${language} task.\n// Please regenerate or provide more specific instructions.`;
|
|
3408
|
+
}
|
|
3409
|
+
return cleaned;
|
|
3270
3410
|
}
|
|
3271
3411
|
// Senior Developer Mode - Planning + Generation + Quality Check
|
|
3272
3412
|
async generateProject(prompt, projectType, model) {
|
|
@@ -3389,6 +3529,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3389
3529
|
}
|
|
3390
3530
|
}
|
|
3391
3531
|
}
|
|
3532
|
+
// Accumulator/reducer bug: subtraction used in a reduce/total/sum context
|
|
3533
|
+
// e.g. "sum - item.price" in a function named calculateTotal, getTotal, etc.
|
|
3534
|
+
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity|count)\b/.test(line) || /\w+\.\s*(price|cost|amount|value|total)\s*-\s*/.test(line)) {
|
|
3535
|
+
// Check enclosing function/method name for total/sum/aggregate semantics
|
|
3536
|
+
for (let j = i; j >= Math.max(0, i - 10); j--) {
|
|
3537
|
+
if (/(function\s+)?(calculate|calc|get|compute|sum|total|aggregate|accumulate)\s*(Total|Sum|Cost|Price|Amount|All)?\s*[=(]/i.test(lines[j]) || /\.(reduce|forEach|map)\s*\(/.test(lines[j])) {
|
|
3538
|
+
// Also check if this looks like an accumulator pattern (sum - something instead of sum + something)
|
|
3539
|
+
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity)\b/.test(line)) {
|
|
3540
|
+
issues.push({ type: 'logic', line: lineNum, message: 'Subtraction used in an accumulation context (total/sum/reduce) — likely should be addition (+).', severity: 'error' });
|
|
3541
|
+
}
|
|
3542
|
+
break;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
// Mismatched function name vs operator: sum/total function using wrong operator
|
|
3547
|
+
if (/\breturn\b/.test(line)) {
|
|
3548
|
+
for (let j = i; j >= Math.max(0, i - 8); j--) {
|
|
3549
|
+
const funcMatch = lines[j].match(/function\s+(\w+)/i) || lines[j].match(/(?:const|let|var)\s+(\w+)\s*=/i);
|
|
3550
|
+
if (funcMatch) {
|
|
3551
|
+
const funcName = funcMatch[1].toLowerCase();
|
|
3552
|
+
if ((funcName.includes('total') || funcName.includes('sum') || funcName.includes('add')) && /\s-\s/.test(line) && !/\s\+\s/.test(line)) {
|
|
3553
|
+
const alreadyReported = issues.some(iss => iss.line === lineNum && iss.type === 'logic');
|
|
3554
|
+
if (!alreadyReported) {
|
|
3555
|
+
issues.push({ type: 'logic', line: lineNum, message: `Subtraction operator in "${funcMatch[1]}" — function name implies addition. Likely should use + instead of -.`, severity: 'error' });
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
break;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3392
3562
|
// Off-by-one: comparing with === where <= or >= may be needed
|
|
3393
3563
|
// (not implemented yet — would require more complex AST analysis)
|
|
3394
3564
|
// Limit to 15 heuristic issues
|
|
@@ -3401,8 +3571,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3401
3571
|
// Client-side syntax pre-check: detect obvious errors and include
|
|
3402
3572
|
// them in the request so the model has concrete signals.
|
|
3403
3573
|
const syntaxHints = this.detectSyntaxErrors(code, language);
|
|
3404
|
-
|
|
3405
|
-
|
|
3574
|
+
// Also run heuristic logic analysis to find real bugs
|
|
3575
|
+
const heuristicIssues = this.heuristicCodeIssues(code, language);
|
|
3576
|
+
const logicHints = heuristicIssues
|
|
3577
|
+
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
3578
|
+
.join('\n// ');
|
|
3579
|
+
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
3580
|
+
const augmentedCode = allHints
|
|
3581
|
+
? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n\n${code}`
|
|
3406
3582
|
: code;
|
|
3407
3583
|
const response = await this.client.post('/api/ai/fix', {
|
|
3408
3584
|
code: augmentedCode,
|
|
@@ -3412,23 +3588,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3412
3588
|
const raw = response.data ?? {};
|
|
3413
3589
|
let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
|
|
3414
3590
|
let changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
3415
|
-
// If server returned no changes but we found
|
|
3591
|
+
// If server returned no changes but we found issues, strip
|
|
3416
3592
|
// our injected comment prefix from the returned code and attempt
|
|
3417
3593
|
// a basic client-side repair.
|
|
3418
|
-
if (changes.length === 0 &&
|
|
3594
|
+
if (changes.length === 0 && allHints && fixed === augmentedCode) {
|
|
3419
3595
|
fixed = code; // restore original
|
|
3420
3596
|
}
|
|
3421
3597
|
// Strip the injected comment block if it leaked into the output
|
|
3422
|
-
if (fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
|
|
3598
|
+
if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
|
|
3423
3599
|
const idx = fixed.indexOf('\n\n');
|
|
3424
3600
|
if (idx !== -1) {
|
|
3425
3601
|
fixed = fixed.slice(idx + 2);
|
|
3426
3602
|
}
|
|
3427
3603
|
}
|
|
3428
|
-
// If there are still no changes but the fixed code differs,
|
|
3429
|
-
//
|
|
3604
|
+
// If there are still no changes but the fixed code differs, compute
|
|
3605
|
+
// a line-level diff so the user sees exactly what changed.
|
|
3430
3606
|
if (changes.length === 0 && fixed !== code) {
|
|
3431
|
-
|
|
3607
|
+
const origLines = code.split('\n');
|
|
3608
|
+
const fixedLines = fixed.split('\n');
|
|
3609
|
+
const maxLen = Math.max(origLines.length, fixedLines.length);
|
|
3610
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3611
|
+
const orig = origLines[i] ?? '';
|
|
3612
|
+
const fixd = fixedLines[i] ?? '';
|
|
3613
|
+
if (orig !== fixd) {
|
|
3614
|
+
changes.push({
|
|
3615
|
+
line: i + 1,
|
|
3616
|
+
before: orig || '(empty line)',
|
|
3617
|
+
after: fixd || '(line removed)',
|
|
3618
|
+
reason: allHints || 'AI-suggested fix',
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
// If diff produced nothing (whitespace-only?), at least show a summary
|
|
3623
|
+
if (changes.length === 0) {
|
|
3624
|
+
changes = [{ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: allHints || 'AI-suggested fix' }];
|
|
3625
|
+
}
|
|
3432
3626
|
}
|
|
3433
3627
|
return { fixed, changes };
|
|
3434
3628
|
}
|