- 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>
332 lines
10 KiB
JavaScript
Executable file
332 lines
10 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Framework Health Metrics Dashboard
|
|
*
|
|
* Automated health report for Tractatus framework showing:
|
|
* - Component status and activity
|
|
* - Instruction database health
|
|
* - Token checkpoint status
|
|
* - Hook validator metrics
|
|
* - Recent incident trends
|
|
* - Overall health score
|
|
*
|
|
* Copyright 2025 Tractatus Project
|
|
* Licensed under Apache License 2.0
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Paths
|
|
const SESSION_STATE_PATH = path.join(__dirname, '../.claude/session-state.json');
|
|
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
|
const TOKEN_CHECKPOINTS_PATH = path.join(__dirname, '../.claude/token-checkpoints.json');
|
|
const METRICS_PATH = path.join(__dirname, '../.claude/metrics/hooks-metrics.json');
|
|
const INCIDENTS_DIR = path.join(__dirname, '../docs/framework-incidents');
|
|
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
bright: '\x1b[1m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
red: '\x1b[31m',
|
|
cyan: '\x1b[36m'
|
|
};
|
|
|
|
function log(message, color = 'reset') {
|
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
}
|
|
|
|
function header(message) {
|
|
console.log('');
|
|
log('═'.repeat(70), 'cyan');
|
|
log(` ${message}`, 'bright');
|
|
log('═'.repeat(70), 'cyan');
|
|
console.log('');
|
|
}
|
|
|
|
function section(message) {
|
|
console.log('');
|
|
log(`▶ ${message}`, 'blue');
|
|
}
|
|
|
|
function success(message) {
|
|
log(` ✓ ${message}`, 'green');
|
|
}
|
|
|
|
function warning(message) {
|
|
log(` ⚠ ${message}`, 'yellow');
|
|
}
|
|
|
|
function error(message) {
|
|
log(` ✗ ${message}`, 'red');
|
|
}
|
|
|
|
function loadJSON(filepath) {
|
|
try {
|
|
if (!fs.existsSync(filepath)) {
|
|
return null;
|
|
}
|
|
return JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function calculateHealthScore(metrics) {
|
|
let score = 100;
|
|
const issues = [];
|
|
|
|
// Component activity (20 points)
|
|
const activeComponents = metrics.components.filter(c => c.validations > 0).length;
|
|
const componentScore = (activeComponents / metrics.components.length) * 20;
|
|
score = Math.min(score, score - (20 - componentScore));
|
|
if (componentScore < 10) {
|
|
issues.push(`Low component activity: ${activeComponents}/${metrics.components.length} components used`);
|
|
}
|
|
|
|
// Instruction database health (20 points)
|
|
if (metrics.instructions.active > 60) {
|
|
score -= 10;
|
|
issues.push(`Too many active instructions: ${metrics.instructions.active} (target: <50)`);
|
|
}
|
|
if (metrics.instructions.highPersistence < 80) {
|
|
score -= 10;
|
|
issues.push(`Low HIGH persistence ratio: ${metrics.instructions.highPersistence.toFixed(1)}%`);
|
|
}
|
|
|
|
// Token checkpoint compliance (15 points)
|
|
const missedCheckpoints = metrics.checkpoints.total - metrics.checkpoints.completed;
|
|
if (metrics.checkpoints.overdue) {
|
|
score -= 15;
|
|
issues.push(`Token checkpoints overdue: ${missedCheckpoints} missed`);
|
|
}
|
|
|
|
// Hook validator effectiveness (15 points)
|
|
if (metrics.hooks.totalExecutions === 0) {
|
|
score -= 15;
|
|
issues.push('No hook validations recorded');
|
|
} else {
|
|
const blockRate = metrics.hooks.totalBlocks / metrics.hooks.totalExecutions;
|
|
if (blockRate > 0.2) {
|
|
score -= 5;
|
|
issues.push(`High hook block rate: ${(blockRate * 100).toFixed(1)}% (may indicate issues)`);
|
|
}
|
|
}
|
|
|
|
// Recent incidents (30 points)
|
|
const recentIncidents = metrics.incidents.recent30Days;
|
|
if (recentIncidents >= 5) {
|
|
score -= 20;
|
|
issues.push(`High incident rate: ${recentIncidents} in last 30 days`);
|
|
} else if (recentIncidents >= 3) {
|
|
score -= 10;
|
|
issues.push(`Moderate incident rate: ${recentIncidents} in last 30 days`);
|
|
}
|
|
|
|
return {
|
|
score: Math.max(0, score),
|
|
grade: score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F',
|
|
issues
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
header('Framework Health Metrics Dashboard');
|
|
|
|
const metrics = {
|
|
components: [],
|
|
instructions: {},
|
|
checkpoints: {},
|
|
hooks: {},
|
|
incidents: {}
|
|
};
|
|
|
|
// 1. Component Status
|
|
section('1. Framework Component Activity');
|
|
|
|
const sessionState = loadJSON(SESSION_STATE_PATH);
|
|
if (sessionState && sessionState.framework_components) {
|
|
const components = [
|
|
'ContextPressureMonitor',
|
|
'InstructionPersistenceClassifier',
|
|
'CrossReferenceValidator',
|
|
'BoundaryEnforcer',
|
|
'MetacognitiveVerifier',
|
|
'PluralisticDeliberationOrchestrator',
|
|
'BashCommandValidator'
|
|
];
|
|
|
|
components.forEach(name => {
|
|
const component = sessionState.framework_components[name];
|
|
if (component) {
|
|
const validations = component.validations_performed || 0;
|
|
const status = validations > 0 ? 'ACTIVE' : 'READY';
|
|
const color = validations > 0 ? 'green' : 'yellow';
|
|
|
|
log(` ${name}:`, 'cyan');
|
|
log(` Status: ${status} (${validations} validations)`, color);
|
|
|
|
metrics.components.push({ name, status, validations });
|
|
}
|
|
});
|
|
} else {
|
|
warning('Session state not found - run: node scripts/session-init.js');
|
|
}
|
|
|
|
// 2. Instruction Database Health
|
|
section('2. Instruction Database Health');
|
|
|
|
const instructionHistory = loadJSON(INSTRUCTION_HISTORY_PATH);
|
|
if (instructionHistory) {
|
|
const active = instructionHistory.instructions.filter(i => i.active);
|
|
const inactive = instructionHistory.instructions.filter(i => !i.active);
|
|
const highPersistence = active.filter(i => i.persistence === 'HIGH');
|
|
|
|
log(` Total instructions: ${instructionHistory.instructions.length}`, 'cyan');
|
|
log(` Active: ${active.length}`, active.length < 50 ? 'green' : 'yellow');
|
|
log(` Inactive: ${inactive.length}`, 'cyan');
|
|
log(` HIGH persistence: ${highPersistence.length} (${(highPersistence.length/active.length*100).toFixed(1)}%)`, 'cyan');
|
|
log(` Database version: ${instructionHistory.version}`, 'cyan');
|
|
|
|
if (active.length < 50) {
|
|
success('✓ Instruction count optimal (<50)');
|
|
} else {
|
|
warning(`⚠ Instruction count high: ${active.length} (target: <50)`);
|
|
}
|
|
|
|
metrics.instructions = {
|
|
total: instructionHistory.instructions.length,
|
|
active: active.length,
|
|
inactive: inactive.length,
|
|
highPersistence: (highPersistence.length / active.length) * 100
|
|
};
|
|
}
|
|
|
|
// 3. Token Checkpoint Status
|
|
section('3. Token Checkpoint Status');
|
|
|
|
const checkpoints = loadJSON(TOKEN_CHECKPOINTS_PATH);
|
|
if (checkpoints) {
|
|
const completed = checkpoints.checkpoints.filter(c => c.completed).length;
|
|
const total = checkpoints.checkpoints.length;
|
|
|
|
log(` Budget: ${checkpoints.budget.toLocaleString()} tokens`, 'cyan');
|
|
log(` Checkpoints completed: ${completed}/${total}`, completed === total ? 'green' : 'yellow');
|
|
log(` Next checkpoint: ${checkpoints.next_checkpoint ? checkpoints.next_checkpoint.toLocaleString() + ' tokens' : 'All complete'}`, 'cyan');
|
|
|
|
if (checkpoints.overdue) {
|
|
warning('⚠ Checkpoint overdue - run: node scripts/check-token-checkpoint.js');
|
|
} else {
|
|
success('✓ Checkpoint monitoring current');
|
|
}
|
|
|
|
metrics.checkpoints = {
|
|
total,
|
|
completed,
|
|
overdue: checkpoints.overdue
|
|
};
|
|
}
|
|
|
|
// 4. Hook Validator Metrics
|
|
section('4. Hook Validator Metrics');
|
|
|
|
const hooksMetrics = loadJSON(METRICS_PATH);
|
|
if (hooksMetrics && hooksMetrics.session_stats) {
|
|
const stats = hooksMetrics.session_stats;
|
|
|
|
log(` Total hook executions: ${stats.total_bash_hooks || 0}`, 'cyan');
|
|
log(` Blocks issued: ${stats.total_bash_blocks || 0}`, 'cyan');
|
|
|
|
const blockRate = stats.total_bash_hooks > 0
|
|
? ((stats.total_bash_blocks / stats.total_bash_hooks) * 100).toFixed(1)
|
|
: 0;
|
|
log(` Block rate: ${blockRate}%`, parseFloat(blockRate) < 10 ? 'green' : 'yellow');
|
|
|
|
success('✓ Hook validators operational');
|
|
|
|
metrics.hooks = {
|
|
totalExecutions: stats.total_bash_hooks || 0,
|
|
totalBlocks: stats.total_bash_blocks || 0
|
|
};
|
|
} else {
|
|
log(` No hook metrics available yet`, 'yellow');
|
|
metrics.hooks = { totalExecutions: 0, totalBlocks: 0 };
|
|
}
|
|
|
|
// 5. Recent Incidents
|
|
section('5. Recent Framework Incidents');
|
|
|
|
if (fs.existsSync(INCIDENTS_DIR)) {
|
|
const incidents = fs.readdirSync(INCIDENTS_DIR)
|
|
.filter(f => f.endsWith('.md'))
|
|
.map(f => {
|
|
const filepath = path.join(INCIDENTS_DIR, f);
|
|
const stats = fs.statSync(filepath);
|
|
return { file: f, date: stats.mtime };
|
|
})
|
|
.sort((a, b) => b.date - a.date);
|
|
|
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
const recent = incidents.filter(i => i.date > thirtyDaysAgo);
|
|
|
|
log(` Total incidents: ${incidents.length}`, 'cyan');
|
|
log(` Last 30 days: ${recent.length}`, recent.length === 0 ? 'green' : recent.length < 3 ? 'yellow' : 'red');
|
|
|
|
if (recent.length > 0) {
|
|
console.log('');
|
|
log(` Most recent:`, 'bold');
|
|
recent.slice(0, 3).forEach(i => {
|
|
const daysAgo = Math.floor((Date.now() - i.date.getTime()) / (1000 * 60 * 60 * 24));
|
|
log(` - ${i.file} (${daysAgo} days ago)`, 'cyan');
|
|
});
|
|
} else {
|
|
success('✓ No incidents in last 30 days');
|
|
}
|
|
|
|
metrics.incidents = {
|
|
total: incidents.length,
|
|
recent30Days: recent.length
|
|
};
|
|
} else {
|
|
metrics.incidents = { total: 0, recent30Days: 0 };
|
|
}
|
|
|
|
// 6. Overall Health Score
|
|
section('6. Overall Framework Health');
|
|
|
|
const health = calculateHealthScore(metrics);
|
|
|
|
console.log('');
|
|
const scoreColor = health.score >= 90 ? 'green' : health.score >= 70 ? 'yellow' : 'red';
|
|
log(` Health Score: ${health.score}/100 (Grade: ${health.grade})`, scoreColor);
|
|
|
|
if (health.issues.length === 0) {
|
|
console.log('');
|
|
success('✓ No issues detected - framework operating optimally');
|
|
} else {
|
|
console.log('');
|
|
log(` Issues Identified (${health.issues.length}):`, 'yellow');
|
|
health.issues.forEach(issue => {
|
|
warning(issue);
|
|
});
|
|
}
|
|
|
|
console.log('');
|
|
log('═'.repeat(70), 'cyan');
|
|
|
|
if (health.score >= 90) {
|
|
success('Framework Status: EXCELLENT');
|
|
} else if (health.score >= 70) {
|
|
warning('Framework Status: GOOD (minor improvements recommended)');
|
|
} else {
|
|
error('Framework Status: NEEDS ATTENTION');
|
|
}
|
|
|
|
log('═'.repeat(70), 'cyan');
|
|
console.log('');
|
|
}
|
|
|
|
main();
|