Task #163: backfill script for empty task descriptions (6/15 matched)
This commit is contained in:
153
scripts/backfill-task-descriptions.js
Normal file
153
scripts/backfill-task-descriptions.js
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user