tractatus/scripts/validate-deployment.js
TheFlow ac2db33732 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

236 lines
5.9 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* Pre-Deployment Validation
*
* Validates rsync/scp/deployment commands against inst_025 rules:
* - Checks if source files have different subdirectories
* - Ensures separate commands for different directory levels
* - Prevents directory structure flattening
*
* Usage:
* node scripts/validate-deployment.js --command "rsync ..."
* node scripts/validate-deployment.js --files "file1 file2" --target "remote:path"
*
* Exit codes:
* 0 = Valid deployment
* 1 = Invalid (violates inst_025)
* 2 = Error
*
* Copyright 2025 Tractatus Project
* Licensed under Apache License 2.0
*/
const fs = require('fs');
const path = require('path');
/**
* Parse rsync command to extract source files and target
*/
function parseRsyncCommand(command) {
// Match rsync with options and files
const rsyncPattern = /rsync\s+([^"'\s]+(?:\s+[^"'\s]+)*)\s+((?:[^\s]+\s+)*)([\w@.-]+:[^\s]+|[^\s]+)$/;
const match = command.match(rsyncPattern);
if (!match) {
return null;
}
// Extract flags and files
const parts = command.split(/\s+/).filter(p => p.length > 0);
const rsyncIndex = parts.findIndex(p => p === 'rsync' || p.endsWith('/rsync'));
if (rsyncIndex === -1) {
return null;
}
const filesAndTarget = parts.slice(rsyncIndex + 1);
// Last item is target
const target = filesAndTarget[filesAndTarget.length - 1];
// Everything before target that's not a flag is a file
const files = [];
for (let i = 0; i < filesAndTarget.length - 1; i++) {
const item = filesAndTarget[i];
if (!item.startsWith('-')) {
files.push(item);
}
}
return {
files,
target,
command
};
}
/**
* Check if files have different subdirectory paths
*/
function checkDirectoryMismatch(files) {
if (files.length <= 1) {
return { hasMismatch: false, directories: [] };
}
const directories = files.map(f => {
const dir = path.dirname(f);
return dir === '.' ? '' : dir;
});
const uniqueDirs = [...new Set(directories)];
return {
hasMismatch: uniqueDirs.length > 1,
directories: uniqueDirs,
filesByDir: Object.fromEntries(
uniqueDirs.map(dir => [
dir,
files.filter(f => path.dirname(f) === (dir || '.'))
])
)
};
}
/**
* Validate deployment command
*/
function validateDeployment(command) {
const parsed = parseRsyncCommand(command);
if (!parsed) {
return {
valid: false,
error: 'Could not parse rsync command',
suggestion: null
};
}
const { files, target } = parsed;
if (files.length === 0) {
return {
valid: false,
error: 'No source files specified',
suggestion: null
};
}
// Check for directory mismatch
const dirCheck = checkDirectoryMismatch(files);
if (!dirCheck.hasMismatch) {
return {
valid: true,
message: 'Deployment command is valid - all files in same directory',
files,
target
};
}
// Violation detected
return {
valid: false,
error: `inst_025 violation: Files from different subdirectories in single rsync command`,
details: {
file_count: files.length,
unique_directories: dirCheck.directories.length,
directories: dirCheck.directories,
filesByDir: dirCheck.filesByDir
},
suggestion: generateSeparateCommands(dirCheck.filesByDir, target, command)
};
}
/**
* Generate separate rsync commands for each directory
*/
function generateSeparateCommands(filesByDir, target, originalCommand) {
const commands = [];
// Extract rsync flags from original command
const flagMatch = originalCommand.match(/rsync\s+([^/\s][^\s]*)/);
const flags = flagMatch ? flagMatch[1] : '-avz --progress';
Object.entries(filesByDir).forEach(([dir, files]) => {
const targetWithDir = dir ? `${target}/${dir}/` : target;
files.forEach(file => {
const cmd = `rsync ${flags} ${file} ${targetWithDir}`;
commands.push(cmd);
});
});
return commands;
}
/**
* Display validation results
*/
function displayResults(result) {
if (result.valid) {
console.log('\x1b[32m✅ Deployment command is VALID\x1b[0m');
console.log(` Files: ${result.files.length}`);
console.log(` Target: ${result.target}`);
return 0;
}
console.log('\x1b[31m❌ Deployment command VIOLATES inst_025\x1b[0m');
console.log(`\n Error: ${result.error}\n`);
if (result.details) {
console.log(' Details:');
console.log(` File count: ${result.details.file_count}`);
console.log(` Unique directories: ${result.details.unique_directories}`);
console.log(' Directories:');
result.details.directories.forEach(dir => {
const dirDisplay = dir || '(root)';
const fileCount = result.details.filesByDir[dir].length;
console.log(`${dirDisplay} (${fileCount} files)`);
result.details.filesByDir[dir].forEach(file => {
console.log(` - ${path.basename(file)}`);
});
});
}
if (result.suggestion) {
console.log('\n \x1b[33mSuggested fix (separate commands per directory):\x1b[0m\n');
result.suggestion.forEach((cmd, i) => {
console.log(` ${i + 1}. ${cmd}`);
});
console.log('');
}
return 1;
}
/**
* Main
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log('Pre-Deployment Validation');
console.log('\nUsage:');
console.log(' node scripts/validate-deployment.js --command "rsync ..."');
console.log('\nExample:');
console.log(' node scripts/validate-deployment.js --command "rsync -avz file1 file2/sub/file remote:path"');
process.exit(0);
}
const commandIndex = args.indexOf('--command');
if (commandIndex === -1 || !args[commandIndex + 1]) {
console.error('Error: --command flag required');
process.exit(2);
}
const command = args[commandIndex + 1];
const result = validateDeployment(command);
const exitCode = displayResults(result);
process.exit(exitCode);
}
main();