@chengxianglibra/dsh-data-analysis 0.1.1 → 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 (303) hide show
  1. package/README.md +139 -53
  2. package/cordis.patch.yml +1 -1
  3. package/lib/bin/environment.js +9 -7
  4. package/lib/bin/presentation-lint.js +21 -0
  5. package/lib/bridges.js +2 -2
  6. package/lib/client/credentials/changes.js +74 -0
  7. package/lib/client/credentials/create-datasource.js +171 -0
  8. package/lib/client/credentials/credential-draft.js +42 -0
  9. package/lib/client/credentials/defaults.js +11 -0
  10. package/lib/client/credentials/descriptions.js +46 -0
  11. package/lib/client/credentials/install.js +319 -0
  12. package/lib/client/credentials/model.js +766 -0
  13. package/lib/client/credentials/report-publishing.js +96 -0
  14. package/lib/client/credentials/styles.js +21 -0
  15. package/lib/client/i18n/context.js +22 -0
  16. package/lib/client/i18n/copy.js +57 -0
  17. package/lib/client/i18n/credentials.js +475 -0
  18. package/lib/client/i18n/host.js +27 -0
  19. package/lib/client/i18n/navigation.js +41 -0
  20. package/lib/client/i18n/presentation.js +577 -0
  21. package/lib/client/i18n/python.js +36 -0
  22. package/lib/client/i18n/semantic.js +441 -0
  23. package/lib/client/input-context.js +21 -0
  24. package/lib/client/python-tool/card.js +35 -0
  25. package/lib/client/python-tool/execution-error.js +35 -0
  26. package/lib/client/python-tool/install.js +9 -0
  27. package/lib/client/python-tool/model.js +101 -0
  28. package/lib/client/right-tabs/install.js +535 -0
  29. package/lib/client/right-tabs/live-delivery.js +58 -0
  30. package/lib/client/right-tabs/navigation.js +47 -0
  31. package/lib/client/right-tabs/page.js +178 -0
  32. package/lib/client/right-tabs/styles.js +11 -0
  33. package/lib/client/rpc.js +5 -0
  34. package/lib/client/semantic-reference.js +10 -0
  35. package/lib/client/workspace-header-action.js +24 -0
  36. package/lib/client/workspace-shortcuts.js +26 -0
  37. package/lib/client.js +1250 -730
  38. package/lib/compatibility.js +2 -2
  39. package/lib/datasource/authoring.js +109 -0
  40. package/lib/datasource/bridge-programs.js +42 -27
  41. package/lib/datasource/bridge.js +144 -9
  42. package/lib/datasource/changes-contract.js +41 -0
  43. package/lib/datasource/changes.js +49 -0
  44. package/lib/datasource/configure.js +39 -0
  45. package/lib/datasource/defaults.js +25 -0
  46. package/lib/datasource/index.js +4 -2
  47. package/lib/datasource/python-options.js +21 -0
  48. package/lib/datasource/python.js +206 -0
  49. package/lib/datasource/resolver-program.js +112 -0
  50. package/lib/datasource/rpc.js +150 -0
  51. package/lib/datasource/service.js +962 -0
  52. package/lib/datasource/shell-env.js +2 -333
  53. package/lib/datasource/test.js +25 -51
  54. package/lib/disclosure/activation.js +19 -7
  55. package/lib/disclosure/execution-guidance.js +52 -0
  56. package/lib/disclosure/help.js +4 -1
  57. package/lib/environment/binding.js +54 -9
  58. package/lib/environment/runtime.js +120 -108
  59. package/lib/environment/subprocess.js +6 -1
  60. package/lib/environment/summary.js +3 -3
  61. package/lib/environment/workspace.js +23 -1
  62. package/lib/index.js +1 -1
  63. package/lib/lifecycle.js +33 -0
  64. package/lib/plugin-agents.js +154 -0
  65. package/lib/plugin.js +172 -133
  66. package/lib/presentation/asset.js +34 -0
  67. package/lib/presentation/assets/portable.js +606 -0
  68. package/lib/presentation/assets/static.js +1216 -0
  69. package/lib/presentation/build/index.js +72 -0
  70. package/lib/presentation/commit.js +163 -0
  71. package/lib/presentation/contracts/asset-path.js +9 -0
  72. package/lib/presentation/contracts/catalog.js +62 -0
  73. package/lib/presentation/contracts/charts.js +453 -0
  74. package/lib/presentation/contracts/editing.js +58 -0
  75. package/lib/presentation/contracts/index.js +663 -0
  76. package/lib/presentation/contracts/interaction.js +62 -0
  77. package/lib/presentation/contracts/types.js +15 -0
  78. package/lib/presentation/contracts/values.js +28 -0
  79. package/lib/presentation/delivery.js +71 -0
  80. package/lib/presentation/export-html.js +214 -0
  81. package/lib/presentation/files.js +82 -0
  82. package/lib/presentation/index.js +6 -0
  83. package/lib/presentation/lint.js +46 -0
  84. package/lib/presentation/projection/files.js +85 -0
  85. package/lib/presentation/projection/index.js +196 -0
  86. package/lib/presentation/projection/program.js +137 -0
  87. package/lib/presentation/projection/sql-code.js +79 -0
  88. package/lib/presentation/receipt.js +34 -0
  89. package/lib/presentation/reports.js +219 -0
  90. package/lib/presentation/rpc.js +186 -0
  91. package/lib/presentation/tool.js +118 -0
  92. package/lib/python-execution-contracts.js +1 -0
  93. package/lib/python-execution.js +232 -0
  94. package/lib/report-publishing/adapters.js +111 -0
  95. package/lib/report-publishing/config.js +69 -0
  96. package/lib/report-publishing/service.js +180 -0
  97. package/lib/rpc.js +49 -0
  98. package/lib/semantic-browser/contracts.js +97 -0
  99. package/lib/semantic-browser/definition.js +64 -0
  100. package/lib/semantic-browser/program.js +129 -0
  101. package/lib/semantic-browser/service.js +106 -0
  102. package/lib/semantic-reference/bridge.js +127 -0
  103. package/lib/semantic-reference/contracts.js +127 -0
  104. package/lib/semantic-reference/environment.js +56 -0
  105. package/lib/semantic-reference/index.js +3 -0
  106. package/lib/semantic-reference/labels.js +16 -0
  107. package/lib/semantic-reference/rpc.js +143 -0
  108. package/lib/semantic-reference/search.js +95 -0
  109. package/lib/semantic-reference/usage.js +152 -0
  110. package/lib/tool-lifecycle.js +22 -0
  111. package/lib/types/bridges.d.ts +2 -2
  112. package/lib/types/client/credentials/changes.d.ts +9 -0
  113. package/lib/types/client/credentials/create-datasource.d.ts +7 -0
  114. package/lib/types/client/credentials/credential-draft.d.ts +14 -0
  115. package/lib/types/client/credentials/defaults.d.ts +2 -0
  116. package/lib/types/client/credentials/descriptions.d.ts +1 -0
  117. package/lib/types/client/credentials/install.d.ts +19 -0
  118. package/lib/types/client/credentials/model.d.ts +79 -0
  119. package/lib/types/client/credentials/report-publishing.d.ts +5 -0
  120. package/lib/types/client/credentials/styles.d.ts +1 -0
  121. package/lib/types/client/i18n/context.d.ts +17 -0
  122. package/lib/types/client/i18n/copy.d.ts +806 -0
  123. package/lib/types/client/i18n/credentials.d.ts +475 -0
  124. package/lib/types/client/i18n/host.d.ts +16 -0
  125. package/lib/types/client/i18n/navigation.d.ts +41 -0
  126. package/lib/types/client/i18n/presentation.d.ts +577 -0
  127. package/lib/types/client/i18n/python.d.ts +19 -0
  128. package/lib/types/client/i18n/semantic.d.ts +441 -0
  129. package/lib/types/client/input-context.d.ts +19 -0
  130. package/lib/types/client/presentation/ask-dsh.d.ts +5 -0
  131. package/lib/types/client/presentation/catalog-model.d.ts +24 -0
  132. package/lib/types/client/presentation/catalog.d.ts +20 -0
  133. package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
  134. package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
  135. package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
  136. package/lib/types/client/presentation/chart-specials.d.ts +10 -0
  137. package/lib/types/client/presentation/chart-styles.d.ts +1 -0
  138. package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
  139. package/lib/types/client/presentation/chart-view.d.ts +23 -0
  140. package/lib/types/client/presentation/context-reference.d.ts +12 -0
  141. package/lib/types/client/presentation/copy-context.d.ts +5 -0
  142. package/lib/types/client/presentation/delivery-model.d.ts +75 -0
  143. package/lib/types/client/presentation/delivery.d.ts +78 -0
  144. package/lib/types/client/presentation/download.d.ts +2 -0
  145. package/lib/types/client/presentation/editor-controls.d.ts +12 -0
  146. package/lib/types/client/presentation/export-menu.d.ts +26 -0
  147. package/lib/types/client/presentation/export-view.d.ts +63 -0
  148. package/lib/types/client/presentation/global-filters.d.ts +6 -0
  149. package/lib/types/client/presentation/host-entry.d.ts +18 -0
  150. package/lib/types/client/presentation/icons.d.ts +4 -0
  151. package/lib/types/client/presentation/markdown.d.ts +5 -0
  152. package/lib/types/client/presentation/model.d.ts +61 -0
  153. package/lib/types/client/presentation/reader.d.ts +24 -0
  154. package/lib/types/client/presentation/reference-source.d.ts +9 -0
  155. package/lib/types/client/presentation/source-code-model.d.ts +17 -0
  156. package/lib/types/client/presentation/source-code.d.ts +6 -0
  157. package/lib/types/client/presentation/source-dialog.d.ts +12 -0
  158. package/lib/types/client/presentation/source-facts.d.ts +20 -0
  159. package/lib/types/client/presentation/source-format.d.ts +5 -0
  160. package/lib/types/client/presentation/source-highlight.d.ts +7 -0
  161. package/lib/types/client/presentation/sources.d.ts +13 -0
  162. package/lib/types/client/presentation/styles.d.ts +1 -0
  163. package/lib/types/client/presentation/table.d.ts +16 -0
  164. package/lib/types/client/presentation/use-element-width.d.ts +5 -0
  165. package/lib/types/client/python-tool/card.d.ts +2 -0
  166. package/lib/types/client/python-tool/execution-error.d.ts +3 -0
  167. package/lib/types/client/python-tool/install.d.ts +2 -0
  168. package/lib/types/client/python-tool/model.d.ts +20 -0
  169. package/lib/types/client/right-tabs/install.d.ts +18 -0
  170. package/lib/types/client/right-tabs/live-delivery.d.ts +8 -0
  171. package/lib/types/client/right-tabs/navigation.d.ts +17 -0
  172. package/lib/types/client/right-tabs/page.d.ts +36 -0
  173. package/lib/types/client/right-tabs/styles.d.ts +2 -0
  174. package/lib/types/client/rpc.d.ts +3 -0
  175. package/lib/types/client/semantic-reference.d.ts +4 -0
  176. package/lib/types/client/workspace-header-action.d.ts +7 -0
  177. package/lib/types/client/workspace-shortcuts.d.ts +15 -0
  178. package/lib/types/client.d.ts +9 -124
  179. package/lib/types/compatibility.d.ts +3 -3
  180. package/lib/types/datasource/authoring.d.ts +35 -0
  181. package/lib/types/datasource/bridge.d.ts +43 -4
  182. package/lib/types/datasource/changes-contract.d.ts +18 -0
  183. package/lib/types/datasource/changes.d.ts +15 -0
  184. package/lib/types/datasource/configure.d.ts +7 -0
  185. package/lib/types/datasource/defaults.d.ts +7 -0
  186. package/lib/types/datasource/index.d.ts +8 -5
  187. package/lib/types/datasource/python-options.d.ts +9 -0
  188. package/lib/types/datasource/python.d.ts +19 -0
  189. package/lib/types/datasource/resolver-program.d.ts +6 -0
  190. package/lib/types/datasource/rpc.d.ts +5 -0
  191. package/lib/types/datasource/service.d.ts +155 -0
  192. package/lib/types/datasource/shell-env.d.ts +0 -29
  193. package/lib/types/datasource/test.d.ts +6 -29
  194. package/lib/types/disclosure/activation.d.ts +2 -1
  195. package/lib/types/disclosure/execution-guidance.d.ts +6 -0
  196. package/lib/types/disclosure/help.d.ts +1 -1
  197. package/lib/types/environment/binding.d.ts +5 -1
  198. package/lib/types/environment/errors.d.ts +1 -1
  199. package/lib/types/environment/runtime.d.ts +5 -5
  200. package/lib/types/environment/types.d.ts +15 -3
  201. package/lib/types/environment/workspace.d.ts +1 -0
  202. package/lib/types/index.d.ts +1 -1
  203. package/lib/types/lifecycle.d.ts +8 -0
  204. package/lib/types/plugin-agents.d.ts +27 -0
  205. package/lib/types/plugin.d.ts +14 -24
  206. package/lib/types/presentation/asset.d.ts +6 -0
  207. package/lib/types/presentation/build/index.d.ts +16 -0
  208. package/lib/types/presentation/commit.d.ts +4 -0
  209. package/lib/types/presentation/contracts/asset-path.d.ts +2 -0
  210. package/lib/types/presentation/contracts/catalog.d.ts +27 -0
  211. package/lib/types/presentation/contracts/charts.d.ts +11 -0
  212. package/lib/types/presentation/contracts/editing.d.ts +8 -0
  213. package/lib/types/presentation/contracts/index.d.ts +11 -0
  214. package/lib/types/presentation/contracts/interaction.d.ts +8 -0
  215. package/lib/types/presentation/contracts/types.d.ts +255 -0
  216. package/lib/types/presentation/contracts/values.d.ts +9 -0
  217. package/lib/types/presentation/delivery.d.ts +3 -0
  218. package/lib/types/presentation/export-html.d.ts +13 -0
  219. package/lib/types/presentation/files.d.ts +7 -0
  220. package/lib/types/presentation/index.d.ts +6 -0
  221. package/lib/types/presentation/lint.d.ts +13 -0
  222. package/lib/types/presentation/projection/files.d.ts +2 -0
  223. package/lib/types/presentation/projection/index.d.ts +20 -0
  224. package/lib/types/presentation/projection/program.d.ts +2 -0
  225. package/lib/types/presentation/projection/sql-code.d.ts +2 -0
  226. package/lib/types/presentation/receipt.d.ts +14 -0
  227. package/lib/types/presentation/reports.d.ts +10 -0
  228. package/lib/types/presentation/rpc.d.ts +25 -0
  229. package/lib/types/presentation/tool.d.ts +12 -0
  230. package/lib/types/python-execution-contracts.d.ts +10 -0
  231. package/lib/types/python-execution.d.ts +10 -0
  232. package/lib/types/report-publishing/adapters.d.ts +6 -0
  233. package/lib/types/report-publishing/config.d.ts +22 -0
  234. package/lib/types/report-publishing/service.d.ts +68 -0
  235. package/lib/types/rpc.d.ts +3 -0
  236. package/lib/types/semantic-browser/contracts.d.ts +47 -0
  237. package/lib/types/semantic-browser/definition.d.ts +20 -0
  238. package/lib/types/semantic-browser/program.d.ts +2 -0
  239. package/lib/types/semantic-browser/service.d.ts +26 -0
  240. package/lib/types/semantic-reference/bridge.d.ts +12 -0
  241. package/lib/types/semantic-reference/contracts.d.ts +54 -0
  242. package/lib/types/semantic-reference/environment.d.ts +22 -0
  243. package/lib/types/semantic-reference/index.d.ts +3 -0
  244. package/lib/types/semantic-reference/labels.d.ts +16 -0
  245. package/lib/types/semantic-reference/rpc.d.ts +15 -0
  246. package/lib/types/semantic-reference/search.d.ts +10 -0
  247. package/lib/types/semantic-reference/usage.d.ts +43 -0
  248. package/lib/types/tool-lifecycle.d.ts +4 -0
  249. package/lib/types/workspace-identity.d.ts +6 -0
  250. package/lib/workspace-identity.js +9 -0
  251. package/package.json +99 -53
  252. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  253. package/skills/dsh-data-analysis-files/SKILL.md +32 -0
  254. package/skills/dsh-data-analysis-files/references/examples.md +93 -0
  255. package/skills/dsh-data-analysis-presentation/SKILL.md +28 -0
  256. package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
  257. package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
  258. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +36 -0
  259. package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
  260. package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
  261. package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
  262. package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
  263. package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
  264. package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
  265. package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
  266. package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
  267. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +393 -0
  268. package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
  269. package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
  270. package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
  271. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +80 -0
  272. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +42 -0
  273. package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
  274. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +58 -0
  275. package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
  276. package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
  277. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +279 -0
  278. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +27 -0
  279. package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
  280. package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
  281. package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
  282. package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
  283. package/skills/dsh-data-analysis-presentation/references/examples.md +44 -0
  284. package/skills/dsh-data-analysis-presentation/references/export-html.md +7 -0
  285. package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
  286. package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
  287. package/skills/dsh-data-analysis-presentation/references/publishing.md +3 -0
  288. package/skills/dsh-data-analysis-presentation/references/schema.md +162 -0
  289. package/lib/datasource/access.js +0 -60
  290. package/lib/datasource/credentials.js +0 -24
  291. package/lib/evidence/bridge-program.js +0 -153
  292. package/lib/evidence/bridge.js +0 -166
  293. package/lib/evidence/index.js +0 -2
  294. package/lib/evidence/sources.js +0 -263
  295. package/lib/types/datasource/access.d.ts +0 -27
  296. package/lib/types/evidence/bridge.d.ts +0 -40
  297. package/lib/types/evidence/index.d.ts +0 -4
  298. package/lib/types/evidence/sources.d.ts +0 -57
  299. package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
  300. package/skills/dsh-data-analysis-report/SKILL.md +0 -57
  301. package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
  302. package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
  303. package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
@@ -0,0 +1,196 @@
1
+ import { MarivoEnvironmentError } from "../../environment/errors.js";
2
+ import { readPythonExecution } from "../../python-execution.js";
3
+ import { PRESENTATION_BUDGETS, PresentationContractError, parsePresentationDocument, parsePresentationDraft, parseTypedDataset, } from "../contracts/index.js";
4
+ import { readWorkspaceJson } from "./files.js";
5
+ import { MARIVO_PRESENTATION_READ_PROGRAM } from "./program.js";
6
+ export { readWorkspaceJson } from "./files.js";
7
+ function invalidOutput() {
8
+ throw new MarivoEnvironmentError('subprocess-output-invalid', 'Marivo presentation reader returned an invalid projection.');
9
+ }
10
+ function record(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value))
12
+ invalidOutput();
13
+ return value;
14
+ }
15
+ function exactKeys(value, keys) {
16
+ if (Object.keys(value).sort().join(',') !== keys.sort().join(','))
17
+ invalidOutput();
18
+ }
19
+ /** Internal S2 bridge: restores public data without observe, revalidation or credential admission. */
20
+ export class MarivoPresentationProjection {
21
+ #runner;
22
+ constructor(runner) {
23
+ this.#runner = runner;
24
+ }
25
+ get status() {
26
+ return this.#runner.status;
27
+ }
28
+ get binding() {
29
+ return this.#runner.binding;
30
+ }
31
+ async readDraft(relativePath, signal) {
32
+ this.#assertReady(signal);
33
+ return parsePresentationDraft(await readWorkspaceJson(this.binding.projectRoot, relativePath, PRESENTATION_BUDGETS.draftBytes, '/draft_path', signal));
34
+ }
35
+ #assertReady(signal) {
36
+ signal?.throwIfAborted();
37
+ if (this.#runner.status !== 'ready')
38
+ throw new MarivoEnvironmentError('binding-failed', 'Presentation projection requires a ready bound Runtime.');
39
+ if (!this.binding.presentationKit)
40
+ throw new MarivoEnvironmentError('binding-identity-mismatch', 'Presentation projection requires the checked presentation helper identity.');
41
+ }
42
+ async project(value, options) {
43
+ this.#assertReady(options.signal);
44
+ // Snapshot caller-owned input before awaiting IO.
45
+ const draft = structuredClone(parsePresentationDraft(value));
46
+ const datasets = [];
47
+ const artifactSelections = draft.datasets.flatMap((dataset, index) => dataset.kind === 'artifact'
48
+ ? [
49
+ {
50
+ id: dataset.id,
51
+ kind: dataset.kind,
52
+ sourceId: dataset.sourceId,
53
+ rowLimit: dataset.rowLimit,
54
+ ...(dataset.columns ? { columns: dataset.columns } : {}),
55
+ index,
56
+ },
57
+ ]
58
+ : []);
59
+ let sources = [];
60
+ let diagnostics = [];
61
+ const artifactData = new Map();
62
+ if (draft.sources.length > 0) {
63
+ const result = await this.#runner.runChecked({
64
+ program: MARIVO_PRESENTATION_READ_PROGRAM,
65
+ args: [JSON.stringify({ sources: draft.sources, datasets: artifactSelections })],
66
+ limits: {
67
+ timeoutMs: 30_000,
68
+ stdoutMaxBytes: PRESENTATION_BUDGETS.documentBytes,
69
+ stderrMaxBytes: 65_536,
70
+ },
71
+ signal: options.signal,
72
+ });
73
+ this.#assertReady(options.signal);
74
+ if (result.exitCode !== 0)
75
+ throw new MarivoEnvironmentError('subprocess-failed', 'The bound Marivo presentation read failed.', { exitCode: result.exitCode });
76
+ let payload;
77
+ try {
78
+ payload = record(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(result.stdout)));
79
+ }
80
+ catch {
81
+ invalidOutput();
82
+ }
83
+ if (payload.ok === false) {
84
+ exactKeys(payload, ['ok', 'error']);
85
+ const error = record(payload.error);
86
+ exactKeys(error, ['code', 'path', 'message']);
87
+ if (typeof error.code !== 'string' ||
88
+ !/^[a-z_]{1,128}$/.test(error.code) ||
89
+ typeof error.path !== 'string' ||
90
+ error.path.length > 1024 ||
91
+ !/^(?:\/(?:[^~]|~[01])*)*$/.test(error.path) ||
92
+ typeof error.message !== 'string' ||
93
+ error.message.length > 1024)
94
+ invalidOutput();
95
+ throw new PresentationContractError(error.code, error.path, error.message);
96
+ }
97
+ exactKeys(payload, ['ok', 'sources', 'datasets', 'diagnostics']);
98
+ if (payload.ok !== true ||
99
+ !Array.isArray(payload.sources) ||
100
+ !Array.isArray(payload.datasets) ||
101
+ !Array.isArray(payload.diagnostics))
102
+ invalidOutput();
103
+ if (payload.sources.length !== draft.sources.length ||
104
+ payload.datasets.length !== artifactSelections.length)
105
+ invalidOutput();
106
+ for (const [index, source] of payload.sources.entries()) {
107
+ const actual = record(source);
108
+ const ref = record(actual.ref);
109
+ const expected = draft.sources[index];
110
+ if (actual.id !== expected.id ||
111
+ ref.sessionId !== expected.ref.sessionId ||
112
+ ref.artifactRef !== expected.ref.artifactRef ||
113
+ ref.findingId !== expected.ref.findingId)
114
+ invalidOutput();
115
+ }
116
+ for (const [index, entry] of payload.datasets.entries()) {
117
+ const dataset = record(entry);
118
+ exactKeys(dataset, ['id', 'data']);
119
+ if (dataset.id !== artifactSelections[index].id)
120
+ invalidOutput();
121
+ artifactData.set(artifactSelections[index].id, dataset.data);
122
+ }
123
+ sources = payload.sources;
124
+ diagnostics = payload.diagnostics;
125
+ }
126
+ for (const [index, dataset] of draft.datasets.entries()) {
127
+ this.#assertReady(options.signal);
128
+ const location = `/datasets/${index}`;
129
+ let data;
130
+ if (dataset.kind === 'computed') {
131
+ data = await readWorkspaceJson(this.binding.projectRoot, dataset.path, PRESENTATION_BUDGETS.datasetBytes, `${location}/path`, options.signal);
132
+ }
133
+ else
134
+ data = artifactData.get(dataset.id);
135
+ data = parseTypedDataset(data, `${location}/data`);
136
+ if (dataset.kind === 'artifact') {
137
+ const typed = data;
138
+ if (typed.limit > dataset.rowLimit ||
139
+ (dataset.columns !== undefined &&
140
+ (typed.columns.length !== dataset.columns.length ||
141
+ typed.columns.some((column, index) => column.id !== dataset.columns[index]))))
142
+ invalidOutput();
143
+ }
144
+ const code = [];
145
+ for (const [codeIndex, ref] of (dataset.codeRefs ?? []).entries()) {
146
+ this.#assertReady(options.signal);
147
+ try {
148
+ code.push(await readPythonExecution(this.binding.projectRoot, ref, options.signal));
149
+ }
150
+ catch {
151
+ options.signal?.throwIfAborted();
152
+ throw new PresentationContractError('code_unavailable', `${location}/codeRefs/${codeIndex}`, 'The captured Python execution cannot be read with its exact Workspace identity and digest.', 'Check the codeRef returned by the successful marivo_python call; do not rerun analysis to repair presentation.');
153
+ }
154
+ }
155
+ this.#assertReady(options.signal);
156
+ datasets.push({
157
+ id: dataset.id,
158
+ origin: dataset.kind,
159
+ data: data,
160
+ sourceIds: dataset.kind === 'artifact' ? [dataset.sourceId] : dataset.sourceIds,
161
+ ...(dataset.codeRefs !== undefined ? { code } : {}),
162
+ });
163
+ }
164
+ for (const [index, dataset] of datasets.entries()) {
165
+ if (dataset.data.truncated)
166
+ diagnostics.push({
167
+ code: 'truncated',
168
+ path: `/datasets/${index}/data/rows`,
169
+ message: `Showing ${dataset.data.rows.length} of ${dataset.data.rowCount} rows; truncated data must not imply full totals or rankings.`,
170
+ });
171
+ }
172
+ for (const [index, source] of sources.entries()) {
173
+ if (source.status === 'unavailable')
174
+ diagnostics.push({
175
+ code: 'source_unavailable',
176
+ path: `/sources/${index}`,
177
+ message: 'The declared Artifact source cannot be restored in the bound Workspace.',
178
+ });
179
+ }
180
+ this.#assertReady(options.signal);
181
+ return parsePresentationDocument({
182
+ schemaVersion: 3,
183
+ locale: draft.locale,
184
+ reportId: options.reportId,
185
+ workspaceId: options.workspaceId,
186
+ buildId: options.buildId,
187
+ title: draft.title,
188
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
189
+ datasets,
190
+ sources,
191
+ blocks: draft.blocks,
192
+ ...(draft.interaction ? { interaction: draft.interaction } : {}),
193
+ diagnostics,
194
+ });
195
+ }
196
+ }
@@ -0,0 +1,137 @@
1
+ import { MARIVO_PRESENTATION_SQL_CODE_PROGRAM } from "./sql-code.js";
2
+ /** Fixed public read program. Drafts contain references and data paths, never executable code. */
3
+ export const MARIVO_PRESENTATION_READ_PROGRAM = String.raw `
4
+ import json
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ import marivo.analysis as mv
10
+ import marivo.datasource as md
11
+ from dsh_data_analysis_presentation._dataset import encode_dataset
12
+
13
+
14
+ class ProjectionFailure(Exception):
15
+ def __init__(self, code, path, message):
16
+ self.code, self.path, self.message = code, path, message
17
+
18
+
19
+ class DenyCredentials:
20
+ def resolve(self, request):
21
+ raise ProjectionFailure("credential_read_forbidden", "/sources", "Persisted presentation reads cannot obtain datasource credentials.")
22
+
23
+
24
+ def text(value, limit=32768):
25
+ result = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
26
+ result = result.replace("\0", "\\u0000")
27
+ return result if len(result) <= limit else result[:limit - 14] + " [truncated]"
28
+
29
+
30
+ ${MARIVO_PRESENTATION_SQL_CODE_PROGRAM}
31
+
32
+
33
+ def read(request):
34
+ sessions, artifacts, contracts = {}, {}, {}
35
+ sources, datasets, diagnostics = [], [], []
36
+ read_at = datetime.now(timezone.utc).isoformat()
37
+ try:
38
+ for index, declared in enumerate(request["sources"]):
39
+ ref = declared["ref"]
40
+ source_path = "/sources/" + str(index)
41
+ try:
42
+ session = sessions.get(ref["sessionId"])
43
+ if session is None:
44
+ session = mv.session.resume(ref["sessionId"], use_datasources=False)
45
+ sessions[ref["sessionId"]] = session
46
+ if session.id != ref["sessionId"]:
47
+ raise ProjectionFailure("source_identity_mismatch", source_path + "/ref/sessionId", "Restored Session identity differs from the declared source.")
48
+ artifact = session.artifact(ref["artifactRef"])
49
+ contract = artifact.contract()
50
+ meta = artifact.meta
51
+ if artifact.ref != ref["artifactRef"] or meta.session_id != session.id or contract.ref != artifact.ref:
52
+ raise ProjectionFailure("source_identity_mismatch", source_path + "/ref", "Restored Artifact identity differs from the declared source.")
53
+ if Path(meta.project_root).resolve() != Path.cwd().resolve():
54
+ raise ProjectionFailure("source_workspace_mismatch", source_path + "/ref", "Restored Artifact belongs to a different Workspace.")
55
+ except ProjectionFailure:
56
+ raise
57
+ except Exception:
58
+ # Only the requested source becomes unavailable. Exception messages may
59
+ # include credentials, project paths or raw SQL and are never returned.
60
+ sources.append({**declared, "status": "unavailable", "reason": "marivo.presentation.source-unavailable"})
61
+ continue
62
+
63
+ artifacts[declared["id"]], contracts[declared["id"]] = artifact, contract
64
+ facts = []
65
+ def fact(label, value):
66
+ facts.append({"label": label, "value": text(value)})
67
+ fact("读取时间", read_at)
68
+ fact("Artifact kind", meta.kind)
69
+ fact("创建时间", meta.created_at.isoformat())
70
+ fact("内容身份", meta.content_hash)
71
+ fact("Evidence status", meta.evidence_status)
72
+ fact("完整行数", meta.row_count)
73
+ fact("公开语义引用", [{"role": item.role, "kind": item.semantic_kind.value, "path": item.semantic_path, "outputColumn": item.output_column} for item in contract.semantic_inputs])
74
+ quality = meta.quality_summary
75
+ fact("Quality", None if quality is None else {"coverage": quality.coverage, "nullRate": quality.null_rate, "sampleSize": quality.sample_size, "evaluatedCheckCount": quality.evaluated_check_count, "failedCheckCount": quality.failed_check_count, "warningCheckCount": quality.warning_check_count})
76
+ fact("Issues", [{"kind": item.kind, "severity": item.severity} for item in contract.issues])
77
+ if "findingId" in ref:
78
+ try:
79
+ finding = artifact.finding(ref["findingId"])
80
+ if finding.finding_id != ref["findingId"] or finding.artifact_ref != artifact.ref or finding.session_id != session.id:
81
+ raise ProjectionFailure("source_identity_mismatch", source_path + "/ref/findingId", "Restored Finding identity differs from the declared source.")
82
+ excerpt = finding.render(max_output_bytes=4096)
83
+ fact("选择的 Finding", excerpt)
84
+ except ProjectionFailure:
85
+ raise
86
+ except Exception:
87
+ fact("选择的 Finding unavailable", "marivo.presentation.finding-unavailable")
88
+ diagnostics.append({"code": "finding_unavailable", "path": source_path + "/ref/findingId", "message": "Artifact is available; its requested Finding is unavailable."})
89
+ fact("revalidation", "not_requested")
90
+ sources.append({**declared, "status": "available", "label": text(str(meta.kind) + " " + artifact.ref, 512), "facts": facts, "code": sql_code_snapshot(session, artifact)})
91
+
92
+ for selection in request["datasets"]:
93
+ dataset_path = "/datasets/" + str(selection["index"])
94
+ artifact = artifacts.get(selection["sourceId"])
95
+ if artifact is None:
96
+ raise ProjectionFailure("artifact_data_unavailable", dataset_path + "/sourceId", "A direct Artifact dataset requires an available persisted Artifact source.")
97
+ try:
98
+ contract = contracts[selection["sourceId"]]
99
+ frame = artifact.to_pandas()
100
+ public_columns = list(contract.artifact_schema.columns)
101
+ if list(frame.columns) != [column.name for column in public_columns] or len(frame) != artifact.meta.row_count or artifact.row_count != len(frame):
102
+ raise ProjectionFailure("artifact_data_identity_mismatch", dataset_path, "Artifact rows or columns disagree with its public contract.")
103
+ names = selection.get("columns", list(frame.columns))
104
+ by_name = {column.name: column for column in public_columns}
105
+ if any(name not in by_name for name in names):
106
+ raise ProjectionFailure("invalid_reference", dataset_path + "/columns", "A requested column is absent from the public Artifact schema.")
107
+ # Use public terminal values; declared upstream dtypes cannot recover
108
+ # Decimal precision already lost by to_pandas(). No derived metrics.
109
+ data = encode_dataset(frame.loc[:, names], row_limit=selection["rowLimit"])
110
+ for column in data["columns"]:
111
+ column["nullable"] = by_name[column["id"]].nullable
112
+ datasets.append({"id": selection["id"], "data": data})
113
+ except ProjectionFailure:
114
+ raise
115
+ except Exception as error:
116
+ # Preserve helper contract locations without leaking upstream error text.
117
+ code = getattr(error, "code", None)
118
+ location = getattr(error, "path", None)
119
+ if code in ("numeric_precision", "budget", "invalid_value") and isinstance(location, str):
120
+ raise ProjectionFailure(code, dataset_path + "/data" + location, "Public Artifact values cannot be encoded within the presentation data contract.") from None
121
+ raise ProjectionFailure("artifact_data_unavailable", dataset_path + "/data", "The direct Artifact does not expose the required typed presentation data.") from None
122
+ return {"ok": True, "sources": sources, "datasets": datasets, "diagnostics": diagnostics}
123
+ finally:
124
+ for session in sessions.values():
125
+ session.close()
126
+
127
+
128
+ try:
129
+ request = json.loads(sys.argv[1])
130
+ with md.credential_scope(resolver=DenyCredentials()):
131
+ result = read(request)
132
+ except ProjectionFailure as error:
133
+ result = {"ok": False, "error": {"code": error.code, "path": error.path, "message": error.message}}
134
+ except Exception:
135
+ result = {"ok": False, "error": {"code": "projection_read_failed", "path": "/sources", "message": "The bound public presentation read failed."}}
136
+ print(json.dumps(result, ensure_ascii=False, allow_nan=False, separators=(",", ":")))
137
+ `.trim();
@@ -0,0 +1,79 @@
1
+ /** Only persisted public producer records are read; this program never compiles or executes SQL. */
2
+ export const MARIVO_PRESENTATION_SQL_CODE_PROGRAM = String.raw `
3
+ def sql_code_snapshot(session, artifact):
4
+ snippets, notices = [], []
5
+ def notice(message):
6
+ if message not in notices:
7
+ notices.append(message)
8
+ def valid_string(value, limit):
9
+ return isinstance(value, str) and bool(value) and "\0" not in value and len(value.encode("utf-16-le")) // 2 <= limit
10
+
11
+ try:
12
+ succeeded_run = mv.SucceededRun
13
+ get_run = session.get_run
14
+ except Exception:
15
+ return {"snippets": [], "notices": ["marivo.presentation.sql-capability-unavailable"]}
16
+
17
+ pending = [(artifact.ref, artifact)]
18
+ scheduled, runs, seen_queries = {artifact.ref}, {}, set()
19
+ query_count = 0
20
+ for expected_ref, known_artifact in pending:
21
+ try:
22
+ current = known_artifact if known_artifact is not None else session.artifact(expected_ref)
23
+ meta, contract = current.meta, current.contract()
24
+ if current.ref != expected_ref or contract.ref != expected_ref or meta.session_id != session.id or Path(meta.project_root).resolve() != Path.cwd().resolve():
25
+ notice("marivo.presentation.sql-artifact-mismatch")
26
+ continue
27
+ producer = meta.produced_by_job
28
+ if producer is None:
29
+ notice("marivo.presentation.sql-producer-missing")
30
+ continue
31
+ if not valid_string(producer, 512):
32
+ notice("marivo.presentation.sql-producer-invalid")
33
+ continue
34
+ run = runs.get(producer)
35
+ if run is None:
36
+ run = get_run(producer)
37
+ runs[producer] = run
38
+ if not isinstance(run, succeeded_run) or run.run_id != producer or run.output_mode != "produced" or run.output_artifact_ref != expected_ref:
39
+ notice("marivo.presentation.sql-run-mismatch")
40
+ continue
41
+ for query in run.queries:
42
+ if query_count >= 32:
43
+ notice("marivo.presentation.sql-query-limit")
44
+ break
45
+ query_count += 1
46
+ query_id = query.query_id
47
+ if not valid_string(query_id, 512):
48
+ notice("marivo.presentation.sql-query-invalid")
49
+ continue
50
+ query_key = (producer, query_id)
51
+ if query_key in seen_queries:
52
+ continue
53
+ seen_queries.add(query_key)
54
+ sql = query.sql
55
+ if not valid_string(sql, 32768) or not sql.strip():
56
+ notice("marivo.presentation.sql-sql-invalid")
57
+ continue
58
+ snippets.append({"language": "sql", "text": sql, "provenance": "execution", "runId": producer, "queryId": query_id, "artifactRef": expected_ref})
59
+ for input_index, input_ref in enumerate(run.input_artifact_refs):
60
+ if input_index >= 65:
61
+ notice("marivo.presentation.sql-upstream-limit")
62
+ break
63
+ if not valid_string(input_ref, 512):
64
+ notice("marivo.presentation.sql-upstream-invalid")
65
+ continue
66
+ if input_ref in scheduled:
67
+ continue
68
+ if len(scheduled) >= 65:
69
+ notice("marivo.presentation.sql-upstream-limit")
70
+ break
71
+ scheduled.add(input_ref)
72
+ pending.append((input_ref, None))
73
+ except Exception:
74
+ # A missing record or capability affects code only. Never return exception text.
75
+ notice("marivo.presentation.sql-read-unavailable")
76
+ if not snippets and not notices:
77
+ notice("marivo.presentation.sql-not-saved")
78
+ return {"snippets": snippets, "notices": notices}
79
+ `.trim();
@@ -0,0 +1,34 @@
1
+ /** Pure Host delivery envelope shared by the server and browser. */
2
+ import { parsePresentationReceipt } from "./contracts/index.js";
3
+ export const MARIVO_PRESENT_TOOL_NAME = 'marivo_present';
4
+ export const MARIVO_PRESENTATION_DELIVERY_KIND = 'marivo.presentation.delivery';
5
+ export const MARIVO_PRESENTATION_RPC_CHANNEL = '/marivo-presentation';
6
+ export function parsePresentationDelivery(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value))
8
+ throw new Error('invalid-presentation-delivery');
9
+ const input = value;
10
+ if (Object.keys(input).sort().join(',') !== 'dshSessionId,kind,receipt,schemaVersion,turn' ||
11
+ input.kind !== MARIVO_PRESENTATION_DELIVERY_KIND ||
12
+ input.schemaVersion !== 2 ||
13
+ typeof input.dshSessionId !== 'string' ||
14
+ !input.dshSessionId.trim() ||
15
+ input.dshSessionId.length > 512 ||
16
+ !Number.isSafeInteger(input.turn) ||
17
+ input.turn < 0)
18
+ throw new Error('invalid-presentation-delivery');
19
+ parsePresentationReceipt(input.receipt);
20
+ return input;
21
+ }
22
+ export function presentationReceiptText(receipt) {
23
+ return [
24
+ receipt.title,
25
+ receipt.summary,
26
+ `Workspace: ${receipt.workspaceId}`,
27
+ `Report: ${receipt.reportId}`,
28
+ `Build: ${receipt.buildId}`,
29
+ ...Object.values(receipt.files).flatMap((file) => [
30
+ `${file.asset}: ${file.path}`,
31
+ `sha256: ${file.sha256}; bytes: ${file.bytes}`,
32
+ ]),
33
+ ].join('\n');
34
+ }
@@ -0,0 +1,219 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, open, opendir, realpath, rename, rm } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { withFileLock } from '@deepseek-ai/dsh-atomic-write';
6
+ import { buildPresentation } from "./build/index.js";
7
+ import { commitPresentation } from "./commit.js";
8
+ import { parseReportHistory, REPORT_HISTORY_BYTES, REPORT_HISTORY_LIMIT, } from "./contracts/catalog.js";
9
+ import { parsePresentationDocument, parsePresentationReceipt, } from "./contracts/index.js";
10
+ import { presentationAssetPath, presentationReportPath, presentationSha256, readPresentationAsset, readReportFile, } from "./files.js";
11
+ export async function readReceiptDocument(root, receipt, signal) {
12
+ for (const asset of Object.values(receipt.files)) {
13
+ if (asset.path !== presentationAssetPath(root, receipt.reportId, receipt.buildId, asset.asset))
14
+ throw new Error('asset-path-mismatch');
15
+ }
16
+ const bytes = await readPresentationAsset(root, receipt.reportId, receipt.buildId, 'presentation.json', signal);
17
+ if (bytes.length !== receipt.files.document.bytes ||
18
+ presentationSha256(bytes) !== receipt.files.document.sha256)
19
+ throw new Error('asset-digest-mismatch');
20
+ const document = parsePresentationDocument(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
21
+ if (document.workspaceId !== receipt.workspaceId ||
22
+ document.reportId !== receipt.reportId ||
23
+ document.buildId !== receipt.buildId ||
24
+ document.title !== receipt.title)
25
+ throw new Error('asset-owner-mismatch');
26
+ return document;
27
+ }
28
+ /** Current and its publication history become visible through one atomic rename. */
29
+ export async function readReportHistory(root, workspaceId, reportId, signal) {
30
+ const bytes = await readReportFile(root, path.join(presentationReportPath(root, reportId), 'current.json'), REPORT_HISTORY_BYTES, signal);
31
+ const current = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
32
+ if (!current || current.workspaceId !== workspaceId || current.reportId !== reportId)
33
+ throw new Error('invalid-report-current');
34
+ const receipt = parsePresentationReceipt(current.receipt);
35
+ if (receipt.workspaceId !== workspaceId || receipt.reportId !== reportId)
36
+ throw new Error('asset-owner-mismatch');
37
+ if (current.schemaVersion === 2 &&
38
+ Object.keys(current).sort().join(',') === 'receipt,reportId,schemaVersion,workspaceId') {
39
+ return {
40
+ workspaceId,
41
+ reportId,
42
+ currentBuildId: receipt.buildId,
43
+ legacyHistoryUnavailable: true,
44
+ versions: [{ receipt, publishedAt: null, source: null }],
45
+ };
46
+ }
47
+ if (current.schemaVersion !== 3 ||
48
+ Object.keys(current).sort().join(',') !==
49
+ 'legacyHistoryUnavailable,receipt,reportId,schemaVersion,versions,workspaceId')
50
+ throw new Error('invalid-report-current');
51
+ const history = parseReportHistory({
52
+ workspaceId,
53
+ reportId,
54
+ currentBuildId: receipt.buildId,
55
+ legacyHistoryUnavailable: current.legacyHistoryUnavailable,
56
+ versions: current.versions,
57
+ });
58
+ if (JSON.stringify(history.versions[0].receipt) !== JSON.stringify(receipt))
59
+ throw new Error('invalid-report-history');
60
+ return history;
61
+ }
62
+ export async function resolvePresentation(root, workspaceId, reportId, signal) {
63
+ const history = await readReportHistory(root, workspaceId, reportId, signal);
64
+ const receipt = history.versions[0].receipt;
65
+ await readReceiptDocument(root, receipt, signal);
66
+ return receipt;
67
+ }
68
+ /** Enumerate only publication pointers, never completed-but-unpublished build directories. */
69
+ export async function listReports(root, workspaceId, signal) {
70
+ const directory = path.dirname(presentationReportPath(root, 'catalog'));
71
+ const parents = [root, path.dirname(directory), directory];
72
+ const identities = [];
73
+ try {
74
+ for (const name of parents) {
75
+ const stat = await lstat(name);
76
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (await realpath(name)) !== name)
77
+ throw new Error('asset-path-mismatch');
78
+ identities.push(stat);
79
+ }
80
+ }
81
+ catch (error) {
82
+ if (error.code === 'ENOENT')
83
+ return { workspaceId, reports: [], unavailable: 0 };
84
+ throw error;
85
+ }
86
+ const reports = [];
87
+ let unavailable = 0, count = 0;
88
+ for await (const entry of await opendir(directory)) {
89
+ signal?.throwIfAborted();
90
+ if (++count > 4096)
91
+ throw new Error('report-catalog-too-large');
92
+ if (entry.name.startsWith('.'))
93
+ continue;
94
+ try {
95
+ if (!entry.isDirectory() || entry.isSymbolicLink())
96
+ throw new Error('asset-not-file');
97
+ const history = await readReportHistory(root, workspaceId, entry.name, signal);
98
+ reports.push(history.versions[0]);
99
+ }
100
+ catch (error) {
101
+ signal?.throwIfAborted();
102
+ if (error.code !== 'ENOENT')
103
+ unavailable++;
104
+ }
105
+ }
106
+ for (const [index, name] of parents.entries()) {
107
+ const stat = await lstat(name);
108
+ if (stat.dev !== identities[index].dev ||
109
+ stat.ino !== identities[index].ino ||
110
+ stat.isSymbolicLink() ||
111
+ (await realpath(name)) !== name)
112
+ throw new Error('asset-path-mismatch');
113
+ }
114
+ reports.sort((a, b) => (b.publishedAt ?? '').localeCompare(a.publishedAt ?? '') ||
115
+ a.receipt.reportId.localeCompare(b.receipt.reportId));
116
+ return { workspaceId, reports, unavailable };
117
+ }
118
+ /** The mutable pointer is the publication boundary; completed builds are never overwritten. */
119
+ export async function publishPresentation(root, document, expectedBuildId, assertOwner, signal, source = null) {
120
+ await assertOwner();
121
+ signal?.throwIfAborted();
122
+ const built = await buildPresentation(document, { html: false });
123
+ await assertOwner();
124
+ const receipt = await commitPresentation(root, built, assertOwner, signal);
125
+ const directory = presentationReportPath(root, document.reportId);
126
+ const currentPath = path.join(directory, 'current.json');
127
+ const parents = [root];
128
+ for (const segment of path.relative(root, directory).split(path.sep))
129
+ parents.push(path.join(parents.at(-1), segment));
130
+ const identities = await Promise.all(parents.map((parent) => lstat(parent)));
131
+ const check = async () => {
132
+ signal?.throwIfAborted();
133
+ await assertOwner();
134
+ const actual = await Promise.all(parents.map((parent) => lstat(parent)));
135
+ if ((await realpath(directory)) !== directory ||
136
+ actual.some((entry, index) => !entry.isDirectory() ||
137
+ entry.isSymbolicLink() ||
138
+ entry.dev !== identities[index].dev ||
139
+ entry.ino !== identities[index].ino))
140
+ throw new Error('presentation-directory-changed');
141
+ };
142
+ await check();
143
+ await withFileLock(currentPath, async () => {
144
+ await check();
145
+ let history;
146
+ let current;
147
+ try {
148
+ history = await readReportHistory(root, document.workspaceId, document.reportId, signal);
149
+ current = history.versions[0].receipt;
150
+ await readReceiptDocument(root, current, signal);
151
+ }
152
+ catch (error) {
153
+ if (expectedBuildId !== null || error.code !== 'ENOENT')
154
+ throw error;
155
+ // ENOENT is allowed only for an absent pointer, never a dangling current build.
156
+ try {
157
+ await lstat(currentPath);
158
+ throw new Error('invalid-report-current');
159
+ }
160
+ catch (missing) {
161
+ if (missing.code !== 'ENOENT')
162
+ throw missing;
163
+ }
164
+ }
165
+ if ((current?.buildId ?? null) !== expectedBuildId)
166
+ throw new Error('report-save-conflict');
167
+ const versions = [
168
+ { receipt, publishedAt: new Date().toISOString(), source },
169
+ ...(history?.versions ?? []),
170
+ ];
171
+ if (versions.length > REPORT_HISTORY_LIMIT)
172
+ throw new Error('report-history-full');
173
+ parseReportHistory({
174
+ workspaceId: document.workspaceId,
175
+ reportId: document.reportId,
176
+ currentBuildId: receipt.buildId,
177
+ legacyHistoryUnavailable: history?.legacyHistoryUnavailable ?? false,
178
+ versions,
179
+ });
180
+ const pointer = JSON.stringify({
181
+ schemaVersion: 3,
182
+ workspaceId: document.workspaceId,
183
+ reportId: document.reportId,
184
+ receipt,
185
+ versions,
186
+ legacyHistoryUnavailable: history?.legacyHistoryUnavailable ?? false,
187
+ });
188
+ if (Buffer.byteLength(pointer) > REPORT_HISTORY_BYTES)
189
+ throw new Error('report-history-full');
190
+ const temporary = path.join(directory, `.current-${randomUUID()}.tmp`);
191
+ let published = false;
192
+ try {
193
+ const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
194
+ try {
195
+ await file.writeFile(pointer, { signal });
196
+ await file.sync();
197
+ }
198
+ finally {
199
+ await file.close();
200
+ }
201
+ await check();
202
+ await rename(temporary, currentPath);
203
+ published = true;
204
+ const parent = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
205
+ try {
206
+ await parent.sync();
207
+ }
208
+ finally {
209
+ await parent.close();
210
+ }
211
+ // Cancellation after the rename cannot undo a published save.
212
+ }
213
+ finally {
214
+ if (!published)
215
+ await rm(temporary, { force: true });
216
+ }
217
+ }, { waitMs: 5000 });
218
+ return receipt;
219
+ }