tractatus/scripts/migrate-to-memory-proxy.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- 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>
2025-10-24 08:47:42 +13:00

432 lines
13 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 };