/**
* Cell Validator for Matrix Pattern System
* Provides comprehensive validation for cell content, format, and structure
*/
import { z } from 'zod';
// Validation schemas for different horizontal types
const HorizontalTypeSchema = z.enum([
'specification',
'requirements',
'architecture',
'implementation',
'testing',
'documentation',
'deployment',
'maintenance',
'research',
'analysis',
'design',
'review'
]);
// Required sections schema based on horizontal type
const RequiredSectionsSchema = z.record(
HorizontalTypeSchema,
z.array(z.string())
);
// Default required sections for each horizontal type
const DEFAULT_REQUIRED_SECTIONS = {
specification: ['overview', 'requirements', 'acceptance_criteria'],
requirements: ['functional', 'non_functional', 'constraints'],
architecture: ['components', 'patterns', 'interfaces'],
implementation: ['code', 'logic', 'structure'],
testing: ['test_cases', 'coverage', 'validation'],
documentation: ['description', 'usage', 'examples'],
deployment: ['environment', 'configuration', 'procedures'],
maintenance: ['monitoring', 'updates', 'support'],
research: ['hypothesis', 'methodology', 'findings'],
analysis: ['data', 'insights', 'recommendations'],
design: ['concepts', 'wireframes', 'specifications'],
review: ['criteria', 'feedback', 'decisions']
};
// Markdown structure patterns
const MARKDOWN_PATTERNS = {
heading: /^#{1,6}\s+.+$/m,
list_item: /^[\s]*[-*+]\s+.+$/m,
numbered_list: /^[\s]*\d+\.\s+.+$/m,
code_block: /^```[\s\S]*?```$/m,
inline_code: /`[^`]+`/,
link: /\[([^\]]+)\]\(([^)]+)\)/,
bold: /\*\*([^*]+)\*\*/,
italic: /\*([^*]+)\*/
};
// Content quality thresholds
const QUALITY_THRESHOLDS = {
min_content_length: 50,
min_words: 10,
min_sections: 2,
max_line_length: 120,
min_heading_levels: 1
};
export class CellValidator {
constructor(config = {}) {
this.config = {
required_sections: config.required_sections || DEFAULT_REQUIRED_SECTIONS,
quality_thresholds: { ...QUALITY_THRESHOLDS, ...config.quality_thresholds },
strict_mode: config.strict_mode || false,
custom_validators: config.custom_validators || []
};
}
/**
* Validate cell content format and structure
* @param {Object} cellData - Cell data to validate
* @param {Object} options - Validation options
* @returns {Object} Validation result
*/
validateCell(cellData, options = {}) {
const validationResult = {
valid: true,
errors: [],
warnings: [],
suggestions: [],
quality_score: 0,
sections_found: [],
content_stats: {}
};
try {
// Basic structure validation
this._validateBasicStructure(cellData, validationResult);
if (!validationResult.valid) {
return validationResult;
}
// Content analysis
this._analyzeContent(cellData.content, validationResult);
// Markdown structure validation
this._validateMarkdownStructure(cellData.content, validationResult);
// Section requirements validation
if (options.horizontal_type) {
this._validateRequiredSections(cellData.content, options.horizontal_type, validationResult);
}
// Quality assessment
this._assessContentQuality(cellData.content, validationResult);
// Custom validation rules
this._runCustomValidators(cellData, validationResult);
// Empty cell validation
if (this._isEmpty(cellData.content)) {
this._validateEmptyCell(cellData, validationResult);
}
// Calculate final quality score
this._calculateQualityScore(validationResult);
} catch (error) {
validationResult.valid = false;
validationResult.errors.push({
type: 'validation_error',
message: `Validation failed: ${error.message}`,
severity: 'critical'
});
}
return validationResult;
}
/**
* Validate basic cell structure
* @private
*/
_validateBasicStructure(cellData, result) {
if (!cellData) {
result.valid = false;
result.errors.push({
type: 'structure',
message: 'Cell data is required',
severity: 'critical'
});
return;
}
if (!cellData.id || !cellData.id.row || !cellData.id.column) {
result.valid = false;
result.errors.push({
type: 'structure',
message: 'Cell must have valid id with row and column',
severity: 'critical'
});
return;
}
if (typeof cellData.content !== 'string') {
result.valid = false;
result.errors.push({
type: 'structure',
message: 'Cell content must be a string',
severity: 'critical'
});
return;
}
if (!cellData.metadata) {
result.warnings.push({
type: 'structure',
message: 'Cell metadata is missing',
severity: 'low'
});
}
}
/**
* Analyze content statistics
* @private
*/
_analyzeContent(content, result) {
const stats = {
length: content.length,
word_count: content.split(/\s+/).filter(word => word.length > 0).length,
line_count: content.split('\n').length,
paragraph_count: content.split(/\n\s*\n/).filter(para => para.trim().length > 0).length,
has_headings: MARKDOWN_PATTERNS.heading.test(content),
has_lists: MARKDOWN_PATTERNS.list_item.test(content) || MARKDOWN_PATTERNS.numbered_list.test(content),
has_code: MARKDOWN_PATTERNS.code_block.test(content) || MARKDOWN_PATTERNS.inline_code.test(content),
has_links: MARKDOWN_PATTERNS.link.test(content)
};
result.content_stats = stats;
// Basic content requirements
if (stats.length < this.config.quality_thresholds.min_content_length) {
result.warnings.push({
type: 'content_length',
message: `Content is too short (${stats.length} chars, minimum ${this.config.quality_thresholds.min_content_length})`,
severity: 'medium'
});
}
if (stats.word_count < this.config.quality_thresholds.min_words) {
result.warnings.push({
type: 'word_count',
message: `Insufficient word count (${stats.word_count} words, minimum ${this.config.quality_thresholds.min_words})`,
severity: 'medium'
});
}
}
/**
* Validate markdown structure
* @private
*/
_validateMarkdownStructure(content, result) {
const lines = content.split('\n');
let heading_levels = new Set();
let long_lines = [];
lines.forEach((line, index) => {
// Check line length
if (line.length > this.config.quality_thresholds.max_line_length) {
long_lines.push(index + 1);
}
// Extract heading levels
const heading_match = line.match(/^(#{1,6})\s+(.+)$/);
if (heading_match) {
heading_levels.add(heading_match[1].length);
}
});
// Check for proper heading structure
if (heading_levels.size < this.config.quality_thresholds.min_heading_levels) {
result.suggestions.push({
type: 'structure',
message: 'Consider adding more headings to improve content organization',
severity: 'low'
});
}
// Report long lines
if (long_lines.length > 0) {
result.warnings.push({
type: 'formatting',
message: `Lines exceed maximum length (${this.config.quality_thresholds.max_line_length} chars): ${long_lines.join(', ')}`,
severity: 'low'
});
}
// Extract sections
const sections = this._extractSections(content);
result.sections_found = sections;
}
/**
* Extract sections from content
* @private
*/
_extractSections(content) {
const sections = [];
const lines = content.split('\n');
lines.forEach(line => {
const heading_match = line.match(/^#{1,6}\s+(.+)$/);
if (heading_match) {
const section_name = heading_match[1].toLowerCase()
.replace(/[^\w\s]/g, '')
.replace(/\s+/g, '_');
sections.push(section_name);
}
});
return sections;
}
/**
* Validate required sections based on horizontal type
* @private
*/
_validateRequiredSections(content, horizontal_type, result) {
if (!this.config.required_sections[horizontal_type]) {
result.warnings.push({
type: 'horizontal_type',
message: `Unknown horizontal type: ${horizontal_type}`,
severity: 'medium'
});
return;
}
const required_sections = this.config.required_sections[horizontal_type];
const found_sections = result.sections_found;
const missing_sections = [];
required_sections.forEach(required => {
const found = found_sections.some(section =>
section.includes(required) || required.includes(section)
);
if (!found) {
missing_sections.push(required);
}
});
if (missing_sections.length > 0) {
if (this.config.strict_mode) {
result.valid = false;
result.errors.push({
type: 'required_sections',
message: `Missing required sections for ${horizontal_type}: ${missing_sections.join(', ')}`,
severity: 'high'
});
} else {
result.warnings.push({
type: 'required_sections',
message: `Consider adding sections for ${horizontal_type}: ${missing_sections.join(', ')}`,
severity: 'medium'
});
}
}
}
/**
* Assess overall content quality
* @private
*/
_assessContentQuality(content, result) {
let quality_score = 0;
const stats = result.content_stats;
// Content length scoring (0-25 points)
if (stats.length >= this.config.quality_thresholds.min_content_length) {
quality_score += Math.min(25, stats.length / 20);
}
// Structure scoring (0-25 points)
if (stats.has_headings) quality_score += 8;
if (stats.has_lists) quality_score += 5;
if (stats.has_code) quality_score += 7;
if (stats.paragraph_count >= 2) quality_score += 5;
// Content richness scoring (0-25 points)
if (stats.word_count >= this.config.quality_thresholds.min_words) {
quality_score += Math.min(15, stats.word_count / 10);
}
if (stats.has_links) quality_score += 5;
if (result.sections_found.length >= this.config.quality_thresholds.min_sections) {
quality_score += 5;
}
// Error/warning penalty (0-25 points)
let penalty_score = 25;
penalty_score -= result.errors.length * 10;
penalty_score -= result.warnings.length * 3;
quality_score += Math.max(0, penalty_score);
result.quality_score = Math.min(100, Math.max(0, quality_score));
// Quality-based suggestions
if (result.quality_score < 50) {
result.suggestions.push({
type: 'quality',
message: 'Content quality is below average. Consider adding more detail, structure, or examples.',
severity: 'medium'
});
}
}
/**
* Run custom validation rules
* @private
*/
_runCustomValidators(cellData, result) {
this.config.custom_validators.forEach(validator => {
try {
const custom_result = validator(cellData);
if (custom_result.errors) {
result.errors.push(...custom_result.errors);
}
if (custom_result.warnings) {
result.warnings.push(...custom_result.warnings);
}
if (custom_result.suggestions) {
result.suggestions.push(...custom_result.suggestions);
}
if (custom_result.valid === false) {
result.valid = false;
}
} catch (error) {
result.warnings.push({
type: 'custom_validator',
message: `Custom validator failed: ${error.message}`,
severity: 'low'
});
}
});
}
/**
* Check if content is effectively empty
* @private
*/
_isEmpty(content) {
const trimmed = content.trim();
return trimmed.length === 0 ||
trimmed === '# TODO' ||
trimmed === 'TODO' ||
/^#+\s*$/.test(trimmed) ||
trimmed.length < 10;
}
/**
* Validate empty cell with reason
* @private
*/
_validateEmptyCell(cellData, result) {
const metadata = cellData.metadata || {};
if (!metadata.empty_reason) {
if (this.config.strict_mode) {
result.valid = false;
result.errors.push({
type: 'empty_cell',
message: 'Empty cell must provide a reason in metadata.empty_reason',
severity: 'high'
});
} else {
result.warnings.push({
type: 'empty_cell',
message: 'Empty cell should provide a reason in metadata.empty_reason',
severity: 'medium'
});
}
} else {
// Validate empty reason
const valid_reasons = [
'not_applicable',
'future_implementation',
'under_review',
'blocked',
'deprecated',
'external_dependency',
'insufficient_information'
];
if (!valid_reasons.includes(metadata.empty_reason)) {
result.warnings.push({
type: 'empty_reason',
message: `Empty reason should be one of: ${valid_reasons.join(', ')}`,
severity: 'low'
});
}
}
}
/**
* Calculate final quality score
* @private
*/
_calculateQualityScore(result) {
// Quality score already calculated in _assessContentQuality
// Add any final adjustments here if needed
if (result.errors.length > 0) {
result.quality_score = Math.max(0, result.quality_score - 20);
}
if (result.valid === false) {
result.quality_score = Math.min(30, result.quality_score);
}
}
/**
* Get validation summary
* @param {Object} validationResult - Result from validateCell
* @returns {string} Human-readable summary
*/
getValidationSummary(validationResult) {
const { valid, errors, warnings, suggestions, quality_score } = validationResult;
let summary = `Validation ${valid ? 'PASSED' : 'FAILED'} - Quality Score: ${quality_score.toFixed(1)}/100\n`;
if (errors.length > 0) {
summary += `\nErrors (${errors.length}):\n`;
errors.forEach(error => {
summary += ` • [${error.severity.toUpperCase()}] ${error.message}\n`;
});
}
if (warnings.length > 0) {
summary += `\nWarnings (${warnings.length}):\n`;
warnings.forEach(warning => {
summary += ` • [${warning.severity.toUpperCase()}] ${warning.message}\n`;
});
}
if (suggestions.length > 0) {
summary += `\nSuggestions (${suggestions.length}):\n`;
suggestions.forEach(suggestion => {
summary += ` • ${suggestion.message}\n`;
});
}
return summary;
}
/**
* Create a validator with custom configuration
* @param {Object} config - Custom configuration
* @returns {CellValidator} Configured validator instance
*/
static createValidator(config = {}) {
return new CellValidator(config);
}
/**
* Validate multiple cells in batch
* @param {Array} cells - Array of cell data
* @param {Object} options - Validation options
* @returns {Object} Batch validation results
*/
validateBatch(cells, options = {}) {
const results = {
total: cells.length,
passed: 0,
failed: 0,
average_quality: 0,
results: []
};
let total_quality = 0;
cells.forEach((cell, index) => {
const result = this.validateCell(cell, options);
results.results.push({
index,
cell_id: cell.id,
...result
});
if (result.valid) {
results.passed++;
} else {
results.failed++;
}
total_quality += result.quality_score;
});
results.average_quality = total_quality / cells.length;
return results;
}
}
export default CellValidator;