tractatus/scripts/sync-instructions-to-db.js
TheFlow 0958d8d2cd fix(mongodb): resolve production connection drops and add governance sync system
- Fixed sync script disconnecting Mongoose (prevents production errors)
- Created text search index (fixes search in rule-manager)
- Enhanced inst_024 with closedown protocol, added inst_061
- Added sync infrastructure: API routes, dashboard widget, auto-sync
- Fixed MemoryProxy tests MongoDB connection
- Created ADR-001 and integration tests

Result: Production stable, 52 rules synced, search working

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 11:39:05 +13:00

323 lines
13 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Sync Instructions to Database (v3 - Clean programmatic + CLI support)
*/
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 ORPHAN_BACKUP = path.join(__dirname, '../.claude/backups/orphaned-rules-' + new Date().toISOString().replace(/:/g, '-') + '.json');
// Parse CLI args (only used when run from command line)
const args = process.argv.slice(2);
const cliDryRun = args.includes('--dry-run');
const cliForce = args.includes('--force');
const cliSilent = args.includes('--silent');
const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' };
// Log functions (will respect silent flag passed to main function)
let SILENT = false;
function log(message, color = 'reset') { if (!SILENT) console.log(`${colors[color]}${message}${colors.reset}`); }
function logBright(message) { log(message, 'bright'); }
function logSuccess(message) { log(`${message}`, 'green'); }
function logWarning(message) { log(`${message}`, 'yellow'); }
function logError(message) { log(`${message}`, 'red'); }
function logInfo(message) { log(` ${message}`, 'cyan'); }
function mapSource(fileSource) {
const mapping = {
'user': 'user_instruction',
'system': 'framework_default',
'collaborative': 'user_instruction',
'framework': 'framework_default',
'automated': 'automated',
'migration': 'migration'
};
return mapping[fileSource] || 'user_instruction';
}
function mapInstructionToRule(instruction) {
return {
id: instruction.id,
text: instruction.text,
scope: 'PROJECT_SPECIFIC',
applicableProjects: ['*'],
quadrant: instruction.quadrant,
persistence: instruction.persistence,
category: mapCategory(instruction),
priority: mapPriority(instruction),
temporalScope: instruction.temporal_scope || 'PERMANENT',
expiresAt: null,
clarityScore: null,
specificityScore: null,
actionabilityScore: null,
validationStatus: 'NOT_VALIDATED',
active: instruction.active !== false,
source: mapSource(instruction.source || 'user'),
createdBy: 'system',
createdAt: instruction.timestamp ? new Date(instruction.timestamp) : new Date(),
notes: instruction.notes || ''
};
}
function mapCategory(instruction) {
const text = instruction.text.toLowerCase();
const quadrant = instruction.quadrant;
if (text.includes('security') || text.includes('csp') || text.includes('auth')) return 'security';
if (text.includes('privacy') || text.includes('gdpr') || text.includes('consent')) return 'privacy';
if (text.includes('values') || text.includes('pluralism') || text.includes('legitimacy')) return 'values';
if (quadrant === 'SYSTEM') return 'technical';
if (quadrant === 'OPERATIONAL' || quadrant === 'TACTICAL') return 'process';
return 'other';
}
function mapPriority(instruction) {
if (instruction.persistence === 'HIGH') return 80;
if (instruction.persistence === 'MEDIUM') return 50;
return 30;
}
/**
* Main sync function
* @param {Object} options - Sync options
* @param {boolean} options.silent - Silent mode (default: false)
* @param {boolean} options.dryRun - Dry run mode (default: false)
* @param {boolean} options.force - Force sync (default: true when silent)
*/
async function syncInstructions(options = {}) {
// Determine mode: programmatic call or CLI
const isDryRun = options.dryRun !== undefined ? options.dryRun : cliDryRun;
const isSilent = options.silent !== undefined ? options.silent : cliSilent;
const isForce = options.force !== undefined ? options.force : (cliForce || (!isDryRun && isSilent));
// Set global silent flag for log functions
SILENT = isSilent;
// Track if we created the connection (so we know if we should close it)
const wasConnected = mongoose.connection.readyState === 1;
try {
logBright('\n════════════════════════════════════════════════════════════════');
logBright(' Tractatus Instruction → Database Sync');
logBright('════════════════════════════════════════════════════════════════\n');
if (isDryRun) logInfo('DRY RUN MODE - No changes will be made\n');
logInfo('Step 1: Reading instruction file...');
if (!fs.existsSync(INSTRUCTION_FILE)) {
logError(`Instruction file not found: ${INSTRUCTION_FILE}`);
return { success: false, error: 'File not found' };
}
const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
const instructions = fileData.instructions || [];
logSuccess(`Loaded ${instructions.length} instructions from file`);
log(` File version: ${fileData.version}`);
log(` Last updated: ${fileData.last_updated}\n`);
logInfo('Step 2: Connecting to MongoDB...');
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
if (!wasConnected) {
await mongoose.connect(mongoUri);
logSuccess(`Connected to MongoDB: ${mongoUri}\n`);
} else {
logSuccess(`Using existing MongoDB connection\n`);
}
logInfo('Step 3: Analyzing database state...');
const dbRules = await GovernanceRule.find({}).lean();
const dbRuleIds = dbRules.map(r => r.id);
const fileRuleIds = instructions.map(i => i.id);
log(` Database has: ${dbRules.length} rules`);
log(` File has: ${instructions.length} instructions`);
const orphanedRules = dbRules.filter(r => !fileRuleIds.includes(r.id));
const missingRules = instructions.filter(i => !dbRuleIds.includes(i.id));
log(` Orphaned (in DB, not in file): ${orphanedRules.length}`);
log(` Missing (in file, not in DB): ${missingRules.length}`);
log(` Existing (in both): ${instructions.filter(i => dbRuleIds.includes(i.id)).length}\n`);
if (orphanedRules.length > 0) {
logWarning('Orphaned rules found:');
orphanedRules.forEach(r => log(` - ${r.id}: "${r.text.substring(0, 60)}..."`, 'yellow'));
log('');
}
if (missingRules.length > 0) {
logInfo('Missing rules (will be added):');
missingRules.forEach(i => log(` + ${i.id}: "${i.text.substring(0, 60)}..."`, 'green'));
log('');
}
if (orphanedRules.length > 0 && !isDryRun) {
logInfo('Step 4: Handling orphaned rules...');
const backupDir = path.dirname(ORPHAN_BACKUP);
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
const orphanBackup = {
timestamp: new Date().toISOString(),
reason: 'Rules found in MongoDB but not in .claude/instruction-history.json',
action: 'Soft deleted (marked as inactive)',
rules: orphanedRules
};
fs.writeFileSync(ORPHAN_BACKUP, JSON.stringify(orphanBackup, null, 2));
logSuccess(`Exported orphaned rules to: ${ORPHAN_BACKUP}`);
for (const orphan of orphanedRules) {
await GovernanceRule.findByIdAndUpdate(orphan._id, {
active: false,
notes: (orphan.notes || '') + '\n[AUTO-DEACTIVATED: Not found in file-based source of truth on ' + new Date().toISOString() + ']'
});
}
logSuccess(`Deactivated ${orphanedRules.length} orphaned rules\n`);
} else if (orphanedRules.length > 0 && isDryRun) {
logInfo('Step 4: [DRY RUN] Would deactivate orphaned rules\n');
} else {
logSuccess('Step 4: No orphaned rules found\n');
}
logInfo('Step 5: Syncing instructions to database...');
let addedCount = 0;
let updatedCount = 0;
let skippedCount = 0;
for (const instruction of instructions) {
const ruleData = mapInstructionToRule(instruction);
if (isDryRun) {
if (!dbRuleIds.includes(instruction.id)) {
log(` [DRY RUN] Would add: ${instruction.id}`, 'cyan');
addedCount++;
} else {
log(` [DRY RUN] Would update: ${instruction.id}`, 'cyan');
updatedCount++;
}
} else {
try {
const existing = await GovernanceRule.findOne({ id: instruction.id });
if (existing) {
await GovernanceRule.findByIdAndUpdate(existing._id, {
...ruleData,
clarityScore: existing.clarityScore || ruleData.clarityScore,
specificityScore: existing.specificityScore || ruleData.specificityScore,
actionabilityScore: existing.actionabilityScore || ruleData.actionabilityScore,
lastOptimized: existing.lastOptimized,
optimizationHistory: existing.optimizationHistory,
validationStatus: existing.validationStatus,
lastValidated: existing.lastValidated,
validationResults: existing.validationResults,
updatedAt: new Date()
});
updatedCount++;
} else {
await GovernanceRule.create(ruleData);
addedCount++;
}
} catch (error) {
logError(` Failed to sync ${instruction.id}: ${error.message}`);
skippedCount++;
}
}
}
if (isDryRun) {
log('');
logInfo('DRY RUN SUMMARY:');
log(` Would add: ${addedCount} rules`);
log(` Would update: ${updatedCount} rules`);
log(` Would skip: ${skippedCount} rules`);
log(` Would deactivate: ${orphanedRules.length} orphaned rules\n`);
logInfo('Run with --force to execute changes\n');
} else {
log('');
logSuccess('SYNC COMPLETE:');
log(` Added: ${addedCount} rules`, 'green');
log(` Updated: ${updatedCount} rules`, 'green');
log(` Skipped: ${skippedCount} rules`, 'yellow');
log(` Deactivated: ${orphanedRules.length} orphaned rules`, 'yellow');
log('');
}
logInfo('Step 6: Verifying final state...');
const finalCount = await GovernanceRule.countDocuments({ active: true });
const expectedCount = instructions.filter(i => i.active !== false).length;
if (isDryRun) {
log(` Current active rules: ${dbRules.filter(r => r.active).length}`);
log(` After sync would be: ${expectedCount}\n`);
} else {
log(` Active rules in database: ${finalCount}`);
log(` Expected from file: ${expectedCount}`);
if (finalCount === expectedCount) {
logSuccess(' ✓ Counts match!\n');
} else {
logWarning(` ⚠ Mismatch: ${finalCount} vs ${expectedCount}\n`);
}
}
// Only disconnect if we created the connection
if (!wasConnected && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
logSuccess('Disconnected from MongoDB\n');
} else {
logSuccess('Leaving connection open for server\n');
}
logBright('════════════════════════════════════════════════════════════════');
if (isDryRun) {
logInfo('DRY RUN COMPLETE - No changes made');
} else {
logSuccess('SYNC COMPLETE');
}
logBright('════════════════════════════════════════════════════════════════\n');
return { success: true, added: addedCount, updated: updatedCount, skipped: skippedCount, deactivated: orphanedRules.length, finalCount: isDryRun ? null : finalCount };
} catch (error) {
logError(`\nSync failed: ${error.message}`);
if (!isSilent) console.error(error.stack);
// Only disconnect if we created the connection
if (!wasConnected && mongoose.connection.readyState === 1) {
await mongoose.disconnect();
}
return { success: false, error: error.message };
}
}
if (require.main === module) {
if (!cliDryRun && !cliForce && !cliSilent) {
console.log('\nUsage:');
console.log(' node scripts/sync-instructions-to-db.js --dry-run # Preview changes');
console.log(' node scripts/sync-instructions-to-db.js --force # Execute sync');
console.log(' node scripts/sync-instructions-to-db.js --silent # Background mode\n');
process.exit(0);
}
syncInstructions()
.then(result => {
if (result.success) {
process.exit(0);
} else {
process.exit(1);
}
})
.catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});
}
module.exports = { syncInstructions };