sonance-brand-mcp 1.3.96 → 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.
@@ -524,14 +524,23 @@ function findElementLineInFile(
524
524
  * Example: "What's the essence of this process?" might be in EssenceTab.tsx
525
525
  * but the parent QuickAddProcessModal.tsx was selected by Phase 2a.
526
526
  */
527
- function findTextInImportedFiles(
528
- textContent: string,
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[],
529
537
  importedFiles: { path: string; content: string }[]
530
- ): { path: string; lineNumber: number; snippet: string; content: string } | null {
531
- if (!textContent || textContent.length < 5) return null;
538
+ ): { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number } | null {
539
+ if (!textContents || textContents.length === 0) return null;
532
540
 
533
- debugLog("Searching imported files for text content", {
534
- textContent: textContent.substring(0, 40),
541
+ debugLog("Scoring imported files for text content", {
542
+ textCount: textContents.length,
543
+ texts: textContents.slice(0, 5).map(t => t.substring(0, 30)),
535
544
  filesCount: importedFiles.length
536
545
  });
537
546
 
@@ -540,35 +549,63 @@ function findTextInImportedFiles(
540
549
  f.path.includes('components/') || f.path.includes('/ui/')
541
550
  );
542
551
 
543
- // Use first 20 chars for matching (handles dynamic suffixes)
544
- const searchText = textContent.substring(0, 20);
552
+ const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
545
553
 
546
554
  for (const file of componentFiles) {
547
- const lines = file.content.split('\n');
548
- for (let i = 0; i < lines.length; i++) {
549
- const line = lines[i];
550
- // Look for the text in JSX patterns: >Text<, "Text", 'Text', `Text`
551
- if (line.includes(`>${searchText}`) ||
552
- line.includes(`"${searchText}`) ||
553
- line.includes(`'${searchText}`) ||
554
- line.includes(`\`${searchText}`)) {
555
- debugLog("Found text in imported file", {
556
- path: file.path,
557
- lineNumber: i + 1,
558
- matchedText: searchText
559
- });
560
- return {
561
- path: file.path,
562
- lineNumber: i + 1,
563
- snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
564
- content: file.content
565
- };
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
+ }
566
580
  }
567
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
+ }
568
592
  }
569
593
 
570
- debugLog("Text not found in any imported component files");
571
- return null;
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;
572
609
  }
573
610
 
574
611
  /**
@@ -1735,43 +1772,67 @@ User Request: "${userPrompt}"
1735
1772
  }
1736
1773
  }
1737
1774
 
1738
- // TEXT-FIRST REDIRECT: Before accepting medium/low confidence match,
1739
- // search imported files for the actual TEXT CONTENT.
1740
- // This handles cases where parent component is selected but text lives in child.
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.
1741
1778
  if (elementLocation && elementLocation.confidence !== 'high') {
1742
- // Find the first focused element with meaningful text content
1743
- const textToFind = focusedElements.find(e => e.textContent && e.textContent.length > 5)?.textContent;
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!);
1744
1783
 
1745
- if (textToFind) {
1746
- debugLog("Medium/low confidence match - checking imports for text", {
1784
+ if (allTextContent.length > 0) {
1785
+ debugLog("Medium/low confidence match - scoring imports for all text", {
1747
1786
  currentFile: recommendedFileContent.path,
1748
1787
  currentConfidence: elementLocation.confidence,
1749
- searchingFor: textToFind.substring(0, 40)
1788
+ textCount: allTextContent.length,
1789
+ texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1750
1790
  });
1751
1791
 
1752
- const textMatch = findTextInImportedFiles(
1753
- textToFind,
1792
+ // Score all imported files
1793
+ const bestMatch = scoreFilesForTextContent(
1794
+ allTextContent,
1754
1795
  pageContext.componentSources
1755
1796
  );
1756
1797
 
1757
- if (textMatch) {
1758
- debugLog("TEXT REDIRECT: Found text in imported component", {
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", {
1759
1813
  originalFile: recommendedFileContent.path,
1760
- originalConfidence: elementLocation.confidence,
1761
- redirectTo: textMatch.path,
1762
- lineNumber: textMatch.lineNumber
1814
+ originalScore: currentFileScore,
1815
+ redirectTo: bestMatch.path,
1816
+ redirectScore: bestMatch.score,
1817
+ matchedTexts: bestMatch.matchedTexts
1763
1818
  });
1764
1819
 
1765
- // Switch target file to where the text actually is
1820
+ // Switch target file to where most text content lives
1766
1821
  actualTargetFile = {
1767
- path: textMatch.path,
1768
- content: textMatch.content
1822
+ path: bestMatch.path,
1823
+ content: bestMatch.content
1769
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
+
1770
1831
  elementLocation = {
1771
- lineNumber: textMatch.lineNumber,
1772
- snippet: textMatch.snippet,
1832
+ lineNumber: bestMatch.firstMatchLine,
1833
+ snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
1773
1834
  confidence: 'high',
1774
- matchedBy: `textContent in imported file "${textToFind.substring(0, 30)}..."`
1835
+ matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1775
1836
  };
1776
1837
  }
1777
1838
  }
@@ -520,14 +520,23 @@ function findElementLineInFile(
520
520
  * Example: "What's the essence of this process?" might be in EssenceTab.tsx
521
521
  * but the parent QuickAddProcessModal.tsx was selected by Phase 2a.
522
522
  */
523
- function findTextInImportedFiles(
524
- textContent: string,
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[],
525
533
  importedFiles: { path: string; content: string }[]
526
- ): { path: string; lineNumber: number; snippet: string; content: string } | null {
527
- if (!textContent || textContent.length < 5) return null;
534
+ ): { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number } | null {
535
+ if (!textContents || textContents.length === 0) return null;
528
536
 
529
- debugLog("Searching imported files for text content", {
530
- textContent: textContent.substring(0, 40),
537
+ debugLog("Scoring imported files for text content", {
538
+ textCount: textContents.length,
539
+ texts: textContents.slice(0, 5).map(t => t.substring(0, 30)),
531
540
  filesCount: importedFiles.length
532
541
  });
533
542
 
@@ -536,35 +545,63 @@ function findTextInImportedFiles(
536
545
  f.path.includes('components/') || f.path.includes('/ui/')
537
546
  );
538
547
 
539
- // Use first 20 chars for matching (handles dynamic suffixes)
540
- const searchText = textContent.substring(0, 20);
548
+ const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
541
549
 
542
550
  for (const file of componentFiles) {
543
- const lines = file.content.split('\n');
544
- for (let i = 0; i < lines.length; i++) {
545
- const line = lines[i];
546
- // Look for the text in JSX patterns: >Text<, "Text", 'Text', `Text`
547
- if (line.includes(`>${searchText}`) ||
548
- line.includes(`"${searchText}`) ||
549
- line.includes(`'${searchText}`) ||
550
- line.includes(`\`${searchText}`)) {
551
- debugLog("Found text in imported file", {
552
- path: file.path,
553
- lineNumber: i + 1,
554
- matchedText: searchText
555
- });
556
- return {
557
- path: file.path,
558
- lineNumber: i + 1,
559
- snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
560
- content: file.content
561
- };
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
+ }
562
576
  }
563
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
+ }
564
588
  }
565
589
 
566
- debugLog("Text not found in any imported component files");
567
- return null;
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;
568
605
  }
569
606
 
570
607
  /**
@@ -1704,43 +1741,67 @@ User Request: "${userPrompt}"
1704
1741
  }
1705
1742
  }
1706
1743
 
1707
- // TEXT-FIRST REDIRECT: Before accepting medium/low confidence match,
1708
- // search imported files for the actual TEXT CONTENT.
1709
- // This handles cases where parent component is selected but text lives in child.
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.
1710
1747
  if (elementLocation && elementLocation.confidence !== 'high') {
1711
- // Find the first focused element with meaningful text content
1712
- const textToFind = focusedElements.find(e => e.textContent && e.textContent.length > 5)?.textContent;
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!);
1713
1752
 
1714
- if (textToFind) {
1715
- debugLog("Medium/low confidence match - checking imports for text", {
1753
+ if (allTextContent.length > 0) {
1754
+ debugLog("Medium/low confidence match - scoring imports for all text", {
1716
1755
  currentFile: recommendedFileContent.path,
1717
1756
  currentConfidence: elementLocation.confidence,
1718
- searchingFor: textToFind.substring(0, 40)
1757
+ textCount: allTextContent.length,
1758
+ texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
1719
1759
  });
1720
1760
 
1721
- const textMatch = findTextInImportedFiles(
1722
- textToFind,
1761
+ // Score all imported files
1762
+ const bestMatch = scoreFilesForTextContent(
1763
+ allTextContent,
1723
1764
  pageContext.componentSources
1724
1765
  );
1725
1766
 
1726
- if (textMatch) {
1727
- debugLog("TEXT REDIRECT: Found text in imported component", {
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", {
1728
1782
  originalFile: recommendedFileContent.path,
1729
- originalConfidence: elementLocation.confidence,
1730
- redirectTo: textMatch.path,
1731
- lineNumber: textMatch.lineNumber
1783
+ originalScore: currentFileScore,
1784
+ redirectTo: bestMatch.path,
1785
+ redirectScore: bestMatch.score,
1786
+ matchedTexts: bestMatch.matchedTexts
1732
1787
  });
1733
1788
 
1734
- // Switch target file to where the text actually is
1789
+ // Switch target file to where most text content lives
1735
1790
  actualTargetFile = {
1736
- path: textMatch.path,
1737
- content: textMatch.content
1791
+ path: bestMatch.path,
1792
+ content: bestMatch.content
1738
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
+
1739
1800
  elementLocation = {
1740
- lineNumber: textMatch.lineNumber,
1741
- snippet: textMatch.snippet,
1801
+ lineNumber: bestMatch.firstMatchLine,
1802
+ snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
1742
1803
  confidence: 'high',
1743
- matchedBy: `textContent in imported file "${textToFind.substring(0, 30)}..."`
1804
+ matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
1744
1805
  };
1745
1806
  }
1746
1807
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.96",
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",