feat(FN-671): preserve quick entry disclosure and normalize comment migration
- Keep QuickEntryBox disclosure state persisted and covered by updated UI tests - Add normalized legacy comment migration logic to unify steering comments without duplication - Update database migration tests to verify schema v5 comment handling behavior - Carry merged dashboard, engine, and routing changes into the squash commit
This commit is contained in:
@@ -466,6 +466,47 @@ describe("migrateFromLegacy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("comment migration", () => {
|
||||
it("deduplicates overlapping steeringComments and comments during legacy import", async () => {
|
||||
const tasksDir = join(kbDir, "tasks");
|
||||
const taskDir = join(tasksDir, "FN-002");
|
||||
await mkdir(taskDir, { recursive: true });
|
||||
|
||||
await writeFile(
|
||||
join(taskDir, "task.json"),
|
||||
JSON.stringify({
|
||||
id: "FN-002",
|
||||
description: "Comment overlap",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
steeringComments: [
|
||||
{ id: "c1", text: "Use TypeScript", createdAt: "2025-01-01T00:00:00.000Z", author: "user" },
|
||||
],
|
||||
comments: [
|
||||
{ id: "c1", text: "Use TypeScript", createdAt: "2025-01-01T00:00:00.000Z", author: "user", updatedAt: "2025-01-02T00:00:00.000Z" },
|
||||
{ id: "c2", text: "General note", createdAt: "2025-01-03T00:00:00.000Z", author: "alice" },
|
||||
],
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
await migrateFromLegacy(kbDir, db);
|
||||
|
||||
const row = db.prepare("SELECT steeringComments, comments FROM tasks WHERE id = 'FN-002'").get() as any;
|
||||
expect(JSON.parse(row.steeringComments)).toEqual([
|
||||
{ id: "c1", text: "Use TypeScript", createdAt: "2025-01-01T00:00:00.000Z", author: "user" },
|
||||
]);
|
||||
expect(JSON.parse(row.comments)).toEqual([
|
||||
{ id: "c1", text: "Use TypeScript", createdAt: "2025-01-01T00:00:00.000Z", author: "user", updatedAt: "2025-01-02T00:00:00.000Z" },
|
||||
{ id: "c2", text: "General note", createdAt: "2025-01-03T00:00:00.000Z", author: "alice" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("data integrity", () => {
|
||||
it("preserves all task fields through migration", async () => {
|
||||
const tasksDir = join(kbDir, "tasks");
|
||||
@@ -541,9 +582,10 @@ describe("migrateFromLegacy", () => {
|
||||
expect(JSON.parse(row.steps)).toHaveLength(2);
|
||||
expect(JSON.parse(row.log)).toHaveLength(1);
|
||||
expect(JSON.parse(row.attachments)).toHaveLength(1);
|
||||
// steeringComments are merged into comments during migration
|
||||
expect(JSON.parse(row.steeringComments)).toEqual([]); // Now empty - merged into comments
|
||||
expect(JSON.parse(row.comments)).toHaveLength(1); // Migrated from steeringComments
|
||||
expect(JSON.parse(row.steeringComments)).toHaveLength(1);
|
||||
expect(JSON.parse(row.comments)).toEqual([
|
||||
{ id: "c1", text: "Fix this", createdAt: "2025-01-01", author: "user" },
|
||||
]);
|
||||
expect(JSON.parse(row.workflowStepResults)).toHaveLength(1);
|
||||
expect(JSON.parse(row.prInfo).number).toBe(1);
|
||||
expect(JSON.parse(row.issueInfo).number).toBe(10);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { readFile, readdir, rename, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Database } from "./db.js";
|
||||
import { toJson, toJsonNullable } from "./db.js";
|
||||
import { toJson, toJsonNullable, normalizeTaskComments } from "./db.js";
|
||||
import type { Task, BoardConfig, ActivityLogEntry, ArchivedTaskEntry } from "./types.js";
|
||||
import type { ScheduledTask } from "./automation.js";
|
||||
|
||||
@@ -184,20 +184,10 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
const raw = await readFile(taskJsonPath, "utf-8");
|
||||
const task: Task = JSON.parse(raw);
|
||||
|
||||
const steeringComments = Array.isArray((task as any).steeringComments)
|
||||
? (task as any).steeringComments
|
||||
: [];
|
||||
const comments = Array.isArray(task.comments) ? task.comments : [];
|
||||
const mergedComments = [
|
||||
...steeringComments.map((comment: any) => ({
|
||||
id: comment.id,
|
||||
text: comment.text,
|
||||
author: comment.author,
|
||||
createdAt: comment.createdAt,
|
||||
updatedAt: comment.updatedAt ?? comment.createdAt,
|
||||
})),
|
||||
...comments,
|
||||
];
|
||||
const normalizedComments = normalizeTaskComments(
|
||||
task.steeringComments,
|
||||
task.comments,
|
||||
);
|
||||
|
||||
insertStmt.run(
|
||||
task.id,
|
||||
@@ -229,8 +219,8 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
toJson(task.steps || []),
|
||||
toJson(task.log || []),
|
||||
toJson(task.attachments || []),
|
||||
"[]",
|
||||
toJson(mergedComments),
|
||||
toJson(normalizedComments.steeringComments),
|
||||
toJson(normalizedComments.comments),
|
||||
toJson(task.workflowStepResults || []),
|
||||
toJsonNullable(task.prInfo),
|
||||
toJsonNullable(task.issueInfo),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { Database, createDatabase, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||
import { Database, createDatabase, toJson, toJsonNullable, fromJson, normalizeTaskComments } from "./db.js";
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "./types.js";
|
||||
import { mkdtempSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -491,6 +491,26 @@ describe("Database", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("comment normalization", () => {
|
||||
it("merges overlapping legacy and unified comments exactly once", () => {
|
||||
const normalized = normalizeTaskComments(
|
||||
[{ id: "c1", text: "Legacy note", author: "user", createdAt: "2025-01-01T00:00:00.000Z" }],
|
||||
[{ id: "c1", text: "Legacy note", author: "user", createdAt: "2025-01-01T00:00:00.000Z", updatedAt: "2025-01-02T00:00:00.000Z" }],
|
||||
);
|
||||
|
||||
expect(normalized.comments).toEqual([
|
||||
{
|
||||
id: "c1",
|
||||
text: "Legacy note",
|
||||
author: "user",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-02T00:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
expect(normalized.steeringComments).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON helpers", () => {
|
||||
describe("toJson", () => {
|
||||
it("stringifies arrays", () => {
|
||||
@@ -683,7 +703,7 @@ describe("schema migrations", () => {
|
||||
// Now run init() which should trigger migration
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 4 (includes v1→v2, v2→v3, and v3→v4 migrations)
|
||||
// Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations)
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
@@ -807,7 +827,7 @@ describe("schema migrations", () => {
|
||||
// Now run init() which should trigger migrations v2→v3→v4
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 4
|
||||
// Verify version bumped to 5
|
||||
expect(db.getSchemaVersion()).toBe(5);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
@@ -836,6 +856,159 @@ describe("schema migrations", () => {
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("migrates pre-comments databases by copying steering comments into unified comments exactly once", () => {
|
||||
tmpDir = makeTmpDir();
|
||||
const kbDir = join(tmpDir, ".fusion");
|
||||
|
||||
const db = new Database(kbDir);
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
description TEXT NOT NULL,
|
||||
"column" TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
steeringComments TEXT DEFAULT '[]'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
nextId INTEGER DEFAULT 1,
|
||||
nextWorkflowStepId INTEGER DEFAULT 1,
|
||||
settings TEXT DEFAULT '{}',
|
||||
workflowSteps TEXT DEFAULT '[]',
|
||||
updatedAt TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS activityLog (
|
||||
id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, type TEXT NOT NULL,
|
||||
taskId TEXT, taskTitle TEXT, details TEXT NOT NULL, metadata TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS archivedTasks (id TEXT PRIMARY KEY, data TEXT NOT NULL, archivedAt TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS automations (
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
|
||||
scheduleType TEXT NOT NULL, cronExpression TEXT NOT NULL, command TEXT NOT NULL,
|
||||
enabled INTEGER DEFAULT 1, timeoutMs INTEGER, steps TEXT,
|
||||
nextRunAt TEXT, lastRunAt TEXT, lastRunResult TEXT,
|
||||
runCount INTEGER DEFAULT 0, runHistory TEXT DEFAULT '[]',
|
||||
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'idle', taskId TEXT,
|
||||
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
|
||||
lastHeartbeatAt TEXT, metadata TEXT DEFAULT '{}'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS agentHeartbeats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agentId TEXT NOT NULL, timestamp TEXT NOT NULL, status TEXT NOT NULL, runId TEXT NOT NULL,
|
||||
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '1')");
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
|
||||
db.prepare("INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt, steeringComments) VALUES (?, ?, ?, ?, ?, ?)")
|
||||
.run(
|
||||
"FN-100",
|
||||
"legacy comments",
|
||||
"todo",
|
||||
"2025-01-01T00:00:00.000Z",
|
||||
"2025-01-01T00:00:00.000Z",
|
||||
JSON.stringify([{ id: "legacy-1", text: "Use TypeScript", author: "user", createdAt: "2025-01-01T00:00:00.000Z" }]),
|
||||
);
|
||||
|
||||
db.init();
|
||||
|
||||
const row = db.prepare("SELECT steeringComments, comments FROM tasks WHERE id = 'FN-100'").get() as any;
|
||||
expect(JSON.parse(row.steeringComments)).toHaveLength(1);
|
||||
expect(JSON.parse(row.comments)).toEqual([
|
||||
{
|
||||
id: "legacy-1",
|
||||
text: "Use TypeScript",
|
||||
author: "user",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("deduplicates overlapping steeringComments and comments during schema upgrade", () => {
|
||||
tmpDir = makeTmpDir();
|
||||
const kbDir = join(tmpDir, ".fusion");
|
||||
|
||||
const db = new Database(kbDir);
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
description TEXT NOT NULL,
|
||||
"column" TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
steeringComments TEXT DEFAULT '[]',
|
||||
comments TEXT DEFAULT '[]',
|
||||
mergeDetails TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
nextId INTEGER DEFAULT 1,
|
||||
nextWorkflowStepId INTEGER DEFAULT 1,
|
||||
settings TEXT DEFAULT '{}',
|
||||
workflowSteps TEXT DEFAULT '[]',
|
||||
updatedAt TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS activityLog (
|
||||
id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, type TEXT NOT NULL,
|
||||
taskId TEXT, taskTitle TEXT, details TEXT NOT NULL, metadata TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS archivedTasks (id TEXT PRIMARY KEY, data TEXT NOT NULL, archivedAt TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS automations (
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT,
|
||||
scheduleType TEXT NOT NULL, cronExpression TEXT NOT NULL, command TEXT NOT NULL,
|
||||
enabled INTEGER DEFAULT 1, timeoutMs INTEGER, steps TEXT,
|
||||
nextRunAt TEXT, lastRunAt TEXT, lastRunResult TEXT,
|
||||
runCount INTEGER DEFAULT 0, runHistory TEXT DEFAULT '[]',
|
||||
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'idle', taskId TEXT,
|
||||
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
|
||||
lastHeartbeatAt TEXT, metadata TEXT DEFAULT '{}'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS agentHeartbeats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agentId TEXT NOT NULL, timestamp TEXT NOT NULL, status TEXT NOT NULL, runId TEXT NOT NULL,
|
||||
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '4')");
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
|
||||
db.prepare("INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt, steeringComments, comments) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
||||
.run(
|
||||
"FN-101",
|
||||
"mixed comments",
|
||||
"todo",
|
||||
"2025-01-01T00:00:00.000Z",
|
||||
"2025-01-01T00:00:00.000Z",
|
||||
JSON.stringify([{ id: "c1", text: "Keep it simple", author: "user", createdAt: "2025-01-01T00:00:00.000Z" }]),
|
||||
JSON.stringify([
|
||||
{ id: "c1", text: "Keep it simple", author: "user", createdAt: "2025-01-01T00:00:00.000Z", updatedAt: "2025-01-02T00:00:00.000Z" },
|
||||
{ id: "c2", text: "Already unified", author: "alice", createdAt: "2025-01-03T00:00:00.000Z" },
|
||||
]),
|
||||
);
|
||||
|
||||
db.init();
|
||||
|
||||
const row = db.prepare("SELECT comments FROM tasks WHERE id = 'FN-101'").get() as any;
|
||||
expect(JSON.parse(row.comments)).toEqual([
|
||||
{ id: "c1", text: "Keep it simple", author: "user", createdAt: "2025-01-01T00:00:00.000Z", updatedAt: "2025-01-02T00:00:00.000Z" },
|
||||
{ id: "c2", text: "Already unified", author: "alice", createdAt: "2025-01-03T00:00:00.000Z" },
|
||||
]);
|
||||
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDatabase factory", () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DatabaseSync } from "node:sqlite";
|
||||
import { join } from "node:path";
|
||||
import { mkdirSync, existsSync } from "node:fs";
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "./types.js";
|
||||
import type { TaskComment } from "./types.js";
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -60,6 +61,64 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
|
||||
|
||||
const SCHEMA_VERSION = 5;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: TaskComment[] | undefined,
|
||||
comments: TaskComment[] | undefined,
|
||||
): { steeringComments: TaskComment[]; comments: TaskComment[] } {
|
||||
const normalizedComments: TaskComment[] = [];
|
||||
const seenKeys = new Set<string>();
|
||||
|
||||
const pushComment = (comment: TaskComment) => {
|
||||
const key = comment.id || `${comment.text}\u0000${comment.author}\u0000${comment.createdAt}`;
|
||||
const existingIndex = normalizedComments.findIndex((entry) => {
|
||||
if (comment.id && entry.id) {
|
||||
return entry.id === comment.id;
|
||||
}
|
||||
return (
|
||||
entry.text === comment.text &&
|
||||
entry.author === comment.author &&
|
||||
entry.createdAt === comment.createdAt
|
||||
);
|
||||
});
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existing = normalizedComments[existingIndex];
|
||||
normalizedComments[existingIndex] = {
|
||||
...existing,
|
||||
...comment,
|
||||
updatedAt: comment.updatedAt ?? existing.updatedAt,
|
||||
};
|
||||
seenKeys.add(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!seenKeys.has(key)) {
|
||||
normalizedComments.push(comment);
|
||||
seenKeys.add(key);
|
||||
}
|
||||
};
|
||||
|
||||
for (const comment of comments || []) {
|
||||
if (!comment || !comment.id || !comment.createdAt) continue;
|
||||
pushComment(comment);
|
||||
}
|
||||
|
||||
for (const comment of steeringComments || []) {
|
||||
if (!comment || !comment.id || !comment.createdAt) continue;
|
||||
pushComment({
|
||||
id: comment.id,
|
||||
text: comment.text,
|
||||
author: comment.author,
|
||||
createdAt: comment.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
steeringComments: steeringComments || [],
|
||||
comments: normalizedComments,
|
||||
};
|
||||
}
|
||||
|
||||
const SCHEMA_SQL = `
|
||||
-- Tasks table with JSON columns for nested data
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
@@ -336,8 +395,7 @@ export class Database {
|
||||
|
||||
if (version < 5) {
|
||||
this.applyMigration(5, () => {
|
||||
// Migrate steeringComments to comments (unified comments field)
|
||||
this.migrateSteeringCommentsToComments();
|
||||
this.migrateLegacyCommentsToUnifiedComments();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,55 +435,34 @@ export class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate steeringComments data to the unified comments field.
|
||||
* This is a one-way migration from schema version 4 to 5.
|
||||
* Normalize legacy steering comments into the unified comments field exactly once.
|
||||
*
|
||||
* This migration is idempotent: rows already normalized remain unchanged on rerun.
|
||||
* The legacy steeringComments column is preserved for backward compatibility, but
|
||||
* migrated comments are represented canonically in the comments column.
|
||||
*/
|
||||
private migrateSteeringCommentsToComments(): void {
|
||||
// Only run if steeringComments column exists
|
||||
if (!this.hasColumn("tasks", "steeringComments")) {
|
||||
private migrateLegacyCommentsToUnifiedComments(): void {
|
||||
if (!this.hasColumn("tasks", "comments") || !this.hasColumn("tasks", "steeringComments")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all tasks that have steering comments
|
||||
const tasksWithSteering = this.db
|
||||
.prepare("SELECT id, steeringComments, comments FROM tasks WHERE steeringComments != '[]'")
|
||||
.all() as Array<{ id: string; steeringComments: string; comments: string }>;
|
||||
const rows = this.db.prepare("SELECT id, steeringComments, comments FROM tasks").all() as Array<{
|
||||
id: string;
|
||||
steeringComments: string | null;
|
||||
comments: string | null;
|
||||
}>;
|
||||
|
||||
for (const task of tasksWithSteering) {
|
||||
try {
|
||||
const steeringComments = JSON.parse(task.steeringComments) as Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
author: "user" | "agent";
|
||||
}>;
|
||||
const existingComments = JSON.parse(task.comments || "[]") as Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
author: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
const updateStmt = this.db.prepare(
|
||||
"UPDATE tasks SET comments = ? WHERE id = ?",
|
||||
);
|
||||
|
||||
// Convert steering comments to the unified format
|
||||
const migratedComments = steeringComments.map((sc) => ({
|
||||
id: sc.id,
|
||||
text: sc.text,
|
||||
author: sc.author,
|
||||
createdAt: sc.createdAt,
|
||||
updatedAt: sc.createdAt, // Steering comments didn't have updatedAt
|
||||
}));
|
||||
|
||||
// Merge: existing comments first, then migrated steering comments
|
||||
const mergedComments = [...existingComments, ...migratedComments];
|
||||
|
||||
// Update the task with merged comments
|
||||
this.db
|
||||
.prepare("UPDATE tasks SET comments = ? WHERE id = ?")
|
||||
.run(JSON.stringify(mergedComments), task.id);
|
||||
} catch {
|
||||
// Skip tasks with invalid JSON in steeringComments
|
||||
continue;
|
||||
for (const row of rows) {
|
||||
const steeringComments = fromJson<TaskComment[]>(row.steeringComments) || [];
|
||||
const comments = fromJson<TaskComment[]>(row.comments) || [];
|
||||
const normalized = normalizeTaskComments(steeringComments, comments);
|
||||
const nextCommentsJson = toJson(normalized.comments);
|
||||
if ((row.comments || "[]") !== nextCommentsJson) {
|
||||
updateStmt.run(nextCommentsJson, row.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,3 +582,5 @@ export class Database {
|
||||
export function createDatabase(kbDir: string): Database {
|
||||
return new Database(kbDir);
|
||||
}
|
||||
|
||||
export { normalizeTaskComments };
|
||||
|
||||
Reference in New Issue
Block a user