@ansvar/eu-regulations-mcp 0.2.3 → 0.3.0

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.
@@ -12,6 +12,7 @@ export interface SearchResult {
12
12
  title: string;
13
13
  snippet: string;
14
14
  relevance: number;
15
+ type?: 'article' | 'recital';
15
16
  }
16
17
 
17
18
  /**
@@ -45,46 +46,93 @@ export async function searchRegulations(
45
46
  return [];
46
47
  }
47
48
 
48
- // Build the SQL query with optional regulation filter
49
- let sql = `
49
+ const params: (string | number)[] = [escapedQuery];
50
+
51
+ // Build optional regulation filter
52
+ let regulationFilter = '';
53
+ if (regulations && regulations.length > 0) {
54
+ const placeholders = regulations.map(() => '?').join(', ');
55
+ regulationFilter = ` AND regulation IN (${placeholders})`;
56
+ params.push(...regulations);
57
+ }
58
+
59
+ // Search in articles
60
+ const articlesQuery = `
50
61
  SELECT
51
62
  articles_fts.regulation,
52
63
  articles_fts.article_number as article,
53
64
  articles_fts.title,
54
65
  snippet(articles_fts, 3, '>>>', '<<<', '...', 32) as snippet,
55
- bm25(articles_fts) as relevance
66
+ bm25(articles_fts) as relevance,
67
+ 'article' as type
56
68
  FROM articles_fts
57
69
  WHERE articles_fts MATCH ?
70
+ ${regulationFilter}
71
+ ORDER BY bm25(articles_fts)
72
+ LIMIT ?
58
73
  `;
59
74
 
60
- const params: (string | number)[] = [escapedQuery];
75
+ // Search in recitals
76
+ const recitalsQuery = `
77
+ SELECT
78
+ recitals_fts.regulation,
79
+ CAST(recitals_fts.recital_number AS TEXT) as article,
80
+ 'Recital ' || recitals_fts.recital_number as title,
81
+ snippet(recitals_fts, 2, '>>>', '<<<', '...', 32) as snippet,
82
+ bm25(recitals_fts) as relevance,
83
+ 'recital' as type
84
+ FROM recitals_fts
85
+ WHERE recitals_fts MATCH ?
86
+ ${regulationFilter}
87
+ ORDER BY bm25(recitals_fts)
88
+ LIMIT ?
89
+ `;
61
90
 
62
- if (regulations && regulations.length > 0) {
63
- const placeholders = regulations.map(() => '?').join(', ');
64
- sql += ` AND articles_fts.regulation IN (${placeholders})`;
65
- params.push(...regulations);
66
- }
91
+ try {
92
+ // Execute both queries
93
+ const articlesParams = [...params, limit];
94
+ const recitalsParams = [...params, limit];
67
95
 
68
- // Order by relevance (bm25 returns negative scores, more negative = more relevant)
69
- sql += ` ORDER BY bm25(articles_fts)`;
70
- sql += ` LIMIT ?`;
71
- params.push(limit);
96
+ const articleStmt = db.prepare(articlesQuery);
97
+ const recitalStmt = db.prepare(recitalsQuery);
72
98
 
73
- try {
74
- const stmt = db.prepare(sql);
75
- const rows = stmt.all(...params) as Array<{
99
+ const articleRows = articleStmt.all(...articlesParams) as Array<{
100
+ regulation: string;
101
+ article: string;
102
+ title: string;
103
+ snippet: string;
104
+ relevance: number;
105
+ type: 'article' | 'recital';
106
+ }>;
107
+
108
+ const recitalRows = recitalStmt.all(...recitalsParams) as Array<{
76
109
  regulation: string;
77
110
  article: string;
78
111
  title: string;
79
112
  snippet: string;
80
113
  relevance: number;
114
+ type: 'article' | 'recital';
81
115
  }>;
82
116
 
83
- // Convert bm25 scores to positive values (higher = more relevant)
84
- return rows.map(row => ({
85
- ...row,
86
- relevance: Math.abs(row.relevance),
87
- }));
117
+ // Combine and sort by relevance, prioritizing articles
118
+ const combined = [...articleRows, ...recitalRows]
119
+ .map(row => ({
120
+ ...row,
121
+ relevance: Math.abs(row.relevance),
122
+ }))
123
+ .sort((a, b) => {
124
+ // First sort by relevance
125
+ if (Math.abs(a.relevance - b.relevance) > 0.01) {
126
+ return b.relevance - a.relevance;
127
+ }
128
+ // If relevance is similar, prioritize articles over recitals
129
+ if (a.type === 'article' && b.type === 'recital') return -1;
130
+ if (a.type === 'recital' && b.type === 'article') return 1;
131
+ return 0;
132
+ })
133
+ .slice(0, limit);
134
+
135
+ return combined;
88
136
  } catch (error) {
89
137
  // If FTS5 query fails (e.g., syntax error), return empty results
90
138
  if (error instanceof Error && error.message.includes('fts5')) {