mongodb-memory-bank-mcp
Version:
FIXED: MongoDB Memory Bank MCP with bulletproof error handling, smart operations, and session state management. Eliminates [object Object] errors and user confusion.
282 lines (281 loc) • 11 kB
JavaScript
import { getTemplate } from '../entities/memory-templates.js';
/**
* Template Validation Service
* Validates memory content against structured templates
* Designed to work in perfect harmony with existing MongoDB memory system
*/
export class TemplateValidationService {
/**
* Validate memory content against its template
*/
async validateMemoryContent(content, memoryType, fileName) {
const template = getTemplate(memoryType);
const errors = [];
const warnings = [];
const suggestions = [];
try {
// Validate template structure
const structureValidation = this.validateStructure(content, template);
errors.push(...structureValidation.errors);
warnings.push(...structureValidation.warnings);
// Validate required sections
const sectionValidation = this.validateRequiredSections(content, template);
errors.push(...sectionValidation.errors);
warnings.push(...sectionValidation.warnings);
// Validate content quality
const qualityValidation = this.validateContentQuality(content, template);
warnings.push(...qualityValidation.warnings);
suggestions.push(...qualityValidation.suggestions);
// Validate filename conventions
const filenameValidation = this.validateFilename(fileName, memoryType);
if (filenameValidation.error) {
errors.push(filenameValidation.error);
}
if (filenameValidation.warning) {
warnings.push(filenameValidation.warning);
}
return {
isValid: errors.length === 0,
errors,
warnings,
suggestions
};
}
catch (error) {
return {
isValid: false,
errors: [{
field: 'validation',
message: `Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
severity: 'error'
}],
warnings: [],
suggestions: []
};
}
}
/**
* Validate overall template structure
*/
validateStructure(content, template) {
const errors = [];
const warnings = [];
// Check if content is not empty
if (!content || content.trim().length === 0) {
errors.push({
field: 'content',
message: 'Content cannot be empty',
severity: 'error'
});
return { errors, warnings };
}
// Check for basic markdown structure (if template expects markdown)
const hasMarkdownSections = template.schema.sections.some(s => s.format === 'markdown');
if (hasMarkdownSections && !this.hasMarkdownHeaders(content)) {
warnings.push({
field: 'structure',
message: 'Content should include markdown headers for better organization',
suggestion: 'Add section headers using # or ## markdown syntax'
});
}
return { errors, warnings };
}
/**
* Validate required sections are present
*/
validateRequiredSections(content, template) {
const errors = [];
const warnings = [];
const requiredSections = template.schema.sections.filter(s => s.required);
for (const section of requiredSections) {
if (!this.sectionExists(content, section)) {
errors.push({
field: section.name,
message: `Required section "${section.name}" is missing`,
severity: 'error'
});
}
}
// Check for optional sections that might be beneficial
const optionalSections = template.schema.sections.filter(s => !s.required);
for (const section of optionalSections) {
if (!this.sectionExists(content, section)) {
warnings.push({
field: section.name,
message: `Optional section "${section.name}" could enhance the memory`,
suggestion: `Consider adding: ${section.description}`
});
}
}
return { errors, warnings };
}
/**
* Validate content quality and completeness
*/
validateContentQuality(content, template) {
const warnings = [];
const suggestions = [];
// Check content length
const wordCount = content.split(/\s+/).length;
if (wordCount < 50) {
warnings.push({
field: 'content',
message: 'Content seems quite brief',
suggestion: 'Consider adding more detail to make the memory more useful'
});
}
// Check for placeholder content
const placeholderPatterns = [
/\[.*?\]/g, // [placeholder text]
/TODO/gi, // TODO items
/FIXME/gi, // FIXME items
/\.\.\./g // ellipsis
];
for (const pattern of placeholderPatterns) {
if (pattern.test(content)) {
warnings.push({
field: 'content',
message: 'Content contains placeholder text',
suggestion: 'Replace placeholder text with actual content'
});
break;
}
}
// Suggest improvements based on template type
suggestions.push(...this.getTemplateSpecificSuggestions(content, template));
return { warnings, suggestions };
}
/**
* Validate filename follows conventions
*/
validateFilename(fileName, memoryType) {
// Check file extension
if (!fileName.endsWith('.md')) {
return {
error: {
field: 'fileName',
message: 'Memory files should have .md extension',
severity: 'error'
}
};
}
// Check naming convention
const expectedName = `${memoryType}.md`;
if (fileName !== expectedName) {
return {
warning: {
field: 'fileName',
message: `Filename "${fileName}" doesn't follow convention`,
suggestion: `Consider using "${expectedName}" for consistency`
}
};
}
return {};
}
/**
* Check if content has markdown headers
*/
hasMarkdownHeaders(content) {
return /^#+\s+.+$/m.test(content);
}
/**
* Check if a section exists in the content
*/
sectionExists(content, section) {
// Look for section header in various formats
const patterns = [
new RegExp(`^#+\\s*${section.name}\\s*$`, 'mi'), // # Section Name
new RegExp(`^\\*\\*${section.name}\\*\\*`, 'mi'), // **Section Name**
new RegExp(`${section.name}:`, 'mi') // Section Name:
];
return patterns.some(pattern => pattern.test(content));
}
/**
* Get template-specific suggestions
*/
getTemplateSpecificSuggestions(content, template) {
const suggestions = [];
switch (template.type) {
case 'project-brief':
if (!content.includes('Success Criteria')) {
suggestions.push('Add measurable success criteria to track project goals');
}
break;
case 'system-patterns':
if (!content.includes('Architecture') && !content.includes('Pattern')) {
suggestions.push('Include architectural patterns and design decisions');
}
break;
case 'tech-context':
if (!content.includes('version') && !content.includes('Version')) {
suggestions.push('Include version information for technologies and dependencies');
}
break;
case 'active-context':
if (!content.includes(new Date().getFullYear().toString())) {
suggestions.push('Include current date/timestamp for active context relevance');
}
break;
case 'code-patterns':
if (!content.includes('```') && !content.includes('`')) {
suggestions.push('Include code examples to illustrate patterns');
}
break;
case 'error-solutions':
if (!content.includes('Solution') && !content.includes('Fix')) {
suggestions.push('Include specific solutions and fixes for each error');
}
break;
case 'implementation-rules':
if (!content.includes('AI') && !content.includes('rule')) {
suggestions.push('Include specific rules for AI behavior and decision making');
}
break;
}
return suggestions;
}
/**
* Generate template content with placeholders
*/
generateTemplateContent(memoryType, projectName) {
const template = getTemplate(memoryType);
let content = template.defaultContent;
// Replace dynamic placeholders
if (projectName) {
content = content.replace(/\[Project Name\]/g, projectName);
}
content = content.replace(/\${new Date\(\)\.toISOString\(\)\.split\('T'\)\[0\]\}/g, new Date().toISOString().split('T')[0]);
return content;
}
/**
* Extract structured data from content
*/
extractStructuredData(content, memoryType) {
const template = getTemplate(memoryType);
const structuredData = {};
// Extract sections based on template schema
for (const section of template.schema.sections) {
const sectionContent = this.extractSectionContent(content, section.name);
if (sectionContent) {
structuredData[section.name] = sectionContent;
}
}
return structuredData;
}
/**
* Extract content for a specific section
*/
extractSectionContent(content, sectionName) {
// Look for section header and extract content until next header
const headerPattern = new RegExp(`^#+\\s*${sectionName}\\s*$`, 'mi');
const match = content.match(headerPattern);
if (!match)
return null;
const startIndex = match.index + match[0].length;
const nextHeaderPattern = /^#+\s+/gm;
nextHeaderPattern.lastIndex = startIndex;
const nextMatch = nextHeaderPattern.exec(content);
const endIndex = nextMatch ? nextMatch.index : content.length;
return content.slice(startIndex, endIndex).trim();
}
}