chore: add scripts/reimport-tasks.mjs recovery utility
One-off helper to re-import specific FN-* tasks from .fusion/tasks/<id>/task.json back into the live SQLite DB after a restore. Mirrors the column list used by db-migrate.ts:migrateTasks but uses INSERT OR IGNORE so existing rows are never overwritten. Used to rebuild FN-5415..FN-5425 after the FN-5407 backup corruption recovery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
140
scripts/reimport-tasks.mjs
Normal file
140
scripts/reimport-tasks.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
// One-off: re-import specific FN-* tasks from .fusion/tasks/<id>/task.json into
|
||||
// the live SQLite DB. Uses INSERT OR IGNORE — never overwrites existing rows.
|
||||
// Mirrors the column list used by packages/core/src/db-migrate.ts:migrateTasks.
|
||||
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const FUSION_DIR = join(process.cwd(), ".fusion");
|
||||
const DB_PATH = join(FUSION_DIR, "fusion.db");
|
||||
const TASKS_DIR = join(FUSION_DIR, "tasks");
|
||||
|
||||
const TARGET_IDS = process.argv.slice(2);
|
||||
if (TARGET_IDS.length === 0) {
|
||||
console.error("usage: node scripts/reimport-tasks.mjs FN-5414 FN-5415 ...");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const toJson = (v) => JSON.stringify(v ?? []);
|
||||
const toJsonNullable = (v) => (v == null ? null : JSON.stringify(v));
|
||||
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
db.exec("PRAGMA busy_timeout = 5000");
|
||||
db.exec("PRAGMA foreign_keys = ON");
|
||||
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT OR IGNORE INTO tasks (
|
||||
id, title, description, priority, "column", status, size, reviewLevel, currentStep,
|
||||
worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId,
|
||||
modelProvider, modelId, validatorModelProvider, validatorModelId,
|
||||
mergeRetries, recoveryRetryCount, nextRecoveryAt,
|
||||
error, summary, thinkingLevel, createdAt, updatedAt,
|
||||
columnMovedAt, dependencies, steps, log, attachments, steeringComments,
|
||||
comments, workflowStepResults, prInfo, issueInfo,
|
||||
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
|
||||
mergeDetails, breakIntoSubtasks, noCommitsExpected, enabledWorkflowSteps, modifiedFiles, sliceId
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`);
|
||||
|
||||
const existsStmt = db.prepare(`SELECT 1 AS hit FROM tasks WHERE id = ?`);
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let missing = 0;
|
||||
|
||||
for (const id of TARGET_IDS) {
|
||||
const path = join(TASKS_DIR, id, "task.json");
|
||||
if (!existsSync(path)) {
|
||||
console.log(`[skip] ${id}: no task.json on disk`);
|
||||
missing++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existsStmt.get(id)) {
|
||||
console.log(`[skip] ${id}: already exists in DB`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let task;
|
||||
try {
|
||||
task = JSON.parse(readFileSync(path, "utf-8"));
|
||||
} catch (err) {
|
||||
console.warn(`[fail] ${id}: invalid JSON — ${err.message}`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const priority =
|
||||
task.priority === "critical" || task.priority === "high" || task.priority === "low"
|
||||
? task.priority
|
||||
: "normal";
|
||||
|
||||
// Migration normalizes steeringComments + comments together; for fresh
|
||||
// imports it's safe to just trust the persisted JSON.
|
||||
const steeringComments = task.steeringComments ?? [];
|
||||
const comments = task.comments ?? [];
|
||||
|
||||
insertStmt.run(
|
||||
task.id,
|
||||
task.title ?? null,
|
||||
task.description ?? "",
|
||||
priority,
|
||||
task.column,
|
||||
task.status ?? null,
|
||||
task.size ?? null,
|
||||
task.reviewLevel ?? null,
|
||||
task.currentStep || 0,
|
||||
task.worktree ?? null,
|
||||
task.blockedBy ?? null,
|
||||
task.paused ? 1 : 0,
|
||||
task.baseBranch ?? null,
|
||||
task.branch ?? null,
|
||||
task.executionStartBranch ?? null,
|
||||
task.baseCommitSha ?? null,
|
||||
task.modelPresetId ?? null,
|
||||
task.modelProvider ?? null,
|
||||
task.modelId ?? null,
|
||||
task.validatorModelProvider ?? null,
|
||||
task.validatorModelId ?? null,
|
||||
task.mergeRetries ?? null,
|
||||
task.recoveryRetryCount ?? null,
|
||||
task.nextRecoveryAt ?? null,
|
||||
task.error ?? null,
|
||||
task.summary ?? null,
|
||||
task.thinkingLevel ?? null,
|
||||
task.createdAt,
|
||||
task.updatedAt,
|
||||
task.columnMovedAt ?? null,
|
||||
toJson(task.dependencies),
|
||||
toJson(task.steps),
|
||||
toJson(task.log),
|
||||
toJson(task.attachments),
|
||||
toJson(steeringComments),
|
||||
toJson(comments),
|
||||
toJson(task.workflowStepResults),
|
||||
toJsonNullable(task.prInfo),
|
||||
toJsonNullable(task.issueInfo),
|
||||
task.sourceIssue?.provider ?? null,
|
||||
task.sourceIssue?.repository ?? null,
|
||||
task.sourceIssue?.externalIssueId ?? null,
|
||||
task.sourceIssue?.issueNumber ?? null,
|
||||
task.sourceIssue?.url ?? null,
|
||||
toJsonNullable(task.mergeDetails),
|
||||
task.breakIntoSubtasks ? 1 : 0,
|
||||
task.noCommitsExpected ? 1 : 0,
|
||||
toJson(task.enabledWorkflowSteps),
|
||||
toJson(task.modifiedFiles),
|
||||
task.sliceId ?? null,
|
||||
);
|
||||
|
||||
console.log(`[ok] ${id}: imported (col=${task.column} status=${task.status ?? "-"})`);
|
||||
imported++;
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log(`\ndone: imported=${imported} skipped=${skipped} missing=${missing}`);
|
||||
Reference in New Issue
Block a user