sonance-brand-mcp 1.3.87 → 1.3.89

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.
@@ -229,6 +229,109 @@ function findElementIdInFile(
229
229
  return null;
230
230
  }
231
231
 
232
+ /**
233
+ * PHASE 0: Deterministic Element ID Search (Cursor-style)
234
+ * Grep entire codebase for the element ID. If found in multiple files,
235
+ * use the current route to disambiguate (prioritize page file for route).
236
+ *
237
+ * This is the most reliable signal - like Cursor knowing which file you're in.
238
+ * Element IDs should be unique, so if we find one, that's THE file.
239
+ */
240
+ function findFilesByElementId(
241
+ projectRoot: string,
242
+ elementId: string,
243
+ currentRoute: string,
244
+ discoverPageFileFn: (route: string, projectRoot: string) => string | null
245
+ ): { path: string; lineNumber: number; isRouteMatch: boolean }[] {
246
+ const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
247
+ const matches: { path: string; lineNumber: number; isRouteMatch: boolean }[] = [];
248
+
249
+ // Determine expected page file from route
250
+ const routePageFile = discoverPageFileFn(currentRoute, projectRoot);
251
+
252
+ // Search all common project directories that exist
253
+ // This supports: src/app, app/, pages/, components/, lib/ structures
254
+ const commonDirs = ['src', 'app', 'pages', 'components', 'lib'];
255
+ const searchDirs = commonDirs
256
+ .map(dir => path.join(projectRoot, dir))
257
+ .filter(dir => {
258
+ try {
259
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
260
+ } catch {
261
+ return false;
262
+ }
263
+ });
264
+
265
+ // If no standard dirs found, search project root (excluding node_modules)
266
+ if (searchDirs.length === 0) {
267
+ searchDirs.push(projectRoot);
268
+ }
269
+
270
+ debugLog("PHASE 0: Searching for element ID", {
271
+ elementId,
272
+ currentRoute,
273
+ routePageFile,
274
+ searchDirs: searchDirs.map(d => d.replace(projectRoot + '/', ''))
275
+ });
276
+
277
+ function searchDirRecursive(dir: string): void {
278
+ try {
279
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
280
+ for (const entry of entries) {
281
+ const fullPath = path.join(dir, entry.name);
282
+
283
+ // Skip node_modules, hidden directories, and build outputs
284
+ if (entry.isDirectory()) {
285
+ if (
286
+ entry.name.includes('node_modules') ||
287
+ entry.name.startsWith('.') ||
288
+ entry.name === 'dist' ||
289
+ entry.name === 'build' ||
290
+ entry.name === '.next'
291
+ ) {
292
+ continue;
293
+ }
294
+ searchDirRecursive(fullPath);
295
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
296
+ try {
297
+ const content = fs.readFileSync(fullPath, 'utf-8');
298
+ const lines = content.split('\n');
299
+ for (let i = 0; i < lines.length; i++) {
300
+ if (pattern.test(lines[i])) {
301
+ const relativePath = fullPath.replace(projectRoot + '/', '');
302
+ const isRouteMatch = relativePath === routePageFile;
303
+ matches.push({
304
+ path: relativePath,
305
+ lineNumber: i + 1,
306
+ isRouteMatch
307
+ });
308
+ debugLog("PHASE 0: Found ID match", {
309
+ file: relativePath,
310
+ line: i + 1,
311
+ isRouteMatch
312
+ });
313
+ break; // One match per file is enough
314
+ }
315
+ }
316
+ } catch {
317
+ // Skip files that can't be read
318
+ }
319
+ }
320
+ }
321
+ } catch {
322
+ // Skip directories that can't be read
323
+ }
324
+ }
325
+
326
+ // Search all directories
327
+ for (const searchDir of searchDirs) {
328
+ searchDirRecursive(searchDir);
329
+ }
330
+
331
+ // Sort: route matches first
332
+ return matches.sort((a, b) => (b.isRouteMatch ? 1 : 0) - (a.isRouteMatch ? 1 : 0));
333
+ }
334
+
232
335
  /**
233
336
  * Result of LLM screenshot analysis for smart file discovery
234
337
  */
@@ -927,12 +1030,73 @@ export async function POST(request: Request) {
927
1030
  // Generate a unique session ID
928
1031
  const newSessionId = randomUUID().slice(0, 8);
929
1032
 
930
- // PHASE 1+2: LLM-driven smart file discovery
931
- // The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
1033
+ // Initialize file discovery variables
932
1034
  let smartSearchFiles: { path: string; content: string }[] = [];
933
1035
  let recommendedFile: { path: string; reason: string } | null = null;
934
-
935
- if (screenshot) {
1036
+ let deterministicMatch: { path: string; lineNumber: number } | null = null;
1037
+
1038
+ // ========================================================================
1039
+ // PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
1040
+ // If we have an element ID, find it directly - no scoring, no heuristics
1041
+ // This is the most reliable signal, like Cursor knowing which file you're in
1042
+ // ========================================================================
1043
+ if (focusedElements?.some(el => el.elementId)) {
1044
+ const elementWithId = focusedElements.find(el => el.elementId)!;
1045
+ debugLog("PHASE 0: Element has ID, starting deterministic search", {
1046
+ elementId: elementWithId.elementId,
1047
+ route: pageRoute
1048
+ });
1049
+
1050
+ const matches = findFilesByElementId(
1051
+ projectRoot,
1052
+ elementWithId.elementId!,
1053
+ pageRoute || "/",
1054
+ discoverPageFile
1055
+ );
1056
+
1057
+ if (matches.length === 1) {
1058
+ // Single match - 100% confidence
1059
+ deterministicMatch = matches[0];
1060
+ recommendedFile = {
1061
+ path: matches[0].path,
1062
+ reason: `Deterministic ID match: id="${elementWithId.elementId}" (unique in codebase)`
1063
+ };
1064
+ debugLog("PHASE 0 SUCCESS: Single ID match - skipping smart search", deterministicMatch);
1065
+ } else if (matches.length > 1) {
1066
+ // Multiple matches - use route to disambiguate
1067
+ const routeMatch = matches.find(m => m.isRouteMatch);
1068
+ if (routeMatch) {
1069
+ deterministicMatch = routeMatch;
1070
+ recommendedFile = {
1071
+ path: routeMatch.path,
1072
+ reason: `Deterministic ID match: id="${elementWithId.elementId}" (route "${pageRoute}" disambiguated from ${matches.length} files)`
1073
+ };
1074
+ debugLog("PHASE 0 SUCCESS: ID found in multiple files, using route match", {
1075
+ match: deterministicMatch,
1076
+ otherFiles: matches.filter(m => !m.isRouteMatch).map(m => m.path)
1077
+ });
1078
+ } else {
1079
+ debugLog("PHASE 0 FALLBACK: ID found in multiple files, no route match", {
1080
+ elementId: elementWithId.elementId,
1081
+ matchCount: matches.length,
1082
+ files: matches.map(m => m.path),
1083
+ route: pageRoute
1084
+ });
1085
+ // Fall through to smart search
1086
+ }
1087
+ } else {
1088
+ debugLog("PHASE 0 FALLBACK: Element ID not found in codebase", {
1089
+ elementId: elementWithId.elementId
1090
+ });
1091
+ // Fall through to smart search
1092
+ }
1093
+ }
1094
+
1095
+ // ========================================================================
1096
+ // PHASE 1+2: LLM-driven smart file discovery (only if Phase 0 didn't match)
1097
+ // The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
1098
+ // ========================================================================
1099
+ if (!deterministicMatch && screenshot) {
936
1100
  debugLog("Starting Phase 1: LLM screenshot analysis");
937
1101
  const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
938
1102
 
@@ -981,8 +1145,55 @@ export async function POST(request: Request) {
981
1145
  const phase2aConfirmed = phase2aMatches.length > 0 &&
982
1146
  focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
983
1147
 
1148
+ // Helper: Find best Phase 2a match based on user prompt keywords
1149
+ // e.g., "duplicate button" should prefer ProcessRow.tsx over ProcessCatalogueView.tsx
1150
+ const findBestPhase2aMatch = (): string | null => {
1151
+ if (phase2aMatches.length === 0) return null;
1152
+ if (phase2aMatches.length === 1) return phase2aMatches[0];
1153
+
1154
+ // Extract keywords from user prompt
1155
+ const promptLower = userPrompt.toLowerCase();
1156
+ const keywords = ['row', 'cell', 'item', 'button', 'action', 'duplicate', 'edit', 'delete'];
1157
+
1158
+ // Prefer more specific files (Row > View, Cell > Table, etc.)
1159
+ const specificity = ['row', 'cell', 'item', 'card', 'modal', 'panel', 'detail'];
1160
+
1161
+ // Score each Phase 2a match
1162
+ let bestMatch = phase2aMatches[0];
1163
+ let bestScore = 0;
1164
+
1165
+ for (const match of phase2aMatches) {
1166
+ const matchLower = match.toLowerCase();
1167
+ let score = 0;
1168
+
1169
+ // Bonus for specificity (Row/Cell/Item components)
1170
+ for (const spec of specificity) {
1171
+ if (matchLower.includes(spec)) score += 10;
1172
+ }
1173
+
1174
+ // Bonus for keyword match (prompt mentions button → prefer file with button/action)
1175
+ for (const kw of keywords) {
1176
+ if (promptLower.includes(kw) && matchLower.includes(kw)) score += 20;
1177
+ }
1178
+
1179
+ // Penalty for generic view/container files
1180
+ if (matchLower.includes('view.tsx') && !matchLower.includes('detail')) score -= 5;
1181
+
1182
+ if (score > bestScore) {
1183
+ bestScore = score;
1184
+ bestMatch = match;
1185
+ }
1186
+ }
1187
+
1188
+ return bestMatch;
1189
+ };
1190
+
1191
+ // ====================================================================
1192
+ // FALLBACK PRIORITY LOGIC (only reached when Phase 0 didn't find a match)
1193
+ // This handles cases: no element ID, dynamic IDs, or ambiguous ID matches
1194
+ // ====================================================================
984
1195
  if (phase2aConfirmed) {
985
- // PRIORITY 1: Phase 2a match confirmed by element search - highest confidence
1196
+ // FALLBACK PRIORITY 1: Phase 2a match confirmed by element search
986
1197
  const confirmedPath = phase2aMatches.find(p =>
987
1198
  focusedElementHints.some(h => h.path === p && h.score > 0)
988
1199
  );
@@ -990,21 +1201,47 @@ export async function POST(request: Request) {
990
1201
  path: confirmedPath!,
991
1202
  reason: `Phase 2a component-name match confirmed by element search`
992
1203
  };
993
- debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
1204
+ debugLog("FALLBACK PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
994
1205
  } else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
995
- // PRIORITY 2: Focused element has strong absolute score AND relative to smart search
1206
+ // FALLBACK PRIORITY 2: Focused element has strong score
996
1207
  recommendedFile = {
997
1208
  path: focusedTopPath!,
998
1209
  reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
999
1210
  };
1000
- debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
1001
- } else if (smartSearchTopPath) {
1002
- // PRIORITY 3: Smart search top result - trusted baseline
1211
+ debugLog("FALLBACK PRIORITY 2: Strong focused element match", recommendedFile);
1212
+ } else if (phase2aMatches.length > 0) {
1213
+ // FALLBACK PRIORITY 3: Phase 2a match WITHOUT focused element
1214
+ // Use prompt-aware selection to pick the best match
1215
+ const bestMatch = findBestPhase2aMatch()!;
1003
1216
  recommendedFile = {
1004
- path: smartSearchTopPath,
1005
- reason: `Smart search top result (score: ${smartSearchTopScore})`
1217
+ path: bestMatch,
1218
+ reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
1006
1219
  };
1007
- debugLog("PRIORITY 3: Using smart search top result", recommendedFile);
1220
+ debugLog("FALLBACK PRIORITY 3: Phase 2a match (no focused element)", {
1221
+ selectedPath: bestMatch,
1222
+ allCandidates: phase2aMatches
1223
+ });
1224
+ } else {
1225
+ // FALLBACK PRIORITY 4: Use the page file from the current route
1226
+ const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
1227
+
1228
+ if (routePageFile && fs.existsSync(path.join(projectRoot, routePageFile))) {
1229
+ recommendedFile = {
1230
+ path: routePageFile,
1231
+ reason: `Page file from route "${pageRoute || "/"}"`
1232
+ };
1233
+ debugLog("FALLBACK PRIORITY 4: Using page file from route", {
1234
+ route: pageRoute || "/",
1235
+ pageFile: routePageFile
1236
+ });
1237
+ } else if (smartSearchTopPath) {
1238
+ // FALLBACK PRIORITY 5: Smart search top result - last resort
1239
+ recommendedFile = {
1240
+ path: smartSearchTopPath,
1241
+ reason: `Smart search top result (score: ${smartSearchTopScore})`
1242
+ };
1243
+ debugLog("FALLBACK PRIORITY 5: Using smart search top result", recommendedFile);
1244
+ }
1008
1245
  }
1009
1246
 
1010
1247
  debugLog("Phase 1+2 complete", {
@@ -225,6 +225,109 @@ function findElementIdInFile(
225
225
  return null;
226
226
  }
227
227
 
228
+ /**
229
+ * PHASE 0: Deterministic Element ID Search (Cursor-style)
230
+ * Grep entire codebase for the element ID. If found in multiple files,
231
+ * use the current route to disambiguate (prioritize page file for route).
232
+ *
233
+ * This is the most reliable signal - like Cursor knowing which file you're in.
234
+ * Element IDs should be unique, so if we find one, that's THE file.
235
+ */
236
+ function findFilesByElementId(
237
+ projectRoot: string,
238
+ elementId: string,
239
+ currentRoute: string,
240
+ discoverPageFileFn: (route: string, projectRoot: string) => string | null
241
+ ): { path: string; lineNumber: number; isRouteMatch: boolean }[] {
242
+ const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
243
+ const matches: { path: string; lineNumber: number; isRouteMatch: boolean }[] = [];
244
+
245
+ // Determine expected page file from route
246
+ const routePageFile = discoverPageFileFn(currentRoute, projectRoot);
247
+
248
+ // Search all common project directories that exist
249
+ // This supports: src/app, app/, pages/, components/, lib/ structures
250
+ const commonDirs = ['src', 'app', 'pages', 'components', 'lib'];
251
+ const searchDirs = commonDirs
252
+ .map(dir => path.join(projectRoot, dir))
253
+ .filter(dir => {
254
+ try {
255
+ return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
256
+ } catch {
257
+ return false;
258
+ }
259
+ });
260
+
261
+ // If no standard dirs found, search project root (excluding node_modules)
262
+ if (searchDirs.length === 0) {
263
+ searchDirs.push(projectRoot);
264
+ }
265
+
266
+ debugLog("PHASE 0: Searching for element ID", {
267
+ elementId,
268
+ currentRoute,
269
+ routePageFile,
270
+ searchDirs: searchDirs.map(d => d.replace(projectRoot + '/', ''))
271
+ });
272
+
273
+ function searchDirRecursive(dir: string): void {
274
+ try {
275
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
276
+ for (const entry of entries) {
277
+ const fullPath = path.join(dir, entry.name);
278
+
279
+ // Skip node_modules, hidden directories, and build outputs
280
+ if (entry.isDirectory()) {
281
+ if (
282
+ entry.name.includes('node_modules') ||
283
+ entry.name.startsWith('.') ||
284
+ entry.name === 'dist' ||
285
+ entry.name === 'build' ||
286
+ entry.name === '.next'
287
+ ) {
288
+ continue;
289
+ }
290
+ searchDirRecursive(fullPath);
291
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
292
+ try {
293
+ const content = fs.readFileSync(fullPath, 'utf-8');
294
+ const lines = content.split('\n');
295
+ for (let i = 0; i < lines.length; i++) {
296
+ if (pattern.test(lines[i])) {
297
+ const relativePath = fullPath.replace(projectRoot + '/', '');
298
+ const isRouteMatch = relativePath === routePageFile;
299
+ matches.push({
300
+ path: relativePath,
301
+ lineNumber: i + 1,
302
+ isRouteMatch
303
+ });
304
+ debugLog("PHASE 0: Found ID match", {
305
+ file: relativePath,
306
+ line: i + 1,
307
+ isRouteMatch
308
+ });
309
+ break; // One match per file is enough
310
+ }
311
+ }
312
+ } catch {
313
+ // Skip files that can't be read
314
+ }
315
+ }
316
+ }
317
+ } catch {
318
+ // Skip directories that can't be read
319
+ }
320
+ }
321
+
322
+ // Search all directories
323
+ for (const searchDir of searchDirs) {
324
+ searchDirRecursive(searchDir);
325
+ }
326
+
327
+ // Sort: route matches first
328
+ return matches.sort((a, b) => (b.isRouteMatch ? 1 : 0) - (a.isRouteMatch ? 1 : 0));
329
+ }
330
+
228
331
  /**
229
332
  * Result of LLM screenshot analysis for smart file discovery
230
333
  */
@@ -896,12 +999,73 @@ export async function POST(request: Request) {
896
999
  );
897
1000
  }
898
1001
 
899
- // PHASE 1+2: LLM-driven smart file discovery
900
- // The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
1002
+ // Initialize file discovery variables
901
1003
  let smartSearchFiles: { path: string; content: string }[] = [];
902
1004
  let recommendedFile: { path: string; reason: string } | null = null;
903
-
904
- if (screenshot) {
1005
+ let deterministicMatch: { path: string; lineNumber: number } | null = null;
1006
+
1007
+ // ========================================================================
1008
+ // PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
1009
+ // If we have an element ID, find it directly - no scoring, no heuristics
1010
+ // This is the most reliable signal, like Cursor knowing which file you're in
1011
+ // ========================================================================
1012
+ if (focusedElements?.some(el => el.elementId)) {
1013
+ const elementWithId = focusedElements.find(el => el.elementId)!;
1014
+ debugLog("PHASE 0: Element has ID, starting deterministic search", {
1015
+ elementId: elementWithId.elementId,
1016
+ route: pageRoute
1017
+ });
1018
+
1019
+ const matches = findFilesByElementId(
1020
+ projectRoot,
1021
+ elementWithId.elementId!,
1022
+ pageRoute || "/",
1023
+ discoverPageFile
1024
+ );
1025
+
1026
+ if (matches.length === 1) {
1027
+ // Single match - 100% confidence
1028
+ deterministicMatch = matches[0];
1029
+ recommendedFile = {
1030
+ path: matches[0].path,
1031
+ reason: `Deterministic ID match: id="${elementWithId.elementId}" (unique in codebase)`
1032
+ };
1033
+ debugLog("PHASE 0 SUCCESS: Single ID match - skipping smart search", deterministicMatch);
1034
+ } else if (matches.length > 1) {
1035
+ // Multiple matches - use route to disambiguate
1036
+ const routeMatch = matches.find(m => m.isRouteMatch);
1037
+ if (routeMatch) {
1038
+ deterministicMatch = routeMatch;
1039
+ recommendedFile = {
1040
+ path: routeMatch.path,
1041
+ reason: `Deterministic ID match: id="${elementWithId.elementId}" (route "${pageRoute}" disambiguated from ${matches.length} files)`
1042
+ };
1043
+ debugLog("PHASE 0 SUCCESS: ID found in multiple files, using route match", {
1044
+ match: deterministicMatch,
1045
+ otherFiles: matches.filter(m => !m.isRouteMatch).map(m => m.path)
1046
+ });
1047
+ } else {
1048
+ debugLog("PHASE 0 FALLBACK: ID found in multiple files, no route match", {
1049
+ elementId: elementWithId.elementId,
1050
+ matchCount: matches.length,
1051
+ files: matches.map(m => m.path),
1052
+ route: pageRoute
1053
+ });
1054
+ // Fall through to smart search
1055
+ }
1056
+ } else {
1057
+ debugLog("PHASE 0 FALLBACK: Element ID not found in codebase", {
1058
+ elementId: elementWithId.elementId
1059
+ });
1060
+ // Fall through to smart search
1061
+ }
1062
+ }
1063
+
1064
+ // ========================================================================
1065
+ // PHASE 1+2: LLM-driven smart file discovery (only if Phase 0 didn't match)
1066
+ // The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
1067
+ // ========================================================================
1068
+ if (!deterministicMatch && screenshot) {
905
1069
  debugLog("Starting Phase 1: LLM screenshot analysis");
906
1070
  const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
907
1071
 
@@ -950,8 +1114,55 @@ export async function POST(request: Request) {
950
1114
  const phase2aConfirmed = phase2aMatches.length > 0 &&
951
1115
  focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
952
1116
 
1117
+ // Helper: Find best Phase 2a match based on user prompt keywords
1118
+ // e.g., "duplicate button" should prefer ProcessRow.tsx over ProcessCatalogueView.tsx
1119
+ const findBestPhase2aMatch = (): string | null => {
1120
+ if (phase2aMatches.length === 0) return null;
1121
+ if (phase2aMatches.length === 1) return phase2aMatches[0];
1122
+
1123
+ // Extract keywords from user prompt
1124
+ const promptLower = userPrompt.toLowerCase();
1125
+ const keywords = ['row', 'cell', 'item', 'button', 'action', 'duplicate', 'edit', 'delete'];
1126
+
1127
+ // Prefer more specific files (Row > View, Cell > Table, etc.)
1128
+ const specificity = ['row', 'cell', 'item', 'card', 'modal', 'panel', 'detail'];
1129
+
1130
+ // Score each Phase 2a match
1131
+ let bestMatch = phase2aMatches[0];
1132
+ let bestScore = 0;
1133
+
1134
+ for (const match of phase2aMatches) {
1135
+ const matchLower = match.toLowerCase();
1136
+ let score = 0;
1137
+
1138
+ // Bonus for specificity (Row/Cell/Item components)
1139
+ for (const spec of specificity) {
1140
+ if (matchLower.includes(spec)) score += 10;
1141
+ }
1142
+
1143
+ // Bonus for keyword match (prompt mentions button → prefer file with button/action)
1144
+ for (const kw of keywords) {
1145
+ if (promptLower.includes(kw) && matchLower.includes(kw)) score += 20;
1146
+ }
1147
+
1148
+ // Penalty for generic view/container files
1149
+ if (matchLower.includes('view.tsx') && !matchLower.includes('detail')) score -= 5;
1150
+
1151
+ if (score > bestScore) {
1152
+ bestScore = score;
1153
+ bestMatch = match;
1154
+ }
1155
+ }
1156
+
1157
+ return bestMatch;
1158
+ };
1159
+
1160
+ // ====================================================================
1161
+ // FALLBACK PRIORITY LOGIC (only reached when Phase 0 didn't find a match)
1162
+ // This handles cases: no element ID, dynamic IDs, or ambiguous ID matches
1163
+ // ====================================================================
953
1164
  if (phase2aConfirmed) {
954
- // PRIORITY 1: Phase 2a match confirmed by element search - highest confidence
1165
+ // FALLBACK PRIORITY 1: Phase 2a match confirmed by element search
955
1166
  const confirmedPath = phase2aMatches.find(p =>
956
1167
  focusedElementHints.some(h => h.path === p && h.score > 0)
957
1168
  );
@@ -959,21 +1170,47 @@ export async function POST(request: Request) {
959
1170
  path: confirmedPath!,
960
1171
  reason: `Phase 2a component-name match confirmed by element search`
961
1172
  };
962
- debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
1173
+ debugLog("FALLBACK PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
963
1174
  } else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
964
- // PRIORITY 2: Focused element has strong absolute score AND relative to smart search
1175
+ // FALLBACK PRIORITY 2: Focused element has strong score
965
1176
  recommendedFile = {
966
1177
  path: focusedTopPath!,
967
1178
  reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
968
1179
  };
969
- debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
970
- } else if (smartSearchTopPath) {
971
- // PRIORITY 3: Smart search top result - trusted baseline
1180
+ debugLog("FALLBACK PRIORITY 2: Strong focused element match", recommendedFile);
1181
+ } else if (phase2aMatches.length > 0) {
1182
+ // FALLBACK PRIORITY 3: Phase 2a match WITHOUT focused element
1183
+ // Use prompt-aware selection to pick the best match
1184
+ const bestMatch = findBestPhase2aMatch()!;
972
1185
  recommendedFile = {
973
- path: smartSearchTopPath,
974
- reason: `Smart search top result (score: ${smartSearchTopScore})`
1186
+ path: bestMatch,
1187
+ reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
975
1188
  };
976
- debugLog("PRIORITY 3: Using smart search top result", recommendedFile);
1189
+ debugLog("FALLBACK PRIORITY 3: Phase 2a match (no focused element)", {
1190
+ selectedPath: bestMatch,
1191
+ allCandidates: phase2aMatches
1192
+ });
1193
+ } else {
1194
+ // FALLBACK PRIORITY 4: Use the page file from the current route
1195
+ const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
1196
+
1197
+ if (routePageFile && fs.existsSync(path.join(projectRoot, routePageFile))) {
1198
+ recommendedFile = {
1199
+ path: routePageFile,
1200
+ reason: `Page file from route "${pageRoute || "/"}"`
1201
+ };
1202
+ debugLog("FALLBACK PRIORITY 4: Using page file from route", {
1203
+ route: pageRoute || "/",
1204
+ pageFile: routePageFile
1205
+ });
1206
+ } else if (smartSearchTopPath) {
1207
+ // FALLBACK PRIORITY 5: Smart search top result - last resort
1208
+ recommendedFile = {
1209
+ path: smartSearchTopPath,
1210
+ reason: `Smart search top result (score: ${smartSearchTopScore})`
1211
+ };
1212
+ debugLog("FALLBACK PRIORITY 5: Using smart search top result", recommendedFile);
1213
+ }
977
1214
  }
978
1215
 
979
1216
  debugLog("Phase 1+2 complete", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.87",
3
+ "version": "1.3.89",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",