@chengxianglibra/dsh-data-analysis 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -53
- package/cordis.patch.yml +1 -1
- package/lib/bin/environment.js +9 -7
- package/lib/bin/presentation-lint.js +21 -0
- package/lib/bridges.js +2 -2
- package/lib/client/credentials/changes.js +74 -0
- package/lib/client/credentials/create-datasource.js +171 -0
- package/lib/client/credentials/credential-draft.js +42 -0
- package/lib/client/credentials/defaults.js +11 -0
- package/lib/client/credentials/descriptions.js +46 -0
- package/lib/client/credentials/install.js +319 -0
- package/lib/client/credentials/model.js +766 -0
- package/lib/client/credentials/report-publishing.js +96 -0
- package/lib/client/credentials/styles.js +21 -0
- package/lib/client/i18n/context.js +22 -0
- package/lib/client/i18n/copy.js +57 -0
- package/lib/client/i18n/credentials.js +475 -0
- package/lib/client/i18n/host.js +27 -0
- package/lib/client/i18n/navigation.js +41 -0
- package/lib/client/i18n/presentation.js +577 -0
- package/lib/client/i18n/python.js +36 -0
- package/lib/client/i18n/semantic.js +441 -0
- package/lib/client/input-context.js +21 -0
- package/lib/client/python-tool/card.js +35 -0
- package/lib/client/python-tool/execution-error.js +35 -0
- package/lib/client/python-tool/install.js +9 -0
- package/lib/client/python-tool/model.js +101 -0
- package/lib/client/right-tabs/install.js +535 -0
- package/lib/client/right-tabs/live-delivery.js +58 -0
- package/lib/client/right-tabs/navigation.js +47 -0
- package/lib/client/right-tabs/page.js +178 -0
- package/lib/client/right-tabs/styles.js +11 -0
- package/lib/client/rpc.js +5 -0
- package/lib/client/semantic-reference.js +10 -0
- package/lib/client/workspace-header-action.js +24 -0
- package/lib/client/workspace-shortcuts.js +26 -0
- package/lib/client.js +1250 -730
- package/lib/compatibility.js +2 -2
- package/lib/datasource/authoring.js +109 -0
- package/lib/datasource/bridge-programs.js +42 -27
- package/lib/datasource/bridge.js +144 -9
- package/lib/datasource/changes-contract.js +41 -0
- package/lib/datasource/changes.js +49 -0
- package/lib/datasource/configure.js +39 -0
- package/lib/datasource/defaults.js +25 -0
- package/lib/datasource/index.js +4 -2
- package/lib/datasource/python-options.js +21 -0
- package/lib/datasource/python.js +206 -0
- package/lib/datasource/resolver-program.js +112 -0
- package/lib/datasource/rpc.js +150 -0
- package/lib/datasource/service.js +962 -0
- package/lib/datasource/shell-env.js +2 -333
- package/lib/datasource/test.js +25 -51
- package/lib/disclosure/activation.js +19 -7
- package/lib/disclosure/execution-guidance.js +52 -0
- package/lib/disclosure/help.js +4 -1
- package/lib/environment/binding.js +54 -9
- package/lib/environment/runtime.js +120 -108
- package/lib/environment/subprocess.js +6 -1
- package/lib/environment/summary.js +3 -3
- package/lib/environment/workspace.js +23 -1
- package/lib/index.js +1 -1
- package/lib/lifecycle.js +33 -0
- package/lib/plugin-agents.js +154 -0
- package/lib/plugin.js +172 -133
- package/lib/presentation/asset.js +34 -0
- package/lib/presentation/assets/portable.js +606 -0
- package/lib/presentation/assets/static.js +1216 -0
- package/lib/presentation/build/index.js +72 -0
- package/lib/presentation/commit.js +163 -0
- package/lib/presentation/contracts/asset-path.js +9 -0
- package/lib/presentation/contracts/catalog.js +62 -0
- package/lib/presentation/contracts/charts.js +453 -0
- package/lib/presentation/contracts/editing.js +58 -0
- package/lib/presentation/contracts/index.js +663 -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 +71 -0
- package/lib/presentation/export-html.js +214 -0
- package/lib/presentation/files.js +82 -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 +196 -0
- package/lib/presentation/projection/program.js +137 -0
- package/lib/presentation/projection/sql-code.js +79 -0
- package/lib/presentation/receipt.js +34 -0
- package/lib/presentation/reports.js +219 -0
- package/lib/presentation/rpc.js +186 -0
- package/lib/presentation/tool.js +118 -0
- package/lib/python-execution-contracts.js +1 -0
- package/lib/python-execution.js +232 -0
- package/lib/report-publishing/adapters.js +111 -0
- package/lib/report-publishing/config.js +69 -0
- package/lib/report-publishing/service.js +180 -0
- package/lib/rpc.js +49 -0
- package/lib/semantic-browser/contracts.js +97 -0
- package/lib/semantic-browser/definition.js +64 -0
- package/lib/semantic-browser/program.js +129 -0
- package/lib/semantic-browser/service.js +106 -0
- package/lib/semantic-reference/bridge.js +127 -0
- package/lib/semantic-reference/contracts.js +127 -0
- package/lib/semantic-reference/environment.js +56 -0
- package/lib/semantic-reference/index.js +3 -0
- package/lib/semantic-reference/labels.js +16 -0
- package/lib/semantic-reference/rpc.js +143 -0
- package/lib/semantic-reference/search.js +95 -0
- package/lib/semantic-reference/usage.js +152 -0
- package/lib/tool-lifecycle.js +22 -0
- package/lib/types/bridges.d.ts +2 -2
- package/lib/types/client/credentials/changes.d.ts +9 -0
- package/lib/types/client/credentials/create-datasource.d.ts +7 -0
- package/lib/types/client/credentials/credential-draft.d.ts +14 -0
- package/lib/types/client/credentials/defaults.d.ts +2 -0
- package/lib/types/client/credentials/descriptions.d.ts +1 -0
- package/lib/types/client/credentials/install.d.ts +19 -0
- package/lib/types/client/credentials/model.d.ts +79 -0
- package/lib/types/client/credentials/report-publishing.d.ts +5 -0
- package/lib/types/client/credentials/styles.d.ts +1 -0
- package/lib/types/client/i18n/context.d.ts +17 -0
- package/lib/types/client/i18n/copy.d.ts +806 -0
- package/lib/types/client/i18n/credentials.d.ts +475 -0
- package/lib/types/client/i18n/host.d.ts +16 -0
- package/lib/types/client/i18n/navigation.d.ts +41 -0
- package/lib/types/client/i18n/presentation.d.ts +577 -0
- package/lib/types/client/i18n/python.d.ts +19 -0
- package/lib/types/client/i18n/semantic.d.ts +441 -0
- package/lib/types/client/input-context.d.ts +19 -0
- package/lib/types/client/presentation/ask-dsh.d.ts +5 -0
- package/lib/types/client/presentation/catalog-model.d.ts +24 -0
- package/lib/types/client/presentation/catalog.d.ts +20 -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/context-reference.d.ts +12 -0
- package/lib/types/client/presentation/copy-context.d.ts +5 -0
- package/lib/types/client/presentation/delivery-model.d.ts +75 -0
- package/lib/types/client/presentation/delivery.d.ts +78 -0
- package/lib/types/client/presentation/download.d.ts +2 -0
- package/lib/types/client/presentation/editor-controls.d.ts +12 -0
- package/lib/types/client/presentation/export-menu.d.ts +26 -0
- package/lib/types/client/presentation/export-view.d.ts +63 -0
- package/lib/types/client/presentation/global-filters.d.ts +6 -0
- package/lib/types/client/presentation/host-entry.d.ts +18 -0
- package/lib/types/client/presentation/icons.d.ts +4 -0
- package/lib/types/client/presentation/markdown.d.ts +5 -0
- package/lib/types/client/presentation/model.d.ts +61 -0
- package/lib/types/client/presentation/reader.d.ts +24 -0
- package/lib/types/client/presentation/reference-source.d.ts +9 -0
- package/lib/types/client/presentation/source-code-model.d.ts +17 -0
- package/lib/types/client/presentation/source-code.d.ts +6 -0
- package/lib/types/client/presentation/source-dialog.d.ts +12 -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 +16 -0
- package/lib/types/client/presentation/use-element-width.d.ts +5 -0
- package/lib/types/client/python-tool/card.d.ts +2 -0
- package/lib/types/client/python-tool/execution-error.d.ts +3 -0
- package/lib/types/client/python-tool/install.d.ts +2 -0
- package/lib/types/client/python-tool/model.d.ts +20 -0
- package/lib/types/client/right-tabs/install.d.ts +18 -0
- package/lib/types/client/right-tabs/live-delivery.d.ts +8 -0
- package/lib/types/client/right-tabs/navigation.d.ts +17 -0
- package/lib/types/client/right-tabs/page.d.ts +36 -0
- package/lib/types/client/right-tabs/styles.d.ts +2 -0
- package/lib/types/client/rpc.d.ts +3 -0
- package/lib/types/client/semantic-reference.d.ts +4 -0
- package/lib/types/client/workspace-header-action.d.ts +7 -0
- package/lib/types/client/workspace-shortcuts.d.ts +15 -0
- package/lib/types/client.d.ts +9 -124
- package/lib/types/compatibility.d.ts +3 -3
- package/lib/types/datasource/authoring.d.ts +35 -0
- package/lib/types/datasource/bridge.d.ts +43 -4
- package/lib/types/datasource/changes-contract.d.ts +18 -0
- package/lib/types/datasource/changes.d.ts +15 -0
- package/lib/types/datasource/configure.d.ts +7 -0
- package/lib/types/datasource/defaults.d.ts +7 -0
- package/lib/types/datasource/index.d.ts +8 -5
- package/lib/types/datasource/python-options.d.ts +9 -0
- package/lib/types/datasource/python.d.ts +19 -0
- package/lib/types/datasource/resolver-program.d.ts +6 -0
- package/lib/types/datasource/rpc.d.ts +5 -0
- package/lib/types/datasource/service.d.ts +155 -0
- package/lib/types/datasource/shell-env.d.ts +0 -29
- package/lib/types/datasource/test.d.ts +6 -29
- package/lib/types/disclosure/activation.d.ts +2 -1
- package/lib/types/disclosure/execution-guidance.d.ts +6 -0
- package/lib/types/disclosure/help.d.ts +1 -1
- 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 +15 -3
- package/lib/types/environment/workspace.d.ts +1 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/lifecycle.d.ts +8 -0
- package/lib/types/plugin-agents.d.ts +27 -0
- package/lib/types/plugin.d.ts +14 -24
- package/lib/types/presentation/asset.d.ts +6 -0
- package/lib/types/presentation/build/index.d.ts +16 -0
- package/lib/types/presentation/commit.d.ts +4 -0
- package/lib/types/presentation/contracts/asset-path.d.ts +2 -0
- package/lib/types/presentation/contracts/catalog.d.ts +27 -0
- package/lib/types/presentation/contracts/charts.d.ts +11 -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 +255 -0
- package/lib/types/presentation/contracts/values.d.ts +9 -0
- package/lib/types/presentation/delivery.d.ts +3 -0
- package/lib/types/presentation/export-html.d.ts +13 -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 +10 -0
- package/lib/types/presentation/rpc.d.ts +25 -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/report-publishing/adapters.d.ts +6 -0
- package/lib/types/report-publishing/config.d.ts +22 -0
- package/lib/types/report-publishing/service.d.ts +68 -0
- package/lib/types/rpc.d.ts +3 -0
- package/lib/types/semantic-browser/contracts.d.ts +47 -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 +26 -0
- package/lib/types/semantic-reference/bridge.d.ts +12 -0
- package/lib/types/semantic-reference/contracts.d.ts +54 -0
- package/lib/types/semantic-reference/environment.d.ts +22 -0
- package/lib/types/semantic-reference/index.d.ts +3 -0
- package/lib/types/semantic-reference/labels.d.ts +16 -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/lib/types/tool-lifecycle.d.ts +4 -0
- package/lib/types/workspace-identity.d.ts +6 -0
- package/lib/workspace-identity.js +9 -0
- package/package.json +99 -53
- package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-files/SKILL.md +32 -0
- package/skills/dsh-data-analysis-files/references/examples.md +93 -0
- package/skills/dsh-data-analysis-presentation/SKILL.md +28 -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 +36 -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 +393 -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 +80 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +42 -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 +58 -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 +279 -0
- package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +27 -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 +44 -0
- package/skills/dsh-data-analysis-presentation/references/export-html.md +7 -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/publishing.md +3 -0
- package/skills/dsh-data-analysis-presentation/references/schema.md +162 -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,196 @@
|
|
|
1
|
+
import { MarivoEnvironmentError } from "../../environment/errors.js";
|
|
2
|
+
import { readPythonExecution } from "../../python-execution.js";
|
|
3
|
+
import { PRESENTATION_BUDGETS, PresentationContractError, parsePresentationDocument, parsePresentationDraft, parseTypedDataset, } from "../contracts/index.js";
|
|
4
|
+
import { readWorkspaceJson } from "./files.js";
|
|
5
|
+
import { MARIVO_PRESENTATION_READ_PROGRAM } from "./program.js";
|
|
6
|
+
export { readWorkspaceJson } from "./files.js";
|
|
7
|
+
function invalidOutput() {
|
|
8
|
+
throw new MarivoEnvironmentError('subprocess-output-invalid', 'Marivo presentation reader returned an invalid projection.');
|
|
9
|
+
}
|
|
10
|
+
function record(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
12
|
+
invalidOutput();
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function exactKeys(value, keys) {
|
|
16
|
+
if (Object.keys(value).sort().join(',') !== keys.sort().join(','))
|
|
17
|
+
invalidOutput();
|
|
18
|
+
}
|
|
19
|
+
/** Internal S2 bridge: restores public data without observe, revalidation or credential admission. */
|
|
20
|
+
export class MarivoPresentationProjection {
|
|
21
|
+
#runner;
|
|
22
|
+
constructor(runner) {
|
|
23
|
+
this.#runner = runner;
|
|
24
|
+
}
|
|
25
|
+
get status() {
|
|
26
|
+
return this.#runner.status;
|
|
27
|
+
}
|
|
28
|
+
get binding() {
|
|
29
|
+
return this.#runner.binding;
|
|
30
|
+
}
|
|
31
|
+
async readDraft(relativePath, signal) {
|
|
32
|
+
this.#assertReady(signal);
|
|
33
|
+
return parsePresentationDraft(await readWorkspaceJson(this.binding.projectRoot, relativePath, PRESENTATION_BUDGETS.draftBytes, '/draft_path', signal));
|
|
34
|
+
}
|
|
35
|
+
#assertReady(signal) {
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
if (this.#runner.status !== 'ready')
|
|
38
|
+
throw new MarivoEnvironmentError('binding-failed', 'Presentation projection requires a ready bound Runtime.');
|
|
39
|
+
if (!this.binding.presentationKit)
|
|
40
|
+
throw new MarivoEnvironmentError('binding-identity-mismatch', 'Presentation projection requires the checked presentation helper identity.');
|
|
41
|
+
}
|
|
42
|
+
async project(value, options) {
|
|
43
|
+
this.#assertReady(options.signal);
|
|
44
|
+
// Snapshot caller-owned input before awaiting IO.
|
|
45
|
+
const draft = structuredClone(parsePresentationDraft(value));
|
|
46
|
+
const datasets = [];
|
|
47
|
+
const artifactSelections = draft.datasets.flatMap((dataset, index) => dataset.kind === 'artifact'
|
|
48
|
+
? [
|
|
49
|
+
{
|
|
50
|
+
id: dataset.id,
|
|
51
|
+
kind: dataset.kind,
|
|
52
|
+
sourceId: dataset.sourceId,
|
|
53
|
+
rowLimit: dataset.rowLimit,
|
|
54
|
+
...(dataset.columns ? { columns: dataset.columns } : {}),
|
|
55
|
+
index,
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
: []);
|
|
59
|
+
let sources = [];
|
|
60
|
+
let diagnostics = [];
|
|
61
|
+
const artifactData = new Map();
|
|
62
|
+
if (draft.sources.length > 0) {
|
|
63
|
+
const result = await this.#runner.runChecked({
|
|
64
|
+
program: MARIVO_PRESENTATION_READ_PROGRAM,
|
|
65
|
+
args: [JSON.stringify({ sources: draft.sources, datasets: artifactSelections })],
|
|
66
|
+
limits: {
|
|
67
|
+
timeoutMs: 30_000,
|
|
68
|
+
stdoutMaxBytes: PRESENTATION_BUDGETS.documentBytes,
|
|
69
|
+
stderrMaxBytes: 65_536,
|
|
70
|
+
},
|
|
71
|
+
signal: options.signal,
|
|
72
|
+
});
|
|
73
|
+
this.#assertReady(options.signal);
|
|
74
|
+
if (result.exitCode !== 0)
|
|
75
|
+
throw new MarivoEnvironmentError('subprocess-failed', 'The bound Marivo presentation read failed.', { exitCode: result.exitCode });
|
|
76
|
+
let payload;
|
|
77
|
+
try {
|
|
78
|
+
payload = record(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(result.stdout)));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
invalidOutput();
|
|
82
|
+
}
|
|
83
|
+
if (payload.ok === false) {
|
|
84
|
+
exactKeys(payload, ['ok', 'error']);
|
|
85
|
+
const error = record(payload.error);
|
|
86
|
+
exactKeys(error, ['code', 'path', 'message']);
|
|
87
|
+
if (typeof error.code !== 'string' ||
|
|
88
|
+
!/^[a-z_]{1,128}$/.test(error.code) ||
|
|
89
|
+
typeof error.path !== 'string' ||
|
|
90
|
+
error.path.length > 1024 ||
|
|
91
|
+
!/^(?:\/(?:[^~]|~[01])*)*$/.test(error.path) ||
|
|
92
|
+
typeof error.message !== 'string' ||
|
|
93
|
+
error.message.length > 1024)
|
|
94
|
+
invalidOutput();
|
|
95
|
+
throw new PresentationContractError(error.code, error.path, error.message);
|
|
96
|
+
}
|
|
97
|
+
exactKeys(payload, ['ok', 'sources', 'datasets', 'diagnostics']);
|
|
98
|
+
if (payload.ok !== true ||
|
|
99
|
+
!Array.isArray(payload.sources) ||
|
|
100
|
+
!Array.isArray(payload.datasets) ||
|
|
101
|
+
!Array.isArray(payload.diagnostics))
|
|
102
|
+
invalidOutput();
|
|
103
|
+
if (payload.sources.length !== draft.sources.length ||
|
|
104
|
+
payload.datasets.length !== artifactSelections.length)
|
|
105
|
+
invalidOutput();
|
|
106
|
+
for (const [index, source] of payload.sources.entries()) {
|
|
107
|
+
const actual = record(source);
|
|
108
|
+
const ref = record(actual.ref);
|
|
109
|
+
const expected = draft.sources[index];
|
|
110
|
+
if (actual.id !== expected.id ||
|
|
111
|
+
ref.sessionId !== expected.ref.sessionId ||
|
|
112
|
+
ref.artifactRef !== expected.ref.artifactRef ||
|
|
113
|
+
ref.findingId !== expected.ref.findingId)
|
|
114
|
+
invalidOutput();
|
|
115
|
+
}
|
|
116
|
+
for (const [index, entry] of payload.datasets.entries()) {
|
|
117
|
+
const dataset = record(entry);
|
|
118
|
+
exactKeys(dataset, ['id', 'data']);
|
|
119
|
+
if (dataset.id !== artifactSelections[index].id)
|
|
120
|
+
invalidOutput();
|
|
121
|
+
artifactData.set(artifactSelections[index].id, dataset.data);
|
|
122
|
+
}
|
|
123
|
+
sources = payload.sources;
|
|
124
|
+
diagnostics = payload.diagnostics;
|
|
125
|
+
}
|
|
126
|
+
for (const [index, dataset] of draft.datasets.entries()) {
|
|
127
|
+
this.#assertReady(options.signal);
|
|
128
|
+
const location = `/datasets/${index}`;
|
|
129
|
+
let data;
|
|
130
|
+
if (dataset.kind === 'computed') {
|
|
131
|
+
data = await readWorkspaceJson(this.binding.projectRoot, dataset.path, PRESENTATION_BUDGETS.datasetBytes, `${location}/path`, options.signal);
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
data = artifactData.get(dataset.id);
|
|
135
|
+
data = parseTypedDataset(data, `${location}/data`);
|
|
136
|
+
if (dataset.kind === 'artifact') {
|
|
137
|
+
const typed = data;
|
|
138
|
+
if (typed.limit > dataset.rowLimit ||
|
|
139
|
+
(dataset.columns !== undefined &&
|
|
140
|
+
(typed.columns.length !== dataset.columns.length ||
|
|
141
|
+
typed.columns.some((column, index) => column.id !== dataset.columns[index]))))
|
|
142
|
+
invalidOutput();
|
|
143
|
+
}
|
|
144
|
+
const code = [];
|
|
145
|
+
for (const [codeIndex, ref] of (dataset.codeRefs ?? []).entries()) {
|
|
146
|
+
this.#assertReady(options.signal);
|
|
147
|
+
try {
|
|
148
|
+
code.push(await readPythonExecution(this.binding.projectRoot, ref, options.signal));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
options.signal?.throwIfAborted();
|
|
152
|
+
throw new PresentationContractError('code_unavailable', `${location}/codeRefs/${codeIndex}`, 'The captured Python execution cannot be read with its exact Workspace identity and digest.', 'Check the codeRef returned by the successful marivo_python call; do not rerun analysis to repair presentation.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.#assertReady(options.signal);
|
|
156
|
+
datasets.push({
|
|
157
|
+
id: dataset.id,
|
|
158
|
+
origin: dataset.kind,
|
|
159
|
+
data: data,
|
|
160
|
+
sourceIds: dataset.kind === 'artifact' ? [dataset.sourceId] : dataset.sourceIds,
|
|
161
|
+
...(dataset.codeRefs !== undefined ? { code } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const [index, dataset] of datasets.entries()) {
|
|
165
|
+
if (dataset.data.truncated)
|
|
166
|
+
diagnostics.push({
|
|
167
|
+
code: 'truncated',
|
|
168
|
+
path: `/datasets/${index}/data/rows`,
|
|
169
|
+
message: `Showing ${dataset.data.rows.length} of ${dataset.data.rowCount} rows; truncated data must not imply full totals or rankings.`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
for (const [index, source] of sources.entries()) {
|
|
173
|
+
if (source.status === 'unavailable')
|
|
174
|
+
diagnostics.push({
|
|
175
|
+
code: 'source_unavailable',
|
|
176
|
+
path: `/sources/${index}`,
|
|
177
|
+
message: 'The declared Artifact source cannot be restored in the bound Workspace.',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
this.#assertReady(options.signal);
|
|
181
|
+
return parsePresentationDocument({
|
|
182
|
+
schemaVersion: 3,
|
|
183
|
+
locale: draft.locale,
|
|
184
|
+
reportId: options.reportId,
|
|
185
|
+
workspaceId: options.workspaceId,
|
|
186
|
+
buildId: options.buildId,
|
|
187
|
+
title: draft.title,
|
|
188
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
189
|
+
datasets,
|
|
190
|
+
sources,
|
|
191
|
+
blocks: draft.blocks,
|
|
192
|
+
...(draft.interaction ? { interaction: draft.interaction } : {}),
|
|
193
|
+
diagnostics,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
read_at = datetime.now(timezone.utc).isoformat()
|
|
37
|
+
try:
|
|
38
|
+
for index, declared in enumerate(request["sources"]):
|
|
39
|
+
ref = declared["ref"]
|
|
40
|
+
source_path = "/sources/" + str(index)
|
|
41
|
+
try:
|
|
42
|
+
session = sessions.get(ref["sessionId"])
|
|
43
|
+
if session is None:
|
|
44
|
+
session = mv.session.resume(ref["sessionId"], use_datasources=False)
|
|
45
|
+
sessions[ref["sessionId"]] = session
|
|
46
|
+
if session.id != ref["sessionId"]:
|
|
47
|
+
raise ProjectionFailure("source_identity_mismatch", source_path + "/ref/sessionId", "Restored Session identity differs from the declared source.")
|
|
48
|
+
artifact = session.artifact(ref["artifactRef"])
|
|
49
|
+
contract = artifact.contract()
|
|
50
|
+
meta = artifact.meta
|
|
51
|
+
if artifact.ref != ref["artifactRef"] or meta.session_id != session.id or contract.ref != artifact.ref:
|
|
52
|
+
raise ProjectionFailure("source_identity_mismatch", source_path + "/ref", "Restored Artifact identity differs from the declared source.")
|
|
53
|
+
if Path(meta.project_root).resolve() != Path.cwd().resolve():
|
|
54
|
+
raise ProjectionFailure("source_workspace_mismatch", source_path + "/ref", "Restored Artifact belongs to a different Workspace.")
|
|
55
|
+
except ProjectionFailure:
|
|
56
|
+
raise
|
|
57
|
+
except Exception:
|
|
58
|
+
# Only the requested source becomes unavailable. Exception messages may
|
|
59
|
+
# include credentials, project paths or raw SQL and are never returned.
|
|
60
|
+
sources.append({**declared, "status": "unavailable", "reason": "marivo.presentation.source-unavailable"})
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
artifacts[declared["id"]], contracts[declared["id"]] = artifact, contract
|
|
64
|
+
facts = []
|
|
65
|
+
def fact(label, value):
|
|
66
|
+
facts.append({"label": label, "value": text(value)})
|
|
67
|
+
fact("读取时间", read_at)
|
|
68
|
+
fact("Artifact kind", meta.kind)
|
|
69
|
+
fact("创建时间", meta.created_at.isoformat())
|
|
70
|
+
fact("内容身份", meta.content_hash)
|
|
71
|
+
fact("Evidence status", meta.evidence_status)
|
|
72
|
+
fact("完整行数", meta.row_count)
|
|
73
|
+
fact("公开语义引用", [{"role": item.role, "kind": item.semantic_kind.value, "path": item.semantic_path, "outputColumn": item.output_column} for item in contract.semantic_inputs])
|
|
74
|
+
quality = meta.quality_summary
|
|
75
|
+
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})
|
|
76
|
+
fact("Issues", [{"kind": item.kind, "severity": item.severity} for item in contract.issues])
|
|
77
|
+
if "findingId" in ref:
|
|
78
|
+
try:
|
|
79
|
+
finding = artifact.finding(ref["findingId"])
|
|
80
|
+
if finding.finding_id != ref["findingId"] or finding.artifact_ref != artifact.ref or finding.session_id != session.id:
|
|
81
|
+
raise ProjectionFailure("source_identity_mismatch", source_path + "/ref/findingId", "Restored Finding identity differs from the declared source.")
|
|
82
|
+
excerpt = finding.render(max_output_bytes=4096)
|
|
83
|
+
fact("选择的 Finding", excerpt)
|
|
84
|
+
except ProjectionFailure:
|
|
85
|
+
raise
|
|
86
|
+
except Exception:
|
|
87
|
+
fact("选择的 Finding unavailable", "marivo.presentation.finding-unavailable")
|
|
88
|
+
diagnostics.append({"code": "finding_unavailable", "path": source_path + "/ref/findingId", "message": "Artifact is available; its requested Finding is unavailable."})
|
|
89
|
+
fact("revalidation", "not_requested")
|
|
90
|
+
sources.append({**declared, "status": "available", "label": text(str(meta.kind) + " " + artifact.ref, 512), "facts": facts, "code": sql_code_snapshot(session, artifact)})
|
|
91
|
+
|
|
92
|
+
for selection in request["datasets"]:
|
|
93
|
+
dataset_path = "/datasets/" + str(selection["index"])
|
|
94
|
+
artifact = artifacts.get(selection["sourceId"])
|
|
95
|
+
if artifact is None:
|
|
96
|
+
raise ProjectionFailure("artifact_data_unavailable", dataset_path + "/sourceId", "A direct Artifact dataset requires an available persisted Artifact source.")
|
|
97
|
+
try:
|
|
98
|
+
contract = contracts[selection["sourceId"]]
|
|
99
|
+
frame = artifact.to_pandas()
|
|
100
|
+
public_columns = list(contract.artifact_schema.columns)
|
|
101
|
+
if list(frame.columns) != [column.name for column in public_columns] or len(frame) != artifact.meta.row_count or artifact.row_count != len(frame):
|
|
102
|
+
raise ProjectionFailure("artifact_data_identity_mismatch", dataset_path, "Artifact rows or columns disagree with its public contract.")
|
|
103
|
+
names = selection.get("columns", list(frame.columns))
|
|
104
|
+
by_name = {column.name: column for column in public_columns}
|
|
105
|
+
if any(name not in by_name for name in names):
|
|
106
|
+
raise ProjectionFailure("invalid_reference", dataset_path + "/columns", "A requested column is absent from the public Artifact schema.")
|
|
107
|
+
# Use public terminal values; declared upstream dtypes cannot recover
|
|
108
|
+
# Decimal precision already lost by to_pandas(). No derived metrics.
|
|
109
|
+
data = encode_dataset(frame.loc[:, names], row_limit=selection["rowLimit"])
|
|
110
|
+
for column in data["columns"]:
|
|
111
|
+
column["nullable"] = by_name[column["id"]].nullable
|
|
112
|
+
datasets.append({"id": selection["id"], "data": data})
|
|
113
|
+
except ProjectionFailure:
|
|
114
|
+
raise
|
|
115
|
+
except Exception as error:
|
|
116
|
+
# Preserve helper contract locations without leaking upstream error text.
|
|
117
|
+
code = getattr(error, "code", None)
|
|
118
|
+
location = getattr(error, "path", None)
|
|
119
|
+
if code in ("numeric_precision", "budget", "invalid_value") and isinstance(location, str):
|
|
120
|
+
raise ProjectionFailure(code, dataset_path + "/data" + location, "Public Artifact values cannot be encoded within the presentation data contract.") from None
|
|
121
|
+
raise ProjectionFailure("artifact_data_unavailable", dataset_path + "/data", "The direct Artifact does not expose the required typed presentation data.") from None
|
|
122
|
+
return {"ok": True, "sources": sources, "datasets": datasets, "diagnostics": diagnostics}
|
|
123
|
+
finally:
|
|
124
|
+
for session in sessions.values():
|
|
125
|
+
session.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
request = json.loads(sys.argv[1])
|
|
130
|
+
with md.credential_scope(resolver=DenyCredentials()):
|
|
131
|
+
result = read(request)
|
|
132
|
+
except ProjectionFailure as error:
|
|
133
|
+
result = {"ok": False, "error": {"code": error.code, "path": error.path, "message": error.message}}
|
|
134
|
+
except Exception:
|
|
135
|
+
result = {"ok": False, "error": {"code": "projection_read_failed", "path": "/sources", "message": "The bound public presentation read failed."}}
|
|
136
|
+
print(json.dumps(result, ensure_ascii=False, allow_nan=False, separators=(",", ":")))
|
|
137
|
+
`.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.presentation.sql-capability-unavailable"]}
|
|
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("marivo.presentation.sql-artifact-mismatch")
|
|
26
|
+
continue
|
|
27
|
+
producer = meta.produced_by_job
|
|
28
|
+
if producer is None:
|
|
29
|
+
notice("marivo.presentation.sql-producer-missing")
|
|
30
|
+
continue
|
|
31
|
+
if not valid_string(producer, 512):
|
|
32
|
+
notice("marivo.presentation.sql-producer-invalid")
|
|
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("marivo.presentation.sql-run-mismatch")
|
|
40
|
+
continue
|
|
41
|
+
for query in run.queries:
|
|
42
|
+
if query_count >= 32:
|
|
43
|
+
notice("marivo.presentation.sql-query-limit")
|
|
44
|
+
break
|
|
45
|
+
query_count += 1
|
|
46
|
+
query_id = query.query_id
|
|
47
|
+
if not valid_string(query_id, 512):
|
|
48
|
+
notice("marivo.presentation.sql-query-invalid")
|
|
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("marivo.presentation.sql-sql-invalid")
|
|
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("marivo.presentation.sql-upstream-limit")
|
|
62
|
+
break
|
|
63
|
+
if not valid_string(input_ref, 512):
|
|
64
|
+
notice("marivo.presentation.sql-upstream-invalid")
|
|
65
|
+
continue
|
|
66
|
+
if input_ref in scheduled:
|
|
67
|
+
continue
|
|
68
|
+
if len(scheduled) >= 65:
|
|
69
|
+
notice("marivo.presentation.sql-upstream-limit")
|
|
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("marivo.presentation.sql-read-unavailable")
|
|
76
|
+
if not snippets and not notices:
|
|
77
|
+
notice("marivo.presentation.sql-not-saved")
|
|
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,219 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, open, opendir, 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 { parseReportHistory, REPORT_HISTORY_BYTES, REPORT_HISTORY_LIMIT, } from "./contracts/catalog.js";
|
|
9
|
+
import { parsePresentationDocument, parsePresentationReceipt, } from "./contracts/index.js";
|
|
10
|
+
import { presentationAssetPath, presentationReportPath, presentationSha256, readPresentationAsset, readReportFile, } from "./files.js";
|
|
11
|
+
export async function readReceiptDocument(root, receipt, signal) {
|
|
12
|
+
for (const asset of Object.values(receipt.files)) {
|
|
13
|
+
if (asset.path !== presentationAssetPath(root, receipt.reportId, receipt.buildId, asset.asset))
|
|
14
|
+
throw new Error('asset-path-mismatch');
|
|
15
|
+
}
|
|
16
|
+
const bytes = await readPresentationAsset(root, receipt.reportId, receipt.buildId, 'presentation.json', signal);
|
|
17
|
+
if (bytes.length !== receipt.files.document.bytes ||
|
|
18
|
+
presentationSha256(bytes) !== receipt.files.document.sha256)
|
|
19
|
+
throw new Error('asset-digest-mismatch');
|
|
20
|
+
const document = parsePresentationDocument(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)));
|
|
21
|
+
if (document.workspaceId !== receipt.workspaceId ||
|
|
22
|
+
document.reportId !== receipt.reportId ||
|
|
23
|
+
document.buildId !== receipt.buildId ||
|
|
24
|
+
document.title !== receipt.title)
|
|
25
|
+
throw new Error('asset-owner-mismatch');
|
|
26
|
+
return document;
|
|
27
|
+
}
|
|
28
|
+
/** Current and its publication history become visible through one atomic rename. */
|
|
29
|
+
export async function readReportHistory(root, workspaceId, reportId, signal) {
|
|
30
|
+
const bytes = await readReportFile(root, path.join(presentationReportPath(root, reportId), 'current.json'), REPORT_HISTORY_BYTES, signal);
|
|
31
|
+
const current = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
32
|
+
if (!current || current.workspaceId !== workspaceId || current.reportId !== reportId)
|
|
33
|
+
throw new Error('invalid-report-current');
|
|
34
|
+
const receipt = parsePresentationReceipt(current.receipt);
|
|
35
|
+
if (receipt.workspaceId !== workspaceId || receipt.reportId !== reportId)
|
|
36
|
+
throw new Error('asset-owner-mismatch');
|
|
37
|
+
if (current.schemaVersion === 2 &&
|
|
38
|
+
Object.keys(current).sort().join(',') === 'receipt,reportId,schemaVersion,workspaceId') {
|
|
39
|
+
return {
|
|
40
|
+
workspaceId,
|
|
41
|
+
reportId,
|
|
42
|
+
currentBuildId: receipt.buildId,
|
|
43
|
+
legacyHistoryUnavailable: true,
|
|
44
|
+
versions: [{ receipt, publishedAt: null, source: null }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (current.schemaVersion !== 3 ||
|
|
48
|
+
Object.keys(current).sort().join(',') !==
|
|
49
|
+
'legacyHistoryUnavailable,receipt,reportId,schemaVersion,versions,workspaceId')
|
|
50
|
+
throw new Error('invalid-report-current');
|
|
51
|
+
const history = parseReportHistory({
|
|
52
|
+
workspaceId,
|
|
53
|
+
reportId,
|
|
54
|
+
currentBuildId: receipt.buildId,
|
|
55
|
+
legacyHistoryUnavailable: current.legacyHistoryUnavailable,
|
|
56
|
+
versions: current.versions,
|
|
57
|
+
});
|
|
58
|
+
if (JSON.stringify(history.versions[0].receipt) !== JSON.stringify(receipt))
|
|
59
|
+
throw new Error('invalid-report-history');
|
|
60
|
+
return history;
|
|
61
|
+
}
|
|
62
|
+
export async function resolvePresentation(root, workspaceId, reportId, signal) {
|
|
63
|
+
const history = await readReportHistory(root, workspaceId, reportId, signal);
|
|
64
|
+
const receipt = history.versions[0].receipt;
|
|
65
|
+
await readReceiptDocument(root, receipt, signal);
|
|
66
|
+
return receipt;
|
|
67
|
+
}
|
|
68
|
+
/** Enumerate only publication pointers, never completed-but-unpublished build directories. */
|
|
69
|
+
export async function listReports(root, workspaceId, signal) {
|
|
70
|
+
const directory = path.dirname(presentationReportPath(root, 'catalog'));
|
|
71
|
+
const parents = [root, path.dirname(directory), directory];
|
|
72
|
+
const identities = [];
|
|
73
|
+
try {
|
|
74
|
+
for (const name of parents) {
|
|
75
|
+
const stat = await lstat(name);
|
|
76
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (await realpath(name)) !== name)
|
|
77
|
+
throw new Error('asset-path-mismatch');
|
|
78
|
+
identities.push(stat);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code === 'ENOENT')
|
|
83
|
+
return { workspaceId, reports: [], unavailable: 0 };
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const reports = [];
|
|
87
|
+
let unavailable = 0, count = 0;
|
|
88
|
+
for await (const entry of await opendir(directory)) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
if (++count > 4096)
|
|
91
|
+
throw new Error('report-catalog-too-large');
|
|
92
|
+
if (entry.name.startsWith('.'))
|
|
93
|
+
continue;
|
|
94
|
+
try {
|
|
95
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
96
|
+
throw new Error('asset-not-file');
|
|
97
|
+
const history = await readReportHistory(root, workspaceId, entry.name, signal);
|
|
98
|
+
reports.push(history.versions[0]);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
signal?.throwIfAborted();
|
|
102
|
+
if (error.code !== 'ENOENT')
|
|
103
|
+
unavailable++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [index, name] of parents.entries()) {
|
|
107
|
+
const stat = await lstat(name);
|
|
108
|
+
if (stat.dev !== identities[index].dev ||
|
|
109
|
+
stat.ino !== identities[index].ino ||
|
|
110
|
+
stat.isSymbolicLink() ||
|
|
111
|
+
(await realpath(name)) !== name)
|
|
112
|
+
throw new Error('asset-path-mismatch');
|
|
113
|
+
}
|
|
114
|
+
reports.sort((a, b) => (b.publishedAt ?? '').localeCompare(a.publishedAt ?? '') ||
|
|
115
|
+
a.receipt.reportId.localeCompare(b.receipt.reportId));
|
|
116
|
+
return { workspaceId, reports, unavailable };
|
|
117
|
+
}
|
|
118
|
+
/** The mutable pointer is the publication boundary; completed builds are never overwritten. */
|
|
119
|
+
export async function publishPresentation(root, document, expectedBuildId, assertOwner, signal, source = null) {
|
|
120
|
+
await assertOwner();
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
const built = await buildPresentation(document, { html: false });
|
|
123
|
+
await assertOwner();
|
|
124
|
+
const receipt = await commitPresentation(root, built, assertOwner, signal);
|
|
125
|
+
const directory = presentationReportPath(root, document.reportId);
|
|
126
|
+
const currentPath = path.join(directory, 'current.json');
|
|
127
|
+
const parents = [root];
|
|
128
|
+
for (const segment of path.relative(root, directory).split(path.sep))
|
|
129
|
+
parents.push(path.join(parents.at(-1), segment));
|
|
130
|
+
const identities = await Promise.all(parents.map((parent) => lstat(parent)));
|
|
131
|
+
const check = async () => {
|
|
132
|
+
signal?.throwIfAborted();
|
|
133
|
+
await assertOwner();
|
|
134
|
+
const actual = await Promise.all(parents.map((parent) => lstat(parent)));
|
|
135
|
+
if ((await realpath(directory)) !== directory ||
|
|
136
|
+
actual.some((entry, index) => !entry.isDirectory() ||
|
|
137
|
+
entry.isSymbolicLink() ||
|
|
138
|
+
entry.dev !== identities[index].dev ||
|
|
139
|
+
entry.ino !== identities[index].ino))
|
|
140
|
+
throw new Error('presentation-directory-changed');
|
|
141
|
+
};
|
|
142
|
+
await check();
|
|
143
|
+
await withFileLock(currentPath, async () => {
|
|
144
|
+
await check();
|
|
145
|
+
let history;
|
|
146
|
+
let current;
|
|
147
|
+
try {
|
|
148
|
+
history = await readReportHistory(root, document.workspaceId, document.reportId, signal);
|
|
149
|
+
current = history.versions[0].receipt;
|
|
150
|
+
await readReceiptDocument(root, current, signal);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (expectedBuildId !== null || error.code !== 'ENOENT')
|
|
154
|
+
throw error;
|
|
155
|
+
// ENOENT is allowed only for an absent pointer, never a dangling current build.
|
|
156
|
+
try {
|
|
157
|
+
await lstat(currentPath);
|
|
158
|
+
throw new Error('invalid-report-current');
|
|
159
|
+
}
|
|
160
|
+
catch (missing) {
|
|
161
|
+
if (missing.code !== 'ENOENT')
|
|
162
|
+
throw missing;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if ((current?.buildId ?? null) !== expectedBuildId)
|
|
166
|
+
throw new Error('report-save-conflict');
|
|
167
|
+
const versions = [
|
|
168
|
+
{ receipt, publishedAt: new Date().toISOString(), source },
|
|
169
|
+
...(history?.versions ?? []),
|
|
170
|
+
];
|
|
171
|
+
if (versions.length > REPORT_HISTORY_LIMIT)
|
|
172
|
+
throw new Error('report-history-full');
|
|
173
|
+
parseReportHistory({
|
|
174
|
+
workspaceId: document.workspaceId,
|
|
175
|
+
reportId: document.reportId,
|
|
176
|
+
currentBuildId: receipt.buildId,
|
|
177
|
+
legacyHistoryUnavailable: history?.legacyHistoryUnavailable ?? false,
|
|
178
|
+
versions,
|
|
179
|
+
});
|
|
180
|
+
const pointer = JSON.stringify({
|
|
181
|
+
schemaVersion: 3,
|
|
182
|
+
workspaceId: document.workspaceId,
|
|
183
|
+
reportId: document.reportId,
|
|
184
|
+
receipt,
|
|
185
|
+
versions,
|
|
186
|
+
legacyHistoryUnavailable: history?.legacyHistoryUnavailable ?? false,
|
|
187
|
+
});
|
|
188
|
+
if (Buffer.byteLength(pointer) > REPORT_HISTORY_BYTES)
|
|
189
|
+
throw new Error('report-history-full');
|
|
190
|
+
const temporary = path.join(directory, `.current-${randomUUID()}.tmp`);
|
|
191
|
+
let published = false;
|
|
192
|
+
try {
|
|
193
|
+
const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
194
|
+
try {
|
|
195
|
+
await file.writeFile(pointer, { signal });
|
|
196
|
+
await file.sync();
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await file.close();
|
|
200
|
+
}
|
|
201
|
+
await check();
|
|
202
|
+
await rename(temporary, currentPath);
|
|
203
|
+
published = true;
|
|
204
|
+
const parent = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
205
|
+
try {
|
|
206
|
+
await parent.sync();
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await parent.close();
|
|
210
|
+
}
|
|
211
|
+
// Cancellation after the rename cannot undo a published save.
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
if (!published)
|
|
215
|
+
await rm(temporary, { force: true });
|
|
216
|
+
}
|
|
217
|
+
}, { waitMs: 5000 });
|
|
218
|
+
return receipt;
|
|
219
|
+
}
|