vigthoria-cli 1.13.18 → 1.13.22
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/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +84 -135
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +5 -42
- package/dist/commands/chat.js +462 -1122
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +32 -42
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +200 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +245 -196
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +338 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +122 -1498
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +45 -269
- package/dist/utils/api.js +664 -3210
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +227 -99
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +6 -7
- package/dist/utils/fastAgentRouter.js +21 -30
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
- package/dist/utils/preview-screenshot-adapter.js +36 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +34 -2
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +216 -508
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +86 -143
- package/install.sh +76 -81
- package/package.json +44 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +42 -0
- package/scripts/release/validate-live-service-gates.sh +90 -0
- package/scripts/release/validate-no-go-gates.sh +135 -172
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
export class CodeOperationsService {
|
|
2
|
+
complete;
|
|
3
|
+
constructor(complete) {
|
|
4
|
+
this.complete = complete;
|
|
5
|
+
}
|
|
6
|
+
chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
7
|
+
return this.complete(systemPrompt, userPrompt, model, maxTokens);
|
|
8
|
+
}
|
|
9
|
+
// Code operations - Using Vigthoria Centralized API (via Model Router)
|
|
10
|
+
async generateCode(prompt, language, model) {
|
|
11
|
+
const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
|
|
12
|
+
// Prepend a forceful scope-enforcement instruction so the model
|
|
13
|
+
// doesn't expand a small task into an oversized glossy page.
|
|
14
|
+
const buildScopedPrompt = (retry) => {
|
|
15
|
+
if (retry && isNonHtmlLang) {
|
|
16
|
+
// Ultra-minimal retry prompt — no long instruction block
|
|
17
|
+
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}`;
|
|
18
|
+
}
|
|
19
|
+
const lines = [
|
|
20
|
+
'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
|
|
21
|
+
`1. You MUST output ONLY ${language.toUpperCase()} code. Do NOT output HTML, CSS, or DOM manipulation unless the language is html/css.`,
|
|
22
|
+
`2. The target language is: ${language}. Do not switch to a different language.`,
|
|
23
|
+
'3. Output ONLY what the user explicitly asked for. Nothing more.',
|
|
24
|
+
'4. If the prompt is ≤ 15 words, produce ≤ 80 lines of code maximum.',
|
|
25
|
+
'5. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
|
|
26
|
+
'6. NEVER add any of these unless the user EXPLICITLY requests them:',
|
|
27
|
+
' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
|
|
28
|
+
' - CSS animations, gradients, neon effects, glass-morphism, particles',
|
|
29
|
+
' - Google Fonts, Font Awesome, external CDN links, icon libraries',
|
|
30
|
+
' - DOM manipulation, document.createElement, innerHTML',
|
|
31
|
+
' - Responsive breakpoints, media queries (unless asked)',
|
|
32
|
+
' - Multiple pages or components when one was requested',
|
|
33
|
+
' - Input validation, error handling beyond what was requested',
|
|
34
|
+
'7. Prefer inline styles or a small <style> block ONLY if the language is html. No CSS frameworks.',
|
|
35
|
+
'8. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
|
|
36
|
+
'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.',
|
|
37
|
+
];
|
|
38
|
+
if (isNonHtmlLang) {
|
|
39
|
+
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.`);
|
|
40
|
+
}
|
|
41
|
+
lines.push('', prompt);
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
};
|
|
44
|
+
// First attempt — route through Model Router chat completions
|
|
45
|
+
const systemPrompt = `You are a code generator. Output ONLY raw ${language} code. No markdown fences, no explanations, no commentary. Just the code.`;
|
|
46
|
+
let code = await this.chatComplete(systemPrompt, buildScopedPrompt(false), model);
|
|
47
|
+
// Strip markdown fences if model included them
|
|
48
|
+
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
49
|
+
// Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
|
|
50
|
+
const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
|
|
51
|
+
this.codeIsOverEngineered(code, prompt));
|
|
52
|
+
if (needsRetry) {
|
|
53
|
+
// Retry once with stronger constraint — via Model Router
|
|
54
|
+
code = await this.chatComplete(systemPrompt, buildScopedPrompt(true), model);
|
|
55
|
+
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
56
|
+
// If still polluted, strip DOM code client-side
|
|
57
|
+
if (this.codeContainsDomPollution(code)) {
|
|
58
|
+
code = this.stripDomPollution(code, language);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Final cleanup: for non-HTML, if still over-engineered after retry,
|
|
62
|
+
// extract only the first complete function/class definition.
|
|
63
|
+
if (isNonHtmlLang && this.codeIsOverEngineered(code, prompt)) {
|
|
64
|
+
code = this.extractFirstFunction(code);
|
|
65
|
+
}
|
|
66
|
+
// Ensure balanced braces — model sometimes truncates closing braces
|
|
67
|
+
code = this.ensureBalancedBraces(code);
|
|
68
|
+
return code;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
72
|
+
*/
|
|
73
|
+
ensureBalancedBraces(code) {
|
|
74
|
+
// Count braces/parens/brackets outside strings and comments
|
|
75
|
+
let braces = 0, parens = 0, brackets = 0;
|
|
76
|
+
let inStr = null;
|
|
77
|
+
let inLine = false, inBlock = false;
|
|
78
|
+
for (let i = 0; i < code.length; i++) {
|
|
79
|
+
const ch = code[i], nx = code[i + 1] || '';
|
|
80
|
+
if (inLine) {
|
|
81
|
+
if (ch === '\n')
|
|
82
|
+
inLine = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (inBlock) {
|
|
86
|
+
if (ch === '*' && nx === '/') {
|
|
87
|
+
inBlock = false;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (inStr) {
|
|
93
|
+
if (ch === inStr && code[i - 1] !== '\\')
|
|
94
|
+
inStr = null;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (ch === '/' && nx === '/') {
|
|
98
|
+
inLine = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (ch === '/' && nx === '*') {
|
|
102
|
+
inBlock = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
106
|
+
inStr = ch;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ch === '{')
|
|
110
|
+
braces++;
|
|
111
|
+
else if (ch === '}')
|
|
112
|
+
braces--;
|
|
113
|
+
else if (ch === '(')
|
|
114
|
+
parens++;
|
|
115
|
+
else if (ch === ')')
|
|
116
|
+
parens--;
|
|
117
|
+
else if (ch === '[')
|
|
118
|
+
brackets++;
|
|
119
|
+
else if (ch === ']')
|
|
120
|
+
brackets--;
|
|
121
|
+
}
|
|
122
|
+
let result = code.trimEnd();
|
|
123
|
+
for (let i = 0; i < braces; i++)
|
|
124
|
+
result += '\n}';
|
|
125
|
+
for (let i = 0; i < parens; i++)
|
|
126
|
+
result += ')';
|
|
127
|
+
for (let i = 0; i < brackets; i++)
|
|
128
|
+
result += ']';
|
|
129
|
+
return braces > 0 || parens > 0 || brackets > 0 ? result : code;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Quick JS/TS syntax validation using Node's built-in parser.
|
|
133
|
+
* Returns true if the code parses without errors.
|
|
134
|
+
*/
|
|
135
|
+
validateJsSyntax(code) {
|
|
136
|
+
try {
|
|
137
|
+
// Use Function constructor to check syntax without executing
|
|
138
|
+
new Function(code);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract the first complete function/class from code.
|
|
147
|
+
* Used as last-resort when the model keeps over-producing.
|
|
148
|
+
*/
|
|
149
|
+
extractFirstFunction(code) {
|
|
150
|
+
const lines = code.split('\n');
|
|
151
|
+
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()));
|
|
152
|
+
if (funcStart === -1)
|
|
153
|
+
return code;
|
|
154
|
+
let braceDepth = 0;
|
|
155
|
+
let foundOpen = false;
|
|
156
|
+
const result = [];
|
|
157
|
+
for (let i = funcStart; i < lines.length; i++) {
|
|
158
|
+
const line = lines[i];
|
|
159
|
+
result.push(line);
|
|
160
|
+
braceDepth += (line.match(/{/g) || []).length;
|
|
161
|
+
braceDepth -= (line.match(/}/g) || []).length;
|
|
162
|
+
if (braceDepth > 0)
|
|
163
|
+
foundOpen = true;
|
|
164
|
+
if (foundOpen && braceDepth <= 0) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// If we reached the end without balancing braces, add closing
|
|
169
|
+
if (braceDepth > 0) {
|
|
170
|
+
result.push('};');
|
|
171
|
+
}
|
|
172
|
+
return result.join('\n');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Detect if code is excessively over-engineered for a short prompt.
|
|
176
|
+
* E.g. a "multiply function" request producing 20+ lines.
|
|
177
|
+
*/
|
|
178
|
+
codeIsOverEngineered(code, prompt) {
|
|
179
|
+
const wordCount = prompt.trim().split(/\s+/).length;
|
|
180
|
+
const lineCount = code.trim().split('\n').length;
|
|
181
|
+
// Short prompt (≤15 words) should not produce more than 15 lines
|
|
182
|
+
if (wordCount <= 15 && lineCount > 15)
|
|
183
|
+
return true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Detect if generated code contains DOM/HTML pollution inappropriate
|
|
188
|
+
* for a pure programming language like JavaScript, Python, etc.
|
|
189
|
+
*/
|
|
190
|
+
codeContainsDomPollution(code) {
|
|
191
|
+
const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)|CSSAnimation|new\s+Animation\b|\.getAnimations\s*\(|\.animate\s*\(/;
|
|
192
|
+
return domPatterns.test(code);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Strip DOM pollution from generated code, keeping only the pure logic.
|
|
196
|
+
* Used as last-resort fallback when the model repeatedly ignores constraints.
|
|
197
|
+
*/
|
|
198
|
+
stripDomPollution(code, language) {
|
|
199
|
+
const lines = code.split('\n');
|
|
200
|
+
const cleanLines = [];
|
|
201
|
+
let insideDomBlock = false;
|
|
202
|
+
let braceDepth = 0;
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
// Detect start of DOM blocks
|
|
205
|
+
if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener|CSSAnimation|\.getAnimations|\.animate\s*\(/.test(line) && !insideDomBlock) {
|
|
206
|
+
insideDomBlock = true;
|
|
207
|
+
braceDepth = 0;
|
|
208
|
+
}
|
|
209
|
+
if (insideDomBlock) {
|
|
210
|
+
braceDepth += (line.match(/{/g) || []).length;
|
|
211
|
+
braceDepth -= (line.match(/}/g) || []).length;
|
|
212
|
+
if (braceDepth <= 0) {
|
|
213
|
+
insideDomBlock = false;
|
|
214
|
+
}
|
|
215
|
+
continue; // Skip DOM lines
|
|
216
|
+
}
|
|
217
|
+
cleanLines.push(line);
|
|
218
|
+
}
|
|
219
|
+
const cleaned = cleanLines.join('\n').trim();
|
|
220
|
+
// If we stripped too aggressively (less than 2 lines left), return a minimal stub
|
|
221
|
+
if (cleaned.split('\n').length < 2) {
|
|
222
|
+
return `// Auto-generated: model returned DOM code for a ${language} task.\n// Please regenerate or provide more specific instructions.`;
|
|
223
|
+
}
|
|
224
|
+
return cleaned;
|
|
225
|
+
}
|
|
226
|
+
// Senior Developer Mode - Planning + Generation + Quality Check
|
|
227
|
+
async generateProject(prompt, projectType, model) {
|
|
228
|
+
const sysPrompt = [
|
|
229
|
+
`You are a senior developer. Generate a complete ${projectType} project.`,
|
|
230
|
+
'Return a JSON object with these fields:',
|
|
231
|
+
' "code": the full source code as a string,',
|
|
232
|
+
' "plan": an object describing the architecture,',
|
|
233
|
+
' "quality": { "lineCount": number, "score": number (0-100) }',
|
|
234
|
+
'Return ONLY the JSON object, no markdown fences.',
|
|
235
|
+
].join('\n');
|
|
236
|
+
const raw = await this.chatComplete(sysPrompt, prompt, model, 8192);
|
|
237
|
+
try {
|
|
238
|
+
const cleaned = raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
239
|
+
const parsed = JSON.parse(cleaned);
|
|
240
|
+
return {
|
|
241
|
+
code: parsed.code || raw,
|
|
242
|
+
plan: parsed.plan,
|
|
243
|
+
quality: parsed.quality,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return { code: raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim() };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async explainCode(code, language) {
|
|
251
|
+
const sysPrompt = [
|
|
252
|
+
`You are a code explainer. Explain the following ${language} code clearly and concisely.`,
|
|
253
|
+
'Focus on what it does, how it works, and any notable patterns or potential issues.',
|
|
254
|
+
'Format your response as clean Markdown:',
|
|
255
|
+
'- Use ## headers for major sections (e.g. ## Overview, ## How It Works, ## Key Details).',
|
|
256
|
+
'- Use bullet points (- or *) for all lists. Do NOT use numbered lists.',
|
|
257
|
+
'- Wrap code references in backticks.',
|
|
258
|
+
'- Keep paragraphs short (2-3 sentences max).',
|
|
259
|
+
'- Do NOT use raw HTML or excessive blank lines.',
|
|
260
|
+
'- Do NOT nest numbered lists inside sections.',
|
|
261
|
+
].join('\n');
|
|
262
|
+
return this.chatComplete(sysPrompt, code);
|
|
263
|
+
}
|
|
264
|
+
async reviewCode(code, language) {
|
|
265
|
+
const sysPrompt = [
|
|
266
|
+
`You are a senior code reviewer for ${language}. Analyze the code and return a JSON object with:`,
|
|
267
|
+
' "score": number 0-100,',
|
|
268
|
+
' "issues": [{ "type": string, "line": number, "message": string, "severity": "error"|"warning"|"info" }],',
|
|
269
|
+
' "suggestions": [string]',
|
|
270
|
+
'Rules:',
|
|
271
|
+
'- Return concrete, line-specific issues with severity.',
|
|
272
|
+
'- Every issue MUST reference a line number.',
|
|
273
|
+
'- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
|
|
274
|
+
'- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
|
|
275
|
+
'- If you find a real bug (wrong operator, logic error, type mismatch), report ONLY that bug. Do NOT also suggest input validation, type checking, or error handling unless those are ACTUAL bugs.',
|
|
276
|
+
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
277
|
+
'- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
|
|
278
|
+
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
279
|
+
].join('\n');
|
|
280
|
+
let raw = {};
|
|
281
|
+
try {
|
|
282
|
+
const result = await this.chatComplete(sysPrompt, code);
|
|
283
|
+
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
284
|
+
raw = JSON.parse(cleaned);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// If parsing fails, return minimal review
|
|
288
|
+
}
|
|
289
|
+
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
290
|
+
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
291
|
+
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
292
|
+
// Merge client-side heuristics, but with tight dedup to avoid
|
|
293
|
+
// redundant over-reporting when the model already found the bug.
|
|
294
|
+
const modelFoundError = issues.some(i => i.severity === 'error');
|
|
295
|
+
const heuristic = this.heuristicCodeIssues(code, language);
|
|
296
|
+
for (const h of heuristic) {
|
|
297
|
+
// If the model already found a real error, skip non-error heuristics
|
|
298
|
+
// entirely — they're just padding (style, robustness, etc.)
|
|
299
|
+
if (modelFoundError && h.severity !== 'error')
|
|
300
|
+
continue;
|
|
301
|
+
// Semantic duplicate check: same line + (similar type OR overlapping
|
|
302
|
+
// keywords in the message). This catches cases where the model
|
|
303
|
+
// and heuristic describe the same bug with different wording.
|
|
304
|
+
const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
|
|
305
|
+
const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
306
|
+
const isSemanticallyDuplicate = issues.some((existing) => {
|
|
307
|
+
if (existing.line !== h.line)
|
|
308
|
+
return false;
|
|
309
|
+
// Normalize types: "logic-error", "logic_error", "logic" all match
|
|
310
|
+
const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
311
|
+
if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
|
|
312
|
+
return true;
|
|
313
|
+
// Both errors on same line about the same category of problem
|
|
314
|
+
if (existing.severity === 'error' && h.severity === 'error')
|
|
315
|
+
return true;
|
|
316
|
+
// Check keyword overlap — if ≥2 significant words match, it's the same finding
|
|
317
|
+
const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
318
|
+
let overlap = 0;
|
|
319
|
+
for (const w of eWords) {
|
|
320
|
+
if (hWords.has(w))
|
|
321
|
+
overlap++;
|
|
322
|
+
}
|
|
323
|
+
return overlap >= 2;
|
|
324
|
+
});
|
|
325
|
+
if (!isSemanticallyDuplicate) {
|
|
326
|
+
issues.push(h);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Sort: errors first, then warnings, then info
|
|
330
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
331
|
+
issues.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
|
|
332
|
+
// Prevent contradictory output: low score but zero issues.
|
|
333
|
+
if (score < 50 && issues.length === 0) {
|
|
334
|
+
issues.push({
|
|
335
|
+
type: 'quality',
|
|
336
|
+
line: 1,
|
|
337
|
+
message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
|
|
338
|
+
severity: 'warning',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return { score, issues, suggestions };
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Lightweight client-side heuristic scan: catches common code smells
|
|
345
|
+
* AND logic/arithmetic bugs so review never returns "score 30, no issues".
|
|
346
|
+
*/
|
|
347
|
+
heuristicCodeIssues(code, _language) {
|
|
348
|
+
const issues = [];
|
|
349
|
+
const lines = code.split('\n');
|
|
350
|
+
for (let i = 0; i < lines.length; i++) {
|
|
351
|
+
const line = lines[i];
|
|
352
|
+
const lineNum = i + 1;
|
|
353
|
+
// console.log left in production code
|
|
354
|
+
if (/\bconsole\.(log|debug|info)\b/.test(line) && !/\/\//.test(line.slice(0, line.indexOf('console')))) {
|
|
355
|
+
issues.push({ type: 'quality', line: lineNum, message: 'console.log/debug statement — remove or replace with proper logging.', severity: 'warning' });
|
|
356
|
+
}
|
|
357
|
+
// TODO/FIXME/HACK comments
|
|
358
|
+
if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line)) {
|
|
359
|
+
issues.push({ type: 'maintainability', line: lineNum, message: `Unresolved ${line.match(/\b(TODO|FIXME|HACK|XXX)\b/)?.[0]} comment.`, severity: 'info' });
|
|
360
|
+
}
|
|
361
|
+
// Empty catch blocks
|
|
362
|
+
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line)) {
|
|
363
|
+
issues.push({ type: 'error-handling', line: lineNum, message: 'Empty catch block — errors are silently swallowed.', severity: 'warning' });
|
|
364
|
+
}
|
|
365
|
+
// Very long lines (> 200 chars)
|
|
366
|
+
if (line.length > 200) {
|
|
367
|
+
issues.push({ type: 'style', line: lineNum, message: `Line exceeds 200 characters (${line.length}).`, severity: 'info' });
|
|
368
|
+
}
|
|
369
|
+
// Arithmetic / logic bugs: function named 'add' but using subtraction
|
|
370
|
+
if (/\breturn\s+\w+\s*-\s*\w+/.test(line)) {
|
|
371
|
+
// Check if the enclosing function name implies addition
|
|
372
|
+
for (let j = i; j >= Math.max(0, i - 5); j--) {
|
|
373
|
+
if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
|
|
374
|
+
issues.push({ type: 'logic', line: lineNum, message: 'Subtraction operator in a function named "add" — likely should be addition (+).', severity: 'error' });
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Multiplication where addition is expected
|
|
380
|
+
if (/\breturn\s+\w+\s*\*\s*\w+/.test(line)) {
|
|
381
|
+
for (let j = i; j >= Math.max(0, i - 5); j--) {
|
|
382
|
+
if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
|
|
383
|
+
issues.push({ type: 'logic', line: lineNum, message: 'Multiplication operator in a function named "add" — likely should be addition (+).', severity: 'error' });
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Division where subtraction/addition is expected in subtract
|
|
389
|
+
if (/\breturn\s+\w+\s*\+\s*\w+/.test(line)) {
|
|
390
|
+
for (let j = i; j >= Math.max(0, i - 5); j--) {
|
|
391
|
+
if (/function\s+subtract\b/i.test(lines[j]) || /\bsubtract\s*[=(]/.test(lines[j])) {
|
|
392
|
+
issues.push({ type: 'logic', line: lineNum, message: 'Addition operator in a function named "subtract" — likely should be subtraction (-).', severity: 'error' });
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Accumulator/reducer bug: subtraction used in a reduce/total/sum context
|
|
398
|
+
// e.g. "sum - item.price" in a function named calculateTotal, getTotal, etc.
|
|
399
|
+
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)) {
|
|
400
|
+
// Check enclosing function/method name for total/sum/aggregate semantics
|
|
401
|
+
for (let j = i; j >= Math.max(0, i - 10); j--) {
|
|
402
|
+
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])) {
|
|
403
|
+
// Also check if this looks like an accumulator pattern (sum - something instead of sum + something)
|
|
404
|
+
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity)\b/.test(line)) {
|
|
405
|
+
issues.push({ type: 'logic', line: lineNum, message: 'Subtraction used in an accumulation context (total/sum/reduce) — likely should be addition (+).', severity: 'error' });
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Mismatched function name vs operator: sum/total function using wrong operator
|
|
412
|
+
if (/\breturn\b/.test(line)) {
|
|
413
|
+
for (let j = i; j >= Math.max(0, i - 8); j--) {
|
|
414
|
+
const funcMatch = lines[j].match(/function\s+(\w+)/i) || lines[j].match(/(?:const|let|var)\s+(\w+)\s*=/i);
|
|
415
|
+
if (funcMatch) {
|
|
416
|
+
const funcName = funcMatch[1].toLowerCase();
|
|
417
|
+
if ((funcName.includes('total') || funcName.includes('sum') || funcName.includes('add')) && /\s-\s/.test(line) && !/\s\+\s/.test(line)) {
|
|
418
|
+
const alreadyReported = issues.some(iss => iss.line === lineNum && iss.type === 'logic');
|
|
419
|
+
if (!alreadyReported) {
|
|
420
|
+
issues.push({ type: 'logic', line: lineNum, message: `Subtraction operator in "${funcMatch[1]}" — function name implies addition. Likely should use + instead of -.`, severity: 'error' });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Off-by-one: comparing with === where <= or >= may be needed
|
|
428
|
+
// (not implemented yet — would require more complex AST analysis)
|
|
429
|
+
// Limit to 15 heuristic issues
|
|
430
|
+
if (issues.length >= 15)
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
return issues;
|
|
434
|
+
}
|
|
435
|
+
async fixCode(code, language, fixType) {
|
|
436
|
+
// Client-side syntax pre-check: detect obvious errors and include
|
|
437
|
+
// them in the request so the model has concrete signals.
|
|
438
|
+
const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
|
|
439
|
+
// Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
|
|
440
|
+
const heuristicIssues = this.heuristicCodeIssues(code, language);
|
|
441
|
+
const relevantIssues = heuristicIssues.filter(issue => {
|
|
442
|
+
if (fixType === 'bugs' || fixType === 'logic')
|
|
443
|
+
return issue.type === 'logic' || issue.severity === 'error';
|
|
444
|
+
if (fixType === 'syntax')
|
|
445
|
+
return issue.type !== 'logic' && issue.severity === 'error';
|
|
446
|
+
if (fixType === 'style')
|
|
447
|
+
return issue.type === 'style' || issue.type === 'quality';
|
|
448
|
+
if (fixType === 'security')
|
|
449
|
+
return issue.type === 'security';
|
|
450
|
+
if (fixType === 'performance')
|
|
451
|
+
return issue.type === 'performance';
|
|
452
|
+
return true; // 'all' or unknown fixType: include everything
|
|
453
|
+
});
|
|
454
|
+
const logicHints = relevantIssues
|
|
455
|
+
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
456
|
+
.join('\n// ');
|
|
457
|
+
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
458
|
+
// Build a constraint preamble that's stricter for syntax-only mode.
|
|
459
|
+
let preamble;
|
|
460
|
+
if (fixType === 'syntax') {
|
|
461
|
+
preamble = allHints
|
|
462
|
+
? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${allHints}\n// RULES: Fix ONLY bracket/parenthesis/brace mismatches and keyword typos. Do NOT rename functions, do NOT add comments, do NOT restructure or reformat. Output the corrected code ONLY.\n\n`
|
|
463
|
+
: '';
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
preamble = allHints
|
|
467
|
+
? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n`
|
|
468
|
+
: '';
|
|
469
|
+
}
|
|
470
|
+
const augmentedCode = preamble ? `${preamble}${code}` : code;
|
|
471
|
+
const sysPrompt = [
|
|
472
|
+
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
473
|
+
'Return a JSON object with:',
|
|
474
|
+
' "fixed": the COMPLETE corrected source code as a string (not a snippet — the full file),',
|
|
475
|
+
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
476
|
+
'Rules:',
|
|
477
|
+
'- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
|
|
478
|
+
'- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
|
|
479
|
+
'- Fix ONLY the issues related to the fix type.',
|
|
480
|
+
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
481
|
+
'- Return ONLY the JSON object, no markdown fences.',
|
|
482
|
+
].join('\n');
|
|
483
|
+
let raw = {};
|
|
484
|
+
try {
|
|
485
|
+
const result = await this.chatComplete(sysPrompt, augmentedCode);
|
|
486
|
+
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
487
|
+
raw = JSON.parse(cleaned);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// If parsing fails, fall through to client-side handling
|
|
491
|
+
}
|
|
492
|
+
let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
|
|
493
|
+
let changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
494
|
+
// If server returned no changes but we found issues, strip
|
|
495
|
+
// our injected comment prefix from the returned code and attempt
|
|
496
|
+
// a basic client-side repair.
|
|
497
|
+
if (changes.length === 0 && preamble && fixed === augmentedCode) {
|
|
498
|
+
fixed = code; // restore original
|
|
499
|
+
}
|
|
500
|
+
// Strip the injected comment block if it leaked into the output
|
|
501
|
+
if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT')) {
|
|
502
|
+
const idx = fixed.indexOf('\n\n');
|
|
503
|
+
if (idx !== -1) {
|
|
504
|
+
fixed = fixed.slice(idx + 2);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// For syntax-only fixes, strip any comments the model added that
|
|
508
|
+
// weren't in the original code (e.g. "// Fixed mismatched parentheses").
|
|
509
|
+
if (fixType === 'syntax' && fixed !== code) {
|
|
510
|
+
fixed = this.stripInjectedComments(code, fixed, language);
|
|
511
|
+
}
|
|
512
|
+
// Safety net: for syntax fixes, ensure the fix didn't make bracket
|
|
513
|
+
// balance worse. If the original had more closing delimiters than
|
|
514
|
+
// the fix, append the missing ones.
|
|
515
|
+
if (fixType === 'syntax' && fixed !== code) {
|
|
516
|
+
fixed = this.repairBracketBalance(code, fixed);
|
|
517
|
+
}
|
|
518
|
+
// Final bracket-balance guarantee — ensure the emitted code has
|
|
519
|
+
// balanced braces/parens/brackets regardless of what the model returned.
|
|
520
|
+
fixed = this.ensureBalancedBraces(fixed);
|
|
521
|
+
// For JS/TS syntax fixes, validate the output actually parses.
|
|
522
|
+
// If it doesn't, attempt a more aggressive bracket repair.
|
|
523
|
+
if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
|
|
524
|
+
const lang = language.toLowerCase();
|
|
525
|
+
if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
|
|
526
|
+
if (!this.validateJsSyntax(fixed)) {
|
|
527
|
+
// Try once more: strip any remaining injected comments and re-balance
|
|
528
|
+
let repaired = this.stripInjectedComments(code, fixed, language);
|
|
529
|
+
repaired = this.ensureBalancedBraces(repaired);
|
|
530
|
+
if (this.validateJsSyntax(repaired)) {
|
|
531
|
+
fixed = repaired;
|
|
532
|
+
}
|
|
533
|
+
// If still invalid, return the best-effort fix — better than
|
|
534
|
+
// silently reverting to the original broken code.
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// If there are still no changes but the fixed code differs, compute
|
|
539
|
+
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
540
|
+
// every subsequent line to appear as changed.
|
|
541
|
+
if (changes.length === 0 && fixed !== code) {
|
|
542
|
+
// Use a clean reason string — strip verbose static-analysis hints
|
|
543
|
+
const cleanReason = relevantIssues.length > 0
|
|
544
|
+
? relevantIssues.map(i => i.message).join('; ')
|
|
545
|
+
: 'AI-suggested fix';
|
|
546
|
+
changes = this.computeSemanticDiff(code, fixed, cleanReason);
|
|
547
|
+
}
|
|
548
|
+
return { fixed, changes };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Compute a semantic diff between original and fixed code using
|
|
552
|
+
* Longest Common Subsequence (LCS) to avoid the line-shift inflation
|
|
553
|
+
* bug where inserting one line flags all subsequent lines as changed.
|
|
554
|
+
*/
|
|
555
|
+
computeSemanticDiff(original, fixed, reason) {
|
|
556
|
+
const origLines = original.split('\n');
|
|
557
|
+
const fixedLines = fixed.split('\n');
|
|
558
|
+
const changes = [];
|
|
559
|
+
// Build LCS table
|
|
560
|
+
const m = origLines.length;
|
|
561
|
+
const n = fixedLines.length;
|
|
562
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
563
|
+
for (let i = 1; i <= m; i++) {
|
|
564
|
+
for (let j = 1; j <= n; j++) {
|
|
565
|
+
if (origLines[i - 1] === fixedLines[j - 1]) {
|
|
566
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Backtrack to find the diff
|
|
574
|
+
let i = m, j = n;
|
|
575
|
+
const ops = [];
|
|
576
|
+
while (i > 0 || j > 0) {
|
|
577
|
+
if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
|
|
578
|
+
ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
|
|
579
|
+
i--;
|
|
580
|
+
j--;
|
|
581
|
+
}
|
|
582
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
583
|
+
ops.push({ type: 'add', fixedLine: j, text: fixedLines[j - 1] });
|
|
584
|
+
j--;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
ops.push({ type: 'remove', origLine: i, text: origLines[i - 1] });
|
|
588
|
+
i--;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
ops.reverse();
|
|
592
|
+
// Merge adjacent remove+add pairs into single change entries
|
|
593
|
+
let idx = 0;
|
|
594
|
+
while (idx < ops.length) {
|
|
595
|
+
const op = ops[idx];
|
|
596
|
+
if (op.type === 'remove') {
|
|
597
|
+
// Look ahead for a matching 'add' immediately after
|
|
598
|
+
if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
|
|
599
|
+
changes.push({
|
|
600
|
+
line: op.origLine,
|
|
601
|
+
before: op.text,
|
|
602
|
+
after: ops[idx + 1].text,
|
|
603
|
+
reason: reason || 'AI-suggested fix',
|
|
604
|
+
});
|
|
605
|
+
idx += 2;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
changes.push({
|
|
609
|
+
line: op.origLine,
|
|
610
|
+
before: op.text,
|
|
611
|
+
after: '(line removed)',
|
|
612
|
+
reason: reason || 'AI-suggested fix',
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
else if (op.type === 'add') {
|
|
616
|
+
changes.push({
|
|
617
|
+
line: op.fixedLine,
|
|
618
|
+
before: '(new line)',
|
|
619
|
+
after: op.text,
|
|
620
|
+
reason: reason || 'AI-suggested fix',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
idx++;
|
|
624
|
+
}
|
|
625
|
+
if (changes.length === 0) {
|
|
626
|
+
changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
|
|
627
|
+
}
|
|
628
|
+
return changes;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Lightweight client-side syntax error detection.
|
|
632
|
+
* Returns a human-readable description of obvious errors, or empty string.
|
|
633
|
+
*/
|
|
634
|
+
detectSyntaxErrors(code, language) {
|
|
635
|
+
const lang = language.toLowerCase();
|
|
636
|
+
const errors = [];
|
|
637
|
+
if (lang === 'javascript' || lang === 'typescript' || lang === 'js' || lang === 'ts') {
|
|
638
|
+
// Bracket matching
|
|
639
|
+
let braces = 0, brackets = 0, parens = 0;
|
|
640
|
+
// Track string context (simple heuristic — not a full parser)
|
|
641
|
+
let inString = null;
|
|
642
|
+
let inLineComment = false;
|
|
643
|
+
let inBlockComment = false;
|
|
644
|
+
for (let i = 0; i < code.length; i++) {
|
|
645
|
+
const ch = code[i];
|
|
646
|
+
const next = code[i + 1] || '';
|
|
647
|
+
if (inLineComment) {
|
|
648
|
+
if (ch === '\n')
|
|
649
|
+
inLineComment = false;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (inBlockComment) {
|
|
653
|
+
if (ch === '*' && next === '/') {
|
|
654
|
+
inBlockComment = false;
|
|
655
|
+
i++;
|
|
656
|
+
}
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (inString) {
|
|
660
|
+
if (ch === inString && code[i - 1] !== '\\')
|
|
661
|
+
inString = null;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (ch === '/' && next === '/') {
|
|
665
|
+
inLineComment = true;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (ch === '/' && next === '*') {
|
|
669
|
+
inBlockComment = true;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
673
|
+
inString = ch;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (ch === '{')
|
|
677
|
+
braces++;
|
|
678
|
+
else if (ch === '}')
|
|
679
|
+
braces--;
|
|
680
|
+
else if (ch === '[')
|
|
681
|
+
brackets++;
|
|
682
|
+
else if (ch === ']')
|
|
683
|
+
brackets--;
|
|
684
|
+
else if (ch === '(')
|
|
685
|
+
parens++;
|
|
686
|
+
else if (ch === ')')
|
|
687
|
+
parens--;
|
|
688
|
+
}
|
|
689
|
+
if (braces !== 0)
|
|
690
|
+
errors.push(`Mismatched braces (balance: ${braces > 0 ? '+' : ''}${braces})`);
|
|
691
|
+
if (brackets !== 0)
|
|
692
|
+
errors.push(`Mismatched brackets (balance: ${brackets > 0 ? '+' : ''}${brackets})`);
|
|
693
|
+
if (parens !== 0)
|
|
694
|
+
errors.push(`Mismatched parentheses (balance: ${parens > 0 ? '+' : ''}${parens})`);
|
|
695
|
+
// Detect obvious keyword typos
|
|
696
|
+
const lines = code.split('\n');
|
|
697
|
+
for (let li = 0; li < lines.length; li++) {
|
|
698
|
+
const line = lines[li];
|
|
699
|
+
if (/\bfuncion\b|\bfuction\b|\bfuntion\b/i.test(line))
|
|
700
|
+
errors.push(`Line ${li + 1}: Misspelled 'function'`);
|
|
701
|
+
if (/\bretrun\b/i.test(line))
|
|
702
|
+
errors.push(`Line ${li + 1}: Misspelled 'return'`);
|
|
703
|
+
if (/\bconts\b|\bcosnt\b/i.test(line))
|
|
704
|
+
errors.push(`Line ${li + 1}: Misspelled 'const'`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (lang === 'python' || lang === 'py') {
|
|
708
|
+
const lines = code.split('\n');
|
|
709
|
+
for (let li = 0; li < lines.length; li++) {
|
|
710
|
+
const line = lines[li];
|
|
711
|
+
if (/^\s*(def|class|if|elif|else|for|while|try|except|with)\b/.test(line) && !line.trimEnd().endsWith(':') && !line.trimEnd().endsWith('\\')) {
|
|
712
|
+
errors.push(`Line ${li + 1}: Missing colon after '${line.trim().split(/\s/)[0]}'`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return errors.join('; ');
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Strip comment lines that the model added during a fix but were not
|
|
720
|
+
* present in the original code. Used for syntax-only fixes where the
|
|
721
|
+
* model tends to annotate its changes with "// Fixed ..." comments.
|
|
722
|
+
*/
|
|
723
|
+
stripInjectedComments(original, fixed, language) {
|
|
724
|
+
const lang = language.toLowerCase();
|
|
725
|
+
// Only handle JS/TS/Python single-line comment patterns for now
|
|
726
|
+
let wholeLineRe;
|
|
727
|
+
let inlineRe;
|
|
728
|
+
if (lang === 'python' || lang === 'py') {
|
|
729
|
+
wholeLineRe = /^\s*#\s/;
|
|
730
|
+
inlineRe = /\s+#\s.*$/;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
wholeLineRe = /^\s*\/\/\s/;
|
|
734
|
+
inlineRe = /\s+\/\/\s.*$/;
|
|
735
|
+
}
|
|
736
|
+
const origLines = original.split('\n');
|
|
737
|
+
const origSet = new Set(origLines.map(l => l.trim()));
|
|
738
|
+
const fixedLines = fixed.split('\n');
|
|
739
|
+
const result = [];
|
|
740
|
+
for (let idx = 0; idx < fixedLines.length; idx++) {
|
|
741
|
+
let line = fixedLines[idx];
|
|
742
|
+
// Remove whole-line comments not in original
|
|
743
|
+
if (wholeLineRe.test(line) && !origSet.has(line.trim())) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
// Strip inline trailing comments the model added.
|
|
747
|
+
// Only strip if the corresponding original line at the same position
|
|
748
|
+
// didn't have an inline comment at all.
|
|
749
|
+
if (inlineRe.test(line)) {
|
|
750
|
+
const origLine = idx < origLines.length ? origLines[idx] : '';
|
|
751
|
+
if (!inlineRe.test(origLine)) {
|
|
752
|
+
line = line.replace(inlineRe, '');
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
result.push(line);
|
|
756
|
+
}
|
|
757
|
+
return result.join('\n');
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Ensure the fixed code hasn't lost closing delimiters relative to the
|
|
761
|
+
* original. Counts {, }, (, ), [, ] outside strings/comments and if
|
|
762
|
+
* the fix has fewer closers than the original, appends the missing ones.
|
|
763
|
+
*/
|
|
764
|
+
repairBracketBalance(original, fixed) {
|
|
765
|
+
const count = (src) => {
|
|
766
|
+
let braces = 0, parens = 0, brackets = 0;
|
|
767
|
+
let inStr = null;
|
|
768
|
+
let inLine = false, inBlock = false;
|
|
769
|
+
for (let i = 0; i < src.length; i++) {
|
|
770
|
+
const ch = src[i], nx = src[i + 1] || '';
|
|
771
|
+
if (inLine) {
|
|
772
|
+
if (ch === '\n')
|
|
773
|
+
inLine = false;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (inBlock) {
|
|
777
|
+
if (ch === '*' && nx === '/') {
|
|
778
|
+
inBlock = false;
|
|
779
|
+
i++;
|
|
780
|
+
}
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (inStr) {
|
|
784
|
+
if (ch === inStr && src[i - 1] !== '\\')
|
|
785
|
+
inStr = null;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (ch === '/' && nx === '/') {
|
|
789
|
+
inLine = true;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (ch === '/' && nx === '*') {
|
|
793
|
+
inBlock = true;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
797
|
+
inStr = ch;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (ch === '{')
|
|
801
|
+
braces++;
|
|
802
|
+
else if (ch === '}')
|
|
803
|
+
braces--;
|
|
804
|
+
else if (ch === '(')
|
|
805
|
+
parens++;
|
|
806
|
+
else if (ch === ')')
|
|
807
|
+
parens--;
|
|
808
|
+
else if (ch === '[')
|
|
809
|
+
brackets++;
|
|
810
|
+
else if (ch === ']')
|
|
811
|
+
brackets--;
|
|
812
|
+
}
|
|
813
|
+
return { braces, parens, brackets };
|
|
814
|
+
};
|
|
815
|
+
const orig = count(original);
|
|
816
|
+
const fix = count(fixed);
|
|
817
|
+
const append = [];
|
|
818
|
+
// If original was balanced (or close) but fix is missing closers,
|
|
819
|
+
// append them. Only act when the fix is *more unbalanced* than original.
|
|
820
|
+
if (fix.braces > orig.braces) {
|
|
821
|
+
for (let i = 0; i < fix.braces - orig.braces; i++)
|
|
822
|
+
append.push('}');
|
|
823
|
+
}
|
|
824
|
+
if (fix.parens > orig.parens) {
|
|
825
|
+
for (let i = 0; i < fix.parens - orig.parens; i++)
|
|
826
|
+
append.push(')');
|
|
827
|
+
}
|
|
828
|
+
if (fix.brackets > orig.brackets) {
|
|
829
|
+
for (let i = 0; i < fix.brackets - orig.brackets; i++)
|
|
830
|
+
append.push(']');
|
|
831
|
+
}
|
|
832
|
+
if (append.length > 0) {
|
|
833
|
+
return fixed.trimEnd() + '\n' + append.join('\n');
|
|
834
|
+
}
|
|
835
|
+
return fixed;
|
|
836
|
+
}
|
|
837
|
+
}
|