tractatus/scripts/plan-reminder.js
TheFlow 2af47035ac refactor: remove website code and fix critical startup crashes (Phase 8)
CRITICAL FIX: Server would CRASH ON STARTUP (multiple import errors)

REMOVED (2 scripts):
1. scripts/framework-watchdog.js
   - Monitored .claude/session-state.json (OUR Claude Code setup)
   - Monitored .claude/token-checkpoints.json (OUR file structure)
   - Implementers won't have our .claude/ directory

2. scripts/init-db.js
   - Created website collections: blog_posts, media_inquiries, case_submissions
   - Created website collections: resources, moderation_queue, users, citations
   - Created website collections: translations, koha_donations
   - Next steps referenced deleted scripts (npm run seed:admin)

REWRITTEN (2 files):

src/models/index.js (29 lines → 27 lines)
- REMOVED imports: Document, BlogPost, MediaInquiry, CaseSubmission, Resource
- REMOVED imports: ModerationQueue, User (all deleted in Phase 2)
- KEPT imports: AuditLog, DeliberationSession, GovernanceLog, GovernanceRule
- KEPT imports: Precedent, Project, SessionState, VariableValue, VerificationLog
- Result: Only framework models exported

src/server.js (284 lines → 163 lines, 43% reduction)
- REMOVED: Imports to deleted middleware (csrf-protection, response-sanitization)
- REMOVED: Stripe webhook handling (/api/koha/webhook)
- REMOVED: Static file caching (for deleted public/ directory)
- REMOVED: Static file serving (public/ deleted in Phase 6)
- REMOVED: CSRF token endpoint
- REMOVED: Website homepage with "auth, documents, blog, admin" references
- REMOVED: Instruction sync (scripts/sync-instructions-to-db.js reference)
- REMOVED: Hardcoded log path (${process.env.HOME}/var/log/tractatus/...)
- REMOVED: Website-specific security middleware
- KEPT: Security headers, rate limiting, CORS, body parsers
- KEPT: API routes, governance services, MongoDB connections
- RESULT: Clean framework-only server

RESULT: Repository can now start without crashes, all imports resolve

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 22:17:02 +13:00

457 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Plan Reminder System
* Scans for plan documents, tracks status, and reminds about reviews
*/
const fs = require('fs').promises;
const path = require('path');
const PLAN_REGISTRY = path.join(__dirname, '../.claude/plan-registry.json');
const PROJECT_ROOT = path.join(__dirname, '..');
// Only search within tractatus directory, exclude other projects
const SEARCH_DIRS = [
path.join(PROJECT_ROOT, 'docs/plans'),
path.join(PROJECT_ROOT, 'docs'),
path.join(PROJECT_ROOT, 'docs/research'),
path.join(PROJECT_ROOT, 'docs/planning'),
path.join(PROJECT_ROOT, 'docs/governance')
];
// Exclude these directories and projects
const EXCLUDE_PATTERNS = [
/node_modules/,
/\.git/,
/sydigital/,
/passport-consolidated/,
/family-history/,
/mysy/
];
// Plan document patterns
const PLAN_PATTERNS = [
/plan.*\.md$/i,
/roadmap.*\.md$/i,
/session.*handoff.*\.md$/i,
/priority.*\.md$/i,
/-plan\.md$/i
];
/**
* Parse plan metadata from markdown
*/
function parsePlanMetadata(content, filepath) {
const metadata = {
filepath: filepath,
filename: path.basename(filepath),
title: null,
status: null,
priority: null,
created: null,
due: null,
review_schedule: null,
next_review: null,
owner: null,
completeness: null,
last_modified: null
};
// Extract title (first H1)
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
metadata.title = titleMatch[1].trim();
}
// Extract metadata fields
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/i);
if (statusMatch) {
metadata.status = statusMatch[1].trim();
}
const priorityMatch = content.match(/\*\*Priority:\*\*\s*(.+)/i);
if (priorityMatch) {
metadata.priority = priorityMatch[1].trim();
}
const createdMatch = content.match(/\*\*(?:Plan Created|Created):\*\*\s*(.+)/i);
if (createdMatch) {
metadata.created = createdMatch[1].trim();
}
const dueMatch = content.match(/\*\*(?:Target Completion|Due):\*\*\s*(.+)/i);
if (dueMatch) {
metadata.due = dueMatch[1].trim();
}
const reviewMatch = content.match(/\*\*Review Schedule:\*\*\s*(.+)/i);
if (reviewMatch) {
metadata.review_schedule = reviewMatch[1].trim();
}
const nextReviewMatch = content.match(/\*\*Next (?:Review|review):\*\*\s*(.+)/i);
if (nextReviewMatch) {
metadata.next_review = nextReviewMatch[1].trim();
}
const ownerMatch = content.match(/\*\*(?:Plan )?Owner:\*\*\s*(.+)/i);
if (ownerMatch) {
metadata.owner = ownerMatch[1].trim();
}
// Analyze completeness based on checkboxes
const totalCheckboxes = (content.match(/\[[ x✓]\]/gi) || []).length;
const checkedBoxes = (content.match(/\[[x✓]\]/gi) || []).length;
if (totalCheckboxes > 0) {
metadata.completeness = {
total: totalCheckboxes,
completed: checkedBoxes,
percentage: Math.round((checkedBoxes / totalCheckboxes) * 100)
};
}
return metadata;
}
/**
* Scan directories for plan documents
*/
async function scanForPlans() {
const plans = [];
for (const dir of SEARCH_DIRS) {
try {
const items = await fs.readdir(dir, { recursive: true, withFileTypes: true });
for (const item of items) {
if (!item.isFile()) continue;
const filename = item.name;
const filepath = path.join(item.path || dir, filename);
// Skip if matches exclusion patterns
if (EXCLUDE_PATTERNS.some(pattern => pattern.test(filepath))) {
continue;
}
const isPlan = PLAN_PATTERNS.some(pattern => pattern.test(filename));
if (isPlan) {
try {
const content = await fs.readFile(filepath, 'utf-8');
const stats = await fs.stat(filepath);
const metadata = parsePlanMetadata(content, filepath);
metadata.last_modified = stats.mtime.toISOString();
metadata.file_size = stats.size;
plans.push(metadata);
} catch (err) {
console.error(` ✗ Error reading ${filepath}:`, err.message);
}
}
}
} catch (err) {
// Directory might not exist, skip
continue;
}
}
return plans;
}
/**
* Calculate review urgency
*/
function calculateUrgency(plan) {
if (!plan.next_review) return 'unknown';
try {
const nextReview = new Date(plan.next_review);
const now = new Date();
const daysUntil = Math.ceil((nextReview - now) / (1000 * 60 * 60 * 24));
if (daysUntil < 0) return 'overdue';
if (daysUntil === 0) return 'today';
if (daysUntil <= 3) return 'this-week';
if (daysUntil <= 14) return 'soon';
return 'scheduled';
} catch (err) {
return 'unknown';
}
}
/**
* Determine plan health
*/
function assessPlanHealth(plan) {
const issues = [];
const now = new Date();
// Check if status is active but stale (>30 days since last modified)
if (plan.status && plan.status.toLowerCase().includes('active')) {
const lastMod = new Date(plan.last_modified);
const daysSinceUpdate = (now - lastMod) / (1000 * 60 * 60 * 24);
if (daysSinceUpdate > 30) {
issues.push(`Stale: No updates in ${Math.round(daysSinceUpdate)} days`);
}
}
// Check if completion is low but due date approaching
if (plan.completeness && plan.due) {
try {
const dueDate = new Date(plan.due);
const daysUntilDue = (dueDate - now) / (1000 * 60 * 60 * 24);
if (daysUntilDue < 14 && plan.completeness.percentage < 50) {
issues.push(`At risk: ${plan.completeness.percentage}% complete, due in ${Math.round(daysUntilDue)} days`);
}
} catch (err) {
// Invalid date, skip
}
}
// Check if review is overdue
const urgency = calculateUrgency(plan);
if (urgency === 'overdue') {
issues.push('Review overdue');
}
// Check if no owner assigned
if (!plan.owner || plan.owner.includes('TBD') || plan.owner.includes('assigned')) {
issues.push('No owner assigned');
}
// Overall health assessment
if (issues.length === 0) return { status: 'healthy', issues: [] };
if (issues.length === 1) return { status: 'attention', issues };
return { status: 'critical', issues };
}
/**
* Update plan registry
*/
async function updateRegistry(plans) {
// Deduplicate plans by filepath
const uniquePlans = [];
const seenPaths = new Set();
for (const plan of plans) {
if (!seenPaths.has(plan.filepath)) {
seenPaths.add(plan.filepath);
uniquePlans.push(plan);
}
}
const registry = {
version: '1.0.0',
last_scan: new Date().toISOString(),
total_plans: uniquePlans.length,
plans: uniquePlans.map(plan => ({
...plan,
urgency: calculateUrgency(plan),
health: assessPlanHealth(plan)
}))
};
await fs.writeFile(PLAN_REGISTRY, JSON.stringify(registry, null, 2));
return registry;
}
/**
* Display plan reminders
*/
function displayReminders(registry) {
console.log('═══════════════════════════════════════════════════════════');
console.log(' Plan Reminder System');
console.log('═══════════════════════════════════════════════════════════\n');
console.log(`Last Scan: ${new Date(registry.last_scan).toLocaleString()}`);
console.log(`Total Plans: ${registry.total_plans}\n`);
// Group plans by urgency
const overdue = registry.plans.filter(p => p.urgency === 'overdue');
const today = registry.plans.filter(p => p.urgency === 'today');
const thisWeek = registry.plans.filter(p => p.urgency === 'this-week');
const critical = registry.plans.filter(p => p.health.status === 'critical');
const attention = registry.plans.filter(p => p.health.status === 'attention');
// Display overdue reviews
if (overdue.length > 0) {
console.log('🔴 OVERDUE REVIEWS:');
overdue.forEach(plan => {
console.log(`${plan.title || plan.filename}`);
console.log(` Status: ${plan.status || 'Unknown'}`);
console.log(` Next Review: ${plan.next_review}`);
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
if (plan.health.issues.length > 0) {
plan.health.issues.forEach(issue => console.log(`${issue}`));
}
console.log('');
});
}
// Display today's reviews
if (today.length > 0) {
console.log('🟡 REVIEWS DUE TODAY:');
today.forEach(plan => {
console.log(`${plan.title || plan.filename}`);
console.log(` Status: ${plan.status || 'Unknown'}`);
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
console.log('');
});
}
// Display this week's reviews
if (thisWeek.length > 0) {
console.log('🟢 REVIEWS THIS WEEK:');
thisWeek.forEach(plan => {
console.log(`${plan.title || plan.filename}`);
console.log(` Next Review: ${plan.next_review}`);
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
console.log('');
});
}
// Display critical health issues
if (critical.length > 0) {
console.log('🚨 PLANS NEEDING ATTENTION:');
critical.forEach(plan => {
console.log(`${plan.title || plan.filename}`);
console.log(` Status: ${plan.status || 'Unknown'}`);
plan.health.issues.forEach(issue => console.log(`${issue}`));
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
console.log('');
});
}
// Display plans needing attention (not critical)
if (attention.length > 0 && critical.length === 0) {
console.log(' PLANS WITH MINOR ISSUES:');
attention.forEach(plan => {
console.log(`${plan.title || plan.filename}`);
plan.health.issues.forEach(issue => console.log(`${issue}`));
console.log('');
});
}
// Summary
console.log('═══════════════════════════════════════════════════════════');
console.log('SUMMARY:');
console.log(` Overdue Reviews: ${overdue.length}`);
console.log(` Due Today: ${today.length}`);
console.log(` Due This Week: ${thisWeek.length}`);
console.log(` Critical Health: ${critical.length}`);
console.log(` Needs Attention: ${attention.length}`);
console.log(` Healthy: ${registry.plans.filter(p => p.health.status === 'healthy').length}`);
console.log('═══════════════════════════════════════════════════════════\n');
// Recommendations
if (overdue.length > 0 || critical.length > 0) {
console.log('📌 RECOMMENDED ACTIONS:');
if (overdue.length > 0) {
console.log(' 1. Review overdue plans and update next_review dates');
}
if (critical.length > 0) {
console.log(' 2. Address critical health issues (stale plans, missing owners)');
}
console.log(' 3. Update plan status and completeness checkboxes');
console.log(' 4. Run this reminder weekly to stay on track\n');
}
}
/**
* List all plans
*/
function listAllPlans(registry, options = {}) {
console.log('\n📋 ALL TRACKED PLANS:\n');
const sortedPlans = [...registry.plans];
// Sort by status priority
const statusPriority = {
'active': 1,
'in progress': 2,
'pending': 3,
'on hold': 4,
'completed': 5,
'cancelled': 6
};
sortedPlans.sort((a, b) => {
const aStatus = (a.status || '').toLowerCase();
const bStatus = (b.status || '').toLowerCase();
const aPriority = statusPriority[aStatus] || 99;
const bPriority = statusPriority[bStatus] || 99;
return aPriority - bPriority;
});
sortedPlans.forEach((plan, index) => {
console.log(`${index + 1}. ${plan.title || plan.filename}`);
console.log(` Status: ${plan.status || 'Unknown'} | Priority: ${plan.priority || 'Unknown'}`);
if (plan.completeness) {
console.log(` Completeness: ${plan.completeness.completed}/${plan.completeness.total} (${plan.completeness.percentage}%)`);
}
if (plan.next_review) {
console.log(` Next Review: ${plan.next_review} (${plan.urgency})`);
}
if (plan.owner) {
console.log(` Owner: ${plan.owner}`);
}
if (plan.health.status !== 'healthy') {
console.log(` Health: ${plan.health.status}`);
if (options.verbose) {
plan.health.issues.forEach(issue => console.log(`${issue}`));
}
}
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
console.log('');
});
}
/**
* Main execution
*/
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'remind';
try {
console.log('\nScanning for plan documents...');
const plans = await scanForPlans();
console.log(`✓ Found ${plans.length} plan documents\n`);
const registry = await updateRegistry(plans);
if (command === 'list') {
listAllPlans(registry, { verbose: args.includes('--verbose') });
} else {
displayReminders(registry);
}
// Exit with code based on urgency
if (registry.plans.some(p => p.urgency === 'overdue')) {
process.exit(1); // Overdue plans found
}
} catch (error) {
console.error('\n✗ Error:', error.message);
if (args.includes('--verbose')) {
console.error(error.stack);
}
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = { scanForPlans, updateRegistry, calculateUrgency, assessPlanHealth };