fix(projects): restore project key as grouping identity

Random host-local project IDs had replaced the persisted cross-host value carried by projectKey. Keep projectId for routing while reconciliation owns and persists projectKey for grouping.
This commit is contained in:
Mohamed Boudra
2026-07-29 09:12:08 +00:00
parent 39e5006882
commit 649592fe73
40 changed files with 201 additions and 221 deletions
@@ -75,7 +75,7 @@ test("creates a worktree and registers it in the source workspace project withou
expect(deps.workspaceGitService.getSnapshot).not.toHaveBeenCalled();
expect(deps.projects.get(sourceProject.projectId)).toEqual({
...sourceProject,
projectGroupKey: path.resolve(result.repoRoot),
projectKey: path.resolve(result.repoRoot),
updatedAt: expect.any(String),
});
expect(events).toEqual([`workspace:${result.workspace.workspaceId}`]);
@@ -442,7 +442,7 @@ test("an explicit project FK remains unchanged when its worktree comes from anot
expect(result.workspace.projectId).toBe(project.projectId);
expect(deps.projects.get(project.projectId)).toEqual({
...project,
projectGroupKey: path.resolve(project.rootPath),
projectKey: path.resolve(project.rootPath),
updatedAt: expect.any(String),
});
});
@@ -2,14 +2,14 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, test } from "vitest";
import { deriveProjectGroupKey, deriveProjectGroupingDisplayName } from "./project-group-key.js";
import { deriveProjectKey, deriveProjectGroupingDisplayName } from "./project-key.js";
describe("deriveProjectGroupKey", () => {
describe("deriveProjectKey", () => {
test("preserves an explicit remote port", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "https://git.example.com:8443/acme/app.git",
worktreeRoot: rootPath,
@@ -21,7 +21,7 @@ describe("deriveProjectGroupKey", () => {
test("normalizes the default SSH port", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -37,7 +37,7 @@ describe("deriveProjectGroupKey", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "https://git.example.com/repo.git",
worktreeRoot: rootPath,
@@ -49,7 +49,7 @@ describe("deriveProjectGroupKey", () => {
test("preserves meaningful dot-git suffixes on unknown Git servers", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -62,7 +62,7 @@ describe("deriveProjectGroupKey", () => {
test("normalizes dot-git suffixes on known cloud forges", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -75,7 +75,7 @@ describe("deriveProjectGroupKey", () => {
test("normalizes GitHub owner and repository casing", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -92,7 +92,7 @@ describe("deriveProjectGroupKey", () => {
(remoteUrl) => {
const rootPath = path.resolve("repo");
const derive = (value: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: value,
worktreeRoot: rootPath,
@@ -107,7 +107,7 @@ describe("deriveProjectGroupKey", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "github.com:getpaseo/paseo.git",
worktreeRoot: rootPath,
@@ -119,7 +119,7 @@ describe("deriveProjectGroupKey", () => {
test("distinguishes absolute and home-relative SCP paths", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -132,7 +132,7 @@ describe("deriveProjectGroupKey", () => {
test("distinguishes absolute SSH URL paths from home-relative SCP paths", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -147,7 +147,7 @@ describe("deriveProjectGroupKey", () => {
test("distinguishes SSH users for home-relative SCP paths", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -162,7 +162,7 @@ describe("deriveProjectGroupKey", () => {
test("keeps percent sequences literal in SCP paths", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -177,7 +177,7 @@ describe("deriveProjectGroupKey", () => {
test("preserves leading whitespace in SCP repository paths", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -193,7 +193,7 @@ describe("deriveProjectGroupKey", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "C:repo",
worktreeRoot: rootPath,
@@ -205,7 +205,7 @@ describe("deriveProjectGroupKey", () => {
test.each(["git+ssh:", "ssh+git:"])("normalizes SSH alias default ports for %s", (scheme) => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
@@ -221,7 +221,7 @@ describe("deriveProjectGroupKey", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "git@[2001:db8::1]:getpaseo/paseo.git",
worktreeRoot: rootPath,
@@ -235,7 +235,7 @@ describe("deriveProjectGroupKey", () => {
const rootPath = path.join(worktreeRoot, "packages", "app");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "git@github.com:getpaseo/paseo.git",
worktreeRoot,
@@ -246,13 +246,13 @@ describe("deriveProjectGroupKey", () => {
test("keeps a selected path distinct from remote path syntax", () => {
const worktreeRoot = path.resolve("repo");
const selectedKey = deriveProjectGroupKey({
const selectedKey = deriveProjectKey({
rootPath: path.join(worktreeRoot, "packages", "app"),
remoteUrl: "example.com:acme/repo.git",
worktreeRoot,
mainRepoRoot: null,
});
const remoteSyntaxKey = deriveProjectGroupKey({
const remoteSyntaxKey = deriveProjectKey({
rootPath: worktreeRoot,
remoteUrl: "example.com:acme/repo#subdir:packages/app.git",
worktreeRoot,
@@ -266,7 +266,7 @@ describe("deriveProjectGroupKey", () => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: "git@github.com:getpaseo/paseo.git",
worktreeRoot: rootPath,
@@ -280,7 +280,7 @@ describe("deriveProjectGroupKey", () => {
const mainRepoRoot = path.resolve("main");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath: path.join(worktreeRoot, "packages", "app"),
remoteUrl: null,
worktreeRoot,
@@ -292,7 +292,7 @@ describe("deriveProjectGroupKey", () => {
test("keeps path-only project identities scoped to their host", () => {
const rootPath = path.resolve("repo");
const derive = (serverId: string) =>
deriveProjectGroupKey({
deriveProjectKey({
rootPath,
remoteUrl: null,
worktreeRoot: null,
@@ -306,7 +306,7 @@ describe("deriveProjectGroupKey", () => {
test("preserves selected-path casing across Windows and POSIX hosts", () => {
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath: "c:\\repo\\Packages\\App",
remoteUrl: "git@github.com:getpaseo/paseo.git",
worktreeRoot: "C:\\Repo",
@@ -318,7 +318,7 @@ describe("deriveProjectGroupKey", () => {
test.skipIf(process.platform === "win32")(
"preserves a selected subproject reached through a symlink",
() => {
const tempDir = mkdtempSync(path.join(tmpdir(), "project-group-key-"));
const tempDir = mkdtempSync(path.join(tmpdir(), "project-key-"));
try {
const worktreeRoot = path.join(tempDir, "repo");
const selectedRoot = path.join(worktreeRoot, "packages", "app");
@@ -327,7 +327,7 @@ describe("deriveProjectGroupKey", () => {
symlinkSync(selectedRoot, linkedRoot, "dir");
expect(
deriveProjectGroupKey({
deriveProjectKey({
rootPath: linkedRoot,
remoteUrl: "git@github.com:getpaseo/paseo.git",
worktreeRoot,
@@ -23,14 +23,14 @@ const DEFAULT_REMOTE_PORTS: Partial<Record<string, string>> = {
* Derives the persisted, opaque equivalence key used to group projects across hosts.
* A Git remote is today's strongest shared fact, but callers treat the result as opaque.
*/
export function deriveProjectGroupKey(input: {
export function deriveProjectKey(input: {
rootPath: string;
remoteUrl: string | null;
worktreeRoot: string | null;
mainRepoRoot: string | null;
serverId?: string;
}): string {
const remoteKey = deriveRemoteProjectGroupKey(input.remoteUrl);
const remoteKey = deriveRemoteProjectKey(input.remoteUrl);
const selectedPath = deriveSelectedPath(input.rootPath, input.worktreeRoot);
if (!remoteKey) {
const localPath =
@@ -54,7 +54,7 @@ function encodeSelectedPath(selectedPath: string): string {
return selectedPath.split(/[\\/]/u).map(encodeURIComponent).join("/");
}
function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
function deriveRemoteProjectKey(remoteUrl: string | null): string | null {
if (!remoteUrl?.trim()) return null;
const remote = parseRemoteLocation(remoteUrl);
if (!remote) return null;
+1 -1
View File
@@ -649,7 +649,7 @@ describe("project command-center RPCs", () => {
rootPath: directoryPath,
kind: "non_git",
displayName: "new-project",
projectGroupKey: directoryPath,
projectKey: directoryPath,
timestamp: expect.any(String),
});
expect(messages).toEqual([
+3 -4
View File
@@ -1690,8 +1690,7 @@ export class Session {
fallbackWorktreeRoot: snapshot?.git.repoRoot,
});
return {
projectKey: project.projectId,
...(project.projectGroupKey ? { projectGroupKey: project.projectGroupKey } : {}),
projectKey: project.projectKey ?? project.projectId,
projectName: resolveProjectDisplayName(project),
workspaceName: resolveWorkspaceDisplayName(workspace),
checkout,
@@ -4290,7 +4289,7 @@ export class Session {
return {
id: result.workspace.workspaceId,
projectId: result.workspace.projectId,
...(projectRecord?.projectGroupKey ? { projectGroupKey: projectRecord.projectGroupKey } : {}),
...(projectRecord?.projectKey ? { projectKey: projectRecord.projectKey } : {}),
projectDisplayName: projectRecord
? resolveProjectDisplayName(projectRecord)
: result.workspace.projectId,
@@ -4463,7 +4462,7 @@ export class Session {
): WorkspaceProjectDescriptorPayload {
return {
projectId: project.projectId,
...(project.projectGroupKey ? { projectGroupKey: project.projectGroupKey } : {}),
...(project.projectKey ? { projectKey: project.projectKey } : {}),
projectDisplayName: resolveProjectDisplayName(project),
projectCustomName: project.customName ?? null,
projectRootPath: project.rootPath,
@@ -3699,7 +3699,7 @@ test("create paseo worktree response preserves an explicit non-Git project", asy
expect(response?.payload.error).toBeNull();
expect(response?.payload.workspace).toMatchObject({
projectId: explicitProject.projectId,
projectGroupKey: explicitProject.rootPath,
projectKey: explicitProject.rootPath,
projectDisplayName: explicitProject.displayName,
projectRootPath: explicitProject.rootPath,
projectKind: "non_git",
@@ -3713,7 +3713,7 @@ test("create paseo worktree response preserves an explicit non-Git project", asy
expect(workspaces.has(response?.payload.workspace?.id ?? "")).toBe(true);
expect(projects.get(explicitProject.projectId)).toEqual({
...explicitProject,
projectGroupKey: explicitProject.rootPath,
projectKey: explicitProject.rootPath,
});
});
@@ -208,7 +208,7 @@ test("reopening archived exact-root records restores the fresh Git project", asy
});
await projectRegistry.upsert({
...project,
projectGroupKey: "remote:github.com/acme/old-repo",
projectKey: "remote:github.com/acme/old-repo",
});
const workspace = createPersistedWorkspaceRecord({
workspaceId: "ws-archived-root",
@@ -248,7 +248,7 @@ test("reopening archived exact-root records restores the fresh Git project", asy
});
expect(await projectRegistry.get(project.projectId)).toMatchObject({
kind: "git",
projectGroupKey: "remote:github.com/acme/new-repo",
projectKey: "remote:github.com/acme/new-repo",
archivedAt: null,
});
});
@@ -14,7 +14,7 @@ import {
} from "../../workspace-registry.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js";
import { deriveProjectGroupKey } from "../../project-group-key.js";
import { deriveProjectKey } from "../../project-key.js";
import { areEquivalentPaths, createRealpathAwarePathMatcher } from "../../../utils/path.js";
export interface ResolveOrCreateWorkspaceIdInput {
@@ -164,7 +164,7 @@ export function createWorkspaceProvisioningService(deps: {
rootPath,
kind: checkout.isGit ? "git" : "non_git",
displayName: basename(rootPath) || rootPath,
projectGroupKey: deriveProjectGroupKey({
projectKey: deriveProjectKey({
rootPath,
remoteUrl: checkout.remoteUrl,
worktreeRoot: checkout.worktreeRoot,
@@ -270,7 +270,7 @@ export function createWorkspaceProvisioningService(deps: {
rootPath: input.repoRoot,
kind: "git",
displayName: basename(input.repoRoot) || input.repoRoot,
projectGroupKey: deriveProjectGroupKey({
projectKey: deriveProjectKey({
rootPath: input.repoRoot,
remoteUrl: checkout.remoteUrl,
worktreeRoot: checkout.worktreeRoot,
@@ -351,22 +351,18 @@ export function createWorkspaceProvisioningService(deps: {
? checkout
: await workspaceGitService.getCheckout(project.rootPath);
const kind = projectCheckout.isGit ? "git" : "non_git";
const projectGroupKey = deriveProjectGroupKey({
const projectKey = deriveProjectKey({
rootPath: project.rootPath,
remoteUrl: projectCheckout.remoteUrl,
worktreeRoot: projectCheckout.worktreeRoot,
mainRepoRoot: projectCheckout.mainRepoRoot,
serverId,
});
if (
project.archivedAt ||
project.kind !== kind ||
project.projectGroupKey !== projectGroupKey
) {
if (project.archivedAt || project.kind !== kind || project.projectKey !== projectKey) {
await projectRegistry.upsert({
...project,
kind,
projectGroupKey,
projectKey,
archivedAt: null,
updatedAt: timestamp,
});
@@ -405,18 +401,18 @@ export function createWorkspaceProvisioningService(deps: {
? workspaceCheckout
: await workspaceGitService.getCheckout(project.rootPath);
const kind: PersistedProjectRecord["kind"] = projectCheckout.isGit ? "git" : "non_git";
const projectGroupKey = deriveProjectGroupKey({
const projectKey = deriveProjectKey({
rootPath: project.rootPath,
remoteUrl: projectCheckout.remoteUrl,
worktreeRoot: projectCheckout.worktreeRoot,
mainRepoRoot: projectCheckout.mainRepoRoot,
serverId,
});
if (project.kind === kind && project.projectGroupKey === projectGroupKey) return project;
if (project.kind === kind && project.projectKey === projectKey) return project;
const refreshed = {
...project,
kind,
projectGroupKey,
projectKey,
updatedAt: new Date().toISOString(),
};
await projectRegistry.upsert(refreshed);
@@ -260,7 +260,7 @@ function createNoopProjectRegistry(): ProjectRegistry {
rootPath: input.rootPath,
kind: input.kind,
displayName: input.displayName,
projectGroupKey: input.projectGroupKey ?? null,
projectKey: input.projectKey ?? null,
customName: null,
createdAt: input.timestamp,
updatedAt: input.timestamp,
@@ -502,7 +502,7 @@ export class WorkspaceDirectory {
)
.map((project) => ({
projectId: project.projectId,
projectGroupKey: project.projectGroupKey ?? undefined,
projectKey: project.projectKey ?? undefined,
projectDisplayName: resolveProjectDisplayName(project),
projectCustomName: project.customName ?? null,
projectRootPath: project.rootPath,
@@ -258,7 +258,7 @@ describe("WorkspaceReconciliationService", () => {
kind: "project_updated",
projectId: "p1",
directory: projectRoot,
fields: { projectGroupKey: projectRoot },
fields: { projectKey: projectRoot },
},
]);
expect(workspaces.get("w1")?.archivedAt).toBeNull();
@@ -316,7 +316,7 @@ describe("WorkspaceReconciliationService", () => {
kind: "project_updated",
projectId: "p1",
directory: projectRoot,
fields: { projectGroupKey: projectRoot },
fields: { projectKey: projectRoot },
},
]);
expect(afterGitInit.changesApplied).toEqual([
@@ -405,13 +405,13 @@ describe("WorkspaceReconciliationService", () => {
kind: "project_updated",
projectId: "p1",
directory: projectRoot,
fields: { projectGroupKey: projectRoot },
fields: { projectKey: projectRoot },
},
{
kind: "project_updated",
projectId: "p2",
directory: equivalentProjectRoot,
fields: { projectGroupKey: projectRoot },
fields: { projectKey: projectRoot },
},
]);
expect(git.reads).toEqual([projectRoot, workspaceRoot]);
@@ -472,7 +472,7 @@ describe("WorkspaceReconciliationService", () => {
kind: "project_updated",
projectId: "p1",
directory: projectRoot,
fields: { kind: "git", projectGroupKey: projectRoot },
fields: { kind: "git", projectKey: projectRoot },
},
{
kind: "workspace_updated",
@@ -489,7 +489,7 @@ describe("WorkspaceReconciliationService", () => {
expect(projects.get("p1")).toEqual({
...originalProject,
kind: "git",
projectGroupKey: projectRoot,
projectKey: projectRoot,
updatedAt: expect.any(String),
});
expect(workspaces.get("w1")).toEqual({
@@ -815,7 +815,7 @@ describe("WorkspaceReconciliationService", () => {
rootPath: repoDir,
displayName: "blank-dot-page/editor",
customName: null,
projectGroupKey: "remote:github.com/blank-dot-page/editor",
projectKey: "remote:github.com/blank-dot-page/editor",
archivedAt: null,
});
expect(projects.get(repoDir)).toMatchObject({
@@ -823,7 +823,7 @@ describe("WorkspaceReconciliationService", () => {
rootPath: repoDir,
displayName: "editor",
customName: "Editor",
projectGroupKey: "remote:github.com/blank-dot-page/editor",
projectKey: "remote:github.com/blank-dot-page/editor",
archivedAt: null,
});
expect(workspaces.get("focused-bat")).toMatchObject({
@@ -836,7 +836,7 @@ describe("WorkspaceReconciliationService", () => {
});
});
test("updates the group key while keeping the project display name stable", async () => {
test("updates the project key while keeping the project display name stable", async () => {
const dir = createTempGitRepo("reconcile-remote-");
tempDirs.push(dir);
@@ -889,10 +889,10 @@ describe("WorkspaceReconciliationService", () => {
const result = await service.runOnce();
expect(result.changesApplied.find((c) => c.kind === "project_updated")).toMatchObject({
fields: { projectGroupKey: "remote:github.com/new-owner/new-repo" },
fields: { projectKey: "remote:github.com/new-owner/new-repo" },
});
expect(projects.get("p1")!.displayName).toBe("old-owner/old-repo");
expect(projects.get("p1")!.projectGroupKey).toBe("remote:github.com/new-owner/new-repo");
expect(projects.get("p1")!.projectKey).toBe("remote:github.com/new-owner/new-repo");
});
test("falls back to the persisted root when a Git remote disappears", async () => {
@@ -904,7 +904,7 @@ describe("WorkspaceReconciliationService", () => {
"p1",
createPersistedProjectRecord({
projectId: "p1",
projectGroupKey: "remote:github.com/acme/old-repo",
projectKey: "remote:github.com/acme/old-repo",
rootPath: dir,
kind: "git",
displayName: "acme/old-repo",
@@ -941,7 +941,7 @@ describe("WorkspaceReconciliationService", () => {
await service.runOnce();
expect(projects.get("p1")?.projectGroupKey).toBe(path.resolve(dir));
expect(projects.get("p1")?.projectKey).toBe(path.resolve(dir));
});
test("keeps custom and default names stable when the remote changes", async () => {
@@ -1121,7 +1121,7 @@ describe("WorkspaceReconciliationService", () => {
kind: "project_updated",
projectId: "p1",
directory: projectRoot,
fields: { kind: "git", projectGroupKey: projectRoot },
fields: { kind: "git", projectKey: projectRoot },
},
{
kind: "workspace_updated",
@@ -1409,7 +1409,7 @@ describe("WorkspaceReconciliationService", () => {
kind: "project_updated",
projectId: "p1",
directory: rootPath,
fields: { projectGroupKey: path.resolve("/tmp/main-repo") },
fields: { projectKey: path.resolve("/tmp/main-repo") },
},
{
kind: "workspace_updated",
@@ -15,7 +15,7 @@ import {
type MutableWorkspacePlacement,
} from "./workspace-registry-model.js";
import { workspaceIdsForProjects } from "./workspace-directory.js";
import { deriveProjectGroupKey } from "./project-group-key.js";
import { deriveProjectKey } from "./project-key.js";
const DEFAULT_RESCAN_INTERVAL_MS = 5 * 60_000;
const DEFAULT_DEBOUNCE_MS = 100;
@@ -67,7 +67,7 @@ export type ReconciliationChange =
kind: "project_updated";
projectId: string;
directory: string;
fields: Partial<Pick<PersistedProjectRecord, "kind" | "projectGroupKey">>;
fields: Partial<Pick<PersistedProjectRecord, "kind" | "projectKey">>;
}
| {
kind: "workspace_updated";
@@ -343,9 +343,9 @@ export class WorkspaceReconciliationService {
checkout: await readCheckout(workspace.cwd),
})),
);
const projectUpdates: Partial<Pick<PersistedProjectRecord, "kind" | "projectGroupKey">> = {};
const projectUpdates: Partial<Pick<PersistedProjectRecord, "kind" | "projectKey">> = {};
const mappedKind = deriveProjectKind(currentGit);
const projectGroupKey = deriveProjectGroupKey({
const projectKey = deriveProjectKey({
rootPath: project.rootPath,
remoteUrl: currentGit.remoteUrl,
worktreeRoot: currentGit.worktreeRoot,
@@ -356,8 +356,8 @@ export class WorkspaceReconciliationService {
if (project.kind !== mappedKind) {
projectUpdates.kind = mappedKind;
}
if (project.projectGroupKey !== projectGroupKey) {
projectUpdates.projectGroupKey = projectGroupKey;
if (project.projectKey !== projectKey) {
projectUpdates.projectKey = projectKey;
}
if (Object.keys(projectUpdates).length > 0) {
@@ -4,7 +4,7 @@ import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages";
import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js";
import { getRealpathAwareRelativePath } from "../utils/path.js";
import { deriveProjectGroupKey, deriveProjectGroupingDisplayName } from "./project-group-key.js";
import { deriveProjectKey, deriveProjectGroupingDisplayName } from "./project-key.js";
import {
deriveProjectKind,
deriveWorkspaceDisplayName,
@@ -34,7 +34,7 @@ export function classifyDirectoryForProjectMembership(input: {
}): DirectoryProjectMembership {
const cwd = resolve(input.cwd);
const checkout: ProjectCheckoutLitePayload = { ...input.checkout, cwd };
const projectKey = deriveProjectGroupKey({
const projectKey = deriveProjectKey({
rootPath: cwd,
remoteUrl: checkout.remoteUrl,
worktreeRoot: checkout.worktreeRoot,
@@ -16,8 +16,8 @@ const PersistedProjectRecordSchema = z.object({
rootPath: z.string(),
kind: z.enum(["git", "non_git"]),
displayName: z.string(),
// COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28.
projectGroupKey: z
// COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28.
projectKey: z
.string()
.nullable()
.optional()
@@ -110,7 +110,7 @@ export interface ProjectRegistry {
rootPath: string;
kind: PersistedProjectKind;
displayName: string;
projectGroupKey?: string;
projectKey?: string;
timestamp: string;
}): Promise<PersistedProjectRecord>;
upsert(record: PersistedProjectRecord): Promise<void>;
@@ -315,7 +315,7 @@ export class FileBackedProjectRegistry
rootPath: string;
kind: PersistedProjectKind;
displayName: string;
projectGroupKey?: string;
projectKey?: string;
timestamp: string;
}): Promise<PersistedProjectRecord> {
const previous = this.allocationQueue;
@@ -333,15 +333,12 @@ export class FileBackedProjectRegistry
left.projectId.localeCompare(right.projectId),
)[0];
if (active) {
if (
active.kind === input.kind &&
active.projectGroupKey === (input.projectGroupKey ?? null)
)
if (active.kind === input.kind && active.projectKey === (input.projectKey ?? null))
return active;
const refreshed = {
...active,
kind: input.kind,
projectGroupKey: input.projectGroupKey ?? null,
projectKey: input.projectKey ?? null,
updatedAt: input.timestamp,
};
await this.upsert(refreshed);
@@ -356,7 +353,7 @@ export class FileBackedProjectRegistry
rootPath: input.rootPath,
kind: input.kind,
displayName: input.displayName,
projectGroupKey: input.projectGroupKey ?? null,
projectKey: input.projectKey ?? null,
createdAt: input.timestamp,
updatedAt: input.timestamp,
});
@@ -477,7 +474,7 @@ export function createPersistedProjectRecord(input: {
kind: PersistedProjectKind;
displayName: string;
customName?: string | null;
projectGroupKey?: string | null;
projectKey?: string | null;
createdAt: string;
updatedAt: string;
archivedAt?: string | null;
@@ -485,7 +482,7 @@ export function createPersistedProjectRecord(input: {
return PersistedProjectRecordSchema.parse({
...input,
customName: input.customName ?? null,
projectGroupKey: input.projectGroupKey ?? null,
projectKey: input.projectKey ?? null,
archivedAt: input.archivedAt ?? null,
});
}