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
|
|
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
|
|
514
|
+
const VISION_SYSTEM_PROMPT = `You edit code. Make ONLY the change requested.
|
|
515
515
|
|
|
516
516
|
RULES:
|
|
517
|
-
1.
|
|
518
|
-
2.
|
|
519
|
-
3.
|
|
520
|
-
4. Do
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
546
|
-
"patches": [{
|
|
547
|
-
|
|
548
|
-
|
|
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 (
|
|
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
|
-
|
|
796
|
+
FILE TO EDIT: ${recommendedFileContent.path}
|
|
819
797
|
═══════════════════════════════════════════════════════════════════════════════
|
|
820
798
|
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
${
|
|
803
|
+
${linesWithNumbers}
|
|
826
804
|
\`\`\`
|
|
827
805
|
|
|
828
806
|
`;
|
|
829
807
|
usedContext += content.length;
|
|
830
|
-
debugLog("Added TARGET COMPONENT
|
|
808
|
+
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
831
809
|
path: recommendedFileContent.path,
|
|
832
|
-
|
|
833
|
-
|
|
810
|
+
lines: content.split('\n').length,
|
|
811
|
+
size: content.length
|
|
834
812
|
});
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
${
|
|
825
|
+
${linesWithNumbers}
|
|
870
826
|
\`\`\`
|
|
827
|
+
|
|
871
828
|
`;
|
|
872
|
-
|
|
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
|
|
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
|
-
// ==========
|
|
906
|
-
const
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
937
|
-
|
|
857
|
+
EXAMPLE - If line 84 shows:
|
|
858
|
+
84| <div className="mb-10 md:mb-16 grid grid-cols-2">
|
|
938
859
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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:
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
950
|
+
const response = await anthropic.messages.create({
|
|
951
|
+
model: "claude-sonnet-4-20250514",
|
|
952
|
+
max_tokens: 16384,
|
|
1031
953
|
messages: currentMessages,
|
|
1032
|
-
|
|
1033
|
-
|
|
954
|
+
system: VISION_SYSTEM_PROMPT,
|
|
955
|
+
});
|
|
1034
956
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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 -
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1029
|
+
// Process modifications - apply patches to get modified content
|
|
1113
1030
|
modifications = [];
|
|
1114
|
-
|
|
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
|
-
//
|
|
1215
|
-
|
|
1216
|
-
if (
|
|
1217
|
-
|
|
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
|
|
1222
|
-
console.warn(`[Apply-First] No patches
|
|
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
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2153
|
-
|
|
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
|
-
}
|