From 649592fe73b4e6268fe47a008701bee4b8c45981 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 29 Jul 2026 09:12:08 +0000 Subject: [PATCH] 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. --- docs/data-model.md | 26 ++++----- docs/glossary.md | 3 +- .../app/e2e/empty-project-persists.spec.ts | 6 +-- packages/app/e2e/helpers/new-workspace.ts | 4 +- packages/app/e2e/helpers/seed-client.ts | 10 ++-- .../e2e/import-session.opencode.real.spec.ts | 7 +-- .../app/e2e/sidebar-project-grouping.spec.ts | 20 +++---- .../app/e2e/workspace-model-restart.spec.ts | 6 +-- .../worktree-setup-callout-policy.test.ts | 6 +-- .../worktree-setup-callout-policy.ts | 8 +-- .../sidebar-workspaces-view-model.test.ts | 6 +-- .../hooks/sidebar-workspaces-view-model.ts | 6 +-- .../app/src/hooks/use-open-project.test.ts | 4 +- .../app/src/projects/host-project-model.ts | 6 +-- .../{project-group-key.ts => project-key.ts} | 8 +-- .../src/projects/project-settings-target.ts | 2 +- .../src/projects/workspace-structure.test.ts | 2 +- .../app/src/projects/workspace-structure.ts | 28 +++++----- .../directory-sync/workspace-replica.test.ts | 6 +-- .../directory-sync/workspace-replica.ts | 2 +- .../app/src/runtime/replica-cache/index.ts | 2 +- packages/app/src/stores/session-store.ts | 10 ++-- packages/app/src/utils/projects.test.ts | 52 +++++++++--------- packages/app/src/utils/projects.ts | 6 +-- .../src/workspace/legacy-daemon-workspaces.ts | 2 +- packages/protocol/src/messages.ts | 10 ++-- .../src/server/paseo-worktree-service.test.ts | 4 +- ...-group-key.test.ts => project-key.test.ts} | 54 +++++++++---------- .../{project-group-key.ts => project-key.ts} | 6 +-- packages/server/src/server/session.test.ts | 2 +- packages/server/src/server/session.ts | 7 ++- .../src/server/session.workspaces.test.ts | 4 +- .../workspace-provisioning-service.test.ts | 4 +- .../workspace-provisioning-service.ts | 22 ++++---- .../server/src/server/websocket-server.ts | 2 +- .../server/src/server/workspace-directory.ts | 2 +- .../workspace-reconciliation-service.test.ts | 30 +++++------ .../workspace-reconciliation-service.ts | 12 ++--- .../workspace-registry-bootstrap-legacy.ts | 4 +- .../server/src/server/workspace-registry.ts | 21 ++++---- 40 files changed, 201 insertions(+), 221 deletions(-) rename packages/app/src/projects/{project-group-key.ts => project-key.ts} (69%) rename packages/server/src/server/{project-group-key.test.ts => project-key.test.ts} (90%) rename packages/server/src/server/{project-group-key.ts => project-key.ts} (97%) diff --git a/docs/data-model.md b/docs/data-model.md index ce63e6abd..c47cb29a2 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -4,13 +4,13 @@ Projects are allocated for the exact root selected by the caller, normalized lexically with `path.resolve` (never `realpath`). New project IDs are opaque `prj_<16 hex>` values. Existing remote-shaped or path-shaped IDs are retained as readable compatibility records and are never rekeyed. An active exact root is idempotent; archived-only matches do not resurrect an old project. Workspace `projectId` is stable membership: reconciliation may update git-derived kind and branch metadata, but never rehomes a workspace or changes a project's root, ID, or default name. -`projectGroupKey` is a persisted, opaque equivalence key used only to group the same logical project +`projectKey` is a persisted, opaque equivalence key used only to group the same logical project across hosts. It is separate from the host-local `projectId`; today's producer prefers a normalized Git remote and otherwise uses the local project root. Consumers never derive it from live Git. Creation persists it with the project, and normal boot reconciliation fills or refreshes it for older records where the field is absent—there is no migration. -`kind` and `projectGroupKey` are mutable metadata, not identity. Workspace reconciliation watches active project roots and +`kind` and `projectKey` are mutable metadata, not identity. Workspace reconciliation watches active project roots and updates those fields and `updatedAt` when Git facts change, preserving the project's ID, root path, names, and workspace foreign keys. Attached workspaces are independently refreshed from their own cwd, so an explicit project root never implies a workspace checkout. Empty projects @@ -454,17 +454,17 @@ Single file containing an array of all loop records. Writes are direct (not atom Array of project records. -| Field | Type | Description | -| ----------------- | --------------------------- | -------------------------------------------------------------------------------- | -| `projectId` | `string` | Host-local primary key; new records use opaque `prj_<16 hex>` IDs | -| `projectGroupKey` | `string \| null` | Persisted opaque cross-host grouping key; reconciliation backfills absent values | -| `rootPath` | `string` | Exact lexically normalized selected root; never realpathed | -| `kind` | `"git" \| "non_git"` | Mutable Git observation about `rootPath`, never a membership key | -| `displayName` | `string` | Selected-root basename, stable across remote and Git changes | -| `customName` | `string \| null` | User-set override layered over `displayName`. Null means "use the derived name". | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `archivedAt` | `string \| null` (ISO 8601) | Soft-delete timestamp; required nullable | +| Field | Type | Description | +| ------------- | --------------------------- | -------------------------------------------------------------------------------- | +| `projectId` | `string` | Host-local primary key; new records use opaque `prj_<16 hex>` IDs | +| `projectKey` | `string \| null` | Persisted opaque cross-host grouping key; reconciliation backfills absent values | +| `rootPath` | `string` | Exact lexically normalized selected root; never realpathed | +| `kind` | `"git" \| "non_git"` | Mutable Git observation about `rootPath`, never a membership key | +| `displayName` | `string` | Selected-root basename, stable across remote and Git changes | +| `customName` | `string \| null` | User-set override layered over `displayName`. Null means "use the derived name". | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | | +| `archivedAt` | `string \| null` (ISO 8601) | Soft-delete timestamp; required nullable | Active exact roots are idempotent using lexical platform-equivalence semantics. Existing legacy remote-shaped and path-shaped IDs remain readable, including duplicate roots; reconciliation never diff --git a/docs/glossary.md b/docs/glossary.md index 87dcdd362..bf6aa43d9 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -2,8 +2,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here. -- **Project** — A stable, exact selected-root record. Its host-local `projectId` is an opaque `prj_<16 hex>` value. Its separate persisted `projectGroupKey` is an opaque equivalence key that may group the logical project across hosts. Git facts can update mutable kind and grouping metadata but never the ID, root, or default display name. UI: "Project" / "Add project". Forbidden: "Repo", "Repository" as UI label. -- **Project group key** — Persisted opaque equivalence key used to group projects across hosts. A normalized Git remote is the current producer, but the concept is wider than repository identity and consumers must not parse or rederive it. Code: `projectGroupKey`. +- **Project** — A stable, exact selected-root record. Its host-local `projectId` is an opaque `prj_<16 hex>` value. Its persisted `projectKey` is an opaque equivalence key that may group the logical project across hosts. A normalized Git remote is the current key producer, but consumers must not parse or rederive it. Git facts can update mutable kind and grouping metadata but never the ID, root, or default display name. UI: "Project" / "Add project". Forbidden: "Repo", "Repository" as UI label. - **Workspace** — One concrete `cwd` on one daemon, with git state; belongs to exactly one project. Its `id` is opaque workspace identity; its `cwd` is the filesystem directory. UI: "Workspace". Code: `WorkspaceDescriptorPayload` (`packages/protocol/src/messages.ts:2178`). Don't confuse with: Branch (one branch can back many workspaces via worktrees). Forbidden: "Folder", "Directory" as UI label. - **Archive workspace** — Removes one workspace from active use and archives everything it owns. UI, CLI, and MCP always say "Archive workspace", regardless of backing. The daemon leaves ordinary directories intact and removes a Paseo-owned worktree only when no active workspace still references it. - **Workspace kind** — `"directory" | "local_checkout" | "worktree"`. The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality by `deriveWorkspaceKind` in `workspace-registry-model.ts`, not stored from a user choice. Don't confuse with **Isolation** (the create-time intent). diff --git a/packages/app/e2e/empty-project-persists.spec.ts b/packages/app/e2e/empty-project-persists.spec.ts index 613b4255d..0fead9d1b 100644 --- a/packages/app/e2e/empty-project-persists.spec.ts +++ b/packages/app/e2e/empty-project-persists.spec.ts @@ -222,9 +222,9 @@ test.describe("Project remove", () => { expect(readded.error).toBeNull(); expect(readded.project).not.toBeNull(); readdedProjectId = readded.project?.projectId ?? ""; - const readdedProjectGroupKey = readded.project?.projectGroupKey ?? ""; + const readdedProjectKey = readded.project?.projectKey ?? ""; expect(readdedProjectId).not.toBe(workspace.projectId); - expect(readdedProjectGroupKey).toBe(workspace.projectKey); + expect(readdedProjectKey).toBe(workspace.projectKey); expect(readded.project?.projectDisplayName).toBe(workspace.projectDisplayName); await page.reload(); @@ -233,7 +233,7 @@ test.describe("Project remove", () => { await expect(projectRow).toContainText(workspace.projectDisplayName); await expect(projectRow).not.toContainText(workspace.repoPath); await expect( - page.getByTestId(`sidebar-project-new-workspace-row-${readdedProjectGroupKey}`), + page.getByTestId(`sidebar-project-new-workspace-row-${readdedProjectKey}`), ).toBeVisible({ timeout: 30_000 }); } finally { if (readdedProjectId) { diff --git a/packages/app/e2e/helpers/new-workspace.ts b/packages/app/e2e/helpers/new-workspace.ts index d69a4e8b2..a996691e9 100644 --- a/packages/app/e2e/helpers/new-workspace.ts +++ b/packages/app/e2e/helpers/new-workspace.ts @@ -46,9 +46,9 @@ function requireWorkspace(payload: WorkspacePayload) { } function openedProjectFromWorkspace(workspace: WorkspaceDescriptor): OpenedProject { - const projectKey = workspace.projectGroupKey ?? workspace.project?.projectGroupKey; + const projectKey = workspace.projectKey ?? workspace.project?.projectKey; if (!projectKey) { - throw new Error(`Workspace ${workspace.id} has no project group key`); + throw new Error(`Workspace ${workspace.id} has no project key`); } return { workspaceId: workspace.id, diff --git a/packages/app/e2e/helpers/seed-client.ts b/packages/app/e2e/helpers/seed-client.ts index 2e06a5326..a6ffba195 100644 --- a/packages/app/e2e/helpers/seed-client.ts +++ b/packages/app/e2e/helpers/seed-client.ts @@ -8,8 +8,8 @@ export interface SeedWorkspaceDescriptor { id: string; name: string; projectId: string; - projectGroupKey?: string; - project?: { projectGroupKey?: string }; + projectKey?: string; + project?: { projectKey?: string }; projectDisplayName: string; projectRootPath: string; workspaceDirectory: string; @@ -27,7 +27,7 @@ export interface SeedDaemonClient { addProject(cwd: string): Promise<{ project: { projectId: string; - projectGroupKey?: string; + projectKey?: string; projectDisplayName: string; projectRootPath: string; } | null; @@ -213,9 +213,9 @@ export async function seedWorkspace(options: { throw new Error(created.error ?? `Failed to create workspace ${project.path}`); } const workspace = created.workspace; - const projectKey = workspace.projectGroupKey ?? workspace.project?.projectGroupKey; + const projectKey = workspace.projectKey ?? workspace.project?.projectKey; if (!projectKey) { - throw new Error(`Created workspace ${workspace.id} has no project group key`); + throw new Error(`Created workspace ${workspace.id} has no project key`); } return { client, diff --git a/packages/app/e2e/import-session.opencode.real.spec.ts b/packages/app/e2e/import-session.opencode.real.spec.ts index 2f7d09ac5..da47b3000 100644 --- a/packages/app/e2e/import-session.opencode.real.spec.ts +++ b/packages/app/e2e/import-session.opencode.real.spec.ts @@ -65,12 +65,9 @@ async function seedPaseoWorkspaceWithOpenCodeSession(): Promise { await expect(page.getByTestId("host-picker")).toBeVisible({ timeout: 30_000 }); } -async function readPersistedProjectGroupKey(host: IsolatedHostDaemon): Promise { +async function readPersistedProjectKey(host: IsolatedHostDaemon): Promise { const projectsPath = path.join(host.paseoHome, "projects", "projects.json"); const projects = JSON.parse(await readFile(projectsPath, "utf8")) as Array< Record >; - return projects[0]?.projectGroupKey; + return projects[0]?.projectKey; } -async function removePersistedProjectGroupKeys(host: IsolatedHostDaemon): Promise { +async function removePersistedProjectKeys(host: IsolatedHostDaemon): Promise { const projectsPath = path.join(host.paseoHome, "projects", "projects.json"); const projects = JSON.parse(await readFile(projectsPath, "utf8")) as Array< Record >; for (const project of projects) { - delete project.projectGroupKey; + delete project.projectKey; } await writeFile(projectsPath, JSON.stringify(projects)); const persisted = JSON.parse(await readFile(projectsPath, "utf8")) as Array< Record >; - expect(persisted.every((project) => !("projectGroupKey" in project))).toBe(true); + expect(persisted.every((project) => !("projectKey" in project))).toBe(true); } async function rewritePersistedProjectId( @@ -147,8 +147,8 @@ async function createReconciliationFixture(options?: { sharedLegacyProjectId?: s let secondary = await createProject(secondaryClient, secondaryRepo, secondaryHost.serverId); await primaryClient.close(); await secondaryClient.close(); - await removePersistedProjectGroupKeys(primaryHost); - await removePersistedProjectGroupKeys(secondaryHost); + await removePersistedProjectKeys(primaryHost); + await removePersistedProjectKeys(secondaryHost); if (options?.sharedLegacyProjectId) { await Promise.all([ rewritePersistedProjectId(primaryHost, primary.projectId, options.sharedLegacyProjectId), @@ -256,7 +256,7 @@ test.describe("Sidebar project grouping", () => { await expectOneProjectContainsBothWorkspaces(page, crossHostProject); }); - test("groups persisted projects missing group keys after app boot", async ({ + test("groups persisted projects missing project keys after app boot", async ({ page, reconciledCrossHostProject, }) => { @@ -286,10 +286,10 @@ test.describe("Sidebar project grouping", () => { }); await expectOneProjectContainsBothWorkspaces(page, reconciledCrossHostProject); await expect - .poll(() => readPersistedProjectGroupKey(reconciledCrossHostProject.primaryHost)) + .poll(() => readPersistedProjectKey(reconciledCrossHostProject.primaryHost)) .toBe("remote:github.com/paseo-e2e/grouped-project"); await expect - .poll(() => readPersistedProjectGroupKey(reconciledCrossHostProject.secondaryHost)) + .poll(() => readPersistedProjectKey(reconciledCrossHostProject.secondaryHost)) .toBe("remote:github.com/paseo-e2e/grouped-project"); }); diff --git a/packages/app/e2e/workspace-model-restart.spec.ts b/packages/app/e2e/workspace-model-restart.spec.ts index 8c298e483..c77d2f6ae 100644 --- a/packages/app/e2e/workspace-model-restart.spec.ts +++ b/packages/app/e2e/workspace-model-restart.spec.ts @@ -32,8 +32,8 @@ interface RestartDaemonClient { name: string; status: string; workspaceDirectory: string; - projectGroupKey?: string; - project?: { projectGroupKey?: string }; + projectKey?: string; + project?: { projectKey?: string }; }>; }>; fetchAgents(options?: { scope?: "active" }): Promise<{ @@ -467,7 +467,7 @@ test.describe("Workspace model restart regressions", () => { (workspace) => workspace.id === seeded.workspaceA, ); const reconciledProjectKey = - reconciledWorkspace?.projectGroupKey ?? reconciledWorkspace?.project?.projectGroupKey; + reconciledWorkspace?.projectKey ?? reconciledWorkspace?.project?.projectKey; if (!reconciledProjectKey) { throw new Error(`Workspace ${seeded.workspaceA} was not reconciled with a project key`); } diff --git a/packages/app/src/components/worktree-setup-callout-policy.test.ts b/packages/app/src/components/worktree-setup-callout-policy.test.ts index e63d3ebcc..0d47683cc 100644 --- a/packages/app/src/components/worktree-setup-callout-policy.test.ts +++ b/packages/app/src/components/worktree-setup-callout-policy.test.ts @@ -27,13 +27,13 @@ describe("selectActiveGitWorkspaceProject", () => { }); }); - it("uses the persisted project group key for the settings route", () => { + it("uses the persisted project key for the settings route", () => { expect( selectActiveGitWorkspaceProject( "server-1", gitWorkspace({ projectId: "prj_local", - projectGroupKey: "remote:github.com/acme/project", + projectKey: "remote:github.com/acme/project", }), ), ).toMatchObject({ @@ -101,7 +101,7 @@ describe("buildWorktreeSetupCalloutPolicy", () => { }); }); - it("keeps the action route stable when the structural group key changes", () => { + it("keeps the action route stable when the structural project key changes", () => { expect( buildWorktreeSetupCalloutPolicy({ serverId: "server-1", diff --git a/packages/app/src/components/worktree-setup-callout-policy.ts b/packages/app/src/components/worktree-setup-callout-policy.ts index 925d75414..1f6f89a43 100644 --- a/packages/app/src/components/worktree-setup-callout-policy.ts +++ b/packages/app/src/components/worktree-setup-callout-policy.ts @@ -1,12 +1,12 @@ import type { PaseoConfigRaw } from "@getpaseo/protocol/messages"; import { i18n } from "@/i18n/i18next"; -import { resolveProjectGroupKey } from "@/projects/project-group-key"; +import { resolveProjectKey } from "@/projects/project-key"; import { resolveHostProjectSettingsRouteKey } from "@/projects/project-settings-target"; import { buildProjectSettingsRoute } from "@/utils/host-routes"; export interface WorktreeSetupWorkspaceInput { projectId: string; - projectGroupKey?: string | null; + projectKey?: string | null; projectKind: string; projectRootPath: string; } @@ -43,10 +43,10 @@ export function selectActiveGitWorkspaceProject( } const projectId = workspace.projectId.trim(); - const projectKey = resolveProjectGroupKey({ + const projectKey = resolveProjectKey({ serverId, projectId, - projectGroupKey: workspace.projectGroupKey, + projectKey: workspace.projectKey, }); const repoRoot = workspace.projectRootPath.trim(); if (!projectId || !repoRoot) { diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts index 5f137f87b..0ea11ebc3 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts @@ -115,14 +115,14 @@ function workspace(input: { name: string; projectId: string; projectDisplayName: string; - projectGroupKey?: string; + projectKey?: string; status?: WorkspaceDescriptor["status"]; statusEnteredAt?: Date | null; }): WorkspaceDescriptor { return { id: input.id, projectId: input.projectId, - projectGroupKey: input.projectGroupKey, + projectKey: input.projectKey, projectDisplayName: input.projectDisplayName, projectRootPath: `/repo/${input.projectId}`, workspaceDirectory: `/repo/${input.projectId}/${input.id}`, @@ -422,7 +422,7 @@ describe("shared sidebar workspace model", () => { id: "clone-a", name: "main", projectId: "prj_a", - projectGroupKey: "remote:github.com/acme/app", + projectKey: "remote:github.com/acme/app", projectDisplayName: "acme/app", }), ], diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.ts index 8e1e3c34e..d5449ca8e 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.ts @@ -8,7 +8,7 @@ import type { WorkspaceStructureProject, } from "@/projects/workspace-structure"; import { projectDisplayNameFromProjectId } from "@/utils/project-display-name"; -import { resolveProjectGroupKey } from "@/projects/project-group-key"; +import { resolveProjectKey } from "@/projects/project-key"; import type { WorkspaceAgentActivity } from "@/utils/workspace-agent-activity"; import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity"; @@ -148,10 +148,10 @@ export function createSidebarWorkspaceEntry(input: { }): SidebarWorkspaceEntry { const projectKey = input.projectKey ?? - resolveProjectGroupKey({ + resolveProjectKey({ serverId: input.serverId, projectId: input.workspace.projectId, - projectGroupKey: input.workspace.projectGroupKey, + projectKey: input.workspace.projectKey, }); const effectiveStatus = deriveEffectiveWorkspaceStatus(input); return { diff --git a/packages/app/src/hooks/use-open-project.test.ts b/packages/app/src/hooks/use-open-project.test.ts index f07a3a961..2984c7283 100644 --- a/packages/app/src/hooks/use-open-project.test.ts +++ b/packages/app/src/hooks/use-open-project.test.ts @@ -93,7 +93,7 @@ describe("openProjectDirectly", () => { serverId: SERVER_ID, project: { projectId: "project-1", - projectGroupKey: null, + projectKey: null, projectDisplayName: "project", projectCustomName: null, projectKind: "git", @@ -192,7 +192,7 @@ describe("cloneGithubProjectDirectly", () => { project: { ...projectPayload, projectCustomName: null, - projectGroupKey: null, + projectKey: null, }, }, ]); diff --git a/packages/app/src/projects/host-project-model.ts b/packages/app/src/projects/host-project-model.ts index 372159460..54055e70a 100644 --- a/packages/app/src/projects/host-project-model.ts +++ b/packages/app/src/projects/host-project-model.ts @@ -3,7 +3,7 @@ import type { WorkspaceStructureHostPlacement, WorkspaceStructureProject, } from "@/projects/workspace-structure"; -import { resolveProjectGroupKey } from "@/projects/project-group-key"; +import { resolveProjectKey } from "@/projects/project-key"; export interface HostProjectListItem { projectKey: string; @@ -75,10 +75,10 @@ export function hostProjectFromWorkspace(input: { if (!input.workspace) { return null; } - const projectKey = resolveProjectGroupKey({ + const projectKey = resolveProjectKey({ serverId: input.serverId, projectId: input.workspace.projectId.trim(), - projectGroupKey: input.workspace.projectGroupKey, + projectKey: input.workspace.projectKey, }); const iconWorkingDir = input.workspace.projectRootPath.trim(); if (!projectKey || !iconWorkingDir) { diff --git a/packages/app/src/projects/project-group-key.ts b/packages/app/src/projects/project-key.ts similarity index 69% rename from packages/app/src/projects/project-group-key.ts rename to packages/app/src/projects/project-key.ts index 0ef3d2f02..cc8b1e94d 100644 --- a/packages/app/src/projects/project-group-key.ts +++ b/packages/app/src/projects/project-key.ts @@ -1,10 +1,10 @@ -export function resolveProjectGroupKey(input: { +export function resolveProjectKey(input: { serverId: string; projectId: string; - projectGroupKey?: string | null; + projectKey?: string | null; }): string { - if (input.projectGroupKey) return input.projectGroupKey; - // COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove after 2027-01-28. + if (input.projectKey) return input.projectKey; + // COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove after 2027-01-28. // Older daemons used remote/path-shaped project IDs as their grouping key. New opaque IDs // must remain host-local when the new field is absent. return input.projectId.startsWith("prj_") ? frameHostProjectKey(input) : input.projectId; diff --git a/packages/app/src/projects/project-settings-target.ts b/packages/app/src/projects/project-settings-target.ts index c580a472a..dcdcbb53b 100644 --- a/packages/app/src/projects/project-settings-target.ts +++ b/packages/app/src/projects/project-settings-target.ts @@ -1,4 +1,4 @@ -import { frameHostProjectKey } from "./project-group-key"; +import { frameHostProjectKey } from "./project-key"; interface ProjectSettingsTarget { projectKey: string; diff --git a/packages/app/src/projects/workspace-structure.test.ts b/packages/app/src/projects/workspace-structure.test.ts index 80a41192f..1c64182eb 100644 --- a/packages/app/src/projects/workspace-structure.test.ts +++ b/packages/app/src/projects/workspace-structure.test.ts @@ -11,7 +11,7 @@ function workspace(input: { return { id: input.id, projectId: input.projectId ?? `project-${input.id}`, - projectGroupKey: "remote:github.com/acme/app", + projectKey: "remote:github.com/acme/app", projectDisplayName: input.projectName, projectCustomName: input.projectCustomName, projectRootPath: `/repo/${input.id}`, diff --git a/packages/app/src/projects/workspace-structure.ts b/packages/app/src/projects/workspace-structure.ts index 0023c6c5a..fdd8eee0e 100644 --- a/packages/app/src/projects/workspace-structure.ts +++ b/packages/app/src/projects/workspace-structure.ts @@ -1,6 +1,6 @@ import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store"; import { projectDisplayNameFromProjectId } from "@/utils/project-display-name"; -import { frameHostProjectKey, resolveProjectGroupKey } from "@/projects/project-group-key"; +import { frameHostProjectKey, resolveProjectKey } from "@/projects/project-key"; export interface WorkspaceStructureHostPlacement { serverId: string; @@ -65,23 +65,21 @@ interface MaterializedWorkspaceStructureSession { emptyProjects: EmptyProjectDescriptor[]; } -function findAmbiguousProjectGroupKeys( - sessions: MaterializedWorkspaceStructureSession[], -): Set { +function findAmbiguousProjectKeys(sessions: MaterializedWorkspaceStructureSession[]): Set { const projectIdsByHostByGroupKey = new Map>>(); for (const session of sessions) { const projects = [ ...session.emptyProjects.map((project) => ({ projectId: project.projectId, - projectGroupKey: project.projectGroupKey, + projectKey: project.projectKey, })), ...session.workspaces.map((workspace) => ({ projectId: workspace.projectId, - projectGroupKey: workspace.projectGroupKey, + projectKey: workspace.projectKey, })), ]; for (const project of projects) { - const groupKey = resolveProjectGroupKey({ serverId: session.serverId, ...project }); + const groupKey = resolveProjectKey({ serverId: session.serverId, ...project }); const byHost = projectIdsByHostByGroupKey.get(groupKey) ?? new Map(); const projectIds = byHost.get(session.serverId) ?? new Set(); projectIds.add(project.projectId); @@ -97,13 +95,13 @@ function findAmbiguousProjectGroupKeys( ); } -function resolveUnambiguousProjectGroupKey(input: { +function resolveUnambiguousProjectKey(input: { serverId: string; projectId: string; - projectGroupKey?: string | null; + projectKey?: string | null; ambiguousGroupKeys: ReadonlySet; }): string { - const groupKey = resolveProjectGroupKey(input); + const groupKey = resolveProjectKey(input); return input.ambiguousGroupKeys.has(groupKey) ? frameHostProjectKey(input) : groupKey; } @@ -115,7 +113,7 @@ export function buildWorkspaceStructureProjects(input: { workspaces: [...session.workspaces], emptyProjects: [...(session.emptyProjects ?? [])], })); - const ambiguousGroupKeys = findAmbiguousProjectGroupKeys(sessions); + const ambiguousGroupKeys = findAmbiguousProjectKeys(sessions); const byProject = new Map< string, { @@ -131,10 +129,10 @@ export function buildWorkspaceStructureProjects(input: { for (const session of sessions) { for (const emptyProject of session.emptyProjects) { - const projectKey = resolveUnambiguousProjectGroupKey({ + const projectKey = resolveUnambiguousProjectKey({ serverId: session.serverId, projectId: emptyProject.projectId, - projectGroupKey: emptyProject.projectGroupKey, + projectKey: emptyProject.projectKey, ambiguousGroupKeys, }); const placement = { @@ -169,10 +167,10 @@ export function buildWorkspaceStructureProjects(input: { } for (const workspace of session.workspaces) { - const projectKey = resolveUnambiguousProjectGroupKey({ + const projectKey = resolveUnambiguousProjectKey({ serverId: session.serverId, projectId: workspace.projectId, - projectGroupKey: workspace.projectGroupKey, + projectKey: workspace.projectKey, ambiguousGroupKeys, }); const existing = byProject.get(projectKey); diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.test.ts b/packages/app/src/runtime/directory-sync/workspace-replica.test.ts index 348c1fddf..8ebc86132 100644 --- a/packages/app/src/runtime/directory-sync/workspace-replica.test.ts +++ b/packages/app/src/runtime/directory-sync/workspace-replica.test.ts @@ -103,7 +103,7 @@ it("commits the authoritative snapshot before buffered project updates", () => { kind: "upsert", project: { projectId: "attached", - projectGroupKey: "remote:github.com/acme/attached", + projectKey: "remote:github.com/acme/attached", projectDisplayName: "Renamed attached project", projectCustomName: "Personal name", projectRootPath: "/moved/attached", @@ -125,14 +125,14 @@ it("commits the authoritative snapshot before buffered project updates", () => { const session = useSessionStore.getState().sessions[serverId]; expect(session?.workspaces.get(attachedMain.id)).toMatchObject({ - projectGroupKey: "remote:github.com/acme/attached", + projectKey: "remote:github.com/acme/attached", projectDisplayName: "Renamed attached project", projectCustomName: "Personal name", projectRootPath: "/moved/attached", projectKind: "directory", }); expect(session?.workspaces.get(attachedFeature.id)).toMatchObject({ - projectGroupKey: "remote:github.com/acme/attached", + projectKey: "remote:github.com/acme/attached", projectDisplayName: "Renamed attached project", projectRootPath: "/moved/attached", }); diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.ts b/packages/app/src/runtime/directory-sync/workspace-replica.ts index 2c63a93bc..20a3deb61 100644 --- a/packages/app/src/runtime/directory-sync/workspace-replica.ts +++ b/packages/app/src/runtime/directory-sync/workspace-replica.ts @@ -42,7 +42,7 @@ function applyProjectDelta( hasAttachedWorkspace = true; snapshot.workspaces.set(workspaceId, { ...workspace, - projectGroupKey: project.projectGroupKey, + projectKey: project.projectKey, projectDisplayName: project.projectDisplayName, projectCustomName: project.projectCustomName, projectRootPath: project.projectRootPath, diff --git a/packages/app/src/runtime/replica-cache/index.ts b/packages/app/src/runtime/replica-cache/index.ts index 66298b839..6ada34642 100644 --- a/packages/app/src/runtime/replica-cache/index.ts +++ b/packages/app/src/runtime/replica-cache/index.ts @@ -193,7 +193,7 @@ function serializeWorkspace(workspace: WorkspaceDescriptor): WorkspaceDescriptor return { id: workspace.id, projectId: workspace.projectId, - ...(workspace.projectGroupKey ? { projectGroupKey: workspace.projectGroupKey } : {}), + ...(workspace.projectKey ? { projectKey: workspace.projectKey } : {}), projectDisplayName: workspace.projectDisplayName, projectCustomName: workspace.projectCustomName ?? null, projectRootPath: workspace.projectRootPath, diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index e72381878..17e58da35 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -138,7 +138,7 @@ export interface Agent { export interface WorkspaceDescriptor { id: string; projectId: string; - projectGroupKey?: string | null; + projectKey?: string | null; projectDisplayName: string; projectCustomName?: string | null; projectRootPath: string; @@ -170,7 +170,7 @@ export function normalizeWorkspaceDescriptor( return { id: normalizeWorkspaceOpaqueId(payload.id) ?? payload.id, projectId: payload.projectId, - projectGroupKey: payload.projectGroupKey ?? payload.project?.projectGroupKey ?? null, + projectKey: payload.projectKey ?? payload.project?.projectKey ?? null, projectDisplayName: payload.projectDisplayName, projectCustomName: payload.projectCustomName ?? null, projectRootPath: payload.projectRootPath, @@ -197,7 +197,7 @@ export function normalizeWorkspaceDescriptor( export interface EmptyProjectDescriptor { projectId: string; - projectGroupKey?: string | null; + projectKey?: string | null; projectDisplayName: string; projectCustomName: string | null; projectRootPath: string; @@ -209,7 +209,7 @@ export function normalizeEmptyProjectDescriptor( ): EmptyProjectDescriptor { return { projectId: payload.projectId, - projectGroupKey: payload.projectGroupKey ?? null, + projectKey: payload.projectKey ?? null, projectDisplayName: payload.projectDisplayName, projectCustomName: payload.projectCustomName ?? null, projectRootPath: payload.projectRootPath, @@ -257,7 +257,7 @@ function emptyProjectDescriptorFromWorkspace( ): EmptyProjectDescriptor { return { projectId: workspace.projectId, - ...(workspace.projectGroupKey ? { projectGroupKey: workspace.projectGroupKey } : {}), + ...(workspace.projectKey ? { projectKey: workspace.projectKey } : {}), projectDisplayName: workspace.projectDisplayName, projectCustomName: workspace.projectCustomName ?? null, projectRootPath: workspace.projectRootPath, diff --git a/packages/app/src/utils/projects.test.ts b/packages/app/src/utils/projects.test.ts index e72859aea..a63a7d9ce 100644 --- a/packages/app/src/utils/projects.test.ts +++ b/packages/app/src/utils/projects.test.ts @@ -30,7 +30,7 @@ function workspace(input: { repoRoot: string; project?: ProjectPlacementPayload; projectId?: string; - projectGroupKey?: string; + projectKey?: string; projectCustomName?: string | null; projectName?: string; remoteUrl?: string | null; @@ -40,7 +40,7 @@ function workspace(input: { return { id: input.id, projectId: input.projectId ?? input.project?.projectKey ?? input.repoRoot, - projectGroupKey: input.projectGroupKey, + projectKey: input.projectKey, projectDisplayName: input.projectName ?? input.project?.projectName ?? "Project", projectCustomName: input.projectCustomName, projectRootPath: input.repoRoot, @@ -68,25 +68,21 @@ function workspace(input: { } describe("buildProjects", () => { - it("groups distinct host-local project IDs by their persisted group key", () => { - const projectGroupKey = "remote:github.com/acme/app"; + it("groups distinct host-local project IDs by their persisted project key", () => { + const projectKey = "remote:github.com/acme/app"; const result = buildProjects({ hosts: [ { serverId: "desktop", serverName: "Desktop", isOnline: true, - workspaces: [ - workspace({ id: "main-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }), - ], + workspaces: [workspace({ id: "main-a", repoRoot: "/a", projectId: "prj_a", projectKey })], }, { serverId: "laptop", serverName: "Laptop", isOnline: true, - workspaces: [ - workspace({ id: "main-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }), - ], + workspaces: [workspace({ id: "main-b", repoRoot: "/b", projectId: "prj_b", projectKey })], }, ], }); @@ -98,8 +94,8 @@ describe("buildProjects", () => { ]); }); - it("keeps independent same-host clones separate when their group key is ambiguous", () => { - const projectGroupKey = "remote:github.com/acme/app"; + it("keeps independent same-host clones separate when their project key is ambiguous", () => { + const projectKey = "remote:github.com/acme/app"; const result = buildProjects({ hosts: [ { @@ -107,8 +103,8 @@ describe("buildProjects", () => { serverName: "Desktop", isOnline: true, workspaces: [ - workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }), - workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }), + workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectKey }), + workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectKey }), ], }, ], @@ -122,7 +118,7 @@ describe("buildProjects", () => { }); it("uses one ambiguity scope across every host", () => { - const projectGroupKey = "remote:github.com/acme/app"; + const projectKey = "remote:github.com/acme/app"; const result = buildProjects({ hosts: [ { @@ -130,8 +126,8 @@ describe("buildProjects", () => { serverName: "Desktop", isOnline: true, workspaces: [ - workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }), - workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }), + workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectKey }), + workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectKey }), ], }, { @@ -139,21 +135,21 @@ describe("buildProjects", () => { serverName: "Laptop", isOnline: true, workspaces: [ - workspace({ id: "clone-c", repoRoot: "/c", projectId: "prj_c", projectGroupKey }), + workspace({ id: "clone-c", repoRoot: "/c", projectId: "prj_c", projectKey }), ], }, ], }); expect(result.projects.map((project) => project.projectKey).sort()).toEqual([ - "host:desktop:project:prj_a", - "host:desktop:project:prj_b", - "host:laptop:project:prj_c", + "host:6:laptop:project:5:prj_c", + "host:7:desktop:project:5:prj_a", + "host:7:desktop:project:5:prj_b", ]); }); it("keeps host-local metadata when the grouping key differs from the project ID", () => { - const projectGroupKey = "remote:github.com/acme/app"; + const projectKey = "remote:github.com/acme/app"; const result = buildProjects({ hosts: [ { @@ -165,7 +161,7 @@ describe("buildProjects", () => { id: "main", repoRoot: "/repo/app", projectId: "prj_app", - projectGroupKey, + projectKey, projectName: "acme/app", projectCustomName: "My App", }), @@ -175,14 +171,14 @@ describe("buildProjects", () => { }); expect(result.projects[0]).toMatchObject({ - projectKey: projectGroupKey, + projectKey: projectKey, projectName: "acme/app", projectCustomName: "My App", }); }); it("keeps custom-name state on the host that owns it", () => { - const projectGroupKey = "remote:github.com/acme/app"; + const projectKey = "remote:github.com/acme/app"; const result = buildProjects({ hosts: [ { @@ -194,7 +190,7 @@ describe("buildProjects", () => { id: "desktop-main", repoRoot: "/desktop/app", projectId: "prj_desktop", - projectGroupKey, + projectKey, projectName: "My App", projectCustomName: "My App", }), @@ -209,7 +205,7 @@ describe("buildProjects", () => { id: "laptop-main", repoRoot: "/laptop/app", projectId: "prj_laptop", - projectGroupKey, + projectKey, projectName: "acme/app", projectCustomName: null, }), @@ -235,7 +231,7 @@ describe("buildProjects", () => { emptyProjects: [ { projectId: "prj_app", - projectGroupKey: "remote:github.com/acme/app", + projectKey: "remote:github.com/acme/app", projectDisplayName: "My Empty Project", projectCustomName: "My Empty Project", projectRootPath: "/repo/app", diff --git a/packages/app/src/utils/projects.ts b/packages/app/src/utils/projects.ts index 461f5a37b..64e1f7a51 100644 --- a/packages/app/src/utils/projects.ts +++ b/packages/app/src/utils/projects.ts @@ -1,7 +1,7 @@ import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store"; import { buildHostProjectList, type HostProjectListItem } from "@/projects/host-project-model"; import { buildWorkspaceStructureProjects } from "@/projects/workspace-structure"; -import { resolveProjectGroupKey } from "@/projects/project-group-key"; +import { resolveProjectKey } from "@/projects/project-key"; export interface WorkspaceSummary { id: string; @@ -238,10 +238,10 @@ function attachHostWorkspaces( for (const workspace of host.workspaces) { const key = projectKeyByProjectId.get(workspace.projectId) ?? - resolveProjectGroupKey({ + resolveProjectKey({ serverId: host.serverId, projectId: workspace.projectId, - projectGroupKey: workspace.projectGroupKey, + projectKey: workspace.projectKey, }); groups.get(key)?.hostsByServerId.get(host.serverId)?.workspaces.push(workspace); } diff --git a/packages/app/src/workspace/legacy-daemon-workspaces.ts b/packages/app/src/workspace/legacy-daemon-workspaces.ts index f6040a6f2..f75b238bb 100644 --- a/packages/app/src/workspace/legacy-daemon-workspaces.ts +++ b/packages/app/src/workspace/legacy-daemon-workspaces.ts @@ -299,7 +299,7 @@ function createLegacyWorkspace( return { id: workspaceDirectory, projectId: entry.project.projectKey, - projectGroupKey: entry.project.projectGroupKey ?? null, + projectKey: entry.project.projectKey ?? null, projectDisplayName: entry.project.projectName, projectCustomName: null, projectRootPath, diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 4a2cbb3e6..7280f240e 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -3001,8 +3001,6 @@ export const ProjectCheckoutLitePayloadSchema = z.union([ export const ProjectPlacementPayloadSchema = z.object({ projectKey: z.string(), - // COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28. - projectGroupKey: z.string().optional(), projectName: z.string(), workspaceName: z.string().nullable().optional(), checkout: ProjectCheckoutLitePayloadSchema, @@ -3092,8 +3090,8 @@ export const WorkspaceDescriptorPayloadSchema = z .object({ id: z.string(), projectId: z.string(), - // COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28. - projectGroupKey: z.string().optional(), + // COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28. + projectKey: z.string().optional(), projectDisplayName: z.string(), // COMPAT(projectCustomName): added in v0.1.76, drop the optional gate when floor >= v0.1.76. // When the user has renamed a project, projectDisplayName carries the resolved @@ -3234,8 +3232,8 @@ export const FetchRecentProviderSessionsResponseMessageSchema = z.object({ // workspace is archived. export const WorkspaceProjectDescriptorPayloadSchema = z.object({ projectId: z.string(), - // COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28. - projectGroupKey: z.string().optional(), + // COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28. + projectKey: z.string().optional(), projectDisplayName: z.string(), projectCustomName: z.string().nullable().optional(), projectRootPath: z.string(), diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts index e20fd7467..0f40c61fd 100644 --- a/packages/server/src/server/paseo-worktree-service.test.ts +++ b/packages/server/src/server/paseo-worktree-service.test.ts @@ -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), }); }); diff --git a/packages/server/src/server/project-group-key.test.ts b/packages/server/src/server/project-key.test.ts similarity index 90% rename from packages/server/src/server/project-group-key.test.ts rename to packages/server/src/server/project-key.test.ts index 39c2af157..8b67608fa 100644 --- a/packages/server/src/server/project-group-key.test.ts +++ b/packages/server/src/server/project-key.test.ts @@ -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, diff --git a/packages/server/src/server/project-group-key.ts b/packages/server/src/server/project-key.ts similarity index 97% rename from packages/server/src/server/project-group-key.ts rename to packages/server/src/server/project-key.ts index 25a168097..a4f6a3eae 100644 --- a/packages/server/src/server/project-group-key.ts +++ b/packages/server/src/server/project-key.ts @@ -23,14 +23,14 @@ const DEFAULT_REMOTE_PORTS: Partial> = { * 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; diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index bfc0a2c07..29cb619b0 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -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([ diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 0132d37f9..56c939712 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -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, diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index de02b8b7e..e763c0899 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -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, }); }); diff --git a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts index fcc99f153..8c08093f2 100644 --- a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts +++ b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.test.ts @@ -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, }); }); diff --git a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts index d6e79ae9a..61a25b96b 100644 --- a/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts +++ b/packages/server/src/server/session/workspace-provisioning/workspace-provisioning-service.ts @@ -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); diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index fcb564080..523628999 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -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, diff --git a/packages/server/src/server/workspace-directory.ts b/packages/server/src/server/workspace-directory.ts index 757f06fc6..acb18d692 100644 --- a/packages/server/src/server/workspace-directory.ts +++ b/packages/server/src/server/workspace-directory.ts @@ -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, diff --git a/packages/server/src/server/workspace-reconciliation-service.test.ts b/packages/server/src/server/workspace-reconciliation-service.test.ts index c0ad811ec..6dea69b6a 100644 --- a/packages/server/src/server/workspace-reconciliation-service.test.ts +++ b/packages/server/src/server/workspace-reconciliation-service.test.ts @@ -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", diff --git a/packages/server/src/server/workspace-reconciliation-service.ts b/packages/server/src/server/workspace-reconciliation-service.ts index 49dc1e4d8..74cb438de 100644 --- a/packages/server/src/server/workspace-reconciliation-service.ts +++ b/packages/server/src/server/workspace-reconciliation-service.ts @@ -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>; + fields: Partial>; } | { kind: "workspace_updated"; @@ -343,9 +343,9 @@ export class WorkspaceReconciliationService { checkout: await readCheckout(workspace.cwd), })), ); - const projectUpdates: Partial> = {}; + const projectUpdates: Partial> = {}; 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) { diff --git a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts index 0bc326f0e..c290032c1 100644 --- a/packages/server/src/server/workspace-registry-bootstrap-legacy.ts +++ b/packages/server/src/server/workspace-registry-bootstrap-legacy.ts @@ -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, diff --git a/packages/server/src/server/workspace-registry.ts b/packages/server/src/server/workspace-registry.ts index d0eac78a6..fe2804812 100644 --- a/packages/server/src/server/workspace-registry.ts +++ b/packages/server/src/server/workspace-registry.ts @@ -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; upsert(record: PersistedProjectRecord): Promise; @@ -315,7 +315,7 @@ export class FileBackedProjectRegistry rootPath: string; kind: PersistedProjectKind; displayName: string; - projectGroupKey?: string; + projectKey?: string; timestamp: string; }): Promise { 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, }); }