chore(seed): one-time roadmap seed from content/roadmap.md
Parses the H2 sections (In progress / Next / Later) into roadmap_items rows. Maps In-progress → beta (actively built, tested with pilots) and Next/Later → exploring with a target hint. Idempotent: skips entirely if the table is already populated, so admin edits are never overwritten. content/roadmap.md stays in the repo as the seed source. Once admin starts editing via /admin, the DB is the source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1735487ab9
commit
fba369a36d
2 changed files with 86 additions and 1 deletions
|
|
@ -12,7 +12,8 @@
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"db:migrate": "node scripts/migrate.js",
|
"db:migrate": "node scripts/migrate.js",
|
||||||
"db:seed": "node scripts/seed.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": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^8.3.0",
|
"@astrojs/node": "^8.3.0",
|
||||||
|
|
|
||||||
84
scripts/seed-roadmap.js
Normal file
84
scripts/seed-roadmap.js
Normal file
|
|
@ -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();
|
||||||
Loading…
Add table
Reference in a new issue