- 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>
432 lines
13 KiB
JavaScript
Executable file
432 lines
13 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Migration Script: .claude/instruction-history.json → .memory/governance/
|
||
*
|
||
* Migrates Tractatus governance rules from legacy .claude/ directory
|
||
* to new MemoryProxy-managed .memory/governance/ directory.
|
||
*
|
||
* Phase 5 PoC - Week 3: Production Migration
|
||
*
|
||
* Usage:
|
||
* node scripts/migrate-to-memory-proxy.js [--dry-run] [--backup]
|
||
*
|
||
* Options:
|
||
* --dry-run Preview migration without making changes
|
||
* --backup Create backup of source file before migration (default: true)
|
||
* --force Skip confirmation prompts
|
||
*/
|
||
|
||
const fs = require('fs').promises;
|
||
const path = require('path');
|
||
const { MemoryProxyService } = require('../src/services/MemoryProxy.service');
|
||
const logger = require('../src/utils/logger.util');
|
||
|
||
// Configuration
|
||
const SOURCE_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
||
const BACKUP_DIR = path.join(__dirname, '../.claude/backups');
|
||
const MEMORY_BASE_PATH = path.join(__dirname, '../.memory');
|
||
|
||
// Parse command line arguments
|
||
const args = process.argv.slice(2);
|
||
const isDryRun = args.includes('--dry-run');
|
||
const createBackup = !args.includes('--no-backup');
|
||
const forceMode = args.includes('--force');
|
||
|
||
/**
|
||
* Validate source file exists and is readable
|
||
*/
|
||
async function validateSource() {
|
||
try {
|
||
const stats = await fs.stat(SOURCE_PATH);
|
||
if (!stats.isFile()) {
|
||
throw new Error('Source path is not a file');
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
if (error.code === 'ENOENT') {
|
||
throw new Error(`Source file not found: ${SOURCE_PATH}`);
|
||
}
|
||
throw new Error(`Cannot access source file: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load rules from source file
|
||
*/
|
||
async function loadSourceRules() {
|
||
try {
|
||
const data = await fs.readFile(SOURCE_PATH, 'utf8');
|
||
const parsed = JSON.parse(data);
|
||
|
||
if (!parsed.instructions || !Array.isArray(parsed.instructions)) {
|
||
throw new Error('Invalid source format: missing instructions array');
|
||
}
|
||
|
||
return parsed.instructions;
|
||
} catch (error) {
|
||
if (error instanceof SyntaxError) {
|
||
throw new Error(`Invalid JSON in source file: ${error.message}`);
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create backup of source file
|
||
*/
|
||
async function createSourceBackup() {
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const backupPath = path.join(BACKUP_DIR, `instruction-history-${timestamp}.json`);
|
||
|
||
try {
|
||
// Create backup directory if it doesn't exist
|
||
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
||
|
||
// Copy source file to backup
|
||
await fs.copyFile(SOURCE_PATH, backupPath);
|
||
|
||
console.log(` ✓ Backup created: ${backupPath}`);
|
||
return backupPath;
|
||
} catch (error) {
|
||
throw new Error(`Failed to create backup: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Validate rules before migration
|
||
*/
|
||
function validateRules(rules) {
|
||
const issues = [];
|
||
|
||
rules.forEach((rule, index) => {
|
||
if (!rule.id) {
|
||
issues.push(`Rule ${index}: missing 'id' field`);
|
||
}
|
||
if (!rule.text) {
|
||
issues.push(`Rule ${index}: missing 'text' field`);
|
||
}
|
||
if (!rule.quadrant) {
|
||
issues.push(`Rule ${index}: missing 'quadrant' field`);
|
||
}
|
||
if (!rule.persistence) {
|
||
issues.push(`Rule ${index}: missing 'persistence' field`);
|
||
}
|
||
});
|
||
|
||
return issues;
|
||
}
|
||
|
||
/**
|
||
* Analyze rules for migration preview
|
||
*/
|
||
function analyzeRules(rules) {
|
||
const analysis = {
|
||
total: rules.length,
|
||
by_quadrant: {},
|
||
by_persistence: {},
|
||
active: 0,
|
||
inactive: 0,
|
||
critical_rules: []
|
||
};
|
||
|
||
rules.forEach(rule => {
|
||
// Count by quadrant
|
||
analysis.by_quadrant[rule.quadrant] = (analysis.by_quadrant[rule.quadrant] || 0) + 1;
|
||
|
||
// Count by persistence
|
||
analysis.by_persistence[rule.persistence] = (analysis.by_persistence[rule.persistence] || 0) + 1;
|
||
|
||
// Count active/inactive
|
||
if (rule.active !== false) {
|
||
analysis.active++;
|
||
} else {
|
||
analysis.inactive++;
|
||
}
|
||
|
||
// Identify critical enforcement rules
|
||
if (['inst_016', 'inst_017', 'inst_018'].includes(rule.id)) {
|
||
analysis.critical_rules.push(rule.id);
|
||
}
|
||
});
|
||
|
||
return analysis;
|
||
}
|
||
|
||
/**
|
||
* Perform migration
|
||
*/
|
||
async function migrate(rules) {
|
||
const memoryProxy = new MemoryProxyService({
|
||
memoryBasePath: MEMORY_BASE_PATH
|
||
});
|
||
|
||
try {
|
||
// Initialize MemoryProxy
|
||
await memoryProxy.initialize();
|
||
console.log(' ✓ MemoryProxy initialized');
|
||
|
||
// Persist rules
|
||
const result = await memoryProxy.persistGovernanceRules(rules);
|
||
|
||
return result;
|
||
} catch (error) {
|
||
throw new Error(`Migration failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Verify migration success
|
||
*/
|
||
async function verifyMigration(originalRules) {
|
||
const memoryProxy = new MemoryProxyService({
|
||
memoryBasePath: MEMORY_BASE_PATH
|
||
});
|
||
|
||
try {
|
||
await memoryProxy.initialize();
|
||
|
||
// Load rules from memory
|
||
const migratedRules = await memoryProxy.loadGovernanceRules();
|
||
|
||
// Compare counts
|
||
if (migratedRules.length !== originalRules.length) {
|
||
throw new Error(`Rule count mismatch: expected ${originalRules.length}, got ${migratedRules.length}`);
|
||
}
|
||
|
||
// Verify critical rules
|
||
const criticalRuleIds = ['inst_016', 'inst_017', 'inst_018'];
|
||
for (const ruleId of criticalRuleIds) {
|
||
const rule = await memoryProxy.getRule(ruleId);
|
||
if (!rule) {
|
||
throw new Error(`Critical rule ${ruleId} not found after migration`);
|
||
}
|
||
}
|
||
|
||
// Verify data integrity for all rules
|
||
for (let i = 0; i < originalRules.length; i++) {
|
||
const original = originalRules[i];
|
||
const migrated = migratedRules.find(r => r.id === original.id);
|
||
|
||
if (!migrated) {
|
||
throw new Error(`Rule ${original.id} missing after migration`);
|
||
}
|
||
|
||
// Check critical fields
|
||
if (migrated.text !== original.text) {
|
||
throw new Error(`Rule ${original.id}: text mismatch`);
|
||
}
|
||
if (migrated.quadrant !== original.quadrant) {
|
||
throw new Error(`Rule ${original.id}: quadrant mismatch`);
|
||
}
|
||
if (migrated.persistence !== original.persistence) {
|
||
throw new Error(`Rule ${original.id}: persistence mismatch`);
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
rulesVerified: migratedRules.length,
|
||
criticalRulesVerified: criticalRuleIds.length
|
||
};
|
||
|
||
} catch (error) {
|
||
throw new Error(`Verification failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Confirm migration with user (unless --force)
|
||
*/
|
||
async function confirmMigration(analysis) {
|
||
if (forceMode) {
|
||
return true;
|
||
}
|
||
|
||
console.log('\n⚠️ Migration will:');
|
||
console.log(` • Copy ${analysis.total} rules to .memory/governance/`);
|
||
console.log(` • Preserve all rule metadata and fields`);
|
||
if (createBackup) {
|
||
console.log(` • Create backup of source file in .claude/backups/`);
|
||
}
|
||
console.log('\nContinue? (yes/no): ');
|
||
|
||
// Read user input
|
||
const readline = require('readline').createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout
|
||
});
|
||
|
||
return new Promise(resolve => {
|
||
readline.question('', answer => {
|
||
readline.close();
|
||
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y');
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Main migration workflow
|
||
*/
|
||
async function main() {
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
console.log(' Tractatus Governance Rules Migration');
|
||
console.log(' .claude/ → .memory/governance/');
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||
|
||
if (isDryRun) {
|
||
console.log('🔍 DRY RUN MODE - No changes will be made\n');
|
||
}
|
||
|
||
const results = {
|
||
success: false,
|
||
rulesLoaded: 0,
|
||
rulesMigrated: 0,
|
||
backupPath: null,
|
||
errors: []
|
||
};
|
||
|
||
try {
|
||
// Step 1: Validate source
|
||
console.log('[Step 1] Validating source file...');
|
||
await validateSource();
|
||
console.log(` ✓ Source exists: ${SOURCE_PATH}\n`);
|
||
|
||
// Step 2: Load rules
|
||
console.log('[Step 2] Loading governance rules...');
|
||
const rules = await loadSourceRules();
|
||
results.rulesLoaded = rules.length;
|
||
console.log(` ✓ Loaded ${rules.length} rules\n`);
|
||
|
||
// Step 3: Validate rules
|
||
console.log('[Step 3] Validating rule format...');
|
||
const validationIssues = validateRules(rules);
|
||
|
||
if (validationIssues.length > 0) {
|
||
console.log(' ✗ Validation issues found:');
|
||
validationIssues.forEach(issue => console.log(` • ${issue}`));
|
||
throw new Error('Rule validation failed');
|
||
}
|
||
|
||
console.log(` ✓ All ${rules.length} rules valid\n`);
|
||
|
||
// Step 4: Analyze rules
|
||
console.log('[Step 4] Analyzing rules...');
|
||
const analysis = analyzeRules(rules);
|
||
|
||
console.log(` Total: ${analysis.total} rules`);
|
||
console.log(` Active: ${analysis.active} | Inactive: ${analysis.inactive}`);
|
||
console.log('\n By Quadrant:');
|
||
Object.entries(analysis.by_quadrant).forEach(([quadrant, count]) => {
|
||
console.log(` ${quadrant}: ${count}`);
|
||
});
|
||
console.log('\n By Persistence:');
|
||
Object.entries(analysis.by_persistence).forEach(([level, count]) => {
|
||
console.log(` ${level}: ${count}`);
|
||
});
|
||
console.log(`\n Critical Rules: ${analysis.critical_rules.join(', ')}\n`);
|
||
|
||
// Step 5: Confirm migration
|
||
if (!isDryRun) {
|
||
console.log('[Step 5] Confirming migration...');
|
||
const confirmed = await confirmMigration(analysis);
|
||
|
||
if (!confirmed) {
|
||
console.log('\n❌ Migration cancelled by user\n');
|
||
process.exit(0);
|
||
}
|
||
|
||
console.log(' ✓ Migration confirmed\n');
|
||
} else {
|
||
console.log('[Step 5] Skipping confirmation (dry-run mode)\n');
|
||
}
|
||
|
||
// Step 6: Create backup
|
||
if (createBackup && !isDryRun) {
|
||
console.log('[Step 6] Creating backup...');
|
||
results.backupPath = await createSourceBackup();
|
||
console.log();
|
||
} else if (isDryRun) {
|
||
console.log('[Step 6] Backup creation (skipped - dry-run)\n');
|
||
} else {
|
||
console.log('[Step 6] Backup creation (skipped - --no-backup)\n');
|
||
}
|
||
|
||
// Step 7: Migrate rules
|
||
if (!isDryRun) {
|
||
console.log('[Step 7] Migrating rules to MemoryProxy...');
|
||
const migrationResult = await migrate(rules);
|
||
results.rulesMigrated = migrationResult.rulesStored;
|
||
|
||
console.log(` ✓ Migrated ${migrationResult.rulesStored} rules`);
|
||
console.log(` Duration: ${migrationResult.duration}ms`);
|
||
console.log(` Path: ${migrationResult.path}\n`);
|
||
} else {
|
||
console.log('[Step 7] Migration (skipped - dry-run)\n');
|
||
}
|
||
|
||
// Step 8: Verify migration
|
||
if (!isDryRun) {
|
||
console.log('[Step 8] Verifying migration...');
|
||
const verification = await verifyMigration(rules);
|
||
|
||
console.log(` ✓ Verified ${verification.rulesVerified} rules`);
|
||
console.log(` ✓ Critical rules: ${verification.criticalRulesVerified}/3\n`);
|
||
} else {
|
||
console.log('[Step 8] Verification (skipped - dry-run)\n');
|
||
}
|
||
|
||
results.success = true;
|
||
|
||
} catch (error) {
|
||
console.error(`\n✗ MIGRATION FAILED: ${error.message}\n`);
|
||
if (error.stack && process.env.DEBUG) {
|
||
console.error('Stack trace:', error.stack);
|
||
}
|
||
results.errors.push(error.message);
|
||
results.success = false;
|
||
}
|
||
|
||
// Results summary
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
console.log(' MIGRATION RESULTS');
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||
|
||
if (results.success) {
|
||
if (isDryRun) {
|
||
console.log('✅ DRY RUN SUCCESSFUL - Ready for actual migration');
|
||
console.log('\nTo perform migration, run:');
|
||
console.log(' node scripts/migrate-to-memory-proxy.js');
|
||
} else {
|
||
console.log('✅ MIGRATION SUCCESSFUL');
|
||
console.log('\nSummary:');
|
||
console.log(` • Rules loaded: ${results.rulesLoaded}`);
|
||
console.log(` • Rules migrated: ${results.rulesMigrated}`);
|
||
if (results.backupPath) {
|
||
console.log(` • Backup: ${results.backupPath}`);
|
||
}
|
||
console.log('\nNext Steps:');
|
||
console.log(' 1. Initialize services: await service.initialize()');
|
||
console.log(' 2. Verify services load rules from .memory/');
|
||
console.log(' 3. Monitor .memory/audit/ for decision logs');
|
||
}
|
||
} else {
|
||
console.log('❌ MIGRATION FAILED');
|
||
console.log('\nErrors:');
|
||
results.errors.forEach(err => console.log(` • ${err}`));
|
||
}
|
||
|
||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||
|
||
process.exit(results.success ? 0 : 1);
|
||
}
|
||
|
||
// Run migration
|
||
if (require.main === module) {
|
||
main().catch(error => {
|
||
console.error('Fatal error:', error);
|
||
process.exit(1);
|
||
});
|
||
}
|
||
|
||
module.exports = { main };
|