@chengxianglibra/dsh-data-analysis 0.1.1 → 0.1.2

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 (195) hide show
  1. package/README.md +83 -34
  2. package/lib/bin/environment.js +1 -1
  3. package/lib/bin/presentation-lint.js +21 -0
  4. package/lib/bridges.js +2 -2
  5. package/lib/client/credentials/create-datasource.js +67 -0
  6. package/lib/client/credentials/install.js +191 -0
  7. package/lib/client/credentials/model.js +494 -0
  8. package/lib/client/credentials/styles.js +17 -0
  9. package/lib/client/workspace-header-action.js +11 -0
  10. package/lib/client.js +1133 -736
  11. package/lib/compatibility.js +1 -1
  12. package/lib/datasource/authoring.js +70 -0
  13. package/lib/datasource/bridge-programs.js +30 -18
  14. package/lib/datasource/bridge.js +100 -8
  15. package/lib/datasource/index.js +3 -2
  16. package/lib/datasource/python.js +115 -0
  17. package/lib/datasource/resolver-program.js +112 -0
  18. package/lib/datasource/rpc.js +88 -0
  19. package/lib/datasource/service.js +687 -0
  20. package/lib/datasource/shell-env.js +2 -333
  21. package/lib/datasource/test.js +24 -51
  22. package/lib/environment/binding.js +54 -9
  23. package/lib/environment/runtime.js +89 -78
  24. package/lib/environment/subprocess.js +6 -1
  25. package/lib/environment/summary.js +3 -3
  26. package/lib/environment/workspace.js +5 -0
  27. package/lib/index.js +1 -1
  28. package/lib/plugin.js +168 -51
  29. package/lib/presentation/assets/portable.js +606 -0
  30. package/lib/presentation/assets/static.js +945 -0
  31. package/lib/presentation/build/index.js +71 -0
  32. package/lib/presentation/commit.js +156 -0
  33. package/lib/presentation/contracts/charts.js +476 -0
  34. package/lib/presentation/contracts/editing.js +58 -0
  35. package/lib/presentation/contracts/index.js +651 -0
  36. package/lib/presentation/contracts/interaction.js +62 -0
  37. package/lib/presentation/contracts/types.js +15 -0
  38. package/lib/presentation/contracts/values.js +28 -0
  39. package/lib/presentation/delivery.js +56 -0
  40. package/lib/presentation/files.js +83 -0
  41. package/lib/presentation/index.js +6 -0
  42. package/lib/presentation/lint.js +46 -0
  43. package/lib/presentation/projection/files.js +85 -0
  44. package/lib/presentation/projection/index.js +195 -0
  45. package/lib/presentation/projection/program.js +144 -0
  46. package/lib/presentation/projection/sql-code.js +79 -0
  47. package/lib/presentation/receipt.js +34 -0
  48. package/lib/presentation/reports.js +122 -0
  49. package/lib/presentation/rpc.js +148 -0
  50. package/lib/presentation/tool.js +122 -0
  51. package/lib/python-execution-contracts.js +1 -0
  52. package/lib/python-execution.js +232 -0
  53. package/lib/semantic-browser/contracts.js +80 -0
  54. package/lib/semantic-browser/definition.js +64 -0
  55. package/lib/semantic-browser/program.js +119 -0
  56. package/lib/semantic-browser/service.js +72 -0
  57. package/lib/semantic-reference/bridge.js +118 -0
  58. package/lib/semantic-reference/contracts.js +113 -0
  59. package/lib/semantic-reference/index.js +3 -0
  60. package/lib/semantic-reference/rpc.js +122 -0
  61. package/lib/semantic-reference/search.js +94 -0
  62. package/lib/semantic-reference/usage.js +143 -0
  63. package/lib/types/bridges.d.ts +2 -2
  64. package/lib/types/client/credentials/create-datasource.d.ts +5 -0
  65. package/lib/types/client/credentials/install.d.ts +1 -0
  66. package/lib/types/client/credentials/model.d.ts +59 -0
  67. package/lib/types/client/credentials/styles.d.ts +1 -0
  68. package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
  69. package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
  70. package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
  71. package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
  72. package/lib/types/client/presentation/chart-specials.d.ts +10 -0
  73. package/lib/types/client/presentation/chart-styles.d.ts +1 -0
  74. package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
  75. package/lib/types/client/presentation/chart-view.d.ts +23 -0
  76. package/lib/types/client/presentation/copy-context.d.ts +5 -0
  77. package/lib/types/client/presentation/delivery-model.d.ts +54 -0
  78. package/lib/types/client/presentation/delivery.d.ts +77 -0
  79. package/lib/types/client/presentation/editor-controls.d.ts +12 -0
  80. package/lib/types/client/presentation/global-filters.d.ts +6 -0
  81. package/lib/types/client/presentation/host-entry.d.ts +12 -0
  82. package/lib/types/client/presentation/icons.d.ts +4 -0
  83. package/lib/types/client/presentation/install.d.ts +15 -0
  84. package/lib/types/client/presentation/markdown.d.ts +5 -0
  85. package/lib/types/client/presentation/model.d.ts +62 -0
  86. package/lib/types/client/presentation/reader.d.ts +11 -0
  87. package/lib/types/client/presentation/source-code-model.d.ts +16 -0
  88. package/lib/types/client/presentation/source-code.d.ts +11 -0
  89. package/lib/types/client/presentation/source-dialog.d.ts +13 -0
  90. package/lib/types/client/presentation/source-facts.d.ts +20 -0
  91. package/lib/types/client/presentation/source-format.d.ts +5 -0
  92. package/lib/types/client/presentation/source-highlight.d.ts +7 -0
  93. package/lib/types/client/presentation/sources.d.ts +13 -0
  94. package/lib/types/client/presentation/styles.d.ts +1 -0
  95. package/lib/types/client/presentation/table.d.ts +12 -0
  96. package/lib/types/client/presentation/use-element-width.d.ts +5 -0
  97. package/lib/types/client/workspace-header-action.d.ts +7 -0
  98. package/lib/types/client.d.ts +4 -122
  99. package/lib/types/compatibility.d.ts +2 -2
  100. package/lib/types/datasource/authoring.d.ts +25 -0
  101. package/lib/types/datasource/bridge.d.ts +21 -3
  102. package/lib/types/datasource/index.d.ts +3 -5
  103. package/lib/types/datasource/python.d.ts +4 -0
  104. package/lib/types/datasource/resolver-program.d.ts +6 -0
  105. package/lib/types/datasource/rpc.d.ts +4 -0
  106. package/lib/types/datasource/service.d.ts +112 -0
  107. package/lib/types/datasource/shell-env.d.ts +0 -29
  108. package/lib/types/datasource/test.d.ts +6 -29
  109. package/lib/types/environment/binding.d.ts +5 -1
  110. package/lib/types/environment/errors.d.ts +1 -1
  111. package/lib/types/environment/runtime.d.ts +5 -5
  112. package/lib/types/environment/types.d.ts +13 -2
  113. package/lib/types/index.d.ts +1 -1
  114. package/lib/types/plugin.d.ts +11 -5
  115. package/lib/types/presentation/build/index.d.ts +11 -0
  116. package/lib/types/presentation/commit.d.ts +4 -0
  117. package/lib/types/presentation/contracts/charts.d.ts +13 -0
  118. package/lib/types/presentation/contracts/editing.d.ts +8 -0
  119. package/lib/types/presentation/contracts/index.d.ts +11 -0
  120. package/lib/types/presentation/contracts/interaction.d.ts +8 -0
  121. package/lib/types/presentation/contracts/types.d.ts +252 -0
  122. package/lib/types/presentation/contracts/values.d.ts +9 -0
  123. package/lib/types/presentation/delivery.d.ts +3 -0
  124. package/lib/types/presentation/files.d.ts +7 -0
  125. package/lib/types/presentation/index.d.ts +6 -0
  126. package/lib/types/presentation/lint.d.ts +13 -0
  127. package/lib/types/presentation/projection/files.d.ts +2 -0
  128. package/lib/types/presentation/projection/index.d.ts +20 -0
  129. package/lib/types/presentation/projection/program.d.ts +2 -0
  130. package/lib/types/presentation/projection/sql-code.d.ts +2 -0
  131. package/lib/types/presentation/receipt.d.ts +14 -0
  132. package/lib/types/presentation/reports.d.ts +5 -0
  133. package/lib/types/presentation/rpc.d.ts +24 -0
  134. package/lib/types/presentation/tool.d.ts +12 -0
  135. package/lib/types/python-execution-contracts.d.ts +10 -0
  136. package/lib/types/python-execution.d.ts +10 -0
  137. package/lib/types/semantic-browser/contracts.d.ts +40 -0
  138. package/lib/types/semantic-browser/definition.d.ts +20 -0
  139. package/lib/types/semantic-browser/program.d.ts +2 -0
  140. package/lib/types/semantic-browser/service.d.ts +20 -0
  141. package/lib/types/semantic-reference/bridge.d.ts +11 -0
  142. package/lib/types/semantic-reference/contracts.d.ts +51 -0
  143. package/lib/types/semantic-reference/index.d.ts +3 -0
  144. package/lib/types/semantic-reference/rpc.d.ts +15 -0
  145. package/lib/types/semantic-reference/search.d.ts +10 -0
  146. package/lib/types/semantic-reference/usage.d.ts +43 -0
  147. package/package.json +43 -19
  148. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  149. package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
  150. package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
  151. package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
  152. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
  153. package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
  154. package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
  155. package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
  156. package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
  157. package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
  158. package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
  159. package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
  160. package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
  161. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
  162. package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
  163. package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
  164. package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
  165. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
  166. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
  167. package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
  168. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
  169. package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
  170. package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
  171. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
  172. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
  173. package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
  174. package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
  175. package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
  176. package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
  177. package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
  178. package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
  179. package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
  180. package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
  181. package/lib/datasource/access.js +0 -60
  182. package/lib/datasource/credentials.js +0 -24
  183. package/lib/evidence/bridge-program.js +0 -153
  184. package/lib/evidence/bridge.js +0 -166
  185. package/lib/evidence/index.js +0 -2
  186. package/lib/evidence/sources.js +0 -263
  187. package/lib/types/datasource/access.d.ts +0 -27
  188. package/lib/types/evidence/bridge.d.ts +0 -40
  189. package/lib/types/evidence/index.d.ts +0 -4
  190. package/lib/types/evidence/sources.d.ts +0 -57
  191. package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
  192. package/skills/dsh-data-analysis-report/SKILL.md +0 -57
  193. package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
  194. package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
  195. package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
@@ -0,0 +1,144 @@
1
+ import { MARIVO_PRESENTATION_SQL_CODE_PROGRAM } from "./sql-code.js";
2
+ /** Fixed public read program. Drafts contain references and data paths, never executable code. */
3
+ export const MARIVO_PRESENTATION_READ_PROGRAM = String.raw `
4
+ import json
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ import marivo.analysis as mv
10
+ import marivo.datasource as md
11
+ from dsh_data_analysis_presentation._dataset import encode_dataset
12
+
13
+
14
+ class ProjectionFailure(Exception):
15
+ def __init__(self, code, path, message):
16
+ self.code, self.path, self.message = code, path, message
17
+
18
+
19
+ class DenyCredentials:
20
+ def resolve(self, request):
21
+ raise ProjectionFailure("credential_read_forbidden", "/sources", "Persisted presentation reads cannot obtain datasource credentials.")
22
+
23
+
24
+ def text(value, limit=32768):
25
+ result = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
26
+ result = result.replace("\0", "\\u0000")
27
+ return result if len(result) <= limit else result[:limit - 14] + " [truncated]"
28
+
29
+
30
+ ${MARIVO_PRESENTATION_SQL_CODE_PROGRAM}
31
+
32
+
33
+ def read(request):
34
+ sessions, artifacts, contracts = {}, {}, {}
35
+ sources, datasets, diagnostics = [], [], []
36
+ definition_sources = []
37
+ read_at = datetime.now(timezone.utc).isoformat()
38
+ try:
39
+ for index, declared in enumerate(request["sources"]):
40
+ ref = declared["ref"]
41
+ source_path = "/sources/" + str(index)
42
+ try:
43
+ session = sessions.get(ref["sessionId"])
44
+ if session is None:
45
+ session = mv.session.resume(ref["sessionId"], use_datasources=False)
46
+ sessions[ref["sessionId"]] = session
47
+ if session.id != ref["sessionId"]:
48
+ raise ProjectionFailure("source_identity_mismatch", source_path + "/ref/sessionId", "Restored Session identity differs from the declared source.")
49
+ artifact = session.artifact(ref["artifactRef"])
50
+ contract = artifact.contract()
51
+ meta = artifact.meta
52
+ if artifact.ref != ref["artifactRef"] or meta.session_id != session.id or contract.ref != artifact.ref:
53
+ raise ProjectionFailure("source_identity_mismatch", source_path + "/ref", "Restored Artifact identity differs from the declared source.")
54
+ if Path(meta.project_root).resolve() != Path.cwd().resolve():
55
+ raise ProjectionFailure("source_workspace_mismatch", source_path + "/ref", "Restored Artifact belongs to a different Workspace.")
56
+ except ProjectionFailure:
57
+ raise
58
+ except Exception:
59
+ # Only the requested source becomes unavailable. Exception messages may
60
+ # 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."})
62
+ continue
63
+
64
+ artifacts[declared["id"]], contracts[declared["id"]] = artifact, contract
65
+ facts = []
66
+ def fact(label, value):
67
+ facts.append({"label": label, "value": text(value)})
68
+ fact("读取时间", read_at)
69
+ fact("Artifact kind", meta.kind)
70
+ fact("创建时间", meta.created_at.isoformat())
71
+ fact("内容身份", meta.content_hash)
72
+ fact("Evidence status", meta.evidence_status)
73
+ fact("完整行数", meta.row_count)
74
+ fact("公开语义引用", [{"role": item.role, "kind": item.semantic_kind.value, "path": item.semantic_path, "outputColumn": item.output_column} for item in contract.semantic_inputs])
75
+ quality = meta.quality_summary
76
+ fact("Quality", None if quality is None else {"coverage": quality.coverage, "nullRate": quality.null_rate, "sampleSize": quality.sample_size, "evaluatedCheckCount": quality.evaluated_check_count, "failedCheckCount": quality.failed_check_count, "warningCheckCount": quality.warning_check_count})
77
+ fact("Issues", [{"kind": item.kind, "severity": item.severity} for item in contract.issues])
78
+ if "findingId" in ref:
79
+ try:
80
+ finding = artifact.finding(ref["findingId"])
81
+ if finding.finding_id != ref["findingId"] or finding.artifact_ref != artifact.ref or finding.session_id != session.id:
82
+ raise ProjectionFailure("source_identity_mismatch", source_path + "/ref/findingId", "Restored Finding identity differs from the declared source.")
83
+ excerpt = finding.render(max_output_bytes=4096)
84
+ fact("选择的 Finding", excerpt)
85
+ except ProjectionFailure:
86
+ raise
87
+ except Exception:
88
+ fact("选择的 Finding unavailable", "The requested Finding cannot be read through the persisted Artifact public API.")
89
+ 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
+ fact("revalidation", "not_requested")
94
+ sources.append({**declared, "status": "available", "label": text(str(meta.kind) + " " + artifact.ref, 512), "facts": facts, "code": sql_code_snapshot(session, artifact)})
95
+
96
+ if definition_sources:
97
+ diagnostics.append({"code": "definition_unavailable", "path": "/sources", "message": "信息:" + str(len(definition_sources)) + " 个指标来源的 Artifact 公开契约未提供生成时的指标定义快照,来源概要无法展示当时的指标含义与口径。不影响已保存数据、图表及已有代码的展示,正常阅读无需处理;核验历史口径需补充生成时的定义快照,当前语义层定义不能替代历史定义。"})
98
+
99
+ for selection in request["datasets"]:
100
+ dataset_path = "/datasets/" + str(selection["index"])
101
+ artifact = artifacts.get(selection["sourceId"])
102
+ if artifact is None:
103
+ raise ProjectionFailure("artifact_data_unavailable", dataset_path + "/sourceId", "A direct Artifact dataset requires an available persisted Artifact source.")
104
+ try:
105
+ contract = contracts[selection["sourceId"]]
106
+ frame = artifact.to_pandas()
107
+ public_columns = list(contract.artifact_schema.columns)
108
+ if list(frame.columns) != [column.name for column in public_columns] or len(frame) != artifact.meta.row_count or artifact.row_count != len(frame):
109
+ raise ProjectionFailure("artifact_data_identity_mismatch", dataset_path, "Artifact rows or columns disagree with its public contract.")
110
+ names = selection.get("columns", list(frame.columns))
111
+ by_name = {column.name: column for column in public_columns}
112
+ if any(name not in by_name for name in names):
113
+ raise ProjectionFailure("invalid_reference", dataset_path + "/columns", "A requested column is absent from the public Artifact schema.")
114
+ # Use public terminal values; declared upstream dtypes cannot recover
115
+ # Decimal precision already lost by to_pandas(). No derived metrics.
116
+ data = encode_dataset(frame.loc[:, names], row_limit=selection["rowLimit"])
117
+ for column in data["columns"]:
118
+ column["nullable"] = by_name[column["id"]].nullable
119
+ datasets.append({"id": selection["id"], "data": data})
120
+ except ProjectionFailure:
121
+ raise
122
+ except Exception as error:
123
+ # Preserve helper contract locations without leaking upstream error text.
124
+ code = getattr(error, "code", None)
125
+ location = getattr(error, "path", None)
126
+ if code in ("numeric_precision", "budget", "invalid_value") and isinstance(location, str):
127
+ raise ProjectionFailure(code, dataset_path + "/data" + location, "Public Artifact values cannot be encoded within the presentation data contract.") from None
128
+ raise ProjectionFailure("artifact_data_unavailable", dataset_path + "/data", "The direct Artifact does not expose the required typed presentation data.") from None
129
+ return {"ok": True, "sources": sources, "datasets": datasets, "diagnostics": diagnostics}
130
+ finally:
131
+ for session in sessions.values():
132
+ session.close()
133
+
134
+
135
+ try:
136
+ request = json.loads(sys.argv[1])
137
+ with md.credential_scope(resolver=DenyCredentials()):
138
+ result = read(request)
139
+ except ProjectionFailure as error:
140
+ result = {"ok": False, "error": {"code": error.code, "path": error.path, "message": error.message}}
141
+ except Exception:
142
+ result = {"ok": False, "error": {"code": "projection_read_failed", "path": "/sources", "message": "The bound public presentation read failed."}}
143
+ print(json.dumps(result, ensure_ascii=False, allow_nan=False, separators=(",", ":")))
144
+ `.trim();
@@ -0,0 +1,79 @@
1
+ /** Only persisted public producer records are read; this program never compiles or executes SQL. */
2
+ export const MARIVO_PRESENTATION_SQL_CODE_PROGRAM = String.raw `
3
+ def sql_code_snapshot(session, artifact):
4
+ snippets, notices = [], []
5
+ def notice(message):
6
+ if message not in notices:
7
+ notices.append(message)
8
+ def valid_string(value, limit):
9
+ return isinstance(value, str) and bool(value) and "\0" not in value and len(value.encode("utf-16-le")) // 2 <= limit
10
+
11
+ try:
12
+ succeeded_run = mv.SucceededRun
13
+ get_run = session.get_run
14
+ except Exception:
15
+ return {"snippets": [], "notices": ["当前 Marivo 未提供读取持久化 SQL 所需的公开 Run 能力。"]}
16
+
17
+ pending = [(artifact.ref, artifact)]
18
+ scheduled, runs, seen_queries = {artifact.ref}, {}, set()
19
+ query_count = 0
20
+ for expected_ref, known_artifact in pending:
21
+ try:
22
+ current = known_artifact if known_artifact is not None else session.artifact(expected_ref)
23
+ meta, contract = current.meta, current.contract()
24
+ if current.ref != expected_ref or contract.ref != expected_ref or meta.session_id != session.id or Path(meta.project_root).resolve() != Path.cwd().resolve():
25
+ notice("部分上游 Artifact 的身份或 Workspace 不一致,已拒绝读取其 SQL。")
26
+ continue
27
+ producer = meta.produced_by_job
28
+ if producer is None:
29
+ notice("部分 Artifact 未保存生产 Run 标识,无法读取其生成 SQL。")
30
+ continue
31
+ if not valid_string(producer, 512):
32
+ notice("部分 Artifact 的生产 Run 标识无效,已拒绝读取其 SQL。")
33
+ continue
34
+ run = runs.get(producer)
35
+ if run is None:
36
+ run = get_run(producer)
37
+ runs[producer] = run
38
+ if not isinstance(run, succeeded_run) or run.run_id != producer or run.output_mode != "produced" or run.output_artifact_ref != expected_ref:
39
+ notice("部分生产 Run 的身份、完成状态或产出不匹配,已拒绝读取其 SQL。")
40
+ continue
41
+ for query in run.queries:
42
+ if query_count >= 32:
43
+ notice("SQL 快照最多读取 32 条查询记录,其余记录未纳入。")
44
+ break
45
+ query_count += 1
46
+ query_id = query.query_id
47
+ if not valid_string(query_id, 512):
48
+ notice("部分 SQL 记录的查询标识无效,未纳入代码快照。")
49
+ continue
50
+ query_key = (producer, query_id)
51
+ if query_key in seen_queries:
52
+ continue
53
+ seen_queries.add(query_key)
54
+ sql = query.sql
55
+ if not valid_string(sql, 32768) or not sql.strip():
56
+ notice("部分 SQL 为空、包含不支持的字符或超过 32768 字符,未截断也未纳入快照。")
57
+ continue
58
+ snippets.append({"language": "sql", "text": sql, "provenance": "execution", "runId": producer, "queryId": query_id, "artifactRef": expected_ref})
59
+ for input_index, input_ref in enumerate(run.input_artifact_refs):
60
+ if input_index >= 65:
61
+ notice("SQL 追溯最多读取 64 个上游 Artifact,其余来源未纳入。")
62
+ break
63
+ if not valid_string(input_ref, 512):
64
+ notice("部分上游 Artifact 标识无效,已停止该分支的 SQL 追溯。")
65
+ continue
66
+ if input_ref in scheduled:
67
+ continue
68
+ if len(scheduled) >= 65:
69
+ notice("SQL 追溯最多读取 64 个上游 Artifact,其余来源未纳入。")
70
+ break
71
+ scheduled.add(input_ref)
72
+ pending.append((input_ref, None))
73
+ except Exception:
74
+ # A missing record or capability affects code only. Never return exception text.
75
+ notice("部分持久化生产记录无法通过当前 Workspace 的公开 API 读取,SQL 快照可能不完整。")
76
+ if not snippets and not notices:
77
+ notice("该 Artifact 及已读取的上游生产记录未保存 SQL。")
78
+ return {"snippets": snippets, "notices": notices}
79
+ `.trim();
@@ -0,0 +1,34 @@
1
+ /** Pure Host delivery envelope shared by the server and browser. */
2
+ import { parsePresentationReceipt } from "./contracts/index.js";
3
+ export const MARIVO_PRESENT_TOOL_NAME = 'marivo_present';
4
+ export const MARIVO_PRESENTATION_DELIVERY_KIND = 'marivo.presentation.delivery';
5
+ export const MARIVO_PRESENTATION_RPC_CHANNEL = '/marivo-presentation';
6
+ export function parsePresentationDelivery(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value))
8
+ throw new Error('invalid-presentation-delivery');
9
+ const input = value;
10
+ if (Object.keys(input).sort().join(',') !== 'dshSessionId,kind,receipt,schemaVersion,turn' ||
11
+ input.kind !== MARIVO_PRESENTATION_DELIVERY_KIND ||
12
+ input.schemaVersion !== 2 ||
13
+ typeof input.dshSessionId !== 'string' ||
14
+ !input.dshSessionId.trim() ||
15
+ input.dshSessionId.length > 512 ||
16
+ !Number.isSafeInteger(input.turn) ||
17
+ input.turn < 0)
18
+ throw new Error('invalid-presentation-delivery');
19
+ parsePresentationReceipt(input.receipt);
20
+ return input;
21
+ }
22
+ export function presentationReceiptText(receipt) {
23
+ return [
24
+ receipt.title,
25
+ receipt.summary,
26
+ `Workspace: ${receipt.workspaceId}`,
27
+ `Report: ${receipt.reportId}`,
28
+ `Build: ${receipt.buildId}`,
29
+ ...Object.values(receipt.files).flatMap((file) => [
30
+ `${file.asset}: ${file.path}`,
31
+ `sha256: ${file.sha256}; bytes: ${file.bytes}`,
32
+ ]),
33
+ ].join('\n');
34
+ }
@@ -0,0 +1,122 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, open, realpath, rename, rm } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { withFileLock } from '@deepseek-ai/dsh-atomic-write';
6
+ import { buildPresentation } from "./build/index.js";
7
+ import { commitPresentation } from "./commit.js";
8
+ import { parsePresentationDocument, parsePresentationReceipt, } from "./contracts/index.js";
9
+ import { presentationAssetPath, presentationReportPath, presentationSha256, readPresentationAsset, readReportFile, } from "./files.js";
10
+ export async function readReceiptDocument(root, receipt, signal) {
11
+ for (const asset of Object.values(receipt.files)) {
12
+ if (asset.path !== presentationAssetPath(root, receipt.reportId, receipt.buildId, asset.asset))
13
+ throw new Error('asset-path-mismatch');
14
+ }
15
+ const bytes = await readPresentationAsset(root, receipt.reportId, receipt.buildId, 'presentation.json', signal);
16
+ if (bytes.length !== receipt.files.document.bytes ||
17
+ presentationSha256(bytes) !== receipt.files.document.sha256)
18
+ throw new Error('asset-digest-mismatch');
19
+ const document = parsePresentationDocument(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
20
+ if (document.workspaceId !== receipt.workspaceId ||
21
+ document.reportId !== receipt.reportId ||
22
+ document.buildId !== receipt.buildId ||
23
+ document.title !== receipt.title)
24
+ throw new Error('asset-owner-mismatch');
25
+ return document;
26
+ }
27
+ export async function resolvePresentation(root, workspaceId, reportId, signal) {
28
+ const bytes = await readReportFile(root, path.join(presentationReportPath(root, reportId), 'current.json'), 20 * 1024, signal);
29
+ const current = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
30
+ if (!current ||
31
+ Object.keys(current).sort().join(',') !== 'receipt,reportId,schemaVersion,workspaceId' ||
32
+ current.schemaVersion !== 2 ||
33
+ current.workspaceId !== workspaceId ||
34
+ current.reportId !== reportId)
35
+ throw new Error('invalid-report-current');
36
+ const receipt = parsePresentationReceipt(current.receipt);
37
+ if (receipt.workspaceId !== workspaceId || receipt.reportId !== reportId)
38
+ throw new Error('asset-owner-mismatch');
39
+ await readReceiptDocument(root, receipt, signal);
40
+ return receipt;
41
+ }
42
+ /** The mutable pointer is the publication boundary; completed builds are never overwritten. */
43
+ export async function publishPresentation(root, document, expectedBuildId, assertOwner, signal) {
44
+ await assertOwner();
45
+ signal?.throwIfAborted();
46
+ const built = await buildPresentation(document);
47
+ await assertOwner();
48
+ const receipt = await commitPresentation(root, built, assertOwner, signal);
49
+ const directory = presentationReportPath(root, document.reportId);
50
+ const currentPath = path.join(directory, 'current.json');
51
+ const parents = [root];
52
+ for (const segment of path.relative(root, directory).split(path.sep))
53
+ parents.push(path.join(parents.at(-1), segment));
54
+ const identities = await Promise.all(parents.map((parent) => lstat(parent)));
55
+ const check = async () => {
56
+ signal?.throwIfAborted();
57
+ await assertOwner();
58
+ const actual = await Promise.all(parents.map((parent) => lstat(parent)));
59
+ if ((await realpath(directory)) !== directory ||
60
+ actual.some((entry, index) => !entry.isDirectory() ||
61
+ entry.isSymbolicLink() ||
62
+ entry.dev !== identities[index].dev ||
63
+ entry.ino !== identities[index].ino))
64
+ throw new Error('presentation-directory-changed');
65
+ };
66
+ await check();
67
+ await withFileLock(currentPath, async () => {
68
+ await check();
69
+ let current;
70
+ try {
71
+ current = await resolvePresentation(root, document.workspaceId, document.reportId, signal);
72
+ }
73
+ catch (error) {
74
+ if (expectedBuildId !== null || error.code !== 'ENOENT')
75
+ throw error;
76
+ // ENOENT is allowed only for an absent pointer, never a dangling current build.
77
+ try {
78
+ await lstat(currentPath);
79
+ throw new Error('invalid-report-current');
80
+ }
81
+ catch (missing) {
82
+ if (missing.code !== 'ENOENT')
83
+ throw missing;
84
+ }
85
+ }
86
+ if ((current?.buildId ?? null) !== expectedBuildId)
87
+ throw new Error('report-save-conflict');
88
+ const temporary = path.join(directory, `.current-${randomUUID()}.tmp`);
89
+ let published = false;
90
+ try {
91
+ const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
92
+ try {
93
+ await file.writeFile(JSON.stringify({
94
+ schemaVersion: 2,
95
+ workspaceId: document.workspaceId,
96
+ reportId: document.reportId,
97
+ receipt,
98
+ }), { signal });
99
+ await file.sync();
100
+ }
101
+ finally {
102
+ await file.close();
103
+ }
104
+ await check();
105
+ await rename(temporary, currentPath);
106
+ published = true;
107
+ const parent = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
108
+ try {
109
+ await parent.sync();
110
+ }
111
+ finally {
112
+ await parent.close();
113
+ }
114
+ // Cancellation after the rename cannot undo a published save.
115
+ }
116
+ finally {
117
+ if (!published)
118
+ await rm(temporary, { force: true });
119
+ }
120
+ }, { waitMs: 5000 });
121
+ return receipt;
122
+ }
@@ -0,0 +1,148 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { applyPresentationEdits } from "./contracts/editing.js";
4
+ import { parsePresentationBuildId, parsePresentationDocument, parsePresentationReceipt, } from "./contracts/index.js";
5
+ import { presentationAssetPath, presentationSha256, readPresentationAsset } from "./files.js";
6
+ import { MARIVO_PRESENTATION_RPC_CHANNEL } from "./receipt.js";
7
+ import { publishPresentation, readReceiptDocument, resolvePresentation } from "./reports.js";
8
+ export class MarivoPresentationFileService {
9
+ #resolve;
10
+ #abort = new AbortController();
11
+ constructor(resolve) {
12
+ this.#resolve = resolve;
13
+ }
14
+ async #workspace(sessionId) {
15
+ try {
16
+ return await this.#resolve(sessionId);
17
+ }
18
+ catch {
19
+ throw new Error('workspace-unavailable');
20
+ }
21
+ }
22
+ close() {
23
+ this.#abort.abort();
24
+ }
25
+ async report(endpoint, payload, caller = new AbortController().signal) {
26
+ const signal = AbortSignal.any([caller, this.#abort.signal]);
27
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
28
+ throw new Error('invalid-request');
29
+ const input = payload;
30
+ const expected = endpoint === 'reports/save'
31
+ ? 'edits,expectedBuildId,reportId,sessionId'
32
+ : 'reportId,sessionId';
33
+ if (Object.keys(input).sort().join(',') !== expected ||
34
+ typeof input.sessionId !== 'string' ||
35
+ !input.sessionId.trim() ||
36
+ input.sessionId.length > 512)
37
+ throw new Error('invalid-request');
38
+ const reportId = parsePresentationBuildId(input.reportId, '/reportId');
39
+ const workspace = await this.#workspace(input.sessionId);
40
+ if (!workspace)
41
+ throw new Error('workspace-unavailable');
42
+ const root = path.resolve(workspace.path);
43
+ const check = async () => {
44
+ signal.throwIfAborted();
45
+ const current = await this.#workspace(input.sessionId);
46
+ if (!current || current.id !== workspace.id || path.resolve(current.path) !== root)
47
+ throw new Error('workspace-changed');
48
+ };
49
+ await check();
50
+ const receipt = await resolvePresentation(root, workspace.id, reportId, signal);
51
+ await check();
52
+ if (endpoint === 'reports/resolve')
53
+ return receipt;
54
+ const expectedBuildId = parsePresentationBuildId(input.expectedBuildId);
55
+ if (receipt.buildId !== expectedBuildId)
56
+ throw new Error('report-save-conflict');
57
+ const base = await readReceiptDocument(root, receipt, signal);
58
+ let edited;
59
+ try {
60
+ edited = applyPresentationEdits(base, input.edits);
61
+ }
62
+ catch {
63
+ throw new Error('invalid-report-edits');
64
+ }
65
+ await check();
66
+ return publishPresentation(root, { ...edited, buildId: randomUUID(), generatedAt: new Date().toISOString() }, expectedBuildId, check, signal);
67
+ }
68
+ async read(payload, caller = new AbortController().signal) {
69
+ const signal = AbortSignal.any([caller, this.#abort.signal]);
70
+ signal.throwIfAborted();
71
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
72
+ throw new Error('invalid-request');
73
+ const input = payload;
74
+ if (Object.keys(input).sort().join(',') !== 'asset,receipt,sessionId' ||
75
+ typeof input.sessionId !== 'string' ||
76
+ !input.sessionId.trim() ||
77
+ input.sessionId.length > 512 ||
78
+ (input.asset !== 'presentation.json' && input.asset !== 'index.html'))
79
+ throw new Error('invalid-request');
80
+ const receipt = structuredClone(parsePresentationReceipt(input.receipt));
81
+ const asset = input.asset;
82
+ const workspace = await this.#workspace(input.sessionId);
83
+ if (!workspace || workspace.id !== receipt.workspaceId)
84
+ throw new Error('workspace-unavailable');
85
+ const root = path.resolve(workspace.path);
86
+ for (const file of Object.values(receipt.files))
87
+ if (file.path !== presentationAssetPath(root, receipt.reportId, receipt.buildId, file.asset))
88
+ throw new Error('asset-path-mismatch');
89
+ const documentBytes = await readPresentationAsset(root, receipt.reportId, receipt.buildId, 'presentation.json', signal);
90
+ if (documentBytes.length !== receipt.files.document.bytes ||
91
+ presentationSha256(documentBytes) !== receipt.files.document.sha256)
92
+ throw new Error('asset-digest-mismatch');
93
+ const document = parsePresentationDocument(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(documentBytes)));
94
+ if (document.workspaceId !== workspace.id ||
95
+ document.reportId !== receipt.reportId ||
96
+ document.buildId !== receipt.buildId ||
97
+ document.title !== receipt.title)
98
+ throw new Error('asset-owner-mismatch');
99
+ const bytes = asset === 'presentation.json'
100
+ ? documentBytes
101
+ : await readPresentationAsset(root, receipt.reportId, receipt.buildId, asset, signal);
102
+ const file = asset === 'presentation.json' ? receipt.files.document : receipt.files.html;
103
+ if (bytes.length !== file.bytes || presentationSha256(bytes) !== file.sha256)
104
+ throw new Error('asset-digest-mismatch');
105
+ const current = await this.#workspace(input.sessionId);
106
+ if (!current || current.id !== workspace.id || path.resolve(current.path) !== root)
107
+ throw new Error('workspace-changed');
108
+ signal.throwIfAborted();
109
+ return {
110
+ workspaceId: workspace.id,
111
+ reportId: receipt.reportId,
112
+ buildId: receipt.buildId,
113
+ asset,
114
+ mimeType: asset === 'presentation.json' ? 'application/json' : 'text/html',
115
+ sha256: file.sha256,
116
+ bytes: bytes.length,
117
+ bodyBase64: bytes.toString('base64'),
118
+ };
119
+ }
120
+ }
121
+ export function registerMarivoPresentationRpc(connection, service) {
122
+ const unregister = connection.rpc.handle(MARIVO_PRESENTATION_RPC_CHANNEL, async (endpoint, payload, signal) => {
123
+ try {
124
+ if (endpoint === 'reports/resolve' || endpoint === 'reports/save')
125
+ return { ok: true, value: await service.report(endpoint, payload, signal) };
126
+ if (endpoint === 'files/read')
127
+ return { ok: true, value: await service.read(payload, signal) };
128
+ throw new Error('unknown-endpoint');
129
+ }
130
+ catch (error) {
131
+ const message = signal.aborted
132
+ ? 'cancelled'
133
+ : error?.code === 'ENOENT'
134
+ ? 'asset-missing'
135
+ : error instanceof Error &&
136
+ /^(invalid-request|workspace-unavailable|workspace-changed|asset-path-mismatch|asset-not-file|asset-too-large|asset-changed|asset-digest-mismatch|asset-owner-mismatch|unknown-endpoint|invalid-report-current|invalid-report-edits|report-save-conflict|presentation-directory-changed)$/.test(error.message)
137
+ ? error.message
138
+ : error instanceof Error && /lock.*timed out|timed out.*lock/i.test(error.message)
139
+ ? 'report-save-busy'
140
+ : 'presentation-operation-failed';
141
+ return { ok: false, error: { code: 'internal', message, details: {} } };
142
+ }
143
+ }, { authority: 'trusted-host' });
144
+ return async () => {
145
+ service.close();
146
+ await unregister();
147
+ };
148
+ }
@@ -0,0 +1,122 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { parsePresentationBuildId } from "./contracts/index.js";
4
+ import { MARIVO_PRESENT_TOOL_NAME, MARIVO_PRESENTATION_DELIVERY_KIND, parsePresentationDelivery, presentationReceiptText, } from "./receipt.js";
5
+ import { publishPresentation, resolvePresentation } from "./reports.js";
6
+ /** One call projects a draft, builds both assets, commits them, then emits the receipt. */
7
+ export function createMarivoPresentTool(source, session, lifecycleSignal) {
8
+ return defineTool({
9
+ name: MARIVO_PRESENT_TOOL_NAME,
10
+ description: 'Present a complete Workspace-relative analysis draft as one saved document and self-contained offline HTML. Omit report_id and expected_build_id to create a Report; provide both to replace an existing Report with a new immutable Build based on the version you have read. A report-save-conflict requires reading the current report and reconciling the draft; never retry by only changing expected_build_id. Returns the exact published receipt, file locations, and a reader card. Only saved Artifact and computed snapshots are read; sources are declarations, not verification of calculations.',
11
+ parameters: {
12
+ draft_path: {
13
+ type: 'string',
14
+ required: true,
15
+ description: 'Workspace-relative path of the complete presentation draft JSON. Updates replace the whole report content; omitted content is not preserved.',
16
+ },
17
+ report_id: {
18
+ type: 'string',
19
+ description: 'Existing Report ID in the current Workspace to update. Must be provided together with expected_build_id; omit both to create a new Report.',
20
+ },
21
+ expected_build_id: {
22
+ type: 'string',
23
+ description: 'Build ID of the report version you have read and based this complete draft on. Must be provided together with report_id. A stale version fails with report-save-conflict; read and reconcile the current report before retrying.',
24
+ },
25
+ },
26
+ output: {
27
+ schema: {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ properties: { deliveryJson: { type: 'string', required: true } },
31
+ },
32
+ render: (_args, value) => [
33
+ {
34
+ type: 'text',
35
+ text: presentationReceiptText(parsePresentationDelivery(JSON.parse(value.deliveryJson)).receipt),
36
+ },
37
+ ],
38
+ presentationMeta: (_args, value) => JSON.parse(value.deliveryJson),
39
+ },
40
+ timeoutMs: 120_000,
41
+ async execute(args, exec) {
42
+ const signal = lifecycleSignal ? AbortSignal.any([exec.signal, lifecycleSignal]) : exec.signal;
43
+ if (typeof args.draft_path !== 'string' ||
44
+ args.draft_path.length === 0 ||
45
+ args.draft_path.length > 4096)
46
+ throw new Error('invalid-draft-path');
47
+ const hasReportId = args.report_id !== undefined;
48
+ const hasExpectedBuildId = args.expected_build_id !== undefined;
49
+ if (hasReportId !== hasExpectedBuildId)
50
+ throw new Error('invalid-report-update-target');
51
+ const reportId = hasReportId
52
+ ? parsePresentationBuildId(args.report_id, '/report_id')
53
+ : randomUUID();
54
+ const expectedBuildId = hasExpectedBuildId
55
+ ? parsePresentationBuildId(args.expected_build_id, '/expected_build_id')
56
+ : null;
57
+ const owner = exec.agent;
58
+ if (!owner || owner.session !== session)
59
+ throw new Error('presentation-session-mismatch');
60
+ const rootCallId = String(exec.rootCallId ?? exec.callId);
61
+ const call = [...session.events]
62
+ .reverse()
63
+ .find((event) => event.type === 'tool/call' && String(event.data.callId) === rootCallId);
64
+ if (call?.type !== 'tool/call' || !Number.isSafeInteger(call.data.turn) || call.data.turn < 0)
65
+ throw new Error('presentation-turn-unavailable');
66
+ signal.throwIfAborted();
67
+ const binding = await source();
68
+ const identity = {
69
+ workspaceId: binding.workspaceId,
70
+ projectRoot: binding.projection.binding.projectRoot,
71
+ fingerprint: binding.projection.binding.fingerprint,
72
+ };
73
+ const check = async () => {
74
+ signal.throwIfAborted();
75
+ if (owner.session !== session)
76
+ throw new Error('presentation-session-mismatch');
77
+ const current = await source();
78
+ if (current.projection.status !== 'ready' ||
79
+ current.workspaceId !== identity.workspaceId ||
80
+ current.projection.binding.projectRoot !== identity.projectRoot ||
81
+ current.projection.binding.fingerprint !== identity.fingerprint ||
82
+ current.projection.binding !== binding.projection.binding)
83
+ throw new Error('presentation-workspace-changed');
84
+ signal.throwIfAborted();
85
+ };
86
+ await check();
87
+ if (expectedBuildId !== null) {
88
+ const current = await resolvePresentation(identity.projectRoot, identity.workspaceId, reportId, signal);
89
+ await check();
90
+ if (current.buildId !== expectedBuildId)
91
+ throw new Error('report-save-conflict');
92
+ }
93
+ const draft = await binding.projection.readDraft(args.draft_path, signal);
94
+ await check();
95
+ const document = await binding.projection.project(draft, {
96
+ workspaceId: binding.workspaceId,
97
+ reportId,
98
+ buildId: randomUUID(),
99
+ signal,
100
+ });
101
+ await check();
102
+ const receipt = await publishPresentation(identity.projectRoot, document, expectedBuildId, check, signal);
103
+ return {
104
+ deliveryJson: JSON.stringify(parsePresentationDelivery({
105
+ kind: MARIVO_PRESENTATION_DELIVERY_KIND,
106
+ schemaVersion: 2,
107
+ dshSessionId: String(session.id),
108
+ turn: call.data.turn,
109
+ receipt,
110
+ })),
111
+ };
112
+ },
113
+ });
114
+ }
115
+ export function registerMarivoPresentTool(ctx, source, session) {
116
+ const controller = new AbortController();
117
+ const unregister = ctx.tools.register(createMarivoPresentTool(source, session, controller.signal));
118
+ return () => {
119
+ controller.abort();
120
+ unregister();
121
+ };
122
+ }
@@ -0,0 +1 @@
1
+ export {};