diff --git a/package.json b/package.json index ad2b273..0c4ab95 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test": "vitest run", "db:migrate": "node scripts/migrate.js", "db:seed": "node scripts/seed.js", - "db:setup": "node scripts/migrate.js && node scripts/seed.js" + "db:seed:roadmap": "node scripts/seed-roadmap.js", + "db:setup": "node scripts/migrate.js && node scripts/seed.js && node scripts/seed-roadmap.js" }, "dependencies": { "@astrojs/node": "^8.3.0", diff --git a/scripts/seed-roadmap.js b/scripts/seed-roadmap.js new file mode 100644 index 0000000..06abe6f --- /dev/null +++ b/scripts/seed-roadmap.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Parse content/roadmap.md → roadmap_items rows. Idempotent: only seeds if +// the table is empty. Once admin starts managing roadmap via /admin, the +// markdown is no longer the source of truth — but it stays in the repo as +// the initial-seed reference. + +import Database from 'better-sqlite3'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const dbPath = process.env.BIFROST_DB_PATH ?? join(__dirname, '..', 'bifrost.db'); +const mdPath = join(__dirname, '..', 'content', 'roadmap.md'); + +const db = new Database(dbPath); +db.pragma('foreign_keys = ON'); + +const existing = db.prepare('SELECT COUNT(*) AS n FROM roadmap_items').get().n; +if (existing > 0) { + console.log(` roadmap_items already populated (${existing} rows) — skipping seed.`); + db.close(); + process.exit(0); +} + +const md = readFileSync(mdPath, 'utf8'); + +// Section → status mapping. +// Markdown sections "In progress" / "Next" / "Later" map to the council-portal +// schema's three statuses. In-progress items are actively being built and +// tested with pilots → beta. Next/Later are roadmap intent, not started → exploring. +const SECTION_STATUS = { + 'In progress': { status: 'beta', target: null }, + 'Next': { status: 'exploring', target: 'Next quarter' }, + 'Later': { status: 'exploring', target: 'Later this year' }, +}; + +const items = []; +let currentSection = null; +let sectionDisplayOrder = 0; + +for (const rawLine of md.split('\n')) { + const line = rawLine.trim(); + const h2 = line.match(/^##\s+(.+)$/); + if (h2) { + currentSection = h2[1].trim(); + sectionDisplayOrder = 0; + continue; + } + if (!currentSection || !SECTION_STATUS[currentSection]) continue; + + // Format: **Title** — description text. Optional `pilot-only` flag at end. + const m = line.match(/^\*\*([^*]+)\*\*\s*[—-]\s*(.+)$/); + if (!m) continue; + + sectionDisplayOrder += 10; + const title = m[1].trim(); + let description = m[2].trim(); + // strip trailing ` `pilot-only` ` flag; admin can re-add via UI if wanted + description = description.replace(/`pilot-only`\s*$/, '').trim(); + + items.push({ + title, + description, + status: SECTION_STATUS[currentSection].status, + target: SECTION_STATUS[currentSection].target, + display_order: sectionDisplayOrder, + }); +} + +const insert = db.prepare(` + INSERT INTO roadmap_items (title, description, status, target, display_order) + VALUES (?,?,?,?,?) +`); + +const tx = db.transaction(() => { + for (const it of items) { + insert.run(it.title, it.description, it.status, it.target, it.display_order); + } +}); +tx(); + +console.log(` seeded ${items.length} roadmap items from content/roadmap.md`); +db.close();