@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,322 @@
1
+ /**
2
+ * The workbook's own routes (docs/issue/0022 P3).
3
+ *
4
+ * Snapshot read/write, the incremental `changes` cursor, one bounded range read,
5
+ * the operation append, proposals and rollback — everything a browser or another
6
+ * plugin uses to read and write the trunk workbook.
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from 'node:http'
9
+ import { DataSheetError } from '../errors.ts'
10
+ import { lineageOfValue } from '../audit.ts'
11
+ import {
12
+ deleteWorkbook,
13
+ readWorkbook,
14
+ readWorkbookIndex,
15
+ commitWorkbookProposal,
16
+ rejectWorkbookProposal,
17
+ rollbackWorkbook,
18
+ mutateWorkbook,
19
+ } from '../workbook-manager.ts'
20
+ import { parseIndex, parseRange, isOperationType, asOperationPayload, optionalRevision } from '../request-params.ts'
21
+ import { readWorkbookRange, workbookExists } from '../workbook-manager.ts'
22
+ import { usedExtent } from '../contract/snapshots.ts'
23
+ import { parseRevision } from './routes.ts'
24
+ import { standaloneWorkbookHtml } from './standalone.ts'
25
+ import { readJsonBody, requireDataSheetSession, writeOperationError, type WorkbookRoute } from './routes.ts'
26
+ import type { SheetRangeResponse } from '#contracts'
27
+
28
+ export async function handleTrunkRoute(route: WorkbookRoute, req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean> {
29
+ const id = route.id
30
+
31
+ if (route.kind === 'view' && req.method === 'GET') {
32
+ const id = route.id as string
33
+ const meta = await readWorkbook(id)
34
+ const title = meta?.name ?? id
35
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' })
36
+ res.end(standaloneWorkbookHtml(title))
37
+ return true
38
+ }
39
+
40
+ if (route.kind === 'snapshot' && (req.method === 'DELETE' || req.method === 'POST' && url.searchParams.get('action') === 'delete')) {
41
+ let body: Record<string, unknown> = {}
42
+ try { body = await readJsonBody(req) } catch { /* confirmation is checked below */ }
43
+ if (body.confirm !== true) {
44
+ res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
45
+ res.end(JSON.stringify({ ok: false, error: '删除工作簿必须显式传入 confirm: true' }))
46
+ return true
47
+ }
48
+ try {
49
+ if (!await workbookExists(id)) {
50
+ res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' })
51
+ res.end(JSON.stringify({ ok: false, error: `工作簿不存在:${id}` }))
52
+ return true
53
+ }
54
+ await deleteWorkbook(id)
55
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
56
+ res.end(JSON.stringify({ ok: true, workbookId: id }))
57
+ } catch (error) {
58
+ writeOperationError(res, error)
59
+ }
60
+ return true
61
+ }
62
+
63
+ if (route.kind === 'range') {
64
+ const sessionId = requireDataSheetSession(req, res, url)
65
+ if (sessionId === null) return true
66
+ if (req.method !== 'GET') { res.writeHead(405); res.end(); return true }
67
+ const id = route.id as string
68
+ try {
69
+ const meta = await readWorkbook(id)
70
+ if (meta === null) { writeOperationError(res, new DataSheetError('WORKBOOK_NOT_FOUND', '工作簿不存在', 404)); return true }
71
+ const sheetRef = url.searchParams.get('sheet') ?? ''
72
+ const rangeParam = url.searchParams.get('range') ?? ''
73
+ // A bounded, read-only snapshot of one range: this is how another
74
+ // plugin materializes a Sheet as its own data source (§8.6).
75
+ // No range asked for means *the sheet*, so the extent is measured rather
76
+ // than assumed. This used to default to `A1:Z1000` — rows 0–999 and columns
77
+ // A–Z — which silently handed back a prefix of any larger sheet, and the
78
+ // `truncated` flag below could not say so because it compared a value with
79
+ // itself (`values.length - 1 < endRow - startRow` is `n < n`). Data
80
+ // were being dropped before the loader ever saw them.
81
+ const requested = rangeParam.trim() === '' ? undefined : parseRange(rangeParam)
82
+ const extent = usedExtent(meta.snapshot, sheetRef)
83
+ const range = requested ?? { startRow: 0, startColumn: 0, endRow: extent.endRow, endColumn: extent.endColumn }
84
+ // this range twice — as a SQL relation (values only) and as the Sheet
85
+ // Block's read-only preview, which has to draw the range the way the sheet
86
+ // draws it. Making it opt-in keeps the relation path from paying for a
87
+ // matrix it never reads.
88
+ const withStyles = url.searchParams.get('styles') === '1'
89
+ const result = readWorkbookRange(meta.snapshot, sheetRef, range, withStyles ? { styles: true } : {})
90
+ const header = result.values[0] ?? []
91
+ const body = result.values.slice(1)
92
+ // An empty sheet reads as no columns rather than one unnamed one.
93
+ const columns = header.every(value => value === null)
94
+ ? []
95
+ : header.map((value, index) => typeof value === 'string' && value.trim() !== '' ? value.trim() : `column_${index + 1}`)
96
+ // 响应按 wire 契约标注,字段变化会在编译期被发现。
97
+ const payload: SheetRangeResponse = {
98
+ ok: true,
99
+ workbookId: id,
100
+ workbookName: meta.name,
101
+ revision: meta.revision ?? 0,
102
+ sheetId: result.sheetId,
103
+ sheetName: result.sheetName,
104
+ range: result.range,
105
+ columns,
106
+ rows: body,
107
+ // The style matrix spans the **whole** requested range, header row
108
+ // included, so it indexes exactly like `result.values` — the preview
109
+ // pairs `styles[row][column]` with the value it just drew rather than
110
+ // re-deriving which row is the header.
111
+ ...(result.styles === undefined ? {} : { styles: result.styles }),
112
+ // Only a narrow request can be short of the sheet: that is the single
113
+ // case where more data exists than was returned, and saying so is what
114
+ // lets the reader ask for the rest.
115
+ truncated: requested !== undefined && (extent.endRow > requested.endRow || extent.endColumn > requested.endColumn),
116
+ }
117
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
118
+ res.end(JSON.stringify(payload))
119
+ } catch (error) {
120
+ writeOperationError(res, error instanceof DataSheetError ? error : new DataSheetError('VALIDATION_ERROR', error instanceof Error ? error.message : String(error), 400))
121
+ }
122
+ return true
123
+ }
124
+
125
+ if (route.kind === 'proposals' && req.method === 'GET') {
126
+ const index = await readWorkbookIndex(id)
127
+ const meta = index === null ? await readWorkbook(id) : null
128
+ if (index === null && meta === null) {
129
+ res.writeHead(404)
130
+ res.end()
131
+ return true
132
+ }
133
+ const proposals = (index?.proposals ?? meta?.proposals ?? []).filter(proposal => proposal.status === 'pending').map(proposal => ({
134
+ id: proposal.id,
135
+ summary: proposal.summary,
136
+ baseRevision: proposal.baseRevision,
137
+ createdAt: proposal.createdAt,
138
+ changes: proposal.changes,
139
+ }))
140
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
141
+ res.end(JSON.stringify({ ok: true, workbookId: id, revision: index?.revision ?? meta?.revision ?? 0, proposals }))
142
+ return true
143
+ }
144
+
145
+ if (route.kind === 'proposal-action' && req.method === 'POST') {
146
+ let body: Record<string, unknown> = {}
147
+ try { body = await readJsonBody(req) } catch { /* reject does not need a body */ }
148
+ try {
149
+ const proposalId = route.proposalId
150
+ if (route.action === 'commit') {
151
+ const result = await commitWorkbookProposal({ workbookId: id, proposalId, actor: 'user', expectedRevision: optionalRevision(body.baseRevision) })
152
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
153
+ res.end(JSON.stringify({ ok: true, revision: result.meta.revision ?? 0, operation: result.operation }))
154
+ } else {
155
+ const meta = await rejectWorkbookProposal(id, proposalId)
156
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
157
+ res.end(JSON.stringify({ ok: true, revision: meta.revision ?? 0, proposalId }))
158
+ }
159
+ } catch (error) {
160
+ writeOperationError(res, error)
161
+ }
162
+ return true
163
+ }
164
+
165
+ if (route.kind === 'rollback' && req.method === 'POST') {
166
+ let body: Record<string, unknown>
167
+ try { body = await readJsonBody(req) } catch { writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'bad json')); return true }
168
+ try {
169
+ const result = await rollbackWorkbook({
170
+ workbookId: id,
171
+ targetRevision: parseIndex(body.targetRevision, 'targetRevision'),
172
+ expectedRevision: optionalRevision(body.baseRevision),
173
+ actor: 'user',
174
+ // The browser names the Version (`恢复到 V3`) because only it knows
175
+ // the display number; the manager falls back to the revision.
176
+ ...(typeof body.summary === 'string' && body.summary.trim() !== '' ? { summary: body.summary.trim() } : {}),
177
+ lineage: lineageOfValue(body.lineage),
178
+ })
179
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
180
+ res.end(JSON.stringify({
181
+ ok: true,
182
+ revision: result.meta.revision ?? 0,
183
+ operation: result.operation,
184
+ }))
185
+ } catch (error) {
186
+ writeOperationError(res, error)
187
+ }
188
+ return true
189
+ }
190
+
191
+ if (route.kind === 'changes' && req.method === 'GET') {
192
+ const meta = await readWorkbook(id)
193
+ if (meta === null) {
194
+ res.writeHead(404)
195
+ res.end()
196
+ return true
197
+ }
198
+ const after = parseRevision(url.searchParams.get('after'))
199
+ const operations = (meta.operations ?? []).filter(operation => operation.revision > after)
200
+ const currentRevision = meta.revision ?? 0
201
+ const oldestRevision = operations[0]?.revision ?? currentRevision
202
+ const requiresSnapshot = after < oldestRevision - 1
203
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
204
+ res.end(JSON.stringify({
205
+ ok: true,
206
+ workbookId: id,
207
+ revision: currentRevision,
208
+ operations,
209
+ requiresSnapshot,
210
+ snapshot: requiresSnapshot ? meta?.snapshot : undefined,
211
+ }))
212
+ return true
213
+ }
214
+
215
+ if (route.kind === 'snapshot' && req.method === 'GET') {
216
+ const meta = await readWorkbook(id)
217
+ if (meta === null) {
218
+ res.writeHead(404)
219
+ res.end()
220
+ return true
221
+ }
222
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
223
+ res.end(JSON.stringify({
224
+ ok: true,
225
+ workbookId: meta.id,
226
+ revision: meta.revision ?? 0,
227
+ updatedAt: meta.updatedAt,
228
+ snapshot: meta.snapshot,
229
+ }))
230
+ return true
231
+ }
232
+
233
+ if (route.kind === 'operation' && req.method === 'POST') {
234
+ let body: Record<string, unknown>
235
+ try {
236
+ body = await readJsonBody(req)
237
+ } catch {
238
+ writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'bad json'))
239
+ return true
240
+ }
241
+ const type = body.type
242
+ if (!isOperationType(type)) {
243
+ writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'invalid operation type'))
244
+ return true
245
+ }
246
+ try {
247
+ const result = await mutateWorkbook({
248
+ workbookId: id,
249
+ // Browser HTTP routes are always user-originated. Agent writes use
250
+ // the tool host directly and cannot spoof this audit field.
251
+ actor: 'user',
252
+ type,
253
+ payload: asOperationPayload(body.payload),
254
+ expectedRevision: optionalRevision(body.baseRevision),
255
+ opId: typeof body.opId === 'string' ? body.opId : undefined,
256
+ versionId: typeof body.versionId === 'string' ? body.versionId : undefined,
257
+ // `temporary` remains accepted for browser bundles that have not
258
+ // been upgraded yet; the mutation authority normalizes it to
259
+ // the clearer durable `open` status.
260
+ versionStatus: body.versionStatus === 'open' || body.versionStatus === 'temporary' || body.versionStatus === 'final' ? body.versionStatus : undefined,
261
+ // Browser routes are always user-originated, and a user edit
262
+ // session is an auto checkpoint (PRD v3 §4).
263
+ versionKind: 'auto',
264
+ lineage: lineageOfValue(body.lineage),
265
+ })
266
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
267
+ res.end(JSON.stringify({
268
+ ok: true,
269
+ revision: result.meta.revision ?? 0,
270
+ operation: result.operation,
271
+ ...(result.rebasedOperations === undefined ? {} : { rebasedOperations: result.rebasedOperations }),
272
+ ...(result.transformed === undefined ? {} : { transformed: result.transformed }),
273
+ }))
274
+ } catch (error) {
275
+ writeOperationError(res, error)
276
+ }
277
+ return true
278
+ }
279
+
280
+ if (route.kind === 'snapshot' && req.method === 'POST') {
281
+ let body: Record<string, unknown>
282
+ try {
283
+ body = await readJsonBody(req)
284
+ } catch {
285
+ writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'bad json'))
286
+ return true
287
+ }
288
+ if (!Object.prototype.hasOwnProperty.call(body, 'snapshot')) {
289
+ writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'snapshot is required'))
290
+ return true
291
+ }
292
+ const snapshot = body.snapshot
293
+ try {
294
+ const versionSummary = typeof body.versionSummary === 'string' && body.versionSummary.trim() !== '' ? body.versionSummary.trim() : undefined
295
+ const versionId = typeof body.versionId === 'string' && body.versionId.trim() !== '' ? body.versionId.trim() : undefined
296
+ const requestedVersionStatus = body.versionStatus === 'open' || body.versionStatus === 'final' ? body.versionStatus : undefined
297
+ const result = await mutateWorkbook({
298
+ workbookId: id,
299
+ actor: 'user',
300
+ type: 'snapshot_replace',
301
+ payload: { snapshot },
302
+ expectedRevision: optionalRevision(body.baseRevision),
303
+ opId: typeof body.opId === 'string' ? body.opId : undefined,
304
+ lineage: lineageOfValue(body.lineage),
305
+ ...(versionId === undefined ? {} : { versionId }),
306
+ // A named checkpoint is an explicit, self-contained Version even
307
+ // when the content did not change (PRD §8); a plain snapshot flush
308
+ // is one Step of the editing session instead (PRD v3.1 §4).
309
+ versionKind: versionSummary === undefined ? 'auto' as const : 'named' as const,
310
+ ...(requestedVersionStatus === undefined ? {} : { versionStatus: requestedVersionStatus }),
311
+ ...(versionSummary === undefined ? {} : { versionSummary, versionStatus: requestedVersionStatus ?? 'final' as const }),
312
+ })
313
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
314
+ res.end(JSON.stringify({ ok: true, revision: result.meta.revision ?? 0, operation: result.operation }))
315
+ } catch (error) {
316
+ writeOperationError(res, error)
317
+ }
318
+ return true
319
+ }
320
+
321
+ return false
322
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Isolated review worktrees over HTTP (docs/issue/0022 P3).
3
+ *
4
+ * Draft copies are session-scoped, which is why every handler here resolves the
5
+ * session first and checks the owner before touching a worktree.
6
+ */
7
+ import type { IncomingMessage, ServerResponse } from 'node:http'
8
+ import { DataSheetError } from '../errors.ts'
9
+ import { diffReachedLimit, diffWorkbookSnapshots } from '../version/diff.ts'
10
+ import {
11
+ createWorkbookWorktree,
12
+ listWorkbookWorktrees,
13
+ mergeWorkbookWorktree,
14
+ mutateWorkbookWorktree,
15
+ readWorkbookAtRevision,
16
+ readWorkbookWorktree,
17
+ setWorkbookWorktreeStatus,
18
+ } from '../workbook-manager.ts'
19
+ import { isOperationType, asOperationPayload, optionalRevision } from '../request-params.ts'
20
+ import { readJsonBody, requireDataSheetSession, writeOperationError, type WorkbookRoute } from './routes.ts'
21
+
22
+ export async function handleWorktreeRoute(route: WorkbookRoute, req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean> {
23
+ const id = route.id
24
+
25
+ if (route.kind === 'worktrees') {
26
+ const sessionId = requireDataSheetSession(req, res, url)
27
+ if (sessionId === null) return true
28
+ if (req.method === 'GET') {
29
+ const worktrees = await listWorkbookWorktrees(id, sessionId)
30
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
31
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktrees }))
32
+ return true
33
+ }
34
+ if (req.method === 'POST') {
35
+ let body: Record<string, unknown>
36
+ try { body = await readJsonBody(req) } catch (error) { writeOperationError(res, error); return true }
37
+ try {
38
+ const worktree = await createWorkbookWorktree({ workbookId: id, ownerSessionId: sessionId, name: typeof body.name === 'string' ? body.name : undefined, worktreeId: typeof body.worktreeId === 'string' ? body.worktreeId : undefined })
39
+ res.writeHead(201, { 'content-type': 'application/json; charset=utf-8' })
40
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktree }))
41
+ } catch (error) { writeOperationError(res, error) }
42
+ return true
43
+ }
44
+ res.writeHead(405)
45
+ res.end()
46
+ return true
47
+ }
48
+
49
+ if (route.kind === 'worktree' && req.method === 'GET') {
50
+ const sessionId = requireDataSheetSession(req, res, url)
51
+ if (sessionId === null) return true
52
+ const worktree = await readWorkbookWorktree(id, route.worktreeId as string)
53
+ if (worktree === null) { writeOperationError(res, new DataSheetError('WORKTREE_NOT_FOUND', 'worktree 不存在', 404)); return true }
54
+ if (worktree.ownerSessionId !== sessionId) { writeOperationError(res, new DataSheetError('SESSION_SCOPE_DENIED', '当前 session 无权访问该 worktree', 403)); return true }
55
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
56
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktree }))
57
+ return true
58
+ }
59
+
60
+ if (route.kind === 'worktree-operations' && req.method === 'POST') {
61
+ const sessionId = requireDataSheetSession(req, res, url)
62
+ if (sessionId === null) return true
63
+ let body: Record<string, unknown>
64
+ try { body = await readJsonBody(req) } catch (error) { writeOperationError(res, error); return true }
65
+ if (!isOperationType(body.type)) { writeOperationError(res, new DataSheetError('VALIDATION_ERROR', 'invalid operation type')); return true }
66
+ try {
67
+ const result = await mutateWorkbookWorktree({
68
+ workbookId: id,
69
+ worktreeId: route.worktreeId as string,
70
+ ownerSessionId: sessionId,
71
+ actor: 'user',
72
+ type: body.type,
73
+ payload: asOperationPayload(body.payload),
74
+ expectedRevision: optionalRevision(body.baseRevision),
75
+ opId: typeof body.opId === 'string' ? body.opId : undefined,
76
+ })
77
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
78
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktree: result.worktree, revision: result.worktree.revision, operation: result.operation }))
79
+ } catch (error) { writeOperationError(res, error) }
80
+ return true
81
+ }
82
+
83
+ if (route.kind === 'worktree-action' && req.method === 'POST') {
84
+ const sessionId = requireDataSheetSession(req, res, url)
85
+ if (sessionId === null) return true
86
+ let body: Record<string, unknown> = {}
87
+ try { body = await readJsonBody(req) } catch { /* empty action body is valid */ }
88
+ const worktreeId = route.worktreeId as string
89
+ const action = route.action as 'ready' | 'reopen' | 'merge' | 'discard'
90
+ try {
91
+ if (action === 'merge') {
92
+ const result = await mergeWorkbookWorktree({ workbookId: id, worktreeId, ownerSessionId: sessionId, actor: 'user', expectedRevision: optionalRevision(body.baseRevision) })
93
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
94
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktree: result.worktree, revision: result.mutation.meta.revision ?? 0, operation: result.mutation.operation }))
95
+ } else {
96
+ const worktree = await setWorkbookWorktreeStatus({ workbookId: id, worktreeId, ownerSessionId: sessionId, status: action === 'discard' ? 'discarded' : action })
97
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
98
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktree }))
99
+ }
100
+ } catch (error) { writeOperationError(res, error) }
101
+ return true
102
+ }
103
+
104
+ if (route.kind === 'worktree-diff' && req.method === 'GET') {
105
+ const sessionId = requireDataSheetSession(req, res, url)
106
+ if (sessionId === null) return true
107
+ const worktree = await readWorkbookWorktree(id, route.worktreeId as string)
108
+ if (worktree === null) { writeOperationError(res, new DataSheetError('WORKTREE_NOT_FOUND', 'worktree 不存在', 404)); return true }
109
+ if (worktree.ownerSessionId !== sessionId) { writeOperationError(res, new DataSheetError('SESSION_SCOPE_DENIED', '当前 session 无权访问该 worktree', 403)); return true }
110
+ const base = await readWorkbookAtRevision(id, worktree.baseRevision)
111
+ if (base === null) { writeOperationError(res, new DataSheetError('REVISION_UNAVAILABLE', 'worktree 基线 revision 已被压缩或无法重放', 409)); return true }
112
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
113
+ const changes = diffWorkbookSnapshots(base.snapshot, worktree.snapshot)
114
+ res.end(JSON.stringify({ ok: true, workbookId: id, worktreeId: worktree.id, from: worktree.baseRevision, to: worktree.revision, changes, truncated: diffReachedLimit(changes) }))
115
+ return true
116
+ }
117
+
118
+ return false
119
+ }
package/src/index.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Node half of dsh-data-table: the plugin body.
3
+ *
4
+ * It does two things: assemble the HTTP routes (the asset routes plus one
5
+ * prefix route whose sub-paths are matched in `http/routes.ts`) and register the
6
+ * Agent tool surface. Everything it hands to either transport lives in a module
7
+ * beside it, so this file is the list of what the plugin offers — not how any of
8
+ * it works (docs/issue/0022 P3b).
9
+ * @module dsh-data-table
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+ import type { IncomingMessage, ServerResponse } from 'node:http'
13
+ import type {} from '@deepseek-ai/dsh-tools'
14
+ import type {} from '@deepseek-ai/dsh-client-connection'
15
+ import { setLineageScope } from './audit.ts'
16
+ import { UNIVER_API_CATALOG, UNIVER_API_VERSION } from './contract/univer-api-catalog.ts'
17
+ import { listWorkbookSummaries } from './workbook-manager.ts'
18
+ import { assetRoutes } from './http/assets-routes.ts'
19
+ import { handleCollaborationRoute } from './http/collaboration.ts'
20
+ import { handleHistoryRoute } from './http/history.ts'
21
+ import { handleTrunkRoute } from './http/trunk.ts'
22
+ import { handleWorktreeRoute } from './http/worktree.ts'
23
+ import {
24
+ guardDataSheetRequest,
25
+ matchWorkbookRoute,
26
+ readJsonBody,
27
+ registerConnectionRoutes,
28
+ writeOperationError,
29
+ type LegacyRoute,
30
+ } from './http/routes.ts'
31
+ import { projectFor, runForProject, sessionIdOf } from './http/project-scope.ts'
32
+ import { projectFileRoutes } from './http/project-files.ts'
33
+ import { registerWorkbookTools } from './tools/index.ts'
34
+ import { executeWorkbookCatalog } from './tools/workbook-catalog.ts'
35
+
36
+ export const name = 'dsh-data-table'
37
+ // `sessions` is read to turn a Session id into that Session's project directory,
38
+ // in both transports: a browser request names its id in a header, and a tool call
39
+ // names the same id through its Agent. Cordis throws on undeclared service access,
40
+ // and the throw lands inside a request handler whose exception the web server
41
+ // reports as a bodyless 400 — so a missing declaration looked exactly like a
42
+ // malformed request (2026-09-25).
43
+ //
44
+ // `sandboxPolicy` is deliberately *not* declared: it is not reachable from a plugin context in
45
+ // the web deployment (`cannot get property "sandboxPolicy" without inject` in
46
+ // `~/.dsh/logs/dsh-web-3080.log`, 77 times), so reading it as a fallback turned every workbook
47
+ // list into a 400. A Session's own header is the answer, and a caller that names no Session
48
+ // keeps the pre-project store instead of a project guessed from a policy.
49
+ export const inject = ['tools', 'connection', 'sessions'] as const
50
+
51
+ /**
52
+ * Plugin body: chunk route + workbook/sheet tools.
53
+ * @param ctx - registrant context.
54
+ */
55
+ export function apply(ctx: Context): void {
56
+ setLineageScope(ctx)
57
+ const routes: LegacyRoute[] = [...assetRoutes(), {
58
+ // A prefix route: everything under `/data-sheet/workbook/` is one entry
59
+ // point, and `matchWorkbookRoute` decides which domain handles it.
60
+ kind: 'prefix',
61
+ // Workbook projection and operation endpoints.
62
+ // Paths:
63
+ // GET /data-sheet/workbook/<id>.json
64
+ // POST /data-sheet/workbook/<id>.json (legacy snapshot checkpoint)
65
+ // GET /data-sheet/workbook/<id>/changes?after=N
66
+ // GET /data-sheet/workbook/<id>/collaboration (live SSE session)
67
+ // GET /data-sheet/workbook/<id>/history[?revision=N]
68
+ // POST /data-sheet/workbook/<id>/operations (fine-grained mutation)
69
+ // NOTE: prefix route paths must NOT end with a trailing slash — the host
70
+ // matcher appends its own "/" (startsWith(`${prefix}/`)), so a trailing
71
+ // slash makes subpaths unmatchable.
72
+ path: '/data-sheet/workbook',
73
+ handler: async (req, res) => {
74
+ if (!guardDataSheetRequest(req, res)) return
75
+ // Writes must be JSON: a form-encoded body would be parsed as an empty
76
+ // object and silently drop the payload.
77
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
78
+ const contentType = String(req.headers['content-type'] ?? '').toLowerCase()
79
+ if (!contentType.startsWith('application/json')) {
80
+ res.writeHead(415, { 'content-type': 'application/json; charset=utf-8' })
81
+ res.end(JSON.stringify({ ok: false, code: 'VALIDATION_ERROR', error: '写请求必须使用 application/json' }))
82
+ return
83
+ }
84
+ }
85
+ const url = new URL(req.url ?? '/', 'http://x')
86
+ // `decodeURIComponent` throws on a malformed escape (`/x/%E0%A4%A`), and
87
+ // that would escape the handler as an unhandled URIError. A path we cannot
88
+ // decode is simply not one of our routes.
89
+ let pathname: string
90
+ try {
91
+ pathname = decodeURIComponent(url.pathname)
92
+ } catch {
93
+ res.writeHead(400)
94
+ res.end()
95
+ return
96
+ }
97
+ const route = matchWorkbookRoute(pathname)
98
+ if (route === null) {
99
+ res.writeHead(404)
100
+ res.end()
101
+ return
102
+ }
103
+ // Worktrees are session-scoped draft copies and are handled first, so a
104
+ // draft can never be mistaken for a normal workbook mutation. The live
105
+ // stream is next because it holds the response open.
106
+ // Every workbook path resolves inside the requesting Session's project, so
107
+ // the store root it writes to is that project's `storages/data-table`.
108
+ const project = projectFor(ctx, sessionIdOf(req.headers, url))
109
+ const dispatched = await runForProject(project, async () => {
110
+ if (await handleWorktreeRoute(route, req, res, url)) return true
111
+ if (await handleCollaborationRoute(route, req, res, url)) return true
112
+ if (await handleHistoryRoute(route, req, res, url)) return true
113
+ if (await handleTrunkRoute(route, req, res, url)) return true
114
+ return false
115
+ })
116
+ if (dispatched) return
117
+ res.writeHead(405)
118
+ res.end()
119
+ },
120
+ }, {
121
+ kind: 'exact',
122
+ path: '/data-sheet/workbooks.json',
123
+ handler: async (_req, res) => {
124
+ if (!guardDataSheetRequest(_req, res)) return
125
+ const project = projectFor(ctx, sessionIdOf(_req.headers, new URL(_req.url ?? '/', 'http://x')))
126
+ // POST routes to the Agent's catalog implementation so the browser and the
127
+ // model share one create/rename/delete path.
128
+ if (_req.method === 'POST') {
129
+ let body: Record<string, unknown>
130
+ try { body = await readJsonBody(_req) } catch (error) { writeOperationError(res, error); return }
131
+ try {
132
+ const result = await runForProject(project, async () => executeWorkbookCatalog({ ...body, action: body.action }))
133
+ res.writeHead(body.action === 'create' ? 201 : 200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
134
+ res.end(JSON.stringify(result))
135
+ } catch (error) {
136
+ writeOperationError(res, error)
137
+ }
138
+ return
139
+ }
140
+ if (_req.method !== 'GET' && _req.method !== 'HEAD') {
141
+ res.writeHead(405, { 'content-type': 'application/json; charset=utf-8' })
142
+ res.end(JSON.stringify({ ok: false, error: 'method not allowed' }))
143
+ return
144
+ }
145
+ const workbooks = await runForProject(project, async () => listWorkbookSummaries())
146
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
147
+ res.end(JSON.stringify({ ok: true, workbooks }))
148
+ },
149
+ }, {
150
+ // Agent-facing metadata endpoint. It is deliberately separate from the
151
+ // mutation routes so a client can discover the safe Univer surface without
152
+ // receiving a browser-only Injector or executing arbitrary JavaScript.
153
+ kind: 'exact',
154
+ path: '/data-sheet/univer-api.json',
155
+ handler: async (_req, res) => {
156
+ if (!guardDataSheetRequest(_req, res)) return
157
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
158
+ res.end(JSON.stringify({ ok: true, version: UNIVER_API_VERSION, catalog: UNIVER_API_CATALOG }))
159
+ },
160
+ }, ...projectFileRoutes(ctx)]
161
+
162
+ registerConnectionRoutes(ctx, routes, '/data-sheet')
163
+ registerWorkbookTools(ctx)
164
+ }
165
+
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Package-owned invariant companion for dsh-data-table.
3
+ *
4
+ * Runtime invariant: the browser half can only render a sheet that the host
5
+ * tool admitted, because the renderer parses the durable tool-call arguments
6
+ * through {@link isSheetData} — the same shape the tool validated. A renderer
7
+ * reaching an unvalidated sheet is a build-level type violation, not a runtime
8
+ * state, so the companion registers the manifest name only.
9
+ * @module dsh-data-table/invariant
10
+ */
11
+
12
+ export const manifest = 'dsh-data-table'
13
+
14
+ export const invariant = {
15
+ manifest,
16
+ reason: 'renderer input is the host-validated tool-call arguments (isSheetData); no runtime event/data relation to check',
17
+ }