tractatus/scripts/sync-instructions-to-db.js
TheFlow 29011dfd00 feat(infrastructure): add MongoDB sync and CSP compliance checking
DATABASE SYNC INFRASTRUCTURE:
- scripts/sync-instructions-to-db.js
  - Syncs .claude/instruction-history.json to MongoDB governanceRules collection
  - Handles inserts, updates, and deactivations
  - Validates file and database counts match
  - Used in governance audit (54 → 56 → 59 active rules)
  - Required for production deployment of governance rules

CSP COMPLIANCE CHECKING:
- scripts/check-csp-violations.js
  - Enforces Content Security Policy compliance (inst_008)
  - Checks staged HTML files for:
    - Inline scripts (<script> tags with code)
    - Inline event handlers (onclick, onload, etc.)
    - Inline styles (style attributes)
  - Integrated with .git/hooks/pre-commit
  - Blocks commits with CSP violations

REASON FOR CREATION:
- sync-instructions-to-db.js: Needed to deploy governance rules to production
- check-csp-violations.js: Pre-commit hook was calling missing script

USAGE:
- Sync to DB: node scripts/sync-instructions-to-db.js
- CSP check: Runs automatically on git commit (via pre-commit hook)

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:31:54 +13:00

198 lines
7.1 KiB
JavaScript
Executable file

#!/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() {
console.log('═══════════════════════════════════════════════════════════');
console.log(' SYNC INSTRUCTIONS TO MONGODB');
console.log('═══════════════════════════════════════════════════════════\n');
try {
// Connect to MongoDB
console.log('📡 Connecting to MongoDB...');
await mongoose.connect(MONGODB_URI);
console.log(` ✓ Connected to ${MONGODB_URI}\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('');
} catch (err) {
console.error('❌ Sync failed:', err.message);
console.error(err.stack);
process.exit(1);
} finally {
await mongoose.disconnect();
console.log('📡 Disconnected from MongoDB\n');
}
}
syncToDatabase();