@deepseek-ai/dsh-workspace 0.1.2-alpha.5 → 0.1.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/workspace/workspace/README.md
5
- README.md: 49ca861742fe9ea5f36edbaeb8d9700c475b1d50
6
- README.zh.md: 707044970b716577d2f1f95e558c341e217985c4
5
+ README.md: b9512b7f0c52760d7d51305dda24151e8464947d
6
+ README.zh.md: d578182c1fbff2ca2131cd1970090c3ece9b0808
package/README.md CHANGED
@@ -50,7 +50,7 @@ With these rows mounted, creating a project shows up in the list immediately and
50
50
 
51
51
  ### Creating and ordering projects
52
52
 
53
- Create a project from any directory that exists: give its path and an optional title, and the project appears in the list, newest first. A path that does not exist, or a file instead of a directory, is rejected and nothing changes; creating a project for a directory that already has one returns the existing project unchanged. Rename a project at any time, and move it to any position in the list:
53
+ Create a project from any fully qualified directory that exists: filesystem roots such as `C:\` and ordinary directories are valid. Relative paths, Windows drive-relative paths such as `C:work`, missing paths, and files are rejected without creating a project; creating a project for a directory that already has one returns the existing project unchanged. Rename a project at any time, and move it to any position in the list:
54
54
 
55
55
  ```text
56
56
  // Host consumer code, after the composition above is loaded:
@@ -128,7 +128,7 @@ Read these pages when this package's view is not enough: the subsystem reference
128
128
  - [Workspace subsystem](../../../docs/subsystems/workspace.md) — the feature contract for projects and their sessions, and the generated API for the workspace service.
129
129
  - [Workspace package map](../README.md) — the group's single package and its repository position.
130
130
  - [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md) — why project records use the domain data form.
131
- - [Workspace UI product-flow Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md) — how the first start builds projects from session history and how the GUI orders them.
131
+ - [Workspace UI product-flow Agent Note](../../../.agents/notes/archived/feature/2026-07-25-workspace-ui-product-flow.md) — how the first start builds projects from session history and how the GUI orders them.
132
132
  - [Workspace registration deletion decision](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md) — why removing a project never deletes its folder or sessions.
133
133
 
134
134
  -----
@@ -173,6 +173,6 @@ This Dev Note is working context for maintainers: open questions and directions
173
173
 
174
174
  #### Open: the `create(path, title?)` title parameter
175
175
 
176
- The `title` parameter has no production caller since the gateway's create-by-name branch was removed; a code TODO proposes dropping the parameter and its `@param` clause together ([note](../../../.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md)).
176
+ The `title` parameter has no production caller since the gateway's create-by-name branch was removed; a code TODO proposes dropping the parameter and its `@param` clause together ([note](../../../.agents/notes/archived/simplification/2026-07-31-one-route-to-add-a-workspace.md)).
177
177
 
178
178
  </details>
package/README.zh.md CHANGED
@@ -50,7 +50,7 @@ kind: "package-reference"
50
50
 
51
51
  ### 创建与排序项目
52
52
 
53
- 从任何存在的目录创建项目:给出路径和可选标题,项目即出现在列表中,新到旧排列。不存在的路径或文件而非目录会被拒绝,且不会有任何变化;为已有项目的目录再次创建会原样返回现有项目。你可以随时重命名项目,并把它移动到列表中的任意位置:
53
+ 从任何存在且完整限定的目录创建项目:`C:\` 等文件系统根目录和普通目录都有效。相对路径、`C:work` 等 Windows 盘符相对路径、不存在的路径和文件都会被拒绝,且不会创建项目;为已有项目的目录再次创建会原样返回现有项目。你可以随时重命名项目,并把它移动到列表中的任意位置:
54
54
 
55
55
  ```text
56
56
  // Host consumer code, after the composition above is loaded:
@@ -128,7 +128,7 @@ ctx.workspaceRegistry.list() // shows the project, newest first
128
128
  - [Workspace 子系统](../../../docs/subsystems/workspace.zh.md)——项目及其会话的功能约定,以及 workspace 服务的生成 API。
129
129
  - [Workspace 包映射](../README.zh.md)——本组唯一的包及其仓库位置。
130
130
  - [领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)——为什么项目记录使用领域数据形式。
131
- - [Workspace UI 产品流 Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md)——首次启动如何从会话历史构建项目,以及 GUI 如何排序。
131
+ - [Workspace UI 产品流 Agent Note](../../../.agents/notes/archived/feature/2026-07-25-workspace-ui-product-flow.md)——首次启动如何从会话历史构建项目,以及 GUI 如何排序。
132
132
  - [删除 Workspace 注册记录决策](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.zh.md)——为什么移除项目绝不会删除其文件夹或会话。
133
133
 
134
134
  -----
@@ -173,6 +173,6 @@ ctx.workspaceRegistry.list() // shows the project, newest first
173
173
 
174
174
  #### 开放:`create(path, title?)` 的 title 参数
175
175
 
176
- 网关的按名称创建分支移除后,`title` 参数已无生产调用方;代码中的 TODO 提议把该参数与其 `@param` 子句一并移除(参见[笔记](../../../.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md))。
176
+ 网关的按名称创建分支移除后,`title` 参数已无生产调用方;代码中的 TODO 提议把该参数与其 `@param` 子句一并移除(参见[笔记](../../../.agents/notes/archived/simplification/2026-07-31-one-route-to-add-a-workspace.md))。
177
177
 
178
178
  </details>
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { realpath, stat } from "node:fs/promises";
3
- import { basename } from "node:path";
4
3
  import { Service } from "@deepseek-ai/cordis";
4
+ import { posix, win32 } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { brandString } from "@deepseek-ai/dsh-brand";
7
7
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
@@ -11,18 +11,42 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
11
11
  * @module @deepseek-ai/dsh-workspace/src/paths
12
12
  */
13
13
  /**
14
- * Canonicalize a directory path via `fs.realpath`: trailing slashes, `..`
15
- * segments, and symlinks are all resolved. This is the ONE uniqueness canon of
16
- * the package workspace paths are stored canonicalized, uniqueness is
17
- * string equality of canonicalized paths (a symlink to an existing
18
- * workspace's directory collides), and attach-time session `cwd` checks go
19
- * through the same canon. A path that does not exist rejects with the
20
- * original `ENOENT` — this is `create`'s reject path (a workspace must point
21
- * at an existing directory).
14
+ * Check whether a path names one fixed Host location without process cwd or
15
+ * current-drive resolution.
16
+ * @param path - Candidate Workspace path.
17
+ * @param platform - Host platform; injectable for deterministic path tests.
18
+ * @returns Whether the path is fully qualified on that platform.
19
+ */
20
+ function fullyQualifiedWorkspacePath(path, platform = process.platform) {
21
+ if (platform !== "win32") return posix.isAbsolute(path);
22
+ const root = win32.parse(path).root;
23
+ return win32.isAbsolute(path) && root !== "\\" && root !== "/";
24
+ }
25
+ /**
26
+ * Derive a non-empty default title from a canonical Workspace path.
27
+ * @param path - Canonical Workspace path.
28
+ * @param platform - Host platform; injectable for deterministic path tests.
29
+ * @returns The final segment when present, otherwise the complete root spelling.
30
+ */
31
+ function defaultWorkspaceTitle(path, platform = process.platform) {
32
+ const pathApi = platform === "win32" ? win32 : posix;
33
+ return pathApi.basename(path) || pathApi.parse(path).root;
34
+ }
35
+ /**
36
+ * Canonicalize a fully qualified directory path via `fs.realpath`: trailing
37
+ * slashes, `..` segments, and symlinks are all resolved. This is the ONE
38
+ * uniqueness canon of the package — workspace paths are stored canonicalized,
39
+ * uniqueness is string equality of canonicalized paths (a symlink to an
40
+ * existing workspace's directory collides), and attach-time session `cwd`
41
+ * checks go through the same canon. Relative paths reject before `realpath` can
42
+ * resolve them from the Host cwd or current Windows drive. A path that does not
43
+ * exist rejects with the original `ENOENT` — this is `create`'s reject path (a
44
+ * workspace must point at an existing directory).
22
45
  * @param path - The path to canonicalize.
23
46
  * @returns the canonical absolute path.
24
47
  */
25
48
  async function realpathNormalize(path) {
49
+ if (!fullyQualifiedWorkspacePath(path)) throw new TypeError(`Workspace path is not fully qualified: '${path}'`);
26
50
  return await realpath(path);
27
51
  }
28
52
  //#endregion
@@ -318,23 +342,23 @@ var WorkspaceRegistry = class extends Service {
318
342
  await this.recoverPendingMutation();
319
343
  this.validateStoredState(this.state);
320
344
  if (!this.state.initialized) {
321
- const headers = await this.ctx.sessionPersistence.list();
345
+ const headers = await this.listStoredHeaders();
322
346
  await this.replaceHeaderIndex(headers);
323
347
  await this.bootstrap(headers);
324
- } else if (this.table.size > 0) await this.replaceHeaderIndex(await this.ctx.sessionPersistence.list());
348
+ } else if (this.table.size > 0) await this.replaceHeaderIndex(await this.listStoredHeaders());
325
349
  await this.indexLiveSessions();
326
350
  this.validateStoredState(this.requireState());
327
351
  this.rebuildEntities();
328
352
  this.reportFilteredCandidates();
329
353
  }
330
354
  /**
331
- * Create or reuse a workspace for an existing directory. The path is
332
- * canonicalized through `fs.realpath`; a nonexistent path rejects with the
333
- * original error and a non-directory rejects. Repeated calls for the same
334
- * canonical path return the existing entity without changing its title.
355
+ * Create or reuse a workspace for an existing directory. The fully qualified
356
+ * path is canonicalized through `fs.realpath`; a relative, nonexistent, or
357
+ * non-directory path rejects. Repeated calls for the same canonical path
358
+ * return the existing entity without changing its title.
335
359
  * A newly created workspace is prepended to the durable registry order.
336
360
  * Different canonical paths may share a display title.
337
- * @param path - Existing directory to own, in any path spelling.
361
+ * @param path - Existing directory to own, in a fully qualified path spelling.
338
362
  * @param title - Display title used only when a new record is created.
339
363
  * @returns the existing or newly durable workspace.
340
364
  */
@@ -439,14 +463,14 @@ var WorkspaceRegistry = class extends Service {
439
463
  async sessionKnown(id) {
440
464
  if (this.ctx.get("sessions")?.get(id) !== void 0) return true;
441
465
  if (this.headers.has(id)) return true;
442
- await this.indexHeaders(await this.ctx.sessionPersistence.list());
466
+ await this.indexHeaders(await this.listStoredHeaders());
443
467
  return this.headers.has(id);
444
468
  }
445
469
  /**
446
470
  * Resolve by canonical directory path without creating or mutating a
447
471
  * workspace. A missing path rejects during `realpath`; an existing unowned
448
472
  * directory returns `undefined`.
449
- * @param path - Existing directory path in any spelling.
473
+ * @param path - Existing directory path in a fully qualified spelling.
450
474
  * @returns the workspace owning the canonical path, when one exists.
451
475
  */
452
476
  async resolveByPath(path) {
@@ -455,7 +479,7 @@ var WorkspaceRegistry = class extends Service {
455
479
  }
456
480
  async createCanonical(canonical, title) {
457
481
  for (const entity of this.entities.values()) if (entity.path === canonical) return entity;
458
- const workspaceName = title ?? basename(canonical);
482
+ const workspaceName = title ?? defaultWorkspaceTitle(canonical);
459
483
  const table = this.requireTable();
460
484
  const state = this.requireState();
461
485
  const id = WorkspaceId(randomUUID());
@@ -602,7 +626,7 @@ var WorkspaceRegistry = class extends Service {
602
626
  const createdAt = new Date(group.newestAt).toISOString();
603
627
  const record = {
604
628
  path: group.path,
605
- title: basename(group.path),
629
+ title: defaultWorkspaceTitle(group.path),
606
630
  sessionIds,
607
631
  createdAt,
608
632
  updatedAt: createdAt
@@ -701,6 +725,10 @@ var WorkspaceRegistry = class extends Service {
701
725
  this.invalidSessionPaths.set(header.id, `cwd '${header.cwd}' does not resolve`);
702
726
  }
703
727
  }
728
+ /** Every stored session's header, projected from the persistence snapshot listing. */
729
+ async listStoredHeaders() {
730
+ return (await this.ctx.sessionPersistence.list()).map((snapshot) => snapshot.header);
731
+ }
704
732
  async indexLiveSessions() {
705
733
  const sessions = this.ctx.get("sessions");
706
734
  if (sessions === void 0) return;
@@ -725,7 +753,7 @@ var WorkspaceRegistry = class extends Service {
725
753
  }
726
754
  const cached = this.headers.get(id);
727
755
  if (cached !== void 0) return cached;
728
- const headers = await this.ctx.sessionPersistence.list();
756
+ const headers = await this.listStoredHeaders();
729
757
  await this.indexHeaders(headers);
730
758
  const header = this.headers.get(id);
731
759
  if (header === void 0) throw new Error(`cannot validate session '${id}': session persistence holds no such session`);
package/lib/invariant.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import "node:crypto";
2
2
  import "node:fs/promises";
3
- import "node:path";
4
3
  import { Service } from "@deepseek-ai/cordis";
4
+ import "node:path";
5
5
  import { z } from "zod";
6
6
  import { brandString } from "@deepseek-ai/dsh-brand";
7
7
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
@@ -66,13 +66,13 @@ export declare class WorkspaceRegistry extends Service {
66
66
  /** Open the domain, finish bootstrap when required, and rebuild the ordered cache. */
67
67
  protected [Service.init](): Promise<void>;
68
68
  /**
69
- * Create or reuse a workspace for an existing directory. The path is
70
- * canonicalized through `fs.realpath`; a nonexistent path rejects with the
71
- * original error and a non-directory rejects. Repeated calls for the same
72
- * canonical path return the existing entity without changing its title.
69
+ * Create or reuse a workspace for an existing directory. The fully qualified
70
+ * path is canonicalized through `fs.realpath`; a relative, nonexistent, or
71
+ * non-directory path rejects. Repeated calls for the same canonical path
72
+ * return the existing entity without changing its title.
73
73
  * A newly created workspace is prepended to the durable registry order.
74
74
  * Different canonical paths may share a display title.
75
- * @param path - Existing directory to own, in any path spelling.
75
+ * @param path - Existing directory to own, in a fully qualified path spelling.
76
76
  * @param title - Display title used only when a new record is created.
77
77
  * @returns the existing or newly durable workspace.
78
78
  */
@@ -133,7 +133,7 @@ export declare class WorkspaceRegistry extends Service {
133
133
  * Resolve by canonical directory path without creating or mutating a
134
134
  * workspace. A missing path rejects during `realpath`; an existing unowned
135
135
  * directory returns `undefined`.
136
- * @param path - Existing directory path in any spelling.
136
+ * @param path - Existing directory path in a fully qualified spelling.
137
137
  * @returns the workspace owning the canonical path, when one exists.
138
138
  */
139
139
  resolveByPath(path: string): Promise<Workspace | undefined>;
@@ -151,6 +151,8 @@ export declare class WorkspaceRegistry extends Service {
151
151
  private replaceHeaderIndex;
152
152
  private indexHeaders;
153
153
  private indexHeader;
154
+ /** Every stored session's header, projected from the persistence snapshot listing. */
155
+ private listStoredHeaders;
154
156
  private indexLiveSessions;
155
157
  private reportFilteredCandidates;
156
158
  private readSessionHeader;
@@ -6,11 +6,10 @@
6
6
  */
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { stat } from 'node:fs/promises';
9
- import { basename } from 'node:path';
10
9
  import { Service } from '@deepseek-ai/cordis';
11
10
  import { WorkspaceEntity } from "./entity.js";
12
11
  export { WorkspaceMoveInvalidError } from "./entity.js";
13
- import { realpathNormalize } from "./paths.js";
12
+ import { defaultWorkspaceTitle, realpathNormalize } from "./paths.js";
14
13
  import { workspaceDomainSpec } from "./spec.js";
15
14
  export { workspaceDomainState, workspaceRecord, workspaceDomainSpec } from "./spec.js";
16
15
  export { realpathNormalize } from "./paths.js";
@@ -90,12 +89,12 @@ export class WorkspaceRegistry extends Service {
90
89
  await this.recoverPendingMutation();
91
90
  this.validateStoredState(this.state);
92
91
  if (!this.state.initialized) {
93
- const headers = await this.ctx.sessionPersistence.list();
92
+ const headers = await this.listStoredHeaders();
94
93
  await this.replaceHeaderIndex(headers);
95
94
  await this.bootstrap(headers);
96
95
  }
97
96
  else if (this.table.size > 0) {
98
- await this.replaceHeaderIndex(await this.ctx.sessionPersistence.list());
97
+ await this.replaceHeaderIndex(await this.listStoredHeaders());
99
98
  }
100
99
  await this.indexLiveSessions();
101
100
  this.validateStoredState(this.requireState());
@@ -103,19 +102,19 @@ export class WorkspaceRegistry extends Service {
103
102
  this.reportFilteredCandidates();
104
103
  }
105
104
  /**
106
- * Create or reuse a workspace for an existing directory. The path is
107
- * canonicalized through `fs.realpath`; a nonexistent path rejects with the
108
- * original error and a non-directory rejects. Repeated calls for the same
109
- * canonical path return the existing entity without changing its title.
105
+ * Create or reuse a workspace for an existing directory. The fully qualified
106
+ * path is canonicalized through `fs.realpath`; a relative, nonexistent, or
107
+ * non-directory path rejects. Repeated calls for the same canonical path
108
+ * return the existing entity without changing its title.
110
109
  * A newly created workspace is prepended to the durable registry order.
111
110
  * Different canonical paths may share a display title.
112
- * @param path - Existing directory to own, in any path spelling.
111
+ * @param path - Existing directory to own, in a fully qualified path spelling.
113
112
  * @param title - Display title used only when a new record is created.
114
113
  * @returns the existing or newly durable workspace.
115
114
  */
116
115
  // TODO: `title` lost its last production caller when the gateway's
117
116
  // create-by-name branch was deleted
118
- // (.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md);
117
+ // (.agents/notes/archived/simplification/2026-07-31-one-route-to-add-a-workspace.md);
119
118
  // drop the parameter with its @param clause and the `create(path, title?)`
120
119
  // lines in this package's README pair.
121
120
  async create(path, title) {
@@ -225,14 +224,14 @@ export class WorkspaceRegistry extends Service {
225
224
  return true;
226
225
  if (this.headers.has(id))
227
226
  return true;
228
- await this.indexHeaders(await this.ctx.sessionPersistence.list());
227
+ await this.indexHeaders(await this.listStoredHeaders());
229
228
  return this.headers.has(id);
230
229
  }
231
230
  /**
232
231
  * Resolve by canonical directory path without creating or mutating a
233
232
  * workspace. A missing path rejects during `realpath`; an existing unowned
234
233
  * directory returns `undefined`.
235
- * @param path - Existing directory path in any spelling.
234
+ * @param path - Existing directory path in a fully qualified spelling.
236
235
  * @returns the workspace owning the canonical path, when one exists.
237
236
  */
238
237
  async resolveByPath(path) {
@@ -248,7 +247,7 @@ export class WorkspaceRegistry extends Service {
248
247
  if (entity.path === canonical)
249
248
  return entity;
250
249
  }
251
- const workspaceName = title ?? basename(canonical);
250
+ const workspaceName = title ?? defaultWorkspaceTitle(canonical);
252
251
  const table = this.requireTable();
253
252
  const state = this.requireState();
254
253
  const id = WorkspaceId(randomUUID());
@@ -413,7 +412,7 @@ export class WorkspaceRegistry extends Service {
413
412
  const createdAt = new Date(group.newestAt).toISOString();
414
413
  const record = {
415
414
  path: group.path,
416
- title: basename(group.path),
415
+ title: defaultWorkspaceTitle(group.path),
417
416
  sessionIds,
418
417
  createdAt,
419
418
  updatedAt: createdAt,
@@ -532,6 +531,11 @@ export class WorkspaceRegistry extends Service {
532
531
  this.invalidSessionPaths.set(header.id, `cwd '${header.cwd}' does not resolve`);
533
532
  }
534
533
  }
534
+ /** Every stored session's header, projected from the persistence snapshot listing. */
535
+ async listStoredHeaders() {
536
+ const snapshots = await this.ctx.sessionPersistence.list();
537
+ return snapshots.map(snapshot => snapshot.header);
538
+ }
535
539
  async indexLiveSessions() {
536
540
  const sessions = this.ctx.get('sessions');
537
541
  if (sessions === undefined)
@@ -562,7 +566,7 @@ export class WorkspaceRegistry extends Service {
562
566
  const cached = this.headers.get(id);
563
567
  if (cached !== undefined)
564
568
  return cached;
565
- const headers = await this.ctx.sessionPersistence.list();
569
+ const headers = await this.listStoredHeaders();
566
570
  await this.indexHeaders(headers);
567
571
  const header = this.headers.get(id);
568
572
  if (header === undefined) {
@@ -3,14 +3,30 @@
3
3
  * @module @deepseek-ai/dsh-workspace/src/paths
4
4
  */
5
5
  /**
6
- * Canonicalize a directory path via `fs.realpath`: trailing slashes, `..`
7
- * segments, and symlinks are all resolved. This is the ONE uniqueness canon of
8
- * the package workspace paths are stored canonicalized, uniqueness is
9
- * string equality of canonicalized paths (a symlink to an existing
10
- * workspace's directory collides), and attach-time session `cwd` checks go
11
- * through the same canon. A path that does not exist rejects with the
12
- * original `ENOENT` — this is `create`'s reject path (a workspace must point
13
- * at an existing directory).
6
+ * Check whether a path names one fixed Host location without process cwd or
7
+ * current-drive resolution.
8
+ * @param path - Candidate Workspace path.
9
+ * @param platform - Host platform; injectable for deterministic path tests.
10
+ * @returns Whether the path is fully qualified on that platform.
11
+ */
12
+ export declare function fullyQualifiedWorkspacePath(path: string, platform?: NodeJS.Platform): boolean;
13
+ /**
14
+ * Derive a non-empty default title from a canonical Workspace path.
15
+ * @param path - Canonical Workspace path.
16
+ * @param platform - Host platform; injectable for deterministic path tests.
17
+ * @returns The final segment when present, otherwise the complete root spelling.
18
+ */
19
+ export declare function defaultWorkspaceTitle(path: string, platform?: NodeJS.Platform): string;
20
+ /**
21
+ * Canonicalize a fully qualified directory path via `fs.realpath`: trailing
22
+ * slashes, `..` segments, and symlinks are all resolved. This is the ONE
23
+ * uniqueness canon of the package — workspace paths are stored canonicalized,
24
+ * uniqueness is string equality of canonicalized paths (a symlink to an
25
+ * existing workspace's directory collides), and attach-time session `cwd`
26
+ * checks go through the same canon. Relative paths reject before `realpath` can
27
+ * resolve them from the Host cwd or current Windows drive. A path that does not
28
+ * exist rejects with the original `ENOENT` — this is `create`'s reject path (a
29
+ * workspace must point at an existing directory).
14
30
  * @param path - The path to canonicalize.
15
31
  * @returns the canonical absolute path.
16
32
  */
@@ -3,19 +3,47 @@
3
3
  * @module @deepseek-ai/dsh-workspace/src/paths
4
4
  */
5
5
  import { realpath } from 'node:fs/promises';
6
+ import { posix, win32 } from 'node:path';
6
7
  /**
7
- * Canonicalize a directory path via `fs.realpath`: trailing slashes, `..`
8
- * segments, and symlinks are all resolved. This is the ONE uniqueness canon of
9
- * the package workspace paths are stored canonicalized, uniqueness is
10
- * string equality of canonicalized paths (a symlink to an existing
11
- * workspace's directory collides), and attach-time session `cwd` checks go
12
- * through the same canon. A path that does not exist rejects with the
13
- * original `ENOENT` — this is `create`'s reject path (a workspace must point
14
- * at an existing directory).
8
+ * Check whether a path names one fixed Host location without process cwd or
9
+ * current-drive resolution.
10
+ * @param path - Candidate Workspace path.
11
+ * @param platform - Host platform; injectable for deterministic path tests.
12
+ * @returns Whether the path is fully qualified on that platform.
13
+ */
14
+ export function fullyQualifiedWorkspacePath(path, platform = process.platform) {
15
+ if (platform !== 'win32')
16
+ return posix.isAbsolute(path);
17
+ const root = win32.parse(path).root;
18
+ return win32.isAbsolute(path) && root !== '\\' && root !== '/';
19
+ }
20
+ /**
21
+ * Derive a non-empty default title from a canonical Workspace path.
22
+ * @param path - Canonical Workspace path.
23
+ * @param platform - Host platform; injectable for deterministic path tests.
24
+ * @returns The final segment when present, otherwise the complete root spelling.
25
+ */
26
+ export function defaultWorkspaceTitle(path, platform = process.platform) {
27
+ const pathApi = platform === 'win32' ? win32 : posix;
28
+ return pathApi.basename(path) || pathApi.parse(path).root;
29
+ }
30
+ /**
31
+ * Canonicalize a fully qualified directory path via `fs.realpath`: trailing
32
+ * slashes, `..` segments, and symlinks are all resolved. This is the ONE
33
+ * uniqueness canon of the package — workspace paths are stored canonicalized,
34
+ * uniqueness is string equality of canonicalized paths (a symlink to an
35
+ * existing workspace's directory collides), and attach-time session `cwd`
36
+ * checks go through the same canon. Relative paths reject before `realpath` can
37
+ * resolve them from the Host cwd or current Windows drive. A path that does not
38
+ * exist rejects with the original `ENOENT` — this is `create`'s reject path (a
39
+ * workspace must point at an existing directory).
15
40
  * @param path - The path to canonicalize.
16
41
  * @returns the canonical absolute path.
17
42
  */
18
43
  export async function realpathNormalize(path) {
44
+ if (!fullyQualifiedWorkspacePath(path)) {
45
+ throw new TypeError(`Workspace path is not fully qualified: '${path}'`);
46
+ }
19
47
  return await realpath(path);
20
48
  }
21
49
  //# sourceMappingURL=paths.js.map
@@ -34,7 +34,7 @@ export interface Workspace {
34
34
  * afterwards, even when the directory disappears (see {@link status}).
35
35
  */
36
36
  readonly path: string;
37
- /** Display title. Defaults to `basename(path)` at create; duplicates are allowed. */
37
+ /** Display title. Defaults to the final path segment, or a filesystem root's own spelling; duplicates are allowed. */
38
38
  readonly title: string;
39
39
  /** ISO-8601 creation instant, stamped at create and never rewritten. */
40
40
  readonly createdAt: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-workspace",
3
3
  "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness",
4
- "version": "0.1.2-alpha.5",
4
+ "version": "0.1.3-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -38,24 +38,24 @@
38
38
  "license": "MIT",
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.2",
41
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.5",
42
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
43
- "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.5",
44
- "@deepseek-ai/dsh-storage": "^0.1.2-alpha.5",
45
- "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.5",
46
- "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.5"
41
+ "@deepseek-ai/dsh-invariants": "^0.1.3-alpha.2",
42
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
43
+ "@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
44
+ "@deepseek-ai/dsh-storage": "^0.1.3-alpha.2",
45
+ "@deepseek-ai/dsh-storage-domain": "^0.1.3-alpha.2",
46
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2"
47
47
  },
48
48
  "dependencies": {
49
49
  "zod": "^4.4.3",
50
- "@deepseek-ai/dsh-brand": "^0.1.2-alpha.5"
50
+ "@deepseek-ai/dsh-brand": "^0.1.3-alpha.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@deepseek-ai/cordis": "^4.0.2",
54
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
55
- "@deepseek-ai/dsh-storage": "^0.1.2-alpha.5",
56
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.5",
57
- "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.5",
58
- "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.5",
59
- "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.5"
54
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
55
+ "@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
56
+ "@deepseek-ai/dsh-storage": "^0.1.3-alpha.2",
57
+ "@deepseek-ai/dsh-invariants": "^0.1.3-alpha.2",
58
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2",
59
+ "@deepseek-ai/dsh-storage-domain": "^0.1.3-alpha.2"
60
60
  }
61
61
  }