#
tokens: 25618/50000 1/88 files (page 5/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 5. Use http://codebase.md/marianfoo/mcp-sap-docs?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       ├── 00-overview.mdc
│       ├── 10-search-stack.mdc
│       ├── 20-tools-and-apis.mdc
│       ├── 30-tests-and-output.mdc
│       ├── 40-deploy.mdc
│       ├── 50-metadata-config.mdc
│       ├── 60-adding-github-sources.mdc
│       ├── 70-tool-usage-guide.mdc
│       └── 80-abap-integration.mdc
├── .cursorignore
├── .gitattributes
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── missing-documentation.yml
│   │   └── new-documentation-source.yml
│   └── workflows
│       ├── deploy-mcp-sap-docs.yml
│       ├── test-pr.yml
│       └── update-submodules.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── docs
│   ├── ABAP-INTEGRATION-SUMMARY.md
│   ├── ABAP-MULTI-VERSION-INTEGRATION.md
│   ├── ABAP-STANDARD-INTEGRATION.md
│   ├── ABAP-USAGE-GUIDE.md
│   ├── ARCHITECTURE.md
│   ├── COMMUNITY-SEARCH-IMPLEMENTATION.md
│   ├── CONTENT-SIZE-LIMITS.md
│   ├── CURSOR-SETUP.md
│   ├── DEV.md
│   ├── FTS5-IMPLEMENTATION-COMPLETE.md
│   ├── LLM-FRIENDLY-IMPROVEMENTS.md
│   ├── METADATA-CONSOLIDATION.md
│   ├── TEST-SEARCH.md
│   └── TESTS.md
├── ecosystem.config.cjs
├── index.html
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── REMOTE_SETUP.md
├── scripts
│   ├── build-fts.ts
│   ├── build-index.ts
│   ├── check-version.js
│   └── summarize-src.js
├── server.json
├── setup.sh
├── src
│   ├── global.d.ts
│   ├── http-server.ts
│   ├── lib
│   │   ├── BaseServerHandler.ts
│   │   ├── communityBestMatch.ts
│   │   ├── config.ts
│   │   ├── localDocs.ts
│   │   ├── logger.ts
│   │   ├── metadata.ts
│   │   ├── sapHelp.ts
│   │   ├── search.ts
│   │   ├── searchDb.ts
│   │   ├── truncate.ts
│   │   ├── types.ts
│   │   └── url-generation
│   │       ├── abap.ts
│   │       ├── BaseUrlGenerator.ts
│   │       ├── cap.ts
│   │       ├── cloud-sdk.ts
│   │       ├── dsag.ts
│   │       ├── GenericUrlGenerator.ts
│   │       ├── index.ts
│   │       ├── README.md
│   │       ├── sapui5.ts
│   │       ├── utils.ts
│   │       └── wdi5.ts
│   ├── metadata.json
│   ├── server.ts
│   └── streamable-http-server.ts
├── test
│   ├── _utils
│   │   ├── httpClient.js
│   │   └── parseResults.js
│   ├── community-search.ts
│   ├── comprehensive-url-generation.test.ts
│   ├── performance
│   │   └── README.md
│   ├── prompts.test.ts
│   ├── quick-url-test.ts
│   ├── README.md
│   ├── tools
│   │   ├── run-tests.js
│   │   ├── sap_docs_search
│   │   │   ├── search-cap-docs.js
│   │   │   ├── search-cloud-sdk-ai.js
│   │   │   ├── search-cloud-sdk-js.js
│   │   │   └── search-sapui5-docs.js
│   │   ├── search-url-verification.js
│   │   ├── search.generic.spec.js
│   │   └── search.smoke.js
│   ├── url-status.ts
│   └── validate-urls.ts
├── test-community-search.js
├── test-search-interactive.ts
├── test-search.http
├── test-search.ts
├── tsconfig.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/lib/localDocs.ts:
--------------------------------------------------------------------------------

```typescript
   1 | // src/lib/localDocs.ts
   2 | import fs from "fs/promises";
   3 | import { existsSync } from "fs";
   4 | import path from "path";
   5 | import { fileURLToPath } from "url";
   6 | import { SearchResponse, SearchResult } from "./types.js";
   7 | import { getFTSCandidateIds, getFTSStats } from "./searchDb.js";
   8 | import { searchCommunityBestMatch, getCommunityPostByUrl, getCommunityPostById, getCommunityPostsByIds, searchAndGetTopPosts, BestMatchHit } from "./communityBestMatch.js";
   9 | 
  10 | import { 
  11 |   getDocUrlConfig, 
  12 |   getAllDocUrlConfigs, 
  13 |   getSourcePath, 
  14 |   getAllContextBoosts, 
  15 |   getContextBoosts, 
  16 |   getAllContextEmojis,
  17 |   getContextEmoji,
  18 |   type DocUrlConfig 
  19 | } from "./metadata.js";
  20 | import { generateDocumentationUrl as generateUrl } from "./url-generation/index.js";
  21 | 
  22 | // Use the new URL generation system
  23 | function generateDocumentationUrl(libraryId: string, relFile: string, content: string): string | null {
  24 |   const config = getDocUrlConfig(libraryId);
  25 |   if (!config) {
  26 |     return null;
  27 |   }
  28 | 
  29 |   return generateUrl(libraryId, relFile, content, config);
  30 | }
  31 | 
  32 | // Get the directory of this script and find the project root
  33 | const __filename = fileURLToPath(import.meta.url);
  34 | const __dirname = path.dirname(__filename);
  35 | 
  36 | // Find project root by looking for package.json
  37 | let PROJECT_ROOT = __dirname;
  38 | while (PROJECT_ROOT !== path.dirname(PROJECT_ROOT)) {
  39 |   try {
  40 |     if (existsSync(path.join(PROJECT_ROOT, 'package.json'))) {
  41 |       break;
  42 |     }
  43 |   } catch {
  44 |     // Continue searching
  45 |   }
  46 |   PROJECT_ROOT = path.dirname(PROJECT_ROOT);
  47 | }
  48 | 
  49 | // Fallback: assume dist structure
  50 | if (!existsSync(path.join(PROJECT_ROOT, 'package.json'))) {
  51 |   PROJECT_ROOT = path.resolve(__dirname, "../../..");
  52 | }
  53 | 
  54 | const DATA_DIR = path.join(PROJECT_ROOT, "dist", "data");
  55 | 
  56 | // Note: SAP Community search now uses HTML scraping via communityBestMatch.ts
  57 | 
  58 | // Search SAP Community for relevant posts using HTML scraping
  59 | async function searchSAPCommunity(query: string): Promise<SearchResult[]> {
  60 |   try {
  61 |     const hits: BestMatchHit[] = await searchCommunityBestMatch(query, {
  62 |       includeBlogs: true,
  63 |       limit: 20,
  64 |       userAgent: 'SAP-Docs-MCP/1.0'
  65 |     });
  66 | 
  67 |     return hits.map(hit => ({
  68 |       library_id: hit.postId ? `community-${hit.postId}` : `community-url-${encodeURIComponent(hit.url)}`,
  69 |       topic: '',
  70 |       id: hit.postId ? `community-${hit.postId}` : `community-url-${encodeURIComponent(hit.url)}`,
  71 |       title: hit.title,
  72 |       url: hit.url,
  73 |       snippet: hit.snippet || '',
  74 |       score: 0,
  75 |       metadata: {
  76 |         source: 'community',
  77 |         postTime: hit.published,
  78 |         author: hit.author,
  79 |         likes: hit.likes,
  80 |         tags: hit.tags,
  81 |         totalSnippets: 1
  82 |       },
  83 |       // Legacy fields for backward compatibility
  84 |       description: hit.snippet || '',
  85 |       totalSnippets: 1,
  86 |       source: 'community',
  87 |       postTime: hit.published,
  88 |       author: hit.author,
  89 |       likes: hit.likes,
  90 |       tags: hit.tags
  91 |     }));
  92 |   } catch (error) {
  93 |     console.warn('Failed to search SAP Community:', error);
  94 |     return [];
  95 |   }
  96 | }
  97 | 
  98 | // Get full content of a community post using LiQL API
  99 | async function getCommunityPost(postId: string): Promise<string | null> {
 100 |   try {
 101 |     // Handle both postId formats: "community-postId" and "community-url-encodedUrl"
 102 |     if (postId.startsWith('community-url-')) {
 103 |       // Extract URL from encoded format and fall back to URL scraping
 104 |       const encodedUrl = postId.replace('community-url-', '');
 105 |       const postUrl = decodeURIComponent(encodedUrl);
 106 |       return await getCommunityPostByUrl(postUrl, 'SAP-Docs-MCP/1.0');
 107 |     } else {
 108 |       // For standard post IDs, use the efficient LiQL API
 109 |       const numericId = postId.replace('community-', '');
 110 |       return await getCommunityPostById(numericId, 'SAP-Docs-MCP/1.0');
 111 |     }
 112 |   } catch (error) {
 113 |     console.warn('Failed to get community post:', error);
 114 |     return null;
 115 |   }
 116 | }
 117 | 
 118 | // Helper function to parse document ID into library_id and topic
 119 | function parseDocumentId(docId: string): { libraryId: string; topic: string } {
 120 |   // docId formats:
 121 |   // - "/cap" -> libraryId="/cap", topic=""
 122 |   // - "/cap/guides/domain-modeling#compositions" -> libraryId="/cap", topic="guides/domain-modeling#compositions"
 123 |   // - "/openui5-api/sap/m/Button" -> libraryId="/openui5-api", topic="sap/m/Button"
 124 |   
 125 |   // Find the first slash after the initial slash
 126 |   const match = docId.match(/^(\/[^\/]+)(?:\/(.*))?$/);
 127 |   if (match) {
 128 |     const [, libraryId, topic = ""] = match;
 129 |     return { libraryId, topic };
 130 |   }
 131 |   
 132 |   // Fallback: treat the whole thing as library_id
 133 |   return { libraryId: docId, topic: "" };
 134 | }
 135 | 
 136 | // Helper function to format search result entries with clear library_id and topic
 137 | function formatSearchResultEntry(result: any, queryContext: string): string {
 138 |   const { libraryId, topic } = parseDocumentId(result.docId);
 139 |   const score = result.score.toFixed(0);
 140 |   const description = result.docDescription.substring(0, 100);
 141 |   const contextEmoji = getContextEmoji(queryContext);
 142 |   
 143 |   let formatted = `${contextEmoji} **${result.docTitle}** (Score: ${score})\n`;
 144 |   formatted += `   📄 ${description}${description.length >= 100 ? '...' : ''}\n`;
 145 |   formatted += `   📂 Library: \`${libraryId}\`\n`;
 146 |   
 147 |   if (topic) {
 148 |     formatted += `   🎯 Topic: \`${topic}\`\n`;
 149 |     formatted += `   ✅ **Call:** \`fetch(id="${libraryId}", topic="${topic}")\`\n\n`;
 150 |   } else {
 151 |     formatted += `   ✅ **Call:** \`fetch(id="${libraryId}")\`\n\n`;
 152 |   }
 153 |   
 154 |   return formatted;
 155 | }
 156 | 
 157 | // Format JavaScript content for better readability in documentation context
 158 | function formatJSDocContent(content: string, controlName: string): string {
 159 |   const lines = content.split('\n');
 160 |   const result: string[] = [];
 161 |   
 162 |   result.push(`# ${controlName} - OpenUI5 Control API`);
 163 |   result.push('');
 164 |   
 165 |   // Extract main JSDoc comment
 166 |   const mainJSDocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
 167 |   if (mainJSDocMatch) {
 168 |     const cleanDoc = mainJSDocMatch[1]
 169 |       .split('\n')
 170 |       .map(line => line.replace(/^\s*\*\s?/, ''))
 171 |       .join('\n')
 172 |       .trim();
 173 |     
 174 |     result.push('## Description');
 175 |     result.push('');
 176 |     result.push(cleanDoc);
 177 |     result.push('');
 178 |   }
 179 |   
 180 |   // Extract metadata section
 181 |   const metadataMatch = content.match(/metadata\s*:\s*\{([\s\S]*?)\n\s*\}/);
 182 |   if (metadataMatch) {
 183 |     result.push('## Control Metadata');
 184 |     result.push('');
 185 |     result.push('```javascript');
 186 |     result.push('metadata: {');
 187 |     result.push(metadataMatch[1]);
 188 |     result.push('}');
 189 |     result.push('```');
 190 |     result.push('');
 191 |   }
 192 |   
 193 |   // Extract properties
 194 |   const propertiesMatch = content.match(/properties\s*:\s*\{([\s\S]*?)\n\s*\}/);
 195 |   if (propertiesMatch) {
 196 |     result.push('## Properties');
 197 |     result.push('');
 198 |     result.push('```javascript');
 199 |     result.push(propertiesMatch[1]);
 200 |     result.push('```');
 201 |     result.push('');
 202 |   }
 203 |   
 204 |   // Extract events
 205 |   const eventsMatch = content.match(/events\s*:\s*\{([\s\S]*?)\n\s*\}/);
 206 |   if (eventsMatch) {
 207 |     result.push('## Events');
 208 |     result.push('');
 209 |     result.push('```javascript');
 210 |     result.push(eventsMatch[1]);
 211 |     result.push('```');
 212 |     result.push('');
 213 |   }
 214 |   
 215 |   // Extract aggregations
 216 |   const aggregationsMatch = content.match(/aggregations\s*:\s*\{([\s\S]*?)\n\s*\}/);
 217 |   if (aggregationsMatch) {
 218 |     result.push('## Aggregations');
 219 |     result.push('');
 220 |     result.push('```javascript');
 221 |     result.push(aggregationsMatch[1]);
 222 |     result.push('```');
 223 |     result.push('');
 224 |   }
 225 |   
 226 |   // Extract associations
 227 |   const associationsMatch = content.match(/associations\s*:\s*\{([\s\S]*?)\n\s*\}/);
 228 |   if (associationsMatch) {
 229 |     result.push('## Associations');
 230 |     result.push('');
 231 |     result.push('```javascript');
 232 |     result.push(associationsMatch[1]);
 233 |     result.push('```');
 234 |     result.push('');
 235 |   }
 236 |   
 237 |   result.push('---');
 238 |   result.push('');
 239 |   result.push('### Full Source Code');
 240 |   result.push('');
 241 |   result.push('```javascript');
 242 |   result.push(content);
 243 |   result.push('```');
 244 |   
 245 |   return result.join('\n');
 246 | }
 247 | 
 248 | // Format sample content for better readability in documentation context
 249 | function formatSampleContent(content: string, filePath: string, title: string): string {
 250 |   const fileExt = path.extname(filePath);
 251 |   const controlName = title.split(' ')[0]; // Extract control name from title
 252 |   const fileName = path.basename(filePath);
 253 |   
 254 |   const result: string[] = [];
 255 |   
 256 |   result.push(`# ${title}`);
 257 |   result.push('');
 258 |   
 259 |   // Add file information
 260 |   result.push(`## File Information`);
 261 |   result.push(`- **File**: \`${fileName}\``);
 262 |   result.push(`- **Type**: ${fileExt.slice(1).toUpperCase()} ${getFileTypeDescription(fileExt)}`);
 263 |   result.push(`- **Control**: ${controlName}`);
 264 |   result.push('');
 265 |   
 266 |   // Add description based on file type
 267 |   if (fileExt === '.js') {
 268 |     if (content.toLowerCase().includes('controller')) {
 269 |       result.push(`## Controller Implementation`);
 270 |       result.push(`This file contains the controller logic for the ${controlName} sample.`);
 271 |     } else if (content.toLowerCase().includes('component')) {
 272 |       result.push(`## Component Definition`);
 273 |       result.push(`This file defines the application component for the ${controlName} sample.`);
 274 |     } else {
 275 |       result.push(`## JavaScript Implementation`);
 276 |       result.push(`This file contains JavaScript code for the ${controlName} sample.`);
 277 |     }
 278 |   } else if (fileExt === '.xml') {
 279 |     result.push(`## XML View`);
 280 |     result.push(`This file contains the XML view definition for the ${controlName} sample.`);
 281 |   } else if (fileExt === '.json') {
 282 |     result.push(`## JSON Configuration`);
 283 |     if (fileName.includes('manifest')) {
 284 |       result.push(`This file contains the application manifest configuration.`);
 285 |     } else {
 286 |       result.push(`This file contains JSON configuration or sample data.`);
 287 |     }
 288 |   } else if (fileExt === '.html') {
 289 |     result.push(`## HTML Page`);
 290 |     result.push(`This file contains the HTML page setup for the ${controlName} sample.`);
 291 |   }
 292 |   
 293 |   result.push('');
 294 |   
 295 |   // Add the actual content with syntax highlighting
 296 |   result.push(`## Source Code`);
 297 |   result.push('');
 298 |   result.push(`\`\`\`${getSyntaxHighlighting(fileExt)}`);
 299 |   result.push(content);
 300 |   result.push('```');
 301 |   
 302 |   // Add usage tips
 303 |   result.push('');
 304 |   result.push('## Usage Tips');
 305 |   result.push('');
 306 |   if (fileExt === '.js' && content.includes('onPress')) {
 307 |     result.push('- This sample includes event handlers (onPress methods)');
 308 |   }
 309 |   if (fileExt === '.xml' && content.includes('{')) {
 310 |     result.push('- This view uses data binding patterns');
 311 |   }
 312 |   if (content.toLowerCase().includes('model')) {
 313 |     result.push('- This sample demonstrates model usage');
 314 |   }
 315 |   if (content.toLowerCase().includes('router') || content.toLowerCase().includes('routing')) {
 316 |     result.push('- This sample includes routing configuration');
 317 |   }
 318 |   
 319 |   return result.join('\n');
 320 | }
 321 | 
 322 | function getFileTypeDescription(ext: string): string {
 323 |   switch (ext) {
 324 |     case '.js': return 'JavaScript';
 325 |     case '.xml': return 'XML View';
 326 |     case '.json': return 'JSON Configuration';
 327 |     case '.html': return 'HTML Page';
 328 |     default: return 'File';
 329 |   }
 330 | }
 331 | 
 332 | function getSyntaxHighlighting(ext: string): string {
 333 |   switch (ext) {
 334 |     case '.js': return 'javascript';
 335 |     case '.xml': return 'xml';
 336 |     case '.json': return 'json';
 337 |     case '.html': return 'html';
 338 |     default: return 'text';
 339 |   }
 340 | }
 341 | 
 342 | type LibraryBundle = {
 343 |   id: string;
 344 |   name: string;
 345 |   description: string;
 346 |   docs: {
 347 |     id: string;
 348 |     title: string;
 349 |     description: string;
 350 |     snippetCount: number;
 351 |     relFile: string;
 352 |   }[];
 353 | };
 354 | 
 355 | let INDEX: Record<string, LibraryBundle> | null = null;
 356 | 
 357 | async function loadIndex() {
 358 |   if (!INDEX) {
 359 |     const raw = await fs.readFile(path.join(DATA_DIR, "index.json"), "utf8");
 360 |     INDEX = JSON.parse(raw) as Record<string, LibraryBundle>;
 361 |   }
 362 |   return INDEX;
 363 | }
 364 | 
 365 | // Utility: Expand query with synonyms and related terms
 366 | function expandQuery(query: string): string[] {
 367 |   const synonyms: Record<string, string[]> = {
 368 |     // === UI5 CONTROL TERMS ===
 369 |     // Wizard-related terms (UI5 only)
 370 |     wizard: ["wizard", "sap.m.Wizard", "WizardStep", "sap.m.WizardStep", "WizardRenderMode", 
 371 |              "sap.ui.webc.fiori.IWizardStep", "sap.ui.webc.fiori.WizardContentLayout", 
 372 |              "wizard control", "wizard fragment", "wizard step", "step wizard", "multi-step"],
 373 |     
 374 |     // Button-related terms (UI5 + general)
 375 |     button: ["button", "sap.m.Button", "sap.ui.webc.main.Button", "button control", 
 376 |              "button press", "action button", "toggle button", "click", "press event"],
 377 |     
 378 |     // Table-related terms (UI5 + CAP)
 379 |     table: ["table", "sap.m.Table", "sap.ui.table.Table", "sap.ui.table.TreeTable", 
 380 |             "table control", "table row", "data table", "tree table", "grid table",
 381 |             "entity", "table entity", "database table", "cds table"],
 382 |     
 383 |     // === CAP-SPECIFIC TERMS ===
 384 |     // CDS and modeling
 385 |     cds: ["cds", "Core Data Services", "cds model", "cds file", "schema", "data model",
 386 |           "entity", "service", "view", "type", "aspect", "composition", "association"],
 387 |     
 388 |     entity: ["entity", "cds entity", "data entity", "business entity", "table", 
 389 |              "model", "schema", "composition", "association", "key", "managed"],
 390 |     
 391 |     service: ["service", "cds service", "odata service", "rest service", "api", 
 392 |               "endpoint", "business service", "application service", "crud"],
 393 |     
 394 |     aspect: ["aspect", "cds aspect", "managed", "cuid", "audited", 
 395 |              "reuse aspect", "mixin", "common aspect"],
 396 |     
 397 |     temporal: ["temporal", "temporal data", "time slice", "valid from", "valid to", 
 398 |                "temporal entity", "temporal aspect", "time travel", "as-of-now"],
 399 |     
 400 |     annotation: ["annotation", "annotations", "@", "annotation file", "annotation target",
 401 |                  "UI annotation", "Common annotation", "Capabilities annotation", 
 402 |                  "odata annotation", "fiori annotation"],
 403 |     
 404 |     // CAP Authentication & Security  
 405 |     auth: ["authentication", "authorization", "auth", "security", "user", "role", 
 406 |            "scopes", "jwt", "oauth", "saml", "xsuaa", "ias", "passport", "login"],
 407 |     
 408 |     // CAP Database & Persistence
 409 |     database: ["database", "db", "hana", "sqlite", "postgres", "h2", "persistence",
 410 |                "connection", "schema", "migration", "deploy"],
 411 |     
 412 |     deployment: ["deployment", "deploy", "cf push", "cloud foundry", "kubernetes", 
 413 |                  "helm", "docker", "mta", "build", "production"],
 414 |     
 415 |     // === WDI5-SPECIFIC TERMS ===
 416 |     testing: ["testing", "test", "e2e", "end-to-end", "integration test", "ui test",
 417 |               "browser test", "selenium", "webdriver", "automation", "test framework"],
 418 |     
 419 |     wdi5: ["wdi5", "webdriver", "ui5 testing", "browser automation", "page object", 
 420 |            "test framework", "selector", "locator", "element", "assertion", "wdio",
 421 |            "ui5 test api", "sap.ui.test", "test library", "fe-testlib"],
 422 |     
 423 |     selector: ["selector", "locator", "element", "control selector", "id", "property",
 424 |                "binding", "aggregation", "wdi5 selector", "ui5 control", "matcher",
 425 |                "sap.ui.test.matchers", "byId", "byProperty", "byBinding"],
 426 |     
 427 |     browser: ["browser", "chrome", "firefox", "safari", "webdriver", "headless",
 428 |               "viewport", "screenshot", "debugging", "browser automation"],
 429 |     
 430 |     pageobject: ["page object", "pageobject", "page objects", "test structure", 
 431 |                  "test organization", "test patterns", "ui5 test patterns"],
 432 |     
 433 |     // === CROSS-PLATFORM TERMS ===
 434 |     // Navigation & Routing (UI5 + CAP)
 435 |     routing: ["routing", "router", "navigation", "route", "target", "pattern",
 436 |               "manifest routing", "app router", "destination"],
 437 |     
 438 |     // Forms (UI5 + CAP + wdi5)
 439 |     form: ["form", "sap.ui.layout.form.Form", "sap.ui.layout.form.SimpleForm",
 440 |            "form control", "form layout", "smart form", "input validation", 
 441 |            "form testing"],
 442 |     
 443 |     // Data & Models (UI5 + CAP)
 444 |     model: ["model", "data model", "json model", "odata model", "entity model",
 445 |             "binding", "property binding", "aggregation binding"],
 446 |     
 447 |     // Fiori & Elements
 448 |     fiori: ["fiori", "fiori elements", "list report", "object page", "overview page",
 449 |             "analytical list page", "worklist", "freestyle fiori", "fiori launchpad"],
 450 |     
 451 |     // Development & Tools
 452 |     development: ["development", "dev", "local", "debugging", "console", "devtools",
 453 |                   "hot reload", "live reload", "build", "compile"]
 454 |   };
 455 |   
 456 |   const q = query.toLowerCase().trim();
 457 |   
 458 |   // Check for exact matches first
 459 |   for (const [key, values] of Object.entries(synonyms)) {
 460 |     if (q === key || values.some(v => v.toLowerCase() === q)) {
 461 |       return values;
 462 |     }
 463 |   }
 464 |   
 465 |   // Generic approach: Build smart query variations with term prioritization
 466 |   const queryTerms = q.toLowerCase().split(/\s+/);
 467 |   const importantMatches: string[] = [];
 468 |   const supplementaryMatches: string[] = [];
 469 |   let hasSpecificMatch = false;
 470 |   
 471 |   for (const [key, values] of Object.entries(synonyms)) {
 472 |     // Check if query contains this key term
 473 |     const keyLower = key.toLowerCase();
 474 |     // Avoid substring false-positives for certain keys (e.g., 'form' in 'information')
 475 |     const wholeWordMatch = keyLower === 'form' ? /\bforms?\b/.test(q) : q.includes(keyLower);
 476 |     if (wholeWordMatch || values.some(v => q.includes(v.toLowerCase()))) {
 477 |       // If the key term is a major word in the query, prioritize it
 478 |       const isMajorWord = queryTerms.includes(keyLower) || queryTerms.some(term => term.length > 3 && keyLower.includes(term));
 479 |       if (isMajorWord) {
 480 |         importantMatches.unshift(...values);
 481 |         hasSpecificMatch = true;
 482 |       } else {
 483 |         // Minor/partial matches go to supplementary
 484 |         supplementaryMatches.push(...values);
 485 |       }
 486 |     }
 487 |   }
 488 |   
 489 |   if (importantMatches.length > 0 || supplementaryMatches.length > 0) {
 490 |     // Always start with original query variations, then important matches, then supplementary
 491 |     const result = [
 492 |       query, // Original exact query first
 493 |       query.toLowerCase(),
 494 |       ...new Set(importantMatches), // Important domain-specific terms
 495 |       ...new Set(supplementaryMatches.slice(0, 5)) // Limit supplementary to avoid pollution
 496 |     ];
 497 |     return result;
 498 |   }
 499 |   
 500 |   // Handle common UI5 control patterns
 501 |   const ui5ControlPattern = /^sap\.[a-z]+\.[A-Z][a-zA-Z0-9]*$/;
 502 |   if (ui5ControlPattern.test(query)) {
 503 |     const controlName = query.split('.').pop()!;
 504 |     return [query, controlName, controlName.toLowerCase(), `${controlName} control`];
 505 |   }
 506 |   
 507 |   // Generate contextual variations based on query type
 508 |   const variations = [query, query.toLowerCase()];
 509 | 
 510 |   // Add technology-specific variations
 511 |   if (q.includes('cap') || q.includes('cds')) {
 512 |     variations.push('CAP', 'cds', 'Core Data Services', 'service', 'entity');
 513 |   }
 514 |   if (q.includes('wdi5') || q.includes('test') || q.includes('testing') || q.includes('e2e')) {
 515 |     variations.push('wdi5', 'testing', 'e2e', 'webdriver', 'ui5 testing', 'wdio', 'pageobject', 'selector', 'locator');
 516 |   }
 517 |   if ((q.includes('ui5') || q.includes('sap.')) && !q.includes('web components') && !q.includes('web component')) {
 518 |     variations.push('UI5', 'SAPUI5', 'OpenUI5', 'control', 'Fiori');
 519 |   }
 520 |   if (q.includes('web components') || q.includes('ui5-') || q.includes('@ui5/') || q.includes('web component')) {
 521 |     variations.push('UI5 Web Components', 'Web Components', 'Web Component', 'component');
 522 |   }
 523 |   if (q.includes('sdk') || q.includes('cloud sdk')) {
 524 |     variations.push('SAP Cloud SDK', 'SAP Cloud SDK for AI');
 525 |   }
 526 |   if ((q.includes('ui5') && q.includes('tooling')) || (q.includes('ui5') && q.includes('cli'))) {
 527 |     variations.push('UI5 Tooling', 'UI5 CLI', 'tasks', 'middleware', 'shims');
 528 |   }
 529 |   if (q.includes('mta') || q.includes('build') || q.includes('multitarget') || q.includes('mtar')) {
 530 |     variations.push('MTA', 'MTA Build', 'MTA Build Tool', 'Cloud MTA Build Tool', 'Multitarget Application', 'Multitarget Application Archive Builder', 'mtar');
 531 |   }
 532 | 
 533 |   // Add common variations
 534 |   variations.push(
 535 |     query.charAt(0).toUpperCase() + query.slice(1).toLowerCase(),
 536 |     query.replace(/[_-]/g, ' '),
 537 |     query.replace(/\s+/g, ''),
 538 |     ...query.split(/[_\s-]/).filter(part => part.length > 2)
 539 |   );
 540 |   
 541 |   return [...new Set(variations)];
 542 | }
 543 | 
 544 | // Utility: Extract control names/properties from file content (XML/JS)
 545 | function extractControlsFromContent(content: string): string[] {
 546 |   const controls = new Set<string>();
 547 |   
 548 |   // XML: <sap.m.Wizard ...> or <Wizard ...>
 549 |   const xmlMatches = content.matchAll(/<([a-zA-Z0-9_.:]+)[\s>]/g);
 550 |   for (const m of xmlMatches) {
 551 |     let tag = m[1];
 552 |     if (tag.includes(":")) tag = tag.split(":").pop()!;
 553 |     controls.add(tag);
 554 |   }
 555 |   
 556 |   // JS: Enhanced pattern matching for all UI5 namespaces
 557 |   const sapNamespaces = ['sap.m', 'sap.ui', 'sap.f', 'sap.tnt', 'sap.suite', 'sap.viz', 'sap.uxap'];
 558 |   for (const namespace of sapNamespaces) {
 559 |     const pattern = new RegExp(`${namespace.replace('.', '\\.')}\.([A-Za-z0-9_]+)`, 'g');
 560 |     const matches = content.matchAll(pattern);
 561 |     for (const m of matches) {
 562 |       controls.add(`${namespace}.${m[1]}`);
 563 |       controls.add(m[1]); // Also add just the control name
 564 |     }
 565 |   }
 566 |   
 567 |   // Extract from extend() calls
 568 |   const extendMatches = content.matchAll(/\.extend\s*\(\s*["']([^"']+)["']/g);
 569 |   for (const m of extendMatches) {
 570 |     controls.add(m[1]);
 571 |     const controlName = m[1].split('.').pop();
 572 |     if (controlName) controls.add(controlName);
 573 |   }
 574 |   
 575 |   // Extract control names from metadata
 576 |   const metadataMatch = content.match(/metadata\s*:\s*\{[\s\S]*?library\s*:\s*["']([^"']+)["'][\s\S]*?\}/);
 577 |   if (metadataMatch) {
 578 |     controls.add(metadataMatch[1]);
 579 |   }
 580 |   
 581 |   return Array.from(controls);
 582 | }
 583 | 
 584 | // Utility: Calculate similarity score between strings
 585 | function calculateSimilarity(str1: string, str2: string): number {
 586 |   const s1 = str1.toLowerCase();
 587 |   const s2 = str2.toLowerCase();
 588 |   
 589 |   // Exact match
 590 |   if (s1 === s2) return 100;
 591 |   
 592 |   // One contains the other
 593 |   if (s1.includes(s2) || s2.includes(s1)) return 90;
 594 |   
 595 |   // Levenshtein distance for fuzzy matching
 596 |   const matrix = Array(s2.length + 1).fill(null).map(() => Array(s1.length + 1).fill(null));
 597 |   
 598 |   for (let i = 0; i <= s1.length; i++) matrix[0][i] = i;
 599 |   for (let j = 0; j <= s2.length; j++) matrix[j][0] = j;
 600 |   
 601 |   for (let j = 1; j <= s2.length; j++) {
 602 |     for (let i = 1; i <= s1.length; i++) {
 603 |       const indicator = s1[i - 1] === s2[j - 1] ? 0 : 1;
 604 |       matrix[j][i] = Math.min(
 605 |         matrix[j][i - 1] + 1,     // deletion
 606 |         matrix[j - 1][i] + 1,     // insertion
 607 |         matrix[j - 1][i - 1] + indicator // substitution
 608 |       );
 609 |     }
 610 |   }
 611 |   
 612 |   const distance = matrix[s2.length][s1.length];
 613 |   const maxLength = Math.max(s1.length, s2.length);
 614 |   return Math.max(0, (maxLength - distance) / maxLength * 100);
 615 | }
 616 | 
 617 | // Utility: Enhanced control matching with fuzzy matching
 618 | function isControlMatch(controlName: string, query: string): boolean {
 619 |   const name = controlName.toLowerCase();
 620 |   const q = query.toLowerCase();
 621 |   
 622 |   // Exact match
 623 |   if (name === q) return true;
 624 |   
 625 |   // Direct contains
 626 |   if (name.includes(q)) return true;
 627 |   
 628 |   // Check if query matches the control class name
 629 |   const controlParts = name.split('.');
 630 |   const lastPart = controlParts[controlParts.length - 1];
 631 |   if (lastPart && (lastPart === q || lastPart.includes(q))) return true;
 632 |   
 633 |   // Check for fuzzy similarity (threshold: 70%)
 634 |   if (calculateSimilarity(name, q) > 70) return true;
 635 |   if (lastPart && calculateSimilarity(lastPart, q) > 70) return true;
 636 |   
 637 |   // Check for common UI5 control patterns with synonyms
 638 |   const controlKeywords = [
 639 |     'wizard', 'button', 'table', 'input', 'list', 'panel', 'dialog', 'form',
 640 |     'navigation', 'layout', 'chart', 'page', 'app', 'shell', 'toolbar', 'menu',
 641 |     'container', 'text', 'label', 'image', 'card', 'tile', 'icon', 'bar',
 642 |     'picker', 'select', 'switch', 'slider', 'progress', 'busy', 'message',
 643 |     'notification', 'popover', 'tooltip', 'breadcrumb', 'rating', 'feed',
 644 |     'upload', 'calendar', 'date', 'time', 'color', 'file', 'search'
 645 |   ];
 646 |   
 647 |   // Check if query is a control keyword and the control name contains it
 648 |   if (controlKeywords.includes(q)) {
 649 |     return controlKeywords.some(kw => name.includes(kw));
 650 |   }
 651 |   
 652 |   return false;
 653 | }
 654 | 
 655 | // Determine the primary context of a query for smart filtering
 656 | function determineQueryContext(originalQuery: string, expandedQueries: string[]): string {
 657 |   const q = originalQuery.toLowerCase();
 658 |   const allQueries = [originalQuery, ...expandedQueries].map(s => s.toLowerCase());
 659 |   const contextScores: { context: string, score: number }[] = [];
 660 |   
 661 |   // Check for UI5 annotation qualifiers (e.g., "UI.Chart #SpecificationWidthColumnChart")
 662 |   // These should be treated as UI5/SAPUI5 context, not wdi5/testing context
 663 |   const isAnnotationQualifier = /UI\.\w+\s*#\w+|@UI\.\w+|UI\s+\w+\s*#|annotation.*#/.test(originalQuery);
 664 |   
 665 |   // CAP context indicators
 666 |   const capIndicators = ['cds', 'cap', 'entity', 'service', 'aspect', 'annotation', 'odata', 'hana'];
 667 |   const capScore = capIndicators.filter(term => 
 668 |     allQueries.some(query => query.includes(term))
 669 |   ).length;
 670 |   contextScores.push({ context: 'CAP', score: capScore });
 671 |   
 672 |   // wdi5 context indicators - but reduce weight if this looks like an annotation qualifier
 673 |   const wdi5Indicators = ['wdi5', 'test', 'testing', 'e2e', 'browser', 'webdriver', 'selenium', 'automation', 'wdio', 'pageobject', 'selector', 'locator', 'assertion', 'fe-testlib'];
 674 |   let wdi5Score = wdi5Indicators.filter(term => 
 675 |     allQueries.some(query => query.includes(term))
 676 |   ).length;
 677 |   // Reduce wdi5 score if this looks like an annotation qualifier
 678 |   if (isAnnotationQualifier) {
 679 |     wdi5Score = Math.max(0, wdi5Score - 2); // Reduce by 2 to counter false positives from 'selector'
 680 |   }
 681 |   contextScores.push({ context: 'wdi5', score: wdi5Score });
 682 | 
 683 |   // UI5 context indicators - boost score if this looks like an annotation qualifier
 684 |   const ui5Indicators = ['sap.m', 'sap.ui', 'sap.f', 'control', 'wizard', 'button', 'table', 'fiori', 'ui5', 'sapui5', 'chart', 'micro'];
 685 |   let ui5Score = ui5Indicators.filter(term => 
 686 |     allQueries.some(query => query.includes(term))
 687 |   ).length;
 688 |   // Boost UI5 score for annotation qualifiers since they're typically UI5/Fiori Elements
 689 |   if (isAnnotationQualifier) {
 690 |     ui5Score += 2;
 691 |   }
 692 |   contextScores.push({ context: 'UI5', score: ui5Score });
 693 | 
 694 |   // UI5 Web Components context indicators
 695 |   const ui5WebComponentsIndicators = ['ui5 web components','web components','ui5-', '@ui5/', 'web-component', 'web-components', 'component'];
 696 |   const ui5WebComponentsScore = ui5WebComponentsIndicators.filter(term => 
 697 |     allQueries.some(query => query.includes(term))
 698 |   ).length;
 699 |   contextScores.push({ context: 'UI5 Web Components', score: ui5WebComponentsScore });
 700 | 
 701 |   // SAP Cloud SDK context indicators
 702 |   const sapCloudSdkIndicators = ['cloud sdk', 'sdk', 'cloud', 'sdk for ai', 'ai'];
 703 |   const sapCloudSdkScore = sapCloudSdkIndicators.filter(term => 
 704 |     allQueries.some(query => query.includes(term))
 705 |   ).length;
 706 |   contextScores.push({ context: 'SAP Cloud SDK', score: sapCloudSdkScore });
 707 | 
 708 |   // UI5 Tooling context indicators
 709 |   const ui5ToolingIndicators = ['ui5', 'ui5-', '@ui5/', 'tooling', 'cli', 'tasks', 'middleware', 'shims'];
 710 |   const ui5ToolingScore = ui5ToolingIndicators.filter(term => 
 711 |     allQueries.some(query => query.includes(term))
 712 |   ).length;
 713 |   contextScores.push({ context: 'UI5 Tooling', score: ui5ToolingScore });
 714 | 
 715 |   // Cloud MTA Build Tool context indicators
 716 |   const mtaIndicators = ['mta', 'mtar', 'multitarget', 'build', 'cloud mta build tool'];
 717 |   const mtaScore = mtaIndicators.filter(term => 
 718 |     allQueries.some(query => query.includes(term))
 719 |   ).length;
 720 |   contextScores.push({ context: 'Cloud MTA Build Tool', score: mtaScore });
 721 | 
 722 |   // Sort by score and return the strongest context, if the first two are the same, return 'MIXED'
 723 |   contextScores.sort((a, b) => b.score - a.score);
 724 |   if (contextScores[0].score === contextScores[1].score) return 'MIXED';
 725 |   return contextScores[0].context;
 726 |   
 727 |   // Return strongest context
 728 |   /*if (capScore > 0 && capScore >= wdi5Score && capScore >= ui5Score && capScore >= ui5WebComponentsScore) return 'CAP';
 729 |   if (wdi5Score > 0 && wdi5Score >= capScore && wdi5Score >= ui5Score && wdi5Score >= ui5WebComponentsScore) return 'wdi5';
 730 |   if (ui5Score > 0 && ui5Score >= ui5WebComponentsScore) return 'UI5';
 731 |   if (ui5WebComponentsScore > 0) return 'UI5 Web Components';
 732 |   
 733 |   return 'MIXED'; // No clear context*/
 734 | }
 735 | 
 736 | // Soft priors per library to bias scores by detected intent without hard-filtering
 737 | function computeLibraryPriors(queryContext: string): Record<string, number> {
 738 |   // Get baseline priors from metadata (all sources get 0.05 baseline)
 739 |   const allContextBoosts = getAllContextBoosts();
 740 |   const priors: Record<string, number> = {};
 741 |   
 742 |   // Initialize baseline priors for all known library IDs
 743 |   const allBoosts = allContextBoosts;
 744 |   const allLibraryIds = new Set<string>();
 745 |   Object.values(allBoosts).forEach(contextBoosts => {
 746 |     Object.keys(contextBoosts).forEach(libId => allLibraryIds.add(libId));
 747 |   });
 748 |   
 749 |   // Set baseline priors
 750 |   allLibraryIds.forEach(libId => {
 751 |     priors[libId] = 0.05;
 752 |   });
 753 | 
 754 |   // Apply context-specific boosts from metadata
 755 |   const contextBoosts = getContextBoosts(queryContext);
 756 |   Object.entries(contextBoosts).forEach(([libId, boost]) => {
 757 |     priors[libId] = Math.max(priors[libId] || 0, boost);
 758 |   });
 759 | 
 760 |   return priors;
 761 | }
 762 | 
 763 | // Apply context-aware penalties/boosts
 764 | function applyContextPenalties(score: number, libraryId: string, queryContext: string, query: string): number {
 765 |   const q = query.toLowerCase();
 766 |   
 767 |   // Strong penalties for off-context matches
 768 |   if (queryContext === 'CAP') {
 769 |     if (libraryId === '/openui5-api' || libraryId === '/openui5-samples') {
 770 |       // Penalize UI5 results for CAP queries unless they're integration-related
 771 |       if (!q.includes('ui5') && !q.includes('fiori') && !q.includes('integration')) {
 772 |         score *= 0.3; // 70% penalty
 773 |       }
 774 |     }
 775 |     if (libraryId === '/wdi5') {
 776 |       score *= 0.5; // 50% penalty for wdi5 on CAP queries
 777 |     }
 778 |   } else if (queryContext === 'wdi5') {
 779 |     if (libraryId === '/openui5-api' || libraryId === '/openui5-samples') {
 780 |       // Heavily penalize UI5 API for wdi5 queries unless testing-related
 781 |       if (!q.includes('testing') && !q.includes('test') && !q.includes('ui5')) {
 782 |         score *= 0.2; // 80% penalty
 783 |       }
 784 |     }
 785 |     if (libraryId === '/cap') {
 786 |       score *= 0.4; // 60% penalty for CAP on wdi5 queries
 787 |     }
 788 |   } else if (queryContext === 'UI5') {
 789 |     if (libraryId === '/cap') {
 790 |       // Penalize CAP for pure UI5 queries unless integration-related
 791 |       if (!q.includes('service') && !q.includes('odata') && !q.includes('backend')) {
 792 |         score *= 0.4; // 60% penalty
 793 |       }
 794 |     }
 795 |     if (libraryId === '/wdi5') {
 796 |       // Penalize wdi5 for UI5 queries unless testing-related
 797 |       if (!q.includes('test') && !q.includes('testing')) {
 798 |         score *= 0.3; // 70% penalty
 799 |       }
 800 |     }
 801 |   }
 802 |   
 803 |   return score;
 804 | }
 805 | 
 806 | export async function searchLibraries(query: string, fileContent?: string): Promise<SearchResponse> {
 807 |   const index = await loadIndex();
 808 |   let queries = expandQuery(query);
 809 |   
 810 |   // Generic query prioritization: ensure original user query is always first and most important
 811 |   const originalQuery = query.trim();
 812 |   const lowercaseQuery = originalQuery.toLowerCase();
 813 |   
 814 |   // Remove duplicates and ensure original query variants come first
 815 |   queries = [
 816 |     originalQuery,                    // User's exact query (highest priority)
 817 |     lowercaseQuery,                   // Lowercase version
 818 |     ...queries.filter(q => q !== originalQuery && q !== lowercaseQuery)
 819 |   ];
 820 | 
 821 |   // If file content is provided, extract controls/properties and add to queries
 822 |   if (fileContent) {
 823 |     const extracted = extractControlsFromContent(fileContent);
 824 |     queries = [...new Set([...queries, ...extracted])];
 825 |   }
 826 | 
 827 |   let allMatches: Array<any> = [];
 828 |   let triedQueries: string[] = [];
 829 | 
 830 |   // Determine query context for smart filtering
 831 |   const queryContext = determineQueryContext(query, queries);
 832 |   
 833 |   // HYBRID FTS APPROACH: Use FTS for fast candidate filtering, then apply sophisticated scoring
 834 |   let candidateDocIds = new Set<string>();
 835 |   let usedFTS = false;
 836 |   
 837 |   try {
 838 |     // Prefer relevant libraries based on detected context
 839 |     const preferredLibs: string[] = [];
 840 |     if (queryContext === 'SAP Cloud SDK') {
 841 |       preferredLibs.push('/cloud-sdk-js','/cloud-sdk-java','/cloud-sdk-ai-js','/cloud-sdk-ai-java');
 842 |     } else if (queryContext === 'UI5') {
 843 |       preferredLibs.push('/sapui5','/openui5-api','/openui5-samples');
 844 |     } else if (queryContext === 'wdi5') {
 845 |       preferredLibs.push('/wdi5');
 846 |     } else if (queryContext === 'UI5 Web Components') {
 847 |       preferredLibs.push('/ui5-webcomponents');
 848 |     } else if (queryContext === 'UI5 Tooling') {
 849 |       preferredLibs.push('/ui5-tooling');
 850 |     } else if (queryContext === 'Cloud MTA Build Tool') {
 851 |       preferredLibs.push('/cloud-mta-build-tool');
 852 |     } else if (queryContext === 'CAP') {
 853 |       preferredLibs.push('/cap');
 854 |     }
 855 |     // Get FTS candidates with smart prioritization
 856 |     for (let i = 0; i < queries.length; i++) {
 857 |       const q = queries[i];
 858 |               // Higher limit for original/important queries, lower for supplementary
 859 |         const limit = i < 3 ? 200 : 80; // First 3 queries get more candidates
 860 |       
 861 |       const ftsResults = getFTSCandidateIds(q, { libraries: preferredLibs.length ? preferredLibs : undefined }, limit);
 862 |       
 863 |       if (ftsResults.length > 0) {
 864 |         // For original query (first), add all candidates
 865 |         // For others, prioritize candidates that aren't already included
 866 |         if (i === 0) {
 867 |           // Original query gets full priority
 868 |           ftsResults.forEach(id => candidateDocIds.add(id));
 869 |         } else {
 870 |           // Add new candidates, but don't overwhelm
 871 |           let added = 0;
 872 |           for (const id of ftsResults) {
 873 |             if (!candidateDocIds.has(id) && added < 50) {
 874 |               candidateDocIds.add(id);
 875 |               added++;
 876 |             }
 877 |           }
 878 |         }
 879 |         usedFTS = true;
 880 |       }
 881 |     }
 882 |     
 883 |     // If FTS found no candidates or failed, fall back to searching everything
 884 |     if (candidateDocIds.size === 0) {
 885 |       console.log("FTS found no candidates, falling back to full search");
 886 |       usedFTS = false;
 887 |     }
 888 |   } catch (error) {
 889 |     console.warn("FTS search failed, falling back to full search:", error);
 890 |     usedFTS = false;
 891 |   }
 892 |   
 893 |   // Score matches more comprehensively with context awareness
 894 |   for (const q of queries) {
 895 |     triedQueries.push(q);
 896 |     
 897 |     // Search across all documents in all libraries (filtered by FTS candidates if available)
 898 |     for (const lib of Object.values(index)) {
 899 |       // Check if library name/description matches
 900 |       const libNameSimilarity = calculateSimilarity(lib.name, q);
 901 |       const libDescSimilarity = calculateSimilarity(lib.description, q);
 902 |       
 903 |       if (libNameSimilarity > 60 || libDescSimilarity > 40) {
 904 |         allMatches.push({
 905 |           score: Math.max(libNameSimilarity, libDescSimilarity),
 906 |           libraryId: lib.id,
 907 |           libraryName: lib.name,
 908 |           docId: lib.id,
 909 |           docTitle: `${lib.name} (Full Library)`,
 910 |           docDescription: lib.description,
 911 |           matchType: libNameSimilarity > libDescSimilarity ? 'Library Name' : 'Library Description',
 912 |           snippetCount: lib.docs.reduce((s, d) => s + d.snippetCount, 0),
 913 |           source: 'docs'
 914 |         });
 915 |       }
 916 |       
 917 |       // Search within individual documents with enhanced scoring
 918 |       for (const doc of lib.docs) {
 919 |         // If we're using FTS filtering, only process documents that are in the candidate set
 920 |         if (usedFTS && !candidateDocIds.has(doc.id)) {
 921 |           // Also check if any section of this document is in the candidate set
 922 |           const hasSectionCandidate = Array.from(candidateDocIds).some(candidateId => 
 923 |             candidateId.startsWith(doc.id + '#')
 924 |           );
 925 |           if (!hasSectionCandidate) {
 926 |             continue;
 927 |           }
 928 |         }
 929 |         let score = 0;
 930 |         let matchType = '';
 931 |         
 932 |         // Calculate similarity scores for different aspects
 933 |         const titleSimilarity = calculateSimilarity(doc.title, q);
 934 |         const descSimilarity = calculateSimilarity(doc.description, q);
 935 |         
 936 |         // Check enhanced metadata fields if available (new index format)
 937 |         const docAny = doc as any;
 938 |         const controlName = docAny.controlName;
 939 |         const keywords = docAny.keywords || [];
 940 |         const properties = docAny.properties || [];
 941 |         const events = docAny.events || [];
 942 |         
 943 |         let keywordMatch = false;
 944 |         let controlNameMatch = false;
 945 |         let propertyMatch = false;
 946 |         
 947 |         // Check keyword matches
 948 |         if (keywords.length > 0) {
 949 |           keywordMatch = keywords.some((kw: string) => 
 950 |             kw.toLowerCase() === q.toLowerCase() || 
 951 |             kw.toLowerCase().includes(q.toLowerCase()) ||
 952 |             calculateSimilarity(kw.toLowerCase(), q.toLowerCase()) > 80
 953 |           );
 954 |         }
 955 |         
 956 |         // Check control name matches
 957 |         if (controlName) {
 958 |           controlNameMatch = controlName.toLowerCase() === q.toLowerCase() ||
 959 |                            controlName.toLowerCase().includes(q.toLowerCase()) ||
 960 |                            calculateSimilarity(controlName.toLowerCase(), q.toLowerCase()) > 80;
 961 |         }
 962 |         
 963 |         // Check property/event matches  
 964 |         if (properties.length > 0 || events.length > 0) {
 965 |           const allProps = [...properties, ...events];
 966 |           propertyMatch = allProps.some((prop: string) => 
 967 |             prop.toLowerCase() === q.toLowerCase() ||
 968 |             calculateSimilarity(prop.toLowerCase(), q.toLowerCase()) > 75
 969 |           );
 970 |         }
 971 |         
 972 |         // Enhanced generic scoring for better relevance detection
 973 |         
 974 |         // 1. Check for multi-word query matches in title
 975 |         const queryWords = q.toLowerCase().split(/\s+/).filter(w => w.length > 2);
 976 |         const titleWords = doc.title.toLowerCase().split(/\s+/);
 977 |         const wordMatchCount = queryWords.filter(qw => 
 978 |           titleWords.some(tw => tw.includes(qw) || qw.includes(tw))
 979 |         ).length;
 980 |         const wordMatchRatio = queryWords.length > 0 ? wordMatchCount / queryWords.length : 0;
 981 |         
 982 |         // 2. Check for phrase containment (e.g., "temporal entities" in "Declaring Temporal Entities")
 983 |         const titleContainsQuery = doc.title.toLowerCase().includes(q.toLowerCase());
 984 |         const queryContainsTitle = q.toLowerCase().includes(doc.title.toLowerCase());
 985 |         
 986 |         // 3. Exact matches get highest priority with heading level scoring
 987 |         if (doc.title.toLowerCase() === q.toLowerCase()) {
 988 |           // Base score for exact match
 989 |           score = 150;
 990 |           
 991 |           // Adjust score based on heading level for sections
 992 |           if ((doc as any).headingLevel) {
 993 |             const headingLevel = (doc as any).headingLevel;
 994 |             if (headingLevel === 2) {
 995 |               score = 160; // ## sections get highest score
 996 |             } else if (headingLevel === 3) {
 997 |               score = 155; // ### sections get medium score  
 998 |             } else if (headingLevel === 4) {
 999 |               score = 152; // #### sections get lower score
1000 |             }
1001 |             matchType = `Exact Title Match (H${headingLevel})`;
1002 |           } else {
1003 |             // Main document title (# level) gets the highest score
1004 |             score = 165;
1005 |             matchType = 'Exact Title Match (Main)';
1006 |           }
1007 |         }
1008 |         // 4. High word match ratio (most query words found in title)
1009 |         else if (wordMatchRatio >= 0.6 && wordMatchCount >= 2) {
1010 |           score = 140 + (wordMatchRatio * 20); // 140-160 range
1011 |           matchType = `High Word Match (${Math.round(wordMatchRatio * 100)}%)`;
1012 |         }
1013 |         // 5. Title contains full query phrase
1014 |         else if (titleContainsQuery && q.length > 5) {
1015 |           score = 135;
1016 |           matchType = 'Title Contains Query';
1017 |         }
1018 |         // 6. Query contains title (searching for specific within general)
1019 |         else if (queryContainsTitle && doc.title.length > 5) {
1020 |           score = 130;
1021 |           matchType = 'Query Contains Title';
1022 |         }
1023 |         // 7. Fall back to existing logic for other cases
1024 |         else if (controlNameMatch && controlName?.toLowerCase() === q.toLowerCase()) {
1025 |           score = 98;
1026 |           matchType = 'Exact Control Name Match';
1027 |         } else if (keywordMatch && keywords.some((kw: string) => kw.toLowerCase() === q.toLowerCase())) {
1028 |           score = 96;
1029 |           matchType = 'Exact Keyword Match';
1030 |         } else if (titleSimilarity > 80) {
1031 |           score = 95;
1032 |           matchType = 'High Title Similarity';
1033 |         } else if (controlNameMatch) {
1034 |           score = 92;
1035 |           matchType = 'Control Name Match';
1036 |         } else if (doc.title.toLowerCase().includes(q.toLowerCase())) {
1037 |           score = 90;
1038 |           matchType = 'Title Contains Query';
1039 |         } else if (keywordMatch) {
1040 |           score = 87;
1041 |           matchType = 'Keyword Match';
1042 |         } else if (descSimilarity > 70) {
1043 |           score = 85;
1044 |           matchType = 'High Description Similarity';
1045 |         } else if (propertyMatch) {
1046 |           score = 82;
1047 |           matchType = 'Property/Event Match';
1048 |         } else if (lib.id === '/openui5-api' && isControlMatch(doc.title, q)) {
1049 |           score = 80;
1050 |           matchType = 'UI5 Control Pattern Match';
1051 |         } else if (doc.description.toLowerCase().includes(q.toLowerCase())) {
1052 |           score = 75;
1053 |           matchType = 'Description Contains Query';
1054 |         } else if (titleSimilarity > 60) {
1055 |           score = 70;
1056 |           matchType = 'Moderate Title Similarity';
1057 |         } else if (descSimilarity > 50) {
1058 |           score = 65;
1059 |           matchType = 'Moderate Description Similarity';
1060 |         } else if (doc.title.toLowerCase().split(/[.\s_-]/).some(part => calculateSimilarity(part, q) > 70)) {
1061 |           score = 60;
1062 |           matchType = 'Partial Title Match';
1063 |         }
1064 |         
1065 |         // Context-aware scoring boosts
1066 |         if (score > 0) {
1067 |           // UI5-specific boosts
1068 |           if (lib.id === '/openui5-api') {
1069 |             score += 5; // Base boost for API docs
1070 |             // Extra boost for UI5 control terms
1071 |             const ui5Terms = ['control', 'sap.m', 'sap.ui', 'sap.f', 'wizard', 'button', 'table'];
1072 |             if (ui5Terms.some(term => q.includes(term))) score += 8;
1073 |           }
1074 |           
1075 |           if (lib.id === '/openui5-samples') {
1076 |             // Boost samples for implementation queries
1077 |             if (q.includes('sample') || q.includes('example') || q.includes('demo')) score += 10;
1078 |             // Boost for UI5 control samples
1079 |             if (controlName && q.includes(controlName.toLowerCase())) score += 12;
1080 |           }
1081 |           
1082 |           if (lib.id === '/sapui5') {
1083 |             // Boost SAPUI5 docs for UI5-specific queries
1084 |             const ui5Queries = ['fiori', 'ui5', 'sapui5', 'control', 'binding', 'routing'];
1085 |             if (ui5Queries.some(term => q.includes(term))) score += 8;
1086 |             // Boost for conceptual queries
1087 |             if (q.includes('guide') || q.includes('tutorial') || q.includes('how')) score += 6;
1088 |           }
1089 |           
1090 |           // CAP-specific boosts
1091 |           if (lib.id === '/cap') {
1092 |             const capTerms = ['cds', 'cap', 'service', 'entity', 'annotation', 'aspect', 'odata', 'hana', 'temporal'];
1093 |             if (capTerms.some(term => q.includes(term))) score += 10;
1094 |             // Extra boost for CAP core concepts
1095 |             const coreCapTerms = ['cds', 'entity', 'service', 'aspect', 'temporal'];
1096 |             if (coreCapTerms.some(term => q.includes(term))) score += 5;
1097 |             // Boost for development guides
1098 |             if (q.includes('guide') || q.includes('tutorial') || q.includes('how')) score += 8;
1099 |           }
1100 |           
1101 |           // wdi5-specific boosts
1102 |           if (lib.id === '/wdi5') {
1103 |             const wdi5Terms = ['wdi5', 'test', 'testing', 'e2e', 'browser', 'selector', 'webdriver', 'wdio', 'pageobject', 'fe-testlib'];
1104 |             if (wdi5Terms.some(term => q.includes(term))) score += 15;
1105 |             // Extra boost for testing concepts
1106 |             const testingTerms = ['test', 'testing', 'assertion', 'automation', 'locator', 'matcher'];
1107 |             if (testingTerms.some(term => q.includes(term))) score += 10;
1108 |             // Boost for UI5 testing specific terms
1109 |             const ui5TestingTerms = ['sap.ui.test', 'ui5 test', 'control selector', 'byId', 'byProperty'];
1110 |             if (ui5TestingTerms.some(term => q.includes(term))) score += 12;
1111 |           }
1112 | 
1113 |           // UI5 Web Components specific boosts
1114 |           if (lib.id === '/ui5-webcomponents') {
1115 |             const ui5WebComponentsTerms = ['component', 'web', 'web-component', 'web-components'];
1116 |             if (ui5WebComponentsTerms.some(term => q.includes(term))) score += 15;
1117 |             // Boost for conceptual queries
1118 |             if (q.includes('guide') || q.includes('tutorial') || q.includes('how')) score += 6;
1119 |           }
1120 | 
1121 |           // SAP Cloud SDK specific boosts
1122 |           if (lib.id === '/cloud-sdk-js' || lib.id === '/cloud-sdk-ai-js' || lib.id === '/cloud-sdk-java' || lib.id === '/cloud-sdk-ai-java') {
1123 |             const sapCloudSdkTerms = ['cloud sdk', 'sdk', 'cloud', 'sdk for ai', 'ai'];
1124 |             if (sapCloudSdkTerms.some(term => q.includes(term))) score += 15;
1125 |             // Boost for development guides
1126 |             if (q.includes('guide') || q.includes('tutorial') || q.includes('how')) score += 8;
1127 |           }
1128 | 
1129 |           // UI5 Tooling specific boosts
1130 |           if (lib.id === '/ui5-tooling') {
1131 |             const ui5ToolingTerms = ['ui5', '@ui5/', 'tooling', 'cli', 'tasks', 'middleware', 'shims'];
1132 |             if (ui5ToolingTerms.some(term => q.includes(term))) score += 15;
1133 |             // Boost for development guides
1134 |             if (q.includes('guide') || q.includes('tutorial') || q.includes('how')) score += 8;
1135 |           }
1136 | 
1137 |           // Cloud MTA Build Tool specific boosts
1138 |           if (lib.id === '/cloud-mta-build-tool') {
1139 |             const cloudMtaBuildToolTerms = ['mta', 'mtar', 'multitarget', 'build', 'cloud mta build tool'];
1140 |             if (cloudMtaBuildToolTerms.some(term => q.includes(term))) score += 15;
1141 |             // Boost for development guides
1142 |             if (q.includes('guide') || q.includes('tutorial') || q.includes('how')) score += 8;
1143 |           }
1144 |           
1145 |           // Apply context-aware penalties to reduce off-topic results
1146 |           score = applyContextPenalties(score, lib.id, queryContext, q);
1147 | 
1148 |           // Intent-weighted library priors (soft bias)
1149 |           const priors = computeLibraryPriors(queryContext);
1150 |           const prior = priors[lib.id] ?? 0;
1151 |           const alpha = 0.25; // strength of prior influence (moderate)
1152 |           let finalScore = score * (1 + alpha * prior);
1153 | 
1154 |           // Topic-specific additive bonus: error-handling intent for SDK/AI docs
1155 |           const oq = originalQuery.toLowerCase();
1156 |           const errorIntent = oq.includes('error handling') || oq.includes('error information') || (/\berror\b/.test(oq) && (oq.includes('sdk') || oq.includes('ai')));
1157 |           if (errorIntent) {
1158 |             const titleL = (doc.title || '').toLowerCase();
1159 |             const relL = String((doc as any).relFile || '').toLowerCase();
1160 |             if (lib.id.startsWith('/cloud-sdk') && (titleL.includes('error') || relL.includes('error'))) {
1161 |               finalScore += 15;
1162 |             }
1163 |           }
1164 | 
1165 |           allMatches.push({
1166 |             score: Math.max(0, finalScore), // floor at 0, no hard cap to preserve ordering
1167 |             libraryId: lib.id,
1168 |             libraryName: lib.name,
1169 |             docId: doc.id,
1170 |             docTitle: doc.title,
1171 |             docDescription: doc.description,
1172 |             matchType,
1173 |             snippetCount: doc.snippetCount,
1174 |             source: 'docs',
1175 |             titleSimilarity,
1176 |             descSimilarity
1177 |           });
1178 |         }
1179 |       }
1180 |     }
1181 |   }
1182 | 
1183 |   // If still no results, try comprehensive fuzzy search
1184 |   if (allMatches.length === 0) {
1185 |     const originalQuery = query.toLowerCase();
1186 |     const queryParts = originalQuery.split(/[\s_-]+/).filter(part => part.length > 2);
1187 |     
1188 |     for (const lib of Object.values(index)) {
1189 |       for (const doc of lib.docs) {
1190 |         // If we're using FTS filtering, only process documents that are in the candidate set
1191 |         if (usedFTS && !candidateDocIds.has(doc.id)) {
1192 |           continue;
1193 |         }
1194 |         let maxScore = 0;
1195 |         let bestMatchType = 'Fuzzy Search';
1196 |         
1197 |         // Try fuzzy matching against title parts
1198 |         const titleParts = doc.title.toLowerCase().split(/[\s._-]+/);
1199 |         for (const titlePart of titleParts) {
1200 |           for (const queryPart of queryParts) {
1201 |             const similarity = calculateSimilarity(titlePart, queryPart);
1202 |             if (similarity > maxScore && similarity > 50) {
1203 |               maxScore = similarity * 0.8; // Reduce score for fuzzy matches
1204 |               bestMatchType = 'Fuzzy Title Match';
1205 |             }
1206 |           }
1207 |         }
1208 |         
1209 |         // Try fuzzy matching against description parts
1210 |         const descParts = doc.description.toLowerCase().split(/[\s._-]+/);
1211 |         for (const descPart of descParts) {
1212 |           if (descPart.length > 3) { // Only check meaningful words
1213 |             for (const queryPart of queryParts) {
1214 |               const similarity = calculateSimilarity(descPart, queryPart);
1215 |               if (similarity > maxScore && similarity > 50) {
1216 |                 maxScore = similarity * 0.6; // Further reduce for description matches
1217 |                 bestMatchType = 'Fuzzy Description Match';
1218 |               }
1219 |             }
1220 |           }
1221 |         }
1222 |         
1223 |         if (maxScore > 30) { // Lower threshold for fuzzy results
1224 |           allMatches.push({
1225 |             score: maxScore,
1226 |             libraryId: lib.id,
1227 |             libraryName: lib.name,
1228 |             docId: doc.id,
1229 |             docTitle: doc.title,
1230 |             docDescription: doc.description,
1231 |             matchType: bestMatchType,
1232 |             snippetCount: doc.snippetCount,
1233 |             source: 'docs'
1234 |           });
1235 |         }
1236 |       }
1237 |     }
1238 |   }
1239 | 
1240 |   // Sort all results by relevance score (highest first), then by title
1241 |   function sortByScore(a: any, b: any) {
1242 |     if (b.score !== a.score) return b.score - a.score;
1243 |     return a.docTitle.localeCompare(b.docTitle);
1244 |   }
1245 |   // Deduplicate by docId (keep highest-score variant)
1246 |   const byId = new Map<string, any>();
1247 |   for (const m of allMatches) {
1248 |     const prev = byId.get(m.docId);
1249 |     if (!prev || m.score > prev.score) byId.set(m.docId, m);
1250 |   }
1251 |   allMatches = Array.from(byId.values());
1252 |   allMatches.sort(sortByScore);
1253 | 
1254 |   // Take top results regardless of library, but ensure diversity
1255 |   const topResults = [];
1256 |   const seenLibraries = new Set();
1257 |   const maxPerLibrary = queryContext === 'MIXED' ? 3 : 5; // More diversity for mixed queries
1258 |   
1259 |   for (const result of allMatches) {
1260 |     if (topResults.length >= 20) break; // Limit total results
1261 |     
1262 |     const libraryCount = topResults.filter(r => r.libraryId === result.libraryId).length;
1263 |     if (libraryCount < maxPerLibrary) {
1264 |       topResults.push(result);
1265 |       seenLibraries.add(result.libraryId);
1266 |     }
1267 |   }
1268 |   
1269 |   // Group results by library for presentation (but maintain score order)
1270 |   const apiDocs = topResults.filter(r => r.libraryId === '/openui5-api');
1271 |   const samples = topResults.filter(r => r.libraryId === '/openui5-samples');
1272 |   const guides = topResults.filter(r => r.libraryId === '/sapui5' || r.libraryId === '/cap' || r.libraryId === '/wdi5' || r.libraryId === '/ui5-webcomponents' || r.libraryId === '/cloud-sdk-js' || r.libraryId === '/cloud-sdk-ai-js' || r.libraryId === '/cloud-sdk-java' || r.libraryId === '/cloud-sdk-ai-java' || r.libraryId === '/ui5-tooling' || r.libraryId === '/cloud-mta-build-tool');
1273 | 
1274 |   if (!topResults.length) {
1275 |     // User feedback loop: suggest alternatives
1276 |     let suggestion = "No documentation found for '" + query + "'. ";
1277 |     if (fileContent) {
1278 |       suggestion += "Try searching for: " + extractControlsFromContent(fileContent).join(", ") + ". ";
1279 |     }
1280 |     suggestion += "Try terms like 'button', 'table', 'wizard', 'routing', 'annotation', or check for typos.";
1281 |     return { results: [], error: suggestion };
1282 |   }
1283 | 
1284 |   // Group results for presentation with context awareness
1285 |   const contextEmojis = getAllContextEmojis();
1286 |   
1287 |   // Add FTS info to response for transparency
1288 |   const ftsInfo = usedFTS ? ` (🚀 FTS-filtered from ${candidateDocIds.size} candidates)` : ' (🔍 Full search)';
1289 |   let response = `Found ${topResults.length} results for '${query}' ${getContextEmoji(queryContext)} **${queryContext} Context**${ftsInfo}:\n\n`;
1290 |   
1291 |   // Show results in score order, grouped by type
1292 |   if (guides.length > 0) {
1293 |     const capGuides = guides.filter(r => r.libraryId === '/cap');
1294 |     const wdi5Guides = guides.filter(r => r.libraryId === '/wdi5');
1295 |     const sapui5Guides = guides.filter(r => r.libraryId === '/sapui5');
1296 |     const ui5WebComponentsGuides = guides.filter(r => r.libraryId === '/ui5-webcomponents');
1297 |     const sapCloudSdkGuides = guides.filter(r => r.libraryId === '/cloud-sdk-js' || r.libraryId === '/cloud-sdk-ai-js' || r.libraryId === '/cloud-sdk-java' || r.libraryId === '/cloud-sdk-ai-java');
1298 |     const ui5ToolingGuides = guides.filter(r => r.libraryId === '/ui5-tooling');
1299 |     const cloudMtaBuildToolGuides = guides.filter(r => r.libraryId === '/cloud-mta-build-tool');
1300 |     
1301 |     if (capGuides.length > 0) {
1302 |       response += `🏗️ **CAP Documentation:**\n`;
1303 |       for (const r of capGuides) {
1304 |         response += formatSearchResultEntry(r, queryContext);
1305 |       }
1306 |     }
1307 |     
1308 |     if (wdi5Guides.length > 0) {
1309 |       response += `🧪 **wdi5 Documentation:**\n`;
1310 |       for (const r of wdi5Guides) {
1311 |         response += formatSearchResultEntry(r, queryContext);
1312 |       }
1313 |     }
1314 |     
1315 |     if (sapui5Guides.length > 0) {
1316 |       response += `📖 **SAPUI5 Guides:**\n`;
1317 |       for (const r of sapui5Guides) {
1318 |         response += formatSearchResultEntry(r, queryContext);
1319 |       }
1320 |     }
1321 | 
1322 |     if (ui5WebComponentsGuides.length > 0) {
1323 |       response += `🕹️ **UI5 Web Components Guides:**\n`;
1324 |       for (const r of ui5WebComponentsGuides) {
1325 |         response += formatSearchResultEntry(r, queryContext);
1326 |       }
1327 |     }
1328 | 
1329 |     if (ui5ToolingGuides.length > 0) {
1330 |       response += `🔧 **UI5 Tooling Guides:**\n`;
1331 |       for (const r of ui5ToolingGuides) {
1332 |         response += formatSearchResultEntry(r, queryContext);
1333 |       }
1334 |     }
1335 | 
1336 |     if (sapCloudSdkGuides.length > 0) {
1337 |       response += `🌐 **SAP Cloud SDK Guides:**\n`;
1338 |       for (const r of sapCloudSdkGuides) {
1339 |         response += formatSearchResultEntry(r, queryContext);
1340 |       }
1341 |     }
1342 | 
1343 |     if (cloudMtaBuildToolGuides.length > 0) {
1344 |       response += `🚢 **Cloud MTA Build Tool Guides:**\n`;
1345 |       for (const r of cloudMtaBuildToolGuides) {
1346 |         response += formatSearchResultEntry(r, queryContext);
1347 |       }
1348 |     }
1349 |   }
1350 |   
1351 |   if (apiDocs.length > 0) {
1352 |     response += `🔹 **UI5 API Documentation:**\n`;
1353 |     for (const r of apiDocs.slice(0, 8)) {
1354 |       response += formatSearchResultEntry(r, queryContext);
1355 |     }
1356 |   }
1357 |   
1358 |   if (samples.length > 0) {
1359 |     response += `🔸 **UI5 Samples:**\n`;
1360 |     for (const r of samples.slice(0, 8)) {
1361 |       response += formatSearchResultEntry(r, queryContext);
1362 |     }
1363 |   }
1364 |   
1365 |   response += `💡 **Context**: ${queryContext} query detected. Scores reflect relevance to this context.\n`;
1366 |   response += `🔍 **Tried queries**: ${triedQueries.slice(0, 3).join(", ")}${triedQueries.length > 3 ? '...' : ''}`;
1367 | 
1368 |   return {
1369 |     results: topResults.map((r, index) => {
1370 |       const { libraryId, topic } = parseDocumentId(r.docId);
1371 |       return {
1372 |         library_id: libraryId,
1373 |         topic: topic,
1374 |         id: r.docId,
1375 |         title: r.docTitle,
1376 |         url: r.url || `#${r.docId}`,
1377 |         snippet: r.docDescription,
1378 |         score: r.score,
1379 |         metadata: {
1380 |           source: 'sap-docs',
1381 |           library: libraryId,
1382 |           rank: index + 1,
1383 |           context: queryContext
1384 |         }
1385 |       };
1386 |     })
1387 |   };
1388 | }
1389 | 
1390 | 
1391 | 
1392 | export async function fetchLibraryDocumentation(
1393 |   libraryIdOrDocId: string,
1394 |   topic = ""
1395 | ): Promise<string | null> {
1396 |   // Check if this is a community post ID
1397 |   if (libraryIdOrDocId.startsWith('community-')) {
1398 |     return await getCommunityPost(libraryIdOrDocId);
1399 |   }
1400 | 
1401 |   const index = await loadIndex();
1402 |   
1403 |   // Check if this is a specific document ID
1404 |   const allDocs: Array<{lib: any, doc: any}> = [];
1405 |   for (const lib of Object.values(index)) {
1406 |     for (const doc of lib.docs) {
1407 |       allDocs.push({ lib, doc });
1408 |       // Try exact match first (for section documents with fragments)
1409 |       if (doc.id === libraryIdOrDocId) {
1410 |         const sourcePath = getSourcePath(lib.id);
1411 |         if (!sourcePath) {
1412 |           throw new Error(`Unknown library ID: ${lib.id}`);
1413 |         }
1414 |         
1415 |         const abs = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1416 |         const content = await fs.readFile(abs, "utf8");
1417 |         
1418 |         // For JavaScript API files, format the content for better readability
1419 |         if (doc.relFile && doc.relFile.endsWith('.js') && lib.id === '/openui5-api') {
1420 |           return formatJSDocContent(content, doc.title || '');
1421 |         }
1422 |         // For sample files, format them appropriately
1423 |         else if (lib.id === '/openui5-samples') {
1424 |           return formatSampleContent(content, doc.relFile, doc.title || '');
1425 |         }
1426 |         // For documented libraries, add URL context
1427 |         else if (getDocUrlConfig(lib.id)) {
1428 |           const documentationUrl = generateDocumentationUrl(lib.id, doc.relFile, content);
1429 |           const libName = lib.id.replace('/', '').toUpperCase();
1430 |           
1431 |           return `**Source:** ${libName} Documentation
1432 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1433 | **File:** ${doc.relFile}
1434 | 
1435 | ---
1436 | 
1437 | ${content}
1438 | 
1439 | ---
1440 | 
1441 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1442 |         } else {
1443 |           return content;
1444 |         }
1445 |       }
1446 |     }
1447 |   }
1448 |   
1449 |   // If no exact match found and the ID contains a fragment, try stripping the fragment
1450 |   // This handles cases where fragments are used for navigation but don't exist as separate documents
1451 |   if (libraryIdOrDocId.includes('#')) {
1452 |     const baseId = libraryIdOrDocId.split('#')[0];
1453 |     
1454 |     // Try to find a document with the base ID (without fragment)
1455 |     for (const lib of Object.values(index)) {
1456 |       for (const doc of lib.docs) {
1457 |         if (doc.id === baseId) {
1458 |           const sourcePath = getSourcePath(lib.id);
1459 |           if (!sourcePath) {
1460 |             throw new Error(`Unknown library ID: ${lib.id}`);
1461 |           }
1462 |           
1463 |           const abs = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1464 |           const content = await fs.readFile(abs, "utf8");
1465 |           
1466 |           // For JavaScript API files, format the content for better readability
1467 |           if (doc.relFile && doc.relFile.endsWith('.js') && lib.id === '/openui5-api') {
1468 |             return formatJSDocContent(content, doc.title || '');
1469 |           }
1470 |           // For sample files, format them appropriately
1471 |           else if (lib.id === '/openui5-samples') {
1472 |             return formatSampleContent(content, doc.relFile, doc.title || '');
1473 |           }
1474 |           // For documented libraries, add URL context
1475 |           else if (getDocUrlConfig(lib.id)) {
1476 |             const documentationUrl = generateDocumentationUrl(lib.id, doc.relFile, content);
1477 |             const libName = lib.id.replace('/', '').toUpperCase();
1478 |             
1479 |             return `**Source:** ${libName} Documentation
1480 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1481 | **File:** ${doc.relFile}
1482 | 
1483 | ---
1484 | 
1485 | ${content}
1486 | 
1487 | ---
1488 | 
1489 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1490 |           } else {
1491 |             return content;
1492 |           }
1493 |         }
1494 |       }
1495 |     }
1496 |     
1497 |     // Try library-level lookup with base ID
1498 |     const baseLib = index[baseId];
1499 |     if (baseLib) {
1500 |       const term = topic.toLowerCase();
1501 |       const targets = term
1502 |         ? baseLib.docs.filter(
1503 |             (d) =>
1504 |               d.title.toLowerCase().includes(term) ||
1505 |               d.description.toLowerCase().includes(term)
1506 |           )
1507 |         : baseLib.docs;
1508 | 
1509 |       if (!targets.length) return `No topic "${topic}" found inside ${baseId}.`;
1510 | 
1511 |       const parts: string[] = [];
1512 |       for (const doc of targets) {
1513 |         const sourcePath = getSourcePath(baseLib.id);
1514 |         if (!sourcePath) {
1515 |           throw new Error(`Unknown library ID: ${baseLib.id}`);
1516 |         }
1517 |         
1518 |         const abs = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1519 |         const content = await fs.readFile(abs, "utf8");
1520 |         
1521 |         // For JavaScript API files, format the content for better readability
1522 |         if (doc.relFile && doc.relFile.endsWith('.js') && baseLib.id === '/openui5-api') {
1523 |           const formattedContent = formatJSDocContent(content, doc.title || '');
1524 |           parts.push(formattedContent);
1525 |         }
1526 |         // For sample files, format them appropriately
1527 |         else if (baseLib.id === '/openui5-samples') {
1528 |           const formattedContent = formatSampleContent(content, doc.relFile, doc.title || '');
1529 |           parts.push(formattedContent);
1530 |         }
1531 |         // For documented libraries, add URL context
1532 |         else if (getDocUrlConfig(baseLib.id)) {
1533 |           const documentationUrl = generateDocumentationUrl(baseLib.id, doc.relFile, content);
1534 |           const libName = baseLib.id.replace('/', '').toUpperCase();
1535 |           
1536 |           const formattedContent = `**Source:** ${libName} Documentation
1537 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1538 | **File:** ${doc.relFile}
1539 | 
1540 | ---
1541 | 
1542 | ${content}
1543 | 
1544 | ---
1545 | 
1546 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1547 |           parts.push(formattedContent);
1548 |         } else {
1549 |           parts.push(content);
1550 |         }
1551 |       }
1552 |       return parts.join("\n\n---\n\n");
1553 |     }
1554 |   }
1555 |   
1556 |   // If not a specific document ID, treat as library ID with optional topic
1557 |   const lib = index[libraryIdOrDocId];
1558 |   if (!lib) return null;
1559 | 
1560 |   // If topic is provided, first try to construct the full document ID
1561 |   if (topic) {
1562 |     const fullDocId = `${libraryIdOrDocId}/${topic}`;
1563 |     
1564 |     // Try to find exact document match first
1565 |     for (const doc of lib.docs) {
1566 |       if (doc.id === fullDocId) {
1567 |         const sourcePath = getSourcePath(lib.id);
1568 |         if (!sourcePath) {
1569 |           throw new Error(`Unknown library ID: ${lib.id}`);
1570 |         }
1571 |         
1572 |         const abs = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1573 |         const content = await fs.readFile(abs, "utf8");
1574 |         
1575 |         // Format the content appropriately based on library type
1576 |         if (doc.relFile && doc.relFile.endsWith('.js') && lib.id === '/openui5-api') {
1577 |           return formatJSDocContent(content, doc.title || '');
1578 |         } else if (lib.id === '/openui5-samples') {
1579 |           return formatSampleContent(content, doc.relFile, doc.title || '');
1580 |         } else if (getDocUrlConfig(lib.id)) {
1581 |           const documentationUrl = generateDocumentationUrl(lib.id, doc.relFile, content);
1582 |           const libName = lib.id.replace('/', '').toUpperCase();
1583 |           
1584 |           return `**Source:** ${libName} Documentation
1585 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1586 | **File:** ${doc.relFile}
1587 | 
1588 | ---
1589 | 
1590 | ${content}
1591 | 
1592 | ---
1593 | 
1594 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1595 |         } else {
1596 |           return content;
1597 |         }
1598 |       }
1599 |     }
1600 |     
1601 |     // If exact match not found, fall back to topic keyword search
1602 |     const term = topic.toLowerCase();
1603 |     const targets = lib.docs.filter(
1604 |       (d) =>
1605 |         d.title.toLowerCase().includes(term) ||
1606 |         d.description.toLowerCase().includes(term)
1607 |     );
1608 |     
1609 |     if (targets.length > 0) {
1610 |       // Process the filtered documents
1611 |       const parts: string[] = [];
1612 |       for (const doc of targets) {
1613 |         const sourcePath = getSourcePath(lib.id);
1614 |         if (!sourcePath) {
1615 |           throw new Error(`Unknown library ID: ${lib.id}`);
1616 |         }
1617 |         
1618 |         const abs = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1619 |         const content = await fs.readFile(abs, "utf8");
1620 |         
1621 |         if (doc.relFile && doc.relFile.endsWith('.js') && lib.id === '/openui5-api') {
1622 |           const formattedContent = formatJSDocContent(content, doc.title || '');
1623 |           parts.push(formattedContent);
1624 |         } else if (lib.id === '/openui5-samples') {
1625 |           const formattedContent = formatSampleContent(content, doc.relFile, doc.title || '');
1626 |           parts.push(formattedContent);
1627 |         } else if (getDocUrlConfig(lib.id)) {
1628 |           const documentationUrl = generateDocumentationUrl(lib.id, doc.relFile, content);
1629 |           const libName = lib.id.replace('/', '').toUpperCase();
1630 |           
1631 |           const formattedContent = `**Source:** ${libName} Documentation
1632 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1633 | **File:** ${doc.relFile}
1634 | 
1635 | ---
1636 | 
1637 | ${content}
1638 | 
1639 | ---
1640 | 
1641 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1642 |           parts.push(formattedContent);
1643 |         } else {
1644 |           parts.push(content);
1645 |         }
1646 |       }
1647 |       return parts.join("\n\n---\n\n");
1648 |     }
1649 |     
1650 |     return `No topic "${topic}" found inside ${libraryIdOrDocId}.`;
1651 |   }
1652 |   
1653 |   // No topic provided, return all library documents
1654 |   const targets = lib.docs;
1655 |   if (!targets.length) return `No documents found inside ${libraryIdOrDocId}.`;
1656 | 
1657 |   const parts: string[] = [];
1658 |   for (const doc of targets) {
1659 |     const sourcePath = getSourcePath(lib.id);
1660 |     if (!sourcePath) {
1661 |       throw new Error(`Unknown library ID: ${lib.id}`);
1662 |     }
1663 |     
1664 |     const abs = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1665 |     const content = await fs.readFile(abs, "utf8");
1666 |     
1667 |     // For JavaScript API files, format the content for better readability
1668 |     if (doc.relFile && doc.relFile.endsWith('.js') && lib.id === '/openui5-api') {
1669 |       const formattedContent = formatJSDocContent(content, doc.title || '');
1670 |       parts.push(formattedContent);
1671 |     }
1672 |     // For sample files, format them appropriately
1673 |     else if (lib.id === '/openui5-samples') {
1674 |       const formattedContent = formatSampleContent(content, doc.relFile, doc.title || '');
1675 |       parts.push(formattedContent);
1676 |     }
1677 |     // For documented libraries, add URL context
1678 |     else if (getDocUrlConfig(lib.id)) {
1679 |       const documentationUrl = generateDocumentationUrl(lib.id, doc.relFile, content);
1680 |       const libName = lib.id.replace('/', '').toUpperCase();
1681 |       
1682 |       const formattedContent = `**Source:** ${libName} Documentation
1683 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1684 | **File:** ${doc.relFile}
1685 | 
1686 | ---
1687 | 
1688 | ${content}
1689 | 
1690 | ---
1691 | 
1692 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1693 |       parts.push(formattedContent);
1694 |     } else {
1695 |       parts.push(content);
1696 |     }
1697 |   }
1698 |   return parts.join("\n\n---\n\n");
1699 | }
1700 | 
1701 | // Resource support for MCP
1702 | export async function listDocumentationResources() {
1703 |   const index = await loadIndex();
1704 |   const resources: Array<{
1705 |     uri: string;
1706 |     name: string;
1707 |     description?: string;
1708 |     mimeType?: string;
1709 |   }> = [];
1710 | 
1711 |   // Add library overview resources
1712 |   for (const lib of Object.values(index)) {
1713 |     resources.push({
1714 |       uri: `sap-docs://${lib.id}`,
1715 |       name: `${lib.name} Documentation Overview`,
1716 |       description: lib.description,
1717 |       mimeType: "text/markdown"
1718 |     });
1719 | 
1720 |     // Add individual document resources
1721 |     for (const doc of lib.docs) {
1722 |       resources.push({
1723 |         uri: `sap-docs://${lib.id}/${encodeURIComponent(doc.relFile)}`,
1724 |         name: doc.title,
1725 |         description: `${doc.description} (${doc.snippetCount} code snippets)`,
1726 |         mimeType: "text/markdown"
1727 |       });
1728 |     }
1729 |   }
1730 | 
1731 |   // Add SAP Community as a searchable resource
1732 |   resources.push({
1733 |     uri: "sap-docs:///community",
1734 |     name: "SAP Community Posts",
1735 |     description: "Real-time access to SAP Community blog posts, discussions, and solutions. Search for topics to find community insights and practical solutions.",
1736 |     mimeType: "text/markdown"
1737 |   });
1738 | 
1739 |   return resources;
1740 | }
1741 | 
1742 | export async function readDocumentationResource(uri: string) {
1743 |   const index = await loadIndex();
1744 |   
1745 |   // Handle community overview
1746 |   if (uri === "sap-docs:///community") {
1747 |     const overview = [
1748 |       `# SAP Community`,
1749 |       ``,
1750 |       `Real-time access to SAP Community blog posts, discussions, and solutions.`,
1751 |       ``,
1752 |       `## How to Use`,
1753 |       ``,
1754 |       `1. Use the search function to find community posts on specific topics`,
1755 |       `2. Search for terms like "wizard", "button", "authentication", "deployment", etc.`,
1756 |       `3. Get access to real-world solutions and community best practices`,
1757 |       `4. Use specific community post IDs (e.g., "community-12345") to retrieve full content`,
1758 |       ``,
1759 |       `## What You'll Find`,
1760 |       ``,
1761 |       `- **Blog Posts**: Technical tutorials and deep-dives`,
1762 |       `- **Solutions**: Answers to common problems`,
1763 |       `- **Best Practices**: Community-tested approaches`,
1764 |       `- **Code Examples**: Real-world implementations`,
1765 |       ``,
1766 |       `Community content includes engagement data (kudos) when available and follows SAP Community's Best Match ranking.`,
1767 |       ``,
1768 |       `*Note: Community content is fetched in real-time from the SAP Community API.*`
1769 |     ].join('\n');
1770 | 
1771 |     return {
1772 |       contents: [{
1773 |         uri,
1774 |         mimeType: "text/markdown",
1775 |         text: overview
1776 |       }]
1777 |     };
1778 |   }
1779 | 
1780 |   // Parse URI: sap-docs://[libraryId]/[optional-file-path]
1781 |   // Note: libraryId starts with '/', so we may have sap-docs:///cap/... (3 slashes)
1782 |   const match = uri.match(/^sap-docs:\/\/\/?([^\/]+)(?:\/(.+))?$/);
1783 |   if (!match) {
1784 |     throw new Error(`Invalid resource URI: ${uri}`);
1785 |   }
1786 | 
1787 |   const [, libIdPart, encodedFilePath] = match;
1788 |   // Library ID should have leading slash
1789 |   const libraryId = libIdPart.startsWith('/') ? libIdPart : `/${libIdPart}`;
1790 |   const lib = index[libraryId];
1791 |   if (!lib) {
1792 |     throw new Error(`Library not found: ${libraryId}`);
1793 |   }
1794 | 
1795 |   // If no file path, return library overview
1796 |   if (!encodedFilePath) {
1797 |     const overview = [
1798 |       `# ${lib.name}`,
1799 |       ``,
1800 |       `${lib.description}`,
1801 |       ``,
1802 |       `## Available Documents`,
1803 |       ``,
1804 |       ...lib.docs.map(doc => 
1805 |         `- **${doc.title}**: ${doc.description} (${doc.snippetCount} code snippets)`
1806 |       ),
1807 |       ``,
1808 |       `Total documents: ${lib.docs.length}`,
1809 |       `Total code snippets: ${lib.docs.reduce((sum, doc) => sum + doc.snippetCount, 0)}`
1810 |     ].join('\n');
1811 | 
1812 |     return {
1813 |       contents: [{
1814 |         uri,
1815 |         mimeType: "text/markdown",
1816 |         text: overview
1817 |       }]
1818 |     };
1819 |   }
1820 | 
1821 |   // Find and return specific document
1822 |   const filePath = decodeURIComponent(encodedFilePath);
1823 |   const doc = lib.docs.find(d => d.relFile === filePath);
1824 |   if (!doc) {
1825 |     throw new Error(`Document not found: ${filePath}`);
1826 |   }
1827 | 
1828 |   const sourcePath = getSourcePath(libraryId);
1829 |   if (!sourcePath) {
1830 |     throw new Error(`Unknown library ID: ${libraryId}`);
1831 |   }
1832 |   
1833 |   const absPath = path.join(PROJECT_ROOT, "sources", sourcePath, doc.relFile);
1834 | 
1835 |   try {
1836 |     const content = await fs.readFile(absPath, "utf8");
1837 |     
1838 |     // Format files for better readability
1839 |     let formattedContent = content;
1840 |     if (doc.relFile && doc.relFile.endsWith('.js') && libraryId === '/openui5-api') {
1841 |       formattedContent = formatJSDocContent(content, doc.title || '');
1842 |     } else if (libraryId === '/openui5-samples') {
1843 |       formattedContent = formatSampleContent(content, doc.relFile, doc.title || '');
1844 |     } else if (getDocUrlConfig(libraryId)) {
1845 |       const documentationUrl = generateDocumentationUrl(libraryId, doc.relFile, content);
1846 |       const libName = libraryId.replace('/', '').toUpperCase();
1847 |       
1848 |       formattedContent = `**Source:** ${libName} Documentation
1849 | **URL:** ${documentationUrl || 'Documentation URL not available'}
1850 | **File:** ${doc.relFile}
1851 | 
1852 | ---
1853 | 
1854 | ${content}
1855 | 
1856 | ---
1857 | 
1858 | *This content is from the ${libName} documentation. Visit the URL above for the latest version and interactive examples.*`;
1859 |     }
1860 |     
1861 |     return {
1862 |       contents: [{
1863 |         uri,
1864 |         mimeType: "text/markdown",
1865 |         text: formattedContent
1866 |       }]
1867 |     };
1868 |   } catch (error) {
1869 |     throw new Error(`Failed to read document: ${error}`);
1870 |   }
1871 | }
1872 | 
1873 | // Export the community search function for use as a separate tool
1874 | export async function searchCommunity(query: string): Promise<SearchResponse> {
1875 |   try {
1876 |     // Use the convenience function to search and get top 3 posts with full content
1877 |     const result = await searchAndGetTopPosts(query, 3, {
1878 |       includeBlogs: true,
1879 |       userAgent: 'SAP-Docs-MCP/1.0'
1880 |     });
1881 |     
1882 |     if (result.search.length === 0) {
1883 |       return { 
1884 |         results: [], 
1885 |         error: `No SAP Community posts found for "${query}". Try different keywords or check your connection.` 
1886 |       };
1887 |     }
1888 | 
1889 |     // Format the results with full post content
1890 |     let response = `Found ${result.search.length} SAP Community posts for "${query}" with full content:\n\n`;
1891 |     response += `🌐 **SAP Community Posts with Full Content:**\n\n`;
1892 | 
1893 |     for (const searchResult of result.search) {
1894 |       const postContent = result.posts[searchResult.postId || ''];
1895 |       
1896 |       if (postContent) {
1897 |         // Add the full post content
1898 |         response += postContent + '\n\n';
1899 |         response += `---\n\n`;
1900 |       } else {
1901 |         // Fallback to search result info if full content not available
1902 |         const postDate = searchResult.published || 'Unknown';
1903 |         response += `### **${searchResult.title}**\n`;
1904 |         response += `**Posted:** ${postDate}\n`;
1905 |         response += `**Description:** ${searchResult.snippet || 'No description available'}\n`;
1906 |         response += `**URL:** ${searchResult.url}\n`;
1907 |         response += `**ID:** \`community-${searchResult.postId}\`\n\n`;
1908 |         response += `---\n\n`;
1909 |       }
1910 |     }
1911 | 
1912 |     response += `💡 **Note:** These results include the full content from ${Object.keys(result.posts).length} SAP Community posts, representing real-world developer experiences and solutions.`;
1913 | 
1914 |     return { 
1915 |       results: result.search.map((searchResult, index) => ({
1916 |         library_id: `community-${searchResult.postId || index}`,
1917 |         topic: '',
1918 |         id: `community-${searchResult.postId || index}`,
1919 |         title: searchResult.title,
1920 |         url: searchResult.url,
1921 |         snippet: searchResult.snippet || '',
1922 |         score: 0,
1923 |         metadata: {
1924 |           source: 'community',
1925 |           postTime: searchResult.published,
1926 |           author: searchResult.author,
1927 |           likes: searchResult.likes,
1928 |           tags: searchResult.tags,
1929 |           rank: index + 1
1930 |         }
1931 |       }))
1932 |     };
1933 |   } catch (error: any) {
1934 |     console.error("Error searching SAP Community:", error);
1935 |     return { 
1936 |       results: [], 
1937 |       error: `Error searching SAP Community: ${error?.message || 'Unknown error'}. Please try again later.` 
1938 |     };
1939 |   }
1940 | } 
```
Page 5/5FirstPrevNextLast