/** * Rule Editor Modal * Handles creation, editing, and viewing of governance rules with real-time validation * * @class RuleEditor * * @description * Modal component for rule CRUD operations with these features: * - Three modes: create, edit, view (read-only) * - Real-time variable detection (${VAR_NAME} pattern) * - Live clarity score calculation using heuristics * - Dynamic example fields (add/remove) * - Form validation before submission * - Integration with rule-manager for list refresh * * @example * // Create global instance * const ruleEditor = new RuleEditor(); * * // Open in create mode * ruleEditor.openCreate(); * * // Open in edit mode * ruleEditor.openEdit('68e8c3a6499d095048311f03'); * * // Open in view mode (read-only) * ruleEditor.openView('68e8c3a6499d095048311f03'); * * @property {string} mode - Current mode (create | edit | view) * @property {string} ruleId - MongoDB ObjectId of rule being edited * @property {Object} originalRule - Original rule data (for edit mode) * @property {Array} detectedVariables - Variables detected in rule text */ class RuleEditor { constructor() { this.mode = 'create'; // 'create' or 'edit' this.ruleId = null; this.originalRule = null; this.detectedVariables = []; } /** * Open editor in create mode */ openCreate() { this.mode = 'create'; this.ruleId = null; this.originalRule = null; this.detectedVariables = []; this.render(); this.attachEventListeners(); } /** * Open editor in edit mode */ async openEdit(ruleId) { this.mode = 'edit'; this.ruleId = ruleId; try { const response = await apiRequest(`/api/admin/rules/${ruleId}`); if (!response.success || !response.rule) { throw new Error('Failed to load rule'); } this.originalRule = response.rule; this.detectedVariables = response.rule.variables || []; this.render(); this.populateForm(response.rule); this.attachEventListeners(); } catch (error) { console.error('Failed to load rule:', error); showToast('Failed to load rule for editing', 'error'); } } /** * Open editor in view mode (read-only) */ async openView(ruleId) { this.mode = 'view'; this.ruleId = ruleId; try { const response = await apiRequest(`/api/admin/rules/${ruleId}`); if (!response.success || !response.rule) { throw new Error('Failed to load rule'); } this.originalRule = response.rule; this.detectedVariables = response.rule.variables || []; this.renderViewMode(response.rule); } catch (error) { console.error('Failed to load rule:', error); showToast('Failed to load rule', 'error'); } } /** * Render the editor modal */ render() { const container = document.getElementById('modal-container'); const title = this.mode === 'create' ? 'Create New Rule' : 'Edit Rule'; container.innerHTML = `

${title}

Unique identifier (e.g., inst_019, inst_020)

Use \${VARIABLE_NAME} for dynamic values

Universal rules apply to all projects

Low (0) High (100)
100

Based on language strength and specificity

${this.mode === 'edit' ? `

Get AI-powered suggestions to improve clarity, specificity, and actionability

` : ''}
`; } /** * Render view-only mode */ renderViewMode(rule) { const container = document.getElementById('modal-container'); container.innerHTML = `

Rule Details

${rule.id}

${rule.scope} ${rule.quadrant} ${rule.persistence} ${rule.validationStatus}
${this.escapeHtml(rule.text)}
${rule.variables && rule.variables.length > 0 ? `
${rule.variables.map(v => ` \${${v}} `).join('')}
` : ''}

${rule.category}

${rule.priority}

${rule.temporalScope}

${rule.active ? 'Active' : 'Inactive'}

${rule.clarityScore !== null ? `
Clarity ${rule.clarityScore}%
${rule.specificityScore !== null ? `
Specificity ${rule.specificityScore}%
` : ''} ${rule.actionabilityScore !== null ? `
Actionability ${rule.actionabilityScore}%
` : ''}
` : ''} ${rule.notes ? `
${this.escapeHtml(rule.notes)}
` : ''}
Created: ${this.formatDate(rule.createdAt)}
Updated: ${this.formatDate(rule.updatedAt)}
Created by: ${rule.createdBy}
Source: ${rule.source}
`; // Attach close handler document.querySelectorAll('#close-modal').forEach(btn => { btn.addEventListener('click', () => this.close()); }); } /** * Populate form with existing rule data (edit mode) */ populateForm(rule) { document.getElementById('rule-id').value = rule.id; document.getElementById('rule-text').value = rule.text; document.getElementById('rule-scope').value = rule.scope; document.getElementById('rule-quadrant').value = rule.quadrant; document.getElementById('rule-persistence').value = rule.persistence; document.getElementById('rule-category').value = rule.category || 'other'; document.getElementById('rule-priority').value = rule.priority || 50; document.getElementById('priority-value').textContent = rule.priority || 50; document.getElementById('rule-temporal').value = rule.temporalScope || 'PERMANENT'; document.getElementById('rule-active').checked = rule.active !== false; document.getElementById('rule-notes').value = rule.notes || ''; // Populate examples if any if (rule.examples && rule.examples.length > 0) { rule.examples.forEach(example => { this.addExampleField(example); }); } // Trigger variable detection this.detectVariables(); this.calculateClarityScore(); } /** * Attach event listeners */ attachEventListeners() { // Close modal document.querySelectorAll('#close-modal, #cancel-btn').forEach(btn => { btn.addEventListener('click', () => this.close()); }); // Variable detection on text change document.getElementById('rule-text').addEventListener('input', () => { this.detectVariables(); this.calculateClarityScore(); }); // Priority slider document.getElementById('rule-priority').addEventListener('input', (e) => { document.getElementById('priority-value').textContent = e.target.value; }); // Add example button document.getElementById('add-example').addEventListener('click', () => { this.addExampleField(); }); // AI Optimization (edit mode only) if (this.mode === 'edit') { const optimizeBtn = document.getElementById('optimize-rule-btn'); if (optimizeBtn) { optimizeBtn.addEventListener('click', () => this.runOptimization()); } const applyBtn = document.getElementById('apply-optimization-btn'); if (applyBtn) { applyBtn.addEventListener('click', () => this.applyOptimization()); } } // Form submission document.getElementById('save-btn').addEventListener('click', (e) => { e.preventDefault(); this.saveRule(); }); } /** * Detect variables in rule text */ detectVariables() { const text = document.getElementById('rule-text').value; const varPattern = /\$\{([A-Z_]+)\}/g; const variables = []; let match; while ((match = varPattern.exec(text)) !== null) { if (!variables.includes(match[1])) { variables.push(match[1]); } } this.detectedVariables = variables; // Update UI const section = document.getElementById('variables-section'); const list = document.getElementById('variables-list'); if (variables.length > 0) { section.classList.remove('hidden'); list.innerHTML = variables.map(v => ` \${${v}} `).join(''); } else { section.classList.add('hidden'); } } /** * Calculate clarity score (heuristic) */ calculateClarityScore() { const text = document.getElementById('rule-text').value; let score = 100; if (!text) { score = 0; } else { // Deduct for weak language const weakWords = ['try', 'maybe', 'consider', 'might', 'probably', 'possibly', 'perhaps']; weakWords.forEach(word => { if (new RegExp(`\\b${word}\\b`, 'i').test(text)) { score -= 10; } }); // Bonus for strong imperatives const strongWords = ['MUST', 'SHALL', 'REQUIRED', 'PROHIBITED', 'NEVER']; const hasStrong = strongWords.some(word => new RegExp(`\\b${word}\\b`).test(text)); if (!hasStrong) score -= 10; // Bonus for specificity (has numbers or variables) if (!/\d/.test(text) && !/\$\{[A-Z_]+\}/.test(text)) { score -= 5; } } score = Math.max(0, Math.min(100, score)); // Update UI document.getElementById('clarity-score').textContent = score; const bar = document.getElementById('clarity-bar'); bar.style.width = `${score}%`; bar.className = `h-2 rounded-full transition-all ${ score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-yellow-500' : 'bg-red-500' }`; } /** * Add example field */ addExampleField(value = '') { const list = document.getElementById('examples-list'); const index = list.children.length; const div = document.createElement('div'); div.className = 'flex space-x-2'; div.innerHTML = ` `; list.appendChild(div); } /** * Save rule (create or update) */ async saveRule() { const form = document.getElementById('rule-form'); // Get form data const formData = { id: document.getElementById('rule-id').value.trim(), text: document.getElementById('rule-text').value.trim(), scope: document.getElementById('rule-scope').value, quadrant: document.getElementById('rule-quadrant').value, persistence: document.getElementById('rule-persistence').value, category: document.getElementById('rule-category').value, priority: parseInt(document.getElementById('rule-priority').value), temporalScope: document.getElementById('rule-temporal').value, active: document.getElementById('rule-active').checked, notes: document.getElementById('rule-notes').value.trim() }; // Collect examples const exampleInputs = document.querySelectorAll('[name^="example-"]'); formData.examples = Array.from(exampleInputs) .map(input => input.value.trim()) .filter(val => val.length > 0); // Validation if (!formData.id) { showToast('Rule ID is required', 'error'); return; } if (!formData.text) { showToast('Rule text is required', 'error'); return; } if (!formData.quadrant) { showToast('Quadrant is required', 'error'); return; } if (!formData.persistence) { showToast('Persistence is required', 'error'); return; } // Save try { const saveBtn = document.getElementById('save-btn'); saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; let response; if (this.mode === 'create') { response = await apiRequest('/api/admin/rules', { method: 'POST', body: JSON.stringify(formData) }); } else { response = await apiRequest(`/api/admin/rules/${this.ruleId}`, { method: 'PUT', body: JSON.stringify(formData) }); } if (response.success) { showToast( this.mode === 'create' ? 'Rule created successfully' : 'Rule updated successfully', 'success' ); this.close(); // Refresh the rules list if (typeof loadRules === 'function') loadRules(); if (typeof loadStatistics === 'function') loadStatistics(); } else { throw new Error(response.message || 'Failed to save rule'); } } catch (error) { console.error('Save error:', error); showToast(error.message || 'Failed to save rule', 'error'); const saveBtn = document.getElementById('save-btn'); saveBtn.disabled = false; saveBtn.textContent = this.mode === 'create' ? 'Create Rule' : 'Save Changes'; } } /** * Close the modal */ close() { const container = document.getElementById('modal-container'); container.innerHTML = ''; } // Utility methods escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } formatDate(dateString) { if (!dateString) return 'Unknown'; const date = new Date(dateString); return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } getQuadrantColor(quadrant) { const colors = { STRATEGIC: 'bg-purple-100 text-purple-800', OPERATIONAL: 'bg-green-100 text-green-800', TACTICAL: 'bg-yellow-100 text-yellow-800', SYSTEM: 'bg-blue-100 text-blue-800', STORAGE: 'bg-gray-100 text-gray-800' }; return colors[quadrant] || 'bg-gray-100 text-gray-800'; } getPersistenceColor(persistence) { const colors = { HIGH: 'bg-red-100 text-red-800', MEDIUM: 'bg-orange-100 text-orange-800', LOW: 'bg-yellow-100 text-yellow-800' }; return colors[persistence] || 'bg-gray-100 text-gray-800'; } getValidationColor(status) { const colors = { PASSED: 'bg-green-100 text-green-800', FAILED: 'bg-red-100 text-red-800', NEEDS_REVIEW: 'bg-yellow-100 text-yellow-800', NOT_VALIDATED: 'bg-gray-100 text-gray-800' }; return colors[status] || 'bg-gray-100 text-gray-800'; } /** * Run AI optimization analysis */ async runOptimization() { if (!this.ruleId) return; const optimizeBtn = document.getElementById('optimize-rule-btn'); const resultsSection = document.getElementById('optimization-results'); try { // Show loading state optimizeBtn.disabled = true; optimizeBtn.innerHTML = ` `; // Call optimization API const response = await apiRequest(`/api/admin/rules/${this.ruleId}/optimize`, { method: 'POST', body: JSON.stringify({ mode: 'aggressive' }) }); if (!response.success) { throw new Error(response.message || 'Optimization failed'); } // Store optimization result this.optimizationResult = response; // Display results this.displayOptimizationResults(response); // Show results section resultsSection.classList.remove('hidden'); showToast('Analysis complete', 'success'); } catch (error) { console.error('Optimization error:', error); showToast(error.message || 'Failed to run optimization', 'error'); } finally { optimizeBtn.disabled = false; optimizeBtn.textContent = 'Analyze & Optimize'; } } /** * Display optimization results in UI */ displayOptimizationResults(result) { const { analysis, optimization } = result; // Update score bars this.updateScoreBar('ai-clarity', analysis.clarity.score, analysis.clarity.grade); this.updateScoreBar('ai-specificity', analysis.specificity.score, analysis.specificity.grade); this.updateScoreBar('ai-actionability', analysis.actionability.score, analysis.actionability.grade); // Display suggestions const suggestionsList = document.getElementById('suggestions-list'); const allIssues = [ ...analysis.clarity.issues, ...analysis.specificity.issues, ...analysis.actionability.issues ]; if (allIssues.length > 0) { suggestionsList.innerHTML = allIssues.map((issue, index) => `
${index + 1} ${this.escapeHtml(issue)}
`).join(''); } else { suggestionsList.innerHTML = `
✓ No issues found - this rule is well-formed!
`; } // Show/hide apply button based on whether there are optimizations const applySection = document.getElementById('auto-optimize-section'); if (optimization.optimizedText !== result.rule.originalText) { applySection.classList.remove('hidden'); } else { applySection.classList.add('hidden'); } } /** * Update score bar visualization */ updateScoreBar(prefix, score, grade) { const scoreElement = document.getElementById(`${prefix}-score`); const barElement = document.getElementById(`${prefix}-bar`); scoreElement.textContent = `${score} (${grade})`; barElement.style.width = `${score}%`; // Update color based on score const colorClass = score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-yellow-500' : 'bg-red-500'; barElement.className = `h-1.5 rounded-full transition-all ${colorClass}`; } /** * Apply AI optimizations to rule text */ async applyOptimization() { if (!this.optimizationResult) return; const { optimization } = this.optimizationResult; const ruleTextArea = document.getElementById('rule-text'); // Confirm with user if (!confirm('Apply AI optimizations to rule text? This will overwrite your current text.')) { return; } // Update text area ruleTextArea.value = optimization.optimizedText; // Trigger variable detection and clarity recalculation this.detectVariables(); this.calculateClarityScore(); // Hide results and reset document.getElementById('optimization-results').classList.add('hidden'); this.optimizationResult = null; showToast(`Applied ${optimization.changes.length} optimization(s)`, 'success'); } } // Create global instance window.ruleEditor = new RuleEditor();