- 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>
310 lines
9.3 KiB
JavaScript
Executable file
310 lines
9.3 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* CrossReferenceValidator - Active Enforcement Module
|
|
*
|
|
* Validates proposed actions against instruction history to prevent
|
|
* pattern recognition bias (the 27027 problem).
|
|
*
|
|
* This module can be called from:
|
|
* - Hook validators (automatic enforcement)
|
|
* - Pre-action-check script (manual invocation)
|
|
* - Directly by AI in code (voluntary invocation)
|
|
*
|
|
* Copyright 2025 Tractatus Project
|
|
* Licensed under Apache License 2.0
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../.claude/instruction-history.json');
|
|
const SESSION_STATE_PATH = path.join(__dirname, '../../.claude/session-state.json');
|
|
|
|
class CrossReferenceValidator {
|
|
constructor(options = {}) {
|
|
this.silent = options.silent || false;
|
|
this.instructions = null;
|
|
this.loadInstructions();
|
|
}
|
|
|
|
loadInstructions() {
|
|
try {
|
|
if (!fs.existsSync(INSTRUCTION_HISTORY_PATH)) {
|
|
this.log('warning', 'Instruction history not found');
|
|
this.instructions = [];
|
|
return;
|
|
}
|
|
|
|
const data = JSON.parse(fs.readFileSync(INSTRUCTION_HISTORY_PATH, 'utf8'));
|
|
this.instructions = data.instructions || [];
|
|
this.log('info', `Loaded ${this.instructions.length} instructions`);
|
|
} catch (error) {
|
|
this.log('error', `Failed to load instructions: ${error.message}`);
|
|
this.instructions = [];
|
|
}
|
|
}
|
|
|
|
log(level, message) {
|
|
if (this.silent && level !== 'error') return;
|
|
|
|
const colors = {
|
|
info: '\x1b[36m',
|
|
warning: '\x1b[33m',
|
|
error: '\x1b[31m',
|
|
success: '\x1b[32m',
|
|
reset: '\x1b[0m'
|
|
};
|
|
|
|
const prefix = {
|
|
info: '[CrossReferenceValidator]',
|
|
warning: '[⚠ CrossReferenceValidator]',
|
|
error: '[✗ CrossReferenceValidator]',
|
|
success: '[✓ CrossReferenceValidator]'
|
|
}[level];
|
|
|
|
console.log(`${colors[level]}${prefix} ${message}${colors.reset}`);
|
|
}
|
|
|
|
/**
|
|
* Validate a file edit action
|
|
*/
|
|
validateFileEdit(filePath, oldString, newString) {
|
|
const relevantInstructions = this.findRelevantInstructions('file-edit', filePath);
|
|
|
|
if (relevantInstructions.length === 0) {
|
|
return { passed: true, relevantInstructions: [] };
|
|
}
|
|
|
|
this.log('info', `Found ${relevantInstructions.length} relevant instructions for file edit`);
|
|
|
|
// Check for conflicts
|
|
const conflicts = [];
|
|
|
|
relevantInstructions.forEach(inst => {
|
|
// Check CSP instructions (inst_008)
|
|
if (inst.id === 'inst_008' && (filePath.endsWith('.html') || filePath.endsWith('.js'))) {
|
|
const cspPatterns = [
|
|
{ name: 'inline event handlers', regex: /\son\w+\s*=\s*["'][^"']*["']/gi },
|
|
{ name: 'inline styles', regex: /\sstyle\s*=\s*["'][^"']+["']/gi },
|
|
{ name: 'javascript: URLs', regex: /href\s*=\s*["']javascript:[^"']*["']/gi }
|
|
];
|
|
|
|
cspPatterns.forEach(pattern => {
|
|
if (pattern.regex.test(newString)) {
|
|
conflicts.push({
|
|
instruction: inst.id,
|
|
issue: `New content contains ${pattern.name} (CSP violation)`,
|
|
severity: 'HIGH'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check pre-action-check requirement (inst_038)
|
|
if (inst.id === 'inst_038') {
|
|
// This will be checked in the hook itself
|
|
}
|
|
});
|
|
|
|
return {
|
|
passed: conflicts.length === 0,
|
|
conflicts,
|
|
relevantInstructions
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate a Bash command
|
|
*/
|
|
validateBashCommand(command) {
|
|
const relevantInstructions = this.findRelevantInstructions('bash', command);
|
|
|
|
if (relevantInstructions.length === 0) {
|
|
return { passed: true, relevantInstructions: [] };
|
|
}
|
|
|
|
this.log('info', `Found ${relevantInstructions.length} relevant instructions for Bash command`);
|
|
|
|
const conflicts = [];
|
|
|
|
relevantInstructions.forEach(inst => {
|
|
// Check deployment instructions
|
|
if (inst.id === 'inst_025' && command.includes('rsync')) {
|
|
// Deployment directory structure check
|
|
// This is handled in validate-bash-command.js for detailed parsing
|
|
// Here we just flag it as relevant
|
|
this.log('info', `inst_025 (deployment structure) applies to this rsync command`);
|
|
}
|
|
|
|
// Check permission instructions
|
|
if (inst.id === 'inst_022' && command.includes('rsync') && !command.includes('--chmod')) {
|
|
if (command.includes('vps-93a693da') || command.includes('/var/www/')) {
|
|
conflicts.push({
|
|
instruction: inst.id,
|
|
issue: 'rsync to production without --chmod flag',
|
|
severity: 'MEDIUM'
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
passed: conflicts.length === 0,
|
|
conflicts,
|
|
relevantInstructions
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Find instructions relevant to a specific action
|
|
*/
|
|
findRelevantInstructions(actionType, context) {
|
|
if (!this.instructions) {
|
|
return [];
|
|
}
|
|
|
|
const relevant = [];
|
|
|
|
this.instructions.forEach(inst => {
|
|
// Only check active HIGH or MEDIUM persistence instructions
|
|
if (!inst.active) return;
|
|
if (inst.persistence !== 'HIGH' && inst.persistence !== 'MEDIUM') return;
|
|
|
|
// Match by action type and context
|
|
let isRelevant = false;
|
|
|
|
if (actionType === 'file-edit' || actionType === 'file-write') {
|
|
// CSP instructions
|
|
if (inst.id === 'inst_008') isRelevant = true;
|
|
|
|
// Pre-action-check requirement
|
|
if (inst.id === 'inst_038') isRelevant = true;
|
|
|
|
// File-specific instructions
|
|
if (inst.text && inst.text.toLowerCase().includes('file')) isRelevant = true;
|
|
}
|
|
|
|
if (actionType === 'bash') {
|
|
// Deployment instructions
|
|
if (inst.id === 'inst_025' && context.includes('rsync')) isRelevant = true;
|
|
if (inst.id === 'inst_020' && context.includes('rsync')) isRelevant = true;
|
|
if (inst.id === 'inst_022' && context.includes('rsync')) isRelevant = true;
|
|
|
|
// Git commit instructions
|
|
if (context.includes('git commit') && inst.text && inst.text.includes('git')) {
|
|
isRelevant = true;
|
|
}
|
|
}
|
|
|
|
if (isRelevant) {
|
|
relevant.push(inst);
|
|
}
|
|
});
|
|
|
|
return relevant;
|
|
}
|
|
|
|
/**
|
|
* Update session state with validation activity
|
|
*/
|
|
updateSessionState() {
|
|
try {
|
|
let sessionState = {};
|
|
if (fs.existsSync(SESSION_STATE_PATH)) {
|
|
sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
}
|
|
|
|
if (!sessionState.framework_components) {
|
|
sessionState.framework_components = {};
|
|
}
|
|
|
|
if (!sessionState.framework_components.CrossReferenceValidator) {
|
|
sessionState.framework_components.CrossReferenceValidator = {
|
|
message: 0,
|
|
tokens: 0,
|
|
timestamp: null,
|
|
last_validation: null,
|
|
validations_performed: 0
|
|
};
|
|
}
|
|
|
|
sessionState.framework_components.CrossReferenceValidator.last_validation = new Date().toISOString();
|
|
sessionState.framework_components.CrossReferenceValidator.validations_performed += 1;
|
|
sessionState.framework_components.CrossReferenceValidator.timestamp = new Date().toISOString();
|
|
|
|
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2));
|
|
} catch (error) {
|
|
this.log('warning', `Could not update session state: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main validation entry point
|
|
*/
|
|
validate(actionType, context) {
|
|
this.log('info', `Validating ${actionType} action`);
|
|
|
|
let result;
|
|
|
|
if (actionType === 'file-edit') {
|
|
result = this.validateFileEdit(context.filePath, context.oldString, context.newString);
|
|
} else if (actionType === 'file-write') {
|
|
result = this.validateFileEdit(context.filePath, '', context.content);
|
|
} else if (actionType === 'bash') {
|
|
result = this.validateBashCommand(context.command);
|
|
} else {
|
|
result = { passed: true, relevantInstructions: [] };
|
|
}
|
|
|
|
this.updateSessionState();
|
|
|
|
if (!result.passed) {
|
|
this.log('error', `Validation failed: ${result.conflicts.length} conflicts found`);
|
|
result.conflicts.forEach(c => {
|
|
this.log('error', ` ${c.instruction}: ${c.issue}`);
|
|
});
|
|
} else if (result.relevantInstructions.length > 0) {
|
|
this.log('success', `Validation passed (${result.relevantInstructions.length} relevant instructions checked)`);
|
|
} else {
|
|
this.log('success', 'Validation passed (no relevant instructions)');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Export for use as module
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = CrossReferenceValidator;
|
|
}
|
|
|
|
// CLI mode - allow direct invocation
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2);
|
|
const actionType = args[0];
|
|
const contextJSON = args[1];
|
|
|
|
if (!actionType || !contextJSON) {
|
|
console.error('Usage: node CrossReferenceValidator.js <action-type> <context-json>');
|
|
console.error('Example: node CrossReferenceValidator.js bash \'{"command":"rsync ..."}\'');
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const context = JSON.parse(contextJSON);
|
|
const validator = new CrossReferenceValidator();
|
|
const result = validator.validate(actionType, context);
|
|
|
|
if (result.passed) {
|
|
console.log('✓ Validation passed');
|
|
process.exit(0);
|
|
} else {
|
|
console.log('✗ Validation failed');
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error: ${error.message}`);
|
|
process.exit(2);
|
|
}
|
|
}
|