@x-otto/workspace 0.0.1-alpha.2 → 0.0.1-alpha.3

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.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @x-otto/workspace
2
2
 
3
- > Workspace identity primitives: unique IDs, local marker read/write, upward search, and three runtime forms (L/R/V).
3
+ > Workspace identity primitives: unique IDs, local marker read/write, and upward search (single Local form).
4
4
 
5
- `@x-otto/workspace` defines how otto identifies and associates a workspace with a directory. A workspace is a first-class logical entity with an immutable `id`, persisted via a local marker file (`.otto/workspace.json`). The package handles id generation and validation, upward directory search for existing markers, and resolution of the three workspace forms (Local/Remote/Virtual). Zero internal dependencies — only Node.js built-ins.
5
+ `@x-otto/workspace` defines how otto identifies and associates a workspace with a directory. A workspace is a first-class logical entity with an immutable `id`, persisted via a local marker file (`.otto/workspace.json`). The package handles id generation and validation, upward directory search for existing markers, and resolution of the single Local workspace form. Zero internal dependencies — only Node.js built-ins.
6
6
 
7
7
  ## Installation
8
8
 
@@ -36,7 +36,6 @@ const resolver = createWorkspaceResolver(io)
36
36
  // Finds existing marker via upward search, or creates one
37
37
  const ref = await resolver.resolveLocal('/path/to/project')
38
38
  // ref.key === 'ws_<id>'
39
- // ref.backend === 'local'
40
39
  // ref.root === '/path/to/project'
41
40
 
42
41
  // Explicit workspace id override (--id mode)
@@ -69,10 +68,9 @@ workspaceKey(id) // → 'ws_a1b2c3d4e5f6789012345678abcdef01'
69
68
  ## Key Exports
70
69
 
71
70
  ### Types
72
- - `Workspace` — Logical entity: id, name, owner, createdAt
73
- - `WorkspaceRef` — Runtime reference: key, backend (local/remote), root, remote URL, memorySync
74
- - `WorkspaceMarker` — `.otto/workspace.json` shape: id, name, remote, memorySyncEnabled
75
- - `WorkspaceIO` — Injected I/O interface: readFile, writeFile, genId, now, confirmTrust
71
+ - `WorkspaceRef` — Runtime reference: key, id, root, memorySyncEnabled
72
+ - `WorkspaceMarker` — `.otto/workspace.json` shape: id, name, createdAt, memorySyncEnabled
73
+ - `WorkspaceIO` — Injected I/O interface: readFile, writeFile, writeFileExclusive, genId, now
76
74
  - `WorkspaceResolver` — `resolveLocal()` + `createLocal()` interface
77
75
 
78
76
  ### Functions
package/dist/index.d.ts CHANGED
@@ -2,63 +2,43 @@
2
2
  /**
3
3
  * @x-otto/workspace — workspace 身份原语(纯层)。
4
4
  *
5
- * workspace 是一等 DB 逻辑实体(id 不可变、不绑定文件),持久化第一维 = `workspaceKey = ws_<id>`。
6
- * 两个正交轴:`backend`(持久化在哪)× `root`(有无本地文件)→ L/R/V 三形态。
7
- * docs/rfc/RFC-034-workspace-mechanism-persistence-dimension.md §4.1(workspace-mechanism-flows.md
8
- * 设计稿已删,内容并入本 RFC,见 commit 5d6a9f5b)。
5
+ * workspace 是一等逻辑实体(id 不可变、不绑定文件),持久化第一维 = `workspaceKey = ws_<id>`。
6
+ * 唯一运行形态 = **L**(本地盘 + 本地文件根)。见
7
+ * docs/rfc/RFC-034-workspace-mechanism-persistence-dimension.md §4.1
8
+ *
9
+ * 历史注(RFC-067 M115-04,2026-08-14 执行):早期设计有 `backend`(local/remote) × `root`
10
+ * 两正交轴推导出的 L/R/V 三形态 + `RemoteRef`/信任门。经核实 R/V 生产不可达(无代码写带
11
+ * remote 的 marker、`confirmTrust` 从不注入、`backend` 恒 'local' 且无真实分支),且实际上线
12
+ * 的 workspace 记忆远端同步(RFC-164)走的是独立的 `sessionUrl + memorySyncEnabled opt-in`
13
+ * 轴(见 coding/storage-wiring.ts),与 `backend` 无关——证伪了原抽象方向。故塌成 L。将来若
14
+ * 真需远端 workspace 后端,应以可替换 provider seam 重新长出,而非复活死枚举。
9
15
  */
10
- /** workspace 一等实体(DB 行 / web-ui CRUD 对象)。 */
11
- interface Workspace {
12
- /** 不可变;客户端生成(见 id.ts)。 */
13
- readonly id: string;
14
- readonly name: string;
15
- /** 多租户(对接 RFC-009 M29);本地 unmanaged 形态可缺省。 */
16
- readonly ownerId?: string;
17
- readonly orgId?: string;
18
- /** ISO 时间串;由调用方注入(纯层不取系统时钟)。 */
19
- readonly createdAt: string;
20
- }
21
- /** 远端绑定引用(R/V 形态携带)。 */
22
- interface RemoteRef {
23
- /** 远端持久化服务 base URL。 */
24
- readonly url: string;
25
- }
26
16
  /**
27
- * 运行时引用:身份永远是 `ws_<id>`。两个正交轴:
28
- * - `backend`:持久化后端在哪(`local` 本地盘 / `remote` 远端服务)。
29
- * - `root`:本地文件根;在场 = 有文件工具,缺省 = headless(V)。
30
- *
31
- * 三形态:L = local+root · R = remote+root · V = remote+无 root。
17
+ * 运行时引用:身份永远是 `ws_<id>`,本地文件根为 `root`(唯一 L 形态)。
32
18
  */
33
19
  interface WorkspaceRef {
34
20
  /** = `ws_` + id,持久化第一维。 */
35
21
  readonly key: string;
36
22
  readonly id: string;
37
- readonly backend: 'local' | 'remote';
38
- /** 本地文件根(工具 root);headless 缺省。 */
39
- readonly root?: string;
40
- /** backend=remote 时携带。 */
41
- readonly remote?: RemoteRef;
23
+ /** 本地文件根(工具 root)。 */
24
+ readonly root: string;
42
25
  /**
43
- * R 形态 workspace 记忆远端同步 opt-in(RFC-164 M164-2 D2),透传自 `WorkspaceMarker`。
26
+ * workspace 记忆远端同步 opt-in(RFC-164 M164-2 D2),透传自 `WorkspaceMarker`。
44
27
  * 缺省/`false` = 记忆走本地文件(M58-02 现状);`true` = auto-memory/lessons 走远端。
45
28
  */
46
29
  readonly memorySyncEnabled?: boolean;
47
30
  }
48
31
  /**
49
- * `.otto/workspace.json` 的形状(本地关联 marker)。
50
- * 默认 gitignore(RFC 规则 3);携 `remote` 时启动需过信任门(RFC 规则 3/§4.2.1)。
32
+ * `.otto/workspace.json` 的形状(本地关联 marker)。默认 gitignore(RFC 规则 3)
51
33
  */
52
34
  interface WorkspaceMarker {
53
35
  readonly id: string;
54
36
  readonly name: string;
55
37
  readonly createdAt: string;
56
- readonly remote?: RemoteRef;
57
38
  /**
58
- * R 形态 workspace 记忆远端同步 opt-in(RFC-164 M164-2 D2)。缺省/`false` = 记忆走本地文件
39
+ * workspace 记忆远端同步 opt-in(RFC-164 M164-2 D2)。缺省/`false` = 记忆走本地文件
59
40
  * (M58-02 现状,向后兼容);显式 `true` = auto-memory/lessons 走远端 HttpMemoryStore。
60
- * 用户需通过 `otto workspace memory-sync enable/disable` 命令显式切换,不隐式跟随
61
- * session backend 的选择(D2:认知边界,用户须知情同意)。
41
+ * 用户需通过 `otto workspace memory-sync enable/disable` 命令显式切换。
62
42
  */
63
43
  readonly memorySyncEnabled?: boolean;
64
44
  }
@@ -75,20 +55,13 @@ interface WorkspaceIO {
75
55
  * RFC-303 D7(M2 T303-05):独占创建写——O_EXCL 语义,目标文件已存在则失败
76
56
  * (返回 `false`,不覆盖、不抛异常),成功创建返回 `true`。用于并发首启创建互斥
77
57
  * (两进程同时对空目录 createLocal 时,只有一个能真正创建 marker,另一个应改为
78
- * 读取刚创建的 marker 复用其 id,而非各自生成不同 id 互相覆盖)。可选字段——缺省
79
- * 时 resolver 降级为"先读后写"近似方案(TOCTOU 窗口极小但非零,见 resolver.ts
80
- * create() 注释)。
58
+ * 读取刚创建的 marker 复用其 id,而非各自生成不同 id 互相覆盖)。
81
59
  */
82
- writeFileExclusive?(path: string, content: string): Promise<boolean>;
60
+ writeFileExclusive(path: string, content: string): Promise<boolean>;
83
61
  /** 生成不可变 workspace id(默认实现见 id.ts)。 */
84
62
  genId(): string;
85
63
  /** 当前时间 ISO 串;注入式(纯层不直接取系统时钟,便于测试)。 */
86
64
  now(): string;
87
- /**
88
- * 信任门:发现带 `remote` 的 marker 时调用,返回是否信任连接。
89
- * 缺省 = fail-closed(不信任,降级本地);见 RFC 规则 3。
90
- */
91
- confirmTrust?(remote: RemoteRef, markerDir: string): Promise<boolean>;
92
65
  }
93
66
  //#endregion
94
67
  //#region src/id.d.ts
@@ -159,8 +132,7 @@ declare function findMarker(startCwd: string, io: WorkspaceIO): Promise<MarkerHi
159
132
  * RFC-303 D7(M2 T303-05):`exclusive:true` 时用 `io.writeFileExclusive`(O_EXCL 语义)写
160
133
  * marker 本体——若目标已存在则不覆盖、返回 `false`(调用方应改为读取已存在的 marker)。
161
134
  * gitignore 始终用普通覆盖写(无并发创建互斥语义需求,其内容由 `memorySyncEnabled` 派生,
162
- * 覆盖写是安全的幂等操作)。`io` 未实现 `writeFileExclusive` 时退化为普通覆盖写(向后兼容,
163
- * 老 IO 实现不受影响,但不具备本次修复的互斥保证——调用方应尽快升级 IO 实现)。
135
+ * 覆盖写是安全的幂等操作)。
164
136
  */
165
137
  declare function writeMarker(dir: string, marker: WorkspaceMarker, io: WorkspaceIO, options?: {
166
138
  exclusive?: boolean;
@@ -212,7 +184,7 @@ interface WorkspaceResolver {
212
184
  * 本地 resolve-or-create:
213
185
  * - `opts.id` 在场 → 显式绑定该 id(写/刷新 marker),忽略 autoCreate(DevPod/`--id` 模式无歧义)。
214
186
  * - 否则向上 walk 找 marker:
215
- * - 命中且 `hit.dir === cwd` → 当前目录已有 workspace 身份 → L/R(取决于 remote/trust)。
187
+ * - 命中且 `hit.dir === cwd` → 当前目录已有 workspace 身份 → L 形态。
216
188
  * - 命中且 `hit.dir !== cwd` + `autoCreate:false` → 返回 `ParentWorkspaceExisted`,
217
189
  * 调用层应提示用户选择"使用上级 workspace"还是"在当前目录创建新 workspace",
218
190
  * 防止子目录 workspace 被静默吞入父 workspace。
@@ -231,5 +203,5 @@ interface WorkspaceResolver {
231
203
  }
232
204
  declare function createWorkspaceResolver(io: WorkspaceIO): WorkspaceResolver;
233
205
  //#endregion
234
- export { DEFAULT_WORKSPACE_KEY, GITIGNORE_FILE, MARKER_DIR, MARKER_FILE, MarkerHit, OTTO_GITIGNORE, ParentWorkspaceExisted, RemoteRef, ResolveLocalOptions, UninitializedWorkspace, WS_KEY_PREFIX, Workspace, WorkspaceIO, WorkspaceMarker, WorkspaceRef, WorkspaceResolver, assertValidWorkspaceId, buildGitignoreContent, createWorkspaceResolver, findMarker, genWorkspaceId, gitignorePath, isValidWorkspaceId, markerPath, readMarker, workspaceKey, writeMarker };
206
+ export { DEFAULT_WORKSPACE_KEY, GITIGNORE_FILE, MARKER_DIR, MARKER_FILE, MarkerHit, OTTO_GITIGNORE, ParentWorkspaceExisted, ResolveLocalOptions, UninitializedWorkspace, WS_KEY_PREFIX, WorkspaceIO, WorkspaceMarker, WorkspaceRef, WorkspaceResolver, assertValidWorkspaceId, buildGitignoreContent, createWorkspaceResolver, findMarker, genWorkspaceId, gitignorePath, isValidWorkspaceId, markerPath, readMarker, workspaceKey, writeMarker };
235
207
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/id.ts","../src/marker.ts","../src/resolver.ts"],"mappings":";;AAUA;;;;;;;;UAAiB,SAAA;EAQG;EAAA,SANT,EAAA;EAAA,SACA,IAAA;EASe;EAAA,SAPf,OAAA;EAAA,SACA,KAAA;EAQG;EAAA,SANH,SAAA;AAAA;;UAIM,SAAA;EAcN;EAAA,SAZA,GAAA;AAAA;;;;;;;AA8BX;UApBiB,YAAA;;WAEN,GAAA;EAAA,SACA,EAAA;EAAA,SACA,OAAA;EAmBA;EAAA,SAjBA,IAAA;EAkBS;EAAA,SAhBT,MAAA,GAAS,SAAA;EAuBQ;;AAO5B;;EAP4B,SAlBjB,iBAAA;AAAA;;;;;UAOM,eAAA;EAAA,SACN,EAAA;EAAA,SACA,IAAA;EAAA,SACA,SAAA;EAAA,SACA,MAAA,GAAS,SAAA;EAkBlB;;;;;;EAAA,SAXS,iBAAA;AAAA;;;;;UAOM,WAAA;EAsBkB;EApBjC,QAAA,CAAS,IAAA,WAAe,OAAA;EAoBoC;EAlB5D,SAAA,CAAU,IAAA,UAAc,OAAA,WAAkB,OAAA;;;;ACzE5C;;;;;EDkFE,kBAAA,EAAoB,IAAA,UAAc,OAAA,WAAkB,OAAA;EC3EpB;ED6EhC,KAAA;EC7EgC;ED+EhC,GAAA;ECtEc;;;;ED2Ed,YAAA,EAAc,MAAA,EAAQ,SAAA,EAAW,SAAA,WAAoB,OAAA;AAAA;;;;cC3F1C,aAAA;;;;;;cAOA,qBAAA;;iBASG,kBAAA,CAAmB,EAAA;;iBAKnB,sBAAA,CAAuB,EAAA;ADFvC;;;;AAAA,iBCYgB,YAAA,CAAa,EAAA;ADA7B;;;;;AAAA,iBCUgB,cAAA,CAAA;;;ADlChB;AAAA,cEHa,UAAA;;cAEA,WAAA;;cAEA,cAAA;;;;;;cAOA,cAAA;;;;;AFgBb;;;iBEcgB,qBAAA,CAAsB,iBAAA;;iBAKtB,UAAA,CAAW,GAAA;;iBAKX,aAAA,CAAc,GAAA;;iBAkCR,UAAA,CAAW,GAAA,UAAa,EAAA,EAAI,WAAA,GAAc,OAAA,CAAQ,eAAA;;UAOvD,SAAA;EAAA,SACN,GAAA;EAAA,SACA,MAAA,EAAQ,eAAA;AAAA;;;;;iBAOG,UAAA,CAAW,QAAA,UAAkB,EAAA,EAAI,WAAA,GAAc,OAAA,CAAQ,SAAA;;;;;;AFpC7E;;;;iBEwDsB,WAAA,CACpB,GAAA,UACA,MAAA,EAAQ,eAAA,EACR,EAAA,EAAI,WAAA,EACJ,OAAA;EAAY,SAAA;AAAA,IACX,OAAA;;;AF3HH;;;;;AAAA,UGEiB,sBAAA;EAAA,SACN,IAAA;EHGA;EAAA,SGDA,UAAA;EHGS;EAAA,SGDT,WAAA;EHKM;EAAA,SGHN,MAAA,EAAQ,eAAA;AAAA;;UAIF,mBAAA;EHWA;EGTf,EAAA;;EAEA,IAAA;EHSS;EGPT,SAAA;EHSS;;;;;;EGFT,UAAA;AAAA;;;;;;UA6Be,sBAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA;AAAA;;UAIM,iBAAA;EHCA;;;;;;;;;;;;;EGaf,YAAA,CACE,GAAA,UACA,IAAA,GAAO,mBAAA,GACN,OAAA,CAAQ,YAAA,GAAe,sBAAA,GAAyB,sBAAA;EHZzC;;;;;EGkBV,WAAA,CAAY,IAAA,UAAc,IAAA,GAAO,mBAAA,GAAsB,OAAA,CAAQ,YAAA;AAAA;AAAA,iBAGjD,uBAAA,CAAwB,EAAA,EAAI,WAAA,GAAc,iBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/id.ts","../src/marker.ts","../src/resolver.ts"],"mappings":";;AAkBA;;;;;;;;;;AAgBA;;;;;;UAhBiB,YAAA;EAyBN;EAAA,SAvBA,GAAA;EAAA,SACA,EAAA;EA6BM;EAAA,SA3BN,IAAA;;;;;WAKA,iBAAA;AAAA;;;;UAMM,eAAA;EAAA,SACN,EAAA;EAAA,SACA,IAAA;EAAA,SACA,SAAA;EAwBT;;;;;EAAA,SAlBS,iBAAA;AAAA;;;;;UAOM,WAAA;EC/CS;EDiDxB,QAAA,CAAS,IAAA,WAAe,OAAA;ECjDA;EDmDxB,SAAA,CAAU,IAAA,UAAc,OAAA,WAAkB,OAAA;EC5C/B;;;;;AASb;ED0CE,kBAAA,CAAmB,IAAA,UAAc,OAAA,WAAkB,OAAA;;EAEnD,KAAA;EC5C2C;ED8C3C,GAAA;AAAA;;;;cC9DW,aAAA;;;;;;cAOA,qBAAA;;iBASG,kBAAA,CAAmB,EAAA;;iBAKnB,sBAAA,CAAuB,EAAA;;;;;iBAUvB,YAAA,CAAa,EAAA;;;;;ADgB7B;iBCNgB,cAAA,CAAA;;;AD1BhB;AAAA,cEXa,UAAA;;cAEA,WAAA;;cAEA,cAAA;;;;;AFuBb;cEhBa,cAAA;;;;;;;;iBA8BG,qBAAA,CAAsB,iBAAA;AFEtC;AAAA,iBEGgB,UAAA,CAAW,GAAA;;iBAKX,aAAA,CAAc,GAAA;;iBA0BR,UAAA,CAAW,GAAA,UAAa,EAAA,EAAI,WAAA,GAAc,OAAA,CAAQ,eAAA;;UAOvD,SAAA;EAAA,SACN,GAAA;EAAA,SACA,MAAA,EAAQ,eAAA;AAAA;;;;;iBAOG,UAAA,CAAW,QAAA,UAAkB,EAAA,EAAI,WAAA,GAAc,OAAA,CAAQ,SAAA;;;;;;;;;iBAmBvD,WAAA,CACpB,GAAA,UACA,MAAA,EAAQ,eAAA,EACR,EAAA,EAAI,WAAA,EACJ,OAAA;EAAY,SAAA;AAAA,IACX,OAAA;;;AF1GH;;;;;AAAA,UGNiB,sBAAA;EAAA,SACN,IAAA;EHeA;EAAA,SGbA,UAAA;EHaiB;EAAA,SGXjB,WAAA;EHiBqB;EAAA,SGfrB,MAAA,EAAQ,eAAA;AAAA;;UAIF,mBAAA;EHcN;EGZT,EAAA;EHkB0B;EGhB1B,IAAA;EHuBe;EGrBf,SAAA;;;;;;;EAOA,UAAA;AAAA;;;;;;UAiBe,sBAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA;AAAA;;UAIM,iBAAA;EHMZ;;;;;AC9DL;;;;;AAOA;;;EE+DE,YAAA,CACE,GAAA,UACA,IAAA,GAAO,mBAAA,GACN,OAAA,CAAQ,YAAA,GAAe,sBAAA,GAAyB,sBAAA;EFlEnB;AASlC;;;;EE+DE,WAAA,CAAY,IAAA,UAAc,IAAA,GAAO,mBAAA,GAAsB,OAAA,CAAQ,YAAA;AAAA;AAAA,iBAGjD,uBAAA,CAAwB,EAAA,EAAI,WAAA,GAAc,iBAAA"}
package/dist/index.js CHANGED
@@ -12,6 +12,6 @@ local/
12
12
  MEMORY.md
13
13
  memory/
14
14
  lessons.json
15
- `:h}function _(e){return r(e,f,p)}function v(e){return r(e,f,m)}function y(e){let t=JSON.parse(e),n=t.id,r=t.name,i=t.createdAt;if(typeof n!=`string`)throw Error(`workspace marker: missing id`);if(l(n),typeof r!=`string`)throw Error(`workspace marker: missing name`);if(typeof i!=`string`)throw Error(`workspace marker: missing createdAt`);let a=t.remote,o;if(a&&typeof a==`object`){let e=a.url;typeof e==`string`&&(o={url:e})}let s=t.memorySyncEnabled===!0?!0:void 0,c={id:n,name:r,createdAt:i};return o&&(c.remote=o),s!==void 0&&(c.memorySyncEnabled=s),c}async function b(e,t){let n=await t.readFile(_(e));return n==null?null:y(n)}async function x(e,t){let r=i(e);for(;;){let e=await b(r,t);if(e)return{dir:r,marker:e};let i=n(r);if(i===r)return null;r=i}}async function S(e,t,n,r){l(t.id);let i=_(e),a=JSON.stringify(t,null,2)+`
16
- `;if(r?.exclusive&&n.writeFileExclusive){if(!await n.writeFileExclusive(i,a))return!1}else await n.writeFile(i,a);return await n.writeFile(v(e),g(t.memorySyncEnabled===!0)),!0}function C(e,t){return{key:u(e.id),id:e.id,backend:`local`,root:t,...e.memorySyncEnabled?{memorySyncEnabled:!0}:{}}}function w(e,t){return{key:u(e.id),id:e.id,backend:`remote`,root:t,...e.remote?{remote:e.remote}:{},...e.memorySyncEnabled?{memorySyncEnabled:!0}:{}}}function T(e){async function n(n,r,i){let a={id:i,name:r.name??(t(n)||`workspace`),createdAt:r.createdAt??e.now()};if(r.id==null){if(!await S(n,a,e,{exclusive:!0})){let t=await b(n,e);if(t)return C(t,n);await S(n,a,e)}}else await S(n,a,e);return C(a,n)}return{async resolveLocal(t,r={}){let a=i(t);if(r.id!=null)return l(r.id),n(a,r,r.id);let o=await x(a,e);return o?o.dir!==a&&r.autoCreate===!1?{kind:`parentWorkspaceExisted`,parentRoot:o.dir,currentRoot:a,marker:o.marker}:o.marker.remote&&e.confirmTrust&&await e.confirmTrust(o.marker.remote,o.dir)?w(o.marker,o.dir):C(o.marker,o.dir):r.autoCreate===!1?{kind:`uninitialized`,root:a}:n(a,r,e.genId())},async createLocal(t,r={}){return n(i(t),r,r.id??e.genId())}}}export{o as DEFAULT_WORKSPACE_KEY,m as GITIGNORE_FILE,f as MARKER_DIR,p as MARKER_FILE,h as OTTO_GITIGNORE,a as WS_KEY_PREFIX,l as assertValidWorkspaceId,g as buildGitignoreContent,T as createWorkspaceResolver,x as findMarker,d as genWorkspaceId,v as gitignorePath,c as isValidWorkspaceId,_ as markerPath,b as readMarker,u as workspaceKey,S as writeMarker};
15
+ `:h}function _(e){return r(e,f,p)}function v(e){return r(e,f,m)}function y(e){let t=JSON.parse(e),n=t.id,r=t.name,i=t.createdAt;if(typeof n!=`string`)throw Error(`workspace marker: missing id`);if(l(n),typeof r!=`string`)throw Error(`workspace marker: missing name`);if(typeof i!=`string`)throw Error(`workspace marker: missing createdAt`);let a=t.memorySyncEnabled===!0?!0:void 0,o={id:n,name:r,createdAt:i};return a!==void 0&&(o.memorySyncEnabled=a),o}async function b(e,t){let n=await t.readFile(_(e));return n==null?null:y(n)}async function x(e,t){let r=i(e);for(;;){let e=await b(r,t);if(e)return{dir:r,marker:e};let i=n(r);if(i===r)return null;r=i}}async function S(e,t,n,r){l(t.id);let i=_(e),a=JSON.stringify(t,null,2)+`
16
+ `;if(r?.exclusive){if(!await n.writeFileExclusive(i,a))return!1}else await n.writeFile(i,a);return await n.writeFile(v(e),g(t.memorySyncEnabled===!0)),!0}function C(e,t){return{key:u(e.id),id:e.id,root:t,...e.memorySyncEnabled?{memorySyncEnabled:!0}:{}}}function w(e){async function n(n,r,i){let a={id:i,name:r.name??(t(n)||`workspace`),createdAt:r.createdAt??e.now()};if(r.id==null){if(!await S(n,a,e,{exclusive:!0})){let t=await b(n,e);if(t)return C(t,n);await S(n,a,e)}}else await S(n,a,e);return C(a,n)}return{async resolveLocal(t,r={}){let a=i(t);if(r.id!=null)return l(r.id),n(a,r,r.id);let o=await x(a,e);return o?o.dir!==a&&r.autoCreate===!1?{kind:`parentWorkspaceExisted`,parentRoot:o.dir,currentRoot:a,marker:o.marker}:C(o.marker,o.dir):r.autoCreate===!1?{kind:`uninitialized`,root:a}:n(a,r,e.genId())},async createLocal(t,r={}){return n(i(t),r,r.id??e.genId())}}}export{o as DEFAULT_WORKSPACE_KEY,m as GITIGNORE_FILE,f as MARKER_DIR,p as MARKER_FILE,h as OTTO_GITIGNORE,a as WS_KEY_PREFIX,l as assertValidWorkspaceId,g as buildGitignoreContent,w as createWorkspaceResolver,x as findMarker,d as genWorkspaceId,v as gitignorePath,c as isValidWorkspaceId,_ as markerPath,b as readMarker,u as workspaceKey,S as writeMarker};
17
17
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/id.ts","../src/marker.ts","../src/resolver.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\n/** `workspaceKey` 前缀。 */\nexport const WS_KEY_PREFIX = 'ws_'\n\n/**\n * 缺省 workspace key(无 workspace 身份标识时的回退分区键)。\n * 供服务端路由/会话池/持久化层统一引用——定义在此为单一真源,\n * 防止各消费方各自硬编码 `'default'` 字符串。\n */\nexport const DEFAULT_WORKSPACE_KEY = 'default'\n\n/**\n * workspace id 合法字符集与长度上限(RFC 规则 11):\n * id 会进 storage 分区 key + URL 路径段 + SQL,必须可控,防注入/路径穿越。\n */\nconst WS_ID_RE = /^[A-Za-z0-9_-]{1,64}$/\n\n/** id 是否合法(charset `[A-Za-z0-9_-]`,长度 1..64)。 */\nexport function isValidWorkspaceId(id: string): boolean {\n return WS_ID_RE.test(id)\n}\n\n/** 校验 id,不合法即抛(fail-closed;读 marker / bind 入口用)。 */\nexport function assertValidWorkspaceId(id: string): void {\n if (!isValidWorkspaceId(id)) {\n throw new Error(`invalid workspace id: ${JSON.stringify(id)} (expect ${WS_ID_RE})`)\n }\n}\n\n/**\n * 持久化第一维 key。id 不可变 ⇒ key 稳定。\n * 校验后拼接,杜绝非法 id 进入 storage/URL。\n */\nexport function workspaceKey(id: string): string {\n assertValidWorkspaceId(id)\n return WS_KEY_PREFIX + id\n}\n\n/**\n * 默认 id 生成器:客户端生成、全局唯一、不可变(RFC 规则 1)。\n * 用 uuid(去横线 → 32 hex),落在合法字符集内;远端 bind 时直接接受(不重分配)。\n * resolver 接受注入覆盖以便测试。\n */\nexport function genWorkspaceId(): string {\n return randomUUID().replace(/-/g, '')\n}\n","import { dirname, join, resolve } from 'node:path'\n\nimport { assertValidWorkspaceId } from './id'\n\nimport type { WorkspaceIO, WorkspaceMarker } from './types'\n\n/** workspace 本地足迹目录名。 */\nexport const MARKER_DIR = '.otto'\n/** 关联 marker 文件名。 */\nexport const MARKER_FILE = 'workspace.json'\n/** 生成的 gitignore 文件名。 */\nexport const GITIGNORE_FILE = '.gitignore'\n\n/**\n * `.otto/.gitignore` 模板(RFC-034 规则 3 / 决策 D4):\n * 身份(`workspace.json`)默认私有 + 易变忽略;项目知识(AGENTS.md/MEMORY.md/memory/)默认提交。\n * 团队共享 workspace 实体需显式 `git add -f .otto/workspace.json`。\n */\nexport const OTTO_GITIGNORE = `# otto workspace 本地足迹 — RFC-034 规则 3\n# 身份私有 + 易变忽略;项目知识(AGENTS.md / MEMORY.md / memory/)默认提交。\n# 团队共享 workspace 实体:显式 \\`git add -f .otto/workspace.json\\`。\nworkspace.json\ncache/\nlogs/\n*.tmp\n# Machine-specific memory (RFC-164 M164-1 D3) — local/ is never synced across devices, never committed.\nlocal/\n`\n\n/**\n * opt-in 远端记忆同步生效时(RFC-164 M164-2 D2/D4)追加的 gitignore 行——\n * auto-memory(`MEMORY.md`/`memory/`)与 `lessons.json` 改由远端 store 单一权威,\n * 不再随 git 提交;`AGENTS.md`(人工维护的项目知识)不受影响,继续走 git(D4 分离处理)。\n */\nconst MEMORY_SYNC_GITIGNORE_SUFFIX = `# Remote memory sync enabled (RFC-164 M164-2 D2/D4) — auto-memory/lessons\n# now have a single source of truth in the remote store; AGENTS.md still tracked by git.\nMEMORY.md\nmemory/\nlessons.json\n`\n\n/**\n * 按 `memorySyncEnabled` 状态生成 gitignore 内容——opt-in 生效时追加 D4 单一权威切换规则。\n * `writeMarker` 用此替代裸 `OTTO_GITIGNORE` 常量,使 marker 与 gitignore 内容随 opt-in 状态同步。\n *\n * 注意:`writeMarker` 会同时写入 gitignore 以保证 marker 的 `memorySyncEnabled` 字段与\n * gitignore 规则一致——用户自定义 gitignore 规则应写在 `.otto/.gitignore` 文件末尾。\n */\nexport function buildGitignoreContent(memorySyncEnabled: boolean): string {\n return memorySyncEnabled ? OTTO_GITIGNORE + MEMORY_SYNC_GITIGNORE_SUFFIX : OTTO_GITIGNORE\n}\n\n/** `<dir>/.otto/workspace.json` 绝对路径。 */\nexport function markerPath(dir: string): string {\n return join(dir, MARKER_DIR, MARKER_FILE)\n}\n\n/** `<dir>/.otto/.gitignore` 绝对路径。 */\nexport function gitignorePath(dir: string): string {\n return join(dir, MARKER_DIR, GITIGNORE_FILE)\n}\n\nfunction parseMarker(raw: string): WorkspaceMarker {\n const obj = JSON.parse(raw) as Record<string, unknown>\n const id = obj['id']\n const name = obj['name']\n const createdAt = obj['createdAt']\n if (typeof id !== 'string') throw new Error('workspace marker: missing id')\n assertValidWorkspaceId(id)\n if (typeof name !== 'string') throw new Error('workspace marker: missing name')\n if (typeof createdAt !== 'string') throw new Error('workspace marker: missing createdAt')\n const remoteRaw = obj['remote']\n let remote: WorkspaceMarker['remote']\n if (remoteRaw && typeof remoteRaw === 'object') {\n const url = (remoteRaw as Record<string, unknown>)['url']\n if (typeof url === 'string') remote = { url }\n }\n const memorySyncEnabled = obj['memorySyncEnabled'] === true ? true : undefined\n\n const result: {\n id: string\n name: string\n createdAt: string\n remote?: WorkspaceMarker['remote']\n memorySyncEnabled?: boolean\n } = { id, name, createdAt }\n if (remote) result.remote = remote\n if (memorySyncEnabled !== undefined) result.memorySyncEnabled = memorySyncEnabled\n return result\n}\n\n/** 读取某目录的 marker;不存在返回 null,内容非法则抛(fail-closed)。 */\nexport async function readMarker(dir: string, io: WorkspaceIO): Promise<WorkspaceMarker | null> {\n const raw = await io.readFile(markerPath(dir))\n if (raw == null) return null\n return parseMarker(raw)\n}\n\n/** 命中结果:marker 所在目录 + 解析内容。 */\nexport interface MarkerHit {\n readonly dir: string\n readonly marker: WorkspaceMarker\n}\n\n/**\n * 从 `startCwd` 向上 walk 找最近的 `.otto/workspace.json`(类比 git 找 `.git`)。\n * 命中即返回;到文件系统根仍未命中返回 null。\n */\nexport async function findMarker(startCwd: string, io: WorkspaceIO): Promise<MarkerHit | null> {\n let dir = resolve(startCwd)\n for (;;) {\n const marker = await readMarker(dir, io)\n if (marker) return { dir, marker }\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * 在 `<dir>/.otto/` 写入 marker + gitignore(内容随 `marker.memorySyncEnabled` 状态调整,见 D4)。\n *\n * RFC-303 D7(M2 T303-05):`exclusive:true` 时用 `io.writeFileExclusive`(O_EXCL 语义)写\n * marker 本体——若目标已存在则不覆盖、返回 `false`(调用方应改为读取已存在的 marker)。\n * gitignore 始终用普通覆盖写(无并发创建互斥语义需求,其内容由 `memorySyncEnabled` 派生,\n * 覆盖写是安全的幂等操作)。`io` 未实现 `writeFileExclusive` 时退化为普通覆盖写(向后兼容,\n * 老 IO 实现不受影响,但不具备本次修复的互斥保证——调用方应尽快升级 IO 实现)。\n */\nexport async function writeMarker(\n dir: string,\n marker: WorkspaceMarker,\n io: WorkspaceIO,\n options?: { exclusive?: boolean },\n): Promise<boolean> {\n assertValidWorkspaceId(marker.id)\n const path = markerPath(dir)\n const content = JSON.stringify(marker, null, 2) + '\\n'\n\n if (options?.exclusive && io.writeFileExclusive) {\n const created = await io.writeFileExclusive(path, content)\n if (!created) return false\n } else {\n await io.writeFile(path, content)\n }\n\n await io.writeFile(gitignorePath(dir), buildGitignoreContent(marker.memorySyncEnabled === true))\n return true\n}\n","import { basename, resolve } from 'node:path'\n\nimport { assertValidWorkspaceId, workspaceKey } from './id'\nimport { findMarker, readMarker, writeMarker } from './marker'\n\nimport type { WorkspaceIO, WorkspaceMarker, WorkspaceRef } from './types'\n\n/**\n * `resolveLocal` 在 `autoCreate:false` 模式下,向上递归在**父级目录**命中 marker 时的返回值。\n * 调用方(交互态 CLI)应提示用户选择\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n * 而非静默采用父级身份(防止子目录 workspace 被意外吞入父 workspace)。\n */\nexport interface ParentWorkspaceExisted {\n readonly kind: 'parentWorkspaceExisted'\n /** marker 所在目录(父级 workspace 根)。 */\n readonly parentRoot: string\n /** 用户当前工作目录(pwd)。 */\n readonly currentRoot: string\n /** 父级 marker 内容。 */\n readonly marker: WorkspaceMarker\n}\n\n/** `resolveLocal` 可选项。 */\nexport interface ResolveLocalOptions {\n /** `--id` 覆盖:在此目录显式绑定指定 workspace(一目录多 workspace,DevPod 模式)。 */\n id?: string\n /** 新建时的名字;缺省取目录名。 */\n name?: string\n /** 新建时的 createdAt;缺省取 `io.now()`。 */\n createdAt?: string\n /**\n * marker 未命中时是否自动创建。缺省 `true`(旧行为,headless/`--id` 场景零感知)。\n * 传 `false`(交互态新流程):未命中时不写文件,返回 `{kind:'uninitialized', root}`,\n * 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。对 `opts.id` 显式给定\n * 的场景无效(该分支必然走 create,无歧义可言)。\n */\n autoCreate?: boolean\n}\n\nfunction localRef(marker: WorkspaceMarker, root: string): WorkspaceRef {\n return {\n key: workspaceKey(marker.id),\n id: marker.id,\n backend: 'local',\n root,\n ...(marker.memorySyncEnabled ? { memorySyncEnabled: true } : {}),\n }\n}\n\nfunction remoteRef(marker: WorkspaceMarker, root: string): WorkspaceRef {\n return {\n key: workspaceKey(marker.id),\n id: marker.id,\n backend: 'remote',\n root,\n ...(marker.remote ? { remote: marker.remote } : {}),\n ...(marker.memorySyncEnabled ? { memorySyncEnabled: true } : {}),\n }\n}\n\n/**\n * `resolveLocal` 未命中 marker 时的结果(`autoCreate:false` 专用,见下方 opts)——\n * 调用方须显式 `createLocal()` 才会真正写 marker(本次修复:交互态先过信任门,用户确认后\n * 才建立 workspace 身份,防止 `.otto/workspace.json` 缺失时静默生成新随机 id 致旧会话孤儿化)。\n */\nexport interface UninitializedWorkspace {\n readonly kind: 'uninitialized'\n readonly root: string\n}\n\n/** workspace 身份解析器(注入式 IO,纯可测)。 */\nexport interface WorkspaceResolver {\n /**\n * 本地 resolve-or-create:\n * - `opts.id` 在场 → 显式绑定该 id(写/刷新 marker),忽略 autoCreate(DevPod/`--id` 模式无歧义)。\n * - 否则向上 walk 找 marker:\n * - 命中且 `hit.dir === cwd` → 当前目录已有 workspace 身份 → L/R(取决于 remote/trust)。\n * - 命中且 `hit.dir !== cwd` + `autoCreate:false` → 返回 `ParentWorkspaceExisted`,\n * 调用层应提示用户选择\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n * 防止子目录 workspace 被静默吞入父 workspace。\n * - 命中且 `hit.dir !== cwd` + `autoCreate` 未传/`true`(headless)→ 静默采用父级。\n * - 未命中:`opts.autoCreate !== false`(缺省/headless 等旧行为)→ 在 cwd 生成 id + 写 marker → L;\n * `opts.autoCreate === false`(交互态新流程)→ 返回 `UninitializedWorkspace`,不写文件,\n * 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。\n */\n resolveLocal(\n cwd: string,\n opts?: ResolveLocalOptions,\n ): Promise<WorkspaceRef | UninitializedWorkspace | ParentWorkspaceExisted>\n /**\n * 显式创建 L 形态 workspace(写 marker)——`resolveLocal({autoCreate:false})` 返回\n * `uninitialized` 后,调用方(信任确认后)调此方法完成创建。与 resolveLocal 内部的\n * 自动创建分支同一实现,只是从\"自动触发\"变为\"显式调用\"。\n */\n createLocal(root: string, opts?: ResolveLocalOptions): Promise<WorkspaceRef>\n}\n\nexport function createWorkspaceResolver(io: WorkspaceIO): WorkspaceResolver {\n /**\n * RFC-303 D7(M2 T303-05):并发首启创建互斥——此前两个进程在同一空目录首启,\n * `opts.id` 缺省时各自 `io.genId()` 生成不同随机 id、各自 `writeMarker` 覆盖写,\n * 后写者覆盖先写者,两进程持有不同 `workspace_key` 分区(会话列表互相隔离)。\n * 修复:`opts.id` 未显式给定时,用 `writeMarker(..., {exclusive:true})`(O_EXCL\n * 语义)尝试创建——若目标已存在(`writeMarker` 返回 `false`,说明另一进程刚\n * 抢先创建成功),改为读取该 marker 复用其 id,而非用自己生成的新 id 覆盖。\n * `opts.id` 显式给定的路径(DevPod/`--id` 模式)不受影响,语义上本就无歧义\n * (用户明确指定了 id,不存在\"谁先谁后\"的竞态问题)。\n */\n async function create(\n root: string,\n opts: ResolveLocalOptions,\n id: string,\n ): Promise<WorkspaceRef> {\n const marker: WorkspaceMarker = {\n id,\n name: opts.name ?? (basename(root) || 'workspace'),\n createdAt: opts.createdAt ?? io.now(),\n }\n if (opts.id == null) {\n const created = await writeMarker(root, marker, io, { exclusive: true })\n if (!created) {\n // 独占写失败——目标已存在(另一进程抢先创建),读取复用其 id。\n const existing = await readMarker(root, io)\n if (existing) return localRef(existing, root)\n // 理论不可达(写入失败即意味着文件存在,读取应该成功);若发生说明\n // 极端竞态下文件又被删除,退化为覆盖写保底(不阻断用户)。\n await writeMarker(root, marker, io)\n }\n } else {\n await writeMarker(root, marker, io)\n }\n return localRef(marker, root)\n }\n\n return {\n async resolveLocal(cwd, opts = {}) {\n const root = resolve(cwd)\n\n if (opts.id != null) {\n assertValidWorkspaceId(opts.id)\n return create(root, opts, opts.id)\n }\n\n const hit = await findMarker(root, io)\n if (hit) {\n // 父级目录命中 marker(非当前目录)且 autoCreate:false →\n // 返回信号让调用层决定\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n // 而非静默采用父级身份(防止子项目被意外吞入父 workspace)。\n // headless 态(autoCreate 缺省 true)不受影响——静默采用父级,保持旧行为。\n if (hit.dir !== root && opts.autoCreate === false) {\n return {\n kind: 'parentWorkspaceExisted' as const,\n parentRoot: hit.dir,\n currentRoot: root,\n marker: hit.marker,\n }\n }\n if (hit.marker.remote) {\n const trusted = io.confirmTrust\n ? await io.confirmTrust(hit.marker.remote, hit.dir)\n : false\n if (trusted) return remoteRef(hit.marker, hit.dir)\n return localRef(hit.marker, hit.dir)\n } else {\n return localRef(hit.marker, hit.dir)\n }\n }\n\n if (opts.autoCreate === false) {\n return { kind: 'uninitialized', root }\n }\n return create(root, opts, io.genId())\n },\n\n async createLocal(root, opts = {}) {\n return create(resolve(root), opts, opts.id ?? io.genId())\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/id.ts","../src/marker.ts","../src/resolver.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\n/** `workspaceKey` 前缀。 */\nexport const WS_KEY_PREFIX = 'ws_'\n\n/**\n * 缺省 workspace key(无 workspace 身份标识时的回退分区键)。\n * 供服务端路由/会话池/持久化层统一引用——定义在此为单一真源,\n * 防止各消费方各自硬编码 `'default'` 字符串。\n */\nexport const DEFAULT_WORKSPACE_KEY = 'default'\n\n/**\n * workspace id 合法字符集与长度上限(RFC 规则 11):\n * id 会进 storage 分区 key + URL 路径段 + SQL,必须可控,防注入/路径穿越。\n */\nconst WS_ID_RE = /^[A-Za-z0-9_-]{1,64}$/\n\n/** id 是否合法(charset `[A-Za-z0-9_-]`,长度 1..64)。 */\nexport function isValidWorkspaceId(id: string): boolean {\n return WS_ID_RE.test(id)\n}\n\n/** 校验 id,不合法即抛(fail-closed;读 marker / bind 入口用)。 */\nexport function assertValidWorkspaceId(id: string): void {\n if (!isValidWorkspaceId(id)) {\n throw new Error(`invalid workspace id: ${JSON.stringify(id)} (expect ${WS_ID_RE})`)\n }\n}\n\n/**\n * 持久化第一维 key。id 不可变 ⇒ key 稳定。\n * 校验后拼接,杜绝非法 id 进入 storage/URL。\n */\nexport function workspaceKey(id: string): string {\n assertValidWorkspaceId(id)\n return WS_KEY_PREFIX + id\n}\n\n/**\n * 默认 id 生成器:客户端生成、全局唯一、不可变(RFC 规则 1)。\n * 用 uuid(去横线 → 32 hex),落在合法字符集内;远端 bind 时直接接受(不重分配)。\n * resolver 接受注入覆盖以便测试。\n */\nexport function genWorkspaceId(): string {\n return randomUUID().replace(/-/g, '')\n}\n","import { dirname, join, resolve } from 'node:path'\n\nimport { assertValidWorkspaceId } from './id'\n\nimport type { WorkspaceIO, WorkspaceMarker } from './types'\n\n/** workspace 本地足迹目录名。 */\nexport const MARKER_DIR = '.otto'\n/** 关联 marker 文件名。 */\nexport const MARKER_FILE = 'workspace.json'\n/** 生成的 gitignore 文件名。 */\nexport const GITIGNORE_FILE = '.gitignore'\n\n/**\n * `.otto/.gitignore` 模板(RFC-034 规则 3 / 决策 D4):\n * 身份(`workspace.json`)默认私有 + 易变忽略;项目知识(AGENTS.md/MEMORY.md/memory/)默认提交。\n * 团队共享 workspace 实体需显式 `git add -f .otto/workspace.json`。\n */\nexport const OTTO_GITIGNORE = `# otto workspace 本地足迹 — RFC-034 规则 3\n# 身份私有 + 易变忽略;项目知识(AGENTS.md / MEMORY.md / memory/)默认提交。\n# 团队共享 workspace 实体:显式 \\`git add -f .otto/workspace.json\\`。\nworkspace.json\ncache/\nlogs/\n*.tmp\n# Machine-specific memory (RFC-164 M164-1 D3) — local/ is never synced across devices, never committed.\nlocal/\n`\n\n/**\n * opt-in 远端记忆同步生效时(RFC-164 M164-2 D2/D4)追加的 gitignore 行——\n * auto-memory(`MEMORY.md`/`memory/`)与 `lessons.json` 改由远端 store 单一权威,\n * 不再随 git 提交;`AGENTS.md`(人工维护的项目知识)不受影响,继续走 git(D4 分离处理)。\n */\nconst MEMORY_SYNC_GITIGNORE_SUFFIX = `# Remote memory sync enabled (RFC-164 M164-2 D2/D4) — auto-memory/lessons\n# now have a single source of truth in the remote store; AGENTS.md still tracked by git.\nMEMORY.md\nmemory/\nlessons.json\n`\n\n/**\n * 按 `memorySyncEnabled` 状态生成 gitignore 内容——opt-in 生效时追加 D4 单一权威切换规则。\n * `writeMarker` 用此替代裸 `OTTO_GITIGNORE` 常量,使 marker 与 gitignore 内容随 opt-in 状态同步。\n *\n * 注意:`writeMarker` 会同时写入 gitignore 以保证 marker 的 `memorySyncEnabled` 字段与\n * gitignore 规则一致——用户自定义 gitignore 规则应写在 `.otto/.gitignore` 文件末尾。\n */\nexport function buildGitignoreContent(memorySyncEnabled: boolean): string {\n return memorySyncEnabled ? OTTO_GITIGNORE + MEMORY_SYNC_GITIGNORE_SUFFIX : OTTO_GITIGNORE\n}\n\n/** `<dir>/.otto/workspace.json` 绝对路径。 */\nexport function markerPath(dir: string): string {\n return join(dir, MARKER_DIR, MARKER_FILE)\n}\n\n/** `<dir>/.otto/.gitignore` 绝对路径。 */\nexport function gitignorePath(dir: string): string {\n return join(dir, MARKER_DIR, GITIGNORE_FILE)\n}\n\nfunction parseMarker(raw: string): WorkspaceMarker {\n const obj = JSON.parse(raw) as Record<string, unknown>\n const id = obj['id']\n const name = obj['name']\n const createdAt = obj['createdAt']\n if (typeof id !== 'string') throw new Error('workspace marker: missing id')\n assertValidWorkspaceId(id)\n if (typeof name !== 'string') throw new Error('workspace marker: missing name')\n if (typeof createdAt !== 'string') throw new Error('workspace marker: missing createdAt')\n const memorySyncEnabled = obj['memorySyncEnabled'] === true ? true : undefined\n\n const result: {\n id: string\n name: string\n createdAt: string\n memorySyncEnabled?: boolean\n } = { id, name, createdAt }\n if (memorySyncEnabled !== undefined) result.memorySyncEnabled = memorySyncEnabled\n return result\n}\n\n/** 读取某目录的 marker;不存在返回 null,内容非法则抛(fail-closed)。 */\nexport async function readMarker(dir: string, io: WorkspaceIO): Promise<WorkspaceMarker | null> {\n const raw = await io.readFile(markerPath(dir))\n if (raw == null) return null\n return parseMarker(raw)\n}\n\n/** 命中结果:marker 所在目录 + 解析内容。 */\nexport interface MarkerHit {\n readonly dir: string\n readonly marker: WorkspaceMarker\n}\n\n/**\n * 从 `startCwd` 向上 walk 找最近的 `.otto/workspace.json`(类比 git 找 `.git`)。\n * 命中即返回;到文件系统根仍未命中返回 null。\n */\nexport async function findMarker(startCwd: string, io: WorkspaceIO): Promise<MarkerHit | null> {\n let dir = resolve(startCwd)\n for (;;) {\n const marker = await readMarker(dir, io)\n if (marker) return { dir, marker }\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * 在 `<dir>/.otto/` 写入 marker + gitignore(内容随 `marker.memorySyncEnabled` 状态调整,见 D4)。\n *\n * RFC-303 D7(M2 T303-05):`exclusive:true` 时用 `io.writeFileExclusive`(O_EXCL 语义)写\n * marker 本体——若目标已存在则不覆盖、返回 `false`(调用方应改为读取已存在的 marker)。\n * gitignore 始终用普通覆盖写(无并发创建互斥语义需求,其内容由 `memorySyncEnabled` 派生,\n * 覆盖写是安全的幂等操作)。\n */\nexport async function writeMarker(\n dir: string,\n marker: WorkspaceMarker,\n io: WorkspaceIO,\n options?: { exclusive?: boolean },\n): Promise<boolean> {\n assertValidWorkspaceId(marker.id)\n const path = markerPath(dir)\n const content = JSON.stringify(marker, null, 2) + '\\n'\n\n if (options?.exclusive) {\n const created = await io.writeFileExclusive(path, content)\n if (!created) return false\n } else {\n await io.writeFile(path, content)\n }\n\n await io.writeFile(gitignorePath(dir), buildGitignoreContent(marker.memorySyncEnabled === true))\n return true\n}\n","import { basename, resolve } from 'node:path'\n\nimport { assertValidWorkspaceId, workspaceKey } from './id'\nimport { findMarker, readMarker, writeMarker } from './marker'\n\nimport type { WorkspaceIO, WorkspaceMarker, WorkspaceRef } from './types'\n\n/**\n * `resolveLocal` 在 `autoCreate:false` 模式下,向上递归在**父级目录**命中 marker 时的返回值。\n * 调用方(交互态 CLI)应提示用户选择\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n * 而非静默采用父级身份(防止子目录 workspace 被意外吞入父 workspace)。\n */\nexport interface ParentWorkspaceExisted {\n readonly kind: 'parentWorkspaceExisted'\n /** marker 所在目录(父级 workspace 根)。 */\n readonly parentRoot: string\n /** 用户当前工作目录(pwd)。 */\n readonly currentRoot: string\n /** 父级 marker 内容。 */\n readonly marker: WorkspaceMarker\n}\n\n/** `resolveLocal` 可选项。 */\nexport interface ResolveLocalOptions {\n /** `--id` 覆盖:在此目录显式绑定指定 workspace(一目录多 workspace,DevPod 模式)。 */\n id?: string\n /** 新建时的名字;缺省取目录名。 */\n name?: string\n /** 新建时的 createdAt;缺省取 `io.now()`。 */\n createdAt?: string\n /**\n * marker 未命中时是否自动创建。缺省 `true`(旧行为,headless/`--id` 场景零感知)。\n * 传 `false`(交互态新流程):未命中时不写文件,返回 `{kind:'uninitialized', root}`,\n * 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。对 `opts.id` 显式给定\n * 的场景无效(该分支必然走 create,无歧义可言)。\n */\n autoCreate?: boolean\n}\n\nfunction localRef(marker: WorkspaceMarker, root: string): WorkspaceRef {\n return {\n key: workspaceKey(marker.id),\n id: marker.id,\n root,\n ...(marker.memorySyncEnabled ? { memorySyncEnabled: true } : {}),\n }\n}\n\n/**\n * `resolveLocal` 未命中 marker 时的结果(`autoCreate:false` 专用,见下方 opts)——\n * 调用方须显式 `createLocal()` 才会真正写 marker(本次修复:交互态先过信任门,用户确认后\n * 才建立 workspace 身份,防止 `.otto/workspace.json` 缺失时静默生成新随机 id 致旧会话孤儿化)。\n */\nexport interface UninitializedWorkspace {\n readonly kind: 'uninitialized'\n readonly root: string\n}\n\n/** workspace 身份解析器(注入式 IO,纯可测)。 */\nexport interface WorkspaceResolver {\n /**\n * 本地 resolve-or-create:\n * - `opts.id` 在场 → 显式绑定该 id(写/刷新 marker),忽略 autoCreate(DevPod/`--id` 模式无歧义)。\n * - 否则向上 walk 找 marker:\n * - 命中且 `hit.dir === cwd` → 当前目录已有 workspace 身份 → L 形态。\n * - 命中且 `hit.dir !== cwd` + `autoCreate:false` → 返回 `ParentWorkspaceExisted`,\n * 调用层应提示用户选择\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n * 防止子目录 workspace 被静默吞入父 workspace。\n * - 命中且 `hit.dir !== cwd` + `autoCreate` 未传/`true`(headless)→ 静默采用父级。\n * - 未命中:`opts.autoCreate !== false`(缺省/headless 等旧行为)→ 在 cwd 生成 id + 写 marker → L;\n * `opts.autoCreate === false`(交互态新流程)→ 返回 `UninitializedWorkspace`,不写文件,\n * 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。\n */\n resolveLocal(\n cwd: string,\n opts?: ResolveLocalOptions,\n ): Promise<WorkspaceRef | UninitializedWorkspace | ParentWorkspaceExisted>\n /**\n * 显式创建 L 形态 workspace(写 marker)——`resolveLocal({autoCreate:false})` 返回\n * `uninitialized` 后,调用方(信任确认后)调此方法完成创建。与 resolveLocal 内部的\n * 自动创建分支同一实现,只是从\"自动触发\"变为\"显式调用\"。\n */\n createLocal(root: string, opts?: ResolveLocalOptions): Promise<WorkspaceRef>\n}\n\nexport function createWorkspaceResolver(io: WorkspaceIO): WorkspaceResolver {\n /**\n * RFC-303 D7(M2 T303-05):并发首启创建互斥——此前两个进程在同一空目录首启,\n * `opts.id` 缺省时各自 `io.genId()` 生成不同随机 id、各自 `writeMarker` 覆盖写,\n * 后写者覆盖先写者,两进程持有不同 `workspace_key` 分区(会话列表互相隔离)。\n * 修复:`opts.id` 未显式给定时,用 `writeMarker(..., {exclusive:true})`(O_EXCL\n * 语义)尝试创建——若目标已存在(`writeMarker` 返回 `false`,说明另一进程刚\n * 抢先创建成功),改为读取该 marker 复用其 id,而非用自己生成的新 id 覆盖。\n * `opts.id` 显式给定的路径(DevPod/`--id` 模式)不受影响,语义上本就无歧义\n * (用户明确指定了 id,不存在\"谁先谁后\"的竞态问题)。\n */\n async function create(\n root: string,\n opts: ResolveLocalOptions,\n id: string,\n ): Promise<WorkspaceRef> {\n const marker: WorkspaceMarker = {\n id,\n name: opts.name ?? (basename(root) || 'workspace'),\n createdAt: opts.createdAt ?? io.now(),\n }\n if (opts.id == null) {\n const created = await writeMarker(root, marker, io, { exclusive: true })\n if (!created) {\n // 独占写失败——目标已存在(另一进程抢先创建),读取复用其 id。\n const existing = await readMarker(root, io)\n if (existing) return localRef(existing, root)\n // 理论不可达(写入失败即意味着文件存在,读取应该成功);若发生说明\n // 极端竞态下文件又被删除,退化为覆盖写保底(不阻断用户)。\n await writeMarker(root, marker, io)\n }\n } else {\n await writeMarker(root, marker, io)\n }\n return localRef(marker, root)\n }\n\n return {\n async resolveLocal(cwd, opts = {}) {\n const root = resolve(cwd)\n\n if (opts.id != null) {\n assertValidWorkspaceId(opts.id)\n return create(root, opts, opts.id)\n }\n\n const hit = await findMarker(root, io)\n if (hit) {\n // 父级目录命中 marker(非当前目录)且 autoCreate:false →\n // 返回信号让调用层决定\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n // 而非静默采用父级身份(防止子项目被意外吞入父 workspace)。\n // headless 态(autoCreate 缺省 true)不受影响——静默采用父级,保持旧行为。\n if (hit.dir !== root && opts.autoCreate === false) {\n return {\n kind: 'parentWorkspaceExisted' as const,\n parentRoot: hit.dir,\n currentRoot: root,\n marker: hit.marker,\n }\n }\n return localRef(hit.marker, hit.dir)\n }\n\n if (opts.autoCreate === false) {\n return { kind: 'uninitialized', root }\n }\n return create(root, opts, io.genId())\n },\n\n async createLocal(root, opts = {}) {\n return create(resolve(root), opts, opts.id ?? io.genId())\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-otto/workspace",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.3",
4
4
  "files": [
5
5
  "dist"
6
6
  ],