sonance-brand-mcp 1.3.95 → 1.3.97

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.
@@ -516,6 +516,98 @@ function findElementLineInFile(
516
516
  return null;
517
517
  }
518
518
 
519
+ /**
520
+ * Search imported component files for specific TEXT CONTENT
521
+ * This is used to redirect from parent components to child components
522
+ * when the actual visible text lives in an imported file.
523
+ *
524
+ * Example: "What's the essence of this process?" might be in EssenceTab.tsx
525
+ * but the parent QuickAddProcessModal.tsx was selected by Phase 2a.
526
+ */
527
+ /**
528
+ * Score imported files by how many discovered text strings they contain.
529
+ * This finds the TRUE file by matching ALL visible text, not just one substring.
530
+ *
531
+ * Example: If annotation discovers ["What's the essence?", "Essence Quality", "Show Example"]
532
+ * and EssenceTab.tsx contains 2 of them while QuickAddProcessModal.tsx contains 1,
533
+ * EssenceTab.tsx wins with the higher score.
534
+ */
535
+ function scoreFilesForTextContent(
536
+ textContents: string[],
537
+ importedFiles: { path: string; content: string }[]
538
+ ): { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number } | null {
539
+ if (!textContents || textContents.length === 0) return null;
540
+
541
+ debugLog("Scoring imported files for text content", {
542
+ textCount: textContents.length,
543
+ texts: textContents.slice(0, 5).map(t => t.substring(0, 30)),
544
+ filesCount: importedFiles.length
545
+ });
546
+
547
+ // Only search component files (where JSX text lives)
548
+ const componentFiles = importedFiles.filter(f =>
549
+ f.path.includes('components/') || f.path.includes('/ui/')
550
+ );
551
+
552
+ const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
553
+
554
+ for (const file of componentFiles) {
555
+ let score = 0;
556
+ const matchedTexts: string[] = [];
557
+ let firstMatchLine = 0;
558
+
559
+ for (const text of textContents) {
560
+ if (!text || text.length < 5) continue;
561
+
562
+ // Use first 20 chars for matching (handles dynamic suffixes)
563
+ const searchText = text.substring(0, 20);
564
+
565
+ // Check if this text exists in the file
566
+ if (file.content.includes(searchText)) {
567
+ score++;
568
+ matchedTexts.push(text.substring(0, 30) + (text.length > 30 ? '...' : ''));
569
+
570
+ // Find line number for first match (for element location)
571
+ if (firstMatchLine === 0) {
572
+ const lines = file.content.split('\n');
573
+ for (let i = 0; i < lines.length; i++) {
574
+ if (lines[i].includes(searchText)) {
575
+ firstMatchLine = i + 1;
576
+ break;
577
+ }
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ if (score > 0) {
584
+ results.push({
585
+ path: file.path,
586
+ content: file.content,
587
+ score,
588
+ matchedTexts,
589
+ firstMatchLine
590
+ });
591
+ }
592
+ }
593
+
594
+ // Sort by score descending (highest match count wins)
595
+ results.sort((a, b) => b.score - a.score);
596
+
597
+ if (results.length > 0) {
598
+ debugLog("Text scoring results", {
599
+ topFile: results[0].path,
600
+ topScore: results[0].score,
601
+ topMatches: results[0].matchedTexts,
602
+ allResults: results.slice(0, 3).map(r => ({ path: r.path, score: r.score }))
603
+ });
604
+ } else {
605
+ debugLog("No text matches found in any imported component files");
606
+ }
607
+
608
+ return results.length > 0 ? results[0] : null;
609
+ }
610
+
519
611
  /**
520
612
  * Search imported component files for the focused element
521
613
  * Reuses findElementLineInFile() for consistent detection
@@ -1528,7 +1620,7 @@ export async function POST(request: Request) {
1528
1620
  const confirmedPath = phase2aMatches.find(p =>
1529
1621
  focusedElementHints.some(h => h.path === p && h.score > 0)
1530
1622
  );
1531
- recommendedFile = {
1623
+ recommendedFile = {
1532
1624
  path: confirmedPath!,
1533
1625
  reason: `Phase 2a component-name match confirmed by element search`
1534
1626
  };
@@ -1552,7 +1644,7 @@ export async function POST(request: Request) {
1552
1644
  selectedPath: bestMatch,
1553
1645
  allCandidates: phase2aMatches
1554
1646
  });
1555
- } else {
1647
+ } else {
1556
1648
  // FALLBACK PRIORITY 4: Use the page file from the current route
1557
1649
  const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
1558
1650
 
@@ -1666,7 +1758,7 @@ User Request: "${userPrompt}"
1666
1758
  let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1667
1759
  let actualTargetFile = recommendedFileContent; // May change if we redirect
1668
1760
 
1669
- if (focusedElements && focusedElements.length > 0) {
1761
+ if (focusedElements && focusedElements.length > 0) {
1670
1762
  for (const el of focusedElements) {
1671
1763
  elementLocation = findElementLineInFile(content, el);
1672
1764
  if (elementLocation) {
@@ -1680,6 +1772,72 @@ User Request: "${userPrompt}"
1680
1772
  }
1681
1773
  }
1682
1774
 
1775
+ // TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
1776
+ // text strings they contain. The file with the highest score is the TRUE target.
1777
+ // This handles parent/child component cases accurately.
1778
+ if (elementLocation && elementLocation.confidence !== 'high') {
1779
+ // Collect ALL text content from focused elements
1780
+ const allTextContent = focusedElements
1781
+ .filter(e => e.textContent && e.textContent.length > 5)
1782
+ .map(e => e.textContent!);
1783
+
1784
+ if (allTextContent.length > 0) {
1785
+ debugLog("Medium/low confidence match - scoring imports for all text", {
1786
+ currentFile: recommendedFileContent.path,
1787
+ currentConfidence: elementLocation.confidence,
1788
+ textCount: allTextContent.length,
1789
+ texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1790
+ });
1791
+
1792
+ // Score all imported files
1793
+ const bestMatch = scoreFilesForTextContent(
1794
+ allTextContent,
1795
+ pageContext.componentSources
1796
+ );
1797
+
1798
+ // Also score the current file for comparison
1799
+ const currentFileScore = allTextContent.filter(text =>
1800
+ recommendedFileContent.content.includes(text.substring(0, 20))
1801
+ ).length;
1802
+
1803
+ debugLog("Text scoring comparison", {
1804
+ currentFile: recommendedFileContent.path,
1805
+ currentScore: currentFileScore,
1806
+ bestImport: bestMatch?.path || 'none',
1807
+ bestImportScore: bestMatch?.score || 0
1808
+ });
1809
+
1810
+ // Redirect only if imported file has MORE matches than current file
1811
+ if (bestMatch && bestMatch.score > currentFileScore) {
1812
+ debugLog("TEXT REDIRECT: Imported file has more text matches", {
1813
+ originalFile: recommendedFileContent.path,
1814
+ originalScore: currentFileScore,
1815
+ redirectTo: bestMatch.path,
1816
+ redirectScore: bestMatch.score,
1817
+ matchedTexts: bestMatch.matchedTexts
1818
+ });
1819
+
1820
+ // Switch target file to where most text content lives
1821
+ actualTargetFile = {
1822
+ path: bestMatch.path,
1823
+ content: bestMatch.content
1824
+ };
1825
+
1826
+ // Find a line with matched text for element location
1827
+ const lines = bestMatch.content.split('\n');
1828
+ const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
1829
+ const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
1830
+
1831
+ elementLocation = {
1832
+ lineNumber: bestMatch.firstMatchLine,
1833
+ snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
1834
+ confidence: 'high',
1835
+ matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1836
+ };
1837
+ }
1838
+ }
1839
+ }
1840
+
1683
1841
  // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
1684
1842
  if (!elementLocation) {
1685
1843
  debugLog("Element not in main file, searching imported components...", {
@@ -512,6 +512,98 @@ function findElementLineInFile(
512
512
  return null;
513
513
  }
514
514
 
515
+ /**
516
+ * Search imported component files for specific TEXT CONTENT
517
+ * This is used to redirect from parent components to child components
518
+ * when the actual visible text lives in an imported file.
519
+ *
520
+ * Example: "What's the essence of this process?" might be in EssenceTab.tsx
521
+ * but the parent QuickAddProcessModal.tsx was selected by Phase 2a.
522
+ */
523
+ /**
524
+ * Score imported files by how many discovered text strings they contain.
525
+ * This finds the TRUE file by matching ALL visible text, not just one substring.
526
+ *
527
+ * Example: If annotation discovers ["What's the essence?", "Essence Quality", "Show Example"]
528
+ * and EssenceTab.tsx contains 2 of them while QuickAddProcessModal.tsx contains 1,
529
+ * EssenceTab.tsx wins with the higher score.
530
+ */
531
+ function scoreFilesForTextContent(
532
+ textContents: string[],
533
+ importedFiles: { path: string; content: string }[]
534
+ ): { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number } | null {
535
+ if (!textContents || textContents.length === 0) return null;
536
+
537
+ debugLog("Scoring imported files for text content", {
538
+ textCount: textContents.length,
539
+ texts: textContents.slice(0, 5).map(t => t.substring(0, 30)),
540
+ filesCount: importedFiles.length
541
+ });
542
+
543
+ // Only search component files (where JSX text lives)
544
+ const componentFiles = importedFiles.filter(f =>
545
+ f.path.includes('components/') || f.path.includes('/ui/')
546
+ );
547
+
548
+ const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
549
+
550
+ for (const file of componentFiles) {
551
+ let score = 0;
552
+ const matchedTexts: string[] = [];
553
+ let firstMatchLine = 0;
554
+
555
+ for (const text of textContents) {
556
+ if (!text || text.length < 5) continue;
557
+
558
+ // Use first 20 chars for matching (handles dynamic suffixes)
559
+ const searchText = text.substring(0, 20);
560
+
561
+ // Check if this text exists in the file
562
+ if (file.content.includes(searchText)) {
563
+ score++;
564
+ matchedTexts.push(text.substring(0, 30) + (text.length > 30 ? '...' : ''));
565
+
566
+ // Find line number for first match (for element location)
567
+ if (firstMatchLine === 0) {
568
+ const lines = file.content.split('\n');
569
+ for (let i = 0; i < lines.length; i++) {
570
+ if (lines[i].includes(searchText)) {
571
+ firstMatchLine = i + 1;
572
+ break;
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }
578
+
579
+ if (score > 0) {
580
+ results.push({
581
+ path: file.path,
582
+ content: file.content,
583
+ score,
584
+ matchedTexts,
585
+ firstMatchLine
586
+ });
587
+ }
588
+ }
589
+
590
+ // Sort by score descending (highest match count wins)
591
+ results.sort((a, b) => b.score - a.score);
592
+
593
+ if (results.length > 0) {
594
+ debugLog("Text scoring results", {
595
+ topFile: results[0].path,
596
+ topScore: results[0].score,
597
+ topMatches: results[0].matchedTexts,
598
+ allResults: results.slice(0, 3).map(r => ({ path: r.path, score: r.score }))
599
+ });
600
+ } else {
601
+ debugLog("No text matches found in any imported component files");
602
+ }
603
+
604
+ return results.length > 0 ? results[0] : null;
605
+ }
606
+
515
607
  /**
516
608
  * Search imported component files for the focused element
517
609
  * Reuses findElementLineInFile() for consistent detection
@@ -1497,7 +1589,7 @@ export async function POST(request: Request) {
1497
1589
  const confirmedPath = phase2aMatches.find(p =>
1498
1590
  focusedElementHints.some(h => h.path === p && h.score > 0)
1499
1591
  );
1500
- recommendedFile = {
1592
+ recommendedFile = {
1501
1593
  path: confirmedPath!,
1502
1594
  reason: `Phase 2a component-name match confirmed by element search`
1503
1595
  };
@@ -1521,7 +1613,7 @@ export async function POST(request: Request) {
1521
1613
  selectedPath: bestMatch,
1522
1614
  allCandidates: phase2aMatches
1523
1615
  });
1524
- } else {
1616
+ } else {
1525
1617
  // FALLBACK PRIORITY 4: Use the page file from the current route
1526
1618
  const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
1527
1619
 
@@ -1635,7 +1727,7 @@ User Request: "${userPrompt}"
1635
1727
  let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
1636
1728
  let actualTargetFile = recommendedFileContent; // May change if we redirect
1637
1729
 
1638
- if (focusedElements && focusedElements.length > 0) {
1730
+ if (focusedElements && focusedElements.length > 0) {
1639
1731
  for (const el of focusedElements) {
1640
1732
  elementLocation = findElementLineInFile(content, el);
1641
1733
  if (elementLocation) {
@@ -1649,6 +1741,72 @@ User Request: "${userPrompt}"
1649
1741
  }
1650
1742
  }
1651
1743
 
1744
+ // TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
1745
+ // text strings they contain. The file with the highest score is the TRUE target.
1746
+ // This handles parent/child component cases accurately.
1747
+ if (elementLocation && elementLocation.confidence !== 'high') {
1748
+ // Collect ALL text content from focused elements
1749
+ const allTextContent = focusedElements
1750
+ .filter(e => e.textContent && e.textContent.length > 5)
1751
+ .map(e => e.textContent!);
1752
+
1753
+ if (allTextContent.length > 0) {
1754
+ debugLog("Medium/low confidence match - scoring imports for all text", {
1755
+ currentFile: recommendedFileContent.path,
1756
+ currentConfidence: elementLocation.confidence,
1757
+ textCount: allTextContent.length,
1758
+ texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1759
+ });
1760
+
1761
+ // Score all imported files
1762
+ const bestMatch = scoreFilesForTextContent(
1763
+ allTextContent,
1764
+ pageContext.componentSources
1765
+ );
1766
+
1767
+ // Also score the current file for comparison
1768
+ const currentFileScore = allTextContent.filter(text =>
1769
+ recommendedFileContent.content.includes(text.substring(0, 20))
1770
+ ).length;
1771
+
1772
+ debugLog("Text scoring comparison", {
1773
+ currentFile: recommendedFileContent.path,
1774
+ currentScore: currentFileScore,
1775
+ bestImport: bestMatch?.path || 'none',
1776
+ bestImportScore: bestMatch?.score || 0
1777
+ });
1778
+
1779
+ // Redirect only if imported file has MORE matches than current file
1780
+ if (bestMatch && bestMatch.score > currentFileScore) {
1781
+ debugLog("TEXT REDIRECT: Imported file has more text matches", {
1782
+ originalFile: recommendedFileContent.path,
1783
+ originalScore: currentFileScore,
1784
+ redirectTo: bestMatch.path,
1785
+ redirectScore: bestMatch.score,
1786
+ matchedTexts: bestMatch.matchedTexts
1787
+ });
1788
+
1789
+ // Switch target file to where most text content lives
1790
+ actualTargetFile = {
1791
+ path: bestMatch.path,
1792
+ content: bestMatch.content
1793
+ };
1794
+
1795
+ // Find a line with matched text for element location
1796
+ const lines = bestMatch.content.split('\n');
1797
+ const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
1798
+ const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
1799
+
1800
+ elementLocation = {
1801
+ lineNumber: bestMatch.firstMatchLine,
1802
+ snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
1803
+ confidence: 'high',
1804
+ matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1805
+ };
1806
+ }
1807
+ }
1808
+ }
1809
+
1652
1810
  // DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
1653
1811
  if (!elementLocation) {
1654
1812
  debugLog("Element not in main file, searching imported components...", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.95",
3
+ "version": "1.3.97",
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",