feat(FN-1690): add roadmap schema migration v32 with covering indexes

This commit is contained in:
gsxdsm
2026-04-14 09:36:39 -07:00
parent 4f23883bac
commit b5f14d442b
5 changed files with 81 additions and 14 deletions

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
const index = db
.prepare(

View File

@@ -75,6 +75,10 @@ describe("Database", () => {
expect(tableNames).toContain("agentRatings");
expect(tableNames).toContain("task_documents");
expect(tableNames).toContain("task_document_revisions");
// Roadmap tables
expect(tableNames).toContain("roadmaps");
expect(tableNames).toContain("roadmap_milestones");
expect(tableNames).toContain("roadmap_features");
});
it("creates all expected indexes", () => {
@@ -109,10 +113,13 @@ describe("Database", () => {
expect(indexNames).toContain("idxTaskDocumentsTaskId");
expect(indexNames).toContain("idxTaskDocumentRevisionsTaskKey");
expect(indexNames).toContain("idxTasksCreatedAt");
// Roadmap indexes
expect(indexNames).toContain("idxRoadmapMilestonesRoadmapOrder");
expect(indexNames).toContain("idxRoadmapFeaturesMilestoneOrder");
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
});
it("seeds lastModified", () => {
@@ -135,7 +142,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
});
it("does not overwrite existing config on re-init", () => {
@@ -742,7 +749,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -767,11 +774,11 @@ describe("schema migrations", () => {
const db = new Database(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
db.close();
});
@@ -787,7 +794,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -811,7 +818,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -915,7 +922,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1281,7 +1288,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -59,7 +59,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 31;
const SCHEMA_VERSION = 32;
function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined,
@@ -1176,6 +1176,66 @@ export class Database {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxFixLineageRunId ON mission_fix_feature_lineage(runId)`);
});
}
// Roadmap persistence tables (FN-1690)
// Standalone roadmap: Roadmap → RoadmapMilestone → RoadmapFeature
// with deterministic ordering indexes and FK cascade integrity
if (version < 32) {
this.applyMigration(32, () => {
// Roadmaps table
this.db.exec(`
CREATE TABLE IF NOT EXISTS roadmaps (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
)
`);
// Roadmap milestones table
this.db.exec(`
CREATE TABLE IF NOT EXISTS roadmap_milestones (
id TEXT PRIMARY KEY,
roadmapId TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
orderIndex INTEGER NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (roadmapId) REFERENCES roadmaps(id) ON DELETE CASCADE
)
`);
// Roadmap features table
this.db.exec(`
CREATE TABLE IF NOT EXISTS roadmap_features (
id TEXT PRIMARY KEY,
milestoneId TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
orderIndex INTEGER NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (milestoneId) REFERENCES roadmap_milestones(id) ON DELETE CASCADE
)
`);
// Covering index for deterministic milestone ordering within a roadmap
// Covers: WHERE roadmapId = ? ORDER BY orderIndex ASC, createdAt ASC, id ASC
this.db.exec(`
CREATE INDEX IF NOT EXISTS idxRoadmapMilestonesRoadmapOrder
ON roadmap_milestones(roadmapId, orderIndex, createdAt, id)
`);
// Covering index for deterministic feature ordering within a milestone
// Covers: WHERE milestoneId = ? ORDER BY orderIndex ASC, createdAt ASC, id ASC
this.db.exec(`
CREATE INDEX IF NOT EXISTS idxRoadmapFeaturesMilestoneOrder
ON roadmap_features(milestoneId, orderIndex, createdAt, id)
`);
});
}
}
/**

View File

@@ -2457,8 +2457,8 @@ describe("MissionStore", () => {
// ── Loop State & Validator Run Schema Tests ───────────────────────────
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 31 after migration", () => {
expect(db.getSchemaVersion()).toBe(31);
it("schema version is 32 after migration", () => {
expect(db.getSchemaVersion()).toBe(32);
});
it("mission_features table has loop state columns", () => {

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 28", () => {
expect(db.getSchemaVersion()).toBe(31);
expect(db.getSchemaVersion()).toBe(32);
});
});
});