- 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>
169 lines
6.4 KiB
JavaScript
169 lines
6.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Analyze ProhibitedTermsScanner violations
|
|
* Filter to only show violations that need user review
|
|
*/
|
|
|
|
const { execSync } = require('child_process');
|
|
|
|
// Run scanner and capture output (ignore exit code - scanner exits with 1 if violations found)
|
|
let output;
|
|
try {
|
|
output = execSync('node scripts/framework-components/ProhibitedTermsScanner.js --details', {
|
|
encoding: 'utf8'
|
|
});
|
|
} catch (err) {
|
|
// Scanner exits with code 1 when violations found (for pre-commit hooks)
|
|
output = err.stdout;
|
|
}
|
|
|
|
// Parse violations
|
|
const violations = [];
|
|
const lines = output.split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
if (line.match(/^ \/home\/theflow\/projects\/tractatus\//)) {
|
|
const filePath = line.trim().split(':')[0];
|
|
const lineNum = line.trim().split(':')[1];
|
|
|
|
// Get next 4 lines for context
|
|
const rule = lines[i + 1]?.match(/Rule: (inst_\d+)/)?.[1];
|
|
const found = lines[i + 2]?.match(/Found: "(.+)"/)?.[1];
|
|
const context = lines[i + 3]?.match(/Context: (.+)/)?.[1];
|
|
const suggestion = lines[i + 4]?.match(/Suggestion: (.+)/)?.[1];
|
|
|
|
if (rule && found) {
|
|
violations.push({
|
|
file: filePath.replace('/home/theflow/projects/tractatus/', ''),
|
|
line: lineNum,
|
|
rule,
|
|
found,
|
|
context: context || '',
|
|
suggestion: suggestion || ''
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter to only relevant violations
|
|
const relevantViolations = violations.filter(v => {
|
|
const file = v.file;
|
|
|
|
// Public-facing UI
|
|
if (file.startsWith('public/')) return true;
|
|
|
|
// GitHub repository files
|
|
if (file === 'README.md') return true;
|
|
if (file === 'CLAUDE.md') return true;
|
|
if (file.startsWith('governance/')) return true;
|
|
if (file.startsWith('docs/markdown/')) return true; // Published docs
|
|
|
|
return false;
|
|
});
|
|
|
|
// Categorize by type and file
|
|
const byFile = {};
|
|
relevantViolations.forEach(v => {
|
|
if (!byFile[v.file]) {
|
|
byFile[v.file] = {
|
|
inst_017: [],
|
|
inst_018: [],
|
|
inst_016: []
|
|
};
|
|
}
|
|
byFile[v.file][v.rule].push(v);
|
|
});
|
|
|
|
// Print summary
|
|
console.log('\n═══════════════════════════════════════════════════════════');
|
|
console.log(' VIOLATIONS REQUIRING REVIEW');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
console.log(`Total violations in codebase: ${violations.length}`);
|
|
console.log(`Violations requiring review: ${relevantViolations.length}`);
|
|
console.log(`Files to review: ${Object.keys(byFile).length}\n`);
|
|
|
|
console.log('───────────────────────────────────────────────────────────');
|
|
console.log('CATEGORIZED BY FILE AND VIOLATION TYPE');
|
|
console.log('───────────────────────────────────────────────────────────\n');
|
|
|
|
const fileCategories = {
|
|
'Public UI Files': [],
|
|
'GitHub Repo Files': [],
|
|
'Published Documentation': []
|
|
};
|
|
|
|
Object.keys(byFile).forEach(file => {
|
|
if (file.startsWith('public/')) {
|
|
fileCategories['Public UI Files'].push(file);
|
|
} else if (file === 'README.md' || file === 'CLAUDE.md' || file.startsWith('governance/')) {
|
|
fileCategories['GitHub Repo Files'].push(file);
|
|
} else {
|
|
fileCategories['Published Documentation'].push(file);
|
|
}
|
|
});
|
|
|
|
Object.entries(fileCategories).forEach(([category, files]) => {
|
|
if (files.length === 0) return;
|
|
|
|
console.log(`\n📁 ${category.toUpperCase()}\n`);
|
|
|
|
files.forEach(file => {
|
|
const fileCounts = byFile[file];
|
|
const total = fileCounts.inst_017.length + fileCounts.inst_018.length + fileCounts.inst_016.length;
|
|
|
|
console.log(` ${file} (${total} violations)`);
|
|
|
|
if (fileCounts.inst_017.length > 0) {
|
|
console.log(` inst_017 (Absolute Assurance): ${fileCounts.inst_017.length}`);
|
|
fileCounts.inst_017.slice(0, 3).forEach(v => {
|
|
console.log(` Line ${v.line}: "${v.found}" → ${v.suggestion}`);
|
|
});
|
|
if (fileCounts.inst_017.length > 3) {
|
|
console.log(` ... and ${fileCounts.inst_017.length - 3} more`);
|
|
}
|
|
}
|
|
|
|
if (fileCounts.inst_018.length > 0) {
|
|
console.log(` inst_018 (Unverified Claims): ${fileCounts.inst_018.length}`);
|
|
fileCounts.inst_018.slice(0, 3).forEach(v => {
|
|
console.log(` Line ${v.line}: "${v.found}" → ${v.suggestion}`);
|
|
});
|
|
if (fileCounts.inst_018.length > 3) {
|
|
console.log(` ... and ${fileCounts.inst_018.length - 3} more`);
|
|
}
|
|
}
|
|
|
|
if (fileCounts.inst_016.length > 0) {
|
|
console.log(` inst_016 (Fabricated Statistics): ${fileCounts.inst_016.length}`);
|
|
fileCounts.inst_016.slice(0, 3).forEach(v => {
|
|
console.log(` Line ${v.line}: "${v.found}" → ${v.suggestion}`);
|
|
});
|
|
if (fileCounts.inst_016.length > 3) {
|
|
console.log(` ... and ${fileCounts.inst_016.length - 3} more`);
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
});
|
|
});
|
|
|
|
console.log('═══════════════════════════════════════════════════════════');
|
|
console.log('SUMMARY BY VIOLATION TYPE');
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|
|
|
|
const totals = {
|
|
inst_017: relevantViolations.filter(v => v.rule === 'inst_017').length,
|
|
inst_018: relevantViolations.filter(v => v.rule === 'inst_018').length,
|
|
inst_016: relevantViolations.filter(v => v.rule === 'inst_016').length
|
|
};
|
|
|
|
console.log(`inst_017 (Absolute Assurance - "guarantee", "never fails"): ${totals.inst_017}`);
|
|
console.log(`inst_018 (Unverified Claims - "production-ready", "battle-tested"): ${totals.inst_018}`);
|
|
console.log(`inst_016 (Fabricated Statistics - percentages without sources): ${totals.inst_016}`);
|
|
console.log(`\nTotal: ${totals.inst_017 + totals.inst_018 + totals.inst_016}\n`);
|
|
|
|
console.log('═══════════════════════════════════════════════════════════\n');
|