bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
586 lines • 22.7 kB
JavaScript
/**
* Enhanced Layer Service
*
* Central service for managing and resolving knowledge from multiple layers.
* Supports configuration-driven layer loading with git repositories,
* handles layer initialization, topic resolution with override logic, and
* provides unified access to layered knowledge system.
*/
import { getEmbeddedKnowledgePath } from '../utils/path-utils.js';
import { getDomainList } from '../types/bc-knowledge.js';
import { LayerSourceType } from '../types/index.js';
import { EmbeddedKnowledgeLayer } from './embedded-layer.js';
import { ProjectKnowledgeLayer } from './project-layer.js';
import { GitKnowledgeLayer } from './git-layer.js';
import { AdvancedCacheManager } from '../cache/cache-manager.js';
import Fuse from 'fuse.js';
export class LayerService {
embeddedPath;
projectPath;
config;
layers = [];
initialized = false;
loadResults = new Map();
topicCache = new Map();
searchIndex = null;
cacheManager = null;
constructor(embeddedPath = getEmbeddedKnowledgePath(), projectPath = './bckb-overrides', config = {}) {
this.embeddedPath = embeddedPath;
this.projectPath = projectPath;
this.config = config;
// Initialize basic layers for simple constructor usage
console.error(`🔧 Creating embedded layer from path: ${this.embeddedPath}`);
this.layers.push(new EmbeddedKnowledgeLayer(this.embeddedPath));
// Add project layer if path exists (will be checked during initialization)
if (this.projectPath) {
console.error(`🔧 Creating project layer from path: ${this.projectPath}`);
this.layers.push(new ProjectKnowledgeLayer(this.projectPath));
}
}
/**
* Initialize layers from BCKB configuration
*/
async initializeFromConfiguration(config) {
console.log(`🔧 Initializing ${config.layers.length} layers from configuration...`);
// Initialize advanced cache manager
this.cacheManager = new AdvancedCacheManager(config.cache);
console.log(`💾 Advanced cache manager initialized with ${config.cache.max_size_mb}MB limit`);
const results = [];
this.layers = [];
// Create layers based on configuration in priority order
const sortedLayers = [...config.layers]
.filter(layer => layer.enabled)
.sort((a, b) => a.priority - b.priority);
for (const layerConfig of sortedLayers) {
try {
console.log(`📦 Creating layer: ${layerConfig.name} (priority: ${layerConfig.priority})`);
const layer = await this.createLayerFromConfig(layerConfig);
this.layers.push(layer);
// Initialize the layer
const result = await layer.initialize();
results.push({
layer_name: layerConfig.name,
success: result.success,
topics_loaded: result.topicsLoaded,
load_time_ms: result.loadTimeMs,
errors: result.error ? [result.error] : [],
warnings: [],
source_info: {
type: layerConfig.source.type,
location: this.getLayerLocation(layerConfig),
last_updated: new Date()
}
});
this.loadResults.set(layerConfig.name, result);
}
catch (error) {
const errorMessage = `Failed to create layer ${layerConfig.name}: ${error instanceof Error ? error.message : String(error)}`;
console.error(`❌ ${errorMessage}`);
results.push({
layer_name: layerConfig.name,
success: false,
topics_loaded: 0,
load_time_ms: 0,
errors: [errorMessage],
warnings: [],
source_info: {
type: layerConfig.source.type,
location: this.getLayerLocation(layerConfig)
}
});
}
}
// Build resolution index
await this.buildResolutionIndex();
// Warm up cache with frequently accessed topics
if (this.cacheManager) {
await this.warmUpCache(config);
}
this.initialized = true;
console.log(`✅ Initialized ${results.length} layers successfully`);
return results;
}
/**
* Initialize the default layer stack (legacy method)
*/
initializeLayers() {
// Add layers in priority order (lowest to highest priority)
this.layers = [
new EmbeddedKnowledgeLayer(this.embeddedPath), // Priority 0 (base layer)
new ProjectKnowledgeLayer(this.projectPath) // Priority 300 (highest)
];
// Sort layers by priority (lowest first)
this.layers.sort((a, b) => a.priority - b.priority);
}
/**
* Initialize all layers
*/
async initialize() {
if (this.initialized) {
return Array.from(this.loadResults.values());
}
console.error('🔄 Initializing Layer Service...');
const startTime = Date.now();
const results = [];
// Initialize layers in parallel for better performance
if (this.config.enableParallelLoading !== false) {
const promises = this.layers.map(layer => this.initializeLayer(layer));
results.push(...await Promise.allSettled(promises).then(settled => settled.map(result => result.status === 'fulfilled'
? result.value
: {
layerName: 'unknown',
success: false,
topicsLoaded: 0,
indexesLoaded: 0,
error: result.reason?.message || 'Unknown error',
loadTimeMs: 0
})));
}
else {
// Sequential initialization
for (const layer of this.layers) {
try {
const result = await this.initializeLayer(layer);
results.push(result);
}
catch (error) {
results.push({
layerName: layer.name,
success: false,
topicsLoaded: 0,
indexesLoaded: 0,
error: error instanceof Error ? error.message : String(error),
loadTimeMs: 0
});
}
}
}
// Build unified search index from all layers
this.buildUnifiedSearchIndex();
const totalTime = Date.now() - startTime;
const successfulLayers = results.filter(r => r.success);
const totalTopics = successfulLayers.reduce((sum, r) => sum + r.topicsLoaded, 0);
this.initialized = true;
console.error(`✅ Layer Service initialized: ${successfulLayers.length}/${results.length} layers, ${totalTopics} topics (${totalTime}ms)`);
return results;
}
/**
* Initialize a single layer
*/
async initializeLayer(layer) {
if (!layer.enabled) {
const result = {
layerName: layer.name,
success: false,
topicsLoaded: 0,
indexesLoaded: 0,
error: 'Layer disabled',
loadTimeMs: 0
};
this.loadResults.set(layer.name, result);
return result;
}
try {
const result = await layer.initialize();
this.loadResults.set(layer.name, result);
return result;
}
catch (error) {
const result = {
layerName: layer.name,
success: false,
topicsLoaded: 0,
indexesLoaded: 0,
error: error instanceof Error ? error.message : String(error),
loadTimeMs: 0
};
this.loadResults.set(layer.name, result);
throw error;
}
}
/**
* Create a layer from configuration
*/
async createLayerFromConfig(config) {
switch (config.source.type) {
case LayerSourceType.EMBEDDED:
return new EmbeddedKnowledgeLayer(config.source.path || this.embeddedPath);
case LayerSourceType.GIT:
if (!('url' in config.source) || !config.source.url) {
throw new Error(`Git layer ${config.name} requires URL`);
}
return new GitKnowledgeLayer(config.name, config.priority, config.source, // Cast to avoid type issues
config.auth);
case LayerSourceType.LOCAL:
if (!('path' in config.source) || !config.source.path) {
throw new Error(`Local layer ${config.name} requires path`);
}
return new ProjectKnowledgeLayer(config.source.path);
case LayerSourceType.HTTP:
throw new Error(`HTTP layers not yet implemented for ${config.name}`);
case LayerSourceType.NPM:
throw new Error(`NPM layers not yet implemented for ${config.name}`);
default:
throw new Error(`Unsupported layer type for ${config.name}: ${config.source.type}`);
}
}
/**
* Get location string for a layer configuration
*/
getLayerLocation(config) {
switch (config.source.type) {
case LayerSourceType.GIT:
return 'url' in config.source ? config.source.url || '' : '';
case LayerSourceType.LOCAL:
return 'path' in config.source ? config.source.path || '' : '';
case LayerSourceType.EMBEDDED:
return 'path' in config.source ? config.source.path || this.embeddedPath : this.embeddedPath;
case LayerSourceType.HTTP:
return 'url' in config.source ? config.source.url || '' : '';
case LayerSourceType.NPM:
return 'package' in config.source ? config.source.package || '' : '';
default:
return 'unknown';
}
}
/**
* Build resolution index after configuration-based initialization
*/
async buildResolutionIndex() {
// Clear cache
this.topicCache.clear();
// Build unified search index
this.buildUnifiedSearchIndex();
console.log(`📚 Built resolution index for ${this.layers.length} layers`);
}
/**
* Get session storage configuration from layers
* Higher priority layers override lower priority ones
*/
getSessionStorageConfig() {
// Check if we have configuration from initialization
if (this.config.sessionStorage) {
return this.config.sessionStorage;
}
// For now, return undefined to use default (memory storage)
// In the future, this could read from layer configuration files
return undefined;
}
/**
* Build unified search index across all layers
*/
buildUnifiedSearchIndex() {
const allTopics = [];
// Collect all topics from all layers (higher priority layers override)
const topicMap = new Map();
for (const layer of this.layers) {
for (const topicId of layer.getTopicIds()) {
// Always use the highest priority version (last one wins due to layer sorting)
const topic = topicMap.get(topicId);
if (!topic || layer.priority > this.getTopicLayer(topicId)?.priority) {
const layerTopic = this.getTopicFromLayer(layer, topicId);
if (layerTopic) {
topicMap.set(topicId, layerTopic);
}
}
}
}
allTopics.push(...topicMap.values());
// Create Fuse search index
this.searchIndex = new Fuse(allTopics, {
keys: [
{ name: 'frontmatter.title', weight: 0.3 },
{ name: 'frontmatter.tags', weight: 0.25 },
{ name: 'frontmatter.domain', weight: 0.2 },
{ name: 'content', weight: 0.15 },
{ name: 'frontmatter.prerequisites', weight: 0.05 },
{ name: 'frontmatter.related_topics', weight: 0.05 }
],
threshold: 0.4,
includeScore: true,
includeMatches: true
});
console.error(`🔍 Search index built with ${allTopics.length} unique topics`);
}
/**
* Resolve a topic with layer override logic
*/
async resolveTopic(topicId) {
if (!this.initialized) {
await this.initialize();
}
// Check advanced cache first if available
if (this.cacheManager) {
const cachedTopic = this.cacheManager.getTopic(topicId);
if (cachedTopic) {
// Return a resolution result from cached topic
return {
topic: cachedTopic,
sourceLayer: 'cache', // We'd need to store source layer info in cache
isOverride: false,
overriddenLayers: []
};
}
}
// Check legacy cache
const cached = this.topicCache.get(topicId);
if (cached) {
return cached;
}
// Find the highest priority layer that has this topic
let resolvedTopic = null;
let sourceLayer = '';
let overriddenLayers = [];
// Go through layers in reverse order (highest priority first)
const reversedLayers = [...this.layers].reverse();
for (const layer of reversedLayers) {
if (layer.hasTopic(topicId)) {
if (!resolvedTopic) {
// This is the highest priority layer with this topic
const topic = await layer.getTopic(topicId);
if (topic) {
resolvedTopic = topic;
sourceLayer = layer.name;
}
}
else {
// This layer has the topic but was overridden
overriddenLayers.push(layer.name);
}
}
}
if (!resolvedTopic) {
return null;
}
const result = {
topic: resolvedTopic,
sourceLayer,
isOverride: overriddenLayers.length > 0,
overriddenLayers
};
// Cache the result in both caches
this.topicCache.set(topicId, result);
if (this.cacheManager && resolvedTopic) {
// Determine source type for intelligent TTL
const sourceLayer = this.layers.find(l => l.name === result.sourceLayer);
const sourceType = this.getSourceTypeForLayer(sourceLayer);
this.cacheManager.cacheTopic(topicId, resolvedTopic, sourceType);
}
return result;
}
/**
* Get all available topic IDs from all layers
*/
getAllTopicIds() {
const topicIds = new Set();
for (const layer of this.layers) {
for (const topicId of layer.getTopicIds()) {
topicIds.add(topicId);
}
}
return Array.from(topicIds);
}
/**
* Search topics across all layers
*/
async searchTopics(params) {
if (!this.initialized) {
await this.initialize();
}
const results = [];
const limit = params.limit || 50;
// Use unified search index for fuzzy search
if (this.searchIndex) {
const query = [
params.domain,
...(params.tags || []),
params.code_context
].filter(Boolean).join(' ');
if (query) {
const searchResults = this.searchIndex.search(query, { limit });
for (const result of searchResults) {
const searchResult = this.topicToSearchResult(result.item, result.score || 0);
results.push(searchResult);
}
}
}
// Filter by additional criteria
let filteredResults = results;
if (params.difficulty) {
filteredResults = filteredResults.filter(r => r.difficulty === params.difficulty);
}
if (params.bc_version) {
filteredResults = this.filterByBCVersion(filteredResults, params.bc_version);
}
return filteredResults.slice(0, limit);
}
/**
* Get layer statistics
*/
getLayerStatistics() {
const stats = this.layers.map(layer => layer.getStatistics());
const total = {
layers: stats.length,
totalTopics: stats.reduce((sum, s) => sum + s.topicCount, 0),
totalIndexes: stats.reduce((sum, s) => sum + s.indexCount, 0),
memoryUsage: stats.reduce((sum, s) => sum + (s.memoryUsage?.total || 0), 0)
};
return { layers: stats, total };
}
/**
* Get topics that are overridden in higher layers
*/
getOverriddenTopics() {
const overridden = {};
for (const topicId of this.getAllTopicIds()) {
const cached = this.topicCache.get(topicId);
if (cached?.isOverride) {
overridden[topicId] = cached;
}
}
return overridden;
}
/**
* Refresh layer cache (useful for development)
*/
async refreshCache() {
this.topicCache.clear();
if (this.cacheManager) {
this.cacheManager.clearAll();
}
this.initialized = false;
await this.initialize();
}
/**
* Get cache statistics for monitoring
*/
getCacheStats() {
if (!this.cacheManager) {
return { advanced_cache_enabled: false };
}
return {
advanced_cache_enabled: true,
...this.cacheManager.getStats()
};
}
/**
* Get a specific layer by name
*/
getLayer(layerName) {
return this.layers.find(layer => layer.name === layerName) || null;
}
/**
* Get all layers
*/
getLayers() {
return [...this.layers];
}
// Private helper methods
getTopicLayer(topicId) {
// Find highest priority layer that has this topic
const reversedLayers = [...this.layers].reverse();
return reversedLayers.find(layer => layer.hasTopic(topicId)) || null;
}
getTopicFromLayer(layer, topicId) {
if (!layer.hasTopic(topicId))
return null;
// Use the synchronous method to get already loaded topics
return layer.getTopicSync(topicId);
}
topicToSearchResult(topic, relevanceScore) {
const firstParagraph = topic.content.split('\n\n')[0]?.replace(/[#*`]/g, '').trim() || '';
const summary = firstParagraph.length > 200
? firstParagraph.substring(0, 200) + '...'
: firstParagraph;
const domains = getDomainList(topic.frontmatter.domain);
return {
id: topic.id,
title: topic.frontmatter.title,
domain: domains[0] || 'unknown',
domains: domains.length > 1 ? domains : undefined,
difficulty: topic.frontmatter.difficulty,
relevance_score: 1 - relevanceScore,
summary,
tags: topic.frontmatter.tags,
prerequisites: topic.frontmatter.prerequisites || [],
estimated_time: topic.frontmatter.estimated_time || undefined
};
}
filterByBCVersion(results, bcVersion) {
const requestedVersion = parseInt(bcVersion.replace(/\D/g, ''));
return results.filter(result => {
// This would need access to the actual topic to check bc_versions
// For now, accept all results
return true; // TODO: Implement proper BC version filtering
});
}
/**
* Get source type for cache TTL determination
*/
getSourceTypeForLayer(layer) {
if (!layer)
return 'embedded';
const layerType = layer.constructor.name;
switch (layerType) {
case 'GitKnowledgeLayer': return 'git';
case 'ProjectKnowledgeLayer': return 'local';
case 'EmbeddedKnowledgeLayer': return 'embedded';
default: return 'embedded';
}
}
/**
* Get all topics from all layers with resolution applied
*/
async getAllResolvedTopics() {
const allTopics = [];
const processedIds = new Set();
// Get all topic IDs from all layers
for (const layer of this.layers) {
const topicIds = layer.getTopicIds();
for (const topicId of topicIds) {
if (!processedIds.has(topicId)) {
processedIds.add(topicId);
const resolution = await this.resolveTopic(topicId);
if (resolution && resolution.topic) {
allTopics.push(resolution.topic);
}
}
}
}
return allTopics;
}
/**
* Warm up cache with frequently accessed topics
*/
async warmUpCache(config) {
if (!this.cacheManager)
return;
console.log('🔥 Warming up advanced cache...');
const warmUpTopics = [];
// Sample some topics from each layer for warm-up
for (const layer of this.layers) {
const topicIds = layer.getTopicIds().slice(0, 10); // First 10 topics per layer
const sourceType = this.getSourceTypeForLayer(layer);
for (const topicId of topicIds) {
try {
const topic = await layer.getTopic(topicId);
if (topic) {
warmUpTopics.push({
key: topicId,
topic,
sourceType
});
}
}
catch (error) {
// Skip topics that fail to load during warm-up
continue;
}
}
}
if (warmUpTopics.length > 0) {
await this.cacheManager.warmUpCache(warmUpTopics);
}
}
}
//# sourceMappingURL=layer-service.js.map