@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.
- package/README.md +83 -34
- package/lib/bin/environment.js +1 -1
- package/lib/bin/presentation-lint.js +21 -0
- package/lib/bridges.js +2 -2
- package/lib/client/credentials/create-datasource.js +67 -0
- package/lib/client/credentials/install.js +191 -0
- package/lib/client/credentials/model.js +494 -0
- package/lib/client/credentials/styles.js +17 -0
- package/lib/client/workspace-header-action.js +11 -0
- package/lib/client.js +1133 -736
- package/lib/compatibility.js +1 -1
- package/lib/datasource/authoring.js +70 -0
- package/lib/datasource/bridge-programs.js +30 -18
- package/lib/datasource/bridge.js +100 -8
- package/lib/datasource/index.js +3 -2
- package/lib/datasource/python.js +115 -0
- package/lib/datasource/resolver-program.js +112 -0
- package/lib/datasource/rpc.js +88 -0
- package/lib/datasource/service.js +687 -0
- package/lib/datasource/shell-env.js +2 -333
- package/lib/datasource/test.js +24 -51
- package/lib/environment/binding.js +54 -9
- package/lib/environment/runtime.js +89 -78
- package/lib/environment/subprocess.js +6 -1
- package/lib/environment/summary.js +3 -3
- package/lib/environment/workspace.js +5 -0
- package/lib/index.js +1 -1
- package/lib/plugin.js +168 -51
- package/lib/presentation/assets/portable.js +606 -0
- package/lib/presentation/assets/static.js +945 -0
- package/lib/presentation/build/index.js +71 -0
- package/lib/presentation/commit.js +156 -0
- package/lib/presentation/contracts/charts.js +476 -0
- package/lib/presentation/contracts/editing.js +58 -0
- package/lib/presentation/contracts/index.js +651 -0
- package/lib/presentation/contracts/interaction.js +62 -0
- package/lib/presentation/contracts/types.js +15 -0
- package/lib/presentation/contracts/values.js +28 -0
- package/lib/presentation/delivery.js +56 -0
- package/lib/presentation/files.js +83 -0
- package/lib/presentation/index.js +6 -0
- package/lib/presentation/lint.js +46 -0
- package/lib/presentation/projection/files.js +85 -0
- package/lib/presentation/projection/index.js +195 -0
- package/lib/presentation/projection/program.js +144 -0
- package/lib/presentation/projection/sql-code.js +79 -0
- package/lib/presentation/receipt.js +34 -0
- package/lib/presentation/reports.js +122 -0
- package/lib/presentation/rpc.js +148 -0
- package/lib/presentation/tool.js +122 -0
- package/lib/python-execution-contracts.js +1 -0
- package/lib/python-execution.js +232 -0
- package/lib/semantic-browser/contracts.js +80 -0
- package/lib/semantic-browser/definition.js +64 -0
- package/lib/semantic-browser/program.js +119 -0
- package/lib/semantic-browser/service.js +72 -0
- package/lib/semantic-reference/bridge.js +118 -0
- package/lib/semantic-reference/contracts.js +113 -0
- package/lib/semantic-reference/index.js +3 -0
- package/lib/semantic-reference/rpc.js +122 -0
- package/lib/semantic-reference/search.js +94 -0
- package/lib/semantic-reference/usage.js +143 -0
- package/lib/types/bridges.d.ts +2 -2
- package/lib/types/client/credentials/create-datasource.d.ts +5 -0
- package/lib/types/client/credentials/install.d.ts +1 -0
- package/lib/types/client/credentials/model.d.ts +59 -0
- package/lib/types/client/credentials/styles.d.ts +1 -0
- package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
- package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
- package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
- package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
- package/lib/types/client/presentation/chart-specials.d.ts +10 -0
- package/lib/types/client/presentation/chart-styles.d.ts +1 -0
- package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
- package/lib/types/client/presentation/chart-view.d.ts +23 -0
- package/lib/types/client/presentation/copy-context.d.ts +5 -0
- package/lib/types/client/presentation/delivery-model.d.ts +54 -0
- package/lib/types/client/presentation/delivery.d.ts +77 -0
- package/lib/types/client/presentation/editor-controls.d.ts +12 -0
- package/lib/types/client/presentation/global-filters.d.ts +6 -0
- package/lib/types/client/presentation/host-entry.d.ts +12 -0
- package/lib/types/client/presentation/icons.d.ts +4 -0
- package/lib/types/client/presentation/install.d.ts +15 -0
- package/lib/types/client/presentation/markdown.d.ts +5 -0
- package/lib/types/client/presentation/model.d.ts +62 -0
- package/lib/types/client/presentation/reader.d.ts +11 -0
- package/lib/types/client/presentation/source-code-model.d.ts +16 -0
- package/lib/types/client/presentation/source-code.d.ts +11 -0
- package/lib/types/client/presentation/source-dialog.d.ts +13 -0
- package/lib/types/client/presentation/source-facts.d.ts +20 -0
- package/lib/types/client/presentation/source-format.d.ts +5 -0
- package/lib/types/client/presentation/source-highlight.d.ts +7 -0
- package/lib/types/client/presentation/sources.d.ts +13 -0
- package/lib/types/client/presentation/styles.d.ts +1 -0
- package/lib/types/client/presentation/table.d.ts +12 -0
- package/lib/types/client/presentation/use-element-width.d.ts +5 -0
- package/lib/types/client/workspace-header-action.d.ts +7 -0
- package/lib/types/client.d.ts +4 -122
- package/lib/types/compatibility.d.ts +2 -2
- package/lib/types/datasource/authoring.d.ts +25 -0
- package/lib/types/datasource/bridge.d.ts +21 -3
- package/lib/types/datasource/index.d.ts +3 -5
- package/lib/types/datasource/python.d.ts +4 -0
- package/lib/types/datasource/resolver-program.d.ts +6 -0
- package/lib/types/datasource/rpc.d.ts +4 -0
- package/lib/types/datasource/service.d.ts +112 -0
- package/lib/types/datasource/shell-env.d.ts +0 -29
- package/lib/types/datasource/test.d.ts +6 -29
- package/lib/types/environment/binding.d.ts +5 -1
- package/lib/types/environment/errors.d.ts +1 -1
- package/lib/types/environment/runtime.d.ts +5 -5
- package/lib/types/environment/types.d.ts +13 -2
- package/lib/types/index.d.ts +1 -1
- package/lib/types/plugin.d.ts +11 -5
- package/lib/types/presentation/build/index.d.ts +11 -0
- package/lib/types/presentation/commit.d.ts +4 -0
- package/lib/types/presentation/contracts/charts.d.ts +13 -0
- package/lib/types/presentation/contracts/editing.d.ts +8 -0
- package/lib/types/presentation/contracts/index.d.ts +11 -0
- package/lib/types/presentation/contracts/interaction.d.ts +8 -0
- package/lib/types/presentation/contracts/types.d.ts +252 -0
- package/lib/types/presentation/contracts/values.d.ts +9 -0
- package/lib/types/presentation/delivery.d.ts +3 -0
- package/lib/types/presentation/files.d.ts +7 -0
- package/lib/types/presentation/index.d.ts +6 -0
- package/lib/types/presentation/lint.d.ts +13 -0
- package/lib/types/presentation/projection/files.d.ts +2 -0
- package/lib/types/presentation/projection/index.d.ts +20 -0
- package/lib/types/presentation/projection/program.d.ts +2 -0
- package/lib/types/presentation/projection/sql-code.d.ts +2 -0
- package/lib/types/presentation/receipt.d.ts +14 -0
- package/lib/types/presentation/reports.d.ts +5 -0
- package/lib/types/presentation/rpc.d.ts +24 -0
- package/lib/types/presentation/tool.d.ts +12 -0
- package/lib/types/python-execution-contracts.d.ts +10 -0
- package/lib/types/python-execution.d.ts +10 -0
- package/lib/types/semantic-browser/contracts.d.ts +40 -0
- package/lib/types/semantic-browser/definition.d.ts +20 -0
- package/lib/types/semantic-browser/program.d.ts +2 -0
- package/lib/types/semantic-browser/service.d.ts +20 -0
- package/lib/types/semantic-reference/bridge.d.ts +11 -0
- package/lib/types/semantic-reference/contracts.d.ts +51 -0
- package/lib/types/semantic-reference/index.d.ts +3 -0
- package/lib/types/semantic-reference/rpc.d.ts +15 -0
- package/lib/types/semantic-reference/search.d.ts +10 -0
- package/lib/types/semantic-reference/usage.d.ts +43 -0
- package/package.json +43 -19
- package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
- package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
- package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
- package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
- package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
- package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
- package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
- package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
- package/lib/datasource/access.js +0 -60
- package/lib/datasource/credentials.js +0 -24
- package/lib/evidence/bridge-program.js +0 -153
- package/lib/evidence/bridge.js +0 -166
- package/lib/evidence/index.js +0 -2
- package/lib/evidence/sources.js +0 -263
- package/lib/types/datasource/access.d.ts +0 -27
- package/lib/types/evidence/bridge.d.ts +0 -40
- package/lib/types/evidence/index.d.ts +0 -4
- package/lib/types/evidence/sources.d.ts +0 -57
- package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-report/SKILL.md +0 -57
- package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
- package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
- 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 {};
|