@chengxianglibra/dsh-data-analysis 0.1.2 → 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 (212) hide show
  1. package/README.md +136 -99
  2. package/cordis.patch.yml +1 -1
  3. package/lib/bin/environment.js +8 -6
  4. package/lib/client/credentials/changes.js +74 -0
  5. package/lib/client/credentials/create-datasource.js +126 -22
  6. package/lib/client/credentials/credential-draft.js +42 -0
  7. package/lib/client/credentials/defaults.js +11 -0
  8. package/lib/client/credentials/descriptions.js +46 -0
  9. package/lib/client/credentials/install.js +209 -81
  10. package/lib/client/credentials/model.js +323 -51
  11. package/lib/client/credentials/report-publishing.js +96 -0
  12. package/lib/client/credentials/styles.js +13 -9
  13. package/lib/client/i18n/context.js +22 -0
  14. package/lib/client/i18n/copy.js +57 -0
  15. package/lib/client/i18n/credentials.js +475 -0
  16. package/lib/client/i18n/host.js +27 -0
  17. package/lib/client/i18n/navigation.js +41 -0
  18. package/lib/client/i18n/presentation.js +577 -0
  19. package/lib/client/i18n/python.js +36 -0
  20. package/lib/client/i18n/semantic.js +441 -0
  21. package/lib/client/input-context.js +21 -0
  22. package/lib/client/python-tool/card.js +35 -0
  23. package/lib/client/python-tool/execution-error.js +35 -0
  24. package/lib/client/python-tool/install.js +9 -0
  25. package/lib/client/python-tool/model.js +101 -0
  26. package/lib/client/right-tabs/install.js +535 -0
  27. package/lib/client/right-tabs/live-delivery.js +58 -0
  28. package/lib/client/right-tabs/navigation.js +47 -0
  29. package/lib/client/right-tabs/page.js +178 -0
  30. package/lib/client/right-tabs/styles.js +11 -0
  31. package/lib/client/rpc.js +5 -0
  32. package/lib/client/semantic-reference.js +10 -0
  33. package/lib/client/workspace-header-action.js +14 -1
  34. package/lib/client/workspace-shortcuts.js +26 -0
  35. package/lib/client.js +365 -242
  36. package/lib/compatibility.js +1 -1
  37. package/lib/datasource/authoring.js +43 -4
  38. package/lib/datasource/bridge-programs.js +23 -20
  39. package/lib/datasource/bridge.js +46 -3
  40. package/lib/datasource/changes-contract.js +41 -0
  41. package/lib/datasource/changes.js +49 -0
  42. package/lib/datasource/configure.js +39 -0
  43. package/lib/datasource/defaults.js +25 -0
  44. package/lib/datasource/index.js +2 -1
  45. package/lib/datasource/python-options.js +21 -0
  46. package/lib/datasource/python.js +116 -25
  47. package/lib/datasource/rpc.js +75 -13
  48. package/lib/datasource/service.js +286 -11
  49. package/lib/datasource/test.js +2 -1
  50. package/lib/disclosure/activation.js +19 -7
  51. package/lib/disclosure/execution-guidance.js +52 -0
  52. package/lib/disclosure/help.js +4 -1
  53. package/lib/environment/runtime.js +35 -34
  54. package/lib/environment/workspace.js +18 -1
  55. package/lib/lifecycle.js +33 -0
  56. package/lib/plugin-agents.js +154 -0
  57. package/lib/plugin.js +94 -172
  58. package/lib/presentation/asset.js +34 -0
  59. package/lib/presentation/assets/portable.js +14 -14
  60. package/lib/presentation/assets/static.js +344 -73
  61. package/lib/presentation/build/index.js +3 -2
  62. package/lib/presentation/commit.js +19 -12
  63. package/lib/presentation/contracts/asset-path.js +9 -0
  64. package/lib/presentation/contracts/catalog.js +62 -0
  65. package/lib/presentation/contracts/charts.js +0 -23
  66. package/lib/presentation/contracts/index.js +17 -5
  67. package/lib/presentation/contracts/types.js +1 -1
  68. package/lib/presentation/delivery.js +45 -30
  69. package/lib/presentation/export-html.js +214 -0
  70. package/lib/presentation/files.js +4 -5
  71. package/lib/presentation/projection/index.js +2 -1
  72. package/lib/presentation/projection/program.js +2 -9
  73. package/lib/presentation/projection/sql-code.js +13 -13
  74. package/lib/presentation/reports.js +114 -17
  75. package/lib/presentation/rpc.js +96 -58
  76. package/lib/presentation/tool.js +6 -10
  77. package/lib/report-publishing/adapters.js +111 -0
  78. package/lib/report-publishing/config.js +69 -0
  79. package/lib/report-publishing/service.js +180 -0
  80. package/lib/rpc.js +49 -0
  81. package/lib/semantic-browser/contracts.js +17 -0
  82. package/lib/semantic-browser/program.js +10 -0
  83. package/lib/semantic-browser/service.js +40 -6
  84. package/lib/semantic-reference/bridge.js +10 -1
  85. package/lib/semantic-reference/contracts.js +20 -6
  86. package/lib/semantic-reference/environment.js +56 -0
  87. package/lib/semantic-reference/labels.js +16 -0
  88. package/lib/semantic-reference/rpc.js +39 -18
  89. package/lib/semantic-reference/search.js +5 -4
  90. package/lib/semantic-reference/usage.js +12 -3
  91. package/lib/tool-lifecycle.js +22 -0
  92. package/lib/types/client/credentials/changes.d.ts +9 -0
  93. package/lib/types/client/credentials/create-datasource.d.ts +3 -1
  94. package/lib/types/client/credentials/credential-draft.d.ts +14 -0
  95. package/lib/types/client/credentials/defaults.d.ts +2 -0
  96. package/lib/types/client/credentials/descriptions.d.ts +1 -0
  97. package/lib/types/client/credentials/install.d.ts +19 -1
  98. package/lib/types/client/credentials/model.d.ts +31 -11
  99. package/lib/types/client/credentials/report-publishing.d.ts +5 -0
  100. package/lib/types/client/credentials/styles.d.ts +1 -1
  101. package/lib/types/client/i18n/context.d.ts +17 -0
  102. package/lib/types/client/i18n/copy.d.ts +806 -0
  103. package/lib/types/client/i18n/credentials.d.ts +475 -0
  104. package/lib/types/client/i18n/host.d.ts +16 -0
  105. package/lib/types/client/i18n/navigation.d.ts +41 -0
  106. package/lib/types/client/i18n/presentation.d.ts +577 -0
  107. package/lib/types/client/i18n/python.d.ts +19 -0
  108. package/lib/types/client/i18n/semantic.d.ts +441 -0
  109. package/lib/types/client/input-context.d.ts +19 -0
  110. package/lib/types/client/presentation/ask-dsh.d.ts +4 -8
  111. package/lib/types/client/presentation/catalog-model.d.ts +24 -0
  112. package/lib/types/client/presentation/catalog.d.ts +20 -0
  113. package/lib/types/client/presentation/chart-tooltip.d.ts +2 -2
  114. package/lib/types/client/presentation/context-reference.d.ts +12 -0
  115. package/lib/types/client/presentation/copy-context.d.ts +2 -2
  116. package/lib/types/client/presentation/delivery-model.d.ts +27 -6
  117. package/lib/types/client/presentation/delivery.d.ts +8 -7
  118. package/lib/types/client/presentation/download.d.ts +2 -0
  119. package/lib/types/client/presentation/export-menu.d.ts +26 -0
  120. package/lib/types/client/presentation/export-view.d.ts +63 -0
  121. package/lib/types/client/presentation/host-entry.d.ts +8 -2
  122. package/lib/types/client/presentation/icons.d.ts +1 -1
  123. package/lib/types/client/presentation/model.d.ts +14 -15
  124. package/lib/types/client/presentation/reader.d.ts +15 -2
  125. package/lib/types/client/presentation/reference-source.d.ts +9 -0
  126. package/lib/types/client/presentation/source-code-model.d.ts +2 -1
  127. package/lib/types/client/presentation/source-code.d.ts +0 -5
  128. package/lib/types/client/presentation/source-dialog.d.ts +1 -2
  129. package/lib/types/client/presentation/sources.d.ts +5 -5
  130. package/lib/types/client/presentation/styles.d.ts +1 -1
  131. package/lib/types/client/presentation/table.d.ts +5 -1
  132. package/lib/types/client/python-tool/card.d.ts +2 -0
  133. package/lib/types/client/python-tool/execution-error.d.ts +3 -0
  134. package/lib/types/client/python-tool/install.d.ts +2 -0
  135. package/lib/types/client/python-tool/model.d.ts +20 -0
  136. package/lib/types/client/right-tabs/install.d.ts +18 -0
  137. package/lib/types/client/right-tabs/live-delivery.d.ts +8 -0
  138. package/lib/types/client/right-tabs/navigation.d.ts +17 -0
  139. package/lib/types/client/right-tabs/page.d.ts +36 -0
  140. package/lib/types/client/right-tabs/styles.d.ts +2 -0
  141. package/lib/types/client/rpc.d.ts +3 -0
  142. package/lib/types/client/semantic-reference.d.ts +4 -0
  143. package/lib/types/client/workspace-header-action.d.ts +1 -1
  144. package/lib/types/client/workspace-shortcuts.d.ts +15 -0
  145. package/lib/types/client.d.ts +6 -3
  146. package/lib/types/compatibility.d.ts +1 -1
  147. package/lib/types/datasource/authoring.d.ts +10 -0
  148. package/lib/types/datasource/bridge.d.ts +23 -2
  149. package/lib/types/datasource/changes-contract.d.ts +18 -0
  150. package/lib/types/datasource/changes.d.ts +15 -0
  151. package/lib/types/datasource/configure.d.ts +7 -0
  152. package/lib/types/datasource/defaults.d.ts +7 -0
  153. package/lib/types/datasource/index.d.ts +6 -1
  154. package/lib/types/datasource/python-options.d.ts +9 -0
  155. package/lib/types/datasource/python.d.ts +17 -2
  156. package/lib/types/datasource/rpc.d.ts +2 -1
  157. package/lib/types/datasource/service.d.ts +48 -5
  158. package/lib/types/datasource/test.d.ts +1 -1
  159. package/lib/types/disclosure/activation.d.ts +2 -1
  160. package/lib/types/disclosure/execution-guidance.d.ts +6 -0
  161. package/lib/types/disclosure/help.d.ts +1 -1
  162. package/lib/types/environment/types.d.ts +2 -1
  163. package/lib/types/environment/workspace.d.ts +1 -0
  164. package/lib/types/lifecycle.d.ts +8 -0
  165. package/lib/types/plugin-agents.d.ts +27 -0
  166. package/lib/types/plugin.d.ts +12 -28
  167. package/lib/types/presentation/asset.d.ts +6 -0
  168. package/lib/types/presentation/build/index.d.ts +7 -2
  169. package/lib/types/presentation/commit.d.ts +1 -1
  170. package/lib/types/presentation/contracts/asset-path.d.ts +2 -0
  171. package/lib/types/presentation/contracts/catalog.d.ts +27 -0
  172. package/lib/types/presentation/contracts/charts.d.ts +0 -2
  173. package/lib/types/presentation/contracts/types.d.ts +7 -4
  174. package/lib/types/presentation/delivery.d.ts +1 -1
  175. package/lib/types/presentation/export-html.d.ts +13 -0
  176. package/lib/types/presentation/reports.d.ts +6 -1
  177. package/lib/types/presentation/rpc.d.ts +3 -2
  178. package/lib/types/presentation/tool.d.ts +2 -2
  179. package/lib/types/report-publishing/adapters.d.ts +6 -0
  180. package/lib/types/report-publishing/config.d.ts +22 -0
  181. package/lib/types/report-publishing/service.d.ts +68 -0
  182. package/lib/types/rpc.d.ts +3 -0
  183. package/lib/types/semantic-browser/contracts.d.ts +7 -0
  184. package/lib/types/semantic-browser/service.d.ts +7 -1
  185. package/lib/types/semantic-reference/bridge.d.ts +1 -0
  186. package/lib/types/semantic-reference/contracts.d.ts +5 -2
  187. package/lib/types/semantic-reference/environment.d.ts +22 -0
  188. package/lib/types/semantic-reference/labels.d.ts +16 -0
  189. package/lib/types/semantic-reference/rpc.d.ts +1 -1
  190. package/lib/types/semantic-reference/search.d.ts +1 -1
  191. package/lib/types/tool-lifecycle.d.ts +4 -0
  192. package/lib/types/workspace-identity.d.ts +6 -0
  193. package/lib/workspace-identity.js +9 -0
  194. package/package.json +83 -61
  195. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  196. package/skills/dsh-data-analysis-files/SKILL.md +32 -0
  197. package/skills/dsh-data-analysis-files/references/examples.md +93 -0
  198. package/skills/dsh-data-analysis-presentation/SKILL.md +10 -6
  199. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +13 -4
  200. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +3 -2
  201. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +3 -2
  202. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +12 -5
  203. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +3 -2
  204. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +3 -2
  205. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +12 -4
  206. package/skills/dsh-data-analysis-presentation/references/examples.md +3 -1
  207. package/skills/dsh-data-analysis-presentation/references/export-html.md +7 -0
  208. package/skills/dsh-data-analysis-presentation/references/interaction.md +1 -1
  209. package/skills/dsh-data-analysis-presentation/references/narrative.md +1 -1
  210. package/skills/dsh-data-analysis-presentation/references/publishing.md +3 -0
  211. package/skills/dsh-data-analysis-presentation/references/schema.md +25 -1
  212. package/lib/types/client/presentation/install.d.ts +0 -15
@@ -0,0 +1,69 @@
1
+ import { isCredentialRefName } from '@deepseek-ai/dsh-credentials';
2
+ import { z } from 'zod';
3
+ const reference = z.string().refine(isCredentialRefName);
4
+ const url = z
5
+ .string()
6
+ .url()
7
+ .refine((value) => {
8
+ let parsed;
9
+ try {
10
+ parsed = new URL(value);
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ return (['http:', 'https:'].includes(parsed.protocol) &&
16
+ !parsed.username &&
17
+ !parsed.password &&
18
+ !parsed.search &&
19
+ !parsed.hash);
20
+ });
21
+ export const publishingConfigSchema = z
22
+ .object({
23
+ enabled: z.boolean().default(false),
24
+ storage: z
25
+ .object({
26
+ name: z.string().min(1).max(100),
27
+ protocol: z.literal('s3').default('s3'),
28
+ endpoint: url,
29
+ region: z.string().min(1),
30
+ bucket: z
31
+ .string()
32
+ .min(1)
33
+ .max(255)
34
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/),
35
+ forcePathStyle: z.boolean().default(false),
36
+ accessKeyIdRef: reference,
37
+ secretAccessKeyRef: reference,
38
+ sessionTokenRef: reference.optional(),
39
+ })
40
+ .strict(),
41
+ pathPrefix: z
42
+ .string()
43
+ .default('reports')
44
+ .refine((value) => value === '' ||
45
+ value.split('/').every((part) => /^[a-zA-Z0-9_-][a-zA-Z0-9._-]*$/.test(part))),
46
+ publicBaseUrl: url,
47
+ })
48
+ .strict()
49
+ .superRefine((config, ctx) => {
50
+ const refs = [
51
+ config.storage.accessKeyIdRef,
52
+ config.storage.secretAccessKeyRef,
53
+ config.storage.sessionTokenRef,
54
+ ].filter(Boolean);
55
+ if (new Set(refs).size !== refs.length)
56
+ ctx.addIssue({ code: 'custom', message: 'distinct-credential-references-required' });
57
+ });
58
+ export function resolvePublishingConfig(value) {
59
+ if (value === undefined)
60
+ return undefined;
61
+ if (!value || typeof value !== 'object' || Array.isArray(value))
62
+ throw new Error('report-publishing-config-invalid');
63
+ if (value.enabled === false || value.enabled === undefined)
64
+ return undefined;
65
+ const result = publishingConfigSchema.safeParse(value);
66
+ if (!result.success)
67
+ throw new Error('report-publishing-config-invalid');
68
+ return result.data;
69
+ }
@@ -0,0 +1,180 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
3
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
4
+ import { PRESENTATION_BUDGETS } from "../presentation/contracts/index.js";
5
+ export function publishingPathName(value, fallback) {
6
+ const name = Array.from(value.normalize('NFKC').replace(/[^\p{L}\p{N}_-]+/gu, '-'))
7
+ .slice(0, 48)
8
+ .join('')
9
+ .replace(/^-+|-+$/g, '');
10
+ return name || fallback;
11
+ }
12
+ export async function uploadReport(input) {
13
+ const { config, credentials, key, bytes, signal } = input;
14
+ const client = new S3Client({
15
+ endpoint: config.storage.endpoint,
16
+ region: config.storage.region,
17
+ forcePathStyle: config.storage.forcePathStyle,
18
+ credentials,
19
+ maxAttempts: 3,
20
+ requestChecksumCalculation: 'WHEN_REQUIRED',
21
+ });
22
+ try {
23
+ await client.send(new PutObjectCommand({
24
+ Bucket: config.storage.bucket,
25
+ Key: key,
26
+ Body: bytes,
27
+ ContentType: 'text/html; charset=utf-8',
28
+ ContentDisposition: 'inline',
29
+ CacheControl: 'public, max-age=31536000, immutable',
30
+ }), { abortSignal: signal });
31
+ }
32
+ finally {
33
+ client.destroy();
34
+ }
35
+ }
36
+ export class ReportPublishingService {
37
+ configId = randomUUID();
38
+ config;
39
+ store;
40
+ files;
41
+ upload;
42
+ workspaceName;
43
+ constructor(config, store, files, upload = uploadReport, workspaceName = () => 'workspace') {
44
+ this.workspaceName = workspaceName;
45
+ this.config = config
46
+ ? Object.freeze({ ...config, storage: Object.freeze({ ...config.storage }) })
47
+ : undefined;
48
+ this.store = store;
49
+ this.files = files;
50
+ this.upload = upload;
51
+ }
52
+ assertConfig(configId) {
53
+ if (configId !== this.configId)
54
+ throw new Error('report-publishing-config-changed');
55
+ }
56
+ references() {
57
+ const storage = this.config?.storage;
58
+ if (!storage)
59
+ return [];
60
+ return [
61
+ { field: 'accessKeyId', reference: storage.accessKeyIdRef, required: true },
62
+ { field: 'secretAccessKey', reference: storage.secretAccessKeyRef, required: true },
63
+ ...(storage.sessionTokenRef
64
+ ? [{ field: 'sessionToken', reference: storage.sessionTokenRef, required: true }]
65
+ : []),
66
+ ];
67
+ }
68
+ async describe() {
69
+ if (!this.config)
70
+ return { enabled: false, fields: [] };
71
+ const fields = await Promise.all(this.references().map(async (field) => {
72
+ const info = await this.store.describe(credentialRef(field.reference));
73
+ return {
74
+ ...field,
75
+ configured: info.configured,
76
+ writable: info.writable,
77
+ ...(info.source ? { source: info.source } : {}),
78
+ };
79
+ }));
80
+ return {
81
+ enabled: true,
82
+ configId: this.configId,
83
+ name: this.config.storage.name,
84
+ bucket: this.config.storage.bucket,
85
+ fields,
86
+ };
87
+ }
88
+ async change(field, value, signal) {
89
+ const item = this.references().find((item) => item.field === field);
90
+ if (!item)
91
+ throw new Error('report-publishing-disabled-or-field-unavailable');
92
+ signal.throwIfAborted();
93
+ if (value === undefined)
94
+ await this.store.unset(credentialRef(item.reference));
95
+ else
96
+ await this.store.set(credentialRef(item.reference), value);
97
+ }
98
+ async publish(scope, reportId, buildId, signal, viewHtml) {
99
+ const config = this.config;
100
+ if (!config)
101
+ throw new Error('report-publishing-disabled');
102
+ const history = await this.files.catalog('reports/history', { ...scope, reportId }, signal);
103
+ if (!('versions' in history))
104
+ throw new Error('report-publishing-build-unavailable');
105
+ const receipt = history.versions.find((entry) => entry.receipt.buildId === buildId)?.receipt;
106
+ if (!receipt)
107
+ throw new Error('report-publishing-build-unavailable');
108
+ const credentials = {};
109
+ for (const field of this.references()) {
110
+ signal.throwIfAborted();
111
+ const hit = await this.store.resolve(credentialRef(field.reference));
112
+ if (!hit?.value)
113
+ throw new Error('report-publishing-credentials-missing');
114
+ credentials[field.field] = hit.value;
115
+ }
116
+ const asset = await this.files.read({ ...scope, receipt, asset: viewHtml === undefined ? 'index.html' : 'presentation.json' }, signal);
117
+ let bytes;
118
+ if (viewHtml === undefined)
119
+ bytes = Buffer.from(asset.bodyBase64, 'base64');
120
+ else {
121
+ if (Buffer.byteLength(viewHtml) > PRESENTATION_BUDGETS.htmlBytes ||
122
+ !viewHtml.startsWith('<!doctype html><html lang="zh-CN"><head>'))
123
+ throw new Error('report-publishing-view-invalid');
124
+ // Browser-authored snapshot: force a restrictive policy before any client-supplied markup.
125
+ bytes = Buffer.from(viewHtml.replace('<head>', `<head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">`));
126
+ if (bytes.length > PRESENTATION_BUDGETS.htmlBytes)
127
+ throw new Error('report-publishing-view-invalid');
128
+ }
129
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
130
+ const identity = createHash('sha256')
131
+ .update(JSON.stringify([
132
+ receipt.workspaceId,
133
+ reportId,
134
+ buildId,
135
+ viewHtml === undefined ? 'report' : 'view',
136
+ sha256,
137
+ ]))
138
+ .digest('hex')
139
+ .slice(0, 32);
140
+ const key = [
141
+ config.pathPrefix,
142
+ publishingPathName(this.workspaceName(receipt.workspaceId), 'workspace'),
143
+ publishingPathName(receipt.title, 'report'),
144
+ identity,
145
+ 'index.html',
146
+ ]
147
+ .filter(Boolean)
148
+ .join('/');
149
+ signal.throwIfAborted();
150
+ try {
151
+ await this.upload({
152
+ config,
153
+ credentials: credentials,
154
+ key,
155
+ bytes,
156
+ signal,
157
+ });
158
+ }
159
+ catch {
160
+ throw new Error('report-publishing-upload-unconfirmed');
161
+ }
162
+ finally {
163
+ for (const key of Object.keys(credentials))
164
+ delete credentials[key];
165
+ }
166
+ return {
167
+ storage: config.storage.name,
168
+ bucket: config.storage.bucket,
169
+ key,
170
+ url: `${config.publicBaseUrl.replace(/\/+$/, '')}/${key.split('/').map(encodeURIComponent).join('/')}`,
171
+ workspaceId: receipt.workspaceId,
172
+ reportId,
173
+ buildId,
174
+ sha256,
175
+ bytes: bytes.length,
176
+ kind: viewHtml === undefined ? 'report' : 'view',
177
+ publishedAt: new Date().toISOString(),
178
+ };
179
+ }
180
+ }
package/lib/rpc.js ADDED
@@ -0,0 +1,49 @@
1
+ /** Dedicated plugin RPC envelopes over Harness-owned authenticated exact Fetch routes. */
2
+ import { clientRequestSchema, } from '@deepseek-ai/dsh-client-connection';
3
+ import { finishCleanup, PendingTasks } from "./lifecycle.js";
4
+ export function registerPluginRpc(connection, channel, endpoints, handler) {
5
+ const lifetime = new AbortController();
6
+ const tasks = new PendingTasks();
7
+ let closing;
8
+ const disposers = [];
9
+ const close = () => {
10
+ if (closing)
11
+ return closing;
12
+ lifetime.abort();
13
+ closing = finishCleanup([...disposers.reverse(), () => tasks.drain()]);
14
+ return closing;
15
+ };
16
+ try {
17
+ for (const endpoint of endpoints) {
18
+ const method = `${channel.slice(1)}/${endpoint}`;
19
+ disposers.push(connection.fetch.register({
20
+ path: `/api/${method}`,
21
+ methods: ['POST'],
22
+ requestBody: 'buffered',
23
+ fetch(request) {
24
+ if (lifetime.signal.aborted)
25
+ return Promise.resolve(new Response('Plugin disposed', { status: 503 }));
26
+ return tasks.track((async () => {
27
+ if (request.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase() !==
28
+ 'application/json')
29
+ return new Response('Expected application/json', { status: 415 });
30
+ const body = await request.json().catch(() => undefined);
31
+ const parsed = clientRequestSchema.safeParse(body);
32
+ if (!parsed.success || parsed.data.method !== method)
33
+ return new Response('Invalid plugin request', { status: 400 });
34
+ // A request admitted before withdrawal may still be decoding its body.
35
+ if (lifetime.signal.aborted)
36
+ return new Response('Plugin disposed', { status: 503 });
37
+ const result = await handler(endpoint, parsed.data.payload, AbortSignal.any([request.signal, lifetime.signal]));
38
+ return Response.json({ type: 'server-response', rpcId: parsed.data.rpcId, result });
39
+ })());
40
+ },
41
+ }));
42
+ }
43
+ }
44
+ catch (error) {
45
+ void close();
46
+ throw error;
47
+ }
48
+ return close;
49
+ }
@@ -2,6 +2,8 @@ import { boundedText, closed, parseRef, refKey, } from "../semantic-reference/co
2
2
  import { parseComputation } from "./definition.js";
3
3
  export const BROWSER_ENDPOINT = 'semantic-browser/catalog';
4
4
  export const CATALOG_MAX_BYTES = 32 * 1024 * 1024;
5
+ export const CATALOG_ERROR_SCHEMA = 'marivo.semantic_catalog_error/v1';
6
+ const CATALOG_ERROR_MAX_CHARS = 16 * 1024;
5
7
  function array(value, max) {
6
8
  if (!Array.isArray(value) || value.length > max)
7
9
  throw new Error('invalid-catalog');
@@ -57,6 +59,21 @@ export function parseCatalogProjection(value) {
57
59
  throw new Error('invalid-catalog');
58
60
  return { fingerprint: boundedText(r.fingerprint, 256), kinds, objects };
59
61
  }
62
+ export function parseCatalogFailure(value) {
63
+ try {
64
+ const r = closed(value, ['schema', 'kind', 'message']);
65
+ if (r.schema !== CATALOG_ERROR_SCHEMA || r.kind !== 'semantic')
66
+ return undefined;
67
+ return {
68
+ schema: CATALOG_ERROR_SCHEMA,
69
+ kind: 'semantic',
70
+ message: boundedText(r.message, CATALOG_ERROR_MAX_CHARS),
71
+ };
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
60
77
  export function parseCatalogSnapshot(value) {
61
78
  const r = closed(value, [
62
79
  'workspaceId',
@@ -14,6 +14,7 @@ class DiscardOutput:
14
14
  with contextlib.redirect_stdout(DiscardOutput()), contextlib.redirect_stderr(DiscardOutput()):
15
15
  import marivo.semantic as ms
16
16
  from marivo.refs import Ref, RefPayloadV1, SemanticKind
17
+ from marivo.semantic.errors import SemanticError, SemanticLoadFailed
17
18
 
18
19
  FIELDS = {
19
20
  "domain": "owner default",
@@ -109,11 +110,20 @@ with contextlib.redirect_stdout(DiscardOutput()), contextlib.redirect_stderr(Dis
109
110
  "fields": fields, "relations": list(unique.values()),
110
111
  }
111
112
 
113
+ try:
112
114
  catalog = ms.load(workspace_dir=sys.argv[1])
113
115
  result = {
114
116
  "fingerprint": catalog.definition_fingerprint,
115
117
  "kinds": [kind.value for kind in SemanticKind],
116
118
  "objects": [project(entry) for kind in SemanticKind for entry in catalog.items(kind).items],
117
119
  }
120
+ except (SemanticLoadFailed, SemanticError) as error:
121
+ print(json.dumps({
122
+ "schema": "marivo.semantic_catalog_error/v1",
123
+ "kind": "semantic",
124
+ "message": str(error),
125
+ }, ensure_ascii=True))
126
+ raise SystemExit(1)
127
+
118
128
  print(json.dumps(result, ensure_ascii=True))
119
129
  `.trim();
@@ -1,12 +1,25 @@
1
1
  import { realpath } from 'node:fs/promises';
2
+ import { finishCleanup, PendingTasks } from "../lifecycle.js";
2
3
  import { boundedText, closed } from "../semantic-reference/contracts.js";
4
+ import { semanticEnvironmentFingerprint } from "../semantic-reference/environment.js";
3
5
  import { abortable } from "../semantic-reference/rpc.js";
4
- import { CATALOG_MAX_BYTES, parseCatalogProjection } from "./contracts.js";
6
+ import { CATALOG_MAX_BYTES, parseCatalogFailure, parseCatalogProjection, } from "./contracts.js";
5
7
  import { BROWSER_CATALOG_PROGRAM } from "./program.js";
8
+ export class SemanticCatalogLoadError extends Error {
9
+ code = 'catalog-load-failed';
10
+ diagnostic;
11
+ constructor(diagnostic) {
12
+ super(diagnostic);
13
+ this.diagnostic = diagnostic;
14
+ this.name = 'SemanticCatalogLoadError';
15
+ }
16
+ }
6
17
  /** Workspace-addressed read service; intentionally has no Agent or credential dependency. */
7
18
  export class SemanticBrowserService {
8
19
  #host;
9
20
  #lifetime = new AbortController();
21
+ #tasks = new PendingTasks();
22
+ #closing;
10
23
  constructor(host) {
11
24
  this.#host = host;
12
25
  }
@@ -15,7 +28,10 @@ export class SemanticBrowserService {
15
28
  if (!current || current.path !== workspacePath || this.#host.projectRoot(current) !== root)
16
29
  throw new Error('workspace-changed');
17
30
  }
18
- async read(payload, caller) {
31
+ read(payload, caller) {
32
+ return this.#tasks.track(this.#read(payload, caller));
33
+ }
34
+ async #read(payload, caller) {
19
35
  const signal = AbortSignal.any([caller, this.#lifetime.signal]);
20
36
  signal.throwIfAborted();
21
37
  const request = closed(payload, ['workspaceId']);
@@ -24,7 +40,7 @@ export class SemanticBrowserService {
24
40
  if (!workspace)
25
41
  throw new Error('workspace-unavailable');
26
42
  const root = this.#host.projectRoot(workspace);
27
- const runner = await abortable(this.#host.resolve(root), signal);
43
+ const runner = await abortable(this.#tasks.track(this.#host.resolve(root)), signal);
28
44
  this.#assertOwner(id, workspace.path, root);
29
45
  if (runner.status !== 'ready')
30
46
  throw new Error('environment-failed');
@@ -40,8 +56,19 @@ export class SemanticBrowserService {
40
56
  throw new Error('workspace-changed');
41
57
  this.#assertOwner(id, workspace.path, root);
42
58
  signal.throwIfAborted();
43
- if (result.exitCode !== 0)
59
+ if (result.exitCode !== 0) {
60
+ const failure = (() => {
61
+ try {
62
+ return parseCatalogFailure(JSON.parse(result.stdout.toString('utf8')));
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ })();
68
+ if (failure)
69
+ throw new SemanticCatalogLoadError(failure.message);
44
70
  throw new Error('catalog-load-failed');
71
+ }
45
72
  if (result.stdout.byteLength > CATALOG_MAX_BYTES)
46
73
  throw new Error('catalog-too-large');
47
74
  const projection = parseCatalogProjection(JSON.parse(result.stdout.toString('utf8')));
@@ -49,17 +76,24 @@ export class SemanticBrowserService {
49
76
  ...projection,
50
77
  workspaceId: id,
51
78
  projectRoot: runner.binding.projectRoot,
52
- environmentFingerprint: runner.binding.fingerprint,
79
+ environmentFingerprint: semanticEnvironmentFingerprint(id, runner.binding.fingerprint),
53
80
  loadedAt: new Date().toISOString(),
54
81
  };
55
82
  }
83
+ close() {
84
+ this.dispose();
85
+ this.#closing ??= finishCleanup([() => this.#tasks.drain()]);
86
+ return this.#closing;
87
+ }
56
88
  dispose() {
57
89
  this.#lifetime.abort();
58
90
  }
59
91
  }
60
- /** Fixed diagnostics only: Python errors may contain model code or datasource secrets. */
92
+ /** Preserve Marivo semantic diagnostics; keep unknown Python failures behind a fixed fallback. */
61
93
  export function browserFailure(error) {
62
94
  const code = error instanceof Error && 'code' in error ? error.code : undefined;
95
+ if (error instanceof SemanticCatalogLoadError)
96
+ return `语义层模型加载失败:\n${error.diagnostic}`;
63
97
  if (code === 'subprocess-timeout')
64
98
  return '语义层加载超时,请检查模型后重试。';
65
99
  if (code === 'subprocess-output-limit' ||
@@ -1,3 +1,4 @@
1
+ import { finishCleanup, PendingTasks } from "../lifecycle.js";
1
2
  import { parseProjection } from "./contracts.js";
2
3
  export const CATALOG_PROGRAM = String.raw `
3
4
  import contextlib, io, json, sys
@@ -19,6 +20,8 @@ export class SemanticReferenceBridge {
19
20
  #slots = new Map();
20
21
  #clock;
21
22
  #disposed = false;
23
+ #tasks = new PendingTasks();
24
+ #closing;
22
25
  constructor(clock = Date.now) {
23
26
  this.#clock = clock;
24
27
  }
@@ -47,7 +50,8 @@ export class SemanticReferenceBridge {
47
50
  promise: Promise.resolve({ kinds: [], items: [] }),
48
51
  };
49
52
  current.flight = flight;
50
- flight.promise = this.#load(runner, controller.signal)
53
+ flight.promise = this.#tasks
54
+ .track(this.#load(runner, controller.signal))
51
55
  .then((projection) => {
52
56
  if (!controller.signal.aborted &&
53
57
  !this.#disposed &&
@@ -105,6 +109,11 @@ export class SemanticReferenceBridge {
105
109
  throw new Error('catalog-load-failed');
106
110
  return parseProjection(JSON.parse(result.stdout.toString('utf8')));
107
111
  }
112
+ close() {
113
+ this.dispose();
114
+ this.#closing ??= finishCleanup([() => this.#tasks.drain()]);
115
+ return this.#closing;
116
+ }
108
117
  invalidate(key) {
109
118
  const slot = this.#slots.get(key);
110
119
  this.#slots.delete(key);
@@ -1,7 +1,6 @@
1
1
  /** Browser-safe wire contracts. These bounds do not reimplement Marivo's domain grammar. */
2
2
  export const CHANNEL = '/dsh-data-analysis';
3
3
  export const SOURCE = 'marivo-semantic';
4
- export const LIMIT = 40;
5
4
  export const MAX_WIRE_BYTES = 1024 * 1024;
6
5
  export function closed(value, keys) {
7
6
  if (typeof value !== 'object' || value === null || Array.isArray(value))
@@ -87,20 +86,19 @@ export function parseProjection(value) {
87
86
  return Object.freeze({ kinds: Object.freeze(kinds), items: Object.freeze(items) });
88
87
  }
89
88
  export function parseCandidatesRequest(value) {
90
- const r = closed(value, ['version', 'sessionId', 'query', 'quoted', 'limit']);
91
- if (r.version !== 1 || r.limit !== LIMIT || typeof r.quoted !== 'boolean')
89
+ const r = closed(value, ['version', 'sessionId', 'query', 'quoted']);
90
+ if (r.version !== 1 || typeof r.quoted !== 'boolean')
92
91
  throw new Error('invalid-request');
93
92
  return {
94
93
  version: 1,
95
94
  sessionId: boundedText(r.sessionId, 256),
96
95
  query: boundedText(r.query, 128, true),
97
96
  quoted: r.quoted,
98
- limit: LIMIT,
99
97
  };
100
98
  }
101
99
  export function parseCandidatesResponse(value) {
102
100
  const r = closed(value, ['environmentFingerprint', 'items']);
103
- if (!Array.isArray(r.items) || r.items.length > LIMIT)
101
+ if (!Array.isArray(r.items))
104
102
  throw new Error('invalid-response');
105
103
  const items = r.items.map((raw) => {
106
104
  const item = closed(raw, ['ref', 'refKey', 'name', 'businessDefinition', 'section']);
@@ -109,5 +107,21 @@ export function parseCandidatesResponse(value) {
109
107
  throw new Error('invalid-response');
110
108
  return { ...parseCandidate(candidate), section };
111
109
  });
112
- return { environmentFingerprint: boundedText(r.environmentFingerprint, 256), items };
110
+ return {
111
+ environmentFingerprint: boundedText(r.environmentFingerprint, 256),
112
+ items,
113
+ };
114
+ }
115
+ /** Browser selection identity only; preparation never reloads semantic content. */
116
+ export function parsePrepareRequest(value) {
117
+ const r = closed(value, ['sessionId', 'workspaceId', 'environmentFingerprint', 'ref']);
118
+ return {
119
+ workspaceId: boundedText(r.workspaceId, 256),
120
+ envelope: parseEnvelope({
121
+ schema: 'dsh-data-analysis-semantic-reference/v1',
122
+ sessionId: r.sessionId,
123
+ environmentFingerprint: r.environmentFingerprint,
124
+ ref: r.ref,
125
+ }),
126
+ };
113
127
  }
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Reference identity includes Harness ownership; the underlying Runtime stays unchanged. */
3
+ export function semanticEnvironmentFingerprint(workspaceId, runtimeFingerprint) {
4
+ return createHash('sha256')
5
+ .update(JSON.stringify(['marivo-semantic-workspace/v1', workspaceId, runtimeFingerprint]))
6
+ .digest('hex');
7
+ }
8
+ /** Explicit selection establishes ownership; serialization never repairs it. */
9
+ export function referenceEnvironmentResolver(host) {
10
+ const owners = new WeakMap();
11
+ return async (sessionId, purpose, workspaceId) => {
12
+ const agent = host.agent(sessionId);
13
+ if (!agent)
14
+ throw new Error('unknown-session');
15
+ const priorOwner = owners.get(agent);
16
+ const owner = purpose === 'reference' ? priorOwner?.workspace : host.workspace(sessionId);
17
+ if (purpose === 'prepare' && (!workspaceId || owner?.id !== workspaceId))
18
+ throw new Error('workspace-mismatch');
19
+ const checkOwner = () => {
20
+ if (!owner)
21
+ return;
22
+ const current = host.workspace(sessionId);
23
+ if (current.id !== owner.id || current.path !== owner.path)
24
+ throw new Error('workspace-changed');
25
+ };
26
+ checkOwner();
27
+ const bound = host.binding(agent);
28
+ if (purpose === 'reference' &&
29
+ (!owner || !bound || bound !== priorOwner?.binding || bound.root !== host.projectRoot(agent)))
30
+ throw new Error('environment-unbound');
31
+ const pending = purpose === 'reference' ? bound.environment : host.resolve(agent);
32
+ const expected = host.binding(agent);
33
+ const environment = await pending;
34
+ if (host.agent(sessionId) !== agent ||
35
+ host.binding(agent) !== expected ||
36
+ expected?.root !== host.projectRoot(agent))
37
+ throw new Error('environment-changed');
38
+ checkOwner();
39
+ if (!owner || !expected)
40
+ throw new Error('environment-unbound');
41
+ if (purpose !== 'reference')
42
+ owners.set(agent, { workspace: owner, binding: expected });
43
+ // Only the semantic surface uses the ownership fingerprint. Execution is delegated
44
+ // to the original runner, preserving its Runtime identity and dynamic health.
45
+ return {
46
+ binding: {
47
+ ...environment.binding,
48
+ fingerprint: semanticEnvironmentFingerprint(owner.id, environment.binding.fingerprint),
49
+ },
50
+ get status() {
51
+ return environment.status;
52
+ },
53
+ runChecked: (request) => environment.runChecked(request),
54
+ };
55
+ };
56
+ }
@@ -0,0 +1,16 @@
1
+ /** Shared display labels and search aliases; object kinds still come from the live Catalog. */
2
+ export const semanticKindLabels = {
3
+ domain: '业务域',
4
+ datasource: '数据源',
5
+ entity: '实体',
6
+ dimension: '维度',
7
+ measure: '度量',
8
+ time_dimension: '时间维度',
9
+ metric: '指标',
10
+ relationship: '关系',
11
+ event: '事件',
12
+ state_model: '状态模型',
13
+ period_calendar: '周期日历',
14
+ temporal_set: '时间集合',
15
+ work_schedule: '工作日程',
16
+ };