sonance-brand-mcp 1.3.59 → 1.3.61

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.
@@ -3,7 +3,7 @@ import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import Anthropic from "@anthropic-ai/sdk";
5
5
  import { randomUUID } from "crypto";
6
- import { discoverTheme, formatThemeForPrompt } from "./theme-discovery";
6
+ import { discoverTheme } from "./theme-discovery";
7
7
 
8
8
  /**
9
9
  * Sonance DevTools API - Apply-First Vision Mode
@@ -511,48 +511,21 @@ function searchFilesSmart(
511
511
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
512
512
  }
513
513
 
514
- const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
514
+ const VISION_SYSTEM_PROMPT = `You edit code. Make ONLY the change requested.
515
515
 
516
516
  RULES:
517
- 1. Make the SMALLEST possible change to accomplish the request
518
- 2. Do NOT refactor, rebrand, or "improve" anything else
519
- 3. Do NOT change import statements unless explicitly required
520
- 4. Do NOT change component libraries (e.g., heroui to shadcn)
521
- 5. If fixing a color/visibility issue, change ONLY that element's classes
522
- 6. Your patch should typically be 1-5 lines, not 50+
523
- 7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
524
- 8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
525
- 9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
526
-
527
- CRITICAL - ELEMENT VERIFICATION:
528
- - When a focused element is mentioned, SEARCH the provided file content for that EXACT element
529
- - Look for the actual JSX/HTML code: <button, <Button, <div, className, onClick, etc.
530
- - If you cannot find the element in the TARGET COMPONENT section, it may be in a child component
531
- - NEVER guess what element code looks like - find it in the file or report "element not found"
532
- - If the element is not in the provided file, return: {"modifications": [], "explanation": "The focused element appears to be in a child component, not in [filename]"}
533
-
534
- CRITICAL - DATA INTEGRITY:
535
- - If code references database values (like icon_name, type, status), DO NOT change the mapping keys
536
- - You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
537
- - Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
538
- - If user wants different icons, they must tell you the EXACT icon names they want
539
-
540
- PATCH FORMAT:
541
- Return ONLY raw JSON (no markdown, no preamble):
517
+ 1. Copy code EXACTLY from the file - character for character
518
+ 2. Make the SMALLEST possible change
519
+ 3. For color changes, just change the className
520
+ 4. Do not restructure or reorganize code
521
+
522
+ Return JSON:
542
523
  {
543
- "reasoning": "brief explanation",
544
524
  "modifications": [{
545
- "filePath": "path/to/file.tsx",
546
- "patches": [{
547
- "search": "exact original code (copy from provided file)",
548
- "replace": "minimal change",
549
- "explanation": "what this does"
550
- }]
551
- }],
552
- "explanation": "summary"
553
- }
554
-
555
- If you cannot find the exact code to modify, OR if you would need to guess data values, return empty modifications array with explanation.`;
525
+ "filePath": "path",
526
+ "patches": [{ "search": "exact code from file", "replace": "changed code" }]
527
+ }]
528
+ }`;
556
529
 
557
530
  export async function POST(request: Request) {
558
531
  // Only allow in development
@@ -809,143 +782,93 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
809
782
  `;
810
783
  }
811
784
 
812
- // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
785
+ // ========== TARGET COMPONENT ONLY (with line numbers) ==========
786
+ // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
813
787
  if (recommendedFileContent) {
814
- // Never truncate the recommended file - AI needs full context to avoid hallucination
815
788
  const content = recommendedFileContent.content;
816
789
 
790
+ // Add line numbers to make it easy for LLM to reference exact code
791
+ const linesWithNumbers = content.split('\n').map((line, i) =>
792
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
793
+ ).join('\n');
794
+
817
795
  textContent += `═══════════════════════════════════════════════════════════════════════════════
818
- TARGET COMPONENT - YOU MUST EDIT THIS FILE
796
+ FILE TO EDIT: ${recommendedFileContent.path}
819
797
  ═══════════════════════════════════════════════════════════════════════════════
820
798
 
821
- This is the component that renders the UI you see in the screenshot.
822
- File: ${recommendedFileContent.path}
799
+ IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
800
+ Your "search" string must match the code CHARACTER FOR CHARACTER.
823
801
 
824
802
  \`\`\`tsx
825
- ${content}
803
+ ${linesWithNumbers}
826
804
  \`\`\`
827
805
 
828
806
  `;
829
807
  usedContext += content.length;
830
- debugLog("Added TARGET COMPONENT to context", {
808
+ debugLog("Added TARGET COMPONENT with line numbers", {
831
809
  path: recommendedFileContent.path,
832
- originalSize: recommendedFileContent.content.length,
833
- includedSize: content.length
810
+ lines: content.split('\n').length,
811
+ size: content.length
834
812
  });
835
- }
836
-
837
- // ========== PAGE CONTEXT (wrapper - de-emphasized) ==========
838
- const pageContentTruncated = pageContext.pageContent.substring(0, MAX_PAGE_FILE);
839
- const pageWasTruncated = pageContext.pageContent.length > MAX_PAGE_FILE;
840
-
841
- textContent += `PAGE CONTEXT (wrapper only${recommendedFileContent ? " - DO NOT edit this, edit the TARGET COMPONENT above" : ""}):
842
-
843
- Page File: ${pageContext.pageFile || "Not found"}
844
- ${pageContext.pageContent ? `\`\`\`tsx\n${pageContentTruncated}${pageWasTruncated ? "\n// ... (wrapper truncated)" : ""}\n\`\`\`` : ""}
845
-
846
- `;
847
- usedContext += pageContentTruncated.length;
848
-
849
- // ========== SUPPORTING COMPONENTS (dynamic budget) ==========
850
- if (pageContext.componentSources.length > 0) {
851
- const remainingBudget = TOTAL_CONTEXT_BUDGET - usedContext - MAX_GLOBALS_CSS - 5000; // Reserve 5k for instructions
852
- const filesToInclude = pageContext.componentSources.slice(0, MAX_FILES);
853
- const perFileLimit = Math.max(1000, Math.floor(remainingBudget / Math.max(filesToInclude.length, 1)));
854
-
855
- textContent += `SUPPORTING COMPONENTS (${filesToInclude.length} files, ~${Math.round(perFileLimit/1000)}k chars each):\n`;
813
+ } else if (pageContext.pageContent) {
814
+ // Fallback: use page file if no recommended file
815
+ const content = pageContext.pageContent;
816
+ const linesWithNumbers = content.split('\n').map((line, i) =>
817
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
818
+ ).join('\n');
856
819
 
857
- for (const comp of filesToInclude) {
858
- if (usedContext > TOTAL_CONTEXT_BUDGET - 10000) {
859
- textContent += `\n// ... (remaining files omitted to stay within context limits)\n`;
860
- break;
861
- }
862
-
863
- const truncatedContent = comp.content.substring(0, perFileLimit);
864
- const wasTruncated = comp.content.length > perFileLimit;
865
-
866
- textContent += `
867
- File: ${comp.path}
820
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
821
+ FILE TO EDIT: ${pageContext.pageFile}
822
+ ═══════════════════════════════════════════════════════════════════════════════
823
+
868
824
  \`\`\`tsx
869
- ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
825
+ ${linesWithNumbers}
870
826
  \`\`\`
827
+
871
828
  `;
872
- usedContext += truncatedContent.length;
873
- }
829
+ usedContext += content.length;
874
830
  }
831
+
832
+ // NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
833
+ // The LLM only needs the TARGET file to make accurate edits
875
834
 
876
835
  // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
877
- // Dynamically discover theme tokens from the target codebase
878
- // This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
836
+ // Dynamically discover theme tokens (minimal - just for logging)
879
837
  const discoveredTheme = await discoverTheme(projectRoot);
880
- const themeContext = formatThemeForPrompt(discoveredTheme);
881
838
 
882
839
  if (discoveredTheme.discoveredFiles.length > 0) {
883
- textContent += `
884
- ═══════════════════════════════════════════════════════════════════════════════
885
- REFERENCE ONLY (do not use this to justify additional changes)
886
- ═══════════════════════════════════════════════════════════════════════════════
887
-
888
- If you need to pick a color for a VISIBILITY fix, these are safe choices:
889
- - bg-accent text-white (cyan button with white text)
890
- - bg-primary text-white (charcoal button with white text)
891
- - bg-success text-white (green button with white text)
892
- - bg-destructive text-white (red button with white text)
893
-
894
- But ONLY use these if the user is asking for a color/visibility change.
895
- Do NOT rebrand or change other elements to match.
896
-
897
- `;
898
840
  debugLog("Theme discovery complete", {
899
841
  filesFound: discoveredTheme.discoveredFiles,
900
- cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
901
- tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
902
842
  });
903
843
  }
904
844
 
905
- // ========== GLOBALS CSS ==========
906
- const globalsTruncated = pageContext.globalsCSS.substring(0, MAX_GLOBALS_CSS);
907
- textContent += `
908
- GLOBALS.CSS (theme variables):
909
- \`\`\`css
910
- ${globalsTruncated}${pageContext.globalsCSS.length > MAX_GLOBALS_CSS ? "\n/* ... (truncated) */" : ""}
911
- \`\`\`
912
-
913
- `;
914
-
915
- // ========== VALID FILES LIST ==========
916
- const validFilesList: string[] = [];
917
- const recommendedPath = recommendedFile?.path;
918
-
919
- // Add recommended file first with marker
920
- if (recommendedPath) {
921
- validFilesList.push(`- ${recommendedPath} (*** TARGET - EDIT THIS FILE ***)`);
922
- }
845
+ // ========== SIMPLIFIED INSTRUCTIONS ==========
846
+ const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
923
847
 
924
- // Add page file (if not the recommended file)
925
- if (pageContext.pageFile && pageContext.pageFile !== recommendedPath) {
926
- validFilesList.push(`- ${pageContext.pageFile} (wrapper only)`);
927
- }
928
-
929
- // Add other component sources (excluding recommended file)
930
- for (const comp of pageContext.componentSources) {
931
- if (comp.path !== recommendedPath) {
932
- validFilesList.push(`- ${comp.path}`);
933
- }
934
- }
848
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
849
+ HOW TO MAKE YOUR EDIT
850
+ ═══════════════════════════════════════════════════════════════════════════════
851
+
852
+ 1. Find the EXACT code in the file above that needs to change
853
+ 2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
854
+ 3. Make your change to the copied code
855
+ 4. Return a patch with the exact "search" and "replace" strings
935
856
 
936
- textContent += `VALID FILES YOU MAY EDIT:
937
- ${validFilesList.join("\n")}
857
+ EXAMPLE - If line 84 shows:
858
+ 84| <div className="mb-10 md:mb-16 grid grid-cols-2">
938
859
 
939
- INSTRUCTIONS:
940
- 1. Look at the screenshot and identify elements mentioned in the user's request
941
- 2. The TARGET COMPONENT shown first contains the UI - EDIT THAT FILE
942
- 3. Make SURGICAL EDITS - change only the specific lines needed
943
- 4. PRESERVE all existing logic, hooks, API calls, and error handling
944
- 5. Return patches in the specified format (search/replace)
945
- 6. Only use file paths from the VALID FILES list above
946
- 7. DO NOT edit the page wrapper unless the TARGET COMPONENT is unavailable
860
+ Your patch should be:
861
+ {
862
+ "modifications": [{
863
+ "filePath": "${targetPath}",
864
+ "patches": [{
865
+ "search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
866
+ "replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
867
+ }]
868
+ }]
869
+ }
947
870
 
948
- CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
871
+ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
949
872
 
950
873
  messageContent.push({
951
874
  type: "text",
@@ -975,7 +898,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
975
898
  let lastPatchErrors: string[] = [];
976
899
  let modifications: VisionFileModification[] = [];
977
900
  let finalExplanation: string | undefined;
978
- let finalReasoning: string | undefined;
979
901
 
980
902
  while (retryCount <= MAX_RETRIES) {
981
903
  // Build messages for this attempt
@@ -1025,93 +947,88 @@ This is better than generating patches with made-up code.`,
1025
947
  });
1026
948
  }
1027
949
 
1028
- const response = await anthropic.messages.create({
1029
- model: "claude-sonnet-4-20250514",
1030
- max_tokens: 16384,
950
+ const response = await anthropic.messages.create({
951
+ model: "claude-sonnet-4-20250514",
952
+ max_tokens: 16384,
1031
953
  messages: currentMessages,
1032
- system: VISION_SYSTEM_PROMPT,
1033
- });
954
+ system: VISION_SYSTEM_PROMPT,
955
+ });
1034
956
 
1035
- // Extract text content from response
1036
- const textResponse = response.content.find((block) => block.type === "text");
1037
- if (!textResponse || textResponse.type !== "text") {
1038
- return NextResponse.json(
1039
- { error: "No text response from AI" },
1040
- { status: 500 }
1041
- );
1042
- }
957
+ // Extract text content from response
958
+ const textResponse = response.content.find((block) => block.type === "text");
959
+ if (!textResponse || textResponse.type !== "text") {
960
+ return NextResponse.json(
961
+ { error: "No text response from AI" },
962
+ { status: 500 }
963
+ );
964
+ }
1043
965
 
1044
- // Parse AI response - now expecting patches instead of full file content
1045
- let aiResponse: {
1046
- reasoning?: string;
1047
- modifications: Array<{
1048
- filePath: string;
1049
- patches?: Patch[];
1050
- // Legacy support for modifiedContent (will be deprecated)
1051
- modifiedContent?: string;
1052
- explanation?: string;
1053
- }>;
966
+ // Parse AI response - expecting modifications array
967
+ let aiResponse: {
968
+ modifications: Array<{
969
+ filePath: string;
970
+ patches?: Patch[];
1054
971
  explanation?: string;
1055
- };
1056
-
1057
- try {
1058
- let jsonText = textResponse.text.trim();
1059
-
1060
- // Try to extract JSON from markdown code blocks
1061
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
1062
- jsonText.match(/```\n([\s\S]*?)\n```/);
972
+ }>;
973
+ explanation?: string;
974
+ };
1063
975
 
1064
- if (jsonMatch) {
1065
- jsonText = jsonMatch[1];
1066
- } else if (jsonText.includes("```json")) {
1067
- const start = jsonText.indexOf("```json") + 7;
1068
- const end = jsonText.lastIndexOf("```");
1069
- if (end > start) {
1070
- jsonText = jsonText.substring(start, end);
1071
- }
976
+ try {
977
+ let jsonText = textResponse.text.trim();
978
+
979
+ // Try to extract JSON from markdown code blocks
980
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
981
+ jsonText.match(/```\n([\s\S]*?)\n```/);
982
+
983
+ if (jsonMatch) {
984
+ jsonText = jsonMatch[1];
985
+ } else if (jsonText.includes("```json")) {
986
+ const start = jsonText.indexOf("```json") + 7;
987
+ const end = jsonText.lastIndexOf("```");
988
+ if (end > start) {
989
+ jsonText = jsonText.substring(start, end);
1072
990
  }
991
+ }
1073
992
 
1074
- jsonText = jsonText.trim();
1075
-
1076
- // Robust JSON extraction: find the first { and last } to extract JSON object
1077
- // This handles cases where the LLM includes preamble text before the JSON
1078
- const firstBrace = jsonText.indexOf('{');
1079
- const lastBrace = jsonText.lastIndexOf('}');
1080
- if (firstBrace !== -1 && lastBrace > firstBrace) {
1081
- jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1082
- }
1083
-
1084
- aiResponse = JSON.parse(jsonText);
1085
- } catch {
1086
- console.error("Failed to parse AI response:", textResponse.text);
1087
- return NextResponse.json(
1088
- { error: "Failed to parse AI response. Please try again." },
1089
- { status: 500 }
1090
- );
993
+ jsonText = jsonText.trim();
994
+
995
+ // Robust JSON extraction: find the first { and last } to extract JSON object
996
+ // This handles cases where the LLM includes preamble text before the JSON
997
+ const firstBrace = jsonText.indexOf('{');
998
+ const lastBrace = jsonText.lastIndexOf('}');
999
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
1000
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1091
1001
  }
1002
+
1003
+ aiResponse = JSON.parse(jsonText);
1004
+ } catch {
1005
+ console.error("Failed to parse AI response:", textResponse.text);
1006
+ return NextResponse.json(
1007
+ { error: "Failed to parse AI response. Please try again." },
1008
+ { status: 500 }
1009
+ );
1010
+ }
1092
1011
 
1093
1012
  finalExplanation = aiResponse.explanation;
1094
- finalReasoning = aiResponse.reasoning;
1095
-
1096
- if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
1097
- return NextResponse.json({
1098
- success: true,
1099
- sessionId: newSessionId,
1100
- modifications: [],
1101
- explanation: aiResponse.explanation || "No changes needed.",
1102
- reasoning: aiResponse.reasoning,
1013
+
1014
+ if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
1015
+ return NextResponse.json({
1016
+ success: true,
1017
+ sessionId: newSessionId,
1018
+ modifications: [],
1019
+ explanation: aiResponse.explanation || "No changes needed.",
1103
1020
  });
1104
1021
  }
1105
1022
 
1106
- debugLog("VALIDATION: Valid file paths from page context", {
1107
- pageFile: pageContext.pageFile,
1108
- validFilePaths: Array.from(validFilePaths),
1109
- aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
1110
- });
1023
+ debugLog("VALIDATION: Valid file paths from page context", {
1024
+ pageFile: pageContext.pageFile,
1025
+ validFilePaths: Array.from(validFilePaths),
1026
+ aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
1027
+ });
1111
1028
 
1112
- // Process modifications - apply patches to get modified content
1029
+ // Process modifications - apply patches to get modified content
1113
1030
  modifications = [];
1114
- const patchErrors: string[] = [];
1031
+ const patchErrors: string[] = [];
1115
1032
 
1116
1033
  for (const mod of aiResponse.modifications) {
1117
1034
  // Validate that the file path is in the page context
@@ -1131,6 +1048,19 @@ This is better than generating patches with made-up code.`,
1131
1048
  continue;
1132
1049
  }
1133
1050
 
1051
+ // CRITICAL: Warn if LLM is trying to modify a file OTHER than the TARGET COMPONENT
1052
+ // This usually means the LLM is trying to modify a file it doesn't have full visibility into
1053
+ const targetComponentPath = recommendedFileContent?.path;
1054
+ if (targetComponentPath && mod.filePath !== targetComponentPath) {
1055
+ debugLog("WARNING: LLM trying to modify non-target file", {
1056
+ targetComponent: targetComponentPath,
1057
+ attemptedFile: mod.filePath,
1058
+ warning: "LLM may be hallucinating code since it only has full content of the TARGET COMPONENT"
1059
+ });
1060
+ console.warn(`[Apply-First] ⚠️ LLM is modifying ${mod.filePath} but TARGET COMPONENT is ${targetComponentPath}`);
1061
+ console.warn(`[Apply-First] ⚠️ This may cause hallucination since LLM only has full visibility into the TARGET COMPONENT`);
1062
+ }
1063
+
1134
1064
  const fullPath = path.join(projectRoot, mod.filePath);
1135
1065
  let originalContent = "";
1136
1066
  if (fs.existsSync(fullPath)) {
@@ -1206,20 +1136,37 @@ This is better than generating patches with made-up code.`,
1206
1136
  modifiedContent = patchResult.modifiedContent;
1207
1137
  console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
1208
1138
  }
1209
- } else if (mod.modifiedContent) {
1210
- // Legacy: AI returned full file content
1211
- console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
1212
- modifiedContent = mod.modifiedContent;
1213
1139
 
1214
- // Validate the modification using legacy validation
1215
- const validation = validateModification(originalContent, modifiedContent, mod.filePath);
1216
- if (!validation.valid) {
1217
- patchErrors.push(`${mod.filePath}: ${validation.error}`);
1140
+ // SYNTAX VALIDATION: Check for common JSX/HTML tag mismatches
1141
+ // This catches cases where the LLM changes opening tags but not closing tags
1142
+ if (mod.filePath.endsWith('.tsx') || mod.filePath.endsWith('.jsx')) {
1143
+ const openDivs = (modifiedContent.match(/<div[\s>]/g) || []).length;
1144
+ const closeDivs = (modifiedContent.match(/<\/div>/g) || []).length;
1145
+ const openSpans = (modifiedContent.match(/<span[\s>]/g) || []).length;
1146
+ const closeSpans = (modifiedContent.match(/<\/span>/g) || []).length;
1147
+
1148
+ if (openDivs !== closeDivs || openSpans !== closeSpans) {
1149
+ debugLog("SYNTAX WARNING: Tag mismatch detected", {
1150
+ filePath: mod.filePath,
1151
+ divs: { open: openDivs, close: closeDivs },
1152
+ spans: { open: openSpans, close: closeSpans },
1153
+ });
1154
+ console.warn(`[Apply-First] ⚠️ SYNTAX WARNING: Tag mismatch in ${mod.filePath}`);
1155
+ console.warn(`[Apply-First] divs: ${openDivs} open, ${closeDivs} close`);
1156
+ console.warn(`[Apply-First] spans: ${openSpans} open, ${closeSpans} close`);
1157
+
1158
+ // If there's a significant mismatch, reject the change
1159
+ const divDiff = Math.abs(openDivs - closeDivs);
1160
+ const spanDiff = Math.abs(openSpans - closeSpans);
1161
+ if (divDiff > 0 || spanDiff > 0) {
1162
+ patchErrors.push(`${mod.filePath}: LLM introduced syntax error - tag mismatch detected (${divDiff} div, ${spanDiff} span). Change rejected.`);
1218
1163
  continue;
1164
+ }
1165
+ }
1219
1166
  }
1220
1167
  } else {
1221
- // No patches and no modifiedContent - skip
1222
- console.warn(`[Apply-First] No patches or modifiedContent for ${mod.filePath}`);
1168
+ // No patches - skip
1169
+ console.warn(`[Apply-First] No patches for ${mod.filePath}`);
1223
1170
  continue;
1224
1171
  }
1225
1172
 
@@ -1233,7 +1180,7 @@ This is better than generating patches with made-up code.`,
1233
1180
  }
1234
1181
 
1235
1182
  // If all modifications failed, check if we should retry
1236
- if (patchErrors.length > 0 && modifications.length === 0) {
1183
+ if (patchErrors.length > 0 && modifications.length === 0) {
1237
1184
  if (retryCount < MAX_RETRIES) {
1238
1185
  console.warn(`[Apply-First] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1239
1186
  debugLog("Retry triggered due to patch failures", {
@@ -1248,20 +1195,20 @@ This is better than generating patches with made-up code.`,
1248
1195
 
1249
1196
  // Exhausted retries, return error
1250
1197
  console.error("All AI patches failed after retries:", patchErrors);
1251
- return NextResponse.json(
1252
- {
1253
- success: false,
1198
+ return NextResponse.json(
1199
+ {
1200
+ success: false,
1254
1201
  error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1255
- },
1256
- { status: 400 }
1257
- );
1258
- }
1202
+ },
1203
+ { status: 400 }
1204
+ );
1205
+ }
1206
+
1207
+ // Log patch errors as warnings if some modifications succeeded
1208
+ if (patchErrors.length > 0) {
1209
+ console.warn("Some patches failed:", patchErrors);
1210
+ }
1259
1211
 
1260
- // Log patch errors as warnings if some modifications succeeded
1261
- if (patchErrors.length > 0) {
1262
- console.warn("Some patches failed:", patchErrors);
1263
- }
1264
-
1265
1212
  // Successfully processed at least some modifications - break out of retry loop
1266
1213
  break;
1267
1214
  } // End of retry loop
@@ -1278,7 +1225,6 @@ This is better than generating patches with made-up code.`,
1278
1225
  preview: true,
1279
1226
  modifications,
1280
1227
  explanation: finalExplanation,
1281
- reasoning: finalReasoning,
1282
1228
  });
1283
1229
  }
1284
1230
 
@@ -1302,7 +1248,6 @@ This is better than generating patches with made-up code.`,
1302
1248
  modifications,
1303
1249
  backupPaths: applyResult.backupPaths,
1304
1250
  explanation: finalExplanation,
1305
- reasoning: finalReasoning,
1306
1251
  });
1307
1252
  }
1308
1253
 
@@ -2114,13 +2059,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
2114
2059
 
2115
2060
  // Only log parse error once to avoid log spam
2116
2061
  if (!cachedParseError) {
2117
- const posMatch = errorStr.match(/position (\d+)/);
2118
- let context = "";
2119
- if (posMatch) {
2120
- const pos = parseInt(posMatch[1], 10);
2121
- const content = fs.readFileSync(tsconfigPath, "utf-8");
2122
- context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
2123
- }
2062
+ const posMatch = errorStr.match(/position (\d+)/);
2063
+ let context = "";
2064
+ if (posMatch) {
2065
+ const pos = parseInt(posMatch[1], 10);
2066
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
2067
+ context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
2068
+ }
2124
2069
  debugLog("[apply] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
2125
2070
  cachedParseError = errorStr;
2126
2071
  }
@@ -2143,14 +2088,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
2143
2088
  }
2144
2089
  // Only log default alias once (not when using cached error fallback)
2145
2090
  if (!cachedParseError) {
2146
- debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
2091
+ debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
2147
2092
  }
2148
2093
  }
2149
2094
 
2150
2095
  // Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
2151
2096
  if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
2152
- cachedPathAliases = aliases;
2153
- cachedProjectRoot = projectRoot;
2097
+ cachedPathAliases = aliases;
2098
+ cachedProjectRoot = projectRoot;
2154
2099
  }
2155
2100
 
2156
2101
  return aliases;
@@ -2452,105 +2397,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
2452
2397
  failedPatches,
2453
2398
  };
2454
2399
  }
2455
-
2456
- /**
2457
- * Validate that AI modifications are surgical edits, not complete rewrites
2458
- */
2459
- interface ValidationResult {
2460
- valid: boolean;
2461
- error?: string;
2462
- warnings: string[];
2463
- }
2464
-
2465
- function validateModification(
2466
- originalContent: string,
2467
- modifiedContent: string,
2468
- filePath: string
2469
- ): ValidationResult {
2470
- const warnings: string[] = [];
2471
-
2472
- // Skip validation for new files (no original content)
2473
- if (!originalContent || originalContent.trim() === "") {
2474
- return { valid: true, warnings: ["New file - no original to compare"] };
2475
- }
2476
-
2477
- const originalLines = originalContent.split("\n");
2478
- const modifiedLines = modifiedContent.split("\n");
2479
-
2480
- // Check 1: Truncation detection - look for placeholder comments
2481
- const truncationPatterns = [
2482
- /\/\/\s*\.\.\.\s*existing/i,
2483
- /\/\/\s*\.\.\.\s*rest\s*of/i,
2484
- /\/\/\s*\.\.\.\s*more\s*code/i,
2485
- /\/\*\s*\.\.\.\s*\*\//,
2486
- /\/\/\s*\.\.\./,
2487
- ];
2488
-
2489
- for (const pattern of truncationPatterns) {
2490
- if (pattern.test(modifiedContent)) {
2491
- return {
2492
- valid: false,
2493
- error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
2494
- warnings,
2495
- };
2496
- }
2497
- }
2498
-
2499
- // Check 2: Line count shrinkage - reject if file shrinks by more than 30%
2500
- const lineDelta = modifiedLines.length - originalLines.length;
2501
- const shrinkagePercent = (lineDelta / originalLines.length) * 100;
2502
-
2503
- if (shrinkagePercent < -30) {
2504
- return {
2505
- valid: false,
2506
- error: `File ${filePath} shrank from ${originalLines.length} to ${modifiedLines.length} lines (${Math.abs(shrinkagePercent).toFixed(0)}% reduction). This suggests the AI rewrote the file instead of making surgical edits. Please try a more specific request.`,
2507
- warnings,
2508
- };
2509
- }
2510
-
2511
- if (shrinkagePercent < -15) {
2512
- warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
2513
- }
2514
-
2515
- // Check 3: Change percentage - warn if too many lines are different
2516
- let changedLines = 0;
2517
- const minLines = Math.min(originalLines.length, modifiedLines.length);
2518
-
2519
- for (let i = 0; i < minLines; i++) {
2520
- if (originalLines[i] !== modifiedLines[i]) {
2521
- changedLines++;
2522
- }
2523
- }
2524
-
2525
- // Add lines that were added or removed
2526
- changedLines += Math.abs(originalLines.length - modifiedLines.length);
2527
-
2528
- const changePercent = (changedLines / originalLines.length) * 100;
2529
-
2530
- if (changePercent > 50) {
2531
- return {
2532
- valid: false,
2533
- error: `File ${filePath} has ${changePercent.toFixed(0)}% of lines changed. This suggests the AI rewrote the file instead of making surgical edits. For safety, this change has been rejected. Please try a more specific request.`,
2534
- warnings,
2535
- };
2536
- }
2537
-
2538
- if (changePercent > 30) {
2539
- warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
2540
- }
2541
-
2542
- // Check 4: Import preservation - ensure imports aren't removed
2543
- const importRegex = /^import\s+/gm;
2544
- const originalImports = (originalContent.match(importRegex) || []).length;
2545
- const modifiedImports = (modifiedContent.match(importRegex) || []).length;
2546
-
2547
- if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
2548
- return {
2549
- valid: false,
2550
- error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
2551
- warnings,
2552
- };
2553
- }
2554
-
2555
- return { valid: true, warnings };
2556
- }