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:
@@ -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