@chengxianglibra/dsh-data-analysis 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +83 -34
  2. package/lib/bin/environment.js +1 -1
  3. package/lib/bin/presentation-lint.js +21 -0
  4. package/lib/bridges.js +2 -2
  5. package/lib/client/credentials/create-datasource.js +67 -0
  6. package/lib/client/credentials/install.js +191 -0
  7. package/lib/client/credentials/model.js +494 -0
  8. package/lib/client/credentials/styles.js +17 -0
  9. package/lib/client/workspace-header-action.js +11 -0
  10. package/lib/client.js +1133 -736
  11. package/lib/compatibility.js +1 -1
  12. package/lib/datasource/authoring.js +70 -0
  13. package/lib/datasource/bridge-programs.js +30 -18
  14. package/lib/datasource/bridge.js +100 -8
  15. package/lib/datasource/index.js +3 -2
  16. package/lib/datasource/python.js +115 -0
  17. package/lib/datasource/resolver-program.js +112 -0
  18. package/lib/datasource/rpc.js +88 -0
  19. package/lib/datasource/service.js +687 -0
  20. package/lib/datasource/shell-env.js +2 -333
  21. package/lib/datasource/test.js +24 -51
  22. package/lib/environment/binding.js +54 -9
  23. package/lib/environment/runtime.js +89 -78
  24. package/lib/environment/subprocess.js +6 -1
  25. package/lib/environment/summary.js +3 -3
  26. package/lib/environment/workspace.js +5 -0
  27. package/lib/index.js +1 -1
  28. package/lib/plugin.js +168 -51
  29. package/lib/presentation/assets/portable.js +606 -0
  30. package/lib/presentation/assets/static.js +945 -0
  31. package/lib/presentation/build/index.js +71 -0
  32. package/lib/presentation/commit.js +156 -0
  33. package/lib/presentation/contracts/charts.js +476 -0
  34. package/lib/presentation/contracts/editing.js +58 -0
  35. package/lib/presentation/contracts/index.js +651 -0
  36. package/lib/presentation/contracts/interaction.js +62 -0
  37. package/lib/presentation/contracts/types.js +15 -0
  38. package/lib/presentation/contracts/values.js +28 -0
  39. package/lib/presentation/delivery.js +56 -0
  40. package/lib/presentation/files.js +83 -0
  41. package/lib/presentation/index.js +6 -0
  42. package/lib/presentation/lint.js +46 -0
  43. package/lib/presentation/projection/files.js +85 -0
  44. package/lib/presentation/projection/index.js +195 -0
  45. package/lib/presentation/projection/program.js +144 -0
  46. package/lib/presentation/projection/sql-code.js +79 -0
  47. package/lib/presentation/receipt.js +34 -0
  48. package/lib/presentation/reports.js +122 -0
  49. package/lib/presentation/rpc.js +148 -0
  50. package/lib/presentation/tool.js +122 -0
  51. package/lib/python-execution-contracts.js +1 -0
  52. package/lib/python-execution.js +232 -0
  53. package/lib/semantic-browser/contracts.js +80 -0
  54. package/lib/semantic-browser/definition.js +64 -0
  55. package/lib/semantic-browser/program.js +119 -0
  56. package/lib/semantic-browser/service.js +72 -0
  57. package/lib/semantic-reference/bridge.js +118 -0
  58. package/lib/semantic-reference/contracts.js +113 -0
  59. package/lib/semantic-reference/index.js +3 -0
  60. package/lib/semantic-reference/rpc.js +122 -0
  61. package/lib/semantic-reference/search.js +94 -0
  62. package/lib/semantic-reference/usage.js +143 -0
  63. package/lib/types/bridges.d.ts +2 -2
  64. package/lib/types/client/credentials/create-datasource.d.ts +5 -0
  65. package/lib/types/client/credentials/install.d.ts +1 -0
  66. package/lib/types/client/credentials/model.d.ts +59 -0
  67. package/lib/types/client/credentials/styles.d.ts +1 -0
  68. package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
  69. package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
  70. package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
  71. package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
  72. package/lib/types/client/presentation/chart-specials.d.ts +10 -0
  73. package/lib/types/client/presentation/chart-styles.d.ts +1 -0
  74. package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
  75. package/lib/types/client/presentation/chart-view.d.ts +23 -0
  76. package/lib/types/client/presentation/copy-context.d.ts +5 -0
  77. package/lib/types/client/presentation/delivery-model.d.ts +54 -0
  78. package/lib/types/client/presentation/delivery.d.ts +77 -0
  79. package/lib/types/client/presentation/editor-controls.d.ts +12 -0
  80. package/lib/types/client/presentation/global-filters.d.ts +6 -0
  81. package/lib/types/client/presentation/host-entry.d.ts +12 -0
  82. package/lib/types/client/presentation/icons.d.ts +4 -0
  83. package/lib/types/client/presentation/install.d.ts +15 -0
  84. package/lib/types/client/presentation/markdown.d.ts +5 -0
  85. package/lib/types/client/presentation/model.d.ts +62 -0
  86. package/lib/types/client/presentation/reader.d.ts +11 -0
  87. package/lib/types/client/presentation/source-code-model.d.ts +16 -0
  88. package/lib/types/client/presentation/source-code.d.ts +11 -0
  89. package/lib/types/client/presentation/source-dialog.d.ts +13 -0
  90. package/lib/types/client/presentation/source-facts.d.ts +20 -0
  91. package/lib/types/client/presentation/source-format.d.ts +5 -0
  92. package/lib/types/client/presentation/source-highlight.d.ts +7 -0
  93. package/lib/types/client/presentation/sources.d.ts +13 -0
  94. package/lib/types/client/presentation/styles.d.ts +1 -0
  95. package/lib/types/client/presentation/table.d.ts +12 -0
  96. package/lib/types/client/presentation/use-element-width.d.ts +5 -0
  97. package/lib/types/client/workspace-header-action.d.ts +7 -0
  98. package/lib/types/client.d.ts +4 -122
  99. package/lib/types/compatibility.d.ts +2 -2
  100. package/lib/types/datasource/authoring.d.ts +25 -0
  101. package/lib/types/datasource/bridge.d.ts +21 -3
  102. package/lib/types/datasource/index.d.ts +3 -5
  103. package/lib/types/datasource/python.d.ts +4 -0
  104. package/lib/types/datasource/resolver-program.d.ts +6 -0
  105. package/lib/types/datasource/rpc.d.ts +4 -0
  106. package/lib/types/datasource/service.d.ts +112 -0
  107. package/lib/types/datasource/shell-env.d.ts +0 -29
  108. package/lib/types/datasource/test.d.ts +6 -29
  109. package/lib/types/environment/binding.d.ts +5 -1
  110. package/lib/types/environment/errors.d.ts +1 -1
  111. package/lib/types/environment/runtime.d.ts +5 -5
  112. package/lib/types/environment/types.d.ts +13 -2
  113. package/lib/types/index.d.ts +1 -1
  114. package/lib/types/plugin.d.ts +11 -5
  115. package/lib/types/presentation/build/index.d.ts +11 -0
  116. package/lib/types/presentation/commit.d.ts +4 -0
  117. package/lib/types/presentation/contracts/charts.d.ts +13 -0
  118. package/lib/types/presentation/contracts/editing.d.ts +8 -0
  119. package/lib/types/presentation/contracts/index.d.ts +11 -0
  120. package/lib/types/presentation/contracts/interaction.d.ts +8 -0
  121. package/lib/types/presentation/contracts/types.d.ts +252 -0
  122. package/lib/types/presentation/contracts/values.d.ts +9 -0
  123. package/lib/types/presentation/delivery.d.ts +3 -0
  124. package/lib/types/presentation/files.d.ts +7 -0
  125. package/lib/types/presentation/index.d.ts +6 -0
  126. package/lib/types/presentation/lint.d.ts +13 -0
  127. package/lib/types/presentation/projection/files.d.ts +2 -0
  128. package/lib/types/presentation/projection/index.d.ts +20 -0
  129. package/lib/types/presentation/projection/program.d.ts +2 -0
  130. package/lib/types/presentation/projection/sql-code.d.ts +2 -0
  131. package/lib/types/presentation/receipt.d.ts +14 -0
  132. package/lib/types/presentation/reports.d.ts +5 -0
  133. package/lib/types/presentation/rpc.d.ts +24 -0
  134. package/lib/types/presentation/tool.d.ts +12 -0
  135. package/lib/types/python-execution-contracts.d.ts +10 -0
  136. package/lib/types/python-execution.d.ts +10 -0
  137. package/lib/types/semantic-browser/contracts.d.ts +40 -0
  138. package/lib/types/semantic-browser/definition.d.ts +20 -0
  139. package/lib/types/semantic-browser/program.d.ts +2 -0
  140. package/lib/types/semantic-browser/service.d.ts +20 -0
  141. package/lib/types/semantic-reference/bridge.d.ts +11 -0
  142. package/lib/types/semantic-reference/contracts.d.ts +51 -0
  143. package/lib/types/semantic-reference/index.d.ts +3 -0
  144. package/lib/types/semantic-reference/rpc.d.ts +15 -0
  145. package/lib/types/semantic-reference/search.d.ts +10 -0
  146. package/lib/types/semantic-reference/usage.d.ts +43 -0
  147. package/package.json +43 -19
  148. package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
  149. package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
  150. package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
  151. package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
  152. package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
  153. package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
  154. package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
  155. package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
  156. package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
  157. package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
  158. package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
  159. package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
  160. package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
  161. package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
  162. package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
  163. package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
  164. package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
  165. package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
  166. package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
  167. package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
  168. package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
  169. package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
  170. package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
  171. package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
  172. package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
  173. package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
  174. package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
  175. package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
  176. package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
  177. package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
  178. package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
  179. package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
  180. package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
  181. package/lib/datasource/access.js +0 -60
  182. package/lib/datasource/credentials.js +0 -24
  183. package/lib/evidence/bridge-program.js +0 -153
  184. package/lib/evidence/bridge.js +0 -166
  185. package/lib/evidence/index.js +0 -2
  186. package/lib/evidence/sources.js +0 -263
  187. package/lib/types/datasource/access.d.ts +0 -27
  188. package/lib/types/evidence/bridge.d.ts +0 -40
  189. package/lib/types/evidence/index.d.ts +0 -4
  190. package/lib/types/evidence/sources.d.ts +0 -57
  191. package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
  192. package/skills/dsh-data-analysis-report/SKILL.md +0 -57
  193. package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
  194. package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
  195. package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
@@ -0,0 +1,62 @@
1
+ export function defaultSelection(interaction) {
2
+ return Object.fromEntries(interaction.filters.map((filter) => [filter.id, filter.allOptionId]));
3
+ }
4
+ export function selectedSlice(interaction, selection) {
5
+ const slice = interaction.slices.find((slice) => interaction.filters.every((filter) => Object.hasOwn(selection, filter.id) && slice.selection[filter.id] === selection[filter.id]));
6
+ if (!slice)
7
+ throw new Error('Unknown presentation filter combination.');
8
+ return slice;
9
+ }
10
+ export function interactionRows(interaction, selection, block) {
11
+ if (!interaction?.blockIds.includes(block.id) || !('datasetId' in block))
12
+ return undefined;
13
+ const binding = selectedSlice(interaction, selection).datasets.find((entry) => entry.datasetId === block.datasetId);
14
+ if (!binding)
15
+ throw new Error('Missing presentation slice binding.');
16
+ return binding.rowIndices;
17
+ }
18
+ export function filterSummary(interaction, selection) {
19
+ return interaction.filters
20
+ .map((filter) => {
21
+ const option = filter.options.find((option) => option.id === selection[filter.id]);
22
+ if (!option)
23
+ throw new Error('Unknown presentation filter option.');
24
+ return `${filter.label}:${option.label}`;
25
+ })
26
+ .join(' · ');
27
+ }
28
+ /** Preserve the authored before / interactive / after boundary during host edits. */
29
+ export function blockRegion(document, id) {
30
+ if (!document.interaction)
31
+ return 0;
32
+ if (document.interaction.blockIds.includes(id))
33
+ return 1;
34
+ return document.blocks.findIndex((block) => block.id === id) <
35
+ document.blocks.findIndex((block) => block.id === document.interaction.blockIds[0])
36
+ ? 0
37
+ : 2;
38
+ }
39
+ export function editedInteraction(document, blocks) {
40
+ if (!document.interaction)
41
+ return undefined;
42
+ const original = document.interaction;
43
+ const targets = blocks.filter((block) => original.blockIds.includes(block.id));
44
+ if (!targets.length)
45
+ return undefined;
46
+ const datasets = new Set(targets.flatMap((block) => 'datasetId' in block
47
+ ? [
48
+ block.datasetId,
49
+ ...(block.kind === 'chart'
50
+ ? (block.preparedViews ?? []).map((view) => view.datasetId)
51
+ : []),
52
+ ]
53
+ : []));
54
+ return {
55
+ ...original,
56
+ blockIds: targets.map((block) => block.id),
57
+ slices: original.slices.map((slice) => ({
58
+ ...slice,
59
+ datasets: slice.datasets.filter((entry) => datasets.has(entry.datasetId)),
60
+ })),
61
+ };
62
+ }
@@ -0,0 +1,15 @@
1
+ export const PRESENTATION_SCHEMA_VERSION = 2;
2
+ export const PRESENTATION_BUDGETS = {
3
+ documentBytes: 4 * 1024 * 1024,
4
+ draftBytes: 256 * 1024,
5
+ datasetBytes: 2 * 1024 * 1024,
6
+ htmlBytes: 8 * 1024 * 1024,
7
+ datasets: 16,
8
+ columns: 64,
9
+ rows: 5_000,
10
+ cells: 100_000,
11
+ sources: 64,
12
+ codeSnippets: 32,
13
+ blocks: 64,
14
+ text: 32_768,
15
+ };
@@ -0,0 +1,28 @@
1
+ export class PresentationContractError extends Error {
2
+ code;
3
+ path;
4
+ hint;
5
+ reason;
6
+ constructor(code, path, message, hint = 'Correct the value at this path.') {
7
+ super(`${path || '/'}: ${message}`);
8
+ this.name = 'PresentationContractError';
9
+ this.reason = message;
10
+ this.code = code;
11
+ this.path = path;
12
+ this.hint = hint;
13
+ }
14
+ }
15
+ export function chartNumber(value, column, mode, path = '') {
16
+ if (value === null)
17
+ return null;
18
+ if (!['float64', 'int64', 'decimal'].includes(column.type))
19
+ throw new PresentationContractError('invalid_value', path, 'Chart series must be numeric.');
20
+ if (mode === 'exact' &&
21
+ (column.type === 'decimal' || (column.type === 'int64' && !Number.isSafeInteger(Number(value))))) {
22
+ throw new PresentationContractError('numeric_precision', path, 'Exact chart encoding cannot represent this value.', 'Set numericMode to approximate explicitly, or show the exact value in a table.');
23
+ }
24
+ const number = Number(value);
25
+ if (!Number.isFinite(number))
26
+ throw new PresentationContractError('numeric_precision', path, 'Chart encoding must remain finite.');
27
+ return number;
28
+ }
@@ -0,0 +1,56 @@
1
+ import { MARIVO_PRESENT_TOOL_NAME, MARIVO_PRESENTATION_DELIVERY_KIND, parsePresentationDelivery, } from "./receipt.js";
2
+ /** Persist the same envelope on nested Code dispatch even when code discards the return value. */
3
+ export function installMarivoPresentationCodeDelivery(ctx) {
4
+ const pending = new Map();
5
+ const stopResult = ctx.on('tools/result', (exec, result) => {
6
+ if (exec.name !== MARIVO_PRESENT_TOOL_NAME || exec.parent === undefined || result.isError)
7
+ return;
8
+ try {
9
+ const value = result.value;
10
+ const delivery = parsePresentationDelivery(JSON.parse(value.deliveryJson));
11
+ if (delivery.dshSessionId !== String(exec.agent?.session.id))
12
+ return;
13
+ pending.set(String(exec.callId), { rootCallId: String(exec.rootCallId), delivery });
14
+ // Bounded transient dispatch rendezvous; receipts remain in Host events, never this map.
15
+ if (pending.size > 256)
16
+ pending.delete(pending.keys().next().value);
17
+ }
18
+ catch {
19
+ /* Invalid output cannot create a success card. */
20
+ }
21
+ });
22
+ const stopLog = ctx.on('tools/code-dispatch-log', async (dispatch, next) => {
23
+ const content = await next();
24
+ if (dispatch.name !== MARIVO_PRESENT_TOOL_NAME)
25
+ return content;
26
+ const key = String(dispatch.subCallId);
27
+ const item = pending.get(key);
28
+ pending.delete(key);
29
+ if (dispatch.isError ||
30
+ !item ||
31
+ item.delivery.dshSessionId !== String(dispatch.agent?.session.id) ||
32
+ item.rootCallId !== String(dispatch.exec.rootCallId))
33
+ return content;
34
+ const root = [...(dispatch.agent?.session.events ?? [])]
35
+ .reverse()
36
+ .find((event) => event.type === 'tool/call' && String(event.data.callId) === item.rootCallId);
37
+ if (root?.type !== 'tool/call' || root.data.turn !== item.delivery.turn)
38
+ return content;
39
+ return [
40
+ ...content,
41
+ {
42
+ type: MARIVO_PRESENTATION_DELIVERY_KIND,
43
+ delivery: item.delivery,
44
+ },
45
+ ];
46
+ }, { prepend: true });
47
+ let active = true;
48
+ return () => {
49
+ if (!active)
50
+ return;
51
+ active = false;
52
+ stopLog();
53
+ stopResult();
54
+ pending.clear();
55
+ };
56
+ }
@@ -0,0 +1,83 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, open, realpath } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { PRESENTATION_BUDGETS, parsePresentationBuildId, } from "./contracts/index.js";
6
+ export function presentationReportPath(root, reportId) {
7
+ parsePresentationBuildId(reportId, '/reportId');
8
+ return path.join(root, '.dsh-data-analysis', 'presentations', reportId);
9
+ }
10
+ export function presentationAssetPath(root, reportId, buildId, asset) {
11
+ parsePresentationBuildId(buildId);
12
+ if (asset !== 'presentation.json' && asset !== 'index.html')
13
+ throw new Error('invalid-asset');
14
+ return path.join(presentationReportPath(root, reportId), 'builds', buildId, asset);
15
+ }
16
+ export function presentationSha256(bytes) {
17
+ return createHash('sha256').update(bytes).digest('hex');
18
+ }
19
+ /** Bounded raw-byte reader with no symbolic links and stable directory/file identity. */
20
+ export async function readPresentationAsset(workspaceRoot, reportId, buildId, asset, signal) {
21
+ const filename = presentationAssetPath(workspaceRoot, reportId, buildId, asset);
22
+ return readReportFile(workspaceRoot, filename, asset === 'presentation.json'
23
+ ? PRESENTATION_BUDGETS.documentBytes
24
+ : PRESENTATION_BUDGETS.htmlBytes, signal);
25
+ }
26
+ export async function readReportFile(workspaceRoot, filename, maximum, signal) {
27
+ signal?.throwIfAborted();
28
+ const root = await realpath(workspaceRoot);
29
+ if (root !== path.resolve(workspaceRoot) || !filename.startsWith(`${root}${path.sep}`))
30
+ throw new Error('asset-path-mismatch');
31
+ const parents = [root];
32
+ for (const segment of path.relative(root, path.dirname(filename)).split(path.sep)) {
33
+ if (!segment || segment === '..' || segment === '.')
34
+ throw new Error('asset-path-mismatch');
35
+ parents.push(path.join(parents.at(-1), segment));
36
+ }
37
+ const beforeParents = await Promise.all(parents.map((parent) => lstat(parent)));
38
+ if (beforeParents.some((stat) => !stat.isDirectory() || stat.isSymbolicLink()))
39
+ throw new Error('asset-path-mismatch');
40
+ if ((await realpath(filename)) !== filename)
41
+ throw new Error('asset-path-mismatch');
42
+ const file = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
43
+ try {
44
+ const before = await file.stat();
45
+ if (!before.isFile())
46
+ throw new Error('asset-not-file');
47
+ if (before.size > maximum)
48
+ throw new Error('asset-too-large');
49
+ const bytes = Buffer.alloc(maximum + 1);
50
+ let length = 0;
51
+ while (length < bytes.length) {
52
+ signal?.throwIfAborted();
53
+ const read = await file.read(bytes, length, bytes.length - length, null);
54
+ if (read.bytesRead === 0)
55
+ break;
56
+ length += read.bytesRead;
57
+ }
58
+ if (length > maximum)
59
+ throw new Error('asset-too-large');
60
+ const after = await file.stat();
61
+ const current = await lstat(filename);
62
+ const afterParents = await Promise.all(parents.map((parent) => lstat(parent)));
63
+ if (afterParents.some((stat, index) => !stat.isDirectory() ||
64
+ stat.isSymbolicLink() ||
65
+ stat.dev !== beforeParents[index].dev ||
66
+ stat.ino !== beforeParents[index].ino) ||
67
+ (await realpath(workspaceRoot)) !== root ||
68
+ (await realpath(filename)) !== filename)
69
+ throw new Error('asset-path-mismatch');
70
+ if (current.dev !== after.dev ||
71
+ current.ino !== after.ino ||
72
+ before.size !== after.size ||
73
+ before.mtimeMs !== after.mtimeMs ||
74
+ before.ctimeMs !== after.ctimeMs ||
75
+ length !== after.size)
76
+ throw new Error('asset-changed');
77
+ signal?.throwIfAborted();
78
+ return bytes.subarray(0, length);
79
+ }
80
+ finally {
81
+ await file.close();
82
+ }
83
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./contracts/index.js";
2
+ export * from "./delivery.js";
3
+ export * from "./projection/index.js";
4
+ export * from "./receipt.js";
5
+ export * from "./rpc.js";
6
+ export * from "./tool.js";
@@ -0,0 +1,46 @@
1
+ import { PRESENTATION_BUDGETS, PresentationContractError, parsePresentationDraft, parseTypedDataset, } from "./contracts/index.js";
2
+ import { readWorkspaceJson } from "./projection/files.js";
3
+ /** Static preflight only: never starts a Runtime or publishes presentation assets. */
4
+ export async function lintPresentationDraft(workspaceRoot, draftPath) {
5
+ const diagnostics = [];
6
+ let checkedComputedDatasets = 0;
7
+ const record = (error) => {
8
+ if (!(error instanceof PresentationContractError))
9
+ throw error;
10
+ diagnostics.push({
11
+ code: error.code,
12
+ path: error.path,
13
+ message: error.message,
14
+ hint: error.hint,
15
+ });
16
+ };
17
+ try {
18
+ const draft = parsePresentationDraft(await readWorkspaceJson(workspaceRoot, draftPath, PRESENTATION_BUDGETS.draftBytes, '/draft_path'));
19
+ for (const [index, dataset] of draft.datasets.entries()) {
20
+ if (dataset.kind !== 'computed')
21
+ continue;
22
+ try {
23
+ const data = await readWorkspaceJson(workspaceRoot, dataset.path, PRESENTATION_BUDGETS.datasetBytes, `/datasets/${index}/path`);
24
+ parseTypedDataset(data, `/datasets/${index}/data`);
25
+ checkedComputedDatasets++;
26
+ }
27
+ catch (error) {
28
+ record(error);
29
+ }
30
+ }
31
+ }
32
+ catch (error) {
33
+ record(error);
34
+ }
35
+ return {
36
+ ok: diagnostics.length === 0,
37
+ scope: 'draft-structure-and-computed-data',
38
+ checkedComputedDatasets,
39
+ diagnostics,
40
+ deferredChecks: [
41
+ 'artifact-availability-and-data',
42
+ 'codeRefs',
43
+ 'projected-document-and-rendering',
44
+ ],
45
+ };
46
+ }
@@ -0,0 +1,85 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { PresentationContractError } from "../contracts/index.js";
5
+ function boundary(location) {
6
+ throw new PresentationContractError('file_boundary', location, 'Expected an unchanged regular file inside the bound Workspace without symbolic links.');
7
+ }
8
+ /** Read raw bytes under a strict budget before JSON parsing, including concurrent growth. */
9
+ export async function readWorkspaceJson(workspaceRoot, relativePath, maximum, location, signal) {
10
+ signal?.throwIfAborted();
11
+ if (path.isAbsolute(relativePath) ||
12
+ relativePath.includes('\\') ||
13
+ relativePath.includes('\0') ||
14
+ relativePath.split('/').some((part) => !part || part === '.' || part === '..'))
15
+ boundary(location);
16
+ try {
17
+ const root = await realpath(workspaceRoot);
18
+ if (root !== path.resolve(workspaceRoot))
19
+ boundary(location);
20
+ const segments = relativePath.split('/');
21
+ const filename = path.join(root, ...segments);
22
+ const parents = [
23
+ root,
24
+ ...segments.slice(0, -1).map((_, index) => path.join(root, ...segments.slice(0, index + 1))),
25
+ ];
26
+ const beforeParents = await Promise.all(parents.map((parent) => lstat(parent)));
27
+ if (beforeParents.some((stat) => !stat.isDirectory() || stat.isSymbolicLink()))
28
+ boundary(location);
29
+ if ((await realpath(filename)) !== filename)
30
+ boundary(location);
31
+ if (!(await lstat(filename)).isFile())
32
+ boundary(location);
33
+ const file = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
34
+ try {
35
+ const before = await file.stat();
36
+ if (!before.isFile())
37
+ boundary(location);
38
+ if (before.size > maximum)
39
+ throw new PresentationContractError('budget', location, 'Input file exceeds the presentation byte budget.');
40
+ const bytes = Buffer.alloc(maximum + 1);
41
+ let length = 0;
42
+ while (length < bytes.length) {
43
+ signal?.throwIfAborted();
44
+ const { bytesRead } = await file.read(bytes, length, bytes.length - length, null);
45
+ if (bytesRead === 0)
46
+ break;
47
+ length += bytesRead;
48
+ }
49
+ if (length > maximum)
50
+ throw new PresentationContractError('budget', location, 'Input file exceeds the presentation byte budget.');
51
+ const after = await file.stat();
52
+ const current = await lstat(filename);
53
+ const afterParents = await Promise.all(parents.map((parent) => lstat(parent)));
54
+ if (afterParents.some((stat, index) => !stat.isDirectory() ||
55
+ stat.isSymbolicLink() ||
56
+ stat.dev !== beforeParents[index].dev ||
57
+ stat.ino !== beforeParents[index].ino) ||
58
+ (await realpath(workspaceRoot)) !== root ||
59
+ (await realpath(filename)) !== filename ||
60
+ current.dev !== after.dev ||
61
+ current.ino !== after.ino ||
62
+ before.size !== after.size ||
63
+ before.mtimeMs !== after.mtimeMs ||
64
+ before.ctimeMs !== after.ctimeMs ||
65
+ length !== after.size)
66
+ boundary(location);
67
+ signal?.throwIfAborted();
68
+ try {
69
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, length)));
70
+ }
71
+ catch {
72
+ throw new PresentationContractError('invalid_json', location, 'Expected valid UTF-8 JSON.');
73
+ }
74
+ }
75
+ finally {
76
+ await file.close();
77
+ }
78
+ }
79
+ catch (error) {
80
+ signal?.throwIfAborted();
81
+ if (error instanceof PresentationContractError)
82
+ throw error;
83
+ boundary(location);
84
+ }
85
+ }
@@ -0,0 +1,195 @@
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: 2,
183
+ reportId: options.reportId,
184
+ workspaceId: options.workspaceId,
185
+ buildId: options.buildId,
186
+ title: draft.title,
187
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
188
+ datasets,
189
+ sources,
190
+ blocks: draft.blocks,
191
+ ...(draft.interaction ? { interaction: draft.interaction } : {}),
192
+ diagnostics,
193
+ });
194
+ }
195
+ }