@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,232 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { link, lstat, mkdir, open, realpath, unlink } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
|
|
6
|
+
const CODE_BYTES = 131072;
|
|
7
|
+
const RECORD_BYTES = 262144;
|
|
8
|
+
const EXECUTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
9
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
10
|
+
function invalid() {
|
|
11
|
+
throw new Error('Python execution source is unavailable or invalid');
|
|
12
|
+
}
|
|
13
|
+
function same(left, right) {
|
|
14
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
15
|
+
}
|
|
16
|
+
async function directory(filename) {
|
|
17
|
+
const stat = await lstat(filename);
|
|
18
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (await realpath(filename)) !== filename)
|
|
19
|
+
invalid();
|
|
20
|
+
return stat;
|
|
21
|
+
}
|
|
22
|
+
async function directories(workspaceRoot, create, signal) {
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
const root = await realpath(workspaceRoot);
|
|
25
|
+
if (root !== path.resolve(workspaceRoot))
|
|
26
|
+
invalid();
|
|
27
|
+
const workspaceIdentity = await directory(root);
|
|
28
|
+
const hostRoot = path.resolve(resolveDshHome());
|
|
29
|
+
const paths = [
|
|
30
|
+
hostRoot,
|
|
31
|
+
path.join(hostRoot, 'dsh-data-analysis'),
|
|
32
|
+
path.join(hostRoot, 'dsh-data-analysis', 'python-executions'),
|
|
33
|
+
path.join(hostRoot, 'dsh-data-analysis', 'python-executions', sha256(Buffer.from(root))),
|
|
34
|
+
];
|
|
35
|
+
const identities = [];
|
|
36
|
+
const check = async (checkSignal = true) => {
|
|
37
|
+
if (checkSignal)
|
|
38
|
+
signal?.throwIfAborted();
|
|
39
|
+
if (!same(await directory(root), workspaceIdentity) ||
|
|
40
|
+
path.resolve(resolveDshHome()) !== hostRoot)
|
|
41
|
+
invalid();
|
|
42
|
+
for (const [index, expected] of identities.entries())
|
|
43
|
+
if (!same(await directory(paths[index]), expected))
|
|
44
|
+
invalid();
|
|
45
|
+
if (checkSignal)
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
};
|
|
48
|
+
for (const [index, filename] of paths.entries()) {
|
|
49
|
+
await check();
|
|
50
|
+
if (create && index > 0) {
|
|
51
|
+
try {
|
|
52
|
+
await mkdir(filename, { mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code !== 'EEXIST')
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
identities.push(await directory(filename));
|
|
60
|
+
}
|
|
61
|
+
await check();
|
|
62
|
+
return { root, parent: paths[3], check };
|
|
63
|
+
}
|
|
64
|
+
function sourceText(text) {
|
|
65
|
+
return (typeof text === 'string' &&
|
|
66
|
+
!!text.trim() &&
|
|
67
|
+
!text.includes('\0') &&
|
|
68
|
+
!/[\uD800-\uDFFF]/u.test(text) &&
|
|
69
|
+
Buffer.byteLength(text) <= CODE_BYTES);
|
|
70
|
+
}
|
|
71
|
+
function timestamp(value) {
|
|
72
|
+
return (typeof value === 'string' &&
|
|
73
|
+
Number.isFinite(Date.parse(value)) &&
|
|
74
|
+
new Date(value).toISOString() === value);
|
|
75
|
+
}
|
|
76
|
+
function sha256(bytes) {
|
|
77
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
78
|
+
}
|
|
79
|
+
/** Publish a complete record once. Its final name never exposes a partial write or overwrites. */
|
|
80
|
+
export async function savePythonExecution(workspaceRoot, input, signal) {
|
|
81
|
+
try {
|
|
82
|
+
signal?.throwIfAborted();
|
|
83
|
+
const { text, startedAt, finishedAt } = input;
|
|
84
|
+
if (!sourceText(text) ||
|
|
85
|
+
!timestamp(startedAt) ||
|
|
86
|
+
!timestamp(finishedAt) ||
|
|
87
|
+
finishedAt < startedAt)
|
|
88
|
+
invalid();
|
|
89
|
+
const workspace = await directories(workspaceRoot, true, signal);
|
|
90
|
+
const executionId = randomUUID();
|
|
91
|
+
const bytes = Buffer.from(JSON.stringify({
|
|
92
|
+
schemaVersion: 1,
|
|
93
|
+
executionId,
|
|
94
|
+
projectRoot: workspace.root,
|
|
95
|
+
language: 'python',
|
|
96
|
+
text,
|
|
97
|
+
startedAt,
|
|
98
|
+
finishedAt,
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
}));
|
|
101
|
+
if (bytes.length > RECORD_BYTES)
|
|
102
|
+
invalid();
|
|
103
|
+
const ref = { executionId, sha256: sha256(bytes) };
|
|
104
|
+
const temporary = path.join(workspace.parent, `.${executionId}.pending`);
|
|
105
|
+
const final = path.join(workspace.parent, `${executionId}.json`);
|
|
106
|
+
await workspace.check();
|
|
107
|
+
const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
108
|
+
const identity = await file.stat();
|
|
109
|
+
let published = false;
|
|
110
|
+
try {
|
|
111
|
+
await file.writeFile(bytes, { signal });
|
|
112
|
+
await file.sync();
|
|
113
|
+
await workspace.check();
|
|
114
|
+
if (!same(await lstat(temporary), identity))
|
|
115
|
+
invalid();
|
|
116
|
+
// Hard-link publication is atomic and fails if a final path already exists.
|
|
117
|
+
await link(temporary, final);
|
|
118
|
+
published = true;
|
|
119
|
+
await workspace.check();
|
|
120
|
+
if (!same(await lstat(final), identity))
|
|
121
|
+
invalid();
|
|
122
|
+
const verified = await readPythonExecution(workspace.root, ref, signal);
|
|
123
|
+
if (verified.text !== text)
|
|
124
|
+
invalid();
|
|
125
|
+
await workspace.check();
|
|
126
|
+
return ref;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (published) {
|
|
130
|
+
try {
|
|
131
|
+
await workspace.check(false);
|
|
132
|
+
if (same(await lstat(final), identity))
|
|
133
|
+
await unlink(final);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* Do not remove a replaced path or hide the original error. */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await file.close();
|
|
143
|
+
try {
|
|
144
|
+
await workspace.check(false);
|
|
145
|
+
if (same(await lstat(temporary), identity))
|
|
146
|
+
await unlink(temporary);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* Cleanup cannot override successful execution or follow a replaced parent. */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
invalid();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Resolve Host-stored execution identities for the bound Workspace. Never executes code. */
|
|
159
|
+
export async function readPythonExecution(workspaceRoot, ref, signal) {
|
|
160
|
+
try {
|
|
161
|
+
signal?.throwIfAborted();
|
|
162
|
+
if (!ref || !EXECUTION_ID.test(ref.executionId) || !DIGEST.test(ref.sha256))
|
|
163
|
+
invalid();
|
|
164
|
+
const { executionId, sha256: expectedDigest } = ref;
|
|
165
|
+
const workspace = await directories(workspaceRoot, false, signal);
|
|
166
|
+
const filename = path.join(workspace.parent, `${executionId}.json`);
|
|
167
|
+
const expected = await lstat(filename);
|
|
168
|
+
if (!expected.isFile() || expected.isSymbolicLink() || (await realpath(filename)) !== filename)
|
|
169
|
+
invalid();
|
|
170
|
+
const file = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
171
|
+
try {
|
|
172
|
+
const before = await file.stat();
|
|
173
|
+
if (!before.isFile() || !same(before, expected) || before.size > RECORD_BYTES)
|
|
174
|
+
invalid();
|
|
175
|
+
const bytes = Buffer.alloc(RECORD_BYTES + 1);
|
|
176
|
+
let length = 0;
|
|
177
|
+
while (length < bytes.length) {
|
|
178
|
+
signal?.throwIfAborted();
|
|
179
|
+
const { bytesRead } = await file.read(bytes, length, bytes.length - length, null);
|
|
180
|
+
if (!bytesRead)
|
|
181
|
+
break;
|
|
182
|
+
length += bytesRead;
|
|
183
|
+
}
|
|
184
|
+
const after = await file.stat();
|
|
185
|
+
const current = await lstat(filename);
|
|
186
|
+
await workspace.check();
|
|
187
|
+
if (length > RECORD_BYTES ||
|
|
188
|
+
length !== after.size ||
|
|
189
|
+
!same(current, after) ||
|
|
190
|
+
!current.isFile() ||
|
|
191
|
+
before.size !== after.size ||
|
|
192
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
193
|
+
before.ctimeMs !== after.ctimeMs ||
|
|
194
|
+
(await realpath(filename)) !== filename)
|
|
195
|
+
invalid();
|
|
196
|
+
const raw = bytes.subarray(0, length);
|
|
197
|
+
if (sha256(raw) !== expectedDigest)
|
|
198
|
+
invalid();
|
|
199
|
+
const record = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw));
|
|
200
|
+
if (!record ||
|
|
201
|
+
typeof record !== 'object' ||
|
|
202
|
+
Array.isArray(record) ||
|
|
203
|
+
Object.keys(record).sort().join(',') !==
|
|
204
|
+
'executionId,exitCode,finishedAt,language,projectRoot,schemaVersion,startedAt,text' ||
|
|
205
|
+
record.schemaVersion !== 1 ||
|
|
206
|
+
record.executionId !== executionId ||
|
|
207
|
+
record.projectRoot !== workspace.root ||
|
|
208
|
+
record.language !== 'python' ||
|
|
209
|
+
record.exitCode !== 0 ||
|
|
210
|
+
!sourceText(record.text) ||
|
|
211
|
+
!timestamp(record.startedAt) ||
|
|
212
|
+
!timestamp(record.finishedAt) ||
|
|
213
|
+
record.finishedAt < record.startedAt)
|
|
214
|
+
invalid();
|
|
215
|
+
signal?.throwIfAborted();
|
|
216
|
+
return {
|
|
217
|
+
language: 'python',
|
|
218
|
+
text: record.text,
|
|
219
|
+
provenance: 'execution',
|
|
220
|
+
executionId,
|
|
221
|
+
sha256: expectedDigest,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
await file.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
signal?.throwIfAborted();
|
|
230
|
+
invalid();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { boundedText, closed, parseRef, refKey, } from "../semantic-reference/contracts.js";
|
|
2
|
+
import { parseComputation } from "./definition.js";
|
|
3
|
+
export const BROWSER_ENDPOINT = 'semantic-browser/catalog';
|
|
4
|
+
export const CATALOG_MAX_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
function array(value, max) {
|
|
6
|
+
if (!Array.isArray(value) || value.length > max)
|
|
7
|
+
throw new Error('invalid-catalog');
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function text(value) {
|
|
11
|
+
return boundedText(value, CATALOG_MAX_BYTES, true);
|
|
12
|
+
}
|
|
13
|
+
function nullable(value) {
|
|
14
|
+
return value === null ? null : text(value);
|
|
15
|
+
}
|
|
16
|
+
export function parseCatalogProjection(value) {
|
|
17
|
+
const r = closed(value, ['fingerprint', 'kinds', 'objects']);
|
|
18
|
+
const kinds = array(r.kinds, 128).map((item) => boundedText(item, 128));
|
|
19
|
+
const objects = array(r.objects, 100_000).map((item) => {
|
|
20
|
+
const row = closed(item, [
|
|
21
|
+
'ref',
|
|
22
|
+
'computation',
|
|
23
|
+
'name',
|
|
24
|
+
'domain',
|
|
25
|
+
'definition',
|
|
26
|
+
'guardrails',
|
|
27
|
+
'source',
|
|
28
|
+
'fields',
|
|
29
|
+
'relations',
|
|
30
|
+
]);
|
|
31
|
+
const ref = parseRef(row.ref);
|
|
32
|
+
if (!kinds.includes(ref.kind))
|
|
33
|
+
throw new Error('invalid-catalog');
|
|
34
|
+
const source = closed(row.source, ['file', 'line', 'symbol']);
|
|
35
|
+
if (!Number.isSafeInteger(source.line) || source.line < 0)
|
|
36
|
+
throw new Error('invalid-catalog');
|
|
37
|
+
return {
|
|
38
|
+
ref,
|
|
39
|
+
computation: parseComputation(row.computation, ref, boundedText(r.fingerprint, 256)),
|
|
40
|
+
name: text(row.name),
|
|
41
|
+
domain: nullable(row.domain),
|
|
42
|
+
definition: nullable(row.definition),
|
|
43
|
+
guardrails: array(row.guardrails, 100_000).map(text),
|
|
44
|
+
source: { file: text(source.file), line: source.line, symbol: text(source.symbol) },
|
|
45
|
+
fields: array(row.fields, 128).map((value) => {
|
|
46
|
+
const field = closed(value, ['name', 'value']);
|
|
47
|
+
return { name: boundedText(field.name, 128), value: text(field.value) };
|
|
48
|
+
}),
|
|
49
|
+
relations: array(row.relations, 100_000).map((value) => {
|
|
50
|
+
const relation = closed(value, ['field', 'ref']);
|
|
51
|
+
return { field: boundedText(relation.field, 128), ref: parseRef(relation.ref) };
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
if (new Set(kinds).size !== kinds.length ||
|
|
56
|
+
new Set(objects.map((x) => refKey(x.ref))).size !== objects.length)
|
|
57
|
+
throw new Error('invalid-catalog');
|
|
58
|
+
return { fingerprint: boundedText(r.fingerprint, 256), kinds, objects };
|
|
59
|
+
}
|
|
60
|
+
export function parseCatalogSnapshot(value) {
|
|
61
|
+
const r = closed(value, [
|
|
62
|
+
'workspaceId',
|
|
63
|
+
'projectRoot',
|
|
64
|
+
'environmentFingerprint',
|
|
65
|
+
'loadedAt',
|
|
66
|
+
'fingerprint',
|
|
67
|
+
'kinds',
|
|
68
|
+
'objects',
|
|
69
|
+
]);
|
|
70
|
+
const loadedAt = boundedText(r.loadedAt, 64);
|
|
71
|
+
if (!Number.isFinite(Date.parse(loadedAt)))
|
|
72
|
+
throw new Error('invalid-catalog');
|
|
73
|
+
return {
|
|
74
|
+
...parseCatalogProjection({ fingerprint: r.fingerprint, kinds: r.kinds, objects: r.objects }),
|
|
75
|
+
workspaceId: boundedText(r.workspaceId, 256),
|
|
76
|
+
projectRoot: boundedText(r.projectRoot, 8192),
|
|
77
|
+
environmentFingerprint: boundedText(r.environmentFingerprint, 256),
|
|
78
|
+
loadedAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { closed, parseRef, refKey } from "../semantic-reference/contracts.js";
|
|
2
|
+
// Transport allowlist for the public definition payload, not a semantic validator.
|
|
3
|
+
const keys = new Set('kind operation q target target_kind filter dimension operator values value weight numerator denominator terms sign metric base over selection ref resolution anchor count unit grain calendar level status expression reason entity name value_type operand data_type left right condition when_true when_false declared override effective source fold schema path display language form text bindings alias redacted_literals'.split(' '));
|
|
4
|
+
function value(input, depth = 0, budget = { left: 10000 }) {
|
|
5
|
+
if (--budget.left < 0 || depth > 64)
|
|
6
|
+
throw new Error('definition-too-large');
|
|
7
|
+
if (input === null || typeof input === 'boolean')
|
|
8
|
+
return input;
|
|
9
|
+
if (typeof input === 'string' && input.length <= 1024 * 1024)
|
|
10
|
+
return input;
|
|
11
|
+
if (typeof input === 'number' && Number.isFinite(input))
|
|
12
|
+
return input;
|
|
13
|
+
if (Array.isArray(input))
|
|
14
|
+
return input.map((x) => value(x, depth + 1, budget));
|
|
15
|
+
if (typeof input !== 'object' || input === null)
|
|
16
|
+
throw new Error('invalid-definition');
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const [key, item] of Object.entries(input)) {
|
|
19
|
+
if (!keys.has(key))
|
|
20
|
+
throw new Error('invalid-definition-field');
|
|
21
|
+
result[key] = value(item, depth + 1, budget);
|
|
22
|
+
}
|
|
23
|
+
if (result.schema === 'marivo.semantic_ref/v1')
|
|
24
|
+
parseRef(result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
export function parseComputation(input, ref, fingerprint) {
|
|
28
|
+
if (input === null)
|
|
29
|
+
return null;
|
|
30
|
+
const r = closed(input, [
|
|
31
|
+
'schema',
|
|
32
|
+
'ref',
|
|
33
|
+
'catalog_definition_fingerprint',
|
|
34
|
+
'node',
|
|
35
|
+
'temporal',
|
|
36
|
+
'source_location',
|
|
37
|
+
]);
|
|
38
|
+
if (r.schema !== 'marivo.semantic_definition/v1' ||
|
|
39
|
+
refKey(parseRef(r.ref)) !== refKey(ref) ||
|
|
40
|
+
r.catalog_definition_fingerprint !== fingerprint)
|
|
41
|
+
throw new Error('definition-identity-mismatch');
|
|
42
|
+
const source = closed(r.source_location, ['file', 'line']);
|
|
43
|
+
if (typeof source.file !== 'string' ||
|
|
44
|
+
!Number.isSafeInteger(source.line) ||
|
|
45
|
+
source.line < 0)
|
|
46
|
+
throw new Error('invalid-definition-source');
|
|
47
|
+
const node = value(r.node), temporal = value(r.temporal);
|
|
48
|
+
if (!node ||
|
|
49
|
+
typeof node !== 'object' ||
|
|
50
|
+
Array.isArray(node) ||
|
|
51
|
+
typeof node.kind !== 'string' ||
|
|
52
|
+
!temporal ||
|
|
53
|
+
typeof temporal !== 'object' ||
|
|
54
|
+
Array.isArray(temporal))
|
|
55
|
+
throw new Error('invalid-definition');
|
|
56
|
+
return {
|
|
57
|
+
schema: r.schema,
|
|
58
|
+
ref,
|
|
59
|
+
catalog_definition_fingerprint: fingerprint,
|
|
60
|
+
node,
|
|
61
|
+
temporal,
|
|
62
|
+
source_location: { file: source.file, line: source.line },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Explicit public Details projection. No object dump, source-code read or data operation. */
|
|
2
|
+
export const BROWSER_CATALOG_PROGRAM = String.raw `
|
|
3
|
+
import contextlib, json, sys
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
class DiscardOutput:
|
|
9
|
+
def write(self, text):
|
|
10
|
+
return len(text)
|
|
11
|
+
def flush(self):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
with contextlib.redirect_stdout(DiscardOutput()), contextlib.redirect_stderr(DiscardOutput()):
|
|
15
|
+
import marivo.semantic as ms
|
|
16
|
+
from marivo.refs import Ref, RefPayloadV1, SemanticKind
|
|
17
|
+
|
|
18
|
+
FIELDS = {
|
|
19
|
+
"domain": "owner default",
|
|
20
|
+
"datasource": "backend_type",
|
|
21
|
+
"entity": "datasource primary_key",
|
|
22
|
+
"dimension": "entity",
|
|
23
|
+
"measure": "entity unit additivity",
|
|
24
|
+
"time_dimension": "entity parse_kind data_type granularity format timezone is_default sample_interval",
|
|
25
|
+
"metric": "metric_type entities root_entity aggregation aggregation_target aggregation_target_kind measure unit additivity fold status_time_dimension fanout_policy filter effective_entities candidate_dimensions candidate_time_dimensions measure_lineage weighted_mean_value weighted_mean_weight composition components linear_terms required_relationships parity_status",
|
|
26
|
+
"relationship": "from_entity to_entity from_keys to_keys",
|
|
27
|
+
"event": "source_entity identity occurred_at participants predicate_kind definition_fingerprint",
|
|
28
|
+
"state_model": "subject states inceptions transitions definition_fingerprint",
|
|
29
|
+
"period_calendar": "date boundary_timezone coverage correspondences snapshot_status snapshot_digest",
|
|
30
|
+
"temporal_set": "boundary_timezone coverage occurrence_id start end category occurrence_count snapshot_status",
|
|
31
|
+
"work_schedule": "boundary_timezone coverage date is_working snapshot_status",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def ref_payload(value):
|
|
35
|
+
return RefPayloadV1.from_ref(value).to_dict()
|
|
36
|
+
|
|
37
|
+
# Traversal is restricted to values of the explicitly selected public fields.
|
|
38
|
+
def display(value, field, relations):
|
|
39
|
+
if isinstance(value, Ref):
|
|
40
|
+
relations.append({"field": field, "ref": ref_payload(value)})
|
|
41
|
+
return value.key
|
|
42
|
+
if isinstance(value, Enum):
|
|
43
|
+
return str(value.value)
|
|
44
|
+
if isinstance(value, (date, datetime)):
|
|
45
|
+
return value.isoformat()
|
|
46
|
+
if value is None:
|
|
47
|
+
return "—"
|
|
48
|
+
if isinstance(value, bool):
|
|
49
|
+
return "true" if value else "false"
|
|
50
|
+
if isinstance(value, (str, int, float)):
|
|
51
|
+
return str(value)
|
|
52
|
+
if isinstance(value, (tuple, list)):
|
|
53
|
+
return "\n".join(display(v, field, relations) for v in value)
|
|
54
|
+
if isinstance(value, Mapping):
|
|
55
|
+
return "\n".join(str(k) + ": " + display(v, field, relations) for k, v in value.items())
|
|
56
|
+
# Public temporal value, not a generic repr of a Python object.
|
|
57
|
+
if field == "sample_interval":
|
|
58
|
+
return value.to_token()
|
|
59
|
+
raise TypeError("unsupported display field: " + field)
|
|
60
|
+
|
|
61
|
+
def project(entry):
|
|
62
|
+
d = entry.details()
|
|
63
|
+
relations, fields = [], []
|
|
64
|
+
def add(name, value):
|
|
65
|
+
if value is not None:
|
|
66
|
+
fields.append({"name": name, "value": display(value, name, relations)})
|
|
67
|
+
for name in FIELDS.get(entry.kind.value, "").split():
|
|
68
|
+
if hasattr(d, name):
|
|
69
|
+
add(name, getattr(d, name))
|
|
70
|
+
for name in ("parents", "children", "dependents"):
|
|
71
|
+
for ref in getattr(d, name):
|
|
72
|
+
relations.append({"field": name, "ref": ref_payload(ref)})
|
|
73
|
+
if entry.kind.value == "entity":
|
|
74
|
+
source = d.source
|
|
75
|
+
add("source_kind", source.kind)
|
|
76
|
+
# Remote file URLs may contain credentials. Only table identity is projected.
|
|
77
|
+
if source.kind == "table":
|
|
78
|
+
add("source_table", source.table)
|
|
79
|
+
add("source_database", source.database)
|
|
80
|
+
version = d.versioning
|
|
81
|
+
if version is not None:
|
|
82
|
+
for name in ("kind", "partition_field", "grain", "timezone", "format", "valid_from", "valid_to", "interval", "open_end"):
|
|
83
|
+
if hasattr(version, name):
|
|
84
|
+
add("version_" + name, getattr(version, name))
|
|
85
|
+
if entry.kind.value == "period_calendar":
|
|
86
|
+
for level in d.levels:
|
|
87
|
+
add("level", level.name)
|
|
88
|
+
for name in ("key_ref", "period_count", "direct_finer_levels", "direct_coarser_levels", "rollup_targets"):
|
|
89
|
+
add("level_" + name, getattr(level, name))
|
|
90
|
+
computation = d.definition.to_dict() if entry.kind.value in ("metric", "measure", "dimension", "time_dimension") else None
|
|
91
|
+
def definition_refs(value, role="definition"):
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
if value.get("schema") == "marivo.semantic_ref/v1":
|
|
94
|
+
relations.append({"field": role, "ref": value})
|
|
95
|
+
else:
|
|
96
|
+
for key, item in value.items():
|
|
97
|
+
definition_refs(item, role + "." + key)
|
|
98
|
+
elif isinstance(value, list):
|
|
99
|
+
for item in value:
|
|
100
|
+
definition_refs(item, role)
|
|
101
|
+
if computation is not None:
|
|
102
|
+
definition_refs(computation["node"])
|
|
103
|
+
definition_refs(computation["temporal"], "temporal")
|
|
104
|
+
unique = {(r["field"], r["ref"]["kind"], r["ref"]["path"]): r for r in relations}
|
|
105
|
+
return {
|
|
106
|
+
"computation": computation, "ref": ref_payload(entry.ref), "name": entry.name, "domain": d.domain,
|
|
107
|
+
"definition": d.context.business_definition, "guardrails": list(d.context.guardrails),
|
|
108
|
+
"source": {"file": d.source_location.file, "line": d.source_location.line, "symbol": d.python_symbol},
|
|
109
|
+
"fields": fields, "relations": list(unique.values()),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
catalog = ms.load(workspace_dir=sys.argv[1])
|
|
113
|
+
result = {
|
|
114
|
+
"fingerprint": catalog.definition_fingerprint,
|
|
115
|
+
"kinds": [kind.value for kind in SemanticKind],
|
|
116
|
+
"objects": [project(entry) for kind in SemanticKind for entry in catalog.items(kind).items],
|
|
117
|
+
}
|
|
118
|
+
print(json.dumps(result, ensure_ascii=True))
|
|
119
|
+
`.trim();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { boundedText, closed } from "../semantic-reference/contracts.js";
|
|
3
|
+
import { abortable } from "../semantic-reference/rpc.js";
|
|
4
|
+
import { CATALOG_MAX_BYTES, parseCatalogProjection } from "./contracts.js";
|
|
5
|
+
import { BROWSER_CATALOG_PROGRAM } from "./program.js";
|
|
6
|
+
/** Workspace-addressed read service; intentionally has no Agent or credential dependency. */
|
|
7
|
+
export class SemanticBrowserService {
|
|
8
|
+
#host;
|
|
9
|
+
#lifetime = new AbortController();
|
|
10
|
+
constructor(host) {
|
|
11
|
+
this.#host = host;
|
|
12
|
+
}
|
|
13
|
+
#assertOwner(id, workspacePath, root) {
|
|
14
|
+
const current = this.#host.getWorkspace(id);
|
|
15
|
+
if (!current || current.path !== workspacePath || this.#host.projectRoot(current) !== root)
|
|
16
|
+
throw new Error('workspace-changed');
|
|
17
|
+
}
|
|
18
|
+
async read(payload, caller) {
|
|
19
|
+
const signal = AbortSignal.any([caller, this.#lifetime.signal]);
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
const request = closed(payload, ['workspaceId']);
|
|
22
|
+
const id = boundedText(request.workspaceId, 256);
|
|
23
|
+
const workspace = this.#host.getWorkspace(id);
|
|
24
|
+
if (!workspace)
|
|
25
|
+
throw new Error('workspace-unavailable');
|
|
26
|
+
const root = this.#host.projectRoot(workspace);
|
|
27
|
+
const runner = await abortable(this.#host.resolve(root), signal);
|
|
28
|
+
this.#assertOwner(id, workspace.path, root);
|
|
29
|
+
if (runner.status !== 'ready')
|
|
30
|
+
throw new Error('environment-failed');
|
|
31
|
+
const result = await runner.runChecked({
|
|
32
|
+
program: BROWSER_CATALOG_PROGRAM,
|
|
33
|
+
args: [runner.binding.projectRoot],
|
|
34
|
+
signal,
|
|
35
|
+
environmentOverlay: { MARIVO_TELEMETRY: 'off', PYTHONDONTWRITEBYTECODE: '1' },
|
|
36
|
+
limits: { timeoutMs: 30_000, stdoutMaxBytes: CATALOG_MAX_BYTES, stderrMaxBytes: 8192 },
|
|
37
|
+
});
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
if ((await realpath(root)) !== runner.binding.projectRoot)
|
|
40
|
+
throw new Error('workspace-changed');
|
|
41
|
+
this.#assertOwner(id, workspace.path, root);
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
if (result.exitCode !== 0)
|
|
44
|
+
throw new Error('catalog-load-failed');
|
|
45
|
+
if (result.stdout.byteLength > CATALOG_MAX_BYTES)
|
|
46
|
+
throw new Error('catalog-too-large');
|
|
47
|
+
const projection = parseCatalogProjection(JSON.parse(result.stdout.toString('utf8')));
|
|
48
|
+
return {
|
|
49
|
+
...projection,
|
|
50
|
+
workspaceId: id,
|
|
51
|
+
projectRoot: runner.binding.projectRoot,
|
|
52
|
+
environmentFingerprint: runner.binding.fingerprint,
|
|
53
|
+
loadedAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
dispose() {
|
|
57
|
+
this.#lifetime.abort();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Fixed diagnostics only: Python errors may contain model code or datasource secrets. */
|
|
61
|
+
export function browserFailure(error) {
|
|
62
|
+
const code = error instanceof Error && 'code' in error ? error.code : undefined;
|
|
63
|
+
if (code === 'subprocess-timeout')
|
|
64
|
+
return '语义层加载超时,请检查模型后重试。';
|
|
65
|
+
if (code === 'subprocess-output-limit' ||
|
|
66
|
+
(error instanceof Error && error.message === 'catalog-too-large'))
|
|
67
|
+
return '语义层超过读取大小上限,未加载不完整目录。';
|
|
68
|
+
if (error instanceof Error &&
|
|
69
|
+
['workspace-unavailable', 'workspace-changed'].includes(error.message))
|
|
70
|
+
return 'Workspace 已不可用或绑定发生变化,请重新选择。';
|
|
71
|
+
return '无法加载语义层,请检查项目模型与 Marivo Runtime 后重试。';
|
|
72
|
+
}
|