#!/usr/bin/env node /** * Sync Instructions to MongoDB * * Syncs .claude/instruction-history.json to MongoDB governanceRules collection * Handles: inserts, updates, deactivations */ const fs = require('fs'); const path = require('path'); const mongoose = require('mongoose'); require('dotenv').config(); const GovernanceRule = require('../src/models/GovernanceRule.model'); const INSTRUCTION_FILE = path.join(__dirname, '../.claude/instruction-history.json'); const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev'; async function syncToDatabase(options = {}) { const { silent = false } = options; if (!silent) { console.log('═══════════════════════════════════════════════════════════'); console.log(' SYNC INSTRUCTIONS TO MONGODB'); console.log('═══════════════════════════════════════════════════════════\n'); } try { // Connect to MongoDB only if not already connected if (mongoose.connection.readyState !== 1) { if (!silent) console.log('📡 Connecting to MongoDB...'); await mongoose.connect(MONGODB_URI); if (!silent) console.log(` ✓ Connected to ${MONGODB_URI}\n`); } else { if (!silent) console.log('📡 Using existing MongoDB connection\n'); } // Read instruction history console.log('📖 Reading instruction-history.json...'); const data = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8')); console.log(` ✓ Version: ${data.version}`); console.log(` ✓ Total instructions: ${data.instructions.length}`); console.log(` ✓ Active instructions: ${data.instructions.filter(i => i.active !== false).length}\n`); // Get current rules from database console.log('📊 Fetching current rules from database...'); const existingRules = await GovernanceRule.find({}); const existingRuleIds = new Set(existingRules.map(r => r.id)); console.log(` ✓ Found ${existingRules.length} existing rules\n`); // Sync stats let inserted = 0; let updated = 0; let deactivated = 0; let skipped = 0; const errors = []; console.log('🔄 Syncing instructions...\n'); // Process each instruction for (const inst of data.instructions) { try { const ruleData = { id: inst.id, text: inst.text, quadrant: inst.quadrant, persistence: inst.persistence, temporalScope: inst.temporal_scope || 'PERMANENT', active: inst.active !== false, notes: inst.notes || '', source: inst.session_id ? 'user_instruction' : 'framework_default', createdBy: 'claude-code' }; // Handle additional fields if present if (inst.parameters) { // Store parameters as notes if not already in notes if (!ruleData.notes.includes('Parameters:')) { ruleData.notes += `\n\nParameters: ${JSON.stringify(inst.parameters)}`; } } if (inst.deprecates) { if (!ruleData.notes.includes('Deprecates:')) { ruleData.notes += `\n\nDeprecates: ${inst.deprecates.join(', ')}`; } } if (inst.replaces) { if (!ruleData.notes.includes('Replaces:')) { ruleData.notes += `\n\nReplaces: ${inst.replaces.join(', ')}`; } } if (inst.part_of) { if (!ruleData.notes.includes('Part of:')) { ruleData.notes += `\n\nPart of: ${inst.part_of}`; } } if (inst.created_date) { if (!ruleData.notes.includes('Created:')) { ruleData.notes += `\n\nCreated: ${inst.created_date}`; } } if (inst.deprecation_reason) { if (!ruleData.notes.includes('Deprecation reason:')) { ruleData.notes += `\n\nDeprecation reason: ${inst.deprecation_reason}`; } } // Clean up notes (remove leading/trailing whitespace) ruleData.notes = ruleData.notes.trim(); if (existingRuleIds.has(inst.id)) { // Update existing rule const result = await GovernanceRule.findOneAndUpdate( { id: inst.id }, ruleData, { new: true, runValidators: true } ); if (result) { updated++; console.log(` ↻ Updated ${inst.id}`); } else { errors.push({ id: inst.id, error: 'Update returned null' }); console.log(` ✗ Failed to update ${inst.id}`); } } else { // Insert new rule const newRule = new GovernanceRule(ruleData); await newRule.save(); inserted++; console.log(` + Inserted ${inst.id}`); } } catch (err) { errors.push({ id: inst.id, error: err.message }); console.log(` ✗ Error processing ${inst.id}: ${err.message}`); } } console.log(''); // Deactivate rules that no longer exist in JSON const jsonRuleIds = new Set(data.instructions.map(i => i.id)); for (const existingRule of existingRules) { if (!jsonRuleIds.has(existingRule.id) && existingRule.active) { existingRule.active = false; existingRule.notes += `\n\nDeactivated during sync on ${new Date().toISOString()} - no longer in instruction-history.json`; await existingRule.save(); deactivated++; console.log(` ⊝ Deactivated ${existingRule.id}`); } } if (deactivated > 0) console.log(''); // Summary console.log('═══════════════════════════════════════════════════════════'); console.log(' SYNC SUMMARY'); console.log('═══════════════════════════════════════════════════════════\n'); console.log(` Inserted: ${inserted}`); console.log(` Updated: ${updated}`); console.log(` Deactivated: ${deactivated}`); console.log(` Errors: ${errors.length}`); console.log(''); if (errors.length > 0) { console.log(' Errors encountered:'); errors.forEach(({ id, error }) => { console.log(` - ${id}: ${error}`); }); console.log(''); } // Verify final counts const activeCount = await GovernanceRule.countDocuments({ active: true }); const totalCount = await GovernanceRule.countDocuments({}); console.log(` Database: ${activeCount} active / ${totalCount} total`); console.log(` JSON file: ${data.stats.active_instructions} active / ${data.stats.total_instructions} total`); console.log(''); if (activeCount === data.stats.active_instructions) { console.log('✅ Sync successful - counts match!'); } else { console.log('⚠️ WARNING: Active counts do not match'); console.log(` Expected ${data.stats.active_instructions}, got ${activeCount}`); } console.log(''); // Return success with stats return { success: true, added: inserted, updated: updated, deactivated: deactivated, errors: errors.length, finalCount: activeCount }; } catch (err) { console.error('❌ Sync failed:', err.message); console.error(err.stack); if (require.main === module) { process.exit(1); } return { success: false, error: err.message }; } finally { // Only disconnect if running as standalone script (not imported) if (require.main === module) { await mongoose.disconnect(); console.log('📡 Disconnected from MongoDB\n'); } } } // Export for use by server.js and other modules module.exports = { syncInstructions: syncToDatabase }; // Run directly if not imported if (require.main === module) { syncToDatabase(); }