bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
991 lines • 40.8 kB
JavaScript
/**
* Enhanced Multi-Content Layer Service
*
* Extends the existing layer system to support specialists alongside
* atomic topics, enabling companies/teams/projects to have custom specialists
* that override or supplement the base specialist personas.
*/
export class MultiContentLayerService {
specialistResolutionStrategy;
layers = new Map();
layerPriorities = []; // Ordered by priority (high to low)
contentCache = new Map();
initialized = false;
availableMcps = []; // Track available MCP servers for conditional topics
constructor(specialistResolutionStrategy = {
conflict_resolution: 'override',
inherit_collaborations: true,
merge_expertise: false
}) {
this.specialistResolutionStrategy = specialistResolutionStrategy;
}
/**
* Set available MCP servers for conditional topic filtering
*/
setAvailableMcps(mcps) {
this.availableMcps = mcps;
this.clearCache(); // Clear cache when MCP availability changes
}
/**
* Get currently available MCP servers
*/
getAvailableMcps() {
return [...this.availableMcps];
}
/**
* Filter topic based on conditional_mcp frontmatter
*/
shouldIncludeTopic(topic) {
const fm = topic.frontmatter;
// Positive conditional: include only if MCP present
if (fm.conditional_mcp) {
return this.availableMcps.includes(fm.conditional_mcp);
}
// Negative conditional: include only if MCP absent
if (fm.conditional_mcp_missing) {
return !this.availableMcps.includes(fm.conditional_mcp_missing);
}
// No conditional: always include
return true;
}
/**
* Add a layer to the service
*/
addLayer(layer) {
this.layers.set(layer.name, layer);
this.updateLayerPriorities();
this.clearCache();
}
/**
* Initialize all layers
*/
async initialize() {
const results = new Map();
console.error('🚀 Initializing multi-content layer service...');
for (const [name, layer] of this.layers) {
try {
console.error(`📋 Initializing layer: ${name}`);
let result = await layer.initialize();
// Convert legacy LayerLoadResult to EnhancedLayerLoadResult if needed
if (result && !result.content_counts && result.topicsLoaded !== undefined) {
// This is a legacy LayerLoadResult, convert to enhanced format
result = {
success: result.success,
layer_name: result.layerName,
load_time_ms: result.loadTimeMs,
content_counts: {
topics: result.topicsLoaded || 0,
specialists: 0, // Will be updated by layer-specific logic
methodologies: result.indexesLoaded || 0
},
topics_loaded: result.topicsLoaded || 0,
indexes_loaded: result.indexesLoaded || 0,
error: result.success ? undefined : 'Layer load failed'
};
// For embedded layer, update specialist count from the layer itself
if (layer.name === 'embedded' && 'specialists' in layer && layer.specialists instanceof Map) {
result.content_counts.specialists = layer.specialists.size;
}
}
results.set(name, result);
if (result.success) {
const contentCounts = result.content_counts || {};
console.error(`✅ Layer ${name}: ${Object.entries(contentCounts)
.map(([type, count]) => `${count} ${type}`)
.join(', ')}`);
}
else {
console.error(`❌ Layer ${name} failed: ${result.error}`);
}
}
catch (error) {
console.error(`❌ Layer ${name} initialization error:`, error);
results.set(name, {
success: false,
layer_name: name,
load_time_ms: 0,
content_counts: { topics: 0, specialists: 0, methodologies: 0 },
topics_loaded: 0,
indexes_loaded: 0,
error: error instanceof Error ? error.message : String(error)
});
}
}
this.initialized = true;
console.error(`🎯 Multi-content layer service initialized with ${this.layers.size} layers`);
return results;
}
/**
* Get a specialist by ID with layer resolution
*/
async getSpecialist(specialistId) {
if (!this.initialized) {
await this.initialize();
}
// Check cache first
const cached = this.getCachedContent('specialists', specialistId);
if (cached) {
return cached;
}
// Search layers in priority order (highest priority first)
for (const layerName of this.layerPriorities) {
const layer = this.layers.get(layerName);
if (!layer || !(layer.supported_content_types?.includes('specialists'))) {
continue;
}
const specialist = await layer.getContent('specialists', specialistId);
if (specialist) {
this.setCachedContent('specialists', specialistId, specialist);
return specialist;
}
}
return null;
}
/**
* Get all specialists across all layers with resolution
*/
async getAllSpecialists() {
if (!this.initialized) {
await this.initialize();
}
const specialistMap = new Map();
// Process layers in reverse priority order (lowest priority first)
// so higher priority layers can override
const reversePriorities = [...this.layerPriorities].reverse();
for (const layerName of reversePriorities) {
const layer = this.layers.get(layerName);
if (!layer || !(layer.supported_content_types?.includes('specialists'))) {
continue;
}
const layerSpecialistIds = layer.getContentIds('specialists');
for (const specialistId of layerSpecialistIds) {
const specialist = await layer.getContent('specialists', specialistId);
if (specialist) {
if (specialistMap.has(specialistId)) {
// Apply resolution strategy
const existing = specialistMap.get(specialistId);
const resolved = this.resolveSpecialistConflict(existing, specialist, layerName);
specialistMap.set(specialistId, resolved);
}
else {
specialistMap.set(specialistId, specialist);
}
}
}
}
return Array.from(specialistMap.values());
}
/**
* Suggest specialists based on context across all layers
*/
async suggestSpecialists(context, queryContext, limit = 5) {
const allSpecialists = await this.getAllSpecialists();
const suggestions = [];
for (const specialist of allSpecialists) {
const score = this.calculateSpecialistScore(specialist, context, queryContext);
if (score > 0) {
const collaborationOptions = await this.getCollaborationOptions(specialist);
suggestions.push({
specialist,
source_layer: this.findSpecialistSourceLayer(specialist.specialist_id),
confidence_score: score,
match_reasons: this.getMatchReasons(specialist, context, queryContext),
collaboration_options: collaborationOptions
});
}
}
// Sort by confidence score
suggestions.sort((a, b) => b.confidence_score - a.confidence_score);
const primarySuggestions = suggestions.slice(0, limit);
const alternativeSuggestions = suggestions.slice(limit, limit * 2);
return {
primary_suggestions: primarySuggestions,
alternative_specialists: alternativeSuggestions,
cross_layer_collaboration: await this.getCrossLayerCollaboration(primarySuggestions),
resolution_strategy_used: this.specialistResolutionStrategy
};
}
/**
* Get specialists from a specific layer
*/
async getSpecialistsByLayer(layerName) {
const layer = this.layers.get(layerName);
if (!layer || !layer.supported_content_types.includes('specialists')) {
return [];
}
const specialistIds = layer.getContentIds('specialists');
const specialists = [];
for (const id of specialistIds) {
const specialist = await layer.getContent('specialists', id);
if (specialist) {
specialists.push(specialist);
}
}
return specialists;
}
/**
* Get layer statistics including specialist counts
*/
getLayerStatistics() {
const stats = {};
for (const [name, layer] of this.layers) {
stats[name] = layer.getEnhancedStatistics();
}
return stats;
}
/**
* Resolve specialist conflicts between layers
*/
resolveSpecialistConflict(existing, incoming, incomingLayerName) {
switch (this.specialistResolutionStrategy.conflict_resolution) {
case 'override':
return incoming; // Higher priority layer wins
case 'merge':
return this.mergeSpecialists(existing, incoming);
case 'extend':
return this.extendSpecialist(existing, incoming);
default:
return incoming;
}
}
/**
* Merge two specialist definitions
*/
mergeSpecialists(base, override) {
return {
...base,
...override,
persona: {
...base.persona,
...override.persona,
personality: [
...new Set([...base.persona.personality, ...override.persona.personality])
]
},
expertise: {
primary: [...new Set([...base.expertise.primary, ...override.expertise.primary])],
secondary: [...new Set([...base.expertise.secondary, ...override.expertise.secondary])]
},
domains: [...new Set([...base.domains, ...override.domains])],
when_to_use: [...new Set([...base.when_to_use, ...override.when_to_use])],
collaboration: {
natural_handoffs: this.specialistResolutionStrategy.inherit_collaborations
? [...new Set([...base.collaboration.natural_handoffs, ...override.collaboration.natural_handoffs])]
: override.collaboration.natural_handoffs,
team_consultations: this.specialistResolutionStrategy.inherit_collaborations
? [...new Set([...base.collaboration.team_consultations, ...override.collaboration.team_consultations])]
: override.collaboration.team_consultations
},
related_specialists: [...new Set([...base.related_specialists, ...override.related_specialists])]
};
}
/**
* Extend specialist with additional capabilities
*/
extendSpecialist(base, extension) {
// Similar to merge but preserves base identity more strongly
return {
...base,
// Only extend non-identity fields
expertise: {
primary: base.expertise.primary, // Keep base primary expertise
secondary: [...new Set([...base.expertise.secondary, ...extension.expertise.secondary])]
},
domains: [...new Set([...base.domains, ...extension.domains])],
when_to_use: [...new Set([...base.when_to_use, ...extension.when_to_use])],
collaboration: {
natural_handoffs: [...new Set([...base.collaboration.natural_handoffs, ...extension.collaboration.natural_handoffs])],
team_consultations: [...new Set([...base.collaboration.team_consultations, ...extension.collaboration.team_consultations])]
},
related_specialists: [...new Set([...base.related_specialists, ...extension.related_specialists])],
content: `${base.content}\n\n## Extended Capabilities\n${extension.content}`
};
}
/**
* Calculate specialist relevance score
*/
calculateSpecialistScore(specialist, context, queryContext) {
let score = 0;
const contextLower = context.toLowerCase();
// Check when_to_use scenarios
for (const scenario of specialist.when_to_use) {
if (contextLower.includes(scenario.toLowerCase()) ||
scenario.toLowerCase().includes(contextLower)) {
score += 10;
}
}
// Check expertise areas
for (const expertise of specialist.expertise.primary) {
if (contextLower.includes(expertise.toLowerCase())) {
score += 8;
}
}
for (const expertise of specialist.expertise.secondary) {
if (contextLower.includes(expertise.toLowerCase())) {
score += 5;
}
}
// Check domains
for (const domain of specialist.domains) {
if (contextLower.includes(domain.toLowerCase())) {
score += 6;
}
}
// Apply query context modifiers
if (queryContext) {
if (queryContext.domain && specialist.domains.includes(queryContext.domain)) {
score += 15;
}
if (queryContext.urgency === 'high') {
// Prefer specialists with quick response traits
if (specialist.persona.personality.some(p => p.toLowerCase().includes('quick') ||
p.toLowerCase().includes('direct') ||
p.toLowerCase().includes('efficient'))) {
score += 5;
}
}
}
return score;
}
/**
* Get match reasons for a specialist suggestion
*/
getMatchReasons(specialist, context, queryContext) {
const reasons = [];
const contextLower = context.toLowerCase();
// Check expertise matches
const primaryMatches = specialist.expertise.primary.filter(e => contextLower.includes(e.toLowerCase()));
if (primaryMatches.length > 0) {
reasons.push(`Primary expertise: ${primaryMatches.join(', ')}`);
}
// Check scenario matches
const scenarioMatches = specialist.when_to_use.filter(s => contextLower.includes(s.toLowerCase()) || s.toLowerCase().includes(contextLower));
if (scenarioMatches.length > 0) {
reasons.push(`Relevant scenarios: ${scenarioMatches.join(', ')}`);
}
// Check domain matches
const domainMatches = specialist.domains.filter(d => contextLower.includes(d.toLowerCase()));
if (domainMatches.length > 0) {
reasons.push(`Domain expertise: ${domainMatches.join(', ')}`);
}
return reasons;
}
/**
* Get collaboration options for a specialist
*/
async getCollaborationOptions(specialist) {
const handoffs = [];
const consultations = [];
// Get handoff specialists
for (const handoffId of specialist.collaboration.natural_handoffs) {
const handoffSpecialist = await this.getSpecialist(handoffId);
if (handoffSpecialist) {
handoffs.push(handoffSpecialist);
}
}
// Get consultation specialists
for (const consultationId of specialist.collaboration.team_consultations) {
const consultationSpecialist = await this.getSpecialist(consultationId);
if (consultationSpecialist) {
consultations.push(consultationSpecialist);
}
}
return { available_handoffs: handoffs, recommended_consultations: consultations };
}
/**
* Get cross-layer collaboration opportunities
*/
async getCrossLayerCollaboration(suggestions) {
const crossLayerCollab = [];
for (const [layerName] of this.layers) {
const layerSpecialists = await this.getSpecialistsByLayer(layerName);
if (layerSpecialists.length > 0) {
crossLayerCollab.push({
layer_name: layerName,
specialists: layerSpecialists
});
}
}
return crossLayerCollab;
}
/**
* Find which layer a specialist comes from
*/
findSpecialistSourceLayer(specialistId) {
for (const layerName of this.layerPriorities) {
const layer = this.layers.get(layerName);
if (layer?.hasContent('specialists', specialistId)) {
return layerName;
}
}
return 'unknown';
}
/**
* Update layer priority ordering
*/
updateLayerPriorities() {
this.layerPriorities = Array.from(this.layers.values())
.sort((a, b) => a.priority - b.priority) // Lower number = higher priority
.map(layer => layer.name);
}
/**
* Cache management
*/
getCachedContent(type, id) {
return this.contentCache.get(type)?.get(id);
}
setCachedContent(type, id, content) {
if (!this.contentCache.has(type)) {
this.contentCache.set(type, new Map());
}
this.contentCache.get(type).set(id, content);
}
clearCache() {
this.contentCache.clear();
}
/**
* Search topics across all layers
*/
async searchTopics(params) {
if (!this.initialized) {
await this.initialize();
}
const results = [];
const limit = params.limit || 50;
// Search across all layers in priority order
for (const layerName of this.layerPriorities) {
const layer = this.layers.get(layerName);
if (!layer)
continue;
try {
// Get all topic IDs from this layer
const topicIds = layer.getContentIds('topics');
// Load each topic and filter
for (const topicId of topicIds) {
const topic = await layer.getContent('topics', topicId);
if (topic && this.shouldIncludeTopic(topic) && this.matchesSearchCriteria(topic, params)) {
try {
const score = this.calculateRelevanceScore(topic, params);
const searchResult = this.topicToSearchResult(topic, score);
results.push(searchResult);
// Note: Don't break early here - let all matching topics be scored
// The final limit will be applied after sorting by relevance
}
catch (scoreError) {
console.error(`Error scoring topic ${topic.title}:`, scoreError);
// Skip this topic if scoring fails
}
}
}
}
catch (error) {
console.error(`Error searching topics in layer ${layerName}:`, error);
// Continue with other layers
}
}
// Sort by relevance score and apply limit
return results
.sort((a, b) => b.relevance_score - a.relevance_score)
.slice(0, limit);
}
/**
* Find specialists by query across all layers
*/
async findSpecialistsByQuery(query) {
if (!this.initialized) {
await this.initialize();
}
const results = [];
const queryLower = query.toLowerCase();
// Search across all layers in priority order
for (const layerName of this.layerPriorities) {
const layer = this.layers.get(layerName);
if (!layer || !layer.supported_content_types || !layer.supported_content_types.includes('specialists')) {
continue;
}
try {
// Get all specialist IDs from this layer
const specialistIds = layer.getContentIds('specialists');
// Load each specialist and check for matches
for (const specialistId of specialistIds) {
const specialist = await layer.getContent('specialists', specialistId);
if (specialist && this.matchesSpecialistQuery(specialist, queryLower)) {
results.push(specialist);
}
}
}
catch (error) {
console.error(`Error searching specialists in layer ${layerName}:`, error);
// Continue with other layers
}
}
return results;
}
/**
* Check if specialist matches query using token-based matching
*
* Fixes Issue #17: Complex compound questions now tokenized for matching.
* Instead of requiring full query as substring, matches any individual token.
*/
matchesSpecialistQuery(specialist, queryLower) {
const searchableFields = [
specialist.title,
specialist.role,
specialist.specialist_id,
...(specialist.expertise?.primary || []),
...(specialist.expertise?.secondary || []),
...(specialist.domains || []),
...(specialist.when_to_use || [])
].filter(Boolean).map(field => field.toLowerCase());
// Tokenize query into individual keywords (filter out short words)
const queryTokens = queryLower
.split(/[\s,]+/)
.filter(token => token.length > 3)
.map(token => token.replace(/[^a-z0-9]/g, ''));
// Match if ANY query token matches ANY searchable field (bidirectional partial matching)
return queryTokens.some(token => searchableFields.some(field => field.includes(token) || token.includes(field)));
}
/**
* Ask a specialist a question (simulated consultation)
*/
async askSpecialist(question, preferredSpecialist) {
if (!this.initialized) {
await this.initialize();
}
let specialist = null;
// Try to find the preferred specialist first
if (preferredSpecialist) {
specialist = await this.findSpecialistById(preferredSpecialist);
}
// If no preferred specialist or not found, find best match
if (!specialist) {
const specialists = await this.findSpecialistsByQuery(question);
specialist = specialists[0] || null;
}
if (!specialist) {
throw new Error('No suitable specialist found for this question');
}
// Return a consultation response
return {
specialist: {
id: specialist.specialist_id,
name: specialist.title,
role: specialist.role
},
response: `${specialist.title} would approach this question: "${question}" using their expertise in ${specialist.expertise?.primary?.join(', ') || 'general BC development'}.`,
consultation_guidance: specialist.content.substring(0, 200) + '...' || 'General BC development guidance',
follow_up_suggestions: specialist.related_specialists || []
};
}
/**
* Find specialist by ID across all layers
*/
async findSpecialistById(specialistId) {
for (const layerName of this.layerPriorities) {
const layer = this.layers.get(layerName);
if (!layer || !layer.supported_content_types.includes('specialists')) {
continue;
}
try {
if (layer.hasContent('specialists', specialistId)) {
return await layer.getContent('specialists', specialistId);
}
}
catch (error) {
console.error(`Error finding specialist ${specialistId} in layer ${layerName}:`, error);
// Continue with other layers
}
}
return null;
}
/**
* Check if topic matches search criteria
*/
matchesSearchCriteria(topic, params) {
// Text matching using code_context parameter
if (params.code_context) {
const searchTerm = params.code_context.toLowerCase();
const title = topic.title.toLowerCase();
const content = topic.content.toLowerCase();
const tags = (topic.frontmatter.tags || []).map(tag => tag.toLowerCase());
// Prioritize individual word matching over full phrase matching
const searchWords = searchTerm.split(' ').filter(word => word.length > 2);
// Check individual words first (more flexible)
const wordMatches = searchWords.some(word => title.includes(word) || content.includes(word) || tags.some(tag => tag.includes(word)));
// Also check exact phrase match (bonus case)
const exactPhraseMatch = title.includes(searchTerm) ||
content.includes(searchTerm) ||
tags.some(tag => tag.includes(searchTerm));
const matches = wordMatches || exactPhraseMatch;
if (!matches) {
return false;
}
}
// Domain filtering
if (params.domain) {
const topicDomains = Array.isArray(topic.frontmatter.domain)
? topic.frontmatter.domain
: topic.frontmatter.domain ? [topic.frontmatter.domain] : [];
if (!topicDomains.includes(params.domain)) {
return false;
}
}
// BC version filtering
if (params.bc_version && topic.frontmatter.bc_versions) {
const bcVersions = topic.frontmatter.bc_versions;
const requestedVersion = params.bc_version;
// Handle version ranges like "14+", "18+", "BC14+", "BC18+"
const rangeMatch = bcVersions.match(/^(?:BC)?(\d+)\+$/);
if (rangeMatch) {
const minVersion = parseInt(rangeMatch[1], 10);
const requestedVersionNum = parseInt(requestedVersion.replace(/^BC/, ''), 10);
if (requestedVersionNum < minVersion) {
return false; // Requested version is below minimum
}
}
else if (!bcVersions.includes(requestedVersion)) {
// Exact version match required if not a range
return false;
}
}
// Difficulty filtering
if (params.difficulty && topic.frontmatter.difficulty !== params.difficulty) {
return false;
}
// Tag filtering
if (params.tags && params.tags.length > 0) {
const topicTags = topic.frontmatter.tags || [];
if (!params.tags.some(tag => topicTags.includes(tag))) {
return false;
}
}
return true;
}
/**
* Calculate relevance score for topic
*/
calculateRelevanceScore(topic, params) {
let score = 0; // Start with 0, add points for relevance
// Text relevance scoring - the most important factor
if (params.code_context) {
const searchTerm = params.code_context.toLowerCase();
const searchWords = searchTerm.split(/\s+/).filter(word => word.length > 2);
const title = topic.title.toLowerCase();
const content = topic.content.toLowerCase();
const tags = (topic.frontmatter.tags || []).map(tag => tag.toLowerCase());
// **MAJOR FIX**: Prioritize exact phrase matches and semantic clusters
// 1. Exact phrase match in title (highest priority)
if (title.includes(searchTerm)) {
score += 100; // Massive bonus for exact phrase in title
}
// 2. High-value word combinations in title
let titleWordMatches = 0;
for (const word of searchWords) {
if (title.includes(word)) {
titleWordMatches++;
score += 15; // Higher than before for title matches
}
}
// 3. Title clustering bonus - reward multiple words in title
if (titleWordMatches > 1) {
score += titleWordMatches * titleWordMatches * 10; // Exponential bonus for clustering
}
// 4. Semantic tag matches (better than content)
let tagMatches = 0;
for (const word of searchWords) {
for (const tag of tags) {
if (tag.includes(word)) {
tagMatches++;
score += 12; // Increased from 8
}
}
}
// 5. Tag clustering bonus
if (tagMatches > 1) {
score += tagMatches * 8;
}
// 6. Content matches (lower priority than before)
for (const word of searchWords) {
const matches = (content.match(new RegExp(`\\b${word}\\b`, 'g')) || []).length;
score += Math.min(matches * 1.5, 4); // Reduced from 2 and 6
}
// 7. **NEW**: Exact phrase in content
if (content.includes(searchTerm)) {
score += 20;
}
// 8. **NEW**: Multi-word phrase detection in title (highest semantic value)
if (searchWords.length > 1) {
// Check for consecutive word sequences in title
for (let i = 0; i < searchWords.length - 1; i++) {
const phrase = searchWords.slice(i, i + 2).join(' ');
if (title.includes(phrase)) {
score += 50; // Massive bonus for 2-word phrases in title
}
}
// Check for 3+ word phrases
for (let i = 0; i < searchWords.length - 2; i++) {
const phrase = searchWords.slice(i, i + 3).join(' ');
if (title.includes(phrase)) {
score += 100; // Even bigger bonus for longer phrases
}
}
}
// 9. **NEW**: Semantic relevance bonus for technical terms
const technicalTerms = ['al', 'naming', 'convention', 'field', 'table', 'extension'];
const queryTechTerms = searchWords.filter(word => technicalTerms.includes(word));
const topicTechTerms = [...title.split(/\s+/), ...tags.join(' ').split(/\s+/)]
.filter(word => technicalTerms.includes(word.toLowerCase()));
const techTermOverlap = queryTechTerms.filter(term => topicTechTerms.some(topicTerm => topicTerm.toLowerCase().includes(term))).length;
if (techTermOverlap > 0) {
score += techTermOverlap * 25; // High bonus for technical term matches
}
// 10. Penalty for very long content (prefer focused topics)
if (topic.content.length > 5000) {
score *= 0.8; // Increased penalty
}
}
// Domain exact match bonus (secondary factor)
if (params.domain) {
const topicDomains = Array.isArray(topic.frontmatter.domain)
? topic.frontmatter.domain
: topic.frontmatter.domain ? [topic.frontmatter.domain] : [];
if (topicDomains.includes(params.domain)) {
score += 5;
}
}
// BC version exact match bonus
if (params.bc_version && topic.frontmatter.bc_versions?.includes(params.bc_version)) {
score += 3;
}
// Difficulty match bonus
if (params.difficulty && topic.frontmatter.difficulty === params.difficulty) {
score += 2;
}
// Tag match bonus (from explicit tag parameters)
if (params.tags && params.tags.length > 0) {
const topicTags = topic.frontmatter.tags || [];
const matchingTags = params.tags.filter(tag => topicTags.includes(tag));
score += matchingTags.length * 2;
}
// **NEW**: Boost recommendation topics when their conditional is active
// These are high-value suggestions that guide users to install missing tools
try {
const fm = topic?.frontmatter;
if (fm?.conditional_mcp_missing && Array.isArray(this.availableMcps)) {
if (!this.availableMcps.includes(fm.conditional_mcp_missing)) {
// Recommendation topic is active (tool is missing) - boost HEAVILY to surface prominently
score += 150; // Large boost to ensure recommendations surface in top results
}
}
}
catch (boostError) {
// Silent fail - don't break scoring if boost logic fails
console.error('Error applying recommendation boost:', boostError);
}
return Math.max(score, 0.1); // Ensure minimum score > 0
}
/**
* Convert topic to search result
*/
topicToSearchResult(topic, relevanceScore) {
const primaryDomain = Array.isArray(topic.frontmatter.domain)
? topic.frontmatter.domain[0]
: topic.frontmatter.domain || '';
const allDomains = Array.isArray(topic.frontmatter.domain)
? topic.frontmatter.domain
: topic.frontmatter.domain ? [topic.frontmatter.domain] : [];
return {
id: topic.id,
title: topic.title,
summary: topic.content.substring(0, 200) + '...', // First 200 chars as summary
domain: primaryDomain,
domains: allDomains,
difficulty: topic.frontmatter.difficulty || 'beginner',
relevance_score: relevanceScore,
tags: topic.frontmatter.tags || [],
prerequisites: topic.frontmatter.prerequisites || [],
estimated_time: topic.frontmatter.estimated_time
};
}
/**
* Get a specific layer by name (adapted from LayerService)
*/
getLayer(layerName) {
return this.layers.get(layerName) || null;
}
/**
* Get all layers (adapted from LayerService)
*/
getLayers() {
return Array.from(this.layers.values());
}
/**
* Get all available topic IDs from all layers (adapted from LayerService)
*/
getAllTopicIds() {
const topicIds = new Set();
for (const layer of this.layers.values()) {
if ('getTopicIds' in layer) {
for (const topicId of layer.getTopicIds()) {
topicIds.add(topicId);
}
}
}
return Array.from(topicIds);
}
/**
* Resolve a topic with layer override logic (adapted from LayerService)
*/
async resolveTopic(topicId) {
if (!this.initialized) {
await this.initialize();
}
// Find the highest priority layer that has this topic
let resolvedTopic = null;
let sourceLayer = '';
// Go through layers in priority order (highest first)
for (const layerName of this.layerPriorities) {
const layer = this.layers.get(layerName);
if (layer && 'getTopic' in layer) {
try {
const topic = await layer.getTopic(topicId);
if (topic) {
resolvedTopic = topic;
sourceLayer = layerName;
break; // Highest priority wins
}
}
catch (error) {
// Continue to next layer
}
}
}
if (resolvedTopic) {
return {
topic: resolvedTopic,
sourceLayer,
isOverride: false,
overriddenLayers: []
};
}
return null;
}
/**
* Get all resolved topics (adapted from LayerService)
*/
async getAllResolvedTopics() {
const allTopics = [];
const topicIds = this.getAllTopicIds();
for (const topicId of topicIds) {
const resolution = await this.resolveTopic(topicId);
if (resolution && resolution.topic) {
allTopics.push(resolution.topic);
}
}
return allTopics;
}
/**
* Get overridden topics statistics (adapted from LayerService)
*/
getOverriddenTopics() {
// For now, return empty stats - can be enhanced later
return {
totalOverrides: 0,
overridesByLayer: {},
conflictResolutions: []
};
}
/**
* Get layer statistics - adapts new enhanced statistics to old LayerStatistics format
*/
getStatistics() {
return Array.from(this.layers.values()).map(layer => {
// Try to use getEnhancedStatistics if available, otherwise fall back to basic stats
if ('getEnhancedStatistics' in layer && typeof layer.getEnhancedStatistics === 'function') {
const enhanced = layer.getEnhancedStatistics();
return {
name: enhanced.name,
priority: enhanced.priority,
enabled: layer.enabled,
topicCount: enhanced.content_counts.topics || 0,
indexCount: enhanced.content_counts.methodologies || 0,
lastLoaded: enhanced.initialized ? new Date() : undefined,
loadTimeMs: enhanced.load_time_ms,
memoryUsage: {
topics: 0,
indexes: 0,
total: 0
}
};
}
else {
// Fallback to standard layer properties
const layerAsAny = layer;
return {
name: layer.name,
priority: layer.priority,
enabled: layer.enabled,
topicCount: layerAsAny.topics?.size || 0,
indexCount: layerAsAny.indexes?.size || 0,
lastLoaded: undefined,
loadTimeMs: undefined,
memoryUsage: {
topics: 0,
indexes: 0,
total: 0
}
};
}
});
}
/**
* Initialize from configuration (adapted from LayerService)
*/
async initializeFromConfiguration(config) {
// For now, just call regular initialize
// This can be enhanced to handle configuration-based layer loading
return await this.initialize();
}
/**
* Get session storage configuration (adapted from LayerService)
*/
getSessionStorageConfig() {
return {
enabled: false,
type: 'memory',
options: {}
};
}
/**
* Refresh cache (adapted from LayerService)
*/
async refreshCache() {
this.clearCache();
// Force re-initialization if needed
if (this.initialized) {
await this.initialize();
}
}
/**
* Get cache statistics (adapted from LayerService)
*/
getCacheStats() {
return {
topicCacheSize: this.contentCache.size,
layerCount: this.layers.size,
totalMemoryUsage: 0 // Could be calculated if needed
};
}
/**
* Dispose of all layers
*/
async dispose() {
for (const layer of this.layers.values()) {
await layer.dispose();
}
this.layers.clear();
this.clearCache();
this.initialized = false;
}
}
//# sourceMappingURL=multi-content-layer-service.js.map