@chengxianglibra/dsh-data-analysis 0.1.2 → 0.2.1

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 (222) hide show
  1. package/README.en.md +135 -0
  2. package/README.md +105 -98
  3. package/cordis.patch.yml +1 -1
  4. package/lib/bin/environment.js +8 -6
  5. package/lib/client/credentials/changes.js +74 -0
  6. package/lib/client/credentials/create-datasource.js +126 -22
  7. package/lib/client/credentials/credential-draft.js +42 -0
  8. package/lib/client/credentials/defaults.js +11 -0
  9. package/lib/client/credentials/descriptions.js +46 -0
  10. package/lib/client/credentials/install.js +209 -81
  11. package/lib/client/credentials/model.js +323 -51
  12. package/lib/client/credentials/report-publishing.js +96 -0
  13. package/lib/client/credentials/styles.js +13 -9
  14. package/lib/client/i18n/context.js +22 -0
  15. package/lib/client/i18n/copy.js +60 -0
  16. package/lib/client/i18n/credentials.js +475 -0
  17. package/lib/client/i18n/host.js +28 -0
  18. package/lib/client/i18n/navigation.js +41 -0
  19. package/lib/client/i18n/presentation.js +577 -0
  20. package/lib/client/i18n/python.js +36 -0
  21. package/lib/client/i18n/semantic.js +441 -0
  22. package/lib/client/i18n/settings.js +30 -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/settings/card.js +90 -0
  36. package/lib/client/settings/install.js +10 -0
  37. package/lib/client/workspace-header-action.js +14 -1
  38. package/lib/client/workspace-shortcuts.js +26 -0
  39. package/lib/client.js +388 -242
  40. package/lib/compatibility.js +1 -1
  41. package/lib/datasource/authoring.js +43 -4
  42. package/lib/datasource/bridge-programs.js +23 -20
  43. package/lib/datasource/bridge.js +46 -3
  44. package/lib/datasource/changes-contract.js +41 -0
  45. package/lib/datasource/changes.js +49 -0
  46. package/lib/datasource/configure.js +39 -0
  47. package/lib/datasource/defaults.js +25 -0
  48. package/lib/datasource/index.js +2 -1
  49. package/lib/datasource/python-options.js +15 -0
  50. package/lib/datasource/python.js +119 -25
  51. package/lib/datasource/rpc.js +75 -13
  52. package/lib/datasource/service.js +286 -11
  53. package/lib/datasource/test.js +3 -2
  54. package/lib/disclosure/activation.js +19 -7
  55. package/lib/disclosure/execution-guidance.js +49 -0
  56. package/lib/disclosure/help.js +4 -1
  57. package/lib/environment/runtime.js +35 -34
  58. package/lib/environment/workspace.js +18 -1
  59. package/lib/lifecycle.js +33 -0
  60. package/lib/plugin-agents.js +154 -0
  61. package/lib/plugin.js +98 -172
  62. package/lib/presentation/asset.js +34 -0
  63. package/lib/presentation/assets/portable.js +14 -14
  64. package/lib/presentation/assets/static.js +344 -73
  65. package/lib/presentation/build/index.js +3 -2
  66. package/lib/presentation/commit.js +19 -12
  67. package/lib/presentation/contracts/asset-path.js +9 -0
  68. package/lib/presentation/contracts/catalog.js +62 -0
  69. package/lib/presentation/contracts/charts.js +0 -23
  70. package/lib/presentation/contracts/index.js +17 -5
  71. package/lib/presentation/contracts/types.js +1 -1
  72. package/lib/presentation/delivery.js +45 -30
  73. package/lib/presentation/export-html.js +214 -0
  74. package/lib/presentation/files.js +4 -5
  75. package/lib/presentation/projection/index.js +2 -1
  76. package/lib/presentation/projection/program.js +2 -9
  77. package/lib/presentation/projection/sql-code.js +13 -13
  78. package/lib/presentation/reports.js +114 -17
  79. package/lib/presentation/rpc.js +96 -58
  80. package/lib/presentation/tool.js +6 -10
  81. package/lib/report-publishing/adapters.js +111 -0
  82. package/lib/report-publishing/config.js +69 -0
  83. package/lib/report-publishing/service.js +180 -0
  84. package/lib/rpc.js +49 -0
  85. package/lib/semantic-browser/contracts.js +17 -0
  86. package/lib/semantic-browser/program.js +10 -0
  87. package/lib/semantic-browser/service.js +40 -6
  88. package/lib/semantic-reference/bridge.js +10 -1
  89. package/lib/semantic-reference/contracts.js +20 -6
  90. package/lib/semantic-reference/environment.js +56 -0
  91. package/lib/semantic-reference/labels.js +16 -0
  92. package/lib/semantic-reference/rpc.js +39 -18
  93. package/lib/semantic-reference/search.js +5 -4
  94. package/lib/semantic-reference/usage.js +12 -3
  95. package/lib/settings.js +21 -0
  96. package/lib/tool-lifecycle.js +22 -0
  97. package/lib/types/client/credentials/changes.d.ts +9 -0
  98. package/lib/types/client/credentials/create-datasource.d.ts +3 -1
  99. package/lib/types/client/credentials/credential-draft.d.ts +14 -0
  100. package/lib/types/client/credentials/defaults.d.ts +2 -0
  101. package/lib/types/client/credentials/descriptions.d.ts +1 -0
  102. package/lib/types/client/credentials/install.d.ts +19 -1
  103. package/lib/types/client/credentials/model.d.ts +31 -11
  104. package/lib/types/client/credentials/report-publishing.d.ts +5 -0
  105. package/lib/types/client/credentials/styles.d.ts +1 -1
  106. package/lib/types/client/i18n/context.d.ts +17 -0
  107. package/lib/types/client/i18n/copy.d.ts +821 -0
  108. package/lib/types/client/i18n/credentials.d.ts +475 -0
  109. package/lib/types/client/i18n/host.d.ts +17 -0
  110. package/lib/types/client/i18n/navigation.d.ts +41 -0
  111. package/lib/types/client/i18n/presentation.d.ts +577 -0
  112. package/lib/types/client/i18n/python.d.ts +19 -0
  113. package/lib/types/client/i18n/semantic.d.ts +441 -0
  114. package/lib/types/client/i18n/settings.d.ts +16 -0
  115. package/lib/types/client/input-context.d.ts +19 -0
  116. package/lib/types/client/presentation/ask-dsh.d.ts +4 -8
  117. package/lib/types/client/presentation/catalog-model.d.ts +24 -0
  118. package/lib/types/client/presentation/catalog.d.ts +20 -0
  119. package/lib/types/client/presentation/chart-tooltip.d.ts +2 -2
  120. package/lib/types/client/presentation/context-reference.d.ts +12 -0
  121. package/lib/types/client/presentation/copy-context.d.ts +2 -2
  122. package/lib/types/client/presentation/delivery-model.d.ts +27 -6
  123. package/lib/types/client/presentation/delivery.d.ts +8 -7
  124. package/lib/types/client/presentation/download.d.ts +2 -0
  125. package/lib/types/client/presentation/export-menu.d.ts +26 -0
  126. package/lib/types/client/presentation/export-view.d.ts +63 -0
  127. package/lib/types/client/presentation/host-entry.d.ts +8 -2
  128. package/lib/types/client/presentation/icons.d.ts +1 -1
  129. package/lib/types/client/presentation/model.d.ts +14 -15
  130. package/lib/types/client/presentation/reader.d.ts +15 -2
  131. package/lib/types/client/presentation/reference-source.d.ts +9 -0
  132. package/lib/types/client/presentation/source-code-model.d.ts +2 -1
  133. package/lib/types/client/presentation/source-code.d.ts +0 -5
  134. package/lib/types/client/presentation/source-dialog.d.ts +1 -2
  135. package/lib/types/client/presentation/sources.d.ts +5 -5
  136. package/lib/types/client/presentation/styles.d.ts +1 -1
  137. package/lib/types/client/presentation/table.d.ts +5 -1
  138. package/lib/types/client/python-tool/card.d.ts +2 -0
  139. package/lib/types/client/python-tool/execution-error.d.ts +3 -0
  140. package/lib/types/client/python-tool/install.d.ts +2 -0
  141. package/lib/types/client/python-tool/model.d.ts +20 -0
  142. package/lib/types/client/right-tabs/install.d.ts +18 -0
  143. package/lib/types/client/right-tabs/live-delivery.d.ts +8 -0
  144. package/lib/types/client/right-tabs/navigation.d.ts +17 -0
  145. package/lib/types/client/right-tabs/page.d.ts +36 -0
  146. package/lib/types/client/right-tabs/styles.d.ts +2 -0
  147. package/lib/types/client/rpc.d.ts +3 -0
  148. package/lib/types/client/semantic-reference.d.ts +4 -0
  149. package/lib/types/client/settings/card.d.ts +9 -0
  150. package/lib/types/client/settings/install.d.ts +2 -0
  151. package/lib/types/client/workspace-header-action.d.ts +1 -1
  152. package/lib/types/client/workspace-shortcuts.d.ts +15 -0
  153. package/lib/types/client.d.ts +5 -2
  154. package/lib/types/compatibility.d.ts +1 -1
  155. package/lib/types/datasource/authoring.d.ts +10 -0
  156. package/lib/types/datasource/bridge.d.ts +23 -2
  157. package/lib/types/datasource/changes-contract.d.ts +18 -0
  158. package/lib/types/datasource/changes.d.ts +15 -0
  159. package/lib/types/datasource/configure.d.ts +7 -0
  160. package/lib/types/datasource/defaults.d.ts +7 -0
  161. package/lib/types/datasource/index.d.ts +6 -1
  162. package/lib/types/datasource/python-options.d.ts +8 -0
  163. package/lib/types/datasource/python.d.ts +17 -2
  164. package/lib/types/datasource/rpc.d.ts +2 -1
  165. package/lib/types/datasource/service.d.ts +48 -5
  166. package/lib/types/datasource/test.d.ts +1 -1
  167. package/lib/types/disclosure/activation.d.ts +2 -1
  168. package/lib/types/disclosure/execution-guidance.d.ts +6 -0
  169. package/lib/types/disclosure/help.d.ts +1 -1
  170. package/lib/types/environment/types.d.ts +2 -1
  171. package/lib/types/environment/workspace.d.ts +1 -0
  172. package/lib/types/lifecycle.d.ts +8 -0
  173. package/lib/types/plugin-agents.d.ts +29 -0
  174. package/lib/types/plugin.d.ts +12 -28
  175. package/lib/types/presentation/asset.d.ts +6 -0
  176. package/lib/types/presentation/build/index.d.ts +7 -2
  177. package/lib/types/presentation/commit.d.ts +1 -1
  178. package/lib/types/presentation/contracts/asset-path.d.ts +2 -0
  179. package/lib/types/presentation/contracts/catalog.d.ts +27 -0
  180. package/lib/types/presentation/contracts/charts.d.ts +0 -2
  181. package/lib/types/presentation/contracts/types.d.ts +7 -4
  182. package/lib/types/presentation/delivery.d.ts +1 -1
  183. package/lib/types/presentation/export-html.d.ts +13 -0
  184. package/lib/types/presentation/reports.d.ts +6 -1
  185. package/lib/types/presentation/rpc.d.ts +3 -2
  186. package/lib/types/presentation/tool.d.ts +2 -2
  187. package/lib/types/report-publishing/adapters.d.ts +6 -0
  188. package/lib/types/report-publishing/config.d.ts +22 -0
  189. package/lib/types/report-publishing/service.d.ts +68 -0
  190. package/lib/types/rpc.d.ts +3 -0
  191. package/lib/types/semantic-browser/contracts.d.ts +7 -0
  192. package/lib/types/semantic-browser/service.d.ts +7 -1
  193. package/lib/types/semantic-reference/bridge.d.ts +1 -0
  194. package/lib/types/semantic-reference/contracts.d.ts +5 -2
  195. package/lib/types/semantic-reference/environment.d.ts +22 -0
  196. package/lib/types/semantic-reference/labels.d.ts +16 -0
  197. package/lib/types/semantic-reference/rpc.d.ts +1 -1
  198. package/lib/types/semantic-reference/search.d.ts +1 -1
  199. package/lib/types/settings.d.ts +14 -0
  200. package/lib/types/tool-lifecycle.d.ts +4 -0
  201. package/lib/types/workspace-identity.d.ts +6 -0
  202. package/lib/workspace-identity.js +9 -0
  203. package/package.json +93 -61
  204. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  205. package/skills/dsh-data-analysis-files/SKILL.md +44 -0
  206. package/skills/dsh-data-analysis-files/references/delivery.md +15 -0
  207. package/skills/dsh-data-analysis-files/references/examples.md +93 -0
  208. package/skills/dsh-data-analysis-presentation/SKILL.md +15 -15
  209. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +13 -4
  210. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +3 -2
  211. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +3 -2
  212. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +12 -5
  213. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +3 -2
  214. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +3 -2
  215. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +12 -4
  216. package/skills/dsh-data-analysis-presentation/references/examples.md +3 -1
  217. package/skills/dsh-data-analysis-presentation/references/export-html.md +7 -0
  218. package/skills/dsh-data-analysis-presentation/references/interaction.md +1 -1
  219. package/skills/dsh-data-analysis-presentation/references/narrative.md +1 -1
  220. package/skills/dsh-data-analysis-presentation/references/publishing.md +3 -0
  221. package/skills/dsh-data-analysis-presentation/references/schema.md +29 -2
  222. package/lib/types/client/presentation/install.d.ts +0 -15
@@ -7,10 +7,11 @@ import {
7
7
  } from "../contracts/index.js";
8
8
  var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9
9
  var inlineJson = (value) => JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
10
- async function buildPresentation(value) {
10
+ async function buildPresentation(value, options = { html: true }) {
11
11
  parsePresentationDocument(value);
12
12
  const documentBytes = Buffer.from(JSON.stringify(value), "utf8");
13
13
  const document = parsePresentationDocument(JSON.parse(documentBytes.toString("utf8")));
14
+ if (!options.html) return { document, documentBytes };
14
15
  const [portable, staticAssets] = await Promise.all([
15
16
  import(new URL("../../../lib/presentation/assets/portable.js", import.meta.url).href),
16
17
  import(new URL("../../../lib/presentation/assets/static.js", import.meta.url).href)
@@ -35,7 +36,7 @@ async function buildPresentation(value) {
35
36
  ].join("; ");
36
37
  const htmlBytes = Buffer.from(
37
38
  `<!doctype html>
38
- <html lang="zh-CN">
39
+ <html lang="${escapeHtml(document.locale)}">
39
40
  <head>
40
41
  <meta charset="utf-8">
41
42
  <meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">
@@ -23,18 +23,18 @@ async function sameDirectory(filename, expected) {
23
23
  if (actual.dev !== expected.dev || actual.ino !== expected.ino)
24
24
  throw new Error('presentation-directory-changed');
25
25
  }
26
- /** Owns only one newly-created build. A single directory rename publishes both complete files. */
26
+ /** Owns only one newly-created build. A single directory rename publishes all requested complete files. */
27
27
  export async function commitPresentation(workspaceRoot, input, assertOwner, signal) {
28
28
  signal?.throwIfAborted();
29
29
  const document = parsePresentationDocument(structuredClone(input.document));
30
30
  const built = {
31
31
  document,
32
32
  documentBytes: Buffer.from(input.documentBytes),
33
- htmlBytes: Buffer.from(input.htmlBytes),
33
+ htmlBytes: input.htmlBytes && Buffer.from(input.htmlBytes),
34
34
  };
35
35
  if (built.documentBytes.length > PRESENTATION_BUDGETS.documentBytes ||
36
- built.htmlBytes.length === 0 ||
37
- built.htmlBytes.length > PRESENTATION_BUDGETS.htmlBytes)
36
+ (built.htmlBytes !== undefined &&
37
+ (built.htmlBytes.length === 0 || built.htmlBytes.length > PRESENTATION_BUDGETS.htmlBytes)))
38
38
  throw new Error('presentation-build-budget');
39
39
  if (!built.documentBytes.equals(Buffer.from(JSON.stringify(document))))
40
40
  throw new Error('presentation-build-document-mismatch');
@@ -82,6 +82,8 @@ export async function commitPresentation(workspaceRoot, input, assertOwner, sign
82
82
  ['presentation.json', built.documentBytes],
83
83
  ['index.html', built.htmlBytes],
84
84
  ]) {
85
+ if (!bytes)
86
+ continue;
85
87
  await sameDirectory(temporary, ownedIdentity);
86
88
  signal?.throwIfAborted();
87
89
  const file = await open(path.join(temporary, asset), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
@@ -112,8 +114,9 @@ export async function commitPresentation(workspaceRoot, input, assertOwner, sign
112
114
  await check();
113
115
  await sameDirectory(final, ownedIdentity);
114
116
  const document = await readPresentationAsset(root, built.document.reportId, built.document.buildId, 'presentation.json', signal);
115
- const html = await readPresentationAsset(root, built.document.reportId, built.document.buildId, 'index.html', signal);
116
- if (!document.equals(built.documentBytes) || !html.equals(built.htmlBytes))
117
+ const html = built.htmlBytes &&
118
+ (await readPresentationAsset(root, built.document.reportId, built.document.buildId, 'index.html', signal));
119
+ if (!document.equals(built.documentBytes) || (html && !html.equals(built.htmlBytes)))
117
120
  throw new Error('presentation-build-changed');
118
121
  await check();
119
122
  return parsePresentationReceipt({
@@ -131,12 +134,16 @@ export async function commitPresentation(workspaceRoot, input, assertOwner, sign
131
134
  sha256: presentationSha256(document),
132
135
  bytes: document.length,
133
136
  },
134
- html: {
135
- asset: 'index.html',
136
- path: path.join(final, 'index.html'),
137
- sha256: presentationSha256(html),
138
- bytes: html.length,
139
- },
137
+ ...(html
138
+ ? {
139
+ html: {
140
+ asset: 'index.html',
141
+ path: path.join(final, 'index.html'),
142
+ sha256: presentationSha256(html),
143
+ bytes: html.length,
144
+ },
145
+ }
146
+ : {}),
140
147
  },
141
148
  });
142
149
  }
@@ -0,0 +1,9 @@
1
+ /** Fixed Build locator shared by the browser and Workspace storage. */
2
+ import { parsePresentationBuildId } from "./index.js";
3
+ export function presentationAssetRelativePath(reportId, buildId, asset) {
4
+ parsePresentationBuildId(reportId, '/reportId');
5
+ parsePresentationBuildId(buildId);
6
+ if (asset !== 'presentation.json' && asset !== 'index.html')
7
+ throw new Error('invalid-asset');
8
+ return `.dsh-data-analysis/presentations/${reportId}/builds/${buildId}/${asset}`;
9
+ }
@@ -0,0 +1,62 @@
1
+ import { parsePresentationReceipt } from "./index.js";
2
+ export const REPORT_HISTORY_BYTES = 16 * 1024 * 1024;
3
+ export const REPORT_HISTORY_LIMIT = 4096;
4
+ export function parseReportVersion(value) {
5
+ const entry = value;
6
+ if (!entry ||
7
+ Object.keys(entry).sort().join(',') !== 'publishedAt,receipt,source' ||
8
+ !(entry.publishedAt === null ||
9
+ (typeof entry.publishedAt === 'string' &&
10
+ Number.isFinite(Date.parse(entry.publishedAt)) &&
11
+ new Date(entry.publishedAt).toISOString() === entry.publishedAt)) ||
12
+ !(entry.source === null ||
13
+ (entry.source &&
14
+ Object.keys(entry.source).sort().join(',') === 'kind,sessionId' &&
15
+ ['agent', 'reader'].includes(entry.source.kind) &&
16
+ (entry.source.sessionId === null ||
17
+ (typeof entry.source.sessionId === 'string' &&
18
+ !!entry.source.sessionId.trim() &&
19
+ entry.source.sessionId.length <= 512)))))
20
+ throw new Error('invalid-report-history');
21
+ parsePresentationReceipt(entry.receipt);
22
+ return entry;
23
+ }
24
+ export function parseReportHistory(value) {
25
+ const history = value;
26
+ if (!history ||
27
+ !Array.isArray(history.versions) ||
28
+ !history.versions.length ||
29
+ history.versions.length > REPORT_HISTORY_LIMIT ||
30
+ typeof history.legacyHistoryUnavailable !== 'boolean')
31
+ throw new Error('invalid-report-history');
32
+ const ids = new Set();
33
+ for (const entry of history.versions) {
34
+ const { receipt } = parseReportVersion(entry);
35
+ if (receipt.workspaceId !== history.workspaceId ||
36
+ receipt.reportId !== history.reportId ||
37
+ ids.has(receipt.buildId))
38
+ throw new Error('invalid-report-history');
39
+ ids.add(receipt.buildId);
40
+ }
41
+ if (history.currentBuildId !== history.versions[0].receipt.buildId)
42
+ throw new Error('invalid-report-history');
43
+ return history;
44
+ }
45
+ export function parseReportCatalog(value) {
46
+ const catalog = value;
47
+ if (!catalog ||
48
+ typeof catalog.workspaceId !== 'string' ||
49
+ !Array.isArray(catalog.reports) ||
50
+ catalog.reports.length > 4096 ||
51
+ !Number.isInteger(catalog.unavailable) ||
52
+ catalog.unavailable < 0)
53
+ throw new Error('invalid-report-catalog');
54
+ const ids = new Set();
55
+ for (const item of catalog.reports) {
56
+ const { receipt } = parseReportVersion(item);
57
+ if (receipt.workspaceId !== catalog.workspaceId || ids.has(receipt.reportId))
58
+ throw new Error('invalid-report-catalog');
59
+ ids.add(receipt.reportId);
60
+ }
61
+ return catalog;
62
+ }
@@ -20,26 +20,6 @@ export const CHART_TYPES = [
20
20
  'funnel',
21
21
  'waterfall',
22
22
  ];
23
- export const CHART_LABELS = {
24
- line: '折线图',
25
- area: '面积图',
26
- stackedArea: '堆叠面积图',
27
- sparkline: '迷你趋势图',
28
- bar: '柱状图',
29
- horizontalBar: '横向条形图',
30
- stackedBar: '堆叠柱状图',
31
- stackedBar100: '百分比堆叠柱状图',
32
- horizontalStackedBar: '横向堆叠条形图',
33
- horizontalStackedBar100: '横向百分比堆叠条形图',
34
- histogram: '直方图',
35
- boxPlot: '箱线图',
36
- scatter: '散点图',
37
- heatmap: '热力图',
38
- pie: '环形图',
39
- leaderboard: '排行榜',
40
- funnel: '漏斗图',
41
- waterfall: '瀑布图',
42
- };
43
23
  const trendTypes = ['line', 'area', 'stackedArea', 'sparkline'];
44
24
  const ratioTypes = ['stackedBar100', 'horizontalStackedBar100'];
45
25
  const singleSeriesTypes = [
@@ -233,9 +213,6 @@ export function parseChartViewShape(value, path = '') {
233
213
  export function chartColumns(view) {
234
214
  return [...new Set([view.x, ...view.y, ...Object.values(view.bindings ?? {})])];
235
215
  }
236
- export function chartSeriesFields(view) {
237
- return [...view.y];
238
- }
239
216
  /** Verifies supplied values only: never bins, ranks, accumulates, normalizes or sorts. */
240
217
  export function validateChartView(view, dataset, path = '', dataPath = '/data', rowIndices) {
241
218
  const rows = rowIndices ? rowIndices.map((index) => dataset.rows[index]) : dataset.rows;
@@ -263,7 +263,7 @@ export function formatCell(value, _column) {
263
263
  return value === null ? '—' : String(value);
264
264
  }
265
265
  function common(value, generated) {
266
- version(value, '', generated ? 2 : 1);
266
+ version(value, '', generated ? 3 : 2);
267
267
  string(value.title, '/title', 512);
268
268
  const sources = array(value.sources, '/sources', budgets.sources);
269
269
  const sourceIds = sources.map((value, i) => {
@@ -566,18 +566,27 @@ function interaction(value, datasets, generated) {
566
566
  }
567
567
  });
568
568
  }
569
+ function presentationLanguage(value, expected) {
570
+ if (value.schemaVersion !== expected)
571
+ fail('/schemaVersion', `Only schemaVersion ${expected} is accepted. Regenerate this report with an explicit locale.`, 'report-version-unsupported');
572
+ if (value.locale !== 'zh-CN' && value.locale !== 'en-US')
573
+ fail('/locale', 'Report locale must be zh-CN or en-US. Ask DSH to regenerate the report with an explicit language.', 'report-locale-invalid');
574
+ }
569
575
  export function parsePresentationDraft(value) {
570
576
  bytes(value, budgets.draftBytes);
571
577
  const entry = object(value, '');
572
- keys(entry, ['schemaVersion', 'title', 'datasets', 'sources', 'blocks'], ['interaction'], '');
578
+ presentationLanguage(entry, 2);
579
+ keys(entry, ['schemaVersion', 'locale', 'title', 'datasets', 'sources', 'blocks'], ['interaction'], '');
573
580
  common(entry, false);
574
581
  return entry;
575
582
  }
576
583
  export function parsePresentationDocument(value) {
577
584
  bytes(value, budgets.documentBytes);
578
585
  const entry = object(value, '');
586
+ presentationLanguage(entry, 3);
579
587
  keys(entry, [
580
588
  'schemaVersion',
589
+ 'locale',
581
590
  'workspaceId',
582
591
  'reportId',
583
592
  'buildId',
@@ -618,11 +627,13 @@ export function parsePresentationReceipt(value) {
618
627
  string(entry.title, '/title', 512);
619
628
  string(entry.summary, '/summary', 2048);
620
629
  const files = object(entry.files, '/files');
621
- keys(files, ['document', 'html'], [], '/files');
630
+ keys(files, ['document'], ['html'], '/files');
622
631
  for (const [key, asset, max] of [
623
632
  ['document', 'presentation.json', budgets.documentBytes],
624
633
  ['html', 'index.html', budgets.htmlBytes],
625
634
  ]) {
635
+ if (key === 'html' && !Object.hasOwn(files, key))
636
+ continue;
626
637
  const path = `/files/${key}`;
627
638
  const file = object(files[key], path);
628
639
  keys(file, ['asset', 'path', 'sha256', 'bytes'], [], path);
@@ -643,8 +654,9 @@ export function parsePresentationReceipt(value) {
643
654
  integer(file.bytes, `${path}/bytes`, 1, max);
644
655
  }
645
656
  const documentPath = String(object(files.document, '/files/document').path);
646
- const htmlPath = String(object(files.html, '/files/html').path);
647
- if (documentPath.slice(0, -'presentation.json'.length) !== htmlPath.slice(0, -'index.html'.length)) {
657
+ const htmlPath = files.html ? String(object(files.html, '/files/html').path) : undefined;
658
+ if (htmlPath !== undefined &&
659
+ documentPath.slice(0, -'presentation.json'.length) !== htmlPath.slice(0, -'index.html'.length)) {
648
660
  fail('/files/html/path', 'Both files must belong to the same build directory.', 'file_boundary');
649
661
  }
650
662
  return entry;
@@ -1,4 +1,4 @@
1
- export const PRESENTATION_SCHEMA_VERSION = 2;
1
+ export const PRESENTATION_SCHEMA_VERSION = 3;
2
2
  export const PRESENTATION_BUDGETS = {
3
3
  documentBytes: 4 * 1024 * 1024,
4
4
  draftBytes: 256 * 1024,
@@ -1,8 +1,13 @@
1
+ import { finishCleanup, PendingTasks } from "../lifecycle.js";
1
2
  import { MARIVO_PRESENT_TOOL_NAME, MARIVO_PRESENTATION_DELIVERY_KIND, parsePresentationDelivery, } from "./receipt.js";
2
3
  /** Persist the same envelope on nested Code dispatch even when code discards the return value. */
3
4
  export function installMarivoPresentationCodeDelivery(ctx) {
5
+ const tasks = new PendingTasks();
6
+ let active = true;
4
7
  const pending = new Map();
5
8
  const stopResult = ctx.on('tools/result', (exec, result) => {
9
+ if (!active)
10
+ return;
6
11
  if (exec.name !== MARIVO_PRESENT_TOOL_NAME || exec.parent === undefined || result.isError)
7
12
  return;
8
13
  try {
@@ -19,38 +24,48 @@ export function installMarivoPresentationCodeDelivery(ctx) {
19
24
  /* Invalid output cannot create a success card. */
20
25
  }
21
26
  });
22
- const stopLog = ctx.on('tools/code-dispatch-log', async (dispatch, next) => {
23
- const content = await next();
24
- if (dispatch.name !== MARIVO_PRESENT_TOOL_NAME)
25
- return content;
26
- const key = String(dispatch.subCallId);
27
- const item = pending.get(key);
28
- pending.delete(key);
29
- if (dispatch.isError ||
30
- !item ||
31
- item.delivery.dshSessionId !== String(dispatch.agent?.session.id) ||
32
- item.rootCallId !== String(dispatch.exec.rootCallId))
33
- return content;
34
- const root = [...(dispatch.agent?.session.events ?? [])]
35
- .reverse()
36
- .find((event) => event.type === 'tool/call' && String(event.data.callId) === item.rootCallId);
37
- if (root?.type !== 'tool/call' || root.data.turn !== item.delivery.turn)
38
- return content;
39
- return [
40
- ...content,
41
- {
42
- type: MARIVO_PRESENTATION_DELIVERY_KIND,
43
- delivery: item.delivery,
44
- },
45
- ];
46
- }, { prepend: true });
47
- let active = true;
27
+ let stopLog;
28
+ try {
29
+ stopLog = ctx.on('tools/ptc-dispatch-log', (dispatch, next) => tasks.track((async () => {
30
+ const content = await next();
31
+ if (!active)
32
+ return content;
33
+ if (dispatch.name !== MARIVO_PRESENT_TOOL_NAME)
34
+ return content;
35
+ const key = String(dispatch.subCallId);
36
+ const item = pending.get(key);
37
+ pending.delete(key);
38
+ if (dispatch.isError ||
39
+ !item ||
40
+ item.delivery.dshSessionId !== String(dispatch.agent?.session.id) ||
41
+ item.rootCallId !== String(dispatch.exec.rootCallId))
42
+ return content;
43
+ const root = [...(dispatch.agent?.session.snapshotEvents() ?? [])]
44
+ .reverse()
45
+ .find((event) => event.type === 'tool/call' && String(event.data.callId) === item.rootCallId);
46
+ if (root?.type !== 'tool/call' || root.data.turn !== item.delivery.turn)
47
+ return content;
48
+ return [
49
+ ...content,
50
+ {
51
+ type: MARIVO_PRESENTATION_DELIVERY_KIND,
52
+ delivery: item.delivery,
53
+ },
54
+ ];
55
+ })()), { prepend: true });
56
+ }
57
+ catch (error) {
58
+ active = false;
59
+ void finishCleanup([stopResult, () => tasks.drain()]);
60
+ throw error;
61
+ }
62
+ let closing;
48
63
  return () => {
49
- if (!active)
50
- return;
64
+ if (closing)
65
+ return closing;
51
66
  active = false;
52
- stopLog();
53
- stopResult();
67
+ closing = finishCleanup([stopLog, stopResult, () => tasks.drain()]);
54
68
  pending.clear();
69
+ return closing;
55
70
  };
56
71
  }
@@ -0,0 +1,214 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { link, lstat, mkdir, open, realpath, unlink } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { defineTool } from '@deepseek-ai/dsh-tools';
6
+ import { registerMarivoTool } from "../tool-lifecycle.js";
7
+ import { readReceiptAsset } from "./asset.js";
8
+ import { parsePresentationBuildId } from "./contracts/index.js";
9
+ import { readReportHistory } from "./reports.js";
10
+ export const MARIVO_EXPORT_HTML_TOOL_NAME = 'marivo_export_html';
11
+ export function parseHtmlOutputPath(value) {
12
+ if (typeof value !== 'string' ||
13
+ !value ||
14
+ value.length > 4096 ||
15
+ value.includes('\\') ||
16
+ value.includes('\0') ||
17
+ path.isAbsolute(value) ||
18
+ !value.endsWith('.html'))
19
+ throw new Error('invalid-html-output-path');
20
+ const segments = value.split('/');
21
+ if (segments.some((part) => !part || part === '.' || part === '..') ||
22
+ segments[0].toLowerCase() === '.dsh-data-analysis')
23
+ throw new Error('invalid-html-output-path');
24
+ return value;
25
+ }
26
+ /** Workspace-local hard-link publication is atomic and never replaces an existing target. */
27
+ async function writeHtml(root, relative, bytes, assertOwner) {
28
+ const parents = [root];
29
+ const identities = [];
30
+ const inspect = async (filename) => {
31
+ const stat = await lstat(filename);
32
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (await realpath(filename)) !== filename)
33
+ throw new Error('html-output-directory-mismatch');
34
+ return stat;
35
+ };
36
+ const check = async () => {
37
+ await assertOwner();
38
+ for (const [index, directory] of parents.entries()) {
39
+ const stat = await inspect(directory);
40
+ if (stat.dev !== identities[index].dev || stat.ino !== identities[index].ino)
41
+ throw new Error('html-output-directory-changed');
42
+ }
43
+ };
44
+ identities.push(await inspect(root));
45
+ for (const part of relative.split('/').slice(0, -1)) {
46
+ await check();
47
+ const directory = path.join(parents.at(-1), part);
48
+ try {
49
+ await mkdir(directory, { mode: 0o700 });
50
+ }
51
+ catch (error) {
52
+ if (error.code !== 'EEXIST')
53
+ throw error;
54
+ }
55
+ identities.push(await inspect(directory));
56
+ parents.push(directory);
57
+ }
58
+ await check();
59
+ const target = path.join(root, relative);
60
+ // Stage at the highest ancestor on the destination filesystem, outside movable
61
+ // output subdirectories where possible, without introducing cross-device links.
62
+ const stagingParent = parents[identities.findIndex((identity) => identity.dev === identities.at(-1).dev)];
63
+ const temporary = path.join(stagingParent, `.html-export-${randomUUID()}.tmp`);
64
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
65
+ const owned = await handle.stat();
66
+ let linked = false;
67
+ const removeOwned = async (filename) => {
68
+ try {
69
+ const stat = await lstat(filename);
70
+ if (stat.dev === owned.dev && stat.ino === owned.ino)
71
+ await unlink(filename);
72
+ }
73
+ catch (error) {
74
+ if (error.code !== 'ENOENT')
75
+ throw error;
76
+ }
77
+ };
78
+ try {
79
+ await check();
80
+ await handle.writeFile(bytes);
81
+ await handle.sync();
82
+ await check();
83
+ await link(temporary, target);
84
+ linked = true;
85
+ await check();
86
+ const stat = await lstat(target);
87
+ if (stat.dev !== owned.dev || stat.ino !== owned.ino || stat.size !== bytes.length)
88
+ throw new Error('html-output-changed');
89
+ await removeOwned(temporary);
90
+ await check();
91
+ return target;
92
+ }
93
+ catch (error) {
94
+ // Paths may have moved or been replaced. The open handle still owns the exact inode,
95
+ // including any published hard link: erase failed output without following new paths.
96
+ const failures = [error];
97
+ try {
98
+ await handle.truncate(0);
99
+ await handle.sync();
100
+ }
101
+ catch (cleanupError) {
102
+ failures.push(cleanupError);
103
+ }
104
+ if (linked) {
105
+ try {
106
+ await removeOwned(target);
107
+ }
108
+ catch (cleanupError) {
109
+ failures.push(cleanupError);
110
+ }
111
+ }
112
+ if (failures.length > 1)
113
+ throw new AggregateError(failures, 'html-export-cleanup-failed');
114
+ throw error;
115
+ }
116
+ finally {
117
+ try {
118
+ await removeOwned(temporary);
119
+ }
120
+ finally {
121
+ await handle.close();
122
+ }
123
+ }
124
+ }
125
+ export function createMarivoExportHtmlTool(source, session) {
126
+ return defineTool({
127
+ name: MARIVO_EXPORT_HTML_TOOL_NAME,
128
+ description: 'Export a saved complete report as self-contained offline HTML only when requested. Uses the current Session Workspace, without analysis, Python, publication, new Builds or unsaved reader state. Specify build_id for an exact version; otherwise freezes current at call start. output_path must be a new Workspace-relative .html file; existing files are never overwritten. Returns file metadata, not HTML content. After success, call native present({files:[{path: result.path, description: "Offline HTML report"}]}) to deliver the file before your final response.',
129
+ parameters: {
130
+ report_id: {
131
+ type: 'string',
132
+ required: true,
133
+ description: 'Saved Report ID from its receipt.',
134
+ },
135
+ build_id: { type: 'string', description: 'Exact saved Build ID; omit to export current.' },
136
+ output_path: {
137
+ type: 'string',
138
+ required: true,
139
+ description: 'New Workspace-relative .html path, e.g. reports/analysis.html. Parent directories may be created.',
140
+ },
141
+ },
142
+ output: {
143
+ schema: {
144
+ type: 'object',
145
+ additionalProperties: false,
146
+ properties: {
147
+ workspace_id: { type: 'string', required: true },
148
+ report_id: { type: 'string', required: true },
149
+ build_id: { type: 'string', required: true },
150
+ path: { type: 'string', required: true },
151
+ mime_type: { type: 'string', required: true },
152
+ bytes: { type: 'number', required: true },
153
+ sha256: { type: 'string', required: true },
154
+ },
155
+ },
156
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
157
+ presentationMeta: (_args, value) => value,
158
+ },
159
+ presentCall(args) {
160
+ return {
161
+ card: 'generic',
162
+ kind: 'edit',
163
+ title: 'Export report HTML',
164
+ locations: [{ path: args.output_path }],
165
+ };
166
+ },
167
+ timeoutMs: 120_000,
168
+ async execute(args, exec) {
169
+ const reportId = parsePresentationBuildId(args.report_id, '/report_id');
170
+ const buildId = args.build_id === undefined
171
+ ? undefined
172
+ : parsePresentationBuildId(args.build_id, '/build_id');
173
+ const relative = parseHtmlOutputPath(args.output_path);
174
+ const owner = exec.agent;
175
+ if (!owner || owner.session !== session)
176
+ throw new Error('presentation-session-mismatch');
177
+ exec.signal.throwIfAborted();
178
+ const workspace = await source();
179
+ const root = path.resolve(workspace.path);
180
+ const check = async () => {
181
+ exec.signal.throwIfAborted();
182
+ if (owner.session !== session)
183
+ throw new Error('presentation-session-mismatch');
184
+ const current = await source();
185
+ if (current.id !== workspace.id || path.resolve(current.path) !== root)
186
+ throw new Error('presentation-workspace-changed');
187
+ exec.signal.throwIfAborted();
188
+ };
189
+ await check();
190
+ const history = await readReportHistory(root, workspace.id, reportId, exec.signal);
191
+ const receipt = buildId === undefined
192
+ ? history.versions[0].receipt
193
+ : history.versions.find((entry) => entry.receipt.buildId === buildId)?.receipt;
194
+ if (!receipt)
195
+ throw new Error('presentation-build-unavailable');
196
+ await check();
197
+ const asset = await readReceiptAsset(root, workspace.id, receipt, 'index.html', exec.signal);
198
+ await check();
199
+ const filename = await writeHtml(root, relative, asset.bytes, check);
200
+ return {
201
+ workspace_id: workspace.id,
202
+ report_id: reportId,
203
+ build_id: receipt.buildId,
204
+ path: filename,
205
+ mime_type: 'text/html',
206
+ bytes: asset.bytes.length,
207
+ sha256: asset.sha256,
208
+ };
209
+ },
210
+ });
211
+ }
212
+ export function registerMarivoExportHtmlTool(ctx, source, session) {
213
+ return registerMarivoTool(ctx, createMarivoExportHtmlTool(source, session));
214
+ }
@@ -2,16 +2,14 @@ import { createHash } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
3
  import { lstat, open, realpath } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { presentationAssetRelativePath } from "./contracts/asset-path.js";
5
6
  import { PRESENTATION_BUDGETS, parsePresentationBuildId, } from "./contracts/index.js";
6
7
  export function presentationReportPath(root, reportId) {
7
8
  parsePresentationBuildId(reportId, '/reportId');
8
9
  return path.join(root, '.dsh-data-analysis', 'presentations', reportId);
9
10
  }
10
11
  export function presentationAssetPath(root, reportId, buildId, asset) {
11
- parsePresentationBuildId(buildId);
12
- if (asset !== 'presentation.json' && asset !== 'index.html')
13
- throw new Error('invalid-asset');
14
- return path.join(presentationReportPath(root, reportId), 'builds', buildId, asset);
12
+ return path.join(root, presentationAssetRelativePath(reportId, buildId, asset));
15
13
  }
16
14
  export function presentationSha256(bytes) {
17
15
  return createHash('sha256').update(bytes).digest('hex');
@@ -46,7 +44,8 @@ export async function readReportFile(workspaceRoot, filename, maximum, signal) {
46
44
  throw new Error('asset-not-file');
47
45
  if (before.size > maximum)
48
46
  throw new Error('asset-too-large');
49
- const bytes = Buffer.alloc(maximum + 1);
47
+ // Allocate for the observed file, retaining one extra byte to detect growth.
48
+ const bytes = Buffer.alloc(before.size + 1);
50
49
  let length = 0;
51
50
  while (length < bytes.length) {
52
51
  signal?.throwIfAborted();
@@ -179,7 +179,8 @@ export class MarivoPresentationProjection {
179
179
  }
180
180
  this.#assertReady(options.signal);
181
181
  return parsePresentationDocument({
182
- schemaVersion: 2,
182
+ schemaVersion: 3,
183
+ locale: draft.locale,
183
184
  reportId: options.reportId,
184
185
  workspaceId: options.workspaceId,
185
186
  buildId: options.buildId,
@@ -33,7 +33,6 @@ ${MARIVO_PRESENTATION_SQL_CODE_PROGRAM}
33
33
  def read(request):
34
34
  sessions, artifacts, contracts = {}, {}, {}
35
35
  sources, datasets, diagnostics = [], [], []
36
- definition_sources = []
37
36
  read_at = datetime.now(timezone.utc).isoformat()
38
37
  try:
39
38
  for index, declared in enumerate(request["sources"]):
@@ -58,7 +57,7 @@ def read(request):
58
57
  except Exception:
59
58
  # Only the requested source becomes unavailable. Exception messages may
60
59
  # include credentials, project paths or raw SQL and are never returned.
61
- sources.append({**declared, "status": "unavailable", "reason": "The declared Artifact cannot be restored through the bound Workspace public API."})
60
+ sources.append({**declared, "status": "unavailable", "reason": "marivo.presentation.source-unavailable"})
62
61
  continue
63
62
 
64
63
  artifacts[declared["id"]], contracts[declared["id"]] = artifact, contract
@@ -85,17 +84,11 @@ def read(request):
85
84
  except ProjectionFailure:
86
85
  raise
87
86
  except Exception:
88
- fact("选择的 Finding unavailable", "The requested Finding cannot be read through the persisted Artifact public API.")
87
+ fact("选择的 Finding unavailable", "marivo.presentation.finding-unavailable")
89
88
  diagnostics.append({"code": "finding_unavailable", "path": source_path + "/ref/findingId", "message": "Artifact is available; its requested Finding is unavailable."})
90
- if meta.kind == "metric_frame" or any(item.semantic_kind.value == "metric" for item in contract.semantic_inputs):
91
- definition_sources.append(declared["id"])
92
- fact("指标定义说明", "信息:Artifact 公开契约未提供生成时的指标定义快照,来源概要无法展示当时的指标含义与口径。不影响已保存数据、图表及已有代码的展示,正常阅读无需处理;核验历史口径需补充生成时的定义快照,当前语义层定义不能替代历史定义。")
93
89
  fact("revalidation", "not_requested")
94
90
  sources.append({**declared, "status": "available", "label": text(str(meta.kind) + " " + artifact.ref, 512), "facts": facts, "code": sql_code_snapshot(session, artifact)})
95
91
 
96
- if definition_sources:
97
- diagnostics.append({"code": "definition_unavailable", "path": "/sources", "message": "信息:" + str(len(definition_sources)) + " 个指标来源的 Artifact 公开契约未提供生成时的指标定义快照,来源概要无法展示当时的指标含义与口径。不影响已保存数据、图表及已有代码的展示,正常阅读无需处理;核验历史口径需补充生成时的定义快照,当前语义层定义不能替代历史定义。"})
98
-
99
92
  for selection in request["datasets"]:
100
93
  dataset_path = "/datasets/" + str(selection["index"])
101
94
  artifact = artifacts.get(selection["sourceId"])