refactor: remove legacy kb compatibility

Drops the .kb/kb.db migration path, legacy backup filename handling, and
backward-compat test suites. Renames internal kbDir identifiers to
fusionDir and hasKbProject/isValidKbProject to their fusion equivalents.

- Remove needsCentralMigration, autoMigrateToCentral, and the
  "needs-migration" FirstRunState; checkAndMigrate and KB_SKIP_MIGRATION
  env var are gone
- Remove LEGACY_BACKUP_DIR and canonicalizeBackupDir; listBackups no
  longer matches kb-* filenames
- Delete backward-compat.test.ts and store-backward-compat.test.ts;
  update remaining tests to new 3-state first-run model

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-23 16:55:46 -07:00
parent 32da0aedac
commit a269f4829f
36 changed files with 365 additions and 1194 deletions

View File

@@ -20,13 +20,6 @@ import { ensureMemoryFileWithBackend } from "./project-memory.js";
import { runCommandAsync } from "./run-command.js";
import { createLogger } from "./logger.js";
/**
* Legacy backup directory default value from the old .kb storage structure.
* Projects that were created before the .fusion rename may still have this
* value persisted in their config. It is canonicalized to the new default
* so that all backup operations use a consistent directory.
*/
const LEGACY_BACKUP_DIR = ".kb/backups";
const TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1_000;
const TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4_000;
const ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
@@ -96,24 +89,9 @@ function compactTaskActivityLog(entries: TaskLogEntry[]): TaskLogEntry[] {
}
/**
* Canonicalizes a settings object by resolving legacy defaults.
* Currently handles the .kb/backups → .fusion/backups migration and
* strips legacy fields that are no longer valid.
*
* This function applies only the exact-match legacy alias transformation.
* Other custom .kb/* paths are preserved as-is.
* Canonicalizes a settings object by stripping legacy fields that are no longer valid.
*/
function canonicalizeSettings(settings: Settings): Settings {
// Canonicalize the legacy backup directory default to the new location.
// Only the exact legacy default value is transformed — custom paths like
// ".kb/my-custom-backups" are preserved unchanged.
if ((settings as Partial<ProjectSettings>).autoBackupDir === LEGACY_BACKUP_DIR) {
return {
...settings,
autoBackupDir: ".fusion/backups",
};
}
// Strip legacy globalMaxConcurrent from project settings - this field was
// deprecated in favor of the global-level maxConcurrent in concurrency settings.
const { globalMaxConcurrent, ...rest } = settings as Settings & { globalMaxConcurrent?: number };
@@ -203,7 +181,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* optional blob files must tolerate missing files/directories because cleanup, migration,
* or manual filesystem changes can remove them independently of the database row.
*/
private kbDir: string;
private fusionDir: string;
private tasksDir: string;
private configPath: string;
/** SQLite database for structured data storage */
@@ -254,9 +232,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
constructor(private rootDir: string, globalSettingsDir?: string) {
super();
this.setMaxListeners(100);
this.kbDir = join(rootDir, ".fusion");
this.tasksDir = join(this.kbDir, "tasks");
this.configPath = join(this.kbDir, "config.json");
this.fusionDir = join(rootDir, ".fusion");
this.tasksDir = join(this.fusionDir, "tasks");
this.configPath = join(this.fusionDir, "config.json");
const resolvedGlobalSettingsDir = globalSettingsDir
?? (process.env.VITEST === "true" ? join(rootDir, ".fusion-global-settings") : undefined);
this.globalSettingsStore = new GlobalSettingsStore(resolvedGlobalSettingsDir);
@@ -268,10 +246,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
*/
private get db(): Database {
if (!this._db) {
this._db = new Database(this.kbDir);
this._db = new Database(this.fusionDir);
this._db.init();
// Auto-migrate legacy data if needed
if (detectLegacyData(this.kbDir)) {
if (detectLegacyData(this.fusionDir)) {
// Note: migrateFromLegacy is async but we need sync access.
// The init() method handles async migration. This getter
// just ensures the DB is available for synchronous operations.
@@ -282,7 +260,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private get archiveDb(): ArchiveDatabase {
if (!this._archiveDb) {
this._archiveDb = new ArchiveDatabase(this.kbDir);
this._archiveDb = new ArchiveDatabase(this.fusionDir);
this._archiveDb.init();
this.migrateLegacyArchiveEntriesToArchiveDb();
}
@@ -294,13 +272,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Initialize SQLite database
if (!this._db) {
this._db = new Database(this.kbDir);
this._db = new Database(this.fusionDir);
this._db.init();
}
// Auto-migrate from legacy file-based storage
if (detectLegacyData(this.kbDir)) {
await migrateFromLegacy(this.kbDir, this._db);
if (detectLegacyData(this.fusionDir)) {
await migrateFromLegacy(this.fusionDir, this._db);
}
await this.migrateActiveArchivedTasksToArchiveDb();
await this.importLegacyAgentLogsOnce();
@@ -1092,7 +1070,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* Returns the combined view that most consumers should use. Project-level
* values in `.fusion/config.json` override global values from `~/.fusion/settings.json`.
*
* Settings are canonicalized to resolve legacy defaults (e.g., `.kb/backups` → `.fusion/backups`).
*
*/
async getSettings(): Promise<Settings> {
const [globalSettings, config] = await Promise.all([
@@ -1116,7 +1094,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
*
* Note: Do NOT use this method when you need workflow steps — use `getSettings()` instead.
*
* Settings are canonicalized to resolve legacy defaults (e.g., `.kb/backups` → `.fusion/backups`).
*
*/
async getSettingsFast(): Promise<Settings> {
const [globalSettings, row] = await Promise.all([
@@ -1138,7 +1116,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* project-level settings independently (useful for the UI to show
* which scope a value comes from).
*
* Settings are canonicalized to resolve legacy defaults (e.g., `.kb/backups` → `.fusion/backups`).
*
*/
async getSettingsByScope(): Promise<{ global: GlobalSettings; project: Partial<ProjectSettings> }> {
const [globalSettings, config] = await Promise.all([
@@ -1171,7 +1149,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* uses the cached global settings from `GlobalSettingsStore`. Use this for
* read-heavy paths like the settings page that don't need workflow steps.
*
* Settings are canonicalized to resolve legacy defaults (e.g., `.kb/backups` → `.fusion/backups`).
*
*/
async getSettingsByScopeFast(): Promise<{ global: GlobalSettings; project: Partial<ProjectSettings> }> {
const [globalSettings, row] = await Promise.all([
@@ -5064,7 +5042,7 @@ ${stepsSection}`;
/** Return the `.fusion` directory path (e.g. `/project/.fusion`). */
getFusionDir(): string {
return this.kbDir;
return this.fusionDir;
}
getTasksDir(): string {
@@ -5310,7 +5288,7 @@ ${notificationsSection}`;
*/
getMissionStore(): MissionStore {
if (!this.missionStore) {
this.missionStore = new MissionStore(this.kbDir, this.db, this);
this.missionStore = new MissionStore(this.fusionDir, this.db, this);
}
return this.missionStore;
}