@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
@@ -56,7 +56,7 @@ const packageManifest = parsePackageManifest(createRequire(import.meta.url)('../
56
56
  /** The package semver and the compatibility contract shipped in this exact installation. */
57
57
  export const PLUGIN_VERSION = packageManifest.version;
58
58
  export const DSH_DATA_ANALYSIS_COMPATIBILITY = packageManifest.dshDataAnalysisCompatibility;
59
- /** Every DSH peer is required and must use this one verified release. */
59
+ /** Every DSH peer is required and must satisfy this declared compatibility range. */
60
60
  export const DSH_PEER_RANGE = DSH_DATA_ANALYSIS_COMPATIBILITY.dsh.peerRange;
61
61
  export const DSH_DISTRIBUTION = DSH_DATA_ANALYSIS_COMPATIBILITY.dsh.distribution;
62
62
  export const DSH_PEER_DEPENDENCIES = Object.freeze(Object.fromEntries(Object.entries(packageManifest.peerDependencies).filter(([name]) => name.startsWith('@deepseek-ai/dsh-'))));
@@ -17,6 +17,7 @@ export const datasourceAuthoringSchema = z.object({
17
17
  export const DATASOURCE_AUTHORING_PROGRAM = String.raw `
18
18
  import contextlib
19
19
  import dataclasses
20
+ import hashlib
20
21
  import json
21
22
  import sys
22
23
  import typing
@@ -43,28 +44,66 @@ def spec_fields(cls):
43
44
  result.append(item)
44
45
  return result
45
46
 
47
+ def configuration(name, cls):
48
+ import marivo.semantic as ms
49
+ description = md.describe(name)
50
+ details = ms.load().datasources.get(name).details()
51
+ if details.backend_type != description.backend_type or dict(details.fields) != description.literal_fields or dict(details.env_refs) != description.env_refs:
52
+ raise ValueError("context-changed")
53
+ context = {"business_definition": details.context.business_definition,
54
+ "guardrails": list(details.context.guardrails)}
55
+ allowed = {field["name"] for field in spec_fields(cls)}
56
+ fields = {"name": name}
57
+ extra = {}
58
+ for key, value in description.literal_fields.items():
59
+ if key in allowed and key != "extra": fields[key] = value
60
+ else: extra[key] = value
61
+ if extra: fields["extra"] = extra
62
+ for key, value in description.env_refs.items():
63
+ if key.startswith("http_header:"):
64
+ fields.setdefault("http_headers_env", {})[key.removeprefix("http_header:")] = value
65
+ elif key + "_env" in allowed: fields[key + "_env"] = value
66
+ else: raise ValueError("datasource-definition-invalid")
67
+ version = hashlib.sha256(json.dumps([description.backend_type, fields, context], sort_keys=True).encode()).hexdigest()
68
+ return {"name": name, "backend": description.backend_type, "fields": fields, "version": version}, context
69
+
46
70
  try:
47
71
  payload = json.load(sys.stdin)
48
72
  with contextlib.redirect_stdout(Discard()), contextlib.redirect_stderr(Discard()):
49
73
  specs = {cls.backend_type: cls for cls in typing.get_args(md.DatasourceSpec)}
50
74
  if payload["action"] == "schema":
51
75
  result = {"backends": [{"name": key, "fields": spec_fields(cls)} for key, cls in specs.items()]}
76
+ elif payload["action"] == "read":
77
+ description = md.describe(payload["name"])
78
+ result, _ = configuration(payload["name"], specs[description.backend_type])
52
79
  else:
53
80
  cls = specs[payload["backend"]]
54
81
  fields = payload["fields"]
55
82
  allowed = {field["name"] for field in spec_fields(cls)}
83
+ context = None
84
+ if payload["action"] == "update":
85
+ current = md.describe(payload["name"])
86
+ if fields.get("name") != payload["name"] or current.backend_type != payload["backend"]:
87
+ raise ValueError("datasource-identity-fixed")
88
+ original, context = configuration(payload["name"], cls)
89
+ if original["version"] != payload["version"]:
90
+ raise ValueError("datasource-config-changed")
56
91
  if set(fields) - allowed:
57
92
  raise ValueError("unknown-field")
58
93
  spec = cls(**fields)
59
- if any(item.name == spec.name for item in md.list()):
94
+ if payload["action"] == "create" and any(item.name == spec.name for item in md.list()):
60
95
  result = {"error": "datasource-already-exists"}
61
96
  else:
62
- # Validate references before writing; the Host applies its own reserved-name check too.
63
97
  if sorted(spec.env_refs.values()) != sorted(payload["refs"]):
64
98
  raise ValueError("credential-reference-mismatch")
99
+ if context is not None:
100
+ import marivo.semantic as ms
101
+ spec = cls(**fields, ai_context=ms.ai_context(**context))
65
102
  md.register(spec)
66
103
  result = {"name": spec.name}
67
- except Exception:
68
- result = {"error": "datasource-definition-invalid"}
104
+
105
+ except Exception as exc:
106
+ code = str(exc)
107
+ result = {"error": code if code in ("datasource-config-changed", "datasource-identity-fixed", "context-changed") else "datasource-definition-invalid"}
69
108
  print(json.dumps(result))
70
109
  `.trim();
@@ -1,5 +1,5 @@
1
1
  import { RESOLVER_PROGRAM } from "./resolver-program.js";
2
- export const MARIVO_DATASOURCE_DESCRIBE_PROGRAM = String.raw `
2
+ const DATASOURCE_DESCRIPTION_PROGRAM = String.raw `
3
3
  import json
4
4
  import os
5
5
  import sys
@@ -16,29 +16,32 @@ def project_description(description):
16
16
  "fields": description.env_refs,
17
17
  "definition": hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()}
18
18
 
19
- print(json.dumps(project_description(md.describe(sys.argv[1])), sort_keys=True))
20
19
  `.trim();
21
- export const MARIVO_DATASOURCE_INVENTORY_PROGRAM = String.raw `
22
- import json
23
- import os
24
- import sys
25
- import marivo
26
- import marivo.datasource as md
27
-
28
- import hashlib
29
-
30
- def project_description(description):
31
- payload = {"name": description.name, "backend": description.backend_type,
32
- "fields": description.literal_fields, "refs": description.env_refs}
33
- return {"name": description.name, "backend": description.backend_type,
34
- "properties": description.literal_fields, "refs": list(description.env_refs.values()),
35
- "fields": description.env_refs,
36
- "definition": hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()}
37
-
20
+ export const MARIVO_DATASOURCE_DESCRIBE_PROGRAM = `${DATASOURCE_DESCRIPTION_PROGRAM}
21
+ print(json.dumps(project_description(md.describe(sys.argv[1])), sort_keys=True))`;
22
+ export const MARIVO_DATASOURCE_REMOVE_PROGRAM = `${DATASOURCE_DESCRIPTION_PROGRAM}
23
+ import contextlib
24
+ class Discard:
25
+ def write(self, value): return len(value)
26
+ def flush(self): pass
27
+ try:
28
+ payload = json.load(sys.stdin)
29
+ with contextlib.redirect_stdout(Discard()), contextlib.redirect_stderr(Discard()):
30
+ current = project_description(md.describe(payload["name"]))
31
+ if current["definition"] != payload["definition"]:
32
+ result = {"error": "context-changed"}
33
+ elif md.remove(payload["name"]):
34
+ result = {"name": payload["name"]}
35
+ else:
36
+ result = {"error": "datasource-not-removable"}
37
+ except Exception:
38
+ result = {"error": "datasource-remove-failed"}
39
+ print(json.dumps(result))`;
40
+ export const MARIVO_DATASOURCE_INVENTORY_PROGRAM = `${DATASOURCE_DESCRIPTION_PROGRAM}
38
41
  print(json.dumps({
39
42
  "datasources": [project_description(md.describe(item.name)) for item in md.list()],
40
43
  }, sort_keys=True))
41
- `.trim();
44
+ `;
42
45
  export const MARIVO_DATASOURCE_TEST_PROGRAM = `${RESOLVER_PROGRAM}\n${String.raw `
43
46
  import contextlib
44
47
  import io
@@ -2,7 +2,7 @@ import { isCredentialRefName } from '@deepseek-ai/dsh-credentials';
2
2
  import { MarivoEnvironmentError } from "../environment/errors.js";
3
3
  import { resolveMarivoBridgeSource } from "../environment/source.js";
4
4
  import { DATASOURCE_AUTHORING_PROGRAM, datasourceAuthoringSchema, } from "./authoring.js";
5
- import { MARIVO_DATASOURCE_DESCRIBE_PROGRAM, MARIVO_DATASOURCE_INVENTORY_PROGRAM, MARIVO_DATASOURCE_TEST_PROGRAM, } from "./bridge-programs.js";
5
+ import { MARIVO_DATASOURCE_DESCRIBE_PROGRAM, MARIVO_DATASOURCE_INVENTORY_PROGRAM, MARIVO_DATASOURCE_REMOVE_PROGRAM, MARIVO_DATASOURCE_TEST_PROGRAM, } from "./bridge-programs.js";
6
6
  import { marivoCredentialStorageRef } from "./shell-env.js";
7
7
  const DATASOURCE_LIMITS = Object.freeze({
8
8
  timeoutMs: 30_000,
@@ -210,7 +210,30 @@ export class MarivoDatasourceBridge {
210
210
  fingerprint: this.binding.fingerprint,
211
211
  });
212
212
  }
213
+ async configuration(name, signal) {
214
+ const result = await this.#runner.runChecked({
215
+ program: DATASOURCE_AUTHORING_PROGRAM,
216
+ stdin: JSON.stringify({ action: 'read', name }),
217
+ limits: DATASOURCE_LIMITS,
218
+ signal,
219
+ });
220
+ this.#assertSuccess(result, 'inventory');
221
+ const value = parseJsonObject(result.stdout, 'inventory');
222
+ if (value.name !== name ||
223
+ typeof value.backend !== 'string' ||
224
+ typeof value.version !== 'string' ||
225
+ !object(value.fields) ||
226
+ !jsonValue(value.fields))
227
+ throw new Error('Datasource configuration cannot be read completely');
228
+ return value;
229
+ }
230
+ async update(input, signal) {
231
+ return this.#write(input, 'update', signal);
232
+ }
213
233
  async create(input, signal) {
234
+ return this.#write(input, 'create', signal);
235
+ }
236
+ async #write(input, action, signal) {
214
237
  const refs = Object.entries(input.fields)
215
238
  .filter(([key]) => key.endsWith('_env'))
216
239
  .flatMap(([, value]) => typeof value === 'string'
@@ -233,14 +256,17 @@ export class MarivoDatasourceBridge {
233
256
  }
234
257
  const result = await this.#runner.runChecked({
235
258
  program: DATASOURCE_AUTHORING_PROGRAM,
236
- stdin: JSON.stringify({ action: 'create', ...input, refs }),
259
+ stdin: JSON.stringify({ action, ...input, refs }),
237
260
  limits: DATASOURCE_LIMITS,
238
261
  signal,
239
262
  });
240
263
  this.#assertSuccess(result, 'inventory');
241
264
  const value = parseJsonObject(result.stdout, 'inventory');
242
265
  if (value.error === 'datasource-already-exists' ||
243
- value.error === 'datasource-definition-invalid')
266
+ value.error === 'datasource-definition-invalid' ||
267
+ value.error === 'datasource-config-changed' ||
268
+ value.error === 'datasource-identity-fixed' ||
269
+ value.error === 'context-changed')
244
270
  return { error: value.error };
245
271
  if (typeof value.name !== 'string')
246
272
  throw new Error('Invalid datasource creation result');
@@ -256,6 +282,23 @@ export class MarivoDatasourceBridge {
256
282
  this.#assertSuccess(result, 'describe');
257
283
  return parseDescription(parseJsonObject(result.stdout, 'describe'), 'describe');
258
284
  }
285
+ async remove(description, signal) {
286
+ const result = await this.#runner.runChecked({
287
+ program: MARIVO_DATASOURCE_REMOVE_PROGRAM,
288
+ stdin: JSON.stringify({ name: description.name, definition: description.definition }),
289
+ limits: DATASOURCE_LIMITS,
290
+ signal,
291
+ });
292
+ this.#assertSuccess(result, 'inventory');
293
+ const value = parseJsonObject(result.stdout, 'inventory');
294
+ if (value.error === 'context-changed' ||
295
+ value.error === 'datasource-not-removable' ||
296
+ value.error === 'datasource-remove-failed')
297
+ return { error: value.error };
298
+ if (value.name !== description.name)
299
+ throw new Error('Invalid datasource removal result');
300
+ return { name: description.name };
301
+ }
259
302
  async inventory(signal) {
260
303
  const result = await this.#runner.runChecked({
261
304
  program: MARIVO_DATASOURCE_INVENTORY_PROGRAM,
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ export const CREDENTIAL_CHANGES_SERVICE = 'dshDataAnalysisCredentials';
3
+ const owner = '@chengxianglibra/dsh-data-analysis';
4
+ const sessionId = z.string().min(1).max(256);
5
+ export const credentialRevisionSchema = z
6
+ .object({ generation: z.string().uuid(), cursor: z.string().min(1).max(256) })
7
+ .strict();
8
+ /** Plugin-owned strict boundary shared by both faces; no Host registry is duplicated. */
9
+ const descriptor = {
10
+ id: `${owner}#${CREDENTIAL_CHANGES_SERVICE}/changes`,
11
+ service: CREDENTIAL_CHANGES_SERVICE,
12
+ namespace: CREDENTIAL_CHANGES_SERVICE,
13
+ method: 'changes',
14
+ mode: 'stream',
15
+ invocation: { kind: 'direct' },
16
+ parameters: [
17
+ {
18
+ name: 'sessionId',
19
+ wire: 'sessionId',
20
+ source: 'json',
21
+ codec: { mode: 'strict', typeSymbol: `${owner}#CredentialSessionId`, schema: sessionId },
22
+ },
23
+ ],
24
+ cancellation: { parameter: 'signal' },
25
+ result: {
26
+ mode: 'strict',
27
+ typeSymbol: `${owner}#CredentialRevision`,
28
+ schema: credentialRevisionSchema,
29
+ },
30
+ };
31
+ export const credentialChangesHost = {
32
+ package: owner,
33
+ face: 'host',
34
+ schemas: [],
35
+ model: { services: [], events: [], objects: [] },
36
+ invocations: [descriptor],
37
+ };
38
+ export const credentialChangesRemote = {
39
+ package: owner,
40
+ descriptors: [descriptor],
41
+ };
@@ -0,0 +1,49 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
3
+ import { PendingTasks } from "../lifecycle.js";
4
+ import { CREDENTIAL_CHANGES_SERVICE } from "./changes-contract.js";
5
+ import { CredentialServiceError } from "./service.js";
6
+ /** Reuses the Host's logical stream carrier; credentials and waiting calls keep their owner. */
7
+ export class CredentialChangesService extends TypertRemoteService {
8
+ credentials;
9
+ lifetime = new AbortController();
10
+ tasks = new PendingTasks();
11
+ closing;
12
+ constructor(ctx, credentials) {
13
+ super(ctx, CREDENTIAL_CHANGES_SERVICE);
14
+ this.credentials = credentials;
15
+ }
16
+ async *changes(sessionId, caller) {
17
+ const signal = AbortSignal.any([caller, this.lifetime.signal]);
18
+ let cursor;
19
+ while (!signal.aborted) {
20
+ const snapshot = await this.tasks.track(this.snapshot(sessionId, cursor, signal));
21
+ signal.throwIfAborted();
22
+ if (snapshot.cursor !== cursor) {
23
+ cursor = snapshot.cursor;
24
+ yield { generation: snapshot.generation, cursor };
25
+ }
26
+ }
27
+ }
28
+ async snapshot(sessionId, cursor, signal) {
29
+ let attempt = 0;
30
+ for (;;) {
31
+ signal.throwIfAborted();
32
+ try {
33
+ return await this.credentials.waitWatch(sessionId, cursor, signal);
34
+ }
35
+ catch (error) {
36
+ signal.throwIfAborted();
37
+ if (!(error instanceof CredentialServiceError) ||
38
+ error.code !== 'credential-state-unavailable')
39
+ throw new Error('credential-notification-unavailable');
40
+ await delay([1000, 2000, 5000][Math.min(attempt++, 2)], undefined, { signal });
41
+ }
42
+ }
43
+ }
44
+ close() {
45
+ this.lifetime.abort();
46
+ this.closing ??= this.tasks.drain();
47
+ return this.closing;
48
+ }
49
+ }
@@ -0,0 +1,39 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { registerMarivoTool } from "../tool-lifecycle.js";
3
+ import { resolveMarivoDatasourceBridge } from "./bridge.js";
4
+ export const MARIVO_DATASOURCE_CONFIGURE_TOOL_NAME = 'marivo_datasource_configure';
5
+ export function createMarivoDatasourceConfigureTool(source, service) {
6
+ return defineTool({
7
+ name: MARIVO_DATASOURCE_CONFIGURE_TOOL_NAME,
8
+ description: 'When a requested remote table has no usable configured datasource, use mode=create to let the user create/select one; repair connection configuration with mode=edit and its exact name. The user configures it in the owning Web session right tab; never write connection configuration on their behalf or pass credentials or configuration values. Waits for successful save and connection test, cancellation or failure. Only status=ok permits continuation: rediscover the datasource and verify the requested table and read access before analysis. Success already includes a connection test; do not repeat it unless configuration or credentials change, a connection fails, or the user requests one. Local files need no remote datasource setup.',
9
+ parameters: {
10
+ mode: {
11
+ type: 'string',
12
+ required: true,
13
+ description: 'create (also permits choosing an existing datasource) or edit (fixed name and engine).',
14
+ },
15
+ name: { type: 'string', description: 'Existing datasource name, required only for edit.' },
16
+ reason: {
17
+ type: 'string',
18
+ required: true,
19
+ description: 'Brief reason for configuration, at most 1000 characters. No secrets.',
20
+ },
21
+ },
22
+ output: {
23
+ schema: { type: 'json' },
24
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
25
+ },
26
+ async execute(args, exec) {
27
+ if (args.mode !== 'create' && args.mode !== 'edit')
28
+ throw new Error('Invalid configuration mode');
29
+ return JSON.parse(JSON.stringify(await service.track(service.configure(exec, () => resolveMarivoDatasourceBridge(source), {
30
+ mode: args.mode,
31
+ name: args.name,
32
+ reason: args.reason,
33
+ }))));
34
+ },
35
+ });
36
+ }
37
+ export function registerMarivoDatasourceConfigureTool(ctx, source, service) {
38
+ return registerMarivoTool(ctx, createMarivoDatasourceConfigureTool(source, service));
39
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ import { CredentialServiceError } from "./service.js";
3
+ /** Validate against this Workspace's live Runtime; never include config values in errors. */
4
+ export function withDatasourceDefaults(schema, configured) {
5
+ if (configured === undefined)
6
+ return schema;
7
+ const parsed = z.record(z.string(), z.record(z.string(), z.json())).safeParse(configured);
8
+ if (!parsed.success)
9
+ throw new CredentialServiceError('datasource-defaults-invalid');
10
+ for (const [backend, values] of Object.entries(parsed.data)) {
11
+ const definition = schema.backends.find((item) => item.name === backend);
12
+ if (!definition)
13
+ throw new CredentialServiceError('datasource-defaults-backend-invalid');
14
+ for (const [name, value] of Object.entries(values)) {
15
+ const field = definition.fields.find((item) => item.name === name);
16
+ if (name.endsWith('_env'))
17
+ throw new CredentialServiceError('datasource-defaults-credential-forbidden');
18
+ if (!field)
19
+ throw new CredentialServiceError('datasource-defaults-field-invalid');
20
+ if (field.type !== 'json' && typeof value !== field.type)
21
+ throw new CredentialServiceError('datasource-defaults-type-invalid');
22
+ }
23
+ }
24
+ return { ...schema, creationDefaults: parsed.data };
25
+ }
@@ -1,5 +1,6 @@
1
1
  export { MarivoDatasourceBridge, resolveMarivoDatasourceBridge, resolveMarivoDatasourceInventoryBridge, } from "./bridge.js";
2
- export { registerMarivoPythonTool } from "./python.js";
2
+ export { createMarivoDatasourceConfigureTool, MARIVO_DATASOURCE_CONFIGURE_TOOL_NAME, registerMarivoDatasourceConfigureTool, } from "./configure.js";
3
+ export { MarivoPythonExecutionError, registerMarivoPythonTool } from "./python.js";
3
4
  export { MarivoCredentialService } from "./service.js";
4
5
  export { assertMarivoCredentialReferences, MARIVO_CREDENTIAL_STORAGE_PREFIX, marivoCredentialStorageRef, } from "./shell-env.js";
5
6
  export { createMarivoDatasourceTestTool, MARIVO_DATASOURCE_TEST_TOOL_NAME, registerMarivoDatasourceTestTool, } from "./test.js";
@@ -0,0 +1,15 @@
1
+ export const DEFAULT_PYTHON_TIMEOUT_MS = 120_000;
2
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
3
+ /** Do not echo rejected inputs: tool arguments and configuration are not trusted diagnostics. */
4
+ export function pythonTimeout(value, name) {
5
+ if (typeof value !== 'number' ||
6
+ !Number.isInteger(value) ||
7
+ value <= 0 ||
8
+ value > MAX_TIMER_DELAY_MS)
9
+ throw new Error(`${name} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`);
10
+ return value;
11
+ }
12
+ export function resolvePythonOptions(options) {
13
+ const pythonTimeoutMs = pythonTimeout(options.pythonTimeoutMs === undefined ? DEFAULT_PYTHON_TIMEOUT_MS : options.pythonTimeoutMs, 'pythonTimeoutMs');
14
+ return { pythonTimeoutMs };
15
+ }
@@ -1,29 +1,58 @@
1
+ import { performance } from 'node:perf_hooks';
1
2
  import process from 'node:process';
2
3
  import { defineTool } from '@deepseek-ai/dsh-tools';
3
4
  import { savePythonExecution } from "../python-execution.js";
5
+ import { registerMarivoTool } from "../tool-lifecycle.js";
4
6
  import { resolveMarivoDatasourceBridge } from "./bridge.js";
7
+ import { pythonTimeout, resolvePythonOptions, } from "./python-options.js";
5
8
  import { PYTHON_LAUNCHER, PYTHON_WORKER } from "./resolver-program.js";
9
+ import { CredentialServiceError, } from "./service.js";
6
10
  import { datasourceToolValue } from "./test.js";
11
+ /** The message survives Harness error normalization; it contains only safe adapter facts. */
12
+ export class MarivoPythonExecutionError extends Error {
13
+ execution;
14
+ constructor(message, execution) {
15
+ super(`${message}; execution=${JSON.stringify(execution)}`);
16
+ this.name = 'MarivoPythonExecutionError';
17
+ this.execution = execution;
18
+ }
19
+ }
20
+ const CHECK_EFFECTS = 'Inspect existing effects and the intended Session through current Runtime Help before retrying; do not automatically replay. Saved results and remote query cancellation are not confirmed.';
21
+ const PREPARATION_CODES = new Set([
22
+ 'agent-required',
23
+ 'invalid-datasources',
24
+ 'call-ended',
25
+ 'context-changed',
26
+ 'credentials-changed',
27
+ 'execution-ended',
28
+ 'disposed',
29
+ ]);
7
30
  function quote(value) {
8
31
  return process.platform === 'win32'
9
32
  ? `'${value.replaceAll("'", "''")}'`
10
33
  : `'${value.replaceAll("'", "'\\''")}'`;
11
34
  }
12
- export function registerMarivoPythonTool(ctx, source, service) {
13
- return ctx.tools.register(defineTool({
35
+ export function registerMarivoPythonTool(ctx, source, service, options = {}) {
36
+ const readOptions = typeof options === 'function' ? options : () => options;
37
+ resolvePythonOptions(readOptions());
38
+ return registerMarivoTool(ctx, defineTool({
14
39
  name: 'marivo_python',
15
- description: 'Execute foreground Python in the bound Marivo Workspace using Host-injected credentials. This call waits for missing credentials for every listed datasource before starting Python once; configured credentials do not trigger a connection test. Create/resume Sessions and readers inside this execution; close Sessions in finally. No background execution or secret environment variables. Nonzero exits are reported without replay. Successful execution saves the exact submitted code and returns codeRef; explicitly associate it with presentation datasets through draft codeRefs. A codeCaptureError does not change the execution outcome and must not trigger replay.',
40
+ description: 'Execute foreground Python in the bound Runtime and Workspace, including local pandas and native DuckDB file analysis. Waits for missing credentials for every declared datasource before starting code once; no automatic connection test or execution replay. No background execution or secret environment variables. Success returns codeRef for the submitted code; associate it with presentation datasets through draft codeRefs when needed. codeCaptureError leaves the execution outcome unchanged; do not rerun to recover the code record. Inspect execution phase, outcome and nextAction after failure; timeout does not prove remote query cancellation or absence of saved results.',
16
41
  parameters: {
17
42
  code: {
18
43
  type: 'string',
19
44
  required: true,
20
- description: 'Python code, never credential values. Use try/finally for per-call Session resource cleanup; consult current Runtime Help for Session APIs.',
45
+ description: 'Python code, never credential values. Close resources within this call. When using Marivo Session APIs, consult current Runtime Help and close Sessions in finally.',
21
46
  },
22
47
  datasources: {
23
48
  type: 'array',
24
49
  required: true,
25
50
  items: { type: 'string' },
26
- description: 'All exact datasource names this execution may access through data or metadata connections, including inspection and datasources without passwords; pass [] only when no datasource will be accessed. Catalog-only definition reads need no datasource connection.',
51
+ description: 'All exact Marivo datasource names this execution may access through data or metadata connections, including inspection and datasources without passwords. Pass [] for local pandas or native DuckDB file analysis without Marivo datasource access. Catalog-only definition reads need no datasource connection.',
52
+ },
53
+ timeoutMs: {
54
+ type: 'number',
55
+ description: 'Optional foreground Shell timeout in milliseconds, a positive integer at most 2147483647. Defaults to the current plugin pythonTimeoutMs setting. Credential waiting precedes this budget; Harness Shell limits, outer Code Mode deadlines and cancellation remain independent.',
27
56
  },
28
57
  },
29
58
  output: {
@@ -31,28 +60,62 @@ export function registerMarivoPythonTool(ctx, source, service) {
31
60
  render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
32
61
  },
33
62
  async execute(args, exec) {
34
- const code = args.code;
35
- if (!code.trim() || Buffer.byteLength(code) > 131072)
36
- throw new Error('Invalid Python code size');
37
- const shell = ctx.get('shell');
38
- if (!shell)
39
- throw new Error('DSH Shell execution service is required');
40
- const shellEnv = ctx.get('shellEnv');
41
- if (!shellEnv)
42
- throw new Error('DSH Shell environment service is required');
43
- const policyService = ctx.get('sandboxPolicy');
44
- if (shell.sandboxMode !== undefined && !policyService)
45
- throw new Error('DSH sandbox policy is required');
46
- const prepared = await service.track(service.prepareExecution(exec, () => resolveMarivoDatasourceBridge(source), args.datasources));
47
- if (!('status' in prepared) || prepared.status !== 'ready')
48
- return datasourceToolValue(prepared);
63
+ const calledAt = performance.now();
64
+ let phase = 'preparing';
65
+ let requestedTimeoutMs = null;
66
+ let effectiveTimeoutMs = null;
67
+ let shellStartedAt = null;
68
+ let executionElapsedMs = null;
69
+ let prepared;
70
+ const summary = (reason, nextAction) => {
71
+ const now = performance.now();
72
+ return {
73
+ phase,
74
+ reason,
75
+ requestedTimeoutMs,
76
+ effectiveTimeoutMs,
77
+ elapsedMs: now - calledAt,
78
+ executionElapsedMs: executionElapsedMs ?? (shellStartedAt === null ? null : now - shellStartedAt),
79
+ nextAction,
80
+ };
81
+ };
82
+ const rejectInput = (message) => {
83
+ throw new MarivoPythonExecutionError(message, summary('not-started', 'Correct the input or Host configuration before retrying.'));
84
+ };
49
85
  try {
86
+ try {
87
+ requestedTimeoutMs = pythonTimeout(args.timeoutMs === undefined
88
+ ? resolvePythonOptions(readOptions()).pythonTimeoutMs
89
+ : args.timeoutMs, 'timeoutMs');
90
+ }
91
+ catch {
92
+ rejectInput('timeoutMs must be a positive integer no greater than 2147483647');
93
+ }
94
+ const code = args.code;
95
+ if (typeof code !== 'string' || !code.trim() || Buffer.byteLength(code) > 131072)
96
+ rejectInput('Invalid Python code size');
97
+ const shell = ctx.get('shell');
98
+ if (!shell)
99
+ rejectInput('DSH Shell execution service is required');
100
+ const shellEnv = ctx.get('shellEnv');
101
+ if (!shellEnv)
102
+ rejectInput('DSH Shell environment service is required');
103
+ const policyService = ctx.get('sandboxPolicy');
104
+ if (shell.sandboxMode !== undefined && !policyService)
105
+ rejectInput('DSH sandbox policy is required');
106
+ const admission = await service.track(service.prepareExecution(exec, () => resolveMarivoDatasourceBridge(source), args.datasources));
107
+ if (!('status' in admission) || admission.status !== 'ready')
108
+ return {
109
+ ...datasourceToolValue(admission),
110
+ execution: summary('not-started', 'Resolve the datasource credential requirement or reported connection failure before retrying.'),
111
+ };
112
+ prepared = admission;
50
113
  const { binding } = prepared.bridge;
51
114
  const policy = policyService?.resolve(exec.agent ? { session: exec.agent.session } : {});
52
115
  const spec = shell.resolve({
53
116
  command: `${process.platform === 'win32' ? '& ' : ''}${quote(binding.pythonExecutable)} -c ${quote(PYTHON_LAUNCHER)}`,
54
117
  workdir: binding.projectRoot,
55
- timeoutMs: 120_000,
118
+ timeoutMs: requestedTimeoutMs,
56
119
  signal: prepared.signal,
57
120
  stdin: JSON.stringify({
58
121
  identity: binding,
@@ -66,19 +129,25 @@ export function registerMarivoPythonTool(ctx, source, service) {
66
129
  dshEnv: shellEnv.collect(exec),
67
130
  ...(policy ? { sandboxPolicy: policy } : {}),
68
131
  });
132
+ effectiveTimeoutMs = spec.timeoutMs;
69
133
  // Keep this guard adjacent to the only launch, after synchronous Host hooks.
70
134
  prepared.assertCurrent();
71
135
  const startedAt = new Date().toISOString();
136
+ phase = 'executing';
137
+ shellStartedAt = performance.now();
72
138
  const result = await service.track(shell.run(spec));
139
+ executionElapsedMs = performance.now() - shellStartedAt;
73
140
  const finishedAt = new Date().toISOString();
141
+ const credentialValues = prepared.values;
74
142
  // Defense at the result seam as well as before Harness collection/spill.
75
- const redact = (text) => Object.values(prepared.values)
143
+ const redact = (text) => Object.values(credentialValues)
76
144
  .filter(Boolean)
77
145
  .sort((a, b) => b.length - a.length)
78
146
  .reduce((out, value) => out.split(value).join('[REDACTED]'), text);
79
147
  let codeRef;
80
148
  let codeCaptureError;
81
149
  if (result.exitCode === 0 && !result.timedOut && !result.aborted) {
150
+ phase = 'capturing-code';
82
151
  try {
83
152
  prepared.assertCurrent();
84
153
  if (Object.values(prepared.values).some((value) => value && code.includes(value)))
@@ -92,6 +161,15 @@ export function registerMarivoPythonTool(ctx, source, service) {
92
161
  'Python completed successfully, but its source code could not be saved. Do not rerun the code to recover this snapshot; inspect its existing effects.';
93
162
  }
94
163
  }
164
+ const reason = result.timedOut
165
+ ? 'timed-out'
166
+ : result.aborted
167
+ ? 'cancelled'
168
+ : result.exitCode === 0
169
+ ? 'succeeded'
170
+ : result.exitCode === null
171
+ ? 'unknown'
172
+ : 'nonzero-exit';
95
173
  return JSON.parse(JSON.stringify({
96
174
  exitCode: result.exitCode,
97
175
  timedOut: result.timedOut,
@@ -102,13 +180,29 @@ export function registerMarivoPythonTool(ctx, source, service) {
102
180
  sandbox: result.sandbox ?? null,
103
181
  ...(codeRef ? { codeRef } : {}),
104
182
  ...(codeCaptureError ? { codeCaptureError } : {}),
183
+ execution: summary(reason, codeCaptureError ?? (reason === 'succeeded' ? null : CHECK_EFFECTS)),
105
184
  }));
106
185
  }
107
- catch {
108
- throw new Error('Marivo Python execution failed or was cancelled; inspect the execution policy and retry only after checking effects');
186
+ catch (error) {
187
+ if (error instanceof MarivoPythonExecutionError)
188
+ throw error;
189
+ const cancelled = (prepared?.signal ?? exec.signal).aborted;
190
+ const reason = cancelled
191
+ ? 'cancelled'
192
+ : shellStartedAt === null
193
+ ? 'not-started'
194
+ : 'unknown';
195
+ const code = error instanceof CredentialServiceError && PREPARATION_CODES.has(error.code)
196
+ ? ` (${error.code})`
197
+ : '';
198
+ throw new MarivoPythonExecutionError(shellStartedAt === null
199
+ ? `Marivo Python preparation failed or was cancelled; Python was not started${code}`
200
+ : 'Marivo Python execution outcome is unknown after an execution service failure or cancellation', summary(reason, shellStartedAt === null
201
+ ? 'Check credentials, Workspace binding and Host execution policy before retrying.'
202
+ : CHECK_EFFECTS));
109
203
  }
110
204
  finally {
111
- prepared.release();
205
+ prepared?.release();
112
206
  }
113
207
  },
114
208
  }));