vigthoria-cli 1.13.20 → 1.13.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +63 -128
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +3 -42
  12. package/dist/commands/chat.js +378 -1123
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +23 -29
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +207 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +244 -198
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +368 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +124 -1589
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +44 -266
  71. package/dist/utils/api.js +647 -3180
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +220 -94
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +5 -6
  106. package/dist/utils/fastAgentRouter.js +17 -25
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +15 -0
  128. package/dist/utils/preview-screenshot-adapter.js +51 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/runtime-temp.d.ts +76 -0
  143. package/dist/utils/runtime-temp.js +492 -0
  144. package/dist/utils/secret-policy.d.ts +20 -0
  145. package/dist/utils/secret-policy.js +160 -0
  146. package/dist/utils/session.d.ts +17 -3
  147. package/dist/utils/session.js +354 -110
  148. package/dist/utils/tool-approval-service.d.ts +17 -0
  149. package/dist/utils/tool-approval-service.js +78 -0
  150. package/dist/utils/tool-capability-providers.d.ts +57 -0
  151. package/dist/utils/tool-capability-providers.js +57 -0
  152. package/dist/utils/tools.d.ts +2 -23
  153. package/dist/utils/tools.js +226 -512
  154. package/dist/utils/update-policy.d.ts +18 -0
  155. package/dist/utils/update-policy.js +137 -0
  156. package/dist/utils/v3-agent-client.d.ts +40 -0
  157. package/dist/utils/v3-agent-client.js +165 -0
  158. package/dist/utils/v3-workspace-service.d.ts +28 -0
  159. package/dist/utils/v3-workspace-service.js +279 -0
  160. package/dist/utils/vigflow-client.d.ts +95 -0
  161. package/dist/utils/vigflow-client.js +105 -0
  162. package/dist/utils/workspace-boundary.d.ts +45 -0
  163. package/dist/utils/workspace-boundary.js +377 -0
  164. package/dist/utils/workspace-brain-service.d.ts +2 -0
  165. package/dist/utils/workspace-brain-service.js +11 -1
  166. package/dist/utils/workspace-cache.js +37 -15
  167. package/dist/utils/workspace-stream.js +35 -10
  168. package/install.ps1 +115 -142
  169. package/install.sh +112 -81
  170. package/package.json +43 -9
  171. package/release-policy.json +43 -0
  172. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  173. package/scripts/release/generate-completions.mjs +24 -0
  174. package/scripts/release/install-release.mjs +10 -0
  175. package/scripts/release/publish-cli-release.mjs +116 -0
  176. package/scripts/release/publish-cli-release.sh +2 -70
  177. package/scripts/release/resolve-release-manifest.mjs +26 -0
  178. package/scripts/release/test-balanced-model-live.sh +45 -0
  179. package/scripts/release/validate-live-service-gates.sh +93 -0
  180. package/scripts/release/validate-no-go-gates.sh +145 -174
  181. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  182. package/dist/commands/index.d.ts +0 -12
  183. package/dist/commands/index.js +0 -194
  184. package/dist/commands/update.d.ts +0 -9
  185. package/dist/commands/update.js +0 -301
  186. package/dist/utils/templateInstantPath.d.ts +0 -18
  187. 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
+ }