@highway1/core 0.1.39 → 0.1.41

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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Capability Matcher
3
+ *
4
+ * Matches semantic queries against agent capabilities
5
+ */
6
+
7
+ import type { Capability } from './agent-card-types.js';
8
+ import type { SemanticQuery } from './search-index.js';
9
+
10
+ /**
11
+ * Capability Matcher
12
+ */
13
+ export class CapabilityMatcher {
14
+ /**
15
+ * Match a query against a capability
16
+ * Returns a score between 0 and 1
17
+ */
18
+ match(query: SemanticQuery, capability: Capability): number {
19
+ let score = 0;
20
+ let weights = 0;
21
+
22
+ // Exact capability ID match
23
+ if (query.capability && capability.id === query.capability) {
24
+ return 1.0;
25
+ }
26
+
27
+ // Fuzzy capability name match
28
+ if (query.capability) {
29
+ const nameScore = this.fuzzyMatch(query.capability, capability.name);
30
+ score += nameScore * 0.4;
31
+ weights += 0.4;
32
+ }
33
+
34
+ // Text keyword match
35
+ if (query.text) {
36
+ const keywords = this.extractKeywords(query.text);
37
+ const keywordScore = this.matchKeywords(keywords, capability);
38
+ score += keywordScore * 0.4;
39
+ weights += 0.4;
40
+ }
41
+
42
+ // Type hierarchy match
43
+ if (query.capability && capability['@type']) {
44
+ const typeScore = this.matchesType(query.capability, capability['@type']) ? 0.6 : 0;
45
+ score += typeScore * 0.2;
46
+ weights += 0.2;
47
+ }
48
+
49
+ return weights > 0 ? score / weights : 0;
50
+ }
51
+
52
+ /**
53
+ * Extract keywords from natural language text
54
+ */
55
+ extractKeywords(text: string): string[] {
56
+ // Remove common stop words
57
+ const stopWords = new Set([
58
+ 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been',
59
+ 'to', 'from', 'in', 'on', 'at', 'by', 'for', 'with', 'about',
60
+ 'can', 'could', 'should', 'would', 'will', 'do', 'does', 'did',
61
+ 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her',
62
+ ]);
63
+
64
+ return text
65
+ .toLowerCase()
66
+ .split(/\W+/)
67
+ .filter(word => word.length > 2 && !stopWords.has(word));
68
+ }
69
+
70
+ /**
71
+ * Match keywords against capability
72
+ */
73
+ matchKeywords(keywords: string[], capability: Capability): number {
74
+ if (keywords.length === 0) return 0;
75
+
76
+ const capText = `${capability.name} ${capability.description}`.toLowerCase();
77
+ const matches = keywords.filter(keyword => capText.includes(keyword));
78
+
79
+ return matches.length / keywords.length;
80
+ }
81
+
82
+ /**
83
+ * Check if query matches capability type hierarchy
84
+ */
85
+ matchesType(query: string, type: string): boolean {
86
+ const queryLower = query.toLowerCase();
87
+ const typeLower = type.toLowerCase();
88
+
89
+ // Direct match
90
+ if (typeLower.includes(queryLower)) return true;
91
+
92
+ // Common type mappings
93
+ const typeMap: Record<string, string[]> = {
94
+ translate: ['translation', 'translationservice'],
95
+ review: ['codereview', 'reviewservice'],
96
+ analyze: ['analysis', 'dataanalysis'],
97
+ generate: ['generation', 'textgeneration', 'imagegeneration'],
98
+ search: ['searchservice', 'query'],
99
+ compute: ['computation', 'computationservice'],
100
+ store: ['storage', 'storageservice'],
101
+ message: ['messaging', 'messagingservice'],
102
+ auth: ['authentication', 'authenticationservice'],
103
+ };
104
+
105
+ for (const [key, values] of Object.entries(typeMap)) {
106
+ if (queryLower.includes(key) && values.some(v => typeLower.includes(v))) {
107
+ return true;
108
+ }
109
+ }
110
+
111
+ return false;
112
+ }
113
+
114
+ /**
115
+ * Fuzzy string matching using Levenshtein distance
116
+ */
117
+ private fuzzyMatch(query: string, target: string): number {
118
+ const queryLower = query.toLowerCase();
119
+ const targetLower = target.toLowerCase();
120
+
121
+ // Exact match
122
+ if (queryLower === targetLower) return 1.0;
123
+
124
+ // Substring match
125
+ if (targetLower.includes(queryLower)) return 0.8;
126
+ if (queryLower.includes(targetLower)) return 0.7;
127
+
128
+ // Levenshtein distance
129
+ const distance = this.levenshteinDistance(queryLower, targetLower);
130
+ const maxLen = Math.max(queryLower.length, targetLower.length);
131
+ const similarity = 1 - distance / maxLen;
132
+
133
+ // Only return positive scores for reasonable similarity
134
+ return similarity > 0.5 ? similarity * 0.6 : 0;
135
+ }
136
+
137
+ /**
138
+ * Calculate Levenshtein distance between two strings
139
+ */
140
+ private levenshteinDistance(a: string, b: string): number {
141
+ const matrix: number[][] = [];
142
+
143
+ for (let i = 0; i <= b.length; i++) {
144
+ matrix[i] = [i];
145
+ }
146
+
147
+ for (let j = 0; j <= a.length; j++) {
148
+ matrix[0][j] = j;
149
+ }
150
+
151
+ for (let i = 1; i <= b.length; i++) {
152
+ for (let j = 1; j <= a.length; j++) {
153
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
154
+ matrix[i][j] = matrix[i - 1][j - 1];
155
+ } else {
156
+ matrix[i][j] = Math.min(
157
+ matrix[i - 1][j - 1] + 1, // substitution
158
+ matrix[i][j - 1] + 1, // insertion
159
+ matrix[i - 1][j] + 1 // deletion
160
+ );
161
+ }
162
+ }
163
+ }
164
+
165
+ return matrix[b.length][a.length];
166
+ }
167
+ }
@@ -0,0 +1,285 @@
1
+ import type { Libp2p } from 'libp2p';
2
+ import type { AgentCard } from './agent-card.js';
3
+ import { createLogger } from '../utils/logger.js';
4
+ import { DiscoveryError } from '../utils/errors.js';
5
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
6
+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
7
+ import { encodeForDHT, decodeFromCBOR } from './agent-card-encoder.js';
8
+ import { createSemanticSearch } from './semantic-search.js';
9
+ import type { SemanticQuery } from './search-index.js';
10
+
11
+ const logger = createLogger('dht');
12
+
13
+ export interface ResolvedDID {
14
+ peerId: string;
15
+ multiaddrs: string[];
16
+ }
17
+
18
+ export interface DHTOperations {
19
+ publishAgentCard: (card: AgentCard) => Promise<void>;
20
+ queryAgentCard: (did: string) => Promise<AgentCard | null>;
21
+ queryByCapability: (capability: string) => Promise<AgentCard[]>;
22
+ searchSemantic: (query: SemanticQuery) => Promise<AgentCard[]>;
23
+ resolveDID: (did: string) => Promise<ResolvedDID | null>;
24
+ queryRelayPeers: () => Promise<string[]>;
25
+ }
26
+
27
+ /** Extract a DHT value from a get() event, handling both VALUE and PEER_RESPONSE */
28
+ function extractValue(event: any): Uint8Array | null {
29
+ if (event.name === 'VALUE' && event.value) return event.value;
30
+ if (event.name === 'PEER_RESPONSE' && event.value) return event.value;
31
+ return null;
32
+ }
33
+
34
+ /** Normalize capability name for use as a DHT key segment */
35
+ function capKey(cap: string): string {
36
+ return cap.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
37
+ }
38
+
39
+ /** Read a DID list stored at a DHT key (newline-separated) */
40
+ async function readDIDList(dht: any, key: Uint8Array): Promise<string[]> {
41
+ try {
42
+ for await (const event of dht.get(key, { signal: AbortSignal.timeout(30000) })) {
43
+ const raw = extractValue(event);
44
+ if (raw) {
45
+ const text = uint8ArrayToString(raw);
46
+ return text.split('\n').filter(Boolean);
47
+ }
48
+ }
49
+ } catch {
50
+ // key not found or timeout
51
+ }
52
+ return [];
53
+ }
54
+
55
+ /** Write a DID list to a DHT key (newline-separated), deduplicating */
56
+ async function writeDIDList(dht: any, key: Uint8Array, dids: string[]): Promise<void> {
57
+ const value = uint8ArrayFromString([...new Set(dids)].join('\n'));
58
+ // Retry up to 3 times with 30s timeout each
59
+ for (let attempt = 1; attempt <= 3; attempt++) {
60
+ try {
61
+ for await (const _ of dht.put(key, value, { signal: AbortSignal.timeout(30000) })) { /* consume */ }
62
+ return; // Success
63
+ } catch (e: any) {
64
+ if (e?.name === 'AbortError' && attempt < 3) {
65
+ logger.debug(`DHT put timeout, retrying (${attempt}/3)...`);
66
+ continue;
67
+ }
68
+ if (e?.name !== 'AbortError') throw e;
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Create DHT operations for a libp2p node
75
+ */
76
+ export function createDHTOperations(libp2p: Libp2p): DHTOperations {
77
+ const operations: DHTOperations = {} as DHTOperations;
78
+ const searchEngine = createSemanticSearch(operations);
79
+
80
+ return Object.assign(operations, {
81
+ publishAgentCard: async (card: AgentCard) => {
82
+ try {
83
+ const dht = (libp2p as any).services?.dht;
84
+ if (!dht) throw new DiscoveryError('DHT service not available');
85
+
86
+ // 1. Store Agent Card under /clawiverse/agent/<did>
87
+ const agentKey = uint8ArrayFromString(`/clawiverse/agent/${card.did}`);
88
+ // Retry up to 3 times with 30s timeout each
89
+ for (let attempt = 1; attempt <= 3; attempt++) {
90
+ try {
91
+ for await (const _ of dht.put(agentKey, encodeForDHT(card), { signal: AbortSignal.timeout(30000) })) { /* consume */ }
92
+ break; // Success
93
+ } catch (e: any) {
94
+ if (e?.name === 'AbortError' && attempt < 3) {
95
+ logger.debug(`DHT put agent card timeout, retrying (${attempt}/3)...`);
96
+ continue;
97
+ }
98
+ logger.warn('DHT put agent card failed (non-fatal)', { error: (e as Error).message });
99
+ break;
100
+ }
101
+ }
102
+
103
+ // 2. For each capability, append this DID to /clawiverse/cap/<capability>
104
+ // Skip capability indexing in slow networks - rely on local index and semantic search instead
105
+ const caps: string[] = (card.capabilities ?? []).flatMap((c: any) => {
106
+ if (typeof c === 'string') return [c];
107
+ return [c.name, c.id].filter(Boolean);
108
+ });
109
+
110
+ // Always index under the special "all" key so name/description search works
111
+ caps.push('__all__');
112
+
113
+ // Keep indexing lightweight, but preserve relay discovery path.
114
+ const importantCaps = ['__all__'];
115
+ if (caps.some((cap) => capKey(cap) === 'relay')) {
116
+ importantCaps.push('relay');
117
+ }
118
+ await Promise.all(importantCaps.map(async (cap) => {
119
+ const capKeyStr = `/clawiverse/cap/${capKey(cap)}`;
120
+ const capDHTKey = uint8ArrayFromString(capKeyStr);
121
+ try {
122
+ const existing = await readDIDList(dht, capDHTKey);
123
+ if (!existing.includes(card.did)) {
124
+ await writeDIDList(dht, capDHTKey, [...existing, card.did]);
125
+ logger.debug('Indexed capability in DHT', { cap: capKey(cap), did: card.did });
126
+ }
127
+ } catch (e: any) {
128
+ logger.warn('Failed to index capability (non-fatal)', { cap: capKey(cap), error: (e as Error).message });
129
+ }
130
+ }));
131
+
132
+ // 3. Index locally for fast in-process search
133
+ searchEngine.indexAgentCard(card);
134
+
135
+ logger.info('Published Agent Card to DHT', { did: card.did });
136
+ } catch (error) {
137
+ // DHT publish failure is non-fatal - local index is still available
138
+ logger.warn('Failed to publish Agent Card to DHT (non-fatal)', { error: (error as Error).message });
139
+ // Still index locally even if DHT fails
140
+ searchEngine.indexAgentCard(card);
141
+ }
142
+ },
143
+
144
+ queryAgentCard: async (did: string) => {
145
+ try {
146
+ const dht = (libp2p as any).services?.dht;
147
+ if (!dht) throw new DiscoveryError('DHT service not available');
148
+
149
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
150
+ for await (const event of dht.get(key)) {
151
+ const raw = extractValue(event);
152
+ if (raw) {
153
+ const card = decodeFromCBOR(raw);
154
+ searchEngine.indexAgentCard(card);
155
+ logger.debug('Found Agent Card in DHT', { did });
156
+ return card;
157
+ }
158
+ }
159
+
160
+ logger.debug('Agent Card not found in DHT', { did });
161
+ return null;
162
+ } catch (error) {
163
+ logger.warn('Failed to query Agent Card', { did, error });
164
+ return null;
165
+ }
166
+ },
167
+
168
+ queryByCapability: async (capability: string) => {
169
+ try {
170
+ const dht = (libp2p as any).services?.dht;
171
+
172
+ // 1. Check local index first
173
+ const local = searchEngine.getAllIndexedCards().filter(card =>
174
+ card.capabilities.some((cap: any) => {
175
+ const name = typeof cap === 'string' ? cap : cap.name;
176
+ return name?.toLowerCase().includes(capability.toLowerCase());
177
+ })
178
+ );
179
+ if (local.length > 0) return local;
180
+
181
+ // 2. Fall back to DHT capability index
182
+ if (!dht) return [];
183
+
184
+ const capDHTKey = uint8ArrayFromString(`/clawiverse/cap/${capKey(capability)}`);
185
+ const dids = await readDIDList(dht, capDHTKey);
186
+ logger.debug('DHT capability index', { capability, dids });
187
+
188
+ // Fetch each Agent Card in parallel
189
+ const cards = await Promise.all(
190
+ dids.map(async (did) => {
191
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
192
+ try {
193
+ for await (const event of dht.get(key)) {
194
+ const raw = extractValue(event);
195
+ if (raw) {
196
+ const card = decodeFromCBOR(raw);
197
+ searchEngine.indexAgentCard(card);
198
+ return card;
199
+ }
200
+ }
201
+ } catch { /* skip unreachable */ }
202
+ return null;
203
+ })
204
+ );
205
+
206
+ return cards.filter((c): c is AgentCard => c !== null);
207
+ } catch (error) {
208
+ throw new DiscoveryError('Failed to query by capability', error);
209
+ }
210
+ },
211
+
212
+ searchSemantic: async (query: SemanticQuery) => {
213
+ try {
214
+ // Always pull from DHT __all__ index to discover remote nodes
215
+ const dht = (libp2p as any).services?.dht;
216
+ if (dht) {
217
+ const allKey = uint8ArrayFromString('/clawiverse/cap/__all__');
218
+ const dids = await readDIDList(dht, allKey);
219
+ logger.debug('DHT __all__ index', { count: dids.length });
220
+
221
+ await Promise.all(
222
+ dids.map(async (did) => {
223
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
224
+ try {
225
+ for await (const event of dht.get(key)) {
226
+ const raw = extractValue(event);
227
+ if (raw) {
228
+ searchEngine.indexAgentCard(decodeFromCBOR(raw));
229
+ break;
230
+ }
231
+ }
232
+ } catch { /* skip */ }
233
+ })
234
+ );
235
+ }
236
+
237
+ return searchEngine.search(query);
238
+ } catch (error) {
239
+ throw new DiscoveryError('Failed to perform semantic search', error);
240
+ }
241
+ },
242
+
243
+ queryRelayPeers: async (): Promise<string[]> => {
244
+ const dht = (libp2p as any).services?.dht;
245
+ if (!dht) return [];
246
+ const capDHTKey = uint8ArrayFromString('/clawiverse/cap/relay');
247
+ const dids = await readDIDList(dht, capDHTKey);
248
+ const addrs: string[] = [];
249
+ await Promise.all(dids.map(async (did) => {
250
+ const card = await operations.queryAgentCard(did);
251
+ if (card?.endpoints) {
252
+ addrs.push(...card.endpoints.filter((e: string) => !e.includes('/p2p-circuit/')));
253
+ }
254
+ }));
255
+ return addrs;
256
+ },
257
+
258
+ resolveDID: async (did: string) => {
259
+ try {
260
+ const dht = (libp2p as any).services?.dht;
261
+ if (!dht) throw new DiscoveryError('DHT service not available');
262
+
263
+ const key = uint8ArrayFromString(`/clawiverse/agent/${did}`);
264
+ for await (const event of dht.get(key)) {
265
+ const raw = extractValue(event);
266
+ if (raw) {
267
+ const card = decodeFromCBOR(raw);
268
+ if (card.peerId) {
269
+ logger.debug('Resolved DID to peer', { did, peerId: card.peerId });
270
+ return { peerId: card.peerId, multiaddrs: card.endpoints || [] };
271
+ }
272
+ logger.warn('Agent Card found but has no peerId', { did });
273
+ return null;
274
+ }
275
+ }
276
+
277
+ logger.debug('DID not found in DHT', { did });
278
+ return null;
279
+ } catch (error) {
280
+ logger.warn('Failed to resolve DID', { did, error });
281
+ return null;
282
+ }
283
+ },
284
+ });
285
+ }
@@ -0,0 +1,3 @@
1
+ export * from './agent-card.js';
2
+ export * from './dht.js';
3
+ export * from './bootstrap.js';
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Search Index for Agent Discovery
3
+ *
4
+ * Maintains a local index of discovered agents for fast semantic search
5
+ */
6
+
7
+ import lunr from 'lunr';
8
+ import Fuse from 'fuse.js';
9
+ import type { AgentCard } from './agent-card-types.js';
10
+ import { createLogger } from '../utils/logger.js';
11
+
12
+ const logger = createLogger('search-index');
13
+
14
+ /**
15
+ * Semantic Query Interface
16
+ */
17
+ export interface SemanticQuery {
18
+ text?: string; // Natural language: "translate Japanese"
19
+ capability?: string; // Structured: "translate"
20
+ filters?: {
21
+ language?: string;
22
+ minTrustScore?: number;
23
+ maxCost?: number;
24
+ tags?: string[];
25
+ };
26
+ limit?: number;
27
+ }
28
+
29
+ /**
30
+ * Search Result with Score
31
+ */
32
+ export interface SearchResult {
33
+ card: AgentCard;
34
+ score: number;
35
+ }
36
+
37
+ /**
38
+ * Search Index Implementation
39
+ */
40
+ export class SearchIndex {
41
+ private cards = new Map<string, AgentCard>();
42
+ private lunrIndex?: lunr.Index;
43
+ private fuse?: Fuse<AgentCard>;
44
+ private needsRebuild = false;
45
+
46
+ /**
47
+ * Add or update an Agent Card in the index
48
+ */
49
+ indexAgentCard(card: AgentCard): void {
50
+ this.cards.set(card.did, card);
51
+ this.needsRebuild = true;
52
+ logger.debug('Indexed Agent Card', { did: card.did, capabilities: card.capabilities.length });
53
+ }
54
+
55
+ /**
56
+ * Remove an Agent Card from the index
57
+ */
58
+ removeAgentCard(did: string): void {
59
+ this.cards.delete(did);
60
+ this.needsRebuild = true;
61
+ logger.debug('Removed Agent Card from index', { did });
62
+ }
63
+
64
+ /**
65
+ * Search for agents matching a query
66
+ */
67
+ search(query: SemanticQuery): SearchResult[] {
68
+ if (this.needsRebuild) {
69
+ this.rebuild();
70
+ }
71
+
72
+ let results: SearchResult[] = [];
73
+
74
+ // Text search using Lunr
75
+ if (query.text && this.lunrIndex) {
76
+ results = this.searchByText(query.text);
77
+ }
78
+ // Capability search using Fuse
79
+ else if (query.capability && this.fuse) {
80
+ results = this.searchByCapability(query.capability);
81
+ }
82
+ // No query - return all
83
+ else {
84
+ results = Array.from(this.cards.values()).map(card => ({
85
+ card,
86
+ score: 1.0,
87
+ }));
88
+ }
89
+
90
+ // Apply filters
91
+ if (query.filters) {
92
+ results = this.applyFilters(results, query.filters);
93
+ }
94
+
95
+ // Sort by score (descending)
96
+ results.sort((a, b) => b.score - a.score);
97
+
98
+ // Apply limit
99
+ if (query.limit) {
100
+ results = results.slice(0, query.limit);
101
+ }
102
+
103
+ logger.debug('Search completed', { query, results: results.length });
104
+ return results;
105
+ }
106
+
107
+ /**
108
+ * Get all indexed cards
109
+ */
110
+ getAllCards(): AgentCard[] {
111
+ return Array.from(this.cards.values());
112
+ }
113
+
114
+ /**
115
+ * Clear the index
116
+ */
117
+ clear(): void {
118
+ this.cards.clear();
119
+ this.lunrIndex = undefined;
120
+ this.fuse = undefined;
121
+ this.needsRebuild = false;
122
+ logger.info('Search index cleared');
123
+ }
124
+
125
+ /**
126
+ * Get index size
127
+ */
128
+ size(): number {
129
+ return this.cards.size;
130
+ }
131
+
132
+ /**
133
+ * Rebuild search indexes
134
+ */
135
+ private rebuild(): void {
136
+ logger.info('Rebuilding search indexes', { cards: this.cards.size });
137
+
138
+ const cards = Array.from(this.cards.values());
139
+
140
+ // Build Lunr index for full-text search
141
+ this.lunrIndex = lunr(function (this: lunr.Builder) {
142
+ this.ref('did');
143
+ this.field('name', { boost: 10 });
144
+ this.field('description', { boost: 5 });
145
+ this.field('capabilities');
146
+
147
+ for (const card of cards) {
148
+ this.add({
149
+ did: card.did,
150
+ name: card.name,
151
+ description: card.description,
152
+ capabilities: card.capabilities
153
+ .map((cap: any) => `${cap.name} ${cap.description}`)
154
+ .join(' '),
155
+ });
156
+ }
157
+ });
158
+
159
+ // Build Fuse index for fuzzy matching
160
+ this.fuse = new Fuse(Array.from(this.cards.values()), {
161
+ keys: [
162
+ { name: 'name', weight: 0.3 },
163
+ { name: 'description', weight: 0.2 },
164
+ { name: 'capabilities.name', weight: 0.3 },
165
+ { name: 'capabilities.description', weight: 0.2 },
166
+ ],
167
+ threshold: 0.4,
168
+ includeScore: true,
169
+ });
170
+
171
+ this.needsRebuild = false;
172
+ logger.info('Search indexes rebuilt');
173
+ }
174
+
175
+ /**
176
+ * Search by text using Lunr
177
+ */
178
+ private searchByText(text: string): SearchResult[] {
179
+ if (!this.lunrIndex) return [];
180
+
181
+ const lunrResults = this.lunrIndex.search(text);
182
+ return lunrResults.map(result => ({
183
+ card: this.cards.get(result.ref)!,
184
+ score: result.score,
185
+ }));
186
+ }
187
+
188
+ /**
189
+ * Search by capability using Fuse
190
+ */
191
+ private searchByCapability(capability: string): SearchResult[] {
192
+ if (!this.fuse) return [];
193
+
194
+ const fuseResults = this.fuse.search(capability);
195
+ return fuseResults.map(result => ({
196
+ card: result.item,
197
+ score: 1 - (result.score || 0), // Fuse score is distance, convert to similarity
198
+ }));
199
+ }
200
+
201
+ /**
202
+ * Apply filters to search results
203
+ */
204
+ private applyFilters(
205
+ results: SearchResult[],
206
+ filters: NonNullable<SemanticQuery['filters']>
207
+ ): SearchResult[] {
208
+ return results.filter(result => {
209
+ const { card } = result;
210
+
211
+ // Trust score filter
212
+ if (filters.minTrustScore !== undefined && card.trust) {
213
+ const overallTrust =
214
+ card.trust.interactionScore * 0.4 +
215
+ Math.min(card.trust.endorsements / 10, 1) * 0.2 +
216
+ card.trust.completionRate * 0.2 +
217
+ card.trust.uptime * 0.2;
218
+
219
+ if (overallTrust < filters.minTrustScore) {
220
+ return false;
221
+ }
222
+ }
223
+
224
+ // Language filter (check capability metadata)
225
+ if (filters.language) {
226
+ const hasLanguage = card.capabilities.some(
227
+ cap => {
228
+ const metadata = cap.metadata as Record<string, any> | undefined;
229
+ if (!metadata) return false;
230
+ return metadata.language === filters.language ||
231
+ (Array.isArray(metadata.languages) && metadata.languages.includes(filters.language));
232
+ }
233
+ );
234
+ if (!hasLanguage) return false;
235
+ }
236
+
237
+ // Tags filter
238
+ if (filters.tags && filters.tags.length > 0) {
239
+ const cardTags = Array.isArray(card.metadata?.tags) ? (card.metadata.tags as string[]) : [];
240
+ const hasAllTags = filters.tags.every(tag => cardTags.includes(tag));
241
+ if (!hasAllTags) return false;
242
+ }
243
+
244
+ return true;
245
+ });
246
+ }
247
+ }