## Summary Fusion’s core runtime now treats PostgreSQL as the authoritative metadata store without leaving current CLI, dashboard, desktop, or engine composition roots uncompilable between stack layers. This is the 99-file foundation for the larger cutover: subsequent PRs migrate the remaining consumers, plugins, and operator surfaces. ## Design decisions - Runtime store construction fails closed when an asynchronous PostgreSQL layer is unavailable; SQLite remains readable only at explicit migration and identity-recovery boundaries. - Project ownership is enforced across active, archived, workflow, mission, analytics, and plugin-schema data. - The small set of cross-package files in this layer are compatibility-critical call sites required for a green intermediate commit, not the complete consumer migration. - Schema migration 0008 remains assigned to session-advisor state from current `main`; mission lineage idempotency advances to 0009 so neither invariant can be skipped. ## Validation - All affected package typechecks pass: Core, Engine, Dashboard, CLI, and Desktop. - `pnpm test:gate` passes: 478 tests across the engine gate, PostgreSQL core gate, and CLI workflow shape. - The PR changes exactly 99 files. ## Stack This is the base PR. Engine/dashboard, CLI/desktop/ops, plugins, and docs/release follow as stacked PRs, each below 100 changed files. Related: #2105 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * PostgreSQL is now the standard runtime backend, with embedded PostgreSQL enabled by default. * Added project-scoped storage for tasks, archives, chat sessions, missions, knowledge pages, and operational data. * Improved archived-task search, filtering, pagination, and restoration. * Added safer plugin schema initialization with validation and project isolation. * Added PostgreSQL-backed workflow, mission, validator, and dashboard capabilities. * **Bug Fixes** * Improved startup timeout cancellation and resource cleanup. * Prevented cross-project data access and phantom reservation cleanup errors. * Ensured archived tasks remain read-only and asynchronous writes complete reliably. * Retired SQLite opt-out settings with clear startup errors. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
/**
|
|
* Guard helpers for store constructors that expect a project root and append
|
|
* `.fusion` internally. Passing an existing `.fusion` directory produces the
|
|
* nested `.fusion/.fusion` tree we want to fail loudly on.
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { PROJECT_IDENTITY_FILENAME } from "./project-identity.js";
|
|
|
|
const FUSION_DIR_SUFFIX = /(?:^|[\\/])\.fusion(?:[\\/])?$/;
|
|
|
|
/**
|
|
* FNXC:PostgresProjectDiscovery 2026-07-14-17:30:
|
|
* The PostgreSQL-era project marker protects linked worktrees from accidental
|
|
* nested initialization. `fusion.db` remains a legacy signal only.
|
|
*/
|
|
function hasFusionProjectSignal(rootDir: string): boolean {
|
|
return existsSync(join(rootDir, ".fusion", PROJECT_IDENTITY_FILENAME))
|
|
|| existsSync(join(rootDir, ".fusion", "fusion.db"));
|
|
}
|
|
|
|
export class LinkedWorktreeBootstrapRefusedError extends Error {
|
|
constructor(cwd: string, parentRoot: string) {
|
|
super(
|
|
`Refusing to bootstrap a Fusion project at ${cwd}: this is a linked worktree of ${parentRoot}, which already has a Fusion project at ${parentRoot}/.fusion. Run from the parent, or set FUSION_ALLOW_NESTED_PROJECT=1 to override.`,
|
|
);
|
|
this.name = "LinkedWorktreeBootstrapRefusedError";
|
|
}
|
|
}
|
|
|
|
export function assertProjectRootDir(rootDir: string, caller: string): void {
|
|
if (FUSION_DIR_SUFFIX.test(rootDir)) {
|
|
throw new Error(
|
|
`[fusion] ${caller} expected a project root, got a .fusion directory: ${rootDir}\n` +
|
|
"Pass the project root instead; this store appends `.fusion` internally.",
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertNotLinkedWorktreeOfExistingProject(rootDir: string, _caller: string): void {
|
|
const resolvedRootDir = resolve(rootDir);
|
|
if (hasFusionProjectSignal(resolvedRootDir)) {
|
|
return;
|
|
}
|
|
if (
|
|
process.env.VITEST === "true"
|
|
&& process.env.FUSION_TEST_LINKED_WORKTREE_GUARD !== "1"
|
|
) {
|
|
return;
|
|
}
|
|
if (process.env.FUSION_ALLOW_NESTED_PROJECT === "1") {
|
|
return;
|
|
}
|
|
|
|
const gitCommonDir = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
|
cwd: resolvedRootDir,
|
|
encoding: "utf8",
|
|
});
|
|
const gitDir = spawnSync("git", ["rev-parse", "--git-dir"], {
|
|
cwd: resolvedRootDir,
|
|
encoding: "utf8",
|
|
});
|
|
|
|
if (gitCommonDir.status !== 0 || gitDir.status !== 0) {
|
|
return;
|
|
}
|
|
|
|
const resolvedCommonDir = resolve(resolvedRootDir, gitCommonDir.stdout.trim());
|
|
const resolvedGitDir = resolve(resolvedRootDir, gitDir.stdout.trim());
|
|
if (resolvedCommonDir === resolvedGitDir) {
|
|
return;
|
|
}
|
|
|
|
const parentRoot = resolvedCommonDir.endsWith(`${join("", ".git")}`)
|
|
? dirname(resolvedCommonDir)
|
|
: resolvedCommonDir;
|
|
|
|
if (!hasFusionProjectSignal(parentRoot)) {
|
|
return;
|
|
}
|
|
|
|
throw new LinkedWorktreeBootstrapRefusedError(resolvedRootDir, parentRoot);
|
|
}
|