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.
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import Anthropic from "@anthropic-ai/sdk";
5
- import { discoverTheme, formatThemeForPrompt } from "../sonance-vision-apply/theme-discovery";
5
+ import { discoverTheme } from "../sonance-vision-apply/theme-discovery";
6
6
 
7
7
  /**
8
8
  * Sonance DevTools API - Vision Mode Editor
@@ -507,50 +507,21 @@ function searchFilesSmart(
507
507
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
508
508
  }
509
509
 
510
- const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
510
+ const VISION_SYSTEM_PROMPT = `You edit code. Make ONLY the change requested.
511
511
 
512
512
  RULES:
513
- 1. Make the SMALLEST possible change to accomplish the request
514
- 2. Do NOT refactor, rebrand, or "improve" anything else
515
- 3. Do NOT change import statements unless explicitly required
516
- 4. Do NOT change component libraries (e.g., heroui to shadcn)
517
- 5. If fixing a color/visibility issue, change ONLY that element's classes
518
- 6. Your patch should typically be 1-5 lines, not 50+
519
- 7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
520
- 8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
521
- 9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
522
-
523
- CRITICAL - ELEMENT VERIFICATION:
524
- - When a focused element is mentioned, SEARCH the provided file content for that EXACT element
525
- - Look for the actual JSX/HTML code: <button, <Button, <div, className, onClick, etc.
526
- - If you cannot find the element in the TARGET COMPONENT section, it may be in a child component
527
- - NEVER guess what element code looks like - find it in the file or report "element not found"
528
- - 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]"}
529
-
530
- CRITICAL - DATA INTEGRITY:
531
- - If code references database values (like icon_name, type, status), DO NOT change the mapping keys
532
- - You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
533
- - Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
534
- - If user wants different icons, they must tell you the EXACT icon names they want
535
-
536
- PATCH FORMAT:
537
- Return ONLY raw JSON (no markdown, no preamble):
513
+ 1. Copy code EXACTLY from the file - character for character
514
+ 2. Make the SMALLEST possible change
515
+ 3. For color changes, just change the className
516
+ 4. Do not restructure or reorganize code
517
+
518
+ Return JSON:
538
519
  {
539
- "reasoning": "brief explanation",
540
520
  "modifications": [{
541
- "filePath": "path/to/file.tsx",
542
- "patches": [{
543
- "search": "exact original code (copy from provided file)",
544
- "replace": "minimal change",
545
- "explanation": "what this does"
546
- }],
547
- "previewCSS": "optional CSS for live preview"
548
- }],
549
- "aggregatedPreviewCSS": "combined CSS for all changes",
550
- "explanation": "summary"
551
- }
552
-
553
- If you cannot find the exact code to modify, OR if you would need to guess data values, return empty modifications array with explanation.`;
521
+ "filePath": "path",
522
+ "patches": [{ "search": "exact code from file", "replace": "changed code" }]
523
+ }]
524
+ }`;
554
525
 
555
526
  export async function POST(request: Request) {
556
527
  // Only allow in development
@@ -780,143 +751,92 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
780
751
  `;
781
752
  }
782
753
 
783
- // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
754
+ // ========== TARGET COMPONENT ONLY (with line numbers) ==========
755
+ // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
784
756
  if (recommendedFileContent) {
785
- // Never truncate the recommended file - AI needs full context to avoid hallucination
786
757
  const content = recommendedFileContent.content;
787
758
 
759
+ // Add line numbers to make it easy for LLM to reference exact code
760
+ const linesWithNumbers = content.split('\n').map((line, i) =>
761
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
762
+ ).join('\n');
763
+
788
764
  textContent += `═══════════════════════════════════════════════════════════════════════════════
789
- TARGET COMPONENT - YOU MUST EDIT THIS FILE
765
+ FILE TO EDIT: ${recommendedFileContent.path}
790
766
  ═══════════════════════════════════════════════════════════════════════════════
791
767
 
792
- This is the component that renders the UI you see in the screenshot.
793
- File: ${recommendedFileContent.path}
768
+ IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
769
+ Your "search" string must match the code CHARACTER FOR CHARACTER.
794
770
 
795
771
  \`\`\`tsx
796
- ${content}
772
+ ${linesWithNumbers}
797
773
  \`\`\`
798
774
 
799
775
  `;
800
776
  usedContext += content.length;
801
- debugLog("Added TARGET COMPONENT to context", {
777
+ debugLog("Added TARGET COMPONENT with line numbers", {
802
778
  path: recommendedFileContent.path,
803
- originalSize: recommendedFileContent.content.length,
804
- includedSize: content.length
779
+ lines: content.split('\n').length,
780
+ size: content.length
805
781
  });
806
- }
807
-
808
- // ========== PAGE CONTEXT (wrapper - de-emphasized) ==========
809
- const pageContentTruncated = pageContext.pageContent.substring(0, MAX_PAGE_FILE);
810
- const pageWasTruncated = pageContext.pageContent.length > MAX_PAGE_FILE;
811
-
812
- textContent += `PAGE CONTEXT (wrapper only${recommendedFileContent ? " - DO NOT edit this, edit the TARGET COMPONENT above" : ""}):
813
-
814
- Page File: ${pageContext.pageFile || "Not found"}
815
- ${pageContext.pageContent ? `\`\`\`tsx\n${pageContentTruncated}${pageWasTruncated ? "\n// ... (wrapper truncated)" : ""}\n\`\`\`` : ""}
816
-
817
- `;
818
- usedContext += pageContentTruncated.length;
819
-
820
- // ========== SUPPORTING COMPONENTS (dynamic budget) ==========
821
- if (pageContext.componentSources.length > 0) {
822
- const remainingBudget = TOTAL_CONTEXT_BUDGET - usedContext - MAX_GLOBALS_CSS - 5000; // Reserve 5k for instructions
823
- const filesToInclude = pageContext.componentSources.slice(0, MAX_FILES);
824
- const perFileLimit = Math.max(1000, Math.floor(remainingBudget / Math.max(filesToInclude.length, 1)));
782
+ } else if (pageContext.pageContent) {
783
+ // Fallback: use page file if no recommended file
784
+ const content = pageContext.pageContent;
785
+ const linesWithNumbers = content.split('\n').map((line, i) =>
786
+ `${String(i + 1).padStart(4, ' ')}| ${line}`
787
+ ).join('\n');
825
788
 
826
- textContent += `SUPPORTING COMPONENTS (${filesToInclude.length} files, ~${Math.round(perFileLimit/1000)}k chars each):\n`;
827
-
828
- for (const comp of filesToInclude) {
829
- if (usedContext > TOTAL_CONTEXT_BUDGET - 10000) {
830
- textContent += `\n// ... (remaining files omitted to stay within context limits)\n`;
831
- break;
832
- }
833
-
834
- const truncatedContent = comp.content.substring(0, perFileLimit);
835
- const wasTruncated = comp.content.length > perFileLimit;
836
-
837
- textContent += `
838
- File: ${comp.path}
789
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
790
+ FILE TO EDIT: ${pageContext.pageFile}
791
+ ═══════════════════════════════════════════════════════════════════════════════
792
+
839
793
  \`\`\`tsx
840
- ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
794
+ ${linesWithNumbers}
841
795
  \`\`\`
796
+
842
797
  `;
843
- usedContext += truncatedContent.length;
844
- }
798
+ usedContext += content.length;
845
799
  }
800
+
801
+ // NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
802
+ // The LLM only needs the TARGET file to make accurate edits
846
803
 
847
- // ========== THEME DISCOVERY (REFERENCE ONLY) ==========
848
- // Dynamically discover theme tokens from the target codebase
849
- // This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
804
+ // Dynamically discover theme tokens (minimal - just for logging)
850
805
  const discoveredTheme = await discoverTheme(projectRoot);
851
- const themeContext = formatThemeForPrompt(discoveredTheme);
852
806
 
853
807
  if (discoveredTheme.discoveredFiles.length > 0) {
854
- textContent += `
855
- ═══════════════════════════════════════════════════════════════════════════════
856
- REFERENCE ONLY (do not use this to justify additional changes)
857
- ═══════════════════════════════════════════════════════════════════════════════
858
-
859
- If you need to pick a color for a VISIBILITY fix, these are safe choices:
860
- - bg-accent text-white (cyan button with white text)
861
- - bg-primary text-white (charcoal button with white text)
862
- - bg-success text-white (green button with white text)
863
- - bg-destructive text-white (red button with white text)
864
-
865
- But ONLY use these if the user is asking for a color/visibility change.
866
- Do NOT rebrand or change other elements to match.
867
-
868
- `;
869
808
  debugLog("Theme discovery complete", {
870
809
  filesFound: discoveredTheme.discoveredFiles,
871
- cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
872
- tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
873
810
  });
874
811
  }
875
812
 
876
- // ========== GLOBALS CSS ==========
877
- const globalsTruncated = pageContext.globalsCSS.substring(0, MAX_GLOBALS_CSS);
878
- textContent += `
879
- GLOBALS.CSS (theme variables):
880
- \`\`\`css
881
- ${globalsTruncated}${pageContext.globalsCSS.length > MAX_GLOBALS_CSS ? "\n/* ... (truncated) */" : ""}
882
- \`\`\`
883
-
884
- `;
885
-
886
- // ========== VALID FILES LIST ==========
887
- const validFilesList: string[] = [];
888
- const recommendedPath = recommendedFile?.path;
889
-
890
- // Add recommended file first with marker
891
- if (recommendedPath) {
892
- validFilesList.push(`- ${recommendedPath} (*** TARGET - EDIT THIS FILE ***)`);
893
- }
813
+ // ========== SIMPLIFIED INSTRUCTIONS ==========
814
+ const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
894
815
 
895
- // Add page file (if not the recommended file)
896
- if (pageContext.pageFile && pageContext.pageFile !== recommendedPath) {
897
- validFilesList.push(`- ${pageContext.pageFile} (wrapper only)`);
898
- }
899
-
900
- // Add other component sources (excluding recommended file)
901
- for (const comp of pageContext.componentSources) {
902
- if (comp.path !== recommendedPath) {
903
- validFilesList.push(`- ${comp.path}`);
904
- }
905
- }
816
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
817
+ HOW TO MAKE YOUR EDIT
818
+ ═══════════════════════════════════════════════════════════════════════════════
819
+
820
+ 1. Find the EXACT code in the file above that needs to change
821
+ 2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
822
+ 3. Make your change to the copied code
823
+ 4. Return a patch with the exact "search" and "replace" strings
906
824
 
907
- textContent += `VALID FILES YOU MAY EDIT:
908
- ${validFilesList.join("\n")}
825
+ EXAMPLE - If line 84 shows:
826
+ 84| <div className="mb-10 md:mb-16 grid grid-cols-2">
909
827
 
910
- INSTRUCTIONS:
911
- 1. Look at the screenshot and identify elements mentioned in the user's request
912
- 2. The TARGET COMPONENT shown first contains the UI - EDIT THAT FILE
913
- 3. Choose which of the VALID FILES above need modifications
914
- 4. Generate complete modified code for each file
915
- 5. Provide previewCSS for immediate visual feedback
916
- 6. Return as JSON in the specified format
917
- 7. DO NOT edit the page wrapper unless the TARGET COMPONENT is unavailable
828
+ Your patch should be:
829
+ {
830
+ "modifications": [{
831
+ "filePath": "${targetPath}",
832
+ "patches": [{
833
+ "search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
834
+ "replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
835
+ }]
836
+ }]
837
+ }
918
838
 
919
- CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
839
+ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
920
840
 
921
841
  messageContent.push({
922
842
  type: "text",
@@ -941,8 +861,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
941
861
  let lastPatchErrors: string[] = [];
942
862
  let modificationsWithOriginals: VisionFileModification[] = [];
943
863
  let finalExplanation: string | undefined;
944
- let finalReasoning: string | undefined;
945
- let finalAggregatedCSS: string | undefined;
946
864
 
947
865
  while (retryCount <= MAX_RETRIES) {
948
866
  // Build messages for this attempt
@@ -992,84 +910,86 @@ This is better than generating patches with made-up code.`,
992
910
  });
993
911
  }
994
912
 
995
- const response = await anthropic.messages.create({
996
- model: "claude-sonnet-4-20250514",
997
- max_tokens: 16384,
913
+ const response = await anthropic.messages.create({
914
+ model: "claude-sonnet-4-20250514",
915
+ max_tokens: 16384,
998
916
  messages: currentMessages,
999
- system: VISION_SYSTEM_PROMPT,
1000
- });
917
+ system: VISION_SYSTEM_PROMPT,
918
+ });
1001
919
 
1002
- // Extract text content from response
1003
- const textResponse = response.content.find((block) => block.type === "text");
1004
- if (!textResponse || textResponse.type !== "text") {
1005
- return NextResponse.json(
1006
- { error: "No text response from AI" },
1007
- { status: 500 }
1008
- );
1009
- }
920
+ // Extract text content from response
921
+ const textResponse = response.content.find((block) => block.type === "text");
922
+ if (!textResponse || textResponse.type !== "text") {
923
+ return NextResponse.json(
924
+ { error: "No text response from AI" },
925
+ { status: 500 }
926
+ );
927
+ }
1010
928
 
1011
- // Parse AI response - now expecting patches instead of full file content
1012
- let aiResponse: {
1013
- reasoning?: string;
1014
- modifications: Array<{
1015
- filePath: string;
1016
- patches?: Patch[];
1017
- // Legacy support for modifiedContent (will be deprecated)
1018
- modifiedContent?: string;
1019
- explanation?: string;
1020
- previewCSS?: string;
1021
- }>;
1022
- aggregatedPreviewCSS?: string;
929
+ // Parse AI response - expecting modifications array
930
+ let aiResponse: {
931
+ modifications: Array<{
932
+ filePath: string;
933
+ patches?: Patch[];
1023
934
  explanation?: string;
1024
- };
935
+ previewCSS?: string;
936
+ }>;
937
+ explanation?: string;
938
+ };
1025
939
 
1026
- try {
1027
- let jsonText = textResponse.text.trim();
1028
-
1029
- // Try to extract JSON from markdown code blocks
1030
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
1031
- jsonText.match(/```\n([\s\S]*?)\n```/);
1032
-
1033
- if (jsonMatch) {
1034
- jsonText = jsonMatch[1];
1035
- } else if (jsonText.includes("```json")) {
1036
- // Fallback for cases where regex might miss due to newlines
1037
- const start = jsonText.indexOf("```json") + 7;
1038
- const end = jsonText.lastIndexOf("```");
1039
- if (end > start) {
1040
- jsonText = jsonText.substring(start, end);
1041
- }
1042
- }
940
+ try {
941
+ let jsonText = textResponse.text.trim();
942
+
943
+ // Try to extract JSON from markdown code blocks
944
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
945
+ jsonText.match(/```\n([\s\S]*?)\n```/);
946
+
947
+ if (jsonMatch) {
948
+ jsonText = jsonMatch[1];
949
+ } else if (jsonText.includes("```json")) {
950
+ // Fallback for cases where regex might miss due to newlines
951
+ const start = jsonText.indexOf("```json") + 7;
952
+ const end = jsonText.lastIndexOf("```");
953
+ if (end > start) {
954
+ jsonText = jsonText.substring(start, end);
955
+ }
956
+ }
1043
957
 
1044
- // Clean up any remaining whitespace
1045
- jsonText = jsonText.trim();
1046
-
1047
- // Robust JSON extraction: find the first { and last } to extract JSON object
1048
- // This handles cases where the LLM includes preamble text before the JSON
1049
- const firstBrace = jsonText.indexOf('{');
1050
- const lastBrace = jsonText.lastIndexOf('}');
1051
- if (firstBrace !== -1 && lastBrace > firstBrace) {
1052
- jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1053
- }
1054
-
1055
- aiResponse = JSON.parse(jsonText);
1056
- } catch {
1057
- console.error("Failed to parse AI response:", textResponse.text);
1058
- return NextResponse.json(
1059
- { error: "Failed to parse AI response. Please try again." },
1060
- { status: 500 }
1061
- );
958
+ // Clean up any remaining whitespace
959
+ jsonText = jsonText.trim();
960
+
961
+ // Robust JSON extraction: find the first { and last } to extract JSON object
962
+ // This handles cases where the LLM includes preamble text before the JSON
963
+ const firstBrace = jsonText.indexOf('{');
964
+ const lastBrace = jsonText.lastIndexOf('}');
965
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
966
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1062
967
  }
968
+
969
+ aiResponse = JSON.parse(jsonText);
970
+ } catch {
971
+ console.error("Failed to parse AI response:", textResponse.text);
972
+ return NextResponse.json(
973
+ { error: "Failed to parse AI response. Please try again." },
974
+ { status: 500 }
975
+ );
976
+ }
1063
977
 
1064
978
  finalExplanation = aiResponse.explanation;
1065
- finalReasoning = aiResponse.reasoning;
1066
- finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
1067
979
 
1068
- debugLog("VALIDATION: Known file paths from page context", {
1069
- pageFile: pageContext.pageFile,
1070
- knownPaths: Array.from(knownPaths),
1071
- aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
1072
- });
980
+ if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
981
+ return NextResponse.json({
982
+ success: true,
983
+ modifications: [],
984
+ explanation: aiResponse.explanation || "No changes needed.",
985
+ });
986
+ }
987
+
988
+ debugLog("VALIDATION: Known file paths from page context", {
989
+ pageFile: pageContext.pageFile,
990
+ knownPaths: Array.from(knownPaths),
991
+ aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
992
+ });
1073
993
 
1074
994
  // Validate AI response - trust the LLM to identify the correct file
1075
995
  // Only reject paths that are outside the project or don't exist
@@ -1110,9 +1030,9 @@ This is better than generating patches with made-up code.`,
1110
1030
  }
1111
1031
  }
1112
1032
 
1113
- // Process modifications - apply patches to get modified content
1033
+ // Process modifications - apply patches to get modified content
1114
1034
  modificationsWithOriginals = [];
1115
- const patchErrors: string[] = [];
1035
+ const patchErrors: string[] = [];
1116
1036
 
1117
1037
  for (const mod of aiResponse.modifications || []) {
1118
1038
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -1190,20 +1110,9 @@ This is better than generating patches with made-up code.`,
1190
1110
  modifiedContent = patchResult.modifiedContent;
1191
1111
  console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
1192
1112
  }
1193
- } else if (mod.modifiedContent) {
1194
- // Legacy: AI returned full file content
1195
- console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
1196
- modifiedContent = mod.modifiedContent;
1197
-
1198
- // Validate the modification using legacy validation
1199
- const validation = validateModification(originalContent, modifiedContent, mod.filePath);
1200
- if (!validation.valid) {
1201
- patchErrors.push(`${mod.filePath}: ${validation.error}`);
1202
- continue;
1203
- }
1204
1113
  } else {
1205
- // No patches and no modifiedContent - skip
1206
- console.warn(`[Vision Mode] No patches or modifiedContent for ${mod.filePath}`);
1114
+ // No patches - skip
1115
+ console.warn(`[Vision Mode] No patches for ${mod.filePath}`);
1207
1116
  continue;
1208
1117
  }
1209
1118
 
@@ -1218,7 +1127,7 @@ This is better than generating patches with made-up code.`,
1218
1127
  }
1219
1128
 
1220
1129
  // If all modifications failed, check if we should retry
1221
- if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1130
+ if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1222
1131
  if (retryCount < MAX_RETRIES) {
1223
1132
  console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1224
1133
  debugLog("Retry triggered due to patch failures", {
@@ -1233,19 +1142,19 @@ This is better than generating patches with made-up code.`,
1233
1142
 
1234
1143
  // Exhausted retries, return error
1235
1144
  console.error("All AI patches failed after retries:", patchErrors);
1236
- return NextResponse.json(
1237
- {
1238
- success: false,
1145
+ return NextResponse.json(
1146
+ {
1147
+ success: false,
1239
1148
  error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1240
- } as VisionEditResponse,
1241
- { status: 400 }
1242
- );
1243
- }
1149
+ } as VisionEditResponse,
1150
+ { status: 400 }
1151
+ );
1152
+ }
1244
1153
 
1245
- // Log patch errors as warnings if some modifications succeeded
1246
- if (patchErrors.length > 0) {
1247
- console.warn("Some patches failed:", patchErrors);
1248
- }
1154
+ // Log patch errors as warnings if some modifications succeeded
1155
+ if (patchErrors.length > 0) {
1156
+ console.warn("Some patches failed:", patchErrors);
1157
+ }
1249
1158
 
1250
1159
  // Successfully processed at least some modifications - break out of retry loop
1251
1160
  break;
@@ -1260,9 +1169,8 @@ This is better than generating patches with made-up code.`,
1260
1169
  return NextResponse.json({
1261
1170
  success: true,
1262
1171
  modifications: modificationsWithOriginals,
1263
- aggregatedPreviewCSS: finalAggregatedCSS || aggregatedCSS,
1172
+ aggregatedPreviewCSS: aggregatedCSS,
1264
1173
  explanation: finalExplanation,
1265
- reasoning: finalReasoning,
1266
1174
  } as VisionEditResponse);
1267
1175
  }
1268
1176
 
@@ -1954,13 +1862,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1954
1862
 
1955
1863
  // Only log parse error once to avoid log spam
1956
1864
  if (!cachedParseError) {
1957
- const posMatch = errorStr.match(/position (\d+)/);
1958
- let context = "";
1959
- if (posMatch) {
1960
- const pos = parseInt(posMatch[1], 10);
1961
- const content = fs.readFileSync(tsconfigPath, "utf-8");
1962
- context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1963
- }
1865
+ const posMatch = errorStr.match(/position (\d+)/);
1866
+ let context = "";
1867
+ if (posMatch) {
1868
+ const pos = parseInt(posMatch[1], 10);
1869
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1870
+ context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1871
+ }
1964
1872
  debugLog("[edit] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
1965
1873
  cachedParseError = errorStr;
1966
1874
  }
@@ -1983,14 +1891,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1983
1891
  }
1984
1892
  // Only log default alias once (not when using cached error fallback)
1985
1893
  if (!cachedParseError) {
1986
- debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
1894
+ debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
1987
1895
  }
1988
1896
  }
1989
1897
 
1990
1898
  // Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
1991
1899
  if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
1992
- cachedPathAliases = aliases;
1993
- cachedProjectRoot = projectRoot;
1900
+ cachedPathAliases = aliases;
1901
+ cachedProjectRoot = projectRoot;
1994
1902
  }
1995
1903
 
1996
1904
  return aliases;
@@ -2298,105 +2206,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
2298
2206
  failedPatches,
2299
2207
  };
2300
2208
  }
2301
-
2302
- /**
2303
- * Validate that AI modifications are surgical edits, not complete rewrites
2304
- */
2305
- interface ValidationResult {
2306
- valid: boolean;
2307
- error?: string;
2308
- warnings: string[];
2309
- }
2310
-
2311
- function validateModification(
2312
- originalContent: string,
2313
- modifiedContent: string,
2314
- filePath: string
2315
- ): ValidationResult {
2316
- const warnings: string[] = [];
2317
-
2318
- // Skip validation for new files (no original content)
2319
- if (!originalContent || originalContent.trim() === "") {
2320
- return { valid: true, warnings: ["New file - no original to compare"] };
2321
- }
2322
-
2323
- const originalLines = originalContent.split("\n");
2324
- const modifiedLines = modifiedContent.split("\n");
2325
-
2326
- // Check 1: Truncation detection - look for placeholder comments
2327
- const truncationPatterns = [
2328
- /\/\/\s*\.\.\.\s*existing/i,
2329
- /\/\/\s*\.\.\.\s*rest\s*of/i,
2330
- /\/\/\s*\.\.\.\s*more\s*code/i,
2331
- /\/\*\s*\.\.\.\s*\*\//,
2332
- /\/\/\s*\.\.\./,
2333
- ];
2334
-
2335
- for (const pattern of truncationPatterns) {
2336
- if (pattern.test(modifiedContent)) {
2337
- return {
2338
- valid: false,
2339
- error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
2340
- warnings,
2341
- };
2342
- }
2343
- }
2344
-
2345
- // Check 2: Line count shrinkage - reject if file shrinks by more than 30%
2346
- const lineDelta = modifiedLines.length - originalLines.length;
2347
- const shrinkagePercent = (lineDelta / originalLines.length) * 100;
2348
-
2349
- if (shrinkagePercent < -30) {
2350
- return {
2351
- valid: false,
2352
- 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.`,
2353
- warnings,
2354
- };
2355
- }
2356
-
2357
- if (shrinkagePercent < -15) {
2358
- warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
2359
- }
2360
-
2361
- // Check 3: Change percentage - warn if too many lines are different
2362
- let changedLines = 0;
2363
- const minLines = Math.min(originalLines.length, modifiedLines.length);
2364
-
2365
- for (let i = 0; i < minLines; i++) {
2366
- if (originalLines[i] !== modifiedLines[i]) {
2367
- changedLines++;
2368
- }
2369
- }
2370
-
2371
- // Add lines that were added or removed
2372
- changedLines += Math.abs(originalLines.length - modifiedLines.length);
2373
-
2374
- const changePercent = (changedLines / originalLines.length) * 100;
2375
-
2376
- if (changePercent > 50) {
2377
- return {
2378
- valid: false,
2379
- 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.`,
2380
- warnings,
2381
- };
2382
- }
2383
-
2384
- if (changePercent > 30) {
2385
- warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
2386
- }
2387
-
2388
- // Check 4: Import preservation - ensure imports aren't removed
2389
- const importRegex = /^import\s+/gm;
2390
- const originalImports = (originalContent.match(importRegex) || []).length;
2391
- const modifiedImports = (modifiedContent.match(importRegex) || []).length;
2392
-
2393
- if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
2394
- return {
2395
- valid: false,
2396
- error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
2397
- warnings,
2398
- };
2399
- }
2400
-
2401
- return { valid: true, warnings };
2402
- }