- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
235 lines
8 KiB
JavaScript
Executable file
235 lines
8 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(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();
|
|
}
|