#!/usr/bin/env node /** * Task #163 — Task Description Hygiene Pass * * Reads archived task markdown files from the ops manual, strips YAML * frontmatter, and emits a SQL file with UPDATE statements to backfill * descriptions for tasks in the `tasks` table. * * Run from Nitro (local) — outputs SQL for Michael to apply on dev panel. * * Usage: * node scripts/backfill-task-descriptions.js * * Output: * scripts/out/backfill-task-descriptions.sql * scripts/out/backfill-task-descriptions-report.md */ const fs = require('fs'); const path = require('path'); const ARCHIVE_DIR = path.resolve( __dirname, '..', '..', 'firefrost-operations-manual', 'docs', 'archive', 'tasks-index-archived-2026-04-11' ); const OUT_DIR = path.resolve(__dirname, 'out'); // 16 target tasks with empty descriptions (from REQ-2026-04-14-task-description-hygiene) const TARGETS = [ { num: 22, title: 'Netdata Deployment' }, { num: 23, title: 'Department Structure & Access Control' }, { num: 32, title: 'Terraria Branding Arc' }, { num: 48, title: 'n8n Rebuild' }, { num: 49, title: 'NotebookLM Integration' }, { num: 51, title: 'Ignis Protocol' }, { num: 81, title: 'Memorial Writing Assistant' }, { num: 89, title: 'DERP Protocol Review' }, { num: 97, title: 'Trinity Console Social Hub' }, { num: 99, title: 'Multi-Lineage Claude Architecture' }, { num: 100, title: 'Skill Index & Recommender System' }, { num: 104, title: 'Server-Side Mod Deployment Automation' }, { num: 105, title: 'Trinity Console Review Workflow' }, { num: 106, title: 'Minecraft Log Analyzer Bot' }, { num: 113, title: 'Claude Projects Architecture' } ]; function stripFrontmatter(raw) { // YAML frontmatter is the first block between --- ... --- if (!raw.startsWith('---')) return raw.trim(); const end = raw.indexOf('\n---', 3); if (end < 0) return raw.trim(); return raw.slice(end + 4).replace(/^\s*\n/, '').trim(); } function sqlEscape(s) { // Postgres dollar-quoted string — safest for markdown bodies // Use a tag unlikely to appear in content return `$body$${s}$body$`; } function indexArchive() { if (!fs.existsSync(ARCHIVE_DIR)) { console.error(`Archive dir not found: ${ARCHIVE_DIR}`); process.exit(1); } const files = fs.readdirSync(ARCHIVE_DIR).filter(f => /^task-\d+-.+\.md$/.test(f)); const byNum = new Map(); for (const f of files) { const m = f.match(/^task-(\d+)-/); if (!m) continue; byNum.set(parseInt(m[1], 10), f); } return byNum; } function main() { fs.mkdirSync(OUT_DIR, { recursive: true }); const archiveIndex = indexArchive(); const matched = []; const missing = []; for (const t of TARGETS) { const file = archiveIndex.get(t.num); if (!file) { missing.push(t); continue; } const raw = fs.readFileSync(path.join(ARCHIVE_DIR, file), 'utf8'); const body = stripFrontmatter(raw); matched.push({ ...t, file, body }); } // Emit SQL const sqlLines = [ '-- Task #163 — Task Description Hygiene Pass', '-- Generated: ' + new Date().toISOString(), '-- Source: firefrost-operations-manual/docs/archive/tasks-index-archived-2026-04-11/', `-- Matched: ${matched.length}/${TARGETS.length} Missing: ${missing.length}`, '', 'BEGIN;', '' ]; for (const m of matched) { sqlLines.push(`-- Task #${m.num}: ${m.title} (from ${m.file})`); sqlLines.push( `UPDATE tasks SET description = ${sqlEscape(m.body)}, updated_at = NOW() ` + `WHERE task_number = ${m.num} AND (description IS NULL OR description = '');` ); sqlLines.push(''); } sqlLines.push('COMMIT;', ''); const sqlPath = path.join(OUT_DIR, 'backfill-task-descriptions.sql'); fs.writeFileSync(sqlPath, sqlLines.join('\n')); // Emit report const report = [ '# Task #163 — Backfill Report', '', `**Generated:** ${new Date().toISOString()}`, `**Matched:** ${matched.length}/${TARGETS.length}`, `**Missing:** ${missing.length}`, '', '## Matched (SQL UPDATE emitted)', '', '| # | Title | Source file |', '|---|-------|-------------|', ...matched.map(m => `| ${m.num} | ${m.title} | \`${m.file}\` |`), '', '## Missing (no archive file — needs manual description)', '', missing.length === 0 ? '_(none)_' : ['| # | Title |', '|---|-------|', ...missing.map(m => `| ${m.num} | ${m.title} |`)].join('\n'), '' ].join('\n'); const reportPath = path.join(OUT_DIR, 'backfill-task-descriptions-report.md'); fs.writeFileSync(reportPath, report); console.log(`Matched ${matched.length}/${TARGETS.length}. Missing ${missing.length}.`); console.log(`SQL: ${sqlPath}`); console.log(`Report: ${reportPath}`); } main();