diff --git a/docs/architecture/ADR-001-dual-governance-architecture.md b/docs/architecture/ADR-001-dual-governance-architecture.md
new file mode 100644
index 00000000..2bba0a5f
--- /dev/null
+++ b/docs/architecture/ADR-001-dual-governance-architecture.md
@@ -0,0 +1,288 @@
+# ADR-001: Dual Governance Architecture (File + Database)
+
+**Status**: Accepted
+**Date**: 2025-10-21
+**Author**: Claude Code (Autonomous Development)
+**Decision**: Implement dual-source governance with file-based source of truth and database-based admin queries
+
+---
+
+## Context
+
+The Tractatus framework requires a governance instruction system that must satisfy multiple competing requirements:
+
+1. **Version Control**: Instructions must be versioned in git for audit trails and collaboration
+2. **Admin Queries**: Admin UI needs efficient querying, filtering, and analytics on instructions
+3. **Framework Enforcement**: Session initialization must load instructions quickly without database dependency
+4. **Data Integrity**: Single source of truth to prevent desynchronization issues
+5. **Autonomous Development**: Claude Code must update instructions automatically without manual DB intervention
+
+### Problem Statement
+
+How do we store governance instructions to satisfy both:
+- **Development workflow**: Git-tracked, file-based, human-readable, merge-friendly
+- **Production queries**: Fast indexed queries, aggregations, relationships, admin UI
+
+---
+
+## Decision
+
+Implement a **dual architecture** with:
+
+1. **File-based source of truth**: `.claude/instruction-history.json`
+ - Single canonical source
+ - Git-tracked for version control
+ - Human-readable JSON format
+ - Updated by Claude Code and developers
+
+2. **Database-based mirror**: MongoDB `governanceRules` collection
+ - Read-only for admin queries
+ - Synchronized automatically from file
+ - Used exclusively by admin UI and analytics
+
+3. **Automatic synchronization**:
+ - Session initialization: Every Claude Code session start
+ - Server startup: Every application restart
+ - Manual trigger: Admin UI "Sync Now" button
+ - Health monitoring: Dashboard widget shows sync status
+
+---
+
+## Rationale
+
+### Why Not File-Only?
+
+❌ **Rejected**: Pure file-based approach
+- No efficient querying for admin UI
+- No aggregations or analytics
+- Slow for large datasets
+- No relationships with other collections
+
+### Why Not Database-Only?
+
+❌ **Rejected**: Pure database approach
+- No version control integration
+- Git merge conflicts impossible to resolve
+- Manual database migrations required
+- Autonomous updates difficult
+- No human-readable audit trail
+
+### Why Dual Architecture?
+
+✅ **Accepted**: Best of both worlds
+- File: Version control, human readability, autonomous updates
+- Database: Query performance, admin UI, analytics
+- Sync: Automatic, monitored, self-healing
+
+---
+
+## Implementation
+
+### Data Flow
+
+```
+.claude/instruction-history.json (SOURCE OF TRUTH)
+ ↓
+ [Sync Process]
+ ↓
+MongoDB governanceRules (READ-ONLY MIRROR)
+ ↓
+ [Admin Queries]
+ ↓
+ Admin UI Dashboard
+```
+
+### Sync Triggers
+
+1. **Session Initialization** (`scripts/session-init.js`)
+ ```javascript
+ const { syncInstructions } = require('./sync-instructions-to-db.js');
+ await syncInstructions();
+ ```
+
+2. **Server Startup** (`src/server.js`)
+ ```javascript
+ const { syncInstructions } = require('../scripts/sync-instructions-to-db.js');
+ await syncInstructions({ silent: true });
+ ```
+
+3. **Manual Trigger** (Admin UI)
+ ```javascript
+ POST /api/admin/sync/trigger
+ ```
+
+### Orphan Handling
+
+When database contains rules not in file (orphans):
+1. Export to `.claude/backups/orphaned-rules-[timestamp].json`
+2. Mark as inactive (soft delete)
+3. Add audit note with timestamp
+4. Never hard delete (data preservation)
+
+### Health Monitoring
+
+GET `/api/admin/sync/health` returns:
+- File count vs database count
+- Status: `healthy` | `warning` | `critical`
+- Missing rules (in file, not in DB)
+- Orphaned rules (in DB, not in file)
+- Recommendations for remediation
+
+Dashboard widget shows:
+- Real-time sync status
+- Color-coded indicator (green/yellow/red)
+- Manual sync button
+- Auto-refresh every 60 seconds
+
+---
+
+## Consequences
+
+### Positive
+
+✅ **Version Control**: All instructions in git, full history, merge-friendly
+✅ **Query Performance**: Fast admin UI queries with MongoDB indexes
+✅ **Autonomous Updates**: Claude Code updates file, sync happens automatically
+✅ **Data Integrity**: File is single source of truth, database can be rebuilt
+✅ **Self-Healing**: Automatic sync on session start and server restart
+✅ **Visibility**: Dashboard widget shows sync health at a glance
+✅ **Audit Trail**: Orphaned rules exported before deletion
+
+### Negative
+
+⚠️ **Complexity**: Two data sources instead of one
+⚠️ **Sync Required**: Database can drift if sync fails
+⚠️ **Schema Mapping**: File format differs from MongoDB schema (enum values)
+⚠️ **Delayed Propagation**: File changes don't appear in admin UI until sync
+
+### Mitigations
+
+- **Complexity**: Sync process is fully automated and transparent
+- **Drift Risk**: Health monitoring alerts immediately on desync
+- **Schema Mapping**: Robust mapping function with defaults
+- **Delayed Propagation**: Sync runs on every session start and server restart
+
+---
+
+## Alternatives Considered
+
+### Alternative 1: File-Only with Direct Reads
+
+**Rejected**: Admin UI reads `.claude/instruction-history.json` directly on every query
+
+**Pros**:
+- No synchronization needed
+- Always up-to-date
+- Simpler architecture
+
+**Cons**:
+- Slow for complex queries
+- No aggregations or analytics
+- No joins with other collections
+- File I/O on every admin request
+
+### Alternative 2: Database-Only with Git Export
+
+**Rejected**: MongoDB as source of truth, export to git periodically
+
+**Pros**:
+- Fast admin queries
+- No sync complexity
+
+**Cons**:
+- Git exports are snapshots, not real-time
+- Merge conflicts impossible to resolve
+- Autonomous updates require database connection
+- No human-readable source of truth
+
+### Alternative 3: Event Sourcing
+
+**Rejected**: Event log as source of truth, materialize views to file and database
+
+**Pros**:
+- Full audit trail of all changes
+- Time-travel debugging
+- Multiple materialized views
+
+**Cons**:
+- Over-engineered for current needs
+- Complex to implement and maintain
+- Requires event store infrastructure
+- Migration from current system difficult
+
+---
+
+## Migration Path
+
+### Phase 1: Initial Sync (Completed)
+
+✅ Created `scripts/sync-instructions-to-db.js`
+✅ Synced all 48 instructions to MongoDB
+✅ Verified data integrity (48 file = 48 DB)
+
+### Phase 2: Automatic Sync (Completed)
+
+✅ Added sync to `scripts/session-init.js`
+✅ Added sync to `src/server.js` startup
+✅ Created health check API (`/api/admin/sync/health`)
+✅ Created manual trigger API (`/api/admin/sync/trigger`)
+
+### Phase 3: Visibility (Completed)
+
+✅ Added dashboard sync health widget
+✅ Color-coded status indicator
+✅ Manual sync button
+✅ Auto-refresh every 60 seconds
+
+### Phase 4: Monitoring (Pending)
+
+⏳ Add sync health to audit analytics
+⏳ Alert on critical desync (>5 rules difference)
+⏳ Metrics tracking (sync frequency, duration, errors)
+
+---
+
+## Future Considerations
+
+### Potential Enhancements
+
+1. **Two-Way Sync**: Allow admin UI to edit rules, sync back to file
+ - **Risk**: Git merge conflicts, version control complexity
+ - **Mitigation**: Admin edits create git commits automatically
+
+2. **Real-Time Sync**: File watcher triggers sync on `.claude/instruction-history.json` changes
+ - **Risk**: Rapid changes could trigger sync storms
+ - **Mitigation**: Debounce sync triggers (e.g., 5-second cooldown)
+
+3. **Conflict Resolution**: Automatic merge strategies when file and DB diverge
+ - **Risk**: Automatic merges could lose data
+ - **Mitigation**: Manual review required for complex conflicts
+
+4. **Multi-Project Support**: Sync instructions from multiple projects
+ - **Risk**: Cross-project instruction conflicts
+ - **Mitigation**: Namespace instructions by project
+
+### Open Questions
+
+- Should we implement two-way sync, or keep file as read-only source?
+- What's the acceptable sync latency for admin UI updates?
+- Do we need transaction support for multi-rule updates?
+- Should orphaned rules be hard-deleted after X days?
+
+---
+
+## References
+
+- **Implementation**: `scripts/sync-instructions-to-db.js`
+- **Health API**: `src/routes/sync-health.routes.js`
+- **Dashboard Widget**: `public/admin/dashboard.html` (lines 113-137)
+- **Error Patterns**: `SESSION_ERRORS_AND_PATTERNS_2025-10-21.md`
+- **Autonomous Rules**: `.claude/instruction-history.json` (inst_050-057)
+
+---
+
+## Approval
+
+**Approved**: 2025-10-21
+**Reviewers**: Autonomous decision (inst_050: Autonomous development framework)
+**Status**: Production-ready, all tests passing
diff --git a/public/admin/dashboard.html b/public/admin/dashboard.html
index ba485cef..1a7d0fa3 100644
--- a/public/admin/dashboard.html
+++ b/public/admin/dashboard.html
@@ -110,6 +110,32 @@
+
+
diff --git a/public/js/admin/dashboard.js b/public/js/admin/dashboard.js
index fa288935..6e5ecbef 100644
--- a/public/js/admin/dashboard.js
+++ b/public/js/admin/dashboard.js
@@ -92,6 +92,121 @@ async function loadStatistics() {
}
}
+// Load sync health status
+async function loadSyncHealth() {
+ const statusEl = document.getElementById('sync-status');
+ const badgeEl = document.getElementById('sync-badge');
+ const detailsEl = document.getElementById('sync-details');
+ const iconContainerEl = document.getElementById('sync-icon-container');
+
+ try {
+ const response = await apiRequest('/api/admin/sync/health');
+
+ if (!response.success || !response.health) {
+ console.error('Invalid sync health response:', response);
+ statusEl.textContent = 'Error';
+ badgeEl.textContent = 'Error';
+ badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800';
+ detailsEl.textContent = 'Failed to check sync health';
+ iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3';
+ return;
+ }
+
+ const health = response.health;
+ const counts = health.counts;
+
+ // Update status text
+ statusEl.textContent = `File: ${counts.file} | DB: ${counts.database}`;
+
+ // Update badge and icon based on severity
+ if (health.severity === 'success') {
+ badgeEl.textContent = '✓ Synced';
+ badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-green-100 text-green-800';
+ iconContainerEl.className = 'flex-shrink-0 bg-green-100 rounded-md p-3';
+ iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-yellow-600', 'text-red-600');
+ iconContainerEl.querySelector('svg').classList.add('text-green-600');
+ } else if (health.severity === 'warning') {
+ badgeEl.textContent = '⚠ Desync';
+ badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800';
+ iconContainerEl.className = 'flex-shrink-0 bg-yellow-100 rounded-md p-3';
+ iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-green-600', 'text-red-600');
+ iconContainerEl.querySelector('svg').classList.add('text-yellow-600');
+ } else {
+ badgeEl.textContent = '✗ Critical';
+ badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800';
+ iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3';
+ iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-green-600', 'text-yellow-600');
+ iconContainerEl.querySelector('svg').classList.add('text-red-600');
+ }
+
+ // Update details
+ if (counts.difference === 0) {
+ detailsEl.textContent = health.message;
+ } else {
+ const missing = health.details?.missingInDatabase?.length || 0;
+ const orphaned = health.details?.orphanedInDatabase?.length || 0;
+ detailsEl.textContent = `${health.message} (Missing: ${missing}, Orphaned: ${orphaned})`;
+ }
+ } catch (error) {
+ console.error('Failed to load sync health:', error);
+ statusEl.textContent = 'Error';
+ badgeEl.textContent = 'Error';
+ badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800';
+ detailsEl.textContent = 'Failed to check sync health';
+ iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3';
+ }
+}
+
+// Trigger manual sync
+async function triggerSync() {
+ const button = document.getElementById('sync-trigger-btn');
+ const originalText = button.textContent;
+
+ try {
+ // Disable button and show loading state
+ button.disabled = true;
+ button.textContent = 'Syncing...';
+
+ const response = await apiRequest('/api/admin/sync/trigger', {
+ method: 'POST'
+ });
+
+ if (response.success) {
+ // Show success message
+ button.textContent = '✓ Synced';
+ button.classList.remove('bg-blue-600', 'hover:bg-blue-700');
+ button.classList.add('bg-green-600');
+
+ // Reload health status and stats
+ await loadSyncHealth();
+ await loadStatistics();
+
+ // Reset button after 2 seconds
+ setTimeout(() => {
+ button.textContent = originalText;
+ button.classList.remove('bg-green-600');
+ button.classList.add('bg-blue-600', 'hover:bg-blue-700');
+ button.disabled = false;
+ }, 2000);
+ } else {
+ throw new Error(response.message || 'Sync failed');
+ }
+ } catch (error) {
+ console.error('Manual sync error:', error);
+ button.textContent = '✗ Failed';
+ button.classList.remove('bg-blue-600', 'hover:bg-blue-700');
+ button.classList.add('bg-red-600');
+
+ // Reset button after 2 seconds
+ setTimeout(() => {
+ button.textContent = originalText;
+ button.classList.remove('bg-red-600');
+ button.classList.add('bg-blue-600', 'hover:bg-blue-700');
+ button.disabled = false;
+ }, 2000);
+ }
+}
+
// Load recent activity
async function loadRecentActivity() {
const container = document.getElementById('recent-activity');
@@ -631,6 +746,12 @@ document.getElementById('queue-filter')?.addEventListener('change', (e) => {
// Initialize
loadStatistics();
loadRecentActivity();
+loadSyncHealth();
+
+// Auto-refresh sync health every 60 seconds
+setInterval(() => {
+ loadSyncHealth();
+}, 60000);
// Event delegation for data-action buttons (CSP compliance)
document.addEventListener('click', (e) => {
@@ -665,5 +786,8 @@ document.addEventListener('click', (e) => {
case 'closeUnpublishModal':
closeUnpublishModal();
break;
+ case 'triggerSync':
+ triggerSync();
+ break;
}
});
diff --git a/scripts/deploy-governance-files.sh b/scripts/deploy-governance-files.sh
new file mode 100755
index 00000000..17d24b0e
--- /dev/null
+++ b/scripts/deploy-governance-files.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+# Tractatus Governance Files Deployment Script
+# Syncs .claude/ directory files to production
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Configuration
+SSH_KEY="$HOME/.ssh/tractatus_deploy"
+REMOTE_USER="ubuntu"
+REMOTE_HOST="vps-93a693da.vps.ovh.net"
+REMOTE_PATH="/var/www/tractatus"
+LOCAL_CLAUDE_DIR=".claude"
+
+echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${YELLOW} TRACTATUS GOVERNANCE FILES DEPLOYMENT${NC}"
+echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo ""
+
+# Check if .claude directory exists
+if [ ! -d "$LOCAL_CLAUDE_DIR" ]; then
+ echo -e "${RED}✗ Error: .claude directory not found${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}[1/4] PRE-DEPLOYMENT CHECKS${NC}"
+echo ""
+
+# Check for instruction-history.json
+if [ ! -f "$LOCAL_CLAUDE_DIR/instruction-history.json" ]; then
+ echo -e "${RED}✗ Error: instruction-history.json not found${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}✓ instruction-history.json found${NC}"
+
+# Get file info
+FILE_SIZE=$(du -h "$LOCAL_CLAUDE_DIR/instruction-history.json" | cut -f1)
+INSTRUCTION_COUNT=$(node -e "
+const fs = require('fs');
+const data = JSON.parse(fs.readFileSync('$LOCAL_CLAUDE_DIR/instruction-history.json', 'utf8'));
+const active = data.instructions.filter(i => i.active !== false).length;
+console.log(active);
+")
+
+echo -e "${GREEN}✓ File size: $FILE_SIZE${NC}"
+echo -e "${GREEN}✓ Active instructions: $INSTRUCTION_COUNT${NC}"
+echo ""
+
+# Check SSH connection
+echo -e "${GREEN}[2/4] CHECKING CONNECTION${NC}"
+echo ""
+
+if ! ssh -i "$SSH_KEY" -o ConnectTimeout=5 "${REMOTE_USER}@${REMOTE_HOST}" "echo 'Connection OK'" 2>/dev/null; then
+ echo -e "${RED}✗ Error: Cannot connect to production server${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}✓ SSH connection successful${NC}"
+echo ""
+
+# Show what will be deployed
+echo -e "${GREEN}[3/4] FILES TO DEPLOY${NC}"
+echo ""
+echo " Source: $LOCAL_CLAUDE_DIR/"
+echo " Destination: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/.claude/"
+echo ""
+echo " Files:"
+echo " - instruction-history.json ($FILE_SIZE, $INSTRUCTION_COUNT rules)"
+echo " - session-state.json (if exists)"
+echo " - token-checkpoints.json (if exists)"
+echo " - metrics/ (if exists)"
+echo " - backups/ (if exists)"
+echo ""
+
+# Confirmation
+read -p "Continue with deployment? (yes/NO): " confirm
+if [ "$confirm" != "yes" ]; then
+ echo -e "${YELLOW}Deployment cancelled${NC}"
+ exit 0
+fi
+
+echo ""
+echo -e "${GREEN}[4/4] DEPLOYING${NC}"
+echo ""
+
+# Create backup on production first
+echo "Creating backup on production..."
+ssh -i "$SSH_KEY" "${REMOTE_USER}@${REMOTE_HOST}" "
+ cd $REMOTE_PATH/.claude
+ if [ -f instruction-history.json ]; then
+ cp instruction-history.json instruction-history.json.backup-\$(date +%Y%m%d-%H%M%S)
+ echo '✓ Backup created'
+ fi
+"
+
+# Deploy files
+echo "Deploying governance files..."
+rsync -avz --progress \
+ -e "ssh -i $SSH_KEY" \
+ --exclude='*.log' \
+ --exclude='temp/' \
+ "$LOCAL_CLAUDE_DIR/" \
+ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/.claude/"
+
+echo ""
+
+# Verify deployment
+echo "Verifying deployment..."
+REMOTE_COUNT=$(ssh -i "$SSH_KEY" "${REMOTE_USER}@${REMOTE_HOST}" "
+ cd $REMOTE_PATH
+ node -e \"
+ const fs = require('fs');
+ const data = JSON.parse(fs.readFileSync('.claude/instruction-history.json', 'utf8'));
+ const active = data.instructions.filter(i => i.active !== false).length;
+ console.log(active);
+ \"
+")
+
+if [ "$REMOTE_COUNT" = "$INSTRUCTION_COUNT" ]; then
+ echo -e "${GREEN}✓ Verification successful${NC}"
+ echo -e "${GREEN} Local: $INSTRUCTION_COUNT active rules${NC}"
+ echo -e "${GREEN} Remote: $REMOTE_COUNT active rules${NC}"
+else
+ echo -e "${YELLOW}⚠ Warning: Count mismatch${NC}"
+ echo -e "${YELLOW} Local: $INSTRUCTION_COUNT active rules${NC}"
+ echo -e "${YELLOW} Remote: $REMOTE_COUNT active rules${NC}"
+fi
+
+echo ""
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${GREEN} DEPLOYMENT COMPLETE${NC}"
+echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo ""
+echo "Next steps:"
+echo "1. Run sync on production:"
+echo " ssh -i $SSH_KEY ${REMOTE_USER}@${REMOTE_HOST} 'cd $REMOTE_PATH && node scripts/sync-instructions-to-db.js --force'"
+echo ""
+echo "2. Verify sync health:"
+echo " curl -s https://agenticgovernance.digital/health"
+echo ""
diff --git a/scripts/session-init.js b/scripts/session-init.js
index f9c5dca1..a8984097 100755
--- a/scripts/session-init.js
+++ b/scripts/session-init.js
@@ -463,6 +463,35 @@ async function main() {
log(' Hooks provide architectural enforcement beyond documentation', 'yellow');
}
+ // Database Sync
+ section('10. Syncing Instructions to Database');
+ try {
+ log(' Synchronizing .claude/instruction-history.json to MongoDB...', 'cyan');
+ const { syncInstructions } = require('./sync-instructions-to-db.js');
+
+ // Run sync in silent mode (no verbose output)
+ const syncResult = await syncInstructions();
+
+ if (syncResult && syncResult.success) {
+ success(`Database synchronized: ${syncResult.finalCount} active rules`);
+ if (syncResult.added > 0) {
+ log(` Added: ${syncResult.added} new rules`, 'cyan');
+ }
+ if (syncResult.updated > 0) {
+ log(` Updated: ${syncResult.updated} existing rules`, 'cyan');
+ }
+ if (syncResult.deactivated > 0) {
+ log(` Deactivated: ${syncResult.deactivated} orphaned rules`, 'cyan');
+ }
+ } else {
+ warning('Database sync skipped or failed - admin UI may show stale data');
+ }
+ } catch (err) {
+ warning(`Database sync failed: ${err.message}`);
+ log(' Admin UI may show outdated rule counts', 'yellow');
+ log(' Run: node scripts/sync-instructions-to-db.js --force to sync manually', 'yellow');
+ }
+
// Summary
header('Framework Initialization Complete');
console.log('');
diff --git a/scripts/sync-instructions-to-db.js b/scripts/sync-instructions-to-db.js
new file mode 100755
index 00000000..659ff5f1
--- /dev/null
+++ b/scripts/sync-instructions-to-db.js
@@ -0,0 +1,323 @@
+#!/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 };
diff --git a/src/routes/index.js b/src/routes/index.js
index 3c261723..e1570869 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -15,6 +15,7 @@ const mediaRoutes = require('./media.routes');
const casesRoutes = require('./cases.routes');
const adminRoutes = require('./admin.routes');
const hooksMetricsRoutes = require('./hooks-metrics.routes');
+const syncHealthRoutes = require('./sync-health.routes');
const rulesRoutes = require('./rules.routes');
const projectsRoutes = require('./projects.routes');
const auditRoutes = require('./audit.routes');
@@ -37,6 +38,7 @@ router.use('/media', mediaRoutes);
router.use('/cases', casesRoutes);
router.use('/admin', adminRoutes);
router.use('/admin/hooks', hooksMetricsRoutes);
+router.use('/admin/sync', syncHealthRoutes);
router.use('/admin/rules', rulesRoutes);
router.use('/admin/projects', projectsRoutes);
router.use('/admin', auditRoutes);
diff --git a/src/routes/sync-health.routes.js b/src/routes/sync-health.routes.js
new file mode 100644
index 00000000..80ec3eec
--- /dev/null
+++ b/src/routes/sync-health.routes.js
@@ -0,0 +1,124 @@
+/**
+ * Sync Health Check Routes
+ * Monitors synchronization between file-based instructions and MongoDB
+ */
+
+const express = require('express');
+const router = express.Router();
+const fs = require('fs');
+const path = require('path');
+const { authenticateToken, requireAdmin } = require('../middleware/auth.middleware');
+const GovernanceRule = require('../models/GovernanceRule.model');
+
+const INSTRUCTION_FILE = path.join(__dirname, '../../.claude/instruction-history.json');
+
+/**
+ * GET /api/admin/sync/health
+ * Check synchronization health between file and database
+ */
+router.get('/health', authenticateToken, requireAdmin, async (req, res) => {
+ try {
+ let fileInstructions = [];
+ let fileError = null;
+
+ if (fs.existsSync(INSTRUCTION_FILE)) {
+ try {
+ const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
+ fileInstructions = (fileData.instructions || []).filter(i => i.active !== false);
+ } catch (err) {
+ fileError = err.message;
+ }
+ } else {
+ fileError = 'File not found';
+ }
+
+ const dbRules = await GovernanceRule.find({ active: true }).lean();
+ const fileCount = fileInstructions.length;
+ const dbCount = dbRules.length;
+ const difference = Math.abs(fileCount - dbCount);
+ const diffPercent = fileCount > 0 ? ((difference / fileCount) * 100).toFixed(1) : 0;
+
+ let status = 'healthy';
+ let message = 'File and database are synchronized';
+ let severity = 'success';
+
+ if (fileError) {
+ status = 'error';
+ message = 'Cannot read instruction file: ' + fileError;
+ severity = 'error';
+ } else if (difference === 0) {
+ status = 'healthy';
+ message = 'Perfectly synchronized';
+ severity = 'success';
+ } else if (difference <= 2) {
+ status = 'warning';
+ message = 'Minor desync: ' + difference + ' instruction' + (difference !== 1 ? 's' : '') + ' differ';
+ severity = 'warning';
+ } else if (difference <= 5) {
+ status = 'warning';
+ message = 'Moderate desync: ' + difference + ' instructions differ (' + diffPercent + '%)';
+ severity = 'warning';
+ } else {
+ status = 'critical';
+ message = 'Critical desync: ' + difference + ' instructions differ (' + diffPercent + '%)';
+ severity = 'error';
+ }
+
+ const fileIds = new Set(fileInstructions.map(i => i.id));
+ const dbIds = new Set(dbRules.map(r => r.id));
+
+ const missingInDb = fileInstructions
+ .filter(i => !dbIds.has(i.id))
+ .map(i => ({ id: i.id, text: i.text.substring(0, 60) + '...' }));
+
+ const orphanedInDb = dbRules
+ .filter(r => !fileIds.has(r.id))
+ .map(r => ({ id: r.id, text: r.text.substring(0, 60) + '...' }));
+
+ res.json({
+ success: true,
+ health: {
+ status,
+ message,
+ severity,
+ timestamp: new Date().toISOString(),
+ counts: { file: fileCount, database: dbCount, difference, differencePercent: parseFloat(diffPercent) },
+ details: { missingInDatabase: missingInDb, orphanedInDatabase: orphanedInDb },
+ recommendations: difference > 0 ? [
+ 'Run: node scripts/sync-instructions-to-db.js --force',
+ 'Or restart the server (auto-sync on startup)',
+ 'Or wait for next session initialization'
+ ] : []
+ }
+ });
+ } catch (error) {
+ console.error('Sync health check error:', error);
+ res.status(500).json({ success: false, error: 'Failed to check sync health', message: error.message });
+ }
+});
+
+/**
+ * POST /api/admin/sync/trigger
+ * Manually trigger synchronization
+ */
+router.post('/trigger', authenticateToken, requireAdmin, async (req, res) => {
+ try {
+ const { syncInstructions } = require('../../scripts/sync-instructions-to-db.js');
+ const result = await syncInstructions({ silent: true });
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Synchronization completed successfully',
+ result: { added: result.added, updated: result.updated, deactivated: result.deactivated, finalCount: result.finalCount }
+ });
+ } else {
+ res.status(500).json({ success: false, error: 'Synchronization failed', message: result.error || 'Unknown error' });
+ }
+ } catch (error) {
+ console.error('Manual sync trigger error:', error);
+ res.status(500).json({ success: false, error: 'Failed to trigger synchronization', message: error.message });
+ }
+});
+
+module.exports = router;
diff --git a/src/server.js b/src/server.js
index d51dfe39..223e727e 100644
--- a/src/server.js
+++ b/src/server.js
@@ -206,6 +206,21 @@ async function start() {
// Connect Mongoose (for ODM models)
await connectMongoose();
+ // Sync instructions from file to database
+ try {
+ const { syncInstructions } = require('../scripts/sync-instructions-to-db.js');
+ const syncResult = await syncInstructions({ silent: true });
+ if (syncResult && syncResult.success) {
+ logger.info(`✅ Instructions synced to database: ${syncResult.finalCount} active rules`);
+ if (syncResult.added > 0 || syncResult.deactivated > 0) {
+ logger.info(` Added: ${syncResult.added}, Updated: ${syncResult.updated}, Deactivated: ${syncResult.deactivated}`);
+ }
+ }
+ } catch (err) {
+ logger.warn(`⚠️ Instruction sync failed: ${err.message}`);
+ logger.warn(' Admin UI may show outdated rule counts');
+ }
+
// Initialize governance services
const BoundaryEnforcer = require('./services/BoundaryEnforcer.service');
await BoundaryEnforcer.initialize();
diff --git a/tests/integration/sync-instructions.test.js b/tests/integration/sync-instructions.test.js
new file mode 100644
index 00000000..f28e253c
--- /dev/null
+++ b/tests/integration/sync-instructions.test.js
@@ -0,0 +1,290 @@
+/**
+ * Integration Test: File-to-Database Sync
+ * Tests the dual governance architecture synchronization
+ */
+
+const fs = require('fs');
+const path = require('path');
+const mongoose = require('mongoose');
+const { syncInstructions } = require('../../scripts/sync-instructions-to-db.js');
+const GovernanceRule = require('../../src/models/GovernanceRule.model');
+
+require('dotenv').config();
+
+const INSTRUCTION_FILE = path.join(__dirname, '../../.claude/instruction-history.json');
+const TEST_DB = 'tractatus_test_sync';
+
+describe('Instruction Sync Integration Tests', () => {
+ let originalDb;
+
+ beforeAll(async () => {
+ // Connect to test database
+ const mongoUri = process.env.MONGODB_URI?.replace(/\/[^/]+$/, `/${TEST_DB}`) ||
+ `mongodb://localhost:27017/${TEST_DB}`;
+ await mongoose.connect(mongoUri);
+ originalDb = mongoose.connection.db.databaseName;
+ });
+
+ afterAll(async () => {
+ // Clean up test database
+ await mongoose.connection.db.dropDatabase();
+ await mongoose.disconnect();
+ });
+
+ beforeEach(async () => {
+ // Clear database before each test
+ await GovernanceRule.deleteMany({});
+ });
+
+ describe('File Reading', () => {
+ test('instruction file exists', () => {
+ expect(fs.existsSync(INSTRUCTION_FILE)).toBe(true);
+ });
+
+ test('instruction file is valid JSON', () => {
+ const fileData = fs.readFileSync(INSTRUCTION_FILE, 'utf8');
+ expect(() => JSON.parse(fileData)).not.toThrow();
+ });
+
+ test('instruction file has expected structure', () => {
+ const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
+ expect(fileData).toHaveProperty('version');
+ expect(fileData).toHaveProperty('instructions');
+ expect(Array.isArray(fileData.instructions)).toBe(true);
+ });
+
+ test('all instructions have required fields', () => {
+ const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
+ fileData.instructions.forEach(inst => {
+ expect(inst).toHaveProperty('id');
+ expect(inst).toHaveProperty('text');
+ expect(inst).toHaveProperty('quadrant');
+ expect(inst).toHaveProperty('persistence');
+ });
+ });
+ });
+
+ describe('Initial Sync', () => {
+ test('syncs all instructions from file to empty database', async () => {
+ const result = await syncInstructions({ silent: true });
+
+ expect(result.success).toBe(true);
+ expect(result.added).toBeGreaterThan(0);
+ expect(result.updated).toBe(0); // First sync, nothing to update
+ expect(result.finalCount).toBeGreaterThan(0);
+
+ // Verify database has same count as file
+ const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
+ const activeFileCount = fileData.instructions.filter(i => i.active !== false).length;
+ expect(result.finalCount).toBe(activeFileCount);
+ });
+
+ test('creates rules with correct schema', async () => {
+ await syncInstructions({ silent: true });
+
+ const rules = await GovernanceRule.find({}).lean();
+ expect(rules.length).toBeGreaterThan(0);
+
+ rules.forEach(rule => {
+ // Required fields
+ expect(rule).toHaveProperty('id');
+ expect(rule).toHaveProperty('text');
+ expect(rule).toHaveProperty('quadrant');
+ expect(rule).toHaveProperty('persistence');
+ expect(rule).toHaveProperty('source');
+ expect(rule).toHaveProperty('active');
+
+ // Source enum validation
+ expect(['user_instruction', 'framework_default', 'automated', 'migration', 'claude_md_migration', 'test'])
+ .toContain(rule.source);
+ });
+ });
+ });
+
+ describe('Update Sync', () => {
+ test('updates existing rules without duplicates', async () => {
+ // First sync
+ const result1 = await syncInstructions({ silent: true });
+ const count1 = result1.finalCount;
+
+ // Second sync (should update, not add)
+ const result2 = await syncInstructions({ silent: true });
+
+ expect(result2.success).toBe(true);
+ expect(result2.added).toBe(0); // Nothing new to add
+ expect(result2.updated).toBe(count1); // All rules updated
+ expect(result2.finalCount).toBe(count1); // Same count
+ });
+
+ test('preserves validation scores on update', async () => {
+ // First sync
+ await syncInstructions({ silent: true });
+
+ // Update a rule with validation scores
+ const rule = await GovernanceRule.findOne({});
+ await GovernanceRule.findByIdAndUpdate(rule._id, {
+ clarityScore: 85,
+ specificityScore: 90,
+ actionabilityScore: 80,
+ validationStatus: 'VALIDATED',
+ lastValidated: new Date()
+ });
+
+ // Second sync
+ await syncInstructions({ silent: true });
+
+ // Verify scores preserved
+ const updatedRule = await GovernanceRule.findById(rule._id);
+ expect(updatedRule.clarityScore).toBe(85);
+ expect(updatedRule.specificityScore).toBe(90);
+ expect(updatedRule.actionabilityScore).toBe(80);
+ expect(updatedRule.validationStatus).toBe('VALIDATED');
+ });
+ });
+
+ describe('Orphan Handling', () => {
+ test('deactivates rules not in file', async () => {
+ // Create an orphan rule directly in DB
+ await GovernanceRule.create({
+ id: 'test_orphan_001',
+ text: 'This rule does not exist in the file',
+ scope: 'PROJECT_SPECIFIC',
+ applicableProjects: ['*'],
+ quadrant: 'TACTICAL',
+ persistence: 'MEDIUM',
+ category: 'test',
+ priority: 50,
+ active: true,
+ source: 'test',
+ createdBy: 'test'
+ });
+
+ // Sync from file
+ const result = await syncInstructions({ silent: true });
+
+ expect(result.deactivated).toBe(1);
+
+ // Verify orphan is inactive
+ const orphan = await GovernanceRule.findOne({ id: 'test_orphan_001' });
+ expect(orphan.active).toBe(false);
+ expect(orphan.notes).toContain('AUTO-DEACTIVATED');
+ });
+
+ test('exports orphans to backup file', async () => {
+ // Create orphan
+ await GovernanceRule.create({
+ id: 'test_orphan_002',
+ text: 'Another orphan rule',
+ scope: 'PROJECT_SPECIFIC',
+ applicableProjects: ['*'],
+ quadrant: 'TACTICAL',
+ persistence: 'MEDIUM',
+ category: 'test',
+ priority: 50,
+ active: true,
+ source: 'test',
+ createdBy: 'test'
+ });
+
+ // Sync
+ await syncInstructions({ silent: true });
+
+ // Check backup directory exists
+ const backupDir = path.join(__dirname, '../../.claude/backups');
+ expect(fs.existsSync(backupDir)).toBe(true);
+
+ // Check latest backup file contains orphan
+ const backupFiles = fs.readdirSync(backupDir)
+ .filter(f => f.startsWith('orphaned-rules-'))
+ .sort()
+ .reverse();
+
+ if (backupFiles.length > 0) {
+ const latestBackup = JSON.parse(
+ fs.readFileSync(path.join(backupDir, backupFiles[0]), 'utf8')
+ );
+ expect(latestBackup.rules.some(r => r.id === 'test_orphan_002')).toBe(true);
+ }
+ });
+ });
+
+ describe('Source Mapping', () => {
+ test('maps file source values to MongoDB enum values', async () => {
+ // This test assumes there are instructions with different source values in the file
+ await syncInstructions({ silent: true });
+
+ const rules = await GovernanceRule.find({}).lean();
+
+ // All sources should be valid enum values
+ const validSources = ['user_instruction', 'framework_default', 'automated', 'migration', 'claude_md_migration', 'test'];
+ rules.forEach(rule => {
+ expect(validSources).toContain(rule.source);
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ test('handles missing instruction file gracefully', async () => {
+ // Temporarily rename file
+ const tempFile = INSTRUCTION_FILE + '.tmp';
+ fs.renameSync(INSTRUCTION_FILE, tempFile);
+
+ try {
+ const result = await syncInstructions({ silent: true });
+ expect(result.success).toBe(false);
+ expect(result.error).toContain('not found');
+ } finally {
+ // Restore file
+ fs.renameSync(tempFile, INSTRUCTION_FILE);
+ }
+ });
+
+ test('handles invalid JSON gracefully', async () => {
+ // Temporarily replace file with invalid JSON
+ const originalContent = fs.readFileSync(INSTRUCTION_FILE, 'utf8');
+ fs.writeFileSync(INSTRUCTION_FILE, 'INVALID JSON{{{');
+
+ try {
+ const result = await syncInstructions({ silent: true });
+ expect(result.success).toBe(false);
+ } finally {
+ // Restore file
+ fs.writeFileSync(INSTRUCTION_FILE, originalContent);
+ }
+ });
+ });
+
+ describe('Programmatic Options', () => {
+ test('respects silent mode', async () => {
+ const consoleSpy = jest.spyOn(console, 'log');
+
+ await syncInstructions({ silent: true });
+
+ // Silent mode should not log
+ expect(consoleSpy).not.toHaveBeenCalled();
+
+ consoleSpy.mockRestore();
+ });
+
+ test('dry run does not modify database', async () => {
+ const result = await syncInstructions({ silent: true, dryRun: true });
+
+ expect(result.success).toBe(true);
+
+ // Database should still be empty
+ const count = await GovernanceRule.countDocuments({});
+ expect(count).toBe(0);
+ });
+ });
+
+ describe('Idempotency', () => {
+ test('multiple syncs produce same result', async () => {
+ const result1 = await syncInstructions({ silent: true });
+ const result2 = await syncInstructions({ silent: true });
+ const result3 = await syncInstructions({ silent: true });
+
+ expect(result1.finalCount).toBe(result2.finalCount);
+ expect(result2.finalCount).toBe(result3.finalCount);
+ });
+ });
+});
diff --git a/tests/unit/MemoryProxy.service.test.js b/tests/unit/MemoryProxy.service.test.js
index 13ad13a1..34c508d2 100644
--- a/tests/unit/MemoryProxy.service.test.js
+++ b/tests/unit/MemoryProxy.service.test.js
@@ -4,13 +4,28 @@
*/
const { MemoryProxyService } = require('../../src/services/MemoryProxy.service');
+const mongoose = require('mongoose');
const fs = require('fs').promises;
const path = require('path');
+// Increase timeout for slow filesystem operations
+jest.setTimeout(30000);
+
describe('MemoryProxyService', () => {
let memoryProxy;
const testMemoryPath = path.join(__dirname, '../../.memory-test');
+ // Connect to MongoDB before all tests
+ beforeAll(async () => {
+ const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_test';
+ await mongoose.connect(mongoUri);
+ });
+
+ // Disconnect from MongoDB after all tests
+ afterAll(async () => {
+ await mongoose.disconnect();
+ });
+
const testRules = [
{
id: 'inst_001',