@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,454 @@
1
+ /**
2
+ * Reading a workbook, its sidecar index, and its catalog (docs/issue/0022 P1).
3
+ *
4
+ * The in-process cache lives here because the DSH host is the single mutation
5
+ * authority: a reader either sees a workbook's committed state or misses, and
6
+ * `store/write.ts` is the only thing that invalidates it.
7
+ *
8
+ * The write-ahead journal's *lock* and *recovery* are here too: `readWorkbook`
9
+ * must finish an interrupted commit before it reads, and `store/write.ts`
10
+ * takes the same lock to commit, so the two cannot race. The direction is
11
+ * one-way — `read` never imports `write`.
12
+ */
13
+ import { DataSheetError } from '../errors.ts'
14
+ import { access, readFile, readdir, rename, unlink } from 'node:fs/promises'
15
+ import {
16
+ type SheetSchema,
17
+ type WorkbookChange,
18
+ type WorkbookMeta,
19
+ type WorkbookOperation,
20
+ type WorkbookOperationType,
21
+ type WorkbookProposal,
22
+ type WorkbookSummary,
23
+ type WorkbookWorktree,
24
+ type WorkbookWorktreeStatus,
25
+ type WorkbookWorktreeSummary,
26
+ } from '../types.ts'
27
+ import { normalizeWorkbookId } from '../contract/ids.ts'
28
+ import { cloneSnapshot, isSheetSchema, normalizeSnapshotName } from '../contract/snapshots.ts'
29
+ import { applyWorkbookOperation } from '../contract/mutations.ts'
30
+ import {
31
+ workbookCheckpointsPath,
32
+ workbookIndexPath,
33
+ workbookOperationsPath,
34
+ workbookPath,
35
+ workbookProposalsPath,
36
+ workbookSnapshotPath,
37
+ workbookTransactionPath,
38
+ workbookRoot,
39
+ workbookWorktreeDirectory,
40
+ workbookWorktreePath,
41
+ } from './paths.ts'
42
+
43
+ /** Whether a workbook file exists. */
44
+ export async function workbookExists(id: string): Promise<boolean> {
45
+ return fileExists(workbookPath(id))
46
+ }
47
+
48
+ /** Read only the sidecar index; a missing/invalid sidecar falls back safely. */
49
+ export async function readWorkbookIndex(id: string): Promise<WorkbookIndex | null> {
50
+ try {
51
+ await withWorkbookTransactionQueue(normalizeWorkbookId(id), () => recoverWorkbookTransactionNow(normalizeWorkbookId(id)))
52
+ const parsed = JSON.parse(await readFile(workbookIndexPath(id), 'utf8')) as Partial<WorkbookIndex>
53
+ if (typeof parsed.id !== 'string' || typeof parsed.name !== 'string' || !Number.isInteger(parsed.revision)
54
+ || !Array.isArray(parsed.sheets) || !Array.isArray(parsed.operations) || !Array.isArray(parsed.proposals)) return null
55
+ return {
56
+ id: parsed.id,
57
+ name: parsed.name,
58
+ sheetOrder: Array.isArray(parsed.sheetOrder) ? parsed.sheetOrder.filter(item => typeof item === 'string') : parsed.sheets.map(sheet => sheet.id),
59
+ sheets: parsed.sheets.filter(isStoredSheetSummary),
60
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
61
+ revision: parsed.revision as number,
62
+ operations: parsed.operations.filter(operation => isStoredIndexOperation(operation)),
63
+ proposals: parsed.proposals.filter(isStoredIndexProposal),
64
+ }
65
+ } catch (error) {
66
+ // A missing or malformed sidecar is not an error: the caller learns "no
67
+ // workbook here" and moves on. A *corrupt transaction journal* is, and it
68
+ // used to be swallowed by this same catch — so a workbook that still had
69
+ // every one of its files simply disappeared from the catalog and
70
+ // `workbook_read` answered 「工作簿不存在」, three hand-written error messages
71
+ // away from the reader.
72
+ if (error instanceof DataSheetError) throw error
73
+ return null
74
+ }
75
+ }
76
+
77
+ /** Read one workbook meta. */
78
+ export async function readWorkbook(id: string, options: { includeCheckpoints?: boolean } = {}): Promise<WorkbookMeta | null> {
79
+ try {
80
+ await withWorkbookTransactionQueue(normalizeWorkbookId(id), () => recoverWorkbookTransactionNow(normalizeWorkbookId(id)))
81
+ const cached = workbookCache.get(normalizeWorkbookId(id))
82
+ if (cached !== undefined) {
83
+ if (options.includeCheckpoints === true && !cached.checkpointsLoaded) {
84
+ const stored = await readJsonFile(workbookCheckpointsPath(id)) as { checkpoints?: unknown } | null
85
+ cached.meta.checkpoints = Array.isArray(stored?.checkpoints)
86
+ ? stored.checkpoints.filter(checkpoint => isStoredCheckpoint(checkpoint))
87
+ : []
88
+ cached.checkpointsLoaded = true
89
+ }
90
+ return cached.meta
91
+ }
92
+ const raw = await readFile(workbookPath(id), 'utf8')
93
+ const parsed = JSON.parse(raw) as WorkbookMeta
94
+ if (typeof parsed.id !== 'string' || typeof parsed.name !== 'string') return null
95
+ let snapshot = parsed.snapshot
96
+ let snapshotRevision = 0
97
+ if (snapshot === undefined) {
98
+ const stored = await readJsonFile(workbookSnapshotPath(id)) as { revision?: unknown; snapshot?: unknown } | null
99
+ if (stored === null || !Object.prototype.hasOwnProperty.call(stored, 'snapshot')) return null
100
+ snapshot = stored.snapshot
101
+ snapshotRevision = typeof stored.revision === 'number' && Number.isInteger(stored.revision) ? stored.revision : 0
102
+ }
103
+ const storedCheckpoints = options.includeCheckpoints === true
104
+ ? await readJsonFile(workbookCheckpointsPath(id)) as { checkpoints?: unknown } | null
105
+ : null
106
+ const rawCheckpoints = Array.isArray(parsed.checkpoints)
107
+ ? parsed.checkpoints
108
+ : Array.isArray(storedCheckpoints?.checkpoints) ? storedCheckpoints.checkpoints : []
109
+ const storedOperations = Array.isArray(parsed.operations)
110
+ ? parsed.operations
111
+ : await readJsonFile(workbookOperationsPath(id))
112
+ const operations = Array.isArray(storedOperations)
113
+ ? storedOperations.filter(operation => isStoredOperation(operation))
114
+ : []
115
+ const storedProposals = Array.isArray(parsed.proposals)
116
+ ? parsed.proposals
117
+ : await readJsonFile(workbookProposalsPath(id))
118
+ if (parsed.snapshot === undefined && snapshotRevision < (parsed.revision ?? 0)) {
119
+ for (const operation of operations) {
120
+ if (operation.revision <= snapshotRevision) continue
121
+ snapshot = applyWorkbookOperation(snapshot, operation)
122
+ }
123
+ }
124
+ const result: WorkbookMeta = {
125
+ ...parsed,
126
+ snapshot: normalizeSnapshotName(snapshot, parsed.name),
127
+ revision: Number.isInteger(parsed.revision) && (parsed.revision as number) >= 0 ? parsed.revision : 0,
128
+ snapshotRevision: parsed.snapshot === undefined ? snapshotRevision : (typeof parsed.snapshotRevision === 'number' ? parsed.snapshotRevision : parsed.revision ?? 0),
129
+ operations,
130
+ proposals: Array.isArray(storedProposals)
131
+ ? storedProposals.filter(proposal => isStoredProposal(proposal))
132
+ : [],
133
+ checkpoints: rawCheckpoints.filter(checkpoint => isStoredCheckpoint(checkpoint)),
134
+ }
135
+ workbookCache.set(normalizeWorkbookId(id), { meta: result, checkpointsLoaded: Array.isArray(parsed.checkpoints) || options.includeCheckpoints === true })
136
+ return result
137
+ } catch (error) {
138
+ // See `readWorkbookIndex`: only the deliberate persistence failures escape.
139
+ if (error instanceof DataSheetError) throw error
140
+ return null
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Reconstruct a historical workbook revision from the nearest checkpoint and
146
+ * the retained operation tail. A null result means the requested revision has
147
+ * been compacted or contains an opaque snapshot replacement.
148
+ */
149
+ export async function readWorkbookAtRevision(workbookId: string, targetRevision: number): Promise<{ snapshot: unknown; revision: number } | null> {
150
+ const meta = await readWorkbook(workbookId, { includeCheckpoints: true })
151
+ if (meta === null) return null
152
+ const currentRevision = meta.revision ?? 0
153
+ if (!Number.isInteger(targetRevision) || targetRevision < 0 || targetRevision > currentRevision) return null
154
+ if (targetRevision === currentRevision) return { snapshot: cloneSnapshot(meta.snapshot), revision: currentRevision }
155
+
156
+ // The snapshot sidecar is itself a durable checkpoint. Older workbooks may
157
+ // have been written before the in-memory checkpoint index existed, so use it
158
+ // directly when the requested revision is exactly the sidecar revision.
159
+ const sidecar = await readJsonFile(workbookSnapshotPath(workbookId)) as { revision?: unknown; snapshot?: unknown } | null
160
+ if (sidecar !== null && sidecar.snapshot !== undefined && sidecar.revision === targetRevision) {
161
+ return { snapshot: cloneSnapshot(sidecar.snapshot), revision: targetRevision }
162
+ }
163
+
164
+ const checkpoint = [...(meta.checkpoints ?? [])]
165
+ .filter(item => item.revision <= targetRevision)
166
+ .sort((left, right) => right.revision - left.revision)[0]
167
+ if (checkpoint === undefined) return null
168
+ let snapshot = cloneSnapshot(checkpoint.snapshot)
169
+ const operations = (meta.operations ?? [])
170
+ .filter(operation => operation.revision > checkpoint.revision && operation.revision <= targetRevision)
171
+ .sort((left, right) => left.revision - right.revision)
172
+ if (operations.length !== targetRevision - checkpoint.revision) return null
173
+ for (const operation of operations) {
174
+ if (operation.type === 'snapshot_replace' && operation.payload.snapshot === undefined) return null
175
+ snapshot = applyWorkbookOperation(snapshot, operation)
176
+ }
177
+ return { snapshot, revision: targetRevision }
178
+ }
179
+
180
+ /** List workbook ids (files in the repo root, sorted by name). */
181
+ export async function listWorkbooks(): Promise<string[]> {
182
+ try {
183
+ const entries = await readdir(workbookRoot(), { withFileTypes: true })
184
+ return entries
185
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json')
186
+ && !entry.name.endsWith('.index.json') && !entry.name.endsWith('.snapshot.json')
187
+ && !entry.name.endsWith('.checkpoints.json') && !entry.name.endsWith('.operations.json')
188
+ && !entry.name.endsWith('.proposals.json') && !entry.name.endsWith('.transaction.json'))
189
+ .map(entry => entry.name.slice(0, -'.json'.length))
190
+ .sort()
191
+ } catch {
192
+ return []
193
+ }
194
+ }
195
+
196
+ /** List workbook summaries for the browser picker and other read-only clients. */
197
+ export async function listWorkbookSummaries(): Promise<WorkbookSummary[]> {
198
+ const summaries: WorkbookSummary[] = []
199
+ for (const id of await listWorkbooks()) {
200
+ const index = await readWorkbookIndex(id)
201
+ if (index !== null) summaries.push({ id: index.id, name: index.name, sheets: index.sheets, updatedAt: index.updatedAt, revision: index.revision })
202
+ else {
203
+ const meta = await readWorkbook(id)
204
+ if (meta !== null) summaries.push(summarize(meta))
205
+ }
206
+ }
207
+ return summaries
208
+ }
209
+
210
+ /** List worktrees belonging to a workbook, optionally scoped to one session. */
211
+ export async function listWorkbookWorktrees(workbookId: string, ownerSessionId?: string): Promise<WorkbookWorktreeSummary[]> {
212
+ const normalizedWorkbookId = normalizeWorkbookId(workbookId)
213
+ try {
214
+ const entries = await readdir(workbookWorktreeDirectory(normalizedWorkbookId), { withFileTypes: true })
215
+ const result: WorkbookWorktreeSummary[] = []
216
+ for (const entry of entries) {
217
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue
218
+ const worktree = await readWorkbookWorktree(normalizedWorkbookId, entry.name.slice(0, -'.json'.length))
219
+ if (worktree === null || ownerSessionId !== undefined && worktree.ownerSessionId !== ownerSessionId) continue
220
+ result.push(toWorktreeSummary(worktree))
221
+ }
222
+ return result.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
223
+ } catch {
224
+ return []
225
+ }
226
+ }
227
+
228
+ /** Read one isolated worktree, including its snapshot and operation tail. */
229
+ export async function readWorkbookWorktree(workbookId: string, worktreeId: string): Promise<WorkbookWorktree | null> {
230
+ try {
231
+ const parsed = JSON.parse(await readFile(workbookWorktreePath(workbookId, worktreeId), 'utf8')) as Partial<WorkbookWorktree>
232
+ if (typeof parsed.id !== 'string' || typeof parsed.workbookId !== 'string'
233
+ || typeof parsed.ownerSessionId !== 'string' || typeof parsed.name !== 'string'
234
+ || !Number.isInteger(parsed.baseRevision) || !Number.isInteger(parsed.revision)
235
+ || !isWorktreeStatus(parsed.status) || typeof parsed.createdAt !== 'string'
236
+ || typeof parsed.updatedAt !== 'string' || parsed.snapshot === undefined) return null
237
+ return {
238
+ id: parsed.id,
239
+ workbookId: normalizeWorkbookId(parsed.workbookId),
240
+ ownerSessionId: parsed.ownerSessionId,
241
+ name: parsed.name,
242
+ baseRevision: parsed.baseRevision as number,
243
+ revision: parsed.revision as number,
244
+ status: parsed.status,
245
+ createdAt: parsed.createdAt,
246
+ updatedAt: parsed.updatedAt,
247
+ snapshot: parsed.snapshot,
248
+ operations: Array.isArray(parsed.operations) ? parsed.operations.filter(isStoredOperation) : [],
249
+ }
250
+ } catch {
251
+ return null
252
+ }
253
+ }
254
+
255
+ export async function readJsonFile(path: string): Promise<unknown | null> {
256
+ try { return JSON.parse(await readFile(path, 'utf8')) as unknown } catch { return null }
257
+ }
258
+
259
+ export async function fileExists(path: string): Promise<boolean> {
260
+ try { await access(path); return true } catch { return false }
261
+ }
262
+
263
+ /** Derive a lightweight summary from a stored workbook. */
264
+ export function summarize(meta: WorkbookMeta): WorkbookSummary {
265
+ const snap = meta.snapshot as {
266
+ sheetOrder?: readonly string[]
267
+ sheets?: Record<string, { id?: string; name?: string; dataSheetSchema?: unknown }>
268
+ } | null
269
+ const order = snap?.sheetOrder ?? Object.keys(snap?.sheets ?? {})
270
+ const sheets = order.map(id => ({
271
+ id,
272
+ name: snap?.sheets?.[id]?.name ?? id,
273
+ ...(isSheetSchema(snap?.sheets?.[id]?.dataSheetSchema) ? { schema: snap?.sheets?.[id]?.dataSheetSchema } : {}),
274
+ }))
275
+ return {
276
+ id: meta.id,
277
+ name: meta.name,
278
+ sheets,
279
+ updatedAt: meta.updatedAt,
280
+ revision: meta.revision ?? 0,
281
+ }
282
+ }
283
+
284
+ export async function withWorkbookTransactionQueue<T>(id: string, task: () => Promise<T>): Promise<T> {
285
+ const previous = workbookTransactionQueues.get(id) ?? Promise.resolve()
286
+ const current = previous.catch(() => undefined).then(task)
287
+ workbookTransactionQueues.set(id, current)
288
+ try { return await current } finally {
289
+ if (workbookTransactionQueues.get(id) === current) workbookTransactionQueues.delete(id)
290
+ }
291
+ }
292
+
293
+ /** Finish a prepared multi-file commit after a process interruption. */
294
+ export async function recoverWorkbookTransactionNow(id: string): Promise<void> {
295
+ const journalPath = workbookTransactionPath(id)
296
+ let transaction: StoredWorkbookTransaction
297
+ try {
298
+ transaction = JSON.parse(await readFile(journalPath, 'utf8')) as StoredWorkbookTransaction
299
+ } catch (error) {
300
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
301
+ throw new DataSheetError('PERSISTENCE_FAILED', `工作簿事务日志损坏:${id}`, 500)
302
+ }
303
+ if (!Number.isInteger(transaction.revision) || !Array.isArray(transaction.files) || transaction.files.some(file => typeof file?.target !== 'string' || typeof file?.temp !== 'string')) {
304
+ throw new DataSheetError('PERSISTENCE_FAILED', `工作簿事务日志无效:${id}`, 500)
305
+ }
306
+ for (const file of transaction.files) {
307
+ if (await fileExists(file.temp)) await rename(file.temp, file.target)
308
+ else if (!(await fileExists(file.target))) throw new DataSheetError('PERSISTENCE_FAILED', `工作簿事务缺少文件:${file.target}`, 500)
309
+ }
310
+ await unlink(journalPath).catch(error => {
311
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
312
+ })
313
+ workbookCache.delete(normalizeWorkbookId(id))
314
+ }
315
+
316
+ /**
317
+ * Mirrored by the browser projection's `applyAppendRows`, which runs the same
318
+ * check against live Univer state. The hint names `headerRowIndex` because that
319
+ * is what unblocks a report sheet whose real header is not row 0.
320
+ */
321
+
322
+ export function isStoredOperation(value: unknown): value is WorkbookOperation {
323
+ if (typeof value !== 'object' || value === null) return false
324
+ const operation = value as Partial<WorkbookOperation>
325
+ return typeof operation.opId === 'string'
326
+ && typeof operation.workbookId === 'string'
327
+ && typeof operation.revision === 'number'
328
+ && (operation.actor === 'agent' || operation.actor === 'user')
329
+ && isKnownOperationType(operation.type)
330
+ && typeof operation.timestamp === 'string'
331
+ && typeof operation.payload === 'object'
332
+ && operation.payload !== null
333
+ }
334
+
335
+ export function isStoredIndexOperation(value: unknown): value is WorkbookIndexOperation {
336
+ if (typeof value !== 'object' || value === null) return false
337
+ const operation = value as Partial<WorkbookIndexOperation>
338
+ return typeof operation.opId === 'string'
339
+ && typeof operation.workbookId === 'string'
340
+ && typeof operation.revision === 'number'
341
+ && (operation.actor === 'agent' || operation.actor === 'user')
342
+ && isKnownOperationType(operation.type)
343
+ && typeof operation.timestamp === 'string'
344
+ && typeof operation.summary === 'string'
345
+ }
346
+
347
+ export function isKnownOperationType(value: unknown): value is WorkbookOperationType {
348
+ return value === 'update_cells' || value === 'clear_range' || value === 'delete_cells'
349
+ || value === 'insert_rows' || value === 'delete_rows' || value === 'insert_columns'
350
+ || value === 'delete_columns' || value === 'create_sheet' || value === 'delete_sheet'
351
+ || value === 'rename_sheet' || value === 'append_rows' || value === 'format_cells'
352
+ || value === 'merge_cells' || value === 'unmerge_cells' || value === 'freeze_panes'
353
+ || value === 'set_column_width' || value === 'set_row_height' || value === 'sort_range'
354
+ || value === 'set_hyperlink' || value === 'remove_hyperlink' || value === 'remove_duplicates'
355
+ || value === 'batch' || value === 'snapshot_replace'
356
+ }
357
+
358
+ export function isStoredSheetSummary(value: unknown): value is { id: string; name: string; schema?: SheetSchema } {
359
+ if (typeof value !== 'object' || value === null) return false
360
+ const sheet = value as { id?: unknown; name?: unknown; schema?: unknown }
361
+ return typeof sheet.id === 'string' && typeof sheet.name === 'string'
362
+ && (sheet.schema === undefined || isSheetSchema(sheet.schema))
363
+ }
364
+
365
+ export function isStoredIndexProposal(value: unknown): value is WorkbookIndexProposal {
366
+ if (typeof value !== 'object' || value === null) return false
367
+ const proposal = value as Partial<WorkbookIndexProposal>
368
+ return typeof proposal.id === 'string' && typeof proposal.workbookId === 'string'
369
+ && (proposal.actor === 'agent' || proposal.actor === 'user')
370
+ && typeof proposal.baseRevision === 'number' && typeof proposal.createdAt === 'string'
371
+ && typeof proposal.updatedAt === 'string'
372
+ && (proposal.status === 'pending' || proposal.status === 'committed' || proposal.status === 'rejected' || proposal.status === 'expired')
373
+ && typeof proposal.summary === 'string' && Array.isArray(proposal.changes)
374
+ }
375
+
376
+ export function isStoredProposal(value: unknown): value is WorkbookProposal {
377
+ if (typeof value !== 'object' || value === null) return false
378
+ const proposal = value as Partial<WorkbookProposal>
379
+ return typeof proposal.id === 'string' && typeof proposal.workbookId === 'string'
380
+ && (proposal.actor === 'agent' || proposal.actor === 'user')
381
+ && typeof proposal.baseRevision === 'number' && typeof proposal.createdAt === 'string'
382
+ && typeof proposal.updatedAt === 'string'
383
+ && (proposal.status === 'pending' || proposal.status === 'committed' || proposal.status === 'rejected' || proposal.status === 'expired')
384
+ && Array.isArray(proposal.changes) && 'snapshot' in proposal
385
+ }
386
+
387
+ export function isStoredCheckpoint(value: unknown): value is { revision: number; actor: 'agent' | 'user'; timestamp: string; snapshot: unknown } {
388
+ if (typeof value !== 'object' || value === null) return false
389
+ const checkpoint = value as { revision?: unknown; actor?: unknown; timestamp?: unknown; snapshot?: unknown }
390
+ return typeof checkpoint.revision === 'number'
391
+ && (checkpoint.actor === 'agent' || checkpoint.actor === 'user')
392
+ && typeof checkpoint.timestamp === 'string'
393
+ && 'snapshot' in checkpoint
394
+ }
395
+
396
+ export interface WorkbookIndex {
397
+ id: string
398
+ name: string
399
+ sheetOrder: string[]
400
+ sheets: Array<{ id: string; name: string; schema?: SheetSchema }>
401
+ updatedAt: string
402
+ revision: number
403
+ operations: WorkbookIndexOperation[]
404
+ proposals: WorkbookIndexProposal[]
405
+ }
406
+
407
+ /** Payload-free operation summary used by catalog/history bootstrap. */
408
+ export interface WorkbookIndexOperation {
409
+ opId: string
410
+ workbookId: string
411
+ revision: number
412
+ actor: 'agent' | 'user'
413
+ type: WorkbookOperationType
414
+ timestamp: string
415
+ summary: string
416
+ }
417
+
418
+ export interface WorkbookIndexProposal {
419
+ id: string
420
+ workbookId: string
421
+ actor: 'agent' | 'user'
422
+ baseRevision: number
423
+ createdAt: string
424
+ updatedAt: string
425
+ status: 'pending' | 'committed' | 'rejected' | 'expired'
426
+ summary: string
427
+ changes: WorkbookChange[]
428
+ committedRevision?: number
429
+ }
430
+
431
+ export interface WorkbookTransactionFile {
432
+ target: string
433
+ value: unknown
434
+ }
435
+
436
+ export interface StoredWorkbookTransaction {
437
+ revision: number
438
+ files: Array<{ target: string; temp: string }>
439
+ }
440
+
441
+ /** In-process cache: the DSH host is the single mutation authority. */
442
+ export const workbookCache = new Map<string, { meta: WorkbookMeta; checkpointsLoaded: boolean }>()
443
+
444
+ /** Serialize transaction recovery and commits independently from mutations. */
445
+ export const workbookTransactionQueues = new Map<string, Promise<unknown>>()
446
+
447
+ export function isWorktreeStatus(value: unknown): value is WorkbookWorktreeStatus {
448
+ return value === 'draft' || value === 'ready' || value === 'merged' || value === 'discarded'
449
+ }
450
+
451
+ export function toWorktreeSummary(worktree: WorkbookWorktree): WorkbookWorktreeSummary {
452
+ const { snapshot: _snapshot, operations: _operations, ...summary } = worktree
453
+ return summary
454
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * How long history is kept (docs/issue/0022 P1).
3
+ *
4
+ * Two rules, both policy rather than mechanism: how often a rebuildable
5
+ * checkpoint is taken, and how old a Version's last Step must be before the
6
+ * session that produced it is considered over. Both are read by the write path
7
+ * and by the version session, so they live below both.
8
+ */
9
+ import { readdir, rm, stat } from 'node:fs/promises'
10
+ import { join } from 'node:path'
11
+ import { AUTO_CHECKPOINT_KEEP, VERSION_IDLE_CLOSE_MS, selectAutoCheckpointsToPrune } from '#contracts'
12
+ import { toolOutputRoot } from './paths.ts'
13
+ import { versionKindOfOperations } from '../contract/operations.ts'
14
+ import type { WorkbookMeta, WorkbookOperation } from '../types.ts'
15
+
16
+ /**
17
+ * Drop the operations of auto checkpoints outside the retention window
18
+ * (PRD v3.1 §6.2, D3: newest 100 + the last one of every older day).
19
+ *
20
+ * Only history a checkpoint can rebuild from is removed: nothing at or above
21
+ * `newestCheckpoint - CHECKPOINT_INTERVAL` is touched, so every recent Version
22
+ * still previews and restores exactly as before. Older auto Versions disappear
23
+ * from the list instead of lingering as rows that can no longer be replayed;
24
+ * named Versions and anything a survivor references are always kept.
25
+ */
26
+ export function pruneAutoVersionOperations(meta: WorkbookMeta): number {
27
+ const operations = meta.operations ?? []
28
+ // A conservative early exit, and `AUTO_CHECKPOINT_KEEP` rather than a literal
29
+ // 100: the policy always keeps the newest N *Versions*, and a Version is one or
30
+ // more operations, so fewer operations than N means fewer Versions than N and
31
+ // nothing can be prunable. The count is of operations, the bound is a Version
32
+ // count, and that is only sound for this direction — which is why the constant
33
+ // is the right thing to name here and a bare `100` was not.
34
+ if (operations.length < AUTO_CHECKPOINT_KEEP) return 0
35
+ const checkpoints = (meta.checkpoints ?? []).map(checkpoint => checkpoint.revision).sort((left, right) => left - right)
36
+ const newest = checkpoints.at(-1)
37
+ if (newest === undefined) return 0
38
+ const cutoff = newest - CHECKPOINT_INTERVAL
39
+ if (cutoff <= 0) return 0
40
+ const versionIdOf = (operation: WorkbookOperation): string => operation.versionId ?? `revision:${operation.revision}`
41
+ const byVersion = new Map<string, WorkbookOperation[]>()
42
+ for (const operation of operations) {
43
+ const id = versionIdOf(operation)
44
+ const group = byVersion.get(id)
45
+ if (group === undefined) byVersion.set(id, [operation])
46
+ else group.push(operation)
47
+ }
48
+ const versions = [...byVersion.entries()].map(([versionId, group], index) => ({
49
+ versionId,
50
+ number: index + 1,
51
+ status: (group.at(-1)?.versionStatus === 'final' ? 'committed' : 'open') as 'open' | 'committed',
52
+ kind: versionKindOfOperations(group),
53
+ createdAt: group[0]?.timestamp,
54
+ }))
55
+ const prunable = new Set(selectAutoCheckpointsToPrune(versions).map(version => version.versionId))
56
+ if (prunable.size === 0) return 0
57
+ const kept = operations.filter(operation => {
58
+ const id = versionIdOf(operation)
59
+ if (!prunable.has(id)) return true
60
+ // Only drop what a checkpoint can rebuild from.
61
+ return operation.revision > cutoff
62
+ })
63
+ if (kept.length === operations.length) return 0
64
+ meta.operations = kept
65
+ return operations.length - kept.length
66
+ }
67
+
68
+ /** Whether an operation is older than the shared Version merge window (§4.1). */
69
+ export function isIdlePast(timestamp: string, nowMs = Date.now()): boolean {
70
+ const at = Date.parse(timestamp)
71
+ return Number.isFinite(at) && nowMs - at >= VERSION_IDLE_CLOSE_MS
72
+ }
73
+
74
+ export const CHECKPOINT_INTERVAL = 20
75
+
76
+ /**
77
+ * How many spilled tool results to keep.
78
+ *
79
+ * They exist so the model can page through a result that was too large to inline
80
+ * — a debugging and continuity aid, not data. Nothing pruned them: every
81
+ * oversized `workbook_read` wrote a file under the storage root that stayed
82
+ * forever, which is disk growth with no ceiling and no way for a user to know
83
+ * why their storage directory kept getting bigger.
84
+ */
85
+ export const TOOL_OUTPUT_KEEP = 50
86
+
87
+ /** Delete all but the newest `keep` spilled results. Returns how many went. */
88
+ export async function pruneToolOutput(keep = TOOL_OUTPUT_KEEP): Promise<number> {
89
+ const directory = toolOutputRoot()
90
+ const names = await readdir(directory).catch(() => [])
91
+ const files = []
92
+ for (const name of names) {
93
+ if (!name.endsWith('.json')) continue
94
+ const full = join(directory, name)
95
+ const info = await stat(full).catch(() => null)
96
+ if (info !== null && info.isFile()) files.push({ full, at: info.mtimeMs })
97
+ }
98
+ if (files.length <= keep) return 0
99
+ files.sort((left, right) => right.at - left.at)
100
+ let removed = 0
101
+ for (const file of files.slice(keep)) {
102
+ await rm(file.full, { force: true }).catch(() => undefined)
103
+ removed += 1
104
+ }
105
+ return removed
106
+ }