Files
fusion/packages/core/src/project-root-guard.ts
gsxdsm 2e4fcfcaea fix(FN-7952): establish PostgreSQL core authority (#2108)
## 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 -->
2026-07-14 22:13:30 -07:00

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);
}