154 lines
4.7 KiB
JavaScript
154 lines
4.7 KiB
JavaScript
#!/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();
|