@afterxleep/doc-bot 1.0.2 → 1.1.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.
package/README.md CHANGED
@@ -9,9 +9,10 @@ A generic MCP (Model Context Protocol) server that provides intelligent document
9
9
 
10
10
  doc-bot is an intelligent documentation server that:
11
11
  - 🔍 **Searches** your project documentation instantly
12
- - 🧠 **Infers** relevant docs based on your current work
12
+ - 🧠 **Auto-indexes** content for smart inference (no manual keyword mapping!)
13
13
  - 📋 **Applies** global rules to every AI interaction
14
14
  - 🎯 **Suggests** contextual documentation based on file patterns
15
+ - 🤖 **Detects** code patterns, frameworks, and keywords automatically
15
16
  - 🔄 **Updates** automatically when docs change
16
17
 
17
18
  ## Installation
@@ -66,6 +67,48 @@ your-project/
66
67
  - **Guides** (`guides/`): Step-by-step instructions for specific tasks
67
68
  - **Reference** (`reference/`): Quick lookups and troubleshooting
68
69
 
70
+ ### Example documentation files:
71
+
72
+ **Global Rule Example** (`doc-bot/core/coding-standards.md`):
73
+ ```markdown
74
+ ---
75
+ title: "Coding Standards"
76
+ description: "Core coding standards that apply to all code"
77
+ keywords: ["code-quality", "standards", "best-practices"]
78
+ tags: ["core", "quality"]
79
+ ---
80
+
81
+ # Coding Standards
82
+
83
+ - Use 2 spaces for indentation
84
+ - Maximum line length: 100 characters
85
+ - Always use const/let, never var
86
+ - Prefer async/await over promises
87
+ - Write descriptive variable names
88
+ ```
89
+
90
+ **Contextual Rule Example** (`doc-bot/guides/testing.md`):
91
+ ```markdown
92
+ ---
93
+ title: "Testing Guide"
94
+ description: "How to write and run tests"
95
+ keywords: ["testing", "jest", "tdd", "unit-tests"]
96
+ tags: ["testing", "quality"]
97
+ ---
98
+
99
+ # Testing Guide
100
+
101
+ All test files should:
102
+ - Use describe/it blocks for organization
103
+ - Include both positive and negative test cases
104
+ - Mock external dependencies
105
+ - Aim for 80%+ code coverage
106
+
107
+ Run tests with: `npm test`
108
+ ```
109
+
110
+ **👀 See `examples/` folder for complete example files with proper frontmatter and content structure.**
111
+
69
112
  ## The manifest file
70
113
 
71
114
  The `doc-bot/manifest.json` file controls how your documentation works:
@@ -84,18 +127,6 @@ The `doc-bot/manifest.json` file controls how your documentation works:
84
127
  "*.spec.js": ["guides/testing.md"],
85
128
  "src/components/*": ["guides/react-components.md"],
86
129
  "src/api/*": ["guides/api-development.md"]
87
- },
88
- "inference": {
89
- "keywords": {
90
- "testing": ["guides/testing.md"],
91
- "deployment": ["guides/deployment.md"],
92
- "api": ["guides/api-development.md"]
93
- },
94
- "patterns": {
95
- "describe(": ["guides/testing.md"],
96
- "it(": ["guides/testing.md"],
97
- "fetch(": ["guides/api-development.md"]
98
- }
99
130
  }
100
131
  }
101
132
  ```
@@ -104,8 +135,38 @@ The `doc-bot/manifest.json` file controls how your documentation works:
104
135
 
105
136
  - **`globalRules`**: Documents that apply to every AI interaction
106
137
  - **`contextualRules`**: Documents triggered by specific file patterns (e.g., test files → testing guide)
107
- - **`inference.keywords`**: Documents suggested when certain words appear in queries
108
- - **`inference.patterns`**: Documents suggested when certain code patterns are detected
138
+
139
+ ### 🎯 Automatic Inference (New!)
140
+
141
+ doc-bot automatically analyzes your documentation content to build smart indexes. No more manual keyword mappings! It automatically:
142
+
143
+ - **Extracts keywords** from document metadata (frontmatter)
144
+ - **Detects technical terms** in your documentation content
145
+ - **Recognizes code patterns** in code blocks (React hooks, SQL commands, etc.)
146
+ - **Identifies frameworks** mentioned in your docs
147
+ - **Indexes file extensions** referenced in documentation
148
+
149
+ Just write good documentation with descriptive frontmatter, and doc-bot handles the rest!
150
+
151
+ ### Writing documentation for best results
152
+
153
+ To maximize the automatic inference capabilities, include frontmatter in your markdown files:
154
+
155
+ ```markdown
156
+ ---
157
+ title: "React Component Guidelines"
158
+ description: "Best practices for building React components"
159
+ keywords: ["react", "components", "hooks", "jsx"]
160
+ tags: ["frontend", "development"]
161
+ category: "guides"
162
+ ---
163
+
164
+ # React Component Guidelines
165
+
166
+ Your documentation content here...
167
+ ```
168
+
169
+ The automatic indexing will use this metadata along with analyzing your content to provide intelligent suggestions.
109
170
 
110
171
  ## Development setup
111
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afterxleep/doc-bot",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Generic MCP server for intelligent documentation access in any project",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,388 @@
1
+ class DocumentIndex {
2
+ constructor() {
3
+ this.keywordIndex = new Map();
4
+ this.topicIndex = new Map();
5
+ this.patternIndex = new Map();
6
+ this.extensionIndex = new Map();
7
+ }
8
+
9
+ async buildIndexes(documents) {
10
+ for (const document of documents) {
11
+ await this.indexDocument(document);
12
+ }
13
+ }
14
+
15
+ async indexDocument(document) {
16
+ if (!document) {
17
+ return;
18
+ }
19
+
20
+ // Index keywords from metadata (if present)
21
+ if (document.metadata?.keywords) {
22
+ const keywords = Array.isArray(document.metadata.keywords)
23
+ ? document.metadata.keywords
24
+ : [document.metadata.keywords];
25
+
26
+ for (const keyword of keywords) {
27
+ this.addToIndex(this.keywordIndex, keyword.toLowerCase(), document, 10);
28
+ }
29
+ }
30
+
31
+ // Index topics from tags and category (if present)
32
+ if (document.metadata?.tags) {
33
+ const tags = Array.isArray(document.metadata.tags)
34
+ ? document.metadata.tags
35
+ : [document.metadata.tags];
36
+
37
+ for (const tag of tags) {
38
+ this.addToIndex(this.topicIndex, tag.toLowerCase(), document, 5);
39
+ }
40
+ }
41
+
42
+ if (document.metadata?.category) {
43
+ this.addToIndex(this.topicIndex, document.metadata.category.toLowerCase(), document, 5);
44
+ }
45
+
46
+ // Index content keywords
47
+ if (document.content) {
48
+ await this.indexContentKeywords(document);
49
+ }
50
+ }
51
+
52
+ async indexContentKeywords(document) {
53
+ const content = document.content;
54
+
55
+ // Extract keywords from code blocks
56
+ this.extractCodeBlockKeywords(content, document);
57
+
58
+ // Extract keywords from headings
59
+ this.extractHeadingKeywords(content, document);
60
+
61
+ // Extract file extensions
62
+ this.extractFileExtensions(content, document);
63
+
64
+ // Extract framework and library names
65
+ this.extractFrameworkNames(content, document);
66
+
67
+ // Extract code patterns
68
+ this.extractCodePatterns(content, document);
69
+ }
70
+
71
+ extractCodeBlockKeywords(content, document) {
72
+ // Match code blocks with language specifiers
73
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
74
+ let match;
75
+
76
+ while ((match = codeBlockRegex.exec(content)) !== null) {
77
+ const codeContent = match[2];
78
+
79
+ // Extract common library/framework names from code
80
+ const patterns = [
81
+ /require\(['"]([^'"]+)['"]\)/g,
82
+ /import\s+(\w+)/g,
83
+ /from\s+['"]([^'"]+)['"]/g,
84
+ /\b(express|mongoose|bodyParser|flask|sqlalchemy|react|vue|angular|django|fastapi|axios|lodash|moment|uuid)\b/gi
85
+ ];
86
+
87
+ for (const pattern of patterns) {
88
+ let patternMatch;
89
+ while ((patternMatch = pattern.exec(codeContent)) !== null) {
90
+ const keyword = patternMatch[1]?.toLowerCase() || patternMatch[0]?.toLowerCase();
91
+ if (keyword && !this.isCommonWord(keyword)) {
92
+ this.addToIndex(this.keywordIndex, keyword, document, 3); // Lower score for content keywords
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ extractHeadingKeywords(content, document) {
100
+ // Extract from markdown headings
101
+ const headingRegex = /^#{1,6}\s+(.+)$/gm;
102
+ let match;
103
+
104
+ while ((match = headingRegex.exec(content)) !== null) {
105
+ const heading = match[1];
106
+ const words = heading.split(/\s+/);
107
+
108
+ for (const word of words) {
109
+ const cleanWord = word.toLowerCase().replace(/[^\w\-\/]/g, '');
110
+ if (cleanWord && !this.isCommonWord(cleanWord)) {
111
+ this.addToIndex(this.keywordIndex, cleanWord, document, 2); // Lower score for content keywords
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ extractFileExtensions(content, document) {
118
+ // Extract file extensions mentioned in content
119
+ const extensionRegex = /\*\.(\w+)\b/g;
120
+ let match;
121
+
122
+ while ((match = extensionRegex.exec(content)) !== null) {
123
+ const extension = match[1].toLowerCase();
124
+ this.addToIndex(this.extensionIndex, extension, document);
125
+ }
126
+ }
127
+
128
+ extractFrameworkNames(content, document) {
129
+ // Common framework and technology names
130
+ const techPatterns = [
131
+ /\b(react|vue|angular|svelte|next\.js|nuxt\.js|gatsby)\b/gi,
132
+ /\b(node\.js|express|fastify|koa|nest\.js)\b/gi,
133
+ /\b(postgresql|mysql|mongodb|redis|elasticsearch)\b/gi,
134
+ /\b(docker|kubernetes|terraform|ansible)\b/gi,
135
+ /\b(aws|azure|gcp|heroku|vercel|netlify)\b/gi,
136
+ /\b(typescript|javascript|python|java|golang|rust)\b/gi
137
+ ];
138
+
139
+ for (const pattern of techPatterns) {
140
+ let match;
141
+ while ((match = pattern.exec(content)) !== null) {
142
+ const keyword = match[0].toLowerCase();
143
+ if (!this.isCommonWord(keyword)) {
144
+ this.addToIndex(this.keywordIndex, keyword, document, 2); // Lower score for content keywords
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ extractCodePatterns(content, document) {
151
+ // Match code blocks with language specifiers
152
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
153
+ let match;
154
+
155
+ while ((match = codeBlockRegex.exec(content)) !== null) {
156
+ const language = match[1]?.toLowerCase();
157
+ const codeContent = match[2];
158
+
159
+ // Define patterns for different languages
160
+ const patterns = this.getCodePatterns(language);
161
+
162
+ for (const pattern of patterns) {
163
+ let patternMatch;
164
+ while ((patternMatch = pattern.regex.exec(codeContent)) !== null) {
165
+ const patternKey = pattern.key || patternMatch[0];
166
+ this.addToIndex(this.patternIndex, patternKey, document, 6); // Medium-high score for patterns
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ getCodePatterns(language) {
173
+ const patterns = [];
174
+
175
+ // JavaScript/TypeScript patterns
176
+ if (!language || language === 'javascript' || language === 'js' || language === 'typescript' || language === 'ts') {
177
+ patterns.push(
178
+ { regex: /\buseState\b/g, key: 'useState' },
179
+ { regex: /\buseEffect\b/g, key: 'useEffect' },
180
+ { regex: /\buseCallback\b/g, key: 'useCallback' },
181
+ { regex: /\buseMemo\b/g, key: 'useMemo' },
182
+ { regex: /\buseContext\b/g, key: 'useContext' },
183
+ { regex: /\buseReducer\b/g, key: 'useReducer' },
184
+ { regex: /app\.get\(/g, key: 'app.get' },
185
+ { regex: /app\.post\(/g, key: 'app.post' },
186
+ { regex: /app\.put\(/g, key: 'app.put' },
187
+ { regex: /app\.delete\(/g, key: 'app.delete' },
188
+ { regex: /describe\(/g, key: 'describe(' },
189
+ { regex: /it\(/g, key: 'it(' },
190
+ { regex: /test\(/g, key: 'test(' },
191
+ { regex: /expect\(/g, key: 'expect(' },
192
+ { regex: /async\s+function/g, key: 'async function' },
193
+ { regex: /\.then\(/g, key: '.then(' },
194
+ { regex: /\.catch\(/g, key: '.catch(' },
195
+ { regex: /await\s+/g, key: 'await' }
196
+ );
197
+ }
198
+
199
+ // Python patterns
200
+ if (language === 'python' || language === 'py') {
201
+ patterns.push(
202
+ { regex: /\bdef\s+/g, key: 'def ' },
203
+ { regex: /\bclass\s+/g, key: 'class ' },
204
+ { regex: /\b__init__\b/g, key: '__init__' },
205
+ { regex: /\bif\s+__name__\s*==\s*['"]__main__['"]/g, key: 'if __name__' },
206
+ { regex: /\bimport\s+/g, key: 'import ' },
207
+ { regex: /\bfrom\s+\w+\s+import/g, key: 'from import' },
208
+ { regex: /\btry:/g, key: 'try:' },
209
+ { regex: /\bexcept\s+/g, key: 'except ' },
210
+ { regex: /\bwith\s+/g, key: 'with ' },
211
+ { regex: /@\w+/g, key: 'decorator' }
212
+ );
213
+ }
214
+
215
+ // SQL patterns
216
+ if (language === 'sql') {
217
+ patterns.push(
218
+ { regex: /\bSELECT\b/gi, key: 'SELECT' },
219
+ { regex: /\bINSERT\s+INTO\b/gi, key: 'INSERT INTO' },
220
+ { regex: /\bUPDATE\b/gi, key: 'UPDATE' },
221
+ { regex: /\bDELETE\s+FROM\b/gi, key: 'DELETE FROM' },
222
+ { regex: /\bCREATE\s+TABLE\b/gi, key: 'CREATE TABLE' },
223
+ { regex: /\bALTER\s+TABLE\b/gi, key: 'ALTER TABLE' },
224
+ { regex: /\bDROP\s+TABLE\b/gi, key: 'DROP TABLE' },
225
+ { regex: /\bJOIN\b/gi, key: 'JOIN' },
226
+ { regex: /\bLEFT\s+JOIN\b/gi, key: 'LEFT JOIN' },
227
+ { regex: /\bINNER\s+JOIN\b/gi, key: 'INNER JOIN' }
228
+ );
229
+ }
230
+
231
+ // Java patterns
232
+ if (language === 'java') {
233
+ patterns.push(
234
+ { regex: /\bpublic\s+class\b/g, key: 'public class' },
235
+ { regex: /\bprivate\s+\w+/g, key: 'private' },
236
+ { regex: /\bpublic\s+static\s+void\s+main/g, key: 'main method' },
237
+ { regex: /@Override/g, key: '@Override' },
238
+ { regex: /\bnew\s+\w+\(/g, key: 'new' }
239
+ );
240
+ }
241
+
242
+ // Docker patterns
243
+ if (language === 'dockerfile' || language === 'docker') {
244
+ patterns.push(
245
+ { regex: /\bFROM\b/gi, key: 'FROM' },
246
+ { regex: /\bRUN\b/gi, key: 'RUN' },
247
+ { regex: /\bCOPY\b/gi, key: 'COPY' },
248
+ { regex: /\bADD\b/gi, key: 'ADD' },
249
+ { regex: /\bEXPOSE\b/gi, key: 'EXPOSE' },
250
+ { regex: /\bCMD\b/gi, key: 'CMD' },
251
+ { regex: /\bENTRYPOINT\b/gi, key: 'ENTRYPOINT' }
252
+ );
253
+ }
254
+
255
+ return patterns;
256
+ }
257
+
258
+ isCommonWord(word) {
259
+ const commonWords = new Set([
260
+ 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
261
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
262
+ 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'must', 'shall',
263
+ 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they',
264
+ 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their',
265
+ 'about', 'above', 'across', 'after', 'against', 'along', 'among', 'around', 'before',
266
+ 'behind', 'below', 'beneath', 'beside', 'between', 'beyond', 'during', 'except',
267
+ 'from', 'inside', 'into', 'near', 'outside', 'over', 'since', 'through', 'under',
268
+ 'until', 'up', 'upon', 'within', 'without', 'how', 'what', 'when', 'where', 'why',
269
+ 'who', 'which', 'whose', 'whom', 'very', 'so', 'too', 'quite', 'rather', 'such',
270
+ 'guide', 'documentation', 'helps', 'developers', 'system', 'useful', 'explains',
271
+ 'use', 'using', 'used', 'get', 'getting', 'set', 'setting', 'make', 'making',
272
+ 'create', 'creating', 'build', 'building', 'run', 'running', 'start', 'starting'
273
+ ]);
274
+
275
+ return commonWords.has(word.toLowerCase()) || word.length < 2;
276
+ }
277
+
278
+ addToIndex(index, key, document, score = 1) {
279
+ if (!index.has(key)) {
280
+ index.set(key, []);
281
+ }
282
+ index.get(key).push({ document, score });
283
+ }
284
+
285
+ findRelevantDocs(context) {
286
+ if (!context || Object.keys(context).length === 0) {
287
+ return [];
288
+ }
289
+
290
+ const candidates = new Map();
291
+
292
+ // Search by query keywords
293
+ if (context.query) {
294
+ this.searchKeywords(context.query, candidates);
295
+ }
296
+
297
+ // Search by code snippet patterns
298
+ if (context.codeSnippet) {
299
+ this.searchCodePatterns(context.codeSnippet, candidates);
300
+ }
301
+
302
+ // Search by file extension
303
+ if (context.filePath) {
304
+ this.searchFileExtension(context.filePath, candidates);
305
+ }
306
+
307
+ return this.scoreAndRank(candidates);
308
+ }
309
+
310
+ searchKeywords(query, candidates) {
311
+ const queryLower = query.toLowerCase();
312
+ const words = queryLower.split(/\s+/);
313
+
314
+ for (const word of words) {
315
+ // Search in keyword index
316
+ if (this.keywordIndex.has(word)) {
317
+ const entries = this.keywordIndex.get(word);
318
+ for (const entry of entries) {
319
+ this.addCandidate(candidates, entry.document, entry.score);
320
+ }
321
+ }
322
+
323
+ // Search in topic index
324
+ if (this.topicIndex.has(word)) {
325
+ const entries = this.topicIndex.get(word);
326
+ for (const entry of entries) {
327
+ this.addCandidate(candidates, entry.document, entry.score);
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ searchCodePatterns(codeSnippet, candidates) {
334
+ if (this.patternIndex.size > 0) {
335
+ // Search for patterns in the code snippet
336
+ for (const [pattern, entries] of this.patternIndex) {
337
+ // Check if the pattern exists in the code snippet
338
+ let found = false;
339
+
340
+ // For SQL patterns, do case-insensitive matching
341
+ if (pattern.toUpperCase() === pattern) {
342
+ found = codeSnippet.toUpperCase().includes(pattern);
343
+ } else {
344
+ found = codeSnippet.includes(pattern);
345
+ }
346
+
347
+ if (found) {
348
+ for (const entry of entries) {
349
+ this.addCandidate(candidates, entry.document, 8); // High score for pattern match
350
+ }
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ searchFileExtension(filePath, candidates) {
357
+ // For now, implement basic extension matching
358
+ // This will be enhanced in later iterations
359
+ if (this.extensionIndex.size > 0) {
360
+ const extension = filePath.split('.').pop()?.toLowerCase();
361
+ if (extension && this.extensionIndex.has(extension)) {
362
+ const entries = this.extensionIndex.get(extension);
363
+ for (const entry of entries) {
364
+ this.addCandidate(candidates, entry.document, 3); // Lower score for extension match
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ addCandidate(candidates, document, score) {
371
+ const key = document.fileName || document.filePath;
372
+ if (!candidates.has(key)) {
373
+ candidates.set(key, { document, score: 0 });
374
+ }
375
+ candidates.get(key).score += score;
376
+ }
377
+
378
+ scoreAndRank(candidates) {
379
+ const results = Array.from(candidates.values());
380
+
381
+ // Sort by score (descending)
382
+ results.sort((a, b) => b.score - a.score);
383
+
384
+ return results;
385
+ }
386
+ }
387
+
388
+ module.exports = { DocumentIndex };
@@ -1,13 +1,28 @@
1
1
  const path = require('path');
2
+ const { DocumentIndex } = require('./DocumentIndex');
2
3
 
3
4
  class InferenceEngine {
4
5
  constructor(documentationService, manifestLoader = null) {
5
6
  this.docService = documentationService;
6
7
  this.manifestLoader = manifestLoader;
8
+ this.documentIndex = new DocumentIndex();
9
+ this.isIndexBuilt = false;
7
10
  }
8
11
 
9
12
  async initialize() {
10
- // Initialize any required data
13
+ // Build the document index with all available documents
14
+ await this.buildDocumentIndex();
15
+ }
16
+
17
+ async buildDocumentIndex() {
18
+ try {
19
+ const documents = await this.docService.getAllDocuments();
20
+ await this.documentIndex.buildIndexes(documents);
21
+ this.isIndexBuilt = true;
22
+ } catch (error) {
23
+ console.error('Error building document index:', error);
24
+ this.isIndexBuilt = false;
25
+ }
11
26
  }
12
27
 
13
28
  async getRelevantDocumentation(context) {
@@ -48,6 +63,25 @@ class InferenceEngine {
48
63
  }
49
64
 
50
65
  async getInferredDocs(context) {
66
+ // Ensure the index is built
67
+ if (!this.isIndexBuilt) {
68
+ await this.buildDocumentIndex();
69
+ }
70
+
71
+ // Use the smart DocumentIndex for inference
72
+ if (this.isIndexBuilt) {
73
+ const results = this.documentIndex.findRelevantDocs(context);
74
+ return results.map(result => ({
75
+ ...result.document,
76
+ inferenceScore: result.score
77
+ }));
78
+ }
79
+
80
+ // Fallback to legacy method if index building failed
81
+ return this.getLegacyInferredDocs(context);
82
+ }
83
+
84
+ async getLegacyInferredDocs(context) {
51
85
  const docs = [];
52
86
 
53
87
  // Keyword-based inference
@@ -181,7 +215,14 @@ class InferenceEngine {
181
215
  }
182
216
 
183
217
  if (inferredDocs.length > 0) {
184
- confidence += Math.min(inferredDocs.length * 0.05, 0.2);
218
+ // If using DocumentIndex, factor in inference scores
219
+ if (this.isIndexBuilt && inferredDocs.some(doc => doc.inferenceScore)) {
220
+ const avgScore = inferredDocs.reduce((sum, doc) => sum + (doc.inferenceScore || 0), 0) / inferredDocs.length;
221
+ const normalizedScore = Math.min(avgScore / 20, 1.0); // Normalize to 0-1
222
+ confidence += normalizedScore * 0.3;
223
+ } else {
224
+ confidence += Math.min(inferredDocs.length * 0.05, 0.2);
225
+ }
185
226
  }
186
227
 
187
228
  // Reduce confidence if no matches found