@tita-agent/dsh-data-table 0.0.0-stage → 0.2.0

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.
Files changed (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -2
  3. package/cordis.patch.yml +7 -0
  4. package/lib/client.js +16766 -0
  5. package/lib/contract-envelope.d.ts +91 -0
  6. package/lib/contract-envelope.js +106 -0
  7. package/lib/contract-layout.d.ts +130 -0
  8. package/lib/contract-layout.js +138 -0
  9. package/lib/contract-refs.d.ts +39 -0
  10. package/lib/contract-refs.js +9 -0
  11. package/lib/contract-version.d.ts +311 -0
  12. package/lib/contract-version.js +270 -0
  13. package/lib/contract-wire.d.ts +147 -0
  14. package/lib/contract-wire.js +99 -0
  15. package/lib/contracts.d.ts +12 -0
  16. package/lib/contracts.js +12 -0
  17. package/lib/types/audit.d.ts +42 -0
  18. package/lib/types/audit.js +39 -0
  19. package/lib/types/client/ProjectImportMenu.d.ts +30 -0
  20. package/lib/types/client/ProjectImportMenu.js +70 -0
  21. package/lib/types/client/SheetPanel.d.ts +33 -0
  22. package/lib/types/client/SheetPanel.js +1766 -0
  23. package/lib/types/client/SheetToggle.d.ts +18 -0
  24. package/lib/types/client/SheetToggle.js +26 -0
  25. package/lib/types/client/index.d.ts +15 -0
  26. package/lib/types/client/index.js +110 -0
  27. package/lib/types/client/persistence/changes.d.ts +15 -0
  28. package/lib/types/client/persistence/changes.js +51 -0
  29. package/lib/types/client/persistence/ids.d.ts +34 -0
  30. package/lib/types/client/persistence/ids.js +48 -0
  31. package/lib/types/client/persistence/merge.d.ts +35 -0
  32. package/lib/types/client/persistence/merge.js +362 -0
  33. package/lib/types/client/store.d.ts +61 -0
  34. package/lib/types/client/store.js +152 -0
  35. package/lib/types/client/surface-context.d.ts +27 -0
  36. package/lib/types/client/surface-context.js +104 -0
  37. package/lib/types/client/surface-control.d.ts +2 -0
  38. package/lib/types/client/surface-control.js +12 -0
  39. package/lib/types/client/transport/workbookApi.d.ts +159 -0
  40. package/lib/types/client/transport/workbookApi.js +246 -0
  41. package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
  42. package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
  43. package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
  44. package/lib/types/client/univer/WorkbookCanvas.js +87 -0
  45. package/lib/types/client/univer/WorkbookView.d.ts +3 -0
  46. package/lib/types/client/univer/WorkbookView.js +2 -0
  47. package/lib/types/client/univer/loader.d.ts +18 -0
  48. package/lib/types/client/univer/loader.js +81 -0
  49. package/lib/types/client/univer/standalone-entry.d.ts +1 -0
  50. package/lib/types/client/univer/standalone-entry.js +19 -0
  51. package/lib/types/client/versions/VersionChrome.d.ts +33 -0
  52. package/lib/types/client/versions/VersionChrome.js +21 -0
  53. package/lib/types/client/versions/inspect.d.ts +70 -0
  54. package/lib/types/client/versions/inspect.js +88 -0
  55. package/lib/types/client/versions/preview-decoration.d.ts +11 -0
  56. package/lib/types/client/versions/preview-decoration.js +54 -0
  57. package/lib/types/client/versions/records.d.ts +19 -0
  58. package/lib/types/client/versions/records.js +70 -0
  59. package/lib/types/client/workspace-host.d.ts +61 -0
  60. package/lib/types/client/workspace-host.js +124 -0
  61. package/lib/types/collaboration-hub.d.ts +31 -0
  62. package/lib/types/collaboration-hub.js +69 -0
  63. package/lib/types/contract/ids.d.ts +24 -0
  64. package/lib/types/contract/ids.js +46 -0
  65. package/lib/types/contract/mutations.d.ts +25 -0
  66. package/lib/types/contract/mutations.js +782 -0
  67. package/lib/types/contract/operations.d.ts +54 -0
  68. package/lib/types/contract/operations.js +203 -0
  69. package/lib/types/contract/snapshots.d.ts +176 -0
  70. package/lib/types/contract/snapshots.js +351 -0
  71. package/lib/types/contract/univer-api-catalog.d.ts +29 -0
  72. package/lib/types/contract/univer-api-catalog.js +264 -0
  73. package/lib/types/errors.d.ts +8 -0
  74. package/lib/types/errors.js +12 -0
  75. package/lib/types/http/assets-routes.d.ts +3 -0
  76. package/lib/types/http/assets-routes.js +169 -0
  77. package/lib/types/http/collaboration.d.ts +10 -0
  78. package/lib/types/http/collaboration.js +154 -0
  79. package/lib/types/http/history.d.ts +15 -0
  80. package/lib/types/http/history.js +134 -0
  81. package/lib/types/http/project-files.d.ts +79 -0
  82. package/lib/types/http/project-files.js +206 -0
  83. package/lib/types/http/project-scope.d.ts +50 -0
  84. package/lib/types/http/project-scope.js +52 -0
  85. package/lib/types/http/routes.d.ts +98 -0
  86. package/lib/types/http/routes.js +323 -0
  87. package/lib/types/http/standalone.d.ts +9 -0
  88. package/lib/types/http/standalone.js +31 -0
  89. package/lib/types/http/trunk.d.ts +10 -0
  90. package/lib/types/http/trunk.js +322 -0
  91. package/lib/types/http/worktree.d.ts +9 -0
  92. package/lib/types/http/worktree.js +145 -0
  93. package/lib/types/index.d.ts +18 -0
  94. package/lib/types/index.js +156 -0
  95. package/lib/types/invariant.d.ts +15 -0
  96. package/lib/types/invariant.js +15 -0
  97. package/lib/types/request-params.d.ts +21 -0
  98. package/lib/types/request-params.js +76 -0
  99. package/lib/types/store/paths.d.ts +68 -0
  100. package/lib/types/store/paths.js +148 -0
  101. package/lib/types/store/project.d.ts +88 -0
  102. package/lib/types/store/project.js +120 -0
  103. package/lib/types/store/read.d.ts +110 -0
  104. package/lib/types/store/read.js +404 -0
  105. package/lib/types/store/retention.d.ts +27 -0
  106. package/lib/types/store/retention.js +111 -0
  107. package/lib/types/store/write.d.ts +22 -0
  108. package/lib/types/store/write.js +210 -0
  109. package/lib/types/tools/arguments.d.ts +40 -0
  110. package/lib/types/tools/arguments.js +130 -0
  111. package/lib/types/tools/cells.d.ts +50 -0
  112. package/lib/types/tools/cells.js +298 -0
  113. package/lib/types/tools/index.d.ts +2 -0
  114. package/lib/types/tools/index.js +250 -0
  115. package/lib/types/tools/render.d.ts +47 -0
  116. package/lib/types/tools/render.js +290 -0
  117. package/lib/types/tools/univer-api.d.ts +18 -0
  118. package/lib/types/tools/univer-api.js +198 -0
  119. package/lib/types/tools/workbook-catalog.d.ts +3 -0
  120. package/lib/types/tools/workbook-catalog.js +83 -0
  121. package/lib/types/tools/workbook-edit.d.ts +192 -0
  122. package/lib/types/tools/workbook-edit.js +912 -0
  123. package/lib/types/tools/workbook-find.d.ts +11 -0
  124. package/lib/types/tools/workbook-find.js +89 -0
  125. package/lib/types/tools/workbook-history.d.ts +2 -0
  126. package/lib/types/tools/workbook-history.js +59 -0
  127. package/lib/types/tools/workbook-read.d.ts +21 -0
  128. package/lib/types/tools/workbook-read.js +204 -0
  129. package/lib/types/types.d.ts +377 -0
  130. package/lib/types/types.js +34 -0
  131. package/lib/types/univer/apply-operation.d.ts +14 -0
  132. package/lib/types/univer/apply-operation.js +398 -0
  133. package/lib/types/univer/chunk.d.ts +68 -0
  134. package/lib/types/univer/chunk.js +16 -0
  135. package/lib/types/univer/decoration.d.ts +34 -0
  136. package/lib/types/univer/decoration.js +88 -0
  137. package/lib/types/univer/diagnostics.d.ts +31 -0
  138. package/lib/types/univer/diagnostics.js +141 -0
  139. package/lib/types/univer/engine.d.ts +33 -0
  140. package/lib/types/univer/engine.js +338 -0
  141. package/lib/types/univer/entry.d.ts +8 -0
  142. package/lib/types/univer/entry.js +11 -0
  143. package/lib/types/univer/facade.d.ts +107 -0
  144. package/lib/types/univer/facade.js +1 -0
  145. package/lib/types/univer/fonts.d.ts +202 -0
  146. package/lib/types/univer/fonts.js +140 -0
  147. package/lib/types/univer/plugins/collaboration.d.ts +23 -0
  148. package/lib/types/univer/plugins/collaboration.js +47 -0
  149. package/lib/types/univer/plugins/export.d.ts +39 -0
  150. package/lib/types/univer/plugins/export.js +246 -0
  151. package/lib/types/univer/plugins/history.d.ts +24 -0
  152. package/lib/types/univer/plugins/history.js +50 -0
  153. package/lib/types/univer/plugins/version.d.ts +13 -0
  154. package/lib/types/univer/plugins/version.js +13 -0
  155. package/lib/types/version/diff.d.ts +36 -0
  156. package/lib/types/version/diff.js +156 -0
  157. package/lib/types/version/grouping.d.ts +7 -0
  158. package/lib/types/version/grouping.js +94 -0
  159. package/lib/types/version/records.d.ts +17 -0
  160. package/lib/types/version/records.js +102 -0
  161. package/lib/types/version/session.d.ts +36 -0
  162. package/lib/types/version/session.js +89 -0
  163. package/lib/types/version/summary.d.ts +24 -0
  164. package/lib/types/version/summary.js +111 -0
  165. package/lib/types/workbook-manager.d.ts +191 -0
  166. package/lib/types/workbook-manager.js +935 -0
  167. package/lib/types/xlsx-export-worker.d.ts +5 -0
  168. package/lib/types/xlsx-export-worker.js +24 -0
  169. package/lib/types/xlsx-export.d.ts +21 -0
  170. package/lib/types/xlsx-export.js +371 -0
  171. package/lib/types/xlsx-import-worker.d.ts +1 -0
  172. package/lib/types/xlsx-import-worker.js +19 -0
  173. package/lib/univer-client.js +690 -0
  174. package/lib/workbook-page.js +23826 -0
  175. package/lib/xlsx-export-worker.js +104 -0
  176. package/lib/xlsx-import-worker.js +46 -0
  177. package/package.json +106 -3
  178. package/src/audit.ts +58 -0
  179. package/src/client/ProjectImportMenu.tsx +100 -0
  180. package/src/client/SheetPanel.module.css +1578 -0
  181. package/src/client/SheetPanel.tsx +1934 -0
  182. package/src/client/SheetToggle.module.css +73 -0
  183. package/src/client/SheetToggle.tsx +65 -0
  184. package/src/client/contract.d.ts +19 -0
  185. package/src/client/index.ts +131 -0
  186. package/src/client/persistence/changes.ts +58 -0
  187. package/src/client/persistence/ids.ts +60 -0
  188. package/src/client/persistence/merge.ts +392 -0
  189. package/src/client/store.ts +188 -0
  190. package/src/client/surface-context.ts +123 -0
  191. package/src/client/surface-control.ts +13 -0
  192. package/src/client/transport/workbookApi.ts +355 -0
  193. package/src/client/univer/SheetView.module.css +110 -0
  194. package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
  195. package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
  196. package/src/client/univer/WorkbookCanvas.tsx +128 -0
  197. package/src/client/univer/WorkbookView.module.css +30 -0
  198. package/src/client/univer/WorkbookView.tsx +3 -0
  199. package/src/client/univer/loader.ts +90 -0
  200. package/src/client/univer/react-dom-client.d.ts +10 -0
  201. package/src/client/univer/standalone-entry.tsx +19 -0
  202. package/src/client/univer/standalone.css +12 -0
  203. package/src/client/univer/univer-locales.d.ts +89 -0
  204. package/src/client/univer/univer-modules.d.ts +26 -0
  205. package/src/client/versions/VersionChrome.tsx +114 -0
  206. package/src/client/versions/inspect.ts +138 -0
  207. package/src/client/versions/preview-decoration.ts +54 -0
  208. package/src/client/versions/records.ts +78 -0
  209. package/src/client/workspace-host.ts +147 -0
  210. package/src/collaboration-hub.ts +83 -0
  211. package/src/contract/ids.ts +50 -0
  212. package/src/contract/mutations.ts +791 -0
  213. package/src/contract/operations.ts +221 -0
  214. package/src/contract/snapshots.ts +383 -0
  215. package/src/contract/univer-api-catalog.ts +289 -0
  216. package/src/css-modules.d.ts +6 -0
  217. package/src/errors.ts +25 -0
  218. package/src/http/assets-routes.ts +177 -0
  219. package/src/http/collaboration.ts +144 -0
  220. package/src/http/history.ts +151 -0
  221. package/src/http/project-files.ts +227 -0
  222. package/src/http/project-scope.ts +77 -0
  223. package/src/http/routes.ts +364 -0
  224. package/src/http/standalone.ts +32 -0
  225. package/src/http/trunk.ts +322 -0
  226. package/src/http/worktree.ts +119 -0
  227. package/src/index.ts +165 -0
  228. package/src/invariant.ts +17 -0
  229. package/src/request-params.ts +92 -0
  230. package/src/store/paths.ts +165 -0
  231. package/src/store/project.ts +137 -0
  232. package/src/store/read.ts +454 -0
  233. package/src/store/retention.ts +106 -0
  234. package/src/store/write.ts +230 -0
  235. package/src/tools/arguments.ts +128 -0
  236. package/src/tools/cells.ts +298 -0
  237. package/src/tools/index.ts +274 -0
  238. package/src/tools/render.ts +301 -0
  239. package/src/tools/univer-api.ts +243 -0
  240. package/src/tools/workbook-catalog.ts +94 -0
  241. package/src/tools/workbook-edit.ts +987 -0
  242. package/src/tools/workbook-find.ts +80 -0
  243. package/src/tools/workbook-history.ts +63 -0
  244. package/src/tools/workbook-read.ts +210 -0
  245. package/src/types.ts +432 -0
  246. package/src/univer/apply-operation.ts +369 -0
  247. package/src/univer/chunk.ts +77 -0
  248. package/src/univer/decoration.ts +111 -0
  249. package/src/univer/diagnostics.ts +136 -0
  250. package/src/univer/engine.ts +370 -0
  251. package/src/univer/entry.ts +14 -0
  252. package/src/univer/facade.ts +91 -0
  253. package/src/univer/fonts.ts +148 -0
  254. package/src/univer/overrides.css +33 -0
  255. package/src/univer/plugins/collaboration.ts +47 -0
  256. package/src/univer/plugins/export.ts +236 -0
  257. package/src/univer/plugins/history.ts +49 -0
  258. package/src/univer/plugins/version.ts +13 -0
  259. package/src/version/diff.ts +169 -0
  260. package/src/version/grouping.ts +96 -0
  261. package/src/version/records.ts +113 -0
  262. package/src/version/session.ts +90 -0
  263. package/src/version/summary.ts +117 -0
  264. package/src/workbook-manager.ts +1111 -0
  265. package/src/xlsx-export-worker.ts +34 -0
  266. package/src/xlsx-export.ts +373 -0
  267. package/src/xlsx-import-worker.ts +30 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Request vocabulary shared by both transports (docs/issue/0022 P3).
3
+ *
4
+ * The browser posts a JSON body and the Agent passes tool arguments, but both
5
+ * have to become the same `WorkbookOperationPayload`. These parsers are that
6
+ * translation, and they are validation-first: each one rejects what it cannot
7
+ * represent instead of coercing it.
8
+ *
9
+ * They live here rather than in `http/` because the Agent tools need exactly the
10
+ * same conversions, and a tool importing from the HTTP layer would make the two
11
+ * transports depend on each other.
12
+ */
13
+ import {
14
+ type WorkbookOperationPayload,
15
+ type WorkbookOperationType,
16
+ type WorkbookRange,
17
+ } from './types.ts'
18
+ import { validateRange } from './contract/snapshots.ts'
19
+
20
+ export function parseIndex(value: unknown, label: string): number {
21
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) throw new Error(`${label} 必须是大于等于 0 的整数`)
22
+ return value
23
+ }
24
+
25
+ export function optionalRevision(value: unknown): number | undefined {
26
+ if (value === undefined || value === null) return undefined
27
+ return parseIndex(value, 'baseRevision')
28
+ }
29
+
30
+ export function isOperationType(value: unknown): value is WorkbookOperationType {
31
+ return value === 'update_cells' || value === 'clear_range' || value === 'delete_cells'
32
+ || value === 'insert_rows' || value === 'delete_rows' || value === 'insert_columns'
33
+ || value === 'delete_columns' || value === 'create_sheet' || value === 'delete_sheet'
34
+ || value === 'rename_sheet' || value === 'append_rows' || value === 'format_cells'
35
+ || value === 'merge_cells' || value === 'unmerge_cells' || value === 'freeze_panes'
36
+ || value === 'set_column_width' || value === 'set_row_height' || value === 'sort_range'
37
+ || value === 'set_hyperlink' || value === 'remove_hyperlink'
38
+ || value === 'remove_duplicates'
39
+ || value === 'batch' || value === 'snapshot_replace'
40
+ }
41
+
42
+ export function asOperationPayload(value: unknown): WorkbookOperationPayload {
43
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('operation payload 必须是对象')
44
+ return value as WorkbookOperationPayload
45
+ }
46
+
47
+ export function parseRange(value: unknown): WorkbookRange {
48
+ if (typeof value === 'string') return parseA1Range(value)
49
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('range 必须是对象')
50
+ const raw = value as Record<string, unknown>
51
+ const range = {
52
+ startRow: parseIndex(raw.startRow, 'range.startRow'),
53
+ startColumn: parseIndex(raw.startColumn, 'range.startColumn'),
54
+ endRow: parseIndex(raw.endRow, 'range.endRow'),
55
+ endColumn: parseIndex(raw.endColumn, 'range.endColumn'),
56
+ }
57
+ // One validator, not two opinions. This checked only the span, so a range far
58
+ // past the end of any sheet (`startRow: 9e8`) was accepted here and rejected
59
+ // later by whichever mutator happened to consult the absolute bound — with a
60
+ // different message. `parseA1Range` reaches this through the object branch, so
61
+ // both spellings of a range are now validated identically.
62
+ validateRange(range)
63
+ return range
64
+ }
65
+
66
+ export function a1ColumnIndex(value: string): number {
67
+ let result = 0
68
+ for (const character of value.toUpperCase()) result = result * 26 + character.charCodeAt(0) - 64
69
+ return result - 1
70
+ }
71
+
72
+ export function parseA1Range(value: string): WorkbookRange {
73
+ const match = /^\s*([A-Za-z]+)(\d+)(?::([A-Za-z]+)(\d+))?\s*$/.exec(value)
74
+ if (match === null) throw new Error('range 必须是 0-based 对象,或类似 A1:D4 的范围字符串')
75
+ const startColumn = a1ColumnIndex(match[1] as string)
76
+ const startRow = Number(match[2]) - 1
77
+ const endColumn = match[3] === undefined ? startColumn : a1ColumnIndex(match[3])
78
+ const endRow = match[4] === undefined ? startRow : Number(match[4]) - 1
79
+ return parseRange({ startRow, startColumn, endRow, endColumn })
80
+ }
81
+
82
+ export function normalizeValueMatrix(value: unknown): unknown[][] {
83
+ if (!Array.isArray(value) || value.length === 0) throw new Error('values 必须是非空二维数组')
84
+ const rows = value.every(item => !Array.isArray(item)) ? [value] : value
85
+ if (!rows.every(item => Array.isArray(item))) throw new Error('values 必须是二维数组')
86
+ const width = Math.max(...rows.map(item => (item as unknown[]).length))
87
+ if (width === 0) throw new Error('values 不能包含空行')
88
+ // Ragged model-generated matrices are padded instead of rejected. This
89
+ // makes a four-row write safe when one row has a missing trailing field.
90
+ return rows.map(item => Array.from({ length: width }, (_value, index) => (item as unknown[])[index] ?? null))
91
+ }
92
+
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Where a workbook's files live in the DSH home (docs/issue/0022 P1).
3
+ *
4
+ * The single choke point every read and write goes through, which is why the
5
+ * one-time `data-sheet` → `data-table` rename is performed here rather than at
6
+ * a call site. Nothing else in the plugin may build a storage path.
7
+ */
8
+ import { existsSync, mkdirSync, renameSync } from 'node:fs'
9
+ import { homedir } from 'node:os'
10
+ import { dirname, join, resolve } from 'node:path'
11
+ import { normalizeWorkbookId, normalizeWorktreeId } from '../contract/ids.ts'
12
+ import { currentProjectRoot, projectStoragesRoot } from './project.ts'
13
+
14
+ /**
15
+ * The home the migration has already been attempted for, and what happened.
16
+ *
17
+ * Keyed by the resolved home rather than a bare boolean: `$DSH_HOME` is read on
18
+ * every call, and the previous process-wide flag meant a home that changed
19
+ * mid-process was never migrated at all — it returned the *other* home's
20
+ * outcome. The memo exists to avoid re-stat-ing on every operation, which keying
21
+ * by home still does.
22
+ */
23
+ let storageMigrationHome: string | null = null
24
+ let storageMigrationOutcome: WorkbookStorageMigration = 'no-legacy'
25
+
26
+
27
+ /** Resolve the DSH home, honoring $DSH_HOME (mirror of dsh-home-paths). */
28
+ export function dshHome(): string {
29
+ const env = process.env.DSH_HOME
30
+ const home = env !== undefined && env.trim().length > 0 ? env : join(homedir(), '.dsh')
31
+ return resolve(home)
32
+ }
33
+
34
+ /**
35
+ * Repository root for this plugin.
36
+ *
37
+ * Two answers, and the difference is which project the call belongs to:
38
+ *
39
+ * - **inside a Session** (an HTTP route or a tool call, which establish the
40
+ * scope — see `./project.ts`): `<project>/storages/data-table`, so a
41
+ * project directory is self-contained.
42
+ * - **outside one** (a script, a test, an un-migrated caller): the pre-project
43
+ * per-user location, unchanged. Answering that case with `process.cwd()`
44
+ * would silently start a *new* store for a caller that was reading the old
45
+ * one, which is worse than keeping it.
46
+ *
47
+ * `data-sheet` → `data-table` renamed that per-user root; resolving the root is
48
+ * the single choke point every read and write goes through, so the one-time
49
+ * migration runs here and is memoized afterwards.
50
+ */
51
+ export function workbookRoot(): string {
52
+ migrateLegacyWorkbookStorage()
53
+ const project = currentProjectRoot()
54
+ return project === undefined ? globalWorkbookRoot() : projectStoragesRoot(project)
55
+ }
56
+
57
+ /**
58
+ * The pre-project, per-user storage root.
59
+ *
60
+ * Named rather than inlined because it is read by more than the fallback: the
61
+ * workbook list offers its contents as "from the old location" for a user who
62
+ * has workbooks there and none here yet.
63
+ */
64
+ export function globalWorkbookRoot(): string {
65
+ return join(dshHome(), 'storages', 'data-table')
66
+ }
67
+
68
+ /** Storage root used before the data-sheet → data-table rename. */
69
+ export function legacyWorkbookRoot(): string {
70
+ return join(dshHome(), 'storages', 'data-sheet')
71
+ }
72
+
73
+ /**
74
+ * Move `<dshHome>/storages/data-sheet` to `storages/data-table` exactly once.
75
+ *
76
+ * Idempotent and safe to call from every read/write path: the old root is only
77
+ * moved when the new root does not exist yet, so a second call is a no-op and
78
+ * two populated roots are never merged (the legacy files are left untouched
79
+ * rather than deleted).
80
+ * @returns the outcome of the first migration attempt.
81
+ */
82
+ export function migrateLegacyWorkbookStorage(): WorkbookStorageMigration {
83
+ const home = dshHome()
84
+ if (storageMigrationHome === home) return storageMigrationOutcome
85
+ storageMigrationHome = home
86
+ const legacy = legacyWorkbookRoot()
87
+ const target = workbookRootWithoutMigration()
88
+ try {
89
+ if (legacy === target || !existsSync(legacy)) return (storageMigrationOutcome = 'no-legacy')
90
+ if (existsSync(target)) return (storageMigrationOutcome = 'target-exists')
91
+ mkdirSync(dirname(target), { recursive: true })
92
+ renameSync(legacy, target)
93
+ return (storageMigrationOutcome = 'migrated')
94
+ } catch {
95
+ // A failed rename must never take the plugin down; the legacy root stays
96
+ // where it is and the next call retries.
97
+ storageMigrationHome = null
98
+ return (storageMigrationOutcome = 'failed')
99
+ }
100
+ }
101
+
102
+ /** Current root without triggering the migration (avoids recursion). */
103
+ /**
104
+ * Where oversized tool results are spilled.
105
+ *
106
+ * Under the storage root, and here rather than in the module that writes them:
107
+ * what a spilled result *is* belongs to the tool presentation, but where plugin
108
+ * data lives is the store's business — and so is what to do when there is too
109
+ * much of it.
110
+ */
111
+ export function toolOutputRoot(): string {
112
+ return join(workbookRoot(), 'tool-output')
113
+ }
114
+
115
+ export function workbookRootWithoutMigration(): string {
116
+ return join(dshHome(), 'storages', 'data-table')
117
+ }
118
+
119
+ /** Absolute snapshot path for one workbook. */
120
+ export function workbookPath(id: string): string {
121
+ // Keep this boundary safe even for callers outside the tool/HTTP adapters.
122
+ // Those adapters already normalize ids, but this exported repository helper
123
+ // must not allow `../` segments to escape the Datasheet storage directory.
124
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.json`)
125
+ }
126
+
127
+ /** Lightweight metadata path used by polling, workbook lists, and history. */
128
+ export function workbookIndexPath(id: string): string {
129
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.index.json`)
130
+ }
131
+
132
+ export function workbookSnapshotPath(id: string): string {
133
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.snapshot.json`)
134
+ }
135
+
136
+ export function workbookCheckpointsPath(id: string): string {
137
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.checkpoints.json`)
138
+ }
139
+
140
+ /** Full operation log is kept out of the workbook metadata file. */
141
+ export function workbookOperationsPath(id: string): string {
142
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.operations.json`)
143
+ }
144
+
145
+ /** Pending proposals (which may contain a full preview snapshot) are sidecar data. */
146
+ export function workbookProposalsPath(id: string): string {
147
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.proposals.json`)
148
+ }
149
+
150
+ export function workbookTransactionPath(id: string): string {
151
+ return join(workbookRoot(), `${normalizeWorkbookId(id)}.transaction.json`)
152
+ }
153
+
154
+ /** Isolated review worktrees live below the workbook repository and never
155
+ * participate in the trunk workbook catalog. */
156
+ export function workbookWorktreeDirectory(workbookId: string): string {
157
+ return join(workbookRoot(), '.worktrees', normalizeWorkbookId(workbookId))
158
+ }
159
+
160
+ export function workbookWorktreePath(workbookId: string, worktreeId: string): string {
161
+ return join(workbookWorktreeDirectory(workbookId), `${normalizeWorktreeId(worktreeId)}.json`)
162
+ }
163
+
164
+ /** Outcome of {@link migrateLegacyWorkbookStorage}. */
165
+ export type WorkbookStorageMigration = 'no-legacy' | 'migrated' | 'target-exists' | 'failed'
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Which project a store call belongs to (project-local storage, 2026-09-25).
3
+ *
4
+ * A workbook used to live in one per-user directory (`~/.dsh/storages/data-table`)
5
+ * shared by every Session. It now lives in the project it belongs to —
6
+ * `<project>/storages/data-table` — so copying a project directory copies its
7
+ * workbooks, and deleting one leaves nothing behind in the user's home.
8
+ *
9
+ * The store modules are called from three places that do **not** share a call
10
+ * shape: HTTP route handlers (which name a Session id), Agent tool executions
11
+ * (which carry the calling Agent), and plain scripts and tests (which have
12
+ * neither). Rather than thread a root parameter through every read and write —
13
+ * `workbookRoot()` is the single choke point they all pass through — the project
14
+ * travels in an async context for the duration of one request or tool call.
15
+ * `AsyncLocalStorage` rather than a module variable because one Host process
16
+ * serves several Sessions at once: a global would let two projects' writes
17
+ * interleave into the wrong directory, which is the one failure this must not
18
+ * have.
19
+ *
20
+ * The three callers converge on one question, asked in one place:
21
+ * {@link projectScopeOf} turns "this Session" into "this project directory" by
22
+ * looking the Session up in the Host's registry and reading its **header**. A
23
+ * tool and a browser request naming the same Session therefore mean the same
24
+ * directory by construction rather than by both computing it the same way.
25
+ *
26
+ * @module dsh-data-table/store/project
27
+ */
28
+ import { AsyncLocalStorage } from 'node:async_hooks'
29
+ import { join } from 'node:path'
30
+ import { STORAGES_DIRECTORY, isProjectDirectory } from '#contracts'
31
+
32
+ /** This plugin's directory inside a project's `storages/`. */
33
+ export const PLUGIN_DIRECTORY = 'data-table'
34
+
35
+ const scope = new AsyncLocalStorage<string>()
36
+
37
+ /**
38
+ * Run one unit of work with a project root in scope.
39
+ *
40
+ * @param root - Absolute project directory.
41
+ * @param body - The work; may be async, and keeps the scope across its awaits.
42
+ * @returns whatever `body` returns.
43
+ */
44
+ export function runInProject<T>(root: string, body: () => T): T {
45
+ return scope.run(root, body)
46
+ }
47
+
48
+ /**
49
+ * The project root in scope, when there is one.
50
+ *
51
+ * `undefined` means "no project": a script, a test, or a caller that has not been
52
+ * migrated. `workbookRoot()` answers that case with the pre-project location
53
+ * rather than inventing a project, so an un-migrated caller keeps reading and
54
+ * writing where it always did instead of quietly starting a new store.
55
+ */
56
+ export function currentProjectRoot(): string | undefined {
57
+ return scope.getStore()
58
+ }
59
+
60
+ /**
61
+ * The bits of DSH's Session registry this module reads.
62
+ *
63
+ * Read structurally — it arrives from the Host, and a caller may run without it
64
+ * (a test harness, an embedding with no Host services).
65
+ */
66
+ export interface SessionRegistry {
67
+ /** Look one Session up by id; `undefined` when no such Session is live. */
68
+ get?: (id: string) => { readonly header?: { readonly cwd?: unknown } } | undefined
69
+ }
70
+
71
+ /**
72
+ * The project directory one Session's store calls belong to, or `undefined` for
73
+ * "no project".
74
+ *
75
+ * The Session **header** is the only source, and it is read through the Host's
76
+ * registry (`ctx.sessions.get(id)`) rather than off the calling Agent's own
77
+ * Session object. That is the fix for a real bug (2026-10-02): the tools read
78
+ * `agent.session.cwd`, a field a Session does not have — a Session carries its
79
+ * directory at `session.header.cwd` — so every tool call answered "no project"
80
+ * and wrote to the per-user store while the browser panel wrote to
81
+ * `<project>/storages/data-table`. The Agent and the open Table were editing two
82
+ * different sets of workbooks, and neither could see the other's.
83
+ *
84
+ * Reading the registry also makes the two transports agree by construction: an
85
+ * HTTP request names a Session id, a tool call names the same id through its
86
+ * Agent, and both are answered by the same header. `data-excalidraw` resolves its
87
+ * scene directory the same way, for the same reason.
88
+ *
89
+ * The Host's sandbox policy is deliberately *not* consulted: a plugin context
90
+ * cannot read it (see `src/index.ts`), and the one attempt to use it as a
91
+ * fallback turned every workbook list into a bodyless 400.
92
+ *
93
+ * `undefined` is a real answer and not a failure: it means the caller named no
94
+ * Session, so nobody has said which project this is. The two ways to get it
95
+ * wrong are both worse than a missing project — `process.cwd()` would silently
96
+ * start a *new* per-project store for a caller that was reading the old per-user
97
+ * one (a test, a script, an older client), and a shared per-process default
98
+ * would mix two Sessions' writes. Callers therefore leave the scope unset, and
99
+ * `workbookRoot()` answers with the pre-project location it always used.
100
+ *
101
+ * @param sessions - DSH's Session registry, from the injected `sessions` service.
102
+ * @param sessionId - The Session naming the project; `undefined` or empty for a
103
+ * caller that named none.
104
+ * @returns an absolute project directory, or `undefined` when nothing named one.
105
+ */
106
+ export function projectScopeOf(sessions: SessionRegistry | undefined, sessionId: string | undefined): string | undefined {
107
+ if (sessionId === undefined || sessionId.length === 0) return undefined
108
+ const cwd = sessions?.get?.(sessionId)?.header?.cwd
109
+ // A filesystem root counts as "not named": treating `/` as a project root
110
+ // meant creating `/storages/data-table`.
111
+ return isProjectDirectory(cwd) ? cwd : undefined
112
+ }
113
+
114
+ /**
115
+ * This plugin's storage inside one project.
116
+ *
117
+ * @param root - Project directory.
118
+ * @returns `<root>/storages/data-table`, which may not exist yet.
119
+ */
120
+ export function projectStoragesRoot(root: string): string {
121
+ return join(root, STORAGES_DIRECTORY, PLUGIN_DIRECTORY)
122
+ }
123
+
124
+ /**
125
+ * The project's storage container, shared by every plugin.
126
+ *
127
+ * The file chooser lists this directory rather than the project root: it is
128
+ * where a project's data lives, and it is what a copy of the project directory
129
+ * carries. A user who wants a file from elsewhere uses the "从项目外导入" path,
130
+ * which is deliberately unrestricted.
131
+ *
132
+ * @param root - Project directory.
133
+ * @returns `<root>/storages`, which may not exist yet.
134
+ */
135
+ export function projectStoragesDirectory(root: string): string {
136
+ return join(root, STORAGES_DIRECTORY)
137
+ }